Skip to content

Instantly share code, notes, and snippets.

@sigsegv-mvm
Created October 16, 2015 05:13
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sigsegv-mvm/b51faf8284151423041b to your computer and use it in GitHub Desktop.
Save sigsegv-mvm/b51faf8284151423041b to your computer and use it in GitHub Desktop.
Reverse-engineered logic for CS:GO's "X just saved Y by killing Z" feature
// CS:GO "saved" player logic
// based on the 20151008 version of the CS:GO Linux dedicated server
//
// reverse engineering by sigsegv
// from x86 assembly to C++ with the help of a disassembler and the Source SDK
// (please don't sue me)
// terminology:
// "enemy" player who was just killed by savior
// "saved" player who was "saved" by the savior
// "savior" player who killed the enemy
// e.g. " ** <savior> just saved <saved> by killing <enemy>! **"
void CCSPlayer::Event_Killed(CTakeDamageInfo const& info)
{
// ...
CBaseEntity *attacker = info.GetAttacker();
CCSPlayer *killer = nullptr;
if (attacker != nullptr && attacker->IsPlayer()) {
killer = dynamic_cast<CCSPlayer *>(attacker);
}
// ...
/* look direction vector for the enemy player */
Vector v_look_this;
AngleVectors(this->EyeAngles(), &v_look_this, nullptr, nullptr);
CCSPlayer *savior = killer;
CCSTeam *team_savior = GetGlobalTeam(savior->GetTeamNumber());
if (team_savior != nullptr) {
/* go through all of the killer's teammates and check to see if this
* kill saved any of them */
for (int i = 0; i < team_savior->GetNumPlayers(); ++i) {
/* skip over any non-players */
CBaseEntity *player = team_savior->GetPlayer(i);
if (player == nullptr || !player->IsPlayer()) {
continue;
}
CCSPlayer *saved = dynamic_cast<CCSPlayer *>(player);
/* the enemy, saved, and savior must be three separate, distinct
* players */
if (this == savior || savior == saved || this == saved) {
continue;
}
/* the killer cannot be on the same team as the saved player,
* and the saved player must not be dead already */
if (savior->IsOtherEnemy(ENTINDEX(saved)) || !saved->IsAlive()) {
continue;
}
/* calculate a unit vector pointing from the enemy player's eye
* position to the saved player's eye position */
Vector v_dir_this_to_saved =
saved->EyePosition() - this->EyePosition();
VectorNormalize(v_dir_this_to_saved);
/* the direction the enemy player was aiming at their time of death
* must be within about 36.9 degrees of the direction toward the
* saved player: arccos(0.80) = 36.9 degrees
*
* in other words, the saved player would need to be visible to the
* enemy player, assuming the enemy had an imaginary ~74 degree
* field-of-view */
if (DotProduct(v_look_this, v_dir_this_to_saved) < 0.80) {
continue;
}
/* run a trace along the line between the enemy player's eye
* position and the saved player's eye position to ensure that the
* line-of-sight is not blocked by any solid objects */
CGameTrace trace;
UTIL_TraceLine(this->EyePosition(), saved->EyePosition(),
MASK_SOLID, this, COLLISION_GROUP_NONE, &trace);
/* ensure that the trace is able to go from the enemy player to the
* saved player, unimpeded by any objects in the way */
if (trace.m_pEnt == nullptr || trace.m_pEnt != saved) {
continue;
}
CWeaponCSBase *weapon = this->GetActiveCSWeapon();
if (weapon != nullptr) {
CSWeaponType w_type = weapon->GetWeaponType();
CSWeaponID w_id = weapon->GetCSWeaponID();
/* the enemy player's active weapon must not be a grenade or the
* bomb (because if they had one of those out, you didn't save
* your teammate from much...) */
if (w_type == WEAPONTYPE_GRENADE ||
w_type == WEAPONTYPE_C4) {
continue;
}
float dist = gametrace.startpos.DistTo(gametrace.endpos);
/* if the enemy player has their knife out, then they must be
* within 80 HU of the saved player for a "save" to occur
* (the knife's range is 32/48 HU for stab/slash) */
if (w_type == WEAPONTYPE_KNIFE && dist > 80.0f) {
continue;
}
/* if the enemy player has the taser out, then they must be
* within 200 HU of the saved player for a "save" to occur
* (the taser's max range is 190 HU) */
if (w_id == WEAPON_TASER && dist > 200.0f) {
continue;
}
}
/* look direction vector for the saved player */
Vector v_look_saved;
AngleVectors(saved->EyeAngles(), &v_look_saved, nullptr, nullptr);
/* calculate a unit vector pointing from the saved player's eye
* position to the enemy player's eye position */
Vector v_dir_saved_to_this =
this->EyePosition() - saved->EyePosition();
VectorNormalize(v_dir_saved_to_this);
/* the direction the saved player was aiming at the enemy's time of
* death must be greater than 49.5 degrees away from the direction
* toward the enemy player: arccos(0.65) = 49.5 degrees
*
* in other words, the saved player would need to be unable to see
* the enemy player, assuming they had an imaginary ~89 degree
* field-of-view */
if (DotProduct(v_look_saved, v_dir_saved_to_this) >= 0.65) {
continue;
}
/* if all of the checks above passed, then the game prints a chat
* message to the savior player, the saved player, and all
* spectators */
/* chat message for the saved player */
CSingleUserRecipientFilter filter_saved(saved);
filter_saved.MakeReliable();
UTIL_ClientPrintFilter(filter_saved, HUD_PRINTTALK,
"#Chat_SavePlayer_Saved",
savior->GetPlayerName(),
this->GetPlayerName());
// [csgo_english.txt] " ** %s1 just saved you by killing %s2! **"
/* chat message for the savior player */
CSingleUserRecipientFilter filter_saved(savior);
filter_savior.MakeReliable();
UTIL_ClientPrintFilter(filter_savior, HUD_PRINTTALK,
"#Chat_SavePlayer_Savior",
saved->GetPlayerName(),
this->GetPlayerName());
// [csgo_english.txt] " ** You just saved %s1 by killing %s2! **"
/* chat message for all spectators */
CTeamRecipientFilter filter_spec(TEAM_SPECTATOR, true);
UTIL_ClientPrintFilter(filter_spec, HUD_PRINTTALK,
"#Chat_SavePlayer_Spectator",
savior->GetPlayerName(),
saved->GetPlayerName(),
this->GetPlayerName());
// [csgo_english.txt] " ** %s1 just saved %s2 by killing %s3! **"
/* never print more than one "saved" message per player killed,
* even if multiple players are ostensibly saved by a single kill */
break;
}
}
// ...
}
enum CSWeaponType
{
WEAPONTYPE_KNIFE = 0,
WEAPONTYPE_PISTOL = 1,
WEAPONTYPE_SUBMACHINEGUN = 2,
WEAPONTYPE_RIFLE = 3,
WEAPONTYPE_SHOTGUN = 4,
WEAPONTYPE_SNIPER_RIFLE = 5,
WEAPONTYPE_MACHINEGUN = 6,
WEAPONTYPE_C4 = 7,
WEAPONTYPE_GRENADE = 8,
WEAPONTYPE_UNKNOWN = 9,
};
enum CSWeaponID
{
WEAPON_NONE = 0,
WEAPON_DEAGLE = 1,
WEAPON_ELITE = 2,
WEAPON_FIVESEVEN = 3,
WEAPON_GLOCK = 4,
WEAPON_P228 = 5, // obsolete (CS:Source)
WEAPON_USP = 6,
WEAPON_AK47 = 7,
WEAPON_AUG = 8,
WEAPON_AWP = 9,
WEAPON_FAMAS = 10,
WEAPON_G3SG1 = 11,
WEAPON_GALIL = 12, // obsolete (CS:Source)
WEAPON_GALILAR = 13,
WEAPON_M249 = 14,
WEAPON_M3 = 15, // obsolete (CS:Source)
WEAPON_M4A1 = 16, // M4A4
WEAPON_M4A1_SILENCER = 16, // M4A1-S
WEAPON_MAC10 = 17,
WEAPON_MP5NAVY = 18, // obsolete (CS:Source)
WEAPON_P90 = 19,
WEAPON_SCOUT = 20, // obsolete (CS:Source)
WEAPON_SG550 = 21, // obsolete (CS:Source)
WEAPON_SG552 = 22, // obsolete (CS:Source)
WEAPON_TMP = 23, // obsolete (CS:Source)
WEAPON_UMP45 = 24,
WEAPON_XM1014 = 25,
WEAPON_BIZON = 26,
WEAPON_MAG7 = 27,
WEAPON_NEGEV = 28,
WEAPON_SAWEDOFF = 29,
WEAPON_TEC9 = 30,
WEAPON_TASER = 31,
WEAPON_HKP2000 = 32,
WEAPON_MP7 = 33,
WEAPON_MP9 = 34,
WEAPON_NOVA = 35,
WEAPON_P250 = 36,
WEAPON_SCAR17 = 37, // obsolete (CS:GO Alpha/Beta?)
WEAPON_SCAR20 = 38,
WEAPON_SG556 = 39, // SG 553
WEAPON_SSG08 = 40,
WEAPON_KNIFEGG = 41, // gungame golden knife
WEAPON_KNIFE = 42,
WEAPON_FLASHBANG = 43,
WEAPON_HEGRENADE = 44,
WEAPON_SMOKEGRENADE = 45,
WEAPON_MOLOTOV = 46,
WEAPON_DECOY = 47,
WEAPON_INCGRENADE = 48, // incendiary grenade
WEAPON_C4 = 49, // bomb
WEAPON_KEVLAR = 50, // vest only
WEAPON_ASSAULTSUIT = 51, // vest and helmet
WEAPON_DEFUSER = 53,
WEAPON_CUTTERS = 54, // your guess is as good as mine
};
@sigsegv-mvm
Copy link
Author

Here's a simplified description for people who aren't programmers.

First, some terminology. There are three players involved: the saved player was about to be shot by the enemy player, but the savior player killed the enemy, thereby saving them.

For the "X just saved Y by killing Z" message to show up, the following conditions must be met at the moment when the enemy player is killed:

  • The savior, saved, and enemy must be 3 separate/distinct players
  • The saved player must not be on the same team as the enemy
  • The saved player must not be dead
  • The enemy player must have their crosshair pointed within ~37 degrees of the saved player's head
  • There must be no solid objects blocking the line-of-sight between the enemy player's head and the saved player's head
  • The saved player must have their crosshair pointed at least ~50 degrees away from the enemy player's head
  • The enemy player must have a weapon other than the grenade or C4 active
    • If the enemy player has their knife out, they must be within 80 Hammer units of the saved player
      • The knife's range is 32 (stab) or 48 (slash) Hammer units
    • If the enemy player has their taser out, they must be within 200 Hammer units of the saved player
      • The taser's max range (past which it drops to 0 damage) is 190 Hammer units
    • Note that the 80/200 thresholds are calculated based on the distance between the players' heads, which is actually a bit higher than the distance at which a player can be hit due to the non-zero width of the players' bodies (hulls)

So in other words, the enemy player must be aiming approximately toward the saved player with an unobstructed line-of-sight, and the saved player must be aiming away from the enemy player. Which makes a whole lot of sense when you look back at the July 1, 2014 update notes:

The game now announces if you’ve been “saved” by someone or have “saved” someone. Saving someone means that you eliminate an enemy who is about to kill one of your teammates who is unaware of that enemy. [emphasis added]

One final thing worth pointing out is that the game will never produce more than one "saved" message per player killed, even if multiple players are ostensibly saved by a single kill. In this situation, the priority for deciding which eligible player is considered to have been "saved" is determined by the order in which those players joined their team; as far as I can tell, the eligible player who joined their team the earliest is picked.

Disclaimer: I haven't actually tested any of this in-game, I've only reverse-engineered the dedicated server code to figure out what ought to happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment