diff --git a/core/ConsoleDetours.cpp b/core/ConsoleDetours.cpp index ebbe8e93..b83d0a93 100644 --- a/core/ConsoleDetours.cpp +++ b/core/ConsoleDetours.cpp @@ -31,7 +31,7 @@ /** * On SourceHook v4.3 or lower, there are no DVP hooks. Very sad, right? - * Only do this on newer version. For the older code, we'll do an incredibly + * Only do this on newer versions. For the older code, we'll do an incredibly * hacky detour. * * The idea of the "non-hacky" (yeah... no) code is that every unique @@ -49,6 +49,7 @@ #include "ConCmdManager.h" #include "HalfLife2.h" #include "ConCommandBaseIterator.h" +#include "ShareSys.h" #if defined PLATFORM_LINUX # include @@ -531,10 +532,12 @@ void DummyHook() * BEGIN THE ACTUALLY GENERIC CODE. */ +#define FEATURECAP_COMMANDLISTENER "command listener" + static GenericCommandHooker s_GenericHooker; ConsoleDetours g_ConsoleDetours; -ConsoleDetours::ConsoleDetours() : triedToEnable(false), isEnabled(false) +ConsoleDetours::ConsoleDetours() : status(FeatureStatus_Unknown) { } @@ -542,6 +545,7 @@ void ConsoleDetours::OnSourceModAllInitialized() { m_pForward = g_Forwards.CreateForwardEx("OnAnyCommand", ET_Hook, 3, NULL, Param_Cell, Param_String, Param_Cell); + g_ShareSys.AddCapabilityProvider(NULL, this, FEATURECAP_COMMANDLISTENER); } void ConsoleDetours::OnSourceModShutdown() @@ -559,18 +563,23 @@ void ConsoleDetours::OnSourceModShutdown() s_GenericHooker.Disable(); } -bool ConsoleDetours::IsAvailable() +FeatureStatus ConsoleDetours::GetFeatureStatus(FeatureType type, const char *name) { - if (triedToEnable) - return isEnabled; - isEnabled = s_GenericHooker.Enable(); - triedToEnable = true; - return isEnabled; + return GetStatus(); +} + +FeatureStatus ConsoleDetours::GetStatus() +{ + if (status == FeatureStatus_Unknown) + { + status = s_GenericHooker.Enable() ? FeatureStatus_Available : FeatureStatus_Unavailable; + } + return status; } bool ConsoleDetours::AddListener(IPluginFunction *fun, const char *command) { - if (!IsAvailable()) + if (GetStatus() != FeatureStatus_Available) return false; if (command == NULL) diff --git a/core/ConsoleDetours.h b/core/ConsoleDetours.h index de41e519..d376f9b5 100644 --- a/core/ConsoleDetours.h +++ b/core/ConsoleDetours.h @@ -36,7 +36,9 @@ #include "ForwardSys.h" #include -class ConsoleDetours : public SMGlobalClass +class ConsoleDetours : + public SMGlobalClass, + public IFeatureProvider { friend class PlayerManager; friend class GenericCommandHooker; @@ -50,6 +52,8 @@ public: public: //SMGlobalClass void OnSourceModAllInitialized(); void OnSourceModShutdown(); +public: //IFeatureProvider + FeatureStatus GetFeatureStatus(FeatureType type, const char *name); public: bool AddListener(IPluginFunction *fun, const char *command); bool RemoveListener(IPluginFunction *fun, const char *command); @@ -60,15 +64,14 @@ private: #else static cell_t Dispatch(ConCommand *pBase); #endif - bool IsAvailable(); public: - inline bool IsEnabled() + FeatureStatus GetStatus(); + bool IsEnabled() { - return isEnabled; + return status == FeatureStatus_Available; } private: - bool triedToEnable; - bool isEnabled; + FeatureStatus status; IChangeableForward *m_pForward; KTrie m_CmdLookup; List m_Listeners; diff --git a/core/ShareSys.cpp b/core/ShareSys.cpp index f891392e..9655a4c2 100644 --- a/core/ShareSys.cpp +++ b/core/ShareSys.cpp @@ -527,3 +527,85 @@ NativeEntry *ShareSystem::AddFakeNative(IPluginFunction *pFunc, const char *name return pEntry; } + +void ShareSystem::AddCapabilityProvider(IExtension *myself, IFeatureProvider *provider, + const char *name) +{ + if (m_caps.retrieve(name) != NULL) + return; + + Capability cap; + cap.ext = myself; + cap.provider = provider; + + m_caps.insert(name, cap); +} + +void ShareSystem::DropCapabilityProvider(IExtension *myself, IFeatureProvider *provider, + const char *name) +{ + Capability *pCap = m_caps.retrieve(name); + if (pCap == NULL) + return; + + if (pCap->ext != myself || pCap->provider != provider) + return; + + m_caps.remove(name); +} + +FeatureStatus ShareSystem::TestFeature(IPluginRuntime *pRuntime, FeatureType feature, + const char *name) +{ + switch (feature) + { + case FeatureType_Native: + return TestNative(pRuntime, name); + case FeatureType_Capability: + return TestCap(name); + default: + break; + } + + return FeatureStatus_Unknown; +} + +FeatureStatus ShareSystem::TestNative(IPluginRuntime *pRuntime, const char *name) +{ + uint32_t index; + + if (pRuntime->FindNativeByName(name, &index) == SP_ERROR_NONE) + { + sp_native_t *native; + if (pRuntime->GetNativeByIndex(index, &native) == SP_ERROR_NONE) + { + if (native->status == SP_NATIVE_BOUND) + return FeatureStatus_Available; + else + return FeatureStatus_Unknown; + } + } + + NativeEntry *entry = FindNative(name); + if (entry == NULL) + return FeatureStatus_Unknown; + + if ((entry->replacement.owner != NULL || entry->owner != NULL) && + (entry->replacement.func != NULL || entry->func != NULL)) + { + return FeatureStatus_Available; + } + else + { + return FeatureStatus_Unavailable; + } +} + +FeatureStatus ShareSystem::TestCap(const char *name) +{ + Capability *cap = m_caps.retrieve(name); + if (cap == NULL) + return FeatureStatus_Unknown; + + return cap->provider->GetFeatureStatus(FeatureType_Capability, name); +} diff --git a/core/ShareSys.h b/core/ShareSys.h index b600f9c2..b7deea2c 100644 --- a/core/ShareSys.h +++ b/core/ShareSys.h @@ -87,7 +87,13 @@ struct NativeEntry FakeNative *fake; }; -class ShareSystem : +struct Capability +{ + IExtension *ext; + IFeatureProvider *provider; +}; + +class ShareSystem : public IShareSys, public SMGlobalClass, public IHandleTypeDispatch @@ -110,6 +116,10 @@ public: //IShareSys void AddDependency(IExtension *myself, const char *filename, bool require, bool autoload); void RegisterLibrary(IExtension *myself, const char *name); void OverrideNatives(IExtension *myself, const sp_nativeinfo_t *natives); + void AddCapabilityProvider(IExtension *myself, IFeatureProvider *provider, + const char *name); + void DropCapabilityProvider(IExtension *myself, IFeatureProvider *provider, + const char *name); public: //SMGlobalClass /* Pre-empt in case anything tries to register idents early */ void Initialize(); @@ -119,6 +129,9 @@ public: //IHandleTypeDispatch public: IdentityToken_t *CreateCoreIdentity(); void RemoveInterfaces(IExtension *pExtension); + FeatureStatus TestFeature(IPluginRuntime *pRuntime, FeatureType feature, const char *name); + FeatureStatus TestNative(IPluginRuntime *pRuntime, const char *name); + FeatureStatus TestCap(const char *name); public: inline IdentityToken_t *GetIdentRoot() { @@ -143,6 +156,7 @@ private: HandleType_t m_IfaceType; IdentityType_t m_CoreType; KTrie m_NtvCache; + KTrie m_caps; }; extern ShareSystem g_ShareSys; diff --git a/core/smn_core.cpp b/core/smn_core.cpp index 91acfa66..88b9a534 100644 --- a/core/smn_core.cpp +++ b/core/smn_core.cpp @@ -42,6 +42,7 @@ #include "ForwardSys.h" #include "Logger.h" #include "ExtensionSys.h" +#include #if defined PLATFORM_WINDOWS #include @@ -491,6 +492,11 @@ static cell_t LibraryExists(IPluginContext *pContext, const cell_t *params) char *str; pContext->LocalToString(params[1], &str); + if (strcmp(str, "__CanTestFeatures__") == 0) + { + return 1; + } + if (g_PluginSys.LibraryExists(str)) { return 1; @@ -630,6 +636,43 @@ static cell_t VerifyCoreVersion(IPluginContext *pContext, const cell_t *params) return 4; } +static cell_t GetFeatureStatus(IPluginContext *pContext, const cell_t *params) +{ + FeatureType type = (FeatureType)params[1]; + char *name; + + pContext->LocalToString(params[2], &name); + + return g_ShareSys.TestFeature(pContext->GetRuntime(),type, name); +} + +static cell_t RequireFeature(IPluginContext *pContext, const cell_t *params) +{ + FeatureType type = (FeatureType)params[1]; + char *name; + + pContext->LocalToString(params[2], &name); + + if (g_ShareSys.TestFeature(pContext->GetRuntime(),type, name) != FeatureStatus_Available) + { + char buffer[255]; + char *msg = buffer; + char default_message[255]; + CPlugin *pPlugin = g_PluginSys.GetPluginByCtx(pContext->GetContext()); + + g_SourceMod.FormatString(buffer, sizeof(buffer), pContext, params, 3); + if (pContext->GetLastNativeError() != SP_ERROR_NONE || buffer[0] == '\0') + { + UTIL_Format(default_message, sizeof(default_message), "Feature \"%s\" not available", name); + msg = default_message; + } + pPlugin->SetErrorState(Plugin_Error, "%s", msg); + return pContext->ThrowNativeErrorEx(SP_ERROR_ABORTED, "%s", msg); + } + + return 1; +} + REGISTER_NATIVES(coreNatives) { {"AutoExecConfig", AutoExecConfig}, @@ -654,6 +697,8 @@ REGISTER_NATIVES(coreNatives) {"GetExtensionFileStatus", GetExtensionFileStatus}, {"FindPluginByNumber", FindPluginByNumber}, {"VerifyCoreVersion", VerifyCoreVersion}, + {"GetFeatureStatus", GetFeatureStatus}, + {"RequireFeature", RequireFeature}, {NULL, NULL}, }; diff --git a/plugins/include/console.inc b/plugins/include/console.inc index cc0d758a..28fb959b 100644 --- a/plugins/include/console.inc +++ b/plugins/include/console.inc @@ -858,6 +858,8 @@ native RemoveServerTag(const String:tag[]); */ functag public Action:CommandListener(client, const String:command[], argc); +#define FEATURECAP_COMMANDLISTENER "command listener" + /** * Adds a callback that will fire when a command is sent to the server. * @@ -866,6 +868,9 @@ functag public Action:CommandListener(client, const String:command[], argc); * Using Reg*Cmd to intercept is in poor practice, as it physically creates a * new command and can slow down dispatch in general. * + * To see if this feature is available, use FeatureType_Capability and + * FEATURECAP_COMMANDLISTENER. + * * @param callback Callback. * @param command Command, or if not specified, a global listener. * The command is case insensitive. diff --git a/plugins/include/core.inc b/plugins/include/core.inc index 30b8df12..708702f3 100644 --- a/plugins/include/core.inc +++ b/plugins/include/core.inc @@ -151,8 +151,22 @@ public Extension:__ext_core = native VerifyCoreVersion(); +/** + * Sets a native as optional, such that if it is unloaded, removed, + * or otherwise non-existent, the plugin will still work. Calling + * removed natives results in a run-time error. + * + * @param name Native name. + * @noreturn + */ +native MarkNativeAsOptional(const String:name[]); + public __ext_core_SetNTVOptional() { + MarkNativeAsOptional("GetFeatureStatus"); + MarkNativeAsOptional("RequireFeature"); + MarkNativeAsOptional("AddCommandListener"); + MarkNativeAsOptional("RemoveCommandListener"); VerifyCoreVersion(); } diff --git a/plugins/include/sourcemod.inc b/plugins/include/sourcemod.inc index 3afb5208..407332bb 100644 --- a/plugins/include/sourcemod.inc +++ b/plugins/include/sourcemod.inc @@ -410,16 +410,6 @@ native GetSysTickCount(); */ native AutoExecConfig(bool:autoCreate=true, const String:name[]="", const String:folder[]="sourcemod"); -/** - * Sets a native as optional, such that if it is unloaded, removed, - * or otherwise non-existent, the plugin will still work. Calling - * removed natives results in a run-time error. - * - * @param name Native name. - * @noreturn - */ -native MarkNativeAsOptional(const String:name[]); - /** * Registers a library name for identifying as a dependency to * other plugins. @@ -565,6 +555,81 @@ forward bool:OnClientFloodCheck(client); */ forward OnClientFloodResult(client, bool:blocked); +/** + * Feature types. + */ +enum FeatureType +{ + /** + * A native function call. + */ + FeatureType_Native, + + /** + * A named capability. This is distinctly different from checking for a + * native, because the underlying functionality could be enabled on-demand + * to improve loading time. Thus a native may appear to exist, but it might + * be part of a set of features that are not compatible with the current game + * or version of SourceMod. + */ + FeatureType_Capability +}; + +/** + * Feature statuses. + */ +enum FeatureStatus +{ + /** + * Feature is available for use. + */ + FeatureStatus_Available, + + /** + * Feature is not available. + */ + FeatureStatus_Unavailable, + + /** + * Feature is not known at all. + */ + FeatureStatus_Unknown +}; + +/** + * Returns whether "GetFeatureStatus" will work. Using this native + * or this function will not cause SourceMod to fail loading on older versions, + * however, GetFeatureStatus will only work if this function returns true. + * + * @return True if GetFeatureStatus will work, false otherwise. + */ +stock bool:CanTestFeatures() +{ + return LibraryExists("__CanTestFeatures__"); +} + +/** + * Returns whether a feature exists, and if so, whether it is usable. + * + * @param type Feature type. + * @param name Feature name. + * @return Feature status. + */ +native FeatureStatus:GetFeatureStatus(FeatureType:type, const String:name[]); + +/** + * Requires that a given feature is available. If it is not, SetFailState() + * is called with the given message. + * + * @param type Feature type. + * @param name Feature name. + * @param fmt Message format string, or empty to use default. + * @param ... Message format parameters, if any. + * @noreturn + */ +native RequireFeature(FeatureType:type, const String:name[], + const String:fmt[]="", any:...); + #include #include #include diff --git a/plugins/testsuite/capstest.sp b/plugins/testsuite/capstest.sp new file mode 100644 index 00000000..355bf210 --- /dev/null +++ b/plugins/testsuite/capstest.sp @@ -0,0 +1,33 @@ + + +public OnPluginStart() +{ + RegServerCmd("sm_test_caps1", Test_Caps1); + RegServerCmd("sm_test_caps2", Test_Caps2); + RegServerCmd("sm_test_caps3", Test_Caps3); +} + +public Action:Test_Caps1(args) +{ + PrintToServer("CanTestFeatures: %d", CanTestFeatures()); + PrintToServer("Status PTS: %d", GetFeatureStatus(FeatureType_Native, "PrintToServer")); + PrintToServer("Status ???: %d", GetFeatureStatus(FeatureType_Native, "???")); + PrintToServer("Status CL: %d", GetFeatureStatus(FeatureType_Capability, FEATURECAP_COMMANDLISTENER)); + + return Plugin_Handled +} + +public Action:Test_Caps2(args) +{ + RequireFeature(FeatureType_Native, "VerifyCoreVersion"); + RequireFeature(FeatureType_Native, "Sally ate a worm"); +} + +public Action:Test_Caps3(args) +{ + RequireFeature(FeatureType_Native, "Sally ate a worm", "oh %s %d no", "yam", 23); +} + + + + diff --git a/public/IExtensionSys.h b/public/IExtensionSys.h index f707dbeb..a7652143 100644 --- a/public/IExtensionSys.h +++ b/public/IExtensionSys.h @@ -133,7 +133,7 @@ namespace SourceMod * Note: This is bumped when IShareSys is changed, because IShareSys * itself is not versioned. */ - #define SMINTERFACE_EXTENSIONAPI_VERSION 4 + #define SMINTERFACE_EXTENSIONAPI_VERSION 5 /** * @brief The interface an extension must expose. diff --git a/public/IShareSys.h b/public/IShareSys.h index e097f6f2..367d0b68 100644 --- a/public/IShareSys.h +++ b/public/IShareSys.h @@ -35,9 +35,6 @@ /** * @file IShareSys.h * @brief Defines the Share System, responsible for shared resources and dependencies. - * - * The Share System also manages the Identity_t data type, although this is internally - * implemented with the Handle System. */ #include @@ -54,6 +51,41 @@ namespace SourceMod /** Forward declaration from IHandleSys.h */ typedef HandleType_t IdentityType_t; + /** + * @brief Types of features. + */ + enum FeatureType + { + FeatureType_Native, /**< Native functions for plugins. */ + FeatureType_Capability /**< Named capabilities. */ + }; + + /** + * @brief Feature presence status codes. + */ + enum FeatureStatus + { + FeatureStatus_Available = 0, /**< Feature is available for use. */ + FeatureStatus_Unavailable, /**< Feature is unavailable, but known. */ + FeatureStatus_Unknown /**< Feature is not known. */ + }; + + /** + * @brief Provides a capability feature. + */ + class IFeatureProvider + { + public: + /** + * @brief Must return whether a capability is present. + * + * @param type Feature type (FeatureType_Capability right now). + * @param name Feature name. + * @return Feature status code. + */ + virtual FeatureStatus GetFeatureStatus(FeatureType type, const char *name) = 0; + }; + /** * @brief Defines the base functionality required by a shared interface. */ @@ -221,6 +253,31 @@ namespace SourceMod * lifetime of the extension. */ virtual void OverrideNatives(IExtension *myself, const sp_nativeinfo_t *natives) =0; + + /** + * @brief Adds a capability provider. Feature providers are used by + * plugins to determine if a feature exists at runtime. This is + * distinctly different from checking for a native, because natives + * may be backed by underlying functionality which is not available. + * + * @param myself Extension. + * @param provider Feature provider implementation. + * @param name Capibility name. + */ + virtual void AddCapabilityProvider(IExtension *myself, + IFeatureProvider *provider, + const char *name) =0; + + /** + * @brief Drops a previously added cap provider. + * + * @param myself Extension. + * @param provider Feature provider implementation. + * @param name Capibility name. + */ + virtual void DropCapabilityProvider(IExtension *myself, + IFeatureProvider *provider, + const char *name) =0; }; }