From 300cf0d437f27f01b9ef8e51819968e69c2d2646 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 1 May 2024 18:32:39 +0200 Subject: [PATCH] better RetroAchievements UI and threading Pop ups for authentication, game load, game completed, achievements unlock and progress. Handle disk changes. Issue #761 --- CMakeLists.txt | 6 +- core/achievements/achievements.cpp | 424 +++++++++++++----- core/achievements/achievements.h | 11 +- core/deps/imgui/imgui_stdlib.cpp | 85 ++++ core/deps/imgui/imgui_stdlib.h | 21 + core/emulator.cpp | 27 +- core/emulator.h | 6 +- core/hw/holly/sb.cpp | 1 + core/imgread/common.cpp | 1 + core/lua/lua.cpp | 3 + core/oslib/http_client.cpp | 23 +- core/oslib/http_client.h | 2 +- core/rend/gui.cpp | 71 +-- core/rend/gui_achievements.cpp | 171 +++++++ core/rend/gui_achievements.h | 56 +++ fonts/Roboto-Regular.ttf.zip | Bin 0 -> 88941 bytes .../com/reicast/emulator/emu/HttpClient.java | 4 +- .../flycast/src/main/jni/src/http_client.h | 6 +- shell/apple/common/http_client.mm | 8 +- shell/uwp/http_client.cpp | 8 +- 20 files changed, 754 insertions(+), 180 deletions(-) create mode 100644 core/deps/imgui/imgui_stdlib.cpp create mode 100644 core/deps/imgui/imgui_stdlib.h create mode 100644 core/rend/gui_achievements.cpp create mode 100644 core/rend/gui_achievements.h create mode 100644 fonts/Roboto-Regular.ttf.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index ff6334a76..dd658157a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -712,6 +712,7 @@ if(NOT LIBRETRO) core/deps/imgui/imgui.cpp core/deps/imgui/imgui_demo.cpp core/deps/imgui/imgui_draw.cpp + core/deps/imgui/imgui_stdlib.cpp core/deps/imgui/imgui_tables.cpp core/deps/imgui/imgui_widgets.cpp) @@ -1012,7 +1013,9 @@ cmrc_add_resources(flycast-resources fonts/printer_kanji24x24.bin.zip) if(NOT LIBRETRO) - cmrc_add_resources(flycast-resources fonts/Roboto-Medium.ttf.zip) + cmrc_add_resources(flycast-resources + fonts/Roboto-Medium.ttf.zip + fonts/Roboto-Regular.ttf.zip) if(ANDROID) cmrc_add_resources(flycast-resources WHENCE resources @@ -1292,6 +1295,7 @@ if(NOT LIBRETRO) core/rend/imgui_driver.h core/rend/gui.cpp core/rend/gui.h + core/rend/gui_achievements.cpp core/rend/gui_android.cpp core/rend/gui_android.h core/rend/gui_chat.h diff --git a/core/achievements/achievements.cpp b/core/achievements/achievements.cpp index 028c93ef1..6d544e184 100644 --- a/core/achievements/achievements.cpp +++ b/core/achievements/achievements.cpp @@ -20,9 +20,10 @@ #include "achievements.h" #include "serialize.h" #ifdef USE_RACHIEVEMENTS +#include "oslib/directory.h" #include "oslib/http_client.h" #include "hw/sh4/sh4_mem.h" -#include "rend/gui.h" +#include "rend/gui_achievements.h" #include "imgread/common.h" #include "cfg/option.h" #include "oslib/oslib.h" @@ -31,7 +32,11 @@ #include #include #include -#include +#include +#include +#include +#include +#include namespace achievements { @@ -43,7 +48,8 @@ public: ~Achievements(); bool init(); void term(); - void login(const char *username, const char *password); + std::future login(const char *username, const char *password); + void logout(); bool isLoggedOn() const { return loggedOn; } void serialize(Serializer& ser); void deserialize(Deserializer& deser); @@ -54,31 +60,42 @@ private: bool createClient(); std::string getGameHash(); void loadGame(); + void gameLoaded(int result, const char *errorMessage); void unloadGame(); void pauseGame(); void resumeGame(); + void loadCache(); + std::string getOrDownloadImage(const char *url); + void diskChange(); static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata); static void clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, void *userdata); + void authenticationSuccess(const rc_client_user_t *user); static void clientMessageCallback(const char *message, const rc_client_t *client); static u32 clientReadMemory(u32 address, u8 *buffer, u32 num_bytes, rc_client_t *client); static void clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, void *callback_data, rc_client_t *client); static void clientEventHandler(const rc_client_event_t *event, rc_client_t *client); - static void clientLoadGameCallback(int result, const char *error_message, rc_client_t *client, void *userdata); void handleResetEvent(const rc_client_event_t *event); void handleUnlockEvent(const rc_client_event_t *event); void handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event); void handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event); + void handleGameCompleted(const rc_client_event_t *event); + void handleShowAchievementProgress(const rc_client_event_t *event); + void handleHideAchievementProgress(const rc_client_event_t *event); + void handleUpdateAchievementProgress(const rc_client_event_t *event); static void emuEventCallback(Event event, void *arg); rc_client_t *rc_client = nullptr; bool loggedOn = false; + std::atomic_bool loadingGame {}; bool active = false; bool paused = false; std::string lastError; + std::future asyncServerCall; cResetEvent resetEvent; - std::thread thread; + std::string cachePath; + std::unordered_map cacheMap; }; bool init() @@ -91,9 +108,14 @@ void term() Achievements::Instance().term(); } -void login(const char *username, const char *password) +std::future login(const char *username, const char *password) { - Achievements::Instance().login(username, password); + return Achievements::Instance().login(username, password); +} + +void logout() +{ + Achievements::Instance().logout(); } bool isLoggedOn() @@ -124,6 +146,7 @@ Achievements::Achievements() EventManager::listen(Event::Terminate, emuEventCallback, this); EventManager::listen(Event::Pause, emuEventCallback, this); EventManager::listen(Event::Resume, emuEventCallback, this); + EventManager::listen(Event::DiskChange, emuEventCallback, this); } Achievements::~Achievements() @@ -132,6 +155,7 @@ Achievements::~Achievements() EventManager::unlisten(Event::Terminate, emuEventCallback, this); EventManager::unlisten(Event::Pause, emuEventCallback, this); EventManager::unlisten(Event::Resume, emuEventCallback, this); + EventManager::unlisten(Event::DiskChange, emuEventCallback, this); term(); } @@ -150,6 +174,7 @@ bool Achievements::init() //rc_client_set_encore_mode_enabled(rc_client, 0); //rc_client_set_unofficial_enabled(rc_client, 0); //rc_client_set_spectator_mode_enabled(rc_client, 0); + loadCache(); if (!config::AchievementsUserName.get().empty() && !config::AchievementsToken.get().empty()) { @@ -182,6 +207,58 @@ bool Achievements::createClient() return true; } +void Achievements::loadCache() +{ + cachePath = get_writable_data_path("achievements/"); + flycast::mkdir(cachePath.c_str(), 0755); + DIR *dir = flycast::opendir(cachePath.c_str()); + if (dir != nullptr) + { + while (true) + { + dirent *direntry = flycast::readdir(dir); + if (direntry == nullptr) + break; + std::string name = direntry->d_name; + if (name == "." || name == "..") + continue; + std::string s = get_file_basename(name); + u64 v = strtoull(s.c_str(), nullptr, 16); + cacheMap[v] = name; + } + } +} + +std::string Achievements::getOrDownloadImage(const char *url) +{ + u64 hash = XXH64(url, strlen(url), 13); + auto it = cacheMap.find(hash); + if (it != cacheMap.end()) + return cachePath + it->second; + std::vector content; + std::string content_type; + int rc = http::get(url, content, content_type); + if (!http::success(rc)) + return {}; + std::stringstream stream; + stream << std::hex << hash; + if (content_type == "image/jpeg") + stream << ".jpg"; + else + stream << ".png"; + std::string path = cachePath + stream.str(); + FILE *f = nowide::fopen(path.c_str(), "wb"); + if (f == nullptr) { + WARN_LOG(COMMON, "Can't save image to %s", path.c_str()); + return {}; + } + fwrite(content.data(), 1, content.size(), f); + fclose(f); + cacheMap[hash] = stream.str(); + DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, path.c_str()); + return path; +} + void Achievements::term() { if (rc_client == nullptr) @@ -191,48 +268,56 @@ void Achievements::term() rc_client = nullptr; } -static inline void authenticationSuccessMsg() +void Achievements::authenticationSuccess(const rc_client_user_t *user) { NOTICE_LOG(COMMON, "RA Login successful: token %s", config::AchievementsToken.get().c_str()); - std::string msg = "User " + config::AchievementsUserName.get() + " authenticated to RetroAchievements"; - gui_display_notification(msg.c_str(), 5000); + char url[512]; + int rc = rc_client_user_get_image_url(user, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + std::string text = "User " + config::AchievementsUserName.get() + " authenticated"; + notifier.notify(Notification::Login, image, text); + } + loggedOn = true; + if (!settings.content.fileName.empty()) // TODO better test? + loadGame(); } -void Achievements::clientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, - void* userdata) +void Achievements::clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, + void *userdata) { + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); if (result != RC_OK) { WARN_LOG(COMMON, "RA Login failed: %s", error_message); - gui_display_notification(error_message, 10000); + notifier.notify(Notification::Login, "", "RetroAchievements authentication failed", error_message); return; } - - authenticationSuccessMsg(); - Achievements *achievements = (Achievements *)rc_client_get_userdata(client); - achievements->loggedOn = true; + achievements->authenticationSuccess(rc_client_get_user_info(client)); } -void Achievements::login(const char* username, const char* password) +std::future Achievements::login(const char* username, const char* password) { init(); - rc_client_begin_login_with_password(rc_client, username, password, clientLoginWithPasswordCallback, nullptr); - - if (!loggedOn) - throw FlycastException(lastError); + std::promise *promise = new std::promise(); + rc_client_begin_login_with_password(rc_client, username, password, clientLoginWithPasswordCallback, promise); + return promise->get_future(); } -void Achievements::clientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, - void* userdata) +void Achievements::clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, + void *userdata) { Achievements *achievements = (Achievements *)rc_client_get_userdata(client); - + std::promise *promise = (std::promise *)userdata; if (result != RC_OK) { - achievements->lastError = rc_error_str(result); + std::string errorMsg = rc_error_str(result); if (error_message != nullptr) - achievements->lastError += ": " + std::string(error_message); - WARN_LOG(COMMON, "RA Login failed: %s", achievements->lastError.c_str()); + errorMsg += ": " + std::string(error_message); + promise->set_exception(std::make_exception_ptr(FlycastException(errorMsg))); + delete promise; + WARN_LOG(COMMON, "RA Login failed: %s", errorMsg.c_str()); return; } @@ -240,15 +325,27 @@ void Achievements::clientLoginWithPasswordCallback(int result, const char* error if (!user || !user->token) { WARN_LOG(COMMON, "RA: rc_client_get_user_info() returned NULL"); + promise->set_exception(std::make_exception_ptr(FlycastException("No user token returned"))); + delete promise; return; } // Store token in config config::AchievementsToken = user->token; SaveSettings(); - achievements->loggedOn = true; + achievements->authenticationSuccess(user); + promise->set_value(); + delete promise; +} - authenticationSuccessMsg(); +void Achievements::logout() +{ + unloadGame(); + rc_client_logout(rc_client); + // Reset token in config + config::AchievementsToken = ""; + SaveSettings(); + loggedOn = false; } void Achievements::clientMessageCallback(const char* message, const rc_client_t* client) @@ -282,27 +379,50 @@ u32 Achievements::clientReadMemory(u32 address, u8* buffer, u32 num_bytes, rc_cl return num_bytes; } -void Achievements::clientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, - void* callback_data, rc_client_t* client) +void Achievements::clientServerCall(const rc_api_request_t *request, rc_client_server_callback_t callback, + void *callback_data, rc_client_t *client) { - int rc; - std::vector reply; + Achievements *achievements = (Achievements *)rc_client_get_userdata(client); + std::string url {request->url}; + std::string payload; if (request->post_data != nullptr) - rc = http::post(request->url, request->post_data, reply); - else - rc = http::get(request->url, reply); - rc_api_server_response_t rr; - rr.http_status_code = rc; // TODO RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR if connection fails? - rr.body_length = reply.size(); - rr.body = (const char *)reply.data(); - callback(&rr, callback_data); + payload = request->post_data; + std::string contentType; + if (request->content_type != nullptr) + contentType = request->content_type; + const auto& callServer = [url, contentType, payload, callback, callback_data]() + { + ThreadName _("Flycast-RA"); + int rc; + std::vector reply; + if (!payload.empty()) + rc = http::post(url, payload.c_str(), contentType.empty() ? nullptr : contentType.c_str(), reply); + else + rc = http::get(url, reply); + rc_api_server_response_t rr; + rr.http_status_code = rc; // TODO RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR if connection fails? + rr.body_length = reply.size(); + rr.body = (const char *)reply.data(); + callback(&rr, callback_data); + }; + if (achievements->asyncServerCall.valid()) + { + if (achievements->asyncServerCall.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) + { + INFO_LOG(COMMON, "Async server call already in progress"); + // process synchronously + callServer(); + return; + } + achievements->asyncServerCall.get(); + } + achievements->asyncServerCall = std::async(std::launch::async, callServer); } static void handleServerError(const rc_client_server_error_t* error) { - char buffer[256]; - snprintf(buffer, sizeof(buffer), "%s: %s", error->api, error->error_message); - gui_display_notification(buffer, 5000); + WARN_LOG(COMMON, "RA server error: %s - %s", error->api, error->error_message); + notifier.notify(Notification::Error, "", error->api, error->error_message); } void Achievements::clientEventHandler(const rc_client_event_t* event, rc_client_t* client) @@ -326,13 +446,26 @@ void Achievements::clientEventHandler(const rc_client_event_t* event, rc_client_ achievements->handleAchievementChallengeIndicatorHideEvent(event); break; + case RC_CLIENT_EVENT_GAME_COMPLETED: + achievements->handleGameCompleted(event); + break; + case RC_CLIENT_EVENT_SERVER_ERROR: handleServerError(event->server_error); break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: + achievements->handleShowAchievementProgress(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: + achievements->handleHideAchievementProgress(event); + break; + case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: + achievements->handleUpdateAchievementProgress(event); + break; + /* TODO - case RC_CLIENT_EVENT_GAME_COMPLETED: case RC_CLIENT_EVENT_LEADERBOARD_STARTED: case RC_CLIENT_EVENT_LEADERBOARD_FAILED: case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED: @@ -340,10 +473,6 @@ void Achievements::clientEventHandler(const rc_client_event_t* event, rc_client_ case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW: case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE: case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE: - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW: - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE: - case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE: - case RC_CLIENT_EVENT_SERVER_ERROR: case RC_CLIENT_EVENT_DISCONNECTED: case RC_CLIENT_EVENT_RECONNECTED: */ @@ -366,20 +495,60 @@ void Achievements::handleUnlockEvent(const rc_client_event_t *event) INFO_LOG(COMMON, "RA: Achievement %s (%u) for game %s unlocked", cheevo->title, cheevo->id, settings.content.title.c_str()); - std::string msg = "Achievement " + std::string(cheevo->title) + " unlocked!"; - gui_display_notification(msg.c_str(), 10000); + char url[512]; + int rc = rc_client_achievement_get_image_url(cheevo, cheevo->state, url, sizeof(url)); + if (rc == RC_OK) + { + std::string image = getOrDownloadImage(url); + std::string text = "Achievement " + std::string(cheevo->title) + " unlocked!"; + notifier.notify(Notification::Login, image, text, cheevo->description); + } } void Achievements::handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event) { // TODO there might be more than one. Need to display an icon. - std::string msg = "Challenge: " + std::string(event->achievement->title); - gui_display_notification(msg.c_str(), 10000); + //std::string msg = "Challenge: " + std::string(event->achievement->title); + //gui_display_notification(msg.c_str(), 10000); + INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title); } void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event) { - gui_display_notification("", 1); + // TODO +} + +void Achievements::handleGameCompleted(const rc_client_event_t *event) +{ + const rc_client_game_t* game = rc_client_get_game_info(rc_client); + std::string image; + char url[128]; + if (rc_client_game_get_image_url(game, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + std::string text1 = (rc_client_get_hardcore_enabled(rc_client) ? "Mastered " : "Completed ") + std::string(game->title); + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(rc_client, &summary); + std::stringstream ss; + ss << summary.num_unlocked_achievements << " achievements, " << summary.points_unlocked << " points"; + std::string text3 = rc_client_get_user_info(rc_client)->display_name; + notifier.notify(Notification::Mastery, image, text1, ss.str(), text3); +} + +void Achievements::handleShowAchievementProgress(const rc_client_event_t *event) +{ + handleUpdateAchievementProgress(event); +} +void Achievements::handleHideAchievementProgress(const rc_client_event_t *event) +{ + notifier.notify(Notification::Progress, "", ""); +} +void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *event) +{ + char url[128]; + std::string image; + if (rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + notifier.notify(Notification::Progress, image, event->achievement->measured_progress); } static bool add150; @@ -419,6 +588,7 @@ static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, { //DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %zd sec %d num %zd", reinterpret_cast(track_handle), sector, requested_bytes); if (requested_bytes == 2048) + // add 150 sectors to FAD corresponding to files add150 = true; if (add150) // FIXME how to get rid of this? sector += 150; @@ -457,6 +627,9 @@ std::string Achievements::getGameHash() { if (settings.platform.isConsole()) { + const u32 diskType = libGDR_GetDiscType(); + if (diskType == NoDisk || diskType == Open) + return ""; add150 = false; rc_hash_cdreader hooks = { cdreader_open_track, @@ -485,12 +658,27 @@ void Achievements::pauseGame() { paused = true; if (active) + { resetEvent.Reset(); + if (asyncServerCall.valid()) + asyncServerCall.get(); + asyncServerCall = std::async(std::launch::async, [this]() { + while (paused) + { + resetEvent.Wait(1000); + if (paused) + rc_client_idle(rc_client); + } + }); + } } void Achievements::resumeGame() { paused = false; + resetEvent.Set(); + if (asyncServerCall.valid()) + asyncServerCall.get(); if (config::EnableAchievements) loadGame(); else @@ -510,11 +698,14 @@ void Achievements::emuEventCallback(Event event, void *arg) instance->unloadGame(); break; case Event::VBlank: - instance->resetEvent.Set(); + rc_client_do_frame(instance->rc_client); break; case Event::Pause: instance->pauseGame(); break; + case Event::DiskChange: + instance->diskChange(); + break; default: break; } @@ -522,34 +713,67 @@ void Achievements::emuEventCallback(Event event, void *arg) void Achievements::loadGame() { + if (loadingGame.exchange(true)) + // already loading + return; if (active) + { // already loaded + loadingGame = false; return; - if (!init() || !isLoggedOn()) + } + if (!init() || !isLoggedOn()) { + if (!isLoggedOn()) + WARN_LOG(COMMON, "Not logged on. Not loading game yet"); + loadingGame = false; return; + } std::string gameHash = getGameHash(); if (!gameHash.empty()) - rc_client_begin_load_game(rc_client, gameHash.c_str(), clientLoadGameCallback, nullptr); - if (!active) - return; - thread = std::thread([this] { - ThreadName _("Flycast-RA"); - while (active) - { - if (!resetEvent.Wait(1000)) { - if (paused) { - DEBUG_LOG(COMMON, "RA: rc_client_idle"); - rc_client_idle(rc_client); - } - else { - INFO_LOG(COMMON, "RA: timeout on event and not paused!"); - } - } - else if (active) - rc_client_do_frame(rc_client); + { + rc_client_begin_load_game(rc_client, gameHash.c_str(), [](int result, const char *error_message, rc_client_t *client, void *userdata) { + ((Achievements *)userdata)->gameLoaded(result, error_message); + }, this); + } +} + +void Achievements::gameLoaded(int result, const char *errorMessage) +{ + if (result != RC_OK) + { + if (result == RC_NO_GAME_LOADED) + // Unknown game. + INFO_LOG(COMMON, "RA: Unknown game, disabling achievements."); + else if (result == RC_LOGIN_REQUIRED) { + // We would've asked to re-authenticate, so leave HC on for now. + // Once we've done so, we'll reload the game. } - }); + else + WARN_LOG(COMMON, "RA Loading game failed: %s", errorMessage); + loadingGame = false; + return; + } + const rc_client_game_t* info = rc_client_get_game_info(rc_client); + if (info == nullptr) + { + WARN_LOG(COMMON, "RA: rc_client_get_game_info() returned NULL"); + loadingGame = false; + return; + } + active = true; + loadingGame = false; EventManager::listen(Event::VBlank, emuEventCallback, this); + NOTICE_LOG(COMMON, "RA: game %d loaded: %s, achievements %d leaderboards %d rich presence %d", info->id, info->title, + rc_client_has_achievements(rc_client), rc_client_has_leaderboards(rc_client), rc_client_has_rich_presence(rc_client)); + std::string image; + char url[512]; + if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK) + image = getOrDownloadImage(url); + rc_client_user_game_summary_t summary; + rc_client_get_user_game_summary(rc_client, &summary); + std::string text = "You have " + std::to_string(summary.num_unlocked_achievements) + + " of " + std::to_string(summary.num_core_achievements) + " achievements unlocked"; + notifier.notify(Notification::Login, image, info->title, text); } void Achievements::unloadGame() @@ -557,42 +781,34 @@ void Achievements::unloadGame() if (!active) return; active = false; + paused = false; resetEvent.Set(); - thread.join(); + if (asyncServerCall.valid()) + asyncServerCall.get(); EventManager::unlisten(Event::VBlank, emuEventCallback, this); rc_client_unload_game(rc_client); } -void Achievements::clientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata) +void Achievements::diskChange() { - Achievements *achiev = (Achievements *)rc_client_get_userdata(client); - if (result == RC_NO_GAME_LOADED) - { - // Unknown game. - INFO_LOG(COMMON, "RA: Unknown game, disabling achievements."); + if (!active) + return; + std::string hash = getGameHash(); + if (hash == "") { + unloadGame(); return; } - if (result == RC_LOGIN_REQUIRED) - { - // We would've asked to re-authenticate, so leave HC on for now. - // Once we've done so, we'll reload the game. - return; - } - if (result != RC_OK) - { - WARN_LOG(COMMON, "RA Loading game failed: %s", error_message); - return; - } - const rc_client_game_t* info = rc_client_get_game_info(client); - if (info == nullptr) - { - WARN_LOG(COMMON, "RA: rc_client_get_game_info() returned NULL"); - return; - } - // TODO? rc_client_game_get_image_url(info, buf, std::size(buf)); - achiev->active = true; - NOTICE_LOG(COMMON, "RA: game %d loaded: %s, achievements %d leaderboards %d rich presence %d", info->id, info->title, - rc_client_has_achievements(client), rc_client_has_leaderboards(client), rc_client_has_rich_presence(client)); + rc_client_begin_change_media_from_hash(rc_client, hash.c_str(), [](int result, const char *errorMessage, rc_client_t *client, void *userdata) { + if (result == RC_HARDCORE_DISABLED) { + notifier.notify(Notification::Login, "", "Hardcore disabled", "Unrecognized media inserted"); + } + else if (result != RC_OK) + { + if (errorMessage == nullptr) + errorMessage = rc_error_str(result); + notifier.notify(Notification::Login, "", "Media change failed", errorMessage); + } + }, this); } void Achievements::serialize(Serializer& ser) diff --git a/core/achievements/achievements.h b/core/achievements/achievements.h index e029ff1b9..28023cc28 100644 --- a/core/achievements/achievements.h +++ b/core/achievements/achievements.h @@ -16,6 +16,7 @@ */ #pragma once #include "types.h" +#include namespace achievements { @@ -23,16 +24,10 @@ namespace achievements bool init(); void term(); -void login(const char *username, const char *password); +std::future login(const char *username, const char *password); +void logout(); bool isLoggedOn(); -#else - -static inline bool init() { return false; } -static inline void term() {} -static inline void login(const char *username, const char *password) {} -static inline bool isLoggedOn() { return false; } - #endif void serialize(Serializer& ser); diff --git a/core/deps/imgui/imgui_stdlib.cpp b/core/deps/imgui/imgui_stdlib.cpp new file mode 100644 index 000000000..cf69aa89a --- /dev/null +++ b/core/deps/imgui/imgui_stdlib.cpp @@ -0,0 +1,85 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +// See more C++ related extension (fmt, RAII, syntaxis sugar) on Wiki: +// https://github.com/ocornut/imgui/wiki/Useful-Extensions#cness + +#include "imgui.h" +#include "imgui_stdlib.h" + +// Clang warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#endif + +struct InputTextCallback_UserData +{ + std::string* Str; + ImGuiInputTextCallback ChainCallback; + void* ChainCallbackUserData; +}; + +static int InputTextCallback(ImGuiInputTextCallbackData* data) +{ + InputTextCallback_UserData* user_data = (InputTextCallback_UserData*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + // Resize string callback + // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. + std::string* str = user_data->Str; + IM_ASSERT(data->Buf == str->c_str()); + str->resize(data->BufTextLen); + data->Buf = (char*)str->c_str(); + } + else if (user_data->ChainCallback) + { + // Forward to user callback, if any + data->UserData = user_data->ChainCallbackUserData; + return user_data->ChainCallback(data); + } + return 0; +} + +bool ImGui::InputText(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextMultiline(const char* label, std::string* str, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextMultiline(label, (char*)str->c_str(), str->capacity() + 1, size, flags, InputTextCallback, &cb_user_data); +} + +bool ImGui::InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +{ + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + flags |= ImGuiInputTextFlags_CallbackResize; + + InputTextCallback_UserData cb_user_data; + cb_user_data.Str = str; + cb_user_data.ChainCallback = callback; + cb_user_data.ChainCallbackUserData = user_data; + return InputTextWithHint(label, hint, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); +} + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif diff --git a/core/deps/imgui/imgui_stdlib.h b/core/deps/imgui/imgui_stdlib.h new file mode 100644 index 000000000..835a808f2 --- /dev/null +++ b/core/deps/imgui/imgui_stdlib.h @@ -0,0 +1,21 @@ +// dear imgui: wrappers for C++ standard library (STL) types (std::string, etc.) +// This is also an example of how you may wrap your own similar types. + +// Changelog: +// - v0.10: Initial version. Added InputText() / InputTextMultiline() calls with std::string + +// See more C++ related extension (fmt, RAII, syntaxis sugar) on Wiki: +// https://github.com/ocornut/imgui/wiki/Useful-Extensions#cness + +#pragma once + +#include + +namespace ImGui +{ + // ImGui::InputText() with std::string + // Because text input needs dynamic resizing, we need to setup a callback to grow the capacity + IMGUI_API bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); + IMGUI_API bool InputTextMultiline(const char* label, std::string* str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); + IMGUI_API bool InputTextWithHint(const char* label, const char* hint, std::string* str, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* user_data = nullptr); +} diff --git a/core/emulator.cpp b/core/emulator.cpp index 03da7a54c..bfdd7a219 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -772,33 +772,22 @@ void Emulator::setNetworkState(bool online) void EventManager::registerEvent(Event event, Callback callback, void *param) { unregisterEvent(event, callback, param); - auto it = callbacks.find(event); - if (it != callbacks.end()) - it->second.push_back(std::make_pair(callback, param)); - else - callbacks.insert({ event, { std::make_pair(callback, param) } }); + auto& vector = callbacks[static_cast(event)]; + vector.push_back(std::make_pair(callback, param)); } void EventManager::unregisterEvent(Event event, Callback callback, void *param) { - auto it = callbacks.find(event); - if (it == callbacks.end()) - return; - - auto it2 = std::find(it->second.begin(), it->second.end(), std::make_pair(callback, param)); - if (it2 == it->second.end()) - return; - - it->second.erase(it2); + auto& vector = callbacks[static_cast(event)]; + auto it = std::find(vector.begin(), vector.end(), std::make_pair(callback, param)); + if (it != vector.end()) + vector.erase(it); } void EventManager::broadcastEvent(Event event) { - auto it = callbacks.find(event); - if (it == callbacks.end()) - return; - - for (auto& pair : it->second) + auto& vector = callbacks[static_cast(event)]; + for (auto& pair : vector) pair.first(event, pair.second); } diff --git a/core/emulator.h b/core/emulator.h index 96c7d36c8..f4b8b14c0 100644 --- a/core/emulator.h +++ b/core/emulator.h @@ -23,7 +23,7 @@ #include #include -#include +#include #include #include #include @@ -47,6 +47,8 @@ enum class Event { LoadState, VBlank, Network, + DiskChange, + max = DiskChange }; class EventManager @@ -77,7 +79,7 @@ private: void unregisterEvent(Event event, Callback callback, void *param); void broadcastEvent(Event event); - std::map>> callbacks; + std::array>, static_cast(Event::max) + 1> callbacks; }; struct LoadProgress diff --git a/core/hw/holly/sb.cpp b/core/hw/holly/sb.cpp index 49e47b1df..5966141d4 100644 --- a/core/hw/holly/sb.cpp +++ b/core/hw/holly/sb.cpp @@ -14,6 +14,7 @@ #include "emulator.h" #include "hw/bba/bba.h" #include "serialize.h" +#include u32 sb_regs[0x540]; HollyRegisters hollyRegs; diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index 6aa6853f5..b99ed778f 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -350,6 +350,7 @@ bool DiscSwap(const std::string& path) { if (!doDiscSwap(path)) throw FlycastException("This media cannot be loaded"); + EventManager::event(Event::DiskChange); // Drive is busy after the lid was closed sns_asc = 4; sns_ascq = 1; diff --git a/core/lua/lua.cpp b/core/lua/lua.cpp index 303543d0a..7ad7e5ce3 100644 --- a/core/lua/lua.cpp +++ b/core/lua/lua.cpp @@ -74,6 +74,9 @@ static void emuEventCallback(Event event, void *) case Event::Network: key = "network"; break; + case Event::DiskChange: + key = "diskChange"; + break; } if (v[key].isFunction()) v[key](); diff --git a/core/oslib/http_client.cpp b/core/oslib/http_client.cpp index 52b707743..3742159b1 100644 --- a/core/oslib/http_client.cpp +++ b/core/oslib/http_client.cpp @@ -98,10 +98,7 @@ static int post(const std::string& url, const char *headers, const u8 *payload, if (payloadSize > 0) { char clen[128]; - if (headers == nullptr) - snprintf(clen, sizeof(clen), "Content-Length: %d\r\nContent-Type: application/x-www-form-urlencoded\r\n", payloadSize); - else - snprintf(clen, sizeof(clen), "Content-Length: %d\r\n", payloadSize); + snprintf(clen, sizeof(clen), "Content-Length: %d\r\n", payloadSize); HttpAddRequestHeaders(hreq, clen, -1L, HTTP_ADDREQ_FLAG_ADD_IF_NEW); } if (!HttpSendRequest(hreq, headers, -1, (void *)payload, payloadSize)) @@ -141,9 +138,14 @@ static int post(const std::string& url, const char *headers, const u8 *payload, return rc; } -int post(const std::string& url, const char *payload, std::vector& reply) +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { - return post(url, nullptr, (const u8 *)payload, strlen(payload), reply); + char buf[512]; + if (contentType != nullptr) { + sprintf(buf, "Content-Type: %s", contentType); + contentType = buf; + } + return post(url, contentType, (const u8 *)payload, strlen(payload), reply); } int post(const std::string& url, const std::vector& fields) @@ -272,13 +274,19 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return (int)httpCode; } -int post(const std::string& url, const char *payload, std::vector& reply) +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { CURL *curl = makeCurlEasy(url); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curl, CURLOPT_POST, 1); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); + curl_slist *headers = nullptr; + if (contentType != nullptr) + { + headers = curl_slist_append(headers, ("Content-Type: " + std::string(contentType)).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } std::vector recvBuffer; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData); @@ -291,6 +299,7 @@ int post(const std::string& url, const char *payload, std::vector& reply) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); reply = recvBuffer; } + curl_slist_free_all(headers); curl_easy_cleanup(curl); return (int)httpCode; diff --git a/core/oslib/http_client.h b/core/oslib/http_client.h index f4493670d..c71fb668f 100644 --- a/core/oslib/http_client.h +++ b/core/oslib/http_client.h @@ -50,7 +50,7 @@ struct PostField }; int post(const std::string& url, const std::vector& fields); -int post(const std::string& url, const char *payload, std::vector& reply); +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply); static inline bool success(int status) { return status >= 200 && status < 300; diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index eca4e892a..e24c1e033 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -22,6 +22,7 @@ #include "hw/maple/maple_if.h" #include "hw/maple/maple_devs.h" #include "imgui.h" +#include "imgui_stdlib.h" #include "network/net_handshake.h" #include "network/ggpo.h" #include "wsi/context.h" @@ -46,6 +47,7 @@ #include "hw/naomi/card_reader.h" #include "oslib/resources.h" #include "achievements/achievements.h" +#include "gui_achievements.h" #if defined(USE_SDL) #include "sdl/sdl.h" #endif @@ -95,6 +97,8 @@ static Chat chat; static std::recursive_mutex guiMutex; using LockGuard = std::lock_guard; +ImFont *largeFont; + static void emuEventCallback(Event event, void *) { switch (event) @@ -215,6 +219,7 @@ void gui_initFonts() ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); + largeFont = nullptr; const float fontSize = 17.f * settings.display.uiScale; size_t dataSize; std::unique_ptr data = resource::load("fonts/Roboto-Medium.ttf", dataSize); @@ -304,7 +309,13 @@ void gui_initFonts() // TODO Linux, iOS, ... #endif - NOTICE_LOG(RENDERER, "Screen DPI is %.0f, size %d x %d. Scaling by %.2f", settings.display.dpi, settings.display.width, settings.display.height, settings.display.uiScale); + // Large font without Asian glyphs + data = resource::load("fonts/Roboto-Regular.ttf", dataSize); + verify(data != nullptr); + const float largeFontSize = 21.f * settings.display.uiScale; + largeFont = io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, largeFontSize, nullptr, ranges); + + NOTICE_LOG(RENDERER, "Screen DPI is %.0f, size %d x %d. Scaling by %.2f", settings.display.dpi, settings.display.width, settings.display.height, settings.display.uiScale); } void gui_keyboard_input(u16 wc) @@ -1744,26 +1755,42 @@ static void gui_display_settings() { DisabledScope _(!config::EnableAchievements); ImGui::Indent(); - char username[256]; - strcpy(username, config::AchievementsUserName.get().c_str()); - ImGui::InputText("Username", username, sizeof(username), ImGuiInputTextFlags_None, nullptr, nullptr); - config::AchievementsUserName = username; + ImGui::InputText("Username", &config::AchievementsUserName.get(), + achievements::isLoggedOn() ? ImGuiInputTextFlags_ReadOnly : ImGuiInputTextFlags_None, nullptr, nullptr); if (config::EnableAchievements) { + static std::future futureLogin; achievements::init(); if (achievements::isLoggedOn()) + { ImGui::Text("Authentication successful"); + if (futureLogin.valid()) + futureLogin.get(); + if (ImGui::Button("Logout", ScaledVec2(100, 0))) + achievements::logout(); + } else { static char password[256]; ImGui::InputText("Password", password, sizeof(password), ImGuiInputTextFlags_Password, nullptr, nullptr); - if (ImGui::Button("Login", ScaledVec2(100, 0))) + if (futureLogin.valid()) { - try { - achievements::login(config::AchievementsUserName.get().c_str(), password); - } catch (const FlycastException& e) { - gui_error(e.what()); + if (futureLogin.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout) { + ImGui::Text("Authenticating..."); } + else + { + try { + futureLogin.get(); + } catch (const FlycastException& e) { + gui_error(e.what()); + } + } + } + if (ImGui::Button("Login", ScaledVec2(100, 0)) && !futureLogin.valid()) + { + futureLogin = achievements::login(config::AchievementsUserName.get().c_str(), password); + memset(password, 0, sizeof(password)); } } } @@ -2569,12 +2596,9 @@ static void gui_display_settings() config::NetworkEnable = false; OptionCheckbox("Play as Player 1", config::ActAsServer, "Deselect to play as player 2"); - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::InputText("Peer", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); ImGui::SameLine(); ShowHelpMarker("Your peer IP address and optional port"); - config::NetworkServer.set(server_name); OptionSlider("Frame Delay", config::GGPODelay, 0, 20, "Sets Frame Delay, advisable for sessions with ping >100 ms"); @@ -2608,12 +2632,9 @@ static void gui_display_settings() "Create a local server for Naomi network games"); if (!config::ActAsServer) { - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Server", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::InputText("Server", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); ImGui::SameLine(); ShowHelpMarker("The server to connect to. Leave blank to find a server automatically on the default port"); - config::NetworkServer.set(server_name); } char localPort[256]; sprintf(localPort, "%d", (int)config::LocalPort); @@ -2624,12 +2645,9 @@ static void gui_display_settings() } else if (config::BattleCableEnable) { - char server_name[256]; - strcpy(server_name, config::NetworkServer.get().c_str()); - ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::InputText("Peer", &config::NetworkServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); ImGui::SameLine(); ShowHelpMarker("The peer to connect to. Leave blank to find a player automatically on the default port"); - config::NetworkServer.set(server_name); char localPort[256]; sprintf(localPort, "%d", (int)config::LocalPort); ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr); @@ -2721,14 +2739,9 @@ static void gui_display_settings() #ifdef USE_LUA header("Lua Scripting"); { - char LuaFileName[256]; - - strcpy(LuaFileName, config::LuaFileName.get().c_str()); - ImGui::InputText("Lua Filename", LuaFileName, sizeof(LuaFileName), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::InputText("Lua Filename", &config::LuaFileName.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); ImGui::SameLine(); ShowHelpMarker("Specify lua filename to use. Should be located in Flycast config directory. Defaults to flycast.lua when empty."); - config::LuaFileName = LuaFileName; - } #endif } @@ -3416,7 +3429,7 @@ void gui_display_osd() gui_newFrame(); ImGui::NewFrame(); - if (!message.empty()) + if (!achievements::notifier.draw() && !message.empty()) { ImGui::SetNextWindowBgAlpha(0); ImGui::SetNextWindowPos(ImVec2(0, ImGui::GetIO().DisplaySize.y), ImGuiCond_Always, ImVec2(0.f, 1.f)); // Lower left corner diff --git a/core/rend/gui_achievements.cpp b/core/rend/gui_achievements.cpp new file mode 100644 index 000000000..97ab1d00e --- /dev/null +++ b/core/rend/gui_achievements.cpp @@ -0,0 +1,171 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "gui_achievements.h" +#include "gui.h" +#include "gui_util.h" +#include "imgui_driver.h" +#include "stdclass.h" +#include + +extern ImFont *largeFont; + +namespace achievements +{ + +Notification notifier; + +static constexpr u64 DISPLAY_TIME = 5000; +static constexpr u64 START_ANIM_TIME = 500; +static constexpr u64 END_ANIM_TIME = 1000; + +void Notification::notify(Type type, const std::string& image, const std::string& text1, + const std::string& text2, const std::string& text3) +{ + std::lock_guard _(mutex); + u64 now = getTimeMs(); + if (type == Progress) + { + if (!text1.empty()) + { + if (this->type == None) + { + // New progress + startTime = now; + endTime = 0x1000000000000; // never + } + } + else + { + // Hide progress + endTime = now; + } + } + else { + startTime = now; + endTime = startTime + DISPLAY_TIME; + } + this->type = type; + this->imagePath = image; + this->imageId = {}; + text[0] = text1; + text[1] = text2; + text[2] = text3; +} + +bool Notification::draw() +{ + std::lock_guard _(mutex); + if (type == None) + return false; + u64 now = getTimeMs(); + if (now > endTime + END_ANIM_TIME) { + // Hide notification + type = None; + return false; + } + if (now > endTime) + { + // Fade out + float alpha = (std::cos((now - endTime) / (float)END_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + ImGui::GetStyle().Alpha = alpha; + ImGui::SetNextWindowBgAlpha(alpha / 2.f); + } + else { + ImGui::SetNextWindowBgAlpha(0.5f); + } + if (imageId == ImTextureID{}) + getImage(); + float y = ImGui::GetIO().DisplaySize.y; + if (now - startTime < START_ANIM_TIME) + // Slide up + y += 80.f * settings.display.uiScale * (std::cos((now - startTime) / (float)START_ANIM_TIME * (float)M_PI) + 1.f) / 2.f; + + ImGui::SetNextWindowPos(ImVec2(0, y), ImGuiCond_Always, ImVec2(0.f, 1.f)); // Lower left corner + ImGui::SetNextWindowSizeConstraints(ScaledVec2(80.f, 80.f) + ImVec2(ImGui::GetStyle().WindowPadding.x * 2, 0.f), ImVec2(FLT_MAX, FLT_MAX)); + const float winPaddingX = ImGui::GetStyle().WindowPadding.x; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{}); + + ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav + | ImGuiWindowFlags_NoInputs); + const bool hasPic = imageId != ImTextureID{}; + if (ImGui::BeginTable("achievementNotif", hasPic ? 2 : 1, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) + { + if (hasPic) + ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (hasPic) + { + ImGui::Image(imageId, ScaledVec2(80.f, 80.f), { 0.f, 0.f }, { 1.f, 1.f }); + ImGui::TableSetColumnIndex(1); + } + + float w = largeFont->CalcTextSizeA(largeFont->FontSize, FLT_MAX, -1.f, text[0].c_str()).x; + w = std::max(w, ImGui::CalcTextSize(text[1].c_str()).x); + w = std::max(w, ImGui::CalcTextSize(text[2].c_str()).x) + winPaddingX * 2; + int lines = (int)!text[0].empty() + (int)!text[1].empty() + (int)!text[2].empty(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ hasPic ? 0.f : winPaddingX, (3 - lines) * ImGui::GetTextLineHeight() / 2 }); + if (ImGui::BeginChild("##text", ImVec2(w, 0), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_None)) + { + ImGui::PushFont(largeFont); + ImGui::Text("%s", text[0].c_str()); + ImGui::PopFont(); + if (!text[1].empty()) + ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[1].c_str()); + if (!text[2].empty()) + ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[2].c_str()); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::EndTable(); + } + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::GetStyle().Alpha = 1.f; + + return true; +} + +void Notification::getImage() +{ + if (imagePath.empty()) + return; + + // Get the texture. Load it if needed. + imageId = imguiDriver->getTexture(imagePath); + if (imageId == ImTextureID()) + { + int width, height; + u8 *imgData = loadImage(imagePath, width, height); + if (imgData != nullptr) + { + try { + imageId = imguiDriver->updateTextureAndAspectRatio(imagePath, imgData, width, height); + } catch (...) { + // vulkan can throw during resizing + } + free(imgData); + } + } +} + +} // namespace achievements diff --git a/core/rend/gui_achievements.h b/core/rend/gui_achievements.h new file mode 100644 index 000000000..009581f20 --- /dev/null +++ b/core/rend/gui_achievements.h @@ -0,0 +1,56 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "types.h" +#include "imgui.h" +#include + +namespace achievements +{ + +class Notification +{ +public: + enum Type { + None, + Login, + GameLoaded, + Unlocked, + Progress, + Mastery, + Error + }; + void notify(Type type, const std::string& image, const std::string& text1, + const std::string& text2 = {}, const std::string& text3 = {}); + bool draw(); + +private: + void getImage(); + + u64 startTime = 0; + u64 endTime = 0; + Type type = Type::None; + std::string imagePath; + ImTextureID imageId {}; + std::string text[3]; + std::mutex mutex; +}; + +extern Notification notifier; + +} diff --git a/fonts/Roboto-Regular.ttf.zip b/fonts/Roboto-Regular.ttf.zip new file mode 100644 index 0000000000000000000000000000000000000000..6bc0533e88416a24a6ec1f6689c46f9c6934e2fb GIT binary patch literal 88941 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

$={eLlYWFGRS{UpsG(b3%^EH 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 reply) + int post(const std::string &url, const char *payload, const char *contentType, std::vector& reply) { jni::String jurl(url); jni::String jpayload(payload); + jni::String jcontentType(contentType); jni::ObjectArray replyOut(1); int httpStatus = jni::env()->CallIntMethod(HttpClient, postRawMid, static_cast(jurl), static_cast(jpayload), + static_cast(jcontentType), static_cast(replyOut)); reply = replyOut[0]; @@ -95,5 +97,5 @@ extern "C" JNIEXPORT void JNICALL Java_com_reicast_emulator_emu_HttpClient_nativ http::HttpClient = env->NewGlobalRef(obj); http::openUrlMid = env->GetMethodID(env->GetObjectClass(obj), "openUrl", "(Ljava/lang/String;[[B[Ljava/lang/String;)I"); http::postMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)I"); - http::postRawMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;Ljava/lang/String;[[B)I"); + http::postRawMid = env->GetMethodID(env->GetObjectClass(obj), "post", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[[B)I"); } diff --git a/shell/apple/common/http_client.mm b/shell/apple/common/http_client.mm index 7fd8e26ef..e577face7 100644 --- a/shell/apple/common/http_client.mm +++ b/shell/apple/common/http_client.mm @@ -46,7 +46,7 @@ int get(const std::string& url, std::vector& content, std::string& contentTy return [httpResponse statusCode]; } -int post(const std::string& url, const char *payload, std::vector& reply) +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { NSString *nsurl = [NSString stringWithCString:url.c_str() encoding:[NSString defaultCStringEncoding]]; @@ -59,8 +59,10 @@ int post(const std::string& url, const char *payload, std::vector& reply) [request setHTTPBody:[NSData dataWithBytes:payload length:payloadSize]]; NSString *postLength = [NSString stringWithFormat:@"%ld", (unsigned long)payloadSize]; [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; - NSString *contentType = @"application/x-www-form-urlencoded"; - [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; + NSString *nscontentType = contentType != nullptr ? [NSString stringWithCString:contentType + encoding:[NSString defaultCStringEncoding]] + : @"application/x-www-form-urlencoded"; + [request setValue:nscontentType forHTTPHeaderField:@"Content-Type"]; NSURLResponse *response = nil; NSError *error = nil; diff --git a/shell/uwp/http_client.cpp b/shell/uwp/http_client.cpp index 180bc5da1..68de14302 100644 --- a/shell/uwp/http_client.cpp +++ b/shell/uwp/http_client.cpp @@ -89,7 +89,7 @@ int get(const std::string& url, std::vector& content, std::string& contentTy } } -int post(const std::string& url, const char *payload, std::vector& reply) +int post(const std::string& url, const char *payload, const char *contentType, std::vector& reply) { nowide::wstackstring wurl; if (!wurl.convert(url.c_str())) @@ -97,12 +97,16 @@ int post(const std::string& url, const char *payload, std::vector& reply) nowide::wstackstring wpayload; if (!wpayload.convert(payload)) return 500; + nowide::wstackstring wcontentType; + if (contentType != nullptr && !wcontentType.convert(contentType)) + return 500; try { Uri^ uri = ref new Uri(ref new String(wurl.get())); HttpStringContent^ content = ref new HttpStringContent(ref new String(wpayload.get())); content->Headers->ContentLength = strlen(payload); - content->Headers->ContentType = ref new HttpMediaTypeHeaderValue("application/x-www-form-urlencoded"); + if (contentType != nullptr) + content->Headers->ContentType = ref new HttpMediaTypeHeaderValue(ref new String(wcontentType.get())); IAsyncOperationWithProgress^ op = httpClient->PostAsync(uri, content); cResetEvent asyncEvent;