* 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
This commit is contained in:
otavepto 2024-04-25 03:17:36 +02:00
parent bda4fbca2c
commit 21cce304e8
7 changed files with 265 additions and 91 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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<const Friend, friend_window_state>* frd;
std::weak_ptr<uint64_t> icon;
int id{};
uint8 type{};
std::chrono::seconds start_time{};
std::string message{};
std::pair<const Friend, friend_window_state>* frd{};
std::weak_ptr<uint64_t> 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<uint64_t> icon;
std::weak_ptr<uint64_t> 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<uint64_t> icon{};
std::weak_ptr<uint64_t> 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 &noti, 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<const Friend, friend_window_state> &wnd_state);
void post_achievement_notification(Overlay_Achievement &ach);
void add_chat_message_notification(std::string const& message);
bool open_overlay_hook(bool toggle);

View File

@ -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] = {

View File

@ -14,6 +14,7 @@
#include <sstream>
#include <cctype>
#include <utility>
#include <random>
#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<int> & ids, int base)
int find_free_id(std::vector<int> &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::seconds>(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<std::recursive_mutex> 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<const Friend, friend_window_state> &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 &noti, 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<float>(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<const Friend, friend_windo
auto &name = first_friend.name();
snprintf(tmp, sizeof(tmp), translationInvitedYouToJoinTheGame[current_language], name.c_str(), (uint64)first_friend.id());
submit_notification(notification_type_invite, tmp, &wnd_state);
submit_notification(notification_type::invite, tmp, &wnd_state);
}
void Steam_Overlay::post_achievement_notification(Overlay_Achievement &ach)
{
PRINT_DEBUG_ENTRY();
std::lock_guard<std::recursive_mutex> 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<std::recursive_mutex> 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<Overlay_Achievement*> found_achs{};
{
std::lock_guard<std::recursive_mutex> 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

View File

@ -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