From e17f642eed0be7475c81af9bdda275f78f4ed6ed Mon Sep 17 00:00:00 2001 From: deidara Date: Fri, 1 May 2026 17:16:23 +0300 Subject: [PATCH] =?UTF-8?q?v1.2.0:=203=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B0,=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20save/restore=20=D0=B8=D0=BD=D0=B2=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D1=8F,=20MOTD,=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=BA?= =?UTF-8?q?=D1=83=D0=BB=D0=B4=D0=B0=D1=83=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новые режимы: - Scout NoScope (Scout без зума) - Deagle Only - Deagle HS Only (только хедшоты наносят урон) Багфиксы: - Полное сохранение инвентаря (гранаты, броня, шлем, дефузер, патроны) - Сброс state на disconnect/connect (новый игрок не получает чужой инвентарь) - Убрано двойное применение режима в начале раунда - m_iFOV=90 вместо 0 при сбросе зума - Удалён мёртвый GiveDefaultCombatLoadout Новый функционал: - Показ MOTD-картинки во freezetime (http://37.228.88.57/cr/.html) - Логирование запусков/отмен в addons/sourcemod/logs/custom_rounds.log - Кулдаун 5 раундов для обычных админов (DEIDARA/TESTER/z обходят) - Защита от спама очереди - Команда sm_cr_status --- README.md | 80 ++- scripting/ArcaneGame_CustomRounds_Core.sp | 611 +++++++++++++++++++--- 2 files changed, 599 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 845031a..77dffa7 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,26 @@ ## Функции -- 7 режимов кастомных раундов: +- 10 режимов кастомных раундов: - **AWP Only** — только AWP + нож - - **AWP Only [NoScope]** — AWP без прицела (zoom заблокирован) + - **AWP NoScope** — AWP без прицела (zoom заблокирован) + - **Scout Only** — только SSG-08 + нож + - **Scout NoScope** — Scout без прицела + - **Deagle Only** — только Deagle + нож + - **Deagle HS Only** — только Deagle, **в тело — 0 урона, только хедшоты** - **HE Only** — только гранаты HE с бесконечным боезапасом - **Ножевой раунд** — только ножи - - **Scout Only** — только SSG-08 + нож - **Низкая гравитация** — гравитация уменьшена (настраивается) - **1 HP** — у всех игроков минимальное здоровье -- Сохранение и восстановление снаряжения игроков после кастомного раунда +- Полное сохранение и восстановление инвентаря после кастомного раунда (основное оружие + патроны, запасное + патроны, нож, **все гранаты**, **броня**, **шлем**, **дефузер**) - Блокировка покупок во время режима +- Блокировка зума для NoScope-режимов (через CommandListener + OnPlayerRunCmd) +- Показ **MOTD-картинки** во freezetime в начале кастомного раунда (HTTP-страница, авто-закрытие через 12с) +- Кулдаун **5 раундов** для обычных админов между запусками. Группы DEIDARA / TESTER и флаг `z` (Root) обходят кулдаун +- Логирование действий админов (выбор/отмена режима) в `addons/sourcemod/logs/custom_rounds.log` +- Защита от спама: один и тот же режим нельзя поставить в очередь дважды подряд - Интеграция AG Coins (награда за победу / убийства / хедшоты / выживание) - Объявления в чат о текущем и следующем режиме -- Настройка через ConVars и AutoExecConfig ## Зависимости @@ -35,6 +42,7 @@ | Команда | Доступ | Описание | |---|---|---| | `!cr` / `sm_cr` | Группы DEIDARA / TESTER или по флагу | Открыть меню кастомных раундов | +| `!cr_status` / `sm_cr_status` | Любой админ с доступом к меню | Показать текущий/следующий режим и кулдаун | ## ConVars @@ -50,13 +58,67 @@ | `sm_cr_lowgravity_value` | `0.40` | Значение гравитации (Low Gravity) | | `sm_cr_onehp_value` | `1` | Здоровье игроков в режиме 1 HP | | `sm_cr_announce` | `1` | Показывать объявления в чат | +| `sm_cr_show_motd` | `1` | Показывать картинку режима во freezetime | +| `sm_cr_cooldown_rounds` | `5` | Кулдаун в раундах для обычных админов (DEIDARA/TESTER/z обходят) | ## Уровни доступа -Доступ к меню имеют: -- Группы SM **DEIDARA** и **TESTER** -- Любой игрок с флагом, указанным в `sm_cr_access_flag` (если `sm_cr_access_use_overrides = 1`) +| Уровень | Кулдаун | Возможности | +|---|---|---| +| Группа **DEIDARA** | Нет | Все режимы, все запуски | +| Группа **TESTER** | Нет | Все режимы, все запуски | +| Флаг `z` (Root) | Нет | Все режимы, все запуски | +| Любой другой админ с флагом `sm_cr_access_flag` | **5 раундов** | Все режимы, но запуск раз в 5 раундов | +| Без флага | Нет доступа | — | + +## MOTD-картинка во freezetime + +Когда стартует кастомный раунд, всем игрокам отображается HTML-страница на время freezetime+несколько секунд. Страница хостится по адресу: + +``` +http://37.228.88.57/cr/.html +``` + +Slug-ы: `awp`, `awp-noscope`, `scout`, `scout-noscope`, `deagle`, `deagle-hs`, `he`, `knife`, `lowgrav`, `onehp`. + +По умолчанию страницы — стилизованные баннеры с заголовком режима. Чтобы заменить на реальные изображения — отредактируйте `<тег img>` в HTML на сервере по пути `/srv/cr-images/cr/.html`. URL картинок плагин дёргает динамически по slug-у, так что менять плагин не нужно. + +Отключить показ полностью — `sm_cr_show_motd 0`. + +## Логирование + +Все запуски и отмены кастомных раундов пишутся в: + +``` +addons/sourcemod/logs/custom_rounds.log +``` + +Формат: +``` +[YYYY-MM-DD HH:MM:SS] [FULL|REG] АдминИмя (STEAM_X:Y:Z) -> ВЫБРАЛ/ОТМЕНИЛ режим: <название> [раунд N] +``` + +Где: +- `FULL` — DEIDARA / TESTER / Root (без кулдауна) +- `REG` — обычный админ (с кулдауном) ## Версия -`1.1.0` — Автор: deidara.dev +`1.2.0` — Автор: deidara.dev + +### Changelog + +- **1.2.0** + - Добавлены режимы: **Scout NoScope**, **Deagle Only**, **Deagle HS Only** (только хедшоты) + - Полное сохранение/восстановление инвентаря: гранаты, броня, шлем, дефузер, патроны + - Фикс: при заходе нового игрока на освободившийся слот не выдаётся чужой инвентарь + - Фикс: убрано двойное применение режима в начале раунда + - Фикс: `m_iFOV` восстанавливается в 90 (а не 0) + - Фикс: удалён мёртвый код `GiveDefaultCombatLoadout` + - **MOTD-картинка** во freezetime при старте кастомного раунда + - **Логирование** действий в `custom_rounds.log` (с тегом FULL/REG) + - **Кулдаун 5 раундов** для обычных админов (DEIDARA/TESTER/Root обходят) + - Защита от спама очереди: один и тот же режим нельзя поставить дважды + - Команда `sm_cr_status` + - Унифицирован стиль API (методмапы вместо `GetConVarBool`) +- **1.1.0** — Базовая версия с 7 режимами diff --git a/scripting/ArcaneGame_CustomRounds_Core.sp b/scripting/ArcaneGame_CustomRounds_Core.sp index 24b4f9c..70b1fec 100644 --- a/scripting/ArcaneGame_CustomRounds_Core.sp +++ b/scripting/ArcaneGame_CustomRounds_Core.sp @@ -1,4 +1,4 @@ -#pragma semicolon 1 +#pragma semicolon 1 #pragma newdecls required #include @@ -9,6 +9,9 @@ #define CR_PREFIX "\x04[ArcaneGame CR]\x01" #define CR_REASON_MAX 128 +#define CR_LOG_FILE "logs/custom_rounds.log" +#define CR_MOTD_BASE_URL "http://37.228.88.57/cr/" +#define MAX_SAVED_GRENADES 6 enum CustomRoundType { @@ -18,6 +21,9 @@ enum CustomRoundType CR_HE, CR_Knife, CR_Scout, + CR_ScoutNoScope, + CR_Deagle, + CR_DeagleHS, CR_LowGravity, CR_OneHP }; @@ -27,7 +33,7 @@ public Plugin myinfo = name = "ArcaneGame Custom Rounds Core", author = "deidara.dev", description = "Core plugin for custom rounds with AG Coin integration", - version = "1.1.0", + version = "1.2.0", url = "https://deidara.dev" }; @@ -43,10 +49,27 @@ int g_PlayerHeadshots[MAXPLAYERS + 1]; bool g_LoadoutSaved[MAXPLAYERS + 1]; bool g_RestoreLoadoutOnSpawn[MAXPLAYERS + 1]; + char g_SavedPrimary[MAXPLAYERS + 1][64]; +int g_SavedPrimaryClip[MAXPLAYERS + 1]; +int g_SavedPrimaryReserve[MAXPLAYERS + 1]; + char g_SavedSecondary[MAXPLAYERS + 1][64]; +int g_SavedSecondaryClip[MAXPLAYERS + 1]; +int g_SavedSecondaryReserve[MAXPLAYERS + 1]; + char g_SavedMelee[MAXPLAYERS + 1][64]; +char g_SavedGrenades[MAXPLAYERS + 1][MAX_SAVED_GRENADES][32]; +int g_SavedGrenadeCount[MAXPLAYERS + 1]; + +int g_SavedArmor[MAXPLAYERS + 1]; +bool g_SavedHelmet[MAXPLAYERS + 1]; +bool g_SavedDefuser[MAXPLAYERS + 1]; + +int g_RoundCounter = 0; +int g_LastCustomRoundNumber = -1000; + ConVar gCvarAccessFlag; ConVar gCvarAccessUseOverrides; ConVar gCvarCoinsEnable; @@ -57,6 +80,8 @@ ConVar gCvarCoinsSurvive; ConVar gCvarLowGravityValue; ConVar gCvarOneHPValue; ConVar gCvarAnnounce; +ConVar gCvarShowMOTD; +ConVar gCvarCooldownRounds; ConVar gCvarInfiniteAmmo; int g_OldInfiniteAmmo = 0; @@ -64,6 +89,7 @@ int g_OldInfiniteAmmo = 0; public void OnPluginStart() { RegConsoleCmd("sm_cr", Command_CR, "Opens Custom Rounds menu"); + RegConsoleCmd("sm_cr_status", Command_CR_Status, "Show current/pending custom round and cooldown status"); HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy); HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); @@ -85,19 +111,67 @@ public void OnPluginStart() gCvarLowGravityValue = CreateConVar("sm_cr_lowgravity_value", "0.40", "Gravity value for Low Gravity round.", FCVAR_NONE, true, 0.1, true, 1.0); gCvarOneHPValue = CreateConVar("sm_cr_onehp_value", "1", "Health value for One HP round.", FCVAR_NONE, true, 1.0, true, 100.0); gCvarAnnounce = CreateConVar("sm_cr_announce", "1", "Show chat messages about custom rounds.", FCVAR_NONE, true, 0.0, true, 1.0); + gCvarShowMOTD = CreateConVar("sm_cr_show_motd", "1", "Show MOTD image during freezetime when a custom round starts.", FCVAR_NONE, true, 0.0, true, 1.0); + gCvarCooldownRounds = CreateConVar("sm_cr_cooldown_rounds", "5", "Cooldown in rounds for regular admins between custom rounds. DEIDARA/TESTER and ROOT bypass.", FCVAR_NONE, true, 0.0); gCvarInfiniteAmmo = FindConVar("sv_infinite_ammo"); AutoExecConfig(true, "ArcaneGame_CustomRounds_Core"); + + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i)) + { + OnClientPutInServer(i); + } + } +} + +public void OnClientPutInServer(int client) +{ + FullResetClientState(client); + SDKHook(client, SDKHook_TraceAttack, Hook_TraceAttack); +} + +public void OnClientDisconnect(int client) +{ + FullResetClientState(client); +} + +void FullResetClientState(int client) +{ + if (client < 1 || client > MaxClients) + { + return; + } + + g_PlayerParticipated[client] = false; + g_PlayerKills[client] = 0; + g_PlayerHeadshots[client] = 0; + + g_LoadoutSaved[client] = false; + g_RestoreLoadoutOnSpawn[client] = false; + g_SavedPrimary[client][0] = '\0'; + g_SavedPrimaryClip[client] = 0; + g_SavedPrimaryReserve[client] = 0; + g_SavedSecondary[client][0] = '\0'; + g_SavedSecondaryClip[client] = 0; + g_SavedSecondaryReserve[client] = 0; + g_SavedMelee[client][0] = '\0'; + g_SavedGrenadeCount[client] = 0; + for (int i = 0; i < MAX_SAVED_GRENADES; i++) + { + g_SavedGrenades[client][i][0] = '\0'; + } + g_SavedArmor[client] = 0; + g_SavedHelmet[client] = false; + g_SavedDefuser[client] = false; } public void OnMapStart() { ResetRoundState(true); -} - -public void OnClientDisconnect(int client) -{ - ResetClientStats(client); + g_RoundCounter = 0; + g_LastCustomRoundNumber = -1000; } public Action Command_CR(int client, int args) @@ -118,6 +192,35 @@ public Action Command_CR(int client, int args) return Plugin_Handled; } +public Action Command_CR_Status(int client, int args) +{ + if (client <= 0 || !IsClientInGame(client)) + { + return Plugin_Handled; + } + + char currentName[64], pendingName[64]; + GetRoundDisplayName(g_CurrentRound, currentName, sizeof(currentName)); + GetRoundDisplayName(g_PendingRound, pendingName, sizeof(pendingName)); + + PrintToChat(client, "%s Текущий: \x04%s\x01, следующий: \x04%s\x01.", CR_PREFIX, currentName, pendingName); + + if (HasCustomRoundsAccess(client)) + { + int remaining = GetCooldownRemaining(client); + if (remaining > 0) + { + PrintToChat(client, "%s До конца кулдауна: \x04%d\x01 раунд(ов).", CR_PREFIX, remaining); + } + else + { + PrintToChat(client, "%s Кулдаун: нет.", CR_PREFIX); + } + } + + return Plugin_Handled; +} + bool HasCustomRoundsAccess(int client) { if (client <= 0 || !IsClientInGame(client)) @@ -136,7 +239,7 @@ bool HasCustomRoundsAccess(int client) return true; } - if (!GetConVarBool(gCvarAccessUseOverrides)) + if (!gCvarAccessUseOverrides.BoolValue) { return false; } @@ -145,6 +248,54 @@ bool HasCustomRoundsAccess(int client) return CheckCommandAccess(client, "sm_cr", requiredFlags, false); } +bool IsCooldownExempt(int client) +{ + if (client <= 0 || !IsClientInGame(client)) + { + return false; + } + + AdminId admin = GetUserAdmin(client); + if (admin == INVALID_ADMIN_ID) + { + return false; + } + + if ((GetUserFlagBits(client) & ADMFLAG_ROOT) != 0) + { + return true; + } + + if (IsClientInAllowedAdminGroup(admin, "DEIDARA") || IsClientInAllowedAdminGroup(admin, "TESTER")) + { + return true; + } + + return false; +} + +int GetCooldownRemaining(int client) +{ + if (IsCooldownExempt(client)) + { + return 0; + } + + int cd = gCvarCooldownRounds.IntValue; + if (cd <= 0) + { + return 0; + } + + int passed = g_RoundCounter - g_LastCustomRoundNumber; + if (passed >= cd) + { + return 0; + } + + return cd - passed; +} + bool IsClientInAllowedAdminGroup(AdminId admin, const char[] expectedGroupName) { char groupName[64]; @@ -180,22 +331,32 @@ void OpenMainMenu(int client) { Menu menu = new Menu(MenuHandler_Main); - char currentName[64]; - char pendingName[64]; - char title[256]; + char currentName[64], pendingName[64], title[256]; GetRoundDisplayName(g_CurrentRound, currentName, sizeof(currentName)); GetRoundDisplayName(g_PendingRound, pendingName, sizeof(pendingName)); - Format(title, sizeof(title), "Кастомные раунды\n \nТекущий: %s\nСледующий: %s", currentName, pendingName); + + int cooldown = GetCooldownRemaining(client); + if (cooldown > 0) + { + Format(title, sizeof(title), "Кастомные раунды\n \nТекущий: %s\nСледующий: %s\nКулдаун: %d р.", currentName, pendingName, cooldown); + } + else + { + Format(title, sizeof(title), "Кастомные раунды\n \nТекущий: %s\nСледующий: %s", currentName, pendingName); + } menu.SetTitle(title); menu.AddItem("1", "AWP Only"); - menu.AddItem("2", "AWP Only [NoScope]"); - menu.AddItem("3", "Только HE [беск. гранаты]"); - menu.AddItem("4", "Ножевой раунд"); - menu.AddItem("5", "Только Scout"); - menu.AddItem("6", "Низкая гравитация"); - menu.AddItem("7", "Режим 1 HP"); - menu.AddItem("8", "Отменить следующий кастомный раунд"); + menu.AddItem("2", "AWP NoScope"); + menu.AddItem("3", "Scout Only"); + menu.AddItem("4", "Scout NoScope"); + menu.AddItem("5", "Deagle Only"); + menu.AddItem("6", "Deagle HS Only"); + menu.AddItem("7", "Только HE [беск. гранаты]"); + menu.AddItem("8", "Ножевой раунд"); + menu.AddItem("9", "Низкая гравитация"); + menu.AddItem("10", "Режим 1 HP"); + menu.AddItem("c", "Отменить следующий кастомный раунд"); menu.ExitButton = true; menu.Display(client, 20); @@ -216,18 +377,33 @@ public int MenuHandler_Main(Menu menu, MenuAction action, int client, int item) char info[8]; menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "c")) + { + CancelPendingRound(client); + return 0; + } + int value = StringToInt(info); + CustomRoundType selected = CR_None; switch (value) { - case 1: QueueCustomRound(client, CR_AWP); - case 2: QueueCustomRound(client, CR_NoScope); - case 3: QueueCustomRound(client, CR_HE); - case 4: QueueCustomRound(client, CR_Knife); - case 5: QueueCustomRound(client, CR_Scout); - case 6: QueueCustomRound(client, CR_LowGravity); - case 7: QueueCustomRound(client, CR_OneHP); - case 8: CancelPendingRound(client); + case 1: selected = CR_AWP; + case 2: selected = CR_NoScope; + case 3: selected = CR_Scout; + case 4: selected = CR_ScoutNoScope; + case 5: selected = CR_Deagle; + case 6: selected = CR_DeagleHS; + case 7: selected = CR_HE; + case 8: selected = CR_Knife; + case 9: selected = CR_LowGravity; + case 10: selected = CR_OneHP; + } + + if (selected != CR_None) + { + QueueCustomRound(client, selected); } return 0; @@ -235,14 +411,30 @@ public int MenuHandler_Main(Menu menu, MenuAction action, int client, int item) void QueueCustomRound(int client, CustomRoundType roundType) { + int cooldown = GetCooldownRemaining(client); + if (cooldown > 0) + { + PrintToChat(client, "%s \x02Кулдаун: ещё \x04%d\x01\x02 раунд(ов) до запуска нового кастомного раунда.", CR_PREFIX, cooldown); + return; + } + + if (g_PendingRound == roundType) + { + PrintToChat(client, "%s \x02Этот режим уже в очереди.", CR_PREFIX); + return; + } + g_PendingRound = roundType; - if (GetConVarBool(gCvarAnnounce)) + char roundName[64]; + GetRoundDisplayName(roundType, roundName, sizeof(roundName)); + + if (gCvarAnnounce.BoolValue) { - char roundName[64]; - GetRoundDisplayName(roundType, roundName, sizeof(roundName)); PrintToChatAll("%s \x01Администратор \x03%N\x01 выбрал режим: \x04%s\x01. Он начнётся в следующем раунде.", CR_PREFIX, client, roundName); } + + LogCRAction(client, "ВЫБРАЛ режим: %s", roundName); } void CancelPendingRound(int client) @@ -253,13 +445,16 @@ void CancelPendingRound(int client) return; } - if (GetConVarBool(gCvarAnnounce)) + char roundName[64]; + GetRoundDisplayName(g_PendingRound, roundName, sizeof(roundName)); + + if (gCvarAnnounce.BoolValue) { - char roundName[64]; - GetRoundDisplayName(g_PendingRound, roundName, sizeof(roundName)); PrintToChatAll("%s \x01Администратор \x03%N\x01 отменил режим: \x04%s\x01.", CR_PREFIX, client, roundName); } + LogCRAction(client, "ОТМЕНИЛ режим: %s", roundName); + g_PendingRound = CR_None; } @@ -267,6 +462,7 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { g_RoundLive = true; g_ModeApplied = false; + g_RoundCounter++; ResetAllPlayerStats(); @@ -274,18 +470,24 @@ public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) { g_CurrentRound = g_PendingRound; g_PendingRound = CR_None; + g_LastCustomRoundNumber = g_RoundCounter; } if (g_CurrentRound != CR_None) { CreateTimer(0.2, Timer_ApplyRoundMode, _, TIMER_FLAG_NO_MAPCHANGE); - if (GetConVarBool(gCvarAnnounce)) + if (gCvarAnnounce.BoolValue) { char roundName[64]; GetRoundDisplayName(g_CurrentRound, roundName, sizeof(roundName)); PrintToChatAll("%s \x01Кастомный раунд начался: \x04%s\x01.", CR_PREFIX, roundName); } + + if (gCvarShowMOTD.BoolValue) + { + ShowFreezeImageToAll(g_CurrentRound); + } } } @@ -322,6 +524,13 @@ public void Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast return; } + // Применяем мод per-spawn ТОЛЬКО если глобальный таймер уже отработал (опоздавший игрок). + // Иначе глобальный Timer_ApplyRoundMode сам всех обработает — без двойного применения. + if (!g_ModeApplied) + { + return; + } + CreateTimer(0.15, Timer_ApplyModeToClient, GetClientUserId(client), TIMER_FLAG_NO_MAPCHANGE); } @@ -345,21 +554,19 @@ public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast g_PlayerHeadshots[attacker]++; } - int killCoins = GetConVarInt(gCvarCoinsKill); + int killCoins = gCvarCoinsKill.IntValue; if (killCoins > 0) { - char reason[CR_REASON_MAX]; - char roundName[64]; + char reason[CR_REASON_MAX], roundName[64]; GetRoundDisplayName(g_CurrentRound, roundName, sizeof(roundName)); Format(reason, sizeof(reason), "CustomRound Kill (%s)", roundName); CR_GiveCoins(attacker, killCoins, reason); } - int headshotCoins = GetConVarInt(gCvarCoinsHeadshot); + int headshotCoins = gCvarCoinsHeadshot.IntValue; if (headshot && headshotCoins > 0) { - char reason[CR_REASON_MAX]; - char roundName[64]; + char reason[CR_REASON_MAX], roundName[64]; GetRoundDisplayName(g_CurrentRound, roundName, sizeof(roundName)); Format(reason, sizeof(reason), "CustomRound Headshot (%s)", roundName); CR_GiveCoins(attacker, headshotCoins, reason); @@ -367,7 +574,6 @@ public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast } } - public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3], int &weapon, int &subtype, int &cmdnum, int &tickcount, int &seed, int mouse[2]) { if (client <= 0 || client > MaxClients || !IsClientInGame(client) || !IsPlayerAlive(client)) @@ -375,7 +581,7 @@ public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3 return Plugin_Continue; } - if (g_CurrentRound == CR_NoScope) + if (g_CurrentRound == CR_NoScope || g_CurrentRound == CR_ScoutNoScope) { if (buttons & IN_ATTACK2) { @@ -386,13 +592,35 @@ public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3 if (GetEntProp(client, Prop_Send, "m_bIsScoped") != 0) { SetEntProp(client, Prop_Send, "m_bIsScoped", 0); - SetEntProp(client, Prop_Send, "m_iFOV", 0); + SetEntProp(client, Prop_Send, "m_iFOV", 90); } } return Plugin_Continue; } +public Action Hook_TraceAttack(int victim, int &attacker, int &inflictor, float &damage, int &damagetype, int &ammotype, int hitbox, int hitgroup) +{ + if (g_CurrentRound != CR_DeagleHS) + { + return Plugin_Continue; + } + + if (attacker <= 0 || attacker > MaxClients) + { + return Plugin_Continue; + } + + // HITGROUP_HEAD = 1; всё остальное — 0 урона + if (hitgroup != 1) + { + damage = 0.0; + return Plugin_Changed; + } + + return Plugin_Continue; +} + public Action Timer_ApplyRoundMode(Handle timer) { if (g_CurrentRound == CR_None) @@ -401,7 +629,6 @@ public Action Timer_ApplyRoundMode(Handle timer) } g_ModeApplied = true; - ApplyGlobalModeSettings(true); for (int client = 1; client <= MaxClients; client++) @@ -410,7 +637,6 @@ public Action Timer_ApplyRoundMode(Handle timer) { continue; } - ApplyModeToClient(client); } @@ -455,7 +681,7 @@ void ApplyModeToClient(int client) GivePlayerItem(client, "weapon_knife"); GivePlayerItem(client, "weapon_awp"); SetEntProp(client, Prop_Send, "m_bIsScoped", 0); - SetEntProp(client, Prop_Send, "m_iFOV", 0); + SetEntProp(client, Prop_Send, "m_iFOV", 90); SetEntProp(client, Prop_Data, "m_iHealth", 100); SetEntityGravity(client, 1.0); } @@ -482,34 +708,37 @@ void ApplyModeToClient(int client) SetEntProp(client, Prop_Data, "m_iHealth", 100); SetEntityGravity(client, 1.0); } + case CR_ScoutNoScope: + { + StripPlayerWeapons(client); + GivePlayerItem(client, "weapon_knife"); + GivePlayerItem(client, "weapon_ssg08"); + SetEntProp(client, Prop_Send, "m_bIsScoped", 0); + SetEntProp(client, Prop_Send, "m_iFOV", 90); + SetEntProp(client, Prop_Data, "m_iHealth", 100); + SetEntityGravity(client, 1.0); + } + case CR_Deagle, CR_DeagleHS: + { + StripPlayerWeapons(client); + GivePlayerItem(client, "weapon_knife"); + GivePlayerItem(client, "weapon_deagle"); + SetEntProp(client, Prop_Data, "m_iHealth", 100); + SetEntityGravity(client, 1.0); + } case CR_LowGravity: { SetEntProp(client, Prop_Data, "m_iHealth", 100); - SetEntityGravity(client, GetConVarFloat(gCvarLowGravityValue)); + SetEntityGravity(client, gCvarLowGravityValue.FloatValue); } case CR_OneHP: { - SetEntProp(client, Prop_Data, "m_iHealth", GetConVarInt(gCvarOneHPValue)); + SetEntProp(client, Prop_Data, "m_iHealth", gCvarOneHPValue.IntValue); SetEntityGravity(client, 1.0); } } } -void GiveDefaultCombatLoadout(int client) -{ - GivePlayerItem(client, "weapon_glock"); - GivePlayerItem(client, "weapon_ak47"); - - int team = GetClientTeam(client); - if (team == CS_TEAM_CT) - { - StripPlayerWeapons(client); - GivePlayerItem(client, "weapon_knife"); - GivePlayerItem(client, "weapon_hkp2000"); - GivePlayerItem(client, "weapon_m4a1"); - } -} - void ApplyGlobalModeSettings(bool enable) { if (gCvarInfiniteAmmo == null) @@ -542,7 +771,6 @@ void ResetRoundState(bool fullReset) { SetEntityGravity(client, 1.0); } - ResetClientStats(client); } @@ -580,13 +808,21 @@ void ResetClientStats(int client) g_SavedPrimary[client][0] = '\0'; g_SavedSecondary[client][0] = '\0'; g_SavedMelee[client][0] = '\0'; + g_SavedGrenadeCount[client] = 0; + for (int i = 0; i < MAX_SAVED_GRENADES; i++) + { + g_SavedGrenades[client][i][0] = '\0'; + } + g_SavedArmor[client] = 0; + g_SavedHelmet[client] = false; + g_SavedDefuser[client] = false; } } void AwardEndRoundCoins(int winnerTeam) { - int winCoins = GetConVarInt(gCvarCoinsWin); - int surviveCoins = GetConVarInt(gCvarCoinsSurvive); + int winCoins = gCvarCoinsWin.IntValue; + int surviveCoins = gCvarCoinsSurvive.IntValue; for (int client = 1; client <= MaxClients; client++) { @@ -599,8 +835,7 @@ void AwardEndRoundCoins(int winnerTeam) { if (winCoins > 0) { - char reason[CR_REASON_MAX]; - char roundName[64]; + char reason[CR_REASON_MAX], roundName[64]; GetRoundDisplayName(g_CurrentRound, roundName, sizeof(roundName)); Format(reason, sizeof(reason), "CustomRound Win (%s)", roundName); CR_GiveCoins(client, winCoins, reason); @@ -608,8 +843,7 @@ void AwardEndRoundCoins(int winnerTeam) if (surviveCoins > 0 && IsPlayerAlive(client)) { - char reason[CR_REASON_MAX]; - char roundName[64]; + char reason[CR_REASON_MAX], roundName[64]; GetRoundDisplayName(g_CurrentRound, roundName, sizeof(roundName)); Format(reason, sizeof(reason), "CustomRound Survive (%s)", roundName); CR_GiveCoins(client, surviveCoins, reason); @@ -626,15 +860,35 @@ void SaveClientLoadout(int client) return; } - SaveWeaponClassnameFromSlot(client, 0, g_SavedPrimary[client], sizeof(g_SavedPrimary[])); - SaveWeaponClassnameFromSlot(client, 1, g_SavedSecondary[client], sizeof(g_SavedSecondary[])); - SaveWeaponClassnameFromSlot(client, 2, g_SavedMelee[client], sizeof(g_SavedMelee[])); + SaveWeaponSlot(client, 0, g_SavedPrimary[client], sizeof(g_SavedPrimary[]), + g_SavedPrimaryClip[client], g_SavedPrimaryReserve[client]); + SaveWeaponSlot(client, 1, g_SavedSecondary[client], sizeof(g_SavedSecondary[]), + g_SavedSecondaryClip[client], g_SavedSecondaryReserve[client]); + + int meleeWeapon = GetPlayerWeaponSlot(client, 2); + if (meleeWeapon != -1 && IsValidEntity(meleeWeapon)) + { + GetEntityClassname(meleeWeapon, g_SavedMelee[client], sizeof(g_SavedMelee[])); + } + else + { + g_SavedMelee[client][0] = '\0'; + } + + SaveAllGrenades(client); + + g_SavedArmor[client] = GetEntProp(client, Prop_Send, "m_ArmorValue"); + g_SavedHelmet[client] = GetEntProp(client, Prop_Send, "m_bHasHelmet") != 0; + g_SavedDefuser[client] = GetEntProp(client, Prop_Send, "m_bHasDefuser") != 0; + g_LoadoutSaved[client] = true; } -void SaveWeaponClassnameFromSlot(int client, int slot, char[] buffer, int maxlen) +void SaveWeaponSlot(int client, int slot, char[] classBuf, int classBufLen, int &clipOut, int &reserveOut) { - buffer[0] = '\0'; + classBuf[0] = '\0'; + clipOut = 0; + reserveOut = 0; int weapon = GetPlayerWeaponSlot(client, slot); if (weapon == -1 || !IsValidEntity(weapon)) @@ -642,7 +896,59 @@ void SaveWeaponClassnameFromSlot(int client, int slot, char[] buffer, int maxlen return; } - GetEntityClassname(weapon, buffer, maxlen); + GetEntityClassname(weapon, classBuf, classBufLen); + + if (HasEntProp(weapon, Prop_Send, "m_iClip1")) + { + clipOut = GetEntProp(weapon, Prop_Send, "m_iClip1"); + } + if (HasEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount")) + { + reserveOut = GetEntProp(weapon, Prop_Send, "m_iPrimaryReserveAmmoCount"); + } +} + +void SaveAllGrenades(int client) +{ + g_SavedGrenadeCount[client] = 0; + + int offset = FindSendPropInfo("CCSPlayer", "m_hMyWeapons"); + if (offset <= 0) + { + return; + } + + char classname[32]; + for (int i = 0; i < 64; i++) + { + int weapon = GetEntDataEnt2(client, offset + (i * 4)); + if (weapon == -1 || !IsValidEntity(weapon)) + { + continue; + } + + GetEntityClassname(weapon, classname, sizeof(classname)); + if (!IsGrenadeClassname(classname)) + { + continue; + } + + if (g_SavedGrenadeCount[client] < MAX_SAVED_GRENADES) + { + strcopy(g_SavedGrenades[client][g_SavedGrenadeCount[client]], 32, classname); + g_SavedGrenadeCount[client]++; + } + } +} + +bool IsGrenadeClassname(const char[] classname) +{ + return StrEqual(classname, "weapon_hegrenade") + || StrEqual(classname, "weapon_flashbang") + || StrEqual(classname, "weapon_smokegrenade") + || StrEqual(classname, "weapon_molotov") + || StrEqual(classname, "weapon_incgrenade") + || StrEqual(classname, "weapon_decoy"); } void PrepareLoadoutRestoreForNextSpawn() @@ -687,16 +993,54 @@ void RestoreClientLoadout(int client) if (g_SavedSecondary[client][0] != '\0') { - GivePlayerItem(client, g_SavedSecondary[client]); + int wep = GivePlayerItem(client, g_SavedSecondary[client]); + if (wep != -1 && IsValidEntity(wep)) + { + if (HasEntProp(wep, Prop_Send, "m_iClip1")) + { + SetEntProp(wep, Prop_Send, "m_iClip1", g_SavedSecondaryClip[client]); + } + if (HasEntProp(wep, Prop_Send, "m_iPrimaryReserveAmmoCount")) + { + SetEntProp(wep, Prop_Send, "m_iPrimaryReserveAmmoCount", g_SavedSecondaryReserve[client]); + } + } gaveSomething = true; } if (g_SavedPrimary[client][0] != '\0') { - GivePlayerItem(client, g_SavedPrimary[client]); + int wep = GivePlayerItem(client, g_SavedPrimary[client]); + if (wep != -1 && IsValidEntity(wep)) + { + if (HasEntProp(wep, Prop_Send, "m_iClip1")) + { + SetEntProp(wep, Prop_Send, "m_iClip1", g_SavedPrimaryClip[client]); + } + if (HasEntProp(wep, Prop_Send, "m_iPrimaryReserveAmmoCount")) + { + SetEntProp(wep, Prop_Send, "m_iPrimaryReserveAmmoCount", g_SavedPrimaryReserve[client]); + } + } gaveSomething = true; } + for (int i = 0; i < g_SavedGrenadeCount[client]; i++) + { + if (g_SavedGrenades[client][i][0] != '\0') + { + GivePlayerItem(client, g_SavedGrenades[client][i]); + gaveSomething = true; + } + } + + SetEntProp(client, Prop_Send, "m_ArmorValue", g_SavedArmor[client]); + SetEntProp(client, Prop_Send, "m_bHasHelmet", g_SavedHelmet[client] ? 1 : 0); + if (GetClientTeam(client) == CS_TEAM_CT) + { + SetEntProp(client, Prop_Send, "m_bHasDefuser", g_SavedDefuser[client] ? 1 : 0); + } + if (!gaveSomething) { GivePlayerItem(client, "weapon_knife"); @@ -707,6 +1051,14 @@ void RestoreClientLoadout(int client) g_SavedPrimary[client][0] = '\0'; g_SavedSecondary[client][0] = '\0'; g_SavedMelee[client][0] = '\0'; + g_SavedGrenadeCount[client] = 0; + for (int i = 0; i < MAX_SAVED_GRENADES; i++) + { + g_SavedGrenades[client][i][0] = '\0'; + } + g_SavedArmor[client] = 0; + g_SavedHelmet[client] = false; + g_SavedDefuser[client] = false; } bool CR_GiveCoins(int client, int amount, const char[] reason) @@ -716,7 +1068,7 @@ bool CR_GiveCoins(int client, int amount, const char[] reason) return false; } - if (!GetConVarBool(gCvarCoinsEnable)) + if (!gCvarCoinsEnable.BoolValue) { return false; } @@ -755,7 +1107,7 @@ public Action CommandListener_BuyBlock(int client, const char[] command, int arg switch (g_CurrentRound) { - case CR_AWP, CR_NoScope, CR_HE, CR_Knife, CR_Scout: + case CR_AWP, CR_NoScope, CR_HE, CR_Knife, CR_Scout, CR_ScoutNoScope, CR_Deagle, CR_DeagleHS: { PrintCenterText(client, "Покупка отключена во время кастомного раунда"); return Plugin_Handled; @@ -772,7 +1124,7 @@ public Action CommandListener_ZoomBlock(int client, const char[] command, int ar return Plugin_Continue; } - if (g_CurrentRound == CR_NoScope) + if (g_CurrentRound == CR_NoScope || g_CurrentRound == CR_ScoutNoScope) { return Plugin_Handled; } @@ -799,12 +1151,105 @@ void GetRoundDisplayName(CustomRoundType roundType, char[] buffer, int maxlen) switch (roundType) { case CR_AWP: strcopy(buffer, maxlen, "AWP Only"); - case CR_NoScope: strcopy(buffer, maxlen, "AWP Only [NoScope]"); + case CR_NoScope: strcopy(buffer, maxlen, "AWP NoScope"); case CR_HE: strcopy(buffer, maxlen, "HE Only"); case CR_Knife: strcopy(buffer, maxlen, "Ножевой раунд"); case CR_Scout: strcopy(buffer, maxlen, "Scout Only"); + case CR_ScoutNoScope: strcopy(buffer, maxlen, "Scout NoScope"); + case CR_Deagle: strcopy(buffer, maxlen, "Deagle Only"); + case CR_DeagleHS: strcopy(buffer, maxlen, "Deagle HS Only"); case CR_LowGravity: strcopy(buffer, maxlen, "Низкая гравитация"); case CR_OneHP: strcopy(buffer, maxlen, "1 HP"); default: strcopy(buffer, maxlen, "Нет"); } -} \ No newline at end of file +} + +void GetRoundUrlSlug(CustomRoundType roundType, char[] buffer, int maxlen) +{ + switch (roundType) + { + case CR_AWP: strcopy(buffer, maxlen, "awp"); + case CR_NoScope: strcopy(buffer, maxlen, "awp-noscope"); + case CR_HE: strcopy(buffer, maxlen, "he"); + case CR_Knife: strcopy(buffer, maxlen, "knife"); + case CR_Scout: strcopy(buffer, maxlen, "scout"); + case CR_ScoutNoScope: strcopy(buffer, maxlen, "scout-noscope"); + case CR_Deagle: strcopy(buffer, maxlen, "deagle"); + case CR_DeagleHS: strcopy(buffer, maxlen, "deagle-hs"); + case CR_LowGravity: strcopy(buffer, maxlen, "lowgrav"); + case CR_OneHP: strcopy(buffer, maxlen, "onehp"); + default: strcopy(buffer, maxlen, ""); + } +} + +void ShowFreezeImageToAll(CustomRoundType mode) +{ + char slug[32]; + GetRoundUrlSlug(mode, slug, sizeof(slug)); + if (slug[0] == '\0') + { + return; + } + + char url[256]; + Format(url, sizeof(url), "%s%s.html", CR_MOTD_BASE_URL, slug); + + char title[64]; + GetRoundDisplayName(mode, title, sizeof(title)); + + KeyValues kv = new KeyValues("data"); + kv.SetString("title", title); + kv.SetNum("type", 2); // MOTDPANEL_TYPE_URL + kv.SetString("msg", url); + kv.SetNum("customsvr", 1); + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) + { + continue; + } + ShowVGUIPanel(i, "info", kv, true); + } + + delete kv; +} + +void LogCRAction(int client, const char[] format, any ...) +{ + char message[256]; + VFormat(message, sizeof(message), format, 3); + + char path[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, path, sizeof(path), CR_LOG_FILE); + + char timestamp[32]; + FormatTime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S"); + + char adminName[MAX_NAME_LENGTH], adminSteam[32]; + if (client > 0 && IsClientInGame(client)) + { + GetClientName(client, adminName, sizeof(adminName)); + if (!GetClientAuthId(client, AuthId_Steam2, adminSteam, sizeof(adminSteam))) + { + strcopy(adminSteam, sizeof(adminSteam), "UNKNOWN"); + } + } + else + { + strcopy(adminName, sizeof(adminName), "CONSOLE"); + strcopy(adminSteam, sizeof(adminSteam), "CONSOLE"); + } + + char roleTag[16]; + if (IsCooldownExempt(client)) + { + strcopy(roleTag, sizeof(roleTag), "FULL"); + } + else + { + strcopy(roleTag, sizeof(roleTag), "REG"); + } + + LogToFileEx(path, "[%s] [%s] %s (%s) -> %s [раунд %d]", timestamp, roleTag, adminName, adminSteam, message, g_RoundCounter); +}