/** * [CSRD] Round End Music * * In-house rewrite of Round End Music. Hopefully it'll be much cleaner to work with. */ #pragma semicolon 1 #include #include #pragma newdecls required #include #define PLUGIN_VERSION "0.6.0" public Plugin myinfo = { name = "[CSRD] Round End Music", author = "nosoop", description = "A fresh rewrite of the Round End Music plugin.", version = PLUGIN_VERSION, url = "https://git.csrd.science/" } // Note: All lists share the same handle reference, they aren't cloned. bool g_bQueueLocked = false; ArrayList g_QueuedSongs, g_ActiveSongs, g_PlayedSongs; ConVar g_ConVarMaxActiveSongs, g_ConVarEnabled, g_ConVarSongDelay; Handle g_RequestSongForward; Handle g_OnREMPlayedForward, g_OnREMPostPlayedForward; int g_nMaxActiveSongs = 5; bool g_bRoundEndMusicActive; public void OnPluginStart() { g_QueuedSongs = new ArrayList(); g_ActiveSongs = new ArrayList(); g_PlayedSongs = new ArrayList(); g_RequestSongForward = CreateGlobalForward("OnRoundEndSongsRequested", ET_Ignore); g_OnREMPlayedForward = CreateGlobalForward("OnRoundEndMusicWillPlay", ET_Event, Param_Cell); g_OnREMPostPlayedForward = CreateGlobalForward("OnRoundEndMusicPlayed", ET_Ignore, Param_Cell); HookEvent("teamplay_round_win", OnRoundEnd, EventHookMode_PostNoCopy); RegAdminCmd("sm_playsong", AdminCmd_PlaySong, ADMFLAG_ROOT); RegAdminCmd("sm_peekqueue", AdminCmd_PeekQueue, ADMFLAG_ROOT); g_ConVarMaxActiveSongs = CreateConVar("sm_rem_active_songs", "5", "Maximum number of songs available for playback on a single map.", _, true, 1.0, false); g_ConVarEnabled = CreateConVar("sm_rem_enabled", "1", "Enables Round End Music.", _, true, 0.0, true, 1.0); g_ConVarSongDelay = CreateConVar("sm_rem_song_delay", "4.3", "Amount of time after the round end to wait before playing the song.", _, true, 0.0); AutoExecConfig(); } public Action AdminCmd_PlaySong(int client, int argc) { PlayRoundEndMusic(); return Plugin_Handled; } public Action AdminCmd_PeekQueue(int client, int argc) { if (GetCmdReplySource() == SM_REPLY_TO_CHAT) { ReplyToCommand(client, "See console for output."); } PrintSongList(client, "played songs", g_PlayedSongs); PrintSongList(client, "active songs", g_ActiveSongs); PrintSongList(client, "queued songs", g_QueuedSongs); return Plugin_Handled; } void PrintSongList(int client, const char[] listName, ArrayList songList) { char filePath[PLATFORM_MAX_PATH]; if (songList.Length > 0) { PrintToConsole(client, "---- %s ----", listName); for (int i = 0; i < songList.Length; i++) { MusicEntry song = songList.Get(i); song.GetFilePath(filePath, sizeof(filePath)); PrintToConsole(client, "%d. %s", i + 1, filePath); } } } public void OnConfigsExecuted() { g_nMaxActiveSongs = g_ConVarMaxActiveSongs.IntValue; g_bRoundEndMusicActive = g_ConVarEnabled.BoolValue; g_bQueueLocked = false; Call_StartForward(g_RequestSongForward); Call_Finish(); g_bQueueLocked = true; /** * It's okay if there are songs in the active list, as it won't be processed if the plugin * is set as disabled. However... * * TODO make the active song count implicitly zero when disabled? */ if (g_nMaxActiveSongs < g_ActiveSongs.Length) { // Put excess songs back in the head of the queue // Used if fewer active songs are required for long maps // (or disabled completely e.g. Arena) while (g_ActiveSongs.Length > g_nMaxActiveSongs) { int pos = g_ActiveSongs.Length - 1; MusicEntry song = g_ActiveSongs.Get(pos); // Insert songs at the top of the queued songs list. if (g_QueuedSongs.Length == 0) { // Fix attempting to shift contents of an empty queue up. g_QueuedSongs.Resize(1); } else { g_QueuedSongs.ShiftUp(0); } g_QueuedSongs.Set(0, song); g_ActiveSongs.Erase(pos); } } else { // Take up to g_nMaxActiveSongs songs from queue and move them to g_ActiveSongs while (g_ActiveSongs.Length < g_nMaxActiveSongs && g_QueuedSongs.Length > 0) { MusicEntry song = g_QueuedSongs.Get(0); g_ActiveSongs.Push(song); g_QueuedSongs.Erase(0); } } /** * Check to see if we should play music on this map. If not, then don't process the active * music list. */ if (g_bRoundEndMusicActive) { // Do the Fisher-Yates. // http://spin.atomicobject.com/2014/08/11/fisher-yates-shuffle-randomization-algorithm/ int nActiveSongs = g_ActiveSongs.Length < g_nMaxActiveSongs? g_ActiveSongs.Length : g_nMaxActiveSongs; for (int i = 0; i < nActiveSongs; i+= 1) { int s = GetRandomInt(i, nActiveSongs - 1); SwapArrayItems(g_ActiveSongs, i, s); MusicEntry song = g_ActiveSongs.Get(i); char title[64], source[64], filePath[PLATFORM_MAX_PATH]; song.GetTitle(title, sizeof(title)); song.GetSource(source, sizeof(source)); song.GetFilePath(filePath, sizeof(filePath)); char fileDownloadPath[PLATFORM_MAX_PATH]; Format(fileDownloadPath, sizeof(fileDownloadPath), "sound/%s", filePath); AddFileToDownloadsTable(fileDownloadPath); PrecacheSound(filePath); PrintToServer("[rem] Added song %d: %s", i + 1, filePath); } PrintToServer("[rem] Round End Music plugin enabled."); } else { PrintToServer("[rem] Round End Music plugin disabled."); } } /** * Remove any already played songs from the queue * (so on map start they only contain unplayed tracks). */ public void OnMapEnd() { for (int i = 0; i < g_PlayedSongs.Length; i++) { MusicEntry playedSong = g_PlayedSongs.Get(i); int activePos = -1; if ( (activePos = g_ActiveSongs.FindValue(playedSong)) != -1 ) { g_ActiveSongs.Erase(activePos); } delete playedSong; } g_PlayedSongs.Clear(); } /** * Play pending endround music. */ void PlayRoundEndMusic() { if (g_ActiveSongs.Length == 0 || !g_bRoundEndMusicActive) { PrintToServer("no songs to play :("); return; } // retrieve head of g_ActiveSongs, erase and put into g_PlayedSongs MusicEntry song = g_ActiveSongs.Get(0); // mock play for testing // TODO move into a function with shiny forwards EmitRoundEndMusic(song); // if 'active' is empty, copy all back into 'active' without removing from 'played' if (g_ActiveSongs.Length == 0) { for (int i = 0; i < g_PlayedSongs.Length; i++) { g_ActiveSongs.Push(g_PlayedSongs.Get(i)); } int nActiveSongs = g_ActiveSongs.Length < g_nMaxActiveSongs? g_ActiveSongs.Length : g_nMaxActiveSongs; for (int i = 0; i < nActiveSongs; i+= 1) { int s = GetRandomInt(i, nActiveSongs - 1); SwapArrayItems(g_ActiveSongs, i, s); } } } void EmitRoundEndMusic(MusicEntry song) { Action result = FireOnRoundEndMusicPlayedEvent(song); if (result == Plugin_Continue) { char filePath[PLATFORM_MAX_PATH]; song.GetFilePath(filePath, sizeof(filePath)); PrintToServer("mock play song %s", filePath); EmitSoundToAll(filePath); } if (result != Plugin_Stop) { /** * Plugins listening to OnRoundEndMusicPlayed might check if the song was already * played, so we'll have to fire off the event before it gets put into the "played" * list. * * I can't remember if there was any reason why it was last in the first place... */ FireOnREMPlayedPostEvent(song); if (g_PlayedSongs.FindValue(song) == -1) { g_PlayedSongs.Push(song); } int pos; if ( (pos = g_ActiveSongs.FindValue(song)) != -1 ) { g_ActiveSongs.Erase(pos); } } } Action FireOnRoundEndMusicPlayedEvent(MusicEntry song) { Action result; Call_StartForward(g_OnREMPlayedForward); Call_PushCell(song); Call_Finish(result); return result; } void FireOnREMPlayedPostEvent(MusicEntry song) { Call_StartForward(g_OnREMPostPlayedForward); Call_PushCell(song); Call_Finish(); } public void OnRoundEnd(Event event, const char[] name, bool dontBroadcast) { float flSongDelay = g_ConVarSongDelay.FloatValue, flBonusRoundTime = GetBonusRoundTime(); // Enforce checking of song delay. flSongDelay = flSongDelay > flBonusRoundTime? flBonusRoundTime : flSongDelay; CreateTimer(flSongDelay, RoundEndMusicPlaybackDelay, _, TIMER_FLAG_NO_MAPCHANGE); } public Action RoundEndMusicPlaybackDelay(Handle timer, any data) { PlayRoundEndMusic(); } // menu: read entries from g_PlayedSongs and then g_ActiveSongs if not empty // that *should* maintain initial play order // maybe we should just provide a function that provides the entire list and active counts? /* Utility functions */ float GetBonusRoundTime() { return FindConVar("mp_bonusroundtime").FloatValue; } /* Native function calls */ public APLRes AskPluginLoad2(Handle self, bool late, char[] error, int err_max) { RegPluginLibrary("round-end-music"); CreateNative("REM_AddSong", Native_AddSong); CreateNative("REM_GetActiveSongCount", Native_GetActiveSongCount); CreateNative("REM_SongWasRecentlyPlayed", Native_SongWasRecentlyPlayed); } public int Native_AddSong(Handle hPlugin, int nArgs) { if (g_bQueueLocked) { ThrowNativeError(1, "Queue is currently locked -- are you calling REM_AddSong outside " ... "of the registered callback?"); return false; } MusicEntry song = view_as(GetNativeCell(1)); // Ensure no more songs added than necessary if (g_QueuedSongs.Length < g_nMaxActiveSongs) { // Ensure no duplicates by file path in queue or active songs bool existing = false; char filePath[PLATFORM_MAX_PATH], existingFilePath[PLATFORM_MAX_PATH]; song.GetFilePath(filePath, sizeof(filePath)); for (int i = 0; i < g_QueuedSongs.Length; i++) { (view_as(g_QueuedSongs.Get(i))).GetFilePath( existingFilePath, sizeof(existingFilePath)); existing |= StrEqual(filePath, existingFilePath); } for (int i = 0; i < g_ActiveSongs.Length; i++) { (view_as(g_ActiveSongs.Get(i))) .GetFilePath(existingFilePath, sizeof(existingFilePath)); existing |= StrEqual(filePath, existingFilePath); } if (!existing) { g_QueuedSongs.Push(CloneHandle(song)); return true; } } return false; } public int Native_GetActiveSongCount(Handle hPlugin, int nArgs) { return g_nMaxActiveSongs; } public int Native_SongWasRecentlyPlayed(Handle hPlugin, int nArgs) { MusicEntry song = GetNativeCell(1); for (int i = 0; i < g_PlayedSongs.Length; i++) { MusicEntry playedSong = g_PlayedSongs.Get(i); if (song.Equals(playedSong)) { return true; } } return false; }