diff --git a/exec/load/attr_conv.js b/exec/load/attr_conv.js
index e3630db5fb74f45894bb6756ea0253d245c15e85..2c47200e36ab7f7885fdd71cd35af66a782375a4 100644
--- a/exec/load/attr_conv.js
+++ b/exec/load/attr_conv.js
@@ -1066,16 +1066,22 @@ function ANSIMultiConvertToSyncCodes(pText)
 //
 // Parameters:
 //  pText: The text to be converted
+//  pConvertANSI: Optional boolean - Whether or not to convert ANSI.  Defaults to true.
 //
 // Return value: The text with various other system attribute codes converted
 //               to Synchronet attribute codes, or not, depending on the toggle
 //               options in Extra Attribute Codes in SCFG
-function convertAttrsToSyncPerSysCfg(pText)
+function convertAttrsToSyncPerSysCfg(pText, pConvertANSI)
 {
-	// Convert any ANSI codes to Synchronet attribute codes.
-	// Then convert other BBS attribute codes to Synchronet attribute
-	// codes according to the current system configuration.
-	var convertedText = ANSIAttrsToSyncAttrs(pText);
+	var convertedText = pText;
+	var convertANSI = (typeof(pConvertANSI) === "boolean" ? pConvertANSI : true);
+	if (convertANSI)
+	{
+		// Convert any ANSI codes to Synchronet attribute codes.
+		// Then convert other BBS attribute codes to Synchronet attribute
+		// codes according to the current system configuration.
+		convertedText = ANSIAttrsToSyncAttrs(convertedText);
+	}
 	if ((system.settings & SYS_RENEGADE) == SYS_RENEGADE)
 		convertedText = renegadeAttrsToSyncAttrs(convertedText);
 	if ((system.settings & SYS_WWIV) == SYS_WWIV)
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index fd4f3d1460856d592dc4a5a1209860a0123a2169..6ee9c39aba0fa3b0b9cd8319c9fabb507b0812d6 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -128,6 +128,11 @@
  *                              reader (either from read mode or list mode), it now properly
  *                              says there are no messages and exits, rather than showing
  *                              a list of bogus messages.
+ * 2022-03-14 Eric Oulashin     Version 1.47
+ *                              Updated to make DDMsgReader can be called directly as a
+ *                              loadable module by Synchronet (work started on March 8).
+ *                              Also, refactored to use attr_conv.js and removed the
+ *                              attribute conversion functions from this script.
  */
 
 
@@ -193,12 +198,6 @@
 					 added in the future.
 */
 
-// TODOs:
-// - Search in text: Support current sub-board, group, or all searching
-// - Make use of these lines from text.dat?
-// "\1n\1c(\1h%u \1n\1csub-boards)\1h\1w complete.\r\n"           117 MessageScanComplete
-// "\1r\1iaborted.\1n\r\n"                                    118 MessageScanAborted
-
 // - For pageUp & pageDown, enable alternate keys:
 //  - When reading a message - scrollTextLines()
 //  - When listing messages
@@ -218,6 +217,7 @@ if (requireFnExists)
 	require("dd_lightbar_menu.js", "DDLightbarMenu");
 	require("mouse_getkey.js", "mouse_getkey");
 	require("html2asc.js", 'html2asc');
+	require("attr_conv.js", "convertAttrsToSyncPerSysCfg");
 }
 else
 {
@@ -228,6 +228,7 @@ else
 	load("dd_lightbar_menu.js");
 	load("mouse_getkey.js");
 	load("html2asc.js");
+	load("attr_conv.js");
 }
 load('822header.js');
 
@@ -248,8 +249,8 @@ if (system.version_num < 31500)
 }
 
 // Reader version information
-var READER_VERSION = "1.46";
-var READER_DATE = "2022-03-07";
+var READER_VERSION = "1.47";
+var READER_DATE = "2022-03-14";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -534,6 +535,8 @@ if (file_exists(backslash(system.exec_dir) + "load/smbdefs.js") && file_exists(b
 // Parse the command-line arguments
 
 var gCmdLineArgVals = parseArgs(argv);
+if (gCmdLineArgVals.exitNow)
+	exit(0);
 var gAllPersonalEmailOptSpecified = (gCmdLineArgVals.hasOwnProperty("allpersonalemail") && gCmdLineArgVals.allpersonalemail);
 // Check to see if the command-line argument for reading personal email is enabled
 var gListPersonalEmailCmdLineOpt = ((gCmdLineArgVals.hasOwnProperty("personalemail") && gCmdLineArgVals.personalemail) ||
@@ -648,10 +651,19 @@ if (gDoDDMR)
 	{
 		case SEARCH_NONE:
 			restoreOriginalSubCode = false;
+			if (msgReader.subBoardCode != "mail")
+			{
+				console.print("n");
+				console.crlf();
+				console.print("Loading " + subBoardGrpAndName(msgReader.subBoardCode) + "....");
+				console.line_counter = 0; // To prevent a pause before the message list comes up
+			}
 			msgReader.ReadOrListSubBoard();
 			break;
 		case SEARCH_KEYWORD:
-			msgReader.SearchMsgScan("keyword_search");
+			var txtToSearch = (gCmdLineArgVals.hasOwnProperty("searchtext") ? gCmdLineArgVals.searchtext : null);
+			var subBoardCode = (gCmdLineArgVals.hasOwnProperty("subboard") ? gCmdLineArgVals.subboard : null);
+			msgReader.SearchMsgScan("keyword_search", txtToSearch, subBoardCode);
 			break;
 		case SEARCH_FROM_NAME:
 			msgReader.SearchMessages("from_name_search");
@@ -1763,8 +1775,10 @@ function DigDistMsgReader_RefreshHdrInSavedArrays(pMsgIndex, pAttrib, pSubBoardC
 //  pScanScopeChar: Optional string with a character specifying "A" to scan all sub-boards,
 //                  "G" for the current message group, or "S" for the user's current sub-board.
 //                  If this is not specified, the current sub-board will be used.
-function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanScopeChar)
+//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
+function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanScopeChar, pTxtToSearch)
 {
+	var searchTextProvided = (typeof(pTxtToSearch) === "string" && pTxtToSearch != "");
 	// Convert the search mode string to an integer representing the search
 	// mode.  If we get back -1, that means the search mode string was invalid.
 	// If that's the case, simply list messages.  Otherwise, do the search.
@@ -1797,13 +1811,22 @@ function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanSco
 		switch (this.searchType)
 		{
 			case SEARCH_KEYWORD:
-				console.print("\1n" + replaceAtCodesInStr(this.text.searchTextPromptText));
+				if (!searchTextProvided)
+					console.print("\1n" + replaceAtCodesInStr(this.text.searchTextPromptText));
+				else
+					console.print("\1n\1gSearching for: \1c" + pTxtToSearch + "\1n\r\n");
 				break;
 			case SEARCH_FROM_NAME:
-				console.print("\1n" + replaceAtCodesInStr(this.text.fromNamePromptText));
+				if (!searchTextProvided)
+					console.print("\1n" + replaceAtCodesInStr(this.text.fromNamePromptText));
+				else
+					console.print("\1n\1gSearching for: \1c" + pTxtToSearch + "\1n\r\n");
 				break;
 			case SEARCH_TO_NAME_CUR_MSG_AREA:
-				console.print("\1n" +replaceAtCodesInStr(this.text.toNamePromptText));
+				if (!searchTextProvided)
+					console.print("\1n" +replaceAtCodesInStr(this.text.toNamePromptText));
+				else
+					console.print("\1n\1gSearching for: \1c" + pTxtToSearch + "\1n\r\n");
 				break;
 			case SEARCH_TO_USER_CUR_MSG_AREA:
 				// Note: No prompt needed for this - Will search for the user's name/handle
@@ -1816,7 +1839,12 @@ function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanSco
 		var promptUserForText = this.SearchTypeRequiresSearchText();
 		// Get the search text from the user
 		if (promptUserForText)
-			this.searchString = console.getstr(512, K_UPPER);
+		{
+			if (searchTextProvided)
+				this.searchString = pTxtToSearch;
+			else
+				this.searchString = console.getstr(512, K_UPPER);
+		}
 		// If the user was prompted for search text but no search text was entered,
 		// then show an abort message and don't do anything.  Otherwise, go ahead
 		// and list/read messages.
@@ -1893,17 +1921,31 @@ function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanSco
 //                  "from_name_search": Search messages by from name
 //                  "to_name_search": Search messages by to name
 //                  "to_user_search": Search messages by to name, to the logged-in user
-function DigDistMsgReader_SearchMsgScan(pSearchModeStr)
+//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
+//  pSubCode: Optional - An internal code of a sub-board if scanning just one sub-board
+function DigDistMsgReader_SearchMsgScan(pSearchModeStr, pTxtToSearch, pSubCode)
 {
 	if (typeof(pSearchModeStr) !== "string" || pSearchModeStr.length == 0)
 		return;
 
-	// Prompt the user for sub-board, group, or all, then call SearchMessages
-	// to do the search
-	console.mnemonics(bbs.text(SubGroupOrAll));
-	var scanScopeChar = console.getkeys("SGAC").toString();
+	// If the given sub-board code is valid, then use that and scan only in that
+	// sub-board.  Otherwise, prompt the user for sub-board, group, or all, then
+	// call SearchMessages to do the search.
+	var scanScopeChar = "";
+	var previousSubBoardCode = null;
+	if (typeof(pSubCode) === "string" && subBoardCodeIsValid(pSubCode))
+	{
+		var previousSubBoardCode = this.subBoardCode;
+		this.subBoardCode = pSubCode;
+		scanScopeChar = "S";
+	}
+	else
+	{
+		console.mnemonics(bbs.text(SubGroupOrAll));
+		scanScopeChar = console.getkeys("SGAC").toString();
+	}
 	if (scanScopeChar.length > 0)
-		this.SearchMessages(pSearchModeStr, null, scanScopeChar);
+		this.SearchMessages(pSearchModeStr, null, scanScopeChar, pTxtToSearch);
 	else
 	{
 		console.crlf();
@@ -1911,6 +1953,9 @@ function DigDistMsgReader_SearchMsgScan(pSearchModeStr)
 		console.crlf();
 		console.pause();
 	}
+	// Restore this.subBoardCode if necessary
+	if (typeof(previousSubBoardCode) === "string")
+		this.subBoardCode = previousSubBoardCode;
 }
 
 // This function clears the search data from the object.
@@ -12169,16 +12214,7 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 	msgTextAltered = msgTextAltered.replace(/\t/g, this.tabReplacementText);
 	// Convert other BBS color codes to Synchronet attribute codes if the settings
 	// to do so are enabled.
-	if ((system.settings & SYS_RENEGADE) == SYS_RENEGADE)
-		msgTextAltered = renegadeAttrsToSyncAttrs(msgTextAltered);
-	if ((system.settings & SYS_WWIV) == SYS_WWIV)
-		msgTextAltered = WWIVAttrsToSyncAttrs(msgTextAltered);
-	if ((system.settings & SYS_CELERITY) == SYS_CELERITY)
-		msgTextAltered = celerityAttrsToSyncAttrs(msgTextAltered);
-	if ((system.settings & SYS_PCBOARD) == SYS_PCBOARD)
-		msgTextAltered = PCBoardAttrsToSyncAttrs(msgTextAltered);
-	if ((system.settings & SYS_WILDCAT) == SYS_WILDCAT)
-		msgTextAltered = wildcatAttrsToSyncAttrs(msgTextAltered);
+	msgTextAltered = convertAttrsToSyncPerSysCfg(msgTextAltered, false);
 	// If the message contains ANSI codes, then if frame.js is available and
 	// the user's terminal support ANSI, set up a Frame object for reading the
 	// message with a scrollable interface.
@@ -16361,1217 +16397,6 @@ function msgIsFromUser(pMsgHdr)
 	return isFromUser;
 }
 
-/////////////////////////////////////////////////////////////////////////
-// Functions for converting other BBS color codes to Synchronet attribute codes
-
-// Converts WWIV attribute codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function WWIVAttrsToSyncAttrs(pText)
-{
-	// First, see if the text has any WWIV-style attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (/\x03[0-9]/.test(pText))
-	{
-		var text = pText.replace(/\x030/g, "\1n");			// Normal
-		text = text.replace(/\x031/g, "\1n\1c\1h");			// Bright cyan
-		text = text.replace(/\x032/g, "\1n\1y\1h");			// Bright yellow
-		text = text.replace(/\x033/g, "\1n\1m");			// Magenta
-		text = text.replace(/\x034/g, "\1n\1h\1w\1" + "4");	// Bright white on blue
-		text = text.replace(/\x035/g, "\1n\1g");			// Green
-		text = text.replace(/\x036/g, "\1h\1r\1i");			// Bright red, blinking
-		text = text.replace(/\x037/g, "\1n\1h\1b");			// Bright blue
-		text = text.replace(/\x038/g, "\1n\1b");			// Blue
-		text = text.replace(/\x039/g, "\1n\1c");			// Cyan
-		return text;
-	}
-	else
-		return pText; // No WWIV-style color attribute found, so just return the text.
-}
-
-// Converts PCBoard attribute codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function PCBoardAttrsToSyncAttrs(pText)
-{
-	// First, see if the text has any PCBoard-style attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (/@[xX][0-9A-Fa-f]{2}/.test(pText))
-	{
-		// Black background
-		var text = pText.replace(/@[xX]00/g, "\1n\1k\1" + "0"); // Black on black
-		text = text.replace(/@[xX]01/g, "\1n\1b\1" + "0"); // Blue on black
-		text = text.replace(/@[xX]02/g, "\1n\1g\1" + "0"); // Green on black
-		text = text.replace(/@[xX]03/g, "\1n\1c\1" + "0"); // Cyan on black
-		text = text.replace(/@[xX]04/g, "\1n\1r\1" + "0"); // Red on black
-		text = text.replace(/@[xX]05/g, "\1n\1m\1" + "0"); // Magenta on black
-		text = text.replace(/@[xX]06/g, "\1n\1y\1" + "0"); // Yellow/brown on black
-		text = text.replace(/@[xX]07/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@[xX]08/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@[xX]09/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@[xX]08/g, "\1h\1k\1" + "0"); // Bright black on black
-		text = text.replace(/@[xX]09/g, "\1h\1b\1" + "0"); // Bright blue on black
-		text = text.replace(/@[xX]0[Aa]/g, "\1h\1g\1" + "0"); // Bright green on black
-		text = text.replace(/@[xX]0[Bb]/g, "\1h\1c\1" + "0"); // Bright cyan on black
-		text = text.replace(/@[xX]0[Cc]/g, "\1h\1r\1" + "0"); // Bright red on black
-		text = text.replace(/@[xX]0[Dd]/g, "\1h\1m\1" + "0"); // Bright magenta on black
-		text = text.replace(/@[xX]0[Ee]/g, "\1h\1y\1" + "0"); // Bright yellow on black
-		text = text.replace(/@[xX]0[Ff]/g, "\1h\1w\1" + "0"); // Bright white on black
-		// Blinking foreground
-
-		// Blue background
-		text = text.replace(/@[xX]10/g, "\1n\1k\1" + "4"); // Black on blue
-		text = text.replace(/@[xX]11/g, "\1n\1b\1" + "4"); // Blue on blue
-		text = text.replace(/@[xX]12/g, "\1n\1g\1" + "4"); // Green on blue
-		text = text.replace(/@[xX]13/g, "\1n\1c\1" + "4"); // Cyan on blue
-		text = text.replace(/@[xX]14/g, "\1n\1r\1" + "4"); // Red on blue
-		text = text.replace(/@[xX]15/g, "\1n\1m\1" + "4"); // Magenta on blue
-		text = text.replace(/@[xX]16/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
-		text = text.replace(/@[xX]17/g, "\1n\1w\1" + "4"); // White on blue
-		text = text.replace(/@[xX]18/g, "\1h\1k\1" + "4"); // Bright black on blue
-		text = text.replace(/@[xX]19/g, "\1h\1b\1" + "4"); // Bright blue on blue
-		text = text.replace(/@[xX]1[Aa]/g, "\1h\1g\1" + "4"); // Bright green on blue
-		text = text.replace(/@[xX]1[Bb]/g, "\1h\1c\1" + "4"); // Bright cyan on blue
-		text = text.replace(/@[xX]1[Cc]/g, "\1h\1r\1" + "4"); // Bright red on blue
-		text = text.replace(/@[xX]1[Dd]/g, "\1h\1m\1" + "4"); // Bright magenta on blue
-		text = text.replace(/@[xX]1[Ee]/g, "\1h\1y\1" + "4"); // Bright yellow on blue
-		text = text.replace(/@[xX]1[Ff]/g, "\1h\1w\1" + "4"); // Bright white on blue
-
-		// Green background
-		text = text.replace(/@[xX]20/g, "\1n\1k\1" + "2"); // Black on green
-		text = text.replace(/@[xX]21/g, "\1n\1b\1" + "2"); // Blue on green
-		text = text.replace(/@[xX]22/g, "\1n\1g\1" + "2"); // Green on green
-		text = text.replace(/@[xX]23/g, "\1n\1c\1" + "2"); // Cyan on green
-		text = text.replace(/@[xX]24/g, "\1n\1r\1" + "2"); // Red on green
-		text = text.replace(/@[xX]25/g, "\1n\1m\1" + "2"); // Magenta on green
-		text = text.replace(/@[xX]26/g, "\1n\1y\1" + "2"); // Yellow/brown on green
-		text = text.replace(/@[xX]27/g, "\1n\1w\1" + "2"); // White on green
-		text = text.replace(/@[xX]28/g, "\1h\1k\1" + "2"); // Bright black on green
-		text = text.replace(/@[xX]29/g, "\1h\1b\1" + "2"); // Bright blue on green
-		text = text.replace(/@[xX]2[Aa]/g, "\1h\1g\1" + "2"); // Bright green on green
-		text = text.replace(/@[xX]2[Bb]/g, "\1h\1c\1" + "2"); // Bright cyan on green
-		text = text.replace(/@[xX]2[Cc]/g, "\1h\1r\1" + "2"); // Bright red on green
-		text = text.replace(/@[xX]2[Dd]/g, "\1h\1m\1" + "2"); // Bright magenta on green
-		text = text.replace(/@[xX]2[Ee]/g, "\1h\1y\1" + "2"); // Bright yellow on green
-		text = text.replace(/@[xX]2[Ff]/g, "\1h\1w\1" + "2"); // Bright white on green
-
-		// Cyan background
-		text = text.replace(/@[xX]30/g, "\1n\1k\1" + "6"); // Black on cyan
-		text = text.replace(/@[xX]31/g, "\1n\1b\1" + "6"); // Blue on cyan
-		text = text.replace(/@[xX]32/g, "\1n\1g\1" + "6"); // Green on cyan
-		text = text.replace(/@[xX]33/g, "\1n\1c\1" + "6"); // Cyan on cyan
-		text = text.replace(/@[xX]34/g, "\1n\1r\1" + "6"); // Red on cyan
-		text = text.replace(/@[xX]35/g, "\1n\1m\1" + "6"); // Magenta on cyan
-		text = text.replace(/@[xX]36/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
-		text = text.replace(/@[xX]37/g, "\1n\1w\1" + "6"); // White on cyan
-		text = text.replace(/@[xX]38/g, "\1h\1k\1" + "6"); // Bright black on cyan
-		text = text.replace(/@[xX]39/g, "\1h\1b\1" + "6"); // Bright blue on cyan
-		text = text.replace(/@[xX]3[Aa]/g, "\1h\1g\1" + "6"); // Bright green on cyan
-		text = text.replace(/@[xX]3[Bb]/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
-		text = text.replace(/@[xX]3[Cc]/g, "\1h\1r\1" + "6"); // Bright red on cyan
-		text = text.replace(/@[xX]3[Dd]/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
-		text = text.replace(/@[xX]3[Ee]/g, "\1h\1y\1" + "6"); // Bright yellow on cyan
-		text = text.replace(/@[xX]3[Ff]/g, "\1h\1w\1" + "6"); // Bright white on cyan
-
-		// Red background
-		text = text.replace(/@[xX]40/g, "\1n\1k\1" + "1"); // Black on red
-		text = text.replace(/@[xX]41/g, "\1n\1b\1" + "1"); // Blue on red
-		text = text.replace(/@[xX]42/g, "\1n\1g\1" + "1"); // Green on red
-		text = text.replace(/@[xX]43/g, "\1n\1c\1" + "1"); // Cyan on red
-		text = text.replace(/@[xX]44/g, "\1n\1r\1" + "1"); // Red on red
-		text = text.replace(/@[xX]45/g, "\1n\1m\1" + "1"); // Magenta on red
-		text = text.replace(/@[xX]46/g, "\1n\1y\1" + "1"); // Yellow/brown on red
-		text = text.replace(/@[xX]47/g, "\1n\1w\1" + "1"); // White on red
-		text = text.replace(/@[xX]48/g, "\1h\1k\1" + "1"); // Bright black on red
-		text = text.replace(/@[xX]49/g, "\1h\1b\1" + "1"); // Bright blue on red
-		text = text.replace(/@[xX]4[Aa]/g, "\1h\1g\1" + "1"); // Bright green on red
-		text = text.replace(/@[xX]4[Bb]/g, "\1h\1c\1" + "1"); // Bright cyan on red
-		text = text.replace(/@[xX]4[Cc]/g, "\1h\1r\1" + "1"); // Bright red on red
-		text = text.replace(/@[xX]4[Dd]/g, "\1h\1m\1" + "1"); // Bright magenta on red
-		text = text.replace(/@[xX]4[Ee]/g, "\1h\1y\1" + "1"); // Bright yellow on red
-		text = text.replace(/@[xX]4[Ff]/g, "\1h\1w\1" + "1"); // Bright white on red
-
-		// Magenta background
-		text = text.replace(/@[xX]50/g, "\1n\1k\1" + "5"); // Black on magenta
-		text = text.replace(/@[xX]51/g, "\1n\1b\1" + "5"); // Blue on magenta
-		text = text.replace(/@[xX]52/g, "\1n\1g\1" + "5"); // Green on magenta
-		text = text.replace(/@[xX]53/g, "\1n\1c\1" + "5"); // Cyan on magenta
-		text = text.replace(/@[xX]54/g, "\1n\1r\1" + "5"); // Red on magenta
-		text = text.replace(/@[xX]55/g, "\1n\1m\1" + "5"); // Magenta on magenta
-		text = text.replace(/@[xX]56/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
-		text = text.replace(/@[xX]57/g, "\1n\1w\1" + "5"); // White on magenta
-		text = text.replace(/@[xX]58/g, "\1h\1k\1" + "5"); // Bright black on magenta
-		text = text.replace(/@[xX]59/g, "\1h\1b\1" + "5"); // Bright blue on magenta
-		text = text.replace(/@[xX]5[Aa]/g, "\1h\1g\1" + "5"); // Bright green on magenta
-		text = text.replace(/@[xX]5[Bb]/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
-		text = text.replace(/@[xX]5[Cc]/g, "\1h\1r\1" + "5"); // Bright red on magenta
-		text = text.replace(/@[xX]5[Dd]/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
-		text = text.replace(/@[xX]5[Ee]/g, "\1h\1y\1" + "5"); // Bright yellow on magenta
-		text = text.replace(/@[xX]5[Ff]/g, "\1h\1w\1" + "5"); // Bright white on magenta
-
-		// Brown background
-		text = text.replace(/@[xX]60/g, "\1n\1k\1" + "3"); // Black on brown
-		text = text.replace(/@[xX]61/g, "\1n\1b\1" + "3"); // Blue on brown
-		text = text.replace(/@[xX]62/g, "\1n\1g\1" + "3"); // Green on brown
-		text = text.replace(/@[xX]63/g, "\1n\1c\1" + "3"); // Cyan on brown
-		text = text.replace(/@[xX]64/g, "\1n\1r\1" + "3"); // Red on brown
-		text = text.replace(/@[xX]65/g, "\1n\1m\1" + "3"); // Magenta on brown
-		text = text.replace(/@[xX]66/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
-		text = text.replace(/@[xX]67/g, "\1n\1w\1" + "3"); // White on brown
-		text = text.replace(/@[xX]68/g, "\1h\1k\1" + "3"); // Bright black on brown
-		text = text.replace(/@[xX]69/g, "\1h\1b\1" + "3"); // Bright blue on brown
-		text = text.replace(/@[xX]6[Aa]/g, "\1h\1g\1" + "3"); // Bright breen on brown
-		text = text.replace(/@[xX]6[Bb]/g, "\1h\1c\1" + "3"); // Bright cyan on brown
-		text = text.replace(/@[xX]6[Cc]/g, "\1h\1r\1" + "3"); // Bright red on brown
-		text = text.replace(/@[xX]6[Dd]/g, "\1h\1m\1" + "3"); // Bright magenta on brown
-		text = text.replace(/@[xX]6[Ee]/g, "\1h\1y\1" + "3"); // Bright yellow on brown
-		text = text.replace(/@[xX]6[Ff]/g, "\1h\1w\1" + "3"); // Bright white on brown
-
-		// White background
-		text = text.replace(/@[xX]70/g, "\1n\1k\1" + "7"); // Black on white
-		text = text.replace(/@[xX]71/g, "\1n\1b\1" + "7"); // Blue on white
-		text = text.replace(/@[xX]72/g, "\1n\1g\1" + "7"); // Green on white
-		text = text.replace(/@[xX]73/g, "\1n\1c\1" + "7"); // Cyan on white
-		text = text.replace(/@[xX]74/g, "\1n\1r\1" + "7"); // Red on white
-		text = text.replace(/@[xX]75/g, "\1n\1m\1" + "7"); // Magenta on white
-		text = text.replace(/@[xX]76/g, "\1n\1y\1" + "7"); // Yellow/brown on white
-		text = text.replace(/@[xX]77/g, "\1n\1w\1" + "7"); // White on white
-		text = text.replace(/@[xX]78/g, "\1h\1k\1" + "7"); // Bright black on white
-		text = text.replace(/@[xX]79/g, "\1h\1b\1" + "7"); // Bright blue on white
-		text = text.replace(/@[xX]7[Aa]/g, "\1h\1g\1" + "7"); // Bright green on white
-		text = text.replace(/@[xX]7[Bb]/g, "\1h\1c\1" + "7"); // Bright cyan on white
-		text = text.replace(/@[xX]7[Cc]/g, "\1h\1r\1" + "7"); // Bright red on white
-		text = text.replace(/@[xX]7[Dd]/g, "\1h\1m\1" + "7"); // Bright magenta on white
-		text = text.replace(/@[xX]7[Ee]/g, "\1h\1y\1" + "7"); // Bright yellow on white
-		text = text.replace(/@[xX]7[Ff]/g, "\1h\1w\1" + "7"); // Bright white on white
-
-		// Black background, blinking foreground
-		text = text.replace(/@[xX]80/g, "\1n\1k\1" + "0\1i"); // Blinking black on black
-		text = text.replace(/@[xX]81/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black
-		text = text.replace(/@[xX]82/g, "\1n\1g\1" + "0\1i"); // Blinking green on black
-		text = text.replace(/@[xX]83/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black
-		text = text.replace(/@[xX]84/g, "\1n\1r\1" + "0\1i"); // Blinking red on black
-		text = text.replace(/@[xX]85/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black
-		text = text.replace(/@[xX]86/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black
-		text = text.replace(/@[xX]87/g, "\1n\1w\1" + "0\1i"); // Blinking white on black
-		text = text.replace(/@[xX]88/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black
-		text = text.replace(/@[xX]89/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black
-		text = text.replace(/@[xX]8[Aa]/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black
-		text = text.replace(/@[xX]8[Bb]/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black
-		text = text.replace(/@[xX]8[Cc]/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black
-		text = text.replace(/@[xX]8[Dd]/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black
-		text = text.replace(/@[xX]8[Ee]/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black
-		text = text.replace(/@[xX]8[Ff]/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black
-
-		// Blue background, blinking foreground
-		text = text.replace(/@[xX]90/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue
-		text = text.replace(/@[xX]91/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue
-		text = text.replace(/@[xX]92/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue
-		text = text.replace(/@[xX]93/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue
-		text = text.replace(/@[xX]94/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue
-		text = text.replace(/@[xX]95/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue
-		text = text.replace(/@[xX]96/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue
-		text = text.replace(/@[xX]97/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue
-		text = text.replace(/@[xX]98/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue
-		text = text.replace(/@[xX]99/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue
-		text = text.replace(/@[xX]9[Aa]/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue
-		text = text.replace(/@[xX]9[Bb]/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue
-		text = text.replace(/@[xX]9[Cc]/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue
-		text = text.replace(/@[xX]9[Dd]/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue
-		text = text.replace(/@[xX]9[Ee]/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue
-		text = text.replace(/@[xX]9[Ff]/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue
-
-		// Green background, blinking foreground
-		text = text.replace(/@[xX][Aa]0/g, "\1n\1k\1" + "2\1i"); // Blinking black on green
-		text = text.replace(/@[xX][Aa]1/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green
-		text = text.replace(/@[xX][Aa]2/g, "\1n\1g\1" + "2\1i"); // Blinking green on green
-		text = text.replace(/@[xX][Aa]3/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green
-		text = text.replace(/@[xX][Aa]4/g, "\1n\1r\1" + "2\1i"); // Blinking red on green
-		text = text.replace(/@[xX][Aa]5/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green
-		text = text.replace(/@[xX][Aa]6/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green
-		text = text.replace(/@[xX][Aa]7/g, "\1n\1w\1" + "2\1i"); // Blinking white on green
-		text = text.replace(/@[xX][Aa]8/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green
-		text = text.replace(/@[xX][Aa]9/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green
-		text = text.replace(/@[xX][Aa][Aa]/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green
-		text = text.replace(/@[xX][Aa][Bb]/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green
-		text = text.replace(/@[xX][Aa][Cc]/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green
-		text = text.replace(/@[xX][Aa][Dd]/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green
-		text = text.replace(/@[xX][Aa][Ee]/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green
-		text = text.replace(/@[xX][Aa][Ff]/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green
-
-		// Cyan background, blinking foreground
-		text = text.replace(/@[xX][Bb]0/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan
-		text = text.replace(/@[xX][Bb]1/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan
-		text = text.replace(/@[xX][Bb]2/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan
-		text = text.replace(/@[xX][Bb]3/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan
-		text = text.replace(/@[xX][Bb]4/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan
-		text = text.replace(/@[xX][Bb]5/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan
-		text = text.replace(/@[xX][Bb]6/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan
-		text = text.replace(/@[xX][Bb]7/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan
-		text = text.replace(/@[xX][Bb]8/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan
-		text = text.replace(/@[xX][Bb]9/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan
-		text = text.replace(/@[xX][Bb][Aa]/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan
-		text = text.replace(/@[xX][Bb][Bb]/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan
-		text = text.replace(/@[xX][Bb][Cc]/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan
-		text = text.replace(/@[xX][Bb][Dd]/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan
-		text = text.replace(/@[xX][Bb][Ee]/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan
-		text = text.replace(/@[xX][Bb][Ff]/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan
-
-		// Red background, blinking foreground
-		text = text.replace(/@[xX][Cc]0/g, "\1n\1k\1" + "1\1i"); // Blinking black on red
-		text = text.replace(/@[xX][Cc]1/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red
-		text = text.replace(/@[xX][Cc]2/g, "\1n\1g\1" + "1\1i"); // Blinking green on red
-		text = text.replace(/@[xX][Cc]3/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red
-		text = text.replace(/@[xX][Cc]4/g, "\1n\1r\1" + "1\1i"); // Blinking red on red
-		text = text.replace(/@[xX][Cc]5/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red
-		text = text.replace(/@[xX][Cc]6/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red
-		text = text.replace(/@[xX][Cc]7/g, "\1n\1w\1" + "1\1i"); // Blinking white on red
-		text = text.replace(/@[xX][Cc]8/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red
-		text = text.replace(/@[xX][Cc]9/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red
-		text = text.replace(/@[xX][Cc][Aa]/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red
-		text = text.replace(/@[xX][Cc][Bb]/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red
-		text = text.replace(/@[xX][Cc][Cc]/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red
-		text = text.replace(/@[xX][Cc][Dd]/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red
-		text = text.replace(/@[xX][Cc][Ee]/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red
-		text = text.replace(/@[xX][Cc][Ff]/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red
-
-		// Magenta background, blinking foreground
-		text = text.replace(/@[xX][Dd]0/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta
-		text = text.replace(/@[xX][Dd]1/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta
-		text = text.replace(/@[xX][Dd]2/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta
-		text = text.replace(/@[xX][Dd]3/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta
-		text = text.replace(/@[xX][Dd]4/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta
-		text = text.replace(/@[xX][Dd]5/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta
-		text = text.replace(/@[xX][Dd]6/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta
-		text = text.replace(/@[xX][Dd]7/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta
-		text = text.replace(/@[xX][Dd]8/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta
-		text = text.replace(/@[xX][Dd]9/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta
-		text = text.replace(/@[xX][Dd][Aa]/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta
-		text = text.replace(/@[xX][Dd][Bb]/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta
-		text = text.replace(/@[xX][Dd][Cc]/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta
-		text = text.replace(/@[xX][Dd][Dd]/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta
-		text = text.replace(/@[xX][Dd][Ee]/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta
-		text = text.replace(/@[xX][Dd][Ff]/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta
-
-		// Brown background, blinking foreground
-		text = text.replace(/@[xX][Ee]0/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown
-		text = text.replace(/@[xX][Ee]1/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown
-		text = text.replace(/@[xX][Ee]2/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown
-		text = text.replace(/@[xX][Ee]3/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown
-		text = text.replace(/@[xX][Ee]4/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown
-		text = text.replace(/@[xX][Ee]5/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown
-		text = text.replace(/@[xX][Ee]6/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown
-		text = text.replace(/@[xX][Ee]7/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown
-		text = text.replace(/@[xX][Ee]8/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown
-		text = text.replace(/@[xX][Ee]9/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown
-		text = text.replace(/@[xX][Ee][Aa]/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown
-		text = text.replace(/@[xX][Ee][Bb]/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown
-		text = text.replace(/@[xX][Ee][Cc]/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown
-		text = text.replace(/@[xX][Ee][Dd]/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown
-		text = text.replace(/@[xX][Ee][Ee]/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown
-		text = text.replace(/@[xX][Ee][Ff]/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown
-
-		// White background, blinking foreground
-		text = text.replace(/@[xX][Ff]0/g, "\1n\1k\1" + "7\1i"); // Blinking black on white
-		text = text.replace(/@[xX][Ff]1/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white
-		text = text.replace(/@[xX][Ff]2/g, "\1n\1g\1" + "7\1i"); // Blinking green on white
-		text = text.replace(/@[xX][Ff]3/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white
-		text = text.replace(/@[xX][Ff]4/g, "\1n\1r\1" + "7\1i"); // Blinking red on white
-		text = text.replace(/@[xX][Ff]5/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white
-		text = text.replace(/@[xX][Ff]6/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white
-		text = text.replace(/@[xX][Ff]7/g, "\1n\1w\1" + "7\1i"); // Blinking white on white
-		text = text.replace(/@[xX][Ff]8/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white
-		text = text.replace(/@[xX][Ff]9/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white
-		text = text.replace(/@[xX][Ff][Aa]/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white
-		text = text.replace(/@[xX][Ff][Bb]/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white
-		text = text.replace(/@[xX][Ff][Cc]/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white
-		text = text.replace(/@[xX][Ff][Dd]/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white
-		text = text.replace(/@[xX][Ff][Ee]/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white
-		text = text.replace(/@[xX][Ff][Ff]/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white
-
-		return text;
-	}
-	else
-		return pText; // No PCBoard-style attribute codes found, so just return the text.
-}
-
-// Converts Wildcat attribute codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function wildcatAttrsToSyncAttrs(pText)
-{
-	// First, see if the text has any Wildcat-style attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (/@[0-9A-Fa-f]{2}@/.test(pText))
-	{
-		// Black background
-		var text = pText.replace(/@00@/g, "\1n\1k\1" + "0"); // Black on black
-		text = text.replace(/@01@/g, "\1n\1b\1" + "0"); // Blue on black
-		text = text.replace(/@02@/g, "\1n\1g\1" + "0"); // Green on black
-		text = text.replace(/@03@/g, "\1n\1c\1" + "0"); // Cyan on black
-		text = text.replace(/@04@/g, "\1n\1r\1" + "0"); // Red on black
-		text = text.replace(/@05@/g, "\1n\1m\1" + "0"); // Magenta on black
-		text = text.replace(/@06@/g, "\1n\1y\1" + "0"); // Yellow/brown on black
-		text = text.replace(/@07@/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@08@/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@09@/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/@08@/g, "\1h\1k\1" + "0"); // Bright black on black
-		text = text.replace(/@09@/g, "\1h\1b\1" + "0"); // Bright blue on black
-		text = text.replace(/@0[Aa]@/g, "\1h\1g\1" + "0"); // Bright green on black
-		text = text.replace(/@0[Bb]@/g, "\1h\1c\1" + "0"); // Bright cyan on black
-		text = text.replace(/@0[Cc]@/g, "\1h\1r\1" + "0"); // Bright red on black
-		text = text.replace(/@0[Dd]@/g, "\1h\1m\1" + "0"); // Bright magenta on black
-		text = text.replace(/@0[Ee]@/g, "\1h\1y\1" + "0"); // Bright yellow on black
-		text = text.replace(/@0[Ff]@/g, "\1h\1w\1" + "0"); // Bright white on black
-		// Blinking foreground
-
-		// Blue background
-		text = text.replace(/@10@/g, "\1n\1k\1" + "4"); // Black on blue
-		text = text.replace(/@11@/g, "\1n\1b\1" + "4"); // Blue on blue
-		text = text.replace(/@12@/g, "\1n\1g\1" + "4"); // Green on blue
-		text = text.replace(/@13@/g, "\1n\1c\1" + "4"); // Cyan on blue
-		text = text.replace(/@14@/g, "\1n\1r\1" + "4"); // Red on blue
-		text = text.replace(/@15@/g, "\1n\1m\1" + "4"); // Magenta on blue
-		text = text.replace(/@16@/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
-		text = text.replace(/@17@/g, "\1n\1w\1" + "4"); // White on blue
-		text = text.replace(/@18@/g, "\1h\1k\1" + "4"); // Bright black on blue
-		text = text.replace(/@19@/g, "\1h\1b\1" + "4"); // Bright blue on blue
-		text = text.replace(/@1[Aa]@/g, "\1h\1g\1" + "4"); // Bright green on blue
-		text = text.replace(/@1[Bb]@/g, "\1h\1c\1" + "4"); // Bright cyan on blue
-		text = text.replace(/@1[Cc]@/g, "\1h\1r\1" + "4"); // Bright red on blue
-		text = text.replace(/@1[Dd]@/g, "\1h\1m\1" + "4"); // Bright magenta on blue
-		text = text.replace(/@1[Ee]@/g, "\1h\1y\1" + "4"); // Bright yellow on blue
-		text = text.replace(/@1[Ff]@/g, "\1h\1w\1" + "4"); // Bright white on blue
-
-		// Green background
-		text = text.replace(/@20@/g, "\1n\1k\1" + "2"); // Black on green
-		text = text.replace(/@21@/g, "\1n\1b\1" + "2"); // Blue on green
-		text = text.replace(/@22@/g, "\1n\1g\1" + "2"); // Green on green
-		text = text.replace(/@23@/g, "\1n\1c\1" + "2"); // Cyan on green
-		text = text.replace(/@24@/g, "\1n\1r\1" + "2"); // Red on green
-		text = text.replace(/@25@/g, "\1n\1m\1" + "2"); // Magenta on green
-		text = text.replace(/@26@/g, "\1n\1y\1" + "2"); // Yellow/brown on green
-		text = text.replace(/@27@/g, "\1n\1w\1" + "2"); // White on green
-		text = text.replace(/@28@/g, "\1h\1k\1" + "2"); // Bright black on green
-		text = text.replace(/@29@/g, "\1h\1b\1" + "2"); // Bright blue on green
-		text = text.replace(/@2[Aa]@/g, "\1h\1g\1" + "2"); // Bright green on green
-		text = text.replace(/@2[Bb]@/g, "\1h\1c\1" + "2"); // Bright cyan on green
-		text = text.replace(/@2[Cc]@/g, "\1h\1r\1" + "2"); // Bright red on green
-		text = text.replace(/@2[Dd]@/g, "\1h\1m\1" + "2"); // Bright magenta on green
-		text = text.replace(/@2[Ee]@/g, "\1h\1y\1" + "2"); // Bright yellow on green
-		text = text.replace(/@2[Ff]@/g, "\1h\1w\1" + "2"); // Bright white on green
-
-		// Cyan background
-		text = text.replace(/@30@/g, "\1n\1k\1" + "6"); // Black on cyan
-		text = text.replace(/@31@/g, "\1n\1b\1" + "6"); // Blue on cyan
-		text = text.replace(/@32@/g, "\1n\1g\1" + "6"); // Green on cyan
-		text = text.replace(/@33@/g, "\1n\1c\1" + "6"); // Cyan on cyan
-		text = text.replace(/@34@/g, "\1n\1r\1" + "6"); // Red on cyan
-		text = text.replace(/@35@/g, "\1n\1m\1" + "6"); // Magenta on cyan
-		text = text.replace(/@36@/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
-		text = text.replace(/@37@/g, "\1n\1w\1" + "6"); // White on cyan
-		text = text.replace(/@38@/g, "\1h\1k\1" + "6"); // Bright black on cyan
-		text = text.replace(/@39@/g, "\1h\1b\1" + "6"); // Bright blue on cyan
-		text = text.replace(/@3[Aa]@/g, "\1h\1g\1" + "6"); // Bright green on cyan
-		text = text.replace(/@3[Bb]@/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
-		text = text.replace(/@3[Cc]@/g, "\1h\1r\1" + "6"); // Bright red on cyan
-		text = text.replace(/@3[Dd]@/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
-		text = text.replace(/@3[Ee]@/g, "\1h\1y\1" + "6"); // Bright yellow on cyan
-		text = text.replace(/@3[Ff]@/g, "\1h\1w\1" + "6"); // Bright white on cyan
-
-		// Red background
-		text = text.replace(/@40@/g, "\1n\1k\1" + "1"); // Black on red
-		text = text.replace(/@41@/g, "\1n\1b\1" + "1"); // Blue on red
-		text = text.replace(/@42@/g, "\1n\1g\1" + "1"); // Green on red
-		text = text.replace(/@43@/g, "\1n\1c\1" + "1"); // Cyan on red
-		text = text.replace(/@44@/g, "\1n\1r\1" + "1"); // Red on red
-		text = text.replace(/@45@/g, "\1n\1m\1" + "1"); // Magenta on red
-		text = text.replace(/@46@/g, "\1n\1y\1" + "1"); // Yellow/brown on red
-		text = text.replace(/@47@/g, "\1n\1w\1" + "1"); // White on red
-		text = text.replace(/@48@/g, "\1h\1k\1" + "1"); // Bright black on red
-		text = text.replace(/@49@/g, "\1h\1b\1" + "1"); // Bright blue on red
-		text = text.replace(/@4[Aa]@/g, "\1h\1g\1" + "1"); // Bright green on red
-		text = text.replace(/@4[Bb]@/g, "\1h\1c\1" + "1"); // Bright cyan on red
-		text = text.replace(/@4[Cc]@/g, "\1h\1r\1" + "1"); // Bright red on red
-		text = text.replace(/@4[Dd]@/g, "\1h\1m\1" + "1"); // Bright magenta on red
-		text = text.replace(/@4[Ee]@/g, "\1h\1y\1" + "1"); // Bright yellow on red
-		text = text.replace(/@4[Ff]@/g, "\1h\1w\1" + "1"); // Bright white on red
-
-		// Magenta background
-		text = text.replace(/@50@/g, "\1n\1k\1" + "5"); // Black on magenta
-		text = text.replace(/@51@/g, "\1n\1b\1" + "5"); // Blue on magenta
-		text = text.replace(/@52@/g, "\1n\1g\1" + "5"); // Green on magenta
-		text = text.replace(/@53@/g, "\1n\1c\1" + "5"); // Cyan on magenta
-		text = text.replace(/@54@/g, "\1n\1r\1" + "5"); // Red on magenta
-		text = text.replace(/@55@/g, "\1n\1m\1" + "5"); // Magenta on magenta
-		text = text.replace(/@56@/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
-		text = text.replace(/@57@/g, "\1n\1w\1" + "5"); // White on magenta
-		text = text.replace(/@58@/g, "\1h\1k\1" + "5"); // Bright black on magenta
-		text = text.replace(/@59@/g, "\1h\1b\1" + "5"); // Bright blue on magenta
-		text = text.replace(/@5[Aa]@/g, "\1h\1g\1" + "5"); // Bright green on magenta
-		text = text.replace(/@5[Bb]@/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
-		text = text.replace(/@5[Cc]@/g, "\1h\1r\1" + "5"); // Bright red on magenta
-		text = text.replace(/@5[Dd]@/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
-		text = text.replace(/@5[Ee]@/g, "\1h\1y\1" + "5"); // Bright yellow on magenta
-		text = text.replace(/@5[Ff]@/g, "\1h\1w\1" + "5"); // Bright white on magenta
-
-		// Brown background
-		text = text.replace(/@60@/g, "\1n\1k\1" + "3"); // Black on brown
-		text = text.replace(/@61@/g, "\1n\1b\1" + "3"); // Blue on brown
-		text = text.replace(/@62@/g, "\1n\1g\1" + "3"); // Green on brown
-		text = text.replace(/@63@/g, "\1n\1c\1" + "3"); // Cyan on brown
-		text = text.replace(/@64@/g, "\1n\1r\1" + "3"); // Red on brown
-		text = text.replace(/@65@/g, "\1n\1m\1" + "3"); // Magenta on brown
-		text = text.replace(/@66@/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
-		text = text.replace(/@67@/g, "\1n\1w\1" + "3"); // White on brown
-		text = text.replace(/@68@/g, "\1h\1k\1" + "3"); // Bright black on brown
-		text = text.replace(/@69@/g, "\1h\1b\1" + "3"); // Bright blue on brown
-		text = text.replace(/@6[Aa]@/g, "\1h\1g\1" + "3"); // Bright breen on brown
-		text = text.replace(/@6[Bb]@/g, "\1h\1c\1" + "3"); // Bright cyan on brown
-		text = text.replace(/@6[Cc]@/g, "\1h\1r\1" + "3"); // Bright red on brown
-		text = text.replace(/@6[Dd]@/g, "\1h\1m\1" + "3"); // Bright magenta on brown
-		text = text.replace(/@6[Ee]@/g, "\1h\1y\1" + "3"); // Bright yellow on brown
-		text = text.replace(/@6[Ff]@/g, "\1h\1w\1" + "3"); // Bright white on brown
-
-		// White background
-		text = text.replace(/@70@/g, "\1n\1k\1" + "7"); // Black on white
-		text = text.replace(/@71@/g, "\1n\1b\1" + "7"); // Blue on white
-		text = text.replace(/@72@/g, "\1n\1g\1" + "7"); // Green on white
-		text = text.replace(/@73@/g, "\1n\1c\1" + "7"); // Cyan on white
-		text = text.replace(/@74@/g, "\1n\1r\1" + "7"); // Red on white
-		text = text.replace(/@75@/g, "\1n\1m\1" + "7"); // Magenta on white
-		text = text.replace(/@76@/g, "\1n\1y\1" + "7"); // Yellow/brown on white
-		text = text.replace(/@77@/g, "\1n\1w\1" + "7"); // White on white
-		text = text.replace(/@78@/g, "\1h\1k\1" + "7"); // Bright black on white
-		text = text.replace(/@79@/g, "\1h\1b\1" + "7"); // Bright blue on white
-		text = text.replace(/@7[Aa]@/g, "\1h\1g\1" + "7"); // Bright green on white
-		text = text.replace(/@7[Bb]@/g, "\1h\1c\1" + "7"); // Bright cyan on white
-		text = text.replace(/@7[Cc]@/g, "\1h\1r\1" + "7"); // Bright red on white
-		text = text.replace(/@7[Dd]@/g, "\1h\1m\1" + "7"); // Bright magenta on white
-		text = text.replace(/@7[Ee]@/g, "\1h\1y\1" + "7"); // Bright yellow on white
-		text = text.replace(/@7[Ff]@/g, "\1h\1w\1" + "7"); // Bright white on white
-
-		// Black background, blinking foreground
-		text = text.replace(/@80@/g, "\1n\1k\1" + "0\1i"); // Blinking black on black
-		text = text.replace(/@81@/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black
-		text = text.replace(/@82@/g, "\1n\1g\1" + "0\1i"); // Blinking green on black
-		text = text.replace(/@83@/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black
-		text = text.replace(/@84@/g, "\1n\1r\1" + "0\1i"); // Blinking red on black
-		text = text.replace(/@85@/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black
-		text = text.replace(/@86@/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black
-		text = text.replace(/@87@/g, "\1n\1w\1" + "0\1i"); // Blinking white on black
-		text = text.replace(/@88@/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black
-		text = text.replace(/@89@/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black
-		text = text.replace(/@8[Aa]@/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black
-		text = text.replace(/@8[Bb]@/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black
-		text = text.replace(/@8[Cc]@/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black
-		text = text.replace(/@8[Dd]@/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black
-		text = text.replace(/@8[Ee]@/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black
-		text = text.replace(/@8[Ff]@/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black
-
-		// Blue background, blinking foreground
-		text = text.replace(/@90@/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue
-		text = text.replace(/@91@/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue
-		text = text.replace(/@92@/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue
-		text = text.replace(/@93@/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue
-		text = text.replace(/@94@/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue
-		text = text.replace(/@95@/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue
-		text = text.replace(/@96@/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue
-		text = text.replace(/@97@/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue
-		text = text.replace(/@98@/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue
-		text = text.replace(/@99@/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue
-		text = text.replace(/@9[Aa]@/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue
-		text = text.replace(/@9[Bb]@/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue
-		text = text.replace(/@9[Cc]@/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue
-		text = text.replace(/@9[Dd]@/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue
-		text = text.replace(/@9[Ee]@/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue
-		text = text.replace(/@9[Ff]@/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue
-
-		// Green background, blinking foreground
-		text = text.replace(/@[Aa]0@/g, "\1n\1k\1" + "2\1i"); // Blinking black on green
-		text = text.replace(/@[Aa]1@/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green
-		text = text.replace(/@[Aa]2@/g, "\1n\1g\1" + "2\1i"); // Blinking green on green
-		text = text.replace(/@[Aa]3@/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green
-		text = text.replace(/@[Aa]4@/g, "\1n\1r\1" + "2\1i"); // Blinking red on green
-		text = text.replace(/@[Aa]5@/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green
-		text = text.replace(/@[Aa]6@/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green
-		text = text.replace(/@[Aa]7@/g, "\1n\1w\1" + "2\1i"); // Blinking white on green
-		text = text.replace(/@[Aa]8@/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green
-		text = text.replace(/@[Aa]9@/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green
-		text = text.replace(/@[Aa][Aa]@/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green
-		text = text.replace(/@[Aa][Bb]@/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green
-		text = text.replace(/@[Aa][Cc]@/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green
-		text = text.replace(/@[Aa][Dd]@/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green
-		text = text.replace(/@[Aa][Ee]@/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green
-		text = text.replace(/@[Aa][Ff]@/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green
-
-		// Cyan background, blinking foreground
-		text = text.replace(/@[Bb]0@/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan
-		text = text.replace(/@[Bb]1@/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan
-		text = text.replace(/@[Bb]2@/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan
-		text = text.replace(/@[Bb]3@/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan
-		text = text.replace(/@[Bb]4@/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan
-		text = text.replace(/@[Bb]5@/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan
-		text = text.replace(/@[Bb]6@/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan
-		text = text.replace(/@[Bb]7@/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan
-		text = text.replace(/@[Bb]8@/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan
-		text = text.replace(/@[Bb]9@/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan
-		text = text.replace(/@[Bb][Aa]@/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan
-		text = text.replace(/@[Bb][Bb]@/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan
-		text = text.replace(/@[Bb][Cc]@/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan
-		text = text.replace(/@[Bb][Dd]@/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan
-		text = text.replace(/@[Bb][Ee]@/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan
-		text = text.replace(/@[Bb][Ff]@/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan
-
-		// Red background, blinking foreground
-		text = text.replace(/@[Cc]0@/g, "\1n\1k\1" + "1\1i"); // Blinking black on red
-		text = text.replace(/@[Cc]1@/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red
-		text = text.replace(/@[Cc]2@/g, "\1n\1g\1" + "1\1i"); // Blinking green on red
-		text = text.replace(/@[Cc]3@/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red
-		text = text.replace(/@[Cc]4@/g, "\1n\1r\1" + "1\1i"); // Blinking red on red
-		text = text.replace(/@[Cc]5@/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red
-		text = text.replace(/@[Cc]6@/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red
-		text = text.replace(/@[Cc]7@/g, "\1n\1w\1" + "1\1i"); // Blinking white on red
-		text = text.replace(/@[Cc]8@/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red
-		text = text.replace(/@[Cc]9@/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red
-		text = text.replace(/@[Cc][Aa]@/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red
-		text = text.replace(/@[Cc][Bb]@/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red
-		text = text.replace(/@[Cc][Cc]@/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red
-		text = text.replace(/@[Cc][Dd]@/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red
-		text = text.replace(/@[Cc][Ee]@/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red
-		text = text.replace(/@[Cc][Ff]@/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red
-
-		// Magenta background, blinking foreground
-		text = text.replace(/@[Dd]0@/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta
-		text = text.replace(/@[Dd]1@/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta
-		text = text.replace(/@[Dd]2@/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta
-		text = text.replace(/@[Dd]3@/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta
-		text = text.replace(/@[Dd]4@/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta
-		text = text.replace(/@[Dd]5@/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta
-		text = text.replace(/@[Dd]6@/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta
-		text = text.replace(/@[Dd]7@/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta
-		text = text.replace(/@[Dd]8@/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta
-		text = text.replace(/@[Dd]9@/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta
-		text = text.replace(/@[Dd][Aa]@/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta
-		text = text.replace(/@[Dd][Bb]@/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta
-		text = text.replace(/@[Dd][Cc]@/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta
-		text = text.replace(/@[Dd][Dd]@/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta
-		text = text.replace(/@[Dd][Ee]@/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta
-		text = text.replace(/@[Dd][Ff]@/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta
-
-		// Brown background, blinking foreground
-		text = text.replace(/@[Ee]0@/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown
-		text = text.replace(/@[Ee]1@/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown
-		text = text.replace(/@[Ee]2@/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown
-		text = text.replace(/@[Ee]3@/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown
-		text = text.replace(/@[Ee]4@/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown
-		text = text.replace(/@[Ee]5@/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown
-		text = text.replace(/@[Ee]6@/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown
-		text = text.replace(/@[Ee]7@/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown
-		text = text.replace(/@[Ee]8@/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown
-		text = text.replace(/@[Ee]9@/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown
-		text = text.replace(/@[Ee][Aa]@/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown
-		text = text.replace(/@[Ee][Bb]@/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown
-		text = text.replace(/@[Ee][Cc]@/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown
-		text = text.replace(/@[Ee][Dd]@/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown
-		text = text.replace(/@[Ee][Ee]@/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown
-		text = text.replace(/@[Ee][Ff]@/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown
-
-		// White background, blinking foreground
-		text = text.replace(/@[Ff]0@/g, "\1n\1k\1" + "7\1i"); // Blinking black on white
-		text = text.replace(/@[Ff]1@/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white
-		text = text.replace(/@[Ff]2@/g, "\1n\1g\1" + "7\1i"); // Blinking green on white
-		text = text.replace(/@[Ff]3@/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white
-		text = text.replace(/@[Ff]4@/g, "\1n\1r\1" + "7\1i"); // Blinking red on white
-		text = text.replace(/@[Ff]5@/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white
-		text = text.replace(/@[Ff]6@/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white
-		text = text.replace(/@[Ff]7@/g, "\1n\1w\1" + "7\1i"); // Blinking white on white
-		text = text.replace(/@[Ff]8@/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white
-		text = text.replace(/@[Ff]9@/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white
-		text = text.replace(/@[Ff][Aa]@/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white
-		text = text.replace(/@[Ff][Bb]@/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white
-		text = text.replace(/@[Ff][Cc]@/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white
-		text = text.replace(/@[Ff][Dd]@/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white
-		text = text.replace(/@[Ff][Ee]@/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white
-		text = text.replace(/@[Ff][Ff]@/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white
-
-		return text;
-	}
-	else
-		return pText; // No Wildcat-style attribute codes found, so just return the text.
-}
-
-// Converts Celerity attribute codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function celerityAttrsToSyncAttrs(pText)
-{
-	// First, see if the text has any Celerity-style attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (/\|[kbgcrmywdBGCRMYWS]/.test(pText))
-	{
-		// Using the \|S code (swap foreground & background)
-
-		// Blue background
-		var text = pText.replace(/\|b\|S\|k/g, "\1n\1k\1" + "4"); // Black on blue
-		text = text.replace(/\|b\|S\|b/g, "\1n\1b\1" + "4"); // Blue on blue
-		text = text.replace(/\|b\|S\|g/g, "\1n\1g\1" + "4"); // Green on blue
-		text = text.replace(/\|b\|S\|c/g, "\1n\1c\1" + "4"); // Cyan on blue
-		text = text.replace(/\|b\|S\|r/g, "\1n\1r\1" + "4"); // Red on blue
-		text = text.replace(/\|b\|S\|m/g, "\1n\1m\1" + "4"); // Magenta on blue
-		text = text.replace(/\|b\|S\|y/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
-		text = text.replace(/\|b\|S\|w/g, "\1n\1w\1" + "4"); // White on blue
-		text = text.replace(/\|b\|S\|d/g, "\1h\1k\1" + "4"); // Bright black on blue
-		text = text.replace(/\|b\|S\|B/g, "\1h\1b\1" + "4"); // Bright blue on blue
-		text = text.replace(/\|b\|S\|G/g, "\1h\1g\1" + "4"); // Bright green on blue
-		text = text.replace(/\|b\|S\|C/g, "\1h\1c\1" + "4"); // Bright cyan on blue
-		text = text.replace(/\|b\|S\|R/g, "\1h\1r\1" + "4"); // Bright red on blue
-		text = text.replace(/\|b\|S\|M/g, "\1h\1m\1" + "4"); // Bright magenta on blue
-		text = text.replace(/\|b\|S\|Y/g, "\1h\1y\1" + "4"); // Yellow on blue
-		text = text.replace(/\|b\|S\|W/g, "\1h\1w\1" + "4"); // Bright white on blue
-
-		// Green background
-		text = text.replace(/\|g\|S\|k/g, "\1n\1k\1" + "2"); // Black on green
-		text = text.replace(/\|g\|S\|b/g, "\1n\1b\1" + "2"); // Blue on green
-		text = text.replace(/\|g\|S\|g/g, "\1n\1g\1" + "2"); // Green on green
-		text = text.replace(/\|g\|S\|c/g, "\1n\1c\1" + "2"); // Cyan on green
-		text = text.replace(/\|g\|S\|r/g, "\1n\1r\1" + "2"); // Red on green
-		text = text.replace(/\|g\|S\|m/g, "\1n\1m\1" + "2"); // Magenta on green
-		text = text.replace(/\|g\|S\|y/g, "\1n\1y\1" + "2"); // Yellow/brown on green
-		text = text.replace(/\|g\|S\|w/g, "\1n\1w\1" + "2"); // White on green
-		text = text.replace(/\|g\|S\|d/g, "\1h\1k\1" + "2"); // Bright black on green
-		text = text.replace(/\|g\|S\|B/g, "\1h\1b\1" + "2"); // Bright blue on green
-		text = text.replace(/\|g\|S\|G/g, "\1h\1g\1" + "2"); // Bright green on green
-		text = text.replace(/\|g\|S\|C/g, "\1h\1c\1" + "2"); // Bright cyan on green
-		text = text.replace(/\|g\|S\|R/g, "\1h\1r\1" + "2"); // Bright red on green
-		text = text.replace(/\|g\|S\|M/g, "\1h\1m\1" + "2"); // Bright magenta on green
-		text = text.replace(/\|g\|S\|Y/g, "\1h\1y\1" + "2"); // Yellow on green
-		text = text.replace(/\|g\|S\|W/g, "\1h\1w\1" + "2"); // Bright white on green
-
-		// Cyan background
-		text = text.replace(/\|c\|S\|k/g, "\1n\1k\1" + "6"); // Black on cyan
-		text = text.replace(/\|c\|S\|b/g, "\1n\1b\1" + "6"); // Blue on cyan
-		text = text.replace(/\|c\|S\|g/g, "\1n\1g\1" + "6"); // Green on cyan
-		text = text.replace(/\|c\|S\|c/g, "\1n\1c\1" + "6"); // Cyan on cyan
-		text = text.replace(/\|c\|S\|r/g, "\1n\1r\1" + "6"); // Red on cyan
-		text = text.replace(/\|c\|S\|m/g, "\1n\1m\1" + "6"); // Magenta on cyan
-		text = text.replace(/\|c\|S\|y/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
-		text = text.replace(/\|c\|S\|w/g, "\1n\1w\1" + "6"); // White on cyan
-		text = text.replace(/\|c\|S\|d/g, "\1h\1k\1" + "6"); // Bright black on cyan
-		text = text.replace(/\|c\|S\|B/g, "\1h\1b\1" + "6"); // Bright blue on cyan
-		text = text.replace(/\|c\|S\|G/g, "\1h\1g\1" + "6"); // Bright green on cyan
-		text = text.replace(/\|c\|S\|C/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
-		text = text.replace(/\|c\|S\|R/g, "\1h\1r\1" + "6"); // Bright red on cyan
-		text = text.replace(/\|c\|S\|M/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
-		text = text.replace(/\|c\|S\|Y/g, "\1h\1y\1" + "6"); // Yellow on cyan
-		text = text.replace(/\|c\|S\|W/g, "\1h\1w\1" + "6"); // Bright white on cyan
-
-		// Red background
-		text = text.replace(/\|r\|S\|k/g, "\1n\1k\1" + "1"); // Black on red
-		text = text.replace(/\|r\|S\|b/g, "\1n\1b\1" + "1"); // Blue on red
-		text = text.replace(/\|r\|S\|g/g, "\1n\1g\1" + "1"); // Green on red
-		text = text.replace(/\|r\|S\|c/g, "\1n\1c\1" + "1"); // Cyan on red
-		text = text.replace(/\|r\|S\|r/g, "\1n\1r\1" + "1"); // Red on red
-		text = text.replace(/\|r\|S\|m/g, "\1n\1m\1" + "1"); // Magenta on red
-		text = text.replace(/\|r\|S\|y/g, "\1n\1y\1" + "1"); // Yellow/brown on red
-		text = text.replace(/\|r\|S\|w/g, "\1n\1w\1" + "1"); // White on red
-		text = text.replace(/\|r\|S\|d/g, "\1h\1k\1" + "1"); // Bright black on red
-		text = text.replace(/\|r\|S\|B/g, "\1h\1b\1" + "1"); // Bright blue on red
-		text = text.replace(/\|r\|S\|G/g, "\1h\1g\1" + "1"); // Bright green on red
-		text = text.replace(/\|r\|S\|C/g, "\1h\1c\1" + "1"); // Bright cyan on red
-		text = text.replace(/\|r\|S\|R/g, "\1h\1r\1" + "1"); // Bright red on red
-		text = text.replace(/\|r\|S\|M/g, "\1h\1m\1" + "1"); // Bright magenta on red
-		text = text.replace(/\|r\|S\|Y/g, "\1h\1y\1" + "1"); // Yellow on red
-		text = text.replace(/\|r\|S\|W/g, "\1h\1w\1" + "1"); // Bright white on red
-
-		// Magenta background
-		text = text.replace(/\|m\|S\|k/g, "\1n\1k\1" + "5"); // Black on magenta
-		text = text.replace(/\|m\|S\|b/g, "\1n\1b\1" + "5"); // Blue on magenta
-		text = text.replace(/\|m\|S\|g/g, "\1n\1g\1" + "5"); // Green on magenta
-		text = text.replace(/\|m\|S\|c/g, "\1n\1c\1" + "5"); // Cyan on magenta
-		text = text.replace(/\|m\|S\|r/g, "\1n\1r\1" + "5"); // Red on magenta
-		text = text.replace(/\|m\|S\|m/g, "\1n\1m\1" + "5"); // Magenta on magenta
-		text = text.replace(/\|m\|S\|y/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
-		text = text.replace(/\|m\|S\|w/g, "\1n\1w\1" + "5"); // White on magenta
-		text = text.replace(/\|m\|S\|d/g, "\1h\1k\1" + "5"); // Bright black on magenta
-		text = text.replace(/\|m\|S\|B/g, "\1h\1b\1" + "5"); // Bright blue on magenta
-		text = text.replace(/\|m\|S\|G/g, "\1h\1g\1" + "5"); // Bright green on magenta
-		text = text.replace(/\|m\|S\|C/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
-		text = text.replace(/\|m\|S\|R/g, "\1h\1r\1" + "5"); // Bright red on magenta
-		text = text.replace(/\|m\|S\|M/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
-		text = text.replace(/\|m\|S\|Y/g, "\1h\1y\1" + "5"); // Yellow on magenta
-		text = text.replace(/\|m\|S\|W/g, "\1h\1w\1" + "5"); // Bright white on magenta
-
-		// Brown background
-		text = text.replace(/\|y\|S\|k/g, "\1n\1k\1" + "3"); // Black on brown
-		text = text.replace(/\|y\|S\|b/g, "\1n\1b\1" + "3"); // Blue on brown
-		text = text.replace(/\|y\|S\|g/g, "\1n\1g\1" + "3"); // Green on brown
-		text = text.replace(/\|y\|S\|c/g, "\1n\1c\1" + "3"); // Cyan on brown
-		text = text.replace(/\|y\|S\|r/g, "\1n\1r\1" + "3"); // Red on brown
-		text = text.replace(/\|y\|S\|m/g, "\1n\1m\1" + "3"); // Magenta on brown
-		text = text.replace(/\|y\|S\|y/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
-		text = text.replace(/\|y\|S\|w/g, "\1n\1w\1" + "3"); // White on brown
-		text = text.replace(/\|y\|S\|d/g, "\1h\1k\1" + "3"); // Bright black on brown
-		text = text.replace(/\|y\|S\|B/g, "\1h\1b\1" + "3"); // Bright blue on brown
-		text = text.replace(/\|y\|S\|G/g, "\1h\1g\1" + "3"); // Bright green on brown
-		text = text.replace(/\|y\|S\|C/g, "\1h\1c\1" + "3"); // Bright cyan on brown
-		text = text.replace(/\|y\|S\|R/g, "\1h\1r\1" + "3"); // Bright red on brown
-		text = text.replace(/\|y\|S\|M/g, "\1h\1m\1" + "3"); // Bright magenta on brown
-		text = text.replace(/\|y\|S\|Y/g, "\1h\1y\1" + "3"); // Yellow on brown
-		text = text.replace(/\|y\|S\|W/g, "\1h\1w\1" + "3"); // Bright white on brown
-
-		// White background
-		text = text.replace(/\|w\|S\|k/g, "\1n\1k\1" + "7"); // Black on white
-		text = text.replace(/\|w\|S\|b/g, "\1n\1b\1" + "7"); // Blue on white
-		text = text.replace(/\|w\|S\|g/g, "\1n\1g\1" + "7"); // Green on white
-		text = text.replace(/\|w\|S\|c/g, "\1n\1c\1" + "7"); // Cyan on white
-		text = text.replace(/\|w\|S\|r/g, "\1n\1r\1" + "7"); // Red on white
-		text = text.replace(/\|w\|S\|m/g, "\1n\1m\1" + "7"); // Magenta on white
-		text = text.replace(/\|w\|S\|y/g, "\1n\1y\1" + "7"); // Yellow/brown on white
-		text = text.replace(/\|w\|S\|w/g, "\1n\1w\1" + "7"); // White on white
-		text = text.replace(/\|w\|S\|d/g, "\1h\1k\1" + "7"); // Bright black on white
-		text = text.replace(/\|w\|S\|B/g, "\1h\1b\1" + "7"); // Bright blue on white
-		text = text.replace(/\|w\|S\|G/g, "\1h\1g\1" + "7"); // Bright green on white
-		text = text.replace(/\|w\|S\|C/g, "\1h\1c\1" + "7"); // Bright cyan on white
-		text = text.replace(/\|w\|S\|R/g, "\1h\1r\1" + "7"); // Bright red on white
-		text = text.replace(/\|w\|S\|M/g, "\1h\1m\1" + "7"); // Bright magenta on white
-		text = text.replace(/\|w\|S\|Y/g, "\1h\1y\1" + "7"); // Yellow on white
-		text = text.replace(/\|w\|S\|W/g, "\1h\1w\1" + "7"); // Bright white on white
-
-		// Colors on black background
-		text = text.replace(/\|k/g, "\1n\1k\1" + "0");  // Black on black
-		text = text.replace(/\|k\|S\|k/g, "\1n\1k\1" + "0"); // Black on black
-		text = text.replace(/\|b/g, "\1n\1b\1" + "0");       // Blue on black
-		text = text.replace(/\|k\|S\|b/g, "\1n\1b\1" + "0"); // Blue on black
-		text = text.replace(/\|g/g, "\1n\1g\1" + "0");       // Green on black
-		text = text.replace(/\|k\|S\|g/g, "\1n\1g\1" + "0"); // Green on black
-		text = text.replace(/\|c/g, "\1n\1c\1" + "0");       // Cyan on black
-		text = text.replace(/\|k\|S\|c/g, "\1n\1c\1" + "0"); // Cyan on black
-		text = text.replace(/\|r/g, "\1n\1r\1" + "0");       // Red on black
-		text = text.replace(/\|k\|S\|r/g, "\1n\1r\1" + "0"); // Red on black
-		text = text.replace(/\|m/g, "\1n\1m\1" + "0");       // Magenta on black
-		text = text.replace(/\|k\|S\|m/g, "\1n\1m\1" + "0"); // Magenta on black
-		text = text.replace(/\|y/g, "\1n\1y\1" + "0");       // Yellow/brown on black
-		text = text.replace(/\|k\|S\|y/g, "\1n\1y\1" + "0"); // Yellow/brown on black
-		text = text.replace(/\|w/g, "\1n\1w\1" + "0");       // White on black
-		text = text.replace(/\|k\|S\|w/g, "\1n\1w\1" + "0"); // White on black
-		text = text.replace(/\|d/g, "\1h\1k\1" + "0");       // Bright black on black
-		text = text.replace(/\|k\|S\|d/g, "\1h\1k\1" + "0"); // Bright black on black
-		text = text.replace(/\|B/g, "\1h\1b\1" + "0");       // Bright blue on black
-		text = text.replace(/\|k\|S\|B/g, "\1h\1b\1" + "0"); // Bright blue on black
-		text = text.replace(/\|G/g, "\1h\1g\1" + "0");       // Bright green on black
-		text = text.replace(/\|k\|S\|G/g, "\1h\1g\1" + "0"); // Bright green on black
-		text = text.replace(/\|C/g, "\1h\1c\1" + "0");       // Bright cyan on black
-		text = text.replace(/\|k\|S\|C/g, "\1h\1c\1" + "0"); // Bright cyan on black
-		text = text.replace(/\|R/g, "\1h\1r\1" + "0");       // Bright red on black
-		text = text.replace(/\|k\|S\|R/g, "\1h\1r\1" + "0"); // Bright red on black
-		text = text.replace(/\|M/g, "\1h\1m\1" + "0");       // Bright magenta on black
-		text = text.replace(/\|k\|S\|M/g, "\1h\1m\1" + "0"); // Bright magenta on black
-		text = text.replace(/\|Y/g, "\1h\1y\1" + "0");       // Yellow on black
-		text = text.replace(/\|k\|S\|Y/g, "\1h\1y\1" + "0"); // Yellow on black
-		text = text.replace(/\|W/g, "\1h\1w\1" + "0");       // Bright white on black
-		text = text.replace(/\|k\|S\|W/g, "\1h\1w\1" + "0"); // Bright white on black
-
-		return text;
-	}
-	else
-		return pText; // No Celerity-style attribute codes found, so just return the text.
-}
-
-// Converts Renegade attribute (color) codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function renegadeAttrsToSyncAttrs(pText)
-{
-	// First, see if the text has any Renegade-style attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (/\|[0-3][0-9]/.test(pText))
-	{
-		var text = pText.replace(/\|00/g, "\1n\1k"); // Normal black
-		text = text.replace(/\|01/g, "\1n\1b"); // Normal blue
-		text = text.replace(/\|02/g, "\1n\1g"); // Normal green
-		text = text.replace(/\|03/g, "\1n\1c"); // Normal cyan
-		text = text.replace(/\|04/g, "\1n\1r"); // Normal red
-		text = text.replace(/\|05/g, "\1n\1m"); // Normal magenta
-		text = text.replace(/\|06/g, "\1n\1y"); // Normal brown
-		text = text.replace(/\|07/g, "\1n\1w"); // Normal white
-		text = text.replace(/\|08/g, "\1n\1k\1h"); // High intensity black
-		text = text.replace(/\|09/g, "\1n\1b\1h"); // High intensity blue
-		text = text.replace(/\|10/g, "\1n\1g\1h"); // High intensity green
-		text = text.replace(/\|11/g, "\1n\1c\1h"); // High intensity cyan
-		text = text.replace(/\|12/g, "\1n\1r\1h"); // High intensity red
-		text = text.replace(/\|13/g, "\1n\1m\1h"); // High intensity magenta
-		text = text.replace(/\|14/g, "\1n\1y\1h"); // Yellow (high intensity brown)
-		text = text.replace(/\|15/g, "\1n\1w\1h"); // High intensity white
-		text = text.replace(/\|16/g, "\1" + "0"); // Background black
-		text = text.replace(/\|17/g, "\1" + "4"); // Background blue
-		text = text.replace(/\|18/g, "\1" + "2"); // Background green
-		text = text.replace(/\|19/g, "\1" + "6"); // Background cyan
-		text = text.replace(/\|20/g, "\1" + "1"); // Background red
-		text = text.replace(/\|21/g, "\1" + "5"); // Background magenta
-		text = text.replace(/\|22/g, "\1" + "3"); // Background brown
-		text = text.replace(/\|23/g, "\1" + "7"); // Background white
-		text = text.replace(/\|24/g, "\1i\1w\1" + "0"); // Blinking white on black
-		text = text.replace(/\|25/g, "\1i\1w\1" + "4"); // Blinking white on blue
-		text = text.replace(/\|26/g, "\1i\1w\1" + "2"); // Blinking white on green
-		text = text.replace(/\|27/g, "\1i\1w\1" + "6"); // Blinking white on cyan
-		text = text.replace(/\|28/g, "\1i\1w\1" + "1"); // Blinking white on red
-		text = text.replace(/\|29/g, "\1i\1w\1" + "5"); // Blinking white on magenta
-		text = text.replace(/\|30/g, "\1i\1w\1" + "3"); // Blinking white on yellow/brown
-		text = text.replace(/\|31/g, "\1i\1w\1" + "7"); // Blinking white on white
-		return text;
-	}
-	else
-		return pText; // No Renegade-style attribute codes found, so just return the text.
-}
-
-// Converts ANSI attribute codes to Synchronet attribute codes.
-//
-// Parameters:
-//  pText: A string containing the text to convert
-//
-// Return value: The text with the color codes converted
-function ANSIAttrsToSyncAttrs(pText)
-{
-	// TODO: Test & update this some more..  Not sure if this is working 100% right.
-
-	// Web pages with ANSI code information:
-	// http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
-	// http://ascii-table.com/ansi-escape-sequences.php
-	// http://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
-
-	// First, see if the text has any ANSI attribute codes at all.  We'll be
-	// performing a bunch of search & replace commands, so we don't want to do
-	// all that work for nothing.
-	if (textHasANSICodes(pText))
-	{
-		var text = "";
-		var tempDirExists = true;
-		// Temporary (to get it to run the old way for now)
-		tempDirExists = false;
-		/*
-		var readerTmpDir = backslash(system.node_dir + "DDMsgReaderTemp");
-		if (!file_exists(readerTmpDir))
-			tempDirExists = mkdir(readerTmpDir);
-		*/
-		if (tempDirExists)
-		{
-			var wroteTempFile = true;
-			var tmpFileName = readerTmpDir + "tmpMessage.ans";
-			var msgTmpFile = new File(tmpFileName);
-			if (msgTmpFile.open("w"))
-			{
-				wroteTempFile = msgTmpFile.write(pText);
-				msgTmpFile.close();
-			}
-			// If the temp file was written, then convert it to Synchronet
-			// attributes using ans2asc.
-			if (wroteTempFile)
-			{
-				var convertedTempFileName = readerTmpDir + "tmpMessage.asc";
-				var cmdLine = system.exec_dir + "ans2asc \"" + tmpFileName + "\" \""
-				            + convertedTempFileName + "\"";
-				// Note: Both system.exec(cmdLine) and
-				// bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to
-				// execute the command, but system.exec() seems noticeably faster.
-				system.exec(cmdLine);
-				var convertedTmpFile = new File(convertedTempFileName);
-				if (convertedTmpFile.open("r"))
-				{
-					text = convertedTmpFile.read();
-					convertedTmpFile.close();
-				}
-			}
-			deltree(readerTmpDir);
-		}
-		else // Convert ANSI codes to Synchronet attributes & remove unwanted ANSI codes
-			text = cvtANSIToSyncAndRemoveUnwantedANSI(text);
-
-		return text;
-	}
-	else
-		return pText; // No ANSI codes found, so just return the text.
-}
-
-//////////////////////////////////////////////////////////////////////////////
-
-// Returns whether or not some text has any ANSI codes in it.
-//
-// Parameters:
-//  pText: The text to test
-//
-// Return value: Boolean - Whether or not the text has ANSI codes in it
-function textHasANSICodes(pText)
-{
-	
-	return(/\[[0-9]+[mM]/.test(pText) || /\[[0-9]+(;[0-9]+)+[mM]/.test(pText) ||
-	       /\[[0-9]+[aAbBcCdD]/.test(pText) || /\[[0-9]+;[0-9]+[hHfF]/.test(pText) ||
-	       /\[[sSuUkK]/.test(pText) || /\[2[jJ]/.test(pText));
-	/*
-	var regex1 = new RegExp(ascii(27) + "\[[0-9]+[mM]");
-	var regex2 = new RegExp(ascii(27) + "\[[0-9]+(;[0-9]+)+[mM]");
-	var regex3 = new RegExp(ascii(27) + "\[[0-9]+[aAbBcCdD]");
-	var regex4 = new RegExp(ascii(27) + "\[[0-9]+;[0-9]+[hHfF]");
-	var regex5 = new RegExp(ascii(27) + "\[[sSuUkK]");
-	var regex6 = new RegExp(ascii(27) + "\[2[jJ]");
-	return(regex1.test(pText) || regex2.test(pText) || regex3.test(pText) ||
-	       regex4.test(pText) || regex5.test(pText) || regex6.test(pText));
-	*/
-}
-
-// Returns the index of the last ANSI code in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The index of the last ANSI code in the string, or -1 if not found
-function idxOfLastANSICode(pStr, pANSIRegexes)
-{
-	var lastANSIIdx = -1;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var lastANSIIdxTmp = regexLastIndexOf(pStr, pANSIRegexes[i]);
-		if (lastANSIIdxTmp > lastANSIIdx)
-			lastANSIIdx = lastANSIIdxTmp;
-	}
-	return lastANSIIdx;
-}
-
-// Returns the index of the first ANSI code in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The index of the first ANSI code in the string, or -1 if not found
-function idxOfFirstANSICode(pStr, pANSIRegexes)
-{
-	var firstANSIIdx = -1;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var firstANSIIdxTmp = regexFirstIndexOf(pStr, pANSIRegexes[i]);
-		if (firstANSIIdxTmp > firstANSIIdx)
-			firstANSIIdx = firstANSIIdxTmp;
-	}
-	return firstANSIIdx;
-}
-
-// Returns the number of times an ANSI code is matched in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The number of ANSI code matches in the string
-function countANSICodes(pStr, pANSIRegexes)
-{
-	var ANSICount = 0;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var matches = pStr.match(pANSIRegexes[i]);
-		if (matches != null)
-			ANSICount += matches.length;
-	}
-	return ANSICount;
-}
-
-// Removes ANSI codes from a string.
-//
-// Parameters:
-//  pStr: The string to remove ANSI codes from
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: A version of the string without ANSI codes
-function removeANSIFromStr(pStr, pANSIRegexes)
-{
-	if (typeof(pStr) != "string")
-		return "";
-
-	var theStr = pStr;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-		theStr = theStr.replace(pANSIRegexes[i], "");
-	return theStr;
-}
-
-// Returns the last index in a string where a regex is found.
-// From this page:
-// http://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr
-//
-// Parameters:
-//  pStr: The string to search
-//  pRegex: The regular expression to match in the string
-//  pStartPos: Optional - The starting position in the string.  If this is not
-//             passed, then the end of the string will be used.
-//
-// Return value: The last index in the string where the regex is found, or -1 if not found.
-function regexLastIndexOf(pStr, pRegex, pStartPos)
-{
-	pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : ""));
-	if (typeof(pStartPos) == "undefined")
-		pStartPos = pStr.length;
-	else if (pStartPos < 0)
-		pStartPos = 0;
-	var stringToWorkWith = pStr.substring(0, pStartPos + 1);
-	var lastIndexOf = -1;
-	var nextStop = 0;
-	while ((result = pRegex.exec(stringToWorkWith)) != null)
-	{
-		lastIndexOf = result.index;
-		pRegex.lastIndex = ++nextStop;
-	}
-    return lastIndexOf;
-}
-
-// Returns the first index in a string where a regex is found.
-//
-// Parameters:
-//  pStr: The string to search
-//  pRegex: The regular expression to match in the string
-//
-// Return value: The first index in the string where the regex is found, or -1 if not found.
-function regexFirstIndexOf(pStr, pRegex)
-{
-	pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : ""));
-	var indexOfRegex = -1;
-	var nextStop = 0;
-	while ((result = pRegex.exec(pStr)) != null)
-	{
-		indexOfRegex = result.index;
-		pRegex.lastIndex = ++nextStop;
-	}
-    return indexOfRegex;
-}
-
-// Converts ANSI ;-delimited modes (such as alue;...;Valuem) to Synchronet
-// attribute codes
-//
-// Parameters:
-//  pText: The text with ANSI ;-delimited modes to convert
-//
-// Return value: The text with ANSI ;-delimited codes converted to Synchronet attributes
-function ANSIMultiConvertToSyncCodes(pText)
-{
-	var multiMatches = pText.match(/\[[0-9]+(;[0-9]+)+m/g);
-	if (multiMatches == null)
-		return pText;
-	var updatedText = pText;
-	for (var i = 0; i < multiMatches.length; ++i)
-	{
-		// Copy the string, with the [ removed from the beginning and the
-		// trailing 'm' removed
-		var text = multiMatches[i].substr(2);
-		text = text.substr(0, text.length-1);
-		var codes = text.split(";");
-		var syncCodes = "";
-		for (var idx = 0; idx < codes.length; ++idx)
-		{
-			if (codes[idx] == "0") // All attributes off
-				syncCodes += "\1n";
-			else if (codes[idx] == "1") // Bold on (high intensity)
-				syncCodes += "\1h";
-			else if (codes[idx] == "5") // Blink on
-				syncCodes += "\1i";
-			else if (codes[idx] == "30") // Black foreground
-				syncCodes += "\1k";
-			else if (codes[idx] == "31") // Red foreground
-				syncCodes += "\1r";
-			else if (codes[idx] == "32") // Green foreground
-				syncCodes += "\1g";
-			else if (codes[idx] == "33") // Yellow foreground
-				syncCodes += "\1y";
-			else if (codes[idx] == "34") // Blue foreground
-				syncCodes += "\1b";
-			else if (codes[idx] == "35") // Magenta foreground
-				syncCodes += "\1m";
-			else if (codes[idx] == "36") // Cyan foreground
-				syncCodes += "\1c";
-			else if (codes[idx] == "37") // White foreground
-				syncCodes += "\1w";
-			else if (codes[idx] == "40") // Black background
-				syncCodes += "\1" + "0";
-			else if (codes[idx] == "41") // Red background
-				syncCodes += "\1" + "1";
-			else if (codes[idx] == "42") // Green background
-				syncCodes += "\1" + "2";
-			else if (codes[idx] == "43") // Yellow background
-				syncCodes += "\1" + "3";
-			else if (codes[idx] == "44") // Blue background
-				syncCodes += "\1" + "4";
-			else if (codes[idx] == "45") // Magenta background
-				syncCodes += "\1" + "5";
-			else if (codes[idx] == "46") // Cyan background
-				syncCodes += "\1" + "6";
-			else if (codes[idx] == "47") // White background
-				syncCodes += "\1" + "7";
-		}
-		updatedText = updatedText.replace(multiMatches[i], syncCodes);
-	}
-	return updatedText;
-}
-
-// Converts Synchronet attribute codes to ANSI ;-delimited modes (such as alue;...;Valuem)
-//
-// Parameters:
-//  pText: The text with Synchronet codes to convert
-//
-// Return value: The text with Synchronet attributes converted to ANSI ;-delimited codes
-function syncAttrCodesToANSI(pText)
-{
-	// First, see if the text has any Synchronet attribute codes at
-	// all.  We'll be performing a bunch of search & replace commands,
-	// so we don't want to do all that work for nothing.. :)
-	if (hasSyncAttrCodes(pText))
-	{
-		var ANSIESCCodeStart = "[";
-		var newText = pText.replace(/\1n/gi, ANSIESCCodeStart + "0m"); // Normal
-		newText = newText.replace(/\1-/gi, ANSIESCCodeStart + "0m"); // Normal
-		newText = newText.replace(/\1_/gi, ANSIESCCodeStart + "0m"); // Normal
-		newText = newText.replace(/\1h/gi, ANSIESCCodeStart + "1m"); // High intensity/bold
-		newText = newText.replace(/\1i/gi, ANSIESCCodeStart + "5m"); // Blinking on
-		newText = newText.replace(/\1f/gi, ANSIESCCodeStart + "5m"); // Blinking on
-		newText = newText.replace(/\1k/gi, ANSIESCCodeStart + "30m"); // Black foreground
-		newText = newText.replace(/\1r/gi, ANSIESCCodeStart + "31m"); // Red foreground
-		newText = newText.replace(/\1g/gi, ANSIESCCodeStart + "32m"); // Green foreground
-		newText = newText.replace(/\1y/gi, ANSIESCCodeStart + "33m"); // Yellow/brown foreground
-		newText = newText.replace(/\1b/gi, ANSIESCCodeStart + "34m"); // Blue foreground
-		newText = newText.replace(/\1m/gi, ANSIESCCodeStart + "35m"); // Magenta foreground
-		newText = newText.replace(/\1c/gi, ANSIESCCodeStart + "36m"); // Cyan foreground
-		newText = newText.replace(/\1w/gi, ANSIESCCodeStart + "37m"); // White foreground
-		newText = newText.replace(/\1[0]/gi, ANSIESCCodeStart + "40m"); // Black background
-		newText = newText.replace(/\1[1]/gi, ANSIESCCodeStart + "41m"); // Red background
-		newText = newText.replace(/\1[2]/gi, ANSIESCCodeStart + "42m"); // Green background
-		newText = newText.replace(/\1[3]/gi, ANSIESCCodeStart + "43m"); // Yellow/brown background
-		newText = newText.replace(/\1[4]/gi, ANSIESCCodeStart + "44m"); // Blue background
-		newText = newText.replace(/\1[5]/gi, ANSIESCCodeStart + "45m"); // Magenta background
-		newText = newText.replace(/\1[6]/gi, ANSIESCCodeStart + "46m"); // Cyan background
-		newText = newText.replace(/\1[7]/gi, ANSIESCCodeStart + "47m"); // White background
-		return newText;
-	}
-	else
-		return pText; // No Synchronet-style attribute codes found, so just return the text.
-}
-
 // Given some text, this converts ANSI color codes to Synchronet codes and
 // removes unwanted ANSI codes (such as cursor movement codes, etc.).
 //
@@ -17661,54 +16486,52 @@ function curMsgSubBoardIsLast(pGrpIdx, pSubIdx)
 // be a boolean.  Otherwise, the value will be a string.
 //
 // Parameters:
-//  pArgArr: An array of strings containing values in the format -arg=val
+//  argv: An array of strings containing values in the format -arg=val
 //
 // Return value: An object containing the argument values.  The index will be
 //               the argument names, converted to lowercase.  The values will
 //               be either the string argument values or boolean values, depending
 //               on the formats of the arguments passed in.
-function parseArgs(pArgArr)
+function parseArgs(argv)
 {
-	// Set default values for parameters that are just true/false values
-	var argVals = {
-		chooseareafirst: false,
-		personalemail: false,
-		onlynewpersonalemail: false,
-		personalemailsent: false,
-		verboselogging: false,
-		suppresssearchtypetext: false
-	};
+	var argVals = getDefaultArgParseObj();
 
-	// Sanity checking for pArgArr - Make sure it's an array
-	if ((typeof(pArgArr) != "object") || (typeof(pArgArr.length) != "number"))
+	// Sanity checking for argv - Make sure it's an array
+	if ((typeof(argv) != "object") || (typeof(argv.length) != "number"))
 		return argVals;
 
-	// Go through pArgArr looking for strings in the format -arg=val and parse them
+	// First, test the arguments to see if they're in a format as called by
+	// Synchronet for loadable modules
+	argVals = parseLoadableModuleArgs(argv);
+	if (argVals.loadableModule)
+		return argVals;
+
+	// Go through argv looking for strings in the format -arg=val and parse them
 	// into objects in the argVals array.
 	var equalsIdx = 0;
 	var argName = "";
 	var argVal = "";
 	var argValLower = ""; // For case-insensitive "true"/"false" matching
 	var argValIsTrue = false;
-	for (var i = 0; i < pArgArr.length; ++i)
+	for (var i = 0; i < argv.length; ++i)
 	{
 		// We're looking for strings that start with "-", except strings that are
 		// only "-".
-		if ((typeof(pArgArr[i]) != "string") || (pArgArr[i].length == 0) ||
-		    (pArgArr[i].charAt(0) != "-") || (pArgArr[i] == "-"))
+		if ((typeof(argv[i]) != "string") || (argv[i].length == 0) ||
+		    (argv[i].charAt(0) != "-") || (argv[i] == "-"))
 		{
 			continue;
 		}
 
 		// Look for an = and if found, split the string on the =
-		equalsIdx = pArgArr[i].indexOf("=");
+		equalsIdx = argv[i].indexOf("=");
 		// If a = is found, then split on it and add the argument name & value
 		// to the array.  Otherwise (if the = is not found), then treat the
 		// argument as a boolean and set it to true (to enable an option).
 		if (equalsIdx > -1)
 		{
-			argName = pArgArr[i].substring(1, equalsIdx).toLowerCase();
-			argVal = pArgArr[i].substr(equalsIdx+1);
+			argName = argv[i].substring(1, equalsIdx).toLowerCase();
+			argVal = argv[i].substr(equalsIdx+1);
 			argValLower = argVal.toLowerCase();
 			// If the argument value is the word "true" or "false", then add it as a
 			// boolean.  Otherwise, add it as a string.
@@ -17720,7 +16543,7 @@ function parseArgs(pArgArr)
 		}
 		else // An equals sign (=) was not found.  Add as a boolean set to true to enable the option.
 		{
-			argName = pArgArr[i].substr(1).toLowerCase();
+			argName = argv[i].substr(1).toLowerCase();
 			if ((argName == "chooseareafirst") || (argName == "personalemail") ||
 			    (argName == "personalemailsent") || (argName == "allpersonalemail") ||
 			    (argName == "verboselogging") || (argName == "suppresssearchtypetext") ||
@@ -17774,6 +16597,160 @@ function parseArgs(pArgArr)
 
 	return argVals;
 }
+// Helper for parseArgs() - If we get loadable module arguments from Synchronet, this parses them.
+//
+// Parameters:
+//  argv: An array of strings containing values in the format -arg=val
+//
+// Return value: An object containing the argument values.  The property "loadableModule"
+//               in this object will be a boolean that specifies whether or not loadable
+//               module arguments were specified.
+function parseLoadableModuleArgs(argv)
+{
+	var argVals = getDefaultArgParseObj();
+
+	var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits
+	var arg1Lower = argv[0].toLowerCase();
+	// 2 args, and the 1st arg is a sub-board code & the 2nd arg is numeric & is
+	// the value of SCAN_INDEX: List messages in the specified sub-board (List Msgs module)
+	if (argv.length == 2 && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && +(argv[1]) === SCAN_INDEX)
+	{
+		argVals.loadableModule = true;
+		argVals.subboard = arg1Lower;
+		argVals.startmode = "list";
+	}
+	// 2 parameters: Whether or not all subs are being scanned (0 or 1), and the scan mode (numeric)
+	// (Scan Subs module)
+	else if (argv.length == 2 && /^[0-1]$/.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidScanMode(+(argv[1])))
+	{
+		argVals.loadableModule = true;
+		var scanAllSubs = (argv[0] == "1");
+		var scanMode = +(argv[1]);
+		if ((scanMode & SCAN_NEW) == SCAN_NEW)
+		{
+			// Newscan
+			// TODO: SCAN_CONST and SCAN_BACK could be used along with SCAN_NEW
+			// SCAN_CONST: Continuous message scanning
+			// SCAN_BACK: Display most recent message if none new
+			argVals.search = "new_msg_scan";
+			argVals.suppresssearchtypetext = true;
+			if (scanAllSubs)
+				argVals.search = "new_msg_scan_all";
+		}
+		else if (((scanMode & SCAN_TOYOU) == SCAN_TOYOU) || ((scanMode & SCAN_UNREAD) == SCAN_UNREAD))
+		{
+			// Scan for messages posted to you/new messages posted to you
+			argVals.startmode = "read";
+			argVals.search = "to_user_new_scan";
+			argVals.suppresssearchtypetext = true;
+			if (scanAllSubs)
+				argVals.search = "to_user_new_scan_all";
+		}
+		else if ((scanMode & SCAN_FIND) == SCAN_FIND)
+		{
+			argVals.search = "keyword_search";
+			argVals.startmode = "list";
+		}
+		else
+		{
+			// Stock Synchronet functionality.  Includes SCAN_CONST and SCAN_BACK.
+			bbs.scan_subs(scanMode, scanAllSubs);
+			argVals.exitNow = true;
+		}
+	}
+	// Scan Msgs loadable module support:
+	// 1. The sub-board internal code
+	// 2. The scan mode (numeric)
+	// 3. Optional: Search text (if any)
+	else if ((argv.length == 2 || argv.length == 3) && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && isValidScanMode(+(argv[1])))
+	{
+		argVals.loadableModule = true;
+		var scanMode = +(argv[1]);
+		if (scanMode == SCAN_READ)
+		{
+			argVals.subboard = arg1Lower;
+			argVals.startmode = "read";
+			// If a search string is specified (as the 3rd command-line argument),
+			// then use it for a search scan.
+			if (argv.length == 3 && argv[2] != "")
+			{
+				argVals.search = "keyword_search";
+				argVals.searchtext = argv[2];
+			}
+		}
+		else if (scanMode == SCAN_FIND)
+		{
+			argVals.subboard = arg1Lower;
+			argVals.search = "keyword_search";
+			argVals.startmode = "list";
+			if (argv.length == 3 && argv[2] != "")
+				argVals.searchtext = argv[2];
+		}
+		// Some modes that the Digital Distortion Message Reader doesn't handle yet: Use
+		// Synchronet's stock behavior.
+		else
+		{
+			if (argv.length == 3)
+				bbs.scan_msgs(arg1Lower, scanMode, argv[2]);
+			else
+				bbs.scan_msgs(arg1Lower, scanMode);
+			argVals.exitNow = true;
+		}
+	}
+	// Reading personal email: 'Which' mailbox & user number (both numeric) (Read Mail module)
+	else if ((argv.length == 2 || argv.length == 3) && allDigitsRegex.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidUserNum(+(argv[1])))
+	{
+		argVals.loadableModule = true;
+		var whichMailbox = +(argv[0]);
+		var userNum = +(argv[1]);
+		// The optional 3rd argument in this case is mode bits.  See if we should only display
+		// new (unread) personal email.
+		var newMailOnly = false;
+		if (argv.length >= 3)
+		{
+			var modeVal = +(argv[2]);
+			newMailOnly = (((modeVal & SCAN_FIND) == SCAN_FIND) && ((modeVal & LM_UNREAD) == LM_UNREAD));
+		}
+		// Start in list mode
+		argVals.startmode = "list"; // "read"
+		// Note: MAIL_ANY won't be passed to this script.
+		switch (whichMailbox)
+		{
+			case MAIL_YOUR: // Mail sent to you
+				argVals.personalemail = true;
+				argVals.usernum = argv[1];
+				if (newMailOnly)
+					argVals.onlynewpersonalemail = true;
+				break;
+			case MAIL_SENT: // Mail you have sent
+				argVals.personalemailsent = true;
+				argVals.usernum = argv[1];
+				break;
+			case MAIL_ALL:
+				argVals.allpersonalemail = true;
+				break;
+			default:
+				bbs.read_mail(whichMailbox);
+				argVals.exitNow = true;
+				break;
+		}
+	}
+	return argVals;
+}
+// Returns an object with default settings for argument parsing
+function getDefaultArgParseObj()
+{
+	return {
+		chooseareafirst: false,
+		personalemail: false,
+		onlynewpersonalemail: false,
+		personalemailsent: false,
+		verboselogging: false,
+		suppresssearchtypetext: false,
+		loadableModule: false,
+		exitNow: false
+	};
+}
 
 // Returns a string describing all message attributes (main, auxiliary, and net).
 //
@@ -20246,6 +19223,44 @@ function getSubBoardsToScanArray(pScanScopeChar)
 	return subBoardsToScan;
 }
 
+// Returns whether a number is a valid scan mode
+//
+// Parameters:
+//  pNum: A number to test
+//
+// Return value: Boolean - Whether or not the given number is a valid scan mode
+function isValidScanMode(pNum)
+{
+	if (typeof(pNum) !== "number")
+		return false;
+	// The scan modes are defined in sbbsdefs.js
+	var validScanModes = [SCAN_READ, SCAN_CONST, SCAN_NEW, SCAN_BACK, SCAN_TOYOU,
+	                      SCAN_FIND, SCAN_UNREAD, SCAN_MSGSONLY, SCAN_POLLS, SCAN_INDEX];
+	var numIsValidScanMode = false;
+	for (var i = 0; i < validScanModes.length && !numIsValidScanMode; ++i)
+		numIsValidScanMode = (pNum === validScanModes[i]);
+	return numIsValidScanMode;
+}
+
+// Returns whether a user number is valid (only an actual, active user)
+//
+// Parameters:
+//  pUserNum: A user number
+//
+// Return value: Boolean - Whether or not the given user number is valid
+function isValidUserNum(pUserNum)
+{
+	if (typeof(pUserNum) !== "number")
+		return false;
+	if (pUserNum < 1 || pUserNum > system.lastuser)
+		return false;
+
+	var userIsValid = false;
+	var theUser = new User(pUserNum);
+	if (theUser != null && (theUser.settings & USER_DELETED) == 0 && (theUser.settings & USER_INACTIVE) == 0)
+		userIsValid = true;
+	return userIsValid;
+}
 
 // For debugging: Writes some text on the screen at a given location with a given pause.
 //
diff --git a/xtrn/DDMsgReader/ddmr_lm.js b/xtrn/DDMsgReader/ddmr_lm.js
new file mode 100644
index 0000000000000000000000000000000000000000..87732a34b4c2ddb0321ccb674553c0de0cfe3ae7
--- /dev/null
+++ b/xtrn/DDMsgReader/ddmr_lm.js
@@ -0,0 +1,6 @@
+// SYSOPS: Change the msgReaderPath variable if you put Digital Distortion
+// Message Reader in a different path
+var msgReaderPath = "../xtrn/DDMsgReader";
+
+// Run Digital Distortion Message Reader
+bbs.exec("?" + msgReaderPath + "/DDMsgReader.js " + argv.join(" "));
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/loadable_module_scripts/DDReadPersonalMail.js b/xtrn/DDMsgReader/loadable_module_scripts/DDReadPersonalMail.js
deleted file mode 100644
index 6a96de12aea28904280b07cc1e21fafefa02bd79..0000000000000000000000000000000000000000
--- a/xtrn/DDMsgReader/loadable_module_scripts/DDReadPersonalMail.js
+++ /dev/null
@@ -1,76 +0,0 @@
-// $Id: DDReadPersonalMail.js,v 1.5 2020/05/23 23:35:48 nightfox Exp $
-
-// This script is to be executed for the 'Read mail' loadable module, configured
-// in SCFG in System > Loadable Modules.
-
-if (typeof(require) === "function")
-	require("sbbsdefs.js", "SCAN_UNREAD");
-else
-	load("sbbsdefs.js");
-
-console.print("\1n");
-
-// Synchronet will pass 2 command-line arguments:
-// 1. The 'which' mailbox value (numeric) - MAIL_YOUR, MAIL_SENT, or MAIL_ALL.
-//    MAIL_ANY won't be passed to this script.
-// 2. The user number (numeric)
-if (argc < 2)
-{
-	console.print("\1h\1yNot enough arguments were passed to the Read Mail module!  Please inform the");
-	console.crlf();
-	console.print("sysop.\1n");
-	console.crlf();
-	console.pause();
-	exit(1);
-}
-//bbs.read_mail(whichMailbox);
-//exit(0);
-
-
-var whichMailbox = Number(argv[0]);
-var userNum = Number(argv[1]);
-// The 3rd argument is mode bits.  See if we should only display new (unread)
-// personal email.
-var newMailOnly = false;
-if (argv.length >= 3)
-{
-	var modeVal = +(argv[2]);
-	newMailOnly = (((modeVal & SCAN_FIND) == SCAN_FIND) && ((modeVal & LM_UNREAD) == LM_UNREAD));
-}
-
-
-// SYSOPS: Change the msgReaderPath variable if you put Digital Distortion
-// Message Reader in a different path
-var msgReaderPath = "../xtrn/DDMsgReader";
-
-// The readerStartMode variable, below, controls whether the reader
-// is to start in reader mode or message list mode.  Set it to "list"
-// for list mode or "read" for reader mode.
-var readerStartmode = "list";
-//var readerStartmode = "read";
-
-
-// The start of the command string to use with bbs.exec()
-var rdrCmdStrStart = "?" + msgReaderPath + "/DDMsgReader.js ";
-
-// Launch Digital Distortion depending on the value whichMailBox.
-// Note: MAIL_ANY won't be passed to this script.
-switch (whichMailbox)
-{
-	case MAIL_YOUR: // Mail sent to you
-		var cmdArgs = "-personalEmail -userNum=" + userNum;
-		if (newMailOnly)
-			cmdArgs += " -onlyNewPersonalEmail";
-		cmdArgs += " -startMode=" + readerStartmode;
-		bbs.exec(rdrCmdStrStart + cmdArgs);
-		break;
-	case MAIL_SENT: // Mail you have sent
-		bbs.exec(rdrCmdStrStart + "-personalEmailSent -userNum=" + userNum + " -startMode=" + readerStartmode);
-		break;
-	case MAIL_ALL:
-		bbs.exec(rdrCmdStrStart +  "-allPersonalEmail -startMode=" + readerStartmode);
-		break;
-	default:
-		bbs.read_mail(whichMailbox);
-		break;
-}
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/loadable_module_scripts/DDScanMsgs.js b/xtrn/DDMsgReader/loadable_module_scripts/DDScanMsgs.js
deleted file mode 100644
index a8201f4c1795dac1cfb15cbddc14d5137dac6a1e..0000000000000000000000000000000000000000
--- a/xtrn/DDMsgReader/loadable_module_scripts/DDScanMsgs.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// $Id: DDScanMsgs.js,v 1.9 2020/05/23 23:26:55 nightfox Exp $
-
-// This script is to be executed for the 'Scan Msgs' loadable module, configured
-// in SCFG in System > Loadable Modules.
-//
-// This module is used for:
-// - Simply reading a sub-board
-// - Find text in messages
-//
-// Date       Author            Description
-// 2015-05-06 Eric Oulashin     Version 1.0 - Initial release
-// 2015-06-10 Eric Oulashin     Version 1.02
-//                              Bug fix: Switched to bbs.scan_msgs() instead of
-//                              bbs.scan_subs() for all other scan modes besides
-//                              SCAN_READ.
-
-// For stock Synchronet functionality:
-//bbs.scan_msgs([sub-board=current] [,mode=SCAN_READ] [,string find])
-
-load("sbbsdefs.js");
-
-console.print("\1n");
-
-// Synchronet will pass at least 2 command-line arguments and sometimes 3:
-// 1. The sub-board internal code
-// 2. The scan mode (numeric)
-// 3. Optional: Search text (if any)
-if (argc < 2)
-{
-	console.print("\1h\1yNot enough arguments were passed to the Scan Messages module!  Please inform the");
-	console.crlf();
-	console.print("sysop.\1n");
-	console.crlf();
-	console.pause();
-	exit(1);
-}
-
-var subBoardCode = argv[0];
-var scanMode = Number(argv[1]);
-var searchText = argv[2];
-
-
-// SYSOPS: Change the msgReaderPath variable if you put Digital Distortion
-// Message Reader in a different path
-var msgReaderPath = "../xtrn/DDMsgReader";
-
-// The start of the command string to use with bbs.exec()
-var rdrCmdStrStart = "?" + msgReaderPath + "/DDMsgReader.js ";
-
-// No extra mode bits set, only read: Use Digital Distortion Message Reader
-// in read mode
-if (scanMode == SCAN_READ)
-	bbs.exec(rdrCmdStrStart + "-subBoard=" + subBoardCode + " -startMode=read");
-// Some modes that the Digital Distortion Message Reader doesn't handle yet: Use
-// Synchronet's stock behavior.
-else
-	bbs.scan_msgs(subBoardCode, scanMode, searchText);
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/loadable_module_scripts/DDScanSubs.js b/xtrn/DDMsgReader/loadable_module_scripts/DDScanSubs.js
deleted file mode 100644
index 0c36c737a6b0bef70b8b9deabbe08abd602f78f7..0000000000000000000000000000000000000000
--- a/xtrn/DDMsgReader/loadable_module_scripts/DDScanSubs.js
+++ /dev/null
@@ -1,66 +0,0 @@
-// $Id: DDScanSubs.js,v 1.6 2020/05/23 23:26:55 nightfox Exp $
-
-// This script is to be executed for the 'Scan Subs' loadable module, configured
-// in SCFG in System > Loadable Modules.
-//
-// This script is used for:
-// - Continuous new scan
-// - Browse New Scan
-// - New message scan
-// - Scan for messages to you (new messages to you)
-// - Find text in messages
-
-
-load("sbbsdefs.js");
-
-console.print("\1n");
-
-// Synchronet will pass 2 command-line arguments: Whether or not all subs
-// are being scanned, and the scan mode (numeric).
-if (argc < 2)
-{
-	console.print("\1h\1yNot enough arguments were passed to the Scan Subs module!  Please inform the");
-	console.crlf();
-	console.print("sysop.\1n");
-	console.crlf();
-	console.pause();
-	exit(1);
-}
-
-var scanAllSubs = (argv[0] == "1");
-var scanMode = Number(argv[1]);
-
-// SYSOPS: Change the msgReaderPath variable if you put Digital Distortion
-// Message Reader in a different path
-var msgReaderPath = "../xtrn/DDMsgReader";
-
-// The start of the command string to use with bbs.exec()
-var rdrCmdStrStart = "?" + msgReaderPath + "/DDMsgReader.js ";
-
-// Note: SCAN_READ is 0, so the mode bits will always look like they have
-// SCAN_READ.
-
-// For modes the Digital Distortion Message Reader doesn't handle yet, use
-// Synchronet's stock behavior.
-if (((scanMode & SCAN_CONST) == SCAN_CONST) || ((scanMode & SCAN_BACK) == SCAN_BACK))
-	bbs.scan_subs(scanMode, scanAllSubs);
-else if ((scanMode & SCAN_NEW) == SCAN_NEW)
-{
-	// Newscan
-	if (scanAllSubs)
-		bbs.exec(rdrCmdStrStart +"-search=new_msg_scan_all -suppressSearchTypeText");
-	else // Prompt for sub-board, group, or all
-		bbs.exec(rdrCmdStrStart + "-search=new_msg_scan -suppressSearchTypeText");
-}
-else if (((scanMode & SCAN_TOYOU) == SCAN_TOYOU) || ((scanMode & SCAN_UNREAD) == SCAN_UNREAD))
-{
-	// Scan for messages posted to you/new messages posted to you
-	if (scanAllSubs)
-		bbs.exec(rdrCmdStrStart + "-startMode=read -search=to_user_new_scan_all -suppressSearchTypeText");
-	else // Prompt for sub-board, group, or all
-		bbs.exec(rdrCmdStrStart + "-startMode=read -search=to_user_new_scan -suppressSearchTypeText");
-}
-else if ((scanMode & SCAN_FIND) == SCAN_FIND)
-	bbs.exec(rdrCmdStrStart + "-search=keyword_search -startMode=list");
-else // Stock Synchronet functionality
-	bbs.scan_subs(scanMode, scanAllSubs);
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 66f9e8bd334d802162d6222eaa8d96b07165ec20..f202975db9596b9cc82fcdd313105373175693e6 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.46
-                           Release date: 2022-03-07
+                                 Version 1.47
+                           Release date: 2022-03-14
 
                                      by
 
@@ -189,34 +189,39 @@ sbbs/xtrn/DDMsgReader directory.
 
 Loadable Modules setup
 ----------------------
-In Synchronet 3.16 builds starting on April 27, 2015, there are a few new
-options in the Loadable Lodules configuration in SCFG: "Read Mail", "Scan
-Msgs", and "Scan Subs".  Functionality for these was updated on May 5, 2015.
-These loadable modules options can enable the use of a message reader script
-for the various message reading, searching, and scanning options provided by
-Synchronet.  That is probably the easiest way to install the reader, since it
-only requires specifying a script in the Loadable Modules options and does not
-require modification of your command shell.  Setting it up that way also has
-the advantage that the reader will be used for reading personal email and
-performing a message newscan during the login process.  If you have a recent
-build of Synchronet 3.16 (or newer), then your version of Synchronet will
-support this.  If you are using an older version of Synchronet, skip ahead to
-the "Command shell setup" subsection.
-
-To set up the reader with the Loadable Module scripts, do the following:
-1. Copy DDReadPersonalMail.js, DDScanMsgs.js, and DDScanSubs.js from the
-   loadable_module_scripts directory to your sbbs/mods directory
-2. If you will be running the script from a directory other than
-   xtrn/DDMsgReader, edit the above scripts and search for the text "SYSOPS:"
-   (without the double-quotes).  One or two lines below that, there is a
-   variable called msgReaderPath - Change that so that it contains the path
-   where you copied DDMsgReader.js.
-3. Run Synchronet's configuration program (scfg from the command prompt, or
-   from the GUI, select BBS > Configure).  In there, select System, then
-   Loadable Modules
-4. For the 'Read Mail' option, put in DDReadPersonalMail
-5. For the 'Scan Msgs' option, put in DDScanMsgs
-6. For the 'Scan Subs' option, put in DDScanSubs
+The easiest way to get Digital Distortion Message Reader set up is via the
+Loadable Module options in SCFG > System > LOadable Modules.
+
+The Loadable Modules options let you specify scripts to run for various events
+in Synchronet.  As of Synchronet 3.19, the following Loadable Modules options
+are available in SCFG for message reading/scanning events:
+- Read Mail (added in Synchronet 3.16)
+- Scan Msgs (added in Synchronet 3.16)
+- Scan Subs (added in Synchronet 3.16)
+- List Msgs (added in Synchronet 3.18)
+
+The Loadable Modules options take the filename of the script without the
+extension.  The Loadable Modules options don't allow a leading path in front of
+the name, so if you have DDMsgReader.js in a path other than sbbs/exec or
+sbbs/mods, one solution is to copy the included ddmr_lm.js to either your
+sbbs/exec or sbbs/mods directory (ideally sbbs/mods so it wouldn't get
+accidentally deleted) and specify ddmr_lm in your Loadable Modules as follows:
+
+ Read Mail       ddmr_lm
+ Scan Msgs       ddmr_lm
+ Scan Subs       ddmr_lm
+ List Msgs       ddmr_lm
+
+Also, if you will be running the script from a directory other than
+xtrn/DDMsgReader, edit ddmr_lm.js and look for the text "SYSOPS:" (without the
+double-quotes).  One or two lines below that, there is a variable called
+msgReaderPath - Change that so that it contains the path where you copied
+DDMsgReader.js.
+
+Alternately, you can copy DDMsgReader.js to your sbbs/exec or sbbs/mods
+directory and specify DDMsgReader in your Loadable Modules for the above
+modules in SCFG.  For that to work, you would also need to copy DDMsgReader.cfg
+to your sbbs/ctrl directory or to sbbs/mods along with DDMsgReader.js.
 
 There are a few search modes that Synchronet provides that Digital Distortion
 Message Reader doesn't support yet (such as continuous newscan and browse new
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index d27dce649fa451659888a7fb1e43da0715f2b34d..6ab65d86fe30fb1c2c7624b464da97a0c7f41854 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,10 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.47     2022-03-14   DDMsgReader can now be called directly as a loadable
+                      module by Synchronet (though requires the included
+                      ddmr_lm.js if DDMsgReader.js is not in sbbs/exec or
+                      sbbs/mods)
 1.46     2022-03-07   Fix: When changing to an empty sub-board from within the
                       reader (either from read mode or list mode), it now
                       properly says there are no messages and exits, rather than