From 79e13abf029a89347e0b0975fb13cbdf5a940165 Mon Sep 17 00:00:00 2001
From: notkoen <45914779+notkoen@users.noreply.github.com>
Date: Sat, 4 Jul 2026 08:38:09 -0700
Subject: [PATCH 1/2] feat: bosshud
Co-Authored-By: tilgep <66904238+tilgep@users.noreply.github.com>
---
AMBuilder | 1 +
PackageScript | 2 +
cfg/cs2fixes/cs2fixes.cfg | 11 +
configs/bosshud/template.jsonc | 89 ++
src/bosshud.cpp | 2073 ++++++++++++++++++++++++++++++++
src/bosshud.h | 265 ++++
src/cs2fixes.cpp | 12 +
src/entitylistener.cpp | 4 +
src/events.cpp | 4 +
src/playermanager.cpp | 17 +
src/playermanager.h | 4 +
src/user_preferences.cpp | 5 +
src/utils/hud_manager.h | 1 +
13 files changed, 2488 insertions(+)
create mode 100644 configs/bosshud/template.jsonc
create mode 100644 src/bosshud.cpp
create mode 100644 src/bosshud.h
diff --git a/AMBuilder b/AMBuilder
index bb02216f2..4bba118d4 100644
--- a/AMBuilder
+++ b/AMBuilder
@@ -62,6 +62,7 @@ for sdk_target in MMSPlugin.sdk_targets:
'src/discord.cpp',
'src/map_votes.cpp',
'src/entwatch.cpp',
+ 'src/bosshud.cpp',
'src/user_preferences.cpp',
'src/zombiereborn.cpp',
'src/customio.cpp',
diff --git a/PackageScript b/PackageScript
index ab59251e6..efcd6dfe0 100644
--- a/PackageScript
+++ b/PackageScript
@@ -112,6 +112,7 @@ for task in MMSPlugin.binaries:
cfg_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'cfg', MMSPlugin.metadata['name']))
mapcfg_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'cfg', MMSPlugin.metadata['name'], 'maps'))
gamedata_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'gamedata'))
+ bosshud_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'configs', 'bosshud'))
builder.AddCopy(os.path.join('configs', 'admins.jsonc.example'), configs_folder)
builder.AddCopy(os.path.join('configs', 'discordbots.cfg.example'), configs_folder)
builder.AddCopy(os.path.join('configs', 'maplist.jsonc.example'), configs_folder)
@@ -122,6 +123,7 @@ for task in MMSPlugin.binaries:
builder.AddCopy(os.path.join('configs', 'zr', 'hitgroups.cfg.example'), zr_folder)
builder.AddCopy(os.path.join('configs', 'entwatch', 'maps', 'template.jsonc'), ew_maps_folder)
builder.AddCopy(os.path.join('gamedata', 'cs2fixes.jsonc'), gamedata_folder)
+ builder.AddCopy(os.path.join('configs', 'bosshud', 'template.jsonc'), bosshud_folder)
particles_cs2f_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'particles', MMSPlugin.metadata['name']))
builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'admin_beacon.vpcf_c'), particles_cs2f_folder)
diff --git a/cfg/cs2fixes/cs2fixes.cfg b/cfg/cs2fixes/cs2fixes.cfg
index b3819b0e4..6fa4a8de6 100644
--- a/cfg/cs2fixes/cs2fixes.cfg
+++ b/cfg/cs2fixes/cs2fixes.cfg
@@ -163,6 +163,17 @@ entwatch_score 9999 // Score to give item holders (0 = dont change score at
entwatch_glow 1000 // Distance that dropped item weapon glow will be visible (0 = glow disabled)
entwatch_glow_team 0 // Whether dropped item glow is only visible to the team the item belongs to (0 = glow to all players)
+// BossHud Settings
+bosshud_enable 1 // INCOMPATIBLE WITH CS#. Whether to enable BossHud features
+bosshud_simplehud 0 // Whether a simple BossHud is shown on maps without a config
+bosshud_scoreboard 1 // Whether boss hits are displayed as assists on scoreboard
+bosshud_tophits 1 // Whether top boss deamage is displayed after boss death
+bosshud_reward 0 // Money rewarded to players per boss hit
+bosshud_hitmarker 1 // Whether to enable hitmarkers for bosses
+bosshud_hitmarker_particle "particles/hitmarker/hitmarker_boss.vpcf" // The particle to use for boss hitmarker
+bosshud_rate 0.1 // How often does BossHud update
+bosshud_maxhp 500000 // Bosses with more than this HP will not start showing on the HUD (0.0 = no limit)
+
// Hud settings
cs2f_fix_hud_flashing 0 // Whether to fix hud flashing using a workaround, this BREAKS warmup so pick one or the other
cs2f_disable_hud_outside_round 0 // Whether to disable hud messages that would flash when a round is not ongoing, since flashing fix cannot run then
diff --git a/configs/bosshud/template.jsonc b/configs/bosshud/template.jsonc
new file mode 100644
index 000000000..4aad6f363
--- /dev/null
+++ b/configs/bosshud/template.jsonc
@@ -0,0 +1,89 @@
+//====================================================================================================
+// Notes:
+// - The type of boss is determined based on if "breakable" or "counter" is used
+// - You can specify entities using hammerid by starting with "#" followed by hammerid (ex. "#123456")
+// - Each separate boss needs a separate entry
+//====================================================================================================
+
+[
+ {
+ "name": "", // OPTIONAL - (string) Name of boss that appears in hud
+ "breakable": "", // Targetname/Hammerid of breakable
+ "counter": "", // Targetname/Hammerid of counter
+ "iterator": "", // OPTIONAL - Targetname/Hammerid of hp iterator (segments)
+ "backup": "", // OPTIONAL - Targetname/Hammerid of hp backup
+
+ "trigger": // OPTIONAL - Specifies the event that triggers the boss
+ {
+ "ent": "", // (string) Targetname/Hammerid of entity
+ "output": "", // (string) Output of entity
+ "delay": 0.0 // OPTIONAL - (float) Delay after output that starts boss
+ },
+
+ "showtrigger": // OPTIONAL - Specifies event that starts displaying boss health
+ {
+ "ent": "", // (string) Targetname/Hammerid of entity
+ "output": "", // (string) Output of entity
+ "delay": 0.0 // OPTIONAL - (float) Delay after event that shows boss health
+ },
+
+ "killtrigger": // OPTIONAL - Specifies event that force kills the boss
+ {
+ "ent": "", // (string) Targetname/Hammerid of entity
+ "output": "", // (string) Output of entity
+ "delay": 0.0 // OPTIONAL - (float) Delay after event that force kills boss
+ },
+
+ "hurttrigger": // OPTIONAL - Specifies event that is considered as damaging the boss
+ {
+ "ent": "", // (string) Targetname/hammerid of entity
+ "output": "" // (string) Output of entity
+ },
+
+ "reversecounter": false, // OPTIONAL - (bool) Whether counter should be reversed
+ "reverseiterator": false, // OPTIONAL - (bool) Whether iterator should be reversed
+ "hitmarkeronly": false, // OPTIONAL - (bool) Whether only hitmarkers should be shown when hitting boss
+ "minorhud": false, // OPTIONAL - (bool) Whether boss should should be displayed as no-bar hud variant
+ "multitrigger": false, // OPTIONAL - (bool) Whether boss can be triggered multiple times (multiple instances)
+ "templated": false, // OPTIONAL - (bool) Whether boss is templated and has name fixup
+ "showbeaten": true, // OPTIONAL - (bool) Whether top boss damage should be displayed after boss death
+ "timeout": 0.0, // OPTIONAL - (float) Specify time before boss health is hidden after taking no damage
+ "offset": 0.0, // OPTIONAL - (float) Specify amount of health to ADD to displayed health (negative to subtract)
+ "offsetiterator": 0.0, // OPTIONAL - (float) Specify amount of iterator segments to ADD to displayed health (negative to subtract)
+ "maxhp": 0.0 // OPTIONAL - (float) If the boss has more than this HP, it will not start showing on the HUD (0.0 = no limit)
+ },
+
+ // Breakable boss example
+ {
+ "name": "",
+ "breakable": ""
+ },
+
+ // Counter example
+ {
+ "name": "",
+ "counter": ""
+ },
+
+ // Counter, backup, and iterator example
+ {
+ "name": "",
+ "counter": "",
+ "backup": "",
+ "iterator": ""
+ },
+
+ // Counter and iterator example
+ {
+ "name": "",
+ "counter": "",
+ "iterator": ""
+ },
+
+ // Breakable and iterator example
+ {
+ "name": "",
+ "breakable": "",
+ "iterator": ""
+ }
+]
\ No newline at end of file
diff --git a/src/bosshud.cpp b/src/bosshud.cpp
new file mode 100644
index 000000000..15fe2be2f
--- /dev/null
+++ b/src/bosshud.cpp
@@ -0,0 +1,2073 @@
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2026 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#include "bosshud.h"
+#include "commands.h"
+#include "detours.h"
+#include "entity/cbaseentity.h"
+#include "entity/cmathcounter.h"
+#include "entwatch.h"
+#include "hud_manager.h"
+#include "playermanager.h"
+#include "recipientfilters.h"
+#include
+#undef snprintf
+#include "vendor/nlohmann/json.hpp"
+
+extern CGlobalVars* GetGlobals();
+
+CBossHudHandler* g_pBossHudHandler = nullptr;
+CSimpleHudHandler* g_pSimpleHudHandler = nullptr;
+
+std::weak_ptr m_pBossHudTimer;
+std::weak_ptr m_pAssistUpdateTimer;
+#define BHUD_ASSIST_TIMER_RATE 1.0f
+
+std::set g_setBossEnts = {
+ "math_counter",
+ "func_breakable",
+ "func_physbox"};
+
+std::set g_setBossOutputs = {
+ "OutValue",
+ "OnHealthChanged"};
+
+CConVar g_cvarBossHudEnable("bosshud_enable", FCVAR_NONE, "INCOMPATIBLE WITH CS#. Whether to enable BossHud features", false);
+CConVar g_cvarBossHudSimpleHud("bosshud_simplehud", FCVAR_NONE, "Whether a simple BossHud is shown on maps without a valid config", true);
+CConVar g_cvarBossHudScoreboard("bosshud_scoreboard", FCVAR_NONE, "Whether boss damage dealt is displayed as assists on scoreboard", false);
+CConVar g_cvarBossHudTopHits("bosshud_tophits", FCVAR_NONE, "Whether top boss deamage is displayed after boss death", true);
+CConVar g_cvarBossHudReward("bosshud_reward", FCVAR_NONE, "Money rewarded to players per boss hit", 0, true, 0, false, 0);
+CConVar g_cvarBossHudHitmarker("bosshud_hitmarker", FCVAR_NONE, "Whether to enable hitmarkers for bosses", true);
+CConVar g_cvarBossHudHitmarkerParticle("bosshud_hitmarker_particle", FCVAR_NONE, "The particle to use for boss hitmarker", "particles/hitmarker/hitmarker_boss.vpcf");
+CConVar g_cvarBossHudRate("bosshud_rate", FCVAR_NONE, "How often does BossHUD update", 0.1, true, 0.0, false, 0.0);
+CConVar g_cvarBossHudMaxHp("bosshud_maxhp", FCVAR_NONE, "Bosses with more than this HP will not start showing on the HUD (0.0 = no limit)", 500000.0, true, 0.0, false, 0.0);
+
+void CBossHudHandler::UnloadConfig()
+{
+ if (!bLoadedConfig)
+ return;
+
+ if (BossHud_IsFireOutputHooked())
+ mapIOFunctions.erase("bosshud");
+
+ vecBossConfig.clear();
+ vecBossInstance.clear();
+ bLoadedConfig = false;
+}
+
+void CBossHudHandler::LoadConfig(const char* sMapName)
+{
+ UnloadConfig();
+
+ char szPath[MAX_PATH];
+
+ V_snprintf(szPath, sizeof(szPath), "%s%s%s.jsonc", Plat_GetGameDirectory(), "/csgo/addons/cs2fixes/configs/bosshud/", sMapName);
+ std::ifstream jsoncFile(szPath);
+
+ if (!jsoncFile.is_open())
+ return;
+
+ ordered_json jsonItems = ordered_json::parse(jsoncFile, nullptr, false, true);
+ if (jsonItems.is_discarded())
+ {
+ Panic("[BossHUD] Error parsing json! %s\n", szPath);
+ return;
+ }
+
+ for (auto& [key, jsonItemData] : jsonItems.items())
+ {
+ std::shared_ptr boss = std::make_shared(jsonItemData, vecBossConfig.size());
+
+ if (boss->type == BossHudType::Invalid)
+ {
+ Panic("[BossHUD] Boss without a valid type (name: %s)\n", boss->szBossName.c_str());
+ continue;
+ }
+
+ vecBossConfig.push_back(boss);
+ }
+
+ if (vecBossConfig.size() > 0)
+ {
+ // Hook FireOutput
+ if (!SetupFireOutputInternalDetour())
+ mapIOFunctions.erase("bosshud");
+ else if (!BossHud_IsFireOutputHooked())
+ mapIOFunctions["bosshud"] = BossHud_FireOutput;
+ }
+
+ // Make sure simple hud and hud timer are stopped
+ if (g_pSimpleHudHandler)
+ g_pSimpleHudHandler->ClearSimpleHud();
+
+ if (!m_pBossHudTimer.expired())
+ m_pBossHudTimer.lock()->Cancel();
+
+ if (!m_pAssistUpdateTimer.expired())
+ m_pAssistUpdateTimer.lock()->Cancel();
+
+ bLoadedConfig = true;
+}
+
+BossConfig::BossConfig(int bossId)
+{
+ if (!g_pBossHudHandler || g_pBossHudHandler->vecBossConfig.size() <= bossId || bossId < 0)
+ return;
+
+ std::shared_ptr pBoss = g_pBossHudHandler->vecBossConfig[bossId];
+
+ id = pBoss->id;
+
+ szBossName = pBoss->szBossName;
+ type = pBoss->type;
+
+ bTriggerUseHammerid = pBoss->bTriggerUseHammerid;
+ szTrigger = pBoss->szTrigger;
+ szOutput = pBoss->szOutput;
+ flTriggerDelay = pBoss->flTriggerDelay;
+
+ bShowTriggerUseHammerid = pBoss->bShowTriggerUseHammerid;
+ szShowTrigger = pBoss->szShowTrigger;
+ szShowOutput = pBoss->szShowOutput;
+ flShowTriggerDelay = pBoss->flShowTriggerDelay;
+ bShowOnDecrease = pBoss->bShowOnDecrease;
+
+ bHurtTriggerUseHammerid = pBoss->bHurtTriggerUseHammerid;
+ szHurtTrigger = pBoss->szHurtTrigger;
+ szHurtOutput = pBoss->szHurtOutput;
+
+ bKillTriggerUseHammerid = pBoss->bKillTriggerUseHammerid;
+ szKillTrigger = pBoss->szKillTrigger;
+ szKillOutput = pBoss->szKillOutput;
+ flKillTriggerDelay = pBoss->flKillTriggerDelay;
+
+ bHitmarkerOnly = pBoss->bHitmarkerOnly;
+ bUseMinorHud = pBoss->bUseMinorHud;
+ bMultiTrigger = pBoss->bMultiTrigger;
+ bTemplated = pBoss->bTemplated;
+ bShowBeaten = pBoss->bShowBeaten;
+ flTimeout = pBoss->flTimeout;
+ flOffset = pBoss->flOffset;
+ flOffsetIterator = pBoss->flOffsetIterator;
+ flMaxAllowedHp = pBoss->flMaxAllowedHp;
+
+ szBreakable = pBoss->szBreakable;
+ szCounter = pBoss->szCounter;
+ bCounterReverse = pBoss->bCounterReverse;
+ hpBarType = pBoss->hpBarType;
+ szIterator = pBoss->szIterator;
+ bIteratorReverse = pBoss->bIteratorReverse;
+ szBackup = pBoss->szBackup;
+}
+
+void BossConfig::SetDefaultValues()
+{
+ szBossName = "Boss";
+ type = BossHudType::Invalid;
+
+ szTrigger = "";
+ szOutput = "";
+ flTriggerDelay = 0.0;
+
+ szShowTrigger = "";
+ szShowOutput = "";
+ flShowTriggerDelay = 0.0;
+ bShowOnDecrease = false;
+
+ szKillTrigger = "";
+ szKillOutput = "";
+ flKillTriggerDelay = 0.0;
+
+ bHitmarkerOnly = false;
+ bUseMinorHud = false;
+ bMultiTrigger = false;
+ bTemplated = false;
+ bShowBeaten = true;
+ flTimeout = 0.0;
+ flOffset = 0.0;
+ flOffsetIterator = 0.0;
+ flMaxAllowedHp = g_cvarBossHudMaxHp.Get();
+
+ bCounterReverse = false;
+ bIteratorReverse = false;
+
+ bTriggered = false;
+}
+
+// Sets any unset trigger/killtrigger values to defaults based on type
+void BossConfig::SetDefaultTriggers()
+{
+ // If a trigger or showtrigger is defined, show on that rather than doing weird stuff
+ if (szTrigger == "" && szShowTrigger == "")
+ bShowOnDecrease = !bHitmarkerOnly;
+
+ // Showtrigger doesnt need a default as if its unset,
+ // the boss auto shows when triggered
+
+ if (type == BossHudType::Breakable || type == BossHudType::BreakableHPBar)
+ {
+ if (szTrigger == "")
+ {
+ szTrigger = szBreakable;
+ szOutput = "OnHealthChanged";
+ }
+
+ if (szHurtTrigger == "")
+ {
+ szHurtTrigger = szBreakable;
+ szHurtOutput = "OnHealthChanged";
+ }
+
+ if (type == BossHudType::Breakable)
+ {
+ if (szKillTrigger == "")
+ {
+ szKillTrigger = szBreakable;
+ szKillOutput = "OnBreak";
+ }
+ }
+ else if (szKillTrigger == "") // BreakableHPBar
+ {
+ szKillTrigger = szIterator;
+ if (bIteratorReverse)
+ szKillOutput = "OnHitMax";
+ else
+ szKillOutput = "OnHitMin";
+ }
+ }
+ else if (type != BossHudType::Invalid)
+ {
+ if (szTrigger == "")
+ {
+ szTrigger = szCounter;
+ szOutput = "OutValue";
+ }
+
+ if (szHurtTrigger == "")
+ {
+ szHurtTrigger = szCounter;
+ szHurtOutput = "OutValue";
+ }
+
+ if (szKillTrigger == "")
+ {
+ if (type == BossHudType::Counter)
+ {
+ szKillTrigger = szCounter;
+ if (bCounterReverse)
+ szKillOutput = "OnHitMax";
+ else
+ szKillOutput = "OnHitMin";
+ }
+ else
+ {
+ szKillTrigger = szIterator;
+ if (bIteratorReverse)
+ szKillOutput = "OnHitMax";
+ else
+ szKillOutput = "OnHitMin";
+ }
+ }
+ }
+
+ if (szShowTrigger == "")
+ flShowTriggerDelay = 0.0;
+}
+
+BossConfig::BossConfig(ordered_json jsonKeys, int _id)
+{
+ id = _id;
+
+ SetDefaultValues();
+
+ if (jsonKeys.contains("name"))
+ szBossName = jsonKeys["name"].get();
+
+ if (jsonKeys.contains("breakable"))
+ {
+ szBreakable = jsonKeys["breakable"].get();
+ if (jsonKeys.contains("iterator"))
+ {
+ type = BossHudType::BreakableHPBar;
+ szIterator = jsonKeys["iterator"].get();
+
+ hpBarType = BossHudHPBarType::Double;
+ // if a map needs triple for this i'll add it
+ // until then the only BreakableHPBar i know is ze_mist
+ // which only uses double
+ }
+ else
+ type = BossHudType::Breakable;
+ }
+ else if (jsonKeys.contains("counter"))
+ {
+ szCounter = jsonKeys["counter"].get();
+ if (jsonKeys.contains("iterator"))
+ {
+ type = BossHudType::HPBar;
+ szIterator = jsonKeys["iterator"].get();
+
+ if (jsonKeys.contains("backup"))
+ {
+ hpBarType = BossHudHPBarType::Triple;
+ szBackup = jsonKeys["backup"].get();
+ }
+ else
+ hpBarType = BossHudHPBarType::Double;
+ }
+ else
+ type = BossHudType::Counter;
+
+ if (jsonKeys.contains("reversecounter"))
+ bCounterReverse = jsonKeys["reversecounter"].get();
+ }
+ else
+ {
+ type = BossHudType::Invalid;
+ return;
+ }
+
+ if (jsonKeys.contains("trigger"))
+ {
+ if (jsonKeys["trigger"].contains("ent"))
+ szTrigger = jsonKeys["trigger"]["ent"].get();
+
+ if (jsonKeys["trigger"].contains("output"))
+ szOutput = jsonKeys["trigger"]["output"].get();
+
+ if (jsonKeys["trigger"].contains("delay"))
+ flTriggerDelay = jsonKeys["trigger"]["delay"].get();
+ }
+
+ if (jsonKeys.contains("showtrigger"))
+ {
+ if (jsonKeys["showtrigger"].contains("ent"))
+ szShowTrigger = jsonKeys["showtrigger"]["ent"].get();
+
+ if (jsonKeys["showtrigger"].contains("output"))
+ szShowOutput = jsonKeys["showtrigger"]["output"].get();
+
+ if (jsonKeys["showtrigger"].contains("delay"))
+ flShowTriggerDelay = jsonKeys["showtrigger"]["delay"].get();
+ }
+
+ if (jsonKeys.contains("hurttrigger"))
+ {
+ if (jsonKeys["hurttrigger"].contains("ent"))
+ szHurtTrigger = jsonKeys["hurttrigger"]["ent"].get();
+
+ if (jsonKeys["hurttrigger"].contains("output"))
+ szHurtOutput = jsonKeys["hurttrigger"]["output"].get();
+ }
+
+ if (jsonKeys.contains("killtrigger"))
+ {
+ if (jsonKeys["killtrigger"].contains("ent"))
+ szKillTrigger = jsonKeys["killtrigger"]["ent"].get();
+
+ if (jsonKeys["killtrigger"].contains("output"))
+ szKillOutput = jsonKeys["killtrigger"]["output"].get();
+
+ if (jsonKeys["killtrigger"].contains("delay"))
+ flKillTriggerDelay = jsonKeys["killtrigger"]["delay"].get();
+ }
+
+ if (jsonKeys.contains("reverseiterator"))
+ bIteratorReverse = jsonKeys["reverseiterator"].get();
+
+ if (jsonKeys.contains("hitmarkeronly"))
+ bHitmarkerOnly = jsonKeys["hitmarkeronly"].get();
+
+ if (jsonKeys.contains("minorhud"))
+ bUseMinorHud = jsonKeys["minorhud"].get();
+
+ if (jsonKeys.contains("multitrigger"))
+ bMultiTrigger = jsonKeys["multitrigger"].get();
+
+ if (jsonKeys.contains("templated"))
+ bTemplated = jsonKeys["templated"].get();
+
+ if (jsonKeys.contains("showbeaten"))
+ bShowBeaten = jsonKeys["showbeaten"].get();
+
+ if (jsonKeys.contains("timeout"))
+ flTimeout = jsonKeys["timeout"].get();
+
+ if (jsonKeys.contains("offset"))
+ flOffset = jsonKeys["offset"].get();
+
+ if (jsonKeys.contains("offsetiterator"))
+ flOffsetIterator = jsonKeys["offsetiterator"].get();
+
+ if (jsonKeys.contains("maxhp"))
+ flMaxAllowedHp = jsonKeys["maxhp"].get();
+
+ SetDefaultTriggers();
+
+ bTriggerUseHammerid = (szTrigger[0] == '#');
+ if (bTriggerUseHammerid)
+ szTrigger = szTrigger.substr(1);
+
+ bShowTriggerUseHammerid = (szShowTrigger[0] == '#');
+ if (bShowTriggerUseHammerid)
+ szShowTrigger = szShowTrigger.substr(1);
+
+ bKillTriggerUseHammerid = (szKillTrigger[0] == '#');
+ if (bKillTriggerUseHammerid)
+ szKillTrigger = szKillTrigger.substr(1);
+
+ bHurtTriggerUseHammerid = (szHurtTrigger[0] == '#');
+ if (bHurtTriggerUseHammerid)
+ szHurtTrigger = szHurtTrigger.substr(1);
+}
+
+void BossConfig::Trigger(CHandle hTriggerEntity, int templateNum = -1)
+{
+ bTriggered = true;
+ std::shared_ptr pBoss = std::make_shared(id);
+ pBoss->hTriggerEnt = hTriggerEntity;
+ pBoss->iTemplateNum = templateNum;
+
+ // Place items in order of the config
+ int place = -1;
+ for (int i = 0; i < (g_pBossHudHandler->vecBossInstance).size(); i++)
+ {
+ if (g_pBossHudHandler->vecBossInstance[i]->id >= pBoss->id)
+ {
+ place = i;
+ break;
+ }
+ }
+
+ if (place == -1) // reached the end, our id still higher
+ g_pBossHudHandler->vecBossInstance.push_back(pBoss);
+ else
+ g_pBossHudHandler->vecBossInstance.insert(g_pBossHudHandler->vecBossInstance.begin() + place, pBoss);
+
+ pBoss->Trigger();
+}
+
+bool BossInstance::FindBreakable()
+{
+ if (szBreakable[0] == '#')
+ {
+ if (bTriggerUseHammerid && szTrigger == szBreakable.substr(1))
+ hBreakableEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pBreakableEnt = nullptr;
+ pBreakableEnt = FindEntityByHammerid(pBreakableEnt, szBreakable.substr(1).c_str());
+ if (!pBreakableEnt)
+ return false;
+
+ hBreakableEnt = pBreakableEnt->GetHandle();
+ }
+ }
+ else if (!bTemplated)
+ {
+ if (!bTriggerUseHammerid && szTrigger == szBreakable)
+ hBreakableEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pBreakableEnt = nullptr;
+ pBreakableEnt = UTIL_FindEntityByName(pBreakableEnt, szBreakable.c_str());
+ if (!pBreakableEnt)
+ return false;
+
+ hBreakableEnt = pBreakableEnt->GetHandle();
+ }
+ }
+ else
+ {
+ if (!bTriggerUseHammerid && szTrigger == szBreakable)
+ hBreakableEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szBreakable;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+ else
+ entName += "_*";
+
+ CBaseEntity* pBreakableEnt = nullptr;
+ while ((pBreakableEnt = UTIL_FindEntityByName(pBreakableEnt, entName.c_str())) != nullptr)
+ {
+ bool bSkip = false;
+ for (int i = 0; i < g_pBossHudHandler->vecBossInstance.size(); i++)
+ {
+ std::shared_ptr pBossSearch = g_pBossHudHandler->vecBossInstance[i];
+ if (pBossSearch->hBreakableEnt == pBreakableEnt)
+ {
+ bSkip = true;
+ break;
+ }
+ }
+
+ if (!bSkip)
+ break;
+ }
+
+ if (!pBreakableEnt)
+ return false;
+
+ hBreakableEnt = pBreakableEnt->GetHandle();
+ }
+ }
+
+ flHealth = hBreakableEnt.Get()->m_iHealth();
+ flMaxHealth = flHealth;
+ return true;
+}
+
+bool BossInstance::FindCounter()
+{
+ if (szCounter[0] == '#')
+ {
+ if (bTriggerUseHammerid && szTrigger == szCounter.substr(1))
+ hCounterEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pCounterEnt = nullptr;
+ pCounterEnt = FindEntityByHammerid(pCounterEnt, szCounter.substr(1).c_str());
+ if (!pCounterEnt)
+ return false;
+
+ hCounterEnt = pCounterEnt->GetHandle();
+ }
+ }
+ else if (!bTemplated)
+ {
+ if (!bTriggerUseHammerid && szTrigger == szCounter)
+ hCounterEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pCounterEnt = nullptr;
+ pCounterEnt = UTIL_FindEntityByName(pCounterEnt, szCounter.c_str());
+ if (!pCounterEnt)
+ return false;
+
+ hCounterEnt = pCounterEnt->GetHandle();
+ }
+ }
+ else
+ {
+ if (!bTriggerUseHammerid && szTrigger == szCounter)
+ hCounterEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szCounter;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+ else
+ entName += "_*";
+
+ CBaseEntity* pCounterEnt = nullptr;
+ while ((pCounterEnt = UTIL_FindEntityByName(pCounterEnt, entName.c_str())) != nullptr)
+ {
+ bool bSkip = false;
+ for (int i = 0; i < g_pBossHudHandler->vecBossInstance.size(); i++)
+ {
+ std::shared_ptr pBossSearch = g_pBossHudHandler->vecBossInstance[i];
+ if (pBossSearch->hCounterEnt == pCounterEnt)
+ {
+ bSkip = true;
+ break;
+ }
+ }
+
+ if (!bSkip)
+ break;
+ }
+
+ if (!pCounterEnt)
+ return false;
+
+ hCounterEnt = pCounterEnt->GetHandle();
+ }
+ }
+
+ return true;
+}
+
+bool BossInstance::FindIterator()
+{
+ if (szIterator[0] == '#')
+ {
+ if (bTriggerUseHammerid && szTrigger == szIterator.substr(1))
+ hIteratorEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pIteratorEnt = nullptr;
+ pIteratorEnt = FindEntityByHammerid(pIteratorEnt, szIterator.substr(1).c_str());
+ if (!pIteratorEnt)
+ return false;
+
+ hIteratorEnt = pIteratorEnt->GetHandle();
+ }
+ }
+ else if (!bTemplated)
+ {
+ if (!bTriggerUseHammerid && szTrigger == szIterator)
+ hIteratorEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pIteratorEnt = nullptr;
+ pIteratorEnt = UTIL_FindEntityByName(pIteratorEnt, szIterator.c_str());
+ if (!pIteratorEnt)
+ return false;
+
+ hIteratorEnt = pIteratorEnt->GetHandle();
+ }
+ }
+ else
+ {
+ if (!bTriggerUseHammerid && szTrigger == szIterator)
+ hIteratorEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szIterator;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+ else
+ entName += "_*";
+
+ CBaseEntity* pIteratorEnt = nullptr;
+ while ((pIteratorEnt = UTIL_FindEntityByName(pIteratorEnt, entName.c_str())) != nullptr)
+ {
+ bool bSkip = false;
+ for (int i = 0; i < g_pBossHudHandler->vecBossInstance.size(); i++)
+ {
+ std::shared_ptr pBossSearch = g_pBossHudHandler->vecBossInstance[i];
+ if (pBossSearch->hIteratorEnt == pIteratorEnt)
+ {
+ bSkip = true;
+ break;
+ }
+ }
+
+ if (!bSkip)
+ break;
+ }
+
+ if (!pIteratorEnt)
+ return false;
+
+ hIteratorEnt = pIteratorEnt->GetHandle();
+ }
+ }
+
+ return true;
+}
+
+bool BossInstance::FindBackup()
+{
+ if (szBackup[0] == '#')
+ {
+ if (bTriggerUseHammerid && szTrigger == szBackup.substr(1))
+ hBackupEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pBackupEnt = nullptr;
+ pBackupEnt = FindEntityByHammerid(pBackupEnt, szBackup.substr(1).c_str());
+ if (!pBackupEnt)
+ return false;
+
+ hBackupEnt = pBackupEnt->GetHandle();
+ }
+ }
+ else if (!bTemplated)
+ {
+ if (!bTriggerUseHammerid && szTrigger == szBackup)
+ hBackupEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pBackupEnt = nullptr;
+ pBackupEnt = UTIL_FindEntityByName(pBackupEnt, szBackup.c_str());
+ if (!pBackupEnt)
+ return false;
+
+ hBackupEnt = pBackupEnt->GetHandle();
+ }
+ }
+ else
+ {
+ if (!bTriggerUseHammerid && szTrigger == szBackup)
+ hBackupEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szBackup;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+ else
+ entName += "_*";
+
+ CBaseEntity* pBackupEnt = nullptr;
+ while ((pBackupEnt = UTIL_FindEntityByName(pBackupEnt, entName.c_str())) != nullptr)
+ {
+ bool bSkip = false;
+ for (int i = 0; i < g_pBossHudHandler->vecBossInstance.size(); i++)
+ {
+ std::shared_ptr pBossSearch = g_pBossHudHandler->vecBossInstance[i];
+ if (pBossSearch->hBackupEnt == pBackupEnt)
+ {
+ bSkip = true;
+ break;
+ }
+ }
+
+ if (!bSkip)
+ break;
+ }
+
+ if (!pBackupEnt)
+ return false;
+
+ hBackupEnt = pBackupEnt->GetHandle();
+ }
+ }
+
+ return true;
+}
+
+bool BossInstance::FindShowTrigger()
+{
+ if (szShowTrigger == "")
+ ShowTrigger();
+ else if (bShowTriggerUseHammerid)
+ {
+ if (bTriggerUseHammerid && szShowTrigger == szTrigger)
+ hShowTriggerEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pShowTriggerEnt = nullptr;
+ pShowTriggerEnt = FindEntityByHammerid(pShowTriggerEnt, szShowTrigger.c_str());
+ if (pShowTriggerEnt)
+ hShowTriggerEnt = pShowTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+ }
+ else if (!bTriggerUseHammerid && szShowTrigger == szTrigger)
+ hShowTriggerEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szShowTrigger;
+
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+
+ CBaseEntity* pShowTriggerEnt = nullptr;
+ pShowTriggerEnt = UTIL_FindEntityByName(pShowTriggerEnt, entName.c_str());
+ if (pShowTriggerEnt)
+ hShowTriggerEnt = pShowTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+
+ return true;
+}
+
+bool BossInstance::FindHurtTrigger()
+{
+ if (bHurtTriggerUseHammerid)
+ {
+ if (bTriggerUseHammerid && szHurtTrigger == szTrigger)
+ hHurtTriggerEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pHurtTriggerEnt = nullptr;
+ pHurtTriggerEnt = FindEntityByHammerid(pHurtTriggerEnt, szKillTrigger.c_str());
+ if (pHurtTriggerEnt)
+ hHurtTriggerEnt = pHurtTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+ }
+ else if (!bTriggerUseHammerid && szHurtTrigger == szTrigger)
+ hHurtTriggerEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szHurtTrigger;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+
+ CBaseEntity* pHurtTriggerEnt = nullptr;
+ pHurtTriggerEnt = UTIL_FindEntityByName(pHurtTriggerEnt, entName.c_str());
+ if (pHurtTriggerEnt)
+ hHurtTriggerEnt = pHurtTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+
+ return true;
+}
+
+bool BossInstance::FindKillTrigger()
+{
+ if (bKillTriggerUseHammerid)
+ {
+ if (bTriggerUseHammerid && szKillTrigger == szTrigger)
+ hKillTriggerEnt = hTriggerEnt;
+ else
+ {
+ CBaseEntity* pKillTriggerEnt = nullptr;
+ pKillTriggerEnt = FindEntityByHammerid(pKillTriggerEnt, szKillTrigger.c_str());
+ if (pKillTriggerEnt)
+ hKillTriggerEnt = pKillTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+ }
+ else if (!bTriggerUseHammerid && szKillTrigger == szTrigger)
+ hKillTriggerEnt = hTriggerEnt;
+ else
+ {
+ std::string entName = szKillTrigger;
+ if (iTemplateNum != -1)
+ entName += "_" + std::to_string(iTemplateNum);
+
+ CBaseEntity* pKillTriggerEnt = nullptr;
+ pKillTriggerEnt = UTIL_FindEntityByName(pKillTriggerEnt, entName.c_str());
+ if (pKillTriggerEnt)
+ hKillTriggerEnt = pKillTriggerEnt->GetHandle();
+ else
+ return false;
+ }
+
+ return true;
+}
+
+bool BossInstance::InitHealth()
+{
+ if (IsBreakable())
+ {
+ flDirectHealth = hBreakableEnt.Get()->m_iHealth();
+ flHealth = hBreakableEnt.Get()->m_iHealth() + flOffset;
+ }
+ else if (IsBreakableHPBar())
+ {
+ CBaseEntity* pBreakable = hBreakableEnt.Get();
+ CMathCounter* pIterator = (CMathCounter*)(hIteratorEnt.Get());
+
+ flDirectHealth = pBreakable->m_iHealth();
+ if (hpBarType == BossHudHPBarType::Double)
+ {
+ if (!bIteratorReverse)
+ iSegmentsLeft = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ iSegmentsLeft = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ iSegmentsLeft += flOffsetIterator;
+
+ flHealth = (pBreakable->m_iHealth() + flOffset) * iSegmentsLeft;
+ }
+ // triple not done until a map actually has it
+ }
+ else if (IsCounter())
+ {
+ CMathCounter* pCounter = (CMathCounter*)(hCounterEnt.Get());
+ if (!bCounterReverse) // start high, die onhitmin
+ flHealth = pCounter->GetCounterValue() - pCounter->m_flMin();
+ else // start low, die onhitmax
+ flHealth = pCounter->m_flMax() - pCounter->GetCounterValue();
+
+ flDirectHealth = flHealth;
+ flHealth += flOffset;
+ }
+ else if (IsHPBar())
+ {
+ CMathCounter* pCounter = (CMathCounter*)(hCounterEnt.Get());
+ if (!bCounterReverse)
+ flHealth = pCounter->GetCounterValue() - pCounter->m_flMin();
+ else
+ flHealth = pCounter->m_flMax() - pCounter->GetCounterValue();
+
+ flDirectHealth = flHealth;
+ flHealth += flOffset;
+
+ CMathCounter* pIterator = (CMathCounter*)(hIteratorEnt.Get());
+ if (hpBarType == BossHudHPBarType::Double)
+ {
+ if (!bIteratorReverse)
+ iSegmentsLeft = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ iSegmentsLeft = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ iSegmentsLeft += flOffsetIterator;
+ flHealth += (flHealth * iSegmentsLeft);
+ }
+ else
+ {
+ CMathCounter* pBackup = (CMathCounter*)(hBackupEnt.Get());
+ if (!bIteratorReverse)
+ iSegmentsLeft = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ iSegmentsLeft = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ iSegmentsLeft += flOffsetIterator;
+ flHealth += (iSegmentsLeft * pBackup->GetCounterValue());
+ }
+ }
+
+ flMaxHealth = flHealth;
+
+ if (flMaxAllowedHp == 0.0)
+ return true;
+
+ return (flHealth < flMaxAllowedHp);
+}
+
+void BossInstance::Trigger()
+{
+ if (flTriggerDelay > 0.0)
+ {
+ auto weak_this = weak_from_this();
+ CTimer::Create(flTriggerDelay, TIMERFLAG_MAP | TIMERFLAG_ROUND, [weak_this] {
+ if (auto boss = weak_this.lock())
+ {
+ if (!boss->TriggerPost())
+ {
+ boss->bCleanup = true;
+ g_pBossHudHandler->CleanupBosses();
+ }
+ }
+ return -1.0f;
+ });
+ }
+ else if (!TriggerPost())
+ {
+ bCleanup = true;
+ g_pBossHudHandler->CleanupBosses();
+ }
+
+ bTriggered = true;
+}
+
+bool BossInstance::TriggerPost()
+{
+ Message("[BHUD] Triggered: %s\n", szBossName.c_str());
+
+ switch (type)
+ {
+ case BossHudType::Breakable:
+ if (!FindBreakable())
+ return false;
+ break;
+ case BossHudType::BreakableHPBar:
+ if (!FindBreakable())
+ return false;
+
+ if (!FindIterator())
+ return false;
+
+ // Again, triple is not implemented until theres
+ // an actual need for it
+ break;
+ default:
+ if (!FindCounter())
+ return false;
+
+ if (!IsHPBar())
+ break;
+
+ if (!FindIterator())
+ return false;
+
+ if (hpBarType == BossHudHPBarType::Double)
+ break;
+
+ if (!FindBackup())
+ return false;
+
+ break;
+ }
+
+ if (!bHitmarkerOnly && !FindShowTrigger())
+ Message("[BossHud] Failed to find showtrigger entity for: %s\n", szBossName.c_str());
+
+ if (!FindHurtTrigger())
+ Message("[BossHud] Failed to find hurttrigger entity for: %s\n", szBossName.c_str());
+
+ if (!FindKillTrigger())
+ Message("[BossHud] Failed to find killtrigger entity for: %s\n", szBossName.c_str());
+
+ g_pBossHudHandler->StartAssistTimer();
+
+ bActive = InitHealth();
+
+ return true;
+}
+
+void BossInstance::ShowTrigger()
+{
+ bShowTriggered = true;
+
+ if (bShowOnDecrease)
+ return;
+
+ if (flShowTriggerDelay > 0.0)
+ {
+ auto weak_this = weak_from_this();
+ CTimer::Create(flShowTriggerDelay, TIMERFLAG_MAP | TIMERFLAG_ROUND, [weak_this] {
+ if (auto boss = weak_this.lock())
+ {
+ Message("[bhud] Showtrigger timer tick\n");
+ boss->ShowTriggerPost();
+ }
+
+ return -1.0f;
+ });
+ }
+ else
+ ShowTriggerPost();
+}
+
+void BossInstance::ShowTriggerPost()
+{
+ bShow = true;
+ if (GetGlobals())
+ flLastChange = GetGlobals()->curtime;
+
+ Message("[BHUD] Boss \"%s\" is now being shown!\n", szBossName.c_str());
+
+ if (m_pBossHudTimer.expired())
+ m_pBossHudTimer = CTimer::Create(g_cvarBossHudRate.Get(), TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
+ return BossHud_UpdateHud();
+ });
+}
+
+// Activator can be null
+void BossInstance::Hurt(CBaseEntity* pActivator)
+{
+ if (!bActive)
+ {
+ bActive = InitHealth();
+ // Can't do comparisons for damage if we just initialised so just return
+ return;
+ }
+
+ if (GetGlobals())
+ flLastChange = GetGlobals()->curtime;
+
+ float actualHealth = 0.0; // for accurate player damage
+ int segments = 0;
+ float newHealth = 0.0; // calculated total health
+ CBaseEntity* pBreakable = nullptr;
+ CMathCounter* pCounter = nullptr;
+ CMathCounter* pIterator = nullptr;
+ CMathCounter* pBackup = nullptr;
+ switch (type)
+ {
+ case BossHudType::Breakable:
+ pBreakable = hBreakableEnt.Get();
+ if (!pBreakable)
+ return;
+
+ actualHealth = pBreakable->m_iHealth();
+ newHealth = pBreakable->m_iHealth() + flOffset;
+
+ break;
+ case BossHudType::BreakableHPBar:
+ pBreakable = hBreakableEnt.Get();
+ if (!pBreakable)
+ return;
+
+ pIterator = (CMathCounter*)(hIteratorEnt.Get());
+ if (!pIterator)
+ return;
+
+ actualHealth = pBreakable->m_iHealth();
+
+ if (hpBarType == BossHudHPBarType::Double)
+ {
+ if (!bIteratorReverse)
+ segments = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ segments = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ segments += flOffsetIterator;
+
+ newHealth = (pBreakable->m_iHealth() + flOffset);
+
+ if (flSegmentHealth == -1 || newHealth > flSegmentHealth)
+ flSegmentHealth = newHealth;
+
+ newHealth += (flSegmentHealth * iSegmentsLeft);
+
+ if (segments != iSegmentsLeft)
+ {
+ iSegmentsLeft = segments;
+ flSegmentHealth = -1;
+ }
+ }
+ // triple not implemented until a map uses it
+
+ break;
+ case BossHudType::Counter:
+ pCounter = (CMathCounter*)(hCounterEnt.Get());
+ if (!pCounter)
+ return;
+
+ if (!bCounterReverse) // start high, die onhitmin
+ newHealth = pCounter->GetCounterValue() - pCounter->m_flMin();
+ else // start low, die onhitmax
+ newHealth = pCounter->m_flMax() - pCounter->GetCounterValue();
+
+ actualHealth = newHealth;
+
+ newHealth += flOffset;
+ break;
+ case BossHudType::HPBar:
+ pCounter = (CMathCounter*)(hCounterEnt.Get());
+ if (!pCounter)
+ return;
+
+ pIterator = (CMathCounter*)(hIteratorEnt.Get());
+ if (!pIterator)
+ return;
+
+ if (hpBarType == BossHudHPBarType::Double)
+ {
+ if (!bIteratorReverse)
+ segments = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ segments = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ segments += flOffsetIterator;
+
+ if (!bCounterReverse)
+ newHealth = pCounter->GetCounterValue() - pCounter->m_flMin();
+ else
+ newHealth = pCounter->m_flMax() - pCounter->GetCounterValue();
+
+ actualHealth = newHealth;
+
+ newHealth += flOffset;
+
+ if (flSegmentHealth == -1 || newHealth > flSegmentHealth)
+ flSegmentHealth = newHealth;
+
+ newHealth += flSegmentHealth * iSegmentsLeft;
+
+ if (segments != iSegmentsLeft)
+ {
+ iSegmentsLeft = segments;
+ flSegmentHealth = -1;
+ }
+ }
+ else
+ {
+ pBackup = (CMathCounter*)(hBackupEnt.Get());
+ if (!pBackup)
+ return;
+
+ if (!bCounterReverse)
+ newHealth = pCounter->GetCounterValue() - pCounter->m_flMin();
+ else
+ newHealth = pCounter->m_flMax() - pCounter->GetCounterValue();
+
+ actualHealth = newHealth;
+ newHealth += flOffset;
+
+ if (!bIteratorReverse)
+ segments = pIterator->GetCounterValue() - pIterator->m_flMin() - 1;
+ else
+ segments = pIterator->m_flMax() - pIterator->GetCounterValue() - 1;
+
+ segments += flOffsetIterator;
+ newHealth += (segments * pBackup->GetCounterValue());
+ }
+ break;
+ }
+
+ if (bShowOnDecrease && !bShow)
+ {
+ if (newHealth < flHealth)
+ ShowTriggerPost();
+ }
+
+ // Update maxhealth so percent is always accurate
+ if (newHealth > flMaxHealth)
+ flMaxHealth = newHealth;
+
+ // Only credit player if health decreased
+ if (actualHealth < flDirectHealth)
+ {
+ if (pActivator)
+ {
+ CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pActivator;
+ if (pPawn)
+ {
+ CCSPlayerController* pController = pPawn->GetOriginalController();
+ if (pController)
+ {
+ if (bShowBeaten)
+ {
+ float damage = flDirectHealth - actualHealth;
+ flTotalDamage += damage;
+
+ int slot = pController->GetPlayerSlot();
+
+ // damage array isnt sorted until its dead so we can do this
+ PlayerDamage[slot].damage += damage;
+
+ if (g_cvarBossHudScoreboard.Get())
+ g_pBossHudHandler->iAssists[slot] += 1;
+
+ if (g_cvarBossHudReward.Get() > 0)
+ pController->m_pInGameMoneyServices->m_iAccount += g_cvarBossHudReward.Get();
+ }
+
+ // Disable hitmarkers for now until we make a new particle for it
+ /*
+ if (g_cvarBossHudHitmarker.Get())
+ {
+ ZEPlayer* zpPlayer = pController->GetZEPlayer();
+ if (zpPlayer)
+ {
+ BossHudMode mode = (BossHudMode)(zpPlayer->GetBossHudMode());
+ if (mode == BossHudMode::Display_All || mode == BossHudMode::Display_Hit)
+ {
+ CRecipientFilter filter;
+ filter.AddRecipient(zpPlayer->GetPlayerSlot());
+ addresses::DispatchParticleEffect(g_cvarBossHudHitmarkerParticle.Get().String(), PATTACH_WORLDORIGIN, pPawn, 0, "", false, -1, &filter, 0);
+ }
+ }
+ }
+ */
+ }
+ }
+ }
+ }
+
+ flDirectHealth = actualHealth;
+ flHealth = newHealth;
+}
+
+void BossInstance::KillTrigger()
+{
+ bKillTriggered = true;
+ if (flKillTriggerDelay > 0.0)
+ {
+ auto weak_this = weak_from_this();
+ CTimer::Create(flKillTriggerDelay, TIMERFLAG_MAP | TIMERFLAG_ROUND, [weak_this] {
+ if (auto boss = weak_this.lock())
+ boss->KillTriggerPost();
+
+ return -1.0f;
+ });
+ }
+ else
+ KillTriggerPost();
+}
+
+void BossInstance::KillTriggerPost()
+{
+ Message("[BHUD] Boss \"%s\" is DEAD!\n", szBossName.c_str());
+
+ // Show top damage i guess
+ if (bShowBeaten)
+ {
+ // Sort by descending damage
+ std::sort(PlayerDamage, PlayerDamage + MAXPLAYERS + 1,
+ [](const BossDamage& a, const BossDamage& b) {
+ return (a.damage > b.damage);
+ });
+
+ ClientPrintAll(HUD_PRINTTALK, " \x09%s TOP DAMAGE", szBossName.c_str());
+ if (PlayerDamage[0].damage <= 0.0)
+ ClientPrintAll(HUD_PRINTTALK, " \x0BNobody? Yikes.");
+ else
+ {
+ const char colorMap[] = {'\x10', '\x08', '\x09'};
+ char typeText[8];
+ snprintf(typeText, sizeof(typeText), "%s", (IsBreakable() || IsBreakableHPBar()) ? "damage" : "hits");
+
+ for (int i = 0; i < MAXPLAYERS + 1; i++)
+ {
+ if (PlayerDamage[i].damage <= 0.0)
+ break;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(CPlayerSlot(PlayerDamage[i].slot));
+ if (!pController)
+ continue;
+
+ if (i < 3)
+ ClientPrintAll(HUD_PRINTTALK, " %c%d. %s \x01- \x0F%.0f %s \x05(%.1f%%)",
+ colorMap[i],
+ i + 1,
+ pController->GetPlayerName().c_str(),
+ PlayerDamage[i].damage,
+ typeText,
+ (PlayerDamage[i].damage / flTotalDamage) * 100.0);
+ else
+ ClientPrint(pController, HUD_PRINTTALK, " \x0B%d. %s \x01- \x0F%.0f %s \x05(%.1f%%)",
+ i + 1,
+ pController->GetPlayerName().c_str(),
+ PlayerDamage[i].damage,
+ typeText,
+ (PlayerDamage[i].damage / flTotalDamage) * 100.0);
+ }
+ }
+ }
+
+ bDead = true;
+
+ auto weak_this = weak_from_this();
+ CTimer::Create(3.0f, TIMERFLAG_MAP | TIMERFLAG_ROUND, [weak_this] {
+ if (auto boss = weak_this.lock())
+ {
+ boss->bCleanup = true;
+ g_pBossHudHandler->CleanupBosses();
+ }
+
+ return -1.0f;
+ });
+}
+
+void CBossHudHandler::PrintLoadedConfig(CCSPlayerController* player)
+{
+ if (!bLoadedConfig)
+ {
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "No config loaded.");
+ return;
+ }
+
+ for (int i = 0; i < vecBossConfig.size(); i++)
+ {
+ std::shared_ptr config = vecBossConfig[i];
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ Boss %02d ------------", i + 1);
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Name: %s", config->szBossName.c_str());
+ switch (config->type)
+ {
+ case BossHudType::Breakable:
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Type: Breakable");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Breakable: %s", config->szBreakable.c_str());
+ break;
+ case BossHudType::BreakableHPBar:
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Type: Breakable HPBar");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Breakable: %s", config->szBreakable.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Iterator: %s", config->szIterator.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Backup: %s", config->szBackup.c_str());
+ break;
+ case BossHudType::Counter:
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Type: Counter");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Counter: %s", config->szCounter.c_str());
+ break;
+ case BossHudType::HPBar:
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Type: HPBar");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Counter: %s", config->szCounter.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Iterator: %s", config->szIterator.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Backup: %s", config->szBackup.c_str());
+ break;
+ }
+
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ Trigger ------------");
+
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Ent: %s", config->szTrigger.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Output: %s", config->szOutput.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Delay: %.1f", config->flTriggerDelay);
+
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ ShowTrigger ------------");
+
+ if (config->szShowTrigger != "")
+ {
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Ent: %s", config->szShowTrigger.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Output: %s", config->szShowOutput.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Delay: %.1f", config->flShowTriggerDelay);
+ }
+ else
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "No ShowTrigger set");
+
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ KillTrigger ------------");
+
+ if (config->szKillTrigger != "")
+ {
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Ent: %s", config->szKillTrigger.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "Output: %s", config->szKillOutput.c_str());
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " Delay: %.1f", config->flKillTriggerDelay);
+ }
+ else
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "No KillTrigger set");
+
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ ----------- ------------");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " reversecounter: %s", config->bCounterReverse ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "reverseiterator: %s", config->bIteratorReverse ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " hitmarkeronly: %s", config->bHitmarkerOnly ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " multitrigger: %s", config->bMultiTrigger ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " templated: %s", config->bTemplated ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " showbeaten: %s", config->bShowBeaten ? "True" : "False");
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " timeout: %.1f", config->flTimeout);
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX " offset: %.1f", config->flOffset);
+ ClientPrint(player, HUD_PRINTCONSOLE, BOSSHUD_PREFIX "------------ ----------- ------------");
+ }
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "See console for output.");
+}
+
+void CBossHudHandler::ResetTriggeredBosses()
+{
+ for (int i = 0; i < vecBossConfig.size(); i++)
+ vecBossConfig[i]->bTriggered = false;
+}
+
+void CBossHudHandler::ResetScoreboard()
+{
+ if (!GetGlobals() || !g_cvarBossHudScoreboard.Get())
+ return;
+
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ iAssists[i] = 0;
+
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(i);
+ if (!pController)
+ continue;
+
+ pController->m_pActionTrackingServices->m_matchStats().m_iAssists = 0;
+ }
+}
+
+void CBossHudHandler::ClearBossInstances()
+{
+ vecBossInstance.clear();
+}
+
+void CBossHudHandler::CleanupBosses()
+{
+ for (auto it = vecBossInstance.begin(); it != vecBossInstance.end();)
+ {
+ std::shared_ptr pBoss = *it;
+ if (!pBoss || pBoss->type == BossHudType::Invalid || pBoss->bCleanup)
+ it = vecBossInstance.erase(it);
+ else
+ it++;
+ }
+}
+
+void CBossHudHandler::StartAssistTimer()
+{
+ if (g_cvarBossHudScoreboard.Get() && m_pAssistUpdateTimer.expired())
+ m_pAssistUpdateTimer = CTimer::Create(BHUD_ASSIST_TIMER_RATE, TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
+ return BossHud_UpdateAssists();
+ });
+}
+
+void BossHud_OnLevelInit(const char* sMapName)
+{
+ g_pBossHudHandler->LoadConfig(sMapName);
+
+ if (!SetupFireOutputInternalDetour())
+ mapIOFunctions.erase("bosshud");
+ else if (!BossHud_IsFireOutputHooked())
+ mapIOFunctions["bosshud"] = BossHud_FireOutput;
+}
+
+void BossHud_RoundPreStart()
+{
+ if (!g_pBossHudHandler || !g_pSimpleHudHandler)
+ return;
+
+ // If map has config, we clean everything up
+ if (g_pBossHudHandler->IsConfigLoaded())
+ {
+ g_pBossHudHandler->ResetTriggeredBosses();
+ g_pBossHudHandler->ResetScoreboard();
+ g_pBossHudHandler->ClearBossInstances();
+ }
+
+ g_pSimpleHudHandler->ClearSimpleHud();
+}
+
+void BossHud_OnEntityDeleted(CEntityInstance* pEntity)
+{
+ if (!pEntity || !g_pBossHudHandler)
+ return;
+
+ CBaseEntity* pEnt = (CBaseEntity*)pEntity;
+ if (!pEnt)
+ return;
+
+ // For now, we only check the main entity if it's killed. I doubt checking triggers makes sense anyways
+ // since if the trigger was killed, then trigger/showtrigger wouldnt even fire in the first place
+ // Calling KillTriggerPost() is so the HUD updates and it is instantly counted as dead,
+ // it also then removes the boss from the bosses vector
+ for (auto it = g_pBossHudHandler->vecBossInstance.begin(); it != g_pBossHudHandler->vecBossInstance.end(); it++)
+ {
+ std::shared_ptr pInstance = *it;
+ if (!pInstance || pInstance->bKillTriggered || pInstance->bDead)
+ continue;
+
+ switch (pInstance->type)
+ {
+ case BossHudType::Breakable:
+ if (pInstance->hBreakableEnt == pEnt)
+ pInstance->KillTriggerPost();
+ break;
+ case BossHudType::BreakableHPBar:
+ if (pInstance->hBreakableEnt == pEnt || pInstance->hIteratorEnt == pEnt)
+ pInstance->KillTriggerPost();
+ break;
+ case BossHudType::Counter:
+ if (pInstance->hCounterEnt == pEnt)
+ pInstance->KillTriggerPost();
+ break;
+ case BossHudType::HPBar:
+ if (pInstance->hCounterEnt == pEnt || pInstance->hIteratorEnt == pEnt || pInstance->hBackupEnt == pEnt)
+ pInstance->KillTriggerPost();
+ break;
+ }
+ }
+}
+
+void BossHud_PlayerDisconnect(int slot)
+{
+ if (g_pBossHudHandler && g_pBossHudHandler->IsConfigLoaded())
+ {
+ g_pBossHudHandler->iAssists[slot] = 0;
+
+ for (auto it = g_pBossHudHandler->vecBossInstance.begin(); it != g_pBossHudHandler->vecBossInstance.end(); it++)
+ {
+ std::shared_ptr pBoss = *it;
+ if (!pBoss->bDead)
+ pBoss->PlayerDamage[slot].damage = 0.0;
+ }
+ }
+}
+
+bool BossHud_IsFireOutputHooked()
+{
+ return std::any_of(mapIOFunctions.begin(), mapIOFunctions.end(), [](const auto& p) { return p.first == "bosshud"; });
+}
+
+float BossHud_UpdateSimpleHud()
+{
+ if (!GetGlobals())
+ return g_cvarBossHudRate.Get();
+
+ std::string szHudText = "";
+
+ for (int i = 0; i < (g_pSimpleHudHandler->vecSimpleInstance).size(); i++)
+ {
+ std::shared_ptr pInstance = g_pSimpleHudHandler->vecSimpleInstance[i];
+ if (!pInstance)
+ continue;
+
+ if (!pInstance->bActive)
+ continue;
+
+ if (pInstance->bCounter)
+ {
+ CMathCounter* pCounter = (CMathCounter*)g_pEntitySystem->GetEntityInstance((CEntityIndex)pInstance->iEnt);
+ if (!pCounter)
+ continue;
+
+ if (V_strcmp(pCounter->GetName(), ""))
+ szHudText.append(pCounter->GetName());
+ else
+ {
+ szHudText.append("[");
+ szHudText.append(pCounter->m_sUniqueHammerID.Get().String());
+ szHudText.append("]");
+ }
+
+ szHudText.append(": ");
+ szHudText.append(std::to_string(static_cast(std::round(pCounter->GetCounterValue()))));
+ }
+ else
+ {
+ CBaseEntity* pBreakable = (CBaseEntity*)g_pEntitySystem->GetEntityInstance((CEntityIndex)pInstance->iEnt);
+ if (!pBreakable)
+ continue;
+
+ if (V_strcmp(pBreakable->GetName(), ""))
+ szHudText.append(pBreakable->GetName());
+ else
+ {
+ szHudText.append("[");
+ szHudText.append(pBreakable->m_sUniqueHammerID.Get().String());
+ szHudText.append("]");
+ }
+
+ szHudText.append(": ");
+ szHudText.append(std::to_string(pBreakable->m_iHealth()));
+ }
+
+ if (GetGlobals()->curtime - pInstance->flLastHitTime > 3.0)
+ pInstance->bActive = false;
+
+ if (i < (g_pSimpleHudHandler->vecSimpleInstance.size() - 1))
+ szHudText.append("
");
+ }
+
+ if (szHudText != "")
+ SendHudMessageAll(1, EHudPriority::BossHud, szHudText.c_str());
+
+ return g_cvarBossHudRate.Get();
+}
+
+float BossHud_UpdateHud()
+{
+ if (!GetGlobals())
+ return g_cvarBossHudRate.Get();
+
+ std::string szHudText = "";
+
+ int showCount = 0;
+ int health = 0;
+ int percent = 0;
+ bool singleDead = false;
+ bool singleMinor = false;
+
+ for (int i = 0; i < (g_pBossHudHandler->vecBossInstance).size(); i++)
+ {
+ std::shared_ptr pBoss = g_pBossHudHandler->vecBossInstance[i];
+ if (!pBoss || !pBoss->bActive)
+ continue;
+
+ if (!pBoss->bShow || (pBoss->flTimeout > 0.0 && GetGlobals()->curtime > pBoss->flLastChange + pBoss->flTimeout))
+ continue;
+
+ if (showCount == 1)
+ {
+ // Add first boss in a multiboss style
+ // Name is already added
+ // Health and percent are still the first boss values
+ if (percent <= 33)
+ szHudText.append(": ");
+ else if (percent <= 66)
+ szHudText.append(": ");
+ else
+ szHudText.append(": ");
+
+ szHudText.append(std::to_string(health));
+ szHudText.append(" (");
+ szHudText.append(std::to_string(percent));
+ szHudText.append("%%)");
+ }
+
+ if (pBoss->bDead)
+ {
+ if (showCount == 0)
+ {
+ szHudText.append(pBoss->szBossName);
+ singleDead = true;
+ singleMinor = pBoss->bUseMinorHud;
+ }
+ else
+ {
+ szHudText.append("
");
+ szHudText.append(pBoss->szBossName);
+ szHudText.append(": DEAD");
+ }
+
+ showCount++;
+ continue;
+ }
+
+ health = static_cast(pBoss->flHealth);
+ percent = std::round(clamp((health / pBoss->flMaxHealth) * 100.0f, 0.0, 100.0));
+
+ if (showCount == 0)
+ {
+ szHudText.append(pBoss->szBossName);
+ singleMinor = pBoss->bUseMinorHud;
+ }
+ else
+ {
+ szHudText.append("
");
+ szHudText.append(pBoss->szBossName);
+
+ if (percent <= 33)
+ szHudText.append(": ");
+ else if (percent <= 66)
+ szHudText.append(": ");
+ else
+ szHudText.append(": ");
+
+ szHudText.append(std::to_string(health));
+ szHudText.append(" (");
+ szHudText.append(std::to_string(percent));
+ szHudText.append("%%)");
+ }
+
+ showCount++;
+ }
+
+ if (showCount == 1)
+ {
+ if (singleMinor)
+ {
+ if (singleDead)
+ szHudText.append(": DEAD");
+ else
+ {
+ if (percent <= 33)
+ szHudText.append(": ");
+ else if (percent <= 66)
+ szHudText.append(": ");
+ else
+ szHudText.append(": ");
+
+ szHudText.append(std::to_string(health));
+ szHudText.append(" (");
+ szHudText.append(std::to_string(percent));
+ szHudText.append("%%)");
+ }
+ }
+ else if (singleDead)
+ {
+ szHudText.append("
");
+ szHudText.append("DEAD");
+ }
+ else
+ {
+ // Add boss in solo style
+ // Name is already added
+ if (percent <= 33)
+ szHudText.append("
");
+ else if (percent <= 66)
+ szHudText.append("
");
+ else
+ szHudText.append("
");
+
+ szHudText.append(std::to_string(health));
+ szHudText.append(" (");
+ szHudText.append(std::to_string(percent));
+ szHudText.append("%%)");
+
+ static int barCount = 14;
+ int bars = clamp(std::ceil(barCount * (percent / 100.0f)), 0, barCount);
+ szHudText.append("
");
+
+ if (percent <= 33)
+ szHudText.append("");
+ else if (percent <= 66)
+ szHudText.append("");
+ else
+ szHudText.append("");
+
+ for (int i = 0; i < bars; i++)
+ szHudText.append("▅");
+
+ szHudText.append("");
+ for (int i = 0; i < (barCount - bars); i++)
+ szHudText.append("▅");
+
+ szHudText.append("");
+ }
+ }
+
+ if (szHudText != "")
+ {
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ ZEPlayer* pPlayer = g_playerManager->GetPlayer(i);
+ if (pPlayer)
+ {
+ BossHudMode mode = (BossHudMode)(pPlayer->GetBossHudMode());
+ if (mode == BossHudMode::Display_All || mode == BossHudMode::Display_Hud)
+ SendHudMessage(pPlayer, 1, EHudPriority::BossHud, szHudText.c_str());
+ }
+ }
+ }
+
+ return g_cvarBossHudRate.Get();
+}
+
+float BossHud_UpdateAssists()
+{
+ // it'll restart automatically
+ if (!GetGlobals() || !g_cvarBossHudScoreboard.Get())
+ return -1.0f;
+
+ for (int i = 0; i < GetGlobals()->maxClients; i++)
+ {
+ CCSPlayerController* pController = CCSPlayerController::FromSlot(i);
+ if (!pController)
+ continue;
+
+ int assists = pController->m_pActionTrackingServices->m_matchStats().m_iAssists();
+ if (assists != g_pBossHudHandler->iAssists[i])
+ pController->m_pActionTrackingServices->m_matchStats().m_iAssists = g_pBossHudHandler->iAssists[i];
+ }
+
+ return BHUD_ASSIST_TIMER_RATE;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Simple hud
+//-----------------------------------------------------------------------------
+void CSimpleHudHandler::ClearSimpleHud()
+{
+ vecSimpleInstance.clear();
+}
+
+void BossHud_FireOutput(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay)
+{
+ if (!BossHud_IsFireOutputHooked() || !pCaller)
+ return;
+
+ CBaseEntity* pEnt = (CBaseEntity*)pCaller;
+ if (!pEnt)
+ return;
+
+ // Simple HUD, can display health if no config
+ if (!g_pBossHudHandler || !g_pBossHudHandler->IsConfigLoaded())
+ {
+ if (g_cvarBossHudSimpleHud.Get())
+ SimpleHud_FireOutput(pEnt, pThis->m_pDesc->m_pName);
+ return;
+ }
+
+ std::string szTargetname = pEnt->GetName();
+ std::string szHammerid = pEnt->m_sUniqueHammerID.Get().String();
+
+ // Check trigger
+ for (int i = 0; i < g_pBossHudHandler->vecBossConfig.size(); i++)
+ {
+ std::shared_ptr pConfig = g_pBossHudHandler->vecBossConfig[i];
+ if (!pConfig || pConfig->szTrigger == "" || (!pConfig->bMultiTrigger && pConfig->bTriggered))
+ continue;
+
+ // Check output
+ if (V_stricmp(pThis->m_pDesc->m_pName, pConfig->szOutput.c_str()))
+ continue;
+
+ // If this entity is already in an instance of this boss, don't trigger another
+ if (pConfig->bMultiTrigger)
+ {
+ bool skip = false;
+ for (int j = 0; j < g_pBossHudHandler->vecBossInstance.size(); j++)
+ {
+ if (g_pBossHudHandler->vecBossInstance[j]->id == pConfig->id)
+ {
+ if (g_pBossHudHandler->vecBossInstance[j]->hTriggerEnt == pEnt)
+ {
+ skip = true;
+ break;
+ }
+ }
+ }
+
+ if (skip)
+ continue;
+ }
+
+ if (pConfig->bTriggerUseHammerid)
+ {
+ if (pConfig->szTrigger == szHammerid)
+ pConfig->Trigger(pEnt->GetHandle());
+ }
+ else
+ {
+ if (pConfig->szTrigger == szTargetname)
+ pConfig->Trigger(pEnt->GetHandle());
+ else if (pConfig->bTemplated)
+ {
+ size_t found = szTargetname.find_last_of('_');
+ if (found != std::string::npos)
+ {
+ if (pConfig->szTrigger == szTargetname.substr(0, found))
+ pConfig->Trigger(pEnt->GetHandle(), GetTemplateSuffixNumber(szTargetname.c_str()));
+ }
+ }
+ }
+ }
+
+ // Check if this output is a show/hurt/kill trigger for an active boss
+ for (auto it = g_pBossHudHandler->vecBossInstance.begin(); it != g_pBossHudHandler->vecBossInstance.end(); it++)
+ {
+ std::shared_ptr pBoss = *it;
+ if (!pBoss || pBoss->type == BossHudType::Invalid || !pBoss->bTriggered || pBoss->bDead)
+ continue;
+
+ // Showtrigger (if not hitmarker only)
+ if (!pBoss->bHitmarkerOnly && !pBoss->bShowTriggered)
+ {
+ if (pBoss->hShowTriggerEnt.Get() && pBoss->hShowTriggerEnt == pEnt && !V_stricmp(pThis->m_pDesc->m_pName, pBoss->szShowOutput.c_str()))
+ pBoss->ShowTrigger();
+ }
+
+ // Hurt trigger
+ if (pBoss->hHurtTriggerEnt.Get() && pBoss->hHurtTriggerEnt == pEnt && !V_stricmp(pThis->m_pDesc->m_pName, pBoss->szHurtOutput.c_str()))
+ pBoss->Hurt((CBaseEntity*)pActivator);
+
+ // Killtrigger
+ if (!pBoss->bKillTriggered)
+ {
+ if (pBoss->hKillTriggerEnt.Get() && pBoss->hKillTriggerEnt == pEnt && !V_stricmp(pThis->m_pDesc->m_pName, pBoss->szKillOutput.c_str()))
+ pBoss->KillTrigger();
+ }
+ }
+}
+
+void SimpleHud_FireOutput(CBaseEntity* pEntity, const char* sOutput)
+{
+ if (!g_setBossEnts.contains(pEntity->GetClassname()) || !g_setBossOutputs.contains(sOutput))
+ return;
+
+ bool bFound = false;
+ for (int i = 0; i < g_pSimpleHudHandler->vecSimpleInstance.size(); i++)
+ {
+ std::shared_ptr pInstance = g_pSimpleHudHandler->vecSimpleInstance[i];
+ if (!pInstance)
+ continue;
+
+ if (pEntity->entindex() != pInstance->iEnt)
+ continue;
+
+ if (!pInstance->bActive)
+ pInstance->bActive = true;
+
+ pInstance->flLastHitTime = GetGlobals()->curtime;
+ bFound = true;
+ break;
+ }
+
+ if (bFound)
+ return;
+
+ std::shared_ptr pInstance = std::make_shared();
+ pInstance->iEnt = pEntity->entindex();
+ pInstance->flLastHitTime = GetGlobals()->curtime;
+ pInstance->bActive = true;
+ pInstance->bCounter = (!V_stricmp(pEntity->GetClassname(), "math_counter")) ? true : false;
+ g_pSimpleHudHandler->vecSimpleInstance.push_back(pInstance);
+
+ if (m_pBossHudTimer.expired())
+ m_pBossHudTimer = CTimer::Create(g_cvarBossHudRate.Get(), TIMERFLAG_MAP | TIMERFLAG_ROUND, [] {
+ return BossHud_UpdateSimpleHud();
+ });
+}
+
+CBaseEntity* FindEntityByTargetname(CBaseEntity* pStartEnt, std::string szTargetname)
+{
+ if (!pStartEnt)
+ return nullptr;
+
+ if (szTargetname[0] == '#') // HammerID
+ {
+ std::string szHammerID = szTargetname.substr(1);
+ CBaseEntity* pTarget = pStartEnt;
+ while ((pTarget = UTIL_FindEntityByClassname(pTarget, "*")))
+ if (!V_strcmp(pTarget->m_sUniqueHammerID().Get(), szHammerID.c_str()))
+ return pTarget;
+ }
+ else // Targetname
+ {
+ int iWildcard = szTargetname.find('*');
+ CBaseEntity* pTarget = pStartEnt;
+ while ((pTarget = UTIL_FindEntityByClassname(pTarget, "*")))
+ if (strncmp(szTargetname.c_str(), pTarget->GetName(), iWildcard) == 0)
+ return pTarget;
+ }
+ return nullptr;
+}
+
+CBaseEntity* FindEntityByHammerid(CBaseEntity* pStartEnt, const char* szHammerID)
+{
+ CBaseEntity* pTarget = pStartEnt;
+ while ((pTarget = UTIL_FindEntityByClassname(pTarget, "*")))
+ if (!V_strcmp(pTarget->m_sUniqueHammerID().Get(), szHammerID))
+ return pTarget;
+ return nullptr;
+}
+
+CON_COMMAND_CHAT(bhud_dump, "- Prints the currently loaded BossHUD config to console")
+{
+ if (!g_cvarBossHudEnable.Get())
+ return;
+
+ if (!g_pBossHudHandler)
+ {
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "There has been an error initialising BossHUD.");
+ return;
+ }
+
+ g_pBossHudHandler->PrintLoadedConfig(player);
+}
+
+CON_COMMAND_CHAT_FLAGS(bhud_reload, "- Reloads the current map's BossHUD config", ADMFLAG_CONFIG)
+{
+ if (!g_cvarBossHudEnable.Get() || !GetGlobals())
+ return;
+
+ if (!g_pBossHudHandler)
+ {
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "There has been an error initialising BossHUD.");
+ return;
+ }
+
+ g_pBossHudHandler->LoadConfig(GetGlobals()->mapname.ToCStr());
+ if (!g_pBossHudHandler->IsConfigLoaded())
+ {
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Error reloading config, check console log for details.");
+ return;
+ }
+
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Config reloaded successfully.");
+}
+
+CON_COMMAND_CHAT(bosshud, "- Toggle BossHUD modes")
+{
+ BossHud_Toggle(player);
+}
+
+CON_COMMAND_CHAT(bhud, "- Toggle BossHUD modes")
+{
+ BossHud_Toggle(player);
+}
+
+void BossHud_Toggle(CCSPlayerController* player)
+{
+ if (!g_cvarBossHudEnable.Get())
+ return;
+
+ if (!player)
+ {
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Only usable in game.");
+ return;
+ }
+
+ ZEPlayer* zpPlayer = g_playerManager->GetPlayer(player->GetPlayerSlot());
+ if (!zpPlayer)
+ return;
+
+ // Toggle modes: Show hud/hitmarker -> Hud only -> Hitmarker only -> None
+ BossHudMode mode = (BossHudMode)(zpPlayer->GetBossHudMode());
+ switch (mode)
+ {
+ case BossHudMode::Display_None:
+ zpPlayer->SetBossHudMode((int)BossHudMode::Display_All);
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Showing both boss HUD and hitmarkers.");
+ break;
+ case BossHudMode::Display_All:
+ zpPlayer->SetBossHudMode((int)BossHudMode::Display_Hud);
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Showing boss HUD only.");
+ break;
+ case BossHudMode::Display_Hud:
+ zpPlayer->SetBossHudMode((int)BossHudMode::Display_Hit);
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Showing boss hitmarker only.");
+ break;
+ case BossHudMode::Display_Hit:
+ zpPlayer->SetBossHudMode((int)BossHudMode::Display_None);
+ ClientPrint(player, HUD_PRINTTALK, BOSSHUD_PREFIX "Disabled both boss HUD and hitmarkers.");
+ break;
+ }
+}
\ No newline at end of file
diff --git a/src/bosshud.h b/src/bosshud.h
new file mode 100644
index 000000000..9ca576538
--- /dev/null
+++ b/src/bosshud.h
@@ -0,0 +1,265 @@
+#pragma once
+/**
+ * =============================================================================
+ * CS2Fixes
+ * Copyright (C) 2023-2026 Source2ZE
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+
+#pragma once
+
+#include "ctimer.h"
+#include "detours.h"
+#include "vendor/nlohmann/json_fwd.hpp"
+
+using ordered_json = nlohmann::ordered_json;
+
+#define BOSSHUD_PREFIX " \2[BossHud]\1 "
+#define BOSSHUD_PREF "bhud"
+
+extern CConVar g_cvarBossHudEnable;
+extern CConVar g_cvarBossHudHitmarkerParticle;
+
+enum BossHudMode
+{
+ Display_None, /* Display nothing */
+ Display_All, /* Display hitmarker and hud */
+ Display_Hud, /* Display hud only */
+ Display_Hit, /* Display hitmarker only */
+};
+
+enum BossHudType
+{
+ Invalid, /* Invalid boss type */
+ Breakable, /* Breakable/physbox bosses */
+ BreakableHPBar, /* Breakable + iterator counter + backup breakable bosses*/
+ Counter, /* Math_counter bosses */
+ HPBar, /* Main + iterator + backup counter bosses*/
+};
+
+enum BossHudHPBarType
+{
+ Double, /* Iterator + main only */
+ Triple, /* Iterator + backup + main */
+};
+
+struct BossDamage
+{
+ int slot;
+ float damage;
+};
+
+struct BossConfig
+{
+ int id; // config file order
+
+ std::string szBossName;
+ BossHudType type;
+
+ bool bTriggerUseHammerid;
+ std::string szTrigger;
+ std::string szOutput;
+ float flTriggerDelay;
+
+ bool bShowTriggerUseHammerid;
+ std::string szShowTrigger;
+ std::string szShowOutput;
+ float flShowTriggerDelay;
+ bool bShowOnDecrease; /* Only show when hp decreases (only when trigger & showtrigger arent defined) */
+
+ bool bKillTriggerUseHammerid;
+ std::string szKillTrigger;
+ std::string szKillOutput;
+ float flKillTriggerDelay;
+
+ bool bHurtTriggerUseHammerid;
+ std::string szHurtTrigger;
+ std::string szHurtOutput;
+
+ bool bHitmarkerOnly;
+ bool bUseMinorHud;
+ bool bMultiTrigger;
+ bool bTemplated;
+ bool bShowBeaten;
+ float flTimeout;
+ float flOffset;
+ float flOffsetIterator;
+ float flMaxAllowedHp;
+
+ std::string szBreakable;
+ std::string szCounter;
+ bool bCounterReverse;
+
+ BossHudHPBarType hpBarType;
+ std::string szIterator;
+ bool bIteratorReverse;
+ std::string szBackup;
+
+ void SetDefaultValues();
+ void SetDefaultTriggers();
+ bool IsBreakable() { return type == BossHudType::Breakable; }
+ bool IsBreakableHPBar() { return type == BossHudType::BreakableHPBar; }
+ bool IsCounter() { return type == BossHudType::Counter; }
+ bool IsHPBar() { return type == BossHudType::HPBar; }
+ bool IsInvalidType() { return type == BossHudType::Invalid; }
+
+public:
+ BossConfig(int bossId);
+ BossConfig(ordered_json jsonKeys, int _id);
+
+ bool bTriggered;
+ void Trigger(CHandle hTriggerEntity, int templateNum);
+};
+
+struct BossInstance : BossConfig, public std::enable_shared_from_this
+{
+ bool bCleanup;
+ bool bProcessing; /* Whether boss is being processed */
+ bool bActive; /* Whether boss is active */
+ bool bShowTriggered; /* Whether showtrigger has been triggered */
+ bool bShow; /* Whether boss should be displayed */
+ bool bKillTriggered; /* Whether killtrigger has been triggered */
+ bool bDead; /* Whether boss is dead/invalid */
+
+ float flDirectHealth; /* Actual entity health, used for accurate player damage tracking */
+ float flHealth; /* Health of boss */
+ float flMaxHealth; /* Max health of boss (starting health) */
+ float flSegmentHealth; /* Max health of a segment (double hpbar setup) */
+ int iSegmentsLeft; /* Number of segments left (double hpbar setup) */
+ float flLastChange; /* The game time when boss health changed */
+
+ int iTemplateNum; /* Template number if namefixup */
+ CHandle hBreakableEnt; /* Breakable entity */
+ CHandle hCounterEnt; /* Counter entity */
+ CHandle hIteratorEnt; /* Iterator counter entity */
+ CHandle hBackupEnt; /* Backup counter entity */
+
+ CHandle hTriggerEnt;
+ CHandle hShowTriggerEnt;
+ CHandle hHurtTriggerEnt;
+ CHandle hKillTriggerEnt;
+
+ float flTotalDamage;
+ BossDamage PlayerDamage[MAXPLAYERS + 1];
+
+public:
+ BossInstance(int bossId) :
+ BossConfig(bossId),
+ bCleanup(false),
+ bProcessing(false),
+ bActive(false),
+ bShowTriggered(false),
+ bShow(false),
+ bKillTriggered(false),
+ bDead(false),
+ flDirectHealth(0.0),
+ flHealth(0.0),
+ flMaxHealth(0.0),
+ flSegmentHealth(0.0),
+ iSegmentsLeft(-1),
+ flLastChange(0.0),
+ iTemplateNum(-1),
+ flTotalDamage(0.0)
+ {
+ for (int i = 0; i <= MAXPLAYERS; i++)
+ {
+ PlayerDamage[i].slot = i;
+ PlayerDamage[i].damage = 0.0;
+ }
+ };
+
+ bool FindBreakable();
+ bool FindCounter();
+ bool FindIterator();
+ bool FindBackup();
+ bool FindShowTrigger();
+ bool FindHurtTrigger();
+ bool FindKillTrigger();
+ bool InitHealth();
+
+ void Trigger();
+ bool TriggerPost();
+ void ShowTrigger();
+ void ShowTriggerPost();
+ void Hurt(CBaseEntity* pActivator);
+ void KillTrigger();
+ void KillTriggerPost();
+};
+
+struct SimpleInstance
+{
+ bool bActive;
+ bool bCounter;
+ int iEnt;
+ float flLastHitTime;
+ std::string szTargetname; // Can be hammerID if no targetname
+};
+
+class CSimpleHudHandler
+{
+public:
+ void ClearSimpleHud();
+ std::vector> vecSimpleInstance;
+};
+
+class CBossHudHandler
+{
+public:
+ CBossHudHandler()
+ {
+ bLoadedConfig = false;
+ for (int i = 0; i <= MAXPLAYERS; i++)
+ iAssists[i] = 0;
+ }
+
+ bool bLoadedConfig;
+ bool IsConfigLoaded() { return bLoadedConfig; }
+
+ void UnloadConfig();
+ void LoadConfig(const char* sMapName);
+
+ void PrintLoadedConfig(CCSPlayerController* player);
+
+ void ResetTriggeredBosses();
+ void ResetScoreboard();
+ void ClearBossInstances();
+ void CleanupBosses();
+
+ int iAssists[MAXPLAYERS + 1];
+ void StartAssistTimer();
+
+ std::string szHudText = "";
+
+ std::vector> vecBossConfig; /* Stores all boss configurations in the config */
+ std::vector> vecBossInstance; /* Stores all spawned/triggered boss in the round */
+};
+
+extern CBossHudHandler* g_pBossHudHandler;
+extern CSimpleHudHandler* g_pSimpleHudHandler;
+
+void BossHud_OnLevelInit(const char* sMapName);
+void BossHud_RoundPreStart();
+void BossHud_OnEntityDeleted(CEntityInstance* pEntity);
+void BossHud_PlayerDisconnect(int slot);
+bool BossHud_IsFireOutputHooked();
+void BossHud_FireOutput(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay);
+void SimpleHud_FireOutput(CBaseEntity* pEntity, const char* sOutput);
+void BossHud_Toggle(CCSPlayerController* player);
+float BossHud_UpdateSimpleHud();
+float BossHud_UpdateHud();
+float BossHud_UpdateAssists();
+
+CBaseEntity* FindEntityByTargetname(CBaseEntity* pStartEnt, std::string szTargetname);
+CBaseEntity* FindEntityByHammerid(CBaseEntity* pStartEnt, const char* szHammerID);
\ No newline at end of file
diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp
index cac649b1b..996c77099 100644
--- a/src/cs2fixes.cpp
+++ b/src/cs2fixes.cpp
@@ -22,6 +22,7 @@
#include "adminsystem.h"
#include "appframework/IAppSystem.h"
+#include "bosshud.h"
#include "commands.h"
#include "common.h"
#include "cs_gameevents.pb.h"
@@ -349,6 +350,8 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool
g_pPanoramaVoteHandler = new CPanoramaVoteHandler();
g_pEWHandler = new CEWHandler();
g_pMapMigrations = new CMapMigrations();
+ g_pBossHudHandler = new CBossHudHandler();
+ g_pSimpleHudHandler = new CSimpleHudHandler();
RegisterWeaponCommands();
@@ -498,6 +501,12 @@ bool CS2Fixes::Unload(char* error, size_t maxlen)
if (g_pMapMigrations)
delete g_pMapMigrations;
+ if (g_pBossHudHandler)
+ delete g_pBossHudHandler;
+
+ if (g_pSimpleHudHandler)
+ delete g_pSimpleHudHandler;
+
return true;
}
@@ -1302,6 +1311,9 @@ void CS2Fixes::OnLevelInit(char const* pMapName,
if (g_cvarEnableEntWatch.Get())
EW_OnLevelInit(pMapName);
+ if (g_cvarBossHudEnable.Get())
+ BossHud_OnLevelInit(pMapName);
+
StartFlashingFixTimer();
}
diff --git a/src/entitylistener.cpp b/src/entitylistener.cpp
index 61a672b7b..2ad6d299c 100644
--- a/src/entitylistener.cpp
+++ b/src/entitylistener.cpp
@@ -18,6 +18,7 @@
*/
#include "entitylistener.h"
+#include "bosshud.h"
#include "common.h"
#include "cs2_sdk/entity/cbaseentity.h"
#include "cs2fixes.h"
@@ -56,6 +57,9 @@ void CEntityListener::OnEntityCreated(CEntityInstance* pEntity)
void CEntityListener::OnEntityDeleted(CEntityInstance* pEntity)
{
EW_OnEntityDeleted(pEntity);
+
+ if (g_cvarBossHudEnable.Get())
+ BossHud_OnEntityDeleted(pEntity);
}
void CEntityListener::OnEntityParentChanged(CEntityInstance* pEntity, CEntityInstance* pNewParent)
diff --git a/src/events.cpp b/src/events.cpp
index b8c2af450..809d058d1 100644
--- a/src/events.cpp
+++ b/src/events.cpp
@@ -18,6 +18,7 @@
*/
#include "KeyValues.h"
+#include "bosshud.h"
#include "commands.h"
#include "common.h"
#include "ctimer.h"
@@ -87,6 +88,9 @@ GAME_EVENT_F(round_prestart)
if (g_cvarEnableEntWatch.Get())
EW_RoundPreStart();
+ if (g_cvarBossHudEnable.Get())
+ BossHud_RoundPreStart();
+
g_pMapMigrations->OnRoundPrestart();
}
diff --git a/src/playermanager.cpp b/src/playermanager.cpp
index 4bd473678..d38343785 100644
--- a/src/playermanager.cpp
+++ b/src/playermanager.cpp
@@ -19,6 +19,7 @@
#include "playermanager.h"
#include "adminsystem.h"
+#include "bosshud.h"
#include "commands.h"
#include "ctime"
#include "ctimer.h"
@@ -764,6 +765,19 @@ void ZEPlayer::SetEntwatchHudSize(float flSize)
pText->m_flFontSize = m_flEntwatchHudSize;
}
+int ZEPlayer::GetBossHudMode()
+{
+ if (IsFakeClient())
+ return 0;
+ return m_iBossHudMode;
+}
+
+void ZEPlayer::SetBossHudMode(int iMode)
+{
+ m_iBossHudMode = iMode;
+ g_pUserPreferencesSystem->SetPreferenceInt(m_slot.Get(), BOSSHUD_PREF, m_iBossHudMode);
+}
+
void CPlayerManager::OnBotConnected(CPlayerSlot slot)
{
m_vecPlayers[slot.Get()] = new ZEPlayer(slot, true);
@@ -824,6 +838,9 @@ void CPlayerManager::OnClientDisconnect(CPlayerSlot slot)
if (g_cvarEnableEntWatch.Get())
EW_PlayerDisconnect(slot.Get());
+ if (g_cvarBossHudEnable.Get())
+ BossHud_PlayerDisconnect(slot.Get());
+
delete m_vecPlayers[slot.Get()];
m_vecPlayers[slot.Get()] = nullptr;
diff --git a/src/playermanager.h b/src/playermanager.h
index 458a21a0e..d4bb9c423 100644
--- a/src/playermanager.h
+++ b/src/playermanager.h
@@ -198,6 +198,7 @@ class ZEPlayer
m_flEntwatchHudY = -2.0f;
m_flEntwatchHudSize = 60.0f;
m_bTopDefender = false;
+ m_iBossHudMode = 1;
}
~ZEPlayer()
@@ -269,6 +270,7 @@ class ZEPlayer
void SetEntwatchHudPos(float x, float y);
void SetEntwatchHudSize(float flSize);
void SetTopDefenderStatus(bool bStatus) { m_bTopDefender = bStatus; }
+ void SetBossHudMode(int iMode);
uint64 GetAdminFlags() { return m_iAdminFlags; }
int GetAdminImmunity() { return m_iAdminImmunity; }
@@ -319,6 +321,7 @@ class ZEPlayer
float GetEntwatchHudY() { return m_flEntwatchHudY; }
float GetEntwatchHudSize() { return m_flEntwatchHudSize; }
bool GetTopDefenderStatus() { return m_bTopDefender; }
+ int GetBossHudMode();
void OnSpawn();
void OnAuthenticated();
@@ -393,6 +396,7 @@ class ZEPlayer
float m_flEntwatchHudY;
float m_flEntwatchHudSize;
bool m_bTopDefender;
+ int m_iBossHudMode;
};
class CPlayerManager
diff --git a/src/user_preferences.cpp b/src/user_preferences.cpp
index 04f251e7e..c76993cdc 100644
--- a/src/user_preferences.cpp
+++ b/src/user_preferences.cpp
@@ -18,6 +18,7 @@
*/
#include "user_preferences.h"
+#include "bosshud.h"
#include "commands.h"
#include "common.h"
#include "entwatch.h"
@@ -113,6 +114,7 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot)
bool bNoShake = (bool)GetPreferenceInt(iSlot, NO_SHAKE_PREF_KEY_NAME, 0);
int iButtonWatchMode = GetPreferenceInt(iSlot, BUTTON_WATCH_PREF_KEY_NAME, 0);
bool bZSounds = (bool)GetPreferenceInt(iSlot, ZSOUNDS_PREF_KEY_NAME, 1);
+ int iBossHudMode = GetPreferenceInt(iSlot, BOSSHUD_PREF, 1);
// EntWatch
int iEntwatchMode = GetPreferenceInt(iSlot, EW_PREF_HUD_MODE, 0);
@@ -129,11 +131,14 @@ void CUserPreferencesSystem::OnPutPreferences(int iSlot)
g_playerManager->SetPlayerStopDecals(iSlot, bHideDecals);
g_playerManager->SetPlayerNoShake(iSlot, bNoShake);
+
ZEPlayer* player = g_playerManager->GetPlayer(CPlayerSlot(iSlot));
player->SetHideDistance(iHideDistance);
for (int i = 0; i < iButtonWatchMode; i++)
player->CycleButtonWatch();
+ player->SetBossHudMode(iBossHudMode);
+
// Set EntWatch
player->SetEntwatchHudMode(iEntwatchMode);
player->SetEntwatchHudPos(flEntwatchHudposX, flEntwatchHudposY);
diff --git a/src/utils/hud_manager.h b/src/utils/hud_manager.h
index d4e9615eb..970d2d450 100644
--- a/src/utils/hud_manager.h
+++ b/src/utils/hud_manager.h
@@ -28,6 +28,7 @@ extern CConVar g_cvarFixHudFlashing;
enum class EHudPriority
{
InfectionCountdown = 2,
+ BossHud = 3,
AdminHSay = 99
};
From 1925aca4ed27747e326063fe85e9a51b029a7ec0 Mon Sep 17 00:00:00 2001
From: notkoen <45914779+notkoen@users.noreply.github.com>
Date: Sat, 4 Jul 2026 08:57:07 -0700
Subject: [PATCH 2/2] Update vs solution
---
CS2Fixes.vcxproj | 2 ++
CS2Fixes.vcxproj.filters | 8 +++++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj
index 61b6b5a96..ee8c8eb28 100644
--- a/CS2Fixes.vcxproj
+++ b/CS2Fixes.vcxproj
@@ -168,6 +168,7 @@
+
@@ -224,6 +225,7 @@
+
diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters
index 68bb9f351..dc0136aeb 100644
--- a/CS2Fixes.vcxproj.filters
+++ b/CS2Fixes.vcxproj.filters
@@ -197,6 +197,12 @@
Source Files
+
+ Header Files
+
+
+ Source Files
+
@@ -424,7 +430,7 @@
Header Files
-
+
Header Files