csrd_bot_taunt_randomizer.sp 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /**
  2. * [CSRD] Bot Taunt Randomizer
  3. *
  4. * Patches taunt command handling so bots may use taunt items.
  5. */
  6. #pragma semicolon 1
  7. #include <sourcemod>
  8. #include <tf2_stocks>
  9. #include <tf2idb>
  10. #pragma newdecls required
  11. #include <dhooks>
  12. #include <sdktools>
  13. #include <stocksoup/tf/econ>
  14. #define PLUGIN_VERSION "1.0.1"
  15. public Plugin myinfo = {
  16. name = "[CSRD] Bot Taunt Randomizer",
  17. author = "nosoop",
  18. description = "Allows bots to use taunt items.",
  19. version = PLUGIN_VERSION,
  20. url = "https://git.csrd.science/"
  21. }
  22. Handle g_SDKFindPartnerTauntInitiator;
  23. ArrayList g_TauntEntries[TFClassType];
  24. public void OnPluginStart() {
  25. Handle hGameConf = LoadGameConfigFile("csrd.bot_taunt_randomizer");
  26. if (!hGameConf) {
  27. SetFailState("Failed to load gamedata (csrd.bot_taunt_randomizer).");
  28. }
  29. StartPrepSDKCall(SDKCall_Player);
  30. PrepSDKCall_SetFromConf(hGameConf, SDKConf_Signature,
  31. "CTFPlayer::FindPartnerTauntInitiator()");
  32. PrepSDKCall_SetReturnInfo(SDKType_CBaseEntity, SDKPass_Pointer);
  33. g_SDKFindPartnerTauntInitiator = EndPrepSDKCall();
  34. Handle dt_PlayTauntSceneFromItem = DHookCreateFromConf(hGameConf,
  35. "CTFPlayer::PlayTauntSceneFromItem()");
  36. DHookEnableDetour(dt_PlayTauntSceneFromItem, false, OnPlayTauntSceneFromItem);
  37. Handle dt_HandleTauntCommand = DHookCreateFromConf(hGameConf,
  38. "CTFPlayer::HandleTauntCommand()");
  39. DHookEnableDetour(dt_HandleTauntCommand, false, OnHandleTauntCommand);
  40. delete hGameConf;
  41. for (TFClassType i = TFClass_Unknown; i < TFClassType; i++) {
  42. g_TauntEntries[i] = new ArrayList();
  43. }
  44. }
  45. /**
  46. * Build lists of taunts sorted by class via TF2IDB.
  47. */
  48. public void OnAllPluginsLoaded() {
  49. ArrayList args = new ArrayList();
  50. DBResultSet results = view_as<DBResultSet>(TF2IDB_CustomQuery(
  51. "SELECT id, class FROM tf2idb_class WHERE slot = 'taunt'", args, 4));
  52. if (!results) {
  53. return;
  54. }
  55. while (results.FetchRow()) {
  56. char className[16];
  57. int id = results.FetchInt(0);
  58. results.FetchString(1, className, sizeof(className));
  59. g_TauntEntries[TF2_GetClass(className)].Push(id);
  60. }
  61. delete results;
  62. }
  63. /**
  64. * Makes bots use a taunt command with a non-zero slot if there is no valid taunt partner
  65. * nearby. Without this, bots will only use their default taunt (or participate in any nearby
  66. * partner taunts).
  67. */
  68. public MRESReturn OnHandleTauntCommand(int client, Handle hParams) {
  69. int nTauntSlot = DHookGetParam(hParams, 1);
  70. if (nTauntSlot || !CanRandomTaunt(client)) {
  71. return MRES_Ignored;
  72. }
  73. if (TF2_FindPartnerTauntInitiator(client) > 0) {
  74. return MRES_Ignored;
  75. }
  76. // 90% chance of using a taunt item (10% chance default taunt)
  77. if (GetURandomFloat() > 0.90) {
  78. return MRES_Ignored;
  79. }
  80. // no initiator, consider using a taunt item
  81. DHookSetParam(hParams, 1, 8);
  82. return MRES_ChangedHandled;
  83. }
  84. /**
  85. * Allow bots to use a random taunt item.
  86. *
  87. * `CTFPlayer::PlayTauntSceneFromItem()` gets a null pointer passed in if the player attempts
  88. * to taunt with an empty taunt slot.
  89. */
  90. public MRESReturn OnPlayTauntSceneFromItem(int client, Handle hReturn, Handle hParams) {
  91. if (!CanRandomTaunt(client)) {
  92. return MRES_Ignored;
  93. }
  94. Address pEconItemView = DHookGetParam(hParams, 1);
  95. if (pEconItemView) {
  96. return MRES_Ignored;
  97. }
  98. int taunt = TF2_GetRandomTauntForClass(TF2_GetPlayerClass(client));
  99. if (taunt == DEFINDEX_UNDEFINED) {
  100. return MRES_Ignored;
  101. }
  102. int wearable = TF2_SpawnWearable(taunt);
  103. if (!IsValidEntity(wearable)) {
  104. return MRES_Ignored;
  105. }
  106. // fix taunt slot index so it correctly uses new taunt properties
  107. SetEntProp(client, Prop_Send, "m_nActiveTauntSlot", -1);
  108. SetEntProp(client, Prop_Send, "m_iTauntItemDefIndex", taunt);
  109. DHookSetParam(hParams, 1, TF2_GetEconItemView(wearable));
  110. RemoveEntity(wearable);
  111. return MRES_ChangedHandled;
  112. }
  113. /**
  114. * Returns a pointer to an item entity's `CEconItemView`.
  115. */
  116. Address TF2_GetEconItemView(int item) {
  117. if (!IsValidEntity(item) || !HasEntProp(item, Prop_Send, "m_Item")) {
  118. // we should probably throw an error here
  119. return Address_Null;
  120. }
  121. return GetEntityAddress(item) + view_as<Address>(GetEntSendPropOffs(item, "m_Item", true));
  122. }
  123. /**
  124. * Returns a partner taunt initiator entity if one is nearby, or -1 if not.
  125. */
  126. int TF2_FindPartnerTauntInitiator(int client) {
  127. return SDKCall(g_SDKFindPartnerTauntInitiator, client);
  128. }
  129. /**
  130. * Returns a random class-specific or all-class taunt index.
  131. */
  132. int TF2_GetRandomTauntForClass(TFClassType playerClass) {
  133. int nTauntsAllClass = g_TauntEntries[0].Length;
  134. int nTauntsPlayerClass = g_TauntEntries[playerClass].Length;
  135. if (nTauntsAllClass + nTauntsPlayerClass == 0) {
  136. return DEFINDEX_UNDEFINED;
  137. }
  138. int index = GetRandomInt(0, nTauntsAllClass + nTauntsPlayerClass - 1);
  139. if (index < nTauntsAllClass) {
  140. return g_TauntEntries[0].Get(index);
  141. }
  142. return g_TauntEntries[playerClass].Get(index - nTauntsAllClass);
  143. }
  144. /**
  145. * Helper function for self-testing.
  146. */
  147. bool CanRandomTaunt(int client) {
  148. return IsFakeClient(client);
  149. }