diff --git a/.github/workflows/spcomp.yml b/.github/workflows/spcomp.yml index 5b61827..8367313 100644 --- a/.github/workflows/spcomp.yml +++ b/.github/workflows/spcomp.yml @@ -10,8 +10,8 @@ jobs: - name: Setup SourcePawn Compiler uses: rumblefrog/setup-sp@master with: - version: '1.10.x' + version: '1.11.x' - name: Compile tf2attributes - run: spcomp tf2attributes.sp + run: spcomp scripting/tf2attributes.sp - name: Compile example - run: spcomp -i. tf2attributes_example.sp + run: spcomp -iscripting/include scripting/tf2attributes_example.sp diff --git a/README.md b/README.md index 360f3e2..9cf6bd2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,49 @@ # tf2attributes + TF2Attributes SourceMod plugin https://forums.alliedmods.net/showthread.php?t=210221 + +## Now featuring the following functionality from [nosoop/tf2attributes](https://github.com/nosoop/tf2attributes) + +Add / remove temporary attributes on the player (using the game's own time-based expiry +mechanism for it). + +```sourcepawn +// replicates the temporary health bonus granted by the Dalokohs Bar: +TF2Attrib_AddCustomPlayerAttribute(client, "hidden maxhealth non buffed", 50.0, 30.0); +``` + +Adds the game's "attribute hook" mechanism that collates values using an attribute class: + +```sourcepawn +// computes the final damage multiplier based on the given item and owner's attributes: +float damageBonus = TF2Attrib_HookValueFloat(1.0, "mult_dmg", weapon); +``` + +Support for setting / getting attribute values via strings: + +```sourcepawn +// set an entity's custom projectile model: +TF2Attrib_SetFromStringValue(entity, "custom projectile model", "models/weapons/c_models/c_grenadelauncher/c_grenadelauncher.mdl"); + +// get the name from an item: +TF2Attrib_HookValueString("NO NAME", "custom_name_attr", entity, buffer, sizeof(buffer)); +``` + +Setting custom names / descriptions is not possible. String values that are set by this plugin +are not replicated to the client — this is fine for attributes that are only accessed on +the server, but if you set any that the client will read, the client will crash on access. + +## Installing or updating to 1.7 + +All plugins compiled for previous versions should continue to work with this one. +The installation instructions remain the same as before + +1. Download all the non-source code files in [the latest release][]. +2. Copy `tf2attributes.smx` to `addons/sourcemod/plugins/`. +3. Copy `tf2.attributes.txt` to `addons/sourcemod/gamedata/`. +4. If you're a developer, copy `tf2attributes.inc` to `addons/sourcemod/scripting/include/` +(or the appropriate path for your compiler toolchain / project). + +[the latest release]: https://github.com/flaminsarge/tf2attributes/releases diff --git a/gamedata/tf2.attributes.txt b/gamedata/tf2.attributes.txt index a1aeac4..fdd65b3 100644 --- a/gamedata/tf2.attributes.txt +++ b/gamedata/tf2.attributes.txt @@ -11,6 +11,32 @@ "linux" "14" "mac" "14" } + "CAttributeManager::ApplyAttributeStringWrapper" + { + // linux uses a signature + "windows" "15" + } + "ISchemaAttributeTypeBase::BConvertStringToEconAttributeValue" + { + "windows" "4" + "linux" "5" + } + "ISchemaAttributeTypeBase::InitializeNewEconAttributeValue" + { + "windows" "7" + "linux" "8" + } + "ISchemaAttributeTypeBase::UnloadEconAttributeValue" + { + "windows" "8" + "linux" "9" + } + "ISchemaAttributeTypeBase::BSupportsGame..." + { + // "ISchemaAttributeTypeBase::BSupportsGameplayModificationAndNetworking()" + "windows" "10" + "linux" "11" + } } "Signatures" { @@ -77,6 +103,54 @@ "linux" "@_ZN14CAttributeList20DestroyAllAttributesEv" "mac" "@_ZN14CAttributeList20DestroyAllAttributesEv" } + "CAttributeManager::AttribHookValue" + { + // (float value, string_t attrClass, CBaseEntity* ent, CUtlVector *reentrant, bool const_str) + // called in unique x-ref to "ubercharge_ammo" on Windows + "library" "server" + "linux" "@_ZN17CAttributeManager15AttribHookValueIfEET_S1_PKcPK11CBaseEntityP10CUtlVectorIPS4_10CUtlMemoryIS8_iEEb" + "windows" "\x55\x8B\xEC\x83\xEC\x0C\x8B\x0D\x2A\x2A\x2A\x2A\x53\x56\x57\x33\xF6\x33\xFF\x89\x75\xF4\x89\x7D\xF8\x8B\x41\x08\x85\xC0\x74\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x6A\x6B" + } + "CAttributeManager::AttribHookValue" + { + // (int value, string_t attrClass, CBaseEntity* ent, CUtlVector *reentrant, bool const_str) + // called in unique x-ref to "mod_max_primary_clip_override" on Windows + "library" "server" + "linux" "@_ZN17CAttributeManager15AttribHookValueIiEET_S1_PKcPK11CBaseEntityP10CUtlVectorIPS4_10CUtlMemoryIS8_iEEb" + "windows" "\x55\x8B\xEC\x83\xEC\x10\x8B\x0D\x2A\x2A\x2A\x2A\x53\x56\x57\x33\xFF\x33\xDB\x89\x7D\xF0\x89\x5D\xF4\x8B\x41\x08\x85\xC0\x74\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x68\x2A\x2A\x2A\x2A\x6A\x6B" + } + "CAttributeManager::ApplyAttributeStringWrapper" + { + // uses a hidden pointer, which ends up looking something like this monstrosity: + // (string_t* returnValue, CAttributeManager* this, string_t input, CBaseEntity* initiator, string_t classname, CUtlVector* entityList), returns string_t + // windows uses a (mostly) standard calling convention so we use the vtable call for that + "library" "server" + "linux" "@_ZN17CAttributeManager27ApplyAttributeStringWrapperE8string_tP11CBaseEntityS0_P10CUtlVectorIS2_10CUtlMemoryIS2_iEE" + } + "CTFPlayer::AddCustomAttribute" //(const char*, float, float), returns void + { + "library" "server" + "windows" "\x55\x8B\xEC\xF3\x0F\x10\x4D\x10\x83\xEC\x10" + "linux" "@_ZN9CTFPlayer18AddCustomAttributeEPKcff" + "mac" "@_ZN9CTFPlayer18AddCustomAttributeEPKcff" + } + "CTFPlayer::RemoveCustomAttribute" //(const char*), returns void + { + // called with x-ref string "hidden maxhealth non buffed" + "library" "server" + "windows" "\x55\x8B\xEC\x83\xEC\x10\x53\x56\x57\xFF\x75\x08" + "linux" "@_ZN9CTFPlayer21RemoveCustomAttributeEPKc" + "mac" "@_ZN9CTFPlayer21RemoveCustomAttributeEPKc" + } + "CopyStringAttributeValueToCharPointerOutput" //(CAttribute_String*, char**), returns void + { + // called from CAttributeIterator_GetTypedAttributeValue::OnIterateAttributeValue + // which on Windows has a unique bytesig `55 8B EC 56 8B F1 8B 46 04 3B 45 08 75 ? FF 76 08` + "library" "server" + "windows" "\x55\x8B\xEC\x8B\x45\x08\x8B\x48\x10" + "linux" "@_Z43CopyStringAttributeValueToCharPointerOutputPK17CAttribute_StringPPKc" + "mac" "@_Z43CopyStringAttributeValueToCharPointerOutputPK17CAttribute_StringPPKc" + } } } } diff --git a/scripting/include/tf2attributes.inc b/scripting/include/tf2attributes.inc index 69da43f..d83847a 100644 --- a/scripting/include/tf2attributes.inc +++ b/scripting/include/tf2attributes.inc @@ -13,7 +13,7 @@ * @return True if the attribute was added successfully, false if entity does not have m_AttributeList. * @error Invalid entity index or attribute name passed. */ -native bool TF2Attrib_SetByName(int iEntity, char[] strAttrib, float flValue); +native bool TF2Attrib_SetByName(int iEntity, const char[] strAttrib, float flValue); /** * Sets an attribute's value on an entity, adding it if it isn't on the entity. @@ -27,6 +27,23 @@ native bool TF2Attrib_SetByName(int iEntity, char[] strAttrib, float flValue); */ native bool TF2Attrib_SetByDefIndex(int iEntity, int iDefIndex, float flValue); +/** + * Parses the attribute name and value strings and applies it on the entity. This parses + * numeric and string attributes. + * + * If you use this on a non-numeric attribute, make sure that only the server reads off of that + * attribute. Non-primitive values aren't replicated correctly between the client and the + * server; the client will read garbage and may crash! + * + * @param iEntity Entity index to set the attribute on. Must have m_AttributeList. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * @param strValue Value to set the attribute to. + * + * @return True if the attribute was added successfully, false if the attribute name was invalid. + * @error Invalid entity index or entity does not have m_AttributeList. + */ +native bool TF2Attrib_SetFromStringValue(int iEntity, const char[] strAttrib, const char[] strValue); + /** * Returns the address of an attribute on an entity. * @@ -36,7 +53,7 @@ native bool TF2Attrib_SetByDefIndex(int iEntity, int iDefIndex, float flValue); * @return Address of the attribute on the entity, or Address_Null if the attribute does not exist on the entity. * @error Invalid entity index or attribute name passed. */ -native Address TF2Attrib_GetByName(int iEntity, char[] strAttrib); +native Address TF2Attrib_GetByName(int iEntity, const char[] strAttrib); /** * Returns the address of an attribute (by attribute index) on an entity. @@ -58,7 +75,7 @@ native Address TF2Attrib_GetByDefIndex(int iEntity, int iDefIndex); * @return True if the SDKCall was made, false if entity had invalid address or m_AttributeList missing. * @error Invalid entity index or attribute name passed. */ -native bool TF2Attrib_RemoveByName(int iEntity, char[] strAttrib); +native bool TF2Attrib_RemoveByName(int iEntity, const char[] strAttrib); /** * Removes an attribute from an entity. @@ -134,6 +151,20 @@ native void TF2Attrib_SetValue(Address pAttrib, float flValue); */ native float TF2Attrib_GetValue(Address pAttrib); +/** + * Returns the string data from its raw value representation (a CAttribute_String instance). + * + * WARNING: This dereferences the input value! Feeding it values that aren't CAttribute_String pointers will result in unexpected behavior, potentially crashing the server. + * In the case where you only want the currently active value, use TF2Attrib_HookValueString instead. + * + * @param pRawValue Raw attribute value. You can get this value with either TF2Attrib_GetValue, TF2Attrib_GetSOCAttribs, or TF2Attrib_GetStaticAttribs. + * @param buffer Buffer to store the resulting string to. + * @param maxlen Maximum length of the buffer. + * + * @return Number of bytes written. + */ +native int TF2Attrib_UnsafeGetStringValue(any pRawValue, char[] buffer, int maxlen); + /** * Sets the value of m_nRefundableCurrency on an attribute. * @@ -202,6 +233,72 @@ native int TF2Attrib_GetSOCAttribs(int iEntity, int[] iAttribIndices, float[] fl */ native bool TF2Attrib_IsIntegerValue(int iDefIndex); +/** + * Returns true if an attribute with the specified name exists. + * + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * + * @return True if the attribute exists, false otherwise. + */ +native bool TF2Attrib_IsValidAttributeName(const char[] strAttrib); + +/** + * Adds a custom, potentially temporary attribute to a player. + * + * @param client Client index to set the attribute on. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * @param flValue Value to set m_flValue to. + * @param flDuration Duration of the attribute. If less than 0, the attribute will not be automatically removed. + * + * @noreturn + */ +native void TF2Attrib_AddCustomPlayerAttribute(int client, const char[] strAttrib, float flValue, float flDuration = -1.0); + +/** + * Removes a previously applied custom attribute on a player. + * + * @param client Client index to remove the attribute from. + * @param strAttrib Name of the attribute, as from the "name" key in items_game. + * + * @noreturn + */ +native void TF2Attrib_RemoveCustomPlayerAttribute(int client, const char[] strAttrib); + +/** + * Applies a transformation to the given initial value, following the rules according to the given attribute class. + * + * @param flInitial Initial float value. + * @param attrClass The attribute class, as from the "attribute_class" key in items_game. + * @param iEntity The entity that should be checked. Checking players also checks their equipped items. + * + * @return Transformed initial value. + */ +native float TF2Attrib_HookValueFloat(float flInitial, const char[] attrClass, int iEntity); + +/** + * Applies a transformation to the given initial value, following the rules according to the given attribute class. + * + * @param nInitial Initial integer value. + * @param attrClass The attribute class, as from the "attribute_class" key in items_game. + * @param iEntity The entity that should be checked. Checking players also checks their equipped items. + * + * @return Transformed initial value. + */ +native int TF2Attrib_HookValueInt(int nInitial, const char[] attrClass, int iEntity); + +/** + * Applies a transformation to the given initial value, following the rules according to the given attribute class. + * + * @param initial Initial string value. (This appears to only be returned if the entity doesn't have the attribute.) + * @param attrClass The attribute class, as from the "attribute_class" key in items_game. + * @param iEntity The entity that should be checked. Checking players also checks their equipped items. + * @param buffer Transformed initial value. + * @param maxlen Buffer size. + * + * @return Number of bytes written. + */ +native int TF2Attrib_HookValueString(const char[] initial, const char[] attrClass, int iEntity, char[] buffer, int maxlen); + /** * Gets whether the plugin loaded without ANY errors. * For the purpose of allowing dependencies to ignore the plugin if this returns false. @@ -244,63 +341,12 @@ public void __pl_tf2attributes_SetNTVOptional() MarkNativeAsOptional("TF2Attrib_GetSOCAttribs"); MarkNativeAsOptional("TF2Attrib_ListDefIndices"); MarkNativeAsOptional("TF2Attrib_IsIntegerValue"); + MarkNativeAsOptional("TF2Attrib_IsValidAttributeName"); + MarkNativeAsOptional("TF2Attrib_AddCustomPlayerAttribute"); + MarkNativeAsOptional("TF2Attrib_RemoveCustomPlayerAttribute"); + MarkNativeAsOptional("TF2Attrib_HookValueFloat"); + MarkNativeAsOptional("TF2Attrib_HookValueInt"); + MarkNativeAsOptional("TF2Attrib_IsReady"); - -// MarkNativeAsOptional("TF2Attrib_SetInitialValue"); -// MarkNativeAsOptional("TF2Attrib_GetInitialValue"); -// MarkNativeAsOptional("TF2Attrib_SetIsSetBonus"); -// MarkNativeAsOptional("TF2Attrib_GetIsSetBonus"); } #endif - -//OLD things lie here -//flInitialValue and bSetBonus don't exist anymore -/** - * Sets the value of m_flInitialValue on an attribute. - * - * @param pAttrib Address of the attribute. - * @param flValue Value to set m_flInitialValue to. - * - * @noreturn - */ -//native TF2Attrib_SetInitialValue(Address pAttrib, float flValue); - -/** - * Returns the value of m_flInitialValue on an attribute. - * - * @param pAttrib Address of the attribute. - * - * @return The floating point value of m_flInitialValue on the attribute. - */ -//native float TF2Attrib_GetInitialValue(Address pAttrib); - -/** - * Sets the boolean value of m_bSetBonus on an attribute. - * - * @param pAttrib Address of the attribute. - * @param bSetBonus Value to set m_bSetBonus to. - * - * @noreturn - */ -//native TF2Attrib_SetIsSetBonus(Address pAttrib, bool bSetBonus); - -/** - * Returns the boolean value of m_bSetBonus on an attribute. - * - * @param pAttrib Address of the attribute. - * - * @return The boolean value of m_bSetBonus on the attribute. - */ -//native bool TF2Attrib_GetIsSetBonus(Address pAttrib); - -//stock TF2Attrib_IsIntegerValue(iDefIndex) -//{ -// switch (iDefIndex) -// { -// case 133, 143, 147, 152, 184, 185, 186, 192, 193, 194, 198, 211, 214, 227, 228, 229, 262, 294, 302, 372, 373, 374, 379, 381, 383, 403, 420: -// { -// return true; -// } -// } -// return false; -//} diff --git a/scripting/tf2attributes.sp b/scripting/tf2attributes.sp index ec89f56..4a38213 100644 --- a/scripting/tf2attributes.sp +++ b/scripting/tf2attributes.sp @@ -3,44 +3,86 @@ #include #include +#pragma newdecls required + #define PLUGIN_NAME "[TF2] TF2Attributes" #define PLUGIN_AUTHOR "FlaminSarge" -#define PLUGIN_VERSION "1.3.2" +#define PLUGIN_VERSION "1.7.2" #define PLUGIN_CONTACT "http://forums.alliedmods.net/showthread.php?t=210221" #define PLUGIN_DESCRIPTION "Functions to add/get attributes for TF2 players/items" -public Plugin:myinfo = { - name = PLUGIN_NAME, - author = PLUGIN_AUTHOR, - description = PLUGIN_DESCRIPTION, - version = PLUGIN_VERSION, - url = PLUGIN_CONTACT +public Plugin myinfo = { + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESCRIPTION, + version = PLUGIN_VERSION, + url = PLUGIN_CONTACT }; -new Handle:hSDKGetItemDefinition; -new Handle:hSDKGetSOCData; -new Handle:hSDKSchema; -new Handle:hSDKGetAttributeDef; -new Handle:hSDKGetAttributeDefByName; -new Handle:hSDKSetRuntimeValue; -new Handle:hSDKGetAttributeByID; -new Handle:hSDKOnAttribValuesChanged; -new Handle:hSDKRemoveAttribute; -new Handle:hSDKDestroyAllAttributes; - -//new Handle:hPluginReady; -new bool:g_bPluginReady = false; -public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) -{ - decl String:game[8]; +// "counts as assister is some kind of pet this update is going to be awesome" is 73 characters. Valve... Valve. +#define MAX_ATTRIBUTE_NAME_LENGTH 128 +#define MAX_ATTRIBUTE_VALUE_LENGTH PLATFORM_MAX_PATH + +Handle hSDKGetItemDefinition; +Handle hSDKGetSOCData; +Handle hSDKSchema; +Handle hSDKGetAttributeDef; +Handle hSDKGetAttributeDefByName; +Handle hSDKSetRuntimeValue; +Handle hSDKGetAttributeByID; +Handle hSDKOnAttribValuesChanged; +Handle hSDKRemoveAttribute; +Handle hSDKDestroyAllAttributes; +Handle hSDKAddCustomAttribute; +Handle hSDKRemoveCustomAttribute; +Handle hSDKAttributeHookFloat; +Handle hSDKAttributeHookInt; + +// these two are mutually exclusive +Handle hSDKAttributeApplyStringWrapperWindows; +Handle hSDKAttributeApplyStringWrapperLinux; + +Handle hSDKAttributeValueInitialize; +Handle hSDKAttributeTypeCanBeNetworked; +Handle hSDKAttributeValueFromString; +Handle hSDKAttributeValueUnload; +Handle hSDKAttributeValueUnloadByRef; +Handle hSDKCopyStringAttributeToCharPointer; + +// caches attribute name to definition instance +StringMap g_AttributeDefinitionMapping; + +// caches string_t instances from AllocPooledString +StringMap g_AllocPooledStringCache; + +/** + * since the game doesn't free heap-allocated non-GC attributes, we're taking on that + * responsibility + */ +enum struct HeapAttributeValue { + Address m_pAttributeValue; + int m_iAttributeDefinitionIndex; + + void Destroy() { + Address pAttrDef = GetAttributeDefinitionByID(this.m_iAttributeDefinitionIndex); + UnloadAttributeRawValue(pAttrDef, this.m_pAttributeValue); + } +} +ArrayList g_ManagedAllocatedValues; + +static bool g_bPluginReady = false; +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { + char game[8]; GetGameFolderName(game, sizeof(game)); - if (strncmp(game, "tf", 2, false) != 0) - { + + if (strncmp(game, "tf", 2, false) != 0) { strcopy(error, err_max, "Plugin only available for TF2 and possibly TF2Beta"); return APLRes_Failure; } + CreateNative("TF2Attrib_SetByName", Native_SetAttrib); CreateNative("TF2Attrib_SetByDefIndex", Native_SetAttribByID); + CreateNative("TF2Attrib_SetFromStringValue", Native_SetAttribStringByName); CreateNative("TF2Attrib_GetByName", Native_GetAttrib); CreateNative("TF2Attrib_GetByDefIndex", Native_GetAttribByID); CreateNative("TF2Attrib_RemoveByName", Native_Remove); @@ -50,6 +92,7 @@ public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) CreateNative("TF2Attrib_GetDefIndex", Native_GetID); CreateNative("TF2Attrib_SetValue", Native_SetVal); CreateNative("TF2Attrib_GetValue", Native_GetVal); + CreateNative("TF2Attrib_UnsafeGetStringValue", Native_GetStringVal); CreateNative("TF2Attrib_SetRefundableCurrency", Native_SetCurrency); CreateNative("TF2Attrib_GetRefundableCurrency", Native_GetCurrency); CreateNative("TF2Attrib_ClearCache", Native_ClearCache); @@ -57,98 +100,86 @@ public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) CreateNative("TF2Attrib_GetStaticAttribs", Native_GetStaticAttribs); CreateNative("TF2Attrib_GetSOCAttribs", Native_GetSOCAttribs); CreateNative("TF2Attrib_IsIntegerValue", Native_IsIntegerValue); + CreateNative("TF2Attrib_IsValidAttributeName", Native_IsValidAttributeName); + CreateNative("TF2Attrib_AddCustomPlayerAttribute", Native_AddCustomAttribute); + CreateNative("TF2Attrib_RemoveCustomPlayerAttribute", Native_RemoveCustomAttribute); + CreateNative("TF2Attrib_HookValueFloat", Native_HookValueFloat); + CreateNative("TF2Attrib_HookValueInt", Native_HookValueInt); + CreateNative("TF2Attrib_HookValueString", Native_HookValueString); CreateNative("TF2Attrib_IsReady", Native_IsReady); - //hPluginReady = CreateGlobalForward("TF2Attrib_Ready", ET_Ignore); //unused, backcompat I guess? - CreateNative("TF2Attrib_SetInitialValue", Native_SetInitialVal); - CreateNative("TF2Attrib_GetInitialValue", Native_GetInitialVal); - CreateNative("TF2Attrib_SetIsSetBonus", Native_SetSetBonus); - CreateNative("TF2Attrib_GetIsSetBonus", Native_GetSetBonus); + CreateNative("TF2Attrib_SetInitialValue", Native_DeprecatedPropertyAccess); + CreateNative("TF2Attrib_GetInitialValue", Native_DeprecatedPropertyAccess); + CreateNative("TF2Attrib_SetIsSetBonus", Native_DeprecatedPropertyAccess); + CreateNative("TF2Attrib_GetIsSetBonus", Native_DeprecatedPropertyAccess); RegPluginLibrary("tf2attributes"); return APLRes_Success; } -public Native_IsReady(Handle:plugin, numParams) -{ +public int Native_IsReady(Handle plugin, int numParams) { return g_bPluginReady; } -public OnPluginStart() -{ - new Handle:hGameConf = LoadGameConfigFile("tf2.attributes"); - new bool:bPluginReady = true; //we don't want to set g_bPluginReady BEFORE any of the checks... do we? W/e, I never asked for this. - if (hGameConf == INVALID_HANDLE) - { +public void OnPluginStart() { + Handle hGameConf = LoadGameConfigFile("tf2.attributes"); + if (!hGameConf) { SetFailState("Could not locate gamedata file tf2.attributes.txt for TF2Attributes, pausing plugin"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CEconItemSchema::GetItemDefinition"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of CEconItemDefinition hSDKGetItemDefinition = EndPrepSDKCall(); - if (hSDKGetItemDefinition == INVALID_HANDLE) - { - LogError("Could not initialize call to CEconItemSchema::GetItemDefinition"); - bPluginReady = false; + if (!hSDKGetItemDefinition) { + SetFailState("Could not initialize call to CEconItemSchema::GetItemDefinition"); } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CEconItemView::GetSOCData"); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of CEconItem hSDKGetSOCData = EndPrepSDKCall(); - if (hSDKGetSOCData == INVALID_HANDLE) - { - LogError("Could not initialize call to CEconItemView::GetSOCData"); - bPluginReady = false; + if (!hSDKGetSOCData) { + SetFailState("Could not initialize call to CEconItemView::GetSOCData"); } - + StartPrepSDKCall(SDKCall_Static); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "GEconItemSchema"); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of CEconItemSchema hSDKSchema = EndPrepSDKCall(); - if (hSDKSchema == INVALID_HANDLE) - { + if (!hSDKSchema) { SetFailState("Could not initialize call to GEconItemSchema"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CEconItemSchema::GetAttributeDefinition"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of a CEconItemAttributeDefinition hSDKGetAttributeDef = EndPrepSDKCall(); - if (hSDKGetAttributeDef == INVALID_HANDLE) - { + if (!hSDKGetAttributeDef) { SetFailState("Could not initialize call to CEconItemSchema::GetAttributeDefinition"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CEconItemSchema::GetAttributeDefinitionByName"); PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of a CEconItemAttributeDefinition hSDKGetAttributeDefByName = EndPrepSDKCall(); - if (hSDKGetAttributeDefByName == INVALID_HANDLE) - { + if (!hSDKGetAttributeDefByName) { SetFailState("Could not initialize call to CEconItemSchema::GetAttributeDefinitionByName"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeList::RemoveAttribute"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //not a clue what this return is hSDKRemoveAttribute = EndPrepSDKCall(); - if (hSDKRemoveAttribute == INVALID_HANDLE) - { + if (!hSDKRemoveAttribute) { SetFailState("Could not initialize call to CAttributeList::RemoveAttribute"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeList::SetRuntimeAttributeValue"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); @@ -158,619 +189,1196 @@ public OnPluginStart() //Just a note, the above SDKCall returns ((entindex + 4) * 4) | 0xA000), and you can AND it with 0x1FFF to get back the entindex if you want, though it's pointless) //I don't know any other specifics, such as if the highest 3 bits actually matter //And I don't know what happens when you hit ent index 2047 - + hSDKSetRuntimeValue = EndPrepSDKCall(); - if (hSDKSetRuntimeValue == INVALID_HANDLE) - { + if (!hSDKSetRuntimeValue) { SetFailState("Could not initialize call to CAttributeList::SetRuntimeAttributeValue"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeList::DestroyAllAttributes"); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); hSDKDestroyAllAttributes = EndPrepSDKCall(); - if (hSDKDestroyAllAttributes == INVALID_HANDLE) - { + if (!hSDKDestroyAllAttributes) { SetFailState("Could not initialize call to CAttributeList::DestroyAllAttributes"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeList::GetAttributeByID"); PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); //Returns address of a CEconItemAttribute hSDKGetAttributeByID = EndPrepSDKCall(); - if (hSDKGetAttributeByID == INVALID_HANDLE) - { + if (!hSDKGetAttributeByID) { SetFailState("Could not initialize call to CAttributeList::GetAttributeByID"); - bPluginReady = false; } - + StartPrepSDKCall(SDKCall_Raw); PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, "CAttributeManager::OnAttributeValuesChanged"); hSDKOnAttribValuesChanged = EndPrepSDKCall(); - if (hSDKOnAttribValuesChanged == INVALID_HANDLE) - { + if (!hSDKOnAttribValuesChanged) { SetFailState("Could not initialize call to CAttributeManager::OnAttributeValuesChanged"); - bPluginReady = false; } - + + StartPrepSDKCall(SDKCall_Player); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFPlayer::AddCustomAttribute"); + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); + hSDKAddCustomAttribute = EndPrepSDKCall(); + if (!hSDKAddCustomAttribute) { + SetFailState("Could not initialize call to CTFPlayer::AddCustomAttribute"); + } + + StartPrepSDKCall(SDKCall_Player); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CTFPlayer::RemoveCustomAttribute"); + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + hSDKRemoveCustomAttribute = EndPrepSDKCall(); + if (!hSDKRemoveCustomAttribute) { + SetFailState("Could not initialize call to CTFPlayer::AddCustomAttribute"); + } + + StartPrepSDKCall(SDKCall_Static); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeManager::AttribHookValue"); + PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Float, SDKPass_Plain); // initial value + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); // attribute class + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); // CBaseEntity* entity + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // CUtlVector, set to nullptr + PrepSDKCall_AddParameter(SDKType_Bool, SDKPass_Plain); // bool const_string + hSDKAttributeHookFloat = EndPrepSDKCall(); + if (!hSDKAttributeHookFloat) { + SetFailState("Could not initialize call to CAttributeManager::AttribHookValue"); + } + + StartPrepSDKCall(SDKCall_Static); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeManager::AttribHookValue"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // initial value + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); // attribute class + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); // CBaseEntity* entity + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // CUtlVector, set to nullptr + PrepSDKCall_AddParameter(SDKType_Bool, SDKPass_Plain); // bool const_string + hSDKAttributeHookInt = EndPrepSDKCall(); + if (!hSDKAttributeHookInt) { + SetFailState("Could not initialize call to CAttributeManager::AttribHookValue"); + } + + // linux signature. this uses a hidden pointer passed in before `this` on the stack + // so we'll do our best with static since SM doesn't support that calling convention + // no subclasses override this virtual function so we'll just call it directly + StartPrepSDKCall(SDKCall_Static); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, "CAttributeManager::ApplyAttributeStringWrapper"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); // return string_t + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer); // return value + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // thisptr + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // string_t initial value + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); // initator entity (should contain thisptr) + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // string_t attribute class + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // CUtlVector, set to nullptr + hSDKAttributeApplyStringWrapperLinux = EndPrepSDKCall(); + + if (!hSDKAttributeApplyStringWrapperLinux) { + // windows vcall. this one also uses a hidden pointer, but it's passed as the first param + // `this` remains unchanged so we can still use a vcall + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, "CAttributeManager::ApplyAttributeStringWrapper"); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); // return string_t + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer); // return value too + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // string_t initial value + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); // CBaseEntity* entity + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // string_t attribute class + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // CUtlVector, set to nullptr + hSDKAttributeApplyStringWrapperWindows = EndPrepSDKCall(); + } + + if (!hSDKAttributeApplyStringWrapperWindows && !hSDKAttributeApplyStringWrapperLinux) { + SetFailState("Could not initialize call to CAttributeManager::ApplyAttributeStringWrapper"); + } + + StartPrepSDKCall(SDKCall_Raw); // CEconItemAttribute* + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, + "ISchemaAttributeTypeBase::InitializeNewEconAttributeValue"); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); // CAttributeDefinition* + hSDKAttributeValueInitialize = EndPrepSDKCall(); + if (!hSDKAttributeValueInitialize) { + SetFailState("Could not initialize call to ISchemaAttributeTypeBase::InitializeNewEconAttributeValue"); + } + + StartPrepSDKCall(SDKCall_Raw); // attr_type + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, + "ISchemaAttributeTypeBase::BSupportsGame..."); // 64 chars ought to be enough for anyone -- dvander, probably + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + hSDKAttributeTypeCanBeNetworked = EndPrepSDKCall(); + if (!hSDKAttributeTypeCanBeNetworked) { + SetFailState("Could not initialize call to ISchemaAttributeTypeBase::BSupportsGameplayModificationAndNetworking"); + } + + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, + "ISchemaAttributeTypeBase::BConvertStringToEconAttributeValue"); + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_Bool, SDKPass_Plain); + hSDKAttributeValueFromString = EndPrepSDKCall(); + if (!hSDKAttributeValueFromString) { + SetFailState("Could not initialize call to ISchemaAttributeTypeBase::BConvertStringToEconAttributeValue"); + } + + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, + "ISchemaAttributeTypeBase::UnloadEconAttributeValue"); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + hSDKAttributeValueUnload = EndPrepSDKCall(); + if (!hSDKAttributeValueUnload) { + SetFailState("Could not initialize call to ISchemaAttributeTypeBase::UnloadEconAttributeValue"); + } + + StartPrepSDKCall(SDKCall_Raw); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Virtual, + "ISchemaAttributeTypeBase::UnloadEconAttributeValue"); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer); + hSDKAttributeValueUnloadByRef = EndPrepSDKCall(); + if (!hSDKAttributeValueUnloadByRef) { + SetFailState("Could not initialize call to ISchemaAttributeTypeBase::UnloadEconAttributeValue"); + } + + StartPrepSDKCall(SDKCall_Static); + PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature, + "CopyStringAttributeValueToCharPointerOutput"); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL, VENCODE_FLAG_COPYBACK); // char**, variable contains char* on return + hSDKCopyStringAttributeToCharPointer = EndPrepSDKCall(); + if (!hSDKCopyStringAttributeToCharPointer) { + SetFailState("Could not initialize call to CopyStringAttributeValueToCharPointerOutput"); + } + CreateConVar("tf2attributes_version", PLUGIN_VERSION, "TF2Attributes version number", FCVAR_NOTIFY); -// Call_StartForward(hPluginReady); -// Call_Finish(); - g_bPluginReady = bPluginReady; //I really never asked for this. + + g_bPluginReady = true; + + delete hGameConf; + + g_ManagedAllocatedValues = new ArrayList(sizeof(HeapAttributeValue)); + g_AttributeDefinitionMapping = new StringMap(); + + g_AllocPooledStringCache = new StringMap(); } -stock bool:Internal_IsIntegerValue(iDefIndex) -{ - switch (iDefIndex) - { - case 133, 143, 147, 152, 184, 185, 186, 192, 193, 194, 198, 211, 214, 227, 228, 229, 262, 294, 302, 372, 373, 374, 379, 381, 383, 403, 420, 371, 500, 501, 2010, 2011, 2021, 2023, 2024: - { - return true; - } - } - return false; +public void OnPluginEnd() { + /** + * We don't need to do remove-on-entities on map end since their runtime lists will be gone, + * but we do need to remove them when the plugin is unloaded / reloaded, since we manage + * runtime non-networked attributes ourselves and they don't outlive the plugin. + */ + RemoveNonNetworkedRuntimeAttributesOnEntities(); + DestroyManagedAllocatedValues(); } -public Native_IsIntegerValue(Handle:plugin, numParams) -{ - new iDefIndex = GetNativeCell(1); - return Internal_IsIntegerValue(iDefIndex); +/** + * Free up all attribute values that we allocated ourselves. + */ +public void OnMapEnd() { + DestroyManagedAllocatedValues(); + + // because attribute injection's a thing now, we invalidate our internal mappings + // in case everything changes during the next map + g_AttributeDefinitionMapping.Clear(); + + // pooled strings might get purged only between map changes + g_AllocPooledStringCache.Clear(); } -stock GetStaticAttribs(Address:pItemDef, iAttribIndices[], iAttribValues[], size = 16) -{ - if (!IsValidAddress(pItemDef)) return 0; //...-1 maybe? - new iNumAttribs = LoadFromAddress(pItemDef + Address:0x28, NumberType_Int32); - new Address:pAttribList = Address:LoadFromAddress(pItemDef + Address:0x1C, NumberType_Int32); - for (new i = 0; i < iNumAttribs && i < size; i++) //THIS IS HOW YOU GET THE ATTRIBUTES ON AN ITEMDEF! - { - iAttribIndices[i] = LoadFromAddress(pAttribList + Address:(i * 8), NumberType_Int16); - iAttribValues[i] = LoadFromAddress(pAttribList + Address:(i * 8 + 4), NumberType_Int32); +/* native bool TF2Attrib_IsIntegerValue(int iDefIndex); */ +public int Native_IsIntegerValue(Handle plugin, int numParams) { + int iDefIndex = GetNativeCell(1); + + Address pEconItemAttributeDefinition = GetAttributeDefinitionByID(iDefIndex); + if (!pEconItemAttributeDefinition) { + return ThrowNativeError(1, "Attribute index %d is invalid", iDefIndex); + } + + return LoadFromAddressOffset(pEconItemAttributeDefinition, 0x0E, NumberType_Int8); +} + +static int GetStaticAttribs(Address pItemDef, int[] iAttribIndices, int[] iAttribValues, int size = 16) { + AssertValidAddress(pItemDef); + + // 0x1C = CEconItemDefinition.m_Attributes (type CUtlVector) + // 0x1C = (...) m_Attributes.m_Memory.m_pMemory (m_Attributes + 0x00) + // 0x28 = (...) m_Attributes.m_Size (m_Attributes + 0x0C) + int iNumAttribs = LoadFromAddressOffset(pItemDef, 0x28, NumberType_Int32); + if (!iNumAttribs) { + return 0; + } + + Address pAttribList = DereferencePointer(pItemDef, .offset = 0x1C); + + // Read static_attrib_t (size 0x08) entries from contiguous block of memory + for (int i = 0; i < iNumAttribs && i < size; i++) { + Address pStaticAttrib = pAttribList + view_as
(i * 0x08); + iAttribIndices[i] = LoadFromAddress(pStaticAttrib, NumberType_Int16); + iAttribValues[i] = LoadFromAddressOffset(pStaticAttrib, 0x04, NumberType_Int32); } return iNumAttribs; } -public Native_GetStaticAttribs(Handle:plugin, numParams) -{ - new iItemDefIndex = GetNativeCell(1); - new size = 16; - if (numParams >= 4) - { +/* native int TF2Attrib_GetStaticAttribs(int iItemDefIndex, int[] iAttribIndices, float[] flAttribValues, int iMaxLen=16); */ +public int Native_GetStaticAttribs(Handle plugin, int numParams) { + int iItemDefIndex = GetNativeCell(1); + int size = 16; + if (numParams >= 4) { size = GetNativeCell(4); - if (size <= 0) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetStaticAttribs: Array size (iMaxLen=%d) must be greater than 0", size); - } } - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return -1; - if (hSDKGetItemDefinition == INVALID_HANDLE) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetStaticAttribs: Could not find call to CEconItemSchema::GetItemDefinition"); + + if (size <= 0) { + return ThrowNativeError(SP_ERROR_NATIVE, "Array size must be greater than 0 (currently %d)", size); + } + + Address pSchema = GetItemSchema(); + if (!pSchema) { + return -1; } - new Address:pItemDef = SDKCall(hSDKGetItemDefinition, pSchema, iItemDefIndex); - if (!IsValidAddress(pItemDef)) return -1; - new iAttribIndices[size], iAttribValues[size]; - new iCount = GetStaticAttribs(pItemDef, iAttribIndices, iAttribValues, size); + + Address pItemDef = SDKCall(hSDKGetItemDefinition, pSchema, iItemDefIndex); + AssertValidAddress(pItemDef); + + int[] iAttribIndices = new int[size]; int[] iAttribValues = new int[size]; + int iCount = GetStaticAttribs(pItemDef, iAttribIndices, iAttribValues, size); SetNativeArray(2, iAttribIndices, size); SetNativeArray(3, iAttribValues, size); //cast to float on inc side return iCount; } -stock GetSOCAttribs(iEntity, iAttribIndices[], iAttribValues[], size = 16) { +static int GetSOCAttribs(int iEntity, int[] iAttribIndices, int[] iAttribValues, int size = 16) { if (size <= 0) { return -1; } - int iCEIVOffset = GetEntSendPropOffs(iEntity, "m_Item", true); - if (iCEIVOffset <= 0) { - return -1; - } - Address pEconItemView = GetEntityAddress(iEntity); - if (!IsValidAddress(pEconItemView)) { + Address pEconItemView = GetEntityEconItemView(iEntity); + if (!pEconItemView) { return -1; } - pEconItemView += view_as
(iCEIVOffset); - + + // pEconItem may be null if the item doesn't have SOC data (i.e., not from the item server) Address pEconItem = SDKCall(hSDKGetSOCData, pEconItemView); - if (!IsValidAddress(pEconItem)) { - return -1; + if (!pEconItem) { + return 0; } - Address pCustomData = view_as
(LoadFromAddress(pEconItem + view_as
(0x34), NumberType_Int32)); - if (IsValidAddress(pCustomData)) { - int iCount = LoadFromAddress(pCustomData + view_as
(0x0C), NumberType_Int32); + + // 0x34 = CEconItem.m_pAttributes (type CUtlVector*, possibly null) + Address pCustomData = DereferencePointer(pEconItem, .offset = 0x34); + if (pCustomData) { + AssertValidAddress(pCustomData); + + // 0x0C = (...) m_pAttributes->m_Size (m_pAttributes + 0x0C) + // 0x00 = (...) m_pAttributes->m_Memory.m_pMemory (m_pAttributes + 0x00) + int iCount = LoadFromAddressOffset(pCustomData, 0x0C, NumberType_Int32); + if (!iCount) { + // abort early if the attribute list is empty -- we might deref garbage otherwise + return 0; + } + + Address pCustomDataArray = DereferencePointer(pCustomData); + + // Read static_attrib_t (size 0x08) entries from contiguous block of memory for (int i = 0; i < iCount && i < size; ++i) { - Address pAttribDef = view_as
(LoadFromAddress(pCustomData, NumberType_Int32) + (i * 8)); - Address pAttribVal = view_as
(LoadFromAddress(pCustomData, NumberType_Int32) + (i * 8) + 4); - iAttribIndices[i] = LoadFromAddress(pAttribDef, NumberType_Int16); - iAttribValues[i] = LoadFromAddress(pAttribVal, NumberType_Int32); + Address pSOCAttribEntry = pCustomDataArray + view_as
(i * 0x08); + + iAttribIndices[i] = LoadFromAddress(pSOCAttribEntry, NumberType_Int16); + iAttribValues[i] = LoadFromAddressOffset(pSOCAttribEntry, 0x04, NumberType_Int32); } return iCount; } + //(CEconItem+0x27 & 0b100 & 0xFF) != 0 - bool hasInternalAttribute = (LoadFromAddress(pEconItem + view_as
(0x27), NumberType_Int8) & 0b100) != 0; + bool hasInternalAttribute = !!(LoadFromAddressOffset(pEconItem, 0x27, NumberType_Int8) & 0b100); if (hasInternalAttribute) { - iAttribIndices[0] = LoadFromAddress(pEconItem + view_as
(0x2C), NumberType_Int16); - iAttribValues[0] = LoadFromAddress(pEconItem + view_as
(0x30), NumberType_Int32); + iAttribIndices[0] = LoadFromAddressOffset(pEconItem, 0x2C, NumberType_Int16); + iAttribValues[0] = LoadFromAddressOffset(pEconItem, 0x30, NumberType_Int32); return 1; } return 0; } -public Native_GetSOCAttribs(Handle:plugin, numParams) -{ - new iEntity = GetNativeCell(1); - new size = 16; - if (numParams >= 4) - { +/* native int TF2Attrib_GetSOCAttribs(int iEntity, int[] iAttribIndices, float[] flAttribValues, int iMaxLen=16); */ +public int Native_GetSOCAttribs(Handle plugin, int numParams) { + int iEntity = GetNativeCell(1); + int size = 16; + if (numParams >= 4) { size = GetNativeCell(4); - if (size <= 0) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetSOCAttribs: Array size (iMaxLen=%d) must be greater than 0", size); - } } - if (!IsValidEntity(iEntity)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetSOCAttribs: Invalid entity (iEntity=%d) passed", iEntity); + + if (size <= 0) { + return ThrowNativeError(SP_ERROR_NATIVE, "Array size must be greater than 0 (currently %d)", size); } - if (hSDKGetSOCData == INVALID_HANDLE) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetSOCAttribs: Could not find call to CEconItemView::GetSOCData"); + + if (!IsValidEntity(iEntity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(iEntity), iEntity); } + //maybe move some address stuff to here from the stock, but for now it's okay - new iAttribIndices[size], iAttribValues[size]; - new iCount = GetSOCAttribs(iEntity, iAttribIndices, iAttribValues, size); + int[] iAttribIndices = new int[size]; int[] iAttribValues = new int[size]; + int iCount = GetSOCAttribs(iEntity, iAttribIndices, iAttribValues, size); SetNativeArray(2, iAttribIndices, size); SetNativeArray(3, iAttribValues, size); //cast to float on inc side return iCount; } -public Native_SetAttrib(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByName: Invalid entity (iEntity=%d) passed", entity); -// return; +/* native bool TF2Attrib_SetByName(int iEntity, char[] strAttrib, float flValue); */ +public int Native_SetAttrib(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); } - decl String:strAttrib[128]; //"counts as assister is some kind of pet this update is going to be awesome" is 73 characters. Valve... Valve. + + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; GetNativeString(2, strAttrib, sizeof(strAttrib)); - new Float:flVal = GetNativeCell(3); - - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByName: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return false; + float flVal = GetNativeCell(3); + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) return false; - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return false; - new Address:pAttribDef = SDKCall(hSDKGetAttributeDefByName, pSchema, strAttrib); - if (!IsValidAddress(pAttribDef)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByName: Attribute '%s' not valid", strAttrib); + + Address pAttribDef = GetAttributeDefinitionByName(strAttrib); + if (!pAttribDef) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute name '%s' is invalid", strAttrib); } - SDKCall(hSDKSetRuntimeValue, pEntity+Address:offs, pAttribDef, flVal); + + SDKCall(hSDKSetRuntimeValue, pEntAttributeList, pAttribDef, flVal); return true; +} -// ClearAttributeCache(GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity")); -// decl String:strClassname[64]; -// GetEntityClassname(entity, strClassname, sizeof(strClassname)); -// if (strncmp(strClassname, "tf_wea", 6, false) == 0 || StrEqual(strClassname, "tf_powerup_bottle", false)) -// { -// new client = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); -// if (client > 0 && client <= MaxClients && IsClientInGame(client)) ClearAttributeCache(client); -// } +/* native bool TF2Attrib_SetByDefIndex(int iEntity, int iDefIndex, float flValue); */ +public int Native_SetAttribByID(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + + int iAttrib = GetNativeCell(2); + float flVal = GetNativeCell(3); + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); + } + + Address pAttribDef = GetAttributeDefinitionByID(iAttrib); + if (!pAttribDef) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute index %d is invalid", iAttrib); + } + + SDKCall(hSDKSetRuntimeValue, pEntAttributeList, pAttribDef, flVal); + return true; } -public Native_SetAttribByID(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByDefIndex: Invalid entity (iEntity=%d) passed", entity); -// return; - } - new iAttrib = GetNativeCell(2); - new Float:flVal = GetNativeCell(3); - - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByDefIndex: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); +/* native bool TF2Attrib_SetFromStringValue(int iEntity, const char[] strAttrib, const char[] strValue); */ +public int Native_SetAttribStringByName(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); + } + + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH], strAttribVal[MAX_ATTRIBUTE_VALUE_LENGTH]; + GetNativeString(2, strAttrib, sizeof(strAttrib)); + GetNativeString(3, strAttribVal, sizeof(strAttribVal)); + + int attrdef; + if (!GetAttributeDefIndexByName(strAttrib, attrdef)) { + // we don't throw on nonexistent attributes here; we return false and let the caller handle that return false; } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) return false; - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return false; - new Address:pAttribDef = SDKCall(hSDKGetAttributeDef, pSchema, iAttrib); - if (!IsValidAddress(pAttribDef)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetByDefIndex: Attribute %d not valid", iAttrib); + + // allocate a CEconItemAttribute instance in an entity's runtime attribute list + Address pEconItemAttribute = FindOrAllocateEconItemAttribute(entity, attrdef); + if (!InitializeAttributeValue(attrdef, pEconItemAttribute, strAttribVal)) { + return false; } - SDKCall(hSDKSetRuntimeValue, pEntity+Address:offs, pAttribDef, flVal); + + ClearAttributeCache(entity); return true; -// ClearAttributeCache(GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity")); -// decl String:strClassname[64]; -// GetEntityClassname(entity, strClassname, sizeof(strClassname)); -// if (strncmp(strClassname, "tf_wea", 6, false) == 0 || StrEqual(strClassname, "tf_powerup_bottle", false)) -// { -// new client = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); -// if (client > 0 && client <= MaxClients && IsClientInGame(client)) ClearAttributeCache(client); -// } } -public Native_GetAttrib(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetByName: Invalid entity (iEntity=%d) passed", entity); -// return; +/* native Address TF2Attrib_GetByName(int iEntity, char[] strAttrib); */ +public int Native_GetAttrib(Handle plugin, int numParams) { + // There is a CAttributeList::GetByName, wonder why this is being done instead... + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); } - decl String:strAttrib[128]; + + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; GetNativeString(2, strAttrib, sizeof(strAttrib)); - - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetByName: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return _:Address_Null; - } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) return _:Address_Null; - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return _:Address_Null; - new Address:pAttribDef = SDKCall(hSDKGetAttributeDefByName, pSchema, strAttrib); - if (!IsValidAddress(pAttribDef)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetByName: Attribute '%s' not valid", strAttrib); - } - new iDefIndex = LoadFromAddress(pAttribDef + Address:4, NumberType_Int16); - new Address:pAttrib = Address:SDKCall(hSDKGetAttributeByID, pEntity+Address:offs, iDefIndex); - return (!IsValidAddress(pAttrib) ? (_:Address_Null) : (_:pAttrib)); -} - -public Native_GetAttribByID(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetByDefIndex: Invalid entity (iEntity=%d) passed", entity); -// return; + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); + } + + int iDefIndex; + if (!GetAttributeDefIndexByName(strAttrib, iDefIndex)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute name '%s' is invalid", strAttrib); } - new iDefIndex = GetNativeCell(2); + return SDKCall(hSDKGetAttributeByID, pEntAttributeList, iDefIndex); +} - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetByName: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return _:Address_Null; +/* native Address TF2Attrib_GetByDefIndex(int iEntity, int iDefIndex); */ +public int Native_GetAttribByID(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + + int iDefIndex = GetNativeCell(2); + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return 0; } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) return _:Address_Null; - new Address:pAttrib = Address:SDKCall(hSDKGetAttributeByID, pEntity+Address:offs, iDefIndex); - return (!IsValidAddress(pAttrib) ? (_:Address_Null) : (_:pAttrib)); + + return SDKCall(hSDKGetAttributeByID, pEntAttributeList, iDefIndex); } -public Native_Remove(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveByName: Invalid entity (iEntity=%d) passed", entity); - return false; - // return; +/* native bool TF2Attrib_RemoveByName(int iEntity, char[] strAttrib); */ +public int Native_Remove(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); } - decl String:strAttrib[128]; + + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; GetNativeString(2, strAttrib, sizeof(strAttrib)); - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_Remove: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return false; - // return; + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) - { - return false; - // return; - } - if (pEntity == Address_Null) return false; - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return false; - new Address:pAttribDef = SDKCall(hSDKGetAttributeDefByName, pSchema, strAttrib); - if (!IsValidAddress(pAttribDef)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveByName: Attribute '%s' not valid", strAttrib); - } - SDKCall(hSDKRemoveAttribute, pEntity+Address:offs, pAttribDef); //Not a clue what the return is here, but it's probably a clone of the attrib being removed - -// SDKCall(hSDKRemoveAttribute, pEntity+Address:offs, strAttrib); -// ClearAttributeCache(GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity")); -// decl String:strClassname[64]; -// GetEntityClassname(entity, strClassname, sizeof(strClassname)); -// if (strncmp(strClassname, "tf_wea", 6, false) == 0 || StrEqual(strClassname, "tf_powerup_bottle", false)) -// { -// new client = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); -// if (client > 0 && client <= MaxClients && IsClientInGame(client)) ClearAttributeCache(client); -// } - + + Address pAttribDef = GetAttributeDefinitionByName(strAttrib); + if (!pAttribDef) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute name '%s' is invalid", strAttrib); + } + + SDKCall(hSDKRemoveAttribute, pEntAttributeList, pAttribDef); //Not a clue what the return is here, but it's probably a clone of the attrib being removed return true; } -public Native_RemoveByID(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveByDefIndex: Invalid entity (iEntity=%d) passed", entity); - return false; - // return; +/* native bool TF2Attrib_RemoveByDefIndex(int iEntity, int iDefIndex); */ +public int Native_RemoveByID(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); } - new iAttrib = GetNativeCell(2); + + int iAttrib = GetNativeCell(2); - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_Remove: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return false; - // return; + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) - { - return false; - // return; - } - if (pEntity == Address_Null) return false; - new Address:pSchema = SDKCall(hSDKSchema); - if (pSchema == Address_Null) return false; - new Address:pAttribDef = SDKCall(hSDKGetAttributeDef, pSchema, iAttrib); - if (!IsValidAddress(pAttribDef)) - { - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveByDefIndex: Attribute %d not valid", iAttrib); - } - SDKCall(hSDKRemoveAttribute, pEntity+Address:offs, pAttribDef); //Not a clue what the return is here, but it's probably a clone of the attrib being removed - -// SDKCall(hSDKRemoveAttribute, pEntity+Address:offs, strAttrib); -// ClearAttributeCache(GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity")); -// decl String:strClassname[64]; -// GetEntityClassname(entity, strClassname, sizeof(strClassname)); -// if (strncmp(strClassname, "tf_wea", 6, false) == 0 || StrEqual(strClassname, "tf_powerup_bottle", false)) -// { -// new client = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); -// if (client > 0 && client <= MaxClients && IsClientInGame(client)) ClearAttributeCache(client); -// } - + + Address pAttribDef = GetAttributeDefinitionByID(iAttrib); + if (!pAttribDef) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute index %d is invalid", iAttrib); + } + + SDKCall(hSDKRemoveAttribute, pEntAttributeList, pAttribDef); //Not a clue what the return is here, but it's probably a clone of the attrib being removed return true; } -public Native_RemoveAll(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveAll: Invalid entity (iEntity=%d) passed", entity); - return false; - // return; +/* native bool TF2Attrib_RemoveAll(int iEntity); */ +public int Native_RemoveAll(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + + Address pEntAttributeList = GetEntityAttributeList(entity); + if (!pEntAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); } + + SDKCall(hSDKDestroyAllAttributes, pEntAttributeList); //disregard the return (Valve does!) + return true; +} + +/* native void TF2Attrib_SetDefIndex(Address pAttrib, int iDefIndex); */ +public int Native_SetID(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + int iDefIndex = GetNativeCell(2); + StoreToAddressOffset(pAttrib, 0x04, iDefIndex, NumberType_Int16); +} + +/* native int TF2Attrib_GetDefIndex(Address pAttrib); */ +public int Native_GetID(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + return LoadFromAddressOffset(pAttrib, 0x04, NumberType_Int16); +} + +/* native void TF2Attrib_SetValue(Address pAttrib, float flValue); */ +public int Native_SetVal(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + int flVal = GetNativeCell(2); //It's a float but avoiding tag mismatch warnings from StoreToAddress + StoreToAddressOffset(pAttrib, 0x08, flVal, NumberType_Int32); +} + +/* native float TF2Attrib_GetValue(Address pAttrib); */ +public int Native_GetVal(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + return LoadFromAddressOffset(pAttrib, 0x08, NumberType_Int32); +} - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveAll: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); +/* TF2Attrib_UnsafeGetStringValue(any pRawValue, char[] buffer, int maxlen); */ +public int Native_GetStringVal(Handle plugin, int numParams) { + Address pRawValue = GetNativeCell(1); + + int maxlen = GetNativeCell(3), length; + char[] buffer = new char[maxlen]; + + ReadStringAttributeValue(pRawValue, buffer, maxlen); + SetNativeString(2, buffer, maxlen, .bytes = length); + return length; +} + +/* native void TF2Attrib_SetRefundableCurrency(Address pAttrib, int nCurrency); */ +public int Native_SetCurrency(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + int nCurrency = GetNativeCell(2); + StoreToAddressOffset(pAttrib, 0x0C, nCurrency, NumberType_Int32); +} + +/* native int TF2Attrib_GetRefundableCurrency(Address pAttrib); */ +public int Native_GetCurrency(Handle plugin, int numParams) { + Address pAttrib = GetNativeCell(1); + return LoadFromAddressOffset(pAttrib, 0x0C, NumberType_Int32); +} + +public int Native_DeprecatedPropertyAccess(Handle plugin, int numParams) { + return ThrowNativeError(SP_ERROR_NATIVE, "Property associated with native function no longer exists"); +} + +static bool ClearAttributeCache(int entity) { + if (entity <= 0 || !IsValidEntity(entity)) { return false; - // return; } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) - { + + Address pAttributeManager = GetEntityAttributeManager(entity); + if (!pAttributeManager) { return false; - // return; } - SDKCall(hSDKDestroyAllAttributes, pEntity+Address:offs); //disregard the return (Valve does!) + + SDKCall(hSDKOnAttribValuesChanged, pAttributeManager); + return true; +} -// ClearAttributeCache(GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity")); -// decl String:strClassname[64]; -// GetEntityClassname(entity, strClassname, sizeof(strClassname)); -// if (strncmp(strClassname, "tf_wea", 6, false) == 0 || StrEqual(strClassname, "tf_powerup_bottle", false)) -// { -// new client = GetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity"); -// if (client > 0 && client <= MaxClients && IsClientInGame(client)) ClearAttributeCache(client); -// } +/* native bool TF2Attrib_ClearCache(int iEntity); */ +public int Native_ClearCache(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + return ClearAttributeCache(entity); +} - return true; +/* native int TF2Attrib_ListDefIndices(int iEntity, int[] iDefIndices, int iMaxLen=20); */ +public int Native_ListIDs(Handle plugin, int numParams) { + int entity = GetNativeCell(1); + int size = 20; + if (numParams >= 3) { + size = GetNativeCell(3); + } + + if (size <= 0) { + return ThrowNativeError(SP_ERROR_NATIVE, "Array size must be greater than 0 (currently %d)", size); + } + + if (!IsValidEntity(entity)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) is invalid", EntIndexToEntRef(entity), entity); + } + + Address pAttributeList = GetEntityAttributeList(entity); + if (!pAttributeList) { + return ThrowNativeError(SP_ERROR_NATIVE, "Entity %d (%d) does not have property m_AttributeList", EntIndexToEntRef(entity), entity); + } + + // 0x10 = CAttributeList.m_Attributes.m_Size (m_Attributes + 0x0C) + int iNumAttribs = LoadFromAddressOffset(pAttributeList, 0x10, NumberType_Int32); + if (!iNumAttribs) { + return 0; + } + + // 0x04 = CAttributeList.m_Attributes (type CUtlVector) + // 0x04 = CAttributeList.m_Attributes.m_Memory.m_pMemory + Address pAttribListData = DereferencePointer(pAttributeList, .offset = 0x04); + AssertValidAddress(pAttribListData); + + int[] iAttribIndices = new int[size]; + + // Read CEconItemAttribute (size 0x10) entries from contiguous block of memory + for (int i = 0; i < iNumAttribs && i < size; i++) { + Address pAttributeEntry = pAttribListData + view_as
(i * 0x10); + iAttribIndices[i] = LoadFromAddressOffset(pAttributeEntry, 0x04, NumberType_Int16); + } + SetNativeArray(2, iAttribIndices, size); + return iNumAttribs; } -public Native_SetID(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return; - new iDefIndex = GetNativeCell(2); - StoreToAddress(pAttrib+Address:4, iDefIndex, NumberType_Int16); +/* native bool TF2Attrib_IsValidAttributeName(const char[] strAttrib); */ +public int Native_IsValidAttributeName(Handle plugin, int numParams) { + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; + GetNativeString(1, strAttrib, sizeof(strAttrib)); + + return GetAttributeDefinitionByName(strAttrib)? true : false; } -public Native_GetID(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return -1; - return LoadFromAddress(pAttrib+Address:4, NumberType_Int16); +/* native void TF2Attrib_AddCustomPlayerAttribute(int client, const char[] strAttrib, float flValue, float flDuration = -1.0); */ +public int Native_AddCustomAttribute(Handle plugin, int numParams) { + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; + + int client = GetNativeCell(1); + GetNativeString(2, strAttrib, sizeof(strAttrib)); + + if (!GetAttributeDefinitionByName(strAttrib)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute name '%s' is invalid", strAttrib); + } + + float flValue = GetNativeCell(3); + float flDuration = GetNativeCell(4); + + SDKCall(hSDKAddCustomAttribute, client, strAttrib, flValue, flDuration); + return 0; } -public Native_SetVal(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return; - new flVal = GetNativeCell(2); //It's a float but avoiding tag mismatch warnings - StoreToAddress(pAttrib+Address:8, flVal, NumberType_Int32); +public int Native_RemoveCustomAttribute(Handle plugin, int numParams) { + char strAttrib[MAX_ATTRIBUTE_NAME_LENGTH]; + + int client = GetNativeCell(1); + GetNativeString(2, strAttrib, sizeof(strAttrib)); + + if (!GetAttributeDefinitionByName(strAttrib)) { + return ThrowNativeError(SP_ERROR_NATIVE, "Attribute name '%s' is invalid", strAttrib); + } + + SDKCall(hSDKRemoveCustomAttribute, client, strAttrib); + return 0; } -public Native_GetVal(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return -1; - return LoadFromAddress(pAttrib+Address:8, NumberType_Int32); +/* native float TF2Attrib_HookValueFloat(float flInitial, const char[] attrClass, int iEntity); */ +public int Native_HookValueFloat(Handle plugin, int numParams) { + /** + * CAttributeManager::AttribHookValue(float value, string_t attr_class, + * CBaseEntity const* entity, CUtlVector reentrantList, + * bool is_const_str); + * + * `value` is the value that is returned after modifiers based on `attr_class`. + * `reentrantList` seems to be a list of entities to ignore? + * `is_const_str` is true iff the `attr_class` is hardcoded + * (i.e., it's at a fixed location) -- this is never true from a plugin + * This determines if the game uses AllocPooledString_StaticConstantStringPointer + * (when is_const_str == true) or AllocPooledString (false). + */ + float initial = GetNativeCell(1); + + int buflen; + GetNativeStringLength(2, buflen); + char[] attrClass = new char[++buflen]; + GetNativeString(2, attrClass, buflen); + + int entity = GetNativeCell(3); + + return SDKCall(hSDKAttributeHookFloat, initial, attrClass, entity, + Address_Null, false); } -public Native_SetInitialVal(Handle:plugin, numParams) -{ - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetInitialValue: m_flInitialValue is no longer present on attributes"); +/* native float TF2Attrib_HookValueInt(int nInitial, const char[] attrClass, int iEntity); */ +public int Native_HookValueInt(Handle plugin, int numParams) { + int initial = GetNativeCell(1); + + int buflen; + GetNativeStringLength(2, buflen); + char[] attrClass = new char[++buflen]; + GetNativeString(2, attrClass, buflen); + + int entity = GetNativeCell(3); + + return SDKCall(hSDKAttributeHookInt, initial, attrClass, entity, + Address_Null, false); +} -// new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return; -// new flInitialVal = GetNativeCell(2); //It's a float but avoiding tag mismatch warnings -// StoreToAddress(pAttrib+Address:12, flInitialVal, NumberType_Int32); +/* native void TF2Attrib_HookValueString(const char[] initial, const char[] attrClass, + int iEntity, char[] buffer, int maxlen); */ +public int Native_HookValueString(Handle plugin, int numParams) { + int buflen; + + GetNativeStringLength(1, buflen); + char[] inputValue = new char[++buflen]; + GetNativeString(1, inputValue, buflen); + + GetNativeStringLength(2, buflen); + char[] attrClass = new char[++buflen]; + GetNativeString(2, attrClass, buflen); + + int entity = GetNativeCell(3); + + // string needs to be pooled for caching purposes + Address pInput = AllocPooledString(inputValue); + Address pAttrClass = AllocPooledString(attrClass); + + buflen = GetNativeCell(5); + char[] output = new char[buflen]; + + Address pOutput; + if (hSDKAttributeApplyStringWrapperWindows) { + // windows version; hidden ptr pushes params, `this` still in correct register + Address result; + pOutput = SDKCall(hSDKAttributeApplyStringWrapperWindows, + GetEntityAttributeManager(entity), result, pInput, entity, pAttrClass, + Address_Null); + } else if (hSDKAttributeApplyStringWrapperLinux) { + // linux version; hidden ptr moves the stack and this forward + Address result; + pOutput = SDKCall(hSDKAttributeApplyStringWrapperLinux, result, + GetEntityAttributeManager(entity), pInput, entity, pAttrClass, Address_Null); + } + + // read from the output string_t + LoadStringFromAddress(DereferencePointer(pOutput), output, buflen); + + int written; + SetNativeString(4, output, buflen, .bytes = written); + return written; } -public Native_GetInitialVal(Handle:plugin, numParams) -{ - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetInitialValue: m_flInitialValue is no longer present on attributes"); +/* helper functions */ -// new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return -1; -// return LoadFromAddress(pAttrib+Address:12, NumberType_Int32); +static Address GetItemSchema() { + return SDKCall(hSDKSchema); } -public Native_SetCurrency(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return; - new nCurrency = GetNativeCell(2); - StoreToAddress(pAttrib+Address:12, nCurrency, NumberType_Int32); +static Address GetEntityEconItemView(int entity) { + int iCEIVOffset = GetEntSendPropOffs(entity, "m_Item", true); + if (iCEIVOffset > 0) { + return GetEntityAddress(entity) + view_as
(iCEIVOffset); + } + return Address_Null; } -public Native_GetCurrency(Handle:plugin, numParams) -{ - new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return -1; - return LoadFromAddress(pAttrib+Address:12, NumberType_Int32); +/** + * Returns the m_AttributeList offset. This does not correspond to the CUtlVector instance + * (which is offset by 0x04). + */ +static Address GetEntityAttributeList(int entity) { + int offsAttributeList = GetEntSendPropOffs(entity, "m_AttributeList", true); + if (offsAttributeList > 0) { + return GetEntityAddress(entity) + view_as
(offsAttributeList); + } + return Address_Null; } -public Native_SetSetBonus(Handle:plugin, numParams) -{ - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_SetIsSetBonus: m_bSetBonus is no longer present on attributes"); +static Address GetAttributeDefinitionByName(const char[] name) { + Address cachedResult; + if (g_AttributeDefinitionMapping.GetValue(name, cachedResult)) { + return cachedResult; + } + + Address pSchema = GetItemSchema(); + if (!pSchema) { + return Address_Null; + } + cachedResult = SDKCall(hSDKGetAttributeDefByName, pSchema, name); + g_AttributeDefinitionMapping.SetValue(name, cachedResult); + return cachedResult; +} -// new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return; -// new bool:bSetBonus = !!GetNativeCell(2); -// StoreToAddress(pAttrib+Address:20, bSetBonus, NumberType_Int8); +static Address GetAttributeDefinitionByID(int id) { + Address pSchema = GetItemSchema(); + if (!pSchema) { + return Address_Null; + } + return SDKCall(hSDKGetAttributeDef, pSchema, id); } -public Native_GetSetBonus(Handle:plugin, numParams) -{ - return ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_GetIsSetBonus: m_bSetBonus is no longer present on attributes"); +/** + * Returns true if an attribute with the specified name exists, storing the definition index + * to the given by-ref `iDefIndex` argument. + */ +static bool GetAttributeDefIndexByName(const char[] name, int &iDefIndex) { + Address pAttribDef = GetAttributeDefinitionByName(name); + if (!pAttribDef) { + return false; + } + + iDefIndex = LoadFromAddressOffset(pAttribDef, 0x04, NumberType_Int16); + return true; +} -// new Address:pAttrib = Address:GetNativeCell(1); -// if (!IsValidAddress(pAttrib)) return -1; -// return !!LoadFromAddress(pAttrib+Address:20, NumberType_Int8); +static Address GetEntityAttributeManager(int entity) { + Address pAttributeList = GetEntityAttributeList(entity); + if (!pAttributeList) { + return Address_Null; + } + + Address pAttributeManager = DereferencePointer(pAttributeList, .offset = 0x18); + AssertValidAddress(pAttributeManager); + return pAttributeManager; } -stock bool:ClearAttributeCache(entity) -{ - if (hSDKOnAttribValuesChanged == INVALID_HANDLE) return false; - if (entity <= 0 || !IsValidEntity(entity)) return false; - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) return false; - new Address:pAttribs = GetEntityAddress(entity); - if (!IsValidAddress(pAttribs)) return false; - pAttribs = Address:LoadFromAddress(pAttribs+Address:(offs+24), NumberType_Int32); //AttributeManager - if (!IsValidAddress(pAttribs)) return false; - SDKCall(hSDKOnAttribValuesChanged, pAttribs); - return true; +/** + * Returns the address of a CEconItemAttribute instance on an entity with the given attribute + * definition index, allocating it if one doesn't already exist. For networked attributes this + * is zero-initialized; heap-based attributes are considered uninitialized. + */ +static Address FindOrAllocateEconItemAttribute(int entity, int attrdef) { + Address pAttrDef = GetAttributeDefinitionByID(attrdef); + if (!pAttrDef) { + return Address_Null; + } + + Address pAttributeList = GetEntityAttributeList(entity); + if (!pAttributeList) { + // we checked this before, but just in case refactors happen... + return Address_Null; + } + + Address pEconItemAttribute = SDKCall(hSDKGetAttributeByID, pAttributeList, attrdef); + if (!pEconItemAttribute) { + SDKCall(hSDKSetRuntimeValue, pAttributeList, pAttrDef, 0.0); + pEconItemAttribute = SDKCall(hSDKGetAttributeByID, pAttributeList, attrdef); + } + return pEconItemAttribute; } -public Native_ClearCache(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - if (!IsValidEntity(entity)) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_ClearCache: Invalid entity (iEntity=%d) passed", entity); +/** + * Initializes the space occupied by a given CEconItemAttribute pointer, parsing and allocating + * the raw value based on the attribute's underlying type. This should correctly parse numeric + * and string values. + */ +static bool InitializeAttributeValue(int attrdef, Address pEconItemAttribute, const char[] value) { + Address pAttrDef = GetAttributeDefinitionByID(attrdef); + if (!pAttrDef) { return false; } - return ClearAttributeCache(entity); + + Address pDefType = DereferencePointer(pAttrDef + view_as
(0x08)); + Address pAttributeValue = pEconItemAttribute + view_as
(0x08); + + if (!IsNetworkedRuntimeAttribute(pDefType)) { + // reusing any existing matching attribute value strings + Address rawAttributeValue = GetHeapManagedAttributeString(attrdef, value); + if (rawAttributeValue) { + StoreToAddress(pAttributeValue, view_as(rawAttributeValue), NumberType_Int32); + return true; + } + + /** + * initialize raw value; any existing values present in the CEconItemAttribute* are trashed + * + * that is okay -- tf2attributes is the only one managing heap-allocated values, and + * it holds its own reference to the value for freeing later + * + * we don't attempt to free any existing attribute value mid-game as we don't know if + * the value is present in multiple places (no refcounts!) + */ + SDKCall(hSDKAttributeValueInitialize, pDefType, pAttributeValue); + + // add to our managed values + // this definitely works for heap, not sure if it works for inline + HeapAttributeValue attribute; + attribute.m_iAttributeDefinitionIndex = attrdef; + attribute.m_pAttributeValue = DereferencePointer(pAttributeValue); + + g_ManagedAllocatedValues.PushArray(attribute); + } + + if (!SDKCall(hSDKAttributeValueFromString, pDefType, pAttrDef, value, pAttributeValue, true)) { + // we couldn't parse the attribute value, abort + return false; + } + return true; } -public Native_ListIDs(Handle:plugin, numParams) -{ - new entity = GetNativeCell(1); - new size = 20; - if (numParams >= 3) - { - size = GetNativeCell(3); - if (size <= 0) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_ListDefIndices: Array size (iMaxLen=%d) must be greater than 0", size); - return -1; +/** + * Returns the address of an existing instance for the given attribute definition and string + * value, if it exists. + */ +static Address GetHeapManagedAttributeString(int attrdef, const char[] value) { + /** + * we restrict it to strings as we don't have a way to determine equality on non-string + * attributes. + */ + if (!IsAttributeString(attrdef)) { + return Address_Null; + } + + for (int i, n = g_ManagedAllocatedValues.Length; i < n; i++) { + HeapAttributeValue existingAttribute; + g_ManagedAllocatedValues.GetArray(i, existingAttribute, sizeof(existingAttribute)); + + if (existingAttribute.m_iAttributeDefinitionIndex != attrdef) { + continue; + } + + char attributeString[PLATFORM_MAX_PATH]; + ReadStringAttributeValue(existingAttribute.m_pAttributeValue, attributeString, sizeof(attributeString)); + if (StrEqual(attributeString, value)) { + return existingAttribute.m_pAttributeValue; } } - if (!IsValidEntity(entity)) - { - ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_ListDefIndices: Invalid entity (iEntity=%d) passed", entity); - return -1; + return Address_Null; +} + +/** + * Returns true if the given attribute type can (normally) be networked. + * We make the assumption that non-networked attributes have to be heap / inline allocated. + */ +static bool IsNetworkedRuntimeAttribute(Address pDefType) { + return SDKCall(hSDKAttributeTypeCanBeNetworked, pDefType); +} + +/** + * Unloads the attribute in a given CEconItemAttribute instance. + */ +#pragma unused UnloadAttributeValue +static void UnloadAttributeValue(Address pAttrDef, Address pEconItemAttribute) { + Address pDefType = DereferencePointer(pAttrDef + view_as
(0x08)); + Address pAttributeValue = pEconItemAttribute + view_as
(0x08); + + SDKCall(hSDKAttributeValueUnload, pDefType, pAttributeValue); +} + +/** + * Unloads the given raw attribute value. + */ +static void UnloadAttributeRawValue(Address pAttrDef, Address pAttributeValue) { + Address pAttributeDataUnion = pAttributeValue; + Address pDefType = DereferencePointer(pAttrDef + view_as
(0x08)); + SDKCall(hSDKAttributeValueUnloadByRef, pDefType, pAttributeDataUnion); +} + +/** + * Returns true if the given attribute definition index is a string. + */ +static bool IsAttributeString(int attrdef) { + Address pAttrDef = GetAttributeDefinitionByID(attrdef); + Address pKnownStringAttribDef = GetAttributeDefinitionByName("cosmetic taunt sound"); + return pAttrDef && pKnownStringAttribDef + && DereferencePointer(pAttrDef, 0x08) == DereferencePointer(pKnownStringAttribDef, 0x08); +} + +/** + * Reads the contents of a CAttribute_String raw value. + */ +static int ReadStringAttributeValue(Address pRawValue, char[] buffer, int maxlen) { + /** + * Linux, Windows, and Mac differ slightly on how the std::string is laid out. + * + * For the Linux binary, the first member is a char* containing the contents of the string. + * Deref that and call it a day. + * + * Windows implements it as a union where it's either a `char[16]` or a `char*, size_t @ 0x14`. + * Check if the size_t is less than 16, then read the inline string or deref the char* depending on the results. + * + * Mac implements it as either a `bool, char[]` or `bool, char* @ 0x8`. + * + * I'm too lazy to reimplement the platform-specific bits; we're going to use sigs for this. + */ + Address pString; + SDKCall(hSDKCopyStringAttributeToCharPointer, pRawValue, pString); + return LoadStringFromAddress(pString, buffer, maxlen); +} + +/** + * Iterates over entities and removes any attributes that aren't networked (that is, + * allocated on the heap). + * + * We must do this before we unload ourselves, otherwise the game will crash trying to look up + * the heap runtime attributes we managed. + */ +static void RemoveNonNetworkedRuntimeAttributesOnEntities() { + // remove heap-based attributes from any existing entities so they don't use-after-free + int entity = -1; + while ((entity = FindEntityByClassname(entity, "*")) != -1) { + // iterate runtime attribute list and remove string attributes + // implementation straight from TF2Attrib_ListDefIndices, go over there for details + Address pAttributeList = GetEntityAttributeList(entity); + if (!pAttributeList) { + continue; + } + + // hold attribute defs pointing to heaped attributes so we don't mutate the runtime + // attribute list while we iterate over it - according to the CUtlVector docs the list + // can be realloc'd when an element is removed + + // the runtime attribute list can be any size, the current limit of 20 is on networked + ArrayList heapedAttribDefs = new ArrayList(); + + int iNumAttribs = LoadFromAddressOffset(pAttributeList, 0x10, NumberType_Int32); + if (!iNumAttribs) { + continue; + } + + Address pAttribListData = DereferencePointer(pAttributeList, .offset = 0x04); + + // we know there are attributes; make sure our contiguous memory is valid + AssertValidAddress(pAttribListData); + + for (int i = 0; i < iNumAttribs; i++) { + Address pAttributeEntry = pAttribListData + view_as
(i * 0x10); + int attrdef = LoadFromAddressOffset(pAttributeEntry, 0x04, NumberType_Int16); + + Address pAttrDef = GetAttributeDefinitionByID(attrdef); + if (!pAttrDef) { + // this shouldn't happen, but just in case + continue; + } + + Address pDefType = DereferencePointer(pAttrDef + view_as
(0x08)); + if (IsNetworkedRuntimeAttribute(pDefType)) { + continue; + } + + any rawValue = LoadFromAddressOffset(pAttributeEntry, 0x08, NumberType_Int32); + + // allow plugins to `TF2Attrib_Set*()` their own instances undisturbed by only + // processing attributes that we're aware of + if (IsAttributeValueInHeap(rawValue)) { + // we should be passing around pAttrDef instead, + // but I want the nice display printout + heapedAttribDefs.Push(attrdef); + } + } + + while (heapedAttribDefs.Length) { + int attrdef = heapedAttribDefs.Get(0); + heapedAttribDefs.Erase(0); + + Address pAttribDef = GetAttributeDefinitionByID(attrdef); + + PrintToServer("[tf2attributes] " + ... "Removing heap-allocated attribute index %d from entity %d", + attrdef, entity); + + SDKCall(hSDKRemoveAttribute, pAttributeList, pAttribDef); + } + delete heapedAttribDefs; + + ClearAttributeCache(entity); } +} - new offs = GetEntSendPropOffs(entity, "m_AttributeList", true); - if (offs <= 0) - { -// decl String:strClassname[64]; -// if (!GetEntityClassname(entity, strClassname, sizeof(strClassname))) strClassname = ""; -// ThrowNativeError(SP_ERROR_NATIVE, "TF2Attrib_RemoveAll: \"m_AttributeList\" not found (entity %d/%s)", entity, strClassname); - return -1; - // return; +/** + * Frees our heap-allocated managed attribute values so they don't leak. + * This happens on map change (where runtime attributes are invalidated) and when the plugin is + * unloaded. + */ +void DestroyManagedAllocatedValues() { + while (g_ManagedAllocatedValues.Length) { + HeapAttributeValue attribute; + g_ManagedAllocatedValues.GetArray(0, attribute, sizeof(attribute)); + + attribute.Destroy(); + + g_ManagedAllocatedValues.Erase(0); } - new Address:pEntity = GetEntityAddress(entity); - if (pEntity == Address_Null) - { - return -1; - // return; +} + +bool IsAttributeValueInHeap(any rawValue) { + for (int i, n = g_ManagedAllocatedValues.Length; i < n; i++) { + HeapAttributeValue a; + g_ManagedAllocatedValues.GetArray(i, a, sizeof(a)); + + if (a.m_pAttributeValue == rawValue) { + return true; + } } - new Address:pAttribList = Address:LoadFromAddress(pEntity + Address:(offs + 4), NumberType_Int32); - if (!IsValidAddress(pAttribList)) return -1; - new iNumAttribs = LoadFromAddress(pEntity + Address:(offs + 16), NumberType_Int32); - new iAttribIndices[size]; - for (new i = 0; i < iNumAttribs && i < size; i++) //THIS IS HOW YOU GET THE ATTRIBUTES ON AN ITEM! - { - iAttribIndices[i] = LoadFromAddress(pAttribList + Address:(i * 16 + 4), NumberType_Int16); + return false; +} + +/** + * Inserts a string into the game's string pool. This uses the same implementation that is in + * SourceMod's core: + * + * https://github.com/alliedmodders/sourcemod/blob/b14c18ee64fc822dd6b0f5baea87226d59707d5a/core/HalfLife2.cpp#L1415-L1423 + */ +stock Address AllocPooledString(const char[] value) { + Address pValue; + if (g_AllocPooledStringCache.GetValue(value, pValue)) { + return pValue; } - SetNativeArray(2, iAttribIndices, size); - return iNumAttribs; + + int ent = FindEntityByClassname(-1, "worldspawn"); + if (!IsValidEntity(ent)) { + return Address_Null; + } + int offset = FindDataMapInfo(ent, "m_iName"); + if (offset <= 0) { + return Address_Null; + } + Address pOrig = view_as
(GetEntData(ent, offset)); + DispatchKeyValue(ent, "targetname", value); + pValue = view_as
(GetEntData(ent, offset)); + SetEntData(ent, offset, pOrig); + + g_AllocPooledStringCache.SetValue(value, pValue); + return pValue; } -//TODO Stop using Address_MinimumValid once verified that logic still works without it -stock bool IsValidAddress(Address pAddress) -{ +stock int LoadFromAddressOffset(Address addr, int offset, NumberType size) { + return LoadFromAddress(addr + view_as
(offset), size); +} + +stock void StoreToAddressOffset(Address addr, int offset, int data, NumberType size) { + StoreToAddress(addr + view_as
(offset), data, size); +} + +stock Address DereferencePointer(Address addr, int offset = 0) { + return view_as
(LoadFromAddressOffset(addr, offset, NumberType_Int32)); +} + +stock int LoadStringFromAddress(Address addr, char[] buffer, int maxlen, + bool &bIsNullPointer = false) { + if (!addr) { + bIsNullPointer = true; + return 0; + } + + int c; + char ch; + do { + ch = view_as(LoadFromAddress(addr + view_as
(c), NumberType_Int8)); + buffer[c] = ch; + } while (ch && ++c < maxlen - 1); + return c; +} + +/** + * Runtime assertion that we're receiving valid addresses. + * If we're not, something has gone terribly wrong and we might need to update. + */ +stock void AssertValidAddress(Address pAddress) { static Address Address_MinimumValid = view_as
(0x10000); - if (pAddress == Address_Null) - return false; - return unsigned_compare(view_as(pAddress), view_as(Address_MinimumValid)) >= 0; + if (pAddress == Address_Null) { + ThrowError("Received invalid address (NULL)"); + } + if (unsigned_compare(view_as(pAddress), view_as(Address_MinimumValid)) < 0) { + ThrowError("Received invalid address (%08x)", pAddress); + } } + stock int unsigned_compare(int a, int b) { - if (a == b) + if (a == b) { return 0; - if ((a >>> 31) == (b >>> 31)) + } + if ((a >>> 31) == (b >>> 31)) { return ((a & 0x7FFFFFFF) > (b & 0x7FFFFFFF)) ? 1 : -1; + } return ((a >>> 31) > (b >>> 31)) ? 1 : -1; } /*