From d86431a40cb92f32fd4d22e35d39de66fd21ac18 Mon Sep 17 00:00:00 2001 From: deidara Date: Fri, 1 May 2026 06:57:30 +0300 Subject: [PATCH] Initial commit: arcane-advertisement plugin with documentation and config --- README.md | 105 +++++++ configs/reklama.ini | 60 ++++ scripting/arcane_advertisement.sp | 479 ++++++++++++++++++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 README.md create mode 100644 configs/reklama.ini create mode 100644 scripting/arcane_advertisement.sp diff --git a/README.md b/README.md new file mode 100644 index 0000000..521bba5 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# ARCANEGAME Advertisement + +Плагин рекламных объявлений для CS:GO серверов на SourceMod. Показывает объявления в чат, по центру экрана, в hint-блоке и HUD. + +## Функции + +- Периодический показ объявлений по таймеру (настраиваемый интервал) +- Поддержка 4 каналов вывода: **Chat**, **Center**, **Alert (hint)**, **HUD** +- До **128 объявлений** в очереди, показ по кругу +- Поддержка **цветовых тегов** в чате: `{GREEN}`, `{RED}`, `{BLUE}`, `{OLIVE}` и др. +- Поддержка **информационных тегов**: `{MAP}`, `{NEXTMAP}`, `{PL}`, `{TIME}`, `{DATE}`, `{IP:PORT}`, `{TIMELEFT}`, `{SERVERNAME}`, `{TIC}`, `{ADMINSONLINE1}`, `{ADMINSONLINE2}` +- Красивые названия карт через словарь `Названия карт` +- Живая перезагрузка конфига без рестарта + +## Зависимости + +- [SourceMod](https://www.sourcemod.net/) 1.10+ +- [mapchooser](https://wiki.alliedmods.net/Mapchooser_%28SourceMod%29) или nextmap (только для тега `{NEXTMAP}`) + +## Установка + +1. Скомпилировать `scripting/arcane_advertisement.sp` +2. Положить `.smx` в `addons/sourcemod/plugins/` +3. Положить `configs/reklama.ini` в `addons/sourcemod/configs/` +4. Перезапустить сервер или загрузить плагин: `sm plugins load arcane_advertisement` + +## Конфиг + +Путь: `addons/sourcemod/configs/reklama.ini` + +``` +"Реклама" +{ + "Таймер объявлений" "25" + + "Названия карт" + { + "de_dust2" "Dust II" + "de_mirage" "Mirage" + } + + "Список объявлений" + { + "1" + { + "Chat" "{GREEN}[ARCANEGAME]{DEFAULT} Добро пожаловать на сервер!" + "Center" "Добро пожаловать!" + } + "2" + { + "Chat" "{GREEN}[INFO]{DEFAULT} Карта: {MAP} | Игроков: {PL}" + } + "3" + { + "HUD" + { + "text" "arcanegame.ru | {MAP}" + "x" "-1.0" + "y" "0.85" + "color" "0 255 100 200" + "holdTime" "25" + } + } + } +} +``` + +## Цветовые теги (только для поля `Chat`) + +| Тег | Цвет | +|---|---| +| `{DEFAULT}` | Белый | +| `{GREEN}` | Зелёный | +| `{RED}` | Красный | +| `{BLUE}` | Синий | +| `{OLIVE}` | Оливковый | +| `{PURPLE}` | Фиолетовый | +| `{GRAY}` | Серый | +| `{LIME}` | Лаймовый | + +## Информационные теги + +| Тег | Значение | +|---|---| +| `{MAP}` | Текущая карта (с учётом словаря) | +| `{NEXTMAP}` | Следующая карта | +| `{PL}` | Количество живых игроков | +| `{TIME}` | Текущее время `HH:MM:SS` | +| `{DATE}` | Текущая дата `DD.MM.YYYY` | +| `{IP:PORT}` | IP и порт сервера | +| `{TIMELEFT}` | Оставшееся время карты | +| `{SERVERNAME}` | Имя сервера (hostname) | +| `{TIC}` | Тикрейт сервера | +| `{ADMINSONLINE1}` | Онлайн-админы через запятую | +| `{ADMINSONLINE2}` | Онлайн-админы каждый с новой строки | + +## Команды + +| Команда | Доступ | Описание | +|---|---|---| +| `sm_reklama_reload` | Root | Перезагрузить конфиг рекламы | + +## Версия + +`1.0.0` — Автор: deidara diff --git a/configs/reklama.ini b/configs/reklama.ini new file mode 100644 index 0000000..04404ad --- /dev/null +++ b/configs/reklama.ini @@ -0,0 +1,60 @@ +"Реклама" +{ + // Интервал между объявлениями в секундах (минимум 1) + "Таймер объявлений" "25" + + // Красивые названия карт для тега {MAP} и {NEXTMAP} + "Названия карт" + { + "de_dust2" "Dust II" + "de_mirage" "Mirage" + "de_inferno" "Inferno" + "de_nuke" "Nuke" + "de_overpass" "Overpass" + "de_ancient" "Ancient" + "de_anubis" "Anubis" + } + + // Список объявлений (порядковый номер — любой уникальный ключ) + "Список объявлений" + { + "1" + { + // Chat: поддерживает цветовые теги {GREEN}, {RED}, {BLUE} и т.д. + // \n — перенос строки (создаёт несколько строк в чате) + "Chat" "{GREEN}[ARCANEGAME]{DEFAULT} Добро пожаловать на сервер! {OLIVE}arcanegame.ru" + } + "2" + { + "Chat" "{GREEN}[INFO]{DEFAULT} Карта: {GREEN}{MAP}{DEFAULT} | Игроков: {GREEN}{PL}{DEFAULT} | Время: {OLIVE}{TIME}" + } + "3" + { + "Chat" "{GREEN}[NEXTMAP]{DEFAULT} Следующая карта: {GREEN}{NEXTMAP}" + "Center" "Следующая карта: {NEXTMAP}" + } + "4" + { + // Alert — показывается в hint-блоке (снизу экрана) + "Alert" "Сервер: {SERVERNAME} | {IP:PORT}" + } + "5" + { + // HUD — текст поверх экрана + // x/y: -1.0 = по центру, 0.0..1.0 = доля экрана + // color: R G B A (0-255) + "HUD" + { + "text" "arcanegame.ru | {MAP}" + "x" "-1.0" + "y" "0.85" + "color" "0 255 100 200" + "holdTime" "25" + } + } + "6" + { + "Chat" "{GREEN}[ADMIN]{DEFAULT} Онлайн-администраторы: {OLIVE}{ADMINSONLINE1}" + } + } +} diff --git a/scripting/arcane_advertisement.sp b/scripting/arcane_advertisement.sp new file mode 100644 index 0000000..847421f --- /dev/null +++ b/scripting/arcane_advertisement.sp @@ -0,0 +1,479 @@ +// ============================================================ +// ARCANEGAME Advertisement Plugin +// Конфиг: addons/sourcemod/configs/reklama.ini +// Зависимости: mapchooser / nextmap (для тега {NEXTMAP}) +// Внешние includes НЕ нужны +// ============================================================ + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#define PLUGIN_VERSION "1.0.0" +#define MAX_ADS 128 +#define MAX_MSG_LEN 1024 +#define MAX_MAP_NAME 128 + +// CS:GO фиксированные коды цветов чата (работают с PrintToChatAll) +#define CLR_DEFAULT "\x01" // белый +#define CLR_RED "\x02" // красный (как в музыкальном плагине ARCANE GAME) +#define CLR_LIGHTPURPLE "\x03" // цвет команды / фиолетовый +#define CLR_GREEN "\x04" // зелёный +#define CLR_OLIVE "\x05" // оливковый +#define CLR_LIGHTGREEN "\x06" // светло-зелёный +#define CLR_LIGHTRED "\x02" // светло-красный (аналог красного) +#define CLR_LIME "\x06" // лаймовый +#define CLR_GRAY "\x08" // серый +#define CLR_PURPLE "\x03" // фиолетовый +#define CLR_BLUE "\x0B" // синий +#define CLR_LIGHTBLUE "\x0B" // светло-синий +#define CLR_LIGHTOLIVE "\x05" // светло-оливковый + +// ── HUD-блок внутри объявления ──────────────────────────────── +enum struct HudAd +{ + float x; + float y; + int color[4]; + float holdTime; + char text[MAX_MSG_LEN]; +} + +// ── Одно объявление ─────────────────────────────────────────── +enum struct Advertisement +{ + char chat [MAX_MSG_LEN]; + char center[MAX_MSG_LEN]; + char alert [MAX_MSG_LEN]; + HudAd hud; + bool hasChat; + bool hasCenter; + bool hasAlert; + bool hasHud; +} + +// ── Глобальные переменные ───────────────────────────────────── +float g_fInterval = 25.0; +Advertisement g_Ads[MAX_ADS]; +int g_iAdCount; +int g_iCurrentAd; +StringMap g_MapNames; +Handle g_hTimer = INVALID_HANDLE; +Handle g_hHud = INVALID_HANDLE; + +// ── Информация о плагине ────────────────────────────────────── +public Plugin myinfo = +{ + name = "ARCANEGAME Advertisement", + author = "deidara", + description = "Рекламные сообщения ARCANEGAME", + version = PLUGIN_VERSION, + url = "arcanegame.ru" +}; + +// ============================================================= +// Старт плагина +// ============================================================= +public void OnPluginStart() +{ + g_MapNames = new StringMap(); + g_hHud = CreateHudSynchronizer(); + + LoadConfig(); + + RegAdminCmd("sm_reklama_reload", Cmd_Reload, ADMFLAG_ROOT, + "Перезагрузить рекламные сообщения"); +} + +// ============================================================= +// Конец карты — обнуляем хэндл (SM уже освободил таймер) +// ============================================================= +public void OnMapEnd() +{ + g_hTimer = INVALID_HANDLE; +} + +// ============================================================= +// Старт карты — запускаем таймер +// ============================================================= +public void OnMapStart() +{ + delete g_hTimer; + g_hTimer = INVALID_HANDLE; + g_iCurrentAd = 0; + + if (g_iAdCount > 0) + g_hTimer = CreateTimer(g_fInterval, Timer_ShowAd, _, + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +// ============================================================= +// Команда перезагрузки +// ============================================================= +public Action Cmd_Reload(int client, int args) +{ + delete g_hTimer; + g_hTimer = INVALID_HANDLE; + + LoadConfig(); + + if (g_iAdCount > 0) + g_hTimer = CreateTimer(g_fInterval, Timer_ShowAd, _, + TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); + + ReplyToCommand(client, + "[ARCANEGAME] Реклама перезагружена. Загружено объявлений: %d", + g_iAdCount); + + return Plugin_Handled; +} + +// ============================================================= +// Загрузка конфига +// ============================================================= +void LoadConfig() +{ + char sPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sPath, sizeof(sPath), "configs/reklama.ini"); + + if (!FileExists(sPath)) + { + LogError("[ARCANEGAME AD] Конфиг не найден: %s", sPath); + return; + } + + KeyValues kv = new KeyValues("Реклама"); + if (!kv.ImportFromFile(sPath)) + { + LogError("[ARCANEGAME AD] Ошибка чтения конфига: %s", sPath); + delete kv; + return; + } + + g_fInterval = kv.GetFloat("Таймер объявлений", 25.0); + if (g_fInterval < 1.0) g_fInterval = 1.0; + + g_iAdCount = 0; + g_iCurrentAd = 0; + + // ── Названия карт ───────────────────────────────────────── + delete g_MapNames; + g_MapNames = new StringMap(); + + if (kv.JumpToKey("Названия карт")) + { + if (kv.GotoFirstSubKey(false)) + { + do + { + char sKey[MAX_MAP_NAME], sVal[MAX_MAP_NAME]; + kv.GetSectionName(sKey, sizeof(sKey)); + kv.GetString(NULL_STRING, sVal, sizeof(sVal)); + if (sKey[0] && sVal[0]) + g_MapNames.SetString(sKey, sVal); + } + while (kv.GotoNextKey(false)); + kv.GoBack(); + } + kv.GoBack(); + } + + // ── Список объявлений ───────────────────────────────────── + if (kv.JumpToKey("Список объявлений")) + { + if (kv.GotoFirstSubKey()) + { + do + { + if (g_iAdCount >= MAX_ADS) break; + + Advertisement ad; + char buf[MAX_MSG_LEN]; + + kv.GetString("Chat", buf, sizeof(buf)); + if (buf[0]) { strcopy(ad.chat, sizeof(ad.chat), buf); ad.hasChat = true; } + + kv.GetString("Center", buf, sizeof(buf)); + if (buf[0]) { strcopy(ad.center, sizeof(ad.center), buf); ad.hasCenter = true; } + + kv.GetString("Alert", buf, sizeof(buf)); + if (buf[0]) { strcopy(ad.alert, sizeof(ad.alert), buf); ad.hasAlert = true; } + + if (kv.JumpToKey("HUD")) + { + kv.GetString("text", buf, sizeof(buf)); + if (buf[0]) + { + strcopy(ad.hud.text, sizeof(ad.hud.text), buf); + ad.hud.x = kv.GetFloat("x", -1.0); + ad.hud.y = kv.GetFloat("y", -1.0); + ad.hud.holdTime = kv.GetFloat("holdTime", g_fInterval); + + char sColor[32]; + kv.GetString("color", sColor, sizeof(sColor), "255 255 255 255"); + ParseRGBA(sColor, ad.hud.color); + + ad.hasHud = true; + } + kv.GoBack(); + } + + if (ad.hasChat || ad.hasCenter || ad.hasAlert || ad.hasHud) + g_Ads[g_iAdCount++] = ad; + } + while (kv.GotoNextKey()); + kv.GoBack(); + } + kv.GoBack(); + } + + delete kv; + LogMessage("[ARCANEGAME AD] Загружено %d объявлений, интервал %.1f сек.", + g_iAdCount, g_fInterval); +} + +// ============================================================= +// Парсинг строки "R G B A" → int[4] +// ============================================================= +void ParseRGBA(const char[] str, int rgba[4]) +{ + char p[4][8]; + int n = ExplodeString(str, " ", p, 4, 8); + rgba[0] = (n > 0) ? StringToInt(p[0]) : 255; + rgba[1] = (n > 1) ? StringToInt(p[1]) : 255; + rgba[2] = (n > 2) ? StringToInt(p[2]) : 255; + rgba[3] = (n > 3) ? StringToInt(p[3]) : 255; +} + +// ============================================================= +// Замена {COLOR} тегов на CS:GO escape-коды +// ============================================================= +void ApplyColors(char[] msg, int maxLen) +{ + ReplaceString(msg, maxLen, "{DEFAULT}", CLR_DEFAULT); + ReplaceString(msg, maxLen, "{GREEN}", CLR_GREEN); + ReplaceString(msg, maxLen, "{OLIVE}", CLR_OLIVE); + ReplaceString(msg, maxLen, "{LIGHTGREEN}", CLR_LIGHTGREEN); + ReplaceString(msg, maxLen, "{RED}", CLR_RED); + ReplaceString(msg, maxLen, "{LIGHTRED}", CLR_LIGHTRED); + ReplaceString(msg, maxLen, "{LIGHTPURPLE}", CLR_LIGHTPURPLE); + ReplaceString(msg, maxLen, "{PURPLE}", CLR_PURPLE); + ReplaceString(msg, maxLen, "{GRAY}", CLR_GRAY); + ReplaceString(msg, maxLen, "{BLUE}", CLR_BLUE); + ReplaceString(msg, maxLen, "{LIGHTBLUE}", CLR_LIGHTBLUE); + ReplaceString(msg, maxLen, "{LIME}", CLR_LIME); + ReplaceString(msg, maxLen, "{LIGHTOLIVE}", CLR_LIGHTOLIVE); +} + +// ============================================================= +// Удаление {COLOR} тегов (для Center / Alert / HUD) +// ============================================================= +void StripColors(char[] msg, int maxLen) +{ + ReplaceString(msg, maxLen, "{DEFAULT}", ""); + ReplaceString(msg, maxLen, "{GREEN}", ""); + ReplaceString(msg, maxLen, "{OLIVE}", ""); + ReplaceString(msg, maxLen, "{LIGHTGREEN}", ""); + ReplaceString(msg, maxLen, "{RED}", ""); + ReplaceString(msg, maxLen, "{LIGHTRED}", ""); + ReplaceString(msg, maxLen, "{LIGHTPURPLE}", ""); + ReplaceString(msg, maxLen, "{PURPLE}", ""); + ReplaceString(msg, maxLen, "{GRAY}", ""); + ReplaceString(msg, maxLen, "{BLUE}", ""); + ReplaceString(msg, maxLen, "{LIGHTBLUE}", ""); + ReplaceString(msg, maxLen, "{LIME}", ""); + ReplaceString(msg, maxLen, "{LIGHTOLIVE}", ""); +} + +// ============================================================= +// Таймер — показываем следующее объявление +// ============================================================= +public Action Timer_ShowAd(Handle timer) +{ + if (g_iAdCount == 0) return Plugin_Continue; + + ShowAd(g_iCurrentAd); + g_iCurrentAd = (g_iCurrentAd + 1) % g_iAdCount; + + return Plugin_Continue; +} + +// ============================================================= +// Отображение одного объявления +// ============================================================= +void ShowAd(int idx) +{ + // ── Chat ────────────────────────────────────────────────── + if (g_Ads[idx].hasChat) + { + char msg[MAX_MSG_LEN]; + strcopy(msg, sizeof(msg), g_Ads[idx].chat); + ProcessTags(msg, sizeof(msg)); + + // Конвертируем literal \n (два символа) в реальный перенос строки + ReplaceString(msg, sizeof(msg), "\\n", "\n"); + + char lines[16][512]; + int cnt = ExplodeString(msg, "\n", lines, sizeof(lines), sizeof(lines[])); + for (int i = 0; i < cnt; i++) + { + TrimString(lines[i]); + if (!lines[i][0]) continue; + + char line[512]; + strcopy(line, sizeof(line), lines[i]); + ApplyColors(line, sizeof(line)); + PrintToChatAll(line); + } + } + + // ── Center ──────────────────────────────────────────────── + if (g_Ads[idx].hasCenter) + { + char msg[MAX_MSG_LEN]; + strcopy(msg, sizeof(msg), g_Ads[idx].center); + ProcessTags(msg, sizeof(msg)); + StripColors(msg, sizeof(msg)); + PrintCenterTextAll(msg); + } + + // ── Alert (hint) ────────────────────────────────────────── + if (g_Ads[idx].hasAlert) + { + char msg[MAX_MSG_LEN]; + strcopy(msg, sizeof(msg), g_Ads[idx].alert); + ProcessTags(msg, sizeof(msg)); + StripColors(msg, sizeof(msg)); + PrintHintTextToAll(msg); + } + + // ── HUD ─────────────────────────────────────────────────── + if (g_Ads[idx].hasHud) + { + char msg[MAX_MSG_LEN]; + strcopy(msg, sizeof(msg), g_Ads[idx].hud.text); + ProcessTags(msg, sizeof(msg)); + StripColors(msg, sizeof(msg)); + + SetHudTextParams( + g_Ads[idx].hud.x, g_Ads[idx].hud.y, + g_Ads[idx].hud.holdTime, + g_Ads[idx].hud.color[0], g_Ads[idx].hud.color[1], + g_Ads[idx].hud.color[2], g_Ads[idx].hud.color[3] + ); + + for (int i = 1; i <= MaxClients; i++) + if (IsClientInGame(i) && !IsFakeClient(i)) + ShowSyncHudText(i, g_hHud, msg); + } +} + +// ============================================================= +// Замена информационных тегов в строке +// ============================================================= +void ProcessTags(char[] msg, int maxLen) +{ + // {IP:PORT} + ConVar cvIP = FindConVar("ip"); + ConVar cvPort = FindConVar("hostport"); + if (cvIP && cvPort) + { + char sIP[64], sPort[8], sIPPort[80]; + cvIP.GetString(sIP, sizeof(sIP)); + cvPort.GetString(sPort, sizeof(sPort)); + FormatEx(sIPPort, sizeof(sIPPort), "%s:%s", sIP, sPort); + ReplaceString(msg, maxLen, "{IP:PORT}", sIPPort); + } + + // {DATE} + char sDate[16]; + FormatTime(sDate, sizeof(sDate), "%d.%m.%Y"); + ReplaceString(msg, maxLen, "{DATE}", sDate); + + // {TIME} + char sTime[16]; + FormatTime(sTime, sizeof(sTime), "%H:%M:%S"); + ReplaceString(msg, maxLen, "{TIME}", sTime); + + // {MAP} + char sRaw[MAX_MAP_NAME], sPretty[MAX_MAP_NAME]; + GetCurrentMap(sRaw, sizeof(sRaw)); + if (!g_MapNames.GetString(sRaw, sPretty, sizeof(sPretty))) + strcopy(sPretty, sizeof(sPretty), sRaw); + ReplaceString(msg, maxLen, "{MAP}", sPretty); + + // {PL} + int pl = 0; + for (int i = 1; i <= MaxClients; i++) + if (IsClientConnected(i) && !IsFakeClient(i)) pl++; + char sPL[8]; + IntToString(pl, sPL, sizeof(sPL)); + ReplaceString(msg, maxLen, "{PL}", sPL); + + // {TIC} + char sTick[8]; + IntToString(RoundFloat(1.0 / GetTickInterval()), sTick, sizeof(sTick)); + ReplaceString(msg, maxLen, "{TIC}", sTick); + + // {SERVERNAME} + ConVar cvHost = FindConVar("hostname"); + if (cvHost) + { + char sHost[128]; + cvHost.GetString(sHost, sizeof(sHost)); + ReplaceString(msg, maxLen, "{SERVERNAME}", sHost); + } + + // {TIMELEFT} + int iLeft; + if (GetMapTimeLeft(iLeft)) + { + char sTL[32]; + if (iLeft > 0) + FormatEx(sTL, sizeof(sTL), "%d:%02d", iLeft / 60, iLeft % 60); + else + strcopy(sTL, sizeof(sTL), "последний раунд"); + ReplaceString(msg, maxLen, "{TIMELEFT}", sTL); + } + + // {NEXTMAP} — требует плагин mapchooser или nextmap + if (GetFeatureStatus(FeatureType_Native, "GetNextMap") == FeatureStatus_Available) + { + char sNext[MAX_MAP_NAME], sNextPretty[MAX_MAP_NAME]; + if (GetNextMap(sNext, sizeof(sNext))) + { + if (!g_MapNames.GetString(sNext, sNextPretty, sizeof(sNextPretty))) + strcopy(sNextPretty, sizeof(sNextPretty), sNext); + ReplaceString(msg, maxLen, "{NEXTMAP}", sNextPretty); + } + } + + // {ADMINSONLINE1} — через запятую + // {ADMINSONLINE2} — каждый с новой строки + char sA1[512], sA2[512]; + bool bFirst = true; + + for (int i = 1; i <= MaxClients; i++) + { + if (!IsClientInGame(i) || IsFakeClient(i)) continue; + if (!(GetUserFlagBits(i) & ADMFLAG_GENERIC)) continue; + + char sName[MAX_NAME_LENGTH]; + GetClientName(i, sName, sizeof(sName)); + + if (!bFirst) + { + StrCat(sA1, sizeof(sA1), ", "); + StrCat(sA2, sizeof(sA2), "\n"); + } + StrCat(sA1, sizeof(sA1), sName); + StrCat(sA2, sizeof(sA2), sName); + bFirst = false; + } + + ReplaceString(msg, maxLen, "{ADMINSONLINE1}", sA1); + ReplaceString(msg, maxLen, "{ADMINSONLINE2}", sA2); +}