diff --git a/.gitignore b/.gitignore index 01c0c854..acdc4922 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.smx .sourceknight plugins/ +.omc/ diff --git a/AGENTS.md b/AGENTS.md index 83626e2c..6cc29451 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,39 +8,43 @@ - `-E` = warnings are errors. Fix every warning, including deprecation ones. SM 1.10 is the strictest parser (e.g. missing `;`), so code must build there too. - Alt: `sourceknight build` (`sourceknight.yaml`, SM 1.13). It ignores spcomp's exit code — check output for `error`. - Never commit `*.smx`, `plugins/`, `.sourceknight`. - -## Git -- `dev` is the integration branch: branch from `dev`, merge back into `dev`. `master` is for releases. -- LF line endings only (enforced by `.gitattributes`); don't introduce CRLF. +- Git: `dev` is the integration branch: branch from `dev`, merge back into `dev`. `master` is for releases. LF line endings only (`.gitattributes`). ## Layout - `scripting/VIP_Core.sp` — entry point; `#include`s every `vip/*.sp` into ONE compilation unit (order matters: Global → Debug → ... → API → CMD → Clients). New file = add `#include` there. -- Compile-time switches in `VIP_Core.sp`: `DEBUG_MODE`, `USE_ADMINMENU` (gates `AdminMenu.sp` + `vip/adminmenu/*`), `USE_MORE_SERVERS`. +- Compile-time switches in `VIP_Core.sp`: `DEBUG_MODE`, `USE_ADMINMENU` (gates `AdminMenu.sp` + `vip/adminmenu/*`), `USE_MORE_SERVERS`, `USE_CLIENTPREFS` (toggle statuses in cookies vs `vip_storage`; status I/O only via `Features_GetSavedStatus`/`Features_SaveStatus`). - `vip/Global.sp` — all globals, macros (`SZF`, `UID/CID`, `SET_BIT/UNSET_BIT`), client flag bits (`IS_VIP`, `IS_LOADED`, ...), core state bits (`IS_STARTED`, `IS_MySQL`, `IS_LOADING`). - `vip/API.sp` — forwards (`API_SetupForwards`, `CreateForward_*`) and natives (`AskPluginLoad2`, `RegNative(X)` → `VIP_X` bound to `Native_X`). -- `vip/Database.sp` — connection, schema, queries. `UTIL.sp` — shared helpers; check here before writing new ones. -- `vip/Clients.sp` — client load/unload lifecycle, spawn handling. `Features.sp` — feature registry. -- `include/vip_core.inc` — PUBLIC API for third-party modules. -- `data/vip/cfg/*.ini` — runtime configs (groups, info, sort_menu, times). `translations/vip_core.phrases.txt` — all user-facing strings. +- `vip/Database.sp` — connection, schema, queries. `UTIL.sp` — shared helpers; check here before writing new ones. `Clients.sp` — client load/unload lifecycle, spawn. `Features.sp` — feature registry. +- `include/vip_core.inc` — PUBLIC API for third-party modules. `data/vip/cfg/*.ini` — runtime configs (groups, info, sort_menu, times). `translations/vip_core.phrases.txt` — all user-facing strings. ## Public API rules (vip_core.inc) -- Third-party modules depend on it: never change existing native/forward signatures or semantics. Add new params with defaults or add new natives. +- 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`. -- Deprecate via `#pragma deprecated`, don't delete (see `update_modules.md`). ## Database - Config name `vip_core` in databases.cfg → MySQL; otherwise falls back to local SQLite. Every query must work on BOTH. - Schemas differ: MySQL has `sid` column (PK `account_id, sid`), SQLite has no `sid`. Use `g_szSID` (empty on SQLite) when filtering by server. - MySQL uses `utf8mb4`. Escape user strings with `g_hDatabase.Escape`. All queries are async (`g_hDatabase.Query` + callback); check `szError` in callbacks. -- Players are keyed by `account_id` (SteamID AccountID), not userid/client index. +- Players are keyed by `account_id` (SteamID AccountID), not userid/client index. Never store data under `account_id` 0 (unauthorized client). +- `vip/Storage.sp` — per-player key/value cache over `vip_storage` (filtered by `sm_vip_storage_id` on MySQL), loaded on connect, written on disconnect. + +## 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. +- 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. +- File-private state/helpers are `static`. Never pass already formatted text as a format string: `PrintToServer("%s", szMsg)`, not `PrintToServer(szMsg)`. + +## Comments +- Explain *why*, never restate *what* the code does. A comment doesn't excuse unclear code: rename/restructure first; if a clear comment is hard to write, the code is the problem. +- Comment what a reader would trip over: unidiomatic code, SourceMod quirks, ordering constraints, odd-looking bug fixes. Mark unfinished parts `// TODO: `. Link the source of copied code and external refs (SM docs, AlliedModders) where they help. ## Conventions -- `#pragma semicolon 1`, `#pragma newdecls required`. Tabs. Hungarian-ish names: `iClient`, `szName`, `hMenu`, `bFlag`, globals `g_*`, cvars `g_CVAR_*`. - Pass client across async boundaries as userid (`UID`/`CID`), never raw index. - `GLOBAL_TRIE` / `GLOBAL_INFO` are index `[0]` of per-client arrays — slot 0 = global, not a player. - Use methodmaps (`StringMap`, `Cookie`, `KeyValues`, `delete`), not legacy procedural calls (`CreateTrie`, `SetClientCookie`, `CloseHandle`, `Kv*`). - Debug logging via `DBG_*` / `DebugMessage` functions from `Debug.sp` (empty unless `DEBUG_MODE 1`); calls still need a trailing `;`. -- Any new user-facing text → new phrase in `vip_core.phrases.txt` with at least `en` and `ru`. -- Comments and cvar descriptions are mostly Russian; either language is fine, keep the surrounding style. -- Bump `VIP_VERSION` in `VIP_Core.sp` only when asked; changelog lives in `updates.md`. +- English only in code, comments, cvar descriptions, log/error strings and docs. Other languages live only in `translations/*.phrases.txt` and the per-language values of `data/vip/cfg/*.ini`. Any new user-facing text → new phrase in `vip_core.phrases.txt` with at least `en` and `ru`. Bump `VIP_VERSION` in `VIP_Core.sp` only when asked; changelog lives in `updates.md`. diff --git a/README.md b/README.md index 7556f021..6e9d9e6e 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,15 @@ Download latest vesrion [https://github.com/R1KO/VIP-Core/releases](Releases) ## [VIP] Core -RU: Плагин позволяет выдавать игрокам VIP-статус на определенный срок или навсегда. -Учитывая модульную систему плагина вы всегда имеете возможность расширить функционал плагина. -Плагин очень гибкий и удобный в настройке, может работать как с SQLite так и с MySQL базой. - - -[RU: [VIP] Core Официальная тема на HlMod.ru](http://hlmod.ru/resources/vip-core.245/) - -[RU: Документация](https://r1ko.github.io/VIP-Core/) - - -EN: This plugin allows you to give VIP status to players for a specific period or forever. +This plugin allows you to give VIP status to players for a specific period or forever. 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. -[EN: [VIP] Core official theme on HlMod.ru](http://hlmod.ru/resources/vip-core.245/) +[[VIP] Core official thread on HlMod.ru](http://hlmod.ru/resources/vip-core.245/) + +[Documentation](https://r1ko.github.io/VIP-Core/) -[EN: Documentation](https://r1ko.github.io/VIP-Core/) +### Guides +- [Module update guide](docs/update_modules.md) +- [Issue labels](docs/issue_labels.md) +- [Changelog](updates.md) diff --git a/addons/sourcemod/data/vip/cfg/groups.ini b/addons/sourcemod/data/vip/cfg/groups.ini index 5f20d1a3..5778098e 100644 --- a/addons/sourcemod/data/vip/cfg/groups.ini +++ b/addons/sourcemod/data/vip/cfg/groups.ini @@ -1,8 +1,8 @@ "VIP_GROUPS" { - "vip1" // Имя группы | Group name + "vip1" // Group name { - // Опции (основные VIP-модули) | Options (VIP Core Modules) + // Options (VIP Core Modules) } "vip2" diff --git a/addons/sourcemod/data/vip/cfg/info.ini b/addons/sourcemod/data/vip/cfg/info.ini index 88083cc3..15015a4a 100644 --- a/addons/sourcemod/data/vip/cfg/info.ini +++ b/addons/sourcemod/data/vip/cfg/info.ini @@ -1,25 +1,25 @@ "VIP_INFO" { - "connect_info_time" // Сообщение для VIP-игрока при входе на сервер (когда вип на время) | Message for the VIP player when entering the server (when the VIP is on time) + "connect_info_time" // Message for the VIP player when entering the server (timed VIP) { - // {NAME} - Ник игрока | Player Nickname - // {GROUP} - Группа игрока | Player Group - // {TIMELEFT} - Через сколько истекает VIP-статус | How long does it take for VIP status to expire? - // {EXPIRES} - Когда истекает VIP-статус | When VIP status expires + // {NAME} - Player Nickname + // {GROUP} - Player Group + // {TIMELEFT} - Time left until VIP status expires + // {EXPIRES} - When VIP status expires - "chat" // Сообщение в чат | Chat Message + "chat" // Chat Message { "ru" "{DEFAULT}Здравствуйте, {GREEN}{NAME}{DEFAULT}! Вы являетесь {LIGHTGREEN}VIP-игроком{DEFAULT}. Ваш VIP статус истекает {GREEN}{EXPIRES}" "en" "{DEFAULT}Hello, {GREEN}{NAME}{DEFAULT}! You have {LIGHTGREEN}VIP features {DEFAULT}for {GREEN}{EXPIRES}" "pt" "{DEFAULT}Olá, {GREEN}{NAME}{DEFAULT}! Você tem {LIGHTGREEN}status VIP {DEFAULT}para {GREEN}{EXPIRES}" "fr" "{DEFAULT}Bonjour, {GREEN}{NAME}{DEFAULT}! Vous avez un {LIGHTGREEN}statut VIP {DEFAULT}pendant {GREEN}{EXPIRES}" } - "menu" // Сообщение в меню | Menu Message + "menu" // Menu Message { - "time" "12" // Через сколько секунд меню будет закрыто автоматически (0 - пока игрок сам не закроет) | After how many seconds the menu will be closed automatically (0 - until the player closes himself) + "time" "12" // After how many seconds the menu will be closed automatically (0 - until the player closes it) "ru" { - // Пукнты меню | Menu Items + // Menu Items "item" "Вы имеете VIP-статус" "item" "SPACER" "item" "Ник: {NAME}" @@ -56,24 +56,24 @@ } } } - "connect_info_perm" // Сообщение для VIP-игрока при входе на сервер (когда вип навсегда) | A message for the VIP player when entering the server (when the VIP is forever) + "connect_info_perm" // A message for the VIP player when entering the server (when the VIP is forever) { - // {NAME} - Ник игрока | Player Nickname - // {GROUP} - Группа игрока | Player Group + // {NAME} - Player Nickname + // {GROUP} - Player Group - "chat" // Сообщение в чат | Chat Message + "chat" // Chat Message { "ru" "{DEFAULT}Здравствуйте, {GREEN}{NAME}{DEFAULT}! Вы являетесь {LIGHTGREEN}VIP-игроком{DEFAULT}." "en" "{DEFAULT}Hello, {GREEN}{NAME}{DEFAULT}! You have {LIGHTGREEN}VIP features{DEFAULT}." "pt" "{DEFAULT}Olá, {GREEN}{NAME}{DEFAULT}! Você tem {LIGHTGREEN}status VIP{DEFAULT}." "fr" "{DEFAULT}Bonjour, {GREEN}{NAME}{DEFAULT}! Vous avez un {LIGHTGREEN}statut VIP{DEFAULT}." } - "menu" // Сообщение в меню | Menu Message + "menu" // Menu Message { - "time" "30" // Через сколько секунд меню будет закрыто автоматически (0 - пока игрок сам не закроет) | After how many seconds the menu will be closed automatically (0 - until the player closes himself) + "time" "30" // After how many seconds the menu will be closed automatically (0 - until the player closes it) "ru" { - // Пукнты меню | Menu Items + // Menu Items "item" "Вы имеете VIP-статус" "item" "Группа: {GROUP}" } @@ -94,14 +94,14 @@ } } } - "no_access_info" // Сообщение об остутствии VIP-статуса + "no_access_info" // Message shown when the player has no VIP status { - "menu" // Сообщение в меню | Menu Message + "menu" // Menu Message { - "time" "12" // Через сколько секунд меню будет закрыто автоматически (0 - пока игрок сам не закроет) | After how many seconds the menu will be closed automatically (0 - until the player closes himself) + "time" "12" // After how many seconds the menu will be closed automatically (0 - until the player closes it) "ru" { - // Пукнты меню | Menu Items + // Menu Items "item" "У вас нет доступа к этому меню" "item" "Чтобы приобрести VIP-статус" "item" "Oбратитесь к администратору" @@ -126,7 +126,7 @@ } } - "url" // Ссылка, которую нужно открыть | Link to open + "url" // Link to open { "ru" "http://mysite.ru/vip/no_access.php?lang=ru" "en" "http://mysite.ru/vip/no_access.php?lang=en" @@ -134,14 +134,14 @@ "fr" "http://mysite.ru/vip/no_access.php?lang=fr" } } - "expired_info" // Сообщение об истечении VIP-статуса + "expired_info" // Message shown when the VIP status expires { - "menu" // Сообщение в меню | Menu Message + "menu" // Menu Message { - "time" "12" // Через сколько секунд меню будет закрыто автоматически (0 - пока игрок сам не закроет) | After how many seconds the menu will be closed automatically (0 - until the player closes himself) + "time" "12" // After how many seconds the menu will be closed automatically (0 - until the player closes it) "ru" { - // Пукнты меню | Menu Items + // Menu Items "item" "Срок вашего VIP-статуса истек" "item" "Чтобы вновь приобрести VIP-статус" "item" "Oбратитесь к администратору" diff --git a/addons/sourcemod/data/vip/cfg/times.ini b/addons/sourcemod/data/vip/cfg/times.ini index 52fd4b03..01b9c259 100644 --- a/addons/sourcemod/data/vip/cfg/times.ini +++ b/addons/sourcemod/data/vip/cfg/times.ini @@ -1,8 +1,8 @@ "TIMES" { - "0" // Время в секундах | Time in seconds + "0" // Time in seconds { - "ru" "Навсегда" // Отображение в меню | Display in the menu + "ru" "Навсегда" // Display in the menu "en" "Permanent" "pt" "Permanente" "fr" "Permanent" diff --git a/addons/sourcemod/scripting/VIP_Core.sp b/addons/sourcemod/scripting/VIP_Core.sp index d2f9f583..257c6963 100644 --- a/addons/sourcemod/scripting/VIP_Core.sp +++ b/addons/sourcemod/scripting/VIP_Core.sp @@ -7,15 +7,17 @@ #include #if !defined VIP_VERSION -#define VIP_VERSION "3.0.7 R" +#define VIP_VERSION "3.1.0 R" #endif -#define DEBUG_MODE 0 // Режим отладки +#define DEBUG_MODE 0 // Debug mode -#define USE_ADMINMENU 1 // Включение админ-меню для управления VIP +#define USE_ADMINMENU 1 // Admin menu for managing VIPs -#define USE_MORE_SERVERS 1 // Включить/Отключить режим при котором если ID сервера у игрока 0 - то VIP будет работать на всех серверах +#define USE_MORE_SERVERS 1 // A VIP with server ID 0 is active on all servers + +#define USE_CLIENTPREFS 0 // Store VIP feature statuses in cookies (1) or in the vip_storage table (0, old cookies are migrated automatically) public Plugin myinfo = @@ -31,6 +33,7 @@ public Plugin myinfo = #include "vip/Downloads.sp" #include "vip/Colors.sp" #include "vip/UTIL.sp" +#include "vip/Storage.sp" #include "vip/Features.sp" #include "vip/Sounds.sp" #include "vip/Info.sp" @@ -74,6 +77,7 @@ public void OnPluginStart() ITEM_DRAW = hDataPack.Position; delete hDataPack; + API_SetupForwards(); ReadConfigs(); VIPMenu_Setup(); @@ -82,7 +86,6 @@ public void OnPluginStart() #endif Cvars_Setup(); - API_SetupForwards(); HookEvent("player_spawn", Event_PlayerSpawn); HookEvent("player_death", Event_PlayerDeath); diff --git a/addons/sourcemod/scripting/include/vip_core.inc b/addons/sourcemod/scripting/include/vip_core.inc index e35690d2..b143796f 100644 --- a/addons/sourcemod/scripting/include/vip_core.inc +++ b/addons/sourcemod/scripting/include/vip_core.inc @@ -3,132 +3,134 @@ #endif #define _vip_core_included -/* Типы данных VIP-функций */ +#define VIP_CORE_INTERFACE 310 + +/* VIP feature data types */ enum VIP_ValueType { - VIP_NULL = 0, // Нет данных - INT, // Целочисленный тип - FLOAT, // Числа с плавающей точкой (Дробные) - BOOL, // Логический тип (1/0) - STRING // Строчный тип + VIP_NULL = 0, // No data + INT, // Integer type + FLOAT, // Floating-point type + BOOL, // Boolean type (1/0) + STRING // String type } -/* Статусы VIP-функций */ +/* VIP feature statuses */ enum VIP_ToggleState { - DISABLED = 0, // Выключено - ENABLED, // Включено - NO_ACCESS // Нет доступа + DISABLED = 0, // Disabled + ENABLED, // Enabled + NO_ACCESS // No access } -/* Типы VIP-функций */ +/* VIP feature types */ enum VIP_FeatureType { - TOGGLABLE = 0, // Вкл/Выкл в меню - SELECTABLE, // Только нажатие - HIDE // Скрытый + TOGGLABLE = 0, // Toggled on/off in the menu + SELECTABLE, // Press only + HIDE // Hidden } typeset ItemSelectCallback { /** - * Вызывается когда VIP-игрок нажимает на пункт в меню (для типа TOGGLABLE). + * Called when a VIP player selects a menu item (for the TOGGLABLE type). * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param eOldStatus Старое значение статуса. - * @param eNewStatus Новое значение статуса. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param eOldStatus Old status. + * @param eNewStatus New status. * - * @return Plugin_Stop или Plugin_Handled - остановит переключение статуса; - * Plugin_Continue - разрешит переключение статуса без изменений; - * Plugin_Changed - разрешит переключение статуса на eNewStatus. + * @return Plugin_Stop or Plugin_Handled - stops the status change; + * Plugin_Continue - allows the status change as is; + * Plugin_Changed - allows the status change to eNewStatus. */ function Action (int iClient, const char[] szFeature, VIP_ToggleState eOldStatus, VIP_ToggleState &eNewStatus); /** - * Вызывается когда VIP-игрок нажимает на пункт в меню (для типа SELECTABLE). + * Called when a VIP player selects a menu item (for the SELECTABLE type). * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return true - снова откроет игроку VIP-меню; + * @return true - reopens the VIP menu for the player; * false. */ function bool (int iClient, const char[] szFeature); }; /** - * Вызывается когда VIP-игроку отображается пункт в меню. + * Called when a menu item is displayed to a VIP player. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param szDisplay Буфер. - * @param iMaxLength Размер буфера. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param szDisplay Buffer. + * @param iMaxLength Buffer size. * - * @return true - Отображение содержимого szDisplay. - * false - Отображение без изменений. + * @return true - display the contents of szDisplay. + * false - display unchanged. */ typedef ItemDisplayCallback = function bool (int iClient, const char[] szFeature, char[] szDisplay, int iMaxLength); /** - * Вызывается когда VIP-игроку отображается стиль пункта в меню. + * Called when the style of a menu item is displayed to a VIP player. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param iStyle Стиль. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param iStyle Style. * - * @return Необходимый стиль (ITEMDRAW_*). + * @return Required style (ITEMDRAW_*). */ typedef ItemDrawCallback = function int (int iClient, const char[] szFeature, int iStyle); /** - * Вызывается когда игрок возрождается. + * Called when a player spawns. * - * @param iClient Индекс игрока. - * @param iTeam Команда игрока (2 - Т, 3- КТ). - * @param bIsVIP Имеет ли игрок VIP-статус. + * @param iClient Client index. + * @param iTeam Player team (2 - T, 3 - CT). + * @param bIsVIP Whether the player has VIP status. * * @noreturn */ forward void VIP_OnPlayerSpawn(int iClient, int iTeam, bool bIsVIP); /** - * Вызывается когда игру отображается информация. + * Called when information is displayed to a player. * - * @param iClient Индекс игрока. - * @param szEvent Имя события: - * "connect_info_time" - VIP-игрок вошел на сервер (VIP-статус на время); - * "connect_info_perm" - VIP-игрок вошел на сервер (VIP-статус навсегда); - * "no_access_info" - Игрок попытался открыть VIP-меню; - * "connect_info_time" - VIP-статус игрока истек. - * @param szType Тип сообщения: - * "chat" - В чат; - * "menu" - В меню; - * "url" - В MOTD окно. - * @param hKeyValues KeyValues ключа события. + * @param iClient Client index. + * @param szEvent Event name: + * "connect_info_time" - VIP player joined the server (timed VIP status); + * "connect_info_perm" - VIP player joined the server (permanent VIP status); + * "no_access_info" - Player tried to open the VIP menu; + * "connect_info_time" - Player's VIP status expired. + * @param szType Message type: + * "chat" - In chat; + * "menu" - In a menu; + * "url" - In a MOTD window. + * @param hKeyValues KeyValues of the event key. * - * @return Plugin_Stop или Plugin_Handled - запретит показ уведомления; - * Plugin_Changed - разрешит с изменениями; - * Plugin_Continue - разрешит без изменений. + * @return Plugin_Stop or Plugin_Handled - blocks the notification; + * Plugin_Changed - allows it with changes; + * Plugin_Continue - allows it unchanged. */ forward Action VIP_OnShowClientInfo(int iClient, const char[] szEvent, const char[] szType, KeyValues hKeyValues); /** - * Вызывается когда у VIP-игрока изменяется статус функции. + * Called when a feature status changes for a VIP player. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param eOldStatus Старое значение статуса. - * @param eNewStatus Новое значение статуса. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param eOldStatus Old status. + * @param eNewStatus New status. * - * @return Plugin_Stop или Plugin_Handled - остановит переключение статуса; - * Plugin_Continue - разрешит переключение статуса без изменений; - * Plugin_Changed - разрешит переключение статуса на eNewStatus. + * @return Plugin_Stop or Plugin_Handled - stops the status change; + * Plugin_Continue - allows the status change as is; + * Plugin_Changed - allows the status change to eNewStatus. */ forward Action VIP_OnFeatureToggle(int iClient, const char[] szFeature, VIP_ToggleState eOldStatus, VIP_ToggleState &eNewStatus); /** - * Вызывается когда VIP-плагин загрузился. + * Called when the VIP plugin has loaded. * * @noparams * @@ -137,232 +139,300 @@ forward Action VIP_OnFeatureToggle(int iClient, const char[] szFeature, VIP_Togg forward void VIP_OnVIPLoaded(); /** - * Вызывается когда VIP-функция была зарегистрирована. + * Called when the VIP plugin has loaded its settings (on start and on config reload). + * + * @noparams + * + * @noreturn +*/ +forward void VIP_OnConfigsLoaded(); + +/** + * Called when the player's storage has loaded (VIP_SetClientStorageValue / VIP_GetClientStorageValue). * - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * + * @noreturn +*/ +forward void VIP_OnClientStorageLoaded(int iClient); + +/** + * Called when a VIP feature has been registered. + * + * @param szFeature Unique feature name. * * @noreturn */ forward void VIP_OnFeatureRegistered(const char[] szFeature); /** - * Вызывается когда VIP-функция была удалена. + * Called when a VIP feature has been unregistered. * - * @param szFeature Уникальное имя ф-и. + * @param szFeature Unique feature name. * * @noreturn */ forward void VIP_OnFeatureUnregistered(const char[] szFeature); /** - * Вызывается перед проверкой игрока на наличие VIP-статуса. + * Called before a player is checked for VIP status. * - * @param iClient Индекс игрока. + * @param iClient Client index. * - * @return true - продолжить. - * false - не выполнять проверку. + * @return true - continue. + * false - skip the check. */ forward bool VIP_OnClientPreLoad(int iClient); /** - * Вызывается когда игрок был загружен. + * Called when a player has been loaded. * - * @param iClient Индекс игрока. - * @param bIsVIP Имеет ли игрок VIP-статус. + * @param iClient Client index. + * @param bIsVIP Whether the player has VIP status. * * @noreturn */ forward void VIP_OnClientLoaded(int iClient, bool bIsVIP); /** - * Вызывается когда VIP-игрок был загружен. + * Called when a VIP player has been loaded. * - * @param iClient Индекс игрока. + * @param iClient Client index. * * @noreturn */ forward void VIP_OnVIPClientLoaded(int iClient); /** - * Вызывается когда игрок на стадии отключения. + * Called when a player is disconnecting. * - * @param iClient Индекс игрока. - * @param bIsVIP Имеет ли игрок VIP-статус. + * @param iClient Client index. + * @param bIsVIP Whether the player has VIP status. * * @noreturn */ forward void VIP_OnClientDisconnect(int iClient, bool bIsVIP); /** - * Вызывается когда игрок получает VIP-статус. + * Called when a player receives VIP status. * - * @param iClient Индекс игрока. - * @param iAdmin Индекс админа (0 - сервер). + * @param iClient Client index. + * @param iAdmin Admin index (0 - server). * * @noreturn */ forward void VIP_OnVIPClientAdded(int iClient, int iAdmin); /** - * Вызывается когда игрок лишается VIP-статуса. + * Called when a player loses VIP status. * - * @param iClient Индекс игрока. - * @param szReason Причина: - * "Expired" - Истек; - * "Removed by Admin" - Удален админом; - * "Removed by native" - Удален нативом. - * @param iAdmin Индекс админа (0 - сервер). + * @param iClient Client index. + * @param szReason Reason: + * "Expired" - Expired; + * "Removed by Admin" - Removed by an admin; + * "Removed by native" - Removed by a native. + * @param iAdmin Admin index (0 - server). * * @noreturn */ forward void VIP_OnVIPClientRemoved(int iClient, const char[] szReason, int iAdmin); /** - * Запускает проверку игрока на наличие VIP прав. + * Starts checking a player for VIP rights. * - * @param iClient Индекс игрока. - * @param bNotify Известить ли игрока о результате проверки. - * @param bForward Вызывать ли форврад VIP_OnClientPreLoad перед проверкой. + * @param iClient Client index. + * @param bNotify Whether to notify the player of the check result. + * @param bForward Whether to call the VIP_OnClientPreLoad forward before the check. * * @noreturn */ native void VIP_CheckClient(int iClient, bool bNotify = false, bool bForward = true); /** - * Получает наличие VIP прав у игрока. + * Checks whether a player has VIP rights. * - * @param iClient Индекс игрока. + * @param iClient Client index. * - * @return true - У игрока имеются VIP-статус. - * false - У игрока нет VIP-статуса. + * @return true - the player has VIP status. + * false - the player has no VIP status. */ native bool VIP_IsClientVIP(int iClient); /** - * Получает уникальный ID VIP-игрока. + * Gets the unique ID of a VIP player. * - * @param iClient Индекс игрока. + * @param iClient Client index. * - * @return Уникальный ID или -1 если временный. + * @return Unique ID, or -1 if temporary. */ native int VIP_GetClientID(int iClient); /** - * Получает время окончания VIP-прав у игрока (unix). + * Gets the expiration time of a player's VIP rights (unix). * - * @param iClient Индекс игрока. + * @param iClient Client index. * - * @return Дата в unix формате (0 - навсегда, -1 если не удалось получить). + * @return Unix timestamp (0 - permanent, -1 on failure). */ native int VIP_GetClientAccessTime(int iClient); /** - * Устанавливает время окончания VIP-прав у игрока (unix). + * Sets the expiration time of a player's VIP rights (unix). * - * @param iClient Индекс игрока. - * @param iTime Дата в unix формате, 0 если права навсегда, -1 если временный. - * @param bInDB Изменять ли в базе данных. + * @param iClient Client index. + * @param iTime Unix timestamp, 0 if permanent, -1 if temporary. + * @param bInDB Whether to update the database. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_SetClientAccessTime(int iClient, int iTime, bool bInDB = true); /** - * Получает VIP-группу игрока. + * Gets a player's VIP group. * - * @param iClient Индекс игрока. - * @param szGroup Буфер, в который будет помещен результат. - * @param iMaxLength Размер буфера. + * @param iClient Client index. + * @param szGroup Buffer to store the result in. + * @param iMaxLength Buffer size. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_GetClientVIPGroup(int iClient, char[] szGroup, int iMaxLength); /** - * Устанавливает игроку VIP-группу. + * Sets a player's VIP group. * - * @param iClient Индекс игрока. - * @param szGroup VIP-группа. - * @param bInDBB Изменять ли в базе данных. + * @param iClient Client index. + * @param szGroup VIP group. + * @param bInDBB Whether to update the database. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_SetClientVIPGroup(int iClient, const char[] szGroup, bool bInDB = true); /** - * Получает Handle adt trie настроек VIP-игрока. + * Gets the adt trie handle of a VIP player's settings. * - * @param iClient Индекс игрока. + * @param iClient Client index. * - * @return StringMap (Handle) adt trie настроек VIP-игрока. + * @return StringMap (Handle) adt trie of the VIP player's settings. */ native StringMap VIP_GetVIPClientTrie(int iClient); /** - * Проверяет наличие VIP-группы. + * Checks whether a group exists. * - * @param szGroup Имя VIP-группы. + * @param szGroup Group name. * - * @return true - VIP-группа существует. - * false - VIP-группа не существует. + * @return true - the group exists. + * false - the group does not exist. */ +native bool VIP_IsGroupExists(const char[] szGroup); + +#pragma deprecated Use VIP_IsGroupExists() instead native bool VIP_IsValidVIPGroup(const char[] szGroup); /** - * Выводит игроку сообщение в чат с приставкой (фраза в переводе "VIP_CHAT_PREFIX"). + * Adds a group (in memory only, groups.ini is not modified). + * Groups are reset when configs are reloaded; add them again in VIP_OnConfigsLoaded(). + * + * @param szGroup Group name. + * @param hFeatures Group parameters (keys from the current position are copied into the group). + * + * @return true - on success. + * false - the group already exists. +*/ +native bool VIP_AddGroup(const char[] szGroup, KeyValues hFeatures); + +/** + * Removes a group (in memory only, groups.ini is not modified). + * + * @param szGroup Group name. + * + * @return true - on success. + * false - the group does not exist. +*/ +native bool VIP_RemoveGroup(const char[] szGroup); + +/** + * Saves a value to the player's storage (vip_storage table). + * The storage is available for all players (not only VIP) after VIP_OnClientStorageLoaded(); + * before that, the call is ignored. It is written to the database when the player disconnects. + * + * @param iClient Client index. + * @param szKey Key (up to 127 characters). + * @param szValue Value (up to 255 characters). + * + * @noreturn +*/ +native void VIP_SetClientStorageValue(int iClient, const char[] szKey, const char[] szValue); + +/** + * Gets a value from the player's storage. + * + * @param iClient Client index. + * @param szKey Key. + * @param szValue Buffer to store the result in (empty string if there is no value). + * @param iMaxLength Buffer size. + * + * @noreturn +*/ +native void VIP_GetClientStorageValue(int iClient, const char[] szKey, char[] szValue, int iMaxLength); + +/** + * Prints a chat message to a player with a prefix (translation phrase "VIP_CHAT_PREFIX"). * - * @param iClient Индекс игрока. - * @param szMessage Правила форматирования сообщение. - * @param ... Параметры форматирования. + * @param iClient Client index. + * @param szMessage Formatting rules. + * @param ... Formatting parameters. * * @noreturn */ native void VIP_PrintToChatClient(int iClient, const char[] szMessage, any ...); /** - * Выводит всем игрокам сообщение в чат с приставкой (фраза в переводе "VIP_CHAT_PREFIX"). + * Prints a chat message to all players with a prefix (translation phrase "VIP_CHAT_PREFIX"). * - * @param szMessage Правила форматирования сообщение. - * @param ... Параметры форматирования. + * @param szMessage Formatting rules. + * @param ... Formatting parameters. * * @noreturn */ native void VIP_PrintToChatAll(const char[] szMessage, any ...); /** - * Записывает сообщение в лог. + * Writes a message to the log. * - * @param szMessage Правила форматирования сообщение. - * @param ... Параметры форматирования. + * @param szMessage Formatting rules. + * @param ... Formatting parameters. * * @noreturn */ native void VIP_LogMessage(const char[] szMessage, any ...); /** - * Отправляет игроку VIP-меню. + * Opens the VIP menu for a player. * - * @param iClient Индекс игрока. - * @param bSelection Режим открытия: - * true - на первой странице; - * false - на странице последнего выбранного пункта. + * @param iClient Client index. + * @param bSelection Open mode: + * true - on the first page; + * false - on the page of the last selected item. * * @noreturn */ native void VIP_SendClientVIPMenu(int iClient, bool bSelection = false); /** - * Выдает игроку VIP-права. + * Gives VIP rights to a player. * - * @param iAdmin Индекс админа (0 - сервер, -1 - плагин). - * @param iClient Индекс игрока. - * @param iTime Время в секундах. - * @param szGroup Имя VIP-группы. - * @param bAddToDB Добавлять ли в базу данных. + * @param iAdmin Admin index (0 - server, -1 or any negative - plugin). + * @param iClient Client index. + * @param iTime Time in seconds. + * @param szGroup VIP group name. + * @param bAddToDB Whether to add to the database. * * @noreturn */ @@ -372,15 +442,15 @@ native void VIP_GiveClientVIP(int iAdmin = 0, int iClient, int iTime, const char native bool VIP_SetClientVIP(int iClient, int iTime, any AuthType, const char[] szGroup, bool bAddToDB = true); /** - * Отнимает у игрока VIP-права. + * Removes VIP rights from a player. * - * @param iAdmin Индекс админа (0 - сервер, -1 - плагин). - * @param iClient Индекс игрока. - * @param bInDB Удалять ли из базы данных. - * @param bNotify Уведомление игрока (true - уведомить/false - не уведомлять). + * @param iAdmin Admin index (0 - server, -1 or any negative - plugin). + * @param iClient Client index. + * @param bInDB Whether to remove from the database. + * @param bNotify Player notification (true - notify / false - do not notify). * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_RemoveClientVIP2(int iAdmin = 0, int iClient, bool bInDB, bool bNotify); @@ -388,29 +458,29 @@ native bool VIP_RemoveClientVIP2(int iAdmin = 0, int iClient, bool bInDB, bool b native bool VIP_RemoveClientVIP(int iClient, bool bInDB, bool bNotify); /** - * Проверка что ядро загружено и готово к регистрации функций. + * Checks that the core is loaded and ready for feature registration. * * @noparams * - * @return true - Ядро загружено. - * false - Ядро не загружено. + * @return true - the core is loaded. + * false - the core is not loaded. */ native bool VIP_IsVIPLoaded(); /** - * Добавляет новую VIP-функцию. + * Registers a new VIP feature. * - * @param szFeature Уникальное имя ф-и. - * @param ValType Тип данных ф-и. - * @param FeatureType Тип ф-и. - * @param Item_select_callback Обратный вызов при смене статуса ф-и. - * @param Item_display_callback Обратный вызов при отображении пункта ф-и . - * @param Item_draw_callback Обратный вызов при отображении стиля пункта ф-и. - * @param eDefStatus Значение по-умолчанию: - * ENABLED - Включена - * DISABLED - Отключена - * NO_ACCESS - Не использовать (будет зависеть от sm_vip_features_default_status) - * @param bCookie Регистрировать ли куки для ф-и (действительно только для типа SELECTABLE). + * @param szFeature Unique feature name. + * @param ValType Feature data type. + * @param FeatureType Feature type. + * @param Item_select_callback Callback for a feature status change. + * @param Item_display_callback Callback for displaying the feature menu item. + * @param Item_draw_callback Callback for displaying the feature menu item style. + * @param eDefStatus Default status: + * ENABLED - Enabled + * DISABLED - Disabled + * NO_ACCESS - Not set (depends on sm_vip_features_default_status) + * @param bCookie Whether to register a cookie for the feature (valid only for the SELECTABLE type). * * @noreturn */ @@ -425,16 +495,16 @@ native void VIP_RegisterFeature(const char[] szFeature, bool bCookie = false); /** - * Удаляет VIP-функцию. + * Unregisters a VIP feature. * - * @param szFeature Уникальное имя ф-и. + * @param szFeature Unique feature name. * * @noreturn */ native void VIP_UnregisterFeature(const char[] szFeature); /** - * Удаляет все VIP-функции зарегистрированные плагином (вызывать в OnPluginEnd()). + * Unregisters all VIP features registered by the plugin (call in OnPluginEnd()). * * @noparams * @@ -443,153 +513,153 @@ native void VIP_UnregisterFeature(const char[] szFeature); native void VIP_UnregisterMe(); /** - * Проверяет существование функции. + * Checks whether a feature exists. * - * @param szFeature Уникальное имя ф-и. + * @param szFeature Unique feature name. * - * @return true - VIP-функция существует. - * false - VIP-функция не существует. + * @return true - the VIP feature exists. + * false - the VIP feature does not exist. */ native bool VIP_IsValidFeature(const char[] szFeature); /** - * Получает тип функции. + * Gets the feature type. * - * @param szFeature Уникальное имя ф-и. + * @param szFeature Unique feature name. * - * @return Возвращает элемент списка VIP_FeatureType. + * @return A VIP_FeatureType value. */ native VIP_FeatureType VIP_GetFeatureType(const char[] szFeature); /** - * Получает тип значения функции. + * Gets the feature value type. * - * @param szFeature Уникальное имя ф-и. + * @param szFeature Unique feature name. * - * @return Возвращает элемент списка VIP_ValueType. + * @return A VIP_ValueType value. */ native VIP_ValueType VIP_GetFeatureValueType(const char[] szFeature); /** - * Заполняет adt массив ф-ями. + * Fills an adt array with feature names. * - * @param hArray Массив типа ArrayList. + * @param hArray ArrayList array. * - * @return Количество добавленных элементов. + * @return Number of added elements. */ native int VIP_FillArrayByFeatures(ArrayList hArray); /** - * Возвращает статус VIP-функции у игрока. + * Gets a player's VIP feature status. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return Возвращает элемент списка VIP_ToggleState. + * @return A VIP_ToggleState value. */ native VIP_ToggleState VIP_GetClientFeatureStatus(int iClient, const char[] szFeature); /** - * Устанавливает статус VIP-функции игроку. + * Sets a player's VIP feature status. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param eStatus Статус VIP-функции. - * @param bCallback Вызывать ли toggle каллбэк. - * @param bSave Сохранить ли в куки. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param eStatus VIP feature status. + * @param bCallback Whether to call the toggle callback. + * @param bSave Whether to save to the cookie. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_SetClientFeatureStatus(int iClient, const char[] szFeature, VIP_ToggleState eStatus, bool bCallback = true, bool bSave = false); /** - * Возвращает статус VIP-функции у игрока. + * Checks whether a player's VIP feature is enabled. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return true - VIP-функции включена. - * false - VIP-функции выключена или нет доступа. + * @return true - the VIP feature is enabled. + * false - the VIP feature is disabled or the player has no access. */ native bool VIP_IsClientFeatureUse(int iClient, const char[] szFeature); /** - * Получает целочисленное значение параметра VIP-функции у игрока. + * Gets the integer value of a player's VIP feature parameter. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return Возвращает целочисленное значение параметра VIP-функции у игрока. + * @return Integer value of the player's VIP feature parameter. */ native int VIP_GetClientFeatureInt(int iClient, const char[] szFeature); /** - * Получает дробное значение параметра VIP-функции у игрока. + * Gets the float value of a player's VIP feature parameter. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return Возвращает дробное значение параметра VIP-функции у игрока. + * @return Float value of the player's VIP feature parameter. */ native float VIP_GetClientFeatureFloat(int iClient, const char[] szFeature); /** - * Получает логическое значение параметра VIP-функции у игрока. + * Gets the boolean value of a player's VIP feature parameter. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return Возвращает логическое значение параметра VIP-функции у игрока (true/false). + * @return Boolean value of the player's VIP feature parameter (true/false). */ native bool VIP_GetClientFeatureBool(int iClient, const char[] szFeature); /** - * Получает строчное значение параметра VIP-функции у игрока. + * Gets the string value of a player's VIP feature parameter. * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param szBuffer Буфер, в который будет помещен результат. - * @param iMaxLength Размер буфера. + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param szBuffer Buffer to store the result in. + * @param iMaxLength Buffer size. * * @noreturn */ native void VIP_GetClientFeatureString(int iClient, const char[] szFeature, char[] szBuffer, int iMaxLength); /** - * Выдает игроку VIP-функцию и устанавливает её значение (на время сессии). + * Gives a VIP feature to a player and sets its value (for the session). * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. - * @param szValue Значение (автоматически будет приведено к типу ф-и). + * @param iClient Client index. + * @param szFeature Unique feature name. + * @param szValue Value (automatically converted to the feature type). * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_GiveClientFeature(int iClient, const char[] szFeature, const char[] szValue); /** - * Отнимает у VIP-игрока VIP-функцию (на время сессии). + * Removes a VIP feature from a VIP player (for the session). * - * @param iClient Индекс игрока. - * @param szFeature Уникальное имя ф-и. + * @param iClient Client index. + * @param szFeature Unique feature name. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_RemoveClientFeature(int iClient, const char[] szFeature); /** - * Получает Handle базы данных. После работы необходимо закрыть с помощью CloseHandle() или delete + * Gets the database handle. After use, it must be closed with CloseHandle() or delete * * @noparams * - * @return Database (Handle) базы данных + * @return Database (Handle), or null if the core has not connected to the database yet (before VIP_OnVIPLoaded) */ native Database VIP_GetDatabase(); /** - * Получает тип базы данных. + * Gets the database type. * * @noparams * @@ -599,44 +669,45 @@ native Database VIP_GetDatabase(); native bool VIP_GetDatabaseType(); /** - * Получает время в секундах в зависимости от sm_vip_time_mode. + * Converts a time value to seconds according to sm_vip_time_mode. * - * @param iTime Время. + * @param iTime Time. * - * @return Возвращает количество секунд в зависимости от sm_vip_time_mode. + * @return Number of seconds according to sm_vip_time_mode. + * -1 - unknown sm_vip_time_mode, or the result does not fit into an int. */ native int VIP_TimeToSeconds(int iTime); /** - * Получает время в зависимости от sm_vip_time_mode. + * Converts seconds to a time value according to sm_vip_time_mode. * - * @param iTime Количество секунд. + * @param iTime Number of seconds. * - * @return Возвращает время в зависимости от sm_vip_time_mode. + * @return Time value according to sm_vip_time_mode. */ native int VIP_SecondsToTime(int iTime); /** - * Переводит секунды в человекопонятный вид. + * Converts seconds to a human-readable string. * - * @param szBuffer Буфер, в который будет помещен результат. - * @param iMaxLength Размер буфера. - * @param iTimeStamp Кол-во секунд. - * @param iClient Индекс игрока или LANG_SERVER. + * @param szBuffer Buffer to store the result in. + * @param iMaxLength Buffer size. + * @param iTimeStamp Number of seconds. + * @param iClient Client index or LANG_SERVER. * - * @return true - Успешно. - * false - Не удалось выполнить. + * @return true - on success. + * false - on failure. */ native bool VIP_GetTimeFromStamp(char[] szBuffer, int iMaxLength, int iTimeStamp, int iClient = LANG_SERVER); /** - * Добавляет к строке статус ф-и. + * Appends the feature status to a string. * - * @param szInput Исходная строка. - * @param szBuffer Буфер, в который будет помещен результат. - * @param iMaxLength Размер буфера. - * @param szFeature Уникальное имя ф-и. - * @param iClient Индекс игрока. + * @param szInput Source string. + * @param szBuffer Buffer to store the result in. + * @param iMaxLength Buffer size. + * @param szFeature Unique feature name. + * @param iClient Client index. * * @noreturn */ @@ -679,6 +750,11 @@ public void __pl_vip_core_SetNTVOptional() MarkNativeAsOptional("VIP_GetVIPClientTrie"); MarkNativeAsOptional("VIP_SendClientVIPMenu"); MarkNativeAsOptional("VIP_IsValidVIPGroup"); + MarkNativeAsOptional("VIP_IsGroupExists"); + MarkNativeAsOptional("VIP_AddGroup"); + MarkNativeAsOptional("VIP_RemoveGroup"); + MarkNativeAsOptional("VIP_SetClientStorageValue"); + MarkNativeAsOptional("VIP_GetClientStorageValue"); MarkNativeAsOptional("VIP_GetClientFeatureStatus"); MarkNativeAsOptional("VIP_SetClientFeatureStatus"); MarkNativeAsOptional("VIP_IsClientFeatureUse"); diff --git a/addons/sourcemod/scripting/vip/API.sp b/addons/sourcemod/scripting/vip/API.sp index 65125233..45dfb064 100644 --- a/addons/sourcemod/scripting/vip/API.sp +++ b/addons/sourcemod/scripting/vip/API.sp @@ -10,6 +10,8 @@ static GlobalForward g_hGlobalForward_OnFeatureToggle; static GlobalForward g_hGlobalForward_OnFeatureRegistered; static GlobalForward g_hGlobalForward_OnFeatureUnregistered; static GlobalForward g_hGlobalForward_OnClientDisconnect; +static GlobalForward g_hGlobalForward_OnConfigsLoaded; +static GlobalForward g_hGlobalForward_OnClientStorageLoaded; void API_SetupForwards() { @@ -26,6 +28,8 @@ void API_SetupForwards() g_hGlobalForward_OnFeatureRegistered = new GlobalForward("VIP_OnFeatureRegistered", ET_Ignore, Param_String); g_hGlobalForward_OnFeatureUnregistered = new GlobalForward("VIP_OnFeatureUnregistered", ET_Ignore, Param_String); 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); } // Global Forwards @@ -36,6 +40,21 @@ void CreateForward_OnVIPLoaded() Call_Finish(); } +void CreateForward_OnConfigsLoaded() +{ + DBG_API("CreateForward_OnConfigsLoaded()"); + Call_StartForward(g_hGlobalForward_OnConfigsLoaded); + Call_Finish(); +} + +void CreateForward_OnClientStorageLoaded(int iClient) +{ + DBG_API("CreateForward_OnClientStorageLoaded(%N (%d))", iClient, iClient); + Call_StartForward(g_hGlobalForward_OnClientStorageLoaded); + Call_PushCell(iClient); + Call_Finish(); +} + bool CreateForward_OnClientPreLoad(int iClient) { DBG_API("g_hGlobalForward_OnClientPreLoad(%N (%d), %b)", iClient, iClient, g_iClientInfo[iClient] & IS_VIP); @@ -249,6 +268,12 @@ public APLRes AskPluginLoad2(Handle myself, bool bLate, char[] szError, int err_ RegNative(SendClientVIPMenu); RegNative(IsValidVIPGroup); + RegNative(IsGroupExists); + RegNative(AddGroup); + RegNative(RemoveGroup); + + RegNative(SetClientStorageValue); + RegNative(GetClientStorageValue); RegNative(GetClientFeatureStatus); RegNative(SetClientFeatureStatus); @@ -360,7 +385,7 @@ public int Native_LogMessage(Handle hPlugin, int iNumParams) SetGlobalTransTarget(LANG_SERVER); FormatNativeString(0, 1, 2, sizeof(szMessage), _, szMessage); - LogToFile(g_szLogFile, szMessage); + LogToFile(g_szLogFile, "%s", szMessage); } return 0; @@ -414,6 +439,8 @@ public int Native_SetClientVIPGroup(Handle hPlugin, int iNumParams) GetNativeString(2, SZF(szGroup)); if (UTIL_CheckValidVIPGroup(szGroup)) { + char szOldGroup[64]; + g_hFeatures[iClient].GetString(KEY_GROUP, SZF(szOldGroup)); if (g_hFeatures[iClient].SetString(KEY_GROUP, szGroup)) { if (view_as(GetNativeCell(3))) @@ -428,12 +455,10 @@ public int Native_SetClientVIPGroup(Handle hPlugin, int iNumParams) if (g_CVAR_bLogsEnable) { - char szName[MNL], szAdmin[128], szPluginName[128], szOldGroup[64]; + char szName[MNL], szAdmin[128], szPluginName[128]; GetClientName(iClient, SZF(szName)); GetPluginInfo(hPlugin, PlInfo_Name, SZF(szPluginName)); FormatEx(SZF(szAdmin), "%T %s", "BY_PLUGIN", LANG_SERVER, szPluginName); - g_hFeatures[iClient].GetString(KEY_GROUP, SZF(szOldGroup)); - g_hFeatures[iClient].GetValue(KEY_CID, iClientID); LogToFile(g_szLogFile, "%T", "LOG_CHANGE_GROUP", LANG_SERVER, szName, iClientID, szOldGroup, szGroup, szAdmin); } } @@ -444,7 +469,7 @@ public int Native_SetClientVIPGroup(Handle hPlugin, int iNumParams) } else { - ThrowNativeError(SP_ERROR_NATIVE, "Invalid group/Некорректная VIP-группа (%s)", szGroup); + ThrowNativeError(SP_ERROR_NATIVE, "Invalid VIP group (%s)", szGroup); } } return false; @@ -474,7 +499,7 @@ public int Native_SetClientAccessTime(Handle hPlugin, int iNumParams) if (iTime < 0 || (iTime != 0 && iTime < GetTime())) { - ThrowNativeError(SP_ERROR_NATIVE, "Invalid time/Некорректное время (%i)", iTime); + ThrowNativeError(SP_ERROR_NATIVE, "Invalid time (%i)", iTime); return false; } @@ -563,7 +588,7 @@ public int Native_GiveClientVIP(Handle hPlugin, int iNumParams) char szGroup[64]; GetNativeString(4, SZF(szGroup)); - return API_GiveClientVIP(hPlugin, iAdmin, iClient, iTime, szGroup, bAddToDB); + return API_GiveClientVIP(hPlugin, API_GetAdminOrPlugin(iAdmin), iClient, iTime, szGroup, bAddToDB); } public int Native_SetClientVIP(Handle hPlugin, int iNumParams) @@ -588,23 +613,23 @@ int API_GiveClientVIP(Handle hPlugin, int iAdmin, int iClient, int iTime, const g_hFeatures[iClient].GetValue(KEY_CID, iClientID); if (iClientID == -1 && bAddToDB) { - ResetClient(iClient); + Clients_RemoveVIPStatus(iClient); CreateForward_OnVIPClientRemoved(iClient, "Removed for VIP-status change", iAdmin); } else { - return ThrowNativeError(SP_ERROR_NATIVE, "The player %L is already a VIP/Игрок %L уже является VIP-игроком", iClient, iClient); + return ThrowNativeError(SP_ERROR_NATIVE, "The player %L is already a VIP", iClient); } } if (!UTIL_CheckValidVIPGroup(szGroup)) { - return ThrowNativeError(SP_ERROR_NATIVE, "Invalid VIP-group/Некорректная VIP-группа (%s)", szGroup); + return ThrowNativeError(SP_ERROR_NATIVE, "Invalid VIP group (%s)", szGroup); } if (iTime < 0) { - return ThrowNativeError(SP_ERROR_NATIVE, "Invalid time/Некорректное время (%d)", iTime); + return ThrowNativeError(SP_ERROR_NATIVE, "Invalid time (%d)", iTime); } if (bAddToDB) @@ -661,7 +686,13 @@ public int Native_RemoveClientVIP2(Handle hPlugin, int iNumParams) bool bInDB = GetNativeCell(3); bool bNotify = GetNativeCell(4); - return API_RemoveClientVIP(hPlugin, iAdmin, iClient, bInDB, bNotify); + return API_RemoveClientVIP(hPlugin, API_GetAdminOrPlugin(iAdmin), iClient, bInDB, bNotify); +} + +// The .inc documents -1 as "plugin" while the core uses REASON_PLUGIN (-2): accept any negative value +int API_GetAdminOrPlugin(int iAdmin) +{ + return iAdmin < 0 ? REASON_PLUGIN : iAdmin; } int API_RemoveClientVIP(Handle hPlugin, @@ -672,7 +703,7 @@ int API_RemoveClientVIP(Handle hPlugin, { if (CheckValidClient(iClient)) { - if (iAdmin) + if (iAdmin > 0) { CheckValidClient(iAdmin, false); } @@ -693,7 +724,7 @@ int API_RemoveClientVIP(Handle hPlugin, CancelClientMenu(iClient); } - ResetClient(iClient); + Clients_RemoveVIPStatus(iClient); char szBuffer[PMP]; FormatEx(SZF(szBuffer), "Removed by %s", szPluginName); @@ -717,6 +748,85 @@ public int Native_IsValidVIPGroup(Handle hPlugin, int iNumParams) return UTIL_CheckValidVIPGroup(szGroup); } +public int Native_IsGroupExists(Handle hPlugin, int iNumParams) +{ + char szGroup[64]; + GetNativeString(1, SZF(szGroup)); + return UTIL_CheckValidVIPGroup(szGroup); +} + +public int Native_AddGroup(Handle hPlugin, int iNumParams) +{ + char szGroup[64]; + GetNativeString(1, SZF(szGroup)); + + KeyValues hFeatures = view_as(GetNativeCell(2)); + if (!hFeatures) + { + return ThrowNativeError(SP_ERROR_NATIVE, "Invalid KeyValues handle for group \"%s\"", szGroup); + } + + if (!szGroup[0] || UTIL_CheckValidVIPGroup(szGroup)) + { + return false; + } + + g_hGroups.Rewind(); + g_hGroups.JumpToKey(szGroup, true); + g_hGroups.Import(hFeatures); + g_hGroups.Rewind(); + + return true; +} + +public int Native_RemoveGroup(Handle hPlugin, int iNumParams) +{ + char szGroup[64]; + GetNativeString(1, SZF(szGroup)); + + if (!UTIL_CheckValidVIPGroup(szGroup)) + { + return false; + } + + g_hGroups.DeleteThis(); + g_hGroups.Rewind(); + + return true; +} + +public int Native_SetClientStorageValue(Handle hPlugin, int iNumParams) +{ + int iClient = GetNativeCell(1); + if (!CheckValidClient(iClient, false)) + { + return 0; + } + + char szKey[STORAGE_KEY_LENGTH], szValue[STORAGE_VALUE_LENGTH]; + GetNativeString(2, SZF(szKey)); + GetNativeString(3, SZF(szValue)); + + Storage_SetClientValue(iClient, szKey, szValue); + return 0; +} + +public int Native_GetClientStorageValue(Handle hPlugin, int iNumParams) +{ + int iClient = GetNativeCell(1); + if (!CheckValidClient(iClient, false)) + { + return 0; + } + + char szKey[STORAGE_KEY_LENGTH], szValue[STORAGE_VALUE_LENGTH]; + GetNativeString(2, SZF(szKey)); + + Storage_GetClientValue(iClient, szKey, SZF(szValue)); + SetNativeString(3, szValue, GetNativeCell(4), true); + return 0; +} + public int Native_IsVIPLoaded(Handle hPlugin, int iNumParams) { return ((GLOBAL_INFO & IS_STARTED) && g_hDatabase); @@ -740,14 +850,16 @@ public int Native_RegisterFeature(Handle hPlugin, int iNumParams) g_hVIPMenu.RemoveItem(0); } - g_hFeaturesArray.PushString(szFeature); VIP_ValueType eValType = view_as(GetNativeCell(2)); VIP_FeatureType eType = view_as(GetNativeCell(3)); Function ItemSelectCallback = GetNativeFunction(4); Function ItemDisplayCallback = GetNativeFunction(5); Function ItemDrawCallback = GetNativeFunction(6); - VIP_ToggleState eDefStatus = view_as(GetNativeCell(7)); - bool bCookie = view_as(GetNativeCell(8)); + // Modules built against an older include pass fewer params + VIP_ToggleState eDefStatus = iNumParams > 6 ? view_as(GetNativeCell(7)) : NO_ACCESS; + bool bCookie = iNumParams > 7 && view_as(GetNativeCell(8)); + + g_hFeaturesArray.PushString(szFeature); DBG_API("PushArrayString -> %i", g_hFeaturesArray.FindString(szFeature)); DBG_API("VIP_ValueType -> %s", g_szValueType[view_as(eValType)]); @@ -768,7 +880,7 @@ public int Native_RegisterFeature(Handle hPlugin, int iNumParams) if (eType != HIDE) { Cookie hCookie = null; - bool bRegisterCookie = (eType == SELECTABLE && iNumParams > 7 && (bCookie != false)); + bool bRegisterCookie = (eType == SELECTABLE && bCookie); if (eType == TOGGLABLE || bRegisterCookie) { hCookie = new Cookie(szFeature, szFeature, CookieAccess_Private); @@ -784,7 +896,7 @@ public int Native_RegisterFeature(Handle hPlugin, int iNumParams) if (eType == TOGGLABLE) { - hArray.Push(iNumParams > 6 ? eDefStatus : NO_ACCESS); + hArray.Push(eDefStatus); } AddFeatureToVIPMenu(szFeature); @@ -830,7 +942,7 @@ public int Native_UnregisterFeature(Handle hPlugin, int iNumParams) } else { - ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); } return 0; @@ -870,13 +982,10 @@ public int Native_UnregisterMe(Handle hPlugin, int iNumParams) void UnregisterFeature(const char[] szFeature, ArrayList hArray) { VIP_FeatureType eType = view_as(hArray.Get(FEATURES_ITEM_TYPE)); - if (eType == TOGGLABLE) - { - delete view_as(hArray.Get(FEATURES_COOKIE)); - } - if (eType != HIDE) { + // TOGGLABLE and SELECTABLE (bCookie) features own a cookie handle, others store null + delete view_as(hArray.Get(FEATURES_COOKIE)); delete view_as(hArray.Get(FEATURES_MENU_CALLBACKS)); } @@ -935,7 +1044,7 @@ public int Native_GetFeatureType(Handle hPlugin, int iNumParams) return hArray.Get(FEATURES_ITEM_TYPE); } - return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); } public int Native_GetFeatureValueType(Handle hPlugin, int iNumParams) @@ -949,7 +1058,7 @@ public int Native_GetFeatureValueType(Handle hPlugin, int iNumParams) return hArray.Get(FEATURES_VALUE_TYPE); } - return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); } public int Native_FillArrayByFeatures(Handle hPlugin, int iNumParams) @@ -1032,8 +1141,7 @@ public int Native_SetClientFeatureStatus(Handle hPlugin, int iNumParams) Features_SetStatus(iClient, szFeature, eNewStatus); if (iNumParams > 4 && GetNativeCell(5)) { - IntToString(view_as(eNewStatus), SZF(szFeature)); - view_as(hArray.Get(FEATURES_COOKIE)).Set(iClient, szFeature); + Features_SaveStatus(iClient, szFeature, hArray, eNewStatus); } return true; } @@ -1088,7 +1196,8 @@ public int Native_GetClientFeatureBool(Handle hPlugin, int iNumParams) bool bValue; GetNativeString(2, SZF(szFeature)); - return g_hFeatures[iClient].GetValue(szFeature, bValue); + // VIP_GiveClientFeature can store false, so the key existing is not enough + return g_hFeatures[iClient].GetValue(szFeature, bValue) && bValue; } return false; @@ -1124,7 +1233,7 @@ public int Native_GiveClientFeature(Handle hPlugin, int iNumParams) ArrayList hArray; if (!IsValidFeature(szFeature) || !GLOBAL_TRIE.GetValue(szFeature, hArray)) { - ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); return 0; } @@ -1189,7 +1298,7 @@ public int Native_RemoveClientFeature(Handle hPlugin, int iNumParams) ArrayList hArray; if (!IsValidFeature(szFeature) || !GLOBAL_TRIE.GetValue(szFeature, hArray)) { - ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); return 0; } @@ -1224,6 +1333,12 @@ public int Native_RemoveClientFeature(Handle hPlugin, int iNumParams) public int Native_GetDatabase(Handle hPlugin, int iNumParams) { + // Not connected yet (before VIP_OnVIPLoaded): CloneHandle would throw on null + if (!g_hDatabase) + { + return 0; + } + return view_as(CloneHandle(g_hDatabase, hPlugin)); } @@ -1266,7 +1381,7 @@ public int Native_AddStringToggleStatus(Handle hPlugin, int iNumParams) GetNativeString(4, SZF(szFeature)); if (!IsValidFeature(szFeature)) { - return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid/Функция \"%s\" не существует", szFeature, szFeature); + return ThrowNativeError(SP_ERROR_NATIVE, "Feature \"%s\" is invalid", szFeature); } int iClient = GetNativeCell(5); @@ -1286,12 +1401,12 @@ bool CheckValidClient(const int &iClient, bool bCheckVIP = true) { if (iClient < 1 || iClient > MaxClients) { - ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index/Некорректный индекс игрока (%i)", iClient); + ThrowNativeError(SP_ERROR_NATIVE, "Invalid client index (%i)", iClient); return false; } if (IsClientInGame(iClient) == false) { - ThrowNativeError(SP_ERROR_NATIVE, "Client %i is not connected/Игрок %i не подключен", iClient, iClient); + ThrowNativeError(SP_ERROR_NATIVE, "Client %i is not connected", iClient); return false; } if (bCheckVIP) diff --git a/addons/sourcemod/scripting/vip/AdminMenu.sp b/addons/sourcemod/scripting/vip/AdminMenu.sp index d9b59dbf..f2906302 100644 --- a/addons/sourcemod/scripting/vip/AdminMenu.sp +++ b/addons/sourcemod/scripting/vip/AdminMenu.sp @@ -75,7 +75,10 @@ int IsClientOnline(int ID) int iClientID; for (int i = 1; i <= MaxClients; ++i) { - if (IsClientInGame(i) && g_hFeatures[i] != null && g_hFeatures[i].GetValue(KEY_CID, iClientID) && iClientID == ID) return i; + if (IsClientInGame(i) && g_hFeatures[i] != null && g_hFeatures[i].GetValue(KEY_CID, iClientID) && iClientID == ID) + { + return i; + } } return 0; } @@ -307,7 +310,10 @@ void ShowTimeMenu(int iClient) { hKv.GetSectionName(SZF(szTime)); - if (iMenuType != TIME_SET && szTime[0] == '0') continue; + if (iMenuType != TIME_SET && szTime[0] == '0') + { + continue; + } hKv.GetString(szClientLang, SZF(szBuffer)); if (!szBuffer[0]) @@ -542,12 +548,18 @@ public int MenuHandler_GroupsList(Menu hMenu, MenuAction action, int iClient, in if (iTarget) { g_hClientData[iClient].GetValue(DATA_KEY_Time, iBuffer); + + // Keep the menu to return to (BackToAdminMenu below) + AdminMenuType eThrowMenuType; + g_hClientData[iClient].GetValue(DATA_KEY_ThrowMenuType, eThrowMenuType); g_hClientData[iClient].Clear(); + g_hClientData[iClient].SetValue(DATA_KEY_ThrowMenuType, eThrowMenuType); + UTIL_ADD_VIP_PLAYER(iClient, iTarget, _, iBuffer, szGroup); } else { - VIP_PrintToChatClient(iClient, "%t", "Player no longer available"); + VIP_PrintToChatClient(iClient, "%t", "PLAYER_NO_LONGER_AVAILABLE"); } BackToAdminMenu(iClient); @@ -576,7 +588,7 @@ public int MenuHandler_GroupsList(Menu hMenu, MenuAction action, int iClient, in if (iTarget) { - ResetClient(iTarget); + Clients_RemoveVIPStatus(iTarget); CreateForward_OnVIPClientRemoved(iTarget, "VIP-Group Changed", iClient); Clients_CheckVipAccess(iTarget, false); } diff --git a/addons/sourcemod/scripting/vip/CMD.sp b/addons/sourcemod/scripting/vip/CMD.sp index 16b69966..103828fa 100644 --- a/addons/sourcemod/scripting/vip/CMD.sp +++ b/addons/sourcemod/scripting/vip/CMD.sp @@ -87,8 +87,9 @@ public Action AddVIP_CMD(int iClient, int iArgs) char szGroup[64]; GetCmdArg(3, SZF(szGroup)); - int iTime = StringToInt(szGroup); - if (iTime < 0) + // The expiry date (now + duration) must fit into an int too + int iTime = UTIL_TimeToSeconds(StringToInt(szGroup)); + if (iTime < 0 || iTime > cellmax - GetTime()) { ReplyToCommand(iClient, "[VIP] %t", "INCORRECT_TIME"); return Plugin_Handled; @@ -114,14 +115,14 @@ public Action AddVIP_CMD(int iClient, int iArgs) continue; } - UTIL_ADD_VIP_PLAYER(iClient, iTargetList[i], _, UTIL_TimeToSeconds(iTime), szGroup); + UTIL_ADD_VIP_PLAYER(iClient, iTargetList[i], _, iTime, szGroup); } } return Plugin_Handled; } - UTIL_ADD_VIP_PLAYER(iClient, _, iAccountID, UTIL_TimeToSeconds(iTime), szGroup); + UTIL_ADD_VIP_PLAYER(iClient, _, iAccountID, iTime, szGroup); return Plugin_Handled; } @@ -167,6 +168,7 @@ public void SQL_Callback_OnSelectRemoveClient(Database hOwner, DBResultSet hResu if (hResult == null || szError[0]) { LogError("SQL_Callback_OnSelectRemoveClient: %s", szError); + return; } if (iClient) diff --git a/addons/sourcemod/scripting/vip/Clients.sp b/addons/sourcemod/scripting/vip/Clients.sp index 6ad5ba85..2eb0547c 100644 --- a/addons/sourcemod/scripting/vip/Clients.sp +++ b/addons/sourcemod/scripting/vip/Clients.sp @@ -1,11 +1,25 @@ +// Bumped on every reset, so timers started for a previous load can tell they are stale +static int g_iClientLoadSerial[MAXPLAYERS+1]; +// Loaded before Steam authorized the player: OnClientAuthorized has to check again +static bool g_bClientWaitsForAuth[MAXPLAYERS+1]; + +// Drops the VIP status only: session flags (spawned, menu open, chat input) stay void ResetClient(int iClient) { - g_iClientInfo[iClient] = IS_LOADED; + g_iClientInfo[iClient] = (g_iClientInfo[iClient] & ~IS_VIP) | IS_LOADED; + ++g_iClientLoadSerial[iClient]; delete g_hFeatures[iClient]; delete g_hFeatureStatus[iClient]; } +// Takes the VIP status away from a player in game: modules get their features toggled off first +void Clients_RemoveVIPStatus(int iClient) +{ + Features_TurnOffAll(iClient); + ResetClient(iClient); +} + public void OnClientPutInServer(int iClient) { // g_iClientInfo[iClient] = 0; @@ -13,6 +27,18 @@ public void OnClientPutInServer(int iClient) if(!IsFakeClient(iClient) && !IsClientSourceTV(iClient)) { + Storage_LoadClient(iClient); + Clients_CheckVipAccess(iClient, true, true); + } +} + +// Steam authorized the player after OnClientPutInServer: storage and the VIP check were skipped then (no account id yet) +public void OnClientAuthorized(int iClient, const char[] szAuth) +{ + if (g_bClientWaitsForAuth[iClient] && IsClientInGame(iClient)) + { + g_bClientWaitsForAuth[iClient] = false; + Storage_LoadClient(iClient); Clients_CheckVipAccess(iClient, true, true); } } @@ -27,11 +53,14 @@ public void OnClientDisconnect(int iClient) if(!IsFakeClient(iClient)) { CreateForward_OnClientDisconnect(iClient); + Storage_SaveClient(iClient); } + Storage_ResetClient(iClient); ResetClient(iClient); delete g_hClientData[iClient]; g_iClientInfo[iClient] = 0; + g_bClientWaitsForAuth[iClient] = false; } void Clients_CheckVipAccess(int iClient, bool bNotify = false, bool bForward = false) @@ -41,22 +70,26 @@ void Clients_CheckVipAccess(int iClient, bool bNotify = false, bool bForward = f return; } + Clients_RemoveVIPStatus(iClient); g_iClientInfo[iClient] &= ~IS_LOADED; - ResetClient(iClient); - if (IsFakeClient(iClient) == false && (GLOBAL_INFO & IS_STARTED) && g_hDatabase) { Clients_LoadClient(iClient, bNotify); - // DBG_Clients("Clients_CheckVipAccess %N:\tИгрок %sявляется VIP игроком", iClient, g_bIsClientVIP[iClient] ? "":"не ") + // DBG_Clients("Clients_CheckVipAccess %N:\tis %sa VIP", iClient, g_bIsClientVIP[iClient] ? "":"not ") } else { - g_iClientInfo[iClient] |= IS_LOADED; - CreateForward_OnClientLoaded(iClient); + Clients_SetLoaded(iClient); } } +void Clients_SetLoaded(int iClient) +{ + g_iClientInfo[iClient] |= IS_LOADED; + CreateForward_OnClientLoaded(iClient); +} + void Clients_LoadClient(int iClient, bool bNotify) { char szQuery[512]; @@ -65,6 +98,14 @@ void Clients_LoadClient(int iClient, bool bNotify) DBG_Clients("Clients_LoadClient %N (%d), %b: - > %x, %u", iClient, iClient, g_iClientInfo[iClient], g_hDatabase, g_hDatabase); + // Not authorized yet: loaded as non-VIP for now, checked again from OnClientAuthorized + if (!iAccountID) + { + g_bClientWaitsForAuth[iClient] = true; + Clients_SetLoaded(iClient); + return; + } + char szWhere[64]; if(g_szSID[0]) { @@ -93,13 +134,6 @@ public void SQL_Callback_OnClientAuthorized(Database hOwner, DBResultSet hResult { DBG_SQL_Response("SQL_Callback_OnClientAuthorized"); DataPack hDataPack = view_as(hPack); - if (hResult == null || szError[0]) - { - LogError("SQL_Callback_OnClientAuthorized: %s", szError); - delete hDataPack; - return; - } - hDataPack.Reset(); int iClient = CID(hDataPack.ReadCell()); @@ -113,6 +147,14 @@ public void SQL_Callback_OnClientAuthorized(Database hOwner, DBResultSet hResult return; } + if (hResult == null || szError[0]) + { + LogError("SQL_Callback_OnClientAuthorized: %s", szError); + // Load the player as non-VIP so modules still get OnClientLoaded + Clients_SetLoaded(iClient); + return; + } + if (hResult.FetchRow()) { DBG_SQL_Response("hResult.FetchRow()"); @@ -144,8 +186,7 @@ public void SQL_Callback_OnClientAuthorized(Database hOwner, DBResultSet hResult DisplayClientInfo(iClient, "expired_info"); - g_iClientInfo[iClient] |= IS_LOADED; - CreateForward_OnClientLoaded(iClient); + Clients_SetLoaded(iClient); return; } @@ -163,9 +204,9 @@ public void SQL_Callback_OnClientAuthorized(Database hOwner, DBResultSet hResult g_hFeatures[iClient].SetString(KEY_GROUP, szGroup); - g_iClientInfo[iClient] |= IS_VIP|IS_LOADED; + g_iClientInfo[iClient] |= IS_VIP; - CreateForward_OnClientLoaded(iClient); + Clients_SetLoaded(iClient); char szName[MAX_NAME_LENGTH*2+1]; hResult.FetchString(2, SZF(szName)); @@ -185,12 +226,13 @@ public void SQL_Callback_OnClientAuthorized(Database hOwner, DBResultSet hResult } else { - LogError("Invalid VIP-Group/Некорректная VIP-группа: %s (Игрок: %d)", szGroup, iAccountID); + LogError("Invalid VIP group: %s (player: %d)", szGroup, iAccountID); + Clients_SetLoaded(iClient); } return; } - CreateForward_OnClientLoaded(iClient); + Clients_SetLoaded(iClient); } void Clients_OnVIPClientLoaded(int iClient) @@ -202,6 +244,10 @@ void Clients_OnVIPClientLoaded(int iClient) void Clients_CreateClientVIPSettings(int iClient, int iExp) { + // A reload may overlap with a load still in flight + delete g_hFeatures[iClient]; + delete g_hFeatureStatus[iClient]; + g_hFeatures[iClient] = new StringMap(); g_hFeatureStatus[iClient] = new StringMap(); @@ -217,28 +263,25 @@ public void OnClientCookiesCached(int iClient) } #endif +// Fixed retry window: if the store isn't ready by then, statuses fall back to defaults +#define STATUS_STORE_WAIT_ATTEMPTS 120 // x 0.5s + void Clients_LoadVIPFeaturesPre(int iClient, const char[] szFeature = NULL_STRING) { DBG_Clients("Clients_LoadVIPFeaturesPre %N", iClient); - DBG_Clients("AreClientCookiesCached %b", AreClientCookiesCached(iClient)); - - if (!AreClientCookiesCached(iClient)) + // Load once, when the status store (cookies / storage) is ready: loading fires OnVIPClientLoaded and turns features on + if (!Features_IsStatusStoreReady(iClient)) { - DataPack hDataPack = new DataPack(); - hDataPack.WriteCell(UID(iClient)); - if(szFeature[0]) - { - hDataPack.WriteCell(true); - hDataPack.WriteString(szFeature); - } - else - { - hDataPack.WriteCell(false); - } - CreateTimer(0.5, Timer_CheckCookies, hDataPack, TIMER_FLAG_NO_MAPCHANGE|TIMER_DATA_HNDL_CLOSE); + Clients_WaitForStatusStore(iClient, szFeature, 0); + return; } + Clients_LoadVIPFeaturesNow(iClient, szFeature); +} + +void Clients_LoadVIPFeaturesNow(int iClient, const char[] szFeature) +{ if(szFeature[0]) { Clients_LoadVIPFeature(iClient, szFeature); @@ -248,27 +291,41 @@ void Clients_LoadVIPFeaturesPre(int iClient, const char[] szFeature = NULL_STRIN Clients_LoadVIPFeatures(iClient); } +void Clients_WaitForStatusStore(int iClient, const char[] szFeature, int iAttempt) +{ + DataPack hDataPack = new DataPack(); + hDataPack.WriteCell(UID(iClient)); + hDataPack.WriteCell(g_iClientLoadSerial[iClient]); + hDataPack.WriteCell(iAttempt); + hDataPack.WriteString(szFeature); + CreateTimer(0.5, Timer_CheckCookies, hDataPack, TIMER_FLAG_NO_MAPCHANGE|TIMER_DATA_HNDL_CLOSE); +} + public Action Timer_CheckCookies(Handle hTimer, Handle hDP) { DataPack hDataPack = view_as(hDP); hDataPack.Reset(); int iClient = CID(hDataPack.ReadCell()); + int iLoadSerial = hDataPack.ReadCell(); + int iAttempt = hDataPack.ReadCell(); + char szFeature[FEATURE_NAME_LENGTH]; + hDataPack.ReadString(SZF(szFeature)); - DBG_Clients("Timer_CheckCookies -> iClient: %N (%d), IsClientVIP: %b,", iClient, iClient, view_as(g_iClientInfo[iClient] & IS_VIP)); - if (iClient && g_iClientInfo[iClient] & IS_VIP) + DBG_Clients("Timer_CheckCookies -> iClient: %N (%d), IsClientVIP: %b, attempt: %d", iClient, iClient, view_as(g_iClientInfo[iClient] & IS_VIP), iAttempt); + // Reloaded in the meantime: the new load waits on its own timer + if (!iClient || !(g_iClientInfo[iClient] & IS_VIP) || iLoadSerial != g_iClientLoadSerial[iClient]) { - char szFeature[FEATURE_NAME_LENGTH]; - if(hDataPack.ReadCell()) - { - hDataPack.ReadString(SZF(szFeature)); - } - else - { - szFeature[0] = 0; - } - Clients_LoadVIPFeaturesPre(iClient, szFeature); + return Plugin_Stop; + } + + if (!Features_IsStatusStoreReady(iClient) && ++iAttempt < STATUS_STORE_WAIT_ATTEMPTS) + { + Clients_WaitForStatusStore(iClient, szFeature, iAttempt); + return Plugin_Stop; } + Clients_LoadVIPFeaturesNow(iClient, szFeature); + return Plugin_Stop; } @@ -325,23 +382,25 @@ void Clients_LoadFeature(int iClient, const char[] szFeature) { static ArrayList hArray; if (!GLOBAL_TRIE.GetValue(szFeature, hArray)) + { return; + } DBG_Clients("LoadClientFeature: %s", szFeature); if (!GetValue(iClient, view_as(hArray.Get(FEATURES_VALUE_TYPE)), szFeature)) + { return; + } static VIP_ToggleState eStatus; DBG_Clients("GetValue: == true"); if (view_as(hArray.Get(FEATURES_ITEM_TYPE)) == TOGGLABLE) { static char szBuffer[4]; - static Cookie hCookie; - hCookie = view_as(hArray.Get(FEATURES_COOKIE)); - hCookie.Get(iClient, SZF(szBuffer)); + Features_GetSavedStatus(iClient, szFeature, hArray, SZF(szBuffer)); eStatus = view_as(StringToInt(szBuffer)); - DBG_Clients("GetFeatureCookie: '%s'", szBuffer); + DBG_Clients("GetSavedStatus: '%s'", szBuffer); if (szBuffer[0] == '\0' || !Features_IsValidStatus(eStatus)) { switch(hArray.Get(FEATURES_DEF_STATUS)) @@ -351,8 +410,11 @@ void Clients_LoadFeature(int iClient, const char[] szFeature) case DISABLED: eStatus = DISABLED; } - IntToString(view_as(eStatus), SZF(szBuffer)); - hCookie.Set(iClient, szBuffer); + // Persist the default only once the store is loaded, so it can't shadow a value that is still loading + if (Features_IsStatusStoreReady(iClient)) + { + Features_SaveStatus(iClient, szFeature, hArray, eStatus); + } // Features_SaveStatus(iClient, szFeature, hCookie, eStatus); } } @@ -540,11 +602,10 @@ public Action Timer_VIP_Expired(Handle hTimer, any UserID) void Clients_ExpiredClient(int iClient) { DBG_Clients("Clients_ExpiredClient %N:", iClient); - Features_TurnOffAll(iClient); int iClientID; g_hFeatures[iClient].GetValue(KEY_EXPIRES, iClientID); - if (g_CVAR_iDeleteExpired == 0 || GetTime() >= ((g_CVAR_iDeleteExpired*86400) + iClientID)) + if (g_CVAR_iDeleteExpired == 0 || (g_CVAR_iDeleteExpired > 0 && GetTime() >= ((g_CVAR_iDeleteExpired*86400) + iClientID))) { if (g_hFeatures[iClient].GetValue(KEY_CID, iClientID) && iClientID != -1) { @@ -562,7 +623,7 @@ void Clients_ExpiredClient(int iClient) CancelClientMenu(iClient); } - ResetClient(iClient); + Clients_RemoveVIPStatus(iClient); CreateForward_OnVIPClientRemoved(iClient, "Expired"); diff --git a/addons/sourcemod/scripting/vip/Colors.sp b/addons/sourcemod/scripting/vip/Colors.sp index a0e07067..5e3ebc3a 100644 --- a/addons/sourcemod/scripting/vip/Colors.sp +++ b/addons/sourcemod/scripting/vip/Colors.sp @@ -88,11 +88,23 @@ void Colors_Print(int iClient, const char[] szFormat) int Colors_ReplaceColors(char[] sMsg, int MaxLength) { - if (ReplaceString(sMsg, MaxLength, "{TEAM}", "\x03"))return 0; + if (ReplaceString(sMsg, MaxLength, "{TEAM}", "\x03")) + { + return 0; + } - if (ReplaceString(sMsg, MaxLength, "{BLUE}", "\x03"))return 3; - if (ReplaceString(sMsg, MaxLength, "{RED}", "\x03"))return 2; - if (ReplaceString(sMsg, MaxLength, "{GRAY}", "\x03"))return 1; + if (ReplaceString(sMsg, MaxLength, "{BLUE}", "\x03")) + { + return 3; + } + if (ReplaceString(sMsg, MaxLength, "{RED}", "\x03")) + { + return 2; + } + if (ReplaceString(sMsg, MaxLength, "{GRAY}", "\x03")) + { + return 1; + } return -1; } @@ -101,7 +113,10 @@ int Colors_FindPlayerByTeam(int iTeam) { for (int i = 1; i <= MaxClients; i++) { - if (IsClientInGame(i) && GetClientTeam(i) == iTeam)return i; + if (IsClientInGame(i) && GetClientTeam(i) == iTeam) + { + return i; + } } return 0; diff --git a/addons/sourcemod/scripting/vip/Configs.sp b/addons/sourcemod/scripting/vip/Configs.sp index 6a0daac6..385693e3 100644 --- a/addons/sourcemod/scripting/vip/Configs.sp +++ b/addons/sourcemod/scripting/vip/Configs.sp @@ -18,11 +18,12 @@ void OnReadyToStart() CreateForward_OnVIPLoaded(); + // VIP access is checked right after by UTIL_ReloadVIPPlayers (SQL_Callback_TableCreate) for (int iClient = 1; iClient <= MaxClients; ++iClient) { - if (IsClientInGame(iClient)) + if (IsClientInGame(iClient) && !IsFakeClient(iClient)) { - Clients_CheckVipAccess(iClient, false); + Storage_LoadClient(iClient); } } } @@ -38,6 +39,8 @@ void ReadConfigs() g_hGroups = CreateConfig("data/vip/cfg/groups.ini", "VIP_GROUPS"); g_hInfo = CreateConfig("data/vip/cfg/info.ini", "VIP_INFO"); + + CreateForward_OnConfigsLoaded(); } void ReadSortingMenu() @@ -52,7 +55,9 @@ void ReadSortingMenu() BuildPath(Path_SM, SZF(szFeature), "data/vip/cfg/sort_menu.ini"); File hFile = OpenFile(szFeature, "r"); if (!hFile) + { return; + } g_hSortArray = new ArrayList(ByteCountToCells(FEATURE_NAME_LENGTH)); diff --git a/addons/sourcemod/scripting/vip/Cvars.sp b/addons/sourcemod/scripting/vip/Cvars.sp index 462cc030..eb3e3e45 100644 --- a/addons/sourcemod/scripting/vip/Cvars.sp +++ b/addons/sourcemod/scripting/vip/Cvars.sp @@ -2,49 +2,53 @@ void Cvars_Setup() { CreateConVar("sm_vip_core_version", VIP_VERSION, "VIP-CORE VERSION", FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_CHEAT|FCVAR_DONTRECORD); - ConVar hCvar = CreateConVar("sm_vip_admin_flag", "z", "Флаг админа, необходимый чтобы иметь доступ к управлению VIP-игроками."); + ConVar hCvar = CreateConVar("sm_vip_admin_flag", "z", "Admin flag required to manage VIP players."); hCvar.AddChangeHook(OnAdminFlagChange); OnAdminFlagChange(hCvar, NULL_STRING, NULL_STRING); - g_CVAR_hVIPMenu_CMD = CreateConVar("sm_vip_menu_commands", "vip;sm_vip;sm_vipmenu", "Команды для вызова VIP-меню (разделять ;)"); + g_CVAR_hVIPMenu_CMD = CreateConVar("sm_vip_menu_commands", "vip;sm_vip;sm_vipmenu", "Commands that open the VIP menu (separated by ;)"); - hCvar = CreateConVar("sm_vip_server_id", "0", "ID сервера при использовании MySQL базы данных", _, true, 0.0); + hCvar = CreateConVar("sm_vip_server_id", "0", "Server ID when using a MySQL database", _, true, 0.0); hCvar.AddChangeHook(OnServerIDChange); OnServerIDChange(hCvar, NULL_STRING, NULL_STRING); - hCvar = CreateConVar("sm_vip_auto_open_menu", "0", "Автоматически открывать VIP-меню при входе (0 - Выключено, 1 - Включено)", _, true, 0.0, true, 1.0); + hCvar = CreateConVar("sm_vip_storage_id", "0", "Server group ID for the player data storage when using a MySQL database", _, true, 0.0); + hCvar.AddChangeHook(OnStorageIDChange); + g_CVAR_iStorageID = hCvar.IntValue; + + hCvar = CreateConVar("sm_vip_auto_open_menu", "0", "Open the VIP menu automatically on join (0 - Disabled, 1 - Enabled)", _, true, 0.0, true, 1.0); hCvar.AddChangeHook(OnAutoOpenMenuChange); g_CVAR_bAutoOpenMenu = hCvar.BoolValue; - hCvar = CreateConVar("sm_vip_time_mode", "0", "Формат времени (0 - Секунды, 1 - Минуты, 2 - Часы, 3 - Дни)", _, true, 0.0, true, 3.0); + hCvar = CreateConVar("sm_vip_time_mode", "0", "Time format (0 - Seconds, 1 - Minutes, 2 - Hours, 3 - Days)", _, true, 0.0, true, 3.0); hCvar.AddChangeHook(OnTimeModeChange); g_CVAR_iTimeMode = hCvar.IntValue; - hCvar = CreateConVar("sm_vip_delete_expired", "1", "Удалять VIP-игроков у которых истек срок (-1 - Не удалять, 0 - Удалять сразу, > 0 - Количество дней, по истечению которых удалять)", _, true, -1.0, true, 365.0); + hCvar = CreateConVar("sm_vip_delete_expired", "1", "Delete expired VIP players (-1 - Never, 0 - Immediately, > 0 - Days after expiry)", _, true, -1.0, true, 365.0); hCvar.AddChangeHook(OnDeleteExpiredChange); g_CVAR_iDeleteExpired = hCvar.IntValue; - hCvar = CreateConVar("sm_vip_delete_outdated", "-1", "Удалять VIP-игроков которые не заходили на сервер X дней (-1 - Не удалять, > 0 - Количество дней, по истечению которых удалять (минимум 3 суток))", _, true, -1.0, true, 365.0); + hCvar = CreateConVar("sm_vip_delete_outdated", "-1", "Delete VIP players who have not joined for X days (-1 - Never, > 0 - Number of days (minimum 3))", _, true, -1.0, true, 365.0); hCvar.AddChangeHook(OnDeleteOutdatedChange); g_CVAR_iOutdatedExpired = hCvar.IntValue; - hCvar = CreateConVar("sm_vip_update_name", "1", "Обновлять имена VIP-игроков в базе данных при входе(0 - Выключено, 1 - Включено)", _, true, 0.0, true, 1.0); + hCvar = CreateConVar("sm_vip_update_name", "1", "Update VIP player names in the database on join (0 - Disabled, 1 - Enabled)", _, true, 0.0, true, 1.0); hCvar.AddChangeHook(OnUpdateNameChange); g_CVAR_bUpdateName = hCvar.BoolValue; - hCvar = CreateConVar("sm_vip_spawn_delay", "1.0", "Задержка перед установкой привилегий при возрождении игрока", _, true, 0.1, true, 60.0); + hCvar = CreateConVar("sm_vip_spawn_delay", "1.0", "Delay before applying privileges after the player spawns", _, true, 0.1, true, 60.0); hCvar.AddChangeHook(OnSpawnDelayChange); g_CVAR_fSpawnDelay = hCvar.FloatValue; - hCvar = CreateConVar("sm_vip_hide_no_access_items", "0", "Режим отображения недоступных функций в вип меню (0 - Сделать пункты неактивными, 1 - Скрывать пункты)", _, true, 0.0, true, 1.0); + hCvar = CreateConVar("sm_vip_hide_no_access_items", "0", "How unavailable features are shown in the VIP menu (0 - Disabled items, 1 - Hidden)", _, true, 0.0, true, 1.0); hCvar.AddChangeHook(OnHideNoAccessItemsChange); g_CVAR_bHideNoAccessItems = hCvar.BoolValue; - hCvar = CreateConVar("sm_vip_features_default_status", "1", "Статус функций по-умолчанию (0 - Выключены, 1 - Включены)", _, true, 0.0, true, 1.0); + hCvar = CreateConVar("sm_vip_features_default_status", "1", "Default feature status (0 - Disabled, 1 - Enabled)", _, true, 0.0, true, 1.0); hCvar.AddChangeHook(OnDefaultStatusChange); g_CVAR_bDefaultStatus = hCvar.BoolValue; - hCvar = CreateConVar("sm_vip_logs_enable", "1", "Вести ли лог logs/VIP_Logs.log (0 - Выключено, 1 - Включено)", _, true, 0.0, true, 1.0); + hCvar = CreateConVar("sm_vip_logs_enable", "1", "Write logs/VIP_Logs.log (0 - Disabled, 1 - Enabled)", _, true, 0.0, true, 1.0); hCvar.AddChangeHook(OnLogsEnableChange); g_CVAR_bLogsEnable = hCvar.BoolValue; @@ -82,6 +86,11 @@ public void OnServerIDChange(ConVar hCvar, const char[] szOldValue, const char[] } } +public void OnStorageIDChange(ConVar hCvar, const char[] szOldValue, const char[] szNewValue) +{ + g_CVAR_iStorageID = hCvar.IntValue; +} + public void OnAutoOpenMenuChange(ConVar hCvar, const char[] szOldValue, const char[] szNewValue) { g_CVAR_bAutoOpenMenu = hCvar.BoolValue; diff --git a/addons/sourcemod/scripting/vip/Database.sp b/addons/sourcemod/scripting/vip/Database.sp index 1d297b15..6914e726 100644 --- a/addons/sourcemod/scripting/vip/Database.sp +++ b/addons/sourcemod/scripting/vip/Database.sp @@ -86,6 +86,14 @@ void CreateTables() #else FormatEx(SZF(g_szSID), " AND `sid` = %d", g_CVAR_iServerID); #endif + g_hDatabase.Query(SQL_Callback_StorageTableCreate, "CREATE TABLE IF NOT EXISTS `vip_storage` (\ + `account_id` INT NOT NULL, \ + `sid` INT UNSIGNED NOT NULL, \ + `key` VARCHAR(128) NOT NULL, \ + `value` VARCHAR(256) NOT NULL default '', \ + `updated` INT UNSIGNED NOT NULL default 0, \ + CONSTRAINT pk_StorageID PRIMARY KEY (`account_id`, `sid`, `key`) \ + ) DEFAULT CHARSET=" ... CHARSET ... ";", _, DBPrio_High); g_hDatabase.Query(SQL_Callback_TableCreate, "CREATE TABLE IF NOT EXISTS `vip_users` (\ `account_id` INT NOT NULL, \ `name` VARCHAR(64) NOT NULL default 'unknown' COLLATE '" ... COLLATION ... "', \ @@ -99,6 +107,12 @@ void CreateTables() else { g_szSID[0] = 0; + g_hDatabase.Query(SQL_Callback_StorageTableCreate, "CREATE TABLE IF NOT EXISTS `vip_storage` (\ + `account_id` INTEGER NOT NULL, \ + `key` VARCHAR(128) NOT NULL, \ + `value` TEXT NOT NULL default '', \ + `updated` INTEGER UNSIGNED NOT NULL default 0, \ + PRIMARY KEY (`account_id`, `key`));", _, DBPrio_High); g_hDatabase.Query(SQL_Callback_TableCreate, "CREATE TABLE IF NOT EXISTS `vip_users` (\ `account_id` INTEGER PRIMARY KEY NOT NULL, \ `name` VARCHAR(64) NOT NULL default 'unknown', \ @@ -138,6 +152,16 @@ public void SQL_Callback_TableCreate(Database hOwner, DBResultSet hResult, const } } +public void SQL_Callback_StorageTableCreate(Database hOwner, DBResultSet hResult, const char[] szError, any data) +{ + DBG_SQL_Response("SQL_Callback_StorageTableCreate"); + + if (szError[0]) + { + SetFailState("SQL_Callback_StorageTableCreate: %s", szError); + } +} + void RemoveExpAndOutPlayers() { if (g_CVAR_iDeleteExpired >= 0) @@ -305,6 +329,10 @@ public void SQL_Callback_SelectRemoveClient(Database hOwner, DBResultSet hResult DB_RemoveClient(hPack, iClientID); } + else + { + delete hPack; + } } void DB_RemoveClient(DataPack hDataPack, int iClientID) @@ -341,11 +369,7 @@ public void SQL_Callback_RemoveClient(Database hOwner, DBResultSet hResult, cons hPack.ReadString(SZF(szName)); hPack.ReadString(SZF(szGroup)); - if(iAdmin == -1) - { - return; - } - + // iAdmin is -1 when the admin left before the query finished: still log, just don't reply if (g_CVAR_bLogsEnable) { LogToFile(g_szLogFile, "%T", "LOG_VIP_DELETED", LANG_SERVER, szName, iClientID, szGroup, szAdmin); diff --git a/addons/sourcemod/scripting/vip/Debug.sp b/addons/sourcemod/scripting/vip/Debug.sp index b5284012..8ab07e28 100644 --- a/addons/sourcemod/scripting/vip/Debug.sp +++ b/addons/sourcemod/scripting/vip/Debug.sp @@ -15,7 +15,7 @@ public void DebugMessage(const char[] szMsg, any ...) { static char szBuffer[512]; VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #if defined LOG_DOWNLOADS @@ -24,7 +24,7 @@ public void DBG_Download(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "Download: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_Download(const char[] szMsg, any ...) {} @@ -36,7 +36,7 @@ public void DBG_Config(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "Config: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_Config(const char[] szMsg, any ...) {} @@ -48,7 +48,7 @@ public void DBG_Clients(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "Clients: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_Clients(const char[] szMsg, any ...) {} @@ -60,7 +60,7 @@ public void DBG_SQL_Query(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "SQL_Query: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_SQL_Query(const char[] szMsg, any ...) {} @@ -72,7 +72,7 @@ public void DBG_SQL_Response(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "SQL_Response: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_SQL_Response(const char[] szMsg, any ...) {} @@ -84,7 +84,7 @@ public void DBG_API(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "API: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_API(const char[] szMsg, any ...) {} @@ -96,7 +96,7 @@ public void DBG_Database(const char[] szMsg, any ...) static char szBuffer[512]; Format(szBuffer, sizeof(szBuffer), "Database: %s", szMsg); VFormat(szBuffer, sizeof(szBuffer), szMsg, 2); - LogToFile(g_szDebugLogFile, szBuffer); + LogToFile(g_szDebugLogFile, "%s", szBuffer); } #else public void DBG_Database(const char[] szMsg, any ...) {} diff --git a/addons/sourcemod/scripting/vip/Downloads.sp b/addons/sourcemod/scripting/vip/Downloads.sp index 54bfc0e5..0a4436eb 100644 --- a/addons/sourcemod/scripting/vip/Downloads.sp +++ b/addons/sourcemod/scripting/vip/Downloads.sp @@ -6,8 +6,10 @@ void ReadDownloads() BuildPath(Path_SM, SZF(szBuffer), "data/vip/modules/downloadlist.txt"); File hFile = OpenFile(szBuffer, "r"); - if(!hFile) + if (!hFile) + { return; + } DBG_Download("OpenFile('%s')", szBuffer); int iPos; @@ -17,16 +19,22 @@ void ReadDownloads() iPos = StrContains(szBuffer, "//"); DBG_Download("iPos = %d", iPos); - if(iPos == 0) + if (iPos == 0) + { continue; + } - if(iPos != -1) + if (iPos != -1) + { szBuffer[iPos] = 0; + } TrimString(szBuffer); - if(!szBuffer[0] || !IsCharAlpha(szBuffer[0])) + if (!szBuffer[0] || !IsCharAlpha(szBuffer[0])) + { continue; + } DBG_Download("ReadFileLine: '%s'", szBuffer); @@ -46,8 +54,10 @@ void DownloadPath(const char[] szPath) { DBG_Download("DownloadPath: '%s'", szPath); - if(DownloadFile(szPath)) + if (DownloadFile(szPath)) + { return; + } DBG_Download("DirExists: %d", DirExists(szPath)); if(DirExists(szPath)) @@ -61,8 +71,10 @@ void DownloadDirectory(const char[] szPath) DBG_Download("DownloadDirectory: '%s'", szPath); DirectoryListing hDir = OpenDirectory(szPath); - if(!hDir) + if (!hDir) + { return; + } char szEntry[PMP], szNewPath[PMP]; FileType type; @@ -70,7 +82,9 @@ void DownloadDirectory(const char[] szPath) { DBG_Download("GetNext: '%s' %d", szEntry, type); if (type == FileType_Directory && (!strcmp(szEntry, ".") || !strcmp(szEntry, ".."))) + { continue; + } FormatEx(SZF(szNewPath), "%s/%s", szPath, szEntry); DBG_Download("szNewPath: '%s'", szNewPath); diff --git a/addons/sourcemod/scripting/vip/Features.sp b/addons/sourcemod/scripting/vip/Features.sp index 6b72a147..380b5271 100644 --- a/addons/sourcemod/scripting/vip/Features.sp +++ b/addons/sourcemod/scripting/vip/Features.sp @@ -1,9 +1,16 @@ void Features_TurnOffAll(int iClient) { DebugMessage("Features_TurnOffAll %N (%i)", iClient, iClient); + if (!(g_iClientInfo[iClient] & IS_VIP)) + { + return; + } + int iFeatures = g_hFeaturesArray.Length; - if(iFeatures == 0) + if (iFeatures == 0) + { return; + } char szFeature[FEATURE_NAME_LENGTH]; VIP_ToggleState eOldStatus; @@ -14,11 +21,15 @@ void Features_TurnOffAll(int iClient) for(int i = 0; i < iFeatures; ++i) { g_hFeaturesArray.GetString(i, SZF(szFeature)); - if(!GLOBAL_TRIE.GetValue(szFeature, hArray)) + if (!GLOBAL_TRIE.GetValue(szFeature, hArray)) + { continue; + } - if(view_as(hArray.Get(FEATURES_ITEM_TYPE)) != TOGGLABLE) + if (view_as(hArray.Get(FEATURES_ITEM_TYPE)) != TOGGLABLE) + { continue; + } eOldStatus = Features_GetStatus(iClient, szFeature); hDataPack = view_as(hArray.Get(FEATURES_MENU_CALLBACKS)); @@ -36,8 +47,10 @@ void Features_TurnOnAll(int iClient) DebugMessage("Features_TurnOnAll %N (%i)", iClient, iClient); int iFeatures = g_hFeaturesArray.Length; - if(iFeatures == 0) + if (iFeatures == 0) + { return; + } char szFeature[FEATURE_NAME_LENGTH]; VIP_ToggleState eNewStatus; @@ -48,11 +61,15 @@ void Features_TurnOnAll(int iClient) for(int i = 0; i < iFeatures; ++i) { g_hFeaturesArray.GetString(i, SZF(szFeature)); - if(!GLOBAL_TRIE.GetValue(szFeature, hArray)) + if (!GLOBAL_TRIE.GetValue(szFeature, hArray)) + { continue; + } - if(view_as(hArray.Get(FEATURES_ITEM_TYPE)) != TOGGLABLE) + if (view_as(hArray.Get(FEATURES_ITEM_TYPE)) != TOGGLABLE) + { continue; + } hDataPack = view_as(hArray.Get(FEATURES_MENU_CALLBACKS)); hDataPack.Position = ITEM_SELECT; @@ -85,4 +102,59 @@ VIP_ToggleState Features_GetStatus(const int &iClient, const char[] szFeature) DebugMessage("Features_GetStatus: %N (%i) -> Feature: %s, eStatus: %i", iClient, iClient, szFeature, NO_ACCESS); return NO_ACCESS; -} \ No newline at end of file +} +// Toggle statuses persist in clientprefs cookies (USE_CLIENTPREFS 1) or in the VIP storage (USE_CLIENTPREFS 0). +// In storage mode cookies are still read once to migrate statuses saved by older versions. +bool Features_IsStatusStoreReady(int iClient) +{ + #if USE_CLIENTPREFS + return AreClientCookiesCached(iClient); + #else + return Storage_IsClientLoaded(iClient) && AreClientCookiesCached(iClient); + #endif +} + +void Features_GetSavedStatus(int iClient, const char[] szFeature, ArrayList hArray, char[] szValue, int iMaxLength) +{ + Cookie hCookie = view_as(hArray.Get(FEATURES_COOKIE)); + + #if USE_CLIENTPREFS + #pragma unused szFeature + hCookie.Get(iClient, szValue, iMaxLength); + #else + char szKey[STORAGE_KEY_LENGTH]; + Features_GetStorageKey(szFeature, SZF(szKey)); + if (!Storage_GetClientValue(iClient, szKey, szValue, iMaxLength) && hCookie && AreClientCookiesCached(iClient)) + { + hCookie.Get(iClient, szValue, iMaxLength); + if (szValue[0]) + { + Storage_SetClientValue(iClient, szKey, szValue); + } + } + #endif +} + +void Features_SaveStatus(int iClient, const char[] szFeature, ArrayList hArray, VIP_ToggleState eStatus) +{ + char szValue[4]; + IntToString(view_as(eStatus), SZF(szValue)); + + #if USE_CLIENTPREFS + #pragma unused szFeature + view_as(hArray.Get(FEATURES_COOKIE)).Set(iClient, szValue); + #else + #pragma unused hArray + char szKey[STORAGE_KEY_LENGTH]; + Features_GetStorageKey(szFeature, SZF(szKey)); + Storage_SetClientValue(iClient, szKey, szValue); + #endif +} + +#if !USE_CLIENTPREFS +// Same key format as 3.1-dev, so statuses saved there are picked up +void Features_GetStorageKey(const char[] szFeature, char[] szKey, int iMaxLength) +{ + FormatEx(szKey, iMaxLength, "FeatureStatus-%s", szFeature); +} +#endif diff --git a/addons/sourcemod/scripting/vip/Global.sp b/addons/sourcemod/scripting/vip/Global.sp index 76c9843a..c5348b9a 100644 --- a/addons/sourcemod/scripting/vip/Global.sp +++ b/addons/sourcemod/scripting/vip/Global.sp @@ -20,12 +20,12 @@ #define SET_BIT(%0,%1) %0 |= %1 #define UNSET_BIT(%0,%1) %0 &= ~%1 -#define IS_VIP (1<<0) // VIP ли игрок -#define IS_LOADED (1<<1) // Загружен ли игрок -#define IS_WAIT_CHAT_PASS (1<<2) // Ожидается ввод пароля в чат -#define IS_WAIT_CHAT_SEARCH (1<<3) // Ожидается ввод значения для поиска в чат -#define IS_SPAWNED (1<<4) // Игрок возродился -#define IS_MENU_OPEN (1<<5) // VIP-меню открыто +#define IS_VIP (1<<0) // Player is a VIP +#define IS_LOADED (1<<1) // Player is loaded +#define IS_WAIT_CHAT_PASS (1<<2) // Waiting for a password in chat +#define IS_WAIT_CHAT_SEARCH (1<<3) // Waiting for a search value in chat +#define IS_SPAWNED (1<<4) // Player has spawned +#define IS_MENU_OPEN (1<<5) // VIP menu is open #define IS_STARTED (1<<0) #define IS_MySQL (1<<1) @@ -110,6 +110,7 @@ ConVar g_CVAR_hVIPMenu_CMD; int g_CVAR_iAdminFlag; int g_CVAR_iServerID; +int g_CVAR_iStorageID; int g_CVAR_iTimeMode; int g_CVAR_iDeleteExpired; int g_CVAR_iOutdatedExpired; diff --git a/addons/sourcemod/scripting/vip/Info.sp b/addons/sourcemod/scripting/vip/Info.sp index 3575152d..3c6ad467 100644 --- a/addons/sourcemod/scripting/vip/Info.sp +++ b/addons/sourcemod/scripting/vip/Info.sp @@ -28,12 +28,14 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] g_hInfo.Rewind(); if (g_hInfo.JumpToKey(szEvent) && g_hInfo.JumpToKey(szType)) { + // Modules get a copy; with Plugin_Changed the (edited) copy is shown, otherwise the config itself KeyValues hKeyValues = new KeyValues(szType); hKeyValues.Import(g_hInfo); switch(CreateForward_OnShowClientInfo(iClient, szEvent, szType, hKeyValues)) { case Plugin_Stop, Plugin_Handled: { + delete hKeyValues; return; } case Plugin_Continue: @@ -49,7 +51,7 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] case 'c': { DebugMessage("case 'c'"); - if (KvGetLangString(szBuffer, iBufLen, szClientLang, szServLang)) + if (KvGetLangString(hKeyValues, szBuffer, iBufLen, szClientLang, szServLang)) { DebugMessage("KvGetLangString: (%s, %s) = '%s'", szClientLang, szServLang, szBuffer); ReplaceString(szBuffer, iBufLen, "\\n", " \n"); @@ -57,19 +59,19 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] { ReplaceValues(iClient, szBuffer, iBufLen, (szEvent[13] == 't')); } - VIP_PrintToChatClient(iClient, szBuffer); + VIP_PrintToChatClient(iClient, "%s", szBuffer); } } case 'm': { DebugMessage("case 'm'"); - int iTime = g_hInfo.GetNum("time", 0); - if (!g_hInfo.JumpToKey(szClientLang)) + int iTime = hKeyValues.GetNum("time", 0); + if (!hKeyValues.JumpToKey(szClientLang)) { - if (!g_hInfo.JumpToKey(szServLang)) + if (!hKeyValues.JumpToKey(szServLang)) { - if (!g_hInfo.GotoFirstSubKey()) + if (!hKeyValues.GotoFirstSubKey()) { if(hKeyValues != g_hInfo) { @@ -81,19 +83,19 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] } DebugMessage("KvJumpToKey: (%s|%s)", szClientLang, szServLang); - if (g_hInfo.GotoFirstSubKey(false)) + if (hKeyValues.GotoFirstSubKey(false)) { DebugMessage("KvGotoFirstSubKey"); Panel hPanel = new Panel(); do { - g_hInfo.GetSectionName(szBuffer, 32); + hKeyValues.GetSectionName(szBuffer, 32); if (strcmp(szBuffer, "item")) { continue; } - g_hInfo.GetString(NULL_STRING, szBuffer, 128); + hKeyValues.GetString(NULL_STRING, szBuffer, 128); DebugMessage("KvGetString = '%s'", szBuffer); if (szBuffer[0]) { @@ -109,13 +111,13 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] } hPanel.DrawText(szBuffer); } - } while (g_hInfo.GotoNextKey(false)); + } while (hKeyValues.GotoNextKey(false)); - g_hInfo.GoBack(); + hKeyValues.GoBack(); hPanel.DrawText(" \n"); - hPanel.CurrentKey = g_hInfo.GetNum("exit_button", g_iMaxPageItems); + hPanel.CurrentKey = hKeyValues.GetNum("exit_button", g_iMaxPageItems); FormatEx(szBuffer, 128, "%T", "Exit", iClient); hPanel.DrawItem(szBuffer, ITEMDRAW_CONTROL); @@ -129,12 +131,13 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] DebugMessage("case 'u'"); if (g_EngineVersion != Engine_CSGO) { - if (KvGetLangString(szBuffer, iBufLen, szClientLang, szServLang)) + if (KvGetLangString(hKeyValues, szBuffer, iBufLen, szClientLang, szServLang)) { DebugMessage("KvGetLangString: (%s, %s) = '%s'", szClientLang, szServLang, szBuffer); - if (strncmp(szBuffer, "http://", 7, true) != 0) + // Only add a scheme when there is none (keeps https:// links intact) + if (StrContains(szBuffer, "://") == -1) { - Format(szBuffer, 256, "http://%s", szBuffer); + Format(szBuffer, iBufLen, "http://%s", szBuffer); } ShowMOTDPanel(iClient, "VIP_INFO", szBuffer, MOTDPANEL_TYPE_URL); @@ -150,14 +153,14 @@ void DisplayInfo(int iClient, const char[] szEvent, const char[] szType, char[] } } -bool KvGetLangString(char[] szBuffer, int iBufLen, char[] szClientLang, char[] szServLang) +bool KvGetLangString(KeyValues hKeyValues, char[] szBuffer, int iBufLen, char[] szClientLang, char[] szServLang) { DebugMessage("KvGetLangString: '%s', '%s'", szClientLang, szServLang); - g_hInfo.GetString(szClientLang, szBuffer, iBufLen); + hKeyValues.GetString(szClientLang, szBuffer, iBufLen); DebugMessage("KvGetString (%s) = '%s'", szClientLang, szBuffer); if (!szBuffer[0]) { - g_hInfo.GetString(szServLang, szBuffer, iBufLen); + hKeyValues.GetString(szServLang, szBuffer, iBufLen); DebugMessage("KvGetString (%s) = '%s'", szServLang, szBuffer); if (!szBuffer[0]) { @@ -187,10 +190,10 @@ void ReplaceValues(int iClient, char[] szBuffer, int iBufLen, bool bExt) UTIL_GetTimeFromStamp(SZF(szExpires), iExpires - GetTime(), iClient); ReplaceString(szBuffer, iBufLen, "{TIMELEFT}", szExpires); } - // {NAME} - Ник игрока - // {GROUP} - Группа игрока - // {TIMELEFT} - Через сколько истекает VIP-статус - // {EXPIRES} - Когда истекает VIP-статус + // {NAME} - Player name + // {GROUP} - Player group + // {TIMELEFT} - Time left until VIP status expires + // {EXPIRES} - When VIP status expires } public int SelectInfoPanel(Menu hPanel, MenuAction action, int iClient, int iOption) diff --git a/addons/sourcemod/scripting/vip/Storage.sp b/addons/sourcemod/scripting/vip/Storage.sp new file mode 100644 index 00000000..a09ad491 --- /dev/null +++ b/addons/sourcemod/scripting/vip/Storage.sp @@ -0,0 +1,190 @@ +// Per-player key/value storage (table `vip_storage`), ported from 3.1-dev. +// Loaded on connect for every player with a valid Steam account, saved on disconnect. + +#define STORAGE_KEY_LENGTH 128 +#define STORAGE_VALUE_LENGTH 256 + +static StringMap g_hStorage[MAXPLAYERS+1]; +static int g_iStorageAccountID[MAXPLAYERS+1]; +static bool g_bStorageChanged[MAXPLAYERS+1]; + +bool Storage_IsClientLoaded(int iClient) +{ + return g_hStorage[iClient] != null; +} + +void Storage_SetClientValue(int iClient, const char[] szKey, const char[] szValue) +{ + if (Storage_IsClientLoaded(iClient)) + { + g_hStorage[iClient].SetString(szKey, szValue); + g_bStorageChanged[iClient] = true; + } +} + +bool Storage_GetClientValue(int iClient, const char[] szKey, char[] szValue, int iMaxLength) +{ + szValue[0] = 0; + return Storage_IsClientLoaded(iClient) && g_hStorage[iClient].GetString(szKey, szValue, iMaxLength); +} + +void Storage_LoadClient(int iClient) +{ + if (!g_hDatabase || !(GLOBAL_INFO & IS_STARTED) || Storage_IsClientLoaded(iClient)) + { + return; + } + + // Not authorized yet: retried from OnClientAuthorized, never stored under account 0 + int iAccountID = GetSteamAccountID(iClient); + if (!iAccountID) + { + return; + } + + char szFilter[64], szQuery[256]; + Storage_GetServerFilter(SZF(szFilter)); + FormatEx(SZF(szQuery), "SELECT `key`, `value` FROM `vip_storage` WHERE `account_id` = %d%s;", iAccountID, szFilter); + + DataPack hDataPack = new DataPack(); + hDataPack.WriteCell(UID(iClient)); + hDataPack.WriteCell(iAccountID); + + DBG_SQL_Query(szQuery); + g_hDatabase.Query(SQL_Callback_OnClientLoadStorage, szQuery, hDataPack); +} + +public void SQL_Callback_OnClientLoadStorage(Database hOwner, DBResultSet hResult, const char[] szError, any hPack) +{ + DBG_SQL_Response("SQL_Callback_OnClientLoadStorage"); + + DataPack hDataPack = view_as(hPack); + hDataPack.Reset(); + int iClient = CID(hDataPack.ReadCell()); + int iAccountID = hDataPack.ReadCell(); + delete hDataPack; + + if (hResult == null || szError[0]) + { + LogError("SQL_Callback_OnClientLoadStorage: %s", szError); + return; + } + + if (!iClient || !IsClientInGame(iClient) || Storage_IsClientLoaded(iClient)) + { + return; + } + + g_hStorage[iClient] = new StringMap(); + g_iStorageAccountID[iClient] = iAccountID; + g_bStorageChanged[iClient] = false; + + char szKey[STORAGE_KEY_LENGTH], szValue[STORAGE_VALUE_LENGTH]; + while (hResult.FetchRow()) + { + hResult.FetchString(0, SZF(szKey)); + hResult.FetchString(1, SZF(szValue)); + g_hStorage[iClient].SetString(szKey, szValue); + } + + CreateForward_OnClientStorageLoaded(iClient); +} + +// bSync: write right away (plugin unload, where async queries may never run) +void Storage_SaveClient(int iClient, bool bSync = false) +{ + if (!Storage_IsClientLoaded(iClient) || !g_bStorageChanged[iClient] || !g_hDatabase) + { + return; + } + + bool bMySQL = view_as(GLOBAL_INFO & IS_MySQL); + int iUpdated = GetTime(); + char szKey[STORAGE_KEY_LENGTH], szValue[STORAGE_VALUE_LENGTH], szQuery[1024]; + + Transaction hTxn = null; + if (bSync) + { + SQL_LockDatabase(g_hDatabase); + } + else + { + hTxn = new Transaction(); + } + + StringMapSnapshot hSnapshot = g_hStorage[iClient].Snapshot(); + for (int i = 0, iSize = hSnapshot.Length; i < iSize; ++i) + { + hSnapshot.GetKey(i, SZF(szKey)); + g_hStorage[iClient].GetString(szKey, SZF(szValue)); + + // Database.Format escapes %s + if (bMySQL) + { + g_hDatabase.Format(SZF(szQuery), "INSERT INTO `vip_storage` (`account_id`, `sid`, `key`, `value`, `updated`) \ + VALUES (%d, %d, '%s', '%s', %d) \ + ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `updated` = VALUES(`updated`);", + g_iStorageAccountID[iClient], g_CVAR_iStorageID, szKey, szValue, iUpdated); + } + else + { + g_hDatabase.Format(SZF(szQuery), "INSERT OR REPLACE INTO `vip_storage` (`account_id`, `key`, `value`, `updated`) \ + VALUES (%d, '%s', '%s', %d);", + g_iStorageAccountID[iClient], szKey, szValue, iUpdated); + } + + DBG_SQL_Query(szQuery); + if (bSync) + { + if (!SQL_FastQuery(g_hDatabase, szQuery)) + { + SQL_GetError(g_hDatabase, SZF(szQuery)); + LogError("Storage_SaveClient: %s", szQuery); + } + } + else + { + hTxn.AddQuery(szQuery); + } + } + delete hSnapshot; + + if (bSync) + { + SQL_UnlockDatabase(g_hDatabase); + } + else + { + g_hDatabase.Execute(hTxn, _, SQL_TxnCallback_SaveStorageFailure); + } + g_bStorageChanged[iClient] = false; +} + +public void SQL_TxnCallback_SaveStorageFailure(Database hDatabase, any data, int iNumQueries, const char[] szError, int iFailIndex, any[] queryData) +{ + LogError("SQL_TxnCallback_SaveStorageFailure (%d/%d): %s", iFailIndex, iNumQueries, szError); +} + +public void OnPluginEnd() +{ + for (int iClient = 1; iClient <= MaxClients; ++iClient) + { + Storage_SaveClient(iClient, true); + } +} + +void Storage_ResetClient(int iClient) +{ + delete g_hStorage[iClient]; + g_iStorageAccountID[iClient] = 0; + g_bStorageChanged[iClient] = false; +} + +static void Storage_GetServerFilter(char[] szFilter, int iMaxLength) +{ + szFilter[0] = 0; + if (GLOBAL_INFO & IS_MySQL) + { + FormatEx(szFilter, iMaxLength, " AND `sid` = %d", g_CVAR_iStorageID); + } +} diff --git a/addons/sourcemod/scripting/vip/UTIL.sp b/addons/sourcemod/scripting/vip/UTIL.sp index 3d04dfa0..4551b294 100644 --- a/addons/sourcemod/scripting/vip/UTIL.sp +++ b/addons/sourcemod/scripting/vip/UTIL.sp @@ -32,17 +32,25 @@ stock int UTIL_ReplaceChars(char[] szBuffer, int InChar, int OutChar) return iNum; } +// Returns -1 for an unknown time mode or a value that doesn't fit into an int int UTIL_TimeToSeconds(int iTime) { + int iMultiplier; switch (g_CVAR_iTimeMode) { - case TIME_MODE_SECONDS:return iTime; - case TIME_MODE_MINUTES:return iTime * 60; - case TIME_MODE_HOURS:return iTime * 3600; - case TIME_MODE_DAYS:return iTime * 86400; + case TIME_MODE_SECONDS: iMultiplier = 1; + case TIME_MODE_MINUTES: iMultiplier = 60; + case TIME_MODE_HOURS: iMultiplier = 3600; + case TIME_MODE_DAYS: iMultiplier = 86400; + default: return -1; } - - return -1; + + if (iTime > cellmax / iMultiplier || iTime < -(cellmax / iMultiplier)) + { + return -1; + } + + return iTime * iMultiplier; } int UTIL_SecondsToTime(int iTime) @@ -127,11 +135,11 @@ void UTIL_LoadVipCmd(ConVar &hCvar, ConCmd Call_CMD) if (szPart[0]) { RegConsoleCmd(szPart, Call_CMD); - - if (iPos == -1) - { - return; - } + } + + if (iPos == -1) + { + return; } } } @@ -155,16 +163,35 @@ stock int UTIL_SearchCharInString(const char[] szBuffer, int c) iNum = 0; iLen = strlen(szBuffer); for (i = 0; i < iLen; ++i) - if (szBuffer[i] == c)iNum++; + { + if (szBuffer[i] == c) + { + iNum++; + } + } return iNum; } int UTIL_GetAccountIDFromSteamID(const char[] szSteamID) { + // STEAM_X:Y:Z, Y is 0 or 1, Z is digits (rejects STEAM_ID_PENDING, STEAM_ID_LAN, truncated input) if (!strncmp(szSteamID, "STEAM_", 6)) { - return S2I(szSteamID[10]) << 1 | (szSteamID[8] - 48); + if (strlen(szSteamID) < 11 || szSteamID[7] != ':' || (szSteamID[8] != '0' && szSteamID[8] != '1') || szSteamID[9] != ':') + { + return 0; + } + + for (int i = 10; szSteamID[i]; ++i) + { + if (!IsCharNumeric(szSteamID[i])) + { + return 0; + } + } + + return S2I(szSteamID[10]) << 1 | (szSteamID[8] - '0'); } if (!strncmp(szSteamID, "[U:1:", 5) && szSteamID[strlen(szSteamID)-1] == ']') @@ -255,12 +282,12 @@ void UTIL_Reply(int iClient, const char[] szMsg, any ...) VFormat(SZF(szMessage), szMsg, 3); if (iClient) { - VIP_PrintToChatClient(iClient, szMessage); + VIP_PrintToChatClient(iClient, "%s", szMessage); } else { Colors_RemoveColors(szMessage); - PrintToServer(szMessage); + PrintToServer("%s", szMessage); } } diff --git a/addons/sourcemod/scripting/vip/VipMenu.sp b/addons/sourcemod/scripting/vip/VipMenu.sp index 5a9762b8..142c5e12 100644 --- a/addons/sourcemod/scripting/vip/VipMenu.sp +++ b/addons/sourcemod/scripting/vip/VipMenu.sp @@ -282,7 +282,6 @@ public int Handler_VIPMenu(Menu hMenu, MenuAction action, int iClient, int iOpti hPlugin = view_as(hBuffer.Get(FEATURES_PLUGIN)); if (view_as(hBuffer.Get(FEATURES_ITEM_TYPE)) == TOGGLABLE) { - char szBuffer[4]; VIP_ToggleState eOldStatus, eNewStatus; eOldStatus = Features_GetStatus(iClient, szItemInfo); @@ -298,8 +297,7 @@ public int Handler_VIPMenu(Menu hMenu, MenuAction action, int iClient, int iOpti if (eNewStatus != eOldStatus) { Features_SetStatus(iClient, szItemInfo, eNewStatus); - IntToString(view_as(eNewStatus), SZF(szBuffer)); - view_as(hBuffer.Get(FEATURES_COOKIE)).Set(iClient, szBuffer); + Features_SaveStatus(iClient, szItemInfo, hBuffer, eNewStatus); } } @@ -327,12 +325,14 @@ bool IsValidFeature(const char[] szFeature) bool OnVipMenuFlood(int iClient) { + // Engine time, not game time: game time restarts on map change static float fLastTime[MAXPLAYERS + 1]; - if (fLastTime[iClient] > 0.0) + float fNow = GetEngineTime(); + if (fLastTime[iClient] > 0.0 && (fNow - fLastTime[iClient]) < 3.0) { - float fSec = GetGameTime(); - if ((fSec - fLastTime[iClient]) < 3.0)return true; - fLastTime[iClient] = fSec; + return true; } + + fLastTime[iClient] = fNow; return false; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/vip/adminmenu/Add.sp b/addons/sourcemod/scripting/vip/adminmenu/Add.sp index cc763247..73157ec1 100644 --- a/addons/sourcemod/scripting/vip/adminmenu/Add.sp +++ b/addons/sourcemod/scripting/vip/adminmenu/Add.sp @@ -43,7 +43,10 @@ public int MenuHandler_AddVip_PlayerList(Menu hMenu, MenuAction action, int iCli case MenuAction_End:delete hMenu; case MenuAction_Cancel: { - if (iItem == MenuCancel_ExitBack) BackToAdminMenu(iClient); + if (iItem == MenuCancel_ExitBack) + { + BackToAdminMenu(iClient); + } } case MenuAction_Select: { diff --git a/addons/sourcemod/scripting/vip/adminmenu/Del.sp b/addons/sourcemod/scripting/vip/adminmenu/Del.sp index 8527e5de..40be6e1c 100644 --- a/addons/sourcemod/scripting/vip/adminmenu/Del.sp +++ b/addons/sourcemod/scripting/vip/adminmenu/Del.sp @@ -24,7 +24,10 @@ public int MenuHandler_DeleteVipPlayerMenu(Menu hMenu, MenuAction action, int iC case MenuAction_End: delete hMenu; case MenuAction_Cancel: { - if (iItem == MenuCancel_ExitBack) ShowTargetInfoMenu(iClient); + if (iItem == MenuCancel_ExitBack) + { + ShowTargetInfoMenu(iClient); + } } case MenuAction_Select: { @@ -39,20 +42,21 @@ public int MenuHandler_DeleteVipPlayerMenu(Menu hMenu, MenuAction action, int iC if(g_hClientData[iClient].GetValue(DATA_KEY_TargetUID, iTarget)) { iTarget = CID(iTarget); - if (!iTarget && iTargetID != -1) - { - iTarget = IsClientOnline(iTargetID); - } + } - if (iTarget) - { - DB_RemoveClientFromID(iClient, iTarget, _, true); - ResetClient(iTarget); - CreateForward_OnVIPClientRemoved(iTarget, "Removed by Admin", iClient); - DisplayClientInfo(iTarget, "expired_info"); - BackToAdminMenu(iClient); - return 0; - } + if (!iTarget && iTargetID != -1) + { + iTarget = IsClientOnline(iTargetID); + } + + if (iTarget) + { + DB_RemoveClientFromID(iClient, iTarget, _, true); + Clients_RemoveVIPStatus(iTarget); + CreateForward_OnVIPClientRemoved(iTarget, "Removed by Admin", iClient); + DisplayClientInfo(iTarget, "expired_info"); + BackToAdminMenu(iClient); + return 0; } if(iTargetID != -1) diff --git a/addons/sourcemod/scripting/vip/adminmenu/List.sp b/addons/sourcemod/scripting/vip/adminmenu/List.sp index 34fff2b6..0d194b2c 100644 --- a/addons/sourcemod/scripting/vip/adminmenu/List.sp +++ b/addons/sourcemod/scripting/vip/adminmenu/List.sp @@ -57,21 +57,24 @@ public int MenuHandler_VipPlayersListMenu(Menu hMenu, MenuAction action, int iCl case MenuAction_End: delete hMenu; case MenuAction_Cancel: { - if (iItem == MenuCancel_ExitBack) BackToAdminMenu(iClient); + if (iItem == MenuCancel_ExitBack) + { + BackToAdminMenu(iClient); + } } case MenuAction_Select: { char szUserID[16]; hMenu.GetItem(iItem, SZF(szUserID)); - if (!strcmp(szUserID, "search")) // Найти игрока + if (!strcmp(szUserID, "search")) // Search for a player { ShowWaitSearchMenu(iClient); return 0; } - if (!strcmp(szUserID, "show_all")) // Показать всех + if (!strcmp(szUserID, "show_all")) // Show all { g_hClientData[iClient].Remove(DATA_KEY_Search); ShowVipPlayersFromDBMenu(iClient); @@ -79,7 +82,7 @@ public int MenuHandler_VipPlayersListMenu(Menu hMenu, MenuAction action, int iCl return 0; } - if (!strcmp(szUserID, "more")) // Показать еще + if (!strcmp(szUserID, "more")) // Show more { int iOffset; g_hClientData[iClient].GetValue(DATA_KEY_Offset, iOffset); @@ -115,6 +118,8 @@ public int MenuHandler_VipPlayersListMenu(Menu hMenu, MenuAction action, int iCl } + // Picked from the DB list: forget a player picked earlier from the online list + g_hClientData[iClient].Remove(DATA_KEY_TargetUID); g_hClientData[iClient].SetValue(DATA_KEY_TargetID, S2I(szUserID)); ShowTargetInfo(iClient); @@ -208,7 +213,8 @@ void ShowVipPlayersFromDBMenu(int iClient, int iOffset = 0) } else { - FormatEx(SZF(szWhere), " AND `name` LIKE '%%%s%%'", szSearch); + // Database.Format escapes %s (search text comes straight from admin chat) + g_hDatabase.Format(SZF(szWhere), " AND `name` LIKE '%%%s%%'", szSearch); } } @@ -300,7 +306,8 @@ public void SQL_Callback_SelectVipPlayers(Database hOwner, DBResultSet hResult, if (hResult.RowCount == LIST_OFFSET) { hMenu.AddItem(NULL_STRING, "ITEMDRAW_SPACER", ITEMDRAW_SPACER); - hMenu.AddItem("more", "Показать еще"); + FormatEx(SZF(szName), "%T", "SHOW_MORE", iClient); + hMenu.AddItem("more", szName); } hMenu.Display(iClient, MENU_TIME_FOREVER); @@ -388,7 +395,7 @@ void ShowTemporaryTargetInfo(int iClient) g_hFeatures[iTarget].GetValue(KEY_EXPIRES, iExpires); g_hClientData[iClient].SetValue(DATA_KEY_Time, iExpires); GetClientName(iTarget, SZF(szName)); - iAccountID = GetSteamAccountID(iClient); + iAccountID = GetSteamAccountID(iTarget); g_hClientData[iClient].SetString(DATA_KEY_Name, szName); g_hClientData[iClient].SetValue(DATA_KEY_Auth, iAccountID); @@ -436,11 +443,11 @@ void ShowTargetInfoMenu(int iClient) hMenu.ExitBackButton = true; hMenu.SetTitle("%T\n ", "MENU_INFO_VIP_PLAYER", iClient, szName, szAuth, szGroup, szBuffer); - FormatEx(SZF(szBuffer), "%T", "MENU_DEL_VIP", iClient); // 1. Удалить игрока + FormatEx(SZF(szBuffer), "%T", "MENU_DEL_VIP", iClient); // 1. Delete player hMenu.AddItem(NULL_STRING, szBuffer); - FormatEx(SZF(szBuffer), "%T", "MENU_EDIT_TIME", iClient); // 2. Изменить срок + FormatEx(SZF(szBuffer), "%T", "MENU_EDIT_TIME", iClient); // 2. Change expiry hMenu.AddItem(NULL_STRING, szBuffer, iClientID == -1 ? ITEMDRAW_DISABLED:ITEMDRAW_DEFAULT); - FormatEx(SZF(szBuffer), "%T", "MENU_EDIT_GROUP", iClient); // 3. Изменить группу + FormatEx(SZF(szBuffer), "%T", "MENU_EDIT_GROUP", iClient); // 3. Change group hMenu.AddItem(NULL_STRING, szBuffer); ReductionMenu(hMenu, 2); diff --git a/addons/sourcemod/translations/vip_core.phrases.txt b/addons/sourcemod/translations/vip_core.phrases.txt index c68ccdf8..deb89b7b 100644 --- a/addons/sourcemod/translations/vip_core.phrases.txt +++ b/addons/sourcemod/translations/vip_core.phrases.txt @@ -840,6 +840,12 @@ "pt" "Mostrar todos jogadores" "ru" "Показать всех" } + "SHOW_MORE" + { + "en" "Show more" + "ru" "Показать еще" + "ua" "Показати ще" + } "y." { "en" "y." diff --git a/addons/sourcemod/translations/vip_modules.phrases.txt b/addons/sourcemod/translations/vip_modules.phrases.txt new file mode 100644 index 00000000..ec062a5e --- /dev/null +++ b/addons/sourcemod/translations/vip_modules.phrases.txt @@ -0,0 +1,2018 @@ +"Phrases" +{ + "additional_menu" + { + "ru" "Дополнительное VIP" + "en" "Additional VIP" + "pt" "VIP Adicional" + "fi" "Lisää VIP" + "ua" "Додаткове VIP" + "es" "VIP Adicional" + "ar" "إضافي VIP" + } + "AutoRespawn" + { + "ru" "Авто возрождение" + "en" "Auto Respawn" + "pt" "Respawn Automático" + "fi" "Automaattinen palautuminen" + "ua" "Авто відродження" + "es" "Reaparición automática" + "ar" "بيضة السيارات" + } + "AimTeleport" + { + "ru" "AIM Телепорт" + "en" "AIM Teleport" + "pt" "AIM Teleport" + "fi" "AIM Teleport" + "ua" "AIM Телепорт" + "es" "AIM Teleport" + "ar" "النقل الفضائي AIM" + } + "AustraliumWeapons" + { + "ru" "Оружие из Австралия" + "en" "Weapons from Australium" + "pt" "Armas de Australium" + "fi" "Aseita australiasta" + "ua" "Зброя з Австралія" + "es" "Armas de Australium" + "ar" "أسلحة من أستراليا" + } + "AutoBuy" + { + "ru" "Автозакупка" + "en" "Auto Buy" + "pt" "Auto Buy" + "fi" "Automaattinen ostos" + "ua" "Автозакупівля" + "es" "Compra automática" + "ar" "شراء تلقائي" + } + "AutoBuyMenu" + { + "ru" "Настройка автозакупки" + "en" "Auto Buy Setting" + "pt" "Configurações do Auto Buy" + "fi" "Automaattisen ostoiden valikko" + "ua" "Налаштування автозакупівлі" + "es" "Configuración de compra automática" + "ar" "إعداد شراء السيارات" + } + "AutoSilencer_usp" + { + "ru" "Авто глушитель на USP" + "en" "Auto Silencer for USP" + "pt" "Silenciador Automático USP" + "fi" "Automaatti äänenvaimennin USP varten" + "ua" "Авто глушник на USP-S" + "es" "Silenciador automático para USP" + "ar" "كاتم الصوت التلقائي لـ USP" + } + "AutoSilencer_m4a1" + { + "ru" "Авто глушитель на M4A1" + "en" "Auto Silencer for M4A1" + "pt" "Silenciador Automático M4A1" + "fi" "Automaatti äänenvaimennin M4A1 varten" + "ua" "Авто глушник на M4A1-S" + "es" "Silenciador automático para M4A1" + "ar" "كاتم الصوت التلقائي لـ M4A1" + } + "AntiFlash" + { + "ru" "Анти-флеш" + "en" "Anti-Flash" + "pt" "Anti-Flash" + "fi" "Anti-Flash" + "ua" "Анти-Флеш" + "es" "Anti-Flash" + "ar" "مضاد للفلاش" + } + "Armor" + { + "ru" "Бронь" + "en" "Armor" + "pt" "Colete" + "fi" "Suojaliivi" + "ua" "Броня" + "es" "Armadura" + "ar" "درع" + } + "AURA" + { + "ru" "Аура" + "en" "Aura" + "pt" "Aura" + "fi" "Säteily" + "ua" "Аура" + "es" "Aura" + "ar" "هالة" + } + "AURA_M" + { + "ru" "Цвет ауры" + "en" "Color Aura" + "pt" "Cor da Aura" + "fi" "Säteilyn väri" + "ua" "Колір аури" + "es" "Aura de color" + "ar" "هالة اللون" + } + "Burning" + { + "ru" "Горение" + "en" "Burning" + "pt" "Burning" + "fi" "Polttava" + "ua" "Горіння" + "es" "Ardiente" + "ar" "احتراق" + } + "Breachcharge" + { + "ru" "Взрывчатка" + "en" "Explosive" + "pt" "Explosivo" + "fi" "Räjähtävä" + "ua" "Вибухівка" + "es" "Explosiva" + "ar" "مادة متفجرة" + } + "BunnyHop" + { + "ru" "БанниХоп" + "en" "BunnyHop" + "fi" "BunnyHop" + "ua" "БанніХоп" + "es" "BunnyHop" + "ar" "باني هوب" + } + "BHOP_TIME" + { + "#format" "{1:f}" + "ru" "Банихоп будет доступен через {1} секунд" + "ua" "Банніхоп буде доступний через {1} секунд" + "en" "BunnyHop will be available in {1} seconds" + "pt" "BunnyHop estará disponível em {1} segundos" + "fr" "BunnyHop sera disponible dans {1} secondes" + "ar" "سوف يكون Bunnyhop متاحًا في {1} ثواني" + } + "BuyZona" + { + "ru" "Зона закупки" + "en" "Buy Zona" + "pt" "Buy Zona" + "fi" "Hankinta-alue" + "ua" "Зона закупівлі" + "es" "Comprar Zona" + "ar" "شراء المنطقة" + } + "BoostLadder" + { + "ru" "Ускорение подъема по лестнице" + "en" "Boost Ladder" + "pt" "Boost Ladder" + "fi" "Nopeutuva porraskiipeily" + "ua" "Прискорення підйому по драбині" + "es" "Escalera de refuerzo" + "ar" "رفع السلم" + } + "Chat" + { + "ru" "Чат" + "en" "Chat" + "pt" "Chat" + "fi" "Chatti" + "ua" "Чат" + "es" "Charla" + "ar" "دردشة" + } + "Chat_Menu" + { + "ru" "Настройка чата" + "en" "Chat Settings" + "pt" "Configurações do Chat" + "fi" "Inställning av chatt" + "ua" "Налаштування чату" + "es" "Configuraciones de chat" + "ar" "إعدادات المحادثة" + } + "CT_C4" + { + "ru" "Бомба за КТ" + "en" "C4 CT" + "pt" "C4 CT" + "fi" "C4 Erikoisjoukot" + "ua" "С4 за Спецназ" + "es" "C4 CT" + "ar" "قنبلة CT" + } + "CausedDamage" + { + "ru" "Наносимый урон" + "en" "Caused Damage" + "pt" "Dano Causado" + "fi" "Aiheuttanut vahinkoa" + "ua" "Завдана шкода" + "es" "Daño causado" + "ar" "تسببت في الضرر" + } + "COIN" + { + "ru" "Монеты" + "en" "Coins" + "pt" "Medalhas" + "fi" "Mitaleiden asetukset" + "ua" "Монети" + "es" "Monedas" + "ar" "عملات معدنية" + } + "CarryC4" + { + "ru" "Переносная бомба" + "en" "Carry C4" + "fi" "Kannettava pommi" + "ua" "Переносна С4" + "es" "Llevar C4" + "ar" "حمل C4" + } + "change_color_props" + { + "ru" "Смена цвета пропов" + "en" "Сhange Сolors Props" + "fi" "Värien muutos" + "ua" "Зміна кольорів пропів" + "es" "Сhange Сolors Props" + "ar" "دعائم تغيير الألوان" + } + "Changeteam" + { + "ru" "Смена команды" + "en" "Change Team" + "pt" "Change Team" + "fi" "Tiimin muutos" + "ua" "Зміна команди" + "es" "Cambiar equipo" + "ar" "فريق التغيير" + } + "ColoredFlash" + { + "ru" "Разноцветное ослепление" + "en" "Colored Flash" + "pt" "Flash Colorida" + "fi" "Värikäs sokaisin" + "ua" "Різнокольоровий осліплення" + "es" "Flash de Color" + "ar" "فلاش ملون" + } + "CustomWeapons" + { + "ru" "Модели оружия" + "en" "Weapon Models" + "pt" "Modelos de Armas" + "fi" "Ase Mallit" + "ua" "Моделі зброї" + "es" "Modelos de Armas" + "ar" "نماذج الأسلحة" + } + "C4_Model" + { + "ru" "Измененная бомба" + "en" "C4 Model" + "pt" "Modelo de C4" + "fi" "Muunnettu Pommi" + "ua" "Модель С4" + "es" "Modelo C4" + "ar" "نموذج C4" + } + "CCC_Settings" + { + "ru" "Настройки чата" + "en" "Chat Settings" + "pt" "Configurações do Chat" + "fi" "Chat-asetukset" + "ua" "Налаштування чату" + "es" "Configuraciones de chat" + "ar" "إعدادات المحادثة" + } + "С4" + { + "ru" "Бесплатная бомба" + "en" "Free Bomb" + "fi" "Ilmainen C4" + "ua" "Безкоштовна С4" + "es" "Libre bomba" + "ar" "قنبلة مجانية" + } + "CF_BuyVip" + { + "ru" "Данный функционал доступен только VIP-игрокам!" + "en" "This is feature available only for VIP-players!" + "fi" "Tämä toiminto on käytettävissä vain VIP-pelaajille!" + "ua" "Цей функціонал доступний лише VIP-гравцям!" + "es" "¡Esta función está disponible solo para jugadores VIP!" + "ar" "هذه الميزة متاحة فقط للاعبين VIP!" + } + "CCC_Enabler" + { + "ru" "Активация настроек чата" + "en" "Activate chat settings" + "fi" "Aktivoi chat-asetukset" + "ua" "Активація налаштувань чату" + "es" "Activa la configuración de chat" + "ar" "تنشيط إعدادات الدردشة" + } + "CAUSED_DAMAGE" + { + "ru" "Наносимый урон" + "en" "Caused Damage" + "pt" "Dano Causado" + "fi" "Aiheuttanut vahinkoa" + "ua" "Завдана шкода" + "es" "Daño causado" + "ar" "تسببت في الضرر" + } + "Defuser" + { + "ru" "Набор обезвреживания" + "en" "Defuse Kit" + "pt" "Kit Defuse" + "fi" "Defuse Kit" + "ua" "Набір знешкодження" + "es" "Defuse Kit" + "ar" "طقم نزع الفتيل" + } + "Decoy" + { + "ru" "Телепорт-граната" + "en" "Decoy Teleport" + "fi" "Teleportti-kranaatti" + "ua" "Телепортаційна граната" + "es" "Teletransporte señuelo" + "ar" "شرك التخاطر" + } + "DissolveBody" + { + "ru" "Растворение тела" + "en" "Dissolution of the body" + "fi" "Liukeneminen elimistössä" + "ua" "Розчинення тіла" + "es" "Disolución del cuerpo" + "ar" "انحلال الجسم" + } + "DissolveBodyNew" + { + "ru" "Растворение тела" + "en" "Dissolution of the body" + "pt" "Dissolução do corpo" + "fi" "Liukeneminen elimistössä" + "ua" "Розчинення тіла" + "es" "Disolución del cuerpo" + "ar" "انحلال الجسم" + } + "ElectroEffects" + { + "ru" "Электрические эффекты" + "en" "Electro Effects" + "fi" "Salama Effekti" + "ua" "Електричні ефекти" + "es" "Efectos electricos" + "ar" "التأثيرات الكهربائية" + } + "evade" + { + "ru" "Уворот" + "en" "Dodge" + "fi" "Väistää" + "ua" "Ухилення" + "es" "Esquivar" + "ar" "يتملص" + } + "Endurance" + { + "ru" "Выносливость" + "en" "Endurance" + "fi" "Kestävyys" + "ua" "Витривалість" + "es" "Resistencia" + "ar" "قدرة التحمل" + } + "ExplosiveBullets" + { + "ru" "Взрывные пули" + "en" "Explosive Bullets" + "pt" "Balas Explosivas" + "fi" "Räjähtävät luodit" + "ua" "Вибухові кулі" + "es" "Balas explosivas" + "ar" "الرصاصات المتفجرة" + } + "ExtendedAmmoClip" + { + "ru" "Расширенные патроны в обойме" + "en" "Extended Ammo In Clip" + "fi" "Ammuksien lisäystä pääasiassa aseeseen" + "ua" "Розширені патрони в обоймі" + "es" "Munición extendida en el clip" + "ar" "ذخيرة ممتدة في مقطع" + } + "ExtendedAmmoReserve" + { + "ru" "Расширенные патроны в запасе" + "en" "Extended Ammo In Stock" + "fi" "Ammuksien lisäystä lippaaseen" + "ua" "Розширені патрони в запасі" + "es" "Munición extendida en stock" + "ar" "ممتد الذخيرة في المخزون" + } + "Fzi_Rescued" + { + "#format" "{1:N}" + "ru" "Вы были выбраны первым зомби, но были спасены благодаря VIP. {1} заражен вместо вас." + "en" "You were chosen as the first zombie, but were saved thanks to the VIP. {1} is infected for you." + "fi" "Olet valittu ensimmäiseksi zombiksi, mutta olit tallennettu VIP: n ansiosta. {1} tartunnan saaneet." + "ua" "Ви обрані в якості першого зомбі, але були врятовані завдяки VIP. {1} заражений замість вас." + "es" "Fuiste elegido como el primer zombie, pero te salvaste gracias al VIP. {1} está infectado por ti." + "ar" "تم اختيارك كأول غيبوبة ، ولكن تم إنقاذك بفضل VIP. {1} مصاب لك." + } + "Fzi" + { + "ru" "Иммунитет от первого заражения" + "en" "First Inf. Immunity" + "fi" "Immuniteetti ensimmäisestä infektiosta" + "ua" "Імунітет від першого зараження" + "es" "First Inf. Inmunidad" + "ar" "أول إنف. حصانة" + } + "FastReload" + { + "ru" "Быстрая перезарядка" + "en" "Fast Reload" + "fi" "Nopea lataus" + "ua" "Швидка перезарядка" + "es" "Recarga rápida" + "ar" "إعادة تحميل سريعة" + } + "FastSwitch" + { + "ru" "Быстрая смена оружия" + "en" "Fast Weapon Switch" + "fi" "Nopea aseenvaihto" + "ua" "Швидка заміна зброї" + "es" "Cambio rápido de arma" + "ar" "التبديل سلاح سريع" + } + "Fastplant" + { + "ru" "Быстрый плент бомбы" + "en" "Fast Plant Bomb" + "fi" "Nopean pommin pläntti" + "ua" "Швидкий плент бомби" + "es" "Bomba de planta rápida" + "ar" "قنبلة نباتية سريعة" + } + "FireDamage" + { + "ru" "Поджигающие повреждения" + "en" "Fire Damage" + "fi" "Sytytys Vahinko" + "ua" "Запалювальні пошкодження" + "es" "Daño por fuego" + "ar" "الخسائر الناجمة عن الحرائق" + } + "Gloves" + { + "ru" "Перчатки" + "en" "Gloves" + "pt" "Luvas" + "fi" "Hanskat" + "ua" "Рукавички" + "es" "Guantes" + "ar" "قفاز" + } + "Gloves_MENU" + { + "ru" "Выбор перчаток" + "en" "Choose Gloves" + "pt" "Escolha Luvas" + "fi" "Hanskojen valikko" + "ua" "Вибір рукавичок" + "es" "Elige guantes" + "ar" "اختر القفازات" + } + "GOD" + { + "ru" "Бессмертие" + "en" "GOD" + "fi" "Kuolemattomuus" + "ua" "Безсмертя" + "es" "DIOS" + "ar" "خلود" + } + "GrenadeTrails" + { + "ru" "Трейлы гранат" + "en" "Grenade Trails" + "pt" "Trilhas de Granadas" + "fi" "Kranaattijälki" + "ua" "Сліди гранат" + "es" "Granada Senderos" + "ar" "ممرات القنابل اليدوية" + } + "GrenadeTrails_MENU" + { + "ru" "Цвет трейлов гранат" + "en" "Color Grenade Trails" + "pt" "Cores das Trilhas de Grenadas" + "fi" "Kranaattijäljen väri" + "ua" "Колір слідів гранат" + "es" "Granada de color" + "ar" "مسارات القنابل الملونة" + } + "Grenades" + { + "ru" "Гранаты" + "en" "Grenades" + "pt" "Granadas" + "fi" "Kranaatit" + "ua" "Гранати" + "es" "Granadas" + "ar" "قنبلة يدوية" + } + "GoldStatues" + { + "ru" "Золотые статуи" + "en" "Gold Statues" + "fi" "Kulta patsaat" + "ua" "Золоті статуї" + "es" "Estatuas de oro" + "ar" "التماثيل الذهبية" + } + "Gravity" + { + "ru" "Гравитация" + "en" "Gravity" + "pt" "Gravidade" + "fi" "Gravitaatio" + "ua" "Гравітація" + "es" "Gravedad" + "ar" "الجاذبية" + } + "HP" + { + "ru" "ХП" + "en" "HP" + "fi" "Terveys" + "ua" "Здоров'я" + "es" "Salud" + "ar" "الصحة" + } + "HealCube" + { + "ru" "Регенерационный куб" + "en" "Heal Cube" + "fi" "Paranna kuutio" + "ua" "Відновлювальний куб" + "es" "Curar el cubo" + "ar" "شفاء المكعب" + } + "Healthshot" + { + "ru" "Укол здоровья" + "en" "Healthshot" + "pt" "Healthshot" + "fi" "Terveyden piikki" + "ua" "Укол здоров'я" + "es" "Disparo de salud" + "ar" "لقطة صحية" + } + "HeavyArmor" + { + "ru" "Тяжелая броня" + "en" "Heavy Armor" + "fi" "Kova suojaliivi" + "ua" "Важка броня" + "es" "Armadura pesada" + "ar" "درع ثقيل" + } + "HitMarker" + { + "ru" "Хитмаркер" + "en" "Hit Marker" + "fi" "Hit Marker" + "ua" "Хітмаркер" + "es" "Dar en el objetivo" + "ar" "ضرب علامة" + } + "JoinSound" + { + "ru" "Музыка при входе" + "en" "Join Sound" + "fi" "Musiikki tervehdys" + "ua" "Музика при підключенні" + "es" "Unirse al sonido" + "ar" "انضم إلى الصوت" + } + "jumps" + { + "ru" "Несколько прыжков" + "en" "Some Jumps" + "fi" "Useita hyppyjä" + "ua" "Декілька стрибків" + "es" "Algunos saltos" + "ar" "بعض القفزات" + } + "JoinMessage" + { + "ru" "Оповещение о входе" + "en" "Join Message" + "fi" "Kirjaudu sisään" + "ua" "Оповіщення про вхід" + "es" "Unirse al mensaje" + "ar" "الانضمام إلى الرسالة" + } + "KillScreen" + { + "ru" "Эффект при убийстве" + "en" "Kill Effect" + "fi" "Tappaus effekti" + "ua" "Ефект під час вбивства" + "es" "Efecto de Matar" + "ar" "تأثير القتل" + } + "Kick" + { + "ru" "Кикнуть игрока" + "en" "Kick Player" + "pt" "Kick Player" + "fi" "Potkia pelaaja" + "ua" "Вигнати гравця" + "es" "Jugador de Patada" + "ar" "لاعب ركلة" + } + "Language" + { + "ru" "Изменить язык" + "en" "Change Language" + "pt" "Mudar Idioma" + "fi" "Vaihda kieli" + "ua" "Змінити мову" + "es" "Cambiar Idioma" + "ar" "تغيير اللغة" + } + "LongJump" + { + "ru" "Длинный прыжок" + "en" "Long Jump" + "fi" "Pitkä hyppy" + "ua" "Довгий стрибок" + "es" "Salto Largo" + "ar" "قفزة طويلة" + } + "Left" + { + "#format" "{1:i}" + "ru" "Осталось: {1}" + "en" "Left: {1}" + "fi" "Jäljellä: {1}" + "ua" "Залишилось: {1}" + "es" "Lzquierda: {1}" + "ar" "اليسار: {1}" + } + "MKIT" + { + "ru" "Музыкальный комплект" + "en" "Music Kit" + "pt" "Kit de Música" + "fi" "Music Kit" + "ua" "Музичний набір" + "es" "Kit de Música" + "ar" "إعدادات مجموعة الموسيقى" + } + "Medkit" + { + "ru" "Аптечка" + "en" "Medic Kit" + "pt" "Kit Médico" + "fi" "Lääkkeet" + "ua" "Аптечка" + "es" "Kit Medico" + "ar" "عدة طبية" + } + "MuteGagSilence" + { + "ru" "Чат/Микрофон" + "en" "Mute/Gag/Silence" + "pt" "Mute/Gag/Silence" + "fi" "Chatti/Mikrofoni" + "ua" "Чат/Мікрофон" + "es" "Mute/Gag/Silence" + "ar" "كتم الصوت / الكمامة / الصمت" + } + "Money" + { + "ru" "Деньги" + "en" "Money" + "pt" "Dinheiro" + "fi" "Raha" + "ua" "Гроші" + "es" "Dinero" + "ar" "مال" + } + "mvp" + { + "ru" "MVP значки" + "en" "MVP" + "pt" "MVP" + "fi" "MVP merkit" + "ua" "MVP значки" + "es" "MVP" + "ar" "أفضل لاعب" + } + "NoRecoil" + { + "ru" "Стрельба без отдачи" + "en" "No Recoil" + "fi" "Kuvaaminen ilman palautusta" + "ua" "Стрілянина без віддачі" + "es" "Sin retroceso" + "ar" "لا نكص" + } + "NoSelfDamage" + { + "ru" "Нет урона от себя" + "en" "No Self Damage" + "fi" "Ei vaurioita itseään" + "ua" "Без самопошкодження" + "es" "Sin autolesiones" + "ar" "لا ضرر على النفس" + } + "NEON" + { + "ru" "Неон" + "en" "Neon" + "pt" "Neon" + "fi" "Neon" + "ua" "Неон" + "es" "Neón" + "ar" "نيون" + } + "Non_Lethal_Damage" + { + "ru" "Не смертельный урон" + "en" "No Lethal Damage" + "fi" "Ei tappavaa vahinkoa" + "ua" "Не смертельна шкода" + "es" "Daño no letal" + "ar" "ضرر غير مميت" + } + "NEON_MENU" + { + "ru" "Цвет неона" + "en" "Color Neon" + "pt" "Cor do Neon" + "fi" "Neon Väri" + "ua" "Колір неону" + "es" "Color neón" + "ar" "لون النيون" + } + "nightvision" + { + "ru" "Ночное зрение" + "en" "Night Vision" + "pt" "Visão Noturna" + "fi" "Yö visio" + "ua" "Нічне бачення" + "es" "Vision nocturna" + "ar" "رؤية ليلية" + } + "NoFallDamage" + { + "ru" "Нет урона от мира" + "en" "No Fall Damage" + "fi" "Ei vaurioita maailmalta" + "ua" "Без шкоди від світу" + "es" "No hay daño por caida" + "ar" "لا ضرر السقوط" + } + "OOImmunity" + { + "ru" "Иммунитет от ограничения оружия" + "en" "Weapon restriction immunity" + "fi" "Gun limit immunity" + "ua" "Імунітет від обмеження зброї" + "es" "Inmunidad de restricción de armas" + "ar" "حصانة تقييد السلاح" + } + "paint" + { + "ru" "Рисовалка" + "en" "Paint" + "pt" "Paint" + "fi" "Maalaus" + "ua" "Малювалка" + "es" "Pintar" + "ar" "رسم" + } + "paint_menu" + { + "ru" "Настройка рисовалки" + "en" "Paint Menu" + "pt" "Paint Menu" + "fi" "Maalaus Menu" + "ua" "Налаштування малювалки" + "es" "Menú de pintura" + "ar" "رسم قائمة طعام" + } + "Parachute" + { + "ru" "Парашют" + "en" "Parachute" + "pt" "Paraquedas" + "fi" "Laskuvarjo" + "ua" "Парашут" + "es" "Paracaídas" + "ar" "المظلة" + } + "Pets_Display" + { + "ru" "Питомцы" + "en" "Pets" + "pt" "Pets" + "fi" "Lemmikkieläimet" + "ua" "Вихованці" + "es" "Mascotas" + "ar" "حيوان اليف" + } + "PaintBall" + { + "ru" "Пейнтбол" + "en" "Paintball" + "pt" "Paintball" + "fi" "Paintball" + "ua" "Пейнтбол" + "es" "Paintball" + "ar" "كرات الطلاء" + } + "Pets_Title" + { + "ru" "Выбор питомца" + "en" "Choosing Pet" + "pt" "Escolha um Pet" + "fi" "Lemmikkien valinta" + "ua" "Обрати вихованця" + "es" "Elegir una mascota" + "ar" "اختيار حيوان أليف" + } + "Pets_Disable" + { + "ru" "Выключить" + "en" "Disable" + "pt" "Desabilitar" + "fi" "Sammuta" + "ua" "Вимкнути" + "es" "Inhabilitar" + "ar" "تعطيل" + } + "QuickDefuse" + { + "ru" "Быстрое обезвреживание" + "en" "Quick Defuse" + "fi" "Nopea Purkaus" + "ua" "Швидке знешкодження" + "es" "Desactivación rápida" + "ar" "تحييد سريع" + } + "QHR" + { + "ru" "Быстрое спасение заложников" + "en" "Quick Hostage Rescue" + "fi" "Nopean panttivangien pelastus" + "ua" "Швидке рятування заручників" + "es" "Rescate rápido de rehenes" + "ar" "الإنقاذ السريع للرهائن" + } + "RegenArmor" + { + "ru" "Регенерация брони" + "en" "Regeneration Armor" + "fi" "Suojaliivin uudistaminen" + "ua" "Регенерація броні" + "es" "Armadura de regeneración" + "ar" "تجديد درع" + } + "Radar-Invis" + { + "ru" "Инвиз на радаре" + "en" "Radar Invis" + "fi" "Inviz tutkalla" + "ua" "Невидимість на радарі" + "es" "Radar Invis" + "ar" "رادار إينفيس" + } + "Respawn" + { + "ru" "Возрождение" + "en" "Respawn" + "fi" "Uudestisyntyminen" + "ua" "Відродження" + "es" "Reaparecer" + "ar" "إعادة" + } + "RegenHP" + { + "ru" "Регенерация здоровья" + "en" "HP Regeneration" + "fi" "Terveyden uudistaminen" + "ua" "Регенерація здоров'я" + "es" "Regeneración HP" + "ar" "تجديد الصحة" + } + "ReceivedDamage" + { + "ru" "Получаемый урон" + "en" "Received Damage" + "fi" "Aiheutuvat vahingot" + "ua" "Отримана шкода" + "es" "Daño recibido" + "ar" "تلقى الضرر" + } + "RADAR" + { + "ru" "Невидимость на радаре" + "en" "Hide in Radar" + "pt" "Esconder no Radar" + "fi" "Näkymättömyys tutka" + "ua" "Невидимість на радарі" + "es" "Esconderse en el radar" + "ar" "اختبئ في الرادار" + } + "RANK" + { + "ru" "Настройка ранга" + "en" "Rank Settings" + "fi" "Rank Asetukset" + "ua" "Налаштування рангу" + "es" "Configuraciones de rango" + "ar" "إعدادات الترتيب" + } + "RECEIVED_DAMAGE" + { + "ru" "Получаемый урон" + "en" "Received Damage" + "fi" "Aiheutuvat vahingot" + "ua" "Отримана шкода" + "es" "Daño recibido" + "ar" "تلقى الضرر" + } + "skybox" + { + "ru" "Небо" + "en" "Skybox" + "pt" "SkyBox" + "fi" "Taivas" + "ua" "Небо" + "es" "Skybox" + "ar" "سكايبوكس" + } + "skybox_share" + { + "ru" "Поделиться SkyBox'ом" + "en" "Share SkyBox" + "pt" "Compartilhar SkyBox" + } + "SmokeColors" + { + "ru" "Цвет дыма" + "en" "Smoke Color" + "pt" "Cor da Smoke" + "fi" "Savun väri" + "ua" "Колір диму" + "es" "Color de humo" + "ar" "لون الدخان" + } + "SWGM_VIP_Give" + { + "ru" "Вы получили VIP за вступление в нашу Steam группу." + "en" "You got VIP because you join our Steam group." + "fi" "Sait VIP-jäsenen Steam-ryhmään liittymisestä." + "ua" "Ви отримали VIP за приєднання до нашої групи Steam." + "es" "Tienes VIP porque te unes a nuestro grupo Steam." + "ar" "لقد حصلت على VIP لأنك انضممت إلى مجموعة Steam الخاصة بنا." + } + "SWGM_VIP_Take" + { + "ru" "Вы потеряли VIP, потому что вышли из нашей Steam группы." + "en" "You lost VIP because you leave our Steam group." + "fi" "Menetit VIP, koska jätit Steam-ryhmämme." + "ua" "Ви втратили VIP, тому що покинули нашу групу Steam." + "es" "Perdiste VIP porque dejaste nuestro grupo Steam." + "ar" "لقد فقدت VIP لأنك تركت مجموعة Steam الخاصة بنا." + } + "Skins" + { + "ru" "Скины" + "en" "Skins" + "pt" "Skins" + "fi" "Skinit" + "ua" "Персонаж" + "es" "Piel" + "ar" "بشرة" + } + "Skins_Menu" + { + "ru" "Скины Mеню" + "en" "Skins Menu" + "pt" "Menu Skins" + "fi" "Skini Valikko" + "ua" "Меню Cкінів" + "es" "Menú Skins" + "ar" "اختيار الجلد" + } + "Speed" + { + "ru" "Повышенная скорость" + "en" "Speed" + "fi" "Nopeus" + "ua" "Підвищена швидкість" + "es" "Velocidad" + "ar" "سرعة" + } + "SmokeColors_MENU" + { + "ru" "Выбрать цвет дыма" + "en" "Select Smoke Color" + "fi" "Valitse savun väri" + "ua" "Обрати колір диму" + "es" "Seleccionar color de humo" + "ar" "حدد لون الدخان" + } + "status" + { + "ru" "Показ премиум статуса" + "en" "Show Pemium Status" + "fi" "Näytetään premium-tila" + "ua" "Показ преміум статусу" + "es" "Mostrar estado premium" + "ar" "عرض حالة قسط" + } + "Showdamagehud" + { + "ru" "Показ урона (HUD)" + "en" "Show Damage (HUD)" + "fi" "Vahinkonäyttö (HUD)" + "ua" "Показ пошкоджень (HUD)" + "es" "Mostrar daño (HUD)" + "ar" "إظهار الضرر (HUD)" + } + "ShowDamageExtended" + { + "ru" "Показ урона" + "en" "Show Damage" + "fi" "Näytetään vaurioita" + "ua" "Показ пошкоджень" + "es" "Mostrar daño" + "ar" "عرض الضرر" + } + "Spawn_Effects" + { + "ru" "Эффекты при возрождении" + "en" "Spawn Effects" + "fi" "Uudestisyntymisen effektit" + "ua" "Ефекти при відродженні" + "es" "Efectos de engendro" + "ar" "تأثيرات النشر" + } + "Sparks" + { + "en" "Sparks" + "ru" "Искры" + "fi" "Kipinät" + "ua" "Іскри" + "es" "Chispas" + "ar" "شرارات" + } + "Switchteam" + { + "ru" "Изменить команду" + "en" "Switch Team" + "fi" "Muuta komentoa" + "ua" "Змінити команду" + "es" "Cambiar de equipo" + "ar" "فريق التبديل" + } + "Survivability" + { + "ru" "Выживаемость" + "en" "Survivability" + "fi" "Eloonjäämisaste" + "ua" "Життєздатність" + "es" "Supervivencia" + "ar" "البقاء على قيد الحياة" + } + "ShowDamage" + { + "ru" "Показ урона" + "en" "Displaying Damage" + "fi" "Näytetään vahinkopaneeli" + "ua" "Показ пошкодження" + "es" "Mostrar daño" + "ar" "عرض الضرر" + } + "Trails" + { + "ru" "Трейлы" + "en" "Trails" + "fi" "Jäljet" + "ua" "Трейли" + "es" "Caminos" + "ar" "الممرات" + } + "Trails_MENU" + { + "ru" "Выбор трейла" + "en" "Select Trail" + "fi" "Jäljen Valitseminen" + "ua" "Вибір трейлу" + "es" "Selección de caminos" + "ar" "اختيار الممر" + } + "Tracers" + { + "ru" "Трасеры" + "en" "Tracers" + "fi" "Merkkiaineet" + "ua" "Трасери" + "es" "Trazadores" + "ar" "الراسمات" + } + "Tracers_MENU" + { + "ru" "Цвет трасеров" + "en" "Color Tracers" + "fi" "Merkkiaineiden väri" + "ua" "Колір трасерів" + "es" "Trazadores de color" + "ar" "متتبعات اللون" + } + "Tagrenade" + { + "ru" "Таговая граната" + "en" "TA Grenade" + "pt" "TA Grenade" + "fi" "Tag Granaattiomena" + "ua" "Tag граната" + "es" "Tagrenade" + "ar" "تاجريناد" + } + "TopIcon" + { + "ru" "VIP Mодель над головой" + "en" "VIP Model over the head" + "fi" "VIP Malli päässä" + "ua" "VIP Mодель над головою" + "es" "Modelo VIP sobre la cabeza" + "ar" "نموذج VIP فوق الرأس" + } + "ThrowingKnives" + { + "ru" "Метательные ножи" + "en" "Throwing Knives" + "fi" "Heitto veitset" + "ua" "Метальні ножі" + "es" "Lanzando cuchillos" + "ar" "رمي السكاكين" + } + "TEAMMATES_HEAL" + { + "ru" "Лечение союзников" + "en" "Teammates Heal" + "fi" "Ally-hoito" + "ua" "Лікування союзників" + "es" "Compañeros de equipo sanan" + "ar" "زملاء الفريق شفاء" + } + "Tag" + { + "ru" "Тег" + "en" "TAG" + "fi" "TAG" + "ua" "Тег" + "es" "Etiqueta" + "ar" "بطاقة شعار" + } + "Tag_Menu" + { + "ru" "Настройки тега" + "en" "TAG Settings" + "fi" "TAG Asetukset" + "ua" "Налаштування тегу" + "es" "Configuraciones de etiqueta" + "ar" "إعدادات العلامات" + } + "UnlimitedAmmo" + { + "ru" "Бесконечные Патроны" + "en" "Unlimited Ammo" + "fi" "Rajoittamattomat ammukset" + "ua" "Нескінченні набої" + "es" "Munición ilimitada" + "ar" "إعدادات العلامات" + } + "UnlimPrimaryAmmo" + { + "ru" "Бесконечные патроны в основной обойме" + "en" "Infinite Ammo in the main clip" + "fi" "Loputtomasti ammuksia pääasiassa aseeseen" + "ua" "Нескінченні набої в основній обоймі" + "es" "Munición infinita en el clip principal" + "ar" "الذخيرة اللانهائية في المقطع الرئيسي" + } + "UnlimReserveAmmo" + { + "ru" "Бесконечные патроны в запасе" + "en" "Infinite ammo in stock" + "fi" "Loputtomasti ammuksia lippaaseen" + "ua" "Нескінченні набої у запасі" + "es" "Munición infinita en stock" + "ar" "ذخيرة لانهائية في المخزون" + } + "VIP_SWAPPOSITION" + { + "ru" "Смена позиций" + "en" "Swap Positions" + "fi" "Asemien muuttaminen" + "ua" "Зміна позицій" + "es" "Posiciones de intercambio" + "ar" "مراكز المبادلة" + } + "VIP_ICE" + { + "ru" "Заморозка" + "en" "Freeze" + "fi" "Jäätymispiste" + "ua" "Заморозка" + "es" "Congelar" + "ar" "تجميد" + } + "VIP_PLAYERCOLOR" + { + "ru" "Смена цветов" + "en" "Color Changing" + "fi" "Väri muuttuu" + "ua" "Зміна кольорів" + "es" "Cambio de color" + "ar" "تغيير اللون" + } + "VIP_DROPWEAPON" + { + "ru" "Выбить оружие" + "en" "Drop Weapon" + "fi" "Katkaise aseet" + "ua" "Вибити зброю" + "es" "Soltar arma" + "ar" "إسقاط السلاح" + } + "Vampirism" + { + "ru" "Вампиризм" + "en" "Vampirism" + "pt" "Vampirismo" + "fi" "Vampirismi" + "ua" "Вампіризм" + "es" "Vampirismo" + "ar" "مصاص دماء" + } + "ValveGloves" + { + "ru" "Перчатки (Valve)" + "en" "Gloves (Valve)" + "pt" "Luvas (Valve)" + "fi" "Käsineet (Valve)" + "ua" "Рукавички (Valve)" + "es" "Guantes (Valve)" + "ar" "القفازات (Valve)" + } + "ValveGloves_MENU" + { + "ru" "Выбор перчаток (Valve)" + "en" "Select Gloves (Valve)" + "pt" "Selecione Luvas (Valve)" + "fi" "Käsineiden valinta (Valve)" + "ua" "Вибір рукавичок (Valve)" + "es" "Seleccionar guantes (Valve)" + "ar" "حدد القفازات (Valve)" + } + "VoiceSoundPlayer" + { + "ru" "Фразы" + "en" "Voice Sound" + "fi" "Äänen ääni" + "ua" "Фрази" + "es" "Sonido de voz" + "ar" "صوت الصوت" + } + "WaterEffect" + { + "ru" "Брызги воды" + "en" "Water Splashing" + "fi" "Veden roiskeita" + "ua" "Бризки води" + "es" "Salpicaduras de agua" + "ar" "رش الماء" + } + "Weaponpack" + { + "ru" "Комплект оружий | WP" + "en" "Weapon Pack | WP" + "pt" "Pack de Armas | WP" + "fi" "Aseiden setti | WP" + "ua" "Комплект зброї | WP" + "es" "Set de armas | WP" + "ar" "مجموعة سلاح } WP" + } + "WeaponJumping" + { + "ru" "Прыжок от выстрела" + "en" "Jump from the shot" + "fi" "Shot Jump" + "ua" "Стрибок від пострілу" + "es" "Salta desde el tiro" + "ar" "القفز من اللقطة" + } + "ExoJumpBoots" + { + "ru" "Exo Jump ботинки" + "en" "Exo Jump Boots" + "pt" "Botas Exo Jump" + "fi" "Exo Jump stövlar" + "ua" "Exo Jump черевики" + "es" "Botas Exo Jump" + "ar" "القفز إكسو الروبوتات" + } + "BuyTeamWeapon" + { + "ru" "Покупка оружия команды" + "en" "Buy Team Weapon" + "fi" "Köp lagsvapen" + "ua" "Купівля командої зброї" + "es" "Comprar arma de equipo" + "ar" "شراء سلاح الفريق" + } + "Bumpmine" + { + "ru" "Толчковые мины" + "en" "Bump Mine" + "pt" "Bump Mine" + "fi" "Bump Min" + "ua" "Відштовхувальні міни" + "es" "Golpear la mía" + "ar" "عثرة الألغام" + } + "CashBack" + { + "ru" "Возврат денег" + "en" "Cash Back" + "fi" "Pengar tillbaka" + "ua" "Повернення грошей" + "es" "Devolución de dinero" + "ar" "كاش باك" + } + "BHOP" + { + "ru" "Банихоп" + "ua" "Банніхоп" + "en" "BunnyHop" + "pt" "BunnyHop" + "fi" "BunnyHop" + "es" "BunnyHop" + "ar" "بانيهوب" + } + "TF2_HalloweenFootprints" + { + "ru" "Следы под ногами (Хэллоуин)" + "en" "Footsteps (Halloween)" + "fi" "Fotspår (Halloween)" + "ua" "Сліди під ногами (Хеллоуїн)" + "es" "Pasos (Halloween)" + "ar" "خطى (هالوين)" + } + "TF2_DistortedHalloweenVoice" + { + "ru" "Искажённый голос (Хэллоуин)" + "en" "Distorted Voice (Halloween)" + "fi" "Förvrängd röst (Halloween)" + "ua" "Спотворений голос (Хеллоуїн)" + "es" "Voz distorsionada (Halloween)" + "ar" "صوت مشوه (الهالوين)" + } + "ShowTriggers" + { + "ru" "Показ триггеров" + "en" "Show Triggers" + "fi" "Visa triggers" + "ua" "Показати тригери" + "es" "Mostrar disparadores" + "ar" "إظهار المشغلات" + } + "color_management" + { + "ru" "Управление цветами" + "en" "Color Management" + "fi" "Färghantering" + "ua" "Керування кольорами" + "es" "Manejo del color" + "ar" "إدارة الألوان" + } + "AutoBandage" + { + "ru" "Авто бинтование" + "en" "Auto Bandage" + "fi" "Automaattinen Sidonta" + "ua" "Автоматичне бинтування" + "es" "Vendaje automático" + "ar" "الضمادة التلقائية" + } + "Shield" + { + "ru" "Щит" + "en" "Shield" + "fi" "Skydda" + "ua" "Щит" + "es" "Proteger" + "ar" "درع" + } + "ReWeapon" + { + "ru" "Восстановление боеприпасов" + "en" "Switch Weapon" + "fi" "Byt vapen" + "ua" "Відновлення боєприпасів" + "es" "Cambiar de arma" + "ar" "تبديل السلاح" + } + "FakeRanks" + { + "ru" "Фейк Ранг:" + "en" "Fake Ranks:" + "fi" "Fake Ranks:" + "ua" "Фейк Ранг:" + "es" "Fake Ranks:" + "ar" "الرتب الوهمية:" + } + + "FakeRanks_Menu" + { + "ru" "Меню Фейк-Ранга:" + "en" "Fake Ranks Menu:" + "fi" "Fake Ranks Menyn:" + "ua" "Меню Фейк Рангу:" + "es" "Menú FakeRanks:" + "ar" "قائمة الرتب الوهمية:" + } + "Instruments" + { + "ru" "Инструменты" + "en" "Instruments" + "fi" "instrument" + "ua" "Інструменти" + "es" "Instrumentos" + "ar" "الادوات" + } + "jump_effect" + { + "en" "Jump Effect" + "ru" "Эффект от прыжков" + "fi" "Hoppeffekt" + "ua" "Ефект від стрибків" + "es" "Efecto de salto" + "ar" "تأثير القفز" + } + "nade_wave_effect" + { + "en" "Nade Wave Effect" + "ru" "Волны от гранат" + "fi" "Nade våg effekt" + "ua" "Хвилі від гранат" + "es" "Efecto de onda Nade" + "ar" "تأثير موجة نادى" + } + "HALO" + { + "ru" "Нимб" + "en" "Halo" + "fi" "Halo" + "ua" "Німб" + "es" "Aureola" + "ar" "هالو" + } + "HALO_M" + { + "ru" "Цвет нимба" + "en" "Color Halo" + "fi" "Halo väri" + "ua" "Колір німбу" + "es" "Aureola de color" + "ar" "هالة ملونة" + } + "HALO_BLACK" + { + "ru" "Нимб Черный" + "en" "Halo Black" + "fi" "Halo Musta" + "ua" "Німб Чорний" + "es" "Aureola Svarti" + "ar" "هالو بلاك" + } + "VIP_SOUNDEFFECTS" + { + "ru" "Звуки" + "en" "Sounds" + "fi" "äänet" + "ua" "Звуки" + "es" "Suena" + "ar" "اصوات" + } + "NVD" + { + "ru" "ПНВ" + "en" "NVD" + "fi" "NVD" + "ua" "ПНБ" + "es" "NVD" + "ar" "NVG" + } + "bullet_effect" + { + "ru" "Эффект от выстрела" + "en" "Bullet Effect" + "fi" "Luodin vaikutus" + "ua" "Ефект від пострілу" + "es" "Efecto Bala" + "ar" "تأثير رصاصة" + } + "DuckSpeed" + { + "ru" "Отключить замедление скорости при приседании" + "en" "Disable slowdown when use duck" + "fi" "Poista hidastus käytöstä ankkaa käytettäessä" + "ua" "Відключити уповільнення швидкості під час присідання" + "es" "Desactiva la ralentización cuando usas pato" + "ar" "تعطيل التباطؤ عند استخدام البطة" + } + "vip_name_color" + { + "ru" "Цвет никнейма" + "en" "Client Username Color" + "fi" "Asiakkaan käyttäjänimen väri" + "ua" "Колір нікнейму" + "es" "Color del nombre de usuario del cliente" + "ar" "لون اسم مستخدم العميل" + } + "vip_message_color" + { + "ru" "Цвет сообщения" + "en" "Client Message Color" + "fi" "Asiakkaan viestin väri" + "ua" "Колір повідомлення" + "es" "Color del mensaje del cliente" + "ar" "لون رسالة العميل" + } + "vip_prefix" + { + "ru" "Префикс" + "en" "Client Prefix" + "fi" "Asiakkaan etuliite" + "ua" "Префікс" + "es" "Prefijo de cliente" + "ar" "بادئة العميل" + } + "vip_prefix_color" + { + "ru" "Цвет префикса" + "en" "Client Prefix Color" + "fi" "Asiakkaan etuliitteen väri" + "ua" "Колір префіксу" + "es" "Color del prefijo del cliente" + "ar" "لون بادئة العميل" + } + "vip_chat_nameco" + { + "ru" "Цвет никнейма" + "en" "Client Username Color" + "fi" "Asiakkaan käyttäjänimen väri" + "ua" "Колір нікнейму" + "es" "Color del nombre de usuario del cliente" + "ar" "لون اسم مستخدم العميل" + } + "vip_chat_msgco" + { + "ru" "Цвет сообщения" + "en" "Client Message Color" + "fi" "Asiakkaan viestin väri" + "ua" "Колір повідомлення" + "es" "Color del mensaje del cliente" + "ar" "لون رسالة العميل" + } + "vip_chat_prefix" + { + "ru" "Префикс" + "en" "Client Prefix" + "pt" "Prefixo do Client" + "fi" "Asiakkaan etuliite" + "ua" "Префікс" + "es" "Prefijo de cliente" + "ar" "بادئة العميل" + } + "vip_chat_prefixco" + { + "ru" "Цвет префикса" + "en" "Client Prefix Color" + "fi" "Asiakkaan etuliitteen väri" + "ua" "Колір префіксу" + "es" "Color del prefijo del cliente" + "ar" "لون بادئة العميل" + } + "advanced_message" + { + "ru" "Продвинутые сообщения" + "en" "Advanced Message" + "fi" "Lisäasetukset-viesti" + "ua" "Розширені повідомлення" + "es" "Mensaje avanzado" + "ar" "رسالة متقدمة" + } + "HintShowDamage" + { + "ru" "Отображение урона" + "en" "Damage Display" + "fi" "Skada display" + "ua" "Відображення пошкоджень" + "es" "Pantalla de daños" + "ar" "عرض الضرر" + } + "Rainbow_Player" + { + "ru" "Радужная модель" + "en" "Rainbow Pattern" + "fi" "Sateenkaarikuvio" + "ua" "Райдужна модель" + "es" "Padrão de arco-íris" + "ar" "نمط قوس قزح" + } + "RoundEndBhop" + { + "ru" "Банихоп в конце раунда" + "en" "BunnyHop at the end of the round" + "pt" "BunnyHop no final do round" + "fi" "BunnyHop kierroksen lopussa" + "ua" "Баніхоп у кінці раунду" + "es" "BunnyHop al final de la ronda" + "ar" "بانيهوب في نهاية الجولة" + } + "Xscope" + { + "ru" "Улучшенный снайперский прицел" + "en" "Advanced Sniper Sight" + "fi" "Advanced sniper näky" + "ua" "Покращений снайперський приціл" + "es" "Mira de francotirador avanzada" + "ar" "مشهد قناص متقدم" + } + "AmmoWeapons" + { + "ru" "Расширенная обойма" + "en" "Extended Clip" + "fi" "Laajennettu leike" + "ua" "Розширена обойма" + "es" "Clip extendido" + "ar" "كليب موسع" + } + "Invis" + { + "ru" "Прозрачность" + "en" "Invis" + "fi" "Läpikuultavuus" + "ua" "Прозорість" + "es" "Transparencia" + "ar" "إينفيس" + } + "Equipments" + { + "ru" "Экипировка" + "en" "Equipments" + "pt" "Equipamentos" + "fi" "Tamineet" + "ua" "Спорядження" + "es" "Transparencia" + "ar" "المعدات" + } + "EquipmentsMenu" + { + "ru" "Настройка экипировки" + "en" "Equipment Setting" + "pt" "Configuração do Equipamento" + "fi" "Laitteiden asettaminen" + "ua" "Налаштування екіпіровки" + "es" "Configuración del equipo" + "ar" "إعداد المعدات" + } + "Icon" + { + "ru" "Иконка" + "en" "Icon" + "pt" "Ícone" + "fi" "Ikoni" + "ua" "Іконка" + "es" "Icono" + "ar" "أيقونة" + } + "NadeModels" + { + "ru" "Модели гранат" + "en" "Grenade Models" + "pt" "Modelos de Granada" + "fi" "Kranaatti malli" + "ua" "Моделі гранат" + "es" "Modelos granate" + "ar" "نماذج القنابل اليدوية" + } + "NadeModels_Menu" + { + "ru" "Настройка моделей гранат" + "en" "Grenade Models Settings" + "pt" "Configuração dos Modelos de Granada" + "fi" "Kranaatti mallin asetukset" + "ua" "Налаштування моделей гранат" + "es" "Configuración de modelos granadas" + "ar" "إعدادات نماذج القنبلة" + } + "NoDamageVelocity" + { + "ru" "Не терять скорость, когда в вас стреляют" + "en" "Don't lose speed when you are being shot at" + "fi" "Älä menetä nopeutta, kun olet ammuttu" + "ua" "Не втрачати швидкість, коли по вас стріляють" + "es" "No pierdas la velocidad cuando te disparan" + "ar" "لا تفقد السرعة عندما يتم إطلاق النار عليك" + } + "X16Walls" + { + "ru" "Прострел стен" + "en" "Wall Shot" + "fi" "Seinä laukaus" + "ua" "Простріл стін" + "es" "Muro de tiro" + "ar" "طلقة الجدار" + } + "ReloadProgressBar" + { + "ru" "Индикатор выполнения перезарядки" + "en" "Reload Progress Bar" + "fi" "Latauksen ilmaisin" + "ua" "Індикатор виконання перезарядки" + "es" "Indicador de progreso de recarga" + "ar" "إعادة تحميل شريط التقدم" + } + "Killcredits" + { + "ru" "Кредиты за убийство" + "en" "Credits for Kill" + "pt" "Créditos por Kill" + "fi" "Hyvitykset tappaa" + "ua" "Кредити за вбивство" + "es" "Créditos por matar" + "ar" "قروض للقتل" + } + "HEADcredits" + { + "ru" "Кредиты за HS" + "en" "Credits for HS" + "pt" "Créditos por HS" + "fi" "Hyvitykset HS: lle" + "ua" "Кредити за HS" + "es" "Créditos para HS" + "ar" "قروض لضربة رأس" + } + "KNIFEcredits" + { + "ru" "Кредиты за нож" + "en" "Credits for Knife" + "pt" "Créditos por Faca" + "fi" "Laajuus veitsi" + "ua" "Кредити за ніж" + "es" "Créditos para el cuchillo" + "ar" "قروض للسكين" + } + "two_guns" + { + "ru" "Два оружия в одном слоте" + "en" "Two weapons in one slot" + "fi" "Kaksi asetta yhdessä korttipaikassa" + "ua" "Дві зброї в одному слоті" + "es" "Dos armas en una ranura" + "ar" "سلاحان في فتحة واحدة" + } + "Fists" + { + "ru" "Кулаки" + "en" "Fists" + "pt" "Punhos" + "fi" "Nyrkki" + "ua" "Кулаки" + "es" "Puños" + "ar" "القبضات" + } + "mu_GiveLoots" + { + "ru" "Дополнительные улики" + "en" "Additional Evidence" + "pt" "Evidência Adicional" + "fi" "Lisätodisteet" + "ua" "Додаткові докази" + "es" "Evidencia adicional" + "ar" "أدلة إضافية" + } + "ColoredSmoke" + { + "ru" "Цветной дым" + "en" "Colored Smoke" + "pt" "Smoke Colorida" + "fi" "Värillinen Savu" + "ua" "Кольоровий дим" + "es" "Humo de color" + "ar" "دخان ملون" + } + "SmokeColor" + { + "ru" "Указать цвет" + "en" "Select Color" + "pt" "Selecione a Cor" + "fi" "Valitse väri" + "ua" "Вказати колір" + "es" "Seleccionar color" + "ar" "إختر لون" + } + "Ban" + { + "ru" "Забанить игрока" + "en" "Ban Player" + "pt" "Banir Player" + "fi" "Kieltää pelaaja" + "ua" "Забанити гравця" + "es" "Prohibir al jugador" + "ar" "حظر لاعب" + } + "Agents" + { + "ru" "Агенты" + "en" "Agents" + "pt" "Agentes" + "fi" "Agentti" + "ua" "Агенти" + "es" "Agente" + "ar" "عملاء" + } + "VIP_Agents_M" + { + "ru" "Меню Агентов" + "en" "Agents Menu" + "pt" "Menu Agentes" + "fi" "Agenttien Valikko" + "ua" "Меню Агентів" + "es" "Menú Agentes" + "ar" "قائمة الوكلاء" + } + "resimm" + { + "ru" "Иммунитет от ограничения оружия" + "en" "Weapon Restriction Immunity" + "fi" "Gun limit immunity" + "ua" "Імунітет від обмеження зброї" + "es" "Inmunidad de restricción de armas" + "ar" "حصانة تقييد السلاح" + } + "switch_speed" + { + "ru" "Быстрая смена оружия" + "en" "Quick change of weapons" + "fi" "Nopea aseiden vaihto" + "ua" "Швидка зміна зброї" + "es" "Cambio rápido de armas" + "ar" "تغيير سريع للأسلحة" + } + "Disable" + { + "ru" "Выключить" + "en" "Disable" + "pt" "Desabilitar" + "fi" "Sammuta" + "ua" "Вимкнути" + "es" "Inhabilitar" + "ar" "تعطيل" + } + "FOV" + { + "ru" "Угол обзора" + "en" "Viewing Angle | FOV" + "pt" "Ângulo de Visão | FOV" + "fi" "Katselukulma" + "ua" "Кут огляду" + "es" "Ángulo de visión" + "ar" "زاوية عرض" + } + "ChatSound" + { + "ru" "Звук чата" + "en" "Chat message sound" + "pt" "Som da mensagem do bate-papo" + "fi" "Keskusteluviestin ääni" + "ua" "Звук чату" + "es" "Chat mensaje sonido" + "ar" "رسالة الدردشة الصوتية" + } + "lr_select_fakerank" + { + "ru" "Выбрать ранги" + "en" "Select Ranks" + "pt" "Selecionar Ranks" + "fi" "Valitse rivit" + "ua" "Вибрати ранги" + "es" "Seleccionar rangos" + "ar" "اختر الرتب" + } + "VIP_NO_LIMIT" + { + "#format" "{1:s}" + "ru" "Лимит использования {1} исчерпан!" + "en" "The limit of use {1} has been reached!" + "pt" "O limite de uso {1} foi atingido!" + "fi" "Käyttöraja {1} on saavutettu!" + "ua" "Межа використання {1} досягнута!" + "es" "¡Se ha alcanzado el límite de uso {1}!" + "ar" "تم الوصول إلى حد الاستخدام {1}!" + } + "VIP_END_TIME_USE" + { + "#format" "{1:s}" + "ru" "Время действия {1} закончилось!" + "en" "Action time {1} is over!" + "pt" "O tempo de ação {1} acabou!" + "fi" "Toiminta-aika {1} on ohi!" + "ua" "Час дії {1} закінчився!" + "es" "¡El tiempo de acción {1} ha terminado!" + "ar" "وقت العمل {1} هو أكثر!" + } + "Setclass" + { + "ru" "Смена Классов" + "en" "Set Class" + "pt" "Definir Classe" + "fi" "Luokanvaihto" + "ua" "Зміна КЛАСІВ" + "es" "Cambio De Clases" + "ar" "تغيير الفصول" + } + "sprays" + { + "ru" "Спрей" + "en" "Spray" + "pt" "Spray" + "fi" "Spray" + "ua" "Спрей" + "es" "Spray" + "ar" "رذاذ" + } + "sprays_menu" + { + "ru" "Выбор спрея" + "en" "Spray selection" + "pt" "Selecção da pulverização" + "fi" "Spray valinta" + "ua" "Вибір спрею" + "es" "Selección de Spray" + "ar" "رذاذ اختيار" + } + "AntiKnifeDamage" + { + "ru" "Защита от урона ножом" + "en" "Protection from knife damage" + "pt" "Proteção contra danos com uma faca" + "fi" "Suojaa veitsi vahinko" + "ua" "Захист від шкоди ножем" + "es" "Protección contra daños con cuchillo" + "ar" "الحماية من تلف السكين" + } + "ClanCreate" + { + "ru" "Возможность создавать клан" + "en" "Clan create permission" + "pt" "Clã criar permissão" + "fi" "Klaanin luontioikeus" + "ua" "Дозвіл на створення клану" + "es" "Permiso para crear clan" + "ar" "عشيرة إنشاء إذن" + } +} diff --git a/docs/issue_labels.md b/docs/issue_labels.md new file mode 100644 index 00000000..2a3c49cc --- /dev/null +++ b/docs/issue_labels.md @@ -0,0 +1,14 @@ +# Issue labels +- Issue + - `Priority` - the order in which a task should be done or a defect fixed. + - `P1 (High)` - Must be fixed as soon as possible, the defect is critical for the project. + - `P2 (Medium)` - Must be fixed; not critical, but requires a solution. + - `P3 (Low)` - Must be fixed; not critical and not urgent. + - `Severity` - how much the defect affects the application. + - `S1 (Blocker)` - The application doesn't work; further work with the system or its key features is impossible. Must be fixed for the system to function. + - `S2 (Critical)` - Key logic works incorrectly, a security hole, a server crash, or part of the system is broken with no workaround. Must be fixed to keep working with the key features. + - `S3 (Major)` - Part of the main logic works incorrectly. Not critical, or there is a workaround. + - `S4 (Minor)` - Doesn't break the logic; an obvious UI problem. + - `S5 (Trivial)` - Doesn't affect the logic; a hard-to-reproduce or barely visible UI problem, a third-party library/service problem, no effect on overall quality. + - `Question` + - `Suggestion` diff --git a/docs/update_modules.md b/docs/update_modules.md new file mode 100644 index 00000000..e0901a4c --- /dev/null +++ b/docs/update_modules.md @@ -0,0 +1,166 @@ +# Module update guide + +The update solves the following problems: +1. Proper loading/unloading of a module with SourceMod tools (`sm plugins load/reload/unload`) +2. Fixing the player spawn event: + - Fatal error when starting the module: `Native "VIP_HookClientSpawn" was not found` + - Warning when compiling the module: `warning 234: symbol "VIP_HookClientSpawn" is marked as deprecated: Use VIP_OnPlayerSpawn` + - Fatal error when compiling the module: ` error 017: undefined symbol "VIP_HookClientSpawn"` + +The following code is used as an example: +```cpp +// This is the unique feature name. It is different in every module +#define VIP_MODULE "module1" +// It may also look like this +new const String:g_sFeature[] = "module1"; + +public OnPluginStart() +{ + // code +} + +public VIP_OnVIPLoaded() +{ + VIP_RegisterFeature(VIP_MODULE, BOOL); +} +``` +1. If the unique feature name looks like this: +```cpp +#define VIP_MODULE "module1" +``` +Change it to +```cpp +new const String:g_sFeature[] = "module1"; +``` +Or, for the new syntax +```cpp +static const char g_sFeature[] = "module1"; +``` +2. Then replace every `VIP_MODULE` (it may be named differently) with `g_sFeature` +3. Find the function +```cpp +public OnPluginStart() +{ + // code +} +``` +And add the following at its end +```cpp +if(VIP_IsVIPLoaded()) +{ + VIP_OnVIPLoaded(); +} +``` +It should look like this: +```cpp +public OnPluginStart() +{ + // code + if(VIP_IsVIPLoaded()) + { + VIP_OnVIPLoaded(); + } +} +``` +If there is no `OnPluginStart()` function, add it: +```cpp +public OnPluginStart() +{ + if(VIP_IsVIPLoaded()) + { + VIP_OnVIPLoaded(); + } +} +``` +4. Next, find the function +```cpp +public OnPluginEnd() +{ + // code +} +``` +Most often it does not exist, so add it: +```cpp +public OnPluginEnd() +{ + // code (if there was any) + if(CanTestFeatures() && GetFeatureStatus(FeatureType_Native, "VIP_UnregisterFeature") == FeatureStatus_Available) + { + VIP_UnregisterFeature(g_sFeature); + } +} +``` + +After this, problem `#1` is solved. + +One more example, for modules with 2 features (like skins, trails, etc.): +- A toggle (on/off) feature +- A settings feature + +They look roughly like this: +```cpp +#define VIP_SKINS "Skins" +#define VIP_SKINS_MENU "Skins_Menu" + +public VIP_OnVIPLoaded() +{ + VIP_RegisterFeature(VIP_SKINS, STRING, _, OnToggleItem); + VIP_RegisterFeature(VIP_SKINS_MENU, _, SELECTABLE, OnSelectItem, _, OnDrawItem); +} +``` +To update this type of module, do the following: +1. Replace the feature declarations +```cpp +#define VIP_SKINS "Skins" +#define VIP_SKINS_MENU "Skins_Menu" +``` +with +```cpp +static const String:g_sFeature[][] = {"Skins", "Skins_Menu"}; +``` +Or, for the new syntax +```cpp +static const char g_sFeature[][] = {"Skins", "Skins_Menu"}; +``` +2. Replace every `VIP_SKINS` with `g_sFeature[0]` and `VIP_SKINS_MENU` with `g_sFeature[1]` +3. Modify/add `OnPluginStart()` +```cpp +public OnPluginStart() +{ + // Code, if there was any + if(VIP_IsVIPLoaded()) + { + VIP_OnVIPLoaded(); + } +} +``` +4. Modify/add `OnPluginEnd()` +```cpp +public OnPluginEnd() +{ + // code (if there was any) + if(CanTestFeatures() && GetFeatureStatus(FeatureType_Native, "VIP_UnregisterFeature") == FeatureStatus_Available) + { + VIP_UnregisterFeature(g_sFeature[0]); + VIP_UnregisterFeature(g_sFeature[1]); + } +} +``` + +The main thing is to follow this rule: +>Every ```VIP_RegisterFeature``` in ```VIP_OnVIPLoaded()``` must have a matching ```VIP_UnregisterFeature``` in ```OnPluginEnd()``` + +You can find more examples in the modules that have already been updated. + +To solve problem `#2`: +1. In `OnPluginStart()` find `VIP_HookClientSpawn(OnPlayerSpawn);` +2. Delete this line +3. Then find: +```cpp +public OnPlayerSpawn(iClient, iTeam, bool:bIsVIP) +``` +and replace it with +```cpp +public VIP_OnPlayerSpawn(iClient, iTeam, bool:bIsVIP) +``` + diff --git a/issue_info.md b/issue_info.md deleted file mode 100644 index 22941e7e..00000000 --- a/issue_info.md +++ /dev/null @@ -1,14 +0,0 @@ -# Issues labels | Метки проблем - - - `Приоритет (Priority)` - это атрибут, указывающий на очередность выполнения задачи или устранения дефекта. - - `Высокий приоритет P1 (High)` - Ошибка должна быть исправлена как можно быстрее, т.к. ее наличие является критической для проекта. - - `Средний приоритет P2 (Medium)` - Ошибка должна быть исправлена, ее наличие не является критичной, но требует обязательного решения. - - `Низкий приоритет P3 (Low)` - Ошибка должна быть исправлена, ее наличие не является критичной, и не требует срочного решения. - - `Серьезность (Severity)` - это атрибут, характеризующий влияние дефекта на работоспособность приложения. - - `Блокирующая ошибка S1 (Blocker)` - Блокирующая ошибка, приводящая приложение в нерабочее состояние, в результате которого дальнейшая работа с тестируемой системой или ее ключевыми функциями становится невозможна. Решение проблемы необходимо для дальнейшего функционирования системы. - - `Критическая ошибка S2 (Critical)` - Критическая ошибка, неправильно работающая ключевая логика, дыра в системе безопасности, проблема, приведшая к временному падению сервера или приводящая в нерабочее состояние некоторую часть системы, без возможности решения проблемы, используя другие входные точки. Решение проблемы необходимо для дальнейшей работы с ключевыми функциями системы. - - `Значительная ошибка S3 (Major)` - Значительная ошибка, часть основной логики работает некорректно. Ошибка не критична или есть возможность для работы с тестируемой функцией, используя другие входные точки. - - `Незначительная ошибка S4 (Minor)` - Незначительная ошибка, не нарушающая логику части приложения, очевидная проблема пользовательского интерфейса. - - `Тривиальная ошибка S5 (Trivial)` - Тривиальная ошибка, не касающаяся логики приложения, плохо воспроизводимая проблема, малозаметная посредствам пользовательского интерфейса, проблема сторонних библиотек или сервисов, проблема, не оказывающая никакого влияния на общее качество продукта. - - `Вопрос` - - `Предложение` diff --git a/update_modules.md b/update_modules.md deleted file mode 100644 index 7d19c006..00000000 --- a/update_modules.md +++ /dev/null @@ -1,166 +0,0 @@ -# Мануал по обновлению модулей - -Обновление позволяет решить такие проблемы: -1. Адекватная загрузка/выгрузка модуля средствами SourceMod (`sm plugins load/reload/unload`) -2. Исправление события возрождения игрока: - - Фатальная ошибка при запуске модуля: `Native "VIP_HookClientSpawn" was not found` - - Предупреждение при компиляции модуля: `warning 234: symbol "VIP_HookClientSpawn" is marked as deprecated: Use VIP_OnPlayerSpawn` - - Фатальная ошибка при компиляции модуля: ` error 017: undefined symbol "VIP_HookClientSpawn"` - -Для примера будет использован следующий код: -```cpp -// Это уникальное имя ф-и. Оно разное в каждом модуле -#define VIP_MODULE "module1" -// Может выглядеть так -new const String:g_sFeature[] = "module1"; - -public OnPluginStart() -{ - // код -} - -public VIP_OnVIPLoaded() -{ - VIP_RegisterFeature(VIP_MODULE, BOOL); -} -``` -1. Если уникальное имя ф-и выглядит так: -```cpp -#define VIP_MODULE "module1" -``` -То меняем на -```cpp -new const String:g_sFeature[] = "module1"; -``` -Или для нового синтаксиса -```cpp -static const char g_sFeature[] = "module1"; -``` -2. Затем заменяем все `VIP_MODULE` (может быть другим) на `g_sFeature` -3. Ищем функцию -```cpp -public OnPluginStart() -{ - // код -} -``` -И добавляем в её конце -```cpp -if(VIP_IsVIPLoaded()) -{ - VIP_OnVIPLoaded(); -} -``` -Должно получиться так: -```cpp -public OnPluginStart() -{ - // код - if(VIP_IsVIPLoaded()) - { - VIP_OnVIPLoaded(); - } -} -``` -Если же функции `OnPluginStart()` нет то дописываем её: -```cpp -public OnPluginStart() -{ - if(VIP_IsVIPLoaded()) - { - VIP_OnVIPLoaded(); - } -} -``` -4. Дальше ищем функцию -```cpp -public OnPluginEnd() -{ - // код -} -``` -Чаще всего её нет. Поэтом добавляем её: -```cpp -public OnPluginEnd() -{ - // код (если он был) - if(CanTestFeatures() && GetFeatureStatus(FeatureType_Native, "VIP_UnregisterFeature") == FeatureStatus_Available) - { - VIP_UnregisterFeature(g_sFeature); - } -} -``` - -После этого проблема `#1` будет решена. - -Еще 1 пример для модулей с 2-я функциями (на подобии скинов, трейлов и т.д.): -- Функция вкл/выкл -- Функция настройки - -Выглядят они примерно так: -```cpp -#define VIP_SKINS "Skins" -#define VIP_SKINS_MENU "Skins_Menu" - -public VIP_OnVIPLoaded() -{ - VIP_RegisterFeature(VIP_SKINS, STRING, _, OnToggleItem); - VIP_RegisterFeature(VIP_SKINS_MENU, _, SELECTABLE, OnSelectItem, _, OnDrawItem); -} -``` -Для обновления такого типа модулей делаем так: -1. Заменяем объявление ф-й -```cpp -#define VIP_SKINS "Skins" -#define VIP_SKINS_MENU "Skins_Menu" -``` -на -```cpp -static const String:g_sFeature[][] = {"Skins", "Skins_Menu"}; -``` -Или для нового синтаксиса -```cpp -static const char g_sFeature[][] = {"Skins", "Skins_Menu"}; -``` -2. Заменяем все `VIP_SKINS` на `g_sFeature[0]` и `VIP_SKINS_MENU` на `g_sFeature[1]` -3. Изменяем/добавляем `OnPluginStart()` -```cpp -public OnPluginStart() -{ - // Код если был - if(VIP_IsVIPLoaded()) - { - VIP_OnVIPLoaded(); - } -} -``` -4. Изменяем/добавляем `OnPluginEnd()` -```cpp -public OnPluginEnd() -{ - // код (если он был) - if(CanTestFeatures() && GetFeatureStatus(FeatureType_Native, "VIP_UnregisterFeature") == FeatureStatus_Available) - { - VIP_UnregisterFeature(g_sFeature[0]); - VIP_UnregisterFeature(g_sFeature[1]); - } -} -``` - -Главное соблюдать закономерность: ->Для каждой ```VIP_RegisterFeature``` в ```VIP_OnVIPLoaded()``` должна быть ```VIP_UnregisterFeature``` в ```OnPluginEnd()``` - -Больше примеров можете посмотреть в уже обновлённых модулях. - -Для решения проблемы `#2`: -1. В `OnPluginStart()` ищем `VIP_HookClientSpawn(OnPlayerSpawn);` -2. Удаляем эту строку -3. Далее ищем: -```cpp -public OnPlayerSpawn(iClient, iTeam, bool:bIsVIP) -``` -и заменяем на -```cpp -public VIP_OnPlayerSpawn(iClient, iTeam, bool:bIsVIP) -``` - diff --git a/updates.md b/updates.md deleted file mode 100644 index f28b50b3..00000000 --- a/updates.md +++ /dev/null @@ -1,291 +0,0 @@ -# [VIP] Core 3.0 DEV #41 - -### Изменения: -- Доработана формировка sql запросов для `USE_MORE_SERVERS`. - -# [VIP] Core 3.0 DEV #40 - -### Изменения: -- Изменен натив `VIP_RegisterFeature` (Добавлен параметр `bCookie` - Регистрировать ли куки для ф-и (действительно только для типа SELECTABLE)). - -# [VIP] Core 3.0 DEV #39 - -### Изменения: -- Исправлена ошибка на SQLite `SQL_Callback_RemoveClient: no such column: sid`. - -# [VIP] Core 3.0 DEV #38 - -### Изменения: -- Исправлена ошибка на SQLite `SQL_Callback_ErrorCheck: no such column: sid`. - -# [VIP] Core 3.0 DEV #37 - -### Изменения: -- Исправлена ошибка когда не удалялись истекшие VIP-игроки. - -# [VIP] Core 3.0 DEV #36 - -### Изменения: -- Теперь точно исправлена ошибка `Invalid Handle 0 (error 4)` (`DB_RemoveClientFromID`). - -# [VIP] Core 3.0 DEV #35 - -### Изменения: -- Исправлена ошибка `Invalid Handle 0 (error 4)` (`DB_RemoveClientFromID`). - -# [VIP] Core 3.0 DEV #34 - -### Изменения: -- Натив `VIP_RemoveClientVIP` помечен как устаревший и будет удален в будущем. -- Добавлен новый натив `VIP_RemoveClientVIP2`. -- Исправлена ошибка `Client index 0 is invalid` при добавлении VIP-игрока через `sm_addvip`. - -# [VIP] Core 3.0 DEV #33 - -### Изменения: -- Изменени натив `VIP_SetClientFeatureStatus`. -- Исправлена ошибка `SQL_Callback_SelectExpiredAndOutdated: Unknown column 'iTime' in 'where clause'`. -- Переработана система логов. - -# [VIP] Core 3.0 DEV #32 (спасибо DarklSide за обнаруженные ошибок) - -### Изменения: -- Исправлено описание форварда `VIP_OnClientPreLoad`. -- Исправлен натив `VIP_GiveClientFeature`. -- Исправлена ошибка `SQL_Callback_SelectExpiredAndOutdated: no such function: UNIX_TIMESTAMP` на SQLite. -- Исправлено обновление ников игроков. -- Исправлена работа DataPack на sm 1.10. - -# [VIP] Core 3.0 DEV #31 - -### Изменения: -- Добавлен форвард `VIP_OnClientPreLoad`. -- Исправлен и включен натив `VIP_GiveClientFeature`. -- Добавлен натив `VIP_RemoveClientFeature`. -- Изменен натив `VIP_CheckClient`. -- Значение по-умолчанию для `sm_vip_delete_outdated` изменено на `-1`. -- Исправлено удаление истекших и не активных игроков. - -# [VIP] Core 3.0 DEV #30 - -### Изменения: -- Исправлена работа списка VIP-игроков. - -# [VIP] Core 3.0 DEV #29 - -### Изменения: -- Исправлены SQL ошибки. -- Небольшая оптимизация. -- Исправлена отладка. - -# [VIP] Core 3.0 DEV #28 - -### Изменения: -- Исправлена ошибка из-за которой плагин не запускался `Fatal error creating dynamic native!/unexpected error 23 in AskPluginLoad callback`. -- Изменено имя базы данных с `"vip"` на `"vip_core"` (еще в версии 3.0 DEV #25, просто забыл упомянуть). -- Исправлена попытка установки кодировки для SQLite базы. - -# [VIP] Core 3.0 DEV #27 - -### Изменения: -- Активирована работа квара `sm_vip_delete_outdated`. -- Полный переход на новую структуру базы данных. - -# [VIP] Core 3.0 DEV #26 - -### Изменения: -- Вернул прежний вид натива VIP_SetClientVIP и пометил как устаревший. -- Добавлен новый натив `VIP_GiveClientVIP`, как замена устаревшему `VIP_SetClientVIP`. -- Добавлена обратная совместимость натива `VIP_RemoveClientVIP`. -- Изменента структура таблиц (MySQL). - -# [VIP] Core 3.0 DEV #25 - -### Изменения: -- Добавлено использование `TranslationPhraseExists` для предовтращения ошибок (При отсутствии фразы модуля в переводе). -- Начал переводить отладку более расширенный и гибкий вид. -- Добавлен квар `sm_vip_delete_outdated` (пока не функционирует). - -# [VIP] Core 3.0 DEV #24 - -### Изменения: -- Исправлены SQL ошибки. - -# [VIP] Core 3.0 DEV #23 - -### Изменения: -- Удален столбец `id` из базы данных. -- Исправлена ошибка `SQL_Callback_SelectVipClientInfo` . -- Исправлена ошибка `SQL_Callback_ChangeTime: Unknown column 'user_id' in 'where clause'`. - -# [VIP] Core 3.0 DEV #22 - -### Изменения: -- Попытка исправления ошибки `Invalid Handle 0 (error 4)`. -- Исправлена ошибка когда у no-steam игроков значение `account_id` в базе данных становилось равным 0. - -# [VIP] Core 3.0 DEV #21 - -### Изменения: -- Исправлена ошибка `SQL_Callback_SelectVipPlayers: near ")": syntax error`. - -# [VIP] Core 3.0 DEV #20 - -### Изменения: -- Исправлена ошибка когда значение `name` в базе данных становилось равным 0. - -# [VIP] Core 3.0 DEV #19 - -### Изменения: -- Добавлена мультиязычность кнопки `"Выход"` в инфо-меню. - -# [VIP] Core 3.0 DEV #18 - -### Изменения: -- Исправлена ошибка `SQL_Callback_SelectVipPlayers: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '0, 60' at line 1`. - -# [VIP] Core 3.0 DEV #17 - -### Изменения: -- Добавлено сохранение последнего входа игрока на сервер. - -# [VIP] Core 3.0 DEV #16 - -### Изменения: -- Теперь игроки хранятся в базе по AccountID. -- Исправлена работа команды `sm_addvip`: - - Теперь она может принимать: SteamID2, SteamID3 и ник. - - Исправлена выдача VIP-статуса оффлайн. - -# [VIP] Core 3.0 DEV #15 - -### Изменения: -- Добавлено исправление работы MOTD окна на CS:GO. -- Добавлена директива FIX_CSGO_MOTD, которая позволяет компилировать ядро с фиксом MOTD и без. - -# [VIP] Core 3.0 DEV #14 - -### Изменения: -- Обновлено API: - - Добавлен форвард `VIP_OnShowClientInfo`. - - Изменен натив `VIP_RemoveClientVIP`. - - Доработано и исправлено описание всех нативов и форвадов. -- Обновлен файл `addons/sourcemod/data/vip/cfg/info.ini` (Исправлено описание и опечатки, добавлен английский перевод). - -# [VIP] Core 3.0 DEV #13 - -### Изменения: -- Исправлена логическая ошибка в работе `VIP_UnregisterFeature` когда добавлялись лишние пункты в VIP-меню. -- Изменен натив `VIP_RegisterFeature` для устранения конфликта с `sm_vip_features_default_status`. -- Добавлен натив `VIP_UnregisterMe()`. - -# [VIP] Core 3.0 DEV #12 - -### Изменения: -- Исправлена ошибка `Invalid data pack handle 1f4 (error 1)`. - -# [VIP] Core 3.0 DEV #11 - -### Изменения: -- Удален натив `VIP_SetFeatureDefStatus`. -- Добавлен параметр в натив `VIP_RegisterFeature`: -``` -@param bDefStatus Значение по-умолчанию (true - Включена, false - Выключена). -``` -- Исправлена ошибка работы натива `VIP_SetClientFeatureStatus`. -- Исправлена ошибка при отправке rcon команд для работы с вип от имени сервера. - -# [VIP] Core 3.0 DEV #10 - -### Изменения: -- Исправлена ошибка `DataPack operation is out of bounds`. - -# [VIP] Core 3.0 DEV #9 - -### Изменения: -- Исправлены ошибки компиляции. - -# [VIP] Core 3.0 DEV #8 - -### Изменения: -- Добавлен параметр в натив `VIP_SetClientFeatureStatus`: -``` -@param bCallback Вызывать ли toggle каллбэк. -``` - -# [VIP] Core 3.0 DEV #7 - -### Изменения: -- Доработано меню управления VIP-игроками. -- Добавлены форварды: -``` -forward void VIP_OnFeatureRegistered(const char[] sFeatureName); -forward void VIP_OnFeatureUnregistered(const char[] sFeatureName); -``` -- Теперь `VIP_OnVIPClientLoaded` вызывается когда игрок уже полностью был загружен. -- Изменены нативы: -``` -native void VIP_GetClientID(int iClient); -// На -native int VIP_GetClientID(int iClient); - -native void VIP_SendClientVIPMenu(int iClient); -// На -native void VIP_SendClientVIPMenu(int iClient, bool bSelection = false); -``` -- Добавлены нативы: -``` -native VIP_FeatureType VIP_GetFeatureType(const char[] sFeatureName); -native VIP_ValueType VIP_GetFeatureValueType(const char[] sFeatureName); -native void VIP_SetFeatureDefStatus(const char[] sFeatureName, bool bStatus); -native int VIP_FillArrayByFeatures(ArrayList hArray); -``` - -# [VIP] Core 3.0 DEV #6 - -### Изменения: -- Исправлены ошибки. - -# [VIP] Core 3.0 DEV #5 - -### Изменения: -- Исправлены ошибки. - -# [VIP] Core 3.0 DEV #4 - -### Изменения: -- Полный перевод на новый синтаксис (`newdecls required`) -- VIP_OnVIPClientLoaded теперь вызывается когда полностью загружены настройки игрока. -- Изменен натив `VIP_SetClientVIP`. -- Удалены лишние фразы из файла перевода. - - -# [VIP] Core 3.0 DEV #3 - -### Изменения: -- Удалены все типы авторизации кроме AUTH_STEAM. -- Изменена команда sm_addvip. -- Временно отключены все функции админ-меню, кроме добавления VIP-игроков. - - -# [VIP] Core 3.0 DEV #2 - -### Изменения: -- Перевод на новый синтаксис файла vars.sp и частичный перевод других. -- Удалены пароли (из базы, запросов, api, cvars). - -# [VIP] Core 3.0 DEV #1 - -### Изменения: -- Корректная компиляция на см >= 1.7. -- Добавлена переменная time в информационных сообщениях типа меню. Отвечает за время отображения меню игроку. -- Исправлен сброс настроек при перезаходе в случае когда куки не успели загрузится. - -# [VIP] Core 3.0 DEV - -### Что изменено: -- Добавлена директива USE_ADMINMENU, которая позволяет компилировать ядро с админкой vip и без неё. -- Добавлен квар sm_vip_add_item_to_admin_menu - Добавить пункт "Управление VIP" в админ-меню. -- Добавлено отдельно меню администрирования VIP, команда для открытия sm_vipadmin, !vipadmin, /vipadmin. -- Удален частный форвард спавна игрока (и нативы хука/анхука). -- Теперь для работы требуется SM 1.5.0 или выше.