From b75bfaef67e56afe447b68e5358c9defc9432dbd Mon Sep 17 00:00:00 2001 From: Dmitry Novikov Date: Thu, 9 Jul 2026 07:28:44 +0700 Subject: [PATCH 1/9] fix typo --- regamedll/dlls/wpn_shared/wpn_famas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regamedll/dlls/wpn_shared/wpn_famas.cpp b/regamedll/dlls/wpn_shared/wpn_famas.cpp index 6364606f7..0c0355165 100644 --- a/regamedll/dlls/wpn_shared/wpn_famas.cpp +++ b/regamedll/dlls/wpn_shared/wpn_famas.cpp @@ -216,7 +216,7 @@ void CFamas::FamasFire(float flSpread, float flCycleTime, BOOL fUseAutoAim, BOOL void CFamas::Reload() { -#ifndef REGAMEDLL_FXES +#ifndef REGAMEDLL_FIXES if (m_pPlayer->ammo_556nato <= 0) return; #endif From d3401d70157935442d7bd5f722c13ccf742a3fd7 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Thu, 9 Jul 2026 22:57:35 +0700 Subject: [PATCH 2/9] Fix improved hostages AI freezing after map change when bots are disabled (#1162) When hostage_ai_enable is set in CS 1.6 but bots are not allowed (dedicated server without bot_enable), CCSBotManager::LoadNavigationMap returns early and never frees the navigation mesh of the previous map. LoadNavigationMap() then sees a non-empty TheNavAreaList and keeps the stale nav data, so on the next hostage map CHostageImprov is created against the wrong navigation mesh: hostages freeze and stop responding to +use. Destroy the stale navigation data in CHostageManager::ServerActivate before loading, but only when bots are not allowed - otherwise CCSBotManager has already reloaded the nav mesh earlier in the same ServerActivate and holds pointers into it. Fixes #1161 Co-authored-by: Claude Fable 5 --- regamedll/dlls/hostage/hostage.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/regamedll/dlls/hostage/hostage.cpp b/regamedll/dlls/hostage/hostage.cpp index c6f57091c..666b04078 100644 --- a/regamedll/dlls/hostage/hostage.cpp +++ b/regamedll/dlls/hostage/hostage.cpp @@ -1450,6 +1450,11 @@ void CHostageManager::ServerActivate() #ifdef REGAMEDLL_ADD if (!AreRunningCZero()) { + // Free stale navigation data of the previous map, + // CCSBotManager::LoadNavigationMap does this only when bots are allowed + if (!AreBotsAllowed()) + DestroyNavigationMap(); + LoadNavigationMap(); } #endif From 406a0a175acbeadf06e12d0afeb4af2b440b3ed4 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Thu, 9 Jul 2026 23:26:23 +0700 Subject: [PATCH 3/9] Fix global buffer overflow in `SharedParse()` (#1165) * Fix global buffer overflow in SharedParse() SharedParse() copies each parsed token into the fixed-size global buffer s_shared_token[1500] without any bounds checking. A single token (a quoted string or an unquoted word) longer than 1499 bytes overflows the buffer and corrupts adjacent globals, which can crash the server or corrupt state. The buffer is fed from data files parsed via LOAD_FILE_FOR_ME (botprofile.db, BotChatter.db, mapcycle.txt, tutor messages) and from the default-weapon cvars, so a malformed or malicious file/config triggers the overflow. Bound all three write sites to sizeof(s_shared_token) - 1, matching the existing pattern already used in CBasePlayer::ParseAutoBuyString(). Well-formed input is unaffected; only pathologically long tokens are now truncated. Add unit tests covering oversized unquoted/quoted tokens and normal token parsing. Co-Authored-By: Claude Fable 5 * Fix CHECK macro usage in SharedParse unit tests cppunitlite CHECK takes (message, condition); single-argument form broke the Tests build on CI. Co-Authored-By: Claude Fable 5 * Fix global buffer overflow in PrioritizeAutoBuyString() priorityToken[32] is filled from the client-supplied autobuy string (m_autoBuyString) without any bounds checking. A single autobuy token longer than 31 characters overflows the buffer and corrupts adjacent stack state during career-mode auto-buy. Bound the token copy to sizeof(priorityToken) - 1, matching the pattern in ParseAutoBuyString(), and skip the rest of an oversized token so it is not re-split into a spurious priority token on the next iteration. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- regamedll/CMakeLists.txt | 1 + regamedll/dlls/player.cpp | 6 ++- regamedll/game_shared/shared_util.cpp | 11 ++++-- regamedll/msvc/ReGameDLL.vcxproj | 6 +++ regamedll/unittests/shared_util_tests.cpp | 47 +++++++++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 regamedll/unittests/shared_util_tests.cpp diff --git a/regamedll/CMakeLists.txt b/regamedll/CMakeLists.txt index 680de68c2..b06fbde60 100644 --- a/regamedll/CMakeLists.txt +++ b/regamedll/CMakeLists.txt @@ -337,6 +337,7 @@ set(GAMEDLL_SRCS set(UNITTESTS_SRCS "unittests/animation_tests.cpp" + "unittests/shared_util_tests.cpp" "unittests/struct_offsets_tests.cpp" "unittests/TestRunner.cpp" ) diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp index 90572ada2..7eb8f7878 100644 --- a/regamedll/dlls/player.cpp +++ b/regamedll/dlls/player.cpp @@ -9851,7 +9851,7 @@ void CBasePlayer::PrioritizeAutoBuyString(char (&autobuyString)[MAX_AUTOBUY_LENG int i = 0; // get the next token from the priority string. - while (*priorityChar != '\0' && *priorityChar != ' ') + while (*priorityChar != '\0' && *priorityChar != ' ' && i < (int)sizeof(priorityToken) - 1) { priorityToken[i++] = *priorityChar; priorityChar++; @@ -9859,6 +9859,10 @@ void CBasePlayer::PrioritizeAutoBuyString(char (&autobuyString)[MAX_AUTOBUY_LENG priorityToken[i] = '\0'; + // skip the rest of an oversized token that did not fit + while (*priorityChar != '\0' && *priorityChar != ' ') + priorityChar++; + // skip spaces while (*priorityChar == ' ') priorityChar++; diff --git a/regamedll/game_shared/shared_util.cpp b/regamedll/game_shared/shared_util.cpp index c7a2a19dc..380c4d29f 100644 --- a/regamedll/game_shared/shared_util.cpp +++ b/regamedll/game_shared/shared_util.cpp @@ -162,14 +162,17 @@ char *SharedParse(char *data) return data; } - s_shared_token[len++] = c; + // prevent overflow of the fixed-size token buffer + if (len < (int)sizeof(s_shared_token) - 1) + s_shared_token[len++] = c; } } // parse single characters if (c == '{' || c == '}'|| c == ')'|| c == '(' || c == '\'' || c == ',') { - s_shared_token[len++] = c; + if (len < (int)sizeof(s_shared_token) - 1) + s_shared_token[len++] = c; s_shared_token[len] = '\0'; return data + 1; } @@ -177,9 +180,9 @@ char *SharedParse(char *data) // parse a regular word do { - s_shared_token[len] = c; + if (len < (int)sizeof(s_shared_token) - 1) + s_shared_token[len++] = c; data++; - len++; c = *data; if (c == '{' || c == '}'|| c == ')'|| c == '(' || c == '\'' || c == ',') diff --git a/regamedll/msvc/ReGameDLL.vcxproj b/regamedll/msvc/ReGameDLL.vcxproj index 80e93c922..1aa8081b5 100644 --- a/regamedll/msvc/ReGameDLL.vcxproj +++ b/regamedll/msvc/ReGameDLL.vcxproj @@ -579,6 +579,12 @@ true true + + true + true + true + true + true true diff --git a/regamedll/unittests/shared_util_tests.cpp b/regamedll/unittests/shared_util_tests.cpp new file mode 100644 index 000000000..a60a96f0b --- /dev/null +++ b/regamedll/unittests/shared_util_tests.cpp @@ -0,0 +1,47 @@ +#include "precompiled.h" +#include "cppunitlite/TestHarness.h" + +// Regression test for the SharedParse() global-buffer overflow. +// s_shared_token is char[1500]; SharedParse() must never emit a token that +// does not fit, regardless of the size of a single unquoted/quoted token in +// the input. On the unpatched code the oversized-token cases below smash the +// global buffer (crash / memory corruption) before the CHECK is even reached. +const int SHARED_TOKEN_SIZE = 1500; // must match char s_shared_token[1500] + +TEST(RegularWordOverflow, SharedParse, 10) +{ + // One unquoted "word" of 5000 non-space characters (no quotes/specials). + static char input[5001]; + Q_memset(input, 'A', sizeof(input) - 1); + input[sizeof(input) - 1] = '\0'; + + SharedParse(input); + CHECK("oversized word must be truncated", Q_strlen(SharedGetToken()) < SHARED_TOKEN_SIZE); +} + +TEST(QuotedStringOverflow, SharedParse, 10) +{ + // One quoted string of 5000 characters between the quotes. + static char input[5003]; + input[0] = '\"'; + Q_memset(input + 1, 'B', 5000); + input[5001] = '\"'; + input[5002] = '\0'; + + SharedParse(input); + CHECK("oversized quoted string must be truncated", Q_strlen(SharedGetToken()) < SHARED_TOKEN_SIZE); +} + +TEST(NormalTokensStillWork, SharedParse, 10) +{ + // The fix must not change behavior for well-formed input. + char input[] = " primaryWeapon \"hello world\" , "; + char *p = SharedParse(input); + CHECK("plain word token", Q_strcmp(SharedGetToken(), "primaryWeapon") == 0); + + p = SharedParse(p); + CHECK("quoted string token", Q_strcmp(SharedGetToken(), "hello world") == 0); + + p = SharedParse(p); + CHECK("comma token", Q_strcmp(SharedGetToken(), ",") == 0); +} From 8a233535f65f8c474693f75cb161b4c2ca7ab374 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Thu, 9 Jul 2026 23:48:39 +0700 Subject: [PATCH 4/9] Prevent players from getting into the game without a team when both teams are full (#1168) * Prevent mp_forcerespawn from spawning players without a team PlayerRespawnThink only applied the UNASSIGNED/SPECTATOR check to observers, so a non-observer player stuck at the team select menu (both teams full) with mp_forcerespawn > 0 and mp_auto_join_team 1 got a pending respawn and spawned while on TEAM_UNASSIGNED. Fixes #929 Co-Authored-By: Claude Fable 5 * Deny auto-team-select when both teams are full HandleMenu_ChooseTeam with MENU_SLOT_TEAM_RANDOM fell through with team == UNASSIGNED when SelectDefaultTeam() found both teams full and no bot could be vacated: TeamFull(UNASSIGNED) and TeamStacked() pass, so the player joined UNASSIGNED and got into the game, spawning via GetIntoGame() since FPlayerCanRespawn() always passes with mp_forcerespawn > 0. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- regamedll/dlls/client.cpp | 9 +++++++++ regamedll/dlls/player.cpp | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/regamedll/dlls/client.cpp b/regamedll/dlls/client.cpp index d15cffb93..daee11d12 100644 --- a/regamedll/dlls/client.cpp +++ b/regamedll/dlls/client.cpp @@ -1893,6 +1893,15 @@ BOOL EXT_FUNC __API_HOOK(HandleMenu_ChooseTeam)(CBasePlayer *pPlayer, int slot) } } +#ifdef REGAMEDLL_FIXES + // Both teams are full and no bot could be vacated, deny; + // otherwise the player would get into the game while on UNASSIGNED team + if (team == UNASSIGNED) + { + return FALSE; + } +#endif + break; } case MENU_SLOT_TEAM_SPECT: diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp index 7eb8f7878..56c08156e 100644 --- a/regamedll/dlls/player.cpp +++ b/regamedll/dlls/player.cpp @@ -10735,7 +10735,8 @@ bool EXT_FUNC CBasePlayer::__API_HOOK(GetIntoGame)() void CBasePlayer::PlayerRespawnThink() { #ifdef REGAMEDLL_ADD - if (GetObserverMode() != OBS_NONE && (m_iTeam == UNASSIGNED || m_iTeam == SPECTATOR)) + // Players without a team can never respawn (e.g. stuck at team menu when both teams are full) + if (m_iTeam == UNASSIGNED || m_iTeam == SPECTATOR) return; // Player cannot respawn while in the Choose Appearance menu From 815c3f3a86b3b5ea816abf62d0f0427c11f4f0b3 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Thu, 9 Jul 2026 23:48:48 +0700 Subject: [PATCH 5/9] Add mp_hostage_fear cvar to control CZ hostage fear reactions (#1167) When improved hostage AI (hostage_ai_enable 1) is used in CS 1.6, hostages panic and scatter in response to gunfire, explosions and nearby deaths. Setting mp_hostage_fear 0 keeps them calm, restoring 1.6-like hostage behavior while retaining the rest of the improved AI. Related to #1125 Co-authored-by: Claude Fable 5 --- README.md | 1 + dist/game.cfg | 8 ++++++++ regamedll/dlls/game.cpp | 2 ++ regamedll/dlls/game.h | 1 + regamedll/dlls/hostage/hostage_improv.cpp | 6 ++++++ 5 files changed, 18 insertions(+) diff --git a/README.md b/README.md index 7d0892a1b..be2e6a353 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab | mp_timelimit | 0 | - | - | Period between map rotations.
`0` means no limit | | mp_forcerespawn | 0 | 0 | - | Players will automatically respawn when killed.
`0` disabled
`>0.00001` time delay to respawn | | mp_hostage_hurtable | 1 | 0 | 1 | The hostages can take damage.
`0` disabled
`1` from any team
`2` only from `CT`
`3` only from `T` | +| mp_hostage_fear | 1 | 0 | 1 | The hostages with improved AI (`hostage_ai_enable 1`) get scared of gunfire, explosions and nearby deaths.
`0` disabled (hostages stay calm)
`1` enabled | | mp_show_radioicon | 1 | 0 | 1 | Show radio icon.
`0` disabled
`1` enabled | | mp_show_scenarioicon | 0 | 0 | 1 | Show scenario icon in HUD such as count of alive hostages or ticking bomb.
`0` disabled
`1` enabled | | mp_old_bomb_defused_sound | 1 | 0 | 1 | Play "Bomb has been defused" sound instead of "Counter-Terrorists win" when bomb was defused
`0` disabled
`1` enabled | diff --git a/dist/game.cfg b/dist/game.cfg index 4ca190530..66b7d432d 100644 --- a/dist/game.cfg +++ b/dist/game.cfg @@ -177,6 +177,14 @@ mp_forcerespawn "0" // Default value: "1" mp_hostage_hurtable "1" +// The hostages with improved AI (hostage_ai_enable 1) +// get scared of gunfire, explosions and nearby deaths. +// 0 - disabled (hostages stay calm) +// 1 - enabled (default behaviour) +// +// Default value: "1" +mp_hostage_fear "1" + // Show radio icon. // 0 - disabled // 1 - enabled (default behavior) diff --git a/regamedll/dlls/game.cpp b/regamedll/dlls/game.cpp index 2c6d9fbff..971f235ed 100644 --- a/regamedll/dlls/game.cpp +++ b/regamedll/dlls/game.cpp @@ -123,6 +123,7 @@ cvar_t round_restart_delay = { "mp_round_restart_delay", "5", FCVAR_SERVER, 0. cvar_t showtriggers = { "showtriggers", "0", 0, 0.0f, nullptr }; // debug cvar shows triggers // TODO: Maybe it's better to register in the engine? cvar_t hostagehurtable = { "mp_hostage_hurtable", "1", FCVAR_SERVER, 1.0f, nullptr }; +cvar_t hostagefear = { "mp_hostage_fear", "1", FCVAR_SERVER, 1.0f, nullptr }; cvar_t roundover = { "mp_roundover", "0", FCVAR_SERVER, 0.0f, nullptr }; cvar_t forcerespawn = { "mp_forcerespawn", "0", FCVAR_SERVER, 0.0f, nullptr }; cvar_t show_radioicon = { "mp_show_radioicon", "1", 0, 1.0f, nullptr }; @@ -403,6 +404,7 @@ void EXT_FUNC GameDLLInit() CVAR_REGISTER(&showtriggers); CVAR_REGISTER(&hostagehurtable); + CVAR_REGISTER(&hostagefear); CVAR_REGISTER(&roundover); CVAR_REGISTER(&forcerespawn); CVAR_REGISTER(&show_radioicon); diff --git a/regamedll/dlls/game.h b/regamedll/dlls/game.h index ecf1001c8..397810b80 100644 --- a/regamedll/dlls/game.h +++ b/regamedll/dlls/game.h @@ -152,6 +152,7 @@ extern cvar_t round_restart_delay; extern cvar_t showtriggers; extern cvar_t hostagehurtable; +extern cvar_t hostagefear; extern cvar_t roundover; extern cvar_t forcerespawn; extern cvar_t show_radioicon; diff --git a/regamedll/dlls/hostage/hostage_improv.cpp b/regamedll/dlls/hostage/hostage_improv.cpp index 6932d2185..c98a64f98 100644 --- a/regamedll/dlls/hostage/hostage_improv.cpp +++ b/regamedll/dlls/hostage/hostage_improv.cpp @@ -1560,6 +1560,12 @@ bool CHostageImprov::IsScared() const void CHostageImprov::Frighten(ScareType scare) { +#ifdef REGAMEDLL_ADD + // keep hostages calm, they won't get scared of gunfire, explosions or nearby deaths + if (hostagefear.value <= 0.0f) + return; +#endif + const float ignoreTime = 10.0f; if (!IsScared()) From 5329d22101e0a1dc15bc4ff7c89995f97a119702 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Thu, 9 Jul 2026 23:48:56 +0700 Subject: [PATCH 6/9] Add `mp_show_bomb_timer` cvar: show C4 countdown on the HUD round timer (#1164) * Add mp_show_bomb_timer cvar: show C4 countdown on the HUD round timer When enabled, the HUD round timer shows the time remaining until the planted C4 explodes instead of hiding after the bomb plant, and is restored back to the round time after a successful defuse. Co-Authored-By: Claude Fable 5 * Restore the HUD round timer after the bomb explodes With mp_show_bomb_timer enabled the round timer shows the C4 countdown after a plant. If the bomb round-end check is blocked (mp_round_infinite flag e), the explosion does not end the round and the HUD timer stays frozen at 0:00 until the next resync event. Resync the timer for all players in CGrenade::Explode2 so it falls back to the real round time once the bomb has blown, mirroring the resync already done on defuse. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- README.md | 1 + dist/game.cfg | 7 ++++ regamedll/dlls/game.cpp | 4 +++ regamedll/dlls/game.h | 1 + regamedll/dlls/ggrenade.cpp | 13 +++++++ regamedll/dlls/player.cpp | 54 ++++++++++++++++++++++++++++ regamedll/dlls/player.h | 4 +++ regamedll/dlls/wpn_shared/wpn_c4.cpp | 7 ++++ 8 files changed, 91 insertions(+) diff --git a/README.md b/README.md index be2e6a353..7109ce2c1 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab | mp_hostage_fear | 1 | 0 | 1 | The hostages with improved AI (`hostage_ai_enable 1`) get scared of gunfire, explosions and nearby deaths.
`0` disabled (hostages stay calm)
`1` enabled | | mp_show_radioicon | 1 | 0 | 1 | Show radio icon.
`0` disabled
`1` enabled | | mp_show_scenarioicon | 0 | 0 | 1 | Show scenario icon in HUD such as count of alive hostages or ticking bomb.
`0` disabled
`1` enabled | +| mp_show_bomb_timer | 0 | 0 | 1 | Show the time until the planted bomb explodes on the HUD round timer.
`0` disabled
`1` enabled (when the bomb is planted, the round timer shows the C4 countdown) | | mp_old_bomb_defused_sound | 1 | 0 | 1 | Play "Bomb has been defused" sound instead of "Counter-Terrorists win" when bomb was defused
`0` disabled
`1` enabled | | showtriggers | 0 | 0 | 1 | Debug cvar shows triggers. | | sv_alltalk | 0 | 0 | 5 | When players can hear each other ([further explanation](../../wiki/sv_alltalk)).
`0` dead don't hear alive
`1` no restrictions
`2` teammates hear each other
`3` Same as 2, but spectators hear everybody
`4` alive hear alive, dead hear dead and alive.
`5` alive hear alive teammates, dead hear dead and alive. diff --git a/dist/game.cfg b/dist/game.cfg index 66b7d432d..4da256dfa 100644 --- a/dist/game.cfg +++ b/dist/game.cfg @@ -199,6 +199,13 @@ mp_show_radioicon "1" // Default value: "0" mp_show_scenarioicon "0" +// Show the time until the planted bomb explodes on the HUD round timer +// 0 - disabled (default behavior) +// 1 - enabled (when the bomb is planted, the round timer shows the C4 countdown) +// +// Default value: "0" +mp_show_bomb_timer "0" + // Play "Bomb has been defused" sound instead of "Counter-Terrorists win" when bomb was defused // 0 - disabled (default behavior) // 1 - enabled diff --git a/regamedll/dlls/game.cpp b/regamedll/dlls/game.cpp index 971f235ed..54a6269b4 100644 --- a/regamedll/dlls/game.cpp +++ b/regamedll/dlls/game.cpp @@ -202,6 +202,8 @@ cvar_t playerid_field = { "mp_playerid_field", "3", 0, 3.0f, nullptr }; cvar_t knockback = { "mp_knockback", "170", 0, 170.0f, nullptr }; +cvar_t show_bomb_timer = { "mp_show_bomb_timer", "0", 0, 0.0f, nullptr }; + void GameDLL_Version_f() { if (Q_stricmp(CMD_ARGV(1), "version") != 0) @@ -491,6 +493,8 @@ void EXT_FUNC GameDLLInit() CVAR_REGISTER(&knockback); + CVAR_REGISTER(&show_bomb_timer); + // print version CONSOLE_ECHO("ReGameDLL version: " APP_VERSION "\n"); diff --git a/regamedll/dlls/game.h b/regamedll/dlls/game.h index 397810b80..259ac985d 100644 --- a/regamedll/dlls/game.h +++ b/regamedll/dlls/game.h @@ -218,6 +218,7 @@ extern cvar_t randomspawn; extern cvar_t playerid_showhealth; extern cvar_t playerid_field; extern cvar_t knockback; +extern cvar_t show_bomb_timer; #endif diff --git a/regamedll/dlls/ggrenade.cpp b/regamedll/dlls/ggrenade.cpp index 5d89900ff..c130729bc 100644 --- a/regamedll/dlls/ggrenade.cpp +++ b/regamedll/dlls/ggrenade.cpp @@ -119,6 +119,13 @@ void CGrenade::__API_HOOK(Explode2)(TraceResult *pTrace, int bitsDamageType) m_bJustBlew = true; CSGameRules()->CheckWinConditions(); +#ifdef REGAMEDLL_ADD + // restore the HUD round timer if the round did not end on the explosion + // (e.g. mp_round_infinite blocks the bomb round-end check) + if (show_bomb_timer.value != 0.0f) + SyncRoundTimerForAll(); +#endif + // Pull out of the wall a bit if (pTrace->flFraction != 1.0f) { @@ -1155,6 +1162,12 @@ void CGrenade::__API_HOOK(DefuseBombEnd)(CBasePlayer *pPlayer, bool bDefused) g_pGameRules->m_bBombDropped = FALSE; m_pBombDefuser = nullptr; m_bStartDefuse = false; + +#ifdef REGAMEDLL_ADD + // restore the HUD round timer + if (show_bomb_timer.value != 0.0f) + SyncRoundTimerForAll(); +#endif } else { diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp index 56c08156e..1373778a2 100644 --- a/regamedll/dlls/player.cpp +++ b/regamedll/dlls/player.cpp @@ -3603,15 +3603,60 @@ void CBasePlayer::ResetMenu() MESSAGE_END(); } +#ifdef REGAMEDLL_ADD +// Find the planted C4 in the world, if any +static CGrenade *FindPlantedBomb() +{ + CGrenade *pBomb = nullptr; + while ((pBomb = UTIL_FindEntityByClassname(pBomb, "grenade"))) + { + // skip the ones marked for removal (just defused) + if (pBomb->m_bIsC4 && !(pBomb->pev->flags & FL_KILLME)) + return pBomb; + } + + return nullptr; +} + +// Resync the HUD round timer for all players +// (used when the timer switches between the round time and the C4 countdown) +void SyncRoundTimerForAll() +{ + for (int i = 1; i <= gpGlobals->maxClients; i++) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); + if (UTIL_IsValidPlayer(pPlayer)) + pPlayer->SyncRoundTimer(); + } +} +#endif + void CBasePlayer::SyncRoundTimer() { float tmRemaining = 0; BOOL bFreezePeriod = g_pGameRules->IsFreezePeriod(); +#ifdef REGAMEDLL_ADD + bool bBombTimer = false; +#endif + if (g_pGameRules->IsMultiplayer()) { tmRemaining = CSGameRules()->GetRoundRemainingTimeReal(); +#ifdef REGAMEDLL_ADD + // show the time until the planted bomb explodes instead of the round timer + if (show_bomb_timer.value != 0.0f && !bFreezePeriod) + { + CGrenade *pBomb = FindPlantedBomb(); + if (pBomb && pBomb->m_flC4Blow > gpGlobals->time) + { + tmRemaining = pBomb->m_flC4Blow - gpGlobals->time; + bBombTimer = true; + } + } +#endif + #ifdef REGAMEDLL_FIXES // hide timer HUD because it is useless. if (tmRemaining <= 0.0f && CSGameRules()->m_iRoundTime <= 0) { @@ -3631,6 +3676,15 @@ void CBasePlayer::SyncRoundTimer() if (tmRemaining < 0) tmRemaining = 0; +#ifdef REGAMEDLL_ADD + // the client hides the HUD timer once the bomb is planted, re-show it + if (bBombTimer) + { + MESSAGE_BEGIN(MSG_ONE, gmsgShowTimer, nullptr, pev); + MESSAGE_END(); + } +#endif + MESSAGE_BEGIN(MSG_ONE, gmsgRoundTime, nullptr, pev); WRITE_SHORT(int(tmRemaining)); MESSAGE_END(); diff --git a/regamedll/dlls/player.h b/regamedll/dlls/player.h index 78098344f..3e916a44b 100644 --- a/regamedll/dlls/player.h +++ b/regamedll/dlls/player.h @@ -1060,3 +1060,7 @@ bool IsSecondaryWeaponClass(int classId); bool IsSecondaryWeaponId(int id); const char *GetWeaponAliasFromName(const char *weaponName); bool CurrentWeaponSatisfies(CBasePlayerWeapon *pWeapon, int id, int classId); + +#ifdef REGAMEDLL_ADD +void SyncRoundTimerForAll(); +#endif diff --git a/regamedll/dlls/wpn_shared/wpn_c4.cpp b/regamedll/dlls/wpn_shared/wpn_c4.cpp index f5c52b01a..5b8cc93d0 100644 --- a/regamedll/dlls/wpn_shared/wpn_c4.cpp +++ b/regamedll/dlls/wpn_shared/wpn_c4.cpp @@ -197,6 +197,13 @@ void CC4::PrimaryAttack() WRITE_BYTE(BOMB_FLAG_PLANTED); MESSAGE_END(); +#ifdef REGAMEDLL_ADD + // show the C4 countdown on the HUD round timer; + // must be sent after gmsgBombDrop, the client hides the timer on it + if (show_bomb_timer.value != 0.0f) + SyncRoundTimerForAll(); +#endif + UTIL_ClientPrintAll(HUD_PRINTCENTER, "#Bomb_Planted"); if (TheBots) { From 7ddf4dd663e5edb1718033a640b69781b11f8d44 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Fri, 10 Jul 2026 00:00:35 +0700 Subject: [PATCH 7/9] Add mp_knife_wall_sparks cvar: spark effect when a knife hits a wall (#1163) Co-authored-by: Claude Fable 5 --- README.md | 1 + dist/game.cfg | 7 +++++++ regamedll/dlls/game.cpp | 2 ++ regamedll/dlls/game.h | 1 + regamedll/dlls/wpn_shared/wpn_knife.cpp | 12 ++++++++++++ 5 files changed, 23 insertions(+) diff --git a/README.md b/README.md index 7109ce2c1..d224265e1 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ This means that plugins that do binary code analysis (Orpheu for example) probab | mp_playerid_showhealth | 1 | 0 | 2 | Player ID display mode.
`0` don't show health
`1` show health for teammates only (default CS behaviour)
`2` show health for all players | | mp_playerid_field | 3 | 0 | 3 | Player ID field display mode.
`0` don't show additional information
`1` show team name
`2` show health percentage
`3` show both team name and health percentage | | mp_knockback | 170 | - | - | Knockback force applied to the victim when damaged by strong weapons (e.g. `AWP`, `AK47`).
Works only if not crouching, and not hit in the legs.
Set to `0` to disable. | +| mp_knife_wall_sparks | 0 | 0 | 1 | Show a spark effect at the point of impact when a knife hits a wall.
`0` disabled
`1` enabled | diff --git a/dist/game.cfg b/dist/game.cfg index 4da256dfa..fc59a237e 100644 --- a/dist/game.cfg +++ b/dist/game.cfg @@ -713,3 +713,10 @@ mp_playerid_field "3" // // Default: "170" mp_knockback "170" + +// Show a spark effect at the point of impact when a knife hits a wall +// 0 - disabled (default behavior) +// 1 - enabled +// +// Default value: "0" +mp_knife_wall_sparks "0" diff --git a/regamedll/dlls/game.cpp b/regamedll/dlls/game.cpp index 54a6269b4..01e07b92a 100644 --- a/regamedll/dlls/game.cpp +++ b/regamedll/dlls/game.cpp @@ -202,6 +202,7 @@ cvar_t playerid_field = { "mp_playerid_field", "3", 0, 3.0f, nullptr }; cvar_t knockback = { "mp_knockback", "170", 0, 170.0f, nullptr }; +cvar_t knife_wall_sparks = { "mp_knife_wall_sparks", "0", 0, 0.0f, nullptr }; cvar_t show_bomb_timer = { "mp_show_bomb_timer", "0", 0, 0.0f, nullptr }; void GameDLL_Version_f() @@ -493,6 +494,7 @@ void EXT_FUNC GameDLLInit() CVAR_REGISTER(&knockback); + CVAR_REGISTER(&knife_wall_sparks); CVAR_REGISTER(&show_bomb_timer); // print version diff --git a/regamedll/dlls/game.h b/regamedll/dlls/game.h index 259ac985d..741e0a571 100644 --- a/regamedll/dlls/game.h +++ b/regamedll/dlls/game.h @@ -218,6 +218,7 @@ extern cvar_t randomspawn; extern cvar_t playerid_showhealth; extern cvar_t playerid_field; extern cvar_t knockback; +extern cvar_t knife_wall_sparks; extern cvar_t show_bomb_timer; #endif diff --git a/regamedll/dlls/wpn_shared/wpn_knife.cpp b/regamedll/dlls/wpn_shared/wpn_knife.cpp index 43a2f28f2..45269d5e5 100644 --- a/regamedll/dlls/wpn_shared/wpn_knife.cpp +++ b/regamedll/dlls/wpn_shared/wpn_knife.cpp @@ -456,6 +456,12 @@ BOOL CKnife::Swing(BOOL fFirst) { // also play knife strike EMIT_SOUND_DYN(m_pPlayer->edict(), CHAN_ITEM, "weapons/knife_hitwall1.wav", VOL_NORM, ATTN_NORM, 0, RANDOM_LONG(0, 3) + 98); + +#ifdef REGAMEDLL_ADD + // spark effect at the point of impact + if (knife_wall_sparks.value) + UTIL_Sparks(tr.vecEndPos); +#endif } } @@ -633,6 +639,12 @@ BOOL CKnife::Stab(BOOL fFirst) { // also play knife strike EMIT_SOUND_DYN(m_pPlayer->edict(), CHAN_ITEM, "weapons/knife_hitwall1.wav", VOL_NORM, ATTN_NORM, 0, RANDOM_LONG(0, 3) + 98); + +#ifdef REGAMEDLL_ADD + // spark effect at the point of impact + if (knife_wall_sparks.value) + UTIL_Sparks(tr.vecEndPos); +#endif } } From 2195b472e4f3954fd73d730d0460de6a8b38e470 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Fri, 10 Jul 2026 00:03:02 +0700 Subject: [PATCH 8/9] Fix VIP unable to receive/pick up the knife (#1169) Regression from #1045: replacing the hardcoded pistol allow-list with !IsSecondaryWeapon(m_iId) dropped the explicit knife exception. Since the knife is not a secondary weapon, DefaultTouch rejected it for a VIP, so GiveDefaultItems() (which routes weapon_knife through DispatchTouch) left the VIP without a knife while the pistol was still given. Restore the knife exception in the REGAMEDLL_FIXES branch. Co-authored-by: Claude Opus 4.8 --- regamedll/dlls/weapons.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/regamedll/dlls/weapons.cpp b/regamedll/dlls/weapons.cpp index b7e10162c..6663e2e16 100644 --- a/regamedll/dlls/weapons.cpp +++ b/regamedll/dlls/weapons.cpp @@ -626,7 +626,8 @@ void CBasePlayerItem::DefaultTouch(CBaseEntity *pOther) && m_iId != WEAPON_DEAGLE && m_iId != WEAPON_KNIFE #else - !IsSecondaryWeapon(m_iId) + m_iId != WEAPON_KNIFE + && !IsSecondaryWeapon(m_iId) #endif ) From 679973265e1ac99a43193119e0da212ee568f5f9 Mon Sep 17 00:00:00 2001 From: Roman Kelesidis Date: Fri, 10 Jul 2026 00:07:22 +0700 Subject: [PATCH 9/9] Fix HP recovery after surfacing when drowning with DAMAGE_NO (#1170) Drowning damage is tracked in m_idrowndmg and "given back" as health via DMG_DROWNRECOVER once the player takes a breath. The tracker must only ever hold damage that was actually applied. The existing REGAMEDLL_FIXES guard checked the DMG_DROWN bit in m_bitsDamageType, but CBasePlayer::TakeDamage sets that bit unconditionally at the end, even when CBaseMonster::TakeDamage refused the damage because pev->takedamage == DAMAGE_NO. So an invulnerable player still accumulated m_idrowndmg while drowning, and on surfacing that phantom damage was restored as free health (up to max_health). Gate the tracking on the actual return value of TakeDamage instead: it is FALSE both when damage is refused (DAMAGE_NO) and when the tick was lethal (and possibly followed by respawn), which is exactly when we must not track. Co-authored-by: Claude Opus 4.8 --- regamedll/dlls/player.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp index 1373778a2..25b538ef2 100644 --- a/regamedll/dlls/player.cpp +++ b/regamedll/dlls/player.cpp @@ -3240,13 +3240,14 @@ void CBasePlayer::WaterMove() if (pev->dmg > 5) pev->dmg = 5; - TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN); - #ifdef REGAMEDLL_FIXES - // NOTE: If we died after the damage of above and was followed respawn, - // so we should get out of code. - if (!(m_bitsDamageType & DMG_DROWN)) + // Don't track drowning damage that wasn't actually applied (player is + // invulnerable/DAMAGE_NO, or died from it and possibly respawned) - + // otherwise it would be "given back" as free health when surfacing. + if (!TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN)) return; +#else + TakeDamage(VARS(eoNullEntity), VARS(eoNullEntity), pev->dmg, DMG_DROWN); #endif pev->pain_finished = gpGlobals->time + 1;