#pragma semicolon 1 #pragma newdecls required #include #include #include public Plugin myinfo = { name = "[VIP] Custom Models (R1KO API-safe)", author = "OpenAI", description = "VIP custom player models with persistence, robust downloads parsing and safer default arms handling", version = "1.2.0", url = "" }; #define FEATURE_NAME "CUSTOM_MODELS" #define CONFIG_PATH "configs/vip_custom_models.ini" #define COOKIE_NAME "vip_custom_model_selection" #define MAX_CUSTOM_MODELS 128 #define MAX_DOWNLOADS_PER_MODEL 64 #define TEAM_ANY 0 #define TEAM_T 2 #define TEAM_CT 3 #define DEFAULT_T_ARMS "models/weapons/t_arms_leet.mdl" #define DEFAULT_CT_ARMS "models/weapons/ct_arms_fbi.mdl" // Manual VIP API declarations to avoid include/core version mismatch. enum VIP_ValueType { VIP_NULL = 0, INT, FLOAT, BOOL, STRING }; enum VIP_FeatureType { TOGGLABLE = 0, SELECTABLE, HIDE }; enum VIP_AccessStatus { NO_ACCESS = 0, DISABLED, ENABLED }; native bool VIP_IsVIPLoaded(); native void VIP_RegisterFeature(const char[] sFeatureName, VIP_ValueType valType = VIP_NULL, VIP_FeatureType featureType = TOGGLABLE, Function itemSelectCallback = INVALID_FUNCTION, Function itemDisplayCallback = INVALID_FUNCTION, Function itemDrawCallback = INVALID_FUNCTION, bool bDefStatus = true, bool bCookie = true); native void VIP_UnregisterFeature(const char[] sFeatureName); native int VIP_GetClientFeatureStatus(int client, const char[] sFeatureName); native void VIP_SendClientVIPMenu(int client, bool bSelection = false); native void VIP_GetClientVIPGroup(int client, char[] group, int maxlen); Cookie g_hSelectionCookie = null; Database g_hDb = null; bool g_bFeatureRegistered = false; bool g_bCookiesReady[MAXPLAYERS + 1]; bool g_bDbReady = false; bool g_bDbLoaded[MAXPLAYERS + 1]; char g_sLoadedModelId[MAXPLAYERS + 1][32]; int g_iModelCount = 0; char g_sModelId[MAX_CUSTOM_MODELS][32]; char g_sModelName[MAX_CUSTOM_MODELS][128]; char g_sModelPath[MAX_CUSTOM_MODELS][PLATFORM_MAX_PATH]; char g_sArmsPath[MAX_CUSTOM_MODELS][PLATFORM_MAX_PATH]; char g_sGroupMask[MAX_CUSTOM_MODELS][256]; int g_iTeamLimit[MAX_CUSTOM_MODELS]; int g_iDownloadCount[MAX_CUSTOM_MODELS]; char g_sDownloads[MAX_CUSTOM_MODELS][MAX_DOWNLOADS_PER_MODEL][PLATFORM_MAX_PATH]; int g_iSelectedModel[MAXPLAYERS + 1]; public void OnPluginStart() { LoadTranslations("common.phrases"); LoadTranslations("vip_custom_models.phrases"); g_hSelectionCookie = RegClientCookie(COOKIE_NAME, "VIP custom model selection", CookieAccess_Private); HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post); HookEvent("player_team", Event_PlayerTeam, EventHookMode_Post); RegConsoleCmd("sm_vmodel", Command_OpenMenu); RegConsoleCmd("sm_models", Command_OpenMenu); RegConsoleCmd("sm_agents", Command_OpenMenu); for (int i = 1; i <= MaxClients; i++) { g_iSelectedModel[i] = -1; g_bCookiesReady[i] = false; g_bDbLoaded[i] = false; g_sLoadedModelId[i][0] = '\0'; } LoadModelsConfig(); InitDatabase(); if (VIP_IsVIPLoaded()) { VIP_OnVIPLoaded(); } } public void OnMapStart() { LoadModelsConfig(); PrecacheAllModels(); } public void OnClientPutInServer(int client) { g_iSelectedModel[client] = -1; g_bCookiesReady[client] = false; g_bDbLoaded[client] = false; g_sLoadedModelId[client][0] = '\0'; if (!IsClientInGame(client) || IsFakeClient(client)) { return; } CreateTimer(3.0, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public void OnClientPostAdminCheck(int client) { if (!IsValidHuman(client)) { return; } LoadClientSelectionFromDatabase(client); CreateTimer(1.5, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public void OnClientCookiesCached(int client) { g_iSelectedModel[client] = -1; g_bCookiesReady[client] = true; if (!IsValidHuman(client)) { return; } char value[16]; GetClientCookie(client, g_hSelectionCookie, value, sizeof(value)); if (value[0] != '\0') { int idx = FindModelIndexById(value); if (idx != -1) { g_iSelectedModel[client] = idx; } } CreateTimer(0.5, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public void OnClientDisconnect(int client) { g_iSelectedModel[client] = -1; g_bCookiesReady[client] = false; g_bDbLoaded[client] = false; g_sLoadedModelId[client][0] = '\0'; } void InitDatabase() { char error[256]; g_hDb = SQLite_UseDatabase("storage-local", error, sizeof(error)); if (g_hDb == null) { LogError("[VIP Models] SQLite init failed: %s", error); g_bDbReady = false; return; } g_bDbReady = true; g_hDb.Query(SQL_GenericCallback, "CREATE TABLE IF NOT EXISTS vip_custom_models_selection (steamid TEXT PRIMARY KEY, modelid TEXT NOT NULL, updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')))"); } public void SQL_GenericCallback(Database db, DBResultSet results, const char[] error, any data) { if (error[0] != '') { LogError("[VIP Models] SQL error: %s", error); } } void GetClientSteamId64(int client, char[] buffer, int maxlen) { if (!GetClientAuthId(client, AuthId_SteamID64, buffer, maxlen, true)) { buffer[0] = ''; } } void LoadClientSelectionFromDatabase(int client) { if (!g_bDbReady || !IsValidHuman(client)) { return; } char steamid[32]; GetClientSteamId64(client, steamid, sizeof(steamid)); if (steamid[0] == '') { return; } char query[256]; FormatEx(query, sizeof(query), "SELECT modelid FROM vip_custom_models_selection WHERE steamid = '%s' LIMIT 1", steamid); g_hDb.Query(SQL_LoadClientSelectionCallback, query, GetClientUserId(client)); } public void SQL_LoadClientSelectionCallback(Database db, DBResultSet results, const char[] error, any data) { int client = GetClientOfUserId(data); if (!IsValidHuman(client)) { return; } g_bDbLoaded[client] = true; if (error[0] != '') { LogError("[VIP Models] Failed to load client selection: %s", error); return; } if (results == null || !results.FetchRow()) { return; } char modelId[32]; results.FetchString(0, modelId, sizeof(modelId)); if (modelId[0] == '') { return; } strcopy(g_sLoadedModelId[client], sizeof(g_sLoadedModelId[]), modelId); int idx = FindModelIndexById(modelId); if (idx != -1) { g_iSelectedModel[client] = idx; } if (g_bCookiesReady[client]) { SetClientCookie(client, g_hSelectionCookie, modelId); } CreateTimer(0.5, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } void SaveClientSelectionToDatabase(int client) { if (!g_bDbReady || !IsValidHuman(client)) { return; } char steamid[32]; GetClientSteamId64(client, steamid, sizeof(steamid)); if (steamid[0] == '') { return; } int idx = g_iSelectedModel[client]; if (idx < 0 || idx >= g_iModelCount) { DeleteClientSelectionFromDatabase(client); return; } char escModel[64]; g_hDb.Escape(g_sModelId[idx], escModel, sizeof(escModel)); char query[512]; FormatEx(query, sizeof(query), "REPLACE INTO vip_custom_models_selection (steamid, modelid, updated_at) VALUES ('%s', '%s', strftime('%%s','now'))", steamid, escModel); g_hDb.Query(SQL_GenericCallback, query); } void DeleteClientSelectionFromDatabase(int client) { if (!g_bDbReady || !IsValidHuman(client)) { return; } char steamid[32]; GetClientSteamId64(client, steamid, sizeof(steamid)); if (steamid[0] == '') { return; } char query[256]; FormatEx(query, sizeof(query), "DELETE FROM vip_custom_models_selection WHERE steamid = '%s'", steamid); g_hDb.Query(SQL_GenericCallback, query); } public void VIP_OnVIPLoaded() { RegisterFeature(); } public void VIP_OnVIPClientLoaded(int client) { if (IsValidHuman(client)) { CreateTimer(0.5, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } } public void OnPluginEnd() { if (g_bFeatureRegistered) { VIP_UnregisterFeature(FEATURE_NAME); g_bFeatureRegistered = false; } } void RegisterFeature() { if (g_bFeatureRegistered) { return; } Function fSelect = GetFunctionByName(GetMyHandle(), "OnSelectMainFeature"); Function fDisplay = GetFunctionByName(GetMyHandle(), "OnDisplayMainFeature"); Function fDraw = GetFunctionByName(GetMyHandle(), "OnDrawMainFeature"); if (fSelect == INVALID_FUNCTION || fDisplay == INVALID_FUNCTION || fDraw == INVALID_FUNCTION) { SetFailState("Failed to find VIP menu callback functions"); return; } VIP_RegisterFeature(FEATURE_NAME, VIP_NULL, SELECTABLE, fSelect, fDisplay, fDraw, true, true); g_bFeatureRegistered = true; } public bool OnSelectMainFeature(int client, const char[] feature) { ShowModelsMenu(client); return false; } public bool OnDisplayMainFeature(int client, const char[] feature, char[] display, int maxlen) { FormatEx(display, maxlen, "%T", "VIP_CUSTOM_MODELS_FEATURE", client); return true; } public int OnDrawMainFeature(int client, const char[] feature, int style) { return HasAnyAvailableModel(client) ? style : ITEMDRAW_DISABLED; } public Action Command_OpenMenu(int client, int args) { if (!IsValidHuman(client)) { return Plugin_Handled; } if (VIP_GetClientFeatureStatus(client, FEATURE_NAME) == NO_ACCESS) { PrintToChat(client, "[VIP] У вас нет доступа к агентам."); return Plugin_Handled; } ShowModelsMenu(client); return Plugin_Handled; } public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (!IsValidHuman(client)) { return; } CreateTimer(0.3, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public void Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast) { int client = GetClientOfUserId(event.GetInt("userid")); if (!IsValidHuman(client)) { return; } CreateTimer(0.3, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public Action Timer_ApplyModel(Handle timer, any userid) { int client = GetClientOfUserId(userid); if (!IsValidHuman(client) || !IsPlayerAlive(client)) { return Plugin_Stop; } ApplySavedModel(client); return Plugin_Stop; } void ApplySavedModel(int client) { int idx = g_iSelectedModel[client]; if ((idx < 0 || idx >= g_iModelCount) && g_sLoadedModelId[client][0] != '') { idx = FindModelIndexById(g_sLoadedModelId[client]); if (idx != -1) { g_iSelectedModel[client] = idx; } } if (idx < 0 || idx >= g_iModelCount) { ApplyDefaultArms(client); return; } if (!CanUseModel(client, idx)) { ResetClientSelection(client); ApplyDefaultArms(client); return; } if (g_sModelPath[idx][0] != '\0') { SetEntityModel(client, g_sModelPath[idx]); } CreateTimer(0.1, Timer_ApplyArmsForClient, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public Action Timer_ApplyArmsForClient(Handle timer, any userid) { int client = GetClientOfUserId(userid); if (!IsValidHuman(client) || !IsPlayerAlive(client)) { return Plugin_Stop; } ApplySelectedOrDefaultArms(client); return Plugin_Stop; } void ApplySelectedOrDefaultArms(int client) { int idx = g_iSelectedModel[client]; if (idx >= 0 && idx < g_iModelCount && CanUseModel(client, idx)) { if (g_sArmsPath[idx][0] != '\0') { SetEntPropString(client, Prop_Send, "m_szArmsModel", g_sArmsPath[idx]); AddModelFiles(g_sArmsPath[idx]); PrecacheModel(g_sArmsPath[idx], true); return; } } ApplyDefaultArms(client); } void ApplyDefaultArms(int client) { char armsPath[PLATFORM_MAX_PATH]; GetDefaultArmsForTeam(GetClientTeam(client), armsPath, sizeof(armsPath)); if (armsPath[0] == '\0') { return; } // Стандартные руки уже есть у клиента, поэтому не добавляем их в downloads. PrecacheModel(armsPath, true); SetEntPropString(client, Prop_Send, "m_szArmsModel", armsPath); } void GetDefaultArmsForTeam(int team, char[] buffer, int maxlen) { buffer[0] = '\0'; switch (team) { case TEAM_T: { strcopy(buffer, maxlen, DEFAULT_T_ARMS); } case TEAM_CT: { strcopy(buffer, maxlen, DEFAULT_CT_ARMS); } } } void ShowModelsMenu(int client) { Menu menu = new Menu(MenuHandler_Models); menu.SetTitle("VIP: Кастомные модели"); menu.ExitBackButton = true; char currentId[32]; currentId[0] = '\0'; if (g_iSelectedModel[client] >= 0 && g_iSelectedModel[client] < g_iModelCount) { strcopy(currentId, sizeof(currentId), g_sModelId[g_iSelectedModel[client]]); } menu.AddItem("__default__", "Сбросить модель"); bool hasAny = false; char info[32], title[192]; for (int i = 0; i < g_iModelCount; i++) { if (!CanUseModel(client, i)) { continue; } hasAny = true; strcopy(info, sizeof(info), g_sModelId[i]); if (StrEqual(currentId, info, false)) { FormatEx(title, sizeof(title), "%s [✓]", g_sModelName[i]); } else { strcopy(title, sizeof(title), g_sModelName[i]); } menu.AddItem(info, title); } if (!hasAny) { menu.AddItem("__none__", "Нет доступных моделей", ITEMDRAW_DISABLED); } menu.Display(client, MENU_TIME_FOREVER); } public int MenuHandler_Models(Menu menu, MenuAction action, int client, int item) { switch (action) { case MenuAction_End: { delete menu; } case MenuAction_Cancel: { if (item == MenuCancel_ExitBack && IsValidHuman(client)) { VIP_SendClientVIPMenu(client, true); } } case MenuAction_Select: { if (!IsValidHuman(client)) { return 0; } char info[32]; menu.GetItem(item, info, sizeof(info)); if (StrEqual(info, "__default__", false)) { ResetClientSelection(client); PrintToChat(client, "[VIP] Стандартная модель восстановлена."); ShowModelsMenu(client); return 0; } int idx = FindModelIndexById(info); if (idx == -1 || !CanUseModel(client, idx)) { PrintToChat(client, "[VIP] Эта модель вам недоступна."); ShowModelsMenu(client); return 0; } g_iSelectedModel[client] = idx; if (g_bCookiesReady[client]) { SetClientCookie(client, g_hSelectionCookie, g_sModelId[idx]); } SaveClientSelectionToDatabase(client); if (IsPlayerAlive(client)) { ApplySavedModel(client); } PrintToChat(client, "[VIP] Вы выбрали модель: %s", g_sModelName[idx]); ShowModelsMenu(client); } } return 0; } void ResetClientSelection(int client) { g_iSelectedModel[client] = -1; if (g_bCookiesReady[client]) { SetClientCookie(client, g_hSelectionCookie, ""); } DeleteClientSelectionFromDatabase(client); if (IsValidHuman(client) && IsPlayerAlive(client)) { CreateTimer(0.1, Timer_ApplyArmsForClient, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } } bool HasAnyAvailableModel(int client) { if (!IsValidHuman(client)) { return false; } if (VIP_GetClientFeatureStatus(client, FEATURE_NAME) == NO_ACCESS) { return false; } for (int i = 0; i < g_iModelCount; i++) { if (CanUseModel(client, i)) { return true; } } return false; } bool CanUseModel(int client, int idx) { if (!IsValidHuman(client) || idx < 0 || idx >= g_iModelCount) { return false; } if (VIP_GetClientFeatureStatus(client, FEATURE_NAME) == NO_ACCESS) { return false; } int team = GetClientTeam(client); if (g_iTeamLimit[idx] == TEAM_T && team != TEAM_T) { return false; } if (g_iTeamLimit[idx] == TEAM_CT && team != TEAM_CT) { return false; } if (g_sGroupMask[idx][0] == '\0' || StrEqual(g_sGroupMask[idx], "*", false)) { return true; } char vipGroup[64]; vipGroup[0] = '\0'; VIP_GetClientVIPGroup(client, vipGroup, sizeof(vipGroup)); if (vipGroup[0] == '\0') { return false; } return IsGroupListed(g_sGroupMask[idx], vipGroup); } bool IsGroupListed(const char[] groupsMask, const char[] userGroup) { char parts[16][64]; int count = ExplodeString(groupsMask, ";", parts, sizeof(parts), sizeof(parts[])); for (int i = 0; i < count; i++) { TrimString(parts[i]); if (parts[i][0] == '\0') { continue; } if (StrEqual(parts[i], userGroup, false)) { return true; } } return false; } int FindModelIndexById(const char[] id) { for (int i = 0; i < g_iModelCount; i++) { if (StrEqual(g_sModelId[i], id, false)) { return i; } } return -1; } void LoadModelsConfig() { g_iModelCount = 0; char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), CONFIG_PATH); KeyValues kv = new KeyValues("VIP_CustomModels"); if (!kv.ImportFromFile(path)) { LogError("[VIP Models] Could not load config: %s", path); delete kv; return; } if (!kv.JumpToKey("Models")) { LogError("[VIP Models] Section 'Models' not found in %s", path); delete kv; return; } if (!kv.GotoFirstSubKey()) { delete kv; return; } do { if (g_iModelCount >= MAX_CUSTOM_MODELS) { break; } kv.GetSectionName(g_sModelId[g_iModelCount], sizeof(g_sModelId[])); kv.GetString("name", g_sModelName[g_iModelCount], sizeof(g_sModelName[]), g_sModelId[g_iModelCount]); kv.GetString("model", g_sModelPath[g_iModelCount], sizeof(g_sModelPath[]), ""); kv.GetString("arms", g_sArmsPath[g_iModelCount], sizeof(g_sArmsPath[]), ""); kv.GetString("groups", g_sGroupMask[g_iModelCount], sizeof(g_sGroupMask[]), "*"); g_iDownloadCount[g_iModelCount] = 0; char team[8]; kv.GetString("team", team, sizeof(team), "ANY"); if (StrEqual(team, "T", false)) { g_iTeamLimit[g_iModelCount] = TEAM_T; } else if (StrEqual(team, "CT", false)) { g_iTeamLimit[g_iModelCount] = TEAM_CT; } else { g_iTeamLimit[g_iModelCount] = TEAM_ANY; } ParseDownloadsForCurrentModel(kv, g_iModelCount); if (g_sModelPath[g_iModelCount][0] != '\0') { g_iModelCount++; } } while (kv.GotoNextKey()); delete kv; LogMessage("[VIP Models] Loaded %d models from config", g_iModelCount); } void ParseDownloadsForCurrentModel(KeyValues kv, int modelIdx) { char buffer[2048]; kv.GetString("downloads", buffer, sizeof(buffer), ""); // Support old string format: "file1;file2;file3" if (buffer[0] != '\0') { char parts[128][PLATFORM_MAX_PATH]; int count = ExplodeString(buffer, ";", parts, sizeof(parts), sizeof(parts[])); for (int i = 0; i < count; i++) { TrimString(parts[i]); if (parts[i][0] == '\0') { continue; } AddDownloadPathToModel(modelIdx, parts[i]); } } // Support block format: // "downloads" // { // "1" "path" // "2" "path" // } if (kv.JumpToKey("downloads")) { if (kv.GotoFirstSubKey(false)) { do { char value[PLATFORM_MAX_PATH]; kv.GetString(NULL_STRING, value, sizeof(value), ""); TrimString(value); if (value[0] != '\0') { AddDownloadPathToModel(modelIdx, value); } } while (kv.GotoNextKey(false)); kv.GoBack(); } kv.GoBack(); } } void AddDownloadPathToModel(int modelIdx, const char[] path) { if (modelIdx < 0 || modelIdx >= MAX_CUSTOM_MODELS) { return; } if (g_iDownloadCount[modelIdx] >= MAX_DOWNLOADS_PER_MODEL) { LogError("[VIP Models] Too many downloads for model %s", g_sModelId[modelIdx]); return; } // Avoid duplicates. for (int i = 0; i < g_iDownloadCount[modelIdx]; i++) { if (StrEqual(g_sDownloads[modelIdx][i], path, false)) { return; } } strcopy(g_sDownloads[modelIdx][g_iDownloadCount[modelIdx]], sizeof(g_sDownloads[][]), path); g_iDownloadCount[modelIdx]++; } void PrecacheAllModels() { // Стандартные руки не отправляем в downloads, только кешируем. PrecacheModel(DEFAULT_T_ARMS, true); PrecacheModel(DEFAULT_CT_ARMS, true); for (int i = 0; i < g_iModelCount; i++) { AddModelFiles(g_sModelPath[i]); PrecacheModel(g_sModelPath[i], true); if (g_sArmsPath[i][0] != '\0') { AddModelFiles(g_sArmsPath[i]); PrecacheModel(g_sArmsPath[i], true); } for (int x = 0; x < g_iDownloadCount[i]; x++) { AddIfExists(g_sDownloads[i][x]); } } } void AddModelFiles(const char[] mdlPath) { if (mdlPath[0] == '\0') { return; } AddIfExists(mdlPath); char base[PLATFORM_MAX_PATH]; strcopy(base, sizeof(base), mdlPath); ReplaceString(base, sizeof(base), ".mdl", "", false); char buffer[PLATFORM_MAX_PATH]; // Базовые файлы модели. FormatEx(buffer, sizeof(buffer), "%s.vvd", base); AddIfExists(buffer); FormatEx(buffer, sizeof(buffer), "%s.dx90.vtx", base); AddIfExists(buffer); // Эти файлы часто отсутствуют у кастомных моделей — просто пропускаем без spam в лог. FormatEx(buffer, sizeof(buffer), "%s.dx80.vtx", base); AddIfExists(buffer, false); FormatEx(buffer, sizeof(buffer), "%s.vtx", base); AddIfExists(buffer, false); FormatEx(buffer, sizeof(buffer), "%s.sw.vtx", base); AddIfExists(buffer, false); FormatEx(buffer, sizeof(buffer), "%s.phy", base); AddIfExists(buffer, false); } void AddIfExists(const char[] relPath, bool logMissing = false) { if (relPath[0] == '\0') { return; } if (FileExists(relPath, true)) { AddFileToDownloadsTable(relPath); } else if (logMissing) { LogMessage("[VIP Models] Missing download file: %s", relPath); } } bool IsValidHuman(int client) { return client > 0 && client <= MaxClients && IsClientInGame(client) && !IsFakeClient(client); }