Created
October 16, 2015 05:13
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
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:
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.