Browse Source

Initial commit of REM(2)

Fresh rewrite of REM, using new Best Practices(tm).  (Read:  Not the
awkward pile of a mess I had last time when I had just a few months of
SourceMod experience.)

Implemented song requesting private forward and song shuffling into
active list, precaching and adding to download table.  Implemented
removal of already played songs during map end.  Implemented music
playback, and natives to add a music request listener and to add a song.
nosoop 8 years ago
parent
commit
5a7e550225
2 changed files with 297 additions and 0 deletions
  1. 48 0
      scripting/include/round_end_music.inc
  2. 249 0
      scripting/round_end_music.sp

+ 48 - 0
scripting/include/round_end_music.inc

@@ -0,0 +1,48 @@
+#if defined __round_end_music_included
+	#endinput
+#endif
+
+#define __round_end_music_included
+
+public SharedPlugin __pl_round_end_music = {
+    name = "round-end-music",
+    file = "round_end_music.smx",
+#if defined REQUIRE_PLUGIN
+    required = 1,
+#else
+    required = 0,
+#endif
+};
+
+methodmap MusicEntry < KeyValues {
+	public MusicEntry() {
+		return view_as<MusicEntry>(new KeyValues("music_entry"));
+	}
+	
+	public void SetFilePath(const char[] filePath) {
+		this.SetString("filepath", filePath);
+	}
+	public void GetFilePath(char[] buffer, int maxlen) {
+		this.GetString("filepath", buffer, maxlen);
+	}
+	
+	public void SetTitle(const char[] title) {
+		this.SetString("title", title);
+	}
+	public void GetTitle(char[] buffer, int maxlen) {
+		this.GetString("title", buffer, maxlen);
+	}
+	
+	public void SetSource(const char[] source) {
+		this.SetString("source", source);
+	}
+	public void GetSource(char[] buffer, int maxlen) {
+		this.GetString("source", buffer, maxlen);
+	}
+}
+
+typedef OnSongsRequested = function void ();
+
+native void REM_RegisterSource(OnSongsRequested callback);
+
+native bool REM_AddSong(MusicEntry song);

+ 249 - 0
scripting/round_end_music.sp

@@ -0,0 +1,249 @@
+/**
+ * [CSRD] Round End Music
+ * 
+ * In-house rewrite of Round End Music.  Hopefully it'll be much cleaner to work with.
+ */
+
+#pragma semicolon 1
+#include <sourcemod>
+
+#include <sdktools>
+
+#pragma newdecls required
+#include <round_end_music>
+
+#define PLUGIN_VERSION "0.0.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://pika.nom-nom-nom.us/"
+}
+
+bool g_bQueueLocked = false;
+ArrayList g_QueuedSongs, g_ActiveSongs, g_PlayedSongs;
+
+Handle g_RequestSongForward;
+
+int g_nMaxActiveSongs = 5;
+
+public void OnPluginStart() {
+	g_QueuedSongs = new ArrayList();
+	g_ActiveSongs = new ArrayList();
+	g_PlayedSongs = new ArrayList();
+	
+	g_RequestSongForward = CreateForward(ET_Ignore);
+	
+	HookEvent("teamplay_round_win", OnRoundEnd, EventHookMode_PostNoCopy);
+	
+	RegAdminCmd("sm_playsong", AdminCmd_PlaySong, ADMFLAG_ROOT);
+	RegAdminCmd("sm_peekqueue", AdminCmd_PeekQueue, ADMFLAG_ROOT);
+}
+
+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 OnAutoConfigsBuffered() {
+	// TODO pull g_nMaxActiveSongs from ConVar -- it can't change during map
+	
+	g_bQueueLocked = false;
+	Call_StartForward(g_RequestSongForward);
+	Call_Finish();
+	g_bQueueLocked = true;
+	
+	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);
+			
+			g_QueuedSongs.ShiftUp(0);
+			g_QueuedSongs.Set(0, song);
+			
+			g_ActiveSongs.Erase(0);
+		}
+	} 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);
+		}
+	}
+	
+	// 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);
+	}
+}
+
+/**
+ * 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) {
+		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
+	char filePath[PLATFORM_MAX_PATH];
+	song.GetFilePath(filePath, sizeof(filePath));
+	
+	PrintToServer("mock play song %s", filePath);
+	EmitSoundToAll(filePath);
+	
+	if (g_PlayedSongs.FindValue(song) == -1) {
+		g_PlayedSongs.Push(song);
+	}
+	
+	g_ActiveSongs.Erase(0);
+	
+	// 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);
+		}
+	}
+}
+
+public void OnRoundEnd(Event event, const char[] name, bool dontBroadcast) {
+	PlayRoundEndMusic();
+}
+
+// menu: read entries from g_PlayedSongs and then g_ActiveSongs if not empty
+// that *should* maintain initial play order
+
+/* Native function calls */
+public APLRes AskPluginLoad2(Handle self, bool late, char[] error, int err_max) {
+	RegPluginLibrary("round-end-music");
+	
+	CreateNative("REM_RegisterSource", Native_RegisterSource);
+	CreateNative("REM_AddSong", Native_AddSong);
+}
+
+public int Native_RegisterSource(Handle hPlugin, int nArgs) {
+	Function callback = GetNativeFunction(1);
+	
+	// preemptive removal because it could be forwarded multiple times?
+	for (int i = 0; i < GetForwardFunctionCount(g_RequestSongForward); i++) {
+		RemoveFromForward(g_RequestSongForward, hPlugin, callback);
+	}
+	AddToForward(g_RequestSongForward, hPlugin, callback);
+	return 1;
+}
+
+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<MusicEntry>(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<MusicEntry>(g_QueuedSongs.Get(i))).GetFilePath(
+					existingFilePath, sizeof(existingFilePath));
+			
+			existing |= StrEqual(filePath, existingFilePath);
+		}
+		
+		for (int i = 0; i < g_ActiveSongs.Length; i++) {
+			(view_as<MusicEntry>(g_ActiveSongs.Get(i)))
+					.GetFilePath(existingFilePath, sizeof(existingFilePath));
+			
+			existing |= StrEqual(filePath, existingFilePath);
+		}
+		
+		if (!existing) {
+			g_QueuedSongs.Push(CloneHandle(song));
+			return true;
+		}
+	}
+	return false;
+}