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:
parent
c96e828c63
commit
300cf0d437
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "emulator.h"
|
||||
#include "hw/bba/bba.h"
|
||||
#include "serialize.h"
|
||||
#include <map>
|
||||
|
||||
u32 sb_regs[0x540];
|
||||
HollyRegisters hollyRegs;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue