GameList: Show achievement information in game list
This commit is contained in:
parent
6512ed8a8c
commit
0e3668a7bb
|
@ -0,0 +1,13 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.48 122.88">
|
||||
<!-- https://uxwing.com/wp-content/themes/uxwing/download/sport-and-awards/trophy-icon.png -->
|
||||
<defs>
|
||||
<style>.cls-1{fill:#808080;}.cls-2{fill:#A0A0A0;}.cls-3{fill:#222;}.cls-4{fill:#C0C0C0;}.cls-5{fill:#909090;}.cls-6{fill:#B0B0B0;}.cls-7,.cls-8{fill:#FFF;}.cls-8{fill-rule:evenodd;}</style>
|
||||
</defs>
|
||||
<title>trophy</title>
|
||||
<path class="cls-1" d="M3.21,18.74H19.86q0-4,.06-8.26V0H93.05V10.49c0,2.92,0,5.66,0,8.26h17.24a3.08,3.08,0,0,1,3.07,2.93,77.67,77.67,0,0,1-.4,13.9A34,34,0,0,1,109.11,48a21.77,21.77,0,0,1-8.8,8.6A31.91,31.91,0,0,1,86.41,60C83.14,65.43,78.78,68,73.68,72.67c-6.17,4.71-10.81,8.26-7.2,19.13h5.39a7.84,7.84,0,0,1,7.82,7.82v3.15h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v5.23a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77v-5.23A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26h.77V99.62a7.75,7.75,0,0,1,2.3-5.51v0a7.81,7.81,0,0,1,5.51-2.29h6.06c3.22-10.26-1-13.58-6.83-18.17A44.47,44.47,0,0,1,27.34,60,31.87,31.87,0,0,1,13,56.54a21.47,21.47,0,0,1-8.73-8.6A34.07,34.07,0,0,1,.51,35.58,78.1,78.1,0,0,1,.13,21.9v-.08a3.08,3.08,0,0,1,3.09-3.08ZM92.71,30a121.67,121.67,0,0,1-2,18,15.17,15.17,0,0,0,5-1.9,10.49,10.49,0,0,0,3.69-3.89,18,18,0,0,0,1.93-6,45.37,45.37,0,0,0,.5-6.25H92.71ZM20.12,30H12a49.78,49.78,0,0,0,.45,6.27,18.41,18.41,0,0,0,1.8,6,10.13,10.13,0,0,0,3.57,3.88A14.57,14.57,0,0,0,22.54,48a92,92,0,0,1-2.42-18Z"/>
|
||||
<path class="cls-2" d="M20.08,21.82H3.21C2.75,31.1,3.34,40,7,46.43c3.43,6.11,9.7,10.15,20.62,10.46a27.9,27.9,0,0,1-3.48-5.51c-6.56-.88-10.36-3.59-12.54-7.67S8.94,34.32,8.94,28.14a1.23,1.23,0,0,1,1.23-1.23h9.91V21.82Zm72.77,5.1h10.82a1.23,1.23,0,0,1,1.23,1.22c0,6.2-.56,11.54-2.84,15.6s-6.16,6.75-12.7,7.64a25.38,25.38,0,0,1-3.69,5.52c11-.29,17.29-4.33,20.77-10.45,3.67-6.47,4.29-15.34,3.84-24.62H92.85v5.09Z"/>
|
||||
<path class="cls-3" d="M79.69,102.76h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v10.63a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77V110.48A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26H79.69Z"/>
|
||||
<path class="cls-4" d="M70.64,108H35.72a4.22,4.22,0,0,0-3,1.25h0a4.26,4.26,0,0,0-1.25,3v5.28H81.55v-5.28a4.26,4.26,0,0,0-1.26-3,4.31,4.31,0,0,0-3-1.26Z"/>
|
||||
<path class="cls-5" d="M50.71,93h6V75.21c-22.17-7.88-24.26-35-29.55-72.57H22.53V29.41C23,39.6,24.68,47.14,27,52.91a38.19,38.19,0,0,0,8.39,12.8,68.65,68.65,0,0,0,6.71,5.78C49.11,77,54.19,81,50.71,93Z"/>
|
||||
<path class="cls-6" d="M56.71,93H63c-3.88-12.71,1.68-17,9-22.55,8.05-6.14,18.5-14.12,18.5-40.35V2.64H27.16C30.58,26.92,32.66,46.81,39.67,60A39.14,39.14,0,0,0,49,71.13a29.3,29.3,0,0,0,5.47,3.17,19.1,19.1,0,0,0,2.21.74v.15l.07,0V93Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,16 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.48 122.88">
|
||||
<!-- https://uxwing.com/wp-content/themes/uxwing/download/sport-and-awards/trophy-icon.png -->
|
||||
<defs>
|
||||
<style>.cls-1{fill:#f39d00;}.cls-2{fill:#f9c809;}.cls-3{fill:#222;}.cls-4{fill:#ead79e;}.cls-5{fill:#f8b705;}.cls-6{fill:#fac809;}.cls-7,.cls-8{fill:#fff;}.cls-8{fill-rule:evenodd;}</style>
|
||||
</defs>
|
||||
<title>trophy</title>
|
||||
<path class="cls-1" d="M3.21,18.74H19.86q0-4,.06-8.26V0H93.05V10.49c0,2.92,0,5.66,0,8.26h17.24a3.08,3.08,0,0,1,3.07,2.93,77.67,77.67,0,0,1-.4,13.9A34,34,0,0,1,109.11,48a21.77,21.77,0,0,1-8.8,8.6A31.91,31.91,0,0,1,86.41,60C83.14,65.43,78.78,68,73.68,72.67c-6.17,4.71-10.81,8.26-7.2,19.13h5.39a7.84,7.84,0,0,1,7.82,7.82v3.15h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v5.23a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77v-5.23A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26h.77V99.62a7.75,7.75,0,0,1,2.3-5.51v0a7.81,7.81,0,0,1,5.51-2.29h6.06c3.22-10.26-1-13.58-6.83-18.17A44.47,44.47,0,0,1,27.34,60,31.87,31.87,0,0,1,13,56.54a21.47,21.47,0,0,1-8.73-8.6A34.07,34.07,0,0,1,.51,35.58,78.1,78.1,0,0,1,.13,21.9v-.08a3.08,3.08,0,0,1,3.09-3.08ZM92.71,30a121.67,121.67,0,0,1-2,18,15.17,15.17,0,0,0,5-1.9,10.49,10.49,0,0,0,3.69-3.89,18,18,0,0,0,1.93-6,45.37,45.37,0,0,0,.5-6.25H92.71ZM20.12,30H12a49.78,49.78,0,0,0,.45,6.27,18.41,18.41,0,0,0,1.8,6,10.13,10.13,0,0,0,3.57,3.88A14.57,14.57,0,0,0,22.54,48a92,92,0,0,1-2.42-18Z"/>
|
||||
<path class="cls-2" d="M20.08,21.82H3.21C2.75,31.1,3.34,40,7,46.43c3.43,6.11,9.7,10.15,20.62,10.46a27.9,27.9,0,0,1-3.48-5.51c-6.56-.88-10.36-3.59-12.54-7.67S8.94,34.32,8.94,28.14a1.23,1.23,0,0,1,1.23-1.23h9.91V21.82Zm72.77,5.1h10.82a1.23,1.23,0,0,1,1.23,1.22c0,6.2-.56,11.54-2.84,15.6s-6.16,6.75-12.7,7.64a25.38,25.38,0,0,1-3.69,5.52c11-.29,17.29-4.33,20.77-10.45,3.67-6.47,4.29-15.34,3.84-24.62H92.85v5.09Z"/>
|
||||
<path class="cls-3" d="M79.69,102.76h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v10.63a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77V110.48A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26H79.69Z"/>
|
||||
<path class="cls-4" d="M70.64,108H35.72a4.22,4.22,0,0,0-3,1.25h0a4.26,4.26,0,0,0-1.25,3v5.28H81.55v-5.28a4.26,4.26,0,0,0-1.26-3,4.31,4.31,0,0,0-3-1.26Z"/>
|
||||
<path class="cls-5" d="M50.71,93h6V75.21c-22.17-7.88-24.26-35-29.55-72.57H22.53V29.41C23,39.6,24.68,47.14,27,52.91a38.19,38.19,0,0,0,8.39,12.8,68.65,68.65,0,0,0,6.71,5.78C49.11,77,54.19,81,50.71,93Z"/>
|
||||
<path class="cls-6" d="M56.71,93H63c-3.88-12.71,1.68-17,9-22.55,8.05-6.14,18.5-14.12,18.5-40.35V2.64H27.16C30.58,26.92,32.66,46.81,39.67,60A39.14,39.14,0,0,0,49,71.13a29.3,29.3,0,0,0,5.47,3.17,19.1,19.1,0,0,0,2.21.74v.15l.07,0V93Z"/>
|
||||
<path class="cls-1" d="M58.26,20.13,61.06,27l7.39.56a1.9,1.9,0,0,1,1,3.41l-5.59,4.74,1.76,7.18a1.9,1.9,0,0,1-1.41,2.29,1.88,1.88,0,0,1-1.49-.26L56.5,41l-6.29,3.89a1.9,1.9,0,0,1-2.62-.62,1.85,1.85,0,0,1-.23-1.44l1.75-7.18-5.66-4.8a1.91,1.91,0,0,1,1.09-3.35L51.93,27l2.81-6.84a1.91,1.91,0,0,1,3.52,0Z"/>
|
||||
<polygon class="cls-7" points="56.5 20.86 59.75 28.78 68.31 29.43 61.76 34.98 63.79 43.3 56.5 38.79 49.21 43.3 51.24 34.98 44.69 29.43 53.24 28.78 56.5 20.86 56.5 20.86 56.5 20.86"/>
|
||||
<path class="cls-8" d="M76.62,47.62l-.07.1a3.79,3.79,0,0,0-5.17.83l-.1-.08a3.52,3.52,0,0,0,.62-2.75,3.57,3.57,0,0,0-1.44-2.42,26.79,26.79,0,0,0,2.82.53,3.58,3.58,0,0,0,2.42-1.45l.1.07a3.81,3.81,0,0,0,.82,5.17ZM84.27,34.8l-.07.1a3.78,3.78,0,0,0-5.17.82l-.1-.07a3.79,3.79,0,0,0-.83-5.17l.07-.1a3.8,3.8,0,0,0,5.18-.83l.09.08a3.79,3.79,0,0,0,.83,5.17Zm.06-13.56-.13.18a6.94,6.94,0,0,0-9.46,1.51l-.18-.13a6.5,6.5,0,0,0,1.14-5,6.49,6.49,0,0,0-2.65-4.43l.13-.18a6.94,6.94,0,0,0,9.46-1.51l.18.13a6.5,6.5,0,0,0-1.14,5,6.51,6.51,0,0,0,2.65,4.43Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1,13 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.48 122.88">
|
||||
<!-- https://uxwing.com/wp-content/themes/uxwing/download/sport-and-awards/trophy-icon.png -->
|
||||
<defs>
|
||||
<style>.cls-1{fill:#f39d00;}.cls-2{fill:#f9c809;}.cls-3{fill:#222;}.cls-4{fill:#ead79e;}.cls-5{fill:#f8b705;}.cls-6{fill:#fac809;}.cls-7,.cls-8{fill:#fff;}.cls-8{fill-rule:evenodd;}</style>
|
||||
</defs>
|
||||
<title>trophy</title>
|
||||
<path class="cls-1" d="M3.21,18.74H19.86q0-4,.06-8.26V0H93.05V10.49c0,2.92,0,5.66,0,8.26h17.24a3.08,3.08,0,0,1,3.07,2.93,77.67,77.67,0,0,1-.4,13.9A34,34,0,0,1,109.11,48a21.77,21.77,0,0,1-8.8,8.6A31.91,31.91,0,0,1,86.41,60C83.14,65.43,78.78,68,73.68,72.67c-6.17,4.71-10.81,8.26-7.2,19.13h5.39a7.84,7.84,0,0,1,7.82,7.82v3.15h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v5.23a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77v-5.23A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26h.77V99.62a7.75,7.75,0,0,1,2.3-5.51v0a7.81,7.81,0,0,1,5.51-2.29h6.06c3.22-10.26-1-13.58-6.83-18.17A44.47,44.47,0,0,1,27.34,60,31.87,31.87,0,0,1,13,56.54a21.47,21.47,0,0,1-8.73-8.6A34.07,34.07,0,0,1,.51,35.58,78.1,78.1,0,0,1,.13,21.9v-.08a3.08,3.08,0,0,1,3.09-3.08ZM92.71,30a121.67,121.67,0,0,1-2,18,15.17,15.17,0,0,0,5-1.9,10.49,10.49,0,0,0,3.69-3.89,18,18,0,0,0,1.93-6,45.37,45.37,0,0,0,.5-6.25H92.71ZM20.12,30H12a49.78,49.78,0,0,0,.45,6.27,18.41,18.41,0,0,0,1.8,6,10.13,10.13,0,0,0,3.57,3.88A14.57,14.57,0,0,0,22.54,48a92,92,0,0,1-2.42-18Z"/>
|
||||
<path class="cls-2" d="M20.08,21.82H3.21C2.75,31.1,3.34,40,7,46.43c3.43,6.11,9.7,10.15,20.62,10.46a27.9,27.9,0,0,1-3.48-5.51c-6.56-.88-10.36-3.59-12.54-7.67S8.94,34.32,8.94,28.14a1.23,1.23,0,0,1,1.23-1.23h9.91V21.82Zm72.77,5.1h10.82a1.23,1.23,0,0,1,1.23,1.22c0,6.2-.56,11.54-2.84,15.6s-6.16,6.75-12.7,7.64a25.38,25.38,0,0,1-3.69,5.52c11-.29,17.29-4.33,20.77-10.45,3.67-6.47,4.29-15.34,3.84-24.62H92.85v5.09Z"/>
|
||||
<path class="cls-3" d="M79.69,102.76h.77A7.69,7.69,0,0,1,85.91,105h0a7.67,7.67,0,0,1,2.26,5.45v10.63a1.77,1.77,0,0,1-1.77,1.77H26.58a1.77,1.77,0,0,1-1.77-1.77V110.48A7.66,7.66,0,0,1,27.07,105h0a7.66,7.66,0,0,1,5.44-2.26H79.69Z"/>
|
||||
<path class="cls-4" d="M70.64,108H35.72a4.22,4.22,0,0,0-3,1.25h0a4.26,4.26,0,0,0-1.25,3v5.28H81.55v-5.28a4.26,4.26,0,0,0-1.26-3,4.31,4.31,0,0,0-3-1.26Z"/>
|
||||
<path class="cls-5" d="M50.71,93h6V75.21c-22.17-7.88-24.26-35-29.55-72.57H22.53V29.41C23,39.6,24.68,47.14,27,52.91a38.19,38.19,0,0,0,8.39,12.8,68.65,68.65,0,0,0,6.71,5.78C49.11,77,54.19,81,50.71,93Z"/>
|
||||
<path class="cls-6" d="M56.71,93H63c-3.88-12.71,1.68-17,9-22.55,8.05-6.14,18.5-14.12,18.5-40.35V2.64H27.16C30.58,26.92,32.66,46.81,39.67,60A39.14,39.14,0,0,0,49,71.13a29.3,29.3,0,0,0,5.47,3.17,19.1,19.1,0,0,0,2.21.74v.15l.07,0V93Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -9,6 +9,7 @@
|
|||
#include "bus.h"
|
||||
#include "cpu_core.h"
|
||||
#include "fullscreen_ui.h"
|
||||
#include "game_list.h"
|
||||
#include "gpu_thread.h"
|
||||
#include "host.h"
|
||||
#include "imgui_overlays.h"
|
||||
|
@ -2209,13 +2210,12 @@ void Achievements::Logout()
|
|||
rc_client_logout(s_state.client);
|
||||
}
|
||||
|
||||
ClearProgressDatabase();
|
||||
|
||||
INFO_LOG("Clearing credentials...");
|
||||
Host::DeleteBaseSettingValue("Cheevos", "Username");
|
||||
Host::DeleteBaseSettingValue("Cheevos", "Token");
|
||||
Host::DeleteBaseSettingValue("Cheevos", "LoginTimestamp");
|
||||
Host::CommitBaseSettingChanges();
|
||||
ClearProgressDatabase();
|
||||
}
|
||||
|
||||
bool Achievements::ConfirmSystemReset()
|
||||
|
@ -3836,6 +3836,9 @@ void Achievements::FinishRefreshHashDatabase()
|
|||
s_state.fetch_all_progress_result = nullptr;
|
||||
rc_client_destroy_hash_library(s_state.fetch_hash_library_result);
|
||||
s_state.fetch_hash_library_result = nullptr;
|
||||
|
||||
// update game list, we might have some new games that weren't in the seed database
|
||||
GameList::UpdateAllAchievementData();
|
||||
}
|
||||
|
||||
void Achievements::BuildHashDatabase(const rc_client_hash_library_t* hashlib,
|
||||
|
@ -4204,8 +4207,6 @@ void Achievements::BuildProgressDatabase(const rc_client_all_progress_list_t* al
|
|||
|
||||
if (!writer.Flush(&error))
|
||||
ERROR_LOG("Failed to write progress database: {}", error.GetDescription());
|
||||
|
||||
// TODO: Notify game list
|
||||
}
|
||||
|
||||
void Achievements::UpdateProgressDatabase(bool force)
|
||||
|
@ -4214,7 +4215,13 @@ void Achievements::UpdateProgressDatabase(bool force)
|
|||
if (rc_client_get_spectator_mode_enabled(s_state.client))
|
||||
return;
|
||||
|
||||
// TODO: Update in game list
|
||||
// update the game list, this should be fairly quick
|
||||
if (!s_state.game_hash.has_value())
|
||||
{
|
||||
GameList::UpdateAchievementData(s_state.game_hash.value(), s_state.game_id,
|
||||
s_state.game_summary.num_core_achievements,
|
||||
s_state.game_summary.num_unlocked_achievements, IsHardcoreModeActive());
|
||||
}
|
||||
|
||||
// done asynchronously so we don't hitch on disk I/O
|
||||
System::QueueAsyncTask([game_id = s_state.game_id,
|
||||
|
@ -4339,6 +4346,8 @@ void Achievements::ClearProgressDatabase()
|
|||
if (!FileSystem::DeleteFile(path.c_str(), &error))
|
||||
ERROR_LOG("Failed to delete progress database: {}", error.GetDescription());
|
||||
}
|
||||
|
||||
GameList::UpdateAllAchievementData();
|
||||
}
|
||||
|
||||
Achievements::ProgressDatabase::ProgressDatabase() = default;
|
||||
|
|
|
@ -117,6 +117,7 @@ using ImGuiFullscreen::IsFocusResetFromWindowChange;
|
|||
using ImGuiFullscreen::IsFocusResetQueued;
|
||||
using ImGuiFullscreen::IsGamepadInputSource;
|
||||
using ImGuiFullscreen::LayoutScale;
|
||||
using ImGuiFullscreen::LayoutUnscale;
|
||||
using ImGuiFullscreen::LoadTexture;
|
||||
using ImGuiFullscreen::MenuButton;
|
||||
using ImGuiFullscreen::MenuButtonFrame;
|
||||
|
@ -442,6 +443,7 @@ static void SwitchToGameList();
|
|||
static void PopulateGameListEntryList();
|
||||
static GPUTexture* GetTextureForGameListEntryType(GameList::EntryType type);
|
||||
static GPUTexture* GetGameListCover(const GameList::Entry* entry, bool fallback_to_icon);
|
||||
static GPUTexture* GetGameListCoverTrophy(const GameList::Entry* entry, const ImVec2& image_size);
|
||||
static GPUTexture* GetCoverForCurrentGame();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -543,6 +545,7 @@ struct ALIGN_TO_CACHE_LINE UIState
|
|||
std::unordered_map<std::string, std::string> icon_image_map;
|
||||
std::vector<const GameList::Entry*> game_list_sorted_entries;
|
||||
GameListView game_list_view = GameListView::Grid;
|
||||
bool game_list_show_trophy_icons = true;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
@ -7478,6 +7481,28 @@ void FullscreenUI::PopulateGameListEntryList()
|
|||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 8: // Achievements
|
||||
{
|
||||
// sort by unlock percentage
|
||||
const float unlock_lhs =
|
||||
(lhs->num_achievements > 0) ?
|
||||
(static_cast<float>(std::max(lhs->unlocked_achievements, lhs->unlocked_achievements_hc)) /
|
||||
static_cast<float>(lhs->num_achievements)) :
|
||||
0;
|
||||
const float unlock_rhs =
|
||||
(rhs->num_achievements > 0) ?
|
||||
(static_cast<float>(std::max(rhs->unlocked_achievements, rhs->unlocked_achievements_hc)) /
|
||||
static_cast<float>(rhs->num_achievements)) :
|
||||
0;
|
||||
if (std::abs(unlock_lhs - unlock_rhs) >= 0.0001f)
|
||||
return reverse ? (unlock_lhs >= unlock_rhs) : (unlock_lhs < unlock_rhs);
|
||||
|
||||
// order by achievement count
|
||||
if (lhs->num_achievements != rhs->num_achievements)
|
||||
return reverse ? (rhs->num_achievements < lhs->num_achievements) :
|
||||
(lhs->num_achievements < rhs->num_achievements);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to title when all else is equal
|
||||
|
@ -7752,6 +7777,21 @@ void FullscreenUI::DrawGameList(const ImVec2& heading_size)
|
|||
// release date
|
||||
ImGui::Text(FSUI_CSTR("Release Date: %s"), selected_entry->GetReleaseDateString().c_str());
|
||||
|
||||
// achievements
|
||||
if (selected_entry->num_achievements > 0)
|
||||
{
|
||||
if (selected_entry->unlocked_achievements_hc > 0)
|
||||
{
|
||||
ImGui::Text(FSUI_CSTR("Achievements: %u (%u) / %u"), selected_entry->unlocked_achievements,
|
||||
selected_entry->unlocked_achievements_hc, selected_entry->num_achievements);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text(FSUI_CSTR("Achievements: %u / %u"), selected_entry->unlocked_achievements,
|
||||
selected_entry->num_achievements);
|
||||
}
|
||||
}
|
||||
|
||||
// compatibility
|
||||
ImGui::TextUnformatted(FSUI_CSTR("Compatibility: "));
|
||||
ImGui::SameLine();
|
||||
|
@ -7877,6 +7917,15 @@ void FullscreenUI::DrawGameGrid(const ImVec2& heading_size)
|
|||
ImGui::GetWindowDrawList()->AddImage(cover_texture, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
|
||||
ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
|
||||
|
||||
GPUTexture* const cover_trophy = GetGameListCoverTrophy(entry, image_size);
|
||||
if (cover_trophy)
|
||||
{
|
||||
const ImVec2 trophy_size =
|
||||
ImVec2(static_cast<float>(cover_trophy->GetWidth()), static_cast<float>(cover_trophy->GetHeight()));
|
||||
ImGui::GetWindowDrawList()->AddImage(cover_trophy, image_rect.Max - trophy_size, image_rect.Max,
|
||||
ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
|
||||
}
|
||||
|
||||
const ImRect title_bb(ImVec2(bb.Min.x, bb.Min.y + image_height + title_spacing), bb.Max);
|
||||
const std::string_view title(
|
||||
std::string_view(entry->title).substr(0, (entry->title.length() > 31) ? 31 : std::string_view::npos));
|
||||
|
@ -8174,8 +8223,16 @@ void FullscreenUI::DrawGameListSettingsWindow()
|
|||
{
|
||||
static constexpr const char* view_types[] = {FSUI_NSTR("Game Grid"), FSUI_NSTR("Game List")};
|
||||
static constexpr const char* sort_types[] = {
|
||||
FSUI_NSTR("Type"), FSUI_NSTR("Serial"), FSUI_NSTR("Title"), FSUI_NSTR("File Title"),
|
||||
FSUI_NSTR("Time Played"), FSUI_NSTR("Last Played"), FSUI_NSTR("File Size"), FSUI_NSTR("Uncompressed Size")};
|
||||
FSUI_NSTR("Type"),
|
||||
FSUI_NSTR("Serial"),
|
||||
FSUI_NSTR("Title"),
|
||||
FSUI_NSTR("File Title"),
|
||||
FSUI_NSTR("Time Played"),
|
||||
FSUI_NSTR("Last Played"),
|
||||
FSUI_NSTR("File Size"),
|
||||
FSUI_NSTR("Uncompressed Size"),
|
||||
FSUI_NSTR("Achievement Unlock/Count"),
|
||||
};
|
||||
|
||||
DrawIntListSetting(bsi, FSUI_ICONSTR(ICON_FA_BORDER_ALL, "Default View"),
|
||||
FSUI_CSTR("Selects the view that the game list will open to."), "Main",
|
||||
|
@ -8190,6 +8247,13 @@ void FullscreenUI::DrawGameListSettingsWindow()
|
|||
DrawToggleSetting(bsi, FSUI_ICONSTR(ICON_FA_LIST, "Merge Multi-Disc Games"),
|
||||
FSUI_CSTR("Merges multi-disc games into one item in the game list."), "Main",
|
||||
"FullscreenUIMergeDiscSets", true);
|
||||
if (DrawToggleSetting(
|
||||
bsi, FSUI_ICONSTR(ICON_FA_TROPHY, "Show Achievement Trophy Icons"),
|
||||
FSUI_CSTR("Shows trophy icons in game grid when games have achievements or have been mastered."), "Main",
|
||||
"FullscreenUIShowTrophyIcons", true))
|
||||
{
|
||||
s_state.game_list_show_trophy_icons = bsi->GetBoolValue("Main", "FullscreenUIShowTrophyIcons", true);
|
||||
}
|
||||
}
|
||||
|
||||
MenuHeading(FSUI_CSTR("Cover Settings"));
|
||||
|
@ -8228,6 +8292,7 @@ void FullscreenUI::SwitchToGameList()
|
|||
s_state.current_main_window = MainWindowType::GameList;
|
||||
s_state.game_list_view =
|
||||
static_cast<GameListView>(Host::GetBaseIntSettingValue("Main", "DefaultFullscreenUIGameView", 0));
|
||||
s_state.game_list_show_trophy_icons = Host::GetBaseBoolSettingValue("Main", "FullscreenUIShowTrophyIcons", true);
|
||||
|
||||
// Wipe icon map, because a new save might give us an icon.
|
||||
for (const auto& it : s_state.icon_image_map)
|
||||
|
@ -8264,6 +8329,22 @@ GPUTexture* FullscreenUI::GetGameListCover(const GameList::Entry* entry, bool fa
|
|||
return tex ? tex : GetTextureForGameListEntryType(entry->type);
|
||||
}
|
||||
|
||||
GPUTexture* FullscreenUI::GetGameListCoverTrophy(const GameList::Entry* entry, const ImVec2& image_size)
|
||||
{
|
||||
if (!s_state.game_list_show_trophy_icons || entry->num_achievements == 0)
|
||||
return nullptr;
|
||||
|
||||
// this'll get re-scaled up, so undo layout scale
|
||||
const ImVec2 trophy_size = LayoutUnscale(image_size / 6.0f);
|
||||
|
||||
GPUTexture* texture =
|
||||
GetCachedTextureAsync(entry->AreAchievementsMastered() ? "images/trophy-icon-star.svg" : "images/trophy-icon.svg",
|
||||
static_cast<u32>(trophy_size.x), static_cast<u32>(trophy_size.y));
|
||||
|
||||
// don't draw the placeholder, it's way too large
|
||||
return (texture == GetPlaceholderTexture().get()) ? nullptr : texture;
|
||||
}
|
||||
|
||||
GPUTexture* FullscreenUI::GetTextureForGameListEntryType(GameList::EntryType type)
|
||||
{
|
||||
switch (type)
|
||||
|
@ -8679,9 +8760,12 @@ TRANSLATE_NOOP("FullscreenUI", "About DuckStation");
|
|||
TRANSLATE_NOOP("FullscreenUI", "Account");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Accurate Blending");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievement Notifications");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievement Unlock/Count");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievements");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievements Settings");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievements are not enabled.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievements: %u (%u) / %u");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Achievements: %u / %u");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Add Search Directory");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Add Shader");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Adds a new directory to the game search list.");
|
||||
|
@ -9201,6 +9285,7 @@ TRANSLATE_NOOP("FullscreenUI", "Settings");
|
|||
TRANSLATE_NOOP("FullscreenUI", "Settings and Operations");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shader {} added as stage {}.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shared Card Name");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Achievement Trophy Icons");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show CPU Usage");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Controller Input");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Enhancement Settings");
|
||||
|
@ -9228,6 +9313,7 @@ TRANSLATE_NOOP("FullscreenUI", "Shows the game you are currently playing as part
|
|||
TRANSLATE_NOOP("FullscreenUI", "Shows the host's CPU usage of each system thread in the top-right corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows the host's GPU usage in the top-right corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows the number of frames (or v-syncs) displayed per second by the system in the top-right corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows trophy icons in game grid when games have achievements or have been mastered.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Simulates the region check present in original, unmodified consoles.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Simulates the system ahead of time and rolls back/replays to reduce input lag. Very high system requirements.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Skip Duplicate Frame Display");
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "game_list.h"
|
||||
#include "achievements.h"
|
||||
#include "bios.h"
|
||||
#include "fullscreen_ui.h"
|
||||
#include "host.h"
|
||||
|
@ -50,7 +51,7 @@ namespace {
|
|||
enum : u32
|
||||
{
|
||||
GAME_LIST_CACHE_SIGNATURE = 0x45434C48,
|
||||
GAME_LIST_CACHE_VERSION = 36,
|
||||
GAME_LIST_CACHE_VERSION = 37,
|
||||
|
||||
PLAYED_TIME_SERIAL_LENGTH = 32,
|
||||
PLAYED_TIME_LAST_TIME_LENGTH = 20, // uint64
|
||||
|
@ -86,6 +87,8 @@ using PlayedTimeMap = PreferUnorderedStringMap<PlayedTimeEntry>;
|
|||
|
||||
static_assert(std::is_same_v<decltype(Entry::hash), GameHash>);
|
||||
|
||||
static bool ShouldLoadAchievementsProgress();
|
||||
|
||||
static bool GetExeListEntry(const std::string& path, Entry* entry);
|
||||
static bool GetPsfListEntry(const std::string& path, Entry* entry);
|
||||
static bool GetDiscListEntry(const std::string& path, Entry* entry);
|
||||
|
@ -93,18 +96,22 @@ static bool GetDiscListEntry(const std::string& path, Entry* entry);
|
|||
static void ApplyCustomAttributes(const std::string& path, Entry* entry,
|
||||
const INISettingsInterface& custom_attributes_ini);
|
||||
static bool RescanCustomAttributesForPath(const std::string& path, const INISettingsInterface& custom_attributes_ini);
|
||||
static void PopulateEntryAchievements(Entry* entry, const Achievements::ProgressDatabase& achievements_progress);
|
||||
static bool GetGameListEntryFromCache(const std::string& path, Entry* entry,
|
||||
const INISettingsInterface& custom_attributes_ini);
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress);
|
||||
static Entry* GetMutableEntryForPath(std::string_view path);
|
||||
static void ScanDirectory(const char* path, bool recursive, bool only_cache,
|
||||
const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
|
||||
const INISettingsInterface& custom_attributes_ini, BinaryFileWriter& cache_writer,
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress, BinaryFileWriter& cache_writer,
|
||||
ProgressCallback* progress);
|
||||
static bool AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map,
|
||||
const INISettingsInterface& custom_attributes_ini);
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress);
|
||||
static bool ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
|
||||
const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini,
|
||||
BinaryFileWriter& cache_writer);
|
||||
const Achievements::ProgressDatabase& achievements_progress, BinaryFileWriter& cache_writer);
|
||||
|
||||
static bool LoadOrInitializeCache(std::FILE* fp, bool invalidate_cache);
|
||||
static bool LoadEntriesFromCache(BinaryFileReader& reader);
|
||||
|
@ -173,6 +180,11 @@ bool GameList::IsScannableFilename(std::string_view path)
|
|||
return System::IsLoadablePath(path);
|
||||
}
|
||||
|
||||
bool GameList::ShouldLoadAchievementsProgress()
|
||||
{
|
||||
return Host::ContainsBaseSettingValue("Cheevos", "Token");
|
||||
}
|
||||
|
||||
bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry)
|
||||
{
|
||||
const auto fp = FileSystem::OpenManagedCFile(path.c_str(), "rb");
|
||||
|
@ -289,8 +301,16 @@ bool GameList::GetDiscListEntry(const std::string& path, Entry* entry)
|
|||
entry->uncompressed_size = static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
|
||||
entry->type = EntryType::Disc;
|
||||
|
||||
std::string id;
|
||||
System::GetGameDetailsFromImage(cdi.get(), &id, &entry->hash);
|
||||
// use the same buffer for game and achievement hashing, to avoid double decompression
|
||||
std::string id, executable_name;
|
||||
std::vector<u8> executable_data;
|
||||
if (System::GetGameDetailsFromImage(cdi.get(), &id, &entry->hash, &executable_name, &executable_data))
|
||||
{
|
||||
// used for achievement count lookup later
|
||||
const std::optional<Achievements::GameHash> hash = Achievements::GetGameHash(executable_name, executable_data);
|
||||
if (hash.has_value())
|
||||
entry->achievements_hash = hash.value();
|
||||
}
|
||||
|
||||
// try the database first
|
||||
const GameDatabase::Entry* dentry = GameDatabase::GetEntryForGameDetails(id, entry->hash);
|
||||
|
@ -359,7 +379,8 @@ bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry)
|
|||
}
|
||||
|
||||
bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry,
|
||||
const INISettingsInterface& custom_attributes_ini)
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress)
|
||||
{
|
||||
auto iter = s_cache_map.find(path);
|
||||
if (iter == s_cache_map.end())
|
||||
|
@ -369,6 +390,9 @@ bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry,
|
|||
entry->dbentry = GameDatabase::GetEntryForSerial(entry->serial);
|
||||
s_cache_map.erase(iter);
|
||||
ApplyCustomAttributes(path, entry, custom_attributes_ini);
|
||||
if (entry->IsDisc())
|
||||
PopulateEntryAchievements(entry, achievements_progress);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -395,6 +419,7 @@ bool GameList::LoadEntriesFromCache(BinaryFileReader& reader)
|
|||
!reader.ReadSizePrefixedString(&ge.disc_set_name) || !reader.ReadU64(&ge.hash) ||
|
||||
!reader.ReadS64(&ge.file_size) || !reader.ReadU64(&ge.uncompressed_size) ||
|
||||
!reader.ReadU64(reinterpret_cast<u64*>(&ge.last_modified_time)) || !reader.ReadS8(&ge.disc_set_index) ||
|
||||
!reader.Read(ge.achievements_hash.data(), ge.achievements_hash.size()) ||
|
||||
region >= static_cast<u8>(DiscRegion::Count) || type >= static_cast<u8>(EntryType::Count))
|
||||
{
|
||||
WARNING_LOG("Game list cache entry is corrupted");
|
||||
|
@ -428,6 +453,7 @@ bool GameList::WriteEntryToCache(const Entry* entry, BinaryFileWriter& writer)
|
|||
writer.WriteU64(entry->uncompressed_size);
|
||||
writer.WriteU64(entry->last_modified_time);
|
||||
writer.WriteS8(entry->disc_set_index);
|
||||
writer.Write(entry->achievements_hash.data(), entry->achievements_hash.size());
|
||||
return writer.IsGood();
|
||||
}
|
||||
|
||||
|
@ -471,8 +497,9 @@ static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const
|
|||
|
||||
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
||||
const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
|
||||
const INISettingsInterface& custom_attributes_ini, BinaryFileWriter& cache_writer,
|
||||
ProgressCallback* progress)
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress,
|
||||
BinaryFileWriter& cache_writer, ProgressCallback* progress)
|
||||
{
|
||||
INFO_LOG("Scanning {}{}", path, recursive ? " (recursively)" : "");
|
||||
|
||||
|
@ -503,14 +530,17 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
|||
|
||||
std::unique_lock lock(s_mutex);
|
||||
if (GetEntryForPath(ffd.FileName) ||
|
||||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map, custom_attributes_ini) || only_cache)
|
||||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map, custom_attributes_ini,
|
||||
achievements_progress) ||
|
||||
only_cache)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
progress->SetStatusText(SmallString::from_format(TRANSLATE_FS("GameList", "Scanning '{}'..."),
|
||||
FileSystem::GetDisplayNameFromPath(ffd.FileName)));
|
||||
ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map, custom_attributes_ini, cache_writer);
|
||||
ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map, custom_attributes_ini,
|
||||
achievements_progress, cache_writer);
|
||||
progress->SetProgressValue(files_scanned);
|
||||
}
|
||||
|
||||
|
@ -519,11 +549,15 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
|
|||
}
|
||||
|
||||
bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map,
|
||||
const INISettingsInterface& custom_attributes_ini)
|
||||
const INISettingsInterface& custom_attributes_ini,
|
||||
const Achievements::ProgressDatabase& achievements_progress)
|
||||
{
|
||||
Entry entry;
|
||||
if (!GetGameListEntryFromCache(path, &entry, custom_attributes_ini) || entry.last_modified_time != timestamp)
|
||||
if (!GetGameListEntryFromCache(path, &entry, custom_attributes_ini, achievements_progress) ||
|
||||
entry.last_modified_time != timestamp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto iter = played_time_map.find(entry.serial);
|
||||
if (iter != played_time_map.end())
|
||||
|
@ -538,7 +572,7 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp,
|
|||
|
||||
bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
|
||||
const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini,
|
||||
BinaryFileWriter& cache_writer)
|
||||
const Achievements::ProgressDatabase& achievements_progress, BinaryFileWriter& cache_writer)
|
||||
{
|
||||
// don't block UI while scanning
|
||||
lock.unlock();
|
||||
|
@ -567,6 +601,9 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc
|
|||
|
||||
ApplyCustomAttributes(entry.path, &entry, custom_attributes_ini);
|
||||
|
||||
if (entry.IsDisc())
|
||||
PopulateEntryAchievements(&entry, achievements_progress);
|
||||
|
||||
lock.lock();
|
||||
|
||||
// replace if present
|
||||
|
@ -667,6 +704,124 @@ void GameList::ApplyCustomAttributes(const std::string& path, Entry* entry,
|
|||
}
|
||||
}
|
||||
|
||||
void GameList::PopulateEntryAchievements(Entry* entry, const Achievements::ProgressDatabase& achievements_progress)
|
||||
{
|
||||
const Achievements::HashDatabaseEntry* hentry = Achievements::LookupGameHash(entry->achievements_hash);
|
||||
if (!hentry)
|
||||
return;
|
||||
|
||||
entry->achievements_game_id = hentry->game_id;
|
||||
entry->num_achievements = Truncate16(hentry->num_achievements);
|
||||
if (entry->num_achievements > 0)
|
||||
{
|
||||
const Achievements::ProgressDatabase::Entry* apd_entry = achievements_progress.LookupGame(hentry->game_id);
|
||||
if (apd_entry)
|
||||
{
|
||||
entry->unlocked_achievements = apd_entry->num_achievements_unlocked;
|
||||
entry->unlocked_achievements_hc = apd_entry->num_hc_achievements_unlocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameList::UpdateAchievementData(const std::span<u8, 16> hash, u32 game_id, u32 num_achievements, u32 num_unlocked,
|
||||
bool hardcore)
|
||||
{
|
||||
std::unique_lock<std::recursive_mutex> lock(s_mutex);
|
||||
llvm::SmallVector<u32, 32> changed_indices;
|
||||
|
||||
for (size_t i = 0; i < s_entries.size(); i++)
|
||||
{
|
||||
Entry& entry = s_entries[i];
|
||||
if (std::memcmp(entry.achievements_hash.data(), hash.data(), hash.size()) != 0 &&
|
||||
entry.achievements_game_id != game_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const u32 current_unlocked = hardcore ? entry.unlocked_achievements_hc : entry.unlocked_achievements;
|
||||
if (entry.achievements_game_id == game_id && entry.num_achievements == num_achievements &&
|
||||
current_unlocked == num_unlocked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.achievements_game_id = game_id;
|
||||
entry.num_achievements = Truncate16(num_achievements);
|
||||
if (hardcore)
|
||||
entry.unlocked_achievements_hc = Truncate16(num_achievements);
|
||||
else
|
||||
entry.unlocked_achievements = Truncate16(num_unlocked);
|
||||
|
||||
changed_indices.push_back(static_cast<u32>(i));
|
||||
}
|
||||
|
||||
if (!changed_indices.empty())
|
||||
Host::OnGameListEntriesChanged(changed_indices);
|
||||
}
|
||||
|
||||
void GameList::UpdateAllAchievementData()
|
||||
{
|
||||
Achievements::ProgressDatabase achievements_progress;
|
||||
if (ShouldLoadAchievementsProgress())
|
||||
{
|
||||
Error error;
|
||||
if (!achievements_progress.Load(&error))
|
||||
WARNING_LOG("Failed to load achievements progress: {}", error.GetDescription());
|
||||
}
|
||||
|
||||
std::unique_lock<std::recursive_mutex> lock(s_mutex);
|
||||
|
||||
// this is pretty jank, but the frontend should collapse it into a single update
|
||||
std::vector<u32> changed_indices;
|
||||
for (size_t i = 0; i < s_entries.size(); i++)
|
||||
{
|
||||
Entry& entry = s_entries[i];
|
||||
if (!entry.IsDisc())
|
||||
continue;
|
||||
|
||||
// Game ID is delibately not tested, because it has no effect on the UI.
|
||||
const u16 old_num_achievements = entry.num_achievements;
|
||||
const u16 old_unlocked_achievements = entry.unlocked_achievements;
|
||||
const u16 old_unlocked_achievements_hc = entry.unlocked_achievements_hc;
|
||||
PopulateEntryAchievements(&entry, achievements_progress);
|
||||
if (entry.num_achievements == old_num_achievements && entry.unlocked_achievements == old_unlocked_achievements &&
|
||||
entry.unlocked_achievements_hc == old_unlocked_achievements_hc)
|
||||
{
|
||||
// no update needed
|
||||
continue;
|
||||
}
|
||||
|
||||
changed_indices.push_back(static_cast<u32>(i));
|
||||
}
|
||||
|
||||
// and now the disc sets, messier :(
|
||||
for (size_t i = 0; i < s_entries.size(); i++)
|
||||
{
|
||||
Entry& entry = s_entries[i];
|
||||
if (!entry.IsDiscSet())
|
||||
continue;
|
||||
|
||||
const Entry* any_entry = GetEntryBySerial(entry.serial);
|
||||
if (!any_entry)
|
||||
continue;
|
||||
|
||||
if (entry.num_achievements != any_entry->num_achievements ||
|
||||
entry.unlocked_achievements != any_entry->unlocked_achievements ||
|
||||
entry.unlocked_achievements_hc != any_entry->unlocked_achievements_hc)
|
||||
{
|
||||
changed_indices.push_back(static_cast<u32>(i));
|
||||
}
|
||||
|
||||
entry.achievements_game_id = any_entry->achievements_game_id;
|
||||
entry.num_achievements = any_entry->num_achievements;
|
||||
entry.unlocked_achievements = any_entry->unlocked_achievements;
|
||||
entry.unlocked_achievements_hc = any_entry->unlocked_achievements_hc;
|
||||
}
|
||||
|
||||
if (!changed_indices.empty())
|
||||
Host::OnGameListEntriesChanged(changed_indices);
|
||||
}
|
||||
|
||||
std::unique_lock<std::recursive_mutex> GameList::GetLock()
|
||||
{
|
||||
return std::unique_lock<std::recursive_mutex>(s_mutex);
|
||||
|
@ -814,6 +969,13 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
|
||||
custom_attributes_ini.Load();
|
||||
|
||||
Achievements::ProgressDatabase achievements_progress;
|
||||
if (ShouldLoadAchievementsProgress())
|
||||
{
|
||||
if (!achievements_progress.Load(&error))
|
||||
WARNING_LOG("Failed to load achievements progress: {}", error.GetDescription());
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
recursive_dirs.push_back(Path::Combine(EmuFolders::DataRoot, "games"));
|
||||
#endif
|
||||
|
@ -830,8 +992,8 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
if (progress->IsCancelled())
|
||||
break;
|
||||
|
||||
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, custom_attributes_ini, cache_writer,
|
||||
progress);
|
||||
ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, custom_attributes_ini,
|
||||
achievements_progress, cache_writer, progress);
|
||||
progress->SetProgressValue(++directory_counter);
|
||||
}
|
||||
for (const std::string& dir : recursive_dirs)
|
||||
|
@ -839,8 +1001,8 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
|
|||
if (progress->IsCancelled())
|
||||
break;
|
||||
|
||||
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, custom_attributes_ini, cache_writer,
|
||||
progress);
|
||||
ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, custom_attributes_ini,
|
||||
achievements_progress, cache_writer, progress);
|
||||
progress->SetProgressValue(++directory_counter);
|
||||
}
|
||||
}
|
||||
|
@ -912,6 +1074,10 @@ void GameList::CreateDiscSetEntries(const std::vector<std::string>& excluded_pat
|
|||
set_entry.last_modified_time = entry.last_modified_time;
|
||||
set_entry.last_played_time = 0;
|
||||
set_entry.total_played_time = 0;
|
||||
set_entry.achievements_hash = entry.achievements_hash;
|
||||
set_entry.num_achievements = entry.num_achievements;
|
||||
set_entry.unlocked_achievements = entry.unlocked_achievements;
|
||||
set_entry.unlocked_achievements_hc = entry.unlocked_achievements_hc;
|
||||
|
||||
// figure out play time for all discs, and sum it
|
||||
// we do this via lookups, rather than the other entries, because of duplicates
|
||||
|
|
|
@ -56,6 +56,12 @@ struct Entry
|
|||
std::time_t last_played_time = 0;
|
||||
std::time_t total_played_time = 0;
|
||||
|
||||
std::array<u8, 16> achievements_hash = {};
|
||||
u32 achievements_game_id = 0;
|
||||
u16 num_achievements = 0;
|
||||
u16 unlocked_achievements = 0;
|
||||
u16 unlocked_achievements_hc = 0;
|
||||
|
||||
std::string_view GetLanguageIcon() const;
|
||||
|
||||
TinyString GetLanguageIconName() const;
|
||||
|
@ -67,6 +73,12 @@ struct Entry
|
|||
ALWAYS_INLINE bool IsDiscSet() const { return (type == EntryType::DiscSet); }
|
||||
ALWAYS_INLINE bool HasCustomLanguage() const { return (custom_language != GameDatabase::Language::MaxCount); }
|
||||
ALWAYS_INLINE EntryType GetSortType() const { return (type == EntryType::DiscSet) ? EntryType::Disc : type; }
|
||||
ALWAYS_INLINE bool AreAchievementsMastered() const
|
||||
{
|
||||
return (num_achievements > 0 &&
|
||||
((unlocked_achievements > unlocked_achievements_hc) ? unlocked_achievements : unlocked_achievements_hc) ==
|
||||
num_achievements);
|
||||
}
|
||||
};
|
||||
|
||||
using EntryList = std::vector<Entry>;
|
||||
|
@ -142,6 +154,11 @@ std::optional<DiscRegion> GetCustomRegionForPath(const std::string_view path);
|
|||
std::string GetGameIconPath(std::string_view serial, std::string_view path);
|
||||
void ReloadMemcardTimestampCache();
|
||||
|
||||
/// Updates game list with new achievement unlocks.
|
||||
void UpdateAchievementData(const std::span<u8, 16> hash, u32 game_id, u32 num_achievements, u32 num_unlocked,
|
||||
bool hardcore);
|
||||
void UpdateAllAchievementData();
|
||||
|
||||
}; // namespace GameList
|
||||
|
||||
namespace Host {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
||||
{"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
|
||||
"Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
|
||||
"Last Played", "Size", "File Size", "Region", "Achievements", "Compatibility", "Cover"}};
|
||||
|
||||
static constexpr int COVER_ART_WIDTH = 512;
|
||||
static constexpr int COVER_ART_HEIGHT = 512;
|
||||
|
@ -371,8 +371,8 @@ const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) c
|
|||
|
||||
const QPixmap& GameListModel::getFlagPixmapForEntry(const GameList::Entry* ge) const
|
||||
{
|
||||
static constexpr u32 FLAG_PIXMAP_WIDTH = 42;
|
||||
static constexpr u32 FLAG_PIXMAP_HEIGHT = 30;
|
||||
static constexpr u32 FLAG_PIXMAP_WIDTH = 30;
|
||||
static constexpr u32 FLAG_PIXMAP_HEIGHT = 20;
|
||||
|
||||
const std::string_view name = ge->GetLanguageIcon();
|
||||
auto it = m_flag_pixmap_cache.find(name);
|
||||
|
@ -544,11 +544,14 @@ QVariant GameListModel::data(const QModelIndex& index, int role, const GameList:
|
|||
|
||||
case Column_FileSize:
|
||||
return (ge->file_size >= 0) ?
|
||||
QString("%1 MB").arg(static_cast<double>(ge->file_size) / 1048576.0, 0, 'f', 2) :
|
||||
QStringLiteral("%1 MB").arg(static_cast<double>(ge->file_size) / 1048576.0, 0, 'f', 2) :
|
||||
tr("Unknown");
|
||||
|
||||
case Column_UncompressedSize:
|
||||
return QString("%1 MB").arg(static_cast<double>(ge->uncompressed_size) / 1048576.0, 0, 'f', 2);
|
||||
return QStringLiteral("%1 MB").arg(static_cast<double>(ge->uncompressed_size) / 1048576.0, 0, 'f', 2);
|
||||
|
||||
case Column_Achievements:
|
||||
return {};
|
||||
|
||||
case Column_TimePlayed:
|
||||
{
|
||||
|
@ -829,6 +832,31 @@ bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry*
|
|||
return (left_players < right_players);
|
||||
}
|
||||
|
||||
case Column_Achievements:
|
||||
{
|
||||
// sort by unlock percentage
|
||||
const float unlock_left =
|
||||
(left->num_achievements > 0) ?
|
||||
(static_cast<float>(std::max(left->unlocked_achievements, left->unlocked_achievements_hc)) /
|
||||
static_cast<float>(left->num_achievements)) :
|
||||
0;
|
||||
const float unlock_right =
|
||||
(right->num_achievements > 0) ?
|
||||
(static_cast<float>(std::max(right->unlocked_achievements, right->unlocked_achievements_hc)) /
|
||||
static_cast<float>(right->num_achievements)) :
|
||||
0;
|
||||
if (std::abs(unlock_left - unlock_right) < 0.0001f)
|
||||
{
|
||||
// order by achievement count
|
||||
if (left->num_achievements == right->num_achievements)
|
||||
return titlesLessThan(left, right);
|
||||
|
||||
return (left->num_achievements < right->num_achievements);
|
||||
}
|
||||
|
||||
return (unlock_left < unlock_right);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -851,6 +879,15 @@ void GameListModel::loadCommonImages()
|
|||
}
|
||||
|
||||
m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()));
|
||||
|
||||
constexpr int ACHIEVEMENT_ICON_SIZE = 16;
|
||||
m_no_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon-gray.svg", true)))
|
||||
.pixmap(ACHIEVEMENT_ICON_SIZE);
|
||||
m_has_achievements_pixmap = QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon.svg", true)))
|
||||
.pixmap(ACHIEVEMENT_ICON_SIZE);
|
||||
m_mastered_achievements_pixmap =
|
||||
QIcon(QString::fromStdString(QtHost::GetResourcePath("images/trophy-icon-star.svg", true)))
|
||||
.pixmap(ACHIEVEMENT_ICON_SIZE);
|
||||
}
|
||||
|
||||
void GameListModel::setColumnDisplayNames()
|
||||
|
@ -864,6 +901,7 @@ void GameListModel::setColumnDisplayNames()
|
|||
m_column_display_names[Column_Genre] = tr("Genre");
|
||||
m_column_display_names[Column_Year] = tr("Year");
|
||||
m_column_display_names[Column_Players] = tr("Players");
|
||||
m_column_display_names[Column_Achievements] = tr("Achievements");
|
||||
m_column_display_names[Column_TimePlayed] = tr("Time Played");
|
||||
m_column_display_names[Column_LastPlayed] = tr("Last Played");
|
||||
m_column_display_names[Column_FileSize] = tr("Size");
|
||||
|
|
|
@ -39,6 +39,7 @@ public:
|
|||
Column_FileSize,
|
||||
Column_UncompressedSize,
|
||||
Column_Region,
|
||||
Column_Achievements,
|
||||
Column_Compatibility,
|
||||
Column_Cover,
|
||||
|
||||
|
@ -56,7 +57,10 @@ public:
|
|||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
ALWAYS_INLINE const QString& getColumnDisplayName(int column) { return m_column_display_names[column]; }
|
||||
ALWAYS_INLINE const QString& getColumnDisplayName(int column) const { return m_column_display_names[column]; }
|
||||
ALWAYS_INLINE const QPixmap& getNoAchievementsPixmap() const { return m_no_achievements_pixmap; }
|
||||
ALWAYS_INLINE const QPixmap& getHasAchievementsPixmap() const { return m_has_achievements_pixmap; }
|
||||
ALWAYS_INLINE const QPixmap& getMasteredAchievementsPixmap() const { return m_mastered_achievements_pixmap; }
|
||||
|
||||
bool hasTakenGameList() const;
|
||||
void takeGameList();
|
||||
|
@ -119,6 +123,10 @@ private:
|
|||
QImage m_placeholder_image;
|
||||
QPixmap m_loading_pixmap;
|
||||
|
||||
QPixmap m_no_achievements_pixmap;
|
||||
QPixmap m_has_achievements_pixmap;
|
||||
QPixmap m_mastered_achievements_pixmap;
|
||||
|
||||
mutable PreferUnorderedStringMap<QPixmap> m_flag_pixmap_cache;
|
||||
|
||||
mutable LRUCache<std::string, QPixmap> m_cover_pixmap_cache;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#include "gamelistwidget.h"
|
||||
|
@ -108,33 +108,106 @@ private:
|
|||
};
|
||||
|
||||
namespace {
|
||||
class GameListIconStyleDelegate final : public QStyledItemDelegate
|
||||
class GameListCenterIconStyleDelegate final : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
GameListIconStyleDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
|
||||
~GameListIconStyleDelegate() = default;
|
||||
GameListCenterIconStyleDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
|
||||
~GameListCenterIconStyleDelegate() = default;
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
||||
{
|
||||
// https://stackoverflow.com/questions/32216568/how-to-set-icon-center-in-qtableview
|
||||
Q_ASSERT(index.isValid());
|
||||
|
||||
// draw default item
|
||||
QStyleOptionViewItem opt = option;
|
||||
initStyleOption(&opt, index);
|
||||
opt.icon = QIcon();
|
||||
QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, 0);
|
||||
|
||||
const QRect r = option.rect;
|
||||
const QRect& r = option.rect;
|
||||
const QPixmap pix = qvariant_cast<QPixmap>(index.data(Qt::DecorationRole));
|
||||
const int pix_width = static_cast<int>(pix.width() / pix.devicePixelRatio());
|
||||
const int pix_height = static_cast<int>(pix.width() / pix.devicePixelRatio());
|
||||
const int pix_height = static_cast<int>(pix.height() / pix.devicePixelRatio());
|
||||
|
||||
// draw pixmap at center of item
|
||||
const QPoint p = QPoint((r.width() - pix_width) / 2, (r.height() - pix_height) / 2);
|
||||
painter->drawPixmap(r.topLeft() + p, pix);
|
||||
}
|
||||
};
|
||||
|
||||
class GameListAchievementsStyleDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
GameListAchievementsStyleDelegate(QWidget* parent, GameListModel* model, GameListSortModel* sort_model)
|
||||
: QStyledItemDelegate(parent), m_model(model), m_sort_model(sort_model)
|
||||
{
|
||||
}
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
||||
{
|
||||
Q_ASSERT(index.isValid());
|
||||
|
||||
u32 num_achievements = 0;
|
||||
u32 num_unlocked = 0;
|
||||
u32 num_unlocked_hardcore = 0;
|
||||
bool mastered = false;
|
||||
|
||||
{
|
||||
const QModelIndex source_index = m_sort_model->mapToSource(index);
|
||||
const auto lock = GameList::GetLock();
|
||||
const GameList::Entry* entry = GameList::GetEntryByIndex(static_cast<u32>(source_index.row()));
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
num_achievements = entry->num_achievements;
|
||||
num_unlocked = entry->unlocked_achievements;
|
||||
num_unlocked_hardcore = entry->unlocked_achievements_hc;
|
||||
mastered = entry->AreAchievementsMastered();
|
||||
}
|
||||
|
||||
QRect r = option.rect;
|
||||
|
||||
const QPixmap& icon = (num_achievements > 0) ? (mastered ? m_model->getMasteredAchievementsPixmap() :
|
||||
m_model->getHasAchievementsPixmap()) :
|
||||
m_model->getNoAchievementsPixmap();
|
||||
const int icon_height = static_cast<int>(icon.width() / icon.devicePixelRatio());
|
||||
painter->drawPixmap(r.topLeft() + QPoint(4, (r.height() - icon_height) / 2), icon);
|
||||
r.setLeft(r.left() + 4 + icon.width());
|
||||
|
||||
if (num_achievements > 0)
|
||||
{
|
||||
const QFontMetrics fm(painter->fontMetrics());
|
||||
|
||||
// display hardcore in parenthesis only if there are actually hc unlocks
|
||||
const bool display_hardcore = (num_unlocked > 0 && num_unlocked_hardcore > 0);
|
||||
const bool display_hardcore_only = (num_unlocked == 0 && num_unlocked_hardcore > 0);
|
||||
const QString first = QStringLiteral("%1").arg(display_hardcore_only ? num_unlocked_hardcore : num_unlocked);
|
||||
const QString total = QStringLiteral("/%3").arg(num_achievements);
|
||||
|
||||
const QPalette& palette = static_cast<QWidget*>(parent())->palette();
|
||||
const QColor hc_color = QColor(44, 151, 250);
|
||||
|
||||
painter->setPen(display_hardcore_only ? hc_color : palette.color(QPalette::WindowText));
|
||||
painter->drawText(r, Qt::AlignVCenter, first);
|
||||
r.setLeft(r.left() + fm.size(Qt::TextSingleLine, first).width());
|
||||
|
||||
if (display_hardcore)
|
||||
{
|
||||
const QString hc = QStringLiteral("(%2)").arg(num_unlocked_hardcore);
|
||||
painter->setPen(hc_color);
|
||||
painter->drawText(r, Qt::AlignVCenter, hc);
|
||||
r.setLeft(r.left() + fm.size(Qt::TextSingleLine, hc).width());
|
||||
}
|
||||
|
||||
painter->setPen(palette.color(QPalette::WindowText));
|
||||
painter->drawText(r, Qt::AlignVCenter, total);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter->drawText(r, Qt::AlignVCenter, QStringLiteral("N/A"));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
GameListModel* m_model;
|
||||
GameListSortModel* m_sort_model;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent)
|
||||
|
@ -185,6 +258,7 @@ void GameListWidget::initialize()
|
|||
[this](const QString& text) { m_sort_model->setFilterName(text); });
|
||||
connect(m_ui.searchText, &QLineEdit::returnPressed, this, &GameListWidget::onSearchReturnPressed);
|
||||
|
||||
GameListCenterIconStyleDelegate* center_icon_delegate = new GameListCenterIconStyleDelegate(this);
|
||||
m_table_view = new QTableView(m_ui.stack);
|
||||
m_table_view->setModel(m_sort_model);
|
||||
m_table_view->setSortingEnabled(true);
|
||||
|
@ -199,7 +273,10 @@ void GameListWidget::initialize()
|
|||
m_table_view->verticalHeader()->hide();
|
||||
m_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
|
||||
m_table_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
|
||||
m_table_view->setItemDelegateForColumn(0, new GameListIconStyleDelegate(this));
|
||||
m_table_view->setItemDelegateForColumn(GameListModel::Column_Icon, center_icon_delegate);
|
||||
m_table_view->setItemDelegateForColumn(GameListModel::Column_Region, center_icon_delegate);
|
||||
m_table_view->setItemDelegateForColumn(GameListModel::Column_Achievements,
|
||||
new GameListAchievementsStyleDelegate(this, m_model, m_sort_model));
|
||||
|
||||
loadTableViewColumnVisibilitySettings();
|
||||
loadTableViewColumnSortSettings();
|
||||
|
@ -625,6 +702,7 @@ void GameListWidget::resizeTableViewColumnsToFit()
|
|||
80, // file size
|
||||
80, // size
|
||||
50, // region
|
||||
90, // achievements
|
||||
100 // compatibility
|
||||
});
|
||||
}
|
||||
|
@ -651,7 +729,8 @@ void GameListWidget::loadTableViewColumnVisibilitySettings()
|
|||
true, // file size
|
||||
false, // size
|
||||
true, // region
|
||||
true // compatibility
|
||||
false, // achievements
|
||||
false // compatibility
|
||||
}};
|
||||
|
||||
for (int column = 0; column < GameListModel::Column_Count; column++)
|
||||
|
@ -710,6 +789,17 @@ void GameListWidget::saveTableViewColumnSortSettings()
|
|||
Host::CommitBaseSettingChanges();
|
||||
}
|
||||
|
||||
void GameListWidget::setTableViewColumnHidden(int column, bool hidden)
|
||||
{
|
||||
DebugAssert(column < GameListModel::Column_Count);
|
||||
if (m_table_view->isColumnHidden(column) == hidden)
|
||||
return;
|
||||
|
||||
m_table_view->setColumnHidden(column, hidden);
|
||||
saveTableViewColumnVisibilitySettings(column);
|
||||
resizeTableViewColumnsToFit();
|
||||
}
|
||||
|
||||
const GameList::Entry* GameListWidget::getSelectedEntry() const
|
||||
{
|
||||
if (m_ui.stack->currentIndex() == 0)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
||||
|
||||
#pragma once
|
||||
|
@ -43,6 +43,7 @@ public:
|
|||
|
||||
void initialize();
|
||||
void resizeTableViewColumnsToFit();
|
||||
void setTableViewColumnHidden(int column, bool hidden);
|
||||
|
||||
void refresh(bool invalidate_cache);
|
||||
void refreshModel();
|
||||
|
|
|
@ -2205,6 +2205,7 @@ void MainWindow::connectSignals()
|
|||
connect(g_emu_thread, &EmuThread::mouseModeRequested, this, &MainWindow::onMouseModeRequested);
|
||||
connect(g_emu_thread, &EmuThread::fullscreenUIStartedOrStopped, this, &MainWindow::onFullscreenUIStartedOrStopped);
|
||||
connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested);
|
||||
connect(g_emu_thread, &EmuThread::achievementsLoginSuccess, this, &MainWindow::onAchievementsLoginSuccess);
|
||||
connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this,
|
||||
&MainWindow::onAchievementsChallengeModeChanged);
|
||||
connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered);
|
||||
|
@ -2796,6 +2797,24 @@ void MainWindow::onAchievementsLoginRequested(Achievements::LoginRequestReason r
|
|||
dlg.exec();
|
||||
}
|
||||
|
||||
void MainWindow::onAchievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points,
|
||||
quint32 unread_messages)
|
||||
{
|
||||
m_ui.statusBar->showMessage(tr("RA: Logged in as %1 (%2, %3 softcore). %4 unread messages.")
|
||||
.arg(username)
|
||||
.arg(points)
|
||||
.arg(sc_points)
|
||||
.arg(unread_messages));
|
||||
|
||||
// Automatically show the achievements column after first login. If the user has manually hidden it,
|
||||
// it will not be automatically shown again.
|
||||
if (!Host::GetBaseBoolSettingValue("GameListTableView", "TriedShowingAchievementsColumn", false))
|
||||
{
|
||||
Host::SetBaseBoolSettingValue("GameListTableView", "TriedShowingAchievementsColumn", true);
|
||||
m_game_list_widget->setTableViewColumnHidden(GameListModel::Column_Achievements, false);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onAchievementsChallengeModeChanged(bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
|
|
|
@ -149,6 +149,7 @@ private Q_SLOTS:
|
|||
void onMediaCaptureStarted();
|
||||
void onMediaCaptureStopped();
|
||||
void onAchievementsLoginRequested(Achievements::LoginRequestReason reason);
|
||||
void onAchievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages);
|
||||
void onAchievementsChallengeModeChanged(bool enabled);
|
||||
bool onCreateAuxiliaryRenderWindow(RenderAPI render_api, qint32 x, qint32 y, quint32 width, quint32 height,
|
||||
const QString& title, const QString& icon_name,
|
||||
|
|
|
@ -1586,13 +1586,7 @@ void Host::OnAchievementsLoginRequested(Achievements::LoginRequestReason reason)
|
|||
|
||||
void Host::OnAchievementsLoginSuccess(const char* username, u32 points, u32 sc_points, u32 unread_messages)
|
||||
{
|
||||
const QString message = qApp->translate("QtHost", "RA: Logged in as %1 (%2, %3 softcore). %4 unread messages.")
|
||||
.arg(QString::fromUtf8(username))
|
||||
.arg(points)
|
||||
.arg(sc_points)
|
||||
.arg(unread_messages);
|
||||
|
||||
emit g_emu_thread->statusMessage(message);
|
||||
emit g_emu_thread->achievementsLoginSuccess(QString::fromUtf8(username), points, sc_points, unread_messages);
|
||||
}
|
||||
|
||||
void Host::OnAchievementsRefreshed()
|
||||
|
|
|
@ -152,6 +152,7 @@ Q_SIGNALS:
|
|||
void mouseModeRequested(bool relative, bool hide_cursor);
|
||||
void fullscreenUIStartedOrStopped(bool running);
|
||||
void achievementsLoginRequested(Achievements::LoginRequestReason reason);
|
||||
void achievementsLoginSuccess(const QString& username, quint32 points, quint32 sc_points, quint32 unread_messages);
|
||||
void achievementsRefreshed(quint32 id, const QString& game_info_string);
|
||||
void achievementsChallengeModeChanged(bool enabled);
|
||||
void cheatEnabled(quint32 index, bool enabled);
|
||||
|
|
|
@ -418,6 +418,39 @@ GPUTexture* ImGuiFullscreen::GetCachedTextureAsync(std::string_view name)
|
|||
return tex_ptr->get();
|
||||
}
|
||||
|
||||
GPUTexture* ImGuiFullscreen::GetCachedTextureAsync(std::string_view name, u32 svg_width, u32 svg_height)
|
||||
{
|
||||
// ignore size hints if it's not needed, don't duplicate
|
||||
if (!TextureNeedsSVGDimensions(name))
|
||||
return GetCachedTextureAsync(name);
|
||||
|
||||
svg_width = static_cast<u32>(std::ceil(LayoutScale(static_cast<float>(svg_width))));
|
||||
svg_height = static_cast<u32>(std::ceil(LayoutScale(static_cast<float>(svg_height))));
|
||||
|
||||
const SmallString wh_name = SmallString::from_format("{}#{}x{}", name, svg_width, svg_height);
|
||||
std::shared_ptr<GPUTexture>* tex_ptr = s_state.texture_cache.Lookup(wh_name.view());
|
||||
if (!tex_ptr)
|
||||
{
|
||||
// insert the placeholder
|
||||
tex_ptr = s_state.texture_cache.Insert(std::string(wh_name), s_state.placeholder_texture);
|
||||
|
||||
// queue the actual load
|
||||
System::QueueAsyncTask([path = std::string(name), wh_name = std::string(wh_name), svg_width, svg_height]() mutable {
|
||||
std::optional<Image> image(LoadTextureImage(path.c_str(), svg_width, svg_height));
|
||||
|
||||
// don't bother queuing back if it doesn't exist
|
||||
if (!image.has_value())
|
||||
return;
|
||||
|
||||
std::unique_lock lock(s_state.shared_state_mutex);
|
||||
if (s_state.initialized)
|
||||
s_state.texture_upload_queue.emplace_back(std::move(wh_name), std::move(image.value()));
|
||||
});
|
||||
}
|
||||
|
||||
return tex_ptr->get();
|
||||
}
|
||||
|
||||
bool ImGuiFullscreen::InvalidateCachedTexture(std::string_view path)
|
||||
{
|
||||
// need to do a partial match on this because SVG
|
||||
|
|
|
@ -141,6 +141,7 @@ std::shared_ptr<GPUTexture> LoadTexture(std::string_view path, u32 svg_width = 0
|
|||
GPUTexture* GetCachedTexture(std::string_view name);
|
||||
GPUTexture* GetCachedTexture(std::string_view name, u32 svg_width, u32 svg_height);
|
||||
GPUTexture* GetCachedTextureAsync(std::string_view name);
|
||||
GPUTexture* GetCachedTextureAsync(std::string_view name, u32 svg_width, u32 svg_height);
|
||||
bool InvalidateCachedTexture(std::string_view path);
|
||||
bool TextureNeedsSVGDimensions(std::string_view path);
|
||||
void UploadAsyncTextures();
|
||||
|
|
Loading…
Reference in New Issue