diff --git a/AGENTS.md b/AGENTS.md index 6ec33d7..e3e7497 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ - Third-party modules depend on it: never change existing native/forward signatures or semantics. Add new params with defaults or add new natives. Deprecate via `#pragma deprecated`, don't delete (see `docs/update_modules.md`). - New native: implement `Native_X` in API.sp, `RegNative(X)` in `AskPluginLoad2`, declare in `.inc`, add `MarkNativeAsOptional("VIP_X")` in `__pl_vip_core_SetNTVOptional`. - New forward: `static GlobalForward` field + `new GlobalForward(...)` in `API_SetupForwards` + `CreateForward_*` wrapper + `forward` declaration with doc comment in `.inc`. +- Any added native/forward: bump `VIP_CORE_INTERFACE` in `.inc` once per release (3.2 → 320) and note "Requires VIP_CORE_INTERFACE N" in its doc comment; modules check it via `VIP_GetCurrentVersionInterface()`. ## Database - Config name `vip_core` in databases.cfg → MySQL; otherwise falls back to local SQLite. Every query must work on BOTH. @@ -32,7 +33,7 @@ ## Code style (new/changed code; legacy deviations are not a reason to copy them) - `#pragma semicolon 1`, `#pragma newdecls required`. Tabs, no trailing whitespace. Allman braces, always braced, also single-statement `if`/`for` and `case` bodies (`case MenuAction_End: delete hMenu;` is the only one-liner). -- Space after keywords: `if (`, `for (`, `while (`, `switch (`. `++i` in loops. `!bFlag`, not `bFlag == false`. Buffers: `SZF(szBuf)`, not `szBuf, sizeof(szBuf)`; `FormatEx` unless formatting a buffer into itself. +- Space after keywords: `if (`, `for (`, `while (`, `switch (`. A condition that doesn't fit one line: `if (` on its own line, one operand per line with the operator (`||`, `&&`) at the end, then `)` on its own line. `++i` in loops. `!bFlag`, not `bFlag == false`. Buffers: `SZF(szBuf)`, not `szBuf, sizeof(szBuf)`; `FormatEx` unless formatting a buffer into itself. - Names: locals/params Hungarian `iClient`, `szName`, `hMenu`, `bFlag`, `fTime`, `eStatus`; globals `g_` + prefix (`g_hDatabase`, `g_szSID`), cvars `g_CVAR_*`; macros/consts `UPPER_SNAKE`, StringMap keys `KEY_*` / `DATA_KEY_*`. - Functions: `PascalCase` with file prefix: `Clients_*`, `Features_*`, `Storage_*`, `DB_*`, `UTIL_*`, `API_*`. Callbacks: `SQL_Callback_*`, `SQL_TxnCallback_*`, `Timer_*`, `MenuHandler_*`, `Event_*`; commands `*_CMD`; natives `Native_*`; forward wrappers `CreateForward_*`. - SQL: backticks around identifiers, single quotes for values, trailing `;`, user strings via `g_hDatabase.Format`/`Escape`. Callbacks: `if (hResult == null || szError[0]) { LogError(": %s", szError); return; }`, free the DataPack on every path. diff --git a/README.md b/README.md index 78935e0..185e4d4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This plugin allows you to give VIP status to players for a specific period or fo Given the plugin’s modular system, the functionality can always be expanded. The plugin is very flexible and easy to configure, it can work with both SQLite and MySQL database. -[[VIP] Core official thread on HlMod.ru](http://hlmod.ru/resources/vip-core.245/) +[[VIP] Core official thread on HlMod.net](https://hlmod.net/resources/vip-core.245/) [Documentation](https://r1ko.github.io/VIP-Core/) diff --git a/addons/sourcemod/scripting/VIP_Core.sp b/addons/sourcemod/scripting/VIP_Core.sp index a1d5769..31a9e1b 100644 --- a/addons/sourcemod/scripting/VIP_Core.sp +++ b/addons/sourcemod/scripting/VIP_Core.sp @@ -7,7 +7,7 @@ #include #if !defined VIP_VERSION -#define VIP_VERSION "3.1.1 R" +#define VIP_VERSION "3.2.0 R" #endif diff --git a/addons/sourcemod/scripting/include/vip_core.inc b/addons/sourcemod/scripting/include/vip_core.inc index 301e276..98715e7 100644 --- a/addons/sourcemod/scripting/include/vip_core.inc +++ b/addons/sourcemod/scripting/include/vip_core.inc @@ -3,7 +3,8 @@ #endif #define _vip_core_included -#define VIP_CORE_INTERFACE 310 +// API version of this include; bumped whenever natives or forwards are added. See VIP_GetCurrentVersionInterface(). +#define VIP_CORE_INTERFACE 320 /* VIP feature data types */ enum VIP_ValueType @@ -156,6 +157,20 @@ forward void VIP_OnConfigsLoaded(); */ forward void VIP_OnClientStorageLoaded(int iClient); +/** + * Called when a value in the player's storage has changed (VIP_SetClientStorageValue, + * including toggle statuses saved by the core under "FeatureStatus-" keys). + * Not called when the value is set to what it already was. + * Requires VIP_CORE_INTERFACE 320 (see VIP_GetCurrentVersionInterface). + * + * @param iClient Client index. + * @param szKey Key. + * @param szValue New value. + * + * @noreturn +*/ +forward void VIP_OnClientStorageUpdate(int iClient, const char[] szKey, const char[] szValue); + /** * Called when a VIP feature has been registered. * @@ -345,6 +360,7 @@ native bool VIP_IsValidVIPGroup(const char[] szGroup); /** * Adds a group (in memory only, groups.ini is not modified). * Groups are reset when configs are reloaded; add them again in VIP_OnConfigsLoaded(). + * An "extend" key is not resolved here: pass the final values (inheritance works in groups.ini only). * * @param szGroup Group name. * @param hFeatures Group parameters (keys from the current position are copied into the group). @@ -364,6 +380,36 @@ native bool VIP_AddGroup(const char[] szGroup, KeyValues hFeatures); */ native bool VIP_RemoveGroup(const char[] szGroup); +/** + * Adds a feature to a group or changes its value (in memory only, groups.ini is not modified). + * In-game VIPs of the group get the new value at once; a toggle callback and VIP_OnFeatureToggle + * fire only if the player's access to the feature changes. + * sm_reload_vip_cfg rereads groups.ini and drops the change: apply it again in VIP_OnConfigsLoaded(). + * Requires VIP_CORE_INTERFACE 320 (see VIP_GetCurrentVersionInterface). + * + * @param szGroup Group name. + * @param szFeature Feature name (it may be not registered yet). + * @param szValue Feature value. + * + * @return true - on success. + * false - the group does not exist or szFeature is empty. +*/ +native bool VIP_GroupAddFeature(const char[] szGroup, const char[] szFeature, const char[] szValue); + +/** + * Removes a feature from a group (in memory only, groups.ini is not modified). + * In-game VIPs of the group lose the feature at once (toggle callback and VIP_OnFeatureToggle with NO_ACCESS). + * sm_reload_vip_cfg rereads groups.ini and drops the change: apply it again in VIP_OnConfigsLoaded(). + * Requires VIP_CORE_INTERFACE 320 (see VIP_GetCurrentVersionInterface). + * + * @param szGroup Group name. + * @param szFeature Feature name. + * + * @return true - on success. + * false - the group does not exist or has no such feature. +*/ +native bool VIP_GroupRemoveFeature(const char[] szGroup, const char[] szFeature); + /** * Saves a value to the player's storage (vip_storage table). * The storage is available for all players (not only VIP) after VIP_OnClientStorageLoaded(); @@ -480,6 +526,18 @@ native bool VIP_RemoveClientVIP(int iClient, bool bInDB, bool bNotify); */ native bool VIP_IsVIPLoaded(); +/** + * Gets the API version of the running core (its VIP_CORE_INTERFACE), so a module can tell + * which natives and forwards are available, e.g. VIP_GetCurrentVersionInterface() >= 320. + * Cores before 3.2 don't have this native: check it first with + * GetFeatureStatus(FeatureType_Native, "VIP_GetCurrentVersionInterface") == FeatureStatus_Available. + * + * @noparams + * + * @return API version of the running core. +*/ +native int VIP_GetCurrentVersionInterface(); + /** * Registers a new VIP feature. * @@ -741,6 +799,7 @@ public SharedPlugin __pl_vip_core= public void __pl_vip_core_SetNTVOptional() { MarkNativeAsOptional("VIP_IsVIPLoaded"); + MarkNativeAsOptional("VIP_GetCurrentVersionInterface"); MarkNativeAsOptional("VIP_GetDatabase"); MarkNativeAsOptional("VIP_GetDatabaseType"); MarkNativeAsOptional("VIP_RegisterFeature"); @@ -766,6 +825,8 @@ public void __pl_vip_core_SetNTVOptional() MarkNativeAsOptional("VIP_IsGroupExists"); MarkNativeAsOptional("VIP_AddGroup"); MarkNativeAsOptional("VIP_RemoveGroup"); + MarkNativeAsOptional("VIP_GroupAddFeature"); + MarkNativeAsOptional("VIP_GroupRemoveFeature"); MarkNativeAsOptional("VIP_SetClientStorageValue"); MarkNativeAsOptional("VIP_GetClientStorageValue"); MarkNativeAsOptional("VIP_GetClientFeatureStatus"); diff --git a/addons/sourcemod/scripting/vip/API.sp b/addons/sourcemod/scripting/vip/API.sp index 97e1e64..ef1ee05 100644 --- a/addons/sourcemod/scripting/vip/API.sp +++ b/addons/sourcemod/scripting/vip/API.sp @@ -12,6 +12,7 @@ static GlobalForward g_hGlobalForward_OnFeatureUnregistered; static GlobalForward g_hGlobalForward_OnClientDisconnect; static GlobalForward g_hGlobalForward_OnConfigsLoaded; static GlobalForward g_hGlobalForward_OnClientStorageLoaded; +static GlobalForward g_hGlobalForward_OnClientStorageUpdate; void API_SetupForwards() { @@ -30,6 +31,7 @@ void API_SetupForwards() g_hGlobalForward_OnClientDisconnect = new GlobalForward("VIP_OnClientDisconnect", ET_Ignore, Param_Cell, Param_Cell); g_hGlobalForward_OnConfigsLoaded = new GlobalForward("VIP_OnConfigsLoaded", ET_Ignore); g_hGlobalForward_OnClientStorageLoaded = new GlobalForward("VIP_OnClientStorageLoaded", ET_Ignore, Param_Cell); + g_hGlobalForward_OnClientStorageUpdate = new GlobalForward("VIP_OnClientStorageUpdate", ET_Ignore, Param_Cell, Param_String, Param_String); } // Global Forwards @@ -55,6 +57,16 @@ void CreateForward_OnClientStorageLoaded(int iClient) Call_Finish(); } +void CreateForward_OnClientStorageUpdate(int iClient, const char[] szKey, const char[] szValue) +{ + DBG_API("CreateForward_OnClientStorageUpdate(%N (%d), '%s', '%s')", iClient, iClient, szKey, szValue); + Call_StartForward(g_hGlobalForward_OnClientStorageUpdate); + Call_PushCell(iClient); + Call_PushString(szKey); + Call_PushString(szValue); + Call_Finish(); +} + bool CreateForward_OnClientPreLoad(int iClient) { DBG_API("g_hGlobalForward_OnClientPreLoad(%N (%d), %b)", iClient, iClient, g_ePlayer[iClient].bVIP); @@ -233,6 +245,7 @@ public APLRes AskPluginLoad2(Handle myself, bool bLate, char[] szError, int err_ { // Global RegNative(IsVIPLoaded); + RegNative(GetCurrentVersionInterface); RegNative(GetDatabase); RegNative(GetDatabaseType); @@ -271,6 +284,8 @@ public APLRes AskPluginLoad2(Handle myself, bool bLate, char[] szError, int err_ RegNative(IsGroupExists); RegNative(AddGroup); RegNative(RemoveGroup); + RegNative(GroupAddFeature); + RegNative(GroupRemoveFeature); RegNative(SetClientStorageValue); RegNative(GetClientStorageValue); @@ -795,6 +810,48 @@ public int Native_RemoveGroup(Handle hPlugin, int iNumParams) return true; } +public int Native_GroupAddFeature(Handle hPlugin, int iNumParams) +{ + char szGroup[64], szFeature[FEATURE_NAME_LENGTH], szValue[256]; + GetNativeString(1, SZF(szGroup)); + GetNativeString(2, SZF(szFeature)); + GetNativeString(3, SZF(szValue)); + + if (!szFeature[0] || !UTIL_CheckValidVIPGroup(szGroup)) + { + return false; + } + + g_hGroups.SetString(szFeature, szValue); + g_hGroups.Rewind(); + + Clients_ReloadGroupFeature(szGroup, szFeature); + + return true; +} + +public int Native_GroupRemoveFeature(Handle hPlugin, int iNumParams) +{ + char szGroup[64], szFeature[FEATURE_NAME_LENGTH]; + GetNativeString(1, SZF(szGroup)); + GetNativeString(2, SZF(szFeature)); + + if (!szFeature[0] || !UTIL_CheckValidVIPGroup(szGroup)) + { + return false; + } + + bool bRemoved = g_hGroups.DeleteKey(szFeature); + g_hGroups.Rewind(); + + if (bRemoved) + { + Clients_ReloadGroupFeature(szGroup, szFeature); + } + + return bRemoved; +} + public int Native_SetClientStorageValue(Handle hPlugin, int iNumParams) { int iClient = GetNativeCell(1); @@ -832,6 +889,11 @@ public int Native_IsVIPLoaded(Handle hPlugin, int iNumParams) return (g_bStarted && g_hDatabase); } +public int Native_GetCurrentVersionInterface(Handle hPlugin, int iNumParams) +{ + return VIP_CORE_INTERFACE; +} + public int Native_RegisterFeature(Handle hPlugin, int iNumParams) { char szFeature[FEATURE_NAME_LENGTH]; diff --git a/addons/sourcemod/scripting/vip/Clients.sp b/addons/sourcemod/scripting/vip/Clients.sp index 288f6aa..41671c8 100644 --- a/addons/sourcemod/scripting/vip/Clients.sp +++ b/addons/sourcemod/scripting/vip/Clients.sp @@ -368,6 +368,49 @@ void Clients_LoadVIPFeature(int iClient, const char[] szFeature) */ } +// A group's feature changed at runtime: reload it for the in-game VIPs of that group +void Clients_ReloadGroupFeature(const char[] szGroup, const char[] szFeature) +{ + Feature eFeature; + // Not registered yet: the new value is picked up when the module registers it + if (!Features_Get(szFeature, eFeature)) + { + return; + } + + char szClientGroup[64]; + VIP_ToggleState eOldStatus, eNewStatus; + for (int iClient = 1; iClient <= MaxClients; ++iClient) + { + if ( + !IsClientInGame(iClient) || + !g_ePlayer[iClient].bVIP || + !g_ePlayer[iClient].hFeatures.GetString(KEY_GROUP, SZF(szClientGroup)) || + strcmp(szClientGroup, szGroup) != 0 + ) + { + continue; + } + + eOldStatus = Features_GetStatus(iClient, szFeature); + + // GetValue only sets non-empty values, so the old one must go first or a removed/zeroed feature would stay + g_ePlayer[iClient].hFeatures.Remove(szFeature); + g_ePlayer[iClient].hFeatureStatus.Remove(szFeature); + Clients_LoadVIPFeature(iClient, szFeature); + + eNewStatus = Features_GetStatus(iClient, szFeature); + if (eFeature.eType == TOGGLABLE && eNewStatus != eOldStatus) + { + if (eFeature.fnSelect != INVALID_FUNCTION) + { + Function_OnItemToggle(eFeature.hPlugin, eFeature.fnSelect, iClient, szFeature, eOldStatus, eNewStatus); + } + CreateForward_OnFeatureToggle(iClient, szFeature, eOldStatus, eNewStatus); + } + } +} + void Clients_LoadFeature(int iClient, const char[] szFeature) { Feature eFeature; diff --git a/addons/sourcemod/scripting/vip/Configs.sp b/addons/sourcemod/scripting/vip/Configs.sp index 5357b68..97b2e51 100644 --- a/addons/sourcemod/scripting/vip/Configs.sp +++ b/addons/sourcemod/scripting/vip/Configs.sp @@ -36,8 +36,10 @@ void ReadConfigs() ReadSortingMenu(); delete g_hGroups; + delete g_hInfo; g_hGroups = CreateConfig("data/vip/cfg/groups.ini", "VIP_GROUPS"); + Configs_ResolveGroupExtends(g_hGroups); g_hInfo = CreateConfig("data/vip/cfg/info.ini", "VIP_INFO"); CreateForward_OnConfigsLoaded(); @@ -97,4 +99,154 @@ KeyValues CreateConfig(const char[] szFile, const char[] szKvName) hKeyValues.Rewind(); return hKeyValues; -} \ No newline at end of file +} + +#define GROUP_EXTEND_KEY "extend" + +// "extend" "base;silver" in groups.ini: the group inherits the plain values of the listed groups. +// A later group in the list beats an earlier one, the group's own values beat all of them, and an own "" removes an inherited value. +// Resolved once here into flat groups, so the rest of the core never sees "extend". +// Groups added later by VIP_AddGroup are not resolved. +static void Configs_ResolveGroupExtends(KeyValues hGroups) +{ + ArrayList hNames = new ArrayList(ByteCountToCells(MAX_NAME_LENGTH)); + char szGroup[MAX_NAME_LENGTH]; + hGroups.Rewind(); + if (hGroups.GotoFirstSubKey()) + { + do + { + hGroups.GetSectionName(SZF(szGroup)); + hNames.PushString(szGroup); + } + while (hGroups.GotoNextKey()); + } + + ArrayList hResolved = new ArrayList(ByteCountToCells(MAX_NAME_LENGTH)); + ArrayList hStack = new ArrayList(ByteCountToCells(MAX_NAME_LENGTH)); + for (int i = 0; i < hNames.Length; ++i) + { + hNames.GetString(i, SZF(szGroup)); + Configs_ResolveGroup(hGroups, hNames, szGroup, hResolved, hStack); + } + + delete hResolved; + delete hStack; + delete hNames; + hGroups.Rewind(); +} + +static void Configs_ResolveGroup(KeyValues hGroups, ArrayList hNames, const char[] szGroup, ArrayList hResolved, ArrayList hStack) +{ + if (hResolved.FindString(szGroup) != -1) + { + return; + } + + char szExtend[256]; + hGroups.Rewind(); + hGroups.JumpToKey(szGroup); + hGroups.GetString(GROUP_EXTEND_KEY, SZF(szExtend)); + if (!szExtend[0]) + { + hResolved.PushString(szGroup); + return; + } + + hStack.PushString(szGroup); + + // Parents in list order, then the group's own values on top + KeyValues hMerged = new KeyValues(szGroup); + char szParent[MAX_NAME_LENGTH]; + int iPos = 0, iNext; + do + { + iNext = SplitString(szExtend[iPos], ";", SZF(szParent)); + if (iNext == -1) + { + strcopy(SZF(szParent), szExtend[iPos]); + } + else + { + iPos += iNext; + } + + TrimString(szParent); + if (!szParent[0]) + { + continue; + } + + if (hNames.FindString(szParent) == -1) + { + LogError("groups.ini: group \"%s\" extends unknown group \"%s\"", szGroup, szParent); + continue; + } + + if (hStack.FindString(szParent) != -1) + { + LogError("groups.ini: group \"%s\" extends \"%s\", which leads back to itself", szGroup, szParent); + continue; + } + + Configs_ResolveGroup(hGroups, hNames, szParent, hResolved, hStack); + Configs_CopyGroupValues(hGroups, szParent, hMerged); + } + while (iNext != -1); + + hStack.Erase(hStack.Length - 1); + + Configs_CopyGroupValues(hGroups, szGroup, hMerged); + + hGroups.Rewind(); + hGroups.JumpToKey(szGroup); + hGroups.DeleteKey(GROUP_EXTEND_KEY); + + // Only own "" values are left in hMerged (parents are already resolved): they remove the inherited value + char szKey[FEATURE_NAME_LENGTH], szValue[256]; + if (hMerged.GotoFirstSubKey(false)) + { + do + { + hMerged.GetSectionName(SZF(szKey)); + hMerged.GetString(NULL_STRING, SZF(szValue)); + if (szValue[0]) + { + hGroups.SetString(szKey, szValue); + } + else + { + hGroups.DeleteKey(szKey); + } + } + while (hMerged.GotoNextKey(false)); + } + + delete hMerged; + hResolved.PushString(szGroup); +} + +// Plain values only: nested sections are not inherited +static void Configs_CopyGroupValues(KeyValues hGroups, const char[] szGroup, KeyValues hTo) +{ + hGroups.Rewind(); + if (!hGroups.JumpToKey(szGroup) || !hGroups.GotoFirstSubKey(false)) + { + return; + } + + char szKey[FEATURE_NAME_LENGTH], szValue[256]; + do + { + hGroups.GetSectionName(SZF(szKey)); + if (hGroups.GetDataType(NULL_STRING) == KvData_None || strcmp(szKey, GROUP_EXTEND_KEY, false) == 0) + { + continue; + } + + // Floats come back as "%f" (6 decimals), which is enough for feature values + hGroups.GetString(NULL_STRING, SZF(szValue)); + hTo.SetString(szKey, szValue); + } + while (hGroups.GotoNextKey(false)); +} diff --git a/addons/sourcemod/scripting/vip/Storage.sp b/addons/sourcemod/scripting/vip/Storage.sp index 51de1c1..04b5f93 100644 --- a/addons/sourcemod/scripting/vip/Storage.sp +++ b/addons/sourcemod/scripting/vip/Storage.sp @@ -15,11 +15,20 @@ bool Storage_IsClientLoaded(int iClient) void Storage_SetClientValue(int iClient, const char[] szKey, const char[] szValue) { - if (Storage_IsClientLoaded(iClient)) + if (!Storage_IsClientLoaded(iClient)) { - g_hStorage[iClient].SetString(szKey, szValue); - g_bStorageChanged[iClient] = true; + return; } + + char szOldValue[STORAGE_VALUE_LENGTH]; + if (g_hStorage[iClient].GetString(szKey, SZF(szOldValue)) && strcmp(szOldValue, szValue) == 0) + { + return; + } + + g_hStorage[iClient].SetString(szKey, szValue); + g_bStorageChanged[iClient] = true; + CreateForward_OnClientStorageUpdate(iClient, szKey, szValue); } bool Storage_GetClientValue(int iClient, const char[] szKey, char[] szValue, int iMaxLength)