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)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE elf)
|
||||
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_sources(${PROJECT_NAME} PRIVATE
|
||||
core/deps/imgui/imgui.cpp
|
||||
|
@ -1015,7 +1015,8 @@ cmrc_add_resources(flycast-resources
|
|||
if(NOT LIBRETRO)
|
||||
cmrc_add_resources(flycast-resources
|
||||
fonts/Roboto-Medium.ttf.zip
|
||||
fonts/Roboto-Regular.ttf.zip)
|
||||
fonts/Roboto-Regular.ttf.zip
|
||||
fonts/fa-solid-900.ttf.zip)
|
||||
if(ANDROID)
|
||||
cmrc_add_resources(flycast-resources
|
||||
WHENCE resources
|
||||
|
@ -1292,6 +1293,7 @@ target_sources(${PROJECT_NAME} PRIVATE
|
|||
if(NOT LIBRETRO)
|
||||
target_sources(${PROJECT_NAME} PRIVATE
|
||||
core/rend/game_scanner.h
|
||||
core/rend/imgui_driver.cpp
|
||||
core/rend/imgui_driver.h
|
||||
core/rend/gui.cpp
|
||||
core/rend/gui.h
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include <unordered_map>
|
||||
#include <sstream>
|
||||
#include <atomic>
|
||||
#include <tuple>
|
||||
#include <xxhash.h>
|
||||
|
||||
namespace achievements
|
||||
|
@ -51,6 +52,9 @@ public:
|
|||
std::future<void> login(const char *username, const char *password);
|
||||
void logout();
|
||||
bool isLoggedOn() const { return loggedOn; }
|
||||
bool isActive() const { return active; }
|
||||
Game getCurrentGame();
|
||||
std::vector<Achievement> getAchievementList();
|
||||
void serialize(Serializer& ser);
|
||||
void deserialize(Deserializer& deser);
|
||||
|
||||
|
@ -66,6 +70,7 @@ private:
|
|||
void resumeGame();
|
||||
void loadCache();
|
||||
std::string getOrDownloadImage(const char *url);
|
||||
std::tuple<std::string, bool> getCachedImage(const char *url);
|
||||
void diskChange();
|
||||
|
||||
static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata);
|
||||
|
@ -96,6 +101,8 @@ private:
|
|||
cResetEvent resetEvent;
|
||||
std::string cachePath;
|
||||
std::unordered_map<u64, std::string> cacheMap;
|
||||
std::mutex cacheMutex;
|
||||
std::future<void> asyncImageDownload;
|
||||
};
|
||||
|
||||
bool init()
|
||||
|
@ -123,6 +130,21 @@ bool 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)
|
||||
{
|
||||
Achievements::Instance().serialize(ser);
|
||||
|
@ -224,39 +246,63 @@ void Achievements::loadCache()
|
|||
continue;
|
||||
std::string s = get_file_basename(name);
|
||||
u64 v = strtoull(s.c_str(), nullptr, 16);
|
||||
std::lock_guard<std::mutex> _(cacheMutex);
|
||||
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)
|
||||
{
|
||||
u64 hash = XXH64(url, strlen(url), 13);
|
||||
u64 hash = hashUrl(url);
|
||||
{
|
||||
std::lock_guard<std::mutex> _(cacheMutex);
|
||||
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");
|
||||
stream << std::hex << hash << ".png";
|
||||
std::string localPath = cachePath + stream.str();
|
||||
FILE *f = nowide::fopen(localPath.c_str(), "wb");
|
||||
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 {};
|
||||
}
|
||||
fwrite(content.data(), 1, content.size(), f);
|
||||
fclose(f);
|
||||
{
|
||||
std::lock_guard<std::mutex> _(cacheMutex);
|
||||
cacheMap[hash] = stream.str();
|
||||
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, path.c_str());
|
||||
return path;
|
||||
}
|
||||
DEBUG_LOG(COMMON, "RA: downloaded %s to %s", url, localPath.c_str());
|
||||
return localPath;
|
||||
}
|
||||
|
||||
void Achievements::term()
|
||||
|
@ -264,6 +310,8 @@ void Achievements::term()
|
|||
if (rc_client == nullptr)
|
||||
return;
|
||||
unloadGame();
|
||||
if (asyncImageDownload.valid())
|
||||
asyncImageDownload.get();
|
||||
rc_client_destroy(rc_client);
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
|
@ -551,6 +610,7 @@ void Achievements::handleUpdateAchievementProgress(const rc_client_event_t *even
|
|||
notifier.notify(Notification::Progress, image, event->achievement->measured_progress);
|
||||
}
|
||||
|
||||
static Disc *hashDisk;
|
||||
static bool add150;
|
||||
|
||||
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);
|
||||
if (track == RC_HASH_CDTRACK_FIRST_DATA)
|
||||
{
|
||||
u32 toc[102];
|
||||
libGDR_GetToc(toc, SingleDensity);
|
||||
for (int i = 0; i < 99; i++)
|
||||
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))
|
||||
for (const Track& track : hashDisk->tracks)
|
||||
if (track.isDataTrack())
|
||||
return const_cast<Track *>(&track);
|
||||
return nullptr;
|
||||
}
|
||||
if (track <= hashDisk->tracks.size())
|
||||
return const_cast<Track *>(&hashDisk->tracks[track - 1]);
|
||||
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)
|
||||
{
|
||||
//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
|
||||
// FIXME get rid of this
|
||||
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;
|
||||
u32 count = requested_bytes;
|
||||
u32 secNum = count / 2048;
|
||||
if (secNum > 0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
hashDisk->ReadSectors(sector, 1, locbuf, 2048);
|
||||
requested_bytes = std::min<size_t>(requested_bytes, 2048);
|
||||
memcpy(buffer, locbuf, 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)
|
||||
{
|
||||
u32 trackNum = reinterpret_cast<uintptr_t>(track_handle);
|
||||
u32 start, end;
|
||||
if (!libGDR_GetTrack(trackNum, start, end))
|
||||
return 0;
|
||||
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %d -> %d", trackNum, start);
|
||||
return start;
|
||||
Track& track = *static_cast<Track *>(track_handle);
|
||||
DEBUG_LOG(COMMON, "RA: cdreader_first_track_sector track %p -> %d", track_handle, track.StartFAD);
|
||||
return track.StartFAD;
|
||||
}
|
||||
|
||||
std::string Achievements::getGameHash()
|
||||
|
@ -629,7 +663,13 @@ std::string Achievements::getGameHash()
|
|||
{
|
||||
const u32 diskType = libGDR_GetDiscType();
|
||||
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;
|
||||
rc_hash_cdreader hooks = {
|
||||
cdreader_open_track,
|
||||
|
@ -641,7 +681,7 @@ std::string Achievements::getGameHash()
|
|||
rc_hash_init_error_message_callback([](const char *msg) {
|
||||
WARN_LOG(COMMON, "cdreader: %s", msg);
|
||||
});
|
||||
#ifndef NDEBUG
|
||||
#if !defined(NDEBUG) || defined(DEBUGFAST)
|
||||
rc_hash_init_verbose_message_callback([](const char *msg) {
|
||||
DEBUG_LOG(COMMON, "cdreader: %s", msg);
|
||||
});
|
||||
|
@ -650,6 +690,8 @@ std::string Achievements::getGameHash()
|
|||
char hash[33] {};
|
||||
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
|
||||
delete hashDisk;
|
||||
hashDisk = nullptr;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
@ -724,7 +766,7 @@ void Achievements::loadGame()
|
|||
}
|
||||
if (!init() || !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;
|
||||
return;
|
||||
}
|
||||
|
@ -735,6 +777,10 @@ void Achievements::loadGame()
|
|||
((Achievements *)userdata)->gameLoaded(result, error_message);
|
||||
}, this);
|
||||
}
|
||||
else {
|
||||
INFO_LOG(COMMON, "RA: empty hash. Aborting load");
|
||||
loadingGame = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Achievements::gameLoaded(int result, const char *errorMessage)
|
||||
|
@ -811,6 +857,93 @@ void Achievements::diskChange()
|
|||
}, 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)
|
||||
{
|
||||
u32 size = (u32)rc_client_progress_size(rc_client);
|
||||
|
|
|
@ -17,16 +17,42 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
#include <future>
|
||||
#include <vector>
|
||||
|
||||
namespace achievements
|
||||
{
|
||||
#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();
|
||||
void term();
|
||||
std::future<void> login(const char *username, const char *password);
|
||||
void logout();
|
||||
bool isLoggedOn();
|
||||
bool isActive();
|
||||
Game getCurrentGame();
|
||||
std::vector<Achievement> getAchievementList();
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ void dc_exit();
|
|||
void dc_savestate(int index = 0);
|
||||
void dc_loadstate(int index = 0);
|
||||
void dc_loadstate(Deserializer& deser);
|
||||
std::string dc_getStateUpdateDate(int index);
|
||||
|
||||
enum class Event {
|
||||
Start,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "log/LogManager.h"
|
||||
#include "rend/gui.h"
|
||||
#include "oslib/oslib.h"
|
||||
#include "oslib/directory.h"
|
||||
#include "debug/gdb_server.h"
|
||||
#include "archive/rzip.h"
|
||||
#include "rend/mainui.h"
|
||||
|
@ -14,6 +15,7 @@
|
|||
#include "lua/lua.h"
|
||||
#include "stdclass.h"
|
||||
#include "serialize.h"
|
||||
#include <time.h>
|
||||
|
||||
int flycast_init(int argc, char* argv[])
|
||||
{
|
||||
|
@ -231,4 +233,32 @@ void dc_loadstate(int index)
|
|||
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
|
||||
|
|
|
@ -184,6 +184,7 @@ public:
|
|||
#ifndef _WIN32
|
||||
struct stat st;
|
||||
if (flycast::stat(path.c_str(), &st) != 0) {
|
||||
if (errno != ENOENT)
|
||||
INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno);
|
||||
throw StorageException("Cannot stat " + path);
|
||||
}
|
||||
|
|
|
@ -284,7 +284,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi
|
|||
FILE *f = nowide::fopen((const char *)context, "wb");
|
||||
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
|
||||
{
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -48,6 +48,7 @@
|
|||
#include "oslib/resources.h"
|
||||
#include "achievements/achievements.h"
|
||||
#include "gui_achievements.h"
|
||||
#include "IconsFontAwesome6.h"
|
||||
#if defined(USE_SDL)
|
||||
#include "sdl/sdl.h"
|
||||
#endif
|
||||
|
@ -309,6 +310,12 @@ void gui_initFonts()
|
|||
|
||||
// TODO Linux, iOS, ...
|
||||
#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
|
||||
data = resource::load("fonts/Roboto-Regular.ttf", dataSize);
|
||||
verify(data != nullptr);
|
||||
|
@ -575,103 +582,158 @@ static bool savestateAllowed()
|
|||
static void gui_display_commands()
|
||||
{
|
||||
imguiDriver->displayVmus();
|
||||
fullScreenWindow(false);
|
||||
ImGui::SetNextWindowBgAlpha(0.8f);
|
||||
ImguiStyleVar _{ImGuiStyleVar_WindowBorderSize, 0};
|
||||
|
||||
centerNextWindow();
|
||||
ImGui::SetNextWindowSize(ScaledVec2(330, 0));
|
||||
|
||||
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize);
|
||||
|
||||
ImGui::Begin("##commands", NULL, ImGuiWindowFlags_NoDecoration);
|
||||
{
|
||||
if (card_reader::barcodeAvailable())
|
||||
ImguiStyleVar _{ImGuiStyleVar_ButtonTextAlign, ImVec2(0.f, 0.5f)}; // left aligned
|
||||
|
||||
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)
|
||||
{
|
||||
char cardBuf[64] {};
|
||||
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
||||
if (ImGui::InputText("Card", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
||||
card_reader::barcodeSetCard(cardBuf);
|
||||
// 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));
|
||||
|
||||
DisabledScope scope(!savestateAllowed());
|
||||
|
||||
// Load State
|
||||
if (ImGui::Button("Load State", ScaledVec2(110, 50)) && savestateAllowed())
|
||||
if (!lowH)
|
||||
{
|
||||
gui_setState(GuiState::Closed);
|
||||
dc_loadstate(config::SavestateSlot);
|
||||
}
|
||||
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();
|
||||
|
||||
// Slot #
|
||||
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::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();
|
||||
if (ImGui::Button("Resume", ScaledVec2(150, 50)))
|
||||
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};
|
||||
|
||||
ImGui::NextColumn();
|
||||
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 ? "Insert Disk" : "Eject Disk";
|
||||
if (ImGui::Button(disk_label, ScaledVec2(150, 50)))
|
||||
{
|
||||
if (libGDR_GetDiscType() == Open)
|
||||
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
|
||||
{
|
||||
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);
|
||||
|
||||
// Settings
|
||||
if (ImGui::Button(ICON_FA_GEAR " Settings", ScaledVec2(150, buttonHeight)))
|
||||
gui_setState(GuiState::Settings);
|
||||
// Exit
|
||||
if (ImGui::Button(commandLineStart ? "Exit" : "Close Game", ScaledVec2(300, 50)
|
||||
+ ImVec2(ImGui::GetStyle().ColumnsMinSpacing + ImGui::GetStyle().FramePadding.x * 2 - 1, 0)))
|
||||
{
|
||||
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] {};
|
||||
strncpy(cardBuf, card_reader::barcodeGetCard().c_str(), sizeof(cardBuf) - 1);
|
||||
if (ImGui::InputText("##barcode", cardBuf, sizeof(cardBuf), ImGuiInputTextFlags_None, nullptr, nullptr))
|
||||
card_reader::barcodeSetCard(cardBuf);
|
||||
}
|
||||
|
||||
ImGui::Columns(1, nullptr, false);
|
||||
}
|
||||
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{};
|
||||
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);
|
||||
bool pressed = texture.button("", size, gameName);
|
||||
gameTooltip(tooltip);
|
||||
|
||||
return pressed;
|
||||
|
@ -3036,22 +3058,16 @@ static void gui_display_content()
|
|||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ScaledVec2(8, 20));
|
||||
|
||||
int counter = 0;
|
||||
int loadedImages = 0;
|
||||
if (gui_state != GuiState::SelectDisk && filter.PassFilter("Dreamcast BIOS"))
|
||||
{
|
||||
ImGui::PushID("bios");
|
||||
bool pressed;
|
||||
if (config::BoxartDisplayMode)
|
||||
{
|
||||
ImTextureID textureId{};
|
||||
GameMedia game;
|
||||
GameBoxart art = boxart.getBoxartAndLoad(game);
|
||||
if (getGameImage(art, textureId, loadedImages < 10))
|
||||
loadedImages++;
|
||||
if (textureId != ImTextureID())
|
||||
pressed = gameImageButton(textureId, "Dreamcast BIOS", responsiveBoxVec2);
|
||||
else
|
||||
pressed = ImGui::Button("Dreamcast BIOS", responsiveBoxVec2);
|
||||
ImguiTexture tex(art.boxartPath);
|
||||
pressed = gameImageButton(tex, "Dreamcast BIOS", responsiveBoxVec2, "Dreamcast BIOS");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3084,23 +3100,19 @@ static void gui_display_content()
|
|||
if (filter.PassFilter(gameName.c_str()))
|
||||
{
|
||||
ImGui::PushID(game.path.c_str());
|
||||
bool pressed;
|
||||
bool pressed = false;
|
||||
if (config::BoxartDisplayMode)
|
||||
{
|
||||
if (counter % itemsPerLine != 0)
|
||||
ImGui::SameLine();
|
||||
counter++;
|
||||
ImTextureID textureId{};
|
||||
// Get the boxart texture. Load it if needed (max 10 per frame).
|
||||
if (getGameImage(art, textureId, loadedImages < 10))
|
||||
loadedImages++;
|
||||
if (textureId != ImTextureID())
|
||||
pressed = gameImageButton(textureId, game.name, responsiveBoxVec2);
|
||||
else
|
||||
// Put the image inside a child window so we can detect when it's fully clipped and doesn't need to be loaded
|
||||
if (ImGui::BeginChild("img", ImVec2(0, 0), ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_None))
|
||||
{
|
||||
pressed = ImGui::Button(gameName.c_str(), responsiveBoxVec2);
|
||||
gameTooltip(game.name);
|
||||
ImguiTexture tex(art.boxartPath);
|
||||
pressed = gameImageButton(tex, game.name, responsiveBoxVec2, gameName);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3381,6 +3393,9 @@ void gui_display_ui()
|
|||
case GuiState::Cheats:
|
||||
gui_cheats();
|
||||
break;
|
||||
case GuiState::Achievements:
|
||||
achievements::achievementList();
|
||||
break;
|
||||
default:
|
||||
die("Unknown UI state");
|
||||
break;
|
||||
|
|
|
@ -63,7 +63,8 @@ enum class GuiState {
|
|||
SelectDisk,
|
||||
Loading,
|
||||
NetworkStart,
|
||||
Cheats
|
||||
Cheats,
|
||||
Achievements,
|
||||
};
|
||||
extern GuiState gui_state;
|
||||
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
#include "gui_util.h"
|
||||
#include "imgui_driver.h"
|
||||
#include "stdclass.h"
|
||||
#include "achievements/achievements.h"
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include <cmath>
|
||||
#include <sstream>
|
||||
|
||||
extern ImFont *largeFont;
|
||||
|
||||
|
@ -33,6 +36,7 @@ Notification notifier;
|
|||
static constexpr u64 DISPLAY_TIME = 5000;
|
||||
static constexpr u64 START_ANIM_TIME = 500;
|
||||
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,
|
||||
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
|
||||
startTime = now;
|
||||
endTime = 0x1000000000000; // never
|
||||
endTime = NEVER_ENDS;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -61,24 +65,60 @@ void Notification::notify(Type type, const std::string& image, const std::string
|
|||
endTime = startTime + DISPLAY_TIME;
|
||||
}
|
||||
this->type = type;
|
||||
this->imagePath = image;
|
||||
this->imageId = {};
|
||||
this->image = { image };
|
||||
text[0] = text1;
|
||||
text[1] = text2;
|
||||
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()
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
if (type == None)
|
||||
return false;
|
||||
u64 now = getTimeMs();
|
||||
if (now > endTime + END_ANIM_TIME) {
|
||||
if (now > endTime + END_ANIM_TIME)
|
||||
{
|
||||
if (!challenges.empty())
|
||||
{
|
||||
// Show current challenge indicators
|
||||
type = Challenge;
|
||||
startTime = getTimeMs();
|
||||
endTime = NEVER_ENDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide notification
|
||||
type = None;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (now > endTime)
|
||||
{
|
||||
// Fade out
|
||||
|
@ -89,20 +129,32 @@ bool Notification::draw()
|
|||
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
|
||||
if (type == Challenge)
|
||||
{
|
||||
ImGui::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
|
||||
| ImGuiWindowFlags_NoInputs);
|
||||
for (const auto& img : challenges)
|
||||
{
|
||||
img.draw(ScaledVec2(60.f, 60.f));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
else
|
||||
{
|
||||
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{});
|
||||
ImguiStyleVar _(ImGuiStyleVar_WindowPadding, ImVec2{});
|
||||
|
||||
ImGui::Begin("##achievements", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
|
||||
ImGui::Begin("##achievement", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav
|
||||
| ImGuiWindowFlags_NoInputs);
|
||||
ImTextureID imageId = image.getId();
|
||||
const bool hasPic = imageId != ImTextureID{};
|
||||
if (ImGui::BeginTable("achievementNotif", hasPic ? 2 : 1, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings))
|
||||
{
|
||||
|
@ -114,7 +166,7 @@ bool Notification::draw()
|
|||
ImGui::TableSetColumnIndex(0);
|
||||
if (hasPic)
|
||||
{
|
||||
ImGui::Image(imageId, ScaledVec2(80.f, 80.f), { 0.f, 0.f }, { 1.f, 1.f });
|
||||
image.draw(ScaledVec2(80.f, 80.f));
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
}
|
||||
|
||||
|
@ -122,7 +174,7 @@ bool Notification::draw()
|
|||
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 });
|
||||
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);
|
||||
|
@ -134,38 +186,91 @@ bool Notification::draw()
|
|||
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()
|
||||
void achievementList()
|
||||
{
|
||||
if (imagePath.empty())
|
||||
return;
|
||||
fullScreenWindow(false);
|
||||
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;
|
||||
u8 *imgData = loadImage(imagePath, width, height);
|
||||
if (imgData != nullptr)
|
||||
float w = ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().ItemSpacing.x * 2 - ImGui::GetStyle().WindowPadding.x
|
||||
- (80.f + 20.f * 2) * settings.display.uiScale; // image width and button frame padding
|
||||
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))
|
||||
{
|
||||
try {
|
||||
imageId = imguiDriver->updateTextureAndAspectRatio(imagePath, imgData, width, height);
|
||||
} catch (...) {
|
||||
// vulkan can throw during resizing
|
||||
std::vector<Achievement> achList = getAchievementList();
|
||||
int id = 0;
|
||||
std::string category;
|
||||
for (const auto& ach : achList)
|
||||
{
|
||||
if (ach.category != category)
|
||||
{
|
||||
category = ach.category;
|
||||
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
|
||||
|
|
|
@ -18,7 +18,9 @@
|
|||
*/
|
||||
#include "types.h"
|
||||
#include "imgui.h"
|
||||
#include "gui_util.h"
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace achievements
|
||||
{
|
||||
|
@ -33,24 +35,27 @@ public:
|
|||
Unlocked,
|
||||
Progress,
|
||||
Mastery,
|
||||
Challenge,
|
||||
Error
|
||||
};
|
||||
void notify(Type type, const std::string& image, const std::string& text1,
|
||||
const std::string& text2 = {}, const std::string& text3 = {});
|
||||
void showChallenge(const std::string& image);
|
||||
void hideChallenge(const std::string& image);
|
||||
bool draw();
|
||||
|
||||
private:
|
||||
void getImage();
|
||||
|
||||
u64 startTime = 0;
|
||||
u64 endTime = 0;
|
||||
Type type = Type::None;
|
||||
std::string imagePath;
|
||||
ImTextureID imageId {};
|
||||
ImguiTexture image;
|
||||
std::string text[3];
|
||||
std::mutex mutex;
|
||||
std::vector<ImguiTexture> challenges;
|
||||
};
|
||||
|
||||
extern Notification notifier;
|
||||
|
||||
void achievementList();
|
||||
|
||||
}
|
||||
|
|
|
@ -25,11 +25,9 @@
|
|||
#include "types.h"
|
||||
#include "stdclass.h"
|
||||
#include "oslib/storage.h"
|
||||
#include "imgui_driver.h"
|
||||
#include "imgui.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::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 (file == nullptr)
|
||||
return nullptr;
|
||||
if (path.empty())
|
||||
return {};
|
||||
return imguiDriver->getOrLoadTexture(path);
|
||||
}
|
||||
|
||||
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;
|
||||
static void setUV(float ar, ImVec2& uv0, ImVec2& uv1)
|
||||
{
|
||||
uv0 = { 0.f, 0.f };
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -59,16 +59,6 @@ static inline void centerNextWindow()
|
|||
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 windowDragScroll();
|
||||
|
||||
|
@ -130,20 +120,6 @@ struct ScaledVec2 : public ImVec2
|
|||
inline static ImVec2 min(const ImVec2& l, const ImVec2& r) {
|
||||
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
|
||||
{
|
||||
|
@ -173,3 +149,69 @@ private:
|
|||
};
|
||||
|
||||
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 displayCrosshairs() {}
|
||||
|
||||
virtual void present() = 0;
|
||||
virtual void setFrameRendered() {}
|
||||
|
||||
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;
|
||||
void doPresent() {
|
||||
textureLoadCount = 0;
|
||||
present();
|
||||
}
|
||||
|
||||
virtual void setFrameRendered() {}
|
||||
|
||||
float getAspectRatio(ImTextureID textureId) {
|
||||
auto it = aspectRatios.find(textureId);
|
||||
if (it != aspectRatios.end())
|
||||
|
@ -58,8 +51,25 @@ public:
|
|||
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:
|
||||
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;
|
||||
int textureLoadCount = 0;
|
||||
};
|
||||
|
||||
extern std::unique_ptr<ImGuiDriver> imguiDriver;
|
||||
|
|
|
@ -96,7 +96,7 @@ void mainui_loop()
|
|||
if (imguiDriver == nullptr)
|
||||
forceReinit = true;
|
||||
else
|
||||
imguiDriver->present();
|
||||
imguiDriver->doPresent();
|
||||
|
||||
if (config::RendererType != currentRenderer || forceReinit)
|
||||
{
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue