mmsplugin.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /**
  2. * vim: set ts=4 sw=4 tw=99 noet :
  3. * ======================================================
  4. * TF2 Dynamic Schema Injector
  5. * Written by nosoop
  6. * ======================================================
  7. */
  8. #include <stdio.h>
  9. #include "mmsplugin.h"
  10. #include <utlmap.h>
  11. #include <utlstring.h>
  12. #include <KeyValues.h>
  13. #include <filesystem.h>
  14. #include "memscan.h"
  15. #include <map>
  16. void BindToSourceMod();
  17. bool SM_LoadExtension(char *error, size_t maxlength);
  18. void SM_UnloadExtension();
  19. SH_DECL_HOOK3_void(IServerGameDLL, ServerActivate, SH_NOATTRIB, 0, edict_t *, int, int);
  20. SH_DECL_HOOK6(IServerGameDLL, LevelInit, SH_NOATTRIB, 0, bool, char const *, char const *, char const *, char const *, bool, bool);
  21. DynSchema g_Plugin;
  22. IServerGameDLL *server = nullptr;
  23. IVEngineServer *engine = NULL;
  24. IFileSystem *filesystem = nullptr;
  25. PLUGIN_EXPOSE(DynSchema, g_Plugin);
  26. class ISchemaAttributeType;
  27. // this may need to be updated in the future
  28. class CEconItemAttributeDefinition
  29. {
  30. public:
  31. // TODO implementing ~CEconItemAttributeDefinition segfaults. not sure what's up.
  32. // ideally we implement it to match the game so InsertOrReplace is sure to work correctly
  33. /* 0x00 */ KeyValues *m_KeyValues;
  34. /* 0x04 */ unsigned short m_iIndex;
  35. /* 0x08 */ ISchemaAttributeType *m_AttributeType;
  36. /* 0x0c */ bool m_bHidden;
  37. /* 0x0d */ bool m_bForceOutputDescription;
  38. /* 0x0e */ bool m_bStoreAsInteger;
  39. /* 0x0f */ bool m_bInstanceData;
  40. /* 0x10 */ int m_iAssetClassExportType;
  41. /* 0x14 */ int m_iAssetClassBucket;
  42. /* 0x18 */ bool m_bIsSetBonus;
  43. /* 0x1c */ int m_iIsUserGenerated;
  44. /* 0x20 */ int m_iEffectType;
  45. /* 0x24 */ int m_iDescriptionFormat;
  46. /* 0x28 */ char *m_pszDescriptionString;
  47. /* 0x2c */ char *m_pszArmoryDesc;
  48. /* 0x30 */ char *m_pszName;
  49. /* 0x34 */ char *m_pszAttributeClass;
  50. /* 0x38 */ bool m_bCanAffectMarketName;
  51. /* 0x39 */ bool m_bCanAffectRecipeCompName;
  52. /* 0x3c */ int m_nTagHandle;
  53. /* 0x40 */ string_t m_iszAttributeClass;
  54. };
  55. // binary refers to 0x58 when iterating over the attribute map, so we'll refer to that value
  56. // we could also do a runtime assertion
  57. static_assert(sizeof(CEconItemAttributeDefinition) + 0x14 == 0x58, "CEconItemAttributeDefinition size mismatch");
  58. // pointer to item schema attribute map singleton
  59. using AttributeMap = CUtlMap<int, CEconItemAttributeDefinition, int>;
  60. AttributeMap *g_SchemaAttributes;
  61. size_t g_nAutoAttributeBase = 4000;
  62. std::map<std::string, int> g_AutoNumberedAttributes;
  63. typedef uintptr_t (*GetEconItemSchema_fn)(void);
  64. GetEconItemSchema_fn fnGetEconItemSchema = nullptr;
  65. // https://www.unknowncheats.me/wiki/Calling_Functions_From_Injected_Library_Using_Function_Pointers_in_C%2B%2B
  66. #ifdef WIN32
  67. typedef bool (__thiscall *CEconItemAttributeInitFromKV_fn)(CEconItemAttributeDefinition* pThis, KeyValues* pAttributeKeys, CUtlVector<CUtlString>* pErrors);
  68. #elif defined(_LINUX)
  69. typedef bool (__cdecl *CEconItemAttributeInitFromKV_fn)(CEconItemAttributeDefinition* pThis, KeyValues* pAttributeKeys, CUtlVector<CUtlString>* pErrors);
  70. #endif
  71. CEconItemAttributeInitFromKV_fn fnItemAttributeInitFromKV = nullptr;
  72. const char* NATIVE_ATTRIB_DIR = "addons/sourcemod/configs/tf2nativeattribs";
  73. bool DynSchema::Load(PluginId id, ISmmAPI *ismm, char *error, size_t maxlen, bool late)
  74. {
  75. PLUGIN_SAVEVARS();
  76. GET_V_IFACE_CURRENT(GetEngineFactory, engine, IVEngineServer, INTERFACEVERSION_VENGINESERVER);
  77. GET_V_IFACE_ANY(GetServerFactory, server, IServerGameDLL, INTERFACEVERSION_SERVERGAMEDLL);
  78. GET_V_IFACE_CURRENT(GetFileSystemFactory, filesystem, IFileSystem, FILESYSTEM_INTERFACE_VERSION);
  79. SH_ADD_HOOK_MEMFUNC(IServerGameDLL, LevelInit, server, this, &DynSchema::Hook_LevelInitPost, true);
  80. return true;
  81. }
  82. bool DynSchema::OnExtensionLoad(IExtension *me, IShareSys *sys, char *error, size_t maxlength, bool late) {
  83. sharesys = sys;
  84. myself = me;
  85. /* Get the default interfaces from our configured SDK header */
  86. if (!SM_AcquireInterfaces(error, maxlength)) {
  87. return false;
  88. }
  89. // get the base address of the server
  90. {
  91. #if _WINDOWS
  92. fnGetEconItemSchema = reinterpret_cast<GetEconItemSchema_fn>(sm_memutils->FindPattern(server, "\xE8\x2A\x2A\x2A\x2A\x83\xC0\x04\xC3", 9));
  93. fnItemAttributeInitFromKV = reinterpret_cast<CEconItemAttributeInitFromKV_fn>(sm_memutils->FindPattern(server, "\x55\x8B\xEC\x53\x8B\x5D\x08\x56\x8B\xF1\x8B\xCB\x57\xE8\x2A\x2A\x2A\x2A", 18));
  94. #elif _LINUX
  95. Dl_info info;
  96. if (dladdr(server, &info) == 0) {
  97. snprintf(error, maxlength, "dladdr failed");
  98. return 0;
  99. }
  100. void *handle = dlopen(info.dli_fname, RTLD_NOW);
  101. if (!handle) {
  102. snprintf(error, maxlength, "Failed to dlopen server.");
  103. return 0;
  104. }
  105. fnGetEconItemSchema = reinterpret_cast<GetEconItemSchema_fn>(sm_memutils->ResolveSymbol(handle, "_Z15GEconItemSchemav"));
  106. fnItemAttributeInitFromKV = reinterpret_cast<CEconItemAttributeInitFromKV_fn>(sm_memutils->ResolveSymbol(handle, "_ZN28CEconItemAttributeDefinition11BInitFromKVEP9KeyValuesP10CUtlVectorI10CUtlString10CUtlMemoryIS3_iEE"));
  107. dlclose(handle);
  108. #endif
  109. }
  110. if (fnGetEconItemSchema == nullptr) {
  111. snprintf(error, maxlength, "Failed to setup call to GetEconItemSchema()");
  112. return false;
  113. } else if (fnItemAttributeInitFromKV == nullptr) {
  114. snprintf(error, maxlength, "Failed to setup call to CEconItemAttributeDefinition::BInitFromKV");
  115. return false;
  116. }
  117. // is this late enough in the MM:S load stage? we might just have to hold the function
  118. g_SchemaAttributes = reinterpret_cast<AttributeMap*>(fnGetEconItemSchema() + 0x1BC);
  119. return true;
  120. }
  121. bool DynSchema::Unload(char *error, size_t maxlen) {
  122. SM_UnloadExtension();
  123. SH_REMOVE_HOOK_MEMFUNC(IServerGameDLL, LevelInit, server, this, &DynSchema::Hook_LevelInitPost, true);
  124. return true;
  125. }
  126. /**
  127. * Initializes a CEconItemAttributeDefinition from a KeyValues definition, then inserts or
  128. * replaces the appropriate entry in the schema.
  129. */
  130. bool InsertOrReplaceAttribute(KeyValues *pAttribKV) {
  131. const char* attrID = pAttribKV->GetName();
  132. const char* attrName = pAttribKV->GetString("name");
  133. int attrdef;
  134. if (strcmp(attrID, "auto") == 0) {
  135. /**
  136. * Have the plugin automatically allocate an attribute ID.
  137. * - if the name is already mapped to an ID, then use that
  138. * - otherwise, continue to increment our counter until we find an unused one
  139. */
  140. auto search = g_AutoNumberedAttributes.find(attrName);
  141. if (search != g_AutoNumberedAttributes.end()) {
  142. attrdef = search->second;
  143. } else {
  144. while (g_SchemaAttributes->Find(g_nAutoAttributeBase) != g_SchemaAttributes->InvalidIndex()) {
  145. g_nAutoAttributeBase++;
  146. }
  147. attrdef = g_nAutoAttributeBase;
  148. g_AutoNumberedAttributes[attrName] = attrdef;
  149. }
  150. } else {
  151. attrdef = atoi(attrID);
  152. if (attrdef <= 0) {
  153. META_CONPRINTF("Attribute '%s' has invalid index string '%s'\n", attrName, attrID);
  154. return false;
  155. }
  156. }
  157. // only replace existing injected attributes; fail on schema attributes
  158. auto existingIndex = g_SchemaAttributes->Find(attrdef);
  159. if (existingIndex != g_SchemaAttributes->InvalidIndex()) {
  160. auto &existingAttr = g_SchemaAttributes->Element(existingIndex);
  161. if (!existingAttr.m_KeyValues->GetBool("injected")) {
  162. META_CONPRINTF("WARN: Not overriding native attribute '%s'\n",
  163. existingAttr.m_pszName);
  164. return false;
  165. }
  166. }
  167. // embed additional custom data into attribute KV; econdata and the like can deal with this
  168. // one could also add this data into the file itself, but this leaves less room for error
  169. pAttribKV->SetBool("injected", true);
  170. CEconItemAttributeDefinition def;
  171. fnItemAttributeInitFromKV(&def, pAttribKV, nullptr);
  172. // TODO verify that this doesn't leak, or just shrug it off
  173. g_SchemaAttributes->InsertOrReplace(attrdef, def);
  174. return true;
  175. }
  176. bool DynSchema::Hook_LevelInitPost(const char *pMapName, char const *pMapEntities,
  177. char const *pOldLevel, char const *pLandmarkName, bool loadGame, bool background) {
  178. // this hook should fire shortly after the schema is (re)initialized
  179. char game_path[256];
  180. engine->GetGameDir(game_path, sizeof(game_path));
  181. char buffer[1024];
  182. g_SMAPI->PathFormat(buffer, sizeof(buffer), "%s/%s",
  183. game_path, "addons/dynattrs/items_dynamic.txt");
  184. // always initialize attributes -- it's better than losing attributes on schema reinit
  185. // coughhiddendevattributescough
  186. // read our own dynamic schema file -- this one supports other sections
  187. KeyValues::AutoDelete pKVMainConfig("DynamicSchema");
  188. if (pKVMainConfig->LoadFromFile(filesystem, buffer)) {
  189. KeyValues *pKVAttributes = pKVMainConfig->FindKey( "attributes" );
  190. if (pKVAttributes) {
  191. FOR_EACH_TRUE_SUBKEY(pKVAttributes, kv) {
  192. InsertOrReplaceAttribute(kv);
  193. }
  194. META_CONPRINTF("Successfully injected custom schema %s\n", buffer);
  195. } else {
  196. META_CONPRINTF("Failed to inject custom schema %s\n", buffer);
  197. }
  198. }
  199. // iterate over TF2 Hidden Dev Attributes KV format
  200. // https://forums.alliedmods.net/showthread.php?t=326853
  201. g_SMAPI->PathFormat(buffer, sizeof(buffer), "%s/%s/*", game_path, NATIVE_ATTRIB_DIR);
  202. FileFindHandle_t findHandle;
  203. const char *filename = filesystem->FindFirst(buffer, &findHandle);
  204. while (filename) {
  205. char pathbuf[1024];
  206. g_SMAPI->PathFormat(pathbuf, sizeof(pathbuf), "%s/%s/%s", game_path,
  207. NATIVE_ATTRIB_DIR, filename);
  208. if (!filesystem->FindIsDirectory(findHandle)) {
  209. KeyValues::AutoDelete nativeAttribConfig("attributes");
  210. nativeAttribConfig->LoadFromFile(filesystem, pathbuf);
  211. FOR_EACH_TRUE_SUBKEY(nativeAttribConfig, kv) {
  212. InsertOrReplaceAttribute(kv);
  213. }
  214. META_CONPRINTF("Discovered custom schema %s\n", pathbuf);
  215. }
  216. filename = filesystem->FindNext(findHandle);
  217. }
  218. filesystem->FindClose(findHandle);
  219. // perhaps add some other validations before we actually process our attributes?
  220. // TODO ensure the name doesn't clash with existing / newly injected attributes
  221. return true;
  222. }
  223. void DynSchema::AllPluginsLoaded() {
  224. /* This is where we'd do stuff that relies on the mod or other plugins
  225. * being initialized (for example, cvars added and events registered).
  226. */
  227. BindToSourceMod();
  228. }
  229. void* DynSchema::OnMetamodQuery(const char* iface, int *ret) {
  230. if (strcmp(iface, SOURCEMOD_NOTICE_EXTENSIONS) == 0) {
  231. BindToSourceMod();
  232. }
  233. if (ret != NULL) {
  234. *ret = IFACE_OK;
  235. }
  236. return NULL;
  237. }
  238. void BindToSourceMod() {
  239. char error[256];
  240. if (!SM_LoadExtension(error, sizeof(error))) {
  241. char message[512];
  242. snprintf(message, sizeof(message), "Could not load as a SourceMod extension: %s\n", error);
  243. engine->LogPrint(message);
  244. }
  245. }
  246. bool SM_LoadExtension(char *error, size_t maxlength) {
  247. if ((smexts = (IExtensionManager *)
  248. g_SMAPI->MetaFactory(SOURCEMOD_INTERFACE_EXTENSIONS, NULL, NULL)) == NULL) {
  249. if (error && maxlength) {
  250. snprintf(error, maxlength, SOURCEMOD_INTERFACE_EXTENSIONS " interface not found");
  251. }
  252. return false;
  253. }
  254. /* This could be more dynamic */
  255. char path[256];
  256. g_SMAPI->PathFormat(path, sizeof(path), "addons/dynattrs/tf2dynschema%s",
  257. #if defined __linux__
  258. "_mm.so"
  259. #else
  260. ".dll"
  261. #endif
  262. );
  263. if ((myself = smexts->LoadExternal(&g_Plugin, path, "dynschema.ext", error, maxlength))
  264. == NULL) {
  265. SM_UnsetInterfaces();
  266. return false;
  267. }
  268. return true;
  269. }
  270. void SM_UnloadExtension() {
  271. smexts->UnloadExtension(myself);
  272. }
  273. bool DynSchema::Pause(char *error, size_t maxlen) {
  274. return true;
  275. }
  276. bool DynSchema::Unpause(char *error, size_t maxlen) {
  277. return true;
  278. }
  279. const char *DynSchema::GetLicense() {
  280. return "GPLv3+";
  281. }
  282. const char *DynSchema::GetVersion() {
  283. return "1.3.0";
  284. }
  285. const char *DynSchema::GetDate() {
  286. return __DATE__;
  287. }
  288. const char *DynSchema::GetLogTag() {
  289. return "dynschema";
  290. }
  291. const char *DynSchema::GetAuthor() {
  292. return "nosoop";
  293. }
  294. const char *DynSchema::GetDescription() {
  295. return "Injects user-defined content into the game schema";
  296. }
  297. const char *DynSchema::GetName() {
  298. return "TF2 Dynamic Schema";
  299. }
  300. const char *DynSchema::GetURL() {
  301. return "https://git.csrd.science/";
  302. }
  303. void DynSchema::OnExtensionUnload() {
  304. SM_UnsetInterfaces();
  305. }
  306. void DynSchema::OnExtensionsAllLoaded() {
  307. // no-op
  308. }
  309. void DynSchema::OnExtensionPauseChange(bool pause) {
  310. // no-op
  311. }
  312. bool DynSchema::QueryRunning(char *error, size_t maxlength) {
  313. return true;
  314. }
  315. bool DynSchema::IsMetamodExtension() {
  316. return true;
  317. }
  318. const char *DynSchema::GetExtensionName() {
  319. return this->GetName();
  320. }
  321. const char *DynSchema::GetExtensionURL() {
  322. return this->GetURL();
  323. }
  324. const char *DynSchema::GetExtensionTag() {
  325. return this->GetLogTag();
  326. }
  327. const char *DynSchema::GetExtensionAuthor() {
  328. return this->GetAuthor();
  329. }
  330. const char *DynSchema::GetExtensionVerString() {
  331. return this->GetVersion();
  332. }
  333. const char *DynSchema::GetExtensionDescription() {
  334. return this->GetDescription();
  335. }
  336. const char *DynSchema::GetExtensionDateString() {
  337. return this->GetDate();
  338. }