diff --git a/gamedata/tf2.utils.nosoop.txt b/gamedata/tf2.utils.nosoop.txt index 989294c..c26378a 100644 --- a/gamedata/tf2.utils.nosoop.txt +++ b/gamedata/tf2.utils.nosoop.txt @@ -148,6 +148,12 @@ "linux" "520" "windows" "520" } + "CTFPlayerShared::m_BleedList" + { + // in MakeBleed + "linux" "524" + "windows" "524" + } "TFCondInfo_t::m_flDuration" { @@ -165,6 +171,47 @@ "linux" "20" "windows" "20" } + + "BleedStruct_t::m_hAttacker" + { + "linux" "0" + "windows" "0" + } + "BleedStruct_t::m_hWeapon" + { + "linux" "4" + "windows" "4" + } + "BleedStruct_t::m_flNextTickTime" + { + "linux" "8" + "windows" "8" + } + "BleedStruct_t::m_flExpireTime" + { + "linux" "12" + "windows" "12" + } + "BleedStruct_t::m_nDamage" + { + "linux" "16" + "windows" "16" + } + "BleedStruct_t::m_bPermanent" + { + "linux" "20" + "windows" "20" + } + "BleedStruct_t::m_nCustomDamageType" + { + "linux" "24" + "windows" "24" + } + "sizeof(BleedStruct_t)" + { + "linux" "28" + "windows" "28" + } "CEconWearable::m_bAlwaysValid" { @@ -238,6 +285,13 @@ "linux" "@_ZN15CTFPlayerShared18GetMaxBuffedHealthEbb" "windows" "\x55\x8B\xEC\x83\xEC\x08\x56\x8B\xF1\x57\x8B\x8E\x2A\x01\x00\x00" } + "CTFPlayerShared::MakeBleed()" + { + // copied from sm-tf2.games + "library" "server" + "linux" "@_ZN15CTFPlayerShared9MakeBleedEP9CTFPlayerP13CTFWeaponBasefibi" + "windows" "\x55\x8B\xEC\x83\xEC\x2C\x57\x8B\xF9\x89\x7D\xF0" + } "CTFPlayerShared::RemoveAllCond()" { // first non-virtual call after semi-unique xref to "Player.Spawn" diff --git a/scripting/include/tf2utils.inc b/scripting/include/tf2utils.inc index 3215a21..213a943 100644 --- a/scripting/include/tf2utils.inc +++ b/scripting/include/tf2utils.inc @@ -146,6 +146,84 @@ native void TF2Util_SetPlayerBurnDuration(int client, float duration); native void TF2Util_IgnitePlayer(int client, int attacker, float duration, int weapon = INVALID_ENT_REFERENCE); +/** + * @param client Client index. + * @return Number of entries within the bleed list. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native int TF2Util_GetPlayerActiveBleedCount(int client); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Attacker client associated with the bleed effect. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native int TF2Util_GetPlayerBleedAttacker(int client, int index); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Weapon associated with the bleed effect, or INVALID_ENT_REFERENCE if invalid. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native int TF2Util_GetPlayerBleedWeapon(int client, int index); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Time remaining until this bleed effect deals damage to the given client. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native float TF2Util_GetPlayerBleedNextDamageTick(int client, int index); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Time remaining for the bleed effect. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native float TF2Util_GetPlayerBleedDuration(int client, int index); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Amount of damage inflicted per bleed 'tick'. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native int TF2Util_GetPlayerBleedDamage(int client, int index); + +/** + * @param client Client index. + * @param index Index into the player's bleed list. + * @return Custom damage type associated with the given bleed effect. + * @error Invalid client index, or bleed list index is out of bounds. + */ +native int TF2Util_GetPlayerBleedCustomDamageType(int client, int index); + +/** + * Induces the bleed effect on a client. This is effectively the same as `TF2_MakeBleed`, with + * support for additional parameters that the game provides. + * + * If the player already has a matching attacker / weapon combination, that entry will be + * updated instead. + * + * @param client Client index. + * @param attacker Attacking client's index. + * @param duration Duration of the effect. If this is TFCondDuration_Infinite, + * bleeding continues until the player removes it via medkit or + * resupply. + * @param weapon Weapon associated with the bleed effect, or INVALID_ENT_REFERENCE to + * not provide a weapon (behaving the same as `TF2_MakeBleed`). + * @param damage Amount of damage inflicted per bleed 'tick'. + * @param damagecustom Custom damage type (see TF_CUSTOM_* constants). + * @return Index into the player's bleed list where the effect was added, or -1 + * if the plugin could not determine the index. + */ +native int TF2Util_MakePlayerBleed(int client, int attacker, float duration, + int weapon = INVALID_ENT_REFERENCE, int damage = 4, + int damagecustom = TF_CUSTOM_BLEEDING); + /** * Returns true if the given player is immune to pushback. * diff --git a/scripting/tf2utils.sp b/scripting/tf2utils.sp index 08da546..d34f511 100644 --- a/scripting/tf2utils.sp +++ b/scripting/tf2utils.sp @@ -12,7 +12,7 @@ #include #include -#define PLUGIN_VERSION "1.1.0" +#define PLUGIN_VERSION "1.2.0" public Plugin myinfo = { name = "TF2 Utils", author = "nosoop", @@ -43,6 +43,7 @@ Handle g_SDKCallPlayerEquipWearable; Handle g_SDKCallPointInRespawnRoom; Handle g_SDKCallPlayerSharedImmuneToPushback; Handle g_SDKCallPlayerSharedBurn; +Handle g_SDKCallPlayerSharedMakeBleed; Address offs_ConditionNames; Address offs_CTFPlayer_aObjects; @@ -55,12 +56,22 @@ float g_flRespawnTimeOverride[MAXPLAYERS + 1] = { -1.0, ... }; Address offs_CTFPlayer_hMyWearables; Address offs_CTFPlayerShared_flBurnDuration; +Address offs_CTFPlayerShared_BleedList; Address offs_CTFPlayerShared_ConditionData; Address offs_CTFPlayerShared_pOuter; Address offs_TFCondInfo_flDuration; Address offs_TFCondInfo_hProvider; +Address offs_BleedStruct_t_hAttacker; +Address offs_BleedStruct_t_hWeapon; +Address offs_BleedStruct_t_flNextBleedTime; +Address offs_BleedStruct_t_flBleedEndTime; +Address offs_BleedStruct_t_nDamage; +Address offs_BleedStruct_t_bPermanent; +Address offs_BleedStruct_t_nCustomDmg; +int sizeof_BleedStruct_t; + Address offs_CEconWearable_bAlwaysValid; int sizeof_TFCondInfo; @@ -88,6 +99,14 @@ public APLRes AskPluginLoad2(Handle self, bool late, char[] error, int maxlen) { CreateNative("TF2Util_GetPlayerBurnDuration", Native_GetPlayerBurnDuration); CreateNative("TF2Util_SetPlayerBurnDuration", Native_SetPlayerBurnDuration); CreateNative("TF2Util_IgnitePlayer", Native_IgnitePlayer); + CreateNative("TF2Util_GetPlayerActiveBleedCount", Native_GetPlayerActiveBleedCount); + CreateNative("TF2Util_GetPlayerBleedAttacker", Native_GetPlayerBleedAttacker); + CreateNative("TF2Util_GetPlayerBleedWeapon", Native_GetPlayerBleedWeapon); + CreateNative("TF2Util_GetPlayerBleedNextDamageTick", Native_GetPlayerBleedNextDamageTick); + CreateNative("TF2Util_GetPlayerBleedDuration", Native_GetPlayerBleedDuration); + CreateNative("TF2Util_GetPlayerBleedDamage", Native_GetPlayerBleedDamage); + CreateNative("TF2Util_GetPlayerBleedCustomDamageType", Native_GetPlayerBleedDamageType); + CreateNative("TF2Util_MakePlayerBleed", Native_MakeBleed); CreateNative("TF2Util_IsPlayerImmuneToPushback", Native_IsPlayerImmuneToPushback); CreateNative("TF2Util_GetPlayerRespawnTimeOverride", Native_GetPlayerRespawnTimeOverride); @@ -191,6 +210,19 @@ public void OnPluginStart() { SetFailState("Failed to set up call to " ... "CTFPlayerShared::Burn()"); } + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFPlayerShared::MakeBleed()"); + PrepSDKCall_AddParameter(SDKType_CBasePlayer, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Bool, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + g_SDKCallPlayerSharedMakeBleed = EndPrepSDKCall(); + if (!g_SDKCallPlayerSharedMakeBleed) { + SetFailState("Failed to set up call to " ... "CTFPlayerShared::MakeBleed()"); + } + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFPlayerShared::IsImmuneToPushback()"); @@ -316,6 +348,24 @@ public void OnPluginStart() { offs_TFCondInfo_hProvider = GameConfGetAddressOffset(hGameConf, "TFCondInfo_t::m_hProvider"); + offs_CTFPlayerShared_BleedList = GameConfGetAddressOffset(hGameConf, + "CTFPlayerShared::m_BleedList"); + offs_BleedStruct_t_hAttacker = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_hAttacker"); + offs_BleedStruct_t_hWeapon = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_hWeapon"); + offs_BleedStruct_t_flNextBleedTime = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_flNextTickTime"); + offs_BleedStruct_t_flBleedEndTime = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_flExpireTime"); + offs_BleedStruct_t_nDamage = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_nDamage"); + offs_BleedStruct_t_bPermanent = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_bPermanent"); + offs_BleedStruct_t_nCustomDmg = GameConfGetAddressOffset(hGameConf, + "BleedStruct_t::m_nCustomDamageType"); + sizeof_BleedStruct_t = GameConfGetOffset(hGameConf, "sizeof(BleedStruct_t)"); + Address pNumConds = GameConfGetAddress(hGameConf, "&TF_COND_LAST"); if (!pNumConds) { LogError("Could not determine location to read TF_COND_LAST from. " @@ -864,6 +914,127 @@ any Native_IgnitePlayer(Handle plugin, int numParams) { duration); } +// int(int client); +any Native_GetPlayerActiveBleedCount(Handle plugin, int numParams) { + int client = GetNativeCell(1); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + return GetPlayerBleedCount(client); +} + +// int(int client, int index); +any Native_GetPlayerBleedAttacker(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + return LoadEntityHandleFromAddress(pBleedInfo + offs_BleedStruct_t_hAttacker); +} + +// int(int client, int index); +any Native_GetPlayerBleedWeapon(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + return LoadEntityHandleFromAddress(pBleedInfo + offs_BleedStruct_t_hWeapon); +} + +// float(int client, int index); +any Native_GetPlayerBleedNextDamageTick(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + float flNextBleedTime = view_as(LoadFromAddress( + pBleedInfo + offs_BleedStruct_t_flNextBleedTime, NumberType_Int32)); + return flNextBleedTime - GetGameTime(); +} + +// float(int client, int index); +any Native_GetPlayerBleedDuration(Handle plugin, int numParams) { + // TODO if is permanent, return -1 + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + + if (LoadFromAddress(pBleedInfo + offs_BleedStruct_t_bPermanent, NumberType_Int8)) { + return -1.0; + } + float flBleedEndTime = view_as(LoadFromAddress( + pBleedInfo + offs_BleedStruct_t_flBleedEndTime, NumberType_Int32)); + return flBleedEndTime - GetGameTime(); +} + +// int(int client, int index); +any Native_GetPlayerBleedDamage(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + return LoadFromAddress(pBleedInfo + offs_BleedStruct_t_nDamage, NumberType_Int32); +} + +// int(int client, int index); +any Native_GetPlayerBleedDamageType(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int index = GetNativeCell(2); + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + Address pBleedInfo = GetPlayerBleedInfo(client, index); + return LoadFromAddress(pBleedInfo + offs_BleedStruct_t_nCustomDmg, NumberType_Int32); +} + +// int(int client, int attacker, float duration, int weapon = INVALID_ENT_REFERENCE, int damage = 4, int damagecustom = TF_CUSTOM_BLEEDING); +any Native_MakeBleed(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int attacker = GetNativeCell(2); + float duration = GetNativeCell(3); + int weapon = GetNativeCell(4); + int damage = GetNativeCell(5); + int damagecustom = GetNativeCell(6); + + if (client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_NATIVE, "Client index %d is invalid", client); + } + if (attacker < 1 || attacker > MaxClients || !IsClientInGame(attacker)) { + ThrowNativeError(SP_ERROR_NATIVE, "Attacker index %d is invalid", client); + } + if (weapon != INVALID_ENT_REFERENCE && (!IsValidEntity(weapon) || !IsEntityWeapon(weapon))) { + ThrowNativeError(SP_ERROR_NATIVE, + "Weapon entity %d is not-NULL and invalid or not a weapon", weapon); + } + + SDKCall(g_SDKCallPlayerSharedMakeBleed, GetPlayerSharedAddress(client), attacker, weapon, + duration, damage, duration == TFCondDuration_Infinite, damagecustom); + + int weaponIndex = EntRefToEntIndex(weapon); + for (int i, n = GetPlayerBleedCount(client); i < n; i++) { + Address pBleedInfo = GetPlayerBleedInfo(client, i); + + // search the bleed list for the index of the bleed + if (LoadEntityHandleFromAddress(pBleedInfo + offs_BleedStruct_t_hWeapon) != weaponIndex + || LoadEntityHandleFromAddress(pBleedInfo + offs_BleedStruct_t_hAttacker) != attacker) { + continue; + } + return i; + } + return -1; +} + // float(int client); any Native_GetPlayerRespawnTimeOverride(Handle plugin, int numParams) { int client = GetNativeCell(1); @@ -935,6 +1106,23 @@ static Address GetConditionData(int client, TFCond cond) { return pCondMemory + view_as
(view_as(cond) * sizeof_TFCondInfo); } +static int GetPlayerBleedCount(int client) { + return GetEntData(client, FindSendPropInfo("CTFPlayer", "m_Shared") + + view_as(offs_CTFPlayerShared_BleedList) + 0xC); +} + +static Address GetPlayerBleedInfo(int client, int index) { + int count = GetPlayerBleedCount(client); + if (index < 0 || index >= count) { + ThrowNativeError(SP_ERROR_NATIVE, "Invalid index %d (count %d)", index, count); + } + + Address pBleedMemory = DereferencePointer(GetEntityAddress(client) + + view_as
(FindSendPropInfo("CTFPlayer", "m_Shared")) + + offs_CTFPlayerShared_BleedList); + return pBleedMemory + view_as
(view_as(index) * sizeof_BleedStruct_t); +} + static Address GetPlayerSharedAddress(int client) { return GetEntityAddress(client) + view_as
(FindSendPropInfo("CTFPlayer", "m_Shared"));