From a1332913ec5208f5c5e3e5421872be8d6ea87331 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 28 Apr 2024 19:30:05 +0300 Subject: [PATCH] Fix reset crashes --- CMakeLists.txt | 4 +- src/main.c | 547 +++++---------------- src/retro_achievements.cpp | 976 ++++++++++++++++++++++++++++--------- src/retro_achievements.h | 30 +- 4 files changed, 867 insertions(+), 690 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 87813a77a..17a4d115c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,8 +57,8 @@ if( (NOT ANDROID)) endif() if (NOT EMSCRIPTEN) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O3 ") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O3 ") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g ") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g ") set(UNICODE_GUI 1) else () set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -DNDEBUG") diff --git a/src/main.c b/src/main.c index 7797b8827..02c722c37 100644 --- a/src/main.c +++ b/src/main.c @@ -513,30 +513,6 @@ typedef struct{ } se_cloud_state_t; static void se_sync_cloud_save_states(); #ifdef ENABLE_RETRO_ACHIEVEMENTS -typedef struct se_ra_tracker_node{ - struct rc_client_leaderboard_tracker_t tracker; - struct se_ra_tracker_node* next; -}se_ra_tracker_node_t; -typedef struct se_ra_challenge_indicator_node{ - uint32_t id; - atlas_tile_t* image; - struct se_ra_challenge_indicator_node* next; -} se_ra_challenge_indicator_node_t; -typedef struct{ - rc_client_t* client; - char username[256]; - char password[256]; - atlas_tile_t* image; - atlas_tile_t*** achievement_images; - // TODO: make widgets that use these lists and progress indicator - rc_client_achievement_list_t* achievement_list; - se_ra_tracker_node_t* tracker_list; - se_ra_challenge_indicator_node_t* challenge_indicator_list; - atlas_tile_t* progress_indicator_image; - bool progress_indicator_shown; - char measured_progress[24]; - bool pending_login; -}se_ra_info_t; #endif gui_state_t gui_state={ .update_font_atlas=true }; @@ -626,7 +602,7 @@ char* se_replace_fake_path(char * new_path){ } return new_path; } -static inline const char* se_localize_and_cache(const char* input_str){ +const char* se_localize_and_cache(const char* input_str){ const char * localized_string = se_localize(input_str); se_cache_glyphs(localized_string); return localized_string; @@ -634,7 +610,7 @@ static inline const char* se_localize_and_cache(const char* input_str){ static inline bool se_checkbox(const char* label, bool * v){ return igCheckbox(se_localize_and_cache(label),v); } -static void se_text(const char* label,...){ +void se_text(const char* label,...){ va_list args; va_start(args, label); igTextWrappedV(se_localize_and_cache(label),args); @@ -790,7 +766,7 @@ bool se_slider_int_themed(const char* label, int* v, float v_min, float v_max, c return ret; } -static bool se_button(const char* label, ImVec2 size){ +bool se_button(const char* label, ImVec2 size){ return se_button_themed(SE_REGION_BLANK,se_localize_and_cache(label),size,true); } static bool se_input_path(const char* label, char* new_path, ImGuiInputTextFlags flags){ @@ -1060,9 +1036,6 @@ se_core_rewind_buffer_t rewind_buffer; se_save_state_t save_states[SE_NUM_SAVE_STATES]; se_cheat_t cheats[SE_NUM_CHEATS]; se_cloud_state_t cloud_state; -#ifdef ENABLE_RETRO_ACHIEVEMENTS -se_ra_info_t ra_info; -#endif bool se_more_rewind_deltas(se_core_rewind_buffer_t* rewind, uint32_t index){ return (rewind->deltas[index%SE_REWIND_BUFFER_SIZE].offset&SE_LAST_DELTA_IN_TX)==0; @@ -1389,7 +1362,8 @@ static void se_ra_keep_alive(){ last_time = stm_now(); // Needs to be called once every few seconds if the emulator is paused // to keep the session alive or retrying failed unlocks - rc_client_idle(ra_info.client); + // rc_client_idle(retro_achievements_get_client()); + printf("TODO: move me \n"); } } #endif @@ -1717,7 +1691,7 @@ void se_draw_emu_stats(){ } #ifdef ENABLE_RETRO_ACHIEVEMENTS -static uint32_t se_ra_read_memory_callback(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client){ +uint32_t retro_achievements_read_memory_callback(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client){ if(emu_state.system==SYSTEM_GB){ for(uint32_t i=0;inum_buckets; i++) - { - if(ra_info.achievement_images[i] != NULL) - { - free(ra_info.achievement_images[i]); - } - } - free(ra_info.achievement_images); - ra_info.achievement_images = NULL; - } - { - se_ra_tracker_node_t* next = ra_info.tracker_list; - while(next){ - se_ra_tracker_node_t* tmp = next; - next = next->next; - free(tmp); - } - ra_info.tracker_list = NULL; - } - { - se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; - while(next){ - se_ra_challenge_indicator_node_t* tmp = next; - next = next->next; - free(tmp); - } - ra_info.challenge_indicator_list = NULL; - } -} -static void se_ra_load_game_callback(int result, const char* error_message, rc_client_t* client, void* userdata){ - if (result != RC_OK){ - // TODO: notification error message? - printf("[rcheevos]: failed to load game: %s\n", error_message); - return; - } - - char url[512]; - const rc_client_game_t* game = rc_client_get_game_info(ra_info.client); - if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK){ - ra_get_image(url, &ra_info.image); - } - - if(ra_info.achievement_list) // TODO: deduplicate this code - { - rc_client_destroy_achievement_list(ra_info.achievement_list); - } - ra_info.achievement_list = rc_client_create_achievement_list(ra_info.client, - RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, - RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - rc_client_achievement_list_t* list = ra_info.achievement_list; - ra_info.achievement_images = (atlas_tile_t***)malloc(sizeof(atlas_tile_t**)*list->num_buckets); - for (int i = 0; i < list->num_buckets; i++) - { - uint32_t num_achievements=list->buckets[i].num_achievements; - ra_info.achievement_images[i] = (atlas_tile_t**)malloc(sizeof(atlas_tile_t*)*num_achievements); - memset(ra_info.achievement_images[i], 0, sizeof(atlas_tile_t*)*num_achievements); - for (int j = 0; j < num_achievements; j++) - { - char url[512]; - const rc_client_achievement_t* achievement = list->buckets[i].achievements[j]; - if(rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK){ - ra_get_image(url, &ra_info.achievement_images[i][j]); - } - printf("[rcheevos]: Achievement %s, ", achievement->title); - if (achievement->id == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) - printf("unsupported\n"); - else if (achievement->unlocked) - printf("unlocked\n"); - else if (achievement->measured_percent) - printf("progress: %f%%\n", achievement->measured_percent); - else - printf("locked\n"); - } - } -} -static void se_ra_load_game(){ - if(!emu_state.rom_loaded)return; - switch(emu_state.system){ - case SYSTEM_GB: - rc_client_begin_identify_and_load_game(ra_info.client,RC_CONSOLE_GAMEBOY,NULL,emu_state.rom_data,emu_state.rom_size,se_ra_load_game_callback,NULL); - break; - case SYSTEM_GBA: - rc_client_begin_identify_and_load_game(ra_info.client,RC_CONSOLE_GAMEBOY_ADVANCE,NULL,emu_state.rom_data,emu_state.rom_size,se_ra_load_game_callback,NULL); - break; - case SYSTEM_NDS: - rc_client_begin_identify_and_load_game(ra_info.client,RC_CONSOLE_NINTENDO_DS,NULL,emu_state.rom_data,emu_state.rom_size,se_ra_load_game_callback,NULL); - break; - } -} -static void se_ra_login_callback(int result, const char* error_message, rc_client_t* client, void* userdata) { - // TODO: show cool "logged in" banner or something - const rc_client_user_t* user = rc_client_get_user_info(client); - if(user){ - printf("[rcheevos]: logged in as %s (score: %d)\n", user->display_name, user->score); - memset(ra_info.password,0,sizeof(ra_info.password)); - - char buffer[sizeof(ra_info.username)+sizeof(ra_info.password)+2]; - memset(buffer,0,sizeof(buffer)); - snprintf(buffer,sizeof(buffer),"%s\n%s\n",ra_info.username,user->token); - - char login_info_path[SB_FILE_PATH_SIZE]; - snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_token.txt",se_get_pref_path()); - sb_save_file_data(login_info_path,(uint8_t*)buffer,sizeof(buffer)); - se_ra_load_game(); - } - ra_info.pending_login = false; -} static void se_ra_leaderboard_tracker_show(const rc_client_leaderboard_tracker_t* tracker) { - se_ra_tracker_node_t* new_tracker = (se_ra_tracker_node_t*)malloc(sizeof(se_ra_tracker_node_t)); - new_tracker->next = ra_info.tracker_list; - new_tracker->tracker.id = tracker->id; - memcpy(new_tracker->tracker.display, tracker->display, sizeof(new_tracker->tracker.display)); - ra_info.tracker_list = new_tracker; + printf("TODO: se_ra_leaderboard_tracker_show\n"); + // se_ra_tracker_node_t* new_tracker = (se_ra_tracker_node_t*)malloc(sizeof(se_ra_tracker_node_t)); + // new_tracker->next = ra_info.tracker_list; + // new_tracker->tracker.id = tracker->id; + // memcpy(new_tracker->tracker.display, tracker->display, sizeof(new_tracker->tracker.display)); + // ra_info.tracker_list = new_tracker; } static void se_ra_leaderboard_tracker_update(const rc_client_leaderboard_tracker_t* tracker) { - se_ra_tracker_node_t* next = ra_info.tracker_list; - while(next){ - if(next->tracker.id == tracker->id){ - memcpy(next->tracker.display, tracker->display, sizeof(next->tracker.display)); - break; - } - next = next->next; - } + printf("TODO: se_ra_leaderboard_tracker_update\n"); + // se_ra_tracker_node_t* next = ra_info.tracker_list; + // while(next){ + // if(next->tracker.id == tracker->id){ + // memcpy(next->tracker.display, tracker->display, sizeof(next->tracker.display)); + // break; + // } + // next = next->next; + // } } static void se_ra_leaderboard_tracker_hide(const rc_client_leaderboard_tracker_t* tracker) { + printf("TODO: se_ra_leaderboard_tracker_hide\n"); // "hide" seems to be a misleading name here, the wiki says the tracker is no // longer needed and calls a pseudo function "destroy_tracker", so we destroy it - se_ra_tracker_node_t* prev = NULL; - se_ra_tracker_node_t* next = ra_info.tracker_list; - while(next){ - if(next->tracker.id == tracker->id){ - if(prev){ - prev->next = next->next; - }else{ - ra_info.tracker_list = next->next; - } - free(next); - break; - } - prev = next; - next = next->next; - } + // se_ra_tracker_node_t* prev = NULL; + // se_ra_tracker_node_t* next = ra_info.tracker_list; + // while(next){ + // if(next->tracker.id == tracker->id){ + // if(prev){ + // prev->next = next->next; + // }else{ + // ra_info.tracker_list = next->next; + // } + // free(next); + // break; + // } + // prev = next; + // next = next->next; + // } } static void se_ra_game_mastered() { - rc_client_t* client = ra_info.client; - char message[128]; - char submessage[128]; - const rc_client_game_t* game = rc_client_get_game_info(client); - - // The popup should say "Completed" or "Mastered" depending on whether or not hardcore is enabled. - snprintf(message, sizeof(message), "%s %s", - rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",game->title); + printf("TODO: se_ra_game_mastered\n"); + // rc_client_t* client = ra_info.client; + // char message[128]; + // char submessage[128]; + // const rc_client_game_t* game = rc_client_get_game_info(client); + + // // The popup should say "Completed" or "Mastered" depending on whether or not hardcore is enabled. + // snprintf(message, sizeof(message), "%s %s", + // rc_client_get_hardcore_enabled(client) ? "Mastered" : "Completed",game->title); - // TODO: also display the time played - snprintf(submessage, sizeof(submessage), "%s", - rc_client_get_user_info(client)->display_name); + // // TODO: also display the time played + // snprintf(submessage, sizeof(submessage), "%s", + // rc_client_get_user_info(client)->display_name); - // TODO: display a popup with the message and submessage and the game icon instead of just printing it - // also perhaps play a sound effect - printf("[rcheevos]: %s %s\n", message, submessage); + // // TODO: display a popup with the message and submessage and the game icon instead of just printing it + // // also perhaps play a sound effect + // printf("[rcheevos]: %s %s\n", message, submessage); } // TODO: display a popup on these instead static void se_ra_leaderboard_attempt_started(const rc_client_leaderboard_t* leaderboard) @@ -1920,178 +1787,76 @@ static void se_ra_leaderboard_attempt_submitted(const rc_client_leaderboard_t* l } static void se_ra_challenge_indicator_show(const rc_client_achievement_t* achievement) { - se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; - while(next){ - if(next->id == achievement->id){ - // It shouldn't be the case that "show" is called twice for the same achievement - // but let's be safe and check anyway - return; - } - next = next->next; - } - - se_ra_challenge_indicator_node_t* new_indicator = (se_ra_challenge_indicator_node_t*)malloc(sizeof(se_ra_challenge_indicator_node_t)); - new_indicator->next = ra_info.challenge_indicator_list; - new_indicator->id = achievement->id; - ra_info.challenge_indicator_list = new_indicator; - - char url[128]; - if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK) - { - // ra_get_image(url, &new_indicator->image); - } + printf("TODO: challenge_indicator_show"); + // se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; + // while(next){ + // if(next->id == achievement->id){ + // // It shouldn't be the case that "show" is called twice for the same achievement + // // but let's be safe and check anyway + // return; + // } + // next = next->next; + // } + + // se_ra_challenge_indicator_node_t* new_indicator = (se_ra_challenge_indicator_node_t*)malloc(sizeof(se_ra_challenge_indicator_node_t)); + // new_indicator->next = ra_info.challenge_indicator_list; + // new_indicator->id = achievement->id; + // ra_info.challenge_indicator_list = new_indicator; + + // char url[128]; + // if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url)) == RC_OK) + // { + // // ra_get_image(url, &new_indicator->image); + // } } static void se_ra_challenge_indicator_hide(const rc_client_achievement_t* achievement) { - // Again "hide" seems to destroy the indicator - se_ra_challenge_indicator_node_t* prev = NULL; - se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; - while(next){ - if(next->id == achievement->id){ - if(prev){ - prev->next = next->next; - }else{ - ra_info.challenge_indicator_list = next->next; - } - // free(next); // TODO: crashing because threads are not finished! - break; - } - prev = next; - next = next->next; - } + printf("TODO: challenge_indicator_hide"); + // // Again "hide" seems to destroy the indicator + // se_ra_challenge_indicator_node_t* prev = NULL; + // se_ra_challenge_indicator_node_t* next = ra_info.challenge_indicator_list; + // while(next){ + // if(next->id == achievement->id){ + // if(prev){ + // prev->next = next->next; + // }else{ + // ra_info.challenge_indicator_list = next->next; + // } + // // free(next); // TODO: crashing because threads are not finished! + // break; + // } + // prev = next; + // next = next->next; + // } } static void se_ra_progress_indicator_update(const rc_client_achievement_t* achievement) { - if(!ra_info.progress_indicator_shown){ - printf("[rcheevos]Progress indicator update while it's hidden, this shouldn't happen\n"); - } + printf("TODO: progress_indicator_update"); + // if(!ra_info.progress_indicator_shown){ + // printf("[rcheevos]Progress indicator update while it's hidden, this shouldn't happen\n"); + // } - char url[128]; + // char url[128]; - if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) - { - // ra_get_image(url, &ra_info.progress_indicator_image); - } + // if (rc_client_achievement_get_image_url(achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) + // { + // // ra_get_image(url, &ra_info.progress_indicator_image); + // } - strncpy(ra_info.measured_progress, achievement->measured_progress, sizeof(ra_info.measured_progress)); + // strncpy(ra_info.measured_progress, achievement->measured_progress, sizeof(ra_info.measured_progress)); } static void se_ra_progress_indicator_show(const rc_client_achievement_t* achievement) { - ra_info.progress_indicator_shown = true; - se_ra_progress_indicator_update(achievement); + printf("TODO: progress_indicator_show"); + // ra_info.progress_indicator_shown = true; + // se_ra_progress_indicator_update(achievement); } static void se_ra_progress_indicator_hide(const rc_client_achievement_t* achievement) { + printf("TODO: progress_indicator_hide"); // Contrary to other widgets, only one progress indicator can be shown at a time // and can actually be hidden instead of destroyed - ra_info.progress_indicator_shown = false; -} -static void se_ra_event_handler(const rc_client_event_t* event, rc_client_t* client){ - switch (event->type) - { - case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: - printf("[rcheevos]: Achievement unlocked: %s\n", event->achievement->title); - if(ra_info.achievement_list) - { - rc_client_destroy_achievement_list(ra_info.achievement_list); - } - ra_info.achievement_list = rc_client_create_achievement_list(ra_info.client, - RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, - RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); - // TODO: notification? - break; - case RC_CLIENT_EVENT_LEADERBOARD_STARTED: - se_ra_leaderboard_attempt_started(event->leaderboard); - break; - case RC_CLIENT_EVENT_LEADERBOARD_FAILED: - se_ra_leaderboard_attempt_failed(event->leaderboard); - break; - case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: - se_ra_leaderboard_attempt_submitted(event->leaderboard); - break; - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: - se_ra_leaderboard_tracker_update(event->leaderboard_tracker); - break; - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: - se_ra_leaderboard_tracker_show(event->leaderboard_tracker); - break; - case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: - se_ra_leaderboard_tracker_hide(event->leaderboard_tracker); - break; - case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: - se_ra_challenge_indicator_show(event->achievement); - break; - case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: - se_ra_challenge_indicator_hide(event->achievement); - break; - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: - se_ra_progress_indicator_show(event->achievement); - break; - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: - se_ra_progress_indicator_update(event->achievement); - break; - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: - se_ra_progress_indicator_hide(event->achievement); - break; - case RC_CLIENT_EVENT_GAME_COMPLETED: - se_ra_game_mastered(); - break; - case RC_CLIENT_EVENT_RESET: - emu_state.run_mode = SB_MODE_RESET; - break; - case RC_CLIENT_EVENT_SERVER_ERROR: - printf("[rcheevos]: Server error: %s %s\n", event->server_error->api, event->server_error->error_message); - break; - default: - printf("Unhandled event %d\n", event->type); - break; - } -} -static void se_ra_initialize() { - memset(&ra_info, 0, sizeof(ra_info)); - if(ra_info.client) - { - printf("[rcheevos]: client already initialized?\n"); - } - else - { - ra_info.client = rc_client_create(se_ra_read_memory_callback, ra_server_callback); - // RetroAchievements doesn't enable CORS, so we use a reverse proxy - rc_api_set_host("https://api.achieve.skyemoo.pandasemi.co"); - rc_api_set_image_host("https://media.retroachievements.org"); - #ifndef NDEBUG - rc_client_enable_logging(ra_info.client, RC_CLIENT_LOG_LEVEL_VERBOSE, ra_log_callback); - #endif - // TODO: should probably be an option after we're finished testing - rc_client_set_hardcore_enabled(ra_info.client, 0); - } - rc_client_set_event_handler(ra_info.client,se_ra_event_handler); - - // Check if we have a token saved - char login_info_path[SB_FILE_PATH_SIZE]; - snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_token.txt",se_get_pref_path()); - if (sb_file_exists(login_info_path)){ - size_t size; - uint8_t *text = sb_load_file_data(login_info_path,&size); - if (text){ - int i = 0; - for(i=0;idisplay_name); - if(game&&ra_info.image){ - image.id=ra_info.image->atlas_id; - snprintf(line2,256,se_localize_and_cache("Playing: %s"),game->title); - }else snprintf(line2,256,"%s",se_localize_and_cache("No Game Loaded")); - if (ra_info.image) - se_boxed_image_dual_label(line1,line2, ICON_FK_TROPHY, image, 0, (ImVec2){ra_info.image->x1,ra_info.image->y1}, (ImVec2){ra_info.image->x2,ra_info.image->y2}); - else - se_boxed_image_dual_label(line1,line2, ICON_FK_TROPHY, image, 0, (ImVec2){0,0}, (ImVec2){1,1}); // just draw a placeholder image instead - if(se_button(ICON_FK_SIGN_OUT " Logout", (ImVec2){0,0})){ - char login_info_path[SB_FILE_PATH_SIZE]; - snprintf(login_info_path,SB_FILE_PATH_SIZE,"%sra_token.txt",se_get_pref_path()); - remove(login_info_path); - rc_client_logout(ra_info.client); - } - rc_client_achievement_list_t* list = ra_info.achievement_list; -#ifdef DEBUG_RETRO_ACHIEVEMENTS - if (se_button("Dump atlases", (ImVec2){0,0})){ - ra_dump_atlases(); - for (int i = 0; i < list->num_buckets; i++){ - for (int j = 0; j < list->buckets[i].num_achievements; j++){ - printf("achievement pointer: %p\n", ra_info.achievement_images[i][j]); - } - } - } -#endif - mutex_lock(ra_get_mutex()); - if (list){ - for (int i = 0; i < list->num_buckets; i++){ - se_text(ICON_FK_LOCK " %s",list->buckets[i].label); - for (int j = 0; j < list->buckets[i].num_achievements; j++){ - sg_image image; - ImVec2 uv0, uv1; - bool entered = false; - if(ra_info.achievement_images && ra_info.achievement_images[i] && ra_info.achievement_images[i][j]) { - atlas_tile_t* tile = ra_info.achievement_images[i][j]; - uv0 = (ImVec2){ tile->x1, tile->y1 }; - uv1 = (ImVec2){ tile->x2, tile->y2 }; - image.id = tile->atlas_id; - entered = true; - } -#ifdef DEBUG_RETRO_ACHIEVEMENTS - char name[512]; - sg_image_info info = sg_query_image_info(image); - int ofx = uv0.x * info.width; - int ofy = uv0.y * info.height; - int of1x = uv1.x * info.width; - int of1y = uv1.y * info.height; - snprintf(name,512,"%s %p %p (%d) %d %d %d %d",list->buckets[i].achievements[j]->title, ra_info.achievement_images[i], ra_info.achievement_images[i][j], entered,ofx,ofy,of1x,of1y); - se_boxed_image_dual_label(name,list->buckets[i].achievements[j]->description, ICON_FK_SPINNER, image, 0, uv0, uv1); -#else - se_boxed_image_dual_label(list->buckets[i].achievements[j]->title, - list->buckets[i].achievements[j]->description, ICON_FK_SPINNER, image, 0, uv0, uv1); -#endif - } - } - } - mutex_unlock(ra_get_mutex()); - } + retro_achievements_draw_panel(win_w); #endif { se_bios_info_t * info = &gui_state.bios_info; @@ -7394,8 +7074,7 @@ static void frame(void) { screen_x = left_padding; screen_width-=(left_padding+right_padding)*se_dpi_scale(); #ifdef ENABLE_RETRO_ACHIEVEMENTS - ra_update_atlases(); - ra_run_pending_callbacks(); + retro_achievements_update_atlases(); #endif if(gui_state.sidebar_open){ igSetNextWindowPos((ImVec2){screen_x,menu_height}, ImGuiCond_Always, (ImVec2){0,0}); @@ -8178,7 +7857,7 @@ static void init(void) { gui_state.last_touch_time=-10000; se_init_audio(); #ifdef ENABLE_RETRO_ACHIEVEMENTS - se_ra_initialize(); + retro_achievements_initialize(&emu_state); #endif sg_push_debug_group("LCD Shader Init"); @@ -8228,13 +7907,15 @@ static void init(void) { static void cleanup(void) { simgui_shutdown(); se_free_all_images(); +#ifdef ENABLE_RETRO_ACHIEVEMENTS + retro_achievements_shutdown(); +#endif sg_shutdown(); saudio_shutdown(); #ifdef USE_SDL SDL_Quit(); #endif https_shutdown(); - ra_shutdown(); } #ifdef EMSCRIPTEN static void emsc_load_callback(const sapp_html5_fetch_response* response) { diff --git a/src/retro_achievements.cpp b/src/retro_achievements.cpp index a0bdcd3d5..12aabb8c4 100644 --- a/src/retro_achievements.cpp +++ b/src/retro_achievements.cpp @@ -1,17 +1,35 @@ +#include "IconsForkAwesome.h" +#include "rc_client.h" +#include "sokol_gfx.h" +#include +#include +#include extern "C" { +#define CIMGUI_DEFINE_ENUMS_AND_STRUCTS +#include "cimgui.h" +const char* se_get_pref_path(); +void se_push_disabled(); +void se_pop_disabled(); +void se_text(const char* fmt, ...); +void se_boxed_image_dual_label(const char* title, const char* description, const char* icon, + sg_image image, int flags, ImVec2 uv0, ImVec2 uv1); +bool se_button(const char* label, ImVec2 size); +const char* se_localize_and_cache(const char* input_str); #include "retro_achievements.h" } #include "https.hpp" -#include "mutex.h" -#include "sokol_gfx.h" +#include "rc_consoles.h" +#include "sb_types.h" +#include #include #include +#include #include +#include #include -#include -#include +#include #include #include @@ -19,10 +37,6 @@ extern "C" { #include "stb_image.h" #include "stb_image_write.h" -// Access to some parts such as the atlas and the pending callbacks can happen from multiple threads -// so we need to synchronize access to them -std::mutex* synchronization_mutex = new std::mutex(); - struct downloaded_image_t { uint8_t* data; // always RGBA @@ -30,30 +44,139 @@ struct downloaded_image_t int height; }; -static const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding +bool thread_local is_ui_thread = false; +const int atlas_spacing = 4; // leaving some space between tiles to avoid bleeding -// atlases -> the currently existing atlases, each serving a different image width/height combo -// image_cache -> a mapping of image urls to their atlas and the coordinates within the atlas -// download_cache -> a cache of downloaded images, so we don't download the same image multiple -// times +// atlases -> the currently existing atlases, each serving a different image +// width/height combo image_cache -> a mapping of image urls to their atlas and +// the coordinates within the atlas download_cache -> a cache of downloaded +// images, so we don't download the same image multiple times // download_cache has the lifetime of the program -// atlases and image_cache are reset every time the user loads a *different* game +// atlases and image_cache are reset every time the user loads a *different* +// game struct atlas_t; -static std::vector atlases; -static std::unordered_map image_cache; +struct ra_game_state_t; + +struct ra_tracker_node_t +{ + rc_client_leaderboard_tracker_t tracker; + ra_tracker_node_t* next; +}; + +struct ra_challenge_indicator_node_t +{ + uint32_t id; + atlas_tile_t* image; + ra_challenge_indicator_node_t* next; +}; + +struct ra_achievement_wrapper_t +{ + atlas_tile_t* tile; + std::string title; + std::string description; +}; + +struct ra_achievement_bucket_wrapper_t +{ + std::string label; + std::vector achievements; +}; + +struct ra_achievement_list_wrapper_t +{ + explicit ra_achievement_list_wrapper_t(std::shared_ptr game_state, + rc_client_achievement_list_t* list); + + std::vector buckets{}; +}; + +static std::mutex download_cache_mutex; static std::unordered_map download_cache; +// Per game state +// What needs to happen upon destruction: +// Wait for all threads to finish +// Destroy state +// For this reason we have a reference counter and a condition variable we can +// wait on +struct ra_game_state_t +{ + ~ra_game_state_t() + { + printf("TODO: game_state destroy\n"); + } + + atlas_tile_t* game_image; + std::vector atlases{}; + std::unordered_map image_cache{}; + std::unique_ptr achievement_list; + std::atomic_int outstanding_requests; + std::condition_variable cv; + std::mutex mutex; + + void inc() + { + outstanding_requests++; + printf("inc %d\n", outstanding_requests.load()); + } + + void dec() + { + outstanding_requests--; + printf("dec %d\n", outstanding_requests.load()); + if (outstanding_requests == 0) + { + cv.notify_all(); + } + } +}; + +struct ra_state_t +{ + explicit ra_state_t(sb_emu_state_t* state) : emu_state(state) {} + + ~ra_state_t(); + ra_state_t(const ra_state_t&) = delete; + ra_state_t& operator=(const ra_state_t&) = delete; + ra_state_t(ra_state_t&&) = delete; + ra_state_t& operator=(ra_state_t&&) = delete; + + std::string username, password; + sb_emu_state_t* emu_state = nullptr; + rc_client_t* rc_client = nullptr; + + // TODO: make widgets that use these lists and progress indicator + // ra_tracker_node_t* tracker_list; + // ra_challenge_indicator_node_t* challenge_indicator_list; + // atlas_tile_t* progress_indicator_image; + // bool progress_indicator_shown; + // char measured_progress[24]; + bool pending_login = false; + + std::shared_ptr game_state; + + void download(std::shared_ptr game_state, const std::string& url, + atlas_tile_t*& out_tile); + void handle_downloaded(std::shared_ptr game_state, const std::string& url, + atlas_tile_t*& out_tile); + void rebuild_achievement_list(std::shared_ptr game_state); +}; + +static ra_state_t* ra_state = nullptr; + // Atlases are always square and power of two -// This always starts as a single tile image, but if a new tile needs to be added, it's resized -// to the next power of two +// This always starts as a single tile image, but if a new tile needs to be +// added, it's resized to the next power of two struct atlas_t { atlas_t(uint32_t tile_width, uint32_t tile_height) : tile_width(tile_width), tile_height(tile_height) { + image.id = SG_INVALID_ID; } ~atlas_t() = default; @@ -62,8 +185,8 @@ struct atlas_t atlas_t(atlas_t&&) = default; atlas_t& operator=(atlas_t&&) = default; - sg_image image = {}; std::vector data; // we construct the atlas here before uploading it to the GPU + sg_image image = {}; int pixel_stride = 0; int offset_x = 0, offset_y = 0; // to keep track of where next tile needs to be placed, in pixels @@ -87,22 +210,8 @@ struct atlas_t assert(image->width == tile_width); -#ifdef DEBUG_RETRO_ACHIEVEMENTS - std::string url; - // find the url of the image - for (auto& downloads : download_cache) - { - if (downloads.second == image) - { - url = downloads.first; - break; - } - } - - assert(!url.empty()); - - printf("copying image %s to %d %d\n", url.c_str(), tile_offset_x, tile_offset_y); -#endif + printf("pixel_stride %d, offset_x %d, offset_y %d\n", pixel_stride, tile_offset_x, + tile_offset_y); for (int y = 0; y < tile_height; y++) { @@ -112,6 +221,9 @@ struct atlas_t ((tile_offset_x + x) * 4) + (((tile_offset_y + y) * pixel_stride) * 4); uint32_t tile_offset = x * 4 + (y * 4 * tile_width); + assert(atlas_offset + 3 < data.size()); + assert(tile_offset + 3 < tile_width * tile_height * 4); + data[atlas_offset + 0] = image->data[tile_offset + 0]; data[atlas_offset + 1] = image->data[tile_offset + 1]; data[atlas_offset + 2] = image->data[tile_offset + 2]; @@ -121,78 +233,388 @@ struct atlas_t } }; -// Some stuff needs to run on the UI thread, such as sg_make_image, so we queue it up -static std::vector> pending_callbacks; - -// Used by rcheevos to make http requests -extern "C" void ra_server_callback(const rc_api_request_t* request, - rc_client_server_callback_t callback, void* callback_data, - rc_client_t* client) +namespace { - std::string url = request->url; - std::string content_type = request->content_type; - std::string post_data = request->post_data; - http_request_e type; + void retro_achievements_load_game_callback(int result, const char* error_message, + rc_client_t* client, void* userdata) + { + std::shared_ptr* game_state = (std::shared_ptr*)userdata; - if (post_data.empty()) + if (result != RC_OK) + { + // TODO: notification error message? + printf("[rcheevos]: failed to load game: %s\n", error_message); + } + else + { + char url[512]; + const rc_client_game_t* game = rc_client_get_game_info(ra_state->rc_client); + if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK) + { + ra_state->download(*game_state, url, (*game_state)->game_image); + } + + ra_state->rebuild_achievement_list(*game_state); + } + + (*game_state)->dec(); + + delete game_state; // delete the shared_ptr, thus decrementing the refcounter + } + + void retro_achievements_login_callback(int result, const char* error_message, + rc_client_t* client, void* userdata) { - type = http_request_e::GET; + // TODO: show cool "logged in" banner or something + ra_state_t* state = (ra_state_t*)userdata; + const rc_client_user_t* user = rc_client_get_user_info(client); + + if (user) + { + printf("[rcheevos]: logged in as %s (score: %d)\n", user->display_name, user->score); + state->password.clear(); + + std::string data; + data = state->username + "\n" + user->token + "\n"; + + std::string path = se_get_pref_path(); + path += "ra_token.txt"; + + sb_save_file_data(path.c_str(), (const uint8_t*)data.data(), data.size()); + retro_achievements_load_game(); + } + + state->pending_login = false; } - else + + void retro_achievements_event_handler(const rc_client_event_t* event, rc_client_t* client) { - type = http_request_e::POST; + // This should only be called from when we step the rcheevos library manually. + // At that point, game_state is valid and we can use it + if (!is_ui_thread) + { + printf("event handler called from non-ui thread\n"); + exit(1); + } + + switch (event->type) + { + case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED: + printf("[rcheevos]: Achievement unlocked: %s\n", event->achievement->title); + ra_state->rebuild_achievement_list(ra_state->game_state); + break; + case RC_CLIENT_EVENT_LEADERBOARD_STARTED: + printf("STUB: Leaderboard started\n"); + // se_ra_leaderboard_attempt_started(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_FAILED: + printf("STUB: Leaderboard failed\n"); + // se_ra_leaderboard_attempt_failed(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: + printf("STUB: Leaderboard submitted\n"); + // se_ra_leaderboard_attempt_submitted(event->leaderboard); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: + printf("STUB: Leaderboard tracker update\n"); + // se_ra_leaderboard_tracker_update(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: + printf("STUB: Leaderboard tracker show\n"); + // se_ra_leaderboard_tracker_show(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: + printf("STUB: Leaderboard tracker hide\n"); + // se_ra_leaderboard_tracker_hide(event->leaderboard_tracker); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW: + printf("STUB: Challenge indicator show\n"); + // se_ra_challenge_indicator_show(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE: + printf("STUB: Challenge indicator hide\n"); + // se_ra_challenge_indicator_hide(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + printf("STUB: Progress indicator show\n"); + // se_ra_progress_indicator_show(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + printf("STUB: Progress indicator update\n"); + // se_ra_progress_indicator_update(event->achievement); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + printf("STUB: Progress indicator hide\n"); + // se_ra_progress_indicator_hide(event->achievement); + break; + case RC_CLIENT_EVENT_GAME_COMPLETED: + printf("STUB: Game completed\n"); + // se_ra_game_mastered(); + break; + case RC_CLIENT_EVENT_RESET: + ra_state->emu_state->run_mode = SB_MODE_RESET; + break; + case RC_CLIENT_EVENT_SERVER_ERROR: + printf("[rcheevos]: Server error: %s %s\n", event->server_error->api, + event->server_error->error_message); + break; + default: + printf("Unhandled event %d\n", event->type); + break; + } } - url += "?" + post_data; + // Used by rcheevos to make http requests + void retro_achievements_server_callback(const rc_api_request_t* request, + rc_client_server_callback_t callback, + void* callback_data, rc_client_t* client) + { + std::string url = request->url; + std::string post_data = request->post_data; + http_request_e type; + + if (post_data.empty()) + { + type = http_request_e::GET; + } + else + { + type = http_request_e::POST; + } + + url += "?" + post_data; + std::vector> headers; #ifndef EMSCRIPTEN - std::thread thread( - [type, url, post_data, callback, callback_data]() { // TODO: remove this thread? -#endif - std::vector> headers; -#ifndef EMSCRIPTEN - // TODO(paris): When setting User-Agent from browser side, it sends a CORS preflight - // request which is makes the request fail. - headers.push_back({"User-Agent", "SkyEmu/4.0"}); -#endif - https_request(type, url, {}, headers, - [callback, callback_data](const std::vector& result) { - if (result.empty()) - { - printf("[rcheevos]: empty response\n"); - return; - } - - // Heavy work (http request) is done, do rest of the work in the ui - // thread to avoid potential synchronization issues - std::unique_lock lock(*synchronization_mutex); - pending_callbacks.push_back( - [result, callback, callback_data]() { // TODO: std::move? - rc_api_server_response_t response; - response.body = (const char*)result.data(); - response.body_length = result.size(); - response.http_status_code = 200; - callback(&response, callback_data); - }); - }); -#ifndef EMSCRIPTEN - }); - thread.detach(); + // TODO(paris): When setting User-Agent from browser side, it sends a CORS + // preflight request which is makes the request fail. + headers.push_back({"User-Agent", "SkyEmu/4.0"}); #endif + + https_request(type, url, {}, headers, + [callback, callback_data](const std::vector& result) { + if (result.empty()) + { + printf("[rcheevos]: empty response\n"); + } + else + { + // Heavy work (http request) is done, do rest of the work in + // the ui thread to avoid potential synchronization issues + rc_api_server_response_t response; + response.body = (const char*)result.data(); + response.body_length = result.size(); + response.http_status_code = 200; + callback(&response, callback_data); + } + }); + } + + void retro_achievements_log_callback(const char* message, const rc_client_t* client) + { + printf("[rcheevos - internal]: %s\n", message); + } + + void retro_achievements_draw_achievements() + { + if (ra_state->game_state->achievement_list) + { + for (int i = 0; i < ra_state->game_state->achievement_list->buckets.size(); i++) + { + ra_achievement_bucket_wrapper_t* bucket = + &ra_state->game_state->achievement_list->buckets[i]; + se_text(ICON_FK_LOCK " %s", bucket->label.c_str()); + for (int j = 0; j < bucket->achievements.size(); j++) + { + sg_image image = {SG_INVALID_ID}; + ImVec2 uv0, uv1; + if (bucket->achievements[j].tile) + { + atlas_tile_t* tile = bucket->achievements[j].tile; + uv0 = (ImVec2){tile->x1, tile->y1}; + uv1 = (ImVec2){tile->x2, tile->y2}; + image.id = tile->atlas_id; + } + // #ifdef DEBUG_RETRO_ACHIEVEMENTS + // static char name[2048]; + // memset(name,0,2048); + // if (image.id != SG_INVALID_ID) { + // sg_image_info info = sg_query_image_info(image); + // int ofx = uv0.x * info.width; + // int ofy = uv0.y * info.height; + // int of1x = uv1.x * info.width; + // int of1y = uv1.y * info.height; + // snprintf(name,2048,"%s %p (%d) %d %d %d + // %d",list->buckets[i].achievements[j]->title, + // ra_info.achievement_images[i][j],image.id,ofx,ofy,of1x,of1y); + // } else { + // if (!ra_info.achievement_images[i]) { + // snprintf(name,2048,"%s NULL ROW + // (%d)",list->buckets[i].achievements[j]->title,image.id); + // } else { + // snprintf(name,2048,"%s %p + // (%d)",list->buckets[i].achievements[j]->title, + // ra_info.achievement_images[i][j],image.id); + // } + // } + // se_boxed_image_dual_label(name,list->buckets[i].achievements[j]->description, + // ICON_FK_SPINNER, image, 0, uv0, uv1); + // #else + const ra_achievement_wrapper_t& achievement = bucket->achievements[j]; + se_boxed_image_dual_label(achievement.title.c_str(), + achievement.description.c_str(), ICON_FK_SPINNER, + image, 0, uv0, uv1); + // #endif + } + } + } + } +} // namespace + +ra_achievement_list_wrapper_t::ra_achievement_list_wrapper_t( + std::shared_ptr game_state, rc_client_achievement_list_t* list) +{ + buckets.resize(list->num_buckets); + + for (int i = 0; i < list->num_buckets; i++) + { + buckets[i].label = list->buckets[i].label; + buckets[i].achievements.resize(list->buckets[i].num_achievements); + } + + for (int i = 0; i < list->num_buckets; i++) + { + for (int j = 0; j < list->buckets[i].num_achievements; j++) + { + std::string url; + url.resize(512); + + buckets[i].achievements[j].title = list->buckets[i].achievements[j]->title; + buckets[i].achievements[j].description = list->buckets[i].achievements[j]->description; + + const rc_client_achievement_t* achievement = list->buckets[i].achievements[j]; + if (rc_client_achievement_get_image_url(achievement, achievement->state, &url[0], + url.size()) == RC_OK) + { + ra_state->download(game_state, url, buckets[i].achievements[j].tile); + } + + printf("[rcheevos]: Achievement %s, ", achievement->title); + if (achievement->id == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) + printf("unsupported\n"); + else if (achievement->unlocked) + printf("unlocked\n"); + else if (achievement->measured_percent) + printf("progress: %f%%\n", achievement->measured_percent); + else + printf("locked\n"); + } + } + + rc_client_destroy_achievement_list(list); } -extern "C" void ra_log_callback(const char* message, const rc_client_t* client) +ra_state_t::~ra_state_t() { - printf("[rcheevos - internal]: %s\n", message); + printf("TODO: cleanup\n"); + // ra_reset(); + // Can't run ra_reset now because the sg_images have already been used for + // this frame, we need to run it after the frame is done + // rc_client_achievement_list_t* list = ra_info.achievement_list; + // if (list){ + // for (int i = 0; i < list->num_buckets; i++) + // { + // if(ra_info.achievement_images[i] != NULL) + // { + // free(ra_info.achievement_images[i]); + // } + // } + // free(ra_info.achievement_images); + // ra_info.achievement_images = NULL; + // } + // { + // se_ra_tracker_node_t* next = ra_info.tracker_list; + // while(next){ + // se_ra_tracker_node_t* tmp = next; + // next = next->next; + // free(tmp); + // } + // ra_info.tracker_list = NULL; + // } + // { + // se_ra_challenge_indicator_node_t* next = + // ra_info.challenge_indicator_list; while(next){ + // se_ra_challenge_indicator_node_t* tmp = next; + // next = next->next; + // free(tmp); + // } + // ra_info.challenge_indicator_list = NULL; + // } + // ra_info.image = NULL; + // ra_info.achievement_list = NULL; } -// We got some data (either by downloading it, or from the cache), let's handle it -void handle_downloaded_image(const char* url, atlas_tile_t** out_image) +void ra_state_t::download(std::shared_ptr game_state, const std::string& url, + atlas_tile_t*& out_tile) +{ + std::unique_lock glock(game_state->mutex); + std::unique_lock lock(download_cache_mutex); + + if (download_cache.find(url) != download_cache.end()) + { + // Great, image was already downloaded in the past and is in the cache + // First, let's see if there's already an atlas for this image + if (game_state->image_cache.find(url) != game_state->image_cache.end()) + { + out_tile = &game_state->image_cache[url]; + return; + } + else + { + // We have the image downloaded, but we need to create an atlas for it + handle_downloaded(game_state, url, out_tile); + return; + } + } + lock.unlock(); + + game_state->inc(); + // The image is not already downloaded, let's download it + https_request(http_request_e::GET, url, {}, {}, + [url, &out_tile, game_state](const std::vector& result) { + rc_api_server_response_t response; + response.body = (const char*)result.data(); + response.body_length = result.size(); + response.http_status_code = 200; + + downloaded_image_t* image = new downloaded_image_t(); + image->data = + stbi_load_from_memory((const uint8_t*)response.body, response.body_length, + &image->width, &image->height, NULL, 4); + + if (!image->data) + { + printf("[rcheevos]: failed to load image from memory\n"); + } + else + { + std::unique_lock glock(game_state->mutex); + std::unique_lock lock(download_cache_mutex); + download_cache[url] = image; + ra_state->handle_downloaded(game_state, url, out_tile); + } + + game_state->dec(); + }); +} + +void ra_state_t::handle_downloaded(std::shared_ptr game_state, + const std::string& url, atlas_tile_t*& out_tile) { downloaded_image_t* image = download_cache[url]; atlas_t* atlas = nullptr; // Check if we already have an atlas for this exact tile size - for (atlas_t* a : atlases) + for (atlas_t* a : game_state->atlases) { if (a->tile_width == image->width && a->tile_height == image->height) { @@ -205,7 +627,7 @@ void handle_downloaded_image(const char* url, atlas_tile_t** out_image) if (!atlas) { atlas_t* new_atlas = new atlas_t(image->width, image->height); - atlases.push_back(new_atlas); + game_state->atlases.push_back(new_atlas); atlas = new_atlas; } @@ -216,10 +638,9 @@ void handle_downloaded_image(const char* url, atlas_tile_t** out_image) { // We need to resize and upload the atlas later atlas->resized = true; - printf("resizing atlas from %dx%d ", atlas->pixel_stride, atlas->pixel_stride); // Find a sufficient power of two - uint32_t power = 256; + uint32_t power = 2048; uint32_t max = std::max(minimum_width, minimum_height); while (power < max) { @@ -227,13 +648,11 @@ void handle_downloaded_image(const char* url, atlas_tile_t** out_image) if (power > 4096) { - printf("[rcheevos]: making atlas too big (%dx%d), this shouldn't happen\n", power, - power); + printf("Atlas too large\n"); + exit(1); } } - printf("to %dx%d\n", power, power); - uint32_t old_stride = atlas->pixel_stride; atlas->pixel_stride = power; atlas->offset_x = 0; @@ -250,27 +669,25 @@ void handle_downloaded_image(const char* url, atlas_tile_t** out_image) stbi_write_png(fname.c_str(), old_stride, old_stride, 4, atlas->data.data(), 0); #endif + printf("Resizing atlas from %dx%d (%d) to %dx%d (%d)\n", old_stride, old_stride, + atlas->data.size(), power, power, new_data.size()); atlas->data.swap(new_data); // Copy every existing downloaded image of this size - for (auto& downloaded_image : download_cache) + for (auto& cached_image : game_state->image_cache) { - bool same_size = downloaded_image.second->width == image->width && - downloaded_image.second->height == image->height; - if (same_size && url != downloaded_image.first) + if (cached_image.second.width == image->width && + cached_image.second.height == image->height) { - // First, recalculate the offset - auto& tile = image_cache[downloaded_image.first]; + auto& tile = game_state->image_cache[cached_image.first]; uint32_t tile_offset_x = atlas->offset_x; uint32_t tile_offset_y = atlas->offset_y; tile.x1 = (float)tile_offset_x / atlas->pixel_stride; tile.y1 = (float)tile_offset_y / atlas->pixel_stride; - tile.x2 = - (float)(tile_offset_x + downloaded_image.second->width) / atlas->pixel_stride; - tile.y2 = - (float)(tile_offset_y + downloaded_image.second->height) / atlas->pixel_stride; + tile.x2 = (float)(tile_offset_x + cached_image.second.width) / atlas->pixel_stride; + tile.y2 = (float)(tile_offset_y + cached_image.second.height) / atlas->pixel_stride; - atlas->copy_image(downloaded_image.second); + atlas->copy_image(download_cache[cached_image.first]); } } @@ -281,131 +698,178 @@ void handle_downloaded_image(const char* url, atlas_tile_t** out_image) #endif } - // At this point we should have an atlas that has enough room for our incoming tile + // At this point we should have an atlas that has enough room for our incoming + // tile int offset_x = atlas->offset_x; int offset_y = atlas->offset_y; atlas->copy_image(image); - atlas_tile_t& tile = image_cache[url]; - - tile.atlas_id = atlas->image.id; - tile.x1 = (float)offset_x / atlas->pixel_stride; - tile.y1 = (float)offset_y / atlas->pixel_stride; - tile.x2 = (float)(offset_x + image->width) / atlas->pixel_stride; - tile.y2 = (float)(offset_y + image->height) / atlas->pixel_stride; - - // Pending callbacks are executed after ra_update_atlases which is what we want - printf("Pushing back for %s\n", url); - std::string url_str = url; - pending_callbacks.push_back([out_image, &tile, url_str]() { *out_image = &tile; - printf("running callback for %s\n", url_str.c_str()); - printf("out_image: %p\n", *out_image); - }); + atlas_tile_t* tile = &game_state->image_cache[url]; + + tile->atlas_id = atlas->image.id; + tile->width = image->width; + tile->height = image->height; + tile->x1 = (float)offset_x / atlas->pixel_stride; + tile->y1 = (float)offset_y / atlas->pixel_stride; + tile->x2 = (float)(offset_x + image->width) / atlas->pixel_stride; + tile->y2 = (float)(offset_y + image->height) / atlas->pixel_stride; + + out_tile = tile; } -// This should be getting called from the UI thread only, either from the load game callback -// or from the retro achievements event handler -void ra_get_image(const char* url, atlas_tile_t** out_image) +void ra_state_t::rebuild_achievement_list(std::shared_ptr game_state) { - std::unique_lock lock(*synchronization_mutex); + game_state->achievement_list.reset(new ra_achievement_list_wrapper_t( + game_state, rc_client_create_achievement_list( + rc_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL, + RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS))); +} - if (download_cache.find(url) != download_cache.end()) - { - // Great, image was already downloaded in the past and is in the cache - // First, let's see if there's already an atlas for this image - if (image_cache.find(url) != image_cache.end()) - { - // We already have an atlas for this image, just set out_image to it - // on a pending callback - atlas_tile_t& tile = image_cache[url]; - pending_callbacks.push_back([out_image, &tile]() { *out_image = &tile; }); - return; - } - else - { - // We have the image downloaded, but we need to create an atlas for it - handle_downloaded_image(url, out_image); - return; - } - } +extern "C" uint32_t retro_achievements_read_memory_callback(uint32_t address, uint8_t* buffer, + uint32_t num_bytes, + rc_client_t* client); -#ifdef DEBUG_RETRO_ACHIEVEMENTS - printf("Downloading image %s\n", url); -#endif +void retro_achievements_initialize(void* state) +{ + is_ui_thread = true; + ra_state = new ra_state_t((sb_emu_state_t*)state); + ra_state->rc_client = rc_client_create(retro_achievements_read_memory_callback, + retro_achievements_server_callback); - // The image is not already downloaded, let's download it - std::string url_str = url; // When this function returns, the const char* will be invalid, so we - // need to copy the contents to a string - https_request(http_request_e::GET, url_str, {}, {}, - [out_image, url_str](const std::vector& result) { #ifdef DEBUG_RETRO_ACHIEVEMENTS - printf("Downloaded image %s\n", url_str.c_str()); + rc_client_enable_logging(ra_state->rc_client, RC_CLIENT_LOG_LEVEL_VERBOSE, + retro_achievements_log_callback); #endif - std::unique_lock lock(*synchronization_mutex); - rc_api_server_response_t response; - response.body = (const char*)result.data(); - response.body_length = result.size(); - response.http_status_code = 200; + // TODO: should probably be an option after we're finished testing + rc_client_set_hardcore_enabled(ra_state->rc_client, 0); + rc_client_set_event_handler(ra_state->rc_client, retro_achievements_event_handler); - downloaded_image_t* image = new downloaded_image_t(); - image->data = - stbi_load_from_memory((const uint8_t*)response.body, response.body_length, - &image->width, &image->height, NULL, 4); + // RetroAchievements doesn't enable CORS, so we use a reverse proxy + // rc_api_set_host("https://api.achieve.skyemoo.pandasemi.co"); + // rc_api_set_image_host("https://media.retroachievements.org"); - if (!image->data) - { - printf("[rcheevos]: failed to load image from memory\n"); - return; - } + std::string path = se_get_pref_path(); + path += "ra_token.txt"; - download_cache[url_str] = image; + // Check if we have a token saved + if (sb_file_exists(path.c_str())) + { + size_t size; + uint8_t* data = sb_load_file_data(path.c_str(), &size); + if (data) + { + std::string text = std::string((const char*)data); - handle_downloaded_image(url_str.c_str(), out_image); - }); -} + auto result = std::vector{}; + auto ss = std::stringstream{text}; -void ra_run_pending_callbacks() -{ - // Pending callbacks is always added to from non-UI threads, so before we run them - // we need to lock the mutex - std::unique_lock lock(*synchronization_mutex); - if (pending_callbacks.empty()) - return; + for (std::string line; std::getline(ss, line, '\n');) + result.push_back(line); - std::vector> callbacks; - callbacks.swap(pending_callbacks); - lock.unlock(); + ra_state->username = result[0]; + std::string token = result[1]; - for (auto& callback : callbacks) - { - callback(); + if (!ra_state->username.empty() && !token.empty()) + { + ra_state->pending_login = true; + rc_client_begin_login_with_token(ra_state->rc_client, ra_state->username.c_str(), + token.c_str(), retro_achievements_login_callback, + ra_state); + } + + free(data); + } } } -void ra_reset() +void retro_achievements_shutdown() { - std::unique_lock lock(*synchronization_mutex); - for (auto& atlas : atlases) + // TODO: better way to handle this + if (ra_state->game_state.use_count() != 1) { - if (atlas->image.id != SG_INVALID_ID) + printf("Waiting for RetroAchievements requests to finish to clean up\n"); + while (ra_state->game_state.use_count() != 1) { - sg_destroy_image(atlas->image); + // Wait for all threads to finish and stop owning game_state + std::this_thread::sleep_for(std::chrono::milliseconds(0)); } - delete atlas; } - atlases.clear(); - image_cache.clear(); - pending_callbacks.clear(); + + ra_state->game_state.reset(); + delete ra_state; } -// This function does the actual uploading the atlas to the GPU -void ra_update_atlases() +void retro_achievements_load_game() { - std::unique_lock lock(*synchronization_mutex); - for (atlas_t* atlas : atlases) + if (!ra_state->emu_state->rom_loaded) + return; + + // We need to create a shared_ptr*, so we can pass it to the C api. + // The shared_ptr is because between rc_client_begin_identify_and_load_game + // and the callback the game state could be destroyed, but we want the result + // to still go to the correct place and not the new place. + std::shared_ptr* game_state = + new std::shared_ptr(ra_state->game_state); + + switch (ra_state->emu_state->system) + { + case SYSTEM_GB: + (*game_state)->inc(); + rc_client_begin_identify_and_load_game( + ra_state->rc_client, RC_CONSOLE_GAMEBOY, NULL, ra_state->emu_state->rom_data, + ra_state->emu_state->rom_size, retro_achievements_load_game_callback, game_state); + break; + case SYSTEM_GBA: + (*game_state)->inc(); + rc_client_begin_identify_and_load_game( + ra_state->rc_client, RC_CONSOLE_GAMEBOY_ADVANCE, NULL, + ra_state->emu_state->rom_data, ra_state->emu_state->rom_size, + retro_achievements_load_game_callback, game_state); + break; + case SYSTEM_NDS: + (*game_state)->inc(); + rc_client_begin_identify_and_load_game( + ra_state->rc_client, RC_CONSOLE_NINTENDO_DS, NULL, ra_state->emu_state->rom_data, + ra_state->emu_state->rom_size, retro_achievements_load_game_callback, game_state); + break; + } +} + +void retro_achievements_frame() +{ + rc_client_do_frame(ra_state->rc_client); +} + +void retro_achievements_login(const char* username, const char* password) +{ + ra_state->pending_login = true; + rc_client_begin_login_with_password(ra_state->rc_client, username, password, + retro_achievements_login_callback, NULL); +} + +void retro_achievements_logout() +{ + rc_client_logout(ra_state->rc_client); +} + +atlas_tile_t* retro_achievements_get_game_image() +{ + return ra_state->game_state->game_image; +} + +void retro_achievements_update_atlases() +{ + if (!ra_state->game_state) + return; + + if (ra_state->game_state->outstanding_requests.load() != 0) + return; // probably a lot of outstanding requests hold the mutex, let's wait for them to finish before we try to lock ourselves to prevent stuttering + + std::unique_lock lock(ra_state->game_state->mutex); + + for (atlas_t* atlas : ra_state->game_state->atlases) { - int invalidated_id = atlas->image.id; if (atlas->resized) { if (atlas->image.id != SG_INVALID_ID) @@ -442,18 +906,18 @@ void ra_update_atlases() if (atlas->resized) { - for (auto& image : image_cache) + for (auto& image : ra_state->game_state->image_cache) { // Update the images to point to the new atlas instead - if (image.second.atlas_id == invalidated_id) + if (image.second.width == atlas->tile_width && + image.second.height == atlas->tile_height) { - printf("Updating atlas id from %d to %d for %p\n", invalidated_id, atlas->image.id, &image.second); image.second.atlas_id = atlas->image.id; } } } } - + if (atlas->dirty) { sg_image_data data = {0}; @@ -468,49 +932,77 @@ void ra_update_atlases() } } -mutex_t ra_get_mutex() -{ - return synchronization_mutex; -} - -// Clean up everything for when SkyEmu is about to close -void ra_shutdown() -{ - ra_reset(); - for (auto& downloads : download_cache) - { - stbi_image_free(downloads.second->data); - delete downloads.second; - } - delete synchronization_mutex; -} - -void ra_dump_atlases() +void retro_achievements_draw_panel(int win_w) { - printf("At time of dump, there are %d atlases\n", atlases.size()); - - for (int i = 0; i < atlases.size(); i++) + const rc_client_user_t* user = rc_client_get_user_info(ra_state->rc_client); + if (!user) { - atlas_t* atlas = atlases[i]; - printf("Atlas %d (id: %d): %dx%d, saved as %s\n", i, atlas->image.id, atlas->pixel_stride, - atlas->pixel_stride, ("atlas" + std::to_string(i) + ".png").c_str()); - stbi_write_png(("atlas" + std::to_string(i) + ".png").c_str(), atlas->pixel_stride, - atlas->pixel_stride, 4, atlas->data.data(), 0); + static char username[256] = {0}; + static char password[256] = {0}; + igPushIDStr("RetroAchievementsLogin"); + bool pending_login = ra_state->pending_login; + se_text("Username"); + igSameLine(win_w - 150, 0); + if (pending_login) + se_push_disabled(); + bool enter = igInputText("##Username", username, sizeof(username), + ImGuiInputTextFlags_EnterReturnsTrue, NULL, NULL); + if (pending_login) + se_pop_disabled(); + se_text("Password"); + igSameLine(win_w - 150, 0); + if (pending_login) + se_push_disabled(); + enter |= igInputText("##Password", password, sizeof(password), + ImGuiInputTextFlags_Password | ImGuiInputTextFlags_EnterReturnsTrue, + NULL, NULL); + if (se_button(ICON_FK_SIGN_IN " Login", (ImVec2){0, 0}) || enter) + { + retro_achievements_login(username, password); + } + if (pending_login) + se_pop_disabled(); + igPopID(); } - - printf("Dumped %d atlases\n", atlases.size()); - - printf("There's %d atlas mappings\n", image_cache.size()); - for (auto& image : image_cache) + else { - printf("%s (%p): (points to id: %d) %f %f %f %f\n", image.first.c_str(), &image.second, image.second.atlas_id, - image.second.x1, image.second.y1, image.second.x2, image.second.y2); + const rc_client_game_t* game = rc_client_get_game_info(ra_state->rc_client); + ImVec2 pos; + sg_image image = {SG_INVALID_ID}; + ImVec2 offset1 = {0, 0}; + ImVec2 offset2 = {1, 1}; + const char* play_string = "No Game Loaded"; + char line1[256]; + char line2[256]; + snprintf(line1, 256, se_localize_and_cache("Logged in as %s"), user->display_name); + atlas_tile_t* game_image = retro_achievements_get_game_image(); + if (game && game_image) + { + image.id = game_image->atlas_id; + offset1 = (ImVec2){game_image->x1, game_image->y1}; + offset2 = (ImVec2){game_image->x2, game_image->y2}; +#ifdef DEBUG_RETRO_ACHIEVEMENTS + snprintf(line2, 256, se_localize_and_cache("Playing: %s (%d)"), game->title, image.id); +#else + snprintf(line2, 256, se_localize_and_cache("Playing: %s"), game->title); +#endif + } + else + snprintf(line2, 256, "%s", se_localize_and_cache("No Game Loaded")); + se_boxed_image_dual_label(line1, line2, ICON_FK_TROPHY, image, 0, offset1, offset2); + if (se_button(ICON_FK_SIGN_OUT " Logout", (ImVec2){0, 0})) + { + char login_info_path[SB_FILE_PATH_SIZE]; + snprintf(login_info_path, SB_FILE_PATH_SIZE, "%sra_token.txt", se_get_pref_path()); + remove(login_info_path); + printf("TODO: logout\n"); + } + retro_achievements_draw_achievements(); } +} - printf("There's %d downloads in the cache\n", download_cache.size()); - for (auto& downloads : download_cache) - { - printf("%s: %dx%d\n", downloads.first.c_str(), downloads.second->width, - downloads.second->height); - } +void retro_achievements_destroy_game_state() +{ + ra_state->game_state.reset( + new ra_game_state_t()); // the old one will be destroyed when the last reference is gone } \ No newline at end of file diff --git a/src/retro_achievements.h b/src/retro_achievements.h index 3680c43d1..4d73d847b 100644 --- a/src/retro_achievements.h +++ b/src/retro_achievements.h @@ -15,22 +15,26 @@ typedef struct { uint32_t atlas_id; + uint32_t width, height; float x1, y1; float x2, y2; } atlas_tile_t; -void ra_server_callback(const rc_api_request_t* request, - rc_client_server_callback_t callback, void* callback_data, rc_client_t* client); -void ra_log_callback(const char* message, const rc_client_t* client); - -// Either finds the image in the atlas, sets out_image to it and immediately returns, or starts a download -// and creates a pending callback so out_image is set on the UI thread -void ra_get_image(const char* url, atlas_tile_t** out_image); -void ra_run_pending_callbacks(); -void ra_reset(); -void ra_shutdown(); -void ra_update_atlases(); -mutex_t ra_get_mutex(); // TODO: can we delete this and only lock mutex stuff from .cpp file -void ra_dump_atlases(); +typedef struct ra_state_t ra_state_t; + +void retro_achievements_initialize(void* emu_state); + +void retro_achievements_shutdown(); + +// this will asynchronously free the old state and create a new one +void retro_achievements_destroy_game_state(); + +void retro_achievements_load_game(); + +void retro_achievements_frame(); + +void retro_achievements_draw_panel(int win_w); + +void retro_achievements_update_atlases(); #endif \ No newline at end of file