#pragma semicolon 1 #pragma newdecls required #include #include public Plugin myinfo = { name = "[VIP] Custom Models", author = "deidara.dev", description = "Кастомные модели игроков по SteamID64 — назначаются через конфиг, без меню", version = "2.0.0", url = "https://deidara.dev" }; #define CONFIG_MODELS "configs/vip_custom_models.ini" #define CONFIG_LOCKED "configs/vip_custom_models_locked.cfg" #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" 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]; 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]; // SteamID64 → model_id mapping из locked-конфига StringMap g_smLockedAssignments = null; // Per-client кеш индекса назначенной модели (-1 = нет назначения) int g_iAssignedModel[MAXPLAYERS + 1]; public void OnPluginStart() { HookEvent("player_spawn", Event_PlayerSpawn, EventHookMode_Post); HookEvent("player_team", Event_PlayerTeam, EventHookMode_Post); RegAdminCmd("sm_vmodel_reload", Command_Reload, ADMFLAG_ROOT, "Перезагрузить vip_custom_models.ini и vip_custom_models_locked.cfg"); g_smLockedAssignments = new StringMap(); for (int i = 1; i <= MaxClients; i++) { g_iAssignedModel[i] = -1; } } public void OnMapStart() { LoadModelsConfig(); LoadLockedAssignments(); PrecacheAllModels(); // Re-resolve назначения для тех кто уже в игре (на случай поздней загрузки плагина) for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i) && !IsFakeClient(i)) { ResolveClientAssignment(i); } } } public void OnClientPutInServer(int client) { g_iAssignedModel[client] = -1; } public void OnClientPostAdminCheck(int client) { if (!IsValidHuman(client)) { return; } ResolveClientAssignment(client); // Применяем модель с задержкой — игрок может ещё не быть alive CreateTimer(1.5, Timer_ApplyModel, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public void OnClientDisconnect(int client) { g_iAssignedModel[client] = -1; } void ResolveClientAssignment(int client) { g_iAssignedModel[client] = -1; if (!IsValidHuman(client) || g_smLockedAssignments == null) { return; } char steamid[32]; if (!GetClientAuthId(client, AuthId_SteamID64, steamid, sizeof(steamid), true)) { return; } char modelId[64]; if (!g_smLockedAssignments.GetString(steamid, modelId, sizeof(modelId))) { return; } int idx = FindModelIndexById(modelId); if (idx == -1) { LogMessage("[VIP Models] Игроку %N (%s) назначена модель '%s', но такой модели нет в %s", client, steamid, modelId, CONFIG_MODELS); return; } g_iAssignedModel[client] = idx; } 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; } ApplyAssignedModel(client); return Plugin_Stop; } void ApplyAssignedModel(int client) { int idx = g_iAssignedModel[client]; // Нет назначения — просто стандартные руки для команды if (idx < 0 || idx >= g_iModelCount) { ApplyDefaultArms(client); return; } // Проверка ограничения по команде int team = GetClientTeam(client); if (g_iTeamLimit[idx] != TEAM_ANY && g_iTeamLimit[idx] != team) { // Кастомная модель не подходит этой команде → дефолт ApplyDefaultArms(client); return; } if (g_sModelPath[idx][0] != '\0') { SetEntityModel(client, g_sModelPath[idx]); } // Руки применяем чуть позже — после установки модели CreateTimer(0.1, Timer_ApplyArms, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } public Action Timer_ApplyArms(Handle timer, any userid) { int client = GetClientOfUserId(userid); if (!IsValidHuman(client) || !IsPlayerAlive(client)) { return Plugin_Stop; } int idx = g_iAssignedModel[client]; if (idx >= 0 && idx < g_iModelCount && g_sArmsPath[idx][0] != '\0') { int team = GetClientTeam(client); if (g_iTeamLimit[idx] == TEAM_ANY || g_iTeamLimit[idx] == team) { SetEntPropString(client, Prop_Send, "m_szArmsModel", g_sArmsPath[idx]); PrecacheModel(g_sArmsPath[idx], true); return Plugin_Stop; } } ApplyDefaultArms(client); return Plugin_Stop; } void ApplyDefaultArms(int client) { char armsPath[PLATFORM_MAX_PATH]; GetDefaultArmsForTeam(GetClientTeam(client), armsPath, sizeof(armsPath)); if (armsPath[0] == '\0') { return; } 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); } } public Action Command_Reload(int client, int args) { LoadModelsConfig(); LoadLockedAssignments(); PrecacheAllModels(); int reAssigned = 0; for (int i = 1; i <= MaxClients; i++) { if (!IsClientInGame(i) || IsFakeClient(i)) { continue; } ResolveClientAssignment(i); if (g_iAssignedModel[i] >= 0) { reAssigned++; if (IsPlayerAlive(i)) { ApplyAssignedModel(i); } } } ReplyToCommand(client, "[VIP Models] Перезагружено: %d моделей в конфиге, %d игрокам назначены модели.", g_iModelCount, reAssigned); return Plugin_Handled; } void LoadLockedAssignments() { g_smLockedAssignments.Clear(); char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), CONFIG_LOCKED); KeyValues kv = new KeyValues("VIP_LockedModels"); if (!kv.ImportFromFile(path)) { LogMessage("[VIP Models] Файл %s не найден — никаких назначений не загружено.", path); delete kv; return; } if (!kv.GotoFirstSubKey(false)) { delete kv; LogMessage("[VIP Models] Файл %s пуст.", path); return; } int count = 0; char steamid[32], modelId[64]; do { kv.GetSectionName(steamid, sizeof(steamid)); kv.GetString(NULL_STRING, modelId, sizeof(modelId), ""); TrimString(steamid); TrimString(modelId); if (steamid[0] != '\0' && modelId[0] != '\0') { g_smLockedAssignments.SetString(steamid, modelId); count++; } } while (kv.GotoNextKey(false)); delete kv; LogMessage("[VIP Models] Загружено %d назначений из %s", count, path); } void LoadModelsConfig() { g_iModelCount = 0; char path[PLATFORM_MAX_PATH]; BuildPath(Path_SM, path, sizeof(path), CONFIG_MODELS); KeyValues kv = new KeyValues("VIP_CustomModels"); if (!kv.ImportFromFile(path)) { LogError("[VIP Models] Не удалось загрузить %s", path); delete kv; return; } if (!kv.JumpToKey("Models")) { LogError("[VIP Models] Секция 'Models' не найдена в %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[]), ""); 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] Загружено %d моделей из %s", g_iModelCount, path); } void ParseDownloadsForCurrentModel(KeyValues kv, int modelIdx) { char buffer[2048]; kv.GetString("downloads", buffer, sizeof(buffer), ""); // Формат-строка: "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]); } } // Блочный формат: "downloads" { "1" "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] Слишком много downloads для модели %s", g_sModelId[modelIdx]); return; } // Без дубликатов 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() { 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); 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] Файл для загрузки не найден: %s", relPath); } } int FindModelIndexById(const char[] id) { for (int i = 0; i < g_iModelCount; i++) { if (StrEqual(g_sModelId[i], id, false)) { return i; } } return -1; } bool IsValidHuman(int client) { return client > 0 && client <= MaxClients && IsClientInGame(client) && !IsFakeClient(client); }