ui: achievements list, new pause menu
Fix threading issue when hashing disk for RA. Protect cacheMap with mutex. Achievement current challenges displayed as small icons. Embed FontAwesome symbols. New pause menu. Issue #761
This commit is contained in:
parent
2d4684a462
commit
fe17d459a5
|
@ -706,7 +706,7 @@ target_sources(${PROJECT_NAME} PRIVATE core/deps/lzma/7zArcIn.c core/deps/lzma/7
|
||||||
add_subdirectory(core/deps/libelf)
|
add_subdirectory(core/deps/libelf)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE elf)
|
target_link_libraries(${PROJECT_NAME} PRIVATE elf)
|
||||||
if(NOT LIBRETRO)
|
if(NOT LIBRETRO)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE IMGUI_DISABLE_DEMO_WINDOWS IMGUI_DEFINE_MATH_OPERATORS)
|
||||||
target_include_directories(${PROJECT_NAME} PRIVATE core/deps/imgui core/deps/imgui/backends)
|
target_include_directories(${PROJECT_NAME} PRIVATE core/deps/imgui core/deps/imgui/backends)
|
||||||
target_sources(${PROJECT_NAME} PRIVATE
|
target_sources(${PROJECT_NAME} PRIVATE
|
||||||
core/deps/imgui/imgui.cpp
|
core/deps/imgui/imgui.cpp
|
||||||
|
@ -1015,7 +1015,8 @@ cmrc_add_resources(flycast-resources
|
||||||
if(NOT LIBRETRO)
|
if(NOT LIBRETRO)
|
||||||
cmrc_add_resources(flycast-resources
|
cmrc_add_resources(flycast-resources
|
||||||
fonts/Roboto-Medium.ttf.zip
|
fonts/Roboto-Medium.ttf.zip
|
||||||
fonts/Roboto-Regular.ttf.zip)
|
fonts/Roboto-Regular.ttf.zip
|
||||||
|
fonts/fa-solid-900.ttf.zip)
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
cmrc_add_resources(flycast-resources
|
cmrc_add_resources(flycast-resources
|
||||||
WHENCE resources
|
WHENCE resources
|
||||||
|
@ -1292,6 +1293,7 @@ target_sources(${PROJECT_NAME} PRIVATE
|
||||||
if(NOT LIBRETRO)
|
if(NOT LIBRETRO)
|
||||||
target_sources(${PROJECT_NAME} PRIVATE
|
target_sources(${PROJECT_NAME} PRIVATE
|
||||||
core/rend/game_scanner.h
|
core/rend/game_scanner.h
|
||||||
|
core/rend/imgui_driver.cpp
|
||||||
core/rend/imgui_driver.h
|
core/rend/imgui_driver.h
|
||||||
core/rend/gui.cpp
|
core/rend/gui.cpp
|
||||||
core/rend/gui.h
|
core/rend/gui.h
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <tuple>
|
||||||
#include <xxhash.h>
|
#include <xxhash.h>
|
||||||
|
|
||||||
namespace achievements
|
namespace achievements
|
||||||
|
@ -51,6 +52,9 @@ public:
|
||||||
std::future<void> login(const char *username, const char *password);
|
std::future<void> login(const char *username, const char *password);
|
||||||
void logout();
|
void logout();
|
||||||
bool isLoggedOn() const { return loggedOn; }
|
bool isLoggedOn() const { return loggedOn; }
|
||||||
|
bool isActive() const { return active; }
|
||||||
|
Game getCurrentGame();
|
||||||
|
std::vector<Achievement> getAchievementList();
|
||||||
void serialize(Serializer& ser);
|
void serialize(Serializer& ser);
|
||||||
void deserialize(Deserializer& deser);
|
void deserialize(Deserializer& deser);
|
||||||
|
|
||||||
|
@ -66,6 +70,7 @@ private:
|
||||||
void resumeGame();
|
void resumeGame();
|
||||||
void loadCache();
|
void loadCache();
|
||||||
std::string getOrDownloadImage(const char *url);
|
std::string getOrDownloadImage(const char *url);
|
||||||
|
std::tuple<std::string, bool> getCachedImage(const char *url);
|
||||||
void diskChange();
|
void diskChange();
|
||||||
|
|
||||||
static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata);
|
static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata);
|
||||||
|
@ -96,6 +101,8 @@ private:
|
||||||
cResetEvent resetEvent;
|
cResetEvent resetEvent;
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
std::unordered_map<u64, std::string> cacheMap;
|
std::unordered_map<u64, std::string> cacheMap;
|
||||||
|
std::mutex cacheMutex;
|
||||||
|
std::future<void> asyncImageDownload;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool init()
|
bool init()
|
||||||
|
@ -123,6 +130,21 @@ bool isLoggedOn()
|
||||||
return Achievements::Instance().isLoggedOn();
|
return Achievements::Instance().isLoggedOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isActive()
|
||||||
|
{
|
||||||
|
return Achievements::Instance().isActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
Game getCurrentGame()
|
||||||
|
{
|
||||||
|
return Achievements::Instance().getCurrentGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Achievement> getAchievementList()
|
||||||
|
{
|
||||||
|
return Achievements::Instance().getAchievementList();
|
||||||
|
}
|
||||||
|
|
||||||
void serialize(Serializer& ser)
|
void serialize(Serializer& ser)
|
||||||
{
|
{
|
||||||
Achievements::Instance().serialize(ser);
|
Achievements::Instance().serialize(ser);
|
||||||
|
@ -224,39 +246,63 @@ void Achievements::loadCache()
|
||||||
continue;
|
continue;
|
||||||
std::string s = get_file_basename(name);
|
std::string s = get_file_basename(name);
|
||||||
u64 v = strtoull(s.c_str(), nullptr, 16);
|
u64 v = strtoull(s.c_str(), nullptr, 16);
|
||||||
|
std::lock_guard<std::mutex> _(cacheMutex);
|
||||||
cacheMap[v] = name;
|
cacheMap[v] = name;
|
||||||
}
|
}
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static u64 hashUrl(const char *url) {
|
||||||
|
return XXH64(url, strlen(url), 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tuple<std::string, bool> Achievements::getCachedImage(const char *url)
|
||||||
|
{
|
||||||
|
u64 hash = hashUrl(url);
|
||||||
|
std::lock_guard<std::mutex> _(cacheMutex);
|
||||||
|
auto it = cacheMap.find(hash);
|
||||||
|
if (it != cacheMap.end()) {
|
||||||
|
return make_tuple(cachePath + it->second, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::stringstream stream;
|
||||||
|
stream << std::hex << hash << ".png";
|
||||||
|
return make_tuple(cachePath + stream.str(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Achievements::getOrDownloadImage(const char *url)
|
std::string Achievements::getOrDownloadImage(const char *url)
|
||||||
{
|
{
|
||||||
u64 hash = XXH64(url, strlen(url), 13);
|
u64 hash = hashUrl(url);
|
||||||
auto it = cacheMap.find(hash);
|
{
|
||||||
if (it != cacheMap.end())
|
std::lock_guard<std::mutex> _(cacheMutex);
|
||||||
return cachePath + it->second;
|
auto it = cacheMap.find(hash);
|
||||||
|
if (it != cacheMap.end())
|
||||||
|
return cachePath + it->second;
|
||||||
|
}
|
||||||
std::vector<u8> content;
|
std::vector<u8> content;
|
||||||
std::string content_type;
|
std::string content_type;
|
||||||
int rc = http::get(url, content, content_type);
|
int rc = http::get(url, content, content_type);
|
||||||
if (!http::success(rc))
|
if (!http::success(rc))
|
||||||
return {};
|
return {};
|
||||||
std::stringstream stream;
|
std::stringstream stream;
|
||||||
stream << std::hex << hash;
|
stream << std::hex << hash << ".png";
|
||||||
if (content_type == "image/jpeg")
|
std::string localPath = cachePath + stream.str();
|
||||||
stream << ".jpg";
|
FILE *f = nowide::fopen(localPath.c_str(), "wb");
|
||||||
else
|
|
||||||
stream << ".png";
|
|
||||||
std::string path = cachePath + stream.str();
|
|
||||||
FILE *f = nowide::fopen(path.c_str(), "wb");
|
|
||||||
if (f == nullptr) {
|
if (f == nullptr) {
|
||||||
WARN_LOG(COMMON, "Can't save image to %s", path.c_str());
|
WARN_LOG(COMMON, "Can't save image to %s", localPath.c_str());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
fwrite(content.data(), 1, content.size(), f);
|
fwrite(content.data(), 1, content.size(), f);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
cacheMap[hash] = stream.str();
|
{
|
||||||
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, path.c_str());
|
std::lock_guard<std::mutex> _(cacheMutex);
|
||||||
return path;
|
cacheMap[hash] = stream.str();
|
||||||
|
}
|
||||||
|
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, localPath.c_str());
|
||||||
|
return localPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Achievements::term()
|
void Achievements::term()
|
||||||
|
@ -264,6 +310,8 @@ void Achievements::term()
|
||||||
if (rc_client == nullptr)
|
if (rc_client == nullptr)
|
||||||
return;
|
return;
|
||||||
unloadGame();
|
unloadGame();
|
||||||
|
if (asyncImageDownload.valid())
|
||||||
|
asyncImageDownload.get();
|
||||||
rc_client_destroy(rc_client);
|
rc_client_destroy(rc_client);
|
||||||
rc_client = nullptr;
|
rc_client = nullptr;
|
||||||
}
|
}
|
||||||
|
@ -507,15 +555,26 @@ void Achievements::handleUnlockEvent(const rc_client_event_t *event)
|
||||||
|
|
||||||
void Achievements::handleAchievementChallengeIndicatorShowEvent(const rc_client_event_t *event)
|
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);
|
|
||||||
INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title);
|
INFO_LOG(COMMON, "RA: Challenge: %s", event->achievement->title);
|
||||||
|
char url[128];
|
||||||
|
int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url));
|
||||||
|
if (rc == RC_OK)
|
||||||
|
{
|
||||||
|
std::string image = getOrDownloadImage(url);
|
||||||
|
notifier.showChallenge(image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event)
|
void Achievements::handleAchievementChallengeIndicatorHideEvent(const rc_client_event_t *event)
|
||||||
{
|
{
|
||||||
// TODO
|
INFO_LOG(COMMON, "RA: Challenge hidden: %s", event->achievement->title);
|
||||||
|
char url[128];
|
||||||
|
int rc = rc_client_achievement_get_image_url(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED, url, sizeof(url));
|
||||||
|
if (rc == RC_OK)
|
||||||
|
{
|
||||||
|
std::string image = getOrDownloadImage(url);
|
||||||
|
notifier.hideChallenge(image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Achievements::handleGameCompleted(const rc_client_event_t *event)
|
void Achievements::handleGameCompleted(const rc_client_event_t *event)
|
||||||
|
@ -551,6 +610,7 @@ void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *even
|
||||||
notifier.notify(Notification::Progress, image, event->achievement->measured_progress);
|
notifier.notify(Notification::Progress, image, event->achievement->measured_progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Disc *hashDisk;
|
||||||
static bool add150;
|
static bool add150;
|
||||||
|
|
||||||
static void *cdreader_open_track(const char* path, u32 track)
|
static void *cdreader_open_track(const char* path, u32 track)
|
||||||
|
@ -558,54 +618,31 @@ static void *cdreader_open_track(const char* path, u32 track)
|
||||||
DEBUG_LOG(COMMON, "RA: cdreader_open_track %s track %d", path, track);
|
DEBUG_LOG(COMMON, "RA: cdreader_open_track %s track %d", path, track);
|
||||||
if (track == RC_HASH_CDTRACK_FIRST_DATA)
|
if (track == RC_HASH_CDTRACK_FIRST_DATA)
|
||||||
{
|
{
|
||||||
u32 toc[102];
|
for (const Track& track : hashDisk->tracks)
|
||||||
libGDR_GetToc(toc, SingleDensity);
|
if (track.isDataTrack())
|
||||||
for (int i = 0; i < 99; i++)
|
return const_cast<Track *>(&track);
|
||||||
if (toc[i] != 0xffffffff)
|
|
||||||
{
|
|
||||||
if (((toc[i] >> 4) & 0xf) & 4)
|
|
||||||
return reinterpret_cast<void *>(i + 1);
|
|
||||||
}
|
|
||||||
if (libGDR_GetDiscType() == GdRom)
|
|
||||||
{
|
|
||||||
libGDR_GetToc(toc, DoubleDensity);
|
|
||||||
for (int i = 0; i < 99; i++)
|
|
||||||
if (toc[i] != 0xffffffff)
|
|
||||||
{
|
|
||||||
if (((toc[i] >> 4) & 0xf) & 4)
|
|
||||||
return reinterpret_cast<void *>(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u32 start, end;
|
|
||||||
if (!libGDR_GetTrack(track, start, end))
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (track <= hashDisk->tracks.size())
|
||||||
|
return const_cast<Track *>(&hashDisk->tracks[track - 1]);
|
||||||
else
|
else
|
||||||
return reinterpret_cast<void *>(track);
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, size_t requested_bytes)
|
static size_t cdreader_read_sector(void* track_handle, u32 sector, void* buffer, size_t requested_bytes)
|
||||||
{
|
{
|
||||||
//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)
|
if (requested_bytes == 2048)
|
||||||
// add 150 sectors to FAD corresponding to files
|
// add 150 sectors to FAD corresponding to files
|
||||||
|
// FIXME get rid of this
|
||||||
add150 = true;
|
add150 = true;
|
||||||
if (add150) // FIXME how to get rid of this?
|
//DEBUG_LOG(COMMON, "RA: cdreader_read_sector track %p sec %d+%d num %zd", track_handle, sector, add150 ? 150 : 0, requested_bytes);
|
||||||
|
if (add150)
|
||||||
sector += 150;
|
sector += 150;
|
||||||
u32 count = requested_bytes;
|
u8 locbuf[2048];
|
||||||
u32 secNum = count / 2048;
|
hashDisk->ReadSectors(sector, 1, locbuf, 2048);
|
||||||
if (secNum > 0)
|
requested_bytes = std::min<size_t>(requested_bytes, 2048);
|
||||||
{
|
memcpy(buffer, locbuf, requested_bytes);
|
||||||
libGDR_ReadSector((u8 *)buffer, sector, secNum, 2048);
|
|
||||||
buffer = (u8 *)buffer + secNum * 2048;
|
|
||||||
count -= secNum * 2048;
|
|
||||||
}
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
u8 locbuf[2048];
|
|
||||||
libGDR_ReadSector(locbuf, sector + secNum, 1, 2048);
|
|
||||||
memcpy(buffer, locbuf, count);
|
|
||||||
}
|
|
||||||
return requested_bytes;
|
return requested_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -615,12 +652,9 @@ static void cdreader_close_track(void* track_handle)
|
||||||
|
|
||||||
static u32 cdreader_first_track_sector(void* track_handle)
|
static u32 cdreader_first_track_sector(void* track_handle)
|
||||||
{
|
{
|
||||||
u32 trackNum = reinterpret_cast<uintptr_t>(track_handle);
|
Track& track = *static_cast<Track *>(track_handle);
|
||||||
u32 start, end;
|
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %p -> %d", track_handle, track.StartFAD);
|
||||||
if (!libGDR_GetTrack(trackNum, start, end))
|
return track.StartFAD;
|
||||||
return 0;
|
|
||||||
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %d -> %d", trackNum, start);
|
|
||||||
return start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Achievements::getGameHash()
|
std::string Achievements::getGameHash()
|
||||||
|
@ -629,7 +663,13 @@ std::string Achievements::getGameHash()
|
||||||
{
|
{
|
||||||
const u32 diskType = libGDR_GetDiscType();
|
const u32 diskType = libGDR_GetDiscType();
|
||||||
if (diskType == NoDisk || diskType == Open)
|
if (diskType == NoDisk || diskType == Open)
|
||||||
return "";
|
return {};
|
||||||
|
// Reopen the disk locally to avoid threading issues (CHD)
|
||||||
|
try {
|
||||||
|
hashDisk = OpenDisc(settings.content.path);
|
||||||
|
} catch (const FlycastException& e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
add150 = false;
|
add150 = false;
|
||||||
rc_hash_cdreader hooks = {
|
rc_hash_cdreader hooks = {
|
||||||
cdreader_open_track,
|
cdreader_open_track,
|
||||||
|
@ -641,7 +681,7 @@ std::string Achievements::getGameHash()
|
||||||
rc_hash_init_error_message_callback([](const char *msg) {
|
rc_hash_init_error_message_callback([](const char *msg) {
|
||||||
WARN_LOG(COMMON, "cdreader: %s", msg);
|
WARN_LOG(COMMON, "cdreader: %s", msg);
|
||||||
});
|
});
|
||||||
#ifndef NDEBUG
|
#if !defined(NDEBUG) || defined(DEBUGFAST)
|
||||||
rc_hash_init_verbose_message_callback([](const char *msg) {
|
rc_hash_init_verbose_message_callback([](const char *msg) {
|
||||||
DEBUG_LOG(COMMON, "cdreader: %s", msg);
|
DEBUG_LOG(COMMON, "cdreader: %s", msg);
|
||||||
});
|
});
|
||||||
|
@ -650,6 +690,8 @@ std::string Achievements::getGameHash()
|
||||||
char hash[33] {};
|
char hash[33] {};
|
||||||
rc_hash_generate_from_file(hash, settings.platform.isConsole() ? RC_CONSOLE_DREAMCAST : RC_CONSOLE_ARCADE,
|
rc_hash_generate_from_file(hash, settings.platform.isConsole() ? RC_CONSOLE_DREAMCAST : RC_CONSOLE_ARCADE,
|
||||||
settings.content.fileName.c_str()); // fileName is only used for arcade
|
settings.content.fileName.c_str()); // fileName is only used for arcade
|
||||||
|
delete hashDisk;
|
||||||
|
hashDisk = nullptr;
|
||||||
|
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
@ -724,7 +766,7 @@ void Achievements::loadGame()
|
||||||
}
|
}
|
||||||
if (!init() || !isLoggedOn()) {
|
if (!init() || !isLoggedOn()) {
|
||||||
if (!isLoggedOn())
|
if (!isLoggedOn())
|
||||||
WARN_LOG(COMMON, "Not logged on. Not loading game yet");
|
INFO_LOG(COMMON, "Not logged on. Not loading game yet");
|
||||||
loadingGame = false;
|
loadingGame = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -735,6 +777,10 @@ void Achievements::loadGame()
|
||||||
((Achievements *)userdata)->gameLoaded(result, error_message);
|
((Achievements *)userdata)->gameLoaded(result, error_message);
|
||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
INFO_LOG(COMMON, "RA: empty hash. Aborting load");
|
||||||
|
loadingGame = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Achievements::gameLoaded(int result, const char *errorMessage)
|
void Achievements::gameLoaded(int result, const char *errorMessage)
|
||||||
|
@ -811,6 +857,93 @@ void Achievements::diskChange()
|
||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Game Achievements::getCurrentGame()
|
||||||
|
{
|
||||||
|
if (!active)
|
||||||
|
return Game{};
|
||||||
|
const rc_client_game_t *info = rc_client_get_game_info(rc_client);
|
||||||
|
if (info == nullptr)
|
||||||
|
return Game{};
|
||||||
|
char url[128];
|
||||||
|
std::string image;
|
||||||
|
if (rc_client_game_get_image_url(info, url, sizeof(url)) == RC_OK)
|
||||||
|
{
|
||||||
|
bool cached;
|
||||||
|
std::tie(image, cached) = getCachedImage(url);
|
||||||
|
if (!cached)
|
||||||
|
{
|
||||||
|
if (asyncImageDownload.valid())
|
||||||
|
{
|
||||||
|
if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout)
|
||||||
|
INFO_LOG(COMMON, "Async image download already in progress");
|
||||||
|
else
|
||||||
|
asyncImageDownload.get();
|
||||||
|
}
|
||||||
|
if (!asyncImageDownload.valid())
|
||||||
|
{
|
||||||
|
std::string surl = url;
|
||||||
|
asyncImageDownload = std::async(std::launch::async, [this, surl]() {
|
||||||
|
getOrDownloadImage(surl.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc_client_user_game_summary_t summary;
|
||||||
|
rc_client_get_user_game_summary(rc_client, &summary);
|
||||||
|
|
||||||
|
return Game{ image, info->title, summary.num_unlocked_achievements, summary.num_core_achievements, summary.points_unlocked, summary.points_core };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Achievement> Achievements::getAchievementList()
|
||||||
|
{
|
||||||
|
std::vector<Achievement> achievements;
|
||||||
|
rc_client_achievement_list_t *list = rc_client_create_achievement_list(rc_client,
|
||||||
|
RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
|
||||||
|
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
|
||||||
|
std::vector<std::string> uncachedImages;
|
||||||
|
for (u32 i = 0; i < list->num_buckets; i++)
|
||||||
|
{
|
||||||
|
const char *label = list->buckets[i].label;
|
||||||
|
for (u32 j = 0; j < list->buckets[i].num_achievements; j++)
|
||||||
|
{
|
||||||
|
const rc_client_achievement_t *achievement = list->buckets[i].achievements[j];
|
||||||
|
char url[128];
|
||||||
|
std::string image;
|
||||||
|
if (rc_client_achievement_get_image_url(achievement, achievement->state, url, sizeof(url)) == RC_OK)
|
||||||
|
{
|
||||||
|
bool cached;
|
||||||
|
std::tie(image, cached) = getCachedImage(url);
|
||||||
|
if (!cached)
|
||||||
|
uncachedImages.push_back(url);
|
||||||
|
}
|
||||||
|
std::string status;
|
||||||
|
if (achievement->measured_percent)
|
||||||
|
status = achievement->measured_progress;
|
||||||
|
achievements.emplace_back(image, achievement->title, achievement->description, label, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc_client_destroy_achievement_list(list);
|
||||||
|
if (!uncachedImages.empty())
|
||||||
|
{
|
||||||
|
if (asyncImageDownload.valid())
|
||||||
|
{
|
||||||
|
if (asyncImageDownload.wait_for(std::chrono::seconds::zero()) == std::future_status::timeout)
|
||||||
|
INFO_LOG(COMMON, "Async image download already in progress");
|
||||||
|
else
|
||||||
|
asyncImageDownload.get();
|
||||||
|
}
|
||||||
|
if (!asyncImageDownload.valid())
|
||||||
|
{
|
||||||
|
asyncImageDownload = std::async(std::launch::async, [this, uncachedImages]() {
|
||||||
|
for (const std::string& url : uncachedImages)
|
||||||
|
getOrDownloadImage(url.c_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return achievements;
|
||||||
|
}
|
||||||
|
|
||||||
void Achievements::serialize(Serializer& ser)
|
void Achievements::serialize(Serializer& ser)
|
||||||
{
|
{
|
||||||
u32 size = (u32)rc_client_progress_size(rc_client);
|
u32 size = (u32)rc_client_progress_size(rc_client);
|
||||||
|
|
|
@ -17,16 +17,42 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace achievements
|
namespace achievements
|
||||||
{
|
{
|
||||||
#ifdef USE_RACHIEVEMENTS
|
#ifdef USE_RACHIEVEMENTS
|
||||||
|
|
||||||
|
struct Game
|
||||||
|
{
|
||||||
|
std::string image;
|
||||||
|
std::string title;
|
||||||
|
u32 unlockedAchievements;
|
||||||
|
u32 totalAchievements;
|
||||||
|
u32 points;
|
||||||
|
u32 totalPoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Achievement
|
||||||
|
{
|
||||||
|
Achievement() = default;
|
||||||
|
Achievement(const std::string& image, const std::string& title, const std::string& description, const std::string& category, const std::string& status)
|
||||||
|
: image(image), title(title), description(description), category(category), status(status) {}
|
||||||
|
std::string image;
|
||||||
|
std::string title;
|
||||||
|
std::string description;
|
||||||
|
std::string category;
|
||||||
|
std::string status;
|
||||||
|
};
|
||||||
|
|
||||||
bool init();
|
bool init();
|
||||||
void term();
|
void term();
|
||||||
std::future<void> login(const char *username, const char *password);
|
std::future<void> login(const char *username, const char *password);
|
||||||
void logout();
|
void logout();
|
||||||
bool isLoggedOn();
|
bool isLoggedOn();
|
||||||
|
bool isActive();
|
||||||
|
Game getCurrentGame();
|
||||||
|
std::vector<Achievement> getAchievementList();
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ void dc_exit();
|
||||||
void dc_savestate(int index = 0);
|
void dc_savestate(int index = 0);
|
||||||
void dc_loadstate(int index = 0);
|
void dc_loadstate(int index = 0);
|
||||||
void dc_loadstate(Deserializer& deser);
|
void dc_loadstate(Deserializer& deser);
|
||||||
|
std::string dc_getStateUpdateDate(int index);
|
||||||
|
|
||||||
enum class Event {
|
enum class Event {
|
||||||
Start,
|
Start,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "log/LogManager.h"
|
#include "log/LogManager.h"
|
||||||
#include "rend/gui.h"
|
#include "rend/gui.h"
|
||||||
#include "oslib/oslib.h"
|
#include "oslib/oslib.h"
|
||||||
|
#include "oslib/directory.h"
|
||||||
#include "debug/gdb_server.h"
|
#include "debug/gdb_server.h"
|
||||||
#include "archive/rzip.h"
|
#include "archive/rzip.h"
|
||||||
#include "rend/mainui.h"
|
#include "rend/mainui.h"
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
#include "lua/lua.h"
|
#include "lua/lua.h"
|
||||||
#include "stdclass.h"
|
#include "stdclass.h"
|
||||||
#include "serialize.h"
|
#include "serialize.h"
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
int flycast_init(int argc, char* argv[])
|
int flycast_init(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
|
@ -231,4 +233,32 @@ void dc_loadstate(int index)
|
||||||
EventManager::event(Event::LoadState);
|
EventManager::event(Event::LoadState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
static struct tm *localtime_r(const time_t *_clock, struct tm *_result)
|
||||||
|
{
|
||||||
|
return localtime_s(_result, _clock) ? nullptr : _result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string dc_getStateUpdateDate(int index)
|
||||||
|
{
|
||||||
|
std::string filename = hostfs::getSavestatePath(index, false);
|
||||||
|
struct stat st;
|
||||||
|
if (flycast::stat(filename.c_str(), &st) != 0)
|
||||||
|
return {};
|
||||||
|
time_t ago = time(nullptr) - st.st_mtime;
|
||||||
|
if (ago < 60)
|
||||||
|
return std::to_string(ago) + " seconds ago";
|
||||||
|
if (ago < 3600)
|
||||||
|
return std::to_string(ago / 60) + " minutes ago";
|
||||||
|
if (ago < 3600 * 24)
|
||||||
|
return std::to_string(ago / 3600) + " hours ago";
|
||||||
|
tm t;
|
||||||
|
if (localtime_r(&st.st_mtime, &t) == nullptr)
|
||||||
|
return {};
|
||||||
|
std::string s(32, '\0');
|
||||||
|
s.resize(snprintf(s.data(), 32, "%04d/%02d/%02d %02d:%02d:%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -184,7 +184,8 @@ public:
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (flycast::stat(path.c_str(), &st) != 0) {
|
if (flycast::stat(path.c_str(), &st) != 0) {
|
||||||
INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno);
|
if (errno != ENOENT)
|
||||||
|
INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno);
|
||||||
throw StorageException("Cannot stat " + path);
|
throw StorageException("Cannot stat " + path);
|
||||||
}
|
}
|
||||||
info.isDirectory = S_ISDIR(st.st_mode);
|
info.isDirectory = S_ISDIR(st.st_mode);
|
||||||
|
|
|
@ -284,7 +284,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi
|
||||||
FILE *f = nowide::fopen((const char *)context, "wb");
|
FILE *f = nowide::fopen((const char *)context, "wb");
|
||||||
if (f == nullptr)
|
if (f == nullptr)
|
||||||
{
|
{
|
||||||
WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", context, errno);
|
WARN_LOG(RENDERER, "Dump texture: can't save to file %s: error %d", (const char *)context, errno);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -48,6 +48,7 @@
|
||||||
#include "oslib/resources.h"
|
#include "oslib/resources.h"
|
||||||
#include "achievements/achievements.h"
|
#include "achievements/achievements.h"
|
||||||
#include "gui_achievements.h"
|
#include "gui_achievements.h"
|
||||||
|
#include "IconsFontAwesome6.h"
|
||||||
#if defined(USE_SDL)
|
#if defined(USE_SDL)
|
||||||
#include "sdl/sdl.h"
|
#include "sdl/sdl.h"
|
||||||
#endif
|
#endif
|
||||||
|
@ -309,6 +310,12 @@ void gui_initFonts()
|
||||||
|
|
||||||
// TODO Linux, iOS, ...
|
// TODO Linux, iOS, ...
|
||||||
#endif
|
#endif
|
||||||
|
// Font Awesome symbols (added to default font)
|
||||||
|
data = resource::load("fonts/" FONT_ICON_FILE_NAME_FAS, dataSize);
|
||||||
|
verify(data != nullptr);
|
||||||
|
const float symbolFontSize = 21.f * settings.display.uiScale;
|
||||||
|
static ImWchar faRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 };
|
||||||
|
io.Fonts->AddFontFromMemoryTTF(data.release(), dataSize, symbolFontSize, &font_cfg, faRanges);
|
||||||
// Large font without Asian glyphs
|
// Large font without Asian glyphs
|
||||||
data = resource::load("fonts/Roboto-Regular.ttf", dataSize);
|
data = resource::load("fonts/Roboto-Regular.ttf", dataSize);
|
||||||
verify(data != nullptr);
|
verify(data != nullptr);
|
||||||
|
@ -574,104 +581,159 @@ static bool savestateAllowed()
|
||||||
|
|
||||||
static void gui_display_commands()
|
static void gui_display_commands()
|
||||||
{
|
{
|
||||||
imguiDriver->displayVmus();
|
imguiDriver->displayVmus();
|
||||||
|
fullScreenWindow(false);
|
||||||
|
ImGui::SetNextWindowBgAlpha(0.8f);
|
||||||
|
ImguiStyleVar _{ImGuiStyleVar_WindowBorderSize, 0};
|
||||||
|
|
||||||
centerNextWindow();
|
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoDecoration);
|
||||||
ImGui::SetNextWindowSize(ScaledVec2(330, 0));
|
{
|
||||||
|
ImguiStyleVar _{ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)}; // left aligned
|
||||||
|
|
||||||
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize);
|
float buttonHeight = 50.f; // not scaled
|
||||||
|
bool lowH = ImGui::GetContentRegionAvail().y < ((100 + 50 * 6) * settings.display.uiScale
|
||||||
|
+ ImGui::GetStyle().FramePadding.y * 2 + ImGui::GetStyle().ItemSpacing.y * 5);
|
||||||
|
if (lowH)
|
||||||
|
{
|
||||||
|
// Low height available (phone): Put game icon in first column without text
|
||||||
|
// Button columns in next 2 columns
|
||||||
|
float emptyW = ImGui::GetContentRegionAvail().x - (100 + 150 * 2) * settings.display.uiScale - ImGui::GetStyle().WindowPadding.x * 2;
|
||||||
|
ImGui::Columns(3, "buttons", false);
|
||||||
|
ImGui::SetColumnWidth(0, 100 * settings.display.uiScale + ImGui::GetStyle().FramePadding.x * 2 + emptyW / 3);
|
||||||
|
bool veryLowH = ImGui::GetContentRegionAvail().y < (50 * 6 * settings.display.uiScale
|
||||||
|
+ ImGui::GetStyle().ItemSpacing.y * 5);
|
||||||
|
if (veryLowH)
|
||||||
|
buttonHeight = (ImGui::GetContentRegionAvail().y - ImGui::GetStyle().ItemSpacing.y * 5)
|
||||||
|
/ 6 / settings.display.uiScale;
|
||||||
|
}
|
||||||
|
GameMedia game;
|
||||||
|
game.path = settings.content.path;
|
||||||
|
game.fileName = settings.content.fileName;
|
||||||
|
GameBoxart art = boxart.getBoxart(game);
|
||||||
|
ImguiTexture tex(art.boxartPath);
|
||||||
|
// TODO use placeholder image if not available
|
||||||
|
tex.draw(ScaledVec2(100, 100));
|
||||||
|
|
||||||
{
|
if (!lowH)
|
||||||
if (card_reader::barcodeAvailable())
|
{
|
||||||
{
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginChild("game_info", ScaledVec2(0, 100.f), ImGuiChildFlags_Border, ImGuiWindowFlags_None);
|
||||||
|
ImGui::PushFont(largeFont);
|
||||||
|
ImGui::Text("%s", art.name.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
|
||||||
|
ImGui::TextWrapped("%s", art.fileName.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::Columns(3, "buttons", false);
|
||||||
|
ImGui::SetColumnWidth(0, 100.f * settings.display.uiScale + ImGui::GetStyle().ItemSpacing.x);
|
||||||
|
ImGui::SetColumnWidth(1, 200.f * settings.display.uiScale);
|
||||||
|
}
|
||||||
|
ImGui::NextColumn();
|
||||||
|
ImguiStyleVar _1{ImGuiStyleVar_FramePadding, ScaledVec2(12.f, 3.f)};
|
||||||
|
|
||||||
|
// Resume
|
||||||
|
if (ImGui::Button(ICON_FA_PLAY " Resume", ScaledVec2(150, buttonHeight)))
|
||||||
|
{
|
||||||
|
GamepadDevice::load_system_mappings();
|
||||||
|
gui_setState(GuiState::Closed);
|
||||||
|
}
|
||||||
|
// Cheats
|
||||||
|
{
|
||||||
|
DisabledScope _{settings.network.online};
|
||||||
|
|
||||||
|
if (ImGui::Button(ICON_FA_MASK " Cheats", ScaledVec2(150, buttonHeight)) && !settings.network.online)
|
||||||
|
gui_setState(GuiState::Cheats);
|
||||||
|
}
|
||||||
|
// Achievements
|
||||||
|
{
|
||||||
|
DisabledScope _{!achievements::isActive()};
|
||||||
|
|
||||||
|
if (ImGui::Button(ICON_FA_TROPHY " Achievements", ScaledVec2(150, buttonHeight)) && achievements::isActive())
|
||||||
|
gui_setState(GuiState::Achievements);
|
||||||
|
}
|
||||||
|
// Insert/Eject Disk
|
||||||
|
const char *disk_label = libGDR_GetDiscType() == Open ? ICON_FA_COMPACT_DISC " Insert Disk" : ICON_FA_COMPACT_DISC " Eject Disk";
|
||||||
|
if (ImGui::Button(disk_label, ScaledVec2(150, buttonHeight)))
|
||||||
|
{
|
||||||
|
if (libGDR_GetDiscType() == Open) {
|
||||||
|
gui_setState(GuiState::SelectDisk);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DiscOpenLid();
|
||||||
|
gui_setState(GuiState::Closed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Settings
|
||||||
|
if (ImGui::Button(ICON_FA_GEAR " Settings", ScaledVec2(150, buttonHeight)))
|
||||||
|
gui_setState(GuiState::Settings);
|
||||||
|
// Exit
|
||||||
|
if (ImGui::Button(commandLineStart ? ICON_FA_POWER_OFF " Exit" : ICON_FA_POWER_OFF " Close Game", ScaledVec2(150, buttonHeight)))
|
||||||
|
gui_stop_game();
|
||||||
|
|
||||||
|
ImGui::NextColumn();
|
||||||
|
{
|
||||||
|
DisabledScope _{!savestateAllowed()};
|
||||||
|
|
||||||
|
// Load State
|
||||||
|
if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT " Load State", ScaledVec2(150, buttonHeight)) && savestateAllowed())
|
||||||
|
{
|
||||||
|
gui_setState(GuiState::Closed);
|
||||||
|
dc_loadstate(config::SavestateSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save State
|
||||||
|
if (ImGui::Button(ICON_FA_DOWNLOAD " Save State", ScaledVec2(150, buttonHeight)) && savestateAllowed())
|
||||||
|
{
|
||||||
|
gui_setState(GuiState::Closed);
|
||||||
|
dc_savestate(config::SavestateSlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot #
|
||||||
|
if (ImGui::ArrowButton("##prev-slot", ImGuiDir_Left))
|
||||||
|
{
|
||||||
|
if (config::SavestateSlot == 0)
|
||||||
|
config::SavestateSlot = 9;
|
||||||
|
else
|
||||||
|
config::SavestateSlot--;
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1);
|
||||||
|
float spacingW = (150.f * settings.display.uiScale - ImGui::GetFrameHeight() * 2 - ImGui::CalcTextSize(slot.c_str()).x) / 2;
|
||||||
|
ImGui::SameLine(0, spacingW);
|
||||||
|
ImGui::Text("%s", slot.c_str());
|
||||||
|
ImGui::SameLine(0, spacingW);
|
||||||
|
if (ImGui::ArrowButton("##next-slot", ImGuiDir_Right))
|
||||||
|
{
|
||||||
|
if (config::SavestateSlot == 9)
|
||||||
|
config::SavestateSlot = 0;
|
||||||
|
else
|
||||||
|
config::SavestateSlot++;
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
std::string date = dc_getStateUpdateDate(config::SavestateSlot);
|
||||||
|
{
|
||||||
|
ImVec4 gray(0.75f, 0.75f, 0.75f, 1.f);
|
||||||
|
if (date.empty())
|
||||||
|
ImGui::TextColored(gray, "Empty");
|
||||||
|
else
|
||||||
|
ImGui::TextColored(gray, "%s", date.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Barcode
|
||||||
|
if (card_reader::barcodeAvailable())
|
||||||
|
{
|
||||||
|
ImGui::NewLine();
|
||||||
|
ImGui::Text("Barcode Card");
|
||||||
char cardBuf[64] {};
|
char cardBuf[64] {};
|
||||||
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
||||||
if (ImGui::InputText("Card", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
||||||
card_reader::barcodeSetCard(cardBuf);
|
card_reader::barcodeSetCard(cardBuf);
|
||||||
}
|
|
||||||
|
|
||||||
DisabledScope scope(!savestateAllowed());
|
|
||||||
|
|
||||||
// Load State
|
|
||||||
if (ImGui::Button("Load State", ScaledVec2(110, 50)) && savestateAllowed())
|
|
||||||
{
|
|
||||||
gui_setState(GuiState::Closed);
|
|
||||||
dc_loadstate(config::SavestateSlot);
|
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Slot #
|
ImGui::Columns(1, nullptr, false);
|
||||||
std::string slot = "Slot " + std::to_string((int)config::SavestateSlot + 1);
|
|
||||||
if (ImGui::Button(slot.c_str(), ImVec2(80 * settings.display.uiScale - ImGui::GetStyle().FramePadding.x, 50 * settings.display.uiScale)))
|
|
||||||
ImGui::OpenPopup("slot_select_popup");
|
|
||||||
if (ImGui::BeginPopup("slot_select_popup"))
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
if (ImGui::Selectable(std::to_string(i + 1).c_str(), config::SavestateSlot == i, 0,
|
|
||||||
ImVec2(ImGui::CalcTextSize("Slot 8").x, 0))) {
|
|
||||||
config::SavestateSlot = i;
|
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
// Save State
|
|
||||||
if (ImGui::Button("Save State", ScaledVec2(110, 50)) && savestateAllowed())
|
|
||||||
{
|
|
||||||
gui_setState(GuiState::Closed);
|
|
||||||
dc_savestate(config::SavestateSlot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Columns(2, "buttons", false);
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
if (ImGui::Button("Settings", ScaledVec2(150, 50)))
|
|
||||||
{
|
|
||||||
gui_setState(GuiState::Settings);
|
|
||||||
}
|
}
|
||||||
ImGui::NextColumn();
|
|
||||||
if (ImGui::Button("Resume", ScaledVec2(150, 50)))
|
|
||||||
{
|
|
||||||
GamepadDevice::load_system_mappings();
|
|
||||||
gui_setState(GuiState::Closed);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::NextColumn();
|
|
||||||
|
|
||||||
// Insert/Eject Disk
|
|
||||||
const char *disk_label = libGDR_GetDiscType() == Open ? "Insert Disk" : "Eject Disk";
|
|
||||||
if (ImGui::Button(disk_label, ScaledVec2(150, 50)))
|
|
||||||
{
|
|
||||||
if (libGDR_GetDiscType() == Open)
|
|
||||||
{
|
|
||||||
gui_setState(GuiState::SelectDisk);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DiscOpenLid();
|
|
||||||
gui_setState(GuiState::Closed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::NextColumn();
|
|
||||||
|
|
||||||
// Cheats
|
|
||||||
{
|
|
||||||
DisabledScope scope(settings.network.online);
|
|
||||||
|
|
||||||
if (ImGui::Button("Cheats", ScaledVec2(150, 50)) && !settings.network.online)
|
|
||||||
gui_setState(GuiState::Cheats);
|
|
||||||
}
|
|
||||||
ImGui::Columns(1, nullptr, false);
|
|
||||||
|
|
||||||
// Exit
|
|
||||||
if (ImGui::Button(commandLineStart ? "Exit" : "Close Game", ScaledVec2(300, 50)
|
|
||||||
+ ImVec2(ImGui::GetStyle().ColumnsMinSpacing + ImGui::GetStyle().FramePadding.x * 2 - 1, 0)))
|
|
||||||
{
|
|
||||||
gui_stop_game();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2929,49 +2991,9 @@ static void gameTooltip(const std::string& tip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool getGameImage(const GameBoxart& art, ImTextureID& textureId, bool allowLoad)
|
static bool gameImageButton(ImguiTexture& texture, const std::string& tooltip, ImVec2 size, const std::string& gameName)
|
||||||
{
|
{
|
||||||
textureId = ImTextureID{};
|
bool pressed = texture.button("", size, gameName);
|
||||||
if (art.boxartPath.empty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Get the boxart texture. Load it if needed.
|
|
||||||
textureId = imguiDriver->getTexture(art.boxartPath);
|
|
||||||
if (textureId == ImTextureID() && allowLoad)
|
|
||||||
{
|
|
||||||
int width, height;
|
|
||||||
u8 *imgData = loadImage(art.boxartPath, width, height);
|
|
||||||
if (imgData != nullptr)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
textureId = imguiDriver->updateTextureAndAspectRatio(art.boxartPath, imgData, width, height);
|
|
||||||
} catch (...) {
|
|
||||||
// vulkan can throw during resizing
|
|
||||||
}
|
|
||||||
free(imgData);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool gameImageButton(ImTextureID textureId, const std::string& tooltip, ImVec2 size)
|
|
||||||
{
|
|
||||||
float ar = imguiDriver->getAspectRatio(textureId);
|
|
||||||
ImVec2 uv0 { 0.f, 0.f };
|
|
||||||
ImVec2 uv1 { 1.f, 1.f };
|
|
||||||
if (ar > 1)
|
|
||||||
{
|
|
||||||
uv0.y = -(ar - 1) / 2;
|
|
||||||
uv1.y = 1 + (ar - 1) / 2;
|
|
||||||
}
|
|
||||||
else if (ar != 0)
|
|
||||||
{
|
|
||||||
ar = 1 / ar;
|
|
||||||
uv0.x = -(ar - 1) / 2;
|
|
||||||
uv1.x = 1 + (ar - 1) / 2;
|
|
||||||
}
|
|
||||||
bool pressed = ImGui::ImageButton("", textureId, size - ImGui::GetStyle().FramePadding * 2, uv0, uv1);
|
|
||||||
gameTooltip(tooltip);
|
gameTooltip(tooltip);
|
||||||
|
|
||||||
return pressed;
|
return pressed;
|
||||||
|
@ -3036,22 +3058,16 @@ static void gui_display_content()
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
|
||||||
|
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
int loadedImages = 0;
|
|
||||||
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
|
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
|
||||||
{
|
{
|
||||||
ImGui::PushID("bios");
|
ImGui::PushID("bios");
|
||||||
bool pressed;
|
bool pressed;
|
||||||
if (config::BoxartDisplayMode)
|
if (config::BoxartDisplayMode)
|
||||||
{
|
{
|
||||||
ImTextureID textureId{};
|
|
||||||
GameMedia game;
|
GameMedia game;
|
||||||
GameBoxart art = boxart.getBoxartAndLoad(game);
|
GameBoxart art = boxart.getBoxartAndLoad(game);
|
||||||
if (getGameImage(art, textureId, loadedImages < 10))
|
ImguiTexture tex(art.boxartPath);
|
||||||
loadedImages++;
|
pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS");
|
||||||
if (textureId != ImTextureID())
|
|
||||||
pressed = gameImageButton(textureId, "Dreamcast BIOS", responsiveBoxVec2);
|
|
||||||
else
|
|
||||||
pressed = ImGui::Button("Dreamcast BIOS", responsiveBoxVec2);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -3084,23 +3100,19 @@ static void gui_display_content()
|
||||||
if (filter.PassFilter(gameName.c_str()))
|
if (filter.PassFilter(gameName.c_str()))
|
||||||
{
|
{
|
||||||
ImGui::PushID(game.path.c_str());
|
ImGui::PushID(game.path.c_str());
|
||||||
bool pressed;
|
bool pressed = false;
|
||||||
if (config::BoxartDisplayMode)
|
if (config::BoxartDisplayMode)
|
||||||
{
|
{
|
||||||
if (counter % itemsPerLine != 0)
|
if (counter % itemsPerLine != 0)
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
counter++;
|
counter++;
|
||||||
ImTextureID textureId{};
|
// Put the image inside a child window so we can detect when it's fully clipped and doesn't need to be loaded
|
||||||
// Get the boxart texture. Load it if needed (max 10 per frame).
|
if (ImGui::BeginChild("img", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None))
|
||||||
if (getGameImage(art, textureId, loadedImages < 10))
|
|
||||||
loadedImages++;
|
|
||||||
if (textureId != ImTextureID())
|
|
||||||
pressed = gameImageButton(textureId, game.name, responsiveBoxVec2);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
pressed = ImGui::Button(gameName.c_str(), responsiveBoxVec2);
|
ImguiTexture tex(art.boxartPath);
|
||||||
gameTooltip(game.name);
|
pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName);
|
||||||
}
|
}
|
||||||
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -3381,6 +3393,9 @@ void gui_display_ui()
|
||||||
case GuiState::Cheats:
|
case GuiState::Cheats:
|
||||||
gui_cheats();
|
gui_cheats();
|
||||||
break;
|
break;
|
||||||
|
case GuiState::Achievements:
|
||||||
|
achievements::achievementList();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
die("Unknown UI state");
|
die("Unknown UI state");
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -63,7 +63,8 @@ enum class GuiState {
|
||||||
SelectDisk,
|
SelectDisk,
|
||||||
Loading,
|
Loading,
|
||||||
NetworkStart,
|
NetworkStart,
|
||||||
Cheats
|
Cheats,
|
||||||
|
Achievements,
|
||||||
};
|
};
|
||||||
extern GuiState gui_state;
|
extern GuiState gui_state;
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,10 @@
|
||||||
#include "gui_util.h"
|
#include "gui_util.h"
|
||||||
#include "imgui_driver.h"
|
#include "imgui_driver.h"
|
||||||
#include "stdclass.h"
|
#include "stdclass.h"
|
||||||
|
#include "achievements/achievements.h"
|
||||||
|
#include "IconsFontAwesome6.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
extern ImFont *largeFont;
|
extern ImFont *largeFont;
|
||||||
|
|
||||||
|
@ -33,6 +36,7 @@ Notification notifier;
|
||||||
static constexpr u64 DISPLAY_TIME = 5000;
|
static constexpr u64 DISPLAY_TIME = 5000;
|
||||||
static constexpr u64 START_ANIM_TIME = 500;
|
static constexpr u64 START_ANIM_TIME = 500;
|
||||||
static constexpr u64 END_ANIM_TIME = 1000;
|
static constexpr u64 END_ANIM_TIME = 1000;
|
||||||
|
static constexpr u64 NEVER_ENDS = 1000000000000;
|
||||||
|
|
||||||
void Notification::notify(Type type, const std::string& image, const std::string& text1,
|
void Notification::notify(Type type, const std::string& image, const std::string& text1,
|
||||||
const std::string& text2, const std::string& text3)
|
const std::string& text2, const std::string& text3)
|
||||||
|
@ -47,7 +51,7 @@ void Notification::notify(Type type, const std::string& image, const std::string
|
||||||
{
|
{
|
||||||
// New progress
|
// New progress
|
||||||
startTime = now;
|
startTime = now;
|
||||||
endTime = 0x1000000000000; // never
|
endTime = NEVER_ENDS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -61,23 +65,59 @@ void Notification::notify(Type type, const std::string& image, const std::string
|
||||||
endTime = startTime + DISPLAY_TIME;
|
endTime = startTime + DISPLAY_TIME;
|
||||||
}
|
}
|
||||||
this->type = type;
|
this->type = type;
|
||||||
this->imagePath = image;
|
this->image = { image };
|
||||||
this->imageId = {};
|
|
||||||
text[0] = text1;
|
text[0] = text1;
|
||||||
text[1] = text2;
|
text[1] = text2;
|
||||||
text[2] = text3;
|
text[2] = text3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Notification::showChallenge(const std::string& image)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> _(mutex);
|
||||||
|
ImguiTexture texture{ image };
|
||||||
|
if (std::find(challenges.begin(), challenges.end(), texture) != challenges.end())
|
||||||
|
return;
|
||||||
|
challenges.push_back(texture);
|
||||||
|
if (this->type == None)
|
||||||
|
{
|
||||||
|
this->type = Challenge;
|
||||||
|
startTime = getTimeMs();
|
||||||
|
endTime = NEVER_ENDS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Notification::hideChallenge(const std::string& image)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> _(mutex);
|
||||||
|
auto it = std::find(challenges.begin(), challenges.end(), image);
|
||||||
|
if (it == challenges.end())
|
||||||
|
return;
|
||||||
|
challenges.erase(it);
|
||||||
|
if (this->type == Challenge && challenges.empty())
|
||||||
|
endTime = getTimeMs();
|
||||||
|
}
|
||||||
|
|
||||||
bool Notification::draw()
|
bool Notification::draw()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> _(mutex);
|
std::lock_guard<std::mutex> _(mutex);
|
||||||
if (type == None)
|
if (type == None)
|
||||||
return false;
|
return false;
|
||||||
u64 now = getTimeMs();
|
u64 now = getTimeMs();
|
||||||
if (now > endTime + END_ANIM_TIME) {
|
if (now > endTime + END_ANIM_TIME)
|
||||||
// Hide notification
|
{
|
||||||
type = None;
|
if (!challenges.empty())
|
||||||
return false;
|
{
|
||||||
|
// Show current challenge indicators
|
||||||
|
type = Challenge;
|
||||||
|
startTime = getTimeMs();
|
||||||
|
endTime = NEVER_ENDS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Hide notification
|
||||||
|
type = None;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (now > endTime)
|
if (now > endTime)
|
||||||
{
|
{
|
||||||
|
@ -89,83 +129,148 @@ bool Notification::draw()
|
||||||
else {
|
else {
|
||||||
ImGui::SetNextWindowBgAlpha(0.5f);
|
ImGui::SetNextWindowBgAlpha(0.5f);
|
||||||
}
|
}
|
||||||
if (imageId == ImTextureID{})
|
|
||||||
getImage();
|
|
||||||
float y = ImGui::GetIO().DisplaySize.y;
|
float y = ImGui::GetIO().DisplaySize.y;
|
||||||
if (now - startTime < START_ANIM_TIME)
|
if (now - startTime < START_ANIM_TIME)
|
||||||
// Slide up
|
// Slide up
|
||||||
y += 80.f * settings.display.uiScale * (std::cos((now - startTime) / (float)START_ANIM_TIME * (float)M_PI) + 1.f) / 2.f;
|
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::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));
|
if (type == Challenge)
|
||||||
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::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
|
||||||
ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed);
|
| ImGuiWindowFlags_NoInputs);
|
||||||
ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch);
|
for (const auto& img : challenges)
|
||||||
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableSetColumnIndex(0);
|
|
||||||
if (hasPic)
|
|
||||||
{
|
{
|
||||||
ImGui::Image(imageId, ScaledVec2(80.f, 80.f), { 0.f, 0.f }, { 1.f, 1.f });
|
img.draw(ScaledVec2(60.f, 60.f));
|
||||||
ImGui::TableSetColumnIndex(1);
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
|
ImGui::End();
|
||||||
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);
|
else
|
||||||
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::SetNextWindowSizeConstraints(ScaledVec2(80.f, 80.f) + ImVec2(ImGui::GetStyle().WindowPadding.x * 2, 0.f), ImVec2(FLT_MAX, FLT_MAX));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{ hasPic ? 0.f : winPaddingX, (3 - lines) * ImGui::GetTextLineHeight() / 2 });
|
const float winPaddingX = ImGui::GetStyle().WindowPadding.x;
|
||||||
if (ImGui::BeginChild("##text", ImVec2(w, 0), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_None))
|
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ImVec2{});
|
||||||
{
|
|
||||||
ImGui::PushFont(largeFont);
|
ImGui::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
|
||||||
ImGui::Text("%s", text[0].c_str());
|
| ImGuiWindowFlags_NoInputs);
|
||||||
ImGui::PopFont();
|
ImTextureID imageId = image.getId();
|
||||||
if (!text[1].empty())
|
const bool hasPic = imageId != ImTextureID{};
|
||||||
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[1].c_str());
|
if (ImGui::BeginTable("achievementNotif", hasPic ? 2 : 1, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings))
|
||||||
if (!text[2].empty())
|
{
|
||||||
ImGui::TextColored(ImVec4(1, 1, 0, 0.7f), "%s", text[2].c_str());
|
if (hasPic)
|
||||||
}
|
ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed);
|
||||||
ImGui::EndChild();
|
ImGui::TableSetupColumn("text", ImGuiTableColumnFlags_WidthStretch);
|
||||||
ImGui::PopStyleVar();
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
ImGui::EndTable();
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
if (hasPic)
|
||||||
|
{
|
||||||
|
image.draw(ScaledVec2(80.f, 80.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();
|
||||||
|
ImguiStyleVar _(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::EndTable();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
}
|
}
|
||||||
ImGui::End();
|
|
||||||
ImGui::PopStyleVar();
|
|
||||||
ImGui::GetStyle().Alpha = 1.f;
|
ImGui::GetStyle().Alpha = 1.f;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Notification::getImage()
|
void achievementList()
|
||||||
{
|
{
|
||||||
if (imagePath.empty())
|
fullScreenWindow(false);
|
||||||
return;
|
ImguiStyleVar _(ImGuiStyleVar_WindowBorderSize, 0);
|
||||||
|
|
||||||
|
ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize);
|
||||||
|
|
||||||
// Get the texture. Load it if needed.
|
|
||||||
imageId = imguiDriver->getTexture(imagePath);
|
|
||||||
if (imageId == ImTextureID())
|
|
||||||
{
|
{
|
||||||
int width, height;
|
float w = ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().ItemSpacing.x * 2 - ImGui::GetStyle().WindowPadding.x
|
||||||
u8 *imgData = loadImage(imagePath, width, height);
|
- (80.f + 20.f * 2) * settings.display.uiScale; // image width and button frame padding
|
||||||
if (imgData != nullptr)
|
Game game = getCurrentGame();
|
||||||
|
ImguiTexture tex(game.image);
|
||||||
|
tex.draw(ScaledVec2(80.f, 80.f));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginChild("game_info", ImVec2(w, 80.f * settings.display.uiScale), ImGuiChildFlags_None, ImGuiWindowFlags_None);
|
||||||
|
ImGui::PushFont(largeFont);
|
||||||
|
ImGui::Text("%s", game.title.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "You have unlocked " << game.unlockedAchievements << " of " << game.totalAchievements
|
||||||
|
<< " achievements and " << game.points << " of " << game.totalPoints << " points.";
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
|
||||||
|
ImGui::TextWrapped("%s", ss.str().c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImguiStyleVar _(ImGuiStyleVar_FramePadding, ScaledVec2(20, 8));
|
||||||
|
if (ImGui::Button("Close"))
|
||||||
|
gui_setState(GuiState::Commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginChild(ImGui::GetID("ach_list"), ImVec2(0, 0), ImGuiChildFlags_Border, ImGuiWindowFlags_DragScrolling | ImGuiWindowFlags_NavFlattened))
|
||||||
|
{
|
||||||
|
std::vector<Achievement> achList = getAchievementList();
|
||||||
|
int id = 0;
|
||||||
|
std::string category;
|
||||||
|
for (const auto& ach : achList)
|
||||||
{
|
{
|
||||||
try {
|
if (ach.category != category)
|
||||||
imageId = imguiDriver->updateTextureAndAspectRatio(imagePath, imgData, width, height);
|
{
|
||||||
} catch (...) {
|
category = ach.category;
|
||||||
// vulkan can throw during resizing
|
ImGui::Indent(10 * settings.display.uiScale);
|
||||||
|
if (category == "Locked" || category == "Active Challenges" || category == "Almost There")
|
||||||
|
ImGui::Text(ICON_FA_LOCK);
|
||||||
|
else if (category == "Unlocked" || category == "Recently Unlocked")
|
||||||
|
ImGui::Text(ICON_FA_LOCK_OPEN);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::PushFont(largeFont);
|
||||||
|
ImGui::Text("%s", category.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::Unindent(10 * settings.display.uiScale);
|
||||||
}
|
}
|
||||||
free(imgData);
|
ImguiID _("achiev" + std::to_string(id++));
|
||||||
|
ImguiTexture tex(ach.image);
|
||||||
|
tex.draw(ScaledVec2(80.f, 80.f));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::BeginChild(ImGui::GetID("ach_item"), ImVec2(0, 0), ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None);
|
||||||
|
ImGui::PushFont(largeFont);
|
||||||
|
ImGui::Text("%s", ach.title.c_str());
|
||||||
|
ImGui::PopFont();
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.75f, 0.75f, 0.75f, 1.f));
|
||||||
|
ImGui::TextWrapped("%s", ach.description.c_str());
|
||||||
|
ImGui::TextWrapped("%s", ach.status.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
scrollWhenDraggingOnVoid();
|
||||||
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scrollWhenDraggingOnVoid();
|
||||||
|
windowDragScroll();
|
||||||
|
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace achievements
|
} // namespace achievements
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
*/
|
*/
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
#include "gui_util.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace achievements
|
namespace achievements
|
||||||
{
|
{
|
||||||
|
@ -33,24 +35,27 @@ public:
|
||||||
Unlocked,
|
Unlocked,
|
||||||
Progress,
|
Progress,
|
||||||
Mastery,
|
Mastery,
|
||||||
|
Challenge,
|
||||||
Error
|
Error
|
||||||
};
|
};
|
||||||
void notify(Type type, const std::string& image, const std::string& text1,
|
void notify(Type type, const std::string& image, const std::string& text1,
|
||||||
const std::string& text2 = {}, const std::string& text3 = {});
|
const std::string& text2 = {}, const std::string& text3 = {});
|
||||||
|
void showChallenge(const std::string& image);
|
||||||
|
void hideChallenge(const std::string& image);
|
||||||
bool draw();
|
bool draw();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void getImage();
|
|
||||||
|
|
||||||
u64 startTime = 0;
|
u64 startTime = 0;
|
||||||
u64 endTime = 0;
|
u64 endTime = 0;
|
||||||
Type type = Type::None;
|
Type type = Type::None;
|
||||||
std::string imagePath;
|
ImguiTexture image;
|
||||||
ImTextureID imageId {};
|
|
||||||
std::string text[3];
|
std::string text[3];
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
|
std::vector<ImguiTexture> challenges;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Notification notifier;
|
extern Notification notifier;
|
||||||
|
|
||||||
|
void achievementList();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,11 +25,9 @@
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
#include "stdclass.h"
|
#include "stdclass.h"
|
||||||
#include "oslib/storage.h"
|
#include "oslib/storage.h"
|
||||||
|
#include "imgui_driver.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include "imgui_internal.h"
|
#include "imgui_internal.h"
|
||||||
#define STBI_ONLY_JPEG
|
|
||||||
#define STBI_ONLY_PNG
|
|
||||||
#include <stb_image.h>
|
|
||||||
|
|
||||||
static std::string select_current_directory = "**home**";
|
static std::string select_current_directory = "**home**";
|
||||||
static std::vector<hostfs::FileInfo> subfolders;
|
static std::vector<hostfs::FileInfo> subfolders;
|
||||||
|
@ -697,17 +695,58 @@ void windowDragScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u8 *loadImage(const std::string& path, int& width, int& height)
|
ImTextureID ImguiTexture::getId() const
|
||||||
{
|
{
|
||||||
FILE *file = nowide::fopen(path.c_str(), "rb");
|
if (path.empty())
|
||||||
if (file == nullptr)
|
return {};
|
||||||
return nullptr;
|
return imguiDriver->getOrLoadTexture(path);
|
||||||
|
}
|
||||||
|
|
||||||
int channels;
|
static void setUV(float ar, ImVec2& uv0, ImVec2& uv1)
|
||||||
stbi_set_flip_vertically_on_load(0);
|
{
|
||||||
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
|
uv0 = { 0.f, 0.f };
|
||||||
std::fclose(file);
|
uv1 = { 1.f, 1.f };
|
||||||
return imgData;
|
if (ar > 1)
|
||||||
|
{
|
||||||
|
uv0.y = -(ar - 1) / 2;
|
||||||
|
uv1.y = 1 + (ar - 1) / 2;
|
||||||
|
}
|
||||||
|
else if (ar != 0)
|
||||||
|
{
|
||||||
|
ar = 1 / ar;
|
||||||
|
uv0.x = -(ar - 1) / 2;
|
||||||
|
uv1.x = 1 + (ar - 1) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImguiTexture::draw(const ImVec2& size, const ImVec4& tint_col, const ImVec4& border_col) const
|
||||||
|
{
|
||||||
|
ImTextureID id = getId();
|
||||||
|
if (id == ImTextureID{})
|
||||||
|
ImGui::Dummy(size);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float ar = imguiDriver->getAspectRatio(id);
|
||||||
|
ImVec2 uv0, uv1;
|
||||||
|
setUV(ar, uv0, uv1);
|
||||||
|
ImGui::Image(id, size, uv0, uv1, tint_col, border_col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImguiTexture::button(const char* str_id, const ImVec2& image_size, const std::string& title,
|
||||||
|
const ImVec4& bg_col, const ImVec4& tint_col) const
|
||||||
|
{
|
||||||
|
ImTextureID id = getId();
|
||||||
|
if (id == ImTextureID{})
|
||||||
|
return ImGui::Button(title.c_str(), image_size);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float ar = imguiDriver->getAspectRatio(id);
|
||||||
|
ImVec2 uv0, uv1;
|
||||||
|
setUV(ar, uv0, uv1);
|
||||||
|
ImVec2 size = image_size - ImGui::GetStyle().FramePadding * 2;
|
||||||
|
return ImGui::ImageButton(str_id, id, size, uv0, uv1, bg_col, tint_col);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom version of ImGui::BeginListBox that allows passing window flags
|
// Custom version of ImGui::BeginListBox that allows passing window flags
|
||||||
|
|
|
@ -59,16 +59,6 @@ static inline void centerNextWindow()
|
||||||
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool operator==(const ImVec2& l, const ImVec2& r)
|
|
||||||
{
|
|
||||||
return l.x == r.x && l.y == r.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool operator!=(const ImVec2& l, const ImVec2& r)
|
|
||||||
{
|
|
||||||
return !(l == r);
|
|
||||||
}
|
|
||||||
|
|
||||||
void fullScreenWindow(bool modal);
|
void fullScreenWindow(bool modal);
|
||||||
void windowDragScroll();
|
void windowDragScroll();
|
||||||
|
|
||||||
|
@ -130,20 +120,6 @@ struct ScaledVec2 : public ImVec2
|
||||||
inline static ImVec2 min(const ImVec2& l, const ImVec2& r) {
|
inline static ImVec2 min(const ImVec2& l, const ImVec2& r) {
|
||||||
return ImVec2(std::min(l.x, r.x), std::min(l.y, r.y));
|
return ImVec2(std::min(l.x, r.x), std::min(l.y, r.y));
|
||||||
}
|
}
|
||||||
inline static ImVec2 operator+(const ImVec2& l, const ImVec2& r) {
|
|
||||||
return ImVec2(l.x + r.x, l.y + r.y);
|
|
||||||
}
|
|
||||||
inline static ImVec2 operator-(const ImVec2& l, const ImVec2& r) {
|
|
||||||
return ImVec2(l.x - r.x, l.y - r.y);
|
|
||||||
}
|
|
||||||
inline static ImVec2 operator*(const ImVec2& v, float f) {
|
|
||||||
return ImVec2(v.x * f, v.y * f);
|
|
||||||
}
|
|
||||||
inline static ImVec2 operator/(const ImVec2& v, float f) {
|
|
||||||
return ImVec2(v.x / f, v.y / f);
|
|
||||||
}
|
|
||||||
|
|
||||||
u8 *loadImage(const std::string& path, int& width, int& height);
|
|
||||||
|
|
||||||
class DisabledScope
|
class DisabledScope
|
||||||
{
|
{
|
||||||
|
@ -173,3 +149,69 @@ private:
|
||||||
};
|
};
|
||||||
|
|
||||||
bool BeginListBox(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiWindowFlags windowFlags = 0);
|
bool BeginListBox(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiWindowFlags windowFlags = 0);
|
||||||
|
|
||||||
|
class ImguiID
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImguiID(const std::string& id)
|
||||||
|
: ImguiID(id.c_str()) {}
|
||||||
|
ImguiID(const char *id) {
|
||||||
|
ImGui::PushID(id);
|
||||||
|
}
|
||||||
|
~ImguiID() {
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImguiStyleVar
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImguiStyleVar(ImGuiStyleVar idx, const ImVec2& val) {
|
||||||
|
ImGui::PushStyleVar(idx, val);
|
||||||
|
}
|
||||||
|
ImguiStyleVar(ImGuiStyleVar idx, float val) {
|
||||||
|
ImGui::PushStyleVar(idx, val);
|
||||||
|
}
|
||||||
|
~ImguiStyleVar() {
|
||||||
|
ImGui::PopStyleVar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImguiStyleColor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImguiStyleColor(ImGuiCol idx, const ImVec4& col) {
|
||||||
|
ImGui::PushStyleColor(idx, col);
|
||||||
|
}
|
||||||
|
ImguiStyleColor(ImGuiCol idx, ImU32 col) {
|
||||||
|
ImGui::PushStyleColor(idx, col);
|
||||||
|
}
|
||||||
|
~ImguiStyleColor() {
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImguiTexture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ImguiTexture() = default;
|
||||||
|
ImguiTexture(const std::string& path) : path(path) {}
|
||||||
|
|
||||||
|
void draw(const ImVec2& size, const ImVec4& tint_col = ImVec4(1, 1, 1, 1),
|
||||||
|
const ImVec4& border_col = ImVec4(0, 0, 0, 0)) const;
|
||||||
|
bool button(const char* str_id, const ImVec2& image_size, const std::string& title = {}, const ImVec4& bg_col = ImVec4(0, 0, 0, 0),
|
||||||
|
const ImVec4& tint_col = ImVec4(1, 1, 1, 1)) const;
|
||||||
|
|
||||||
|
ImTextureID getId() const;
|
||||||
|
|
||||||
|
operator ImTextureID() {
|
||||||
|
return getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const ImguiTexture& other) const {
|
||||||
|
return other.path == path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
|
@ -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 "imgui_driver.h"
|
||||||
|
#define STBI_ONLY_JPEG
|
||||||
|
#define STBI_ONLY_PNG
|
||||||
|
#include <stb_image.h>
|
||||||
|
|
||||||
|
static u8 *loadImage(const std::string& path, int& width, int& height)
|
||||||
|
{
|
||||||
|
FILE *file = nowide::fopen(path.c_str(), "rb");
|
||||||
|
if (file == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
int channels;
|
||||||
|
stbi_set_flip_vertically_on_load(0);
|
||||||
|
u8 *imgData = stbi_load_from_file(file, &width, &height, &channels, STBI_rgb_alpha);
|
||||||
|
std::fclose(file);
|
||||||
|
return imgData;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImTextureID ImGuiDriver::getOrLoadTexture(const std::string& path)
|
||||||
|
{
|
||||||
|
ImTextureID id = getTexture(path);
|
||||||
|
if (id == ImTextureID() && textureLoadCount < 10)
|
||||||
|
{
|
||||||
|
textureLoadCount++;
|
||||||
|
int width, height;
|
||||||
|
u8 *imgData = loadImage(path, width, height);
|
||||||
|
if (imgData != nullptr)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
id = updateTextureAndAspectRatio(path, imgData, width, height);
|
||||||
|
} catch (...) {
|
||||||
|
// vulkan can throw during resizing
|
||||||
|
}
|
||||||
|
free(imgData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
|
@ -36,20 +36,13 @@ public:
|
||||||
virtual void displayVmus() {}
|
virtual void displayVmus() {}
|
||||||
virtual void displayCrosshairs() {}
|
virtual void displayCrosshairs() {}
|
||||||
|
|
||||||
virtual void present() = 0;
|
void doPresent() {
|
||||||
virtual void setFrameRendered() {}
|
textureLoadCount = 0;
|
||||||
|
present();
|
||||||
virtual ImTextureID getTexture(const std::string& name) = 0;
|
|
||||||
virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) = 0;
|
|
||||||
|
|
||||||
ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height)
|
|
||||||
{
|
|
||||||
ImTextureID textureId = updateTexture(name, data, width, height);
|
|
||||||
if (textureId != ImTextureID())
|
|
||||||
aspectRatios[textureId] = (float)width / height;
|
|
||||||
return textureId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void setFrameRendered() {}
|
||||||
|
|
||||||
float getAspectRatio(ImTextureID textureId) {
|
float getAspectRatio(ImTextureID textureId) {
|
||||||
auto it = aspectRatios.find(textureId);
|
auto it = aspectRatios.find(textureId);
|
||||||
if (it != aspectRatios.end())
|
if (it != aspectRatios.end())
|
||||||
|
@ -58,8 +51,25 @@ public:
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImTextureID getOrLoadTexture(const std::string& path);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ImTextureID getTexture(const std::string& name) = 0;
|
||||||
|
virtual ImTextureID updateTexture(const std::string& name, const u8 *data, int width, int height) = 0;
|
||||||
|
virtual void present() = 0;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ImTextureID updateTextureAndAspectRatio(const std::string& name, const u8 *data, int width, int height)
|
||||||
|
{
|
||||||
|
textureLoadCount++;
|
||||||
|
ImTextureID textureId = updateTexture(name, data, width, height);
|
||||||
|
if (textureId != ImTextureID())
|
||||||
|
aspectRatios[textureId] = (float)width / height;
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
|
||||||
std::unordered_map<ImTextureID, float> aspectRatios;
|
std::unordered_map<ImTextureID, float> aspectRatios;
|
||||||
|
int textureLoadCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern std::unique_ptr<ImGuiDriver> imguiDriver;
|
extern std::unique_ptr<ImGuiDriver> imguiDriver;
|
||||||
|
|
|
@ -96,7 +96,7 @@ void mainui_loop()
|
||||||
if (imguiDriver == nullptr)
|
if (imguiDriver == nullptr)
|
||||||
forceReinit = true;
|
forceReinit = true;
|
||||||
else
|
else
|
||||||
imguiDriver->present();
|
imguiDriver->doPresent();
|
||||||
|
|
||||||
if (config::RendererType != currentRenderer || forceReinit)
|
if (config::RendererType != currentRenderer || forceReinit)
|
||||||
{
|
{
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue