From c91f74d4d2c0101381b3a49da4a9746e5ea449b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 22:48:59 +0000 Subject: [PATCH] Add active game name source --- CMakeLists.txt | 1 + README.md | 1 + src/common/types.h | 19 ++++ src/integrations/monitoring_service.c | 20 ++++ src/integrations/monitoring_service.h | 13 +++ src/io/state.c | 79 +++++++++++++++ src/io/state.h | 32 ++++++ src/main.c | 3 + src/sources/game_name.c | 139 ++++++++++++++++++++++++++ src/sources/game_name.h | 24 +++++ 10 files changed, 331 insertions(+) create mode 100644 src/sources/game_name.c create mode 100644 src/sources/game_name.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b35bf2e..2630b74 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -479,6 +479,7 @@ target_sources( src/main.c src/sources/gamerpic.c src/sources/game_cover.c + src/sources/game_name.c src/sources/gamerscore.c src/sources/gamertag.c src/sources/achievement_name.c diff --git a/README.md b/README.md index 3387632..3808c1a 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Each text and image source exposes an **Auto show/hide** toggle in its propertie #### Game - **Game Cover**: image source for the currently active game's cover art +- **Game Name**: text source for the currently active game's title #### Achievements diff --git a/src/common/types.h b/src/common/types.h index 18ce3c5..3ea7edc 100644 --- a/src/common/types.h +++ b/src/common/types.h @@ -161,6 +161,25 @@ typedef struct gamertag_configuration { auto_visibility_config_t auto_visibility; } gamertag_configuration_t; +/** + * @brief Configuration used by the active game name overlay/renderer. + * + * Ownership: + * - Strings are treated as borrowed pointers unless otherwise documented by the + * caller. + */ +typedef struct game_name_configuration { + const char *font_face; + const char *font_style; + /** Font size in pixels (height passed to FreeType). */ + uint32_t font_size; + /** Top gradient color in 0xRRGGBBAA format. */ + uint32_t top_color; + /** Bottom gradient color in 0xRRGGBBAA format. */ + uint32_t bottom_color; + auto_visibility_config_t auto_visibility; +} game_name_configuration_t; + /** * @brief Configuration used by the achievement name overlay/renderer. * diff --git a/src/integrations/monitoring_service.c b/src/integrations/monitoring_service.c index 1f4060b..1226c67 100644 --- a/src/integrations/monitoring_service.c +++ b/src/integrations/monitoring_service.c @@ -94,6 +94,22 @@ static game_t *g_retro_game = NULL; */ static identity_source_t g_last_game_source = IDENTITY_SOURCE_XBOX; +static const game_t *get_current_active_game(void) { + if (g_last_game_source == IDENTITY_SOURCE_XBOX) { + if (g_xbox_game) + return g_xbox_game; + if (g_retro_game) + return g_retro_game; + } else { + if (g_retro_game) + return g_retro_game; + if (g_xbox_game) + return g_xbox_game; + } + + return NULL; +} + static const identity_t *get_current_active_identity(void) { /* When both sources have an active game, the one that reported a game * most recently takes priority. */ @@ -647,6 +663,10 @@ const identity_t *monitoring_get_current_active_identity(void) { return get_current_active_identity(); } +const game_t *monitoring_get_current_active_game(void) { + return get_current_active_game(); +} + const achievement_t *monitoring_get_current_game_achievements(void) { return g_current_achievements; } diff --git a/src/integrations/monitoring_service.h b/src/integrations/monitoring_service.h index 69f8b7f..eb97097 100644 --- a/src/integrations/monitoring_service.h +++ b/src/integrations/monitoring_service.h @@ -158,6 +158,19 @@ void monitoring_subscribe_session_ready(on_monitoring_session_ready_t callback); */ const identity_t *monitoring_get_current_active_identity(void); +/** + * @brief Get the currently active game, if any. + * + * Returns the same game that would be delivered to game-played subscribers + * right now. + * + * Ownership/lifetime: the returned pointer is owned by the monitoring service + * and may be replaced on the next game update. Do not free it. + * + * @return The active game, or NULL if no session is established. + */ +const game_t *monitoring_get_current_active_game(void); + /** * @brief Get the cached generic achievements list for the current game. * diff --git a/src/io/state.c b/src/io/state.c index 51edab4..e05f431 100644 --- a/src/io/state.c +++ b/src/io/state.c @@ -48,6 +48,16 @@ #define GAMERTAG_CONFIGURATION_AUTO_VISIBILITY_HIDE_DURATION "source_gamertag_auto_visibility_hide_duration" #define GAMERTAG_CONFIGURATION_AUTO_VISIBILITY_FADE_DURATION "source_gamertag_auto_visibility_fade_duration" +#define GAME_NAME_CONFIGURATION_TOP_COLOR "source_game_name_top_color" +#define GAME_NAME_CONFIGURATION_BOTTOM_COLOR "source_game_name_bottom_color" +#define GAME_NAME_CONFIGURATION_SIZE "source_game_name_size" +#define GAME_NAME_CONFIGURATION_FONT_FACE "source_game_name_font_face" +#define GAME_NAME_CONFIGURATION_FONT_STYLE "source_game_name_font_style" +#define GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_ENABLED "source_game_name_auto_visibility_enabled" +#define GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_SHOW_DURATION "source_game_name_auto_visibility_show_duration" +#define GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_HIDE_DURATION "source_game_name_auto_visibility_hide_duration" +#define GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_FADE_DURATION "source_game_name_auto_visibility_fade_duration" + #define ACHIEVEMENT_NAME_CONFIGURATION_ACTIVE_TOP_COLOR "source_achievement_name_active_top_color" #define ACHIEVEMENT_NAME_CONFIGURATION_ACTIVE_BOTTOM_COLOR "source_achievement_name_active_bottom_color" #define ACHIEVEMENT_NAME_CONFIGURATION_INACTIVE_TOP_COLOR "source_achievement_name_inactive_top_color" @@ -514,6 +524,67 @@ gamertag_configuration_t *state_get_gamertag_configuration() { return configuration; } +void state_set_game_name_configuration(const game_name_configuration_t *configuration) { + + if (!configuration) { + return; + } + + obs_data_set_int(g_state, GAME_NAME_CONFIGURATION_TOP_COLOR, configuration->top_color); + obs_data_set_int(g_state, GAME_NAME_CONFIGURATION_BOTTOM_COLOR, configuration->bottom_color); + obs_data_set_int(g_state, GAME_NAME_CONFIGURATION_SIZE, configuration->font_size); + obs_data_set_string(g_state, GAME_NAME_CONFIGURATION_FONT_FACE, configuration->font_face); + obs_data_set_string(g_state, GAME_NAME_CONFIGURATION_FONT_STYLE, configuration->font_style); + obs_data_set_bool(g_state, GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_ENABLED, configuration->auto_visibility.enabled); + obs_data_set_double(g_state, + GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_SHOW_DURATION, + configuration->auto_visibility.show_duration); + obs_data_set_double(g_state, + GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_HIDE_DURATION, + configuration->auto_visibility.hide_duration); + obs_data_set_double(g_state, + GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_FADE_DURATION, + configuration->auto_visibility.fade_duration); + + save_state(g_state); +} + +game_name_configuration_t *state_get_game_name_configuration() { + + uint32_t top_color = (uint32_t)obs_data_get_int(g_state, GAME_NAME_CONFIGURATION_TOP_COLOR); + uint32_t bottom_color = (uint32_t)obs_data_get_int(g_state, GAME_NAME_CONFIGURATION_BOTTOM_COLOR); + uint32_t size = (uint32_t)obs_data_get_int(g_state, GAME_NAME_CONFIGURATION_SIZE); + const char *font_face = obs_data_get_string(g_state, GAME_NAME_CONFIGURATION_FONT_FACE); + const char *font_style = obs_data_get_string(g_state, GAME_NAME_CONFIGURATION_FONT_STYLE); + bool auto_visibility_enabled = obs_data_get_bool(g_state, GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_ENABLED); + float auto_visibility_show_duration = + (float)obs_data_get_double(g_state, GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_SHOW_DURATION); + float auto_visibility_hide_duration = + (float)obs_data_get_double(g_state, GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_HIDE_DURATION); + float auto_visibility_fade_duration = + (float)obs_data_get_double(g_state, GAME_NAME_CONFIGURATION_AUTO_VISIBILITY_FADE_DURATION); + + game_name_configuration_t *configuration = bzalloc(sizeof(game_name_configuration_t)); + + configuration->top_color = top_color == 0 ? 0xFFFFFFFF : top_color; + configuration->bottom_color = bottom_color == 0 ? 0xFFFFFFFF : bottom_color; + configuration->font_size = size == 0 ? 48 : size; + configuration->font_face = bstrdup(font_face); + configuration->font_style = bstrdup(font_style); + configuration->auto_visibility.enabled = auto_visibility_enabled; + configuration->auto_visibility.show_duration = auto_visibility_show_duration > 0.0f + ? auto_visibility_show_duration + : AUTO_VISIBILITY_DEFAULT_SHARED_SHOW_DURATION; + configuration->auto_visibility.hide_duration = auto_visibility_hide_duration > 0.0f + ? auto_visibility_hide_duration + : AUTO_VISIBILITY_DEFAULT_SHARED_HIDE_DURATION; + configuration->auto_visibility.fade_duration = auto_visibility_fade_duration > 0.0f + ? auto_visibility_fade_duration + : AUTO_VISIBILITY_DEFAULT_SHARED_FADE_DURATION; + + return configuration; +} + void state_set_achievement_name_configuration(const achievement_name_configuration_t *configuration) { if (!configuration) { @@ -837,6 +908,14 @@ void state_free_gamertag_configuration(gamertag_configuration_t **config) { free_memory((void **)config); } +void state_free_game_name_configuration(game_name_configuration_t **config) { + if (!config || !*config) { + return; + } + + free_memory((void **)config); +} + void state_free_achievement_name_configuration(achievement_name_configuration_t **config) { if (!config || !*config) { return; diff --git a/src/io/state.h b/src/io/state.h index edd33ec..ad7a81f 100644 --- a/src/io/state.h +++ b/src/io/state.h @@ -158,6 +158,28 @@ void state_set_gamertag_configuration(const gamertag_configuration_t *configurat */ gamertag_configuration_t *state_get_gamertag_configuration(); +/** + * @brief Set the active game name source configuration. + * + * Stores the configuration for the active game name display source, including + * font path, text size, color, and alignment. The configuration is persisted to disk. + * + * @param configuration Configuration to store (may be NULL to clear). + */ +void state_set_game_name_configuration(const game_name_configuration_t *configuration); + +/** + * @brief Get the currently stored active game name source configuration. + * + * Retrieves the configuration with default values if none has been set: + * - Default color: 0xFFFFFF (white) + * - Default size: 12 pixels + * + * @return Newly allocated configuration structure. Caller must free it with + * state_free_game_name_configuration(). + */ +game_name_configuration_t *state_get_game_name_configuration(); + /** * @brief Set the achievement name source configuration. * @@ -344,6 +366,16 @@ void state_free_gamerscore_configuration(gamerscore_configuration_t **config); */ void state_free_gamertag_configuration(gamertag_configuration_t **config); +/** + * @brief Free a game name configuration structure and its contents. + * + * Frees the font strings and the configuration structure itself. + * Safe to call with NULL. + * + * @param config Configuration structure to free. Set to NULL after freeing. + */ +void state_free_game_name_configuration(game_name_configuration_t **config); + /** * @brief Free an achievement name configuration structure and its contents. * diff --git a/src/main.c b/src/main.c index e630fc7..1798688 100644 --- a/src/main.c +++ b/src/main.c @@ -8,6 +8,7 @@ #include "sources/game_cover.h" #include "sources/gamerscore.h" #include "sources/gamertag.h" +#include "sources/game_name.h" #include "io/state.h" #include "sources/achievement_name.h" @@ -32,6 +33,7 @@ bool obs_module_load(void) { game_cover_source_register(); xbox_gamerscore_source_register(); xbox_gamertag_source_register(); + xbox_game_name_source_register(); /* Initialize the shared achievement display cycle before registering achievement sources */ achievement_cycle_init(); @@ -76,6 +78,7 @@ void obs_module_unload(void) { xbox_gamerpic_source_cleanup(); xbox_gamerscore_source_cleanup(); xbox_gamertag_source_cleanup(); + xbox_game_name_source_cleanup(); monitoring_stop(); io_cleanup(); diff --git a/src/sources/game_name.c b/src/sources/game_name.c new file mode 100644 index 0000000..b467570 --- /dev/null +++ b/src/sources/game_name.c @@ -0,0 +1,139 @@ +#include "sources/game_name.h" + +#include "sources/common/text_source.h" +#include "sources/common/visibility_cycle.h" + +#include + +#include "common/game.h" +#include "integrations/monitoring_service.h" +#include "io/state.h" + +static char g_game_name[512]; +static bool g_must_reload; + +static game_name_configuration_t *g_configuration; +static text_source_config_t g_render_config; + +static void update_render_config(void) { + g_render_config.font_face = g_configuration->font_face; + g_render_config.font_style = g_configuration->font_style; + g_render_config.font_size = g_configuration->font_size; + g_render_config.active_top_color = g_configuration->top_color; + g_render_config.active_bottom_color = g_configuration->bottom_color; + g_render_config.inactive_top_color = g_configuration->top_color; + g_render_config.inactive_bottom_color = g_configuration->bottom_color; + g_render_config.auto_visibility = g_configuration->auto_visibility; +} + +static void update_game_name(const game_t *game) { + if (!game || !game->title || game->title[0] == '\0') { + if (g_game_name[0] != '\0') { + g_game_name[0] = '\0'; + g_must_reload = true; + } + return; + } + + snprintf(g_game_name, sizeof(g_game_name), "%s", game->title); + g_must_reload = true; +} + +static void on_game_played(const game_t *game) { + update_game_name(game); +} + +static void *on_source_create(obs_data_t *settings, obs_source_t *source) { + UNUSED_PARAMETER(settings); + + update_game_name(monitoring_get_current_active_game()); + return text_source_create(source, "Game Name"); +} + +static void on_source_destroy(void *data) { + text_source_t *source = data; + if (source) { + text_source_destroy(source); + } +} + +static uint32_t source_get_width(void *data) { + return text_source_get_width(data); +} + +static uint32_t source_get_height(void *data) { + return text_source_get_height(data); +} + +static void on_source_update(void *data, obs_data_t *settings) { + UNUSED_PARAMETER(data); + + text_source_update_properties(settings, &g_render_config, &g_must_reload); + + g_configuration->font_face = g_render_config.font_face; + g_configuration->font_style = g_render_config.font_style; + g_configuration->font_size = g_render_config.font_size; + g_configuration->top_color = g_render_config.active_top_color; + g_configuration->bottom_color = g_render_config.active_bottom_color; + g_configuration->auto_visibility = g_render_config.auto_visibility; + + state_set_game_name_configuration(g_configuration); +} + +static void on_source_video_render(void *data, gs_effect_t *effect) { + text_source_t *source = data; + + if (text_source_update_text(source, &g_must_reload, &g_render_config, g_game_name, true)) { + text_source_render(source, &g_render_config, effect); + } +} + +static void on_source_video_tick(void *data, float seconds) { + text_source_tick(data, &g_render_config, seconds); +} + +static obs_properties_t *source_get_properties(void *data) { + UNUSED_PARAMETER(data); + + obs_properties_t *props = obs_properties_create(); + text_source_add_properties(props, false); + return props; +} + +static const char *source_get_name(void *unused) { + UNUSED_PARAMETER(unused); + + return "Game Name"; +} + +static struct obs_source_info xbox_game_name_source = { + .id = "xbox_game_name_source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = source_get_name, + .create = on_source_create, + .destroy = on_source_destroy, + .update = on_source_update, + .get_properties = source_get_properties, + .get_width = source_get_width, + .get_height = source_get_height, + .video_tick = on_source_video_tick, + .video_render = on_source_video_render, +}; + +void xbox_game_name_source_register(void) { + + g_configuration = state_get_game_name_configuration(); + state_set_game_name_configuration(g_configuration); + update_render_config(); + + obs_register_source(&xbox_game_name_source); + + auto_visibility_register_config(&g_render_config.auto_visibility); + + monitoring_subscribe_game_played(on_game_played); +} + +void xbox_game_name_source_cleanup(void) { + state_free_game_name_configuration(&g_configuration); +} diff --git a/src/sources/game_name.h b/src/sources/game_name.h new file mode 100644 index 0000000..8998532 --- /dev/null +++ b/src/sources/game_name.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @file game_name.h + * @brief OBS source type that renders the active game name. + */ + +/** + * @brief Register the "Game Name" source with OBS. + */ +void xbox_game_name_source_register(void); + +/** + * @brief Clean up resources allocated by the game name source. + */ +void xbox_game_name_source_cleanup(void); + +#ifdef __cplusplus +} +#endif