diff --git a/README.md b/README.md index 7d0892a1b..d224265e1 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,10 @@ 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_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. @@ -143,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 4ca190530..fc59a237e 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) @@ -191,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 @@ -698,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/CMakeLists.txt b/regamedll/CMakeLists.txt index 60199e526..b1166d023 100644 --- a/regamedll/CMakeLists.txt +++ b/regamedll/CMakeLists.txt @@ -348,6 +348,7 @@ endif() 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/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/game.cpp b/regamedll/dlls/game.cpp index 2c6d9fbff..01e07b92a 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 }; @@ -201,6 +202,9 @@ 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() { if (Q_stricmp(CMD_ARGV(1), "version") != 0) @@ -403,6 +407,7 @@ void EXT_FUNC GameDLLInit() CVAR_REGISTER(&showtriggers); CVAR_REGISTER(&hostagehurtable); + CVAR_REGISTER(&hostagefear); CVAR_REGISTER(&roundover); CVAR_REGISTER(&forcerespawn); CVAR_REGISTER(&show_radioicon); @@ -489,6 +494,9 @@ void EXT_FUNC GameDLLInit() CVAR_REGISTER(&knockback); + CVAR_REGISTER(&knife_wall_sparks); + 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 ecf1001c8..741e0a571 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; @@ -217,6 +218,8 @@ 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/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/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 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()) diff --git a/regamedll/dlls/player.cpp b/regamedll/dlls/player.cpp index 90572ada2..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; @@ -3603,15 +3604,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 +3677,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(); @@ -9851,7 +9906,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 +9914,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++; @@ -10731,7 +10790,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 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/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 ) 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) { 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 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 } } 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); +}