From 26b2bc3b48fe0818631aac38c52fbfa0876b5ec9 Mon Sep 17 00:00:00 2001 From: deidara Date: Fri, 1 May 2026 06:57:29 +0300 Subject: [PATCH] Initial commit: arcane-round-end-music plugin with documentation and config --- README.md | 57 ++++ configs/arcane_round_end_music.cfg | 16 + scripting/arcane_round_end_music.sp | 440 ++++++++++++++++++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 README.md create mode 100644 configs/arcane_round_end_music.cfg create mode 100644 scripting/arcane_round_end_music.sp diff --git a/README.md b/README.md new file mode 100644 index 0000000..18a9b83 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Arcane Round End Music + +Плагин для воспроизведения музыки в конце раунда на CS:GO серверах с SourceMod. + +## Функции + +- Музыка играет автоматически в конце каждого раунда +- Каждый игрок может **включить/выключить** музыку персонально +- Регулировка **громкости** от 0% до 100% (шаг 10%) +- Настройки сохраняются через **ClientPrefs** (cookies) +- Поддержка до **128 треков**, воспроизведение по очереди + +## Зависимости + +- [SourceMod](https://www.sourcemod.net/) 1.10+ +- [ClientPrefs](https://wiki.alliedmods.net/Client_Preferences_%28SourceMod%29) (входит в SourceMod) + +## Установка + +1. Скомпилировать `scripting/arcane_round_end_music.sp` +2. Положить `.smx` в `addons/sourcemod/plugins/` +3. Положить `configs/arcane_round_end_music.cfg` в `addons/sourcemod/configs/` +4. Загрузить аудиофайлы в `sound/arcanegame/music/` на сервере +5. Прописать треки в конфиге +6. Перезапустить сервер или загрузить плагин: `sm plugins load arcane_round_end_music` + +## Конфиг + +Путь: `addons/sourcemod/configs/arcane_round_end_music.cfg` + +``` +"RoundEndMusic" +{ + "songs" + { + "1" + { + "title" "Название трека" + "file" "arcanegame/music/track.mp3" + } + } +} +``` + +> Путь к файлу указывается **относительно папки `sound/`**, без неё. + +## Команды + +| Команда | Доступ | Описание | +|---|---|---| +| `!res` / `sm_res` | Все игроки | Открыть меню управления музыкой | +| `sm_res_reload` | Admin (generic) | Перезагрузить список треков из конфига | +| `sm_res_test` | Admin (generic) | Воспроизвести тестовый трек | + +## Версия + +`1.3` — Автор: Codex diff --git a/configs/arcane_round_end_music.cfg b/configs/arcane_round_end_music.cfg new file mode 100644 index 0000000..08ebdac --- /dev/null +++ b/configs/arcane_round_end_music.cfg @@ -0,0 +1,16 @@ +"RoundEndMusic" +{ + "songs" + { + "1" + { + "title" "Название песни 1" + "file" "arcanegame/music/song1.mp3" + } + "2" + { + "title" "Название песни 2" + "file" "arcanegame/music/song2.mp3" + } + } +} diff --git a/scripting/arcane_round_end_music.sp b/scripting/arcane_round_end_music.sp new file mode 100644 index 0000000..0f79293 --- /dev/null +++ b/scripting/arcane_round_end_music.sp @@ -0,0 +1,440 @@ +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#define PLUGIN_VERSION "1.3" +#define MAX_SONGS 128 +#define MAX_TITLE_LENGTH 128 +#define CHAT_PREFIX "\x01ARCANE \x02GAME" + +char g_SongTitles[MAX_SONGS][MAX_TITLE_LENGTH]; +char g_SongFiles[MAX_SONGS][PLATFORM_MAX_PATH]; +int g_SongCount; +int g_NextSongIndex; + +bool g_MusicEnabled[MAXPLAYERS + 1]; +int g_VolumePercent[MAXPLAYERS + 1]; +int g_LastSongIndex[MAXPLAYERS + 1]; + +Cookie g_CookieEnabled; +Cookie g_CookieVolume; + +public Plugin myinfo = +{ + name = "Arcane Round End Music", + author = "Codex", + description = "End round music with personal toggle and volume control.", + version = PLUGIN_VERSION, + url = "" +}; + +public void OnPluginStart() +{ + RegConsoleCmd("sm_res", Command_ResMenu); + RegAdminCmd("sm_res_reload", Command_ResReload, ADMFLAG_GENERIC); + RegAdminCmd("sm_res_test", Command_ResTest, ADMFLAG_GENERIC); + + HookEvent("round_end", Event_RoundEnd, EventHookMode_PostNoCopy); + HookEvent("round_start", Event_RoundStart, EventHookMode_PostNoCopy); + + g_CookieEnabled = RegClientCookie("arcane_music_enabled", "Round end music enabled", CookieAccess_Private); + g_CookieVolume = RegClientCookie("arcane_music_volume", "Round end music volume", CookieAccess_Private); + + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + ResetClientSettings(client); + + if (!IsFakeClient(client) && AreClientCookiesCached(client)) + { + LoadClientSettings(client); + } + } + } + + LoadSongs(); +} + +public void OnMapStart() +{ + LoadSongs(); +} + +public void OnClientPutInServer(int client) +{ + ResetClientSettings(client); + + if (!IsFakeClient(client) && AreClientCookiesCached(client)) + { + LoadClientSettings(client); + } +} + +public void OnClientDisconnect(int client) +{ + g_LastSongIndex[client] = -1; +} + +public void OnClientCookiesCached(int client) +{ + if (!IsValidMusicClient(client)) + { + return; + } + + LoadClientSettings(client); +} + +public Action Command_ResMenu(int client, int args) +{ + if (!IsValidMusicClient(client)) + { + return Plugin_Handled; + } + + if (g_SongCount <= 0) + { + PrintMusicChat(client, "Песни не загружены. Проверь конфиг и путь к wav."); + } + + ShowResMenu(client); + return Plugin_Handled; +} + +public Action Command_ResReload(int client, int args) +{ + LoadSongs(); + ReplyToCommand(client, "[ArcaneGameMusic] Songs loaded: %d", g_SongCount); + return Plugin_Handled; +} + +public Action Command_ResTest(int client, int args) +{ + if (!IsValidMusicClient(client)) + { + ReplyToCommand(client, "[ArcaneGameMusic] Command is available only for real players in game."); + return Plugin_Handled; + } + + if (g_SongCount <= 0) + { + ReplyToCommand(client, "[ArcaneGameMusic] No songs loaded. Check config and file paths."); + return Plugin_Handled; + } + + int songIndex = GetNextSongIndex(); + PlaySongToClient(client, songIndex); + ReplyToCommand(client, "[ArcaneGameMusic] Test song sent."); + return Plugin_Handled; +} + +public void Event_RoundEnd(Event event, const char[] name, bool dontBroadcast) +{ + if (g_SongCount <= 0) + { + LogError("[ArcaneGameMusic] round_end fired, but no songs were loaded."); + return; + } + + int songIndex = GetNextSongIndex(); + + for (int client = 1; client <= MaxClients; client++) + { + if (!IsValidMusicClient(client) || !g_MusicEnabled[client]) + { + continue; + } + + PlaySongToClient(client, songIndex); + } +} + +public void Event_RoundStart(Event event, const char[] name, bool dontBroadcast) +{ + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) + { + StopClientSong(client); + } + } +} + +public int MenuHandler_Res(Menu menu, MenuAction action, int client, int item) +{ + if (action == MenuAction_End) + { + delete menu; + return 0; + } + + if (action != MenuAction_Select || !IsValidMusicClient(client)) + { + return 0; + } + + char info[16]; + menu.GetItem(item, info, sizeof(info)); + + if (StrEqual(info, "toggle")) + { + g_MusicEnabled[client] = !g_MusicEnabled[client]; + + if (!g_MusicEnabled[client]) + { + StopClientSong(client); + PrintMusicChat(client, "Музыка \x02выключена"); + } + else + { + PrintMusicChat(client, "Музыка \x04включена"); + } + } + else if (StrEqual(info, "down")) + { + int oldVolume = g_VolumePercent[client]; + g_VolumePercent[client] = ClampVolume(g_VolumePercent[client] - 10); + + if (oldVolume == g_VolumePercent[client]) + { + PrintMusicChat(client, "Громкость уже на минимуме"); + } + else + { + PrintMusicChat(client, "Громкость: \x09%d%%", g_VolumePercent[client]); + } + } + else if (StrEqual(info, "up")) + { + int oldVolume = g_VolumePercent[client]; + g_VolumePercent[client] = ClampVolume(g_VolumePercent[client] + 10); + + if (oldVolume == g_VolumePercent[client]) + { + PrintMusicChat(client, "Громкость уже на максимуме"); + } + else + { + PrintMusicChat(client, "Громкость: \x09%d%%", g_VolumePercent[client]); + } + } + + SaveClientSettings(client); + ShowResMenu(client); + return 0; +} + +void ShowResMenu(int client) +{ + Menu menu = new Menu(MenuHandler_Res); + + char title[128]; + FormatEx(title, sizeof(title), "ArcaneGameMusic\nВключено: %s\nГромкость: %d%%", + g_MusicEnabled[client] ? "Да" : "Нет", + g_VolumePercent[client]); + + menu.SetTitle(title); + menu.AddItem("toggle", g_MusicEnabled[client] ? "Выключить музыку" : "Включить музыку"); + menu.AddItem("down", "Уменьшить громкость (-10%)"); + menu.AddItem("up", "Увеличить громкость (+10%)"); + menu.ExitButton = true; + menu.Display(client, 20); +} + +void ResetClientSettings(int client) +{ + g_MusicEnabled[client] = true; + g_VolumePercent[client] = 40; + g_LastSongIndex[client] = -1; +} + +void LoadClientSettings(int client) +{ + ResetClientSettings(client); + + char value[16]; + + GetClientCookie(client, g_CookieEnabled, value, sizeof(value)); + if (value[0] != '\0') + { + g_MusicEnabled[client] = StringToInt(value) != 0; + } + + GetClientCookie(client, g_CookieVolume, value, sizeof(value)); + if (value[0] != '\0') + { + int volume = StringToInt(value); + + if (volume >= 0 && volume <= 100) + { + g_VolumePercent[client] = volume; + } + } +} + +void SaveClientSettings(int client) +{ + char value[16]; + + IntToString(g_MusicEnabled[client] ? 1 : 0, value, sizeof(value)); + SetClientCookie(client, g_CookieEnabled, value); + + IntToString(g_VolumePercent[client], value, sizeof(value)); + SetClientCookie(client, g_CookieVolume, value); +} + +void LoadSongs() +{ + g_SongCount = 0; + + char configPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, configPath, sizeof(configPath), "configs/arcane_round_end_music.cfg"); + + KeyValues kv = new KeyValues("RoundEndMusic"); + if (!kv.ImportFromFile(configPath)) + { + LogError("[ArcaneGameMusic] Could not open config: %s", configPath); + delete kv; + return; + } + + if (!kv.JumpToKey("songs")) + { + LogError("[ArcaneGameMusic] Missing section 'songs' in: %s", configPath); + delete kv; + return; + } + + if (!kv.GotoFirstSubKey(false)) + { + LogError("[ArcaneGameMusic] No songs found in: %s", configPath); + delete kv; + return; + } + + do + { + if (g_SongCount >= MAX_SONGS) + { + LogError("[ArcaneGameMusic] Song limit reached (%d)", MAX_SONGS); + break; + } + + kv.GetString("title", g_SongTitles[g_SongCount], MAX_TITLE_LENGTH); + kv.GetString("file", g_SongFiles[g_SongCount], PLATFORM_MAX_PATH); + + TrimString(g_SongTitles[g_SongCount]); + TrimString(g_SongFiles[g_SongCount]); + + if (g_SongTitles[g_SongCount][0] == '\0' || g_SongFiles[g_SongCount][0] == '\0') + { + continue; + } + + char downloadPath[PLATFORM_MAX_PATH]; + FormatEx(downloadPath, sizeof(downloadPath), "sound/%s", g_SongFiles[g_SongCount]); + + if (!FileExists(downloadPath)) + { + LogError("[ArcaneGameMusic] File not found: %s", downloadPath); + continue; + } + + if (!PrecacheSound(g_SongFiles[g_SongCount], false)) + { + LogError("[ArcaneGameMusic] Could not precache sound: %s", g_SongFiles[g_SongCount]); + continue; + } + + AddFileToDownloadsTable(downloadPath); + g_SongCount++; + } + while (kv.GotoNextKey(false)); + + if (g_SongCount <= 0 || g_NextSongIndex >= g_SongCount) + { + g_NextSongIndex = 0; + } + + LogMessage("[ArcaneGameMusic] Loaded songs: %d", g_SongCount); + delete kv; +} + +void PlaySongToClient(int client, int songIndex) +{ + if (songIndex < 0 || songIndex >= g_SongCount) + { + return; + } + + StopClientSong(client); + + float volume = float(g_VolumePercent[client]) / 100.0; + EmitSoundToClient(client, g_SongFiles[songIndex], SOUND_FROM_PLAYER, SNDCHAN_STATIC, SNDLEVEL_NONE, SND_NOFLAGS, volume); + + g_LastSongIndex[client] = songIndex; + PrintMusicChat(client, "Сейчас играет: \x09%s", g_SongTitles[songIndex]); + PrintMusicChat(client, "Вы можете управлять музыкой по команде \x04!res"); +} + +void StopClientSong(int client) +{ + int songIndex = g_LastSongIndex[client]; + + if (songIndex < 0 || songIndex >= g_SongCount) + { + return; + } + + StopSound(client, SNDCHAN_STATIC, g_SongFiles[songIndex]); + g_LastSongIndex[client] = -1; +} + +bool IsValidMusicClient(int client) +{ + return client > 0 && client <= MaxClients && IsClientInGame(client) && !IsFakeClient(client); +} + +int ClampVolume(int value) +{ + if (value < 0) + { + return 0; + } + + if (value > 100) + { + return 100; + } + + return value; +} + +int GetNextSongIndex() +{ + if (g_SongCount <= 0) + { + return -1; + } + + int songIndex = g_NextSongIndex; + g_NextSongIndex++; + + if (g_NextSongIndex >= g_SongCount) + { + g_NextSongIndex = 0; + } + + return songIndex; +} + +void PrintMusicChat(int client, const char[] format, any ...) +{ + char message[256]; + VFormat(message, sizeof(message), format, 3); + PrintToChat(client, "%s\x01 | %s", CHAT_PREFIX, message); +}