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