Initial commit: arcane-round-end-music plugin with documentation and config

This commit is contained in:
deidara
2026-05-01 06:57:29 +03:00
commit 26b2bc3b48
3 changed files with 513 additions and 0 deletions
+57
View File
@@ -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
+16
View File
@@ -0,0 +1,16 @@
"RoundEndMusic"
{
"songs"
{
"1"
{
"title" "Название песни 1"
"file" "arcanegame/music/song1.mp3"
}
"2"
{
"title" "Название песни 2"
"file" "arcanegame/music/song2.mp3"
}
}
}
+440
View File
@@ -0,0 +1,440 @@
#pragma semicolon 1
#pragma newdecls required
#include <sourcemod>
#include <sdktools>
#include <clientprefs>
#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);
}