From 7629280406e212cb05890002cd5f87bfe8be3330 Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 22 Feb 2026 00:52:10 -0500 Subject: [PATCH 1/3] cl_damageflash_spectator --- docs/CHANGELOG.md | 2 +- game_patch/misc/alpine_settings.cpp | 5 ++++ game_patch/misc/alpine_settings.h | 1 + game_patch/misc/player.cpp | 39 ++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b2003197..fbcc4dc3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -50,7 +50,7 @@ Version 1.3.0 (Bakeapple): Not yet released [@nickalreadyinuse](https://github.com/nickalreadyinuse) - Optimize network performance for `af_obj_update` packets and bot decommission logic - +- Implement damage screen flash in first person spectate mode. `cl_damageflash_spectator` toggles. Version 1.2.2 (Willow): Released Jan-04-2026 -------------------------------- ### Minor features, changes, and enhancements diff --git a/game_patch/misc/alpine_settings.cpp b/game_patch/misc/alpine_settings.cpp index 2bbac2b4..f0f12b9c 100644 --- a/game_patch/misc/alpine_settings.cpp +++ b/game_patch/misc/alpine_settings.cpp @@ -346,6 +346,10 @@ bool alpine_player_settings_load(rf::Player* player) g_alpine_game_config.damage_screen_flash = std::stoi(settings["DamageScreenFlash"]); processed_keys.insert("DamageScreenFlash"); } + if (settings.count("SpectateDamageScreenFlash")) { + g_alpine_game_config.spectate_damage_screen_flash = std::stoi(settings["SpectateDamageScreenFlash"]); + processed_keys.insert("SpectateDamageScreenFlash"); + } if (settings.count("ExplosionFlashLightsWeapons")) { g_alpine_game_config.explosion_weapon_flash_lights = std::stoi(settings["ExplosionFlashLightsWeapons"]); processed_keys.insert("ExplosionFlashLightsWeapons"); @@ -1115,6 +1119,7 @@ void alpine_player_settings_save(rf::Player* player) file << "NeverAutoswitchExplosives=" << player->settings.dont_autoswitch_to_explosives << "\n"; file << "ToggleCrouch=" << player->settings.toggle_crouch << "\n"; file << "DamageScreenFlash=" << g_alpine_game_config.damage_screen_flash << "\n"; + file << "SpectateDamageScreenFlash=" << g_alpine_game_config.spectate_damage_screen_flash << "\n"; file << "ExplosionFlashLightsWeapons=" << g_alpine_game_config.explosion_weapon_flash_lights << "\n"; file << "ExplosionFlashLightsEnv=" << g_alpine_game_config.explosion_env_flash_lights << "\n"; file << "BurningEntityLights=" << g_alpine_game_config.burning_entity_lights << "\n"; diff --git a/game_patch/misc/alpine_settings.h b/game_patch/misc/alpine_settings.h index bc59d432..0826cf20 100644 --- a/game_patch/misc/alpine_settings.h +++ b/game_patch/misc/alpine_settings.h @@ -110,6 +110,7 @@ struct AlpineGameSettings bool show_run_timer = true; bool multi_ricochet = false; bool damage_screen_flash = true; + bool spectate_damage_screen_flash = true; bool explosion_weapon_flash_lights = true; bool explosion_env_flash_lights = true; bool burning_entity_lights = true; diff --git a/game_patch/misc/player.cpp b/game_patch/misc/player.cpp index dc151ceb..4a30d20d 100644 --- a/game_patch/misc/player.cpp +++ b/game_patch/misc/player.cpp @@ -36,6 +36,11 @@ static rf::PlayerHeadlampSettings g_local_headlamp_settings; +// Spectator damage flash tracking +static float g_spectate_last_health = -1.0f; +static float g_spectate_last_armor = -1.0f; +static rf::Player* g_spectate_last_target = nullptr; + void set_headlamp_toggle_enabled(bool enabled) { g_headlamp_toggle_enabled = enabled; @@ -487,9 +492,31 @@ FunHook players_do_frame_hook{ []() { players_do_frame_hook.call_target(); if (multi_spectate_is_spectating()) { - rf::hud_do_frame(multi_spectate_get_target_player()); + rf::Player* target = multi_spectate_get_target_player(); + rf::hud_do_frame(target); + + // Spectator damage flash: detect health or armor decreases on the spectated player + if (target && g_alpine_game_config.spectate_damage_screen_flash) { + rf::Entity* target_entity = rf::entity_from_handle(target->entity_handle); + if (target != g_spectate_last_target) { + g_spectate_last_target = target; + g_spectate_last_health = target_entity ? target_entity->life : -1.0f; + g_spectate_last_armor = target_entity ? target_entity->armor : -1.0f; + } + else if (target_entity && g_spectate_last_health >= 0.0f) { + if (target_entity->life < g_spectate_last_health + || target_entity->armor < g_spectate_last_armor) { + rf::local_screen_flash(rf::local_player, 255, 0, 0, 128); + } + g_spectate_last_health = target_entity->life; + g_spectate_last_armor = target_entity->armor; + } + } } else { + g_spectate_last_target = nullptr; + g_spectate_last_health = -1.0f; + g_spectate_last_armor = -1.0f; local_delayed_spawn_do_frame(); // try to spawn if a delayed spawn is queued } }, @@ -513,6 +540,15 @@ ConsoleCommand2 damage_screen_flash_cmd{ "Toggle damage screen flash effect", }; +ConsoleCommand2 spectate_damage_screen_flash_cmd{ + "cl_damageflash_spectator", + []() { + g_alpine_game_config.spectate_damage_screen_flash = !g_alpine_game_config.spectate_damage_screen_flash; + rf::console::print("Spectator damage screen flash effect is {}", g_alpine_game_config.spectate_damage_screen_flash ? "enabled" : "disabled"); + }, + "Toggle damage screen flash effect while spectating", +}; + ConsoleCommand2 weapon_explosion_flash_lights_cmd{ "cl_explosionflashweapons", []() { @@ -926,6 +962,7 @@ void player_do_patch() // Commands damage_screen_flash_cmd.register_cmd(); + spectate_damage_screen_flash_cmd.register_cmd(); weapon_explosion_flash_lights_cmd.register_cmd(); env_explosion_flash_lights_cmd.register_cmd(); burning_entity_lights_cmd.register_cmd(); From 12d5d43d1f0b25c90b94e34f2fb9ab55978b7a86 Mon Sep 17 00:00:00 2001 From: nick Date: Mon, 23 Feb 2026 15:32:49 -0500 Subject: [PATCH 2/3] inject obj_update to detect hits --- game_patch/misc/player.cpp | 26 -------------------------- game_patch/multi/network.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/game_patch/misc/player.cpp b/game_patch/misc/player.cpp index 4a30d20d..2c87550b 100644 --- a/game_patch/misc/player.cpp +++ b/game_patch/misc/player.cpp @@ -36,11 +36,6 @@ static rf::PlayerHeadlampSettings g_local_headlamp_settings; -// Spectator damage flash tracking -static float g_spectate_last_health = -1.0f; -static float g_spectate_last_armor = -1.0f; -static rf::Player* g_spectate_last_target = nullptr; - void set_headlamp_toggle_enabled(bool enabled) { g_headlamp_toggle_enabled = enabled; @@ -494,29 +489,8 @@ FunHook players_do_frame_hook{ if (multi_spectate_is_spectating()) { rf::Player* target = multi_spectate_get_target_player(); rf::hud_do_frame(target); - - // Spectator damage flash: detect health or armor decreases on the spectated player - if (target && g_alpine_game_config.spectate_damage_screen_flash) { - rf::Entity* target_entity = rf::entity_from_handle(target->entity_handle); - if (target != g_spectate_last_target) { - g_spectate_last_target = target; - g_spectate_last_health = target_entity ? target_entity->life : -1.0f; - g_spectate_last_armor = target_entity ? target_entity->armor : -1.0f; - } - else if (target_entity && g_spectate_last_health >= 0.0f) { - if (target_entity->life < g_spectate_last_health - || target_entity->armor < g_spectate_last_armor) { - rf::local_screen_flash(rf::local_player, 255, 0, 0, 128); - } - g_spectate_last_health = target_entity->life; - g_spectate_last_armor = target_entity->armor; - } - } } else { - g_spectate_last_target = nullptr; - g_spectate_last_health = -1.0f; - g_spectate_last_armor = -1.0f; local_delayed_spawn_do_frame(); // try to spawn if a delayed spawn is queued } }, diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index 9b1237d4..d039b602 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -31,6 +31,7 @@ #include "server_internal.h" #include "../main/main.h" #include "../hud/hud.h" +#include "../hud/multi_spectate.h" #include "../rf/multi.h" #include "../rf/misc.h" #include "../rf/player/player.h" @@ -903,6 +904,26 @@ CodeInjection process_obj_update_check_flags_injection{ }, }; +// Trigger spectator damage screen flash when the spectated player's health or armor decreases. +// Injected at the point in process_obj_update_packet where a health/armor decrease has been +// confirmed (OUF_HEALTH_ARMOR set, new value < entity value) but before the local-player check. +// edi = entity pointer at this address. +CodeInjection process_obj_update_health_armor_spectate_injection{ + 0x0047E4DA, + [](auto& regs) { + if (!g_alpine_game_config.spectate_damage_screen_flash || rf::is_server) + return; + rf::Entity* entity = regs.edi; + if (!entity) + return; + rf::Player* spectated = multi_spectate_get_target_player(); + if (!spectated || spectated == rf::local_player) + return; + if (entity == rf::entity_from_handle(spectated->entity_handle)) + rf::local_screen_flash(rf::local_player, 255, 0, 0, 128); + }, +}; + CodeInjection process_obj_update_weapon_fire_injection{ 0x0047E2FF, [](auto& regs) { @@ -2378,6 +2399,9 @@ void network_init() // Fix obj_update packet handling process_obj_update_check_flags_injection.install(); + // Spectator damage screen flash via obj_update health/armor + process_obj_update_health_armor_spectate_injection.install(); + // Verify on/off weapons handling process_obj_update_weapon_fire_injection.install(); From 0fddeea02c1276c86bb42b54bf891108f078e892 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Tue, 24 Feb 2026 00:39:11 -0330 Subject: [PATCH 3/3] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- game_patch/multi/network.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index d039b602..ccb17c66 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -917,7 +917,7 @@ CodeInjection process_obj_update_health_armor_spectate_injection{ if (!entity) return; rf::Player* spectated = multi_spectate_get_target_player(); - if (!spectated || spectated == rf::local_player) + if (!spectated || spectated == rf::local_player || multi_spectate_is_freelook()) return; if (entity == rf::entity_from_handle(spectated->entity_handle)) rf::local_screen_flash(rf::local_player, 255, 0, 0, 128);