mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
overlay: toast notifications (#704)
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Adds toast notifications to the overlay. Toast notifications are transient windows (shown for about 4 seconds). These are drawn on top of the game even when the overlay is inactive (i.e., when no window is open), and does not cause any input sink behavior. Add toasts for some common user-driven actions that could use a notification, such as: * screenshot * card insert * PIN macro * API client connect/disconnect * virtual printer * screen resize toggle / scene switch Add an option to change where the toasts are displayed, or to turn it off entirely. Add SDK function for it, and add it to the samples. ## Testing Tested DDR (shown at bottom right, as is for most games) and RB (shown top right by default).
This commit is contained in:
@@ -558,6 +558,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
# overlay
|
# overlay
|
||||||
overlay/overlay.cpp
|
overlay/overlay.cpp
|
||||||
|
overlay/notifications.cpp
|
||||||
overlay/window.cpp
|
overlay/window.cpp
|
||||||
overlay/imgui/extensions.cpp
|
overlay/imgui/extensions.cpp
|
||||||
overlay/imgui/impl_spice.cpp
|
overlay/imgui/impl_spice.cpp
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
#include "module.h"
|
#include "module.h"
|
||||||
#include "modules/analogs.h"
|
#include "modules/analogs.h"
|
||||||
#include "modules/buttons.h"
|
#include "modules/buttons.h"
|
||||||
@@ -197,6 +199,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
|||||||
|
|
||||||
// log connection
|
// log connection
|
||||||
log_info("api", "client connected: {}", client_address_str);
|
log_info("api", "client connected: {}", client_address_str);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("API client connected ({})", client_address_str));
|
||||||
client_states_m.lock();
|
client_states_m.lock();
|
||||||
client_states.emplace_back(&client_state);
|
client_states.emplace_back(&client_state);
|
||||||
client_states_m.unlock();
|
client_states_m.unlock();
|
||||||
@@ -277,6 +282,9 @@ void Controller::connection_handler(api::ClientState client_state) {
|
|||||||
|
|
||||||
// log disconnect
|
// log disconnect
|
||||||
log_info("api", "client disconnected: {}", client_address_str);
|
log_info("api", "client disconnected: {}", client_address_str);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("API client disconnected ({})", client_address_str));
|
||||||
client_states_m.lock();
|
client_states_m.lock();
|
||||||
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
|
client_states.erase(std::remove(client_states.begin(), client_states.end(), &client_state));
|
||||||
client_states_m.unlock();
|
client_states_m.unlock();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/rc4.h"
|
#include "util/rc4.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
|
||||||
using namespace headsocket;
|
using namespace headsocket;
|
||||||
@@ -91,12 +92,18 @@ namespace api {
|
|||||||
|
|
||||||
// log connection
|
// log connection
|
||||||
log_info("api::websocket", "client connected");
|
log_info("api::websocket", "client connected");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
"API websocket client connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketClient::on_disconnect() {
|
void WebSocketClient::on_disconnect() {
|
||||||
|
|
||||||
// log disconnection
|
// log disconnection
|
||||||
log_info("api::websocket", "client disconnected");
|
log_info("api::websocket", "client disconnected");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
"API websocket client disconnected");
|
||||||
|
|
||||||
// get pointer to server
|
// get pointer to server
|
||||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "hooks/sleephook.h"
|
#include "hooks/sleephook.h"
|
||||||
#include "hooks/libraryhook.h"
|
#include "hooks/libraryhook.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/fileutils.h"
|
#include "util/fileutils.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
@@ -397,8 +398,14 @@ namespace games::shared {
|
|||||||
// logging
|
// logging
|
||||||
if (success) {
|
if (success) {
|
||||||
log_info("printer", "printer emulation has written an image to {}", image_path);
|
log_info("printer", "printer emulation has written an image to {}", image_path);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
|
||||||
} else {
|
} else {
|
||||||
log_warning("printer", "printer emulation failed to write image to {}", image_path);
|
log_warning("printer", "printer emulation failed to write image to {}", image_path);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -1434,11 +1436,18 @@ static void save_screenshot(const std::string &file_path, UINT height, IDirect3D
|
|||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9", "Failed to save screenshot");
|
log_warning("graphics::d3d9", "Failed to save screenshot");
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
"Screenshot failed to save");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// save to clipboard
|
// save to clipboard
|
||||||
clipboard::copy_image(file_path);
|
clipboard::copy_image(file_path);
|
||||||
|
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
|
||||||
} else {
|
} else {
|
||||||
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
log_warning("graphics::d3d9", "Direct3D save helper function not available");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
#include "misc/sde.h"
|
#include "misc/sde.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "overlay/windows/patch_manager.h"
|
#include "overlay/windows/patch_manager.h"
|
||||||
#include "overlay/windows/iidx_seg.h"
|
#include "overlay/windows/iidx_seg.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
@@ -2467,6 +2468,24 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// eamuse init
|
// eamuse init
|
||||||
eamuse_autodetect_game();
|
eamuse_autodetect_game();
|
||||||
|
|
||||||
|
// notification position: apply per-game default first, then the explicit
|
||||||
|
// user option (if set) wins over the default.
|
||||||
|
overlay::notifications::apply_game_default_position(eamuse_get_game());
|
||||||
|
if (options[launcher::Options::NotificationPosition].is_active()) {
|
||||||
|
const auto txt = options[launcher::Options::NotificationPosition].value_text();
|
||||||
|
if (txt == "topleft") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::TopLeft;
|
||||||
|
} else if (txt == "topright") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::TopRight;
|
||||||
|
} else if (txt == "bottomleft") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::BottomLeft;
|
||||||
|
} else if (txt == "bottomright") {
|
||||||
|
overlay::notifications::POSITION = overlay::notifications::Position::BottomRight;
|
||||||
|
} else if (txt == "off") {
|
||||||
|
overlay::notifications::ENABLED = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unis device hook
|
// unis device hook
|
||||||
unisintrhook_init();
|
unisintrhook_init();
|
||||||
|
|
||||||
|
|||||||
@@ -468,6 +468,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.setting_name = "200",
|
.setting_name = "200",
|
||||||
.category = "Overlay",
|
.category = "Overlay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// NotificationPosition
|
||||||
|
.title = "Notifications",
|
||||||
|
.name = "toast",
|
||||||
|
.desc = "Select where notifications will appear on the screen.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.category = "Overlay",
|
||||||
|
.elements = {
|
||||||
|
{"off", ""},
|
||||||
|
{"topleft", ""},
|
||||||
|
{"topright", ""},
|
||||||
|
{"bottomleft", ""},
|
||||||
|
{"bottomright", ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_FpsAutoShow
|
// spice2x_FpsAutoShow
|
||||||
.title = "Auto Show FPS/Clock",
|
.title = "Auto Show FPS/Clock",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ namespace launcher {
|
|||||||
VREnable,
|
VREnable,
|
||||||
DisableOverlay,
|
DisableOverlay,
|
||||||
OverlayScaling,
|
OverlayScaling,
|
||||||
|
NotificationPosition,
|
||||||
spice2x_FpsAutoShow,
|
spice2x_FpsAutoShow,
|
||||||
spice2x_FpsOpposite,
|
spice2x_FpsOpposite,
|
||||||
spice2x_SubScreenAutoShow,
|
spice2x_SubScreenAutoShow,
|
||||||
|
|||||||
@@ -14,14 +14,16 @@
|
|||||||
#include "util/time.h"
|
#include "util/time.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
#include "bt5api.h"
|
#include "bt5api.h"
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
static constexpr double NOTIFICATION_THROTTLE_SECONDS = 3.0;
|
||||||
static bool CARD_INSERT[2] = {false, false};
|
static bool CARD_INSERT[2] = {false, false};
|
||||||
static double CARD_INSERT_TIME[2] = {0, 0};
|
static double CARD_INSERT_TIME[2] = {0, 0};
|
||||||
static double CARD_INSERT_TIMEOUT = 2.0;
|
static double CARD_INSERT_TIMEOUT = 2.0;
|
||||||
static char CARD_INSERT_UID[2][8];
|
static char CARD_INSERT_UID[2][8] = {{0}, {0}};
|
||||||
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
static char CARD_INSERT_UID_ENABLE[2] = {false, false};
|
||||||
static int COIN_STOCK = 0;
|
static int COIN_STOCK = 0;
|
||||||
static bool COIN_BLOCK = false;
|
static bool COIN_BLOCK = false;
|
||||||
@@ -119,6 +121,12 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
|
|||||||
"{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
"{} card override contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
||||||
card_override, n);
|
card_override, n);
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_override_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] invalid card override", index + 1));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +157,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
std::ifstream f(path);
|
std::ifstream f(path);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
log_warning("eamuse", "{} can not be opened!", path);
|
log_warning("eamuse", "{} can not be opened!", path);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] can't open card file", index + 1));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +173,12 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
// check size
|
// check size
|
||||||
if (length < 16) {
|
if (length < 16) {
|
||||||
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
|
log_warning("eamuse", "{} is too small (must be at least 16 characters)", path);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card file error", index + 1));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +198,11 @@ bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card,
|
|||||||
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
"{} contains an invalid character sequence at byte {} (16 characters, 0-9/A-F only)",
|
||||||
path, n);
|
path, n);
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Error,
|
||||||
|
fmt::format("eamuse.card_file_error.p{}", index + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card file error", index + 1));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,8 +275,16 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
|
|||||||
auto offset = unit_id * games::KeypadButtons::Size;
|
auto offset = unit_id * games::KeypadButtons::Size;
|
||||||
if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
|
if ((CARD_INSERT[index] && fabs(get_performance_seconds() - CARD_INSERT_TIME[index]) < CARD_INSERT_TIMEOUT)
|
||||||
|| GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
|
|| GameAPI::Buttons::getState(RI_MGR, keypad_buttons->at(games::KeypadButtons::InsertCard + offset))) {
|
||||||
|
|
||||||
log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count);
|
log_info("eamuse", "[P{}] Card insert on reader (total active count: {})", unit_id+1, active_count);
|
||||||
CARD_INSERT[index] = false;
|
CARD_INSERT[index] = false;
|
||||||
|
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("eamuse.card_inserted.p{}", unit_id + 1),
|
||||||
|
NOTIFICATION_THROTTLE_SECONDS,
|
||||||
|
fmt::format("[P{}] card inserted", unit_id + 1));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +426,10 @@ void eamuse_pin_macro_start_thread() {
|
|||||||
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
|
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
|
||||||
}
|
}
|
||||||
if (key_press || auto_request) {
|
if (key_press || auto_request) {
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("[P{}] PIN macro fired ({})",
|
||||||
|
unit + 1, auto_request ? "auto" : "manual"));
|
||||||
active_unit = unit;
|
active_unit = unit;
|
||||||
// Reset key index
|
// Reset key index
|
||||||
pin_index[unit] = 0;
|
pin_index[unit] = 0;
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
#include "notifications.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "external/imgui/imgui_internal.h"
|
||||||
|
#include "external/fmt/include/fmt/format.h"
|
||||||
|
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
#include "util/time.h"
|
||||||
|
|
||||||
|
namespace overlay::notifications {
|
||||||
|
|
||||||
|
bool ENABLED = true;
|
||||||
|
Position POSITION = Position::BottomRight;
|
||||||
|
|
||||||
|
struct Notification {
|
||||||
|
uint64_t id;
|
||||||
|
std::string text;
|
||||||
|
Severity severity;
|
||||||
|
double created_ms;
|
||||||
|
float duration_s;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::mutex g_mutex;
|
||||||
|
static std::deque<Notification> g_items;
|
||||||
|
static std::atomic<uint64_t> g_next_id { 1 };
|
||||||
|
static std::atomic<size_t> g_count { 0 };
|
||||||
|
|
||||||
|
// duration in seconds each notification stays visible
|
||||||
|
static constexpr float DURATION_S = 3.0f;
|
||||||
|
|
||||||
|
// maximum number of notifications kept in the queue (oldest dropped beyond this)
|
||||||
|
static constexpr size_t MAX_NOTIFICATIONS = 6;
|
||||||
|
|
||||||
|
// time (ms) over which a toast fades out at the end of its lifetime
|
||||||
|
static constexpr float FADE_OUT_MS = 400.0f;
|
||||||
|
|
||||||
|
// fixed width of each toast window, in unscaled pixels
|
||||||
|
static constexpr float TOAST_WIDTH = 320.0f;
|
||||||
|
|
||||||
|
// gap between the toast stack and the screen edges (right + bottom)
|
||||||
|
static constexpr float TOAST_MARGIN = 20.0f;
|
||||||
|
|
||||||
|
// vertical gap between stacked toasts
|
||||||
|
static constexpr float TOAST_SPACING = 8.0f;
|
||||||
|
|
||||||
|
// inner padding inside a toast window (horizontal / vertical)
|
||||||
|
static constexpr float TOAST_PAD_X = 10.0f;
|
||||||
|
static constexpr float TOAST_PAD_Y = 8.0f;
|
||||||
|
|
||||||
|
// width of the colored severity accent bar drawn on the left edge
|
||||||
|
static constexpr float TOAST_ACCENT_W = 6.0f;
|
||||||
|
|
||||||
|
// base opacity of the toast background (0..1), multiplied by the fade alpha
|
||||||
|
static constexpr float TOAST_BG_ALPHA = 0.85f;
|
||||||
|
|
||||||
|
static constexpr ImGuiWindowFlags TOAST_FLAGS =
|
||||||
|
ImGuiWindowFlags_NoDecoration
|
||||||
|
| ImGuiWindowFlags_NoInputs
|
||||||
|
| ImGuiWindowFlags_NoNav
|
||||||
|
| ImGuiWindowFlags_NoMove
|
||||||
|
| ImGuiWindowFlags_NoSavedSettings
|
||||||
|
| ImGuiWindowFlags_NoFocusOnAppearing
|
||||||
|
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
||||||
|
| ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
|
|
||||||
|
static ImU32 severity_accent(Severity sev) {
|
||||||
|
switch (sev) {
|
||||||
|
case Severity::Success: return IM_COL32(80, 200, 120, 255);
|
||||||
|
case Severity::Warning: return IM_COL32(230, 180, 60, 255);
|
||||||
|
case Severity::Error: return IM_COL32(220, 60, 60, 255);
|
||||||
|
case Severity::Info:
|
||||||
|
default: return IM_COL32(90, 160, 230, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_expired(const Notification &n, double now_ms) {
|
||||||
|
return (now_ms - n.created_ms) >= (n.duration_s * 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns 0.0 .. 1.0 fade alpha based on time remaining
|
||||||
|
static float compute_alpha(const Notification &n, double now_ms) {
|
||||||
|
const double remaining_ms = (n.duration_s * 1000.0) - (now_ms - n.created_ms);
|
||||||
|
if (remaining_ms >= FADE_OUT_MS) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
if (remaining_ms <= 0.0) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
return static_cast<float>(remaining_ms / FADE_OUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop expired items and copy the rest under a single lock acquisition
|
||||||
|
static std::vector<Notification> snapshot_and_prune(double now_ms) {
|
||||||
|
std::vector<Notification> snapshot;
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
for (auto it = g_items.begin(); it != g_items.end();) {
|
||||||
|
if (is_expired(*it, now_ms)) {
|
||||||
|
it = g_items.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_count.store(g_items.size(), std::memory_order_release);
|
||||||
|
snapshot.assign(g_items.begin(), g_items.end());
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the configured anchor on the right edge of the screen?
|
||||||
|
static bool position_is_right(Position p) {
|
||||||
|
return p == Position::BottomRight || p == Position::TopRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the configured anchor on the bottom edge of the screen?
|
||||||
|
static bool position_is_bottom(Position p) {
|
||||||
|
return p == Position::BottomRight || p == Position::BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw a single toast anchored to the configured corner; `cursor_y` is the
|
||||||
|
// y-coordinate of the toast edge nearest the anchor (top edge for Top* anchors,
|
||||||
|
// bottom edge for Bottom* anchors). returns its height in pixels.
|
||||||
|
static float draw_toast(const Notification &n, float cursor_y, float alpha) {
|
||||||
|
const float toast_width = apply_scaling(TOAST_WIDTH);
|
||||||
|
const float margin = apply_scaling(TOAST_MARGIN);
|
||||||
|
const ImVec2 &display = ImGui::GetIO().DisplaySize;
|
||||||
|
const Position pos = POSITION;
|
||||||
|
|
||||||
|
const auto window_id = fmt::format("##spice_notif_{}", n.id);
|
||||||
|
|
||||||
|
// anchor x/pivot.x select the screen edge; pivot.y matches cursor_y semantics
|
||||||
|
const float anchor_x = position_is_right(pos) ? (display.x - margin) : margin;
|
||||||
|
const float pivot_x = position_is_right(pos) ? 1.0f : 0.0f;
|
||||||
|
const float pivot_y = position_is_bottom(pos) ? 1.0f : 0.0f;
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(anchor_x, cursor_y),
|
||||||
|
ImGuiCond_Always, ImVec2(pivot_x, pivot_y));
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(toast_width, 0.f), ImGuiCond_Always);
|
||||||
|
ImGui::SetNextWindowBgAlpha(TOAST_BG_ALPHA * alpha);
|
||||||
|
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
|
||||||
|
ImVec2(apply_scaling(TOAST_PAD_X), apply_scaling(TOAST_PAD_Y)));
|
||||||
|
|
||||||
|
float height = 0.f;
|
||||||
|
if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) {
|
||||||
|
const ImVec2 win_pos = ImGui::GetWindowPos();
|
||||||
|
const ImVec2 win_size = ImGui::GetWindowSize();
|
||||||
|
|
||||||
|
// accent bar on the left edge of the window
|
||||||
|
const ImU32 accent = severity_accent(n.severity);
|
||||||
|
const ImU32 accent_faded =
|
||||||
|
(accent & 0x00FFFFFFu) | (static_cast<ImU32>(alpha * 255.0f) << 24);
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||||
|
win_pos,
|
||||||
|
ImVec2(win_pos.x + apply_scaling(TOAST_ACCENT_W), win_pos.y + win_size.y),
|
||||||
|
accent_faded);
|
||||||
|
|
||||||
|
// small gutter past the accent bar, then wrapped text
|
||||||
|
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X));
|
||||||
|
ImGui::TextUnformatted(n.text.c_str());
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
|
||||||
|
height = ImGui::GetWindowSize().y;
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
ImGui::PopStyleVar(2);
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t add(Severity severity, std::string text) {
|
||||||
|
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification n {
|
||||||
|
.id = g_next_id.fetch_add(1, std::memory_order_relaxed),
|
||||||
|
.text = std::move(text),
|
||||||
|
.severity = severity,
|
||||||
|
.created_ms = get_performance_milliseconds(),
|
||||||
|
.duration_s = DURATION_S,
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_mutex);
|
||||||
|
g_items.push_back(std::move(n));
|
||||||
|
while (g_items.size() > MAX_NOTIFICATIONS) {
|
||||||
|
g_items.pop_front();
|
||||||
|
}
|
||||||
|
g_count.store(g_items.size(), std::memory_order_release);
|
||||||
|
}
|
||||||
|
return n.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t add_throttled(Severity severity, const std::string &key,
|
||||||
|
double cooldown_seconds, std::string text) {
|
||||||
|
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// per-key last-emit timestamps live behind their own lock so we don't
|
||||||
|
// hold g_mutex across the map lookup.
|
||||||
|
static std::mutex throttle_mutex;
|
||||||
|
static std::unordered_map<std::string, double> last_emit_ms;
|
||||||
|
const double now_ms = get_performance_milliseconds();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(throttle_mutex);
|
||||||
|
auto it = last_emit_ms.find(key);
|
||||||
|
if (it != last_emit_ms.end()
|
||||||
|
&& (now_ms - it->second) < (cooldown_seconds * 1000.0)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
last_emit_ms[key] = now_ms;
|
||||||
|
}
|
||||||
|
return add(severity, std::move(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_pending() {
|
||||||
|
return g_count.load(std::memory_order_acquire) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() {
|
||||||
|
const double now_ms = get_performance_milliseconds();
|
||||||
|
const auto snapshot = snapshot_and_prune(now_ms);
|
||||||
|
if (snapshot.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack from the anchored edge with newest toast at the anchor.
|
||||||
|
// Bottom* anchors stack upward; Top* anchors stack downward.
|
||||||
|
const float spacing = apply_scaling(TOAST_SPACING);
|
||||||
|
const float margin = apply_scaling(TOAST_MARGIN);
|
||||||
|
const bool bottom = position_is_bottom(POSITION);
|
||||||
|
float cursor_y = bottom
|
||||||
|
? (ImGui::GetIO().DisplaySize.y - margin)
|
||||||
|
: margin;
|
||||||
|
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) {
|
||||||
|
const float alpha = compute_alpha(*it, now_ms);
|
||||||
|
const float height = draw_toast(*it, cursor_y, alpha);
|
||||||
|
cursor_y += bottom ? -(height + spacing) : (height + spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_game_default_position(const std::string &game_name) {
|
||||||
|
if (game_name == "Reflec Beat") {
|
||||||
|
POSITION = Position::TopRight;
|
||||||
|
}
|
||||||
|
// others keep the default (BottomRight)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace overlay::notifications {
|
||||||
|
|
||||||
|
// master switch for the notification system; when false, add() is a no-op.
|
||||||
|
// controlled by selecting "none" for the -notifypos launcher option.
|
||||||
|
extern bool ENABLED;
|
||||||
|
|
||||||
|
enum class Severity {
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
// screen anchor for the toast stack. toasts stack away from the anchored edge.
|
||||||
|
enum class Position {
|
||||||
|
BottomRight,
|
||||||
|
BottomLeft,
|
||||||
|
TopRight,
|
||||||
|
TopLeft,
|
||||||
|
};
|
||||||
|
|
||||||
|
// current toast anchor. defaults to BottomRight; may be reassigned by
|
||||||
|
// apply_game_default_position() or by the user via -notifypos.
|
||||||
|
extern Position POSITION;
|
||||||
|
|
||||||
|
// apply the default toast position appropriate for a game (by display name,
|
||||||
|
// as returned by eamuse_get_game()). called once after game autodetect, before
|
||||||
|
// any user -notifypos override is applied.
|
||||||
|
void apply_game_default_position(const std::string &game_name);
|
||||||
|
|
||||||
|
// add a notification (thread-safe). returns the assigned id, or 0 if the
|
||||||
|
// notification was dropped (overlay disabled or notifications disabled).
|
||||||
|
uint64_t add(Severity severity, std::string text);
|
||||||
|
|
||||||
|
// rate-limited variant of add(). suppresses the toast if another call with
|
||||||
|
// the same `key` succeeded within the last `cooldown_seconds`. useful for
|
||||||
|
// events that can fire every frame (e.g. a button held down). returns the
|
||||||
|
// assigned id, or 0 if the toast was suppressed or dropped. thread-safe.
|
||||||
|
uint64_t add_throttled(Severity severity, const std::string &key,
|
||||||
|
double cooldown_seconds, std::string text);
|
||||||
|
|
||||||
|
// true if there is at least one notification that still needs to be drawn.
|
||||||
|
// safe to call from the render thread without locking the underlying store.
|
||||||
|
bool has_pending();
|
||||||
|
|
||||||
|
// draw all active notifications and prune expired ones.
|
||||||
|
// must be called from the ImGui render thread inside a NewFrame/EndFrame pair.
|
||||||
|
void draw();
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "external/imgui/backends/imgui_impl_dx9.h"
|
#include "external/imgui/backends/imgui_impl_dx9.h"
|
||||||
#include "overlay/imgui/impl_spice.h"
|
#include "overlay/imgui/impl_spice.h"
|
||||||
#include "overlay/imgui/impl_sw.h"
|
#include "overlay/imgui/impl_sw.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
|
||||||
#include "window.h"
|
#include "window.h"
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
@@ -465,8 +466,16 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
ImGui_ImplSpice_NewFrame();
|
ImGui_ImplSpice_NewFrame();
|
||||||
this->total_elapsed += ImGui::GetIO().DeltaTime;
|
this->total_elapsed += ImGui::GetIO().DeltaTime;
|
||||||
|
|
||||||
// check if inactive
|
// notifications draw on top of the game without flipping `active`, so the input gates
|
||||||
if (!this->active) {
|
// (see touch/touch.cpp WndProc, touch_get_points/events, graphics.cpp WM_CHAR) stay
|
||||||
|
// disabled and input continues to flow to the game.
|
||||||
|
// SOFTWARE renderer (spicecfg / configurator) never gets notifications - no room.
|
||||||
|
const bool draw_notifications = this->renderer != OverlayRenderer::SOFTWARE
|
||||||
|
&& overlay::notifications::has_pending();
|
||||||
|
|
||||||
|
// check if there is nothing to draw
|
||||||
|
this->has_pending_frame = false;
|
||||||
|
if (!this->active && !draw_notifications) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,8 +489,10 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ImGui::NewFrame();
|
ImGui::NewFrame();
|
||||||
|
this->has_pending_frame = true;
|
||||||
|
|
||||||
// build windows
|
// build windows only when the overlay itself is active
|
||||||
|
if (this->active) {
|
||||||
for (auto &window : this->windows) {
|
for (auto &window : this->windows) {
|
||||||
window->build();
|
window->build();
|
||||||
}
|
}
|
||||||
@@ -489,6 +500,12 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
if (SHOW_DEBUG_LOG_WINDOW) {
|
if (SHOW_DEBUG_LOG_WINDOW) {
|
||||||
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
|
ImGui::ShowDebugLogWindow(&SHOW_DEBUG_LOG_WINDOW);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw notifications last so they paint on top of any overlay windows
|
||||||
|
if (draw_notifications) {
|
||||||
|
overlay::notifications::draw();
|
||||||
|
}
|
||||||
|
|
||||||
// end frame
|
// end frame
|
||||||
ImGui::EndFrame();
|
ImGui::EndFrame();
|
||||||
@@ -496,8 +513,8 @@ void overlay::SpiceOverlay::new_frame() {
|
|||||||
|
|
||||||
void overlay::SpiceOverlay::render() {
|
void overlay::SpiceOverlay::render() {
|
||||||
|
|
||||||
// check if inactive
|
// skip if new_frame() didn't begin a frame this tick
|
||||||
if (!this->active) {
|
if (!this->has_pending_frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,6 +558,8 @@ void overlay::SpiceOverlay::render() {
|
|||||||
for (auto &window : this->windows) {
|
for (auto &window : this->windows) {
|
||||||
window->after_render();
|
window->after_render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->has_pending_frame = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void overlay::SpiceOverlay::update() {
|
void overlay::SpiceOverlay::update() {
|
||||||
@@ -700,7 +719,7 @@ void overlay::SpiceOverlay::input_char(unsigned int c) {
|
|||||||
|
|
||||||
uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) {
|
uint32_t *overlay::SpiceOverlay::sw_get_pixel_data(int *width, int *height) {
|
||||||
|
|
||||||
// check if active
|
// check if active (notifications never draw in software renderer, so no extra gate here)
|
||||||
if (!this->active) {
|
if (!this->active) {
|
||||||
*width = 0;
|
*width = 0;
|
||||||
*height = 0;
|
*height = 0;
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ namespace overlay {
|
|||||||
bool hotkey_toggle = false;
|
bool hotkey_toggle = false;
|
||||||
bool hotkey_toggle_last = false;
|
bool hotkey_toggle_last = false;
|
||||||
|
|
||||||
|
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
|
||||||
|
// so render() never runs without a matching NewFrame.
|
||||||
|
bool has_pending_frame = false;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
void add_font(const char* font, ImFontConfig* config, const ImWchar* glyphs);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/imgui/extensions.h"
|
#include "overlay/imgui/extensions.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -323,6 +324,11 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
if (toggle_screen_resize_new && !this->toggle_screen_resize_state) {
|
if (toggle_screen_resize_new && !this->toggle_screen_resize_state) {
|
||||||
cfg::SCREENRESIZE->enable_screen_resize = !cfg::SCREENRESIZE->enable_screen_resize;
|
cfg::SCREENRESIZE->enable_screen_resize = !cfg::SCREENRESIZE->enable_screen_resize;
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
cfg::SCREENRESIZE->enable_screen_resize
|
||||||
|
? "Screen resize enabled"
|
||||||
|
: "Screen resize disabled");
|
||||||
}
|
}
|
||||||
this->toggle_screen_resize_state = toggle_screen_resize_new;
|
this->toggle_screen_resize_state = toggle_screen_resize_new;
|
||||||
}
|
}
|
||||||
@@ -347,10 +353,16 @@ namespace overlay::windows {
|
|||||||
cfg::SCREENRESIZE->enable_screen_resize) {
|
cfg::SCREENRESIZE->enable_screen_resize) {
|
||||||
// this scene is already active, turn scaling off
|
// this scene is already active, turn scaling off
|
||||||
cfg::SCREENRESIZE->enable_screen_resize = false;
|
cfg::SCREENRESIZE->enable_screen_resize = false;
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
"Screen resize disabled");
|
||||||
} else {
|
} else {
|
||||||
// switch to scene
|
// switch to scene
|
||||||
cfg::SCREENRESIZE->enable_screen_resize = true;
|
cfg::SCREENRESIZE->enable_screen_resize = true;
|
||||||
cfg::SCREENRESIZE->screen_resize_current_scene = i;
|
cfg::SCREENRESIZE->screen_resize_current_scene = i;
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("Screen resize: scene {}", i + 1));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ typedef enum SPICE_SDK_LOG_LEVEL {
|
|||||||
SPICE_SDK_LOG_LEVEL_FATAL = 3,
|
SPICE_SDK_LOG_LEVEL_FATAL = 3,
|
||||||
} SPICE_SDK_LOG_LEVEL;
|
} SPICE_SDK_LOG_LEVEL;
|
||||||
|
|
||||||
|
typedef enum SPICE_SDK_TOAST_SEVERITY {
|
||||||
|
SPICE_SDK_TOAST_LEVEL_INFO = 0,
|
||||||
|
SPICE_SDK_TOAST_LEVEL_SUCCESS = 1,
|
||||||
|
SPICE_SDK_TOAST_LEVEL_WARNING = 2,
|
||||||
|
SPICE_SDK_TOAST_LEVEL_ERROR = 3,
|
||||||
|
} SPICE_SDK_TOAST_SEVERITY;
|
||||||
|
|
||||||
typedef struct SPICE_SDK_TOUCH_POINT {
|
typedef struct SPICE_SDK_TOUCH_POINT {
|
||||||
uint32_t id;
|
uint32_t id;
|
||||||
int x;
|
int x;
|
||||||
@@ -219,6 +226,17 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_keypad_func)(
|
|||||||
char key
|
char key
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// add_toast (v0.2 and up)
|
||||||
|
// adds an overlay toast notification
|
||||||
|
//
|
||||||
|
// severity: see SPICE_SDK_TOAST_SEVERITY; controls the accent color (purely cosmetic)
|
||||||
|
// text: null-terminated UTF-8 message to display; wraps inside the toast
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
|
||||||
|
SPICE_SDK_TOAST_SEVERITY severity,
|
||||||
|
const char *text
|
||||||
|
);
|
||||||
|
|
||||||
typedef struct SPICE_SDK_V0 {
|
typedef struct SPICE_SDK_V0 {
|
||||||
uint32_t size;
|
uint32_t size;
|
||||||
|
|
||||||
@@ -242,6 +260,8 @@ typedef struct SPICE_SDK_V0 {
|
|||||||
spice_sdk_insert_card_func *insert_card;
|
spice_sdk_insert_card_func *insert_card;
|
||||||
spice_sdk_set_keypad_func *set_keypad;
|
spice_sdk_set_keypad_func *set_keypad;
|
||||||
|
|
||||||
|
spice_sdk_add_toast_func *add_toast;
|
||||||
|
|
||||||
} SPICE_SDK_V0;
|
} SPICE_SDK_V0;
|
||||||
|
|
||||||
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
||||||
|
|||||||
@@ -84,7 +84,15 @@ static void worker_thread_main(std::stop_token stop_token) {
|
|||||||
spice.set_button(arrow.button, true, 1.f);
|
spice.set_button(arrow.button, true, 1.f);
|
||||||
|
|
||||||
if (!arrow.previous_state) {
|
if (!arrow.previous_state) {
|
||||||
|
|
||||||
|
if (spice.add_toast) {
|
||||||
|
spice.add_toast(
|
||||||
|
SPICE_SDK_TOAST_LEVEL_INFO,
|
||||||
|
std::format("let me hear you say: {}", arrow.name).c_str());
|
||||||
|
} else {
|
||||||
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
|
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
arrow.previous_state = true;
|
arrow.previous_state = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,59 +108,59 @@ static unsigned __stdcall worker_thread(void *arg) {
|
|||||||
phase += 1;
|
phase += 1;
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 1:
|
case 1:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get buttons...");
|
||||||
get_buttons();
|
get_buttons();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set buttons...");
|
||||||
set_buttons();
|
set_buttons();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear buttons...");
|
||||||
clear_buttons();
|
clear_buttons();
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get analogs...");
|
||||||
get_analogs();
|
get_analogs();
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set analogs...");
|
||||||
set_analogs();
|
set_analogs();
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear analogs...");
|
||||||
clear_analogs();
|
clear_analogs();
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 7:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "get lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get lights...");
|
||||||
get_lights();
|
get_lights();
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set lights...");
|
||||||
set_lights();
|
set_lights();
|
||||||
break;
|
break;
|
||||||
case 9:
|
case 9:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear lights...");
|
||||||
clear_lights();
|
clear_lights();
|
||||||
break;
|
break;
|
||||||
case 10:
|
case 10:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set touch...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "set touch...");
|
||||||
set_touch();
|
set_touch();
|
||||||
break;
|
break;
|
||||||
case 11:
|
case 11:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear touch...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear touch...");
|
||||||
clear_touch();
|
clear_touch();
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "insert card...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "insert card...");
|
||||||
insert_card();
|
insert_card();
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 13:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "set keypad...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "set keypad...");
|
||||||
set_keypad();
|
set_keypad();
|
||||||
break;
|
break;
|
||||||
case 14:
|
case 14:
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "clear keypad...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear keypad...");
|
||||||
clear_keypad();
|
clear_keypad();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "sdk/include/spicesdk.h"
|
#include "sdk/include/spicesdk.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -29,6 +30,7 @@ static spice_sdk_set_touch_func sdk_set_touch;
|
|||||||
static spice_sdk_clear_touch_func sdk_clear_touch;
|
static spice_sdk_clear_touch_func sdk_clear_touch;
|
||||||
static spice_sdk_insert_card_func sdk_insert_card;
|
static spice_sdk_insert_card_func sdk_insert_card;
|
||||||
static spice_sdk_set_keypad_func sdk_set_keypad;
|
static spice_sdk_set_keypad_func sdk_set_keypad;
|
||||||
|
static spice_sdk_add_toast_func sdk_add_toast;
|
||||||
|
|
||||||
struct SdkModule {
|
struct SdkModule {
|
||||||
std::string dll;
|
std::string dll;
|
||||||
@@ -169,6 +171,12 @@ sdk_init(
|
|||||||
v0->insert_card = sdk_insert_card;
|
v0->insert_card = sdk_insert_card;
|
||||||
v0->set_keypad = sdk_set_keypad;
|
v0->set_keypad = sdk_set_keypad;
|
||||||
// end of 0.1
|
// end of 0.1
|
||||||
|
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
|
||||||
|
v0->add_toast = sdk_add_toast;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of 0.2
|
||||||
// any newer minor iterations will need to check the size
|
// any newer minor iterations will need to check the size
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -594,5 +602,43 @@ sdk_set_keypad(
|
|||||||
return SPICE_SDK_STATUS_SUCCESS;
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_add_toast(
|
||||||
|
SPICE_SDK_TOAST_SEVERITY severity,
|
||||||
|
const char *text
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay::notifications::Severity sev;
|
||||||
|
switch (severity) {
|
||||||
|
case SPICE_SDK_TOAST_LEVEL_INFO:
|
||||||
|
sev = overlay::notifications::Severity::Info;
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_TOAST_LEVEL_SUCCESS:
|
||||||
|
sev = overlay::notifications::Severity::Success;
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_TOAST_LEVEL_WARNING:
|
||||||
|
sev = overlay::notifications::Severity::Warning;
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_TOAST_LEVEL_ERROR:
|
||||||
|
sev = overlay::notifications::Severity::Error;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay::notifications::add(sev, text);
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace sdk
|
} // namespace sdk
|
||||||
@@ -26,6 +26,14 @@ bool fileutils::file_exists(const std::filesystem::path &file_path) {
|
|||||||
return file_exists(file_path.c_str());
|
return file_exists(file_path.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string fileutils::basename(std::string_view path) {
|
||||||
|
const auto slash = path.find_last_of("\\/");
|
||||||
|
if (slash == std::string_view::npos) {
|
||||||
|
return std::string(path);
|
||||||
|
}
|
||||||
|
return std::string(path.substr(slash + 1));
|
||||||
|
}
|
||||||
|
|
||||||
bool fileutils::verify_header_pe(const std::filesystem::path &file_path) {
|
bool fileutils::verify_header_pe(const std::filesystem::path &file_path) {
|
||||||
if (!file_exists(file_path)) {
|
if (!file_exists(file_path)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,4 +35,6 @@ namespace fileutils {
|
|||||||
|
|
||||||
std::filesystem::path get_config_file_path(const std::string module, const std::string filename, bool* file_exists=nullptr);
|
std::filesystem::path get_config_file_path(const std::string module, const std::string filename, bool* file_exists=nullptr);
|
||||||
bool write_config_file(const std::string_view &module, const std::filesystem::path path, std::string text);
|
bool write_config_file(const std::string_view &module, const std::filesystem::path path, std::string text);
|
||||||
|
|
||||||
|
std::string basename(std::string_view path);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user