Files
vip-custom-models/scripting/vip_custom_models.sp
T

970 lines
24 KiB
SourcePawn

#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#include <clientprefs>
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);
}