better RetroAchievements UI and threading

Pop ups for authentication, game load, game completed, achievements
unlock and progress.
Handle disk changes.
Issue #761
This commit is contained in:
Flyinghead 2024-05-01 18:32:39 +02:00
parent c96e828c63
commit 300cf0d437
20 changed files with 754 additions and 180 deletions

View File

@ -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

View File

@ -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 <cassert>
#include <rc_client.h>
#include <rc_hash.h>
#include <thread>
#include <future>
#include <unordered_map>
#include <sstream>
#include <atomic>
#include <xxhash.h>
namespace achievements
{
@ -43,7 +48,8 @@ public:
~Achievements();
bool init();
void term();
void login(const char *username, const char *password);
std::future<void> 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<void> asyncServerCall;
cResetEvent resetEvent;
std::thread thread;
std::string cachePath;
std::unordered_map<u64, std::string> cacheMap;
};
bool init()
@ -91,9 +108,14 @@ void term()
Achievements::Instance().term();
}
void login(const char *username, const char *password)
std::future<void> 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<u8> 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<void> 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<void> *promise = new std::promise<void>();
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<void> *promise = (std::promise<void> *)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<u8> 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<u8> 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<uintptr_t>(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)

View File

@ -16,6 +16,7 @@
*/
#pragma once
#include "types.h"
#include <future>
namespace achievements
{
@ -23,16 +24,10 @@ namespace achievements
bool init();
void term();
void login(const char *username, const char *password);
std::future<void> 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);

View File

@ -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

View File

@ -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 <string>
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);
}

View File

@ -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<size_t>(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<size_t>(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<size_t>(event)];
for (auto& pair : vector)
pair.first(event, pair.second);
}

View File

@ -23,7 +23,7 @@
#include <atomic>
#include <future>
#include <map>
#include <array>
#include <mutex>
#include <utility>
#include <vector>
@ -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<Event, std::vector<std::pair<Callback, void *>>> callbacks;
std::array<std::vector<std::pair<Callback, void *>>, static_cast<size_t>(Event::max) + 1> callbacks;
};
struct LoadProgress

View File

@ -14,6 +14,7 @@
#include "emulator.h"
#include "hw/bba/bba.h"
#include "serialize.h"
#include <map>
u32 sb_regs[0x540];
HollyRegisters hollyRegs;

View File

@ -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;

View File

@ -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]();

View File

@ -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<u8>& reply)
int post(const std::string& url, const char *payload, const char *contentType, std::vector<u8>& 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<PostField>& fields)
@ -272,13 +274,19 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
return (int)httpCode;
}
int post(const std::string& url, const char *payload, std::vector<u8>& reply)
int post(const std::string& url, const char *payload, const char *contentType, std::vector<u8>& 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<u8> recvBuffer;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, receiveData);
@ -291,6 +299,7 @@ int post(const std::string& url, const char *payload, std::vector<u8>& reply)
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
reply = recvBuffer;
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return (int)httpCode;

View File

@ -50,7 +50,7 @@ struct PostField
};
int post(const std::string& url, const std::vector<PostField>& fields);
int post(const std::string& url, const char *payload, std::vector<u8>& reply);
int post(const std::string& url, const char *payload, const char *contentType, std::vector<u8>& reply);
static inline bool success(int status) {
return status >= 200 && status < 300;

View File

@ -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<std::recursive_mutex>;
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<u8[]> 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<void> 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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "gui_achievements.h"
#include "gui.h"
#include "gui_util.h"
#include "imgui_driver.h"
#include "stdclass.h"
#include <cmath>
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<std::mutex> _(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<std::mutex> _(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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "types.h"
#include "imgui.h"
#include <mutex>
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;
}

Binary file not shown.

View File

@ -78,12 +78,12 @@ public class HttpClient {
return 500;
}
public int post(String urlString, String payload, byte[][] reply) {
public int post(String urlString, String payload, String contentType, byte[][] reply) {
try {
if (httpClient == null)
httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(urlString);
httpPost.setEntity(new StringEntity(payload, ContentType.APPLICATION_FORM_URLENCODED));
httpPost.setEntity(new StringEntity(payload, contentType != null ? ContentType.create(contentType) : ContentType.APPLICATION_FORM_URLENCODED));
CloseableHttpResponse response = httpClient.execute(httpPost);
InputStream is = response.getEntity().getContent();

View File

@ -47,15 +47,17 @@ namespace http {
return httpStatus;
}
int post(const std::string &url, const char *payload, std::vector<u8>& reply)
int post(const std::string &url, const char *payload, const char *contentType, std::vector<u8>& reply)
{
jni::String jurl(url);
jni::String jpayload(payload);
jni::String jcontentType(contentType);
jni::ObjectArray<jni::ByteArray> replyOut(1);
int httpStatus = jni::env()->CallIntMethod(HttpClient, postRawMid,
static_cast<jstring>(jurl),
static_cast<jstring>(jpayload),
static_cast<jstring>(jcontentType),
static_cast<jobjectArray>(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");
}

View File

@ -46,7 +46,7 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
return [httpResponse statusCode];
}
int post(const std::string& url, const char *payload, std::vector<u8>& reply)
int post(const std::string& url, const char *payload, const char *contentType, std::vector<u8>& 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<u8>& 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;

View File

@ -89,7 +89,7 @@ int get(const std::string& url, std::vector<u8>& content, std::string& contentTy
}
}
int post(const std::string& url, const char *payload, std::vector<u8>& reply)
int post(const std::string& url, const char *payload, const char *contentType, std::vector<u8>& 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<u8>& 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<HttpResponseMessage^, HttpProgress>^ op = httpClient->PostAsync(uri, content);
cResetEvent asyncEvent;