From 21cce304e8eb77d5970d3197b0208cc50f6ff55b Mon Sep 17 00:00:00 2001 From: otavepto Date: Thu, 25 Apr 2024 03:17:36 +0200 Subject: [PATCH] * revert the changes to the notifications heights + calculate all notifications heights dynamically * added a new button to the overlay `"Test achievement"` which triggeres a test achievement * added a new overlay appearance option `Achievement_Unlock_Datetime_Format` which allows changing the date/time format of the unlocked achievements * removed the condition which disabled the overlay sounds when it is shown --- CHANGELOG.md | 13 ++ dll/dll/settings.h | 4 +- dll/settings_parser.cpp | 3 + overlay_experimental/overlay/steam_overlay.h | 53 ++--- .../overlay/steam_overlay_translations.h | 92 +++++++++ overlay_experimental/steam_overlay.cpp | 182 ++++++++++++------ .../configs.overlay.EXAMPLE.ini | 9 +- 7 files changed, 265 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc47eb32..818e171b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 2024/4/25 + +* **[schmurger]** improved achievement notification: + - added new overlay appearance option `Notification_Rounding` which allows increasing the roundness of the notifications corners + - the overlay ini file now contains color scheme similar to the one used in steam for the notification background +* added a new button to the overlay `"Test achievement"` which triggeres a test achievement, suggested by **[Kirius88]** + note that the icon for this test achievement is selected randomly from the current list of achievements +* added a new overlay appearance option `Achievement_Unlock_Datetime_Format` which allows changing the date/time format of the unlocked achievements, suggested by **[Clompress]** +* removed the condition which disabled the overlay sounds when it is shown, suggested by **[Vlxst]** +* calculate all notifications heights dynamically + +--- + ## 2024/4/23 * fixed local saving + ignore the global settings folder entirely when using the local save option for a full portable behavior diff --git a/dll/dll/settings.h b/dll/dll/settings.h index 60e79128..6fb99201 100644 --- a/dll/dll/settings.h +++ b/dll/dll/settings.h @@ -124,7 +124,9 @@ struct Overlay_Appearance { float notification_g = 0.29f; float notification_b = 0.48f; float notification_a = 1.0f; - float notification_rounding = 0.f; + float notification_rounding = 0.0f; + std::string ach_unlock_datetime_format = "%Y/%m/%d - %H:%M:%S"; + float background_r = -1.0f; float background_g = -1.0f; float background_b = -1.0f; diff --git a/dll/settings_parser.cpp b/dll/settings_parser.cpp index 4220080e..464a6c34 100644 --- a/dll/settings_parser.cpp +++ b/dll/settings_parser.cpp @@ -256,6 +256,9 @@ static void load_overlay_appearance(class Settings *settings_client, class Setti float nnotification_rounding = std::stof(value, NULL); settings_client->overlay_appearance.notification_rounding = nnotification_rounding; settings_server->overlay_appearance.notification_rounding = nnotification_rounding; + } else if (name.compare("Achievement_Unlock_Datetime_Format") == 0) { + settings_client->overlay_appearance.ach_unlock_datetime_format = value; + settings_server->overlay_appearance.ach_unlock_datetime_format = value; } else if (name.compare("Background_R") == 0) { float nbackground_r = std::stof(value, NULL); settings_client->overlay_appearance.background_r = nbackground_r; diff --git a/overlay_experimental/overlay/steam_overlay.h b/overlay_experimental/overlay/steam_overlay.h index 9c08462f..1591afd6 100644 --- a/overlay_experimental/overlay/steam_overlay.h +++ b/overlay_experimental/overlay/steam_overlay.h @@ -24,6 +24,7 @@ enum window_state window_state_rich_invite = 1<<4, window_state_send_message = 1<<5, window_state_need_attention = 1<<6, + }; struct friend_window_state @@ -50,46 +51,46 @@ struct Friend_Less } }; -enum notification_type +enum class notification_type { - notification_type_message = 0, - notification_type_invite, - notification_type_achievement, - notification_type_auto_accept_invite, + message = 0, + invite, + achievement, + auto_accept_invite, }; struct Notification { static constexpr float width_percent = 0.25f; // percentage from total width - static constexpr float height = 6.5f; static constexpr std::chrono::milliseconds fade_in = std::chrono::milliseconds(2000); static constexpr std::chrono::milliseconds fade_out = std::chrono::milliseconds(2000); static constexpr std::chrono::milliseconds show_time = std::chrono::milliseconds(6000) + fade_in + fade_out; static constexpr std::chrono::milliseconds fade_out_start = show_time - fade_out; - int id; - uint8 type; - std::chrono::seconds start_time; - std::string message; - std::pair* frd; - std::weak_ptr icon; + int id{}; + uint8 type{}; + std::chrono::seconds start_time{}; + std::string message{}; + std::pair* frd{}; + std::weak_ptr icon{}; }; struct Overlay_Achievement { - std::string name; - std::string title; - std::string description; - std::string icon_name; - std::string icon_gray_name; - bool hidden; - bool achieved; - uint32 unlock_time; - std::weak_ptr icon; - std::weak_ptr icon_gray; - // avoids spam loading on failure constexpr const static int ICON_LOAD_MAX_TRIALS = 3; + + std::string name{}; + std::string title{}; + std::string description{}; + std::string icon_name{}; + std::string icon_gray_name{}; + bool hidden{}; + bool achieved{}; + uint32 unlock_time{}; + std::weak_ptr icon{}; + std::weak_ptr icon_gray{}; + uint8_t icon_load_trials = ICON_LOAD_MAX_TRIALS; uint8_t icon_gray_load_trials = ICON_LOAD_MAX_TRIALS; }; @@ -127,6 +128,7 @@ class Steam_Overlay bool show_overlay = false; bool show_achievements = false; bool show_settings = false; + bool show_test_ach = false; // warn when using local save bool warn_local_save = false; @@ -185,7 +187,7 @@ class Steam_Overlay // Double click on friend void build_friend_window(Friend const& frd, friend_window_state &state); // Notifications like achievements, chat and invitations - void set_next_notification_pos(float width, float height, float font_size, notification_type type, struct NotificationsIndexes &idx); + void set_next_notification_pos(float width, float height, const Notification ¬i, struct NotificationsIndexes &idx); void build_notifications(int width, int height); // invite a single friend void invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking); @@ -205,9 +207,8 @@ class Steam_Overlay void obscure_game_input(bool state); void add_auto_accept_invite_notification(); - void add_invite_notification(std::pair &wnd_state); - + void post_achievement_notification(Overlay_Achievement &ach); void add_chat_message_notification(std::string const& message); bool open_overlay_hook(bool toggle); diff --git a/overlay_experimental/overlay/steam_overlay_translations.h b/overlay_experimental/overlay/steam_overlay_translations.h index 3ab6ef2c..777c8602 100644 --- a/overlay_experimental/overlay/steam_overlay_translations.h +++ b/overlay_experimental/overlay/steam_overlay_translations.h @@ -188,6 +188,98 @@ const char translationCopyId[TRANSLATION_NUMBER_OF_LANGUAGES][TRANSLATION_BUFFER u8"Copy ID", }; +const char translationTestAchievement[TRANSLATION_NUMBER_OF_LANGUAGES][TRANSLATION_BUFFER_SIZE] = { + // 0 - English + u8"Test achievement", + + // 1 - Arabic + u8"Test achievement", + + // 2 - Bulgarian + u8"Test achievement", + + // 3 - Simplified Chinese + u8"Test achievement", + + // 4 - Traditional Chinese + u8"Test achievement", + + // 5 - Czech + u8"Test achievement", + + // 6 - Danish + u8"Test achievement", + + // 7 - Dutch + u8"Test achievement", + + // 8 - Finnish + u8"Test achievement", + + // 9 - French + u8"Test achievement", + + // 10 - German + u8"Test achievement", + + // 11 - Greek + u8"Test achievement", + + // 12 - Hungarian + u8"Test achievement", + + // 13 - Italian + u8"Test achievement", + + // 14 - Japanese + u8"Test achievement", + + // 15 - Korean + u8"Test achievement", + + // 16 - Norwegian + u8"Test achievement", + + // 17 - Polish + u8"Test achievement", + + // 18 - Portuguese + u8"Test achievement", + + // 19 - Brazilian Portuguese + u8"Copiar ID", + + // 20 - Romanian + u8"Test achievement", + + // 21 - Russian + u8"Test achievement", + + // 22 - Spanish + u8"Test achievement", + + // 23 - Latin American + u8"Test achievement", + + // 24 - Swedish + u8"Test achievement", + + // 25 - Thai + u8"Test achievement", + + // 26 - Turkish + u8"Test achievement", + + // 27 - Ukrainian + u8"Test achievement", + + // 28 - Vietnamese + u8"Test achievement", + + // 29 - Croatian + u8"Test achievement", +}; + // C:\Program Files (x86)\Steam\tenfoot\resource\localization\tenfoot_*.txt // Friends_ProfileDetails_Action_InviteToGame const char translationInvite[TRANSLATION_NUMBER_OF_LANGUAGES][TRANSLATION_BUFFER_SIZE] = { diff --git a/overlay_experimental/steam_overlay.cpp b/overlay_experimental/steam_overlay.cpp index 9b4fbb47..ba00b816 100644 --- a/overlay_experimental/steam_overlay.cpp +++ b/overlay_experimental/steam_overlay.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include "InGameOverlay/ImGui/imgui.h" @@ -252,6 +253,7 @@ void Steam_Overlay::create_fonts() for (int i = 0; i < TRANSLATION_NUMBER_OF_LANGUAGES; i++) { font_builder.AddText(translationChat[i]); font_builder.AddText(translationCopyId[i]); + font_builder.AddText(translationTestAchievement[i]); font_builder.AddText(translationInvite[i]); font_builder.AddText(translationInviteAll[i]); font_builder.AddText(translationJoin[i]); @@ -510,7 +512,7 @@ void Steam_Overlay::notify_sound_user_invite(friend_window_state& friend_state) { if (settings->disable_overlay_friend_notification) return; - if (!(friend_state.window_state & window_state_show) || !show_overlay) { + if (!(friend_state.window_state & window_state_show)) { friend_state.window_state |= window_state_need_attention; #ifdef __WINDOWS__ auto wav_data = wav_files.find("overlay_friend_notification.wav"); @@ -527,14 +529,12 @@ void Steam_Overlay::notify_sound_user_achievement() { if (settings->disable_overlay_achievement_notification) return; - if (!show_overlay) { #ifdef __WINDOWS__ - auto wav_data = wav_files.find("overlay_achievement_notification.wav"); - if (wav_files.end() != wav_data && wav_data->second.size()) { - PlaySoundA((LPCSTR)&wav_data->second[0], NULL, SND_ASYNC | SND_MEMORY); - } -#endif + auto wav_data = wav_files.find("overlay_achievement_notification.wav"); + if (wav_files.end() != wav_data && wav_data->second.size()) { + PlaySoundA((LPCSTR)&wav_data->second[0], NULL, SND_ASYNC | SND_MEMORY); } +#endif } void Steam_Overlay::notify_sound_auto_accept_friend_invite() @@ -549,7 +549,7 @@ void Steam_Overlay::notify_sound_auto_accept_friend_invite() #endif } -int find_free_id(std::vector & ids, int base) +int find_free_id(std::vector &ids, int base) { std::sort(ids.begin(), ids.end()); @@ -606,7 +606,7 @@ bool Steam_Overlay::submit_notification(notification_type type, const std::strin Notification notif{}; notif.start_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); notif.id = id; - notif.type = type; + notif.type = (uint8)type; notif.message = msg; notif.frd = frd; notif.icon = icon; @@ -616,14 +616,14 @@ bool Steam_Overlay::submit_notification(notification_type type, const std::strin // uncomment this block to obscure cursor input and steal focus for these specific notifications switch (type) { // we want to steal focus for these ones - case notification_type_invite: + case notification_type::invite: obscure_game_input(true); break; // not effective - case notification_type_achievement: - case notification_type_auto_accept_invite: - case notification_type_message: + case notification_type::achievement: + case notification_type::auto_accept_invite: + case notification_type::message: // nothing break; @@ -641,7 +641,7 @@ void Steam_Overlay::add_chat_message_notification(std::string const &message) std::lock_guard lock(overlay_mutex); if (settings->disable_overlay_friend_notification) return; - submit_notification(notification_type_message, message); + submit_notification(notification_type::message, message); } bool Steam_Overlay::is_friend_joinable(std::pair &f) @@ -834,24 +834,49 @@ void Steam_Overlay::build_friend_window(Friend const& frd, friend_window_state& } // set the position of the next notification -void Steam_Overlay::set_next_notification_pos(float width, float height, float font_size, notification_type type, struct NotificationsIndexes &idx) +void Steam_Overlay::set_next_notification_pos(float width, float height, const Notification ¬i, struct NotificationsIndexes &idx) { - // 0 on the y-axis is top, 0 on the x-axis is left const float noti_width = width * Notification::width_percent; - float noti_height = Notification::height * font_size; - + + auto &global_style = ImGui::GetStyle(); + const float padding_all_sides = 2 * (global_style.WindowPadding.y + global_style.WindowPadding.x); + PRINT_DEBUG("%f", padding_all_sides); + + const float msg_height = ImGui::CalcTextSize( + noti.message.c_str(), + noti.message.c_str() + noti.message.size(), + false, + noti_width - padding_all_sides - global_style.ItemSpacing.x + ).y; + float noti_height = msg_height + 2 * global_style.WindowPadding.y; + // get the required position Overlay_Appearance::NotificationPosition pos = Overlay_Appearance::default_pos; - switch (type) { - case notification_type::notification_type_achievement: - pos = settings->overlay_appearance.ach_earned_pos; - noti_height = settings->overlay_appearance.icon_size + ImGui::GetStyle().FramePadding.y * Notification::height; - break; - case notification_type::notification_type_invite: pos = settings->overlay_appearance.invite_pos; break; - case notification_type::notification_type_message: pos = settings->overlay_appearance.chat_msg_pos; break; + switch ((notification_type)noti.type) { + case notification_type::achievement: { + pos = settings->overlay_appearance.ach_earned_pos; + + const float new_msg_height = ImGui::CalcTextSize( + noti.message.c_str(), + noti.message.c_str() + noti.message.size(), + false, + noti_width - padding_all_sides - global_style.ItemSpacing.x - settings->overlay_appearance.icon_size + ).y; + const float new_noti_height = new_msg_height + 2 * global_style.WindowPadding.y; + + float biggest_noti_height = settings->overlay_appearance.icon_size + 2 * global_style.WindowPadding.y; + if (biggest_noti_height < new_noti_height) biggest_noti_height = new_noti_height; + + noti_height = biggest_noti_height; + } + break; + + case notification_type::invite: pos = settings->overlay_appearance.invite_pos; break; + case notification_type::message: pos = settings->overlay_appearance.chat_msg_pos; break; default: /* satisfy compiler warning */ break; } + // 0 on the y-axis is top, 0 on the x-axis is left float x = 0.0f; float y = 0.0f; switch (pos) { @@ -910,7 +935,7 @@ void Steam_Overlay::build_notifications(int width, int height) for (auto it = notifications.begin(); it != notifications.end(); ++it) { auto elapsed_notif = now - it->start_time; - set_next_notification_pos(width, height, font_size, (notification_type)it->type, idx); + set_next_notification_pos(width, height, *it, idx); if ( elapsed_notif < Notification::fade_in) { // still appearing (fading in) float alpha = settings->overlay_appearance.notification_a * (elapsed_notif.count() / static_cast(Notification::fade_in.count())); @@ -930,16 +955,16 @@ void Steam_Overlay::build_notifications(int width, int height) // some extra window flags for each notification type ImGuiWindowFlags extra_flags = ImGuiWindowFlags_NoFocusOnAppearing; - switch (it->type) { + switch ((notification_type)it->type) { // games like "Mafia Definitive Edition" will pause the entire game/scene if focus was stolen // be less intrusive for notifications that do not require interaction - case notification_type_achievement: - case notification_type_auto_accept_invite: - case notification_type_message: + case notification_type::achievement: + case notification_type::auto_accept_invite: + case notification_type::message: extra_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoInputs; break; - case notification_type_invite: + case notification_type::invite: // nothing break; @@ -951,8 +976,8 @@ void Steam_Overlay::build_notifications(int width, int height) std::string wnd_name = "NotiPopupShow" + std::to_string(it->id); if (ImGui::Begin(wnd_name.c_str(), nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | extra_flags)) { - switch (it->type) { - case notification_type_achievement: { + switch ((notification_type)it->type) { + case notification_type::achievement: { if (!it->icon.expired() && ImGui::BeginTable("imgui_table", 2)) { ImGui::TableSetupColumn("imgui_table_image", ImGuiTableColumnFlags_WidthFixed, settings->overlay_appearance.icon_size); ImGui::TableSetupColumn("imgui_table_text"); @@ -971,7 +996,7 @@ void Steam_Overlay::build_notifications(int width, int height) } break; - case notification_type_invite: { + case notification_type::invite: { ImGui::TextWrapped("%s", it->message.c_str()); if (ImGui::Button(translationJoin[current_language])) { @@ -982,11 +1007,11 @@ void Steam_Overlay::build_notifications(int width, int height) } break; - case notification_type_message: + case notification_type::message: ImGui::TextWrapped("%s", it->message.c_str()); break; - case notification_type_auto_accept_invite: + case notification_type::auto_accept_invite: ImGui::TextWrapped("%s", it->message.c_str()); break; @@ -1000,25 +1025,26 @@ void Steam_Overlay::build_notifications(int width, int height) ImGui::End(); ImGui::PopStyleColor(3); - ImGui::PopStyleVar(); } + ImGui::PopStyleVar(); + // erase all notifications whose visible time exceeded the max notifications.erase(std::remove_if(notifications.begin(), notifications.end(), [this, &now](Notification &item) { if ((now - item.start_time) > Notification::show_time) { PRINT_DEBUG("removing a notification"); allow_renderer_frame_processing(false); // uncomment this block to restore app input focus - switch (item.type) { + switch ((notification_type)item.type) { // we want to restore focus for these ones - case notification_type_invite: + case notification_type::invite: obscure_game_input(false); break; // not effective - case notification_type_achievement: - case notification_type_auto_accept_invite: - case notification_type_message: + case notification_type::achievement: + case notification_type::auto_accept_invite: + case notification_type::message: // nothing break; @@ -1050,7 +1076,7 @@ void Steam_Overlay::add_auto_accept_invite_notification() char tmp[TRANSLATION_BUFFER_SIZE]{}; snprintf(tmp, sizeof(tmp), "%s", translationAutoAcceptFriendInvite[current_language]); - submit_notification(notification_type_auto_accept_invite, tmp); + submit_notification(notification_type::auto_accept_invite, tmp); notify_sound_auto_accept_friend_invite(); } @@ -1066,7 +1092,23 @@ void Steam_Overlay::add_invite_notification(std::pair lock(overlay_mutex); + if (settings->disable_overlay_achievement_notification) return; + if (!Ready()) return; + + try_load_ach_icon(ach, true); + submit_notification( + notification_type::achievement, + ach.title + "\n" + ach.description, + {}, + ach.icon + ); } void Steam_Overlay::invite_friend(uint64 friend_id, class Steam_Friends* steamFriends, class Steam_Matchmaking* steamMatchmaking) @@ -1100,8 +1142,8 @@ bool Steam_Overlay::try_load_ach_icon(Overlay_Achievement &ach, bool achieved) file_size = file_size_(file_path); } if (file_size) { - std::string img = Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size); - if (img.length() > 0) { + std::string img(Local_Storage::load_image_resized(file_path, "", settings->overlay_appearance.icon_size)); + if (img.size()) { icon_rsrc = _renderer->CreateImageResource( (void*)img.c_str(), settings->overlay_appearance.icon_size, settings->overlay_appearance.icon_size); @@ -1249,6 +1291,27 @@ void Steam_Overlay::render_main_window() ImGui::SetClipboardText(friend_id_str.c_str()); } + ImGui::SameLine(); + // user clicked on "test achievement" + if (ImGui::Button(translationTestAchievement[current_language])) { + Overlay_Achievement ach{}; + ach.title = translationTestAchievement[current_language]; + ach.description = "~~~ " + ach.title + " ~~~"; + + if (achievements.size()) { + // https://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution + std::random_device rd{}; // a seed source for the random number engine + std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() + std::uniform_int_distribution<> distrib(0, achievements.size() - 1); + + size_t rand_idx = distrib(gen); + ach.icon = achievements[rand_idx].icon; + } + + post_achievement_notification(ach); + notify_sound_user_achievement(); + } + ImGui::Spacing(); ImGui::Spacing(); ImGui::LabelText("##label", "%s", translationFriends[current_language]); @@ -1337,10 +1400,12 @@ void Steam_Overlay::render_main_window() } if (achieved) { - char buffer[80] = {}; + char buffer[80]{}; time_t unlock_time = (time_t)x.unlock_time; - // TODO add this format to the overlay_appearance - std::strftime(buffer, 80, "%Y/%m/%d - %H:%M:%S", std::localtime(&unlock_time)); + size_t written = std::strftime(buffer, sizeof(buffer), settings->overlay_appearance.ach_unlock_datetime_format.c_str(), std::localtime(&unlock_time)); + if (!written) { // count was reached before the entire string could be stored, keep it safe + std::strftime(buffer, sizeof(buffer), "%Y/%m/%d - %H:%M:%S", std::localtime(&unlock_time)); + } ImGui::TextColored(ImVec4(0, 255, 0, 255), translationAchievedOn[current_language], buffer); } else { @@ -1833,7 +1898,7 @@ void Steam_Overlay::FriendDisconnect(Friend _friend) } // show a notification when the user unlocks an achievement -void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach) +void Steam_Overlay::AddAchievementNotification(nlohmann::json const &ach) { PRINT_DEBUG_ENTRY(); std::lock_guard lock(overlay_mutex); @@ -1842,6 +1907,7 @@ void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach) // don't return early when disable_overlay_achievement_notification is true // otherwise when you open the achievements list/menu you won't see the new unlock status + // adjust the local 'is_achieved' and 'unlock_time' std::vector found_achs{}; { std::lock_guard lock2(global_mutex); @@ -1860,19 +1926,11 @@ void Steam_Overlay::AddAchievementNotification(nlohmann::json const& ach) } } - if (!settings->disable_overlay_achievement_notification) { - for (auto found_ach : found_achs) { - try_load_ach_icon(*found_ach, true); - submit_notification( - notification_type_achievement, - ach.value("displayName", std::string()) + "\n" + ach.value("description", std::string()), - {}, - found_ach->icon - ); - } - - notify_sound_user_achievement(); + for (auto found_ach : found_achs) { + post_achievement_notification(*found_ach); } + + notify_sound_user_achievement(); } #endif diff --git a/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini b/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini index 021f38e1..5863b3e4 100644 --- a/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini +++ b/post_build/steam_settings.EXAMPLE/configs.overlay.EXAMPLE.ini @@ -47,8 +47,13 @@ Notification_G=0.15 Notification_B=0.18 Notification_A=1.0 -# Notification rounded corners -Notification_Rounding=10 +# notification rounded corners +Notification_Rounding=10.0 + +# format for the achievement unlock date/time, limited to 79 characters +# if the output formatted string exceeded this limit, the builtin format will be used +# look for the format here: https://en.cppreference.com/w/cpp/chrono/c/strftime +Achievement_Unlock_Datetime_Format=%Y/%m/%d - %H:%M:%S Background_R=-1.0 Background_G=-1.0