doomcat / Kongregate Legacy of a Thousand Suns Raid Link Helper

// ==UserScript==
// @name           Kongregate Legacy of a Thousand Suns Raid Link Helper
// @namespace      tag://kongregate
// @description    Improves the text of raid links and stuff
// @author         doomcat
// @version        1.1.36
// @date           06.03.2015
// @grant          GM_xmlhttpRequest
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_deleteValue
// @include        http://www.kongregate.com/games/*/*
// @include        *50.18.190.248/kong/*
// ==/UserScript==

console.log("doomscript injection context", window);
// Even though we include all of Kongregate, we use a Regex to prevent the script from being injected on the wrong pages
if (!/https?:\/\/www\.kongregate\.com\/games\/5thPlanetGames\/legacy-of-a-thousand-suns.*/i.test(window.location.href) && window.location.host !== '50.18.190.248') throw "";

/*
 License: "Kongregate Legacy of a Thousand Suns Raid Link Helper for Chat" (henceforth known as "doomscript") is free to download and use unlimited times on unlimited devices. You're allowed to modify the script for personal use, but you need written permission from doomcat to distribute those modifications. If you plan to distribute doomscript in whole or in part, modified or not, as part of an application other than in userscript form, some fees may apply. Contact doomcat for pricing.

 Warranty: This userscript comes with no assurance or guarantee of functionality, suitability, or other promise of working as you intend. doomcsript is provided as-is.
 */


/**********************************************\
 |** !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! **|
 |** !!!!!!!!!! NOTE TO DEVELOPERS !!!!!!!!!! **|
 |** !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! **|
 |** !!!!!! If you fork this script,   !!!!!! **|
 |** !!!!!! please change raidStorage  !!!!!! **|
 |** !!!!!! in  DC_LoaTS_Properties    !!!!!! **|
 |** !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! **|
 \**********************************************/


/**
 Change Log:

 2012.02.01 - 1.0.0
 Initial Version

 2012.02.03 - 1.0.1
 Quick add of a single missing id

 2012.02.06 - 1.0.2
 Code Cleanup
 Added a bunch of comments for non-code people to convince themselves it isn't a virus
 As far as I've heard, it works as designed in Chrome and FF on Mac and Win
 Added Additional Alliance raid ids.

 2012.02.06 - 1.0.3
 All Public raid ids are in as far as I know, including the new Vince Vortex. Still missing some alliance ids.
 Should now work in Opera.

 2012.02.07 - 1.0.4
 Added in FairShare calculation for raid

 2012.02.09 - 1.0.5
 Added in /raid command

 2012.02.14 - 1.0.6
 Switched /raid command to report FS*2 instead of *3. Added additional alliance raid ids.

 2012.02.16 - 1.0.7
 Merged in code from SReject's branch http://userscripts.org/scripts/review/125847
 SReject's code adds the ability to click raid links and have them load in the same window, no refresh
 Based on SReject's code, added /loadraid command
 Moderately sized internal code refactors

 2012.02.21 - 1.0.8
 Added in /raidformat command
 Added in /reload command
 Fixed some trouble with /loadraid
 All commands can now do /command help to learn more about the command
 Minor refactors to improve code versatility

 2012.02.22 - 1.0.9
 Mistaken name on Mercury Raid
 Added debugging for bizarre error that can happen only occasionally.
 Now remembers which raids were posted
 Target damage is now possible to add to raid links as {target}
 Added /seenraids command
 Added /clearraids command
 Added /raidformat reset
 Replaced FS*2 with Target Damage in /raid desriptions

 2012.02.22 - 1.0.10
 Fixed bug that links from /seenraids refreshed the whole page
 Added command /raidhelp
 Added /w RaidBot help and /w RaidBot command
 Added ability to do regex in /seenraids name
 /seenraids will put visited raids to the top of the list

 2012.02.27 - 1.0.11
 Lots of formatting tweaks in help texts
 Commented RaidManager code
 Added /clearraids name difficulty {state: stateName}
 Added /seenraids name difficulty {state: stateName}
 Added update button to /raidhelp aka /w RaidBot help
 Automatically checks for new versions. Will show in banner at top.
 Finally compiled all known raid ids into script

 2012.02.29 - 1.0.12
 Fixed collision of hashes - apparently they aren't unique
 Fixed typo in /seenraids and /clearraids where all raids were being shown incorrectly
 Improved automatic update to popdown include notification bar
 Added /autoupdate command
 Added /loadraids as alias to /loadraid
 Now links you load will update all throughout chat

 2012.03.03 - 1.0.13
 Fixed slow down of raids as posted
 Improved memory usage in long term seen storage
 Improved internal processing of raids
 Added simple additional filters {age}, {count}, {page}

 2012.03.08 - 1.0.14
 Fixed a couple of minor reported bugs
 Significantly improved speed of /seenraids
 When changing the raid format, chat will update all other links in the chat, too
 Removed backward compatibilty of old hash indexed raids since those should have all expired by now

 2012.04.23 - 1.1.0
 -- First Alpha Release: 2012-04-23
 First parts of the omnibox UI added
 First parts of the raid menu UI added
 Added {diff} as a raidformat to get N, H, L, and NM instead of full text difficulties
 Added {health}, {time}, {optimal} (alias of target), {ofs} (alias of target), {short-name},
 {shorter-name}, {zone}, {cache-state-nice}, and {cache-state-short} to Raid Format options
 Completely reworked many parts of the internal structure of the code to make alterations by others easier
 Locked script to just the Kongregate game page. Script no longer activates on other games.
 Added Zone, Time Limit, Official Short Name, and Unofficial Short name to raids.
 Corrected some minor errors in raid information, mainly alliance and world raid.
 Attempted to better comment internal code for future extension by others
 Raid links are now aged off at each raids length instead of blanket 200 hours.
 Fixed health and FS for non-standard health pattern raids (currently Wahsh and Pox)
 Added "/linkstate url state" command to force change a link's state, as in /linkstate http://www.kongregate.com/games/5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_raid_boss=telemachus&kv_difficulty=4&kv_raid_id=2769083&kv_hash=9Bo4uUiWIM visited to set that tele to visited
 Added "/wiki searchTerm" to open a page to the wiki like /wiki Snuuth Obliterator
 -- Second Alpha Release: 2012-04-26
 Shift-clicking a link now cycles through all states for that link
 Migrated remaining commands over to new command format
 /command help should now contain clickable links for examples
 /clearraids without parameters will no longer clear all raids. Use /clearraids all
 Added a template command in the new command style in order for others to create custom /commands
 Moved {visited} format to use standard text
 Added {visited-short} format
 Slight rework and improvement to custom command creation
 Corrected bug where visited state was getting overwritten by unseen
 Omnibox should now appropriately respond (at least basically) to all commands
 Pox optimum damage set to 20 epics
 Fixed missing aliases in help text
 Added first version of /farmvalue command. Needs work to be solid.
 -- Third Alpha Release: 2012-05-22
 Pox FS and Target were still broken. They should really be fixed for real now.
 Added simple /time command to display server time. Works nicely in the omnibox.
 Added a catch for just incase you /raidbot instead of /w raidbot. They both work the same way.
 Added an attempt to recover from corrupted raid link storage. Quarantines old storage for examination and clears current bad storage
 Made RaidMenu movable
 Added Right-click visited option to preferences menu
 Added /farmvalue command for simple hard-coded dump of info from spreadsheet
 Added very simple implementation of live search in Raids tab
 Fixed wiki commands making double entries in the omnibox autocomplete
 -- Fourth Alpha Release: 2012-06-13
 Altered RaidLink constructor parameter order - now is id, hash, diff, boss. No longer need all 4, just the first 2
 RaidMenu tabs kind of respect tabPosition now, depending on browser. Will implement real solution later.
 In some browsers, the script appeared to load a number of times only to fail. Most of these should no longer run.
 Added first version of raid format into the raid menu
 Added a simple /update command for those that get confused between scripts. Will eventually like to have the command do more.
 Files are now in an Assembla SVN repo: http://subversion.assembla.com/svn/doomscript/
 Now using Trac for bug tracking: http://trac.assembla.com/doomscript
 Todos all moved to ticketing system.
 Added Z10 raids, first pasee
 -- Fifth Alpha Release: 2012-06-25
 Removed kv_action_type=raidhelp from the required parameters of the link due to changes in SReject's spammer
 Added game specific icons in place of generic LoTS icon
 Can now /raid raidName 0 to get base info about a raid that doesn't change with health
 Fixed bug with command aliases being case sensitive. /SEENRAID colo should now work.
 Added /clearchat command
 Added /raidstyle command
 Kong added some padding to their text boxes that has now been removed from the Omnibox

 2012.08.01 - 1.1.0 Stable
 Too much to even list. See above and tickets.


 2012.08.29 - 1.1.1
 Updated Skorzeny and Temple info
 Updated to add Gut-Phager raid
 Updated autoload timer

 2012.09.04 - 1.1.2
 Improved AutoLoad to have more descriptive messages
 Added Load Raids In Background

 2012.09.05 - 1.1.3
 Commented out Load Raids In Background due to ToS concerns
 Added new Hound alliance raid

 2012.09.12 - 1.1.4
 Added new G. Rahn raid

 2012.09.18 - 1.1.5
 Added /loadpastebin command
 Fixed weird height layout issue with game and chat area

 2012.09.21 - 1.1.6
 Added Cerebral Destroyer WR

 2012.10.02 - 1.1.7
 Fixed export function in Chrome
 Updated hotlinked image location
 Updated Python Data

 2012.10.18 - 1.1.8
 Added /markall filter state command
 Altered /autoload timer in some cases
 Added /linktools command to list tools links
 Added /pasteraids command
 Added blob raid
 Fixed export function in Chrome, again

 2012.11.02 - 1.1.9
 Fixed bug with exportraids killing the omnibox
 Added two new raids, Nosferatu Nick and Haunted House

 2012.11.02 - 1.1.10
 Added 3 new Zone A raids, Boar, Cake, and Guan Yu

 2012.11.15 - 1.1.11
 Added 3 new Zone A raids, Missile Strike, Bashan, and Your Wrath

 2012.11.16 - 1.1.12
 Fixed bug where Cerebral Destroyed had gotten deleted
 Altered the way export raids works until a longer term solution can be provided

 2012.11.16 - 1.1.13
 Added 3 new Zone A2 raids, Cyborg Shark, Vlarg Relic Hunter, and Anthropist Xenocide Warship
 Fixed unknown links to not say Undefined Undefined over and over

 2012.11.28 - 1.1.14
 Added 3 new Zone A2 raids. Bile Beast, Pi, and Lubu
 Added aliases of /loadpastebin as /loadraidbin and /lrb
 Added aliases of /exportraids as /exportraid and /er
 Added /updateraiddata or /updatedata or /urd command to pull down new raid data as it's available without updating script
 Visted and Completed raids won't be matched by a filter unless {state: visited} or {state: completed} are specifically used
 Accepted a patch from sycdan doing the following:
 - Formatting: Added {state} and {status} as aliases to {cache-state-nice}
 - Formatting: Added {state-short} and {status-short} as aliases to {cache-state-short}
 - Raids Tab: Links should now get their state updated to match what they do in chat
 - Formatting Tab: Sample raid (when all raids are cleared) will now always have FS/OS
 - Chat Commands: /clearchat now aliased with /cc and /cls
 - Chat Commands: /raid now aliased with some typo checks
 - Chat Commands: /loadraid now aliased with /lr
 - Chat Commands: /seenraids now aliased with /sr

 2012.11.30 - 1.1.15
 Added new World Raid: Kraken
 Added new World Raid Tab, Timer, and Loot Table
 Fixed visited links not showing up ever in /exportraids
 Fixed update raid data being annoying

 2012.12.11 - 1.1.16
 Removed Kraken World Raid info
 Added Snowman Rare Spawn info
 Altered some WR display code
 Performance tuned some raid loading code
 Added link formatting for Alliance invites
 Added new Alliance Raid: Crazed Santa

 2012.12.14 - 1.1.17
 Added two new Alliance Raids: SANTA's Workshop and Rabid Reindeer
 Updated Snowman rare spawn info, due to new snowman
 Added two new Zone 15 Raids: Tentacled Turkey and Hulking Mutant
 Added new WR: Christmas Campaign
 Added snull preference to snull the snulls in the snull
 Added ignore visited raids preference
 Added ignore invalid commands preference
 Added additional filtering capability to string multiple filters together using ||, like colo|tele 1 || rage|void 4 would give normal tele, normal colossa and colonel, nightmare ragebeasts, nightmare void
 Fixed bug loading WR data during update
 Reworked how pastebin and autoload work by making them use the same code
 With help from Sycdan, added /loadcconoly
 Added some help to keep the spammer up to date with known raid data

 2013.02.10 - 1.1.18
 Fixed issue with /clearraids all not working
 Improved help text of a few commands
 Corrected and updated /farmvalue a bit
 Added marked dead functionality for CConoly
 Added os to filters [Sycdan]
 Fixed WR Space Pox Icon
 Kind of made the broken close icon for the menu suck less, though not totally fixed
 Added Zone 16 raids: Screaming Barracuda and Symphony of Two Worlds
 Added Zone 17 raids: Al-Husam
 Added two Rare Spawns: Cerebral CEO and Space Pox Mary
 WR Info page's forum link should now open in a new window/tab
 Corrected hard health numbers on a bunch of raids from Z10 on

 2013.02.14 - 1.1.19
 Altered /lcc <filter> so it runs /loadall <filter> after fetching raids, rather than just filterting the list of newly-fetched raids [sycdan]
 Added preference for delay between loading raids [sycdan]
 Hid doomscript tabs that were previously labeled under construction.

 2013.02.24 - 1.1.20
 Fixed a bug that was causing raids to be marked as completed when they were actually being re-joined
 Minor code cleanup
 Added more timing data to find slow downs
 Added aliases to /linktools: /advertise, /blatantselfpromotion, /getdoomscript
 Added aliases to /reload: /reloaf, /reloa, /eload
 Moved /loadall to ChatCommands folder from Experimental. It's a main feature now, not just an experimental hack.
 Consequently, /loadall will now appear in the correct alphabetical order in the help list
 Added /refreshlinks command to cause the links to redraw themselves. This is mainly for when a link refuses to mark visited
 All links will now be refreshed after /loadall and /clearraids [sycdan]
 Fixed a bug in /clearraids all that was causing /seenraids to still show raids [sycdan]
 Cleaned up some CConoly communication code [doomcat/sycdan]
 /clearraids ALL was not being accepted. It's now case-insensitive

 2013.03.21 - 1.1.21
 Fixed missing images on toolbar
 Added /rss command
 Moved /checkload out of experimental
 Added noir raid
 Added zone filter
 Invalid Raid Id fix [Solsund]

 2013.04.04 - 1.1.22
 Fixed bug in zone filter not working for /raidstyle
 Added size filter
 Fixed bug with dynamic loading of Kong page (chat_window div)
 Fixed bug where /raidstyle and /markall were not respecting OS filters
 Added /forum command [anonimmm]

 2013.05.26 - 1.1.23
 Fixed critical issues where script is totally broken in Opera.
 Added Penelope Wellerd RS
 Added M & S alliance raid
 Added Zone 19 Raids: Bethany, Vunlac, R. Dule, Master Hao, and Noir (II)
 Tweaked chat icons
 Attempted critical Chrome 27 fix

 2013.06.18 - 1.1.24
 Added H8 RS
 Fixed Critical bug from Kong Change

 2013.12.18 - 1.1.25
 Added Inventor RS & WR
 Added Sweet and Jalfreezi alliance raids
 Changed defaults
 Fixed Critical bug in Latest Firefox [greenkabbage]

 2013.12.31 - 1.1.26
 Added CMM RS
 Fixed default prefs not working
 Added new pref to hide world chat

 2014.02.21 - 1.1.27
 Added Mega Mimes, Neon Knights, and Gamma Hammers Alliance raids
 Fixed raid icons

 2014.03.08 - 1.1.28
 Added Chem-Runners Alliance raid
 Added two KX raids, Battle Station and Subjugator
 Attempt to handle WC's bad link copying bug

 2014.05.25 - 1.1.29
 Added Zone 20 Raid: Weiqi Game
 Added KX Shock Trooper and KX Tank Alliance raids
 Added KX Scout Ships and KX Bombarder RS
 Added Cow Abduction WR
 Added KX Elite Sub
 Fixed raid size for 250 man to 100 man alliance raids
 Added Zone 21 Raids: Sian Dragonfly and Lady Victoria Ashdown
 Corrected values for Zone 5 raids
 Updated the dynamic raid update feature to accept updates to existing raids

 2014.05.?? - 1.1.30
 Updated Ashdown and Dragonfly OS

 2014.08.19 - 1.1.31
 Fixed background loading for raids [greenkabbage]
 Comply with new GreaseMonkey requirements [greenkabbage]
 Fixed Chrome App manifest problem
 Changed update url

 2014.08.25 - 1.1.32
 Fix critical issue with latest Firefox/GM changes around events
 Added Pinata RS and Pinata's Revenge raid data [greenkabbage]
 Minor fix to debugMode
 Added a bunch more logging statements in debug mode

 2014.08.27 - 1.1.33
 Fix XHR for Firefox 32+

 2014.10.?? - 1.1.34
 Added Trouble in Tokyo WR
 Added two new raids, King Krandar and Sultan Shrakzan
 Added /ad alias to /linktools post
 Added Left Click to Whisper preference and functionality
 Added Right Click on Username menu

 2015.02.26 - 1.1.35
 Major Firefox/Greasemonkey fix [greenkabbage]
 Added 31 new raids (ops, zone 22, anniversary), and some updated OS values
 Added new {progress} filter to /lrm -- This still seems a bit buggy
 Included /lrm in the code directly (rather than external inclusion)
 Added url linkification
 Added #wiki linkification
 Updated filtering for noirs: noir i finds Noir only, noir ii finds Noir (II) only, noir iii finds Noir (III) only
 kv_raid_boss is now part of the searchable raid text
 Added new /news command

 2015.08.?? - 1.1.36
 Added 2 new facility raids, Parasite and Exhibit
 Added 2 RS Kleptotherms and Star Turtle
 Added new Alliance Raid Eviscipod
 Fixed UgUp profile loading issue for users who've opted out (Won't spin forever any more)
 Updated filtering for subjugators: sub finds both, nsub finds regular subs, esub finds elites
 Slight tweak to command links
 Tweak to /rss to make it search for latest posts rather than latest threads
 Added /suggest command for submitting suggestions
 Added /mutelist command for showing all muted users
 Moved to using external news site
 Migrated to GitHub script hosting
 Separated Game refresh from World Chat refresh, added World Chat reload button above the chat pane
 Changed Hide World Chat to unload the chat flash object
 Fixed missing raid icons for raids added since 1.1.35
 TODO: Move to using external datasource for raids
 TODO: Use PegJS generated raid parsing filter
 TODO: BUG: Links sometime seem to include an extra space at the end
 TODO: Better /lrm docs
 TODO: Hide previous text from people upon mute
 TODO: /wr command
 TODO: WR/RS timer/damage display
 TODO: WR/RS background image (make sure to hide during power off)

 [IDEA] Text coloring based on who's speaking (mods/admins/friends)
 [IDEA] Highlighting mentions of the user's username, and maybe some keywords
 [TODO] Integrate log analyzer
 [TODO] WR/RS notification
 [TODO] Delete 1.2.0 branch
 [TODO] Integrate GCM/Firefox Push notifications
 [TODO] Post new Opera instructions
 [TODO] Fix missing images on menu
 */

// Wrapper function for the whole thing. This gets extracted into the HTML of the page.
function main()
{
	// Properties for this script
	window.DC_LoaTS_Properties = {
		// Script info

		version: "1.1.36",

		authorURL: "http://www.kongregate.com/accounts/doomcat",
		updateURL: "http://www.kongregate.com/accounts/doomcat.chat",
		scriptURL: "http://bit.ly/doomscript",
		scriptDownloadURL: "https://openuserjs.org/install/doomcat/Kongregate_Legacy_of_a_Thousand_Suns_Raid_Link_Helper.user.js",
		raidDataURL: "http://getKongE.org/old/RaidData.js",
		worldRaidDataURL: "http://getKongE.org/old/WorldRaidData.js",
		docsURL: "http://www.tinyurl.com/doomscript-docs",
		chatzyURL: "http://us5.chatzy.com/46964896557502",
		newsURL: "https://docs.google.com/document/d/1d-r8ZJXPSL8gIY8XviJKM3fzmAUcSOTGlScVdmh0J5A/pub",

		joinRaidURL: "http://web1.legacyofathousandsuns.com/kong/raidjoin.php",
		kongLoaTSURL: "http://web1.legacyofathousandsuns.com/kong/raidjoin.php",
		lotsCDNUrl: "http://5thplanetlots.insnw.net/lots_live/",

		// Other URLS
		RaidToolsURL: "http://userscripts.org:8080/132671",
		QuickFriendURL: "http://userscripts.org:8080/125666",
		PlayNowFixURL: "http://userscripts.org:8080/142619",
		FleetCodesURL: "https://sites.google.com/site/lotsfleetcodes",
		farmSpreadsheetURL: "https://docs.google.com/spreadsheet/ccc?key=0AoPyAHGDsRjhdGYzalZZdTBpYk1DS1M3TjVvYWRwcGc&hl=en_US#gid=4",

		// Do not check code in with this set to true.
		// Preferably, turn it on from the browser command line with DC_LoaTS_Properties.debugMode = true;
		// Or add ?debugMode=true to the game url in the browser
		debugMode: (function() {
			var value = /debugMode=(\w+)/.exec(document.location.href);
			return value && !!value[1];
		})(),

		// GreaseMonkey Storage Keys
		storage: {
			// Auto Update
			autoUpdate: "DC_LoaTS_autoUpdate",

			// Format of messages in chat
			messageFormat: "DC_LoaTS_messageFormat",

			// Format of links in chat
			linkFormat: "DC_LoaTS_linkFormat",

			// Format of links in chat
			customLinkFormatBool: "DC_LoaTS_customLinkFormatBool",

			// Overall container for raid link storage
			raidStorage: "RaidManager_doomcat_v1",

			// RaidType Specific preferences
			raidPrefs: "DC_LoaTS_raidPreferences",

			// General script behaviour preferences
			behaviorPrefs: "DC_LoaTS_behaviorPreferences",

			// Quarantine addendum
			quarantine: "_quarantine",

			// Timestamp of last query to cconoly
			cconolyLastQueryTime: "DC_LoaTS_cconolyLastQueryTime"
		}
	};


	// Class declaring function for Opera compatibility
	function declareClasses()
	{
/************************************/
/****** DC_LoaTS_Helper Class *******/
/************************************/

		// Manager and runner class for this whole thing
		// This is a PrototypeJS class. Kongregate uses the Prototype libraries, so we don't
		// have to link them ourselves in this script
	    window.DC_LoaTS_Helper = Class.create({

	    	// Constructor
			initialize: function() {

				// Initialize the link storage
				RaidManager.init();

				// Whether or not to auto update
				var autoUpdate = GM_getValue(DC_LoaTS_Properties.storage.autoUpdate);

				// If we don't have a set value for auto update
				if (typeof autoUpdate == "undefined")
				{
					// Default to true
					autoUpdate = true;
					GM_getValue(DC_LoaTS_Properties.storage.autoUpdate, true);
				}

				// If we're auto update checking
				if (autoUpdate)
				{
					// Check for updates
					DC_LoaTS_Helper.checkForUpdates();
				}

				// Get the raid link for the current page
				var currentPageLink = new RaidLink(window.location.href);

				// Check to see if this is a raid link
				if (currentPageLink.isValid())
				{
					// Store this page as visited
					RaidManager.store(currentPageLink, RaidManager.STATE.VISITED);
				}

				// Show the raid toolbar
				RaidToolbar.show();

		// Hide the game (or not) -- but delay execution until gameiframe exists ~5 seconds
		window.setTimeout(function(){DC_LoaTS_Helper.handleHideWorldChat(DC_LoaTS_Helper.getPref("HideWorldChat", false));}, 5000);

		// Move the chat timestamps (or not)
		DC_LoaTS_Helper.handleMoveChatTimestamps(DC_LoaTS_Helper.getPref("ChatTimestampRight", false));


		// ChatDialogue is the Kongregate ChatDialogue class that is part of the Kongregate Holodeck
		// See: http://www.kongregate.com/javascripts/holodeck/chat_dialogue.js for readable source
		// We're going to take the normal function that displays a chat message and move it so that
		// we can intercept chat messages and reformat them.
		ChatDialogue.prototype.DC_LoaTS_displayUnsanitizedMessage = ChatDialogue.prototype.displayUnsanitizedMessage;

		// Define the NEW function that will display chat messages (we call the old function at the end
		// this is just a reformatter for the better LoaTS links)
		// params:
		// user - user name of the user who sent the message
		// message - message text
		// attributes - an object that usually is undefined, but somtimes contains {class: "CSSclassname"} among others
		// options - Mostly for use with private messages
		ChatDialogue.prototype.displayUnsanitizedMessage = function(user, msg, attributes, options)
		{
			Timer.start("Process Message");
			// Be careful not to reprocess messages that we ourselves sent
			if (user.toLowerCase() != "raidbot")
			{
				// Just in case we need it
				var originalMsg = msg,
					match;

				// Try to create a RaidLink from this message
				var raidLink = new RaidLink(msg);

				// Alliance Invite Link
				var allianceInvitePattern = /(?:https?:\/\/)?(?:www\.)?kongregate\.com\/games\/5thPlanetGames\/legacy-of-a-thousand-suns\?kv_action_type=guildinvite&(?:amp;)?kv_fbuid=kong_([^<"']+)/i;
				var allianceInviteFormat = "<a href='{0}'>Join {1}'s alliance? (Opens in this window)</a>";

				// Regular external links, borrowed from: http://stackoverflow.com/a/8943487/1449525
//                        var urlPattern = /((?!=)\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
				// Regular external links, borrowed from: http://jmrware.com/articles/2010/linkifyurl/linkify.html
				//var urlPattern = /(\()((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\))|(\[)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\])|(\{)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\})|(<|&(?:lt|#60|#x3c);)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(>|&(?:gt|#62|#x3e);)|((?:^|[^=\s'"\]])\s*['"]?|[^=\s]\s+)(\b(?:ht|f)tps?:\/\/[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]+(?:(?!&(?:gt|#0*62|#x0*3e);|&(?:amp|apos|quot|#0*3[49]|#x0*2[27]);[.!&',:?;]?(?:[^a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]|$))&[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]*)*[a-z0-9\-_~$()*+=\/#[\]@%])/img;
				var urlPattern = /(\()((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\))|(\[)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\])|(\{)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\})|(<|&(?:lt|#60|#x3c);)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(>|&(?:gt|#62|#x3e);)|(\b(?:ht|f)tps?:\/\/[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]+(?:(?!&(?:gt|#0*62|#x0*3e);|&(?:amp|apos|quot|#0*3[49]|#x0*2[27]);[.!&',:?;]?(?:[^a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]|$))&[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]*)*[a-z0-9\-_~$()*+=\/#[\]@%])/img;
				var urlFormat = "<a href='{0}' target='_blank'>{0}</a>";

				// Wiki link generation
				var hashWikiPattern = /#wiki (?:(?!(?:ht|f)tp|<|:\/\/).)*/img;


                        // Make sure we haven't already put a raid link in here and the link we found was valid
						if (msg.indexOf("<span class=\"raidMessage\"") == -1 && raidLink.isValid())
						{
							// Retrieve the message format
							var messageFormat = DC_LoaTS_Helper.getMessageFormat();

							// Retrieve the anchor tag format
							var linkFormat = DC_LoaTS_Helper.getLinkFormat();

							// Mark the link visited if the current user posted
							if (user == holodeck._active_user._attributes._object.username)
							{
								// Store this link as visited
								RaidManager.store(raidLink, RaidManager.STATE.VISITED);
							}
							else
							{
								// Store this link as-is and let raid manager decide its state
								RaidManager.store(raidLink);
							}

							// Get the new message after formatting is applied
							var newMessage = raidLink.getFormattedRaidLink(messageFormat, linkFormat).trim();

							// We don't want to totally blow away the message, though, because people do write text in there some times
							msg = msg.replace(/<a(?:(?!<a class="reply_link).)*<\/a>/i, newMessage);

							// This means our replace didn't catch it, must be IE link
							if (msg == originalMsg)
							{
								//TODO: Missed this case
								//http://cdn2.kongregate.com/game_icons/0033/2679/i.gif?4217-op","5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&amp;kv_difficulty=4&amp;kv_hash=Bil9M7W0s5&amp;kv_raid_boss=centurian_sentinel&amp;kv_raid_id=2969908","Legacy\u00a0of\u00a0a\u00a0T……
								//http://cdn2.kongregate.com/game_icons/0033/2679/i.gif?4217-op","5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_difficulty=4&kv_hash=Nw3p60d02T&kv_raid_boss=kalaxian_cult_mistress&kv_raid_id=3293614

								//s["http://cdn2.kongregate.com/game_icons/0033/2679/i.gif?4217-op","5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_difficulty=4&kv_hash=4cc5r5FTXh&kv_raid_boss=kalaxian_cult_mistress&kv_raid_id=3315012","Legacy of a Thousand Sun…

								//s["http://cdn2.kongregate.com/game_icons/0033/2679/i.gif?4217-op","5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_difficulty=4&kv_hash=VRoC7Po8CD&kv_raid_boss=kalaxian_cult_mistress&kv_raid_id=3324329","Legacy of a Thousand Sun…


								msg = msg.replace(RaidLink.backupLinkReplacementPattern, newMessage);
							}

							// Make sure attributes exists
							if (typeof attributes === "undefined")
							{
								attributes = {};
							}

							// Make sure attributes.class exists
							if (typeof attributes["class"] === "undefined")
							{
								attributes["class"] = "";
							}

							// Get the className of the link
							var className = raidLink.getMatchedStyles().className;
							if (typeof className !== "undefined")
							{
								attributes["class"] += className;
							}


							// If still didn't get it, note the problem
							if (msg == originalMsg)
							{
								console.warn("Failed to replace raid link in chat text");
								console.warn(raidLink);
								console.warn($A(arguments));
							}

							// Extra debugging bit for a very specific weird behavior
							if (typeof raidLink.getRaid() == "undefined" || typeof raidLink.getRaid().fullName == "undefined" || raidLink.getRaid().fullName === "undefined")
							{
								console.warn("Bad Raid link");
								console.warn(raidLink);
								console.warn($A(arguments));
							}
						}
						else if (match = allianceInvitePattern.exec(msg)) {
							msg = msg.replace(/<a(?:(?!<a class="reply_link).)*<\/a>/i, allianceInviteFormat.format(match[0], match[1]));
						}
						else {
                            // Only even check this if it's not another kind of message type
                            if (DC_LoaTS_Helper.getPref("LinkifyUrls", true)) {
                                msg = msg.replace(urlPattern, function(url) {
                                    // Last minute check to make sure the regex didn't flub it
                                    // If the url contains any weird characters, ", ', <, or >, just bail
                                    return /["'><]/g.test(url) ? url : urlFormat.format(url);
                                });

                                // Replace #wiki links
                                msg = msg.replace(hashWikiPattern, function(hashWikiText) {
                                    return DC_LoaTS_Helper.getCommandLink("/" + hashWikiText.substr(1), hashWikiText);
                                });
                            }
                        }
					}

					// Make sure to run the normal version of this function because
					// it does all the heavy lifting for actually displaying the right string
					// and since we can't control what other scripts and addons have also replaced it
					this.DC_LoaTS_displayUnsanitizedMessage(user, msg, attributes, options);
					Timer.stop("Process Message");
				};

				// Take all the chat commands and register them with Kongregate
				for (var commandName in DC_LoaTS_Helper.chatCommands)
				{
					// Get the command
					var command = DC_LoaTS_Helper.chatCommands[commandName];

					// If there's really a command for this name
					if (typeof command !== "undefined")
					{
						// Create a command factory for this command
						var commandFactory = new RaidCommandFactory(command, "chat");

						// Attach the command factory to the holodeck callback
						holodeck.addChatCommand(commandName, commandFactory.createAndExecuteCommand.bind(commandFactory));
					}
				}

				// We want to intercept whispers to the raid bot and alias commands
				// What we're going to do here is snag any attempt to execute a command
				// before that command is actually run. Then, we can either capture it
				// and keep it from running at all, change it to run how we want (aliases),
				// or send the command on as intended (like non-raidbot whispers)
				holodeck.DC_LoaTS_processChatCommand = holodeck.processChatCommand;
				holodeck.processChatCommand = function(str)
				{
                    DC_LoaTS_Helper.recentlySent.unshift(str);

					// Assume it's not /w RaidBot
					var raidBotWhisper = false;

					// If this is a RaidBot whisper, or someone failed to /w
					if (str.substring(0,10).toLowerCase() == "/w raidbot"
						||
						str.substring(0,8).toLowerCase() == "/raidbot"
					   )
					{
						// Grab the command
						var command = str.substring(11).trim();

						// If there was no command or the command was help
						if (command.length == 0 || command.toLowerCase() == "help")
						{
							// Print out the about script screen
							return DC_LoaTS_Helper.printScriptHelp(holodeck, command);
						}
						// If this has a real command in it
						else
						{
							// Make sure it started with a /
							if (command.charAt(0) != '/')
							{
								command = "/" + command;
							}

							// Process the command as if it was a normal command
							str = command;
						}

						// This suppressed the command going to chat, even on failure
						// and even if a real command is not found by that name
						raidBotWhisper = true;
					}

					//TODO: This process could be optimized a bit when the user starts out using the official command name
					// Iterate over the commands to find their aliases
					for (var commandName in DC_LoaTS_Helper.chatCommands)
					{
						DCDebug(commandName);
						// If the regular command name is here, just use that
						if (new RegExp("^\/" + commandName + "\\b", "i").test(str))
						{
							// Stop trying to find aliases
							break;
						}
						// Not this real command name. Check its aliases.
						else
						{
							// Grab the aliases for this command
							var aliases = DC_LoaTS_Helper.chatCommands[commandName].aliases;

							// If there are actually any aliases
							if (typeof aliases != "undefined")
							{
								// For each alias
								for (var i = 0; i < aliases.length; i++)
								{
									// Get this alias
									var alias = aliases[i];

									// If we found an alias
									if (new RegExp("^\/" + alias + "\\b", "i").test(str))
									{
										// Redirect to the official command
										str = str.replace(new RegExp(alias, "i"), commandName);
									}
								}
							}
						}
					}

					// Capture the resulting state of the chat command
					var chatCommandResult = holodeck.DC_LoaTS_processChatCommand(str);
					var ignoredByPreference = false;
					DCDebug("Chat Command Result for " + str + ": ");
					DCDebug(chatCommandResult);

					// If it was a /w RaidBot but we didn't find a command
					if (raidBotWhisper && chatCommandResult)
					{
						// Let the user know the command failed
						holodeck.activeDialogue().raidBotMessage("Did not understand command: <code>" + str + "</code>");
					}
					else if (chatCommandResult && str.indexOf("/") == 0 && str.indexOf("/me") !== 0 && str.indexOf("/wrists") !== 0 && DC_LoaTS_Helper.getPref("IgnoreInvalidCommands", true)) {
						ignoredByPreference = true;

						// Let the user know the command failed
						holodeck.activeDialogue().raidBotMessage("Did not understand command: <code>" + str + "</code> " + DC_LoaTS_Helper.getCommandLink("/w RaidBot help", "Need Help?"));
					}

					// Only pass the message along if it wasn't a /w RaidBot and it's not a command and we're not ignoring this message by preference
					return !raidBotWhisper && chatCommandResult && !ignoredByPreference;
				}; // End Replacement displayUnsanitizedMessage

		// This is how we're going to manage left clicks on the chat area
		DC_LoaTS_Helper.handleMessageWindowClickHandler();
		// This is how we're going to manage right clicks on the chat area
		DC_LoaTS_Helper.handleMessageWindowContextMenuHandler();

				// Make sure the ignore visited thing is working
				// TODO: If we ever do more of these, make a framework for it, or something
				DC_LoaTS_Helper.handleIgnoreVisitedRaids();
		    } // End initialize
	    });

	    // Retrieve the message format
	    DC_LoaTS_Helper.getMessageFormat = function()
	    {
	    	// Retrieve the message format
			var messageFormat = GM_getValue(DC_LoaTS_Properties.storage.messageFormat);

			// Fall back to default messageFormat if necessary
			if (typeof messageFormat == "undefined" || messageFormat.trim().length == 0)
			{
				messageFormat = RaidLink.defaultMessageFormat;
				GM_setValue(DC_LoaTS_Properties.storage.messageFormat, messageFormat);
			}
			return messageFormat;
	    };

	    // Set the message format
	    DC_LoaTS_Helper.setMessageFormat = function(messageFormat)
	    {
			// Fall back to default messageFormat if necessary
			if (typeof messageFormat == "undefined" || messageFormat.trim().length == 0)
			{
				messageFormat = RaidLink.defaultMessageFormat;
			}

			// Set the message format
			GM_setValue(DC_LoaTS_Properties.storage.messageFormat, messageFormat);
	    };

	    // Retrieve the link format
	    DC_LoaTS_Helper.getLinkFormat = function()
	    {
			// Retrieve the boolean of whether or not we're using a custom link format
			var customLinkFormatBool = GM_getValue(DC_LoaTS_Properties.storage.customLinkFormatBool);

			// If we are using a custom link format
			if (customLinkFormatBool == true)
			{
				// Retrieve the custom anchor tag format
				var linkFormat = GM_getValue(DC_LoaTS_Properties.storage.linkFormat);

				// Fall back to default linkFormat if necessary or update old default
				if (typeof linkFormat == "undefined" || linkFormat.trim().length == 0 || linkFormat.trim() == RaidLink.defaultLinkFormat_v1)
				{
					linkFormat = RaidLink.defaultLinkFormat_v2;
					GM_setValue(DC_LoaTS_Properties.storage.linkFormat, linkFormat);
				}
			}
			else
			{
				if (typeof customLinkFormatBool == "undefined")
				{
					GM_setValue(DC_LoaTS_Properties.storage.customLinkFormatBool, false);
				}

				linkFormat = RaidLink.defaultLinkFormat_v2;
			}
			return linkFormat;
	    };

	    // Retrieve a preference value from storage
	    DC_LoaTS_Helper.getPref = function(prefName, defaultValue)
	    {
	    	// Fetch the json
	    	var json = GM_getValue(DC_LoaTS_Properties.storage.behaviorPrefs);

	    	// Make sure there's JSON
	    	if (typeof json === "undefined" || json.length == 0)
	    	{
				json = "{}";
	    	}

	    	var ret;
	    	try
	    	{
	    		var prefs = JSON.parse(json);
	    		ret = prefs[prefName];
	    	}
	    	catch(ex)
	    	{
	    		console.warn("Could not parse prefs to find " + prefName);
	    		console.warn(ex);
	    	}

	    	return (typeof ret !== "undefined") ? ret : defaultValue;
	    };

	    // Store a preference value into storage
	    DC_LoaTS_Helper.setPref = function(prefName, value)
	    {
	    	// Fetch the json
	    	var json = GM_getValue(DC_LoaTS_Properties.storage.behaviorPrefs);

	    	// Make sure there's JSON
	    	if (typeof json == "undefined" || json.length == 0)
	    	{
				json = "{}";
	    	}

	    	// Store value
	    	try
	    	{
	    		var prefs = JSON.parse(json);
	    		prefs[prefName] = value;
	    		GM_setValue(DC_LoaTS_Properties.storage.behaviorPrefs, JSON.stringify(prefs));
	    	}
	    	catch(ex)
	    	{
	    		console.warn("Could not parse prefs to store " + prefName + ": " + value);
	    		console.warn(ex);
	    	}
	    };

	    // Find all raid types matching a given filter
	    DC_LoaTS_Helper.getRaidTypes = function(raidFilter)
	    {
	    	// We're going to return an array of raid types that match
	    	var matchedTypes = [];

			// Iterate over all raids
			for (var raidId in DC_LoaTS_Helper.raids)
			{
				// Get the current raid
				var raid = DC_LoaTS_Helper.raids[raidId];

				// If the user's text matches this raid name
				if (raidFilter.matches({name: raid.getSearchableName(), size: raid.size, zone: raid.zone}))
				{
					// Capture this raid to return
					matchedTypes.push(raid);
				}
			}

			return matchedTypes;
	    };

	    // Print the description of the script
	    DC_LoaTS_Helper.printScriptHelp = function(deck, text)
	    {
   			var helpText = "<b>Kongregate Legacy of a Thousand Suns Raid Link Helper for Chat</b>\n";
			helpText += "by <a href=\"" + DC_LoaTS_Properties.authorURL + "\">doomcat</a>\n";
			helpText += "\n";
			helpText += "<b>Script homepage:</b> <a href=\"" + DC_LoaTS_Properties.scriptURL + "\" target='_blank'>" + DC_LoaTS_Properties.scriptURL + "</a>\n";
			helpText += "<b>Script Docs:</b> <a href=\"" + DC_LoaTS_Properties.docsURL + "\" target='_blank'>" + DC_LoaTS_Properties.docsURL + "</a>\n";
			helpText += "<b>Script Chatzy:</b> <a href=\"" + DC_LoaTS_Properties.chatzyURL + "\" target='_blank'>" + DC_LoaTS_Properties.chatzyURL + "</a>\n";
			helpText += "\n";
			helpText += "<span class=\"DC_LoaTS_versionWrapper\">";
			// If we've checked for version before
			if (typeof DC_LoaTS_Helper.needUpdateState != "undefined")
			{
				// If it's time to update
				if (DC_LoaTS_Helper.needUpdateState == "old")
				{
					helpText += DC_LoaTS_Helper.needUpdateText + "\n";
					helpText += "\n";
					helpText += "\n";
					helpText += "<span class='clearfix'>";
					helpText += "<span style='float:left; padding-top: 5px;'>Update now?</span>";
					helpText += "<span style='float:right;'><a class='DC_LoaTS_updateLink' href='" + DC_LoaTS_Properties.scriptDownloadURL + "' target='_blank'>Update</a></span>";
				}
				// If the user has a newer than public version
				else if (DC_LoaTS_Helper.needUpdateState == "new")
				{
					helpText += DC_LoaTS_Helper.needUpdateText + "\n";
					helpText += "\n";
				}
				// Either current or some kind of failure
				else
				{
					helpText += "<b>Version:</b> " + DC_LoaTS_Helper.needUpdateText + "\n";
					helpText += "\n";
					helpText += "\n";
					helpText += "<span class='clearfix'>";
					helpText += "<span style='float:left; padding-top: 5px;'>Check for updates?</span>";
					helpText += "<span style='float:right;'><a class='DC_LoaTS_updateLink DC_LoaTS_updateNotRun' onclick='DC_LoaTS_Helper.checkForUpdates(); return false' href='#' target='_blank'>Check now</a></span>";
				}

			}
			// We don't really know what the current version is
			else
			{

				helpText += "<b>Version:</b> " + DC_LoaTS_Properties.version + "\n";
				helpText += "\n";
				helpText += "\n";
				helpText += "<span class='clearfix'>";
				helpText += "<span style='float:left; padding-top: 5px;'>Check for updates?</span>";
				helpText += "<span style='float:right;'><a class='DC_LoaTS_updateLink DC_LoaTS_updateNotRun' onclick='DC_LoaTS_Helper.checkForUpdates(); return false' href='#' target='_blank'>Check now</a></span>";
			}

			helpText += "</span>";
			helpText += "\n";
			helpText += "\n";
			helpText += "</span>";
			helpText += "\n";
			helpText += "<b>Commands:</b>\n";
			helpText += "\n";

			// Iterate over all commands and display their summaries
			for (var commandName in DC_LoaTS_Helper.chatCommands)
			{
				var command = DC_LoaTS_Helper.chatCommands[commandName];
				if (typeof command.doNotEnumerateInHelp == "undefined" || command.doNotEnumerateInHelp === false)
				{
					if (typeof command.getParamText === "function")
					{
						helpText += "<code>/" + commandName + " " + command.getParamText() + "</code>\n";
					}
				}
			}

			helpText += "\n";
			helpText += "All commands can do <code>/commandname help</code> to learn more about them. Brackets <code>[]</code> indicate optional parameters; don't actually put brackets in your commands, please.\n";
			deck.activeDialogue().raidBotMessage(helpText);


			return false;
	    };

DC_LoaTS_Helper.recentlySent = [];
DC_LoaTS_Helper.chatCommands = {};
DC_LoaTS_Helper.raidStyles = {};		/************************************/
		/********* RaidButton Class *********/
		/************************************/
		
		window.RaidButton = Class.create({
			initialize: function(name, className, callback)
			{
				this.name = name || "";
				this.callback = callback;
				this.node = new Element("li", {"class": "DC_LoaTS_button_wrapper " + className + "Wrapper"});
				this.anchor = new Element("a", {"class": "DC_LoaTS_button " + className});
				this.anchor.appendChild(document.createTextNode(this.name));
				this.anchor.observe("click", function(clickEvent)
				{
					this.execute(clickEvent);
				}.bindAsEventListener(this));
				
				this.node.insert({bottom: this.anchor});
			},
			
			execute: function(clickEvent)
			{
				this.callback(clickEvent);
			}
			
		});

/************************************/
/********* RaidCommand Class ********/
/************************************/

// Mainly located by the omnibox iterating over all commands checking to see what matches
// and each of these being hard assigned to their names for the chat commands

window.RaidCommand = Class.create({
	initialize: function(context, commandText)
	{
		this.context = context;
		this.isHelp = false;

		if (typeof commandText != "undefined")
		{
			this.processText(commandText);
		}
	},

	processText: function (commandText)
	{
		this.commandText = commandText;
		this.processedText = this.commandText;

		if (this.processedText.charAt(0) == '/')
		{
			this.processedText = this.processedText.substring(1);
		}

		// If the command was explicitly provided, we need to strip it
		if (this.processedText.toLowerCase().indexOf(this.getName()) == 0)
		{
			this.processedText = this.processedText.substring(this.getName().length);
		}
		else
		{
			for (var i = 0; i < this.aliases.length; i++)
			{
				var alias = this.aliases[i];
				if (this.processedText.toLowerCase().indexOf(alias) == 0)
				{
					this.processedText = this.processedText.substring(alias.length);
				}
			}
		}

		// Now processed text should just be the params. Need to trim the whitespace
		this.processedText = this.processedText.trim();

		// Reassemble the normalized commandText
		this.commandText = "/" + this.getName() + " " + this.processedText;

		// Check for help
		if (this.processedText.toLowerCase() == "help")
		{
			this.isHelp = true;
		}
		// Not a help command
		else
		{
			// With the params, get the parser
			if (typeof this.parsingClass != "undefined")
			{
				this.parser = new this.parsingClass(this.processedText);
			}
		}
	},

	// Get the param text for help
	getParamText: function()
	{
		return this.constructor.getParamText();
	},

	// Get all the names for this command, including both it's actual name and aliases
	getNames: function()
	{
		return [this.getName()].concat(this.aliases);
	},

	// Get the name of this command
	getName: function()
	{
		return this.constructor.commandName;
	},

	// Get the help text for the command
	getHelpText: function()
	{
		// Default help text to say there isn't help text
		var helpText = "This command does not have any additional help.";

		// If the subclass has help text
		if (typeof this.buildHelpText != "undefined")
		{
			// Grab it and set it to be our returned help text
			helpText = this.buildHelpText();
		}

		// Append any aliases this command has
		helpText += "\n"
		helpText += "<b>Aliases:</b> " + this.getAliasesText() + "\n";

		return helpText;
	},

	// See if the assigned parser has valid params
	//FIXME - Does not work and/or is not used
	isValid: function()
	{
		var valid = true;
		if (typeof this.parsingClass != "undefined")
		{
			var parser = new this.parsingClass(params);
			valid = parser.isValid();
		}

		return valid;
	},

	// Get the text for all the aliases of this command. Aliases are wrapped in <code></code>tags
	getAliasesText: function()
	{
		var aliasesText = "";

		// If there are any aliases
		if (typeof this.aliases != "undefined" && this.aliases.length > 0)
		{
			// Add all the aliases in
			for (var i = 0; i < this.aliases.length; i++)
			{
				// Format the alias
				aliasesText += "<code>/" + this.aliases[i] + "<code>";

				// Add commas as necessary
				if (i < this.aliases.length - 1)
				{
					aliasesText += ", ";
				}
			}
		}
		// No aliases
		else
		{
			aliasesText = "None.";
		}

		return aliasesText;
	},

	// Get a text link to this command
	getCommandLink: function(params, displayText)
	{
		return DC_LoaTS_Helper.getCommandLink("/" + this.getName() + (params ? " " + params:""), displayText);
	},

	// Get the drop down menu options for this command
	getOptions: function()
	{
		var commandOptions = {

			initialText: {
				text: this.commandName,
				callback: function()
				{
					console.log("mainOption " + this.commandName);
				}
			},

			secondOption: {
				text: "Option #2",
				callback: function()
				{
					console.log("secondOption " + this.commandName);
				}
			}

		};

		return commandOptions;
	},

	// Gets the full HTML line for this command's options
	getOptionLine: function(oldLine)
	{
		var commandOptions = this.getOptions();

		var line;
		// If we're operating on an existing line
		if (typeof oldLine != "undefined")
		{
			// Put new stuff back into this line
			line = oldLine;

			// Clear everything old from this line
			line.childElements().invoke("remove");
		}
		// If there is no existing line
		else
		{
			// Make a new line
			line = new Element("li");
		}

		var subOptions = new Element("div", {style: "float: right;"});

		var config = commandOptions.config || {};

		for (var optionName in commandOptions)
		{
			// Configuration is obviously not a real option
			if (optionName.toLowerCase() == "config")
			{
				continue;
			}

			var option = commandOptions[optionName];

			var textHolder;
			if (typeof option.callback != "undefined" || typeof option.linkParams != "undefined" || false !== option.executable)
			{

				var linkParams = {"href": "#", "class": "DC_LoaTS_omniboxOption DC_LoaTS_" + optionName.toLowerCase()};
				if (typeof option.linkParams != "undefined")
				{
					for (var paramName in option.linkParams)
					{
						linkParams[paramName] = option.linkParams[paramName];
					}
				}

				textHolder = new Element("a", linkParams);


				textHolder.onclick = function(option)
				{
					if (typeof option.callback != "undefined")
					{
						option.callback.apply(this);
					}
					this.execute(option.doNotCallHandler);
					return (typeof option.followLink != "undefined")?option.followLink:false;
				}.bind(this, option);
			}
			else
			{
				textHolder = new Element("div", {"class": "DC_LoaTS_" + optionName.toLowerCase()});
			}

			if (typeof option.text != "undefined")
			{
				textHolder.update(option.text);
			}

			if (optionName == "initialText")
			{
				line.insert({bottom: textHolder});
			}
			else
			{
				subOptions.insert({bottom: textHolder});
			}
		}

		if (subOptions.children.length > 0)
		{
			line.insert({bottom: subOptions});
		}
		else
		{
			line.children[0].setStyle({"float":"none"});
		}
//				
//				var children = $A(line.immediateDescendants());
//				var currentTallest = 0;
//				
//				for (i = 0; i < children.length; i++)
//				{
//			        if (children[i].getHeight() > currentTallest)
//			        {
//		                currentTallest = children[i].getHeight();
//			        }
//				}
//				
//				for (i = 0; i < children.length; i++)
//				{
//			        children[i].setStyle({ height: (currentTallest + 'px') });
//				}
//				
//				
		if (typeof config.refreshEvery == "number" && typeof this.omniboxOptionRefreshInterval == "undefined")
		{
			this.omniboxOptionRefreshInterval = setInterval(this.getOptionLine.bind(this, line), config.refreshEvery);
		}

		return line;
	},

	// Run this command
	execute: function(doNotCallHandler)
	{
		var ret = {};

		// Check for help
		if (this.isHelp == true)
		{
			DCDebug("Executing help for " + this.commandName);
			if (this.context == "chat" || true) //TODO: Remove || true
			{
				holodeck.activeDialogue().raidBotMessage(this.getHelpText());
			}
			else if (this.context == "omnibox")
			{
				console.log("Display help for " + this.commandName);
			}
			else
			{
				console.warn("Could not find help for command " + this.commandText + " in context " + this.context);
			}
		}
		// Not a help command
		else if (typeof doNotCallHandler === "undefined" || !doNotCallHandler)
		{
			DCDebug("Executing non-help for " + this.commandName + " doNotCallHandler: " + doNotCallHandler)
			if (typeof this.parser === "undefined" || (typeof this.parser.isValid === "function" && this.parser.isValid()))
			{
				ret = this.handler(holodeck, this.parser, this.processedText, this.commandText, this.context);

				if (typeof ret.statusMessage != "undefined")
				{
					if (this.context == "chat" || true) //TODO: Remove || true
					{
						holodeck.activeDialogue().raidBotMessage(ret.statusMessage);
					}
					else if (this.context == "omnibox")
					{
						console.log("Display status message: " + ret.statusMessage);
					}
					else
					{
						console.warn("Could not display message " + ret.statusMessage + " for command " + this.commandText + " in context " + this.context);
					}
				}

				DCDebug("Command " + this.commandText + (ret.success===true?" Succeeded":" Failed"));
			}
			else
			{
				console.warn("Could not parse text " + this.commandText + " as command " + this.commandName + " in context " + this.context);
			}
		}

		ret.sendToChat = this.sendToChat && this.context == "chat";

		// Regardless of execution, we need to hide the command options
		RaidToolbar.hideCommandOptions();

		// Clear the omnibox, needs work
//				RaidToolbar.resetOmnibox();

		return ret;
	},

	// Called when the option is no longer in the suggested box
	onRemovedFromOmnibox: function()
	{
		DCDebug("Deactivating " + this.commandName);
		if (typeof this.omniboxOptionRefreshInterval != "undefined")
		{
			clearInterval(this.omniboxOptionRefreshInterval);
			delete this.omniboxOptionRefreshInterval;
		}
	}
});

RaidCommand.create = function(classObject)
{
	if (typeof classObject.commandName === "undefined") {
		throw {message: "Cannot create command without name", cls: classObject};
	}

	if (typeof classObject.aliases === "undefined") {
		classObject.aliases = [];
		console.warn(classObject.commandName + " did not define its aliases");
	}

	var commandClass = Class.create(RaidCommand, classObject);
	//TODO: Need to clean this up
	commandClass.commandName = classObject.commandName;
	commandClass.aliases = classObject.aliases;
	commandClass.paramText = classObject.paramText;
	commandClass.parsingClass = classObject.parsingClass;
	//TODO Implement OO framework at some point
	if (typeof commandClass.parsingClass !== "undefined" && typeof commandClass.parsingClass.prototype.isValid !== "function")
	{
		console.warn(commandClass.commandName + " Command Creation Error: Parser must have isValid method!");
	}
	commandClass.doNotEnumerateInHelp = classObject.doNotEnumerateInHelp;
	commandClass.getParamText = function()
	{
		// Assume empty
		var params = "";

		// If the command provided text, use that
		if (typeof this.paramText != "undefined")
		{
			params = this.paramText;
		}
		// If the parser can provide us param text, that's great, too
		else if (typeof this.parsingClass != "undefined" && typeof this.parsingClass.paramText == "string")
		{
			params = this.parsingClass.paramText;
		}
		else
		{
			DCDebug("No param text for " + this.commandName);
		}

		return params;
	}.bind(commandClass);



	DC_LoaTS_Helper.chatCommands[classObject.commandName] = commandClass;
};
		
		/************************************/
		/****** RaidCommandFactory Class ****/
		/************************************/

		window.RaidCommandFactory = Class.create({
			
			/*public constructor*/ initialize: function(raidCommandClass, context)
			{
				this.raidCommandClass = raidCommandClass;
				this.context = context;
			},
			
			/*public RaidCommand*/ createCommand: function(text)
			{
				return new this.raidCommandClass(this.context, text);
			},
			
			// Returning true will send the message on to Kongregate chat
			// Returning false will stop the message from being sent
			/*public boolean*/ createAndExecuteCommand: function(deck, text)
			{
				var command = this.createCommand(text);
				DCDebug("Created Command " + this.raidCommandClass.commandName);
				var commandRet = command.execute();
				
				// If the commandRet has sendToChat set to true, the command text typed by the user,
				// 		(Not the command output text) will forward on to chat
				// otherwise, we default to absorbing the command locally
				return commandRet.sendToChat || false;
			}
			
			
		});
		
		/************************************/
		/********* RaidFilter Class *********/
		/************************************/
		
		// This class represents a filter on a raid search
		window.RaidFilter = Class.create({
			
			// Constructor
			initialize: function(filterText)
			{
				Timer.start("RaidFilter init");
				try
				{
					// Declare some vars for later. This is more for reference than technical need.
					this.name;
					this.difficulty;
					this.state;
					this.inverseState = false;
					this.age;
					this.size;
					this.count;
					this.page;
					this.fs;
					this.os;
					this.zone;
                    this.progress; // Only usable with lrm (needs raid currentHealth and timeRemaining
					this.valid = true;
	
					// Capture original filterText
					this.filterText = filterText;
					
					// Pattern to pick apart the command for the name and id
					//TODO: /((?:[^{}\d]|[5-9]|\d*\.\d*)+)?\s*([0-4](?:\s*\|\s*[0-4]){0,4})?/
					var commandPattern = /([^0-4{}]+)? ?([0-4])? ?/;
	
					// Attempt to find the matches
					var match = commandPattern.exec(filterText);
					
					// If there were some matches
					if (match != null)
					{
						// If the raid command had a name
						if (typeof match[1] != "undefined")
						{
							this.name = match[1].trim();
						}
						
						// If the raid command had a difficulty
						if (typeof match[2] != "undefined")
						{
							this.difficulty = parseInt(match[2]);
						}
						
					}
					
					// Pattern to match everything that's currently in {filterType: paramValue} form
					var extraFiltersPattern = /(?:{(\w+)[:=]([^{}]+)} ?)/g;
					
					// For every additional parameter type
					while ((match = extraFiltersPattern.exec(filterText)) != null)
					{
						// Name of the param
						var filterType = match[1].trim().toLowerCase();
						
						// Value of the param
						var paramValue = match[2].trim();
						
						// Trace statement
						var traceStatement = "<code>{" + filterType + ":" + paramValue + "}</code> in <code>" + filterText + "</code>";
						
						// Based on the param type, parse the param value
						switch (filterType)
						{
							case "age":
								// Get the pieces of the age
								var match = RaidFilter.numberExpressionPattern.exec(paramValue);
								
								// If there were pieces to get
								if (match != null)
								{
									var condition = match[1];
									var num = parseInt(match[2]);
									
									// If the number wasn't really a number
									if (isNaN(num))
									{
										// Go to the next one.
										continue;
									}
									var period = match[3];
									
									// If there was a period
									if (typeof period != "undefined")
									{
										switch (period.toLowerCase())
										{
                                            // Fall throughs are on purpose
											case "d":
												// 24 hours in a day
												num *= 24;
											case "h":
												// 60 minutes in an hour
												num *= 60;
											case "m":
												// 60 seconds in a minute
												num *= 60;
											case "s":
												// 1000 ms in a second
												num *= 1000;
												break;
											case "ms":
												break;
											default:
												holodeck.activeDialogue().raidBotMessage("Did not understand unit of time <code>" + period + "</code>  for " + traceStatement);
												this.valid = false;
										}
									}
									// else no period, assume ms
									
									// Sanitize the condition. Default to <=
									condition = this.sanitizeConditional(condition, "<=");
									
									if (condition == "undefined")
									{
										holodeck.activeDialogue().raidBotMessage("Did not understand condition <code>" + condition + "</code>  for " + traceStatement);
										this.valid = false;
									}
								}
								else
								{
									// Notify the user that we don't know what that age is
									holodeck.activeDialogue().raidBotMessage("Did not understand " + filterType + " expression <code>" + paramValue + "</code> for " + traceStatement);
									this.valid = false;
								}
								this.age = condition + num;
								break;
							case "count":
								// If the number wasn't really a number
								if (isNaN(parseInt(paramValue)))
								{
									// Go to the next one.
									continue;
								}
								
								this.count = parseInt(paramValue);
								break;
							case "page":
								// If the number wasn't really a number
								if (isNaN(parseInt(paramValue)))
								{
									// Go to the next one.
									continue;
								}
								
								this.page = parseInt(paramValue);
								break;
                            case "progress":
                                // Progress translates to the ratio of health to time
                                switch (paramValue.toLowerCase()) {
                                    case "ahead":
                                        paramValue = this.progress = "<= 1";
                                        break;
                                    case "behind":
                                        paramValue = this.progress = "> 1";
                                        break;
                                }

                            // Fall through on purpose
							case "os":
							case "fs":
							case "size":
								var match = RaidFilter.numberExpressionPattern.exec(paramValue);
								
								if (match != null)
								{
									var condition = match[1];
									var num = parseInt(match[2]);
									
									// If the number wasn't really a number
									if (isNaN(num))
									{
										// Go to the next one.
										continue;
									}
									
									var magnitude = match[3];
									
									// If there was a magnitude
									if (typeof magnitude != "undefined")
									{
										switch (magnitude.toLowerCase())
										{
                                            // Fall throughs are on purpose
											case "t":
												num *= 1000;
											case "b":
												num *= 1000;
											case "m":
												num *= 1000;
											case "k":
												num *= 1000;
												break;
											default:
												holodeck.activeDialogue().raidBotMessage("Did not understand magnitude <code>" + magnitude + "</code>  for " + traceStatement);
												this.valid = false;
										}
									}
									// else no magnitude, assume fully written out damage
									
									// Sanitize the condition. Default to ==
									condition = this.sanitizeConditional(condition, "==");
									
									if (condition == "undefined")
									{
										holodeck.activeDialogue().raidBotMessage("Did not understand condition <code>" + condition + "</code>  for " + traceStatement);
										this.valid = false;
									}
								}
								else
								{
									// Notify the user that we don't know what that fs is
									holodeck.activeDialogue().raidBotMessage("Did not understand " + filterType + " expression " + traceStatement);
									this.valid = false;
								}
								this[filterType] = condition + num;
								break;
							case "state":
								var tmpStateText = paramValue;
							
								// Are we doing inverse state?
								if (tmpStateText.charAt(0) == '!')
								{
									this.inverseState = true;
									tmpStateText = tmpStateText.substring(1);
								}
								
								// Lookup the state enum from the text
								this.state = RaidManager.STATE.valueOf(tmpStateText);
								
								// If the text didn't match any known state
								if (typeof this.state == "undefined")
								{
									// Notify the user that we don't know what that state is
									holodeck.activeDialogue().raidBotMessage("Did not understand state for "  + traceStatement);
									
									// No longer valid
									this.valid = false;
								}
								break;
							case "zone":
								if (isNaN(paramValue)) {
									this.zone = "" + paramValue;
								}
								else {
									this.zone = "Z" + paramValue
								}
								
								this.zone = this.zone.toUpperCase();
								
								break;
							default:
								console.warn("Did not understand filter param " + match[1] + ":" + match[2]);
						}
					}
				}
				catch(ex)
				{
					console.warn("Failed to initialize RaidFilter with text " + filterText);
				}
				Timer.stop("RaidFilter init");
			},
			
			// Takes in a condition and sanitizes it for use in the filter
			sanitizeConditional: function(condition, defaultTo)
			{
				if (typeof condition !== "undefined")
				{
					switch (condition)
					{
						case "=": 
							condition = "==";
							break;
						case "!":
							condition = "!=";
							break;
						case "<=":
						case ">=":
						case "==":
						case "!=":
						case "<":
						case ">":
							break;
						default:
							// Print warning to console
							console.warn("Could not parse condition: " + condition);
							
							// Return undefined since there was a problem
							condition = undefined;
					}
				}
				// If there was no condition passed in
				else
				{
					// Set it to the default
					condition = defaultTo;
				}
				
				// Return the correct condition
				return condition;
			},
			
			// Based on this filter, does a given property match the filter
			matches: function(params)
			{				
				// Init matched to true
				var matched = true;
				
				var STATE = RaidManager.STATE;
				
				// Shortcut to fail any visited, completed, or ignored raids when no state filter is specified
				if (typeof params.state !== "undefined" && !this.state && 
						(
								STATE.equals(params.state, STATE.VISITED) ||
								STATE.equals(params.state, STATE.COMPLETED) ||
								STATE.equals(params.state, STATE.IGNORED)
						)
					) {
					return false;
				}
				
				// Iterate over all the params
				for (var field in params)
				{
					// Grab the value of the field
					var value = params[field];
					
					// If the field is not part of the filter or was undefined in the params
					if (typeof value != "undefined" && typeof this[field] != "undefined")
					{
						switch(field.toLowerCase())
						{
							case "name":
                                var tmpName = RaidFilter.mangleNameForSearch(this.name) || "NOT SET";
								try {
									// If the user's text matches this raid name
									matched = matched && new RegExp(tmpName, "i").test(value);
								}
								catch (ex){
									holodeck.activeDialogue().raidBotMessage("Errors occurred while trying to match "  + tmpName + ". " + ex);
									console.log(ex);
									return false;
								}
								break;
							case "difficulty":
								// If the user's difficulty matches the raid
								matched = matched && this.difficulty == value
								break;
							case "state":
								// If the state matches and shouldn't be inverted
								// Or of the state doesn't match and should be inverted
								matched = matched && ((RaidManager.STATE.equals(value, this.state) && !this.inverseState)
										|| 
										(!RaidManager.STATE.equals(value, this.state) && this.inverseState));
								break;
							case "age":
								// Check against the age condition
								matched = matched && eval(value + this.age);
								break;
                            case "progress":
							case "os":
                            case "fs":
							case "size":
								// Check against the fs condition
								matched = matched && eval(value + this[field]);
								break;
							case "count":
								// Check against the count condition
								matched = matched && value < this.count;
								break;
							case "zone":
								matched = matched && value === this.zone;
								break;
							default:
								// Didn't find the field
								console.warn("Couldn't match RaidFilter property to " + field);
								matched = false;
								break;
						}
					}
				}
				
				return matched;
			},
			
			// Gets a key to define this filter
			getKey: function()
			{
				return 	((typeof this.name 			!== "undefined")?"n=" + this.name + ";":"") +
						((typeof this.difficulty 	!== "undefined")?"d=" + this.difficulty + ";":"") +
						((typeof this.state 		!== "undefined")?"s=" + this.state + ";":"") +
						((typeof this.inverseState 	!== "undefined")?"i=" + this.inverseState + ";":"") +
						((typeof this.age 			!== "undefined")?"a=" + this.age + ";":"") +
						((typeof this.count 		!== "undefined")?"c=" + this.count + ";":"") +
						((typeof this.page 			!== "undefined")?"p=" + this.page + ";":"") +
                        ((typeof this.progress      !== "undefined")?"pr=" + this.progress + ";":"") +
                        ((typeof this.size          !== "undefined")?"e=" + this.size + ";":"") +
                        ((typeof this.fs            !== "undefined")?"f=" + this.fs + ";":"") +
						((typeof this.os 			!== "undefined")?"o=" + this.os + ";":"") +
						((typeof this.zone 			!== "undefined")?"z=" + this.zone + ";":"");
			},
			
			// If it has a name and optionally a difficulty and nothing else, it's simple
			isSimple: function()
			{
				return typeof this.name !== "undefined" &&
					 (typeof this.state			=== "undefined" &&
					  typeof this.inverseState 	=== "undefined" &&
					  typeof this.age			=== "undefined" &&
					  typeof this.count			=== "undefined" &&
					  typeof this.page			=== "undefined" &&
                      typeof this.progress      === "undefined" &&
                      typeof this.size          === "undefined" &&
                      typeof this.fs            === "undefined" &&
					  typeof this.os			=== "undefined" &&
					  typeof this.zone			=== "undefined");
			},
			
			isEmpty: function()
			{
				return 	(typeof this.name 			=== "undefined") &&
						(typeof this.difficulty 	=== "undefined") &&
						(typeof this.state 			=== "undefined") &&
						(typeof this.inverseState 	=== "undefined") &&
						(typeof this.age 			=== "undefined") &&
						(typeof this.count 			=== "undefined") &&
						(typeof this.page 			=== "undefined") &&
                        (typeof this.progress       === "undefined") &&
                        (typeof this.size           === "undefined") &&
                        (typeof this.fs             === "undefined") &&
						(typeof this.os 			=== "undefined") &&
						(typeof this.zone 			=== "undefined");
	
			},
			
			isValid: function()
			{
				return this.valid;
			},
			
			getDifficultyText: function()
			{
				return RaidType.difficulty[this.difficulty];
			},
			
			toString: function()
			{
				return 	(((typeof this.name 			!= "undefined")? this.name + " ":"") +
						 ((typeof this.difficulty 		!= "undefined")? this.difficulty + " ":"") +
						 ((typeof this.state 			!= "undefined")? "{state: " + 
						 
						 ((typeof this.inverseState 	!= "undefined" && this.inverseState == true)? "!":"")
						 + this.state.text + "}"+ " ":"") +
                         ((typeof this.size             != "undefined")? "{size: " + this.size + "} ":"") + 
                         ((typeof this.fs               != "undefined")? "{fs: " + this.fs + "} ":"") + 
						 ((typeof this.os 				!= "undefined")? "{os: " + this.os + "} ":"") + 
						 ((typeof this.progress 		!= "undefined")? "{progress: " + this.progress + "} ":"") +
						 ((typeof this.zone				!= "undefined")? "{zone: " + this.zone + "} ":"") +
						 ((typeof this.age 				!= "undefined")? "{age: " + this.age + "ms} ":"") +
						 ((typeof this.count 			!= "undefined")? "{count: " + this.count + "} ":"") +
						 ((typeof this.page 			!= "undefined")? "{page: " + this.page + "} ":"")).trim();
			},
			
			toPrettyString: function() {
				var ret = "";

				// Find the matching raid types
				var matchedTypes = DC_LoaTS_Helper.getRaidTypes(this);

				if (matchedTypes.length > 0)
				{
					// If there's a difficulty
					if (typeof this.difficulty !== "undefined") {
						if (this.difficulty >= 1 && this.difficulty <= 4) {
							ret += RaidType.difficulty[this.difficulty] + " ";
						}
						else {
							return "Filter does not match any raid difficulties";
						}
					}

					// If there's a name
					if (typeof this.name !== "undefined") {

						// If we matched some raid types
						var raids = [];
						for (var i = 0; i < matchedTypes.length; i++)
						{
							// Grab this raid
							raids.push(matchedTypes[i].fullName);
						}

						if (raids.length == 1) {
							ret += raids[0];
						}
						else if (raids.length == 2) {
							ret += raids[0] + " and " + raids[1];
						}
						else {
							var tmp = raids.join(", ");
							ret += tmp.substring(0, tmp.lastIndexOf(", ") + 2) + "and " + tmp.substring(tmp.lastIndexOf(", ") + 2);
						}

					}
				}
				else {
					return "Filter does not match any raid types";
				}					
				
				return ret;
			}
		});

        RaidFilter.mangleNameForSearch = function(name) {
            // Dirty pi/noir/sub hacks. Some raids are harder to locate because of similar names
            var hacks = {
                "pi": "^pi_", // Pi
                "noir i": "^noir_", // Noir
                "noir ii": "^noir2_", // Noir (2)
                "noir iii": "^noir3_", // Noir (3)
                "noir (i)": "^noir_", // Noir
                "noir (ii)": "^noir2_", // Noir (2)
                "noir (iii)": "^noir3_", // Noir (3)
                "xsub": "x sub", // Kulnar-Xex Subjugator
                "nsub": "x sub", // Kulnar-Xex Subjugator
                "esub": "e sub" // Kulnar-Xex Elite Subjugator
                },
                found = name;

            if (found) {
                for (var hack in hacks) {
                    if (hacks.hasOwnProperty(hack) && name.toLowerCase() === hack) {
                        found = hacks[hack];
                        break;
                    }
                }

                found = found.replace(".", "\\.");
            }

            return found;
        };

		// Parameter text for this parser
		RaidFilter.paramText = "[raidName] [raidDifficulty] [{state: stateParam}] [{size: sizeParam}] [{fs: fsParam}] [{os: osParam}] [{progress: progressParam}] [{zone: zoneParam}] [{age: ageParam}] [{count: countParam} [{page: pageParam}]]";
		
		// Regex to parse number expressions
		RaidFilter.numberExpressionPattern = /(<=?|>=?|==?|!=?)?\s*(\d+)\s*(\w\w?)?/;
		
		/************************************/
		/**** RaidFilterStyleParser Class ***/
		/************************************/
		
		// Class to parse raid link and style parameters
		window.RaidFilterStyleParser = Class.create({
			initialize: function(params)
			{
				// Split the params at the + that divides the filter from the style
				var parts = params.split("\+");
				
				// Parse the first part as a raid filter
				if (parts.length >= 1)
				{
					this.raidFilter = new RaidMultiFilter(parts[0].trim()); 
				}
				
				// The second part as the link style
				if (parts.length >= 2)
				{
					this.linkStyle = new RaidStyle(parts[1].trim());
				}
				
				// The third part as the message style
				if (parts.length >= 3)
				{
					this.messageStyle = new RaidStyle(parts[2].trim());
				}
				
				// The fourth part as the image style
				if (parts.length >= 4)
				{
					this.imageStyle = new RaidStyle(parts[3].trim());
				}
			},
			
			// Whether or not a link style exists for this parser
			hasLinkStyle: function()
			{
				return typeof this.linkStyle !== "undefined" && !this.linkStyle.isEmpty();
			},
			
			// Whether or not a message style exists for this parser
			hasMessageStyle: function()
			{
				return typeof this.messageStyle !== "undefined" && !this.messageStyle.isEmpty();
			},
			
			// Whether or not an image style exists for this parser
			hasImageStyle: function()
			{
				return typeof this.imageStyle !== "undefined" && !this.imageStyle.isEmpty();
			},
			
			// String describing this parser
			toString: function()
			{
				var ret;
				if (this.isValid())
				{
					ret = "Raids Matching <code>" + this.raidFilter.toString() + "</code> will have ";
					
					if (this.hasLinkStyle())
					{
						ret += "\nLink Style: <code>" + this.linkStyle.toString() + "</code>";
					}
					
					if (this.hasMessageStyle())
					{
						ret += "\nMessage Style: <code>" + this.messageStyle.toString() + "</code>";
					}
					
					if (this.hasImageStyle())
					{
						ret += "\nImage Style: <code>" + this.imageStyle.toString() + "</code>";
					}
				}
				else
				{
					ret = "Invalid Raid Filter Style Parser. Filter: " + (this.raidFilter?this.raidFilter.toString():"Not defined.");
				}
				
				return ret;
			},
			
			// Inject the styles from this parser into the page
			injectStyles: function()
			{
				// Create a class name for this set of styles
				this.className = "DCLH-RFSP-" + RaidFilterStyleParser._lastInjectedStyleId++;
				var rulesText = "";
				
				if (this.hasMessageStyle())
				{
					rulesText += "#kong_game_ui .chat_message_window ." + this.className + "{" + this.messageStyle.toString() + "}";
				}
				
				if (this.hasLinkStyle())
				{
					rulesText += "#kong_game_ui .chat_message_window ." + this.className + " a {" + this.linkStyle.toString() + "}";
				}
				
				if (this.hasImageStyle())
				{
					rulesText += "#kong_game_ui .chat_message_window ." + this.className + " img {" + this.imageStyle.toString() + "}";
				}
				
				
				// Locate/Create nodes
				var head = document.getElementsByTagName('head')[0],
				    style = document.createElement('style'),
				    rules = document.createTextNode(rulesText);
				
				// Style tag type
				style.type = 'text/css';
				
				// Browser dependencies
				if(style.styleSheet)
				{
				    style.styleSheet.cssText = rules.nodeValue;
				}
				else
				{
					style.appendChild(rules);
				}
				
				// Drop in the style node
				head.appendChild(style);
			},
			
			// Check validity of the parser
			isValid: function()
			{
				return typeof this.raidFilter !== "undefined" && this.raidFilter.isValid();
			}
		});
		
		RaidFilterStyleParser._lastInjectedStyleId = 0;

		/************************************/
		/********** RaidLink Class **********/
		/************************************/

		// Represents and parses actual raid link
		// Constructor is either
		// new RaidLink(str)
		//	params:
		//		str - Any string containing a raid link. The rest of the string will be ignored.
		//
		// OR
		//
		// new RaidLink(id, hash, difficulty, raidTypeId)
		//	params:
		//		id - kv_raid_id - the unique id for this raid instance
		//		hash - kv_hash - the string hash for the raid
		//		difficulty - kv_difficulty - a number from 1 to 4 where 1 is normal, 4 is nightmare
		//		raidTypeId - kv_raid_boss - the unique key for the raid type
		window.RaidLink = Class.create({
			initialize: function()
			{
				Timer.start("RaidLink init");
				// Capture variable args
				var args = $A(arguments);
				
				// If there's only one arg, must be link text
				if (args.length == 1)
				{
					// Only 1 arg means that it must be a link URL
					var linkURL = args[0];
					
					if (typeof linkURL != "undefined")
					{
                        // Correct for World Chat's broken links
                        linkURL = linkURL.replace("thousand-sunskv_", "thousand-suns?kv_");

						// Execute our regular expression (defined below) against the message
						// This checks to see if the message contained a LoaTS raid link
						var match = RaidLink.linkPattern.exec(linkURL);
						
						// If there was a raid link
						if (match != null)
						{
							// Get Param String
							var paramString = match[0];
							
							// Remove junk
							paramString = paramString.replace(/amp;/gi, "");
							
							// Separate params 
							var params = paramString.split("\?")[1].split("&");
														
							// Put the params together again into this object
							var paramObj = {kv_raid_id: "", kv_hash: ""};
							
							try 
							{
								for (var i = 0; i < 5; i++)
								{
									// If the param wasn't empty
									if (typeof params[i] == "string" &&  params[i] != "")
									{
										// Find the KV pairs
										var paramKV = params[i].split("=");
										
										// If there was something to split
										if (typeof paramKV[1] == "string")
										{
											// Split off any extra junk
											paramKV[1] = paramKV[1].split(/["'] /)[0].replace(/[^\w]/, "");
											
											// Assign the KV pairs
											paramObj[paramKV[0]] = paramKV[1];
										}
									}
								}
							}
							catch(ex)
							{
								console.warn(ex);
							}
													
							// Extract the difficulty
							this.difficulty = paramObj.kv_difficulty;
							
							// Extract the hash
							this.hash = paramObj.kv_hash;
							
							// Extract the raid boss (RaidType.id)
							this.raidTypeId = paramObj.kv_raid_boss;
							
							// Extract the raid id
							this.id = paramObj.kv_raid_id.replace(/\D/g, "");
						}
					}
					else
					{
						console.warn("Attempted make a raid link from an undefined URL");
					}
				}
				// If we got the 4 separate parts rather than a whole link
				else if (args.length > 1)
				{
					// Extract the raid id
					this.id = args[0];

					// Extract the hash
					this.hash = args[1];
					
					// Extract the difficulty
					this.difficulty = args[2];
					
					// Extract the raid boss (RaidType.id)
					this.raidTypeId = args[3];
				}
				else
				{
					console.error("Invalid parameters trying to create a RaidLink. ");
					console.error(args);
				}
								
				Timer.stop("RaidLink init");
			},
			
			getUniqueKey: function()
			{
				return this.id + "_" + this.hash;
			},
			
			getRaid: function()
			{
				// If this link wasn't fully filled out
				if (typeof this.raidTypeId == "undefined" || typeof this.difficulty == "undefined")
				{
					// Look up the same link in the storage
					var savedLink = RaidManager.fetch(this);
					
					// If there's a previous version of the link
					if (typeof savedLink !== "undefined")
					{
						// Capture the save params into this link
						this.raidTypeId = savedLink.raidTypeId;
						this.difficulty = savedLink.difficulty;
					}
				}
				
				// Look up the raid type
				var raid = DC_LoaTS_Helper.raids[(this.raidTypeId||"").toLowerCase()];
				
				// Return the raid type, or if we found nothing, a new empty raid type
				return (typeof raid !== "undefined")? raid : new RaidType(this.raidTypeId);
			},
			
			getMatchedStyles: function()
			{
				// Possible styles to find and apply
				var styleRet = {};
				
				// Attempt to apply styles
				try 
				{
					// Iterate over all the styles
					for (var key in DC_LoaTS_Helper.raidStyles)
					{
						// Get the style manager for the style
						var styleManagers = DC_LoaTS_Helper.raidStyles[key];
						
						// Grab the higher level info about the raid link
						var raidData = RaidManager.fetch(this);
						
						for (var i = 0; i < styleManagers.length; i++)
						{
							// Get the current style manager
							var styleManager = styleManagers[i];
							
							// If this link matches the filter
							if (styleManager.raidFilter.matches(
										{
											age: (new Date()/1) - raidData.firstSeen,
											difficulty: this.difficulty,
											fs:  this.getRaid().getFairShare(this.difficulty),
											os: this.getRaid().getOptimalShare(this.difficulty),
											name: this.getRaid().getSearchableName(),
											state: RaidManager.fetchState(this),
											size: this.getRaid().size,
											zone: this.getRaid().zone
										})
							)
							{
								for (var property in styleManager)
								{
									if ((property.indexOf("Style") > 0 || property.indexOf("className") > -1) && typeof styleManager[property] !== "undefined")
									{
										if (typeof styleRet[property] === "undefined")
										{
											styleRet[property] = "";
										}
										
										styleRet[property] += " " + styleManager[property];
									}
								}
							}
						}
					}
				}
				catch(ex)
				{
					console.warn("Error while finding styles for link:");
					console.warn(this);
					console.warn(ex);
				}
								
				return styleRet;
			},
			
			// Takes in a format returns a formatted text for this link
			getFormattedRaidLinkText: function(messageFormat)
			{
				try
				{
					if (this.isValid())
					{
						// Grab the raid type
						var raid = this.getRaid();
						
						// Start with just an empty template
						var newMessage = messageFormat;
						
						// Grab the link state
						var linkState = RaidManager.fetchState(this);
						
						// Fill in the basic data to the template
						newMessage = newMessage.replace(/{id}/gi, this.id);
						newMessage = newMessage.replace(/{line}/gi, "<br>");
						newMessage = newMessage.replace(/{size}/gi, raid.size);
						newMessage = newMessage.replace(/{stat}/gi, raid.stat);
						newMessage = newMessage.replace(/{health}/gi, raid.getHealthText(this.difficulty));
						newMessage = newMessage.replace(/{difficulty}/gi, RaidType.difficulty[this.difficulty]);
						newMessage = newMessage.replace(/{diff}/gi, RaidType.shortDifficulty[this.difficulty]);
						newMessage = newMessage.replace(/{zone}/gi, raid.zone);
						newMessage = newMessage.replace(/{name}/gi, raid.fullName);
						newMessage = newMessage.replace(/{short-name}/gi, raid.shortName);
						newMessage = newMessage.replace(/{shorter-name}/gi, raid.colloqName);
						newMessage = newMessage.replace(/{shortest-name}/gi, raid.shortestName);
						newMessage = newMessage.replace(/{time}/gi, raid.getTimeText());
						
						newMessage = newMessage.replace(/{(?:fs|fair|fairshare)}/gi, raid.getFairShareText(this.difficulty));
						newMessage = newMessage.replace(/{(?:os|opt|optimal|ofs|target)}/gi, raid.getTargetDamageText(this.difficulty));

						newMessage = newMessage.replace(/{cache-state}/gi, linkState.text);
						newMessage = newMessage.replace(/{(?:cache-state-nice|state|status)}/gi, linkState.niceText);
						newMessage = newMessage.replace(/{(?:cache-state|state|status)-short}/gi, linkState.shortText);
						newMessage = newMessage.replace(/{visited}/gi, (RaidManager.STATE.equals(linkState, RaidManager.STATE.VISITED) || RaidManager.STATE.equals(linkState, RaidManager.STATE.COMPLETED))?RaidManager.STATE.VISITED.niceText:"");
						newMessage = newMessage.replace(/{visited-short}/gi, (RaidManager.STATE.equals(linkState, RaidManager.STATE.VISITED) || RaidManager.STATE.equals(linkState, RaidManager.STATE.COMPLETED))?RaidManager.STATE.VISITED.shortText:"");
						
						if (typeof linkState == "undefined" || linkState.text == "undefined")
						{
							console.warn("Bad Link State");
							console.warn(linkState);
							console.warn(this);
						}
						
						// FS has special properties, so parse it differently
						var fsMatch = /{fs([^}]*)}/.exec(newMessage);
						
						// How many times we matched an FS
						var fsMatchCount = 0;
						
						// If there were any FS calls
						while (fsMatch != null)
						{
							try
							{
								// Don't get in an infite loop
								if (fsMatchCount >= 5)
								{
									console.warn("Can only match 5 {fs} with math")
									holodeck.activeDialogue().raidBotMessage("Can only match 5 {fs} with math while formatting");
									break;
								}
								
								fsMatchCount++;
								
								// Replace FS and do the math
								newMessage = newMessage.replace(/{fs[^}]*}/, 
									DC_LoaTS_Helper.prettyFormatNumber(eval(raid.getFairShare(this.difficulty)+fsMatch[1])));
								
								// Find the next FS
								var fsMatch = /\{fs([^\}]*)\}/.exec(newMessage);
							}
							catch (ex)
							{
								console.error("Error while formatting - Failed to process FS: " + fsMatch[0]);
								console.error(ex);
							}
						}
						
						return newMessage.trim();
					}
					else
					{
						console.warn("Tried to get formatted raid link text from invalid link");
						return "Legacy of a Thousand Suns Raid: [" + RaidType.difficulty[this.difficulty] + "] Unknown Raid (id: " + this.raidTypeId + ")"
					}
				}
				catch(ex)
				{
					console.warn("Error encountered in RaidLink.getFormattedRaidLinkText");
					console.warn(ex);
					return "Legacy of a Thousand Suns Raid: [" + RaidType.difficulty[this.difficulty] + "] Error Formatting Raid (id: " + this.raidTypeId + ")"
				}
			},
			
			// Gets the simplest text possible based on the storage messagen and link formats
			getSimpleText: function()
			{
				if (this.isValid())
				{
					// Retrieve the message format
					var messageFormat = DC_LoaTS_Helper.getMessageFormat();
					
					// Retrieve the anchor tag format
					var linkFormat = DC_LoaTS_Helper.getLinkFormat();
					
					// Start with just an empty template
					var newMessage = messageFormat;
					
					// Grab the link state
					var linkState = RaidManager.fetchState(this);
					
					// Grab the raid type
					var raid = this.getRaid();
					
					// Fill in the basic data to the template
					newMessage = newMessage.replace(/{id}/gi, this.id);
					newMessage = newMessage.replace(/{size}/gi, raid.size);
					newMessage = newMessage.replace(/{stat}/gi, raid.stat);
					newMessage = newMessage.replace(/{health}/gi, raid.getHealthText(this.difficulty));
					newMessage = newMessage.replace(/{difficulty}/gi, RaidType.difficulty[this.difficulty]);
					newMessage = newMessage.replace(/{diff}/gi, RaidType.shortDifficulty[this.difficulty]);
					newMessage = newMessage.replace(/{zone}/gi, raid.zone);
					newMessage = newMessage.replace(/{name}/gi, raid.fullName);
					newMessage = newMessage.replace(/{short-name}/gi, raid.shortName);
					newMessage = newMessage.replace(/{shorter-name}/gi, raid.colloqName);
					newMessage = newMessage.replace(/{shortest-name}/gi, raid.shortestName);
					newMessage = newMessage.replace(/{time}/gi, raid.getTimeText());

					newMessage = newMessage.replace(/{fs}/gi, raid.getFairShareText(this.difficulty));
					newMessage = newMessage.replace(/{target}/gi, raid.getTargetDamageText(this.difficulty));
					newMessage = newMessage.replace(/{optimal}/gi, raid.getTargetDamageText(this.difficulty));
					newMessage = newMessage.replace(/{ofs}/gi, raid.getTargetDamageText(this.difficulty));
					newMessage = newMessage.replace(/{os}/gi, raid.getTargetDamageText(this.difficulty));
					
					newMessage = newMessage.replace(/{cache-state}/gi, linkState.text);
					newMessage = newMessage.replace(/{cache-state-nice}/gi, linkState.niceText);
					newMessage = newMessage.replace(/{cache-state-short}/gi, linkState.shortText);
                    newMessage = newMessage.replace(/{visited}/gi, (RaidManager.STATE.equals(linkState, RaidManager.STATE.VISITED) || RaidManager.STATE.equals(linkState, RaidManager.STATE.COMPLETED))?RaidManager.STATE.VISITED.niceText:"");
                    newMessage = newMessage.replace(/{visited-short}/gi, (RaidManager.STATE.equals(linkState, RaidManager.STATE.VISITED) || RaidManager.STATE.equals(linkState, RaidManager.STATE.COMPLETED))?RaidManager.STATE.VISITED.shortText:"");
					
					// Remove fields we don't want
					newMessage = newMessage.replace(/{line}/gi, "");
					newMessage = newMessage.replace(/{image}/gi, "");
					newMessage = newMessage.replace(/<[^>]+>/gi, "");
					
					
					// FS has special properties, so parse it differently
					var fsMatch = /{fs([^}]*)}/.exec(newMessage);
					
					// How many times we matched an FS
					var fsMatchCount = 0;
					
					// If there were any FS calls
					while (fsMatch != null)
					{
						try
						{
							// Don't get in an infite loop
							if (fsMatchCount >= 5)
							{
								console.warn("Can only match 5 {fs} with math")
								holodeck.activeDialogue().raidBotMessage("Can only match 5 {fs} with math while formatting");
								break;
							}
							
							fsMatchCount++;
							
							// Replace FS and do the math
							newMessage = newMessage.replace(/{fs[^}]*}/, 
								DC_LoaTS_Helper.prettyFormatNumber(eval(raid.getFairShare(this.difficulty)+fsMatch[1])));
							
							// Find the next FS
							var fsMatch = /\{fs([^\}]*)\}/.exec(newMessage);
						}
						catch (ex)
						{
							console.error("Error while formatting - Failed to process FS: " + fsMatch[0]);
							console.error(ex);
						}
					}
					
					return newMessage.trim();
				}
				else
				{
					console.warn("Tried to get simple raid link text from invalid link");
					return "Legacy of a Thousand Suns Raid: [" + RaidType.difficulty[this.difficulty] + "] Unknown Raid (id: " + this.raidTypeId + ")";
				}
							
			},

			// Takes in a message format and a link format and returns a ready to inject link
			getFormattedRaidLink: function(messageFormat, linkFormat)
			{
				Timer.start("getFormattedRaidLink");
				
				// If there was no message format, look it up
				if (typeof messageFormat === "undefined")
				{
					messageFormat = DC_LoaTS_Helper.getMessageFormat();
				}
				
				// If there was no link format, look it up
				if (typeof linkFormat === "undefined")
				{
					linkFormat = DC_LoaTS_Helper.getLinkFormat();
				}
				
				// Get the basics of the message
				var newMessage = this.getFormattedRaidLinkText(messageFormat).trim();
				
				try 
				{
					// Get the text of the message without the image
					var noImage = newMessage.replace(/{image}/gi, "").replace(/<[^>]+>/gi, "").trim();
					
					// Define the image tag
					var imageTag = this.getFormattedImageTag();
					
					// Index of the image tag
					var imageIndex = newMessage.indexOf("{image}");

					// If {image} is in the middle, just lump it in with the text
					if (imageIndex == -1 || (imageIndex > 0 && imageIndex < newMessage.length - "{image}".length))
					{
						newMessage = newMessage.replace(/{image}/gi, imageTag).trim();
						newMessage = linkFormat.replace(/{text}/gi, newMessage);
					}
					// If {image} is at the beginning or end, put it in it's own anchor, for aesthetics
					else
					{
						// At the beginning
						if (newMessage.indexOf("{image}") == 0)
						{
							newMessage = linkFormat.replace(/{text}/gi, imageTag).replace(/class=\"/, "class=\"game_icon_link ") + " " + linkFormat.replace(/{text}/gi, newMessage);
						}
						// At the end
						else
						{
							newMessage = linkFormat.replace(/{text}/gi, newMessage) + " " + linkFormat.replace(/{text}/gi, imageTag);
						}
						
						// Remove images from the message
						newMessage = newMessage.replace(/{image}/gi, "");
					}
					
					
					newMessage = newMessage.replace(/{difficulty}/gi, RaidType.difficulty[this.difficulty]);
					newMessage = newMessage.replace(/{text-no-image}/gi, noImage);
					newMessage = newMessage.replace(/{url}/gi, this.getURL());
					

					
					newMessage = "<span class=\"raidMessage\">" + newMessage + "</span>";


					// Get the styles for this link
//					var styles = this.getMatchedStyles();

//					newMessage = newMessage.replace(/{linkStyle}/gi, styles.linkStyle||"");
				}
				catch(ex)
				{
					console.warn("Error encountered in RaidLink.getFormattedRaidLink");
					console.warn(ex);
				}
				
				Timer.stop("getFormattedRaidLink");
				return newMessage;
			},
			
			// Get the raid name
			getName: function()
			{
				return this.getRaid().fullName;
			},
			
			// Get the difficulty text of the raid
			getDifficultyText: function()
			{
				return RaidType.difficulty[this.difficulty];
			},
			
			// Generate a url for this raid
			getURL: function()
			{
				var raidURL = "http://www.kongregate.com/games/5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp";
				
				if (typeof this.raidTypeId != "undefined")
				{
					raidURL += "&kv_raid_boss=" + this.raidTypeId;	
				}
				if (typeof this.difficulty != "undefined")
				{
					raidURL += "&kv_difficulty=" + this.difficulty;
				}
				
				// It's easier to inspect the link if the easier to read bits are first
				raidURL += "&kv_raid_id=" + this.id + "&kv_hash=" + this.hash;
				
				return raidURL;
			},
			
			// Get the raid image url, or default to LoaTS icon
			getImageSRC: function()
			{
				// Assume default
				var imageSRC = RaidLink.defaultImageSRC;
				
				// If we have a raidTypeId
				if (typeof this.raidTypeId !== "undefined")
				{
					// Locate the offsite image
					imageSRC = DC_LoaTS_Properties.lotsCDNUrl + "images/bosses/post/" + this.raidTypeId + "_1.jpg";
				}
				
				return imageSRC;
			},
			
			// Get the fully formatted <img> tag for this raid
			getFormattedImageTag: function()
			{				
				// Get the image src
				var imageSRC = this.getImageSRC();
				
				// Fill in image SRC
				var imageTag = RaidLink.defaultImageFormat.replace("{imageSRC}", imageSRC);
				
				return imageTag;

			},
			
			// Generate a param array for this link
			getParamArray: function()
			{
				return [this.id, this.hash, this.difficulty, this.raidTypeId];
			},
			
			// Is this a valid link (to the best of our knowledge)
			isValid: function()
			{
				// All a link needs to be valid is an id and a hash
				return typeof this.id != "undefined" && typeof this.hash != "undefined" && this.id != "" && this.hash != "";
			},
			
			// Necessary for reloading the iFrame
			toQueryParams: function() {
			    return {
			        kv_action_type: "raidhelp", 
			        kv_raid_boss: this.raidTypeId, 
			        kv_difficulty: this.difficulty, 
			        kv_raid_id: this.id, 
			        kv_hash: this.hash
			    };
			}
			
		});
		
		// Parameter text for this parser
		RaidLink.paramText = "url";

		// Define the regular expression (regex) that tells us if a link is a raid link or not
		RaidLink.linkPattern = /(?:https?:\/\/www\.kongregate\.com)?(?:\/games\/)?(?:5thPlanetGames\/legacy-of-a-thousand-suns)?(?!\?4217\-op)\?([^\s,"]*)\b/i;

		// Define a regular expresson to catch busted links
		RaidLink.backupLinkReplacementPattern = /.?\[?"?http:\/\/.*?\?4217\-op","5thPlanetGames\/legacy\-of\-a\-thousand\-suns\?.*?(?:\u2026|\u8320|…|\.\.\.|\]|"|')*$/i;
		
		// Fallback image url if we can't get the provided one
		RaidLink.defaultImageFormat = '<img class="raidIcon" src="{imageSRC}" onerror="RaidLink.fixBrokenImage.apply(this);" />';
		
		// Fallback image url if we can't get the nice one
		RaidLink.defaultImageSRC = "http://cdn2.kongregate.com/game_icons/0033/2679/i.gif?4217-op";
		
		// Fallback message format
		RaidLink.defaultMessageFormat = "{image} {visited-short} {diff} [{size}-{stat}-{fs}-{os}] {shorter-name} ({id})";
		
		// Old link format
		RaidLink.defaultLinkFormat_v1 = "<a class=\"raidLink raidDiff{difficulty}\" onclick=\"return DC_LoaTS_Helper.raidLinkClick(event, '{url}');\" href=\"{url}\" title=\"{text-no-image}\">{text}</a>";
		
		// Fallback link format
		RaidLink.defaultLinkFormat_v2 = "<a style=\"{linkStyle}\" class=\"raidLink raidDiff{difficulty}\" onclick=\"return DC_LoaTS_Helper.raidLinkClick(event);\" onmousedown=\"return DC_LoaTS_Helper.raidLinkMouseDown(event);\" href=\"{url}\" title=\"{text-no-image}\">{text}</a>";
		
		// Fix broken images, an inline handler
		RaidLink.fixBrokenImage = function()
		{
			// Get the relevant raid link
			var raidLink = new RaidLink(this.parentNode.href);

			// First time failed, check for alternate fail names
			if (this.src === DC_LoaTS_Properties.lotsCDNUrl + "images/bosses/post/" + raidLink.raidTypeId + "_1.jpg" && this.src !== RaidLink.defaultImageSRC)
			{
				var src = DC_LoaTS_Properties.lotsCDNUrl + "images/bosses/";
				switch(raidLink.raidTypeId)
				{
					// baseURL + "post/" + raidLink.raidTypeId + ".jpg";
					case "celebration_enhancer_1":
					case "weiqi_game_1":
					case "kulnarxex_scout_ships_1":
                    case "cow_abduction_1":
					case "mutated_spacepox_1":
                        src += "post/" + raidLink.raidTypeId + ".jpg";
                        break;
					// Raids with diviating image paths 
					case "wr_space_pox":
						src += "post/space_pox_1.jpg";
						break;
                    case "dule_warmaster_1":
                    case "dule_warmaster":
						src += "post/dule_1.jpg";
						break;
					case "hultex_quibberath":
						src += "post/hultex_1.jpg";
						break;
					case "warden_ramiro":
						src += "post/ramiro_1.jpg";
						break;
                    case "crimzo_the_killer_clown":
                        src += "post/crimzo_1.jpg";
                        break;
                    case "kulnarxex_subjugator_1":                       
                    case "kulnarxex_elite_subjugator_1":
					case "elite_kulnarxex_elite_subjugator":
                        src += "post/kulnarxex_subjugator_1.jpg";
                        break;
					case "elite_birthday_cake_of_doom":
						src += "post/birthday_cake_of_doom_1.jpg";
						break;
					case "elite_centurian_commander":
						src += "post/commander_1.jpg";
						break;
					case "elite_master_hao":
						src += "post/master_hao_1.jpg";
						break;
					case "elite_bashan":
						src += "post/bashan_1.jpg";
						break;
                    // These are ones that we've not found alternate images for except for the raid list image
                    case "pinatas_revenge1":
                    case "king_krandar1":
                    case "sultan_shrakzan1":
                    case "contest_winner1":
                    case "thyestean_banquet1":
					case "rogue_terraformer1":
					case "ruins_of_the_forgotten1":
					case "rak_thun_eviscipod1":
					case "contest_winner1":
					case "temynx_parasite1":
					case "robot_uprising1":
					case "besalaad_exhibit_rampage1":
						src += raidLink.raidTypeId + ".$$repl$$";
						src = src.replace("1.$$repl$$", "_small.jpg");
						break;
					// Raids with image paths that don't fit known patterns
					case "invaders_from_dimension_b":
						src += "Invaders_from_Dimension_B1_small.png";
						break;
					case "training_sim1":
						src += "training_sim_small2.jpg";
						break;
					case "predatory_constellation":
						src += "predatory_constellation_small.png"; //PNG!
						break;
					case "attack_of_the_gourds":
						src += "Attack_of_the_Gourds_small.jpg";
						break;
					case "sun_egg":
						src += "the_sun_egg_small.jpg";
						break;
					case "hate":
						src += "the_hate_walker_small.jpg";
						break;
					case "hukkral_war_crawler":
						src += "huk-kral_war_crawler_small.jpg";
						break;
					case "elite_titanomachy":
						src += "z27_boss_small.jpg"; //omg what 5pg?
						break;
					case "giant_kwelshax":
					case "elite_giant_kwelshax":
						src += "giant_kwelshax_small.jpg";
						break;
                    case "purple_lion":
					case "kang":
					case "tourniquet":
					case "flora":
                    case "talia":
                    case "myrmexidaks":
                    case "tyraness_guard":
                    case "sian_dragonfly_1":
                    case "lady_victoria_ashdown_1":
                    case "rampaging_rackalax_1":
                    case "the_tyraness":
                    case "the_mega_mimes":
                    case "the_neon_knights":
                    case "the_gamma_hammers":
                    case "the_chem_runners":
                    case "kulnar_xex_shock_trooper_1":
                    case "kulnar_xex_battle_station_1":
                    case "kulnarxex_bombarder_1":
                    case "schism":
                    case "trouble_in_tokyo":
                    case "cerebral_monster_mech":
                    case "ship_pinata":
                    case "dimetrodon_riot":
					default:
                        src += raidLink.raidTypeId + "_small.jpg";
				}
				this.src = src;
			}
			// Second time failed, switch to default
			else if (this.src != RaidLink.defaultImageSRC)
			{
				this.src = RaidLink.defaultImageSRC;
			}
			// Default failed, no image
			else
			{
				this.src="";
			}
		};
		/************************************/
		/**** RaidLinkstateParser Class *****/
		/************************************/
		
		window.RaidLinkstateParser = Class.create({
			initialize: function(params)
			{
				var paramsParts = params.trim().replace(/\s+/g, " ").split(" ");
				var ret;
				if (paramsParts.length != 1 && paramsParts.length != 2)
				{
					ret.success = false;
					ret.statusMessage = "Could not understand <code>" + params + "</code>. Should have one or two parameters: url and optionally state.";
				}
				else
				{
					this.raidLink = new RaidLink(paramsParts[0]);
					this.state;
					var raidLinkIndex = 0;
					if (!this.raidLink.isValid())
					{
						if (paramsParts.length == 2)
						{
							this.raidLink = new RaidLink(paramsParts[1]);
							raidLinkIndex = 1;
						}
					}
					
					if (!this.raidLink.isValid())
					{
						ret.success = false;
						ret.statusMessage = "Could not parse raid link from <code>" + params + "</code>";
					}
					else
					{
						if (paramsParts.length == 2)
						{
							this.state = paramsParts[raidLinkIndex*-1+1];
						}
						
						DCDebug("LinkState: ");
						DCDebug("RaidLink for " + this.raidLink.getName());
						DCDebug("State Param: " + this.state);
					}
				}
				
				return ret;
			},
			
			getName: function()
			{
				return (typeof this.raidLink != "undefined")?this.raidLink.getName():"undefined";
			},
			
			getState: function()
			{
				return this.state;
			},
			
			isValid: function()
			{
				return this.raidLink != "undefined" && this.raidLink.isValid();
			}
		});
		
		// Parameter text for this parser
		RaidLinkstateParser.paramText = "url [newState]";

		
		/************************************/
		/******** RaidManager Utility *******/
		/************************************/
		
		// The RaidManager utility stores information about
		// instances of raids that the user has seen/joined
		// It is not actually a class, but rather just a 
		// collection of static functions
		window.RaidManager = {
			// STATE Enum
			STATE: {
				// The ids below are critical. Please do not ever change the ids, ever.
				// If you create custom states, please set the ids of the new states above 20 
				// to allow for new native ids in the future.
				// Every state with id below 0 is an unknown state
				// Note: Priority may be removed or altered in future releases
				//TODO: Maybe add an equals method to these in init? RaidManager.STATE.equals is a pain
				UNSEEN:  	{id:0, text: "unseen", 		niceText: "Unseen", 		shortText: "(NEW)",	priority: 0},
				//DB_ONLY: 	{id:1, text: "db_only", 	niceText: "From Database", 	shortText: "(DB)",	priority: 1},
				SEEN:    	{id:2, text: "seen", 		niceText: "Seen", 			shortText: "(S)",	priority: 2},
				VISITED: 	{id:3, text: "visited", 	niceText: "Visited", 		shortText: "(V)", 	priority:5},
				IGNORED: 	{id:4, text: "ignored", 	niceText: "Ignored", 		shortText: "(X)", 	priority:10},
				COMPLETED: 	{id:5, text: "completed", 	niceText: "Completed", 		shortText: "(!)", 	priority:99},
				
				// Takes in a state.text or a state.id and looks up the corresponding state
				// Returns a STATE if one is found, undefined otherwise
				/*public static STATE*/ 
				valueOf: function(stateParam)
				{
					// Set up the cache for ids, if it's not already set up
					if (!RaidManager.STATE.cacheById) {
						RaidManager.STATE.cacheById = {};
						for (var stateKey in this)
						{
							var item = this[stateKey];
							// Ignore functions. Check for the state.id or state.text to equal the passed in value
							if (item && item !== "function")
							{
								RaidManager.STATE.cacheById[item.id] = item;
							}
						}
					}
					
					// State we found
					var state;
					
					// If the parameter was a string
					if (typeof stateParam === "string")
					{
						// lowercase it just in case
						stateParam = stateParam.toLowerCase();
					}
					// Otherwise return from the id cache
					else
					{
						return RaidManager.STATE.cacheById[stateParam];
					}
					
					// Iterate over all valid states
					for (var stateKey in this)
					{
						// Ignore functions. Check for the state.id or state.text to equal the passed in value
						if (this[stateKey] && typeof this[stateKey] !== "function"
							&& (this[stateKey].id == stateParam || this[stateKey].text == stateParam)
						   )
						{
							// If we found a state that matches, capture it and break the loop
							state = this[stateKey];
							break;
						}
						else if (typeof this[stateKey] === "undefined")
						{
							console.warn("Invalid State:", stateKey);
						}
					}
					
					// Return the state we found, or undefined if we didn't find one
					return state;
				},
				
				// Returns the UNKNOWN state. Not listed as enumerable on purpose to avoid listing when iterated over
				/*public static STATE*/
				getUnknownState: function()
				{
					return {id: -1, text: "unknown", niceText: "Unknown", shortText: "(?)", priority: -1};
				},
				
				// Compares two states and returns true if they are equal, false otherwise
				/*public static boolean*/ 
				equals: function(state1, state2)
				{
					return (state1 == state2 || 
							(typeof state1 != "undefined" && typeof state2 != "undefined" && 
								(
									(typeof state1.id != "undefined" && state1.id == state2.id)
									||
									(typeof state1.text != "undefined" && state1.text == state2.text)
								)
							)
						   );
				}
			},
			
			// Initialize the raid manager. MUST BE INITIALIZED before it can be used
			/*public static void*/
			init: function()
			{
				Timer.start("RaidManager init");
				
				// Load up everything that's been stored
				var rawRaidStorage = GM_getValue(DC_LoaTS_Properties.storage.raidStorage);
				
				// Make sure the raid storage actually finds what it's supposed to
				if (typeof rawRaidStorage == "undefined" || rawRaidStorage.length == 0)
				{
					// Otherwise default to empty
					rawRaidStorage = "{}";
				}
				
				try
				{
					RaidManager.raidStorage = JSON.parse(rawRaidStorage);
				}
				catch(ex)
				{
					try
					{
						holodeck.activeDialogue.raidBotMessage("Raid link storage has become corrupted and will now be cleared. RaidBot apologizes for the inconvenience. If this happens frequently, please report it.");
					}
					catch (ex2)
					{
						console.warn("Could not display raid bot message to user: Raid link storage has become corrupted and will now be cleared. RaidBot apologizes for the inconvenience. If this happens frequently, please report it.");
						console.warn(ex2);
					}
					
					console.warn("rawRaidStorage was not able to be parsed. For debugging purposes, it will now been quarantined, and the normal raid link storage will be cleared.");
					console.warn(ex)
					
					// Quarantine the current raid storage
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage + DC_LoaTS_Properties.storage.quarantine, rawRaidStorage);
					
					// Clear the existing corrupted storage
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage, "{}");
					RaidManager.raidStorage = {};

				}
				
				// Count raids we're about to delete
				var clearCount = 0;
				
				// Iterate over all stored data
				for (var key in RaidManager.raidStorage)
				{
					// Grab this raidData item
					var raidData = RaidManager.raidStorage[key];
					
					// The raidData item is actually RaidLink, but storage doesn't retain methods. Add back in the methods
					Object.extend(raidData.raidLink, RaidLink.prototype);
					
					// Clear items that have been stored for more than their total raid length
					if ((new Date()/1) - raidData.firstSeen > raidData.raidLink.getRaid().time * 60*60*1000)
					{
						delete RaidManager.raidStorage[key];
						clearCount++;
					}
				}

				// If anything was cleared, log it for posterity
				if (clearCount > 0)
				{
					console.info("Cleared " + clearCount + " raid" + ((clearCount != 1)?"s":"") + " for being too old");
				}
				
				// Store back into the DB any modifications we made
				GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify(RaidManager.raidStorage));
				
				// Update raid preferences
				RaidManager.updateRaidPreferences();
				
				Timer.stop("RaidManager init");
			},
			
			// Update the script with the user's preferences
			// This handles the customized bits of text loaded from memory
			/*public static void*/
			updateRaidPreferences: function()
			{
				// Load up the raid preferences
				var raidPrefs = GM_getValue(DC_LoaTS_Properties.storage.raidPrefs);
				
				// Make sure the raid prefs are populated
				if (typeof raidPrefs != "undefined" && raidPrefs.length > 0)
				{
					RaidManager.raidPrefs = JSON.parse(raidPrefs);
					
					// Iterate over every preference type
					for (var key in RaidManager.raidPrefs)
					{
						var pref = RaidManager.raidPrefs[key];
						
						// Iterate over every variable to change in that preference.
						for (var variable in pref)
						{
							try
							{
								eval(variable + "=" + pref[variable]);
							}
							catch (ex)
							{
								console.warn("Could not update `" + variable + "` to be \"" + pref[variable] + "\".")
								console.warn(ex);
							}
						}
					}
				}
				
				
				

			},
			
			// Clear everything from storage
			/*public static void*/
			clear: function(raidList)
			{
				Timer.start("clear");
				// If there was no list passed in, clear them all
				if (typeof raidList == "undefined")
				{
					// Replace the entire stored dataset with an empty object
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify({}));
					
					// Also clear the memcache
					RaidManager.raidStorage = {};
				}
				else
				{
					// Iterate over everything we need to delete
					for (var i = 0; i < raidList.length; i++)
					{
						delete RaidManager.raidStorage[raidList[i].getUniqueKey()];
					}
					
					// Store the storage data back into the browser storage
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify(RaidManager.raidStorage));
				}
				
				// Reset the CConoly query time so all the raids can be loaded again
				CConolyAPI.setLastQueryTime(0);
				
				// Reset all the links to NEW
				DC_LoaTS_Helper.updatePostedLinks();
				
				Timer.stop("clear");
			},
			
			// Store a raid link and the state of the link
			// RaidManager.raidStorage is a write-through cache, and the storage is volatile
			// So we have to look up the storage every time we store. This keeps us in sync with
			// other windows of the same browser running the game simultaneously
			/*public static void*/
			store: function(raidLink, state)
			{
				Timer.start("store");
				// If the link is valid
				if (raidLink.isValid())
				{
					Timer.start("store > loading hardRaidStorage");
					// Load up the storage object
					var hardRaidStorage = JSON.parse(GM_getValue(DC_LoaTS_Properties.storage.raidStorage));
					Timer.stop("store > loading hardRaidStorage");
					
					// Attempt to load the passed in raid link
					var raidData = hardRaidStorage[raidLink.getUniqueKey()];
					
					// Lookup the current state
					var currentState;
					var containedInLocalDB = true;
					if (typeof raidData != "undefined")
					{
						// If there is a stateId, use that first
						if (typeof raidData.stateId != "undefined")
						{
							currentState = RaidManager.STATE.valueOf(raidData.stateId);
						}
						// If there is an old-style state, use that second
						else if (typeof raidData.state != "undefined")
						{
							currentState = RaidManager.STATE.valueOf(raidData.state.text);
						}
					}
					
					// If we couldn't find the current state, set it to UNSEEN
					if (typeof currentState === "undefined")
					{
						currentState = RaidManager.STATE.UNSEEN;
						containedInLocalDB = false;
					}
					
					// If we weren't provided a state param, set it to the current state
					if (typeof state === "undefined")
					{
						state = currentState;
					}
					
					// Keep track of whether or not this link needs to be updated elsewhere
					var shouldUpdateAllLinks = false;
					
					// If we've never seen this link before
					if (typeof raidData == "undefined")
					{
						// Create a new storage container for it, and wrap it
						raidData = {raidLink: raidLink, stateId: state.id, firstSeen: new Date()/1}
						
						// Place this object into the storage
						hardRaidStorage[raidLink.getUniqueKey()] = raidData;						
					}
					// Two unseens upgrade to seen if the link was already in the DB
					else if (containedInLocalDB
							 &&
								RaidManager.STATE.equals(state, RaidManager.STATE.UNSEEN)
								&& 
								RaidManager.STATE.equals(currentState, RaidManager.STATE.UNSEEN)
							 )
					{
					        // Set the new state
					        raidData.stateId = RaidManager.STATE.SEEN.id;
					       
					        // Changed state
					        shouldUpdateAllLinks = true;
					}
					// If we have seen this link before, change the links state if necessary
					else if (!RaidManager.STATE.equals(currentState, state))
					{
						// Set the new state
						raidData.stateId = state.id;
						
						// Since we changed state, need to update all those links
						shouldUpdateAllLinks = true;
					}
					else
					{
						// Just double check to make sure the state id has been set
						// Helps convert old states to new ones
						raidData.stateId = currentState.id;
					}
					
					// If we should report dead raids and this one is dead and it hasn't been reported yet
					if (DC_LoaTS_Helper.getPref("ReportDeadRaids", true) && RaidManager.STATE.equals(state, RaidManager.STATE.COMPLETED) && !raidData.reported) {
						raidData.reported = true;
						DC_LoaTS_Helper.reportDead(raidLink);
					}
					
					// Update the lastSeen time of the link
					raidData.lastSeen = new Date()/1;
					
					Timer.start("store > storing hardRaidStorage");
					// Store the storage data back into the browser storage
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify(hardRaidStorage));
					Timer.stop("store > storing hardRaidStorage");
					
					Timer.start("store > extending raid links");
					//TODO Work around this (update: not really that big of a deal based on timer data)
					// Gotta have the methods attached to the objects
					for (var key in hardRaidStorage)
					{
						// If we're missing methods, add them
						if (typeof hardRaidStorage[key].raidLink.getRaid != "function")
						{
							Object.extend(hardRaidStorage[key].raidLink, RaidLink.prototype);		
						}
					}
					Timer.stop("store > extending raid links");
					
					// Update the cache
					RaidManager.raidStorage = hardRaidStorage;
					
					// If we found a reason to update all versions of this link
					if (shouldUpdateAllLinks)
					{
						// Update the posted links
						DC_LoaTS_Helper.updatePostedLinks(raidLink);
					}
				}
				else
				{
					console.warn("Did not store because link was invalid or storage unavailable");
				}
				
				Timer.stop("store");
			},
			
			// Store a list of raid links and the state of the links
			// RaidManager.raidStorage is a write-through cache, and the storage is volatile
			// So we have to look up the storage every time we store. This keeps us in sync with
			// other windows of the same browser running the game simultaneously
			/*public static void*/
			storeBulk: function(raidLinks, state)
			{
				Timer.start("store bulk");

				// Load up the storage object
				Timer.start("store > loading hardRaidStorage");
				var hardRaidStorage = JSON.parse(GM_getValue(DC_LoaTS_Properties.storage.raidStorage));
				Timer.stop("store > loading hardRaidStorage");
				
				// Declare a bunch of vars. Don't forget there's no such thing as block scope
				var raidLink, raidData, currentState, newState, containedInLocalDB, shouldUpdateAllLinks;
				
				// Capture all the valid links we're actually going to store
				var outboundLinks = [];
				
				for (var i = 0; i < raidLinks.length; i++) {
					
					raidLink = raidLinks[i];
					state = undefined;
					
					// Valid link?
					if (raidLink.isValid()) {
						
						// Remember the valid ones
						outboundLinks.push(raidLink);
					
						// Attempt to load the passed in raid link
						raidData = hardRaidStorage[raidLink.getUniqueKey()];
						
						// Lookup the current state
					    containedInLocalDB = true;
					    currentState = null;
						if (raidData)
						{
							// If there is a stateId, use that first
							if (raidData.stateId)
							{
								currentState = RaidManager.STATE.valueOf(raidData.stateId);
							}
							// If there is an old-style state, use that second
							else if (raidData.state)
							{
								currentState = RaidManager.STATE.valueOf(raidData.state.text);
							}
						}
						
						// If we couldn't find the current state, set it to UNSEEN
						if (!currentState)
						{
							currentState = RaidManager.STATE.UNSEEN;
							containedInLocalDB = false;
						}
						
						// Set the new state. It's either going to be the new parameter state or the existing state
						newState = state || currentState;
						
						// Keep track of whether or not this link needs to be updated elsewhere
						shouldUpdateAllLinks = false;
						
						// If we've never seen this link before
						if (!raidData)
						{
							// Create a new storage container for it, and wrap it
							raidData = {raidLink: raidLink, stateId: newState.id, firstSeen: new Date()/1}
							
							// Place this object into the storage
							hardRaidStorage[raidLink.getUniqueKey()] = raidData;						
						}
						// Two unseens upgrade to seen if the link was already in the DB
						else if (containedInLocalDB
								 &&
									RaidManager.STATE.equals(newState, RaidManager.STATE.UNSEEN)
									&& 
									RaidManager.STATE.equals(currentState, RaidManager.STATE.UNSEEN)
								 )
						{
						        // Set the new state
						        raidData.stateId = RaidManager.STATE.SEEN.id;
						       
						        // Changed state
						        shouldUpdateAllLinks = true;
						}
						// If we have seen this link before, change the links state if necessary
						else if (!RaidManager.STATE.equals(currentState, newState))
						{
							// Set the new state
							raidData.stateId = newState.id;
							
							// Since we changed state, need to update all those links
							shouldUpdateAllLinks = true;
						}
						else
						{
							// Just double check to make sure the state id has been set
							// Helps convert old states to new ones
							raidData.stateId = currentState.id;
						}
												
						// Update the lastSeen time of the link
						raidData.lastSeen = new Date()/1;
						
						// If we should report dead raids and this one is dead and it hasn't been reported yet
						if (DC_LoaTS_Helper.getPref("ReportDeadRaids", true) && RaidManager.STATE.equals(newState, RaidManager.STATE.COMPLETED) && !raidData.reported) {
							raidData.reported = true;
							DC_LoaTS_Helper.reportDead(raidLink);
						}
					}
				} // End for iterating over the links
				
				Timer.start("store > storing hardRaidStorage");
				// Store the storage data back into the browser storage
				GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify(hardRaidStorage));
				Timer.stop("store > storing hardRaidStorage");
				
				Timer.start("store > extending raid links");
				//TODO Work around this (update: not really that big of a deal based on timer data)
				// Must have the methods attached to the objects
				for (var key in hardRaidStorage)
				{
					// If we're missing methods, add them
					if (typeof hardRaidStorage[key].raidLink.getRaid !== "function")
					{
						Object.extend(hardRaidStorage[key].raidLink, RaidLink.prototype);		
					}
				}
				Timer.stop("store > extending raid links");
				
				// Update the cache
				RaidManager.raidStorage = hardRaidStorage;
				
				// Update the posted links. 
				DC_LoaTS_Helper.updatePostedLinks();
				
				Timer.stop("store bulk");
				
				return outboundLinks;
			},
			
			// Lookup RaidData for a given link
			/*public static RaidData*/
			fetch: function(raidLink)
			{
				Timer.start("fetch");
				
				// Declare the return var
				var raidData;
				
				if (raidLink.isValid())
				{
					// Attempt to load the passed in raid link
					raidData = RaidManager.raidStorage[raidLink.getUniqueKey()];
										
					// If the link is in storage
					if (raidData && typeof raidData.raidLink.getRaid !== "function")
					{
						// Add in the functions that you expect to see on those objects
						Object.extend(raidData.raidLink, RaidLink.prototype);
					}
				}
				
				Timer.stop("fetch");
				
				// Return what we found or undefined
				return raidData;
			},
			
			// Returns the raid storage
			/*public static Object*/
			fetchStorage: function()
			{
				// Return the whole thing
				return RaidManager.raidStorage;
			},
			
			// Returns the all stored raid links
			/*public static Array*/
			fetchAll: function()
			{
				// Holder for raid links
				var raidLinks = new Array();
				
				// For everything in storage
				for (var key in RaidManager.raidStorage)
				{
					// Pull the raid data
					var raidData = RaidManager.raidStorage[key];
					
					// It should exist, but just in case
					if (typeof raidData != "undefined")
					{
						// Collect the links into the array
						raidLinks.push(raidData.raidLink);
					}
					
				}
				
				// Return all the links
				return raidLinks;
			},
			
			// Returns the state of a link, or UNSEEN if the link hasn't been stored
			/*public static STATE*/
			fetchState: function(raidLink)
			{
				// Attempt to load the raid link
				var raidData = RaidManager.fetch(raidLink);
				
				// If the raid link has been seen before
				if (typeof raidData !== "undefined")
				{
					if (typeof raidData.stateId !== "undefined")
					{
						// Return its state
						return RaidManager.STATE.valueOf(raidData.stateId);
					}
					else if (typeof raidData.state !== "undefined")
					{
						// Return its state
						return RaidManager.STATE.valueOf(raidData.state.text);
					}
				}
				
				// Since we haven't seen it, return UNSEEN
				return RaidManager.STATE.UNSEEN;
			},
			
			// Parameterized command line raid lookup
			/*public static Array*/
			fetchByFilter: function(filterParam)
			{
				Timer.start("fetchByFilter");
				try 
				{
					var raidFilter;
					
					// If we got text and not a RaidFilter
					if (typeof filterParam == "string")
					{
						// Parse the command into a RaidFilter
						raidFilter = new RaidMultiFilter(filterParam);
					}
					// We got something other than text. Assume it's a RaidFilter
					else if (filterParam instanceof RaidFilter || filterParam instanceof RaidMultiFilter)
					{
						// filterParam was already a raidFilter
						raidFilter = filterParam;
					}
					else
					{
						console.warn("Could not fetch by filter " + filterParam + ". Parameter must be a String or RaidFilter");
						return;
					}
					
					// If the command makes a valid filter
					if (raidFilter.isValid())
					{
						// Collect all matching raid links
						var raidLinks = new Array();
						
						// If there was no name or difficulty
						if (raidFilter.isEmpty())
						{
							// Get all raid links
							raidLinks = RaidManager.fetchAll();
						}
						// If we found a raid name, difficulty, or state 
						else 
						{
							// Get all raids
							var raidStorage = RaidManager.fetchStorage();
							
							// Count of raids seen
							var raidCount = 0;
							
							// Count of raids seen
							var resultsPage = 1;
							
							// Start time of the run
							var commandStartTime = (new Date() / 1);
							
							// Iterate over all raids
							for (var key in raidStorage)
							{
								// Get the raid data from storage
								var raidData = raidStorage[key];
								
								// Get the current raidLink
								var raidLink = raidData.raidLink;
								
								// Get the state of the link
								var currentState;
								if (typeof raidData.stateId != "undefined")
								{
									currentState = RaidManager.STATE.valueOf(raidData.stateId);
								}
								else if (typeof raidData.state != "undefined" && typeof raidData.state.text != "undefined")
								{
									currentState = RaidManager.STATE.valueOf(raidData.state.text);
								}
								
								if (typeof currentState == "undefined")
								{
									console.warn("Could not locate a state for " + raidLink.getSimpleText() + ". This may cause unexpected matching behavior.");
								}
								
								try
								{
									// If this link matches the filter
									if (raidFilter.matches(
										{
											age: commandStartTime - raidData.firstSeen,
											difficulty: raidLink.difficulty,
											fs: raidLink.getRaid().getFairShare(raidLink.difficulty),
											os: raidLink.getRaid().getOptimalShare(raidLink.difficulty),
											name: raidLink.getRaid().getSearchableName(),
											state: currentState,
											count: raidCount,
											size: raidLink.getRaid().size,
											zone: raidLink.getRaid().zone
										}
										))
									{
										//TODO: Improved Sorting
										// If we don't have a defined page, or we're on the right page, or we don't care about the count
										if (typeof raidFilter.page == "undefined" || resultsPage == raidFilter.page || typeof raidFilter.count == "undefined")
										{
											// Put seen links at the end
											if (RaidManager.STATE.equals(currentState, RaidManager.STATE.SEEN))
											{
												raidLinks.push(raidLink);
											}
											// Put visited and other links up top
											else
											{
												raidLinks.unshift(raidLink);
											}
										}
										
										// Keep track of how many raids match the query so we can deal with pagination
										raidCount++;
										if (raidFilter.count != "undefined" && raidCount % raidFilter.count == 0) {resultsPage++;raidCount=0;}
										
										// Once we've collected enough links, bail
										// If count is not set, we'll only break when we've iterated over all raids
										if (raidLinks.length == raidFilter.count)
										{
											break;
										}
									}
								}
								catch(ex)
								{
									console.warn(ex);
									console.warn("Failure while trying to match ");
									console.warn(
										{
											age: commandStartTime - raidData.firstSeen,
											difficulty: raidLink.difficulty,
											fs:  raidLink.getRaid().getFairShare(raidLink.difficulty),
											name: raidLink.getRaid().getSearchableName(),
											state: currentState,
											count: raidCount
										}
									);
								}
							}
						}
						
						Timer.stop("fetchByFilter");
	
						// Return all found links
						return raidLinks;
					}
				}
				catch (ex)
				{
					console.warn("Failed to lookup raids by ");
					console.warn(filterParam);
					console.warn(ex);
				}
				Timer.stop("fetchByFilter");
			},
			
			markByFilter: function(filter, state) {
				Timer.start("markByFilter");
				
				if (typeof filter === "string") {
					filter = new RaidMultiFilter(filter);
				}
				
				if (typeof state === "string") {
					state = RaidManager.STATE.valueOf(state.toUpperCase());
				}
				
				// Count of raids seen
				var raidCount = 0;
				
				// If the command makes a valid filter
				if (filter.isValid())
				{
					// Get all raids
					Timer.start("markByFilter > loading hardRaidStorage");
					// Load up the storage object
					var raidStorage = JSON.parse(GM_getValue(DC_LoaTS_Properties.storage.raidStorage));
					Timer.stop("markByFilter > loading hardRaidStorage");
					
					// Count of raids seen
					var resultsPage = 1;
					
					// Start time of the run
					var commandStartTime = (new Date() / 1);
					
					// Iterate over all raids
					for (var key in raidStorage)
					{
						// Get the raid data from storage
						var raidData = raidStorage[key];
						
						// Get the link from the data
						var raidLink = raidData.raidLink;
						
						// Convert to RaidLink
						Object.extend(raidLink, RaidLink.prototype);
						
						// Get the state of the link
						var currentState;
						if (typeof raidData.stateId != "undefined")
						{
							currentState = RaidManager.STATE.valueOf(raidData.stateId);
						}
						else if (typeof raidData.state != "undefined" && typeof raidData.state.text != "undefined")
						{
							currentState = RaidManager.STATE.valueOf(raidData.state.text);
						}
						
						if (typeof currentState == "undefined")
						{
							console.warn("Could not locate a state for " + raidLink.getSimpleText() + ". This may cause unexpected matching behavior.");
						}
						
						try
						{
							// If this link matches the filter
							if (filter.matches(
								{
									age: commandStartTime - raidData.firstSeen,
									difficulty: raidLink.difficulty,
                                    fs:  raidLink.getRaid().getFairShare(raidLink.difficulty),
                                    os:  raidLink.getRaid().getOptimalShare(raidLink.difficulty),
									name: raidLink.getRaid().getSearchableName(),
									state: currentState,
									count: raidCount,
									size: raidLink.getRaid().size,
									zone: raidLink.getRaid().zone
								}
								))
							{
								raidData.stateId = state.id;
								
								// Keep track of how many raids match the query so we can deal with pagination
								raidCount++;
								if (filter.count != "undefined" && raidCount % filter.count == 0) {resultsPage++;raidCount=0;}
								
								// Once we've changed enough links, bail
								// If count is not set, we'll only break when we've iterated over all raids
								if (raidCount == filter.count)
								{
									break;
								}
							}
						}
						catch(ex)
						{
							console.warn(ex);
							console.warn("Failure while trying to match ");
							console.warn(
								{
									age: commandStartTime - raidData.firstSeen,
									difficulty: raidLink.difficulty,
									fs:  raidLink.getRaid().getFairShare(raidLink.difficulty),
									name: raidLink.getRaid().getSearchableName(),
									state: currentState,
									count: raidCount
								}
							);
						}
					}
					
					Timer.start("markByFilter > storing hardRaidStorage");
					// Store the storage data back into the browser storage
					GM_setValue(DC_LoaTS_Properties.storage.raidStorage, JSON.stringify(raidStorage));
					Timer.stop("markByFilter > storing hardRaidStorage");
				}
				
				Timer.stop("markByFilter");
				
				return raidCount;
			}
		}

		/************************************/
		/********** RaidMenu Class **********/
		/************************************/
		
		// TODO: RaidMenu coming soon!
		// Class to manage a popup raid menu
		// There can only be a single raid menu at a time. The constructor will enforce this
		window.RaidMenu = Class.create({
			initialize: function()
			{
				// Find the existing RaidMenu
				this.container = document.getElementById("DC_LoaTS_raidMenu");
				
				// If a RaidMenu doesn't exist yet, make it
				if (typeof this.container == "undefined" || this.container == null)
				{
					// Hooks to register and unregister
					this._startDragHook = this._startDrag.bind(this);
					this._performDragHook = this._performDrag.bind(this);
					this._stopDragHook = this._stopDrag.bind(this);

					this.container = document.createElement("div");
					this.container.id = "DC_LoaTS_raidMenu";
					$(this.container).hide();
					document.body.appendChild(this.container);
					
					this.titleBarWrapper = document.createElement("div");
					this.titleBarWrapper.id = "DC_LoaTS_raidMenuTitleBarWrapper";
					this.titleBarWrapper.className = "clearfix";
					this.container.appendChild(this.titleBarWrapper);
					
					this.titleBar = document.createElement("div");
					this.titleBar.id = "DC_LoaTS_raidMenuTitleBar";
					this.titleBarWrapper.appendChild(this.titleBar);
					DC_LoaTS_Helper.registerEventHandler(this.titleBar, "mousedown", this._startDragHook);
					
					var titleBarLeft = document.createElement("div");
					titleBarLeft.id = "DC_LoaTS_raidMenuTitleBarLeft";
					titleBarLeft.appendChild(document.createTextNode("LoaTS Helper Menu"));
					this.titleBar.appendChild(titleBarLeft);
					
					this.titleBarCenter = document.createElement("div");
					this.titleBarCenter.id = "DC_LoaTS_raidMenuTitleBarCenter";
					this.titleBar.appendChild(this.titleBarCenter);
					
					var titleBarRight = document.createElement("div");
					titleBarRight.id = "DC_LoaTS_raidMenuTitleBarRight";
					this.titleBar.appendChild(titleBarRight);


					// Set up the close button
					this.titleBarClose = document.createElement("img");
					this.titleBarClose.id = "DC_LoaTS_raidMenuClose";
					this.titleBarClose.src = "https://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/base.png";					
					this.titleBarClose.setAttribute("usemap", "#raidMenuCloseMap");
					this.titleBarWrapper.appendChild(this.titleBarClose);
					
					// We only want the hover effect to work on the red triangle, so we'll need a click map
					this.titleBarCloseMap = document.createElement("map");
					this.titleBarCloseMap.name = "raidMenuCloseMap";
					this.titleBarCloseMap.id = "raidMenuCloseMap";
					this.titleBarWrapper.appendChild(this.titleBarCloseMap);
					
					// Define the click map for the triangle close image.
					// This is the area that responds to clicks and hover effects
					var titleBarCloseArea = document.createElement("area");
					titleBarCloseArea.shape = "poly";
					titleBarCloseArea.coords = "12,6,50,42,50,6,46,1,42,0,19,-1";
					titleBarCloseArea.href = "#";
					titleBarCloseArea.setAttribute("onclick", "RaidMenu.toggle(); return false;");
					titleBarCloseArea.setAttribute("onmouseover", "$('DC_LoaTS_raidMenuClose').src = 'https://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/hover.png';");
					titleBarCloseArea.setAttribute("onmouseout", "$('DC_LoaTS_raidMenuClose').src = 'https://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/base.png';");
					this.titleBarCloseMap.appendChild(titleBarCloseArea);

					
					// This is where the panes go
					this.bodyWrapper = document.createElement("div");
					this.bodyWrapper.id = "DC_LoaTS_raidMenuBodyWrapper";
					this.container.appendChild(this.bodyWrapper);
					
					// This is where the tabs go
					this.tabsList = document.createElement("ul");
					this.tabsList.id = "DC_LoaTS_raidMenuTabs"
					this.bodyWrapper.appendChild(this.tabsList);


					// Activate tab container
					this.tabs = new Control.Tabs('DC_LoaTS_raidMenuTabs');

					// Holder for activated tabs
					this.activatedTabs = {};
					
					// Activate tabs
					this._activateTabs();
				}
			},
			
			// Resorts the tabs according to their position
			// TODO: RaidMenu should probably have an addTab() where it manages this
			resortTabs: function() {
				
				var tabs = this.tabsList.getElementsByTagName("li");
				console.log(tabs);
				
			},
			
			// Toggles the visibility of the raid menu
			/*public void*/
			toggle: function()
			{
				this.container.toggle();
			},
			
			// Show the raid menu
			/*public void*/
			show: function()
			{
				this.container.show();
			},
			
			// Hide the raid menu
			/*public void*/
			hide: function()
			{
				this.container.hide();
			},
			
			// Activates the tab classes. Probably don't call this except once in initialize
			/*private void*/
			_activateTabs: function()
			{
				// Create all the tabs
				for (var tabPosition in RaidMenu.tabClasses)
				{
					try 
					{
						this.activatedTabs[tabPosition] = new RaidMenu.tabClasses[tabPosition](this);
					}
					catch (ex)
					{
						console.warn("Error activating tab in position " + tabPosition);
						console.warn(ex);
					}
				}
				
				// Activate first tab
				this.tabs.first();
			},
			
			activateTab: function(tabClass) {
				this.activatedTabs[tabClass.tabPosition] = new tabClass(this);
			},

            setActiveTab: function(tabClass) {
                this.tabs.setActiveTab(this.activatedTabs[tabClass.tabPosition].tabA);
            },
			
			// Event fired as the menu title has been clicked
			/*private void*/	
			_startDrag: function(e)
			{
				// Half-hearted IE check
				var evt = e || window.event;
				
				// Detect right click
				var rightclick;
				if (evt.which)
				{
					rightclick = (evt.which == 3);
				}
				else if (evt.button)
				{
					rightclick = (evt.button == 2);
				}
				
				// Don't start dragging on right click
				if (rightclick)
				{
					return;
				}
				
				// Mark that it's being dragged
				this.beingDragged = true;
				
				// Capture the drag start point in order to calculate movement
				this.preDragLeft = this.container.offsetLeft;
				this.preDragTop = this.container.offsetTop;
				this.startingMouseX = evt.clientX;
				this.startingMouseY = evt.clientY;
				
				// Register the listeners for the rest of the drag
				DC_LoaTS_Helper.registerEventHandler(document, "mousemove", this._performDragHook);
				DC_LoaTS_Helper.registerEventHandler(document, "mouseup", this._stopDragHook);
				
				// This should hopefully keep it from selecting text and doing anything else
				// that normal clicking would do
				return DC_LoaTS_Helper.stopDefaultEventAction(evt);
			},
			
			// Event fired as the menu is being actually dragged
			/*private void*/
			_performDrag: function(e)
			{
				// Half-hearted IE check
				var evt = e || window.event;
				
				// Move the menu based on the user's mouse movements
				this.container.style.left = Math.max(this.preDragLeft + (evt.clientX-this.startingMouseX), 0) + "px";
				this.container.style.top = Math.max(this.preDragTop + (evt.clientY-this.startingMouseY), 0) + "px";
				
				// This should hopefully keep it from selecting text and doing anything else
				// that normal dragging would do
				return DC_LoaTS_Helper.stopDefaultEventAction(evt);
			},
			
			//Event fired as the mouse is released at the end of a drag
			/*private void*/
			_stopDrag: function(e)
			{
				// Mark that it's no longer being dragged
				this.beingDragged = false;
				
				// Remove the event listeners
				DC_LoaTS_Helper.unregisterEventHandler(document, "mousemove", this._performDragHook);
				DC_LoaTS_Helper.unregisterEventHandler(document, "mouseup", this._stopDragHook);

				// Release the variables holding the previous locations
				delete this.preDragLeft;
				delete this.preDragTop;
				delete this.startingMouseX;
				delete this.startingMouseY;
			}
		});
		
		// Put in a place holder for the tabs
		RaidMenu.tabClasses = {};
		
		// Ensure the raid menu is available
		/*public static RaidMenu*/
		RaidMenu.getInstance = function()
		{
			// Locate or create a raid menu
			var raidMenu = window.raidMenu;
			if (typeof raidMenu == "undefined")
			{
				try
				{
					raidMenu = new RaidMenu();
					window.raidMenu = raidMenu;
				}
				catch(ex)
				{
					console.error("Error while opening raid menu");
					console.error(ex);
					return;
				}
			}
			
			return raidMenu;
		};

		// Toggle the visibility of the raid menu
		/*public static void*/
		RaidMenu.toggle = function()
		{
			// Toggle its visibility
			RaidMenu.getInstance().toggle();
		};

		// Show the raid menu
		/*public static void*/
		RaidMenu.show = function()
		{
			// Show it
			RaidMenu.getInstance().show();
		};

        // Hide the raid menu
        /*public static void*/
        RaidMenu.hide = function()
        {
            // Hide it
            RaidMenu.getInstance().hide();
        };

        // Show a specific tab
        /*public static void*/
        RaidMenu.setActiveTab = function(tabClass)
        {
            // Switch to the tab
            RaidMenu.getInstance().setActiveTab(tabClass);

            // Show the menu itself, if it's hidden
            RaidMenu.show();
        };


		/************************************/
		/******** RaidMenuTab Class *********/
		/************************************/
		
		// Class to manage a tab in the raid popup menu
		window.RaidMenuTab = Class.create({
			initialize: function(parentMenu)
			{
				//console.log("Creating tab ", arguments);
				this.parentMenu = parentMenu;
				var me = this;
				
				var sanitaryName = me.getSanitizedName();
				me.tabLi = document.createElement("li");
				me.tabLi.className = "DC_LoaTS_raidMenuTab";
				me.parentMenu.tabsList.appendChild(me.tabLi);
				
				me.tabA = document.createElement("a");
				me.tabA.href = "#DC_LoaTS_raidMenu" + sanitaryName + "Pane";
				me.tabA.id = "DC_LoaTS_raidMenu" + sanitaryName + "PaneTab";
				me.tabA.appendChild(document.createTextNode(me.tabName));
				me.tabLi.appendChild(me.tabA);
				
				me.pane = document.createElement("div");
				me.pane.id = "DC_LoaTS_raidMenu" + sanitaryName + "Pane";
				me.pane.className = "DC_LoaTS_raidMenuPane";
				me.pane.style.display = "none";
				me.parentMenu.bodyWrapper.appendChild(me.pane);
				
				me.parentMenu.tabs.addTab(me.tabA);

				
				if (me.closeable) {
					me.tabCloseA = document.createElement("a");
					me.tabCloseA.href = "#";
					me.tabCloseA.className = "DC_LoaTS_raidMenuCloseTabA";
					me.tabCloseA.innerText = "X";
					me.tabCloseA.onclick = function() {
						me.parentMenu.tabsList.removeChild(me.tabLi);
						me.parentMenu.bodyWrapper.removeChild(me.pane);
						delete RaidMenu.tabClasses[me.tabPosition];
						delete me.parentMenu.tabs.containers._object[me.tabA.href];
						var links = me.parentMenu.tabs.links;
						for (var i = 0; i < links.length; i++) {
							var link = links[i];
							if (link.key == me.tabA.href) {
								var rest = links.slice((i-1 || i) + 1 || links.length);
								links.length = i < 0 ? links.length + i : i;
								me.parentMenu.tabs.links = links.push.apply(links, rest);

								break;
							}
						}
						me.parentMenu.tabs.previous();
						return false;
					};
					me.tabLi.appendChild(me.tabCloseA);
				} // End closeable logic
				
				me.header = me.createHeader(me.tabHeader || me.tabName);
				me.pane.appendChild(me.header);
								
				
				if (typeof me.initPane === "function") {
					me.initPane();
				}
			},
			
			getSanitizedName: function()
			{
				return this.tabName.trim().replace(" ", "_");
			},
			
			createHeader: function(title)
			{
				var header = document.createElement("h1");
				header.className = "RaidMenuTab-Header";
				header.update(title);
				return header;
			},
			
			createSimpleOptionHTML: function(id, type, value, description, hoverText, additionalAttributes)
			{
				if (type == "boolean" || type == "text") // Not sure if other types would need different wrappers...
				{
						var outerWrapper = document.createElement("div");
						outerWrapper.id = id + "Wrapper";
						outerWrapper.className = "DC_LoaTS_raidMenuOptionWrapper clearfix";
						
						var innerWrapper = document.createElement("div");
						innerWrapper.className = "DC_LoaTS_raidMenuInnerWrapper"
						outerWrapper.appendChild(innerWrapper);
				}
				
				switch(type)
				{
					case "boolean":
					{
						var option = document.createElement("input");
						option.type = "checkbox";
						option.id = id;
						option.title = hoverText;
						
						if (value === "true" || value === true)
						{
							option.checked = true;
						}
						
						for (var attribute in additionalAttributes)
						{
							option[attribute] = additionalAttributes[attribute];
						}
						
						innerWrapper.appendChild(option);
						
						var desc = document.createElement("div");
						desc.className = "DC_LoaTS_raidMenuDescription";
						desc.innerHTML = description;
						outerWrapper.appendChild(desc);
						
						return {wrapper: outerWrapper, option: option};
					}
					case "text":
					{						
						var option = document.createElement("input");
						option.type = "text";
						option.id = id;
						option.title = hoverText;
						option.value = value;
						
						for (var attribute in additionalAttributes)
						{
							option[attribute] = additionalAttributes[attribute];
						}
						
						innerWrapper.appendChild(option);
						
						var desc = document.createElement("div");
						desc.className = "DC_LoaTS_raidMenuDescription";
						desc.innerHTML = description;
						outerWrapper.appendChild(desc);
						
						return {wrapper: outerWrapper, option: option};
					}
				}
			}
		});
		
		RaidMenuTab.create = function(classObject)
		{
			//console.log(classObject);
			try
			{
				// Don't collide with other poorly positioned tabs
				while (typeof RaidMenu.tabClasses[classObject.tabPosition] !== "undefined")
				{
					classObject.tabPosition++;
				}
				
				var tabClass = Class.create(RaidMenuTab, classObject);
				tabClass.tabName = classObject.tabName;
				tabClass.tabPosition = classObject.tabPosition;
				tabClass.closeable = classObject.closeable;
				RaidMenu.tabClasses[classObject.tabPosition] = tabClass;
				return tabClass;
			}
			catch(ex)
			{
				var tabName = (typeof classObject !== "undefined" && typeof classObject.tabName !== "undefined")?classObject.tabName:"";
				
				console.warn("Error while creating RaidMenu tab class " + tabName);
				console.warn(classObject);
				console.warn(ex);
			}
		};

		RaidMenuTab.createDataDumpTab = function(data, title)
		{
			var tabTitle = title.length == 0?"Data Export":title.length > 25?title.substring(0,25):title;
			var tabA;
			RaidMenu.show();
			var tabClass = RaidMenuTab.create({
				tabName: tabTitle,
				tabHeader: "Export: " + title,
				tabPosition: 150,
				closeable: true,
				
				initPane: function()
				{
					var textArea = document.createElement("textarea");
					textArea.className = "DataDumpTab-Data";
					textArea.innerHTML = data;
					
					tabA = this.tabA;
					this.pane.appendChild(textArea);
				}
			});
			RaidMenu.getInstance().activateTab(tabClass);
			RaidMenu.getInstance().tabs.setActiveTab(tabA);
		};

		/************************************/
		/********* RaidMultiFilter Class *********/
		/************************************/
		
		// This class represents a group of filters
		window.RaidMultiFilter = Class.create({
			
			// Constructor
			initialize: function(filterText)
			{
				Timer.start("RaidMultiFilter init");

				// Declare some vars for later
				this.valid = true;

				// Capture original filterText
				this.filterText = filterText;
				
				// Break out any bunch
				var filterTexts = this.filterText.split("||");
				
				// Prepare the filters
				this.filters = [];
				
				for (var i = 0; i < filterTexts.length; i++) {
					this.filters.push(new RaidFilter(filterTexts[i]));
				}

				Timer.stop("RaidMultiFilter init");
			},
			
			// Based on this filter, does a given property match the filter
			matches: function(params)
			{				
				var matched = false;
				
				for (var i = 0; i < this.filters.length; i++) {
					matched = matched || this.filters[i].matches(params);
				}
				
				return matched;
			},
			
			// Gets a key to define this filter
			getKey: function()
			{
				var key = "";
				
				for (var i = 0; i < this.filters.length; i++) {
					key += (i>0?"||":"") + this.filters[i].getKey()
				}
				
				return key;
			},
			
			// If it has a name and optionally a difficulty and nothing else, it's simple
			isSimple: function()
			{
				var simple = true;
				
				for (var i = 0; i < this.filters.length; i++) {
					simple = simple && this.filters[i].isSimple();
				}
				
				return simple;
			},
			
			isEmpty: function()
			{
				var empty = true;
				
				for (var i = 0; i < this.filters.length; i++) {
					empty = empty && this.filters[i].isEmpty();
				}
				
				return empty;

			},
			
			isValid: function()
			{
				var valid = true;
				
				for (var i = 0; i < this.filters.length; i++) {
					valid = valid && this.filters[i].isValid();
				}
				
				return valid;
			},
			
			getDifficultyText: function()
			{
				var text = "";
				
				for (var i = 0; i < this.filters.length; i++) {
					text += (i>0?"||":"") + this.filters[i].getDifficultyText()
				}
				
				return text;
			},
			
			toString: function()
			{
				var str = "";
				
				for (var i = 0; i < this.filters.length; i++) {
					str += (i>0?"||":"") + this.filters[i].toString()
				}
				
				return str;
			},
			
			toPrettyString: function() {
				var pretty = "";
				
				for (var i = 0; i < this.filters.length; i++) {
					pretty += (i>0?"||":"") + this.filters[i].toPrettyString()
				}
				
				return pretty;

			}
		});

		RaidMultiFilter.paramText = "[raidName] [raidDifficulty] [{state: stateParam}] [{fs: fsParam}] [{age: ageParam}] [{count: countParam} [{page: pageParam}]]";

				/************************************/
		/********** RaidStyle Class *********/
		/************************************/
		
		// Class to parse raid style text of any form into CSS stuff
		window.RaidStyle = Class.create({
			initialize: function(styleText)
			{
				var naturalLanguage = "";
				var nativeCSS = "";
				this.css = "";

//				console.log("Parsing styleText: \"" + styleText + "\"")
				
				// Extract from the inputted text the various natural language and native CSS bits
				RaidStyle.parsePattern.lastIndex = 0;
				for (var match = RaidStyle.parsePattern.exec(styleText); match && match[0] != ""; match = RaidStyle.parsePattern.exec(styleText))
				{
					// Combine any natural language pieces together
					if (typeof match[1] !== "undefined")
					{
						naturalLanguage += match[1];
					}
					
					// Combine any native CSS pieces together
					if (typeof match[2] !== "undefined")
					{
						nativeCSS += match[2];
					}
				}
				
				// Trim out any extra whitespace
				naturalLanguage = naturalLanguage.trim().toLowerCase();
				nativeCSS = nativeCSS.trim();
				
//				console.log("styleText yielded naturalLanguage: \"" + naturalLanguage + "\" and nativeCSS: \"" + nativeCSS + "\"");
				
				// Try to parse the natural language bits
				// First, get a copy of the parsable bits
				var parseEls = RaidStyle.getNaturalLanguageParseElements();
				
				for (var i = 0; i < parseEls.length && naturalLanguage.length > 0; i++)
				{
					var el = parseEls[i];
					el.pattern.lastIndex = 0;
					var match = el.pattern.exec(naturalLanguage);
					if (match != null && match[0] != "")
					{
//						console.log(el.property + " captured \"" + match[el.capture] + "\" and will replace \"" + match[el.replace]) +"\"";
						this.css += el.property + ": " + match[el.capture] + "; ";
//						console.log("Natural language before: \"" + naturalLanguage + "\"");
						naturalLanguage = naturalLanguage.replace(match[el.replace], "").trim();
//						console.log("Natural language after: \"" + naturalLanguage + "\"");
					}
					else
					{
//						console.log(el.property + " did not match against \"" + naturalLanguage + "\"");
					}
				}
				
				this.css += nativeCSS;
//				console.log("CSS: ");
//				console.log(this.css);
			},
			
			toString: function()
			{
				return this.css;
			},
			
			isEmpty: function()
			{
				return typeof this.css === "undefined" || this.css.trim().length == 0;
			},
			
			isValid: function()
			{
				//TODO Will a style ever be not valid?
				return true;
			}
		});
		
		// General pattern to pick apart the natural language style from the native CSS styles
		RaidStyle.parsePattern = /([^"]*)?("[^"]*")?/gi;
		
		// Pattern to find bits of text that are colors. Can find #FFF, #FFFFFF, rgb(255,255,255), or white as the color white
		RaidStyle.baseColorPattern = /#[0-9a-f]{3}(?:[0-9a-f]{3})?\b|rgb\((?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]),(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]),(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\)|\b(?:black|white|red|yellow|lime|aqua|blue|fuchsia|orange|gray|silver|maroon|olive|green|teal|navy|purple)\b/i;
		RaidStyle.colorPattern = new RegExp("(?:(?:(?!on +).. )|^)(" + RaidStyle.baseColorPattern.source + ")", "i");
		RaidStyle.backgroundColorPattern = new RegExp("\\bon +(" + RaidStyle.baseColorPattern.source + ")", "i");
		
		// These are the current natural language features we're going to support for now
		RaidStyle.naturalLanguageParseElements = [
												 	{property: "font-weight", 		capture: 0, replace: 0, pattern: /\b(?:[1-9]00(?!px|pt|em|en|%)|bold(?:er)?|lighter|normal)\b/i},
												 	{property: "background-color", 	capture: 1, replace: 0, pattern: RaidStyle.backgroundColorPattern},
												 	{property: "color", 			capture: 1, replace: 1, pattern: RaidStyle.colorPattern},
												 	{property: "font-size", 		capture: 0, replace: 0, pattern: /\b[0-9]?[0-9]?[0-9]?[0-9] ?(?:(?:px|pt|em|en)\b|%)|\bx?x-small\b|\bsmall(?:er)?\b|\bmedium\b|\blarger?\b|\bx?x-large\b/i},
												 	{property: "text-decoration", 	capture: 0, replace: 0, pattern: /\bunderline(?: overline)\b/i},												 	
												 	{property: "font-style", 		capture: 0, replace: 0, pattern: /\b(?:italic|oblique|normal)\b/i},												 	
												 	{property: "whitespace", 		capture: 0, replace: 0, pattern: /\b(?:pre|pre-wrap|pre-line|-moz-pre-wrap|-o-pre-wrap|nowrap|normal)\b/i},												 	
												 	{property: "display", 			capture: 0, replace: 0, pattern: /\b(?:none|inline|block|inline-block|list-item|marker|compact|run-in|table-header-group|table-footer-group|table|inline-table|table-caption|table-cell|table-row|table-row-group|table-column|table-column-group)\b/i}
												 ];
		
		RaidStyle.getNaturalLanguageParseElements = function()
		{
			var el = [];
			for (var i = 0; i < RaidStyle.naturalLanguageParseElements.length; i++)
			{
				el.push(RaidStyle.naturalLanguageParseElements[i]);
			}
			
			return el;
		};
		
												 
		/************************************/
		/********* RaidToolbar Class ********/
		/************************************/
		window.RaidToolbar = Class.create({
			initialize: function()
			{
				// Set up the matched commands container
				this.existingMatchedCommands = [];
				
				// Find the existing RaidToolbar
				this.container = document.getElementById("DC_LoaTS_raidToolbarContainer");
				
				// If a RaidToolbar doesn't exist yet, make it
				if (typeof this.container == "undefined" || this.container == null)
				{
					// Create the space bewteen the kong buttons and game
					var td = new Element("td", {"id": "DC_LoaTS_toolbarCell"});
					var tr = new Element("tr");
					tr.appendChild(td);
					
					this.container = new Element("ul", {id: "DC_LoaTS_raidToolbarContainer"});
					td.appendChild(this.container);
					
					$($("flashframecontent").children[0].children[0].children[0]).insert({after:tr});
					var ccc = $("chat_container_cell");
					ccc.remove();
					td.insert({after: ccc});
					ccc.setAttribute("rowspan", "2");
					
					$("maingame").style.height = parseInt($("maingame").style.height) + 20 + "px";
					$("chat_container").style.height = parseInt($("chat_container").style.height) + 20 + "px";
					$("chat_tab_pane").style.height = parseInt($("chat_tab_pane").style.height) + 20 + "px";
					
					//TODO: Should break these out like the commands?
					this.buttons = {
						reload: new RaidButton("reload", "DC_LoaTS_reloadButton", DC_LoaTS_Helper.reload),
						toggleMenu: new RaidButton("toggleMenu", "DC_LoaTS_menuButton", 
							function(event)
							{
								// Show the raid menu
								RaidMenu.toggle();
								
								// If the menu was spawned by a click, move the menu there
								if (typeof event != "undefined")
								{
									// Fixed Menu positioning - Needs to be relative to window scroll
									var scrollOffsets = document.viewport.getScrollOffsets();
									
									// Move the raid menu to where the mouse clicked this button
									window.raidMenu.container.style.left = Event.pointerX(event) - scrollOffsets.left + "px";
									window.raidMenu.container.style.top = Event.pointerY(event) - scrollOffsets.top + 20 + "px";
								}
							}
						),
						toggleGame: new RaidButton("toggleGame", "DC_LoaTS_toggleGameButton", DC_LoaTS_Helper.toggleGame, "Show / Hide the game"),
						reloadWorldChat: new RaidButton("reloadWC", "DC_LoaTS_reloadWCButton", DC_LoaTS_Helper.reload),
						toggleWorldChat: new RaidButton("toggleWorldChat", "DC_LoaTS_toggleWorldChatButton", DC_LoaTS_Helper.toggleWorldChat, "Show / Hide World Chat")
					};
					for (var buttonName in this.buttons)
					{
						this.container.insert({bottom: this.buttons[buttonName].node});
					}
					
					this.omniboxWrapper = new Element("li", {"class": "DC_LoaTS_omniboxWrapper"});
					
					this.omnibox = new Element("input", {type: "text", "class": "DC_LoaTS_omnibox", autocomplete: "off"});
					this.omniboxWrapper.insert({bottom: this.omnibox});
					this.omnibox.oldValue = "";
					
					this.omniboxCommandsWrapper = new Element("ul", {"class": "DC_LoaTS_omniboxCommandsWrapper"});
					this.omniboxCommandsWrapper.hide();
					
					this.omniboxWrapper.insert({bottom: this.omniboxCommandsWrapper});
					this.container.insert({bottom: this.omniboxWrapper});

					this.omnibox.observe("mouseover", function() {
						$(this).addClassName("DC_LoaTS_omnibox_focus");
					});
					
					this.omniboxWrapper.observe("mouseover", function() {
						$(this).addClassName("DC_LoaTS_omniboxWrapper_hover");
					});
					
					this.omnibox.observe("focus", function(event, raidToolbar) {
						$(this).addClassName("DC_LoaTS_omnibox_focus");
						if (this.value.length >= 3 && raidToolbar.omniboxCommandsWrapper.childElements().length > 0)
						{
							raidToolbar.omniboxCommandsWrapper.show();
						}
					}.bindAsEventListener(this.omnibox, this));
					
					this.omnibox.observe("mouseout", function() {
						if (this.value.length == 0 && this != document.activeElement)
						{
							$(this).removeClassName("DC_LoaTS_omnibox_focus");
						}
					});
					
					this.omniboxWrapper.observe("mouseout", function() {
						$(this).removeClassName("DC_LoaTS_omniboxWrapper_hover");
					});
					
					this.omnibox.observe("blur", function(event, raidToolbar) {
						if (this.value.length == 0 && this != document.activeElement)
						{
							$(this).removeClassName("DC_LoaTS_omnibox_focus");
						}
						
						if (!$(raidToolbar.omniboxWrapper).hasClassName("DC_LoaTS_omniboxWrapper_hover"))
						{
							raidToolbar.omniboxCommandsWrapper.hide();
						}
					}.bindAsEventListener(this.omnibox, this));
					
					this.omnibox.observe("input", function(event, raidToolbar) {
						if (this.oldValue != this.value)
						{
							// Stretch the bar as needed
							this.setAttribute("size", Math.min(Math.max(20, (93/80) * this.value.length), 120));
							this.oldValue = this.value;
							
							DCDebug("Text Changed");
							
							// Process the omnibox text
							raidToolbar.processOmniboxText(this.value);
						}
					}.bindAsEventListener(this.omnibox, this));
					
					//TODO: Rig up keys to control omnibox selection
					this.omnibox.observe("keydown", function(event) {
						// Up arrow
						if (event.keyCode == 38)
						{
							DCDebug("Pressed up")
						}
						// Down arrow
						else if (event.keyCode == 40)
						{
							DCDebug("Pressed down")							
						}
						// Left arrow
						else if (event.keyCode == 41)
						{
							DCDebug("Pressed left")							
						}
						// Right arrow
						else if (event.keyCode == 39)
						{
							DCDebug("Pressed right")							
						}
						// Enter
						else if (event.keyCode == 13)
						{
							DCDebug("Pressed Enter")							
						}
					});
					
					RaidToolbar.createWRButton();
					
				}
				// Else if it does exist, grab the hooks
				//TODO
			},
			
			// Process omnibox text
			processOmniboxText: function(text)
			{
				var matchedCommands = [];
				
				// Clean up whitespace
				text = text.trim();
				
				// We really need at least 3 characters to make sense of the user's input
				if (text.length >= 3)
				{
					// Run through all of the text processors
					var raidLink = new RaidLink(text);
					var raidFilter = new RaidFilter(text);
					
					// If this is a valid link
					if (raidLink.isValid())
					{
						try
						{
							DCDebug("Link refers to: " + raidLink.getSimpleText() + "First seen: " + "Last seen: ");
							
							DCDebug("Load: " + raidLink.getSimpleText());
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.loadraid("omnibox", raidLink.getURL()));
							DCDebug("Info: " + raidLink.getDifficultyText() + " " + raidLink.getName());
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.raid("omnibox", raidLink.getName() + " " + raidLink.difficulty));
							DCDebug("State: Forget/Remember, Un/Mark Visited");
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.linkstate("omnibox", raidLink.getURL()));
							
							DCDebug("Seen:  " + raidLink.getName() + " Any, Normal, Hard, Legendary, Nightmare");
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.seenraids("omnibox", raidLink.getName() + " " + raidLink.difficulty));
							DCDebug("Search: " + raidLink.getName() + " on ZoyWiki");
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.wiki("omnibox", raidLink.getName()));
						}
						catch(ex)
						{
							console.warn("Failure while creating options for omnibox");
							console.warn(raidLink);
							console.warn(ex);
						}
						
					}
					// If it's a valid filter and it's not empty
					else if (raidFilter.isValid() && !raidFilter.isEmpty())
					{
						var matchedTypes = DC_LoaTS_Helper.getRaidTypes(raidFilter);
						if (matchedTypes.length > 0)
						{
							// If it's simple and matches only 1 type, put raid info first
							var raidInfoFirst = matchedTypes == 1 && raidFilter.isSimple();
							if (raidInfoFirst)
							{
								if (typeof raidFilter.difficulty != "undefined")
								{
									DCDebug("Raid info for " + raidFilter.getDifficultyText() + " " + matchedTypes[0].fullName);
								}
								else
								{
									DCDebug("Raid info for " + matchedTypes[0].fullName);
								}
								
								DCDebug("Look up " + matchedTypes[0].fullName + " on ZoyWiki");
							}
							
							DCDebug("Seen raids matching " + text);
							
							// If we didn't offer raid info first
							if (!raidInfoFirst)
							{
								DCDebug("Raid info for all matching raids");
								for (var i = 0; i < matchedTypes.length; i++)
								{
									if (typeof raidFilter.difficulty != "undefined")
									{
										DCDebug("Raid info for " + raidFilter.getDifficultyText() + " " + matchedTypes[i].fullName);
									}
									else
									{
										DCDebug("Raid info for " + matchedTypes[i].fullName);
									}
								}
							}
						}
					}

					// Simple commands
					if (text.toLowerCase().indexOf("lsi") == 0)
					{
						DCDebug("Calculate lsi");
					}
					else if (text.toLowerCase().indexOf("bsi") == 0)
					{
						DCDebug("Calculate bsi");
					}
					else if (text.toLowerCase().indexOf("help") == 0)
					{
						DCDebug("Get help?");
					}

					if (matchedCommands.length == 0)
					{
						try
						{
							// Attempt to match the text to a known command
							for (var commandName in DC_LoaTS_Helper.chatCommands)
							{
								// Going to add a wiki command no matter what
								if (commandName.toLowerCase() == DC_LoaTS_Helper.chatCommands.wiki.commandName ||
									commandName.toLowerCase() == DC_LoaTS_Helper.chatCommands.forum.commandName)
								{
									continue;
								}
								
								// Check command
								if (text.toLowerCase().indexOf(commandName.toLowerCase()) == 0
									||
									text.toLowerCase().indexOf("/" + commandName.toLowerCase()) == 0
								)
								{
									matchedCommands.push(new DC_LoaTS_Helper.chatCommands[commandName]("omnibox", text));
								}
								// Check aliases of this command
								else
								{
									var command = DC_LoaTS_Helper.chatCommands[commandName];
									for (var i = 0; i < command.aliases.length; i++)
									{
										var alias = command.aliases[i];
										if (text.toLowerCase().indexOf(alias.toLowerCase()) == 0
											||
											text.toLowerCase().indexOf("/" + alias.toLowerCase()) == 0
										)
										{
											matchedCommands.push(new DC_LoaTS_Helper.chatCommands[commandName]("omnibox", text));
										}
									}
								}
							}
							
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.wiki("omnibox", text));
							matchedCommands.push(new DC_LoaTS_Helper.chatCommands.forum("omnibox", text));
						}
						catch(ex)
						{
							console.warn("Failure while matching omnibox text (\"" + text + "\") to command.");
							console.warn(ex);
						}
					}
					
					// Clear out any old suggestions
					if (matchedCommands.length > 0 || this.omnibox.value.length < 3)
					{
						// Remove all existing options
						this.omniboxCommandsWrapper.childElements().invoke("remove");

						// Unhook the existing commands
						this.existingMatchedCommands.invoke("onRemovedFromOmnibox");
					}
					
					// Set the new commands we found to tbe ones we remember
					this.existingMatchedCommands = matchedCommands;
					
					// Put in the new suggestions, if any
					if (matchedCommands.length > 0)
					{						
						for (var i = 0; i < matchedCommands.length; i++)
						{
							var command = matchedCommands[i];
							DCDebug(command.commandName);
//							var option = new Element("li", {"class": "omniboxCommandOption"});
//							var anchor = new Element("a", {"href": "#"});
//							anchor.onclick = function(){alert(this.innerHTML); return false;};
//							anchor.update(command.commandName);
//							
//							option.insert({bottom: anchor});
//							this.omniboxCommandsWrapper.insert({bottom: option});

							this.omniboxCommandsWrapper.insert({bottom: command.getOptionLine()});
						}
						
						this.omniboxCommandsWrapper.show();
					}
				}
				else
				{
					this.omniboxCommandsWrapper.hide();
				}
			},
			
			toggle: function()
			{
				this.container.toggle();
			},

			show: function()
			{
				this.container.show();
			},

			hide: function()
			{
				this.container.hide();
			}
		});
		
		// Toggle the visibility of the raid toolbar
		RaidToolbar.toggle = function()
		{
			// Locate or create a raid toolbar
			var raidToolbar = window.raidToolbar;
			if (typeof raidToolbar == "undefined")
			{
				raidToolbar = new RaidToolbar();
				window.raidToolbar = raidToolbar;
			}
			
			// Toggle its visibility
			raidToolbar.toggle();
		};
		
		// Show the raid toolbar
		RaidToolbar.show = function()
		{
			// Locate or create a raid toolbar
			var raidToolbar = window.raidToolbar;
			if (typeof raidToolbar == "undefined")
			{
				raidToolbar = new RaidToolbar();
				window.raidToolbar = raidToolbar;
			}
			
			// Toggle its visibility
			raidToolbar.show();
		};
		
		// Hide the raid toolbar
		RaidToolbar.hide = function()
		{
			// Locate or create a raid toolbar
			var raidToolbar = window.raidtoolbar;
			if (typeof raidToolbar != "undefined")
			{
				// Hide the toolbar
				raidToolbar.hide();
			}
		};
		
		// Hide the command options
		RaidToolbar.hideCommandOptions = function()
		{
			$$(".DC_LoaTS_omniboxCommandsWrapper")[0].hide();
		}
		
		// Hide the command options
		RaidToolbar.resetOmnibox = function()
		{
			$$(".DC_LoaTS_omnibox")[0].value = "";
			$$(".DC_LoaTS_omnibox")[0].focus();			
		}
		
		RaidToolbar.createWRButton = function() {
			var wr = DC_LoaTS_Helper.worldRaidInfo;
			if (!DC_LoaTS_Helper.wrButton && typeof wr === "object" && (!wr.timerEnds || new Date(wr.timerEnds) > new Date())) {
				// Locate or create a raid toolbar
				var raidToolbar = window.raidToolbar;
				if (typeof raidToolbar == "undefined")
				{
					raidToolbar = new RaidToolbar();
					window.raidToolbar = raidToolbar;
				}
				
				DC_LoaTS_Helper.wrButton = new RaidButton(DC_LoaTS_Helper.worldRaidInfo.name + " Info", "DC_LoaTS_WRButton", DC_LoaTS_Helper.showWRInfo);
				raidToolbar.container.insert({bottom: DC_LoaTS_Helper.wrButton.node});
			}
		}
		/************************************/
		/********** RaidType Class **********/
		/************************************/
		
		// The Raid Type class is the generic form of a raid
		/*public class*/
		window.RaidType = Class.create({
	    	
		    // Constructor
		    /*public RaidType*/
			initialize: function(id, zone, fullName, shortName, colloqName, time, size, stat, health, fairShare, target)
			{
				this.id = id;
				this.zone = zone || "?";
				this.fullName = fullName || id;
				this.shortName = shortName || id;
				this.colloqName = colloqName || id;
				this.shortestName = colloqName || id;
				this.time = time || "?";
				this.size = size || "?";
				this.stat = stat || "?";
				
				// Calculate Health
				if (typeof health === "number")
				{
					this.health = [health*RaidType.difficultyHealthFactor[1],
								   health*RaidType.difficultyHealthFactor[2],
								   health*RaidType.difficultyHealthFactor[3],
								   health*RaidType.difficultyHealthFactor[4]
								   ];
				}
				else if (typeof health === "string")
				{
					this.health = [health, health, health, health];
				}
				else if (typeof health === "object" && typeof health !== null)
				{
					this.health = health;
				}
				else
				{
					this.health = ["?", "?", "?", "?"];
				}
				
				// Calculate Fair Share
				if (typeof fairShare === "number" || typeof fairShare === "string")
				{
					this.fairShare = [fairShare, fairShare, fairShare, fairShare];
				}
				else if (typeof fairShare === "object" && fairShare !== null)
				{
					this.fairShare = fairShare;
				}
				//TODO: Cache this instead
				// Else, calculate FS inline


				// Calculate Target
				if (typeof target === "number" || typeof target === "string")
				{
					this.target = [target, target, target, target];
				}
				else if (typeof target === "object" && target !== null)
				{
					this.target = target;
				}
				//TODO: Cache this instead
				// Else, calcuate Target inline

			},
			
			// Get or calculate fair share for a given difficulty raid. 
			// Can be int or String (usually, if applicable, "Unknown")
			/*public int or String*/
			getFairShare: function (difficulty)
			{
				var fs = 0;
				
				// If there is a hardcoded fair share, use that
				if (typeof this.fairShare !== "undefined")
				{
					fs = this.fairShare[difficulty-1];
				}
				// IF there is no hardcoded fair share, calculate it
				// Also, we must have healths, difficulty, and size to calculate it
				else if (typeof difficulty !== "undefined" 
					  && typeof this.size === "number" 
					  && typeof this.getHealth(difficulty) === "number")
				{
					fs = this.getHealth(difficulty) / this.size;
				}
				
				return fs;
			},
			
			// Get pretty text for fair share
			/*public String*/
			getFairShareText: function(difficulty)
			{
				var fs = this.getFairShare(difficulty);
				
				return DC_LoaTS_Helper.prettyFormatNumber(fs);
			},
			
			// Get or calculate optimal share for a given difficulty raid. 
			// Can be int or String (usually, if applicable, "Unknown")
			/*public int or String*/
			getOptimalShare: function (difficulty)
			{
				var target = 0;
				
				// If non-standard target damage is set
				if (typeof this.target !== "undefined")
				{
					target = this.target[difficulty-1];
				}
				// Otherwise assume usual calculation of target
				else
				{
					target = this.getFairShare(difficulty) * RaidType.targetDamageModifier[this.size];
				}
				
				return target;
			},
			
			// Get pretty text for target damage (optimal share)
			/*public String*/
			getTargetDamageText: function(difficulty)
			{
				return DC_LoaTS_Helper.prettyFormatNumber(this.getOptimalShare(difficulty));
			},
			
			// Returns the int of the health or specified String (usually, if applicable, it's "Unknown")
			/*public int or String*/
			getHealth: function(difficulty)
			{
				return this.health[difficulty-1];
			},
			
			// Returns the health of the raid as friendly text
			/*public String*/
			getHealthText: function(difficulty)
			{
				var health = this.getHealth(difficulty);
				return (typeof health == "number")?DC_LoaTS_Helper.prettyFormatNumber(health):health;
			},
			
			// Returns the duration of the raid as text
			/*public String*/
			getTimeText: function()
			{
				return this.time + "H";
			},
			
			// Returns a combination of all acceptable names for the raid
			// So that the string can be searched
			/*public String*/
			getSearchableName: function()
			{
				return this.id + "_" + this.fullName + "_" + this.shortName + "_" + this.colloqName;
			},
			
			// Gets a big descriptive block of text for the raid
			// Difficulty is optional. If provided, narrows down output, otherwise gives all
			/*public String*/ 
			getVerboseText: function(difficulty)
			{
				// Put the name, size, and stat facts into the string
				var text = "<b title=\"" + this.id + " - Search Key: " + this.shortestName + "\">" + this.fullName + "</b> \n";
				text += "Raid Size: " + this.size + " \n";
				text += "Stat(s) Used: " + this.stat + " \n";
				text += "Duration: " + this.getTimeText() + " \n";
				text += "Zone: " + this.zone + " \n";

				// If the user passed in difficulty 0, they only want the above listed stuff
				if (difficulty != 0)
				{
					var difficulties;
					
					// If we're focusing on a single difficulty
					if (typeof difficulty != "undefined")
					{
						difficulties = [difficulty];
						
					}
					// If we didn't get a single difficulty, show all difficulties
					else
					{
						difficulties = [1, 2, 3, 4];
					}
					
					// For each of the difficulties we're addressing
					for (var i = 0; i < difficulties.length; i++)
					{
						var d = difficulties[i];
						
						if (difficulties.length > 1)
						{
							text += " \n-- \n";
						}
						
						// Get text for the difficulty
						var diffText = RaidType.difficulty[d];
	
						if (typeof diffText == "undefined")
						{
							diffText = "Unknown";
						}
						
						var healthText = DC_LoaTS_Helper.prettyFormatNumber(this.getHealth(d));
						
						// Display the difficulty, health, fs, and target damage
						text += "Difficulty: " + diffText + " \n";
						text += "Health: " + healthText + " \n";
						text += "<acronym title=\"FS = Fair Share (of damage) = Raid Health (" + healthText + 
								") / Raid Size (" + this.size + ")\">FS</acronym>: " + this.getFairShareText(d) + " \n";
						text += "<span class=\"abbr\" title=\"Target Damage is FS * Raid Size Multiplier. The multiplier is calculated from user tests in the spreadsheet.\">Target(OS)</span>: " +  this.getTargetDamageText(d) + " ";
	
					}
				}
				
				return text;
			}
			
		});// End RaidType Class
		
		// List of possible difficulties, anything else will show up as Unknown
		RaidType.difficulty = {1: "Normal", 2: "Hard", 3: "Legendary", 4: "Nightmare"};
		
		// List of possible short name difficulties, anything else will show up as Unknown
		RaidType.shortDifficulty = {1: "N", 2: "H", 3: "L", 4: "NM"};
		
		// List of how much each difficulty modifies the base HP of the raid
		RaidType.difficultyHealthFactor = {1: 1, 2: 1.25, 3: 1.6, 4: 2};
		
		// List of *FS modifiers for Target Damage based on raid size.
		// From the raid spreadsheet:
		//		https://docs.google.com/spreadsheet/ccc?key=0AoPyAHGDsRjhdGYzalZZdTBpYk1DS1M3TjVvYWRwcGc&hl=en_US#gid=4
		RaidType.targetDamageModifier = {1: 1, 10: 1.25, 25: 1.5, 50: 2.2, 100: 2.3, 250: 1, 500: 1.5};

		/************************************/
		/********** Timing Utility **********/
		/************************************/
				
		window.Timer = {
			
			start: function(timerName)
			{
				var timer = Timer[timerName];
				if (typeof timer == "undefined")
				{
					timer = {name: timerName, start: 0, totalTime: 0, longestTime: 0, numTimes: 0};
					window.Timer[timerName] = timer;
				}
				timer.start = new Date()/1;
			},
			
			stop: function(timerName)
			{
				var timer = Timer[timerName];
				if (typeof timer == "undefined")
				{
					console.log("Can't stop a timer (" + timerName + ") that wasn't started");
				}
				else
				{
					var elapsed = (new Date()/1) - timer.start;
					timer.totalTime += elapsed;
					if (timer.longestTime < elapsed) {timer.longestTime = elapsed;}
					timer.numTimes++;
					timer.start = 0;
				}
			},
			
			addRun: function(timerName, runTime) {
				var timer = Timer[timerName];

				if (typeof timer === "undefined")
				{
					timer = {name: timerName, start: 0, totalTime: 0, longestTime: 0, numTimes: 0};
					window.Timer[timerName] = timer;
				}

				timer.totalTime += runTime || 0;
				if (timer.longestTime < runTime) {timer.longestTime = runTime;}
				timer.numTimes++;
			},
			
			getTimer: function(timerName)
			{
				var timer = Timer[timerName];
				if (typeof timer === "undefined")
				{
					console.log("Can't get a timer (" + timerName + ") that wasn't started");
				}
				else
				{
					timer.getAverage = function()
					{
						return this.totalTime / this.numTimes;
					}.bind(timer);
				}
				
				return timer;
			},
			
			getReport: function()
			{
				var report = "";
				for (var timerName in window.Timer)
				{
					var timer = window.Timer.getTimer(timerName);
					if (typeof timer !== "function" && typeof timer !== "undefined")
					{
						report += timerName + " > Average: " + timer.getAverage().toFixed(5) + "ms - Total: " + timer.totalTime + "ms - # " + timer.numTimes + "\n\n";
					}
				}
				
				return report;
			},
			
			printReport: function()
			{
				console.log(Timer.getReport());
			}
		};

		/************************************/
		/** UrlParsingFilter Class */
		/************************************/
		
		window.UrlParsingFilter = Class.create({
			initialize: function(params)
			{
				// Capture input params
				this.params = params;
				
				// Default to not forced and not cancelled
				this.force = false;
				this.cancel = false;
				
				// Type is other unless we match something specific
				this.type = "other";
				
				// Break apart the params to find out what this filter is supposed to represent
				var paramsParts = params.trim().replace(/\s+/g, " ").split(" ");
				
				// If we're supposed to force this filter
				if (paramsParts[0] === "force" || paramsParts[0] === "!") {
					this.force = true;
					this.url = paramsParts[1];
					if (paramsParts[2]) {
						this.raidFilter = new RaidMultiFilter(paramsParts.slice(2).join(" "));
					}
				}
				// If we're supposed to cancel this filter
				else if (paramsParts[0] === "cancel") {
					this.cancel = true;
				}
				// Neither forced nor cancelled, just a URL and maybe a RaidFilter
				else {
					this.url = paramsParts[0];
					if (paramsParts[1]) {
						this.raidFilter = new RaidMultiFilter(paramsParts.slice(1).join(" "));
					}
				}
				
				// Does this match the url of any service we already know about? Assume not
				this.known = false;
				
				var match;
				// Try the various urls that this parser knows about
				// Even if we're forcing it, we still need to run this to resolve the regexMatch
				for (var type in UrlParsingFilter.urlPatterns) {
					if ((match = UrlParsingFilter.urlPatterns[type].exec(this.url)) !== null) {
						this.known = true;
						this.type = type;
						this.regexMatch = match;
						break;
					}
				}
			},
			
			getUrlLink: function()
			{
				return "<a href=\"" + this.getWorkingUrl() + "\" target=\"_blank\">" + this.getLinkName() + "</a>";
			},
				
			getLinkName: function()
			{
				switch(this.type)
				{
					case "pastebin":
						return "Pastebin";
						break;
					case "cconoly":
						return "CConoly"
						break;
					default:
						return this.getWorkingUrl();
						break;
				}
			},
			
			getWorkingUrl: function ()
			{
				return this.convertedUrl || this.url;
			},
			
			// It's valid if it provides a url or is a cancel
			isValid: function()
			{
				return this.getWorkingUrl() || this.cancel;
			}
		});
		
		// Parameter text for this parser
		UrlParsingFilter.paramText = "url [raidFilter]";
		
		// Pattern to match different link types
		UrlParsingFilter.urlPatterns = {
			"pastebin": /(?:http:\/\/)?(?:www\.)?pastebin\.com\/(.+)/i, 
			"cconoly": /(?:http:\/\/)?(?:www\.)?cconoly\.com\/lots\/raidlinks\.php/i
		};
		
        /************************************/
        /*********** Styles Tab *************/
        /************************************/

		RaidMenuTab.create(
			{
				tabName: "Profile View",
				tabHeader: "UGUP Character Profile Viewer",
				tabPosition: 40,

				initPane: function()
				{
                    var wrapper = document.createElement("div");

                    var entryArea = document.createElement("div");
                    entryArea.id = "CharacterViewMenu-EntryArea";

                    var usernameLabel = document.createElement("label");
                    usernameLabel.id = "CharacterViewMenu-UsernameLabel";
                    usernameLabel.appendChild(document.createTextNode("Username: "));
                    usernameLabel.title = "This is the name of the user on their platform, not their in game character name";

                    var usernameBox = document.createElement("input");
                    usernameBox.type = "text";
                    usernameBox.id = "CharacterViewMenu-UsernameBox";

                    usernameLabel.appendChild(usernameBox);
                    entryArea.appendChild(usernameLabel);

                    var platformPicker = document.createElement("select");
                    platformPicker.id = "CharacterViewMenu-PlatformSelect";

                    var opt;
                    for (var platform in UGUP.PLATFORM) {
                        if (UGUP.PLATFORM.hasOwnProperty(platform) && typeof UGUP.PLATFORM[platform] !== "function") {
                            opt = document.createElement("option");
                            opt.value = platform;
                            opt.appendChild(document.createTextNode(UGUP.PLATFORM[platform].name));
                            opt.platform = UGUP.PLATFORM[platform];
                            platformPicker.appendChild(opt);
                        }
                    }

                    platformPicker.value = "KONG";

                    entryArea.appendChild(platformPicker);

                    var connector = DC_LoaTS_Helper.getUGUPConnector(null, UGUP.PLATFORM.KONG);
                    var renderArea = document.createElement("div");
                    renderArea.id = "CharacterViewMenu-RenderArea";

                    var runQueryButton = document.createElement("button");
                    runQueryButton.appendChild(document.createTextNode("Lookup User Profile"));
                    runQueryButton.id = "CharacterViewMenu-RunQueryButton";
                    runQueryButton.className = "CharacterViewMenu-Button";
                    runQueryButton.onclick = function() {
                        console.log("Running User Profile Lookup", this, arguments);
                        var platformOpt = platformPicker.selectedOptions[0],
                            username = usernameBox.value.trim();

                        DC_LoaTS_Helper.removeAllChildren(renderArea);
                        var loadingImg = document.createElement("img");
                        loadingImg.id = "CharacterViewMenu-RenderLoadingImg";
                        loadingImg.src = "http://i.imgur.com/NyArTaF.gif";
                        renderArea.appendChild(loadingImg);

                        connector.cfg.platform = platformOpt.platform;
                        console.log("About to fetch profile by username", connector);

                        connector.fetchUserProfileByUsername(username, function(response, model) {
                            if (model.id) {
                                console.log("Fetched Profile: ", response, model);
                                model._modelType.render(model, connector, function(el){
                                    console.log("Rendered Profile", el);
                                    DC_LoaTS_Helper.removeAllChildren(renderArea);
                                    renderArea.appendChild(el);
                                });
                            }
                            else {
                                console.log("Error Fetching Profile: ", response, model);
                                var el = document.createElement("div");
                                var errMsg = "";
                                if (model.code == 402) {
                                    errMsg = "User " + username + " has opted out of UgUp.";
                                }
                                else {
                                    errMsg = "Failed to load profile for " + username + ": " + model.reason + " (Code: " + model.code + ")";
                                }
                                el.appendChild(document.createTextNode(errMsg));
                                DC_LoaTS_Helper.removeAllChildren(renderArea);
                                renderArea.appendChild(el);
                            }
                        });
                    };
                    entryArea.appendChild(runQueryButton);
                    wrapper.appendChild(entryArea);

                    var facebookLookupLink = document.createElement("a");
                    facebookLookupLink.href = "http://findmyfacebookid.com/";
                    facebookLookupLink.target = "_blank";
                    facebookLookupLink.appendChild(document.createTextNode("Lookup a Facebook Username (id) from Profile Url"));
                    wrapper.appendChild(facebookLookupLink);

                    wrapper.appendChild(renderArea);

                    this.pane.appendChild(wrapper);
                }
		});

		/************************************/
		/********** Formatting Tab **********/
		/************************************/
		
		// Class to manage the formatting tab in the raid tab in the popup menu
		RaidMenuTab.create(
			{
				tabName: "Formatting",
				tabPosition: 30,
				
				initPane: function()
				{
					var messageFormatHeader = document.createElement("h2");
					messageFormatHeader.className = "RaidMenuTab-SectionHeader";
					messageFormatHeader.update("Message Format");
					this.pane.appendChild(messageFormatHeader);
					
					var messageFormatDescription = document.createElement("p");
					messageFormatDescription.className = "RaidMenuTab-SectionDescription";
					messageFormatDescription.update("The format of raid links as they will appear in chat.");
					this.pane.appendChild(messageFormatHeader);
					
					this.messageFormatTextArea = document.createElement("textarea");
					this.messageFormatTextArea.id = "FormattingTab-MessageFormatTextArea";
					this.messageFormatTextArea.setAttribute("placeholder", RaidLink.defaultMessageFormat);
					this.currentMessageFormat = DC_LoaTS_Helper.getMessageFormat();
					this.messageFormatTextArea.value = this.currentMessageFormat.replace("{line}", "\n");
					DC_LoaTS_Helper.registerEventHandler(this.messageFormatTextArea, "input", this.handleMessageFormatInput.bind(this));
					this.pane.appendChild(this.messageFormatTextArea);					
					
					var saveButton = document.createElement("button");
					saveButton.update("Save");
					saveButton.className = "FormattingTab-Button";
					DC_LoaTS_Helper.registerEventHandler(saveButton, "click", 
						function()
						{
							holodeck.processChatCommand("/raidformat " + this.currentMessageFormat);
						}.bind(this)
					);
					this.pane.appendChild(saveButton);
					
					var cancelButton = document.createElement("button");
					cancelButton.update("Cancel");
					cancelButton.className = "FormattingTab-Button";
					DC_LoaTS_Helper.registerEventHandler(cancelButton, "click", 
						function()
						{
							this.currentMessageFormat = DC_LoaTS_Helper.getMessageFormat();
							this.messageFormatTextArea.value = this.currentMessageFormat.replace("{line}", "\n");
							this.handleMessageFormatInput();
						}.bind(this)
					);
					this.pane.appendChild(cancelButton);
					
					var defaultButton = document.createElement("button");
					defaultButton.update("Reset to default");
					defaultButton.className = "FormattingTab-Button";
					DC_LoaTS_Helper.registerEventHandler(defaultButton, "click", 
						function()
						{
							holodeck.processChatCommand("/raidformat reset");
						}.bind(this)
					);
					this.pane.appendChild(defaultButton);
					
					
					
					
					
					var samplePostHeader = document.createElement("h2");
					samplePostHeader.className = "RaidMenuTab-SectionHeader";
					samplePostHeader.update("Sample Post");
					samplePostHeader.style.marginTop = "15px";
					this.pane.appendChild(samplePostHeader);

					// --- Sample Link --- \\
					// Create the sample raid link area to display the results of the format
					var raidStorage = RaidManager.fetchStorage();
					
					// Find any valid link to use as a sample
					//TODO: Customizable sample
					for (var id_hash in raidStorage)
					{
						this.sampleRaidLink = raidStorage[id_hash].raidLink;
						var tmpRaid = this.sampleRaidLink.getRaid();
						// Don't sample with invalid links (same full name and id, essentially Unknown raids)
						// Don't pick raids with the same FS and OS (size 250 raids)
						if (tmpRaid.fullName === tmpRaid.id || tmpRaid.size === 250)
						{
							continue;
						}
						break;
					}
					
					// If there wasn't a valid sample in the local storage, generate one
					if (typeof this.sampleRaidLink === "undefined")
					{
						// Will not have state info, though
						this.sampleRaidLink = new RaidLink(9999999999, "hash11hash", 4, "ragebeasts");
					}

					this.messageFormatExampleLinkContainer = document.createElement("div");
					
					var p = document.createElement("p");
					p.className = "even";
					this.messageFormatExampleLinkContainer.appendChild(p);

					var timestamp = document.createElement("p");
					timestamp.className = "timestamp";
					timestamp.appendChild(document.createTextNode(new Date().format("mmm d - h:MMTT")));
					p.appendChild(timestamp);

					var username = holodeck._active_user._attributes._object.username;
					var usernameSpan = document.createElement("span");
					usernameSpan.setAttribute("username", username);
					usernameSpan.className = "username chat_message_window_username";
					usernameSpan.update(username);
					p.appendChild(usernameSpan);
					
					var separatorSpan = document.createElement("span");
					separatorSpan.className = "separator";
					separatorSpan.update(": ");
					p.appendChild(separatorSpan);
					
					this.messageSpan = document.createElement("span");
					this.className = "message";
					this.messageSpan.innerHTML = this.sampleRaidLink.getFormattedRaidLink();
					p.appendChild(this.messageSpan);
					
					var clearSpan = document.createElement("span");
					clearSpan.className = "clear";
					p.appendChild(clearSpan);
					
					
					this.pane.appendChild(this.messageFormatExampleLinkContainer);

				},
				
				handleMessageFormatInput: function()
				{
					this.currentMessageFormat = this.messageFormatTextArea.value.replace(/(?:\r\n|\r|\n)/g, "{line}");
					this.messageSpan.innerHTML = this.sampleRaidLink.getFormattedRaidLink(this.currentMessageFormat);
				}
		});
		
  /************************************/
  /********* Preferences Tab **********/
  /************************************/

  // Class to manage a tab in the raid tab in the popup menu
  RaidMenuTab.create(
    {
      tabName: "Preferences",
      tabPosition: 100,

      rightClickVisitedKey: "RightClickVisited",
      ignoreInvalidCommandsKey: "IgnoreInvalidCommands",
      hideVisitedRaidsKey: "HideVisitedRaids",
      hideWorldChatKey: "HideWorldChat",
      loadRaidsInBackgroundKey: "LoadRaidsInBackground",
      reportDeadRaidsKey: "ReportDeadRaids",
      useQueryTimeDeltaKey: "UseQueryTimeDelta",
      loadRaidsInBackgroundDelayKey: "LoadRaidsInBackgroundDelay",
      leftClickToWhisperKey: "LeftClickToWhisper",
      rightClickUserMenuKey: "RightClickUserMenu",
      linkifyUrlsKey: "LinkifyUrls",
      chatTimestampRight: "ChatTimestampRight",

      initPane: function()
      {
        var wrapper = document.createElement("div");
        var me = this;

        var rightClickOption = me.createSimpleOptionHTML(
          "PreferencesMenu-RightClickVisitedInput",
          "boolean",
          DC_LoaTS_Helper.getPref(me.rightClickVisitedKey, true),
          "Right-click should mark raids visited.",
          "If checked, right clicking a link will mark it visited",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.rightClickVisitedKey, this.checked);
            }
          }
        );
        wrapper.appendChild(rightClickOption.wrapper);

        var ignoreInvalidOption = me.createSimpleOptionHTML(
          "PreferencesMenu-IgnoreInvalidCommandsInput",
          "boolean",
          DC_LoaTS_Helper.getPref(me.ignoreInvalidCommandsKey, true),
          "Ignore invalid commands.",
          "If checked, any command that is not a valid command will be ignored",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.ignoreInvalidCommandsKey, this.checked);
            }
          }
        );
        wrapper.appendChild(ignoreInvalidOption.wrapper);


        var hideVisitedOption = me.createSimpleOptionHTML(
          "PreferencesMenu-HideVisitedRaidsInput",
          "boolean",
          DC_LoaTS_Helper.getPref(me.hideVisitedRaidsKey, false),
          "Hide Visited and Completed Raids.",
          "If checked, Visited and Completed Raids posted into chat will be hidden",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.hideVisitedRaidsKey, this.checked);

              DC_LoaTS_Helper.handleIgnoreVisitedRaids(this.checked);
            }
          }
        );
        wrapper.appendChild(hideVisitedOption.wrapper);

        var hideWorldChat = me.createSimpleOptionHTML(
          "PreferencesMenu-HideWorldChatInput",
          "boolean",
          DC_LoaTS_Helper.getPref(me.hideWorldChatKey, false),
          "Hide World Chat",
          "If checked, World Chat will not be visible",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.hideWorldChatKey, this.checked);

              DC_LoaTS_Helper.handleHideWorldChat(this.checked);
            }
          }
        );
        wrapper.appendChild(hideWorldChat.wrapper);

        var loadBackgroundOption = me.createSimpleOptionHTML(
          "PreferencesMenu-LoadRaidsInBackgroundInput",
          "boolean",
          DC_LoaTS_Helper.getPref(me.loadRaidsInBackgroundKey, true),
          "Load Raids in Background (Skips the Play Now screen when loading raids)",
          "If checked, raids won't load in the game area.",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.loadRaidsInBackgroundKey, this.checked);
            }
          }
        );
        wrapper.appendChild(loadBackgroundOption.wrapper);

        var loadRaidsInBackgroundDelayOption = me.createSimpleOptionHTML(
          "PreferencesMenu-LoadRaidsInBackgroundDelayInput",
          "text",
          DC_LoaTS_Helper.getPref(me.loadRaidsInBackgroundDelayKey, 50),
          "ms. Time Between Loading Raids (Only applicable if Load Raids in Background is enabled)",
          "Default = 50; No delay = 0; Half a second = 500.",
          {
            size: 4,
            maxlength: 4,
            onchange: function()
            {
              var v = this.value;

              if (/^\d+$/.test(v))
              {
                DC_LoaTS_Helper.setPref(me.loadRaidsInBackgroundDelayKey, v);
              }
              else
              {
                holodeck.activeDialogue().raidBotMessage("Load Raids In Background Delay: Please enter only numbers.");
              }
            }
          }
        );
        wrapper.appendChild(loadRaidsInBackgroundDelayOption.wrapper);

        var leftClickToWhisperOption = me.createSimpleOptionHTML(
          "PreferencesMenu-LeftClickToWhisper",
          "boolean",
          DC_LoaTS_Helper.getPref(me.leftClickToWhisperKey, true),
          "Left click a user's name in chat to whisper them (Requires refresh after change)",
          "Overrides default functionality of showing mini-profile",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.leftClickToWhisperKey, this.checked);

              // Attach or detach the handlers
              DC_LoaTS_Helper.handleMessageWindowClickHandler();

              // Ask the user if they want to refresh now
              if (window.confirm && window.confirm("The page needs to be refreshed in order for your preference change to take effect. Confirm to refresh now; Cancel to refresh later.")) {
                document.location.reload();
              }
            }
          }
        );
        wrapper.appendChild(leftClickToWhisperOption.wrapper);

        var rightClickUserMenuOption = me.createSimpleOptionHTML(
          "PreferencesMenu-RightClickUserMenu",
          "boolean",
          DC_LoaTS_Helper.getPref(me.rightClickUserMenuKey, true),
          "Right click a user's name to show an action menu",
          "Contains options to show their profile, friend them, and eventually more",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.rightClickUserMenuKey, this.checked);

              // Attach or detach the handlers
              DC_LoaTS_Helper.handleMessageWindowContextMenuHandler();
            }
          }
        );
        wrapper.appendChild(rightClickUserMenuOption.wrapper);

        var linkifyUrlsOption = me.createSimpleOptionHTML(
          "PreferencesMenu-LinkifyUrls",
          "boolean",
          DC_LoaTS_Helper.getPref(me.linkifyUrlsKey, true),
          "Make URLs and #wiki's posted to chat be clickable links",
          "When someone posts a URL, automatically make it a link that will open in a new tab",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.linkifyUrlsKey, this.checked);
            }
          }
        );
        wrapper.appendChild(linkifyUrlsOption.wrapper);

        var chatTimestampRightOption = me.createSimpleOptionHTML(
          "PreferencesMenu-ChatTimestampRight",
          "boolean",
          DC_LoaTS_Helper.getPref(me.chatTimestampRight, true),
          "Chat timestamps on the right",
          "Moves the chat timestamps down and to the right",
          {
            onclick: function()
            {
              DC_LoaTS_Helper.setPref(me.chatTimestampRight, this.checked);
              DC_LoaTS_Helper.handleMoveChatTimestamps(this.checked);
            }
          }
        );
        wrapper.appendChild(chatTimestampRightOption.wrapper);

        this.pane.appendChild(wrapper);
      }
    });

		/************************************/
		/************ Raids Tab *************/
		/************************************/
		
		// Class to manage a tab in the raid tab in the popup menu
		RaidMenuTab.create(
			{
				tabName: "Raids",
				tabHeader: "Seen Raids",
				tabPosition: 10,
				
				initPane: function()
				{
					this.currentRaidFilter;
					
//					this.header = document.createElement("h1");
//					this.header.className = "RaidMenuTab-Header";
//					this.header.update("Seen Raids");
//					this.pane.appendChild(this.header);
//					
					this.searchWrapper = document.createElement("div");
					this.searchWrapper.id = "RaidsMenu-SearchWrapper";
					this.pane.appendChild(this.searchWrapper);
					
					this.searchWrapper.update("Search for raids: ");
					
					this.searchBox = document.createElement("input");
					this.searchBox.id = "RaidsMenu-SearchBox";
					this.searchBox.observe("input", function(e)
					{
						if (typeof this._searchBoxTypingTimeout)
						this.onSearchBoxChange();
					}.bind(this));
					
					this.searchWrapper.appendChild(this.searchBox);
					
					var afterSearchWrapper = document.createElement("div");
					afterSearchWrapper.update("RaidBot sees: /seenraids ");
					this.searchWrapper.appendChild(afterSearchWrapper);
					
					
					this.searchBoxNormalized = document.createElement("span");
					this.searchBoxNormalized.id = "RaidsMenu-SearchBoxNormalized";
					afterSearchWrapper.appendChild(this.searchBoxNormalized);
					
					this.resultsBox = document.createElement("div");
					this.resultsBox.id = "RaidsMenu-ResultsBox";
					this.pane.appendChild(this.resultsBox);
				},
				
				onSearchBoxChange: function()
				{
					if (this.searchBox.value.length < 3)
					{
						this.clearResults();
						return;
					}
					
					var tmpFilter = new RaidMultiFilter(this.searchBox.value);
					
					if (!this.currentRaidFilter || this.currentRaidFilter.toString() != tmpFilter.toString())
					{
						this.currentRaidFilter = tmpFilter;
						this.searchBoxNormalized.update(this.currentRaidFilter.toString());
						
						
						// Retrieve the message format
						var messageFormat = DC_LoaTS_Helper.getMessageFormat();
					
						// Retrieve the anchor tag format
						var linkFormat = DC_LoaTS_Helper.getLinkFormat();
						
						
						var raidLinks = RaidManager.fetchByFilter(this.currentRaidFilter);
						var raidsHTML = "";
						for (var i = 0; i < raidLinks.length; i++)
						{
							raidsHTML += (i+1) + ") " + raidLinks[i].getFormattedRaidLink(messageFormat.replace("{image}", ""), linkFormat) + "<br><br>";
						}
						
						this.resultsBox.innerHTML = raidsHTML;
					}
				},
				
				getRaidRow: function(link)
				{
					var raid = link.getRaid();
				},
				
				clearResults: function()
				{
					this.resultsBox.childElements().invoke("remove");
				}
			
		});
		
//		RaidMenuTab.raidSearchResultsFields = [{"raid."}]
		
		/************************************/
		/*********** Styles Tab *************/
		/************************************/
		
		// Class to manage a tab in the raid tab in the popup menu
//		RaidMenuTab.create(
//			{
//				tabName: "Styles",
//				tabHeader: "Raid Styles",
//				tabPosition: 20,
//				optionIndex: 0,
//
//				initPane: function()
//				{
//
//				}
//		});

		/************************************/
		/************ Utils Tab *************/
		/************************************/
		
		// Class to manage a tab in the raid tab in the popup menu
//		RaidMenuTab.create(
//			{
//				tabName: "Utils",
//				tabHeader: "Utilities (Under Construction)",
//				tabPosition: 50,
//				
//				initPane: function()
//				{
//					//TODO: Fill out utilities tab
//				}
//							
//		});
		
		RaidCommand.create( 
			{
				commandName: "autoupdate",
				aliases: [],
				// Custom parsing
				/*parsingClass: ,*/
				// Custom parsing means custom param text
				paramText: "[on/off]",
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {sucess: true};
					
					// 
					switch(params.toLowerCase())
					{
						// If there was no command, display current state
						case "":
							var updatesState = GM_getValue(DC_LoaTS_Properties.storage.autoUpdate);
							if (typeof updatesState == "undefined")
							{
								updatesState = true;
							}
							
							if (updatesState)
							{
								ret.statusMessage = "Automatic update checks are <code>ON</code>. Turn them " + this.getCommandLink("OFF","OFF?");
							}
							else
							{
								ret.statusMessage = "Automatic update checks are <code>OFF</code>. Turn them " + this.getCommandLink("ON","ON?");
							}
							
							break;
						// Turn updates on
						case "on":
							var updatesState = GM_setValue(DC_LoaTS_Properties.storage.autoUpdate, true);
							ret.statusMessage = "Automatic update checks are now <code>ON</code>";
							break;
						// Turn updates off
						case "off":
							var updatesState = GM_setValue(DC_LoaTS_Properties.storage.autoUpdate, false);
							ret.statusMessage = "Automatic update checks are now <code>OFF</code>";
							break;
						// Not sure what this command was supposed to be
						default:
							ret.statusMessage = "Did not understand command: <code>" + text + "</code>";
							ret.success = false;
					}
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Get autoupdate status"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/autoupdate toggle</code>\n";
					helpText += "Sets whether or not this script should automatically check for updates.\n";
					helpText += "where <code>toggle</code> <i>(optional)</i> is on or off\n";
					helpText += "\n";
					helpText += "If <code>toggle</code> is omitted, then the current status will be shown\n";
					helpText += "\n";
					helpText += "If there is an update to install and checks are on, when the page loads, a bar will appear";
					helpText += " at the top of the screen offering the option to update.\n";
					helpText += "\n";
					if (GM_getValue(DC_LoaTS_Properties.storage.autoUpdate, false)) {
						helpText += "Updates are currently ON. Turn them " + this.getCommandLink("OFF","OFF?") + "\n";
					}
					else {
						helpText += "Updates are currently OFF. Turn them " + this.getCommandLink("ON","ON?") + "\n";
					}
					
					return helpText;
				}
			}
		);
	
		RaidCommand.create( 
			{
				commandName: "checkload",
				aliases: ["loadcheck", "check", "load"],
				// No parsing needed
				/*parsingClass: ,*/

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
					
					var data = DC_LoaTS_Helper.autoLoader;
						
					if (data) {
						var fractionComplete = data.raidLinks.length / data.startingLinkCount,
						    percentComplete = Math.round(fractionComplete * 100);
						    timeElapsed = new Date()/1 - data.startTime,
						    timeRemaining = timeElapsed / fractionComplete;
						ret.statusMessage = "Attempted " + data.counter.attempted + " of " + data.startingLinkCount + " raids (" + percentComplete + "%) in " + timeElapsed + "ms.";
						ret.statusMessage += "\nEstimated Time Remaining: " + timeRemaining + " ms."
						ret.statusMessage += "\nCurrent Report: \n" + data.counter._generateReportText();
					}
					else {
						ret.statusMessage = "No load being performed at this time.";
					}
						
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Print the timer report"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/timerdata</code>\n";
					helpText += "Prints out timing and performance data about the script\n";
					
					return helpText;
				}
			}
		);
		
RaidCommand.create(
	{
		commandName: "clearchat",
		aliases: ["cc", "cls", "clear", "clean"],
		// No parsing
		//parsingClass: ,
		handler: function(deck, raidLink, params, text, context)
		{
			// Declare ret object
			var ret = {success: true, statusMessage: "Chat cleared at " + (new Date().toLocaleString())};

			// Load the raid from the link's url
			holodeck.activeDialogue().clear();

			return ret;
		},
		getOptions: function()
		{
			var commandOptions = {
				initialText: {
					text: "Clear chat?"
				}
			};

			return commandOptions;
		},
		buildHelpText: function()
		{
			var helpText = "<b>Raid Command:</b> <code>/clearchat</code>\n";
			helpText += "Clears the text of the chat.\n";
			helpText += "\n";
			helpText += this.getCommandLink("","Clear chat now?") + "\n";


			return helpText;
		}
	}
);
		
		RaidCommand.create( 
			{
				commandName: "clearraids",
				aliases: ["clearraid", "raidclear", "raidsclear", "clearcache"],
				parsingClass: RaidMultiFilter,
				handler: function(deck, raidFilter, params, text, context)
				{
					// Declare ret object
					ret = {};
					
					// If the user wants to clear all raids
					if (params && params.toLowerCase() === "all")
					{
						// Clear all raids stored in memory
						RaidManager.clear();
						
						// Notify the user
						ret.statusMessage = "All raids have been cleared from script memory.";
						ret.success = true;
					}
					// If there were no params. 
					// This used to clear all raids, but that was catching some people by surprise, :-P
					else if (params.length == 0)
					{
						// Notify the user
						ret.statusMessage = "/" + this.getName() + " no longer clears all raids. Use " + this.getCommandLink("all") + " to clear all raids or " + this.getCommandLink("help") + " to find out more about this command.";
						ret.success = true;
					}
					// The user posted specific criteria
					else
					{
						// Find all raids that match the user's criteria
						var raidLinks = RaidManager.fetchByFilter(raidFilter);
						
						// If the RaidManager executed successfully
						if (typeof raidLinks !== "undefined")
						{
							// If we didn't match a single raid
							if (raidLinks.length == 0)
							{
								ret.statusMessage = "Could not locate any raids to clear matching <code>" + raidFilter.toString() + "</code>";
								ret.success = true;
							}
							// If we did match some raids
							else
							{
								// Delete all found raids from memory
								RaidManager.clear(raidLinks);
								
								// Notify the user
								ret.statusMessage = "Cleared " + raidLinks.length + " raids from memory.";
								ret.success = true;
							}
						}
						// RaidManager failed
						else
						{
							ret.statusMessage = "Did not understand command: <code>/" + this.getName() + " " + raidFilter.toString() + "</code>";
							ret.success = false;
						}
					}
								
					return ret;
				},
				
				getOptions: function()
				{
					//TODO: Decide what options should go here
					var commandOptions;
					if (this.parser.name == "all")
					{
						commandOptions = {
							initialText: {
								text: "Clear all raids from memory"
							}
						};
					}
					else
					{
						commandOptions = {
							initialText: {
								text: "Clear raids from memory matching " + this.parser.toString()
							}
						};
					}
					
					return commandOptions;
				},

				
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/clearraids raidName difficully {state:stateName} {age: timeFormat} {fs: fsFormat}</code>\n";
					helpText += "Deletes all raids from script memory.\n";
					helpText += "where <code>raidName</code> <i>(optional)</i> is any partial or full raid name\n";
					helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
					helpText += "where <code>stateName</code> <i>(optional)</i> is either seen or visited\n";
					helpText += "where <code>timeFormat</code> <i>(optional)</i> is like <code>&lt;24h</code>, <code>&lt;30m</code>, or <code>&gt;1d</code>\n";
					helpText += "where <code>fsFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
					helpText += "\n"
					helpText += "<b>Examples:</b>\n"
					helpText += "\n"
					helpText += "<i>Clear all seen but keep all visited raids<i>\n"
					helpText += "<code>/clearraids {state:seen}</code>\n"
					helpText += "\n"
					helpText += "<i>Clear all raids you've seen, but not visited that you saw posted in the last 5 hours<i>\n"
					helpText += "<code>/clearraids {state:seen} {age: <5h}</code>\n"
					helpText += "\n"
					helpText += "<i>Clear all raids you've seen, but not visited that you saw posted in the last 5 hours that have FS &lt; 1M<i>\n"
					helpText += "<code>/clearraids {state:seen} {age: <5h} {fs:<1M}</code>\n"
					helpText += "\n"
					helpText += "<i>Clear all normal telemachus raids that you've visited before\n"
					helpText += "<code>/clearraids tele 1 {state:visited}</code>\n"
					helpText += "\n"
					helpText += "<i>Clear all void killer raids in memory\n"
					helpText += "<code>/clearraids killer</code>\n"
					helpText += "\n"
					helpText += "<i>Clear all void nightmare vorden raids\n"
					helpText += "<code>/clearraids vorden 4</code>\n"
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "diaversai",
				aliases: ["dia"],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
                    // Borrowed from: http://stackoverflow.com/a/5915122/1449525
                    function randomItem(items) {
                        return items[Math.floor(Math.random()*items.length)];
                    }

					// Declare ret object
					var ret = {success: true},
                        users_list = holodeck._active_dialogue._user_manager._active_room._users_list,
                        name = (params.trim().length > 0 ? params.trim() : randomItem(users_list).username),
                        quote = randomItem(DC_LoaTS_Helper.quotables);

                    quote = quote.format(randomItem(["dia", "diaversai"]), name);

                    holodeck._chat_window._active_room.sendRoomMessage(quote);
                    if (holodeck.username() !== "diaversai") {
                        DC_LoaTS_Helper.donateSpam("You, too, can have a command for a {donation of $10}!");
                    }

					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {
						initialText: {
							text: "Hear what dia has to say"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/diaversai</code>\n";
					helpText += "Hear what <a href='http://www.kongregate.com/accounts/diaversai'>dia</a> has to say.\n";
					
					return helpText;
				}
            }
		);
		
		RaidCommand.create( 
			{
				commandName: "exportraids",
				parsingClass: RaidMultiFilter,
				aliases: ["exportraid", "er"],
				
				handler: function(deck, raidFilter, params, text, context)
				{
					// Capture the start time of the query
					var queryStartTime = new Date()/1;
				
					// Declare ret object
					var ret = {};
					
					// Find all raids that match the user's criteria
					var raidLinks = RaidManager.fetchByFilter(raidFilter);
					
					// If the RaidManager executed successfully
					if (typeof raidLinks != "undefined")
					{
						// If we didn't match a single raid
						if (raidLinks.length == 0)
						{
							if (params.length == 0)
							{
								ret.statusMessage = "Could not locate any seen raids in memory.";
							}
							else
							{
								ret.statusMessage = "Could not locate any seen raids matching <code>" + params + "<code>";
							}
							
							// The lookup succeeded, we just didn't find anything
							ret.success = true;
						}
						// If we did match some raids
						else
						{
							// Capture all the text in one block
							var outputText = "";
							
							// For every link we found
							for (var i = 0; i < raidLinks.length; i++)
							{
								// Print matched links
								outputText += raidLinks[i].getURL() + "\n";
							}
							
							// Export the data we found
							//DC_LoaTS_Helper.forceDownload(outputText, raidFilter.toString().replace(" ", "_").replace(/\W/gi, ""));
							RaidMenuTab.createDataDumpTab(outputText, raidFilter.toString());
							
							
							// Print out the stats for the query
							ret.statusMessage = "<code>/" + this.commandName + " " + raidFilter.toString() + "</code> took " + (new Date()/1 - queryStartTime) + " ms and exported " + raidLinks.length + " results.";
							
							// Succeeded
							ret.success = true;
						}
					}
					// RaidManager failed
					else
					{
						ret.statusMessage = "Did not understand command: <code>" + text + "</code>";
						ret.success = false;
					}

					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Export matching data"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/exportraids raidName difficulty {state: stateName} {age: timeFormat} {fs: fsFormat} {count: numberResults} {page: resultsPage}</code>\n";
					helpText += "Exports to file raids that you've seen before in chat"
					helpText += "where <code>raidName</code> <i>(optional)</i> is any partial or full raid name\n";
					helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
					helpText += "where <code>stateName</code> <i>(optional)</i> is either seen or visited\n";
					helpText += "where <code>timeFormat</code> <i>(optional)</i> is like <code>&lt;24h</code>, <code>&lt;30m</code>, or <code>&gt;1d</code>\n";
					helpText += "where <code>fsFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
					helpText += "where <code>numberResults</code> <i>(optional)</i> is the number of results to display\n";
					helpText += "where <code>resultsPage</code> <i>(optional)</i> is if you've set count, then which page to show. If page is omitted, it will show the first page of results.\n";
					helpText += "\n";
					helpText += "Example:\n";
					helpText += "Export all seen psychic colonels, including visited: \n";
					helpText += this.getCommandLink("psy {state: !completed}") + "\n";

					return helpText;
				}
			}
		);
		

RaidCommand.create(
    {
        commandName: "exportusers",
        aliases: ["eu"],
        doNotEnumerateInHelp: true,

        handler: function(deck, parser, params, text, context)
        {
            // Declare ret object
            var ret = {success: true, message: "Exported Users"};

            var playerData = "Date: " + new Date() + "\n" +
                "User,Level,Admin,Developer,K+\n";

            for (var i = 0; i < active_room._users_list.length; i++) {
                var ufr = active_room._users_list[i];
                playerData += [ufr.username, ufr._level, ufr._admin, ufr._developer, ufr._premium].join(",") + "\n";
            }

            RaidMenuTab.createDataDumpTab(playerData, "KChat Users");

            return ret;
        },

        getOptions: function()
        {
            return {};
        },

        buildHelpText: function()
        {
            return "";
        }
    }
);
		
		RaidCommand.create( 
			{
				commandName: "farmvalue",
				aliases: [],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};

					var farmText = "<table>";
					farmText += "<tr><th>Stamina Raid</th><th>Norm Farm Val</th><th>NM Farm Val</th></tr>"
					farmText += "<tr><td>Void Killer</td><td>19.8</td><td>6.6</td></tr>";
					farmText += "<tr><td>Ragebeasts</td><td>12.6</td><td>6.3</td></tr>";
					farmText += "<tr><td>Telemachus</td><td>11.0</td><td>3.7</td></tr>";
					farmText += "<tr><td>CC Colonel</td><td>9.3</td><td>2.3</td></tr>";
					farmText += "<tr><td>Supreme Cybertollahs</td><td>8.4</td><td>2.1</td></tr>";
					farmText += "<tr><td>Carnus</td><td>6.6</td><td>2.6</td></tr>";
					farmText += "<tr><td>Carnifex</td><td>6.3</td><td>2.5</td></tr>";
					farmText += "<tr><td>Rautha</td><td>5.9</td><td>1.5</td></tr>";
					farmText += "<tr><td>Vespasia's Android</td><td>5.6</td><td>1.6</td></tr>";
					farmText += "<tr><td>CC Cruiser</td><td>5.3</td><td>1.3</td></tr>";
					farmText += "<tr><td>Assassin</td><td>4.5</td><td>1.4</td></tr>";
					farmText += "<tr><td>Vorden</td><td>4.2</td><td>1.4</td></tr>";
					farmText += "<tr><td>CC General</td><td>4.0</td><td>1.0</td></tr>";
					farmText += "<tr><td>Warmaster </td><td>3.7</td><td>0.8</td></tr>";
					farmText += "<tr><td>Robotic Rautha</td><td>3.7</td><td>0.9</td></tr>";
					farmText += "<tr><td>CC Sentinel</td><td>3.4</td><td>0.8</td></tr>";
					farmText += "<tr><td>Mermara</td><td>3.3</td><td>0.7</td></tr>";
					farmText += "<tr><td>Purple Lion</td><td>3.2</td><td>1.1</td></tr>";
					farmText += "<tr><td>Cybersmash</td><td>3.1</td><td>1.0</td></tr>";
					farmText += "<tr><td>Blood Alley Gang</td><td>2.8</td><td>0.9</td></tr>";
					farmText += "<tr><td>Bashan</td><td>2.3</td><td>0.6</td></tr>";
					farmText += "<tr><td>Missiles</td><td>2.3</td><td>0.6</td></tr>";
					farmText += "<tr><td>Tulk</td><td>2.2</td><td>0.6</td></tr>";
					farmText += "<tr><td>Scarlet Harlot</td><td>2.0</td><td>0.6</td></tr>";
					farmText += "<tr><td>Agony Ecstacy</td><td>1.8</td><td>0.7</td></tr>";
					farmText += "<tr><td>Sludge Serpent</td><td>1.8</td><td>0.7</td></tr>";
					farmText += "<tr><td>Lupin</td><td>1.7</td><td>0.7</td></tr>";
					farmText += "<tr><td>Mercury</td><td>1.6</td><td>0.6</td></tr>";
					farmText += "<tr><td>Storm Commander</td><td>1.6</td><td>0.5</td></tr>";
					farmText += "<tr><td>Sun-Xi</td><td>1.5</td><td>0.6</td></tr>";
					farmText += "<tr><td>Lt. Targe</td><td>1.4</td><td>0.6</td></tr>";
					farmText += "<tr><td>Guldax Quibberath</td><td>1.4</td><td>0.5</td></tr>";
					farmText += "<tr><td>Bachanghenfil</td><td>1.3</td><td>0.3</td></tr>";
					farmText += "<tr><td>Warden Ramiro</td><td>1.3</td><td>0.5</td></tr>";
					farmText += "<tr><td>Nemo</td><td>1.3</td><td>0.5</td></tr>";
					farmText += "<tr><td>Gut-Phager</td><td>1.2</td><td>0.2</td></tr>";
					farmText += "<tr><td>Vulture Gunship</td><td>1.2</td><td>0.5</td></tr>";
					farmText += "<tr><td>Caligula</td><td>1.2</td><td>0.4</td></tr>";
					farmText += "<tr><td>Cyborg Shark</td><td>1.1</td><td>0.3</td></tr>";
					farmText += "<tr><td>Guan Yu</td><td>1.1</td><td>0.3</td></tr>";
					farmText += "<tr><td>Pi</td><td>1.1</td><td>0.4</td></tr>";
					farmText += "<tr><td>Sigurd</td><td>1.1</td><td>0.3</td></tr>";
					farmText += "<tr><td>Bile Beat</td><td>1.0</td><td>0.3</td></tr>";
					farmText += "<tr><td>Fleet Com.</td><td>0.9</td><td>0.3</td></tr>";
					farmText += "<tr><td>Reaver</td><td>0.9</td><td>0.2</td></tr>";
					farmText += "<tr><td>Cult-Mistress</td><td>0.9</td><td>0.2</td></tr>";
					farmText += "<tr><td>Nick</td><td>0.9</td><td>0.3</td></tr>";
					farmText += "<tr><td>Cake</td><td>0.9</td><td>0.2</td></tr>";
					farmText += "<tr><td>The Hat</td><td>0.8</td><td>0.3</td></tr>";
					farmText += "<tr><td>Xenocide</td><td>0.7</td><td>0.2</td></tr>";
					farmText += "<tr><td>Colossa</td><td>0.7</td><td>0.1</td></tr>";
					farmText += "<tr><td>Blob</td><td>0.5</td><td>0.2</td></tr>";
					farmText += "<tr><td>Councilor</td><td>0.5</td><td>0.1</td></tr>";
					farmText += "<tr><td>Boar</td><td>0.5</td><td>0.1</td></tr>";
					farmText += "<tr><td>R. Hunter</td><td>0.4</td><td>0.2</td></tr>";
					farmText += "<tr><td>G. Rahn</td><td>0.3</td><td>0.1</td></tr>";
					farmText += "<tr><td>Dule's Bot</td><td>0.3</td><td>0.1</td></tr>";
					
					farmText += "<tr><td></td><td></td><td></td></tr>";
					
					farmText += "<tr><th>Energy Raid</th><th>Norm Farm Val</th><th>NM Farm Val</th></tr>";
					farmText += "<tr><td>Vince Vortex</td><td>1.7</td><td>0.4</td></tr>";
					
					farmText += "<tr><td></td><td></td><td></td></tr>";
					
					farmText += "<tr><th>Honor Raid</th><th>Norm Farm Val</th><th>NM Farm Val</th></tr>";
					farmText += "<tr><td>Krakak Swarm</td><td>5.6</td><td>1.9</td></tr>";
					farmText += "<tr><td>Infected Squad</td><td>4.4</td><td>1.3</td></tr>";
					farmText += "<tr><td>Flying Saucers</td><td>4.0</td><td>1.6</td></tr>";
					farmText += "<tr><td>Lurking Horror</td><td>3.7</td><td>0.9</td></tr>";
					farmText += "<tr><td>Kang</td><td>3.4</td><td>1.0</td></tr>";
					farmText += "<tr><td>Tourniquet 7</td><td>2.4</td><td>0.9</td></tr>";
					farmText += "<tr><td>Ship of the Damned</td><td>2.3</td><td>0.8</td></tr>";
					farmText += "<tr><td>Wyrm</td><td>2.0</td><td>0.7</td></tr>";
					farmText += "<tr><td>Death Flora</td><td>1.9</td><td>0.6</td></tr>";
					farmText += "<tr><td>Crossbones Squadron</td><td>1.6</td><td>0.5</td></tr>";
					farmText += "<tr><td>Celebrator</td><td>1.6</td><td>0.5</td></tr>";
					farmText += "<tr><td>Shadows</td><td>1.4</td><td>0.3</td></tr>";
					farmText += "<tr><td>Mr. Justice</td><td>1.1</td><td>0.3</td></tr>";
					farmText += "<tr><td>Rylattu Exterminators</td><td>1.1</td><td>0.4</td></tr>";
					farmText += "<tr><td>Colonel Mustard</td><td>1.1</td><td>0.4</td></tr>";
					farmText += "<tr><td>Luna</td><td>1.0</td><td>0.4</td></tr>";
					farmText += "<tr><td>Genesis</td><td>0.9</td><td>0.5</td></tr>";
					farmText += "<tr><td>Grislak</td><td>0.9</td><td>0.3</td></tr>";
					farmText += "<tr><td>Interceptor</td><td>0.9</td><td>0.2</td></tr>";
					farmText += "<tr><td>Peacemaker 500</td><td>0.8</td><td>0.3</td></tr>";
					farmText += "<tr><td>Qin Legion</td><td>0.8</td><td>0.3</td></tr>";
					farmText += "<tr><td>Juggernaut</td><td>0.7</td><td>0.2</td></tr>";
					farmText += "<tr><td>Squid</td><td>0.7</td><td>0.2</td></tr>";
					farmText += "<tr><td>Death Squadron</td><td>0.7</td><td>0.1</td></tr>";
					farmText += "<tr><td>H. House</td><td>0.6</td><td>0.1</td></tr>";
					farmText += "<tr><td>Devourer</td><td>0.6</td><td>0.1</td></tr>";
					farmText += "<tr><td>Colby</td><td>0.5</td><td>0.1</td></tr>";
					farmText += "<tr><td>Legacy Bot</td><td>0.4</td><td>0.1</td></tr>";
					farmText += "<tr><td>Psi-Hound</td><td>0.2</td><td>0.1</td></tr>";
					farmText += "<tr><td>Wahsh</td><td>0.0</td><td>0.0</td></tr>";
					
					farmText += "</table>";
					farmText += "<a href=\"" + DC_LoaTS_Properties.farmSpreadsheetURL + "\" target=\"_blank\">Source</a>\n";

					deck.activeDialogue().raidBotMessage(farmText);
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Display farm values"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/farmvalue</code>\n";
					helpText += "Displays the farm value of the raids per <a href=\"" + 
								DC_LoaTS_Properties.farmSpreadsheetURL + "\" target=\"_blank\">this spreadsheet</a>\n";
					
					return helpText;
				}
			}
		);
		
		
		// Fetch raids command
		RaidCommand.create( 
			{
				commandName: "fetchraids",
				aliases: ["fetch", "fr"],
				parsingClass: UrlParsingFilter,
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: parser.known || parser.force || parser.cancel};
						
					if (ret.success) {
						DC_LoaTS_Helper.fetchAndLoadRaids(parser);
					}
					else {
						if (!parser.known) {
							ret.statusMessage = parser.getWorkingUrl() + " is not from a known raid host. Are you sure you wish to fetch from there? " + DC_LoaTS_Helper.getCommandLink("/fetchraids force " + params);
						}
						else {
							ret.statusMessage = "Could not find a url in <code>" + text + "</code>";
						}
					}
					return ret;
				},
							
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Load raids from: " + this.parser.getWorkingUrl()
						}
					};
					
					return commandOptions;
				},
				
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/fetchraids url</code>\n";
					helpText += "where <code>url</code> is the url of a raid host of some kind\n";
					helpText += "\n";
					helpText += "Loads all raids from the url, or whichever ones match the filter\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "forum",
				urlPattern: "http://www.legacyofathousandsuns.com/forum/search.php?do=process&sortby=rank&query=%searchString%",
				// No parsing
				/*parsingClass: ,*/
				aliases: ["forums"],
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
					
					var url = this.createURL(params);
					
					window.open(url, "_blank");
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Search Forum for: " + this.processedText,
							linkParams: {href: this.createURL(this.processedText), target: "_blank"},
							doNotCallHandler: true,
							followLink: true
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/forum searchText</code>\n";
					helpText += "where <code>searchText</code> is what you want to search for on the LoTS Forum\n";
					
					return helpText;
				},
				
				createURL: function(searchInput)
				{
					searchInput = searchInput || " ";
					return this.urlPattern.replace("%searchString%",searchInput);
				}
			}
		);        RaidCommand.create( 
            {
                commandName: "konginfo",
                aliases: [],
                doNotEnumerateInHelp: true, // Don't list this in the help
               // No parsing needed
                /*parsingClass: ,*/
                handler: function(deck, parser, params, text, context)
                {
                    // Declare ret object
                    var ret = {success: true};
                    
                    ret.statusMessage = "Kong ID: " + active_user.id() + "\n";
                    ret.statusMessage = "Kong Hash: " + active_user.gameAuthToken();

                    return ret;
                },
                
                getOptions: function()
                {
                    var commandOptions = {
                        initialText: {
                            text: "Get important info about your Kong game."
                        }
                    };
                    
                    return commandOptions;
                },
                
                buildHelpText: function()
                {
                    var helpText = "<b>Raid Command:</b> <code>/konginfo</code>\n";
                    helpText += "Displays important Kong info.\n";
                    
                    return helpText;
                }
            }
        );
        
		RaidCommand.create( 
			{
				commandName: "linkstate",
				aliases: ["setcachestate", "setstate"],
				parsingClass: RaidLinkstateParser,
				/*public Object*/ handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					// If there's a raid link but no new state set
					if (typeof parser.raidLink != "undefined" && typeof parser.state == "undefined")
					{
						// Get the current state
						var state = RaidManager.fetchState(parser.raidLink);
						
						// Print the current state
						ret.statusMessage = parser.raidLink.getName() + "(raid_id: " + parser.raidLink.id + ") is in state " + state.niceText;
						
						// Success
						ret.success = true;
					}
					// If there's a raid link and there's a new state to set it to
					else if (typeof parser.raidLink != "undefined" && typeof parser.state != "undefined")
					{
						// Get the actual state
						var actualState = RaidManager.STATE.valueOf(parser.state);
						
						DCDebug("About to set link to state: ");
						DCDebug(actualState);
						
						if (typeof actualState != "undefined")
						{
							// Set the new state for the raid link
							RaidManager.store(parser.raidLink, actualState);
							
							// Get the current state
							var state = RaidManager.fetchState(parser.raidLink);
							
							// Print the current state
							ret.statusMessage = parser.raidLink.getName() + " (raid_id: " + parser.raidLink.id + ") is now in state " + state.niceText;
							
							// Success
							ret.success = RaidManager.STATE.equals(parser.state, state);
						}
						// Could not actually locate the state the user tried to set this link to
						else
						{
							console.warn("Could not locate a corresponding state to " + parser.state + " in command " + text);

							// Failed
							ret.success = false;
							
							// Print the failure message
							ret.statusMessage = "Could not find match <code>" + parser.state + "</code> to a known state in <code>" + text + "</code>";
						}

					}
					// Must not have found a raid link
					else
					{
						// Failure
						ret.success = false;
						ret.statusMessage = "Could not find raid link in <code>" + text + "</code>";
					}
												
					return ret;
				},
				
				/*public Object*/ getOptions: function()
				{
					var linkState = RaidManager.fetchState(this.parser.raidLink);
					
					var commandOptions = {					
						initialText: {
							text: "Mark this " + linkState.niceText + " " + this.parser.getName(),
							executable: false
						}
					};
					
					for (var stateType in RaidManager.STATE)
					{
						if (typeof RaidManager.STATE[stateType] == "object" && linkState.id != RaidManager.STATE[stateType].id)
						{
							commandOptions["mark_" + stateType.toLowerCase()] = this.createStateOption(RaidManager.STATE[stateType]);
						}
					}
					
					return commandOptions;
				},
				
				/*private Object*/ createStateOption: function(state)
				{
					return {
						text: state.niceText,
						callback: function(state){this.parser.state = state.id}.bind(this, state)
					};
				},
				
				/*protected String*/ buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/loadraid url</code>\n";
					helpText += "where <code>url</code> is the url of a raid\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "linktools",
				aliases: ["ad", "advertise", "blatantselfpromotion", "getdoomscript"],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};

                    var toolsText;

                    if (params.indexOf("2") > -1) {
                        toolsText = this.getToolsText2();
                    }
                    else {
                        toolsText = this.getToolsText();
                    }

                    if (DC_LoaTS_Helper.getPref("LinkifyUrls", true)) {
                        toolsText = toolsText.replace(urlPattern, function(url) {
                            // Last minute check to make sure the regex didn't flub it
                            // If the url contains any weird characters, ", ', <, or >, just bail
                            return /["'><]/g.test(url) ? url : urlFormat.format(url);
                        });
                    }

                    // If the user passed in the "post" param or is using /ad, show it publicly
					if (params.trim() === "post" || text.toLowerCase().trim() === "/ad") {
						holodeck._chat_window._active_room.sendRoomMessage(toolsText);
					}
					else {
						deck.activeDialogue().raidBotMessage(toolsText);
					}
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Display tools links" + (this.commandText.indexOf("2") > -1 ? " (Page 2)" : "")
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/linktools [post]</code>\n";
					helpText += "Displays a list of scripts that you might find useful.\n";
					helpText += "<code>" + this.getCommandLink("") + "</code> will post the links just to you.\n";
					helpText += "<code>" + this.getCommandLink("post") + "</code> will post the links to chat.\n";
					helpText += "\n";
					helpText += "Note: The <code>" + DC_LoaTS_Helper.getCommandLink("/ad") + "</code> alias automatically posts, unlike the other aliases.";

					return helpText;
				},

                getToolsText: function() {
                    var toolsText = "\nGet doomscript: " + DC_LoaTS_Properties.scriptURL + " and any of: ";
                    toolsText += "\nRaidTools: " + DC_LoaTS_Properties.RaidToolsURL + " ";
                    toolsText += "\nQuickFriend: " + DC_LoaTS_Properties.QuickFriendURL + " ";
                    toolsText += "\nPlay Now Fix: " + DC_LoaTS_Properties.PlayNowFixURL;

                    return toolsText;
                },
                getToolsText2: function() {
                    var toolsText = "\nFleet Codes: " + DC_LoaTS_Properties.FleetCodesURL;

                    return toolsText;
                }
			}
		);
		
//TODO: Remove Autoload alias. AutoLoad should be for incoming new raids, not loading existing ones
		RaidCommand.create(
			{
				commandName: "loadall",
				aliases: ["autoload"],
				parsingClass: RaidMultiFilter,

				handler: function(deck, raidFilter, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					var isCancelled = params === "cancel";
										
					// Cancel the previous timer, if there is one
					if (typeof DC_LoaTS_Helper.autoLoader !== "undefined" || isCancelled)
					{
						// Clear out the raidLinks array from the previous one.
						// The timeout will detect that there are suddenly no more links
						// and acknowledge the error state and quit.
						DC_LoaTS_Helper.autoLoader.raidLinks.length = 0;
					}
					
					
					// This only works with a valid filter
					if (!isCancelled && raidFilter && raidFilter.isValid())
					{
						// Fetch all the links
						var raidLinks = RaidManager.fetchByFilter(raidFilter);
						
						// If there were any matched links
						if (raidLinks.length > 0)
						{
							ret.success = true;
							ret.statusMessage = "AutoLoad starting for " + raidFilter.toString() + ". Loading " + raidLinks.length + " raids. " + this.getCommandLink("cancel", "Cancel");
							
							DC_LoaTS_Helper.loadAll(raidLinks);
						}
						else
						{
							ret.statusMessage = "AutoLoad could not find any raids matching " + raidFilter.toString() + " to load.";							
						}
						
						ret.success = true;
					}
					else if (!isCancelled)
					{
						ret.success = false;
						ret.statusMessage = "Could not execute autoload due to invalid raid filter: '" + raidFilter.toString() + "'.";
					}
					else 
					{
						ret.success = true;
						ret.statusMessage = "AutoLoad cancelled.";
					}
						
					return ret;
				},
				getOptions: function()
				{
					//TODO: Better options here
					var commandOptions = {					
						initialText: {
							text: "Load all raids matching the filter"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/loadall raidFilter</code>\n";
					helpText += "where <code>raidFilter</code> is a valid raid filter\n";
					helpText += "\n";
					helpText += "\nLoads all seen raids that match the given filter";
					helpText += "\n";
					helpText += "\nFor example, " + this.getCommandLink("colonel 4") + " would load all nightmare colonels not previously visited";
					helpText += "\n";
					helpText += "<b>This feature is implemented for experimental/academic purposes only and should not be distributed!</b>\n";
					
					return helpText;
				}
			}
		);
		
		
		// Load CConoly command
		RaidCommand.create( 
			{
				commandName: "loadcconoly",
				aliases: ["loadcc", "lcc", "cconoly", "cc", "loadcconolyraids", "loadccraids"],
				// No predefined parsing
				// parsingClass: none,
				
				paramText: "[filter]",

				handler: function(deck, parser, params, text, context)
				{
					if (params === "cancel") {
						parser = new UrlParsingFilter("cancel");
					}
					else {
						parser = new UrlParsingFilter(CConolyAPI.getRaidListUrl() + " " + params);
					}
					
					// Declare ret object
					var ret = {success: parser.type === "cconoly"};
						
					if (ret.success) {
						DC_LoaTS_Helper.fetchAndLoadRaids(parser);
					}
					else {
						ret.statusMessage = "Error processing command <code>" + text + "</code>";
						DCDebug("Error with /lcc. Parser: ", parser);
					}
					return ret;
				},
					
							
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Load CConoly raids"
						}
					};
					
					return commandOptions;
				},
				
				buildHelpText: function()
				{
                    var helpText = "<b>Raid Command:</b> <code>/loadcconoly raidName difficulty {state: stateName} {age: timeFormat} {size: sizeFormat} {fs: fsFormat} {os: osFormat} {zone: zoneNumber} {count: numberResults} {page: resultsPage}</code>\n";
					helpText += "\n";
                    helpText += "Looks up raids from CConoly. "
                    helpText += "where <code>raidName</code> <i>(optional)</i> is any partial or full raid name\n";
                    helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
                    helpText += "where <code>stateName</code> <i>(optional)</i> is either seen or visited\n";
                    helpText += "where <code>timeFormat</code> <i>(optional)</i> is like <code>&lt;24h</code>, <code>&lt;30m</code>, or <code>&gt;1d</code>\n";
                    helpText += "where <code>sizeFormat</code> <i>(optional)</i> is like <code>&lt;100</code> or <code>250</code>\n";
                    helpText += "where <code>osFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
                    helpText += "where <code>fsFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
                    helpText += "where <code>zoneNumber</code> <i>(optional)</i> is like <code>1</code>, <code>Z14</code>, <code>ZA</code>, <code>WR</code>\n";
                    helpText += "where <code>numberResults</code> <i>(optional)</i> is the number of results to display\n";
                    helpText += "where <code>resultsPage</code> <i>(optional)</i> is if you've set count, then which page to show. If page is omitted, it will show the first page of results.\n";
                    helpText += "\n";
                    helpText += "<b>Examples:</b>\n";
                    helpText += "\n";
                    helpText += "<i>Find all raids you've seen, but not visited<i>\n";
                    helpText += "<code>" + this.getCommandLink("{state:seen}") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Find all raids you've seen, but not visited that you saw posted in the last 5 hours<i>\n";
                    helpText += "<code>" + this.getCommandLink("{state:seen} {age: <5h}") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Find all raids you've seen, but not visited that you saw posted in the last 5 hours that have FS &lt; 1M<i>\n";
                    helpText += "<code>" + this.getCommandLink("{state:seen} {age: <5h} {fs:<1M}") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Find all normal telemachus raids that you've not visited before\n";
                    helpText += "<code>" + this.getCommandLink("tele 1 {state:!visited}") + " </code>\n";
                    helpText += "\n";
                    helpText += "<i>Find the first 10 void killer raids you've seen\n";
                    helpText += "<code>" + this.getCommandLink("killer {count: 10}") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Find the second 10 void killer raids you've seen\n";
                    helpText += "<code>" + this.getCommandLink("killer {count: 10} {page: 2}") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Find all void nightmare vorden raids you've seen\n";
                    helpText += "<code>" + this.getCommandLink("vorden 4") + "</code>\n";
                    helpText += "\n";
                    helpText += "<i>Looking for <a href=\"http://www.zoywiki.com/index.php/LotS/experiment/multicoloredcloorian\" title=\"Cloorian Material needed to craft some Legendary pants\">Cloorian Material<a/>\n";
                    helpText += "<code>" + this.getCommandLink("vor|gan|nat 4 {age: <24h} {state: !visited}") + "</code>\n";
					
					return helpText;
				}
			}
		);
		
		
			// Load Pastebin command
		RaidCommand.create( 
			{
				commandName: "loadpastebin",
				aliases: ["loadpaste", "loadbin", "lpb", "loadraidbin", "lrb"],
				parsingClass: UrlParsingFilter,
				
				pastebinRawBase: "http://pastebin.com/raw.php?i=",
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: parser.type === "pastebin"};
						
					if (ret.success) {
						// Make sure to convert to the better url
						parser.convertedUrl = this.pastebinRawBase + parser.regexMatch[1];
						DC_LoaTS_Helper.fetchAndLoadRaids(parser);
					}
					else {
						if (parser.getWorkingUrl()) {
							ret.statusMessage = parser.url + " does not appear to be a pastebin link. Try " + DC_LoaTS_Helper.getCommandLink("/fetchraids " + params);
						}
						else {
							ret.statusMessage = "Could not find a pastebin link in <code>" + text + "</code>";
						}
					}
					return ret;
				},
							
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Load bin raids from: " + this.parser.getWorkingUrl()
						}
					};
					
					return commandOptions;
				},
				
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/loadpastebin pastebinURL raidFilter</code>\n";
					helpText += "where <code>pastebinURL</code> is the url of a raid pastebin\n";
					helpText += "where <code>raidFilter</code> (optional) is a seenraids style filter to limit what's loaded from the bin\n";
					helpText += "\n";
					helpText += "Loads all raids from the pastebin, or whichever ones match the filter\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "loadraid",
				aliases: ["addraid", "joinraid", "loadraids", "raidload", "raidadd", "raidjoin", "lr"],
				parsingClass: RaidLink,
				handler: function(deck, raidLink, params, text, context)
				{
					// Declare ret object
					var ret = {};
						
					// Load the raid from the link's url
					ret.success = !DC_LoaTS_Helper.loadRaid(raidLink.getURL());
						
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Load raid: " + this.parser.getDifficultyText() + " " + this.parser.getName()
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/loadraid url</code>\n";
					helpText += "where <code>url</code> is the url of a raid\n";
					
					return helpText;
				}
			}
		);
		
    RaidCommand.create(
        {
            commandName: "loadraidmonitor",
            aliases: ["loadrm", "lrm", "raidmonitor", "rm", "loadraidmonitorraids", "loadrmraids", "lrmalpha"],
            parsingClass: RaidMultiFilter,

            paramText: "[filter]",

            handler: function(deck, parser, params, text, context)
            {
                var ret = {success: true};
                DC_LoaTS_Helper.ajax({
                    url: "http://getKongE.org/games/lots/raids/raids.json",
                    onload: function(response) {
                        var message;
                        if (response.status === 200) {
                            var resp = JSON.parse(response.responseText),
                                raids = resp.raids;

                            if (false === resp.success) {
                                holodeck.activeDialogue().raidBotMessage("Error loading from RaidMonitor: " +
                                    resp.error);
                            }
                            else {
                                var buckets = {
                                    private: {
                                        header: "Private Raids",
                                        key: "PrivateRaid",
                                        raids: []
                                    },
                                    alliance: {
                                        header: "Alliance Raids",
                                        key: "AllianceRaid",
                                        raids: []
                                    },
                                    public: {
                                        header: "Public Raids",
                                        key: "PublicRaid",
                                        raids: []
                                    }
                                };

                                var i, raid;
                                for (i in raids) {
                                    if (!raids.hasOwnProperty(i)) continue;
                                    raid = raids[i];
                                    raid.def = DC_LoaTS_Helper.raids[raid.boss];

                                    if (!raid.def) {
                                        console.log("No def", i, raid);
                                        continue;
                                    }

                                    if (raid.def && "Alliance" === raid.def.type) {
                                        buckets.alliance.raids.push(raid);
                                    }
                                    else if (!raid.pass) {
                                        buckets.public.raids.push(raid);
                                    }
                                    else {
                                        buckets.private.raids.push(raid);
                                    }
                                }

                                var processBucket = function(bucket, markVisited) {
                                    var matchedRaids = [];
                                    for (i in bucket.raids) {
                                        if (!bucket.raids.hasOwnProperty(i)) continue;
                                        raid = bucket.raids[i];

                                        var now = new Date();
                                        var now_utc = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),  now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
                                        var remainingSecs = -1,
                                            elapsedSecs = -1, totalSecs = -1;

                                        var remainingHealth = -1,
                                            totalHealth = -1;

                                        if (raid.summonDate && raid.def) {
                                            var summonDateParts = raid.summonDate.split(" ");
                                            var sdd = summonDateParts[0].split("-");
                                            var sdt = summonDateParts[1].split(":");

                                            totalSecs = raid.def.time*60*60;
                                            elapsedSecs = Math.ceil((now_utc - new Date(sdd[0], sdd[1]-1, sdd[2], sdt[0], sdt[1], sdt[2])) / 1000);
                                            remainingSecs = totalSecs - elapsedSecs;

                                            remainingHealth = raid.health;
                                            totalHealth = raid.def.health[raid.diff];
                                        }

                                        var dcRaidDef = DC_LoaTS_Helper.raids[raid.boss];
                                        var fs = dcRaidDef.getFairShare(raid.diff);
                                        if (parser.matches({
                                            age: elapsedSecs,
                                            difficulty: raid.diff,
                                            fs:  fs !== "N/A"? fs : -1,
                                            os:  dcRaidDef.getOptimalShare(raid.diff),
                                            name: dcRaidDef.getSearchableName(),
                                            progress: (remainingHealth/totalHealth)/(remainingSecs/totalSecs),
                                            size: dcRaidDef.size,
                                            zone: dcRaidDef.zone
                                        })) {
                                            matchedRaids.push(raid);
                                        }
                                    }

                                    holodeck.activeDialogue().raidBotMessage("Downloaded " + bucket.raids.length + " raids. Loading " + matchedRaids.length + " that matched <code>" + parser.toString() + "</code>...");

                                    var joinResults = {joined: 0, visited: 0, dead: 0, invalid: 0};

                                    function loadMatchedRaids() {
                                        if (matchedRaids.length > 0) {
                                            raid = matchedRaids.pop();
                                            DC_LoaTS_Helper.ajax({
                                                url: DC_LoaTS_Properties.joinRaidURL + "?kongregate_user_id=" + active_user.id() + "&kongregate_game_auth_token=" + active_user.gameAuthToken() + "&kv_raid_id=" + raid.raid_id + "&kv_hash=" + raid.hash,
                                                onload: function(response) {
                                                    var raidLink = new RaidLink(raid.raid_id, raid.hash, raid.difficulty, raid.boss);
                                                    var responseText = response.responseText;
                                                    if (responseText.indexOf("You have successfully joined the raid!") >= 0 || responseText.indexOf("You have successfully re-joined the raid!") >= 0)
                                                    {
                                                        // Joined
                                                        joinResults.joined++;
                                                        RaidManager.store(raidLink, RaidManager.STATE.VISITED);
                                                    }
                                                    else if (responseText.indexOf("You are already a member of this raid!") >= 0)
                                                    {
                                                        // Already visited / rejoined
                                                        joinResults.visited++;
                                                        RaidManager.store(raidLink, RaidManager.STATE.VISITED);
                                                    }
                                                    else if (responseText.indexOf("This raid is already completed!") >= 0)
                                                    {
                                                        // Raid is dead
                                                        joinResults.dead++;
                                                        RaidManager.store(raidLink, RaidManager.STATE.COMPLETED);
                                                    }
                                                    else
                                                    {
                                                        // Invalid response (bad hash, wrong alliance, or otherwise broken link)
                                                        joinResults.invalid++;
                                                        RaidManager.store(raidLink, RaidManager.STATE.IGNORED);
                                                    }
                                                    setTimeout(loadMatchedRaids, 10);
                                                }
                                            });
                                        }
                                        else {
                                            var msg = "Raid Monitor loading results for " + bucket.header + ":";
                                            msg += "\nJoined: " + joinResults.joined;
                                            msg += "\nVisited: " + joinResults.visited;
                                            msg += "\nDead: " + joinResults.dead;
                                            msg += "\nInvalid: " + joinResults.invalid;

                                            holodeck.activeDialogue().raidBotMessage(msg);
                                        }
                                    }

                                    loadMatchedRaids();
                                };


                                if (buckets.private.raids.length) {
                                    processBucket(buckets.private);
                                }

                                if (buckets.alliance.raids.length) {
                                    processBucket(buckets.alliance);
                                }

                                // Always try to load public raids
                                processBucket(buckets.public, true);
                            }
                        }
                        else if (response.status > 200 && response.status < 400) {
                            message = "No new raids was returned."
                        }
                        else {
                            message = "Unable to retrieve raids. (status: " + response.status + ")";
                        }

                        if (message && holodeck.activeDialogue()) {
                            holodeck.activeDialogue().raidBotMessage(message);
                        }
                    }
                });
                return ret;
            },


            getOptions: function()
            {
                var commandOptions = {
                    initialText: {
                        text: "Load Raid Monitor raids"
                    }
                };

                return commandOptions;
            },

            buildHelpText: function()
            {
                var helpText = "<b>Raid Command:</b> <code>/loadraidmonitor [filters]</code>\n";
                helpText += "Loads public raids from <a href='http://getKongE.org/games/lots/raids' title='Visit the Raid Monitor page' target='_blank'>Raid Monitor</a>.\n";
                helpText += "\n";
                helpText += "If this service is valueable to you, consider <a href='http://getKongE.org/donate' title='Visit the Donation page' target='_blank'>donating</a>.";
                helpText += "\n";
                helpText += "\n";
                helpText += "In addition to the normal filters, /lrm can filter based on current health/time ratio." + this.getCommandLink("{progress: ahead}") + " will find raids that are ahead. Other options are behind, and a numeric ratio like " + this.getCommandLink("{progress: < 1.5}") + " for raids that are health%/time% <= 1.5. \n";

                return helpText;
            }
        }
    );
		RaidCommand.create( 
			{
				commandName: "markall",
				aliases: ["bulkcachestate"],

				paramText: "filter state",
				
				
				/*public Object*/ handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					params = params.trim();
					
					var space = params.lastIndexOf(" ");
					
					var filter = params.substring(0, space);
					var state = params.substring(space+1);
					
					var count = RaidManager.markByFilter(filter, state);
					
					ret.statusMessage = "Marked " + count + " raid" + (count!=1?"s":"") + " matching \"<code>" + filter + "</code>\" as " + state;
					ret.success = true;
					
					if (count > 0) {
						DC_LoaTS_Helper.updatePostedLinks();
					}
					
					return ret;
				},
				
				/*public Object*/ getOptions: function()
				{					
					var commandOptions = {					
						initialText: {
							text: "Mark all " + this.parser.filterText + " as " + this.parser.stateText
						}
					};
					

					return commandOptions;
				},
				
				/*protected String*/ buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/markall [filter] state</code>\n";
					helpText += "where <code>filter</code> (optional) is a seenraids style filter to limit what gets marked\n";
					helpText += "where <code>state</code> is a valid state to mark the raids to (unseen, seen, visited)\n";
					
					return helpText;
				}
			}
		);
		
    // Show a list of users that you've muted
    RaidCommand.create(
        {
            commandName: "mutelist",
            aliases: ["muted", "ml"],

            handler: function(deck, parser, params, text, context)
            {
                var ret = {success: true},
                    list = "",
                    count = 0;
                for (var p in deck._chat_window._mutings) {
                    count++;
                    list += '<span _username="' + p + '" class="username chat_message_window_username">' + p + '</span>' + '\n';
                }

                ret.statusMessage = "You have " + count + " user" + (count!=1?'s':'') + " muted.\n" + list;
                return ret;
            },


            getOptions: function()
            {
                var commandOptions = {
                    initialText: {
                        text: "See who you've muted"
                    }
                };

                return commandOptions;
            },

            buildHelpText: function()
            {
                var helpText = "<b>Raid Command:</b> <code>/mutelist</code>\n";
                helpText += "See the list of Kongregate users you've muted.\n";

                return helpText;
            }
        }
    );
RaidCommand.create(
	{
		commandName: "news",
		aliases: ["new"],
		// No parsing needed
		/*parsingClass: ,*/
		handler: function(deck, parser, params, text, context)
		{
			// Declare ret object
			var ret = {success: true};

			DC_LoaTS_Helper.loadNews();

			return ret;
		},
		getOptions: function()
		{
			var commandOptions = {
				initialText: {
					text: "Check the latest doomscript news"
				}
			};

			return commandOptions;
		},
		buildHelpText: function()
		{
			var helpText = "<b>Raid Command:</b> <code>/news</code>\n";
			helpText += "Loads news from the doomscript servers.\n";

			return helpText;
		}
	}
);
		
RaidCommand
		.create({
			commandName : "pasteraids",
			aliases : [ "pastebinraids" ],
			parsingClass : RaidMultiFilter,

			handler : function(deck, raidFilter, params, text, context) {
				// Capture the start time of the query
				var queryStartTime = new Date() / 1;

				// Declare ret object
				var ret = {};

				// Find all raids that match the user's criteria
				var raidLinks = RaidManager.fetchByFilter(raidFilter);

				// If the RaidManager executed successfully
				if (typeof raidLinks != "undefined") {
					// If we didn't match a single raid
					if (raidLinks.length == 0) {
						if (params.length == 0) {
							ret.statusMessage = "Could not locate any seen raids in memory.";
						} else {
							ret.statusMessage = "Could not locate any seen raids matching <code>"
									+ params + "<code>";
						}

						// The lookup succeeded, we just didn't find anything
						ret.success = true;
					}
					// If we did match some raids
					else {
						// Capture all the text in one block
						var outputText = "";

						// For every link we found
						for ( var i = 0; i < raidLinks.length; i++) {
							// Print matched links
							outputText += raidLinks[i].getURL() + "\n";
						}

						// Wait a moment, then paste it
						setTimeout(
								function() {
									DC_LoaTS_Helper.PastebinAPI
											.pasteData(
													outputText,
													raidFilter.toPrettyString()
															+ " by "
															+ holodeck._active_user._attributes._object.username,
													raidFilter.toString());
								}, 100);

						// Status
						ret.statusMessage = "Pasting " + raidLinks.length
								+ " raids matching <code>"
								+ raidFilter.toString()
								+ "</code> to Pastebin. Please wait...";

						// Succeeded
						ret.success = true;
					}
				}
				// RaidManager failed
				else {
					ret.statusMessage = "Did not understand command: <code>"
							+ text + "</code>";
					ret.success = false;
				}

				return ret;
			},
			getOptions : function() {
				var commandOptions = {
					initialText : {
						text : "Export matching data to pastebin"
					}
				};

				return commandOptions;
			},
			buildHelpText : function() {
				var helpText = "<b>Raid Command:</b> <code>/pasteraids raidName difficulty {state: stateName} {age: timeFormat} {fs: fsFormat} {count: numberResults} {page: resultsPage}</code>\n";
				helpText += "Exports to pastebin raids that you've seen before in chat"
				helpText += "where <code>raidName</code> <i>(optional)</i> is any partial or full raid name\n";
				helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
				helpText += "where <code>stateName</code> <i>(optional)</i> is either seen or visited\n";
				helpText += "where <code>timeFormat</code> <i>(optional)</i> is like <code>&lt;24h</code>, <code>&lt;30m</code>, or <code>&gt;1d</code>\n";
				helpText += "where <code>fsFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
				helpText += "where <code>numberResults</code> <i>(optional)</i> is the number of results to display\n";
				helpText += "where <code>resultsPage</code> <i>(optional)</i> is if you've set count, then which page to show. If page is omitted, it will show the first page of results.\n";

				return helpText;
			}
		});
		RaidCommand.create( 
			{
				commandName: "raid",
				aliases: ["raids", "radi", "radu", "raud", "radus", "rauds", "radis", "rai", "fs", "os"],
				parsingClass: RaidFilter,
				
				// Doesn't use all the filter params
				paramText: "[raidName] [raidDifficulty]",
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true},
					    filter = parser;
				
					// If this was a valid filter
					if (filter.isValid())
					{
						// Find the matching raid types
						var matchedTypes = DC_LoaTS_Helper.getRaidTypes(filter);
						
						// If we matched some raid types
						if (matchedTypes.length > 0)
						{
							// Iterate over all the matched raid types
							for (var j = 0; j < matchedTypes.length; j++)
							{
								// Grab this raid
								var raid = matchedTypes[j];
								
								// Have the raid bot tell them 
								deck.activeDialogue().raidBotMessage(raid.getVerboseText(filter.difficulty));
							}
							
							ret.success = ret.success && true;
						}
						// If we didn't match a single raid
						else
						{
							ret.success = ret.success && true;
							ret.statusMessage = (i > 0?"\n":"") + "Could not locate any raids matching <code>" + filter.toString() + "</code>";
						}
						
					}
					
					return ret;
				},
				getOptions: function()
				{
				    console.log("Raid Info: ", this, this.parser);
				    
					var commandOptions = {					
						initialText: {
							text: "Raid Info for: " + this.parser.name,
							executable: false
						},
						all: {
							text: "All",
							callback: function()
							{
								DCDebug("Info All " + this.parser.name);
								delete this.parser.difficulty;
							}
						},
						
						normal: {
							text: "Normal",
							callback: function()
							{
								DCDebug("Info Normal " + this.parser.name);
								this.parser.difficulty = 1;
							}
						},
						
						hard: {
							text: "Hard",
							callback: function()
							{
								DCDebug("Info Hard " + this.parser.name);
								this.parser.difficulty = 2;
							}
						},
						
						legendary: {
							text: "Legendary",
							callback: function()
							{
								DCDebug("Info Legendary " + this.parser.name);
								this.parser.difficulty = 3;
							}
						},
						
						nightmare: {
							text: "Nightmare",
							callback: function()
							{
								DCDebug("Info Nightmare " + this.parser.name);
								this.parser.difficulty = 4;
							}
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/raid raidName difficulty</code>\n";
					helpText += "where <code>raidName</code> is any partial or full raid name\n";
					helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
					helpText += "\n";
					helpText += "<b>Example:</b>\n";
					helpText += "Raid data for NM Tulk: " + this.getCommandLink("tulk 4") + "\n";

					return helpText;
				}
			}
		);
		

		RaidCommand.create( 
			{
				commandName: "raidbulkcallback",
				aliases: [],
				doNotEnumerateInHelp: true,
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					// Break apart the guid and possible cancel message
					var paramsParts = params.split(" "),
					    guid = paramsParts[0],
					    bulkRaidObject = DC_LoaTS_Helper.bulkRaids[guid],
					    cancel = paramsParts[1];
					    
					DCDebug(paramsParts, guid, bulkRaidObject, cancel);
					
					// If this load was canceled
					if ( (typeof cancel !== "undefined" && cancel === "cancel") || bulkRaidObject.canceled)
					{
						// Keep track of the total run time so far
						if (typeof bulkRaidObject.runTime === "undefined")
						{
							bulkRaidObject.runTime = 0;
						}
						bulkRaidObject.runTime += (new Date()/1) - bulkRaidObject.startTime;
						bulkRaidObject.canceled = true;
						if (bulkRaidObject.timeout)
						{
							clearTimeout(bulkRaidObject.timeout);
							delete bulkRaidObject.timeout;
						}
						ret.success = true;
						ret.statusMessage = "Canceled bulk load from " + bulkRaidObject.loadSource;
					}
					else
					{
						if (typeof bulkRaidObject.iteration === "undefined")
						{
							bulkRaidObject.iteration = 0;
							bulkRaidObject.startTime = new Date()/1;
							holodeck.activeDialogue().raidBotMessage("Loading bulk raids from " + bulkRaidObject.loadSource + ". " + DC_LoaTS_Helper.getCommandLink("/raidbulkcallback " + guid + " cancel", "Cancel?"));
						}
						
						var raidIndexToLoad = bulkRaidObject.iteration++;
						var raidToLoad = bulkRaidObject.raids[raidIndexToLoad];
						if (raidIndexToLoad < bulkRaidObject.raids.length)
						{
							DC_LoaTS_Helper.loadRaid(raidToLoad);
							
							bulkRaidObject.iteration >= bulkRaidObject.raids.length
							
							bulkRaidObject.timeout = setTimeout("holodeck.processChatCommand(\"/raidbulkcallback " + guid + "\");", 1500);
						}
						else 
						{
							ret.statusMessage = "Completed bulk load from " + bulkRaidObject.loadSource + " in " + ((new Date()/1) - bulkRaidObject.startTime) + "ms.";
						}
						ret.success = true;
					}
					
					
					return ret;
				},
				
				getOptions: function()
				{
					return {};
				},
				
				buildHelpText: function()
				{
					return "";
				}
			}
		);
		

		RaidCommand.create( 
			{
				commandName: "raidformat",
				aliases: [],
				// Custom parsing
				/*parsingClass: ,*/
				// Custom parsing means custom param text
				paramText: "[newFormat]",
				
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
					
					if (params.length == 0)
					{
						// Retrieve the message format
						var messageFormat = DC_LoaTS_Helper.getMessageFormat();
					
						// Let the user know what the format is
						ret.statusMessage = "Current raid format: <code>" + messageFormat + "</code>" + 
											"\nUse <code>" + this.getCommandLink("help") + "</code> to list all formatting options.";

					}
					else if (params == "reset")
					{
						var messageFormat = RaidLink.defaultMessageFormat;
						
						// Retrieve the message format
						GM_setValue(DC_LoaTS_Properties.storage.messageFormat, messageFormat);

						// Let the user know what the format is
						ret.statusMessage = "Raid format reset to: <code>" + messageFormat + "</code>";
					}
					// Has a format and is not a help request
					else
					{
						// Store the message format
						GM_setValue(DC_LoaTS_Properties.storage.messageFormat, params);
						
						// Notify user that we stored it
						ret.statusMessage = "Raid format is now: <code>" + params + "</code>";
						
						// Update the posted links
						DC_LoaTS_Helper.updatePostedLinks();
					}
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Set raid format to " + this.processedText
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					// Get the text of all the cache states
					var cache_state_text = "";
					var cache_state_nice_text = "";
					var cache_state_short_text = "";
					
					for (var stateName in RaidManager.STATE)
					{
						var state = RaidManager.STATE[stateName];
						if (typeof state == "object")
						{
							cache_state_text += state.text + ", ";
							cache_state_nice_text += state.niceText + ", ";
							cache_state_short_text += state.shortText + ", ";
						}
					}
					
					var unknownState = RaidManager.STATE.getUnknownState();
					cache_state_text += "or " + unknownState.text;
					cache_state_nice_text += "or " + unknownState.niceText;
					cache_state_short_text += "or " + unknownState.shortText;
					
					
					// Get the text of all the difficulties
					var difficulty_text = "";					
					for (var diffNum in RaidType.difficulty)
					{
						difficulty_text += RaidType.difficulty[diffNum] + ", ";
					}
					difficulty_text += "or Unknown (error)";
					
					// Get the text of all the short difficulties
					var diff_text = "";
					for (var diffNum in RaidType.shortDifficulty)
					{
						diff_text += RaidType.shortDifficulty[diffNum] + ", ";
					}
					diff_text += "or Unknown (error)";
					
					
					var helpText = "<b>Raid Command:</b> <code>/raidformat newFormat</code>\n";
					helpText += "where <code>newFormat</code> <i>(optional)</i> is the new format for raid links\n";
					helpText += "if <code>newFormat</code> is omitted, it will tell you your current raid format\n";
					helpText += "\n";
					helpText += "<b>Format options (hover for description):</b>\n";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_text + "\">cache-state</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_nice_text + "\">cache-state-nice</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_nice_text + "\">state</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_nice_text + "\">status</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_short_text + "\">cache-state-short</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_short_text + "\">state-short</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + cache_state_short_text + "\">status-short</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + difficulty_text + "\">difficulty</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + diff_text + "\">diff</span>, ";
					helpText += "<span class=\"abbr\" title=\"Fair Share (of damage) = Max raid health / Max raid members\">fs</span>, ";
					helpText += "<span class=\"abbr\" title=\"Fair Share (of damage) = Max raid health / Max raid members\">fair</span>, ";
					helpText += "<span class=\"abbr\" title=\"Fair Share (of damage) = Max raid health / Max raid members\">fairshare</span>, ";
					helpText += "<span class=\"abbr\" title=\"Raid Health\">health</span>, ";
					helpText += "<span class=\"abbr\" title=\"Unique Raid ID Number\">id</span>, ";
					helpText += "<span class=\"abbr\" title=\"Kongregate LoaTS icon\">image</span>, ";
					helpText += "<span class=\"abbr\" title=\"Break to the next line\">line</span>, ";
					helpText += "<span class=\"abbr\" title=\"Official Raid Name\">name</span>, ";
					helpText += "<span class=\"abbr\" title=\"Same as target\">os</span>, ";
					helpText += "<span class=\"abbr\" title=\"Same as target\">optimal</span>, ";
					helpText += "<span class=\"abbr\" title=\"Official Short Raid Name\">short-name</span>, ";
					helpText += "<span class=\"abbr\" title=\"User defined short name\">shorter-name</span>, ";
					helpText += "<span class=\"abbr\" title=\"Shortest unique name of the raid\">shortest-name</span>, ";
					helpText += "<span class=\"abbr\" title=\"Raid Size = Max raid members\">size</span>, ";
					helpText += "<span class=\"abbr\" title=\"S, E, and/or H if the raid uses Stamina, Energy, and/or Honor\">stat</span>, ";
					helpText += "<span class=\"abbr\" title=\"Target (Damage) = FS * multiplier. Changes per raid size.\">target</span>, ";
					helpText += "<span class=\"abbr\" title=\"Duration of the raid\">time</span>, ";
					helpText += "<span class=\"abbr\" title=\"Full text url of the raid\">url</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + RaidManager.STATE.VISITED.niceText + "\" if you've loaded this raid before, blank otherwise\">visited</span>, ";
					helpText += "<span class=\"abbr\" title=\"" + RaidManager.STATE.VISITED.shortText + "\" if you've loaded this raid before, blank otherwise\">visited-short</span>, ";
					helpText += "<span class=\"abbr\" title=\"Raid's Mission Zone. On Alliance raids, indicates Archimedes tier.\">zone</span>";
					helpText += "\n";
					helpText += "\n";
					helpText += "Options should be placed in <code>{}</code>\n";
					helpText += "\n";
					helpText += "<b>Default:</b>\n";
					helpText += "<code>" + this.getCommandLink(RaidLink.defaultMessageFormat) + "</code>\n";
					helpText += "<i class=\"smallText\">(<code>" + this.getCommandLink("reset") + "</code> will set your format back to this)</i>\n";
					helpText += "\n";
					helpText += "<b>SRLTSX Default:</b>\n";
					helpText += "<code>" + this.getCommandLink("{visited} {name} - {diff} - {fs}/{os}") + "</code>";
					helpText += "\n";
					helpText += "<b>Short:</b>\n";
					helpText += "<code>" + this.getCommandLink("{cache-state-short} {diff} {shorter-name}") + "</code>";
					helpText += "\n"
					helpText += "<b>Notes:</b>\n"
					helpText += "<code>{fs}</code> can also do simple math like <code>{fs*2}</code>\n"
					helpText += "Use <code>{line}</code> for new lines, and can be used multiple times.\n"
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "raidhelp",
				aliases: ["raidabout", "raidbot", "help", "doomscript", "doomscripthelp", "dshelp"],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {sucess: true};
					
					DC_LoaTS_Helper.printScriptHelp(deck, text);
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Display doomscript help"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/raidhelp</code>\n";
					helpText += "Displays the help info for the script\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "raidstyle",
				aliases: [],
				parsingClass: RaidFilterStyleParser,

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					DCDebug("raidstyle.command.js: parser: ", parser);
					
					if (typeof parser.raidFilter === "undefined" || parser.raidFilter.isEmpty())
					{
						//TODO: Display all existing raid styles
						
						
					}
					else if (typeof parser.linkStyle === "undefined" && typeof parser.messageStyle === "undefined" && typeof parser.imageStyle === "undefined")
					{
						//TODO: Display all raid styles that have the same filter
					}
					else
					{
						// Find all the styles matching this filter
						var matchingStyles = DC_LoaTS_Helper.raidStyles[parser.raidFilter.toString()];
						if (typeof matchingStyles === "undefined")
						{
							matchingStyles = [];
							DC_LoaTS_Helper.raidStyles[parser.raidFilter.toString()] = matchingStyles;
						}
						
						// Have the parser create CSS styles for itself.
						parser.injectStyles();
						
						// Add this to the list of styles for this filter
						matchingStyles.push(parser);
						
						// Success report
						ret.success = true;
						ret.statusMessage = parser.toString();
						
						// Refresh the links to see the change
						DC_LoaTS_Helper.updatePostedLinks();
					}
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Execute this raid style command"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/raidstyle filter +linkStyle +messageStyle +imageStyle</code>\n";
					helpText += "\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "refreshlinks",
				aliases: [],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
					
					DC_LoaTS_Helper.updatePostedLinks();
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Refresh the links in chat"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/refreshlinks</code>\n";
					helpText += "Will refresh all the links and their states in chat.\n";
					helpText += "This can help if raids aren't looking marked like they should be.\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "reload",
				aliases: ["refresh", "reloaf", "reloa", "eload"],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					params = params.trim().toLowerCase();
					if (params != "game" && params != "chat" && params != "wc" && params != "")
					{
						holodeck.processChatCommand("/reload help");
                        return {success: true};
					}
					// true if we did reload, false otherwise
					ret.success = DC_LoaTS_Helper.reload(null, params);
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Reload the game"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/reload</code>\n";
					helpText += "Attempts to reload just the game and not the window\n";
					helpText += "To reload the World Chat, use <code>/reload chat</code>\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "rss",
				aliases: ["forums", "threads", "posts"],
				// No parsing needed
				/*parsingClass: ,*/

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true, statusMessage: "Reading RSS feed..."};

					DC_LoaTS_Helper.ajax({
						url: "http://www.legacyofathousandsuns.com/forum/external.php?type=RSS2",
						onload:function(response) {
							var xmlDoc = (new DOMParser()).parseFromString(response.responseText, "text/xml"),
							    items = xmlDoc.getElementsByTagName("item"),
							    i, item, j, child, threads = [], thread, 
							    str = "Recent posts (as of " + DC_LoaTS_Helper.getCurrentPrettyDate() + ")";
							
							for (i = 0; i < items.length; i++) {
								item = items[i];
                                threads.push({
                                    title: getNodeValue(item, "title"),
                                    url: getNodeValue(item, "link"),
                                    date: getNodeValue(item, "pubDate"),
                                    relativeDate: DC_LoaTS_Helper.timeDifference(new Date()/1, new Date(getNodeValue(item, "pubDate"))/1),
                                    description: getNodeValue(item, "description"),
                                    category: getNodeValue(item, "category"),
                                    categoryUrl: getNodeValue(item, "category", "domain"),
                                    creator: getNodeValue(item, "creator")
                                });
							}

                            function getNodeValue(parent, tagName, attribute) {
                                tags = parent.getElementsByTagNameNS("*", tagName);
                                if (tags && tags[0]) {
                                	if (attribute) {
                                		return tags[0].attributes[attribute].nodeValue;
                                	}
                                	else {
                                		return tags[0].childNodes[0].nodeValue;
                                	}
                                }
                                
                                return "<i>Unable to locate in RSS feed</i>";
                            }

                            for (i = 0; i < threads.length; i++) {
                            	thread = threads[i];
                            	str += "\n--------------------------------------------------\n"
                                str += thread.relativeDate + " ";
                            	str += "<a href='" + thread.categoryUrl + "' target='_blank'>" + thread.category + "</a>";
                            	str += " &gt; <a href='" + thread.url + "' target='_blank'>" + thread.title + "</a>";
                            }
                            
                            holodeck.activeDialogue().raidBotMessage(str);
                            
						} // end onload
					});					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Lists recent threads from the forums"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/threads</code>\n";
					helpText += "Lists recent threads from the forums\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "seenraids",
				aliases: ["seenraid", "raidseen", "raidseen", "sr"],
				parsingClass: RaidMultiFilter,
				handler: function(deck, raidFilter, params, text, context)
				{
					// Capture the start time of the query
					var queryStartTime = new Date()/1;
				
					// Declare ret object
					var ret = {};
					
					// Find all raids that match the user's criteria
					var raidLinks = RaidManager.fetchByFilter(raidFilter);
					
					// If the RaidManager executed successfully
					if (typeof raidLinks != "undefined")
					{
						// If we didn't match a single raid
						if (raidLinks.length == 0)
						{
							if (params.length == 0)
							{
								ret.statusMessage = "Could not locate any seen raids in memory.";
							}
							else
							{
								ret.statusMessage = "Could not locate any seen raids matching <code>" + params + "<code>";
							}
							
							// The lookup succeeded, we just didn't find anything
							ret.success = true;
						}
						// If we did match some raids
						else
						{
							// Retrieve the message format
							var messageFormat = DC_LoaTS_Helper.getMessageFormat();
						
							// Retrieve the anchor tag format
							var linkFormat = DC_LoaTS_Helper.getLinkFormat();
							
							// Capture all the text in one block
							var outputText = "\n";
							
							// For every link we found
							for (var i = 0; i < raidLinks.length; i++)
							{
								// We need to find the style the user has requested
								var className = raidLinks[i].getMatchedStyles().className;
								
								// Bits to wrap each message raid link with
								var wrapperFront = "<span class=\"seenraidMessageWrapper" + (className?" " + className:"") + "\">" + (i+1) + ") ";
								var wrapperBack = "</span>\n\n";
								
								// Print matched links
								outputText += wrapperFront + raidLinks[i].getFormattedRaidLink(messageFormat, linkFormat) + wrapperBack;
							}
							
							// Print out the raid links we found
							deck.activeDialogue().raidBotMessage(outputText);
							
							// Print out the stats for the query
							ret.statusMessage = "<code>/" + this.commandName + " " + raidFilter.toString() + "</code> took " + (new Date()/1 - queryStartTime) + " ms and yielded " + raidLinks.length + " results.";
							// Succeeded
							ret.success = true;
						}
					}
					// RaidManager failed
					else
					{
						ret.statusMessage = "Did not understand command: <code>" + text + "</code>";
						ret.success = false;
					}
					
					return ret;
				},
				
				getOptions: function()
				{
					var commandOptions = {
						
						initialText: {
							text: "Seen raids: " + ((typeof this.parser.name != "undefined")?this.parser.name : "Unknown")
						},
						
						any: {
							text: "Any",
							callback: function()
							{
								DCDebug("Seen Any " + this.parser.name);
								delete this.parser.difficulty;
							}
						},
						
						normal: {
							text: "Normal",
							callback: function()
							{
								DCDebug("Seen Normal " + this.parser.name);
								this.parser.difficulty = 1;
							}
						},
						
						hard: {
							text: "Hard",
							callback: function()
							{
								DCDebug("Seen Hard " + this.parser.name);
								this.parser.difficulty = 2;
							}
						},
						
						legendary: {
							text: "Legendary",
							callback: function()
							{
								DCDebug("Seen Legendary " + this.parser.name);
								this.parser.difficulty = 3;
							}
						},
						
						nightmare: {
							text: "Nightmare",
							callback: function()
							{
								DCDebug("Seen Nightmare " + this.parser.name);
								this.parser.difficulty = 4;
							}
						}
					};
					
					return commandOptions;
				},

				
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/seenraids raidName difficulty {state: stateName} {age: timeFormat} {size: sizeFormat} {fs: fsFormat} {os: osFormat} {zone: zoneNumber} {count: numberResults} {page: resultsPage}</code>\n";
					helpText += "Looks up raids that you've seen before in chat"
					helpText += "where <code>raidName</code> <i>(optional)</i> is any partial or full raid name\n";
					helpText += "where <code>difficulty</code> <i>(optional)</i> is a number 1 - 4 where 1 is normal, 4 is nightmare\n";
					helpText += "where <code>stateName</code> <i>(optional)</i> is either seen or visited\n";
					helpText += "where <code>timeFormat</code> <i>(optional)</i> is like <code>&lt;24h</code>, <code>&lt;30m</code>, or <code>&gt;1d</code>\n";
                    helpText += "where <code>sizeFormat</code> <i>(optional)</i> is like <code>&lt;100</code> or <code>250</code>\n";
                    helpText += "where <code>osFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
                    helpText += "where <code>fsFormat</code> <i>(optional)</i> is like <code>&lt;1m</code> or <code>&gt;500k</code>\n";
                    helpText += "where <code>zoneNumber</code> <i>(optional)</i> is like <code>1</code>, <code>Z14</code>, <code>ZA</code>, <code>WR</code>\n";
					helpText += "where <code>numberResults</code> <i>(optional)</i> is the number of results to display\n";
					helpText += "where <code>resultsPage</code> <i>(optional)</i> is if you've set count, then which page to show. If page is omitted, it will show the first page of results.\n";
					helpText += "\n";
					helpText += "<b>Examples:</b>\n";
					helpText += "\n";
					helpText += "<i>Find all raids you've seen, but not visited<i>\n";
					helpText += "<code>" + this.getCommandLink("{state:seen}") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Find all raids you've seen, but not visited that you saw posted in the last 5 hours<i>\n";
					helpText += "<code>" + this.getCommandLink("{state:seen} {age: <5h}") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Find all raids you've seen, but not visited that you saw posted in the last 5 hours that have FS &lt; 1M<i>\n";
					helpText += "<code>" + this.getCommandLink("{state:seen} {age: <5h} {fs:<1M}") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Find all normal telemachus raids that you've not visited before\n";
					helpText += "<code>" + this.getCommandLink("tele 1 {state:!visited}") + " </code>\n";
					helpText += "\n";
					helpText += "<i>Find the first 10 void killer raids you've seen\n";
					helpText += "<code>" + this.getCommandLink("killer {count: 10}") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Find the second 10 void killer raids you've seen\n";
					helpText += "<code>" + this.getCommandLink("killer {count: 10} {page: 2}") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Find all void nightmare vorden raids you've seen\n";
					helpText += "<code>" + this.getCommandLink("vorden 4") + "</code>\n";
					helpText += "\n";
					helpText += "<i>Looking for <a href=\"http://www.zoywiki.com/index.php/LotS/experiment/multicoloredcloorian\" title=\"Cloorian Material needed to craft some Legendary pants\">Cloorian Material<a/>\n";
					helpText += "<code>" + this.getCommandLink("vor|gan|nat 4 {age: <24h} {state: !visited}") + "</code>\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create(
			{
				commandName: "suggest",
				aliases: ["idea", "suggestion", "bug", "doomcat", "doomscript", "report", "feature", "feedback"],

                generateResponseMessage: function(command) {
                    switch(command) {
                        case "idea":
                        case "feature":
                        case "doomcat":
                        case "doomscript":
                            return "Thanks for the idea!";
                        case "suggestion":
                            return "Thanks for the suggestion!";
                        case "report":
                        case "bug":
                            return "Thanks for the bug report!";
                        case "feedback":
                        default:
                            return "Thanks for the feedback!"
                    }
                },

                handler: function(deck, parser, params, text, context)
                {
                    if (!params || !params.trim()) {
                        holodeck.processChatCommand("/suggest help");
                        return {success: true};
                    }

                    var command = text.split(" ")[0].toLowerCase().substring(1);

                    var ret = {success: true, message: "Submitting..."};

                    var me = this;
                    DC_LoaTS_Helper.ajax({
                            url: "http://getKongE.org/games/lots/suggestions/",
                            method: "POST",
                            data: DC_LoaTS_Helper.uriSerialize({user: deck.username(), message: params, text: text, url: window.location.href}),
                            onload: function(response) {
                                if (response.status >= 200 && response.status < 300) {
                                    holodeck.activeDialogue().raidBotMessage(me.generateResponseMessage(command));
                                }
                                else {
                                    holodeck.activeDialogue().raidBotMessage("Sorry, there was an error (" + response.status + " processing your request. Please try again later. :-(");
                                    console.error("Server failed to process /suggest", response);
                                }
                            }
                    });

                    return ret;
                },


                getOptions: function()
                {
                    var commandOptions = {
                        initialText: {
                            text: "Suggest a doomscript feature or report a doomscript bug"
                        }
                    };

                    return commandOptions;
                },

                buildHelpText: function()
                {
                    var helpText = "<b>Raid Command:</b> <code>/suggest idea/bug text here</code>\n";
                    helpText += "If you have an idea for a new feature or you've got a bug to report, this command will let you submit them";

                    return helpText;
                }
            }
        );
		// This is the general template which chat commands should follow
		RaidCommand.create( 
			{
				commandName: "template", // This is the /template command
				aliases: ["templateCommand", "commandTemplate"], // Also, /templateCommand and /commandTemplate
				parsingClass: RaidMultiFilter, // Comment out this line, and a parser will not be created
				myCustomAttribute: "Foo",
				doNotEnumerateInHelp: true, // Don't list this in the help
				
				
				// It's highly advised to just delete this function entirely
				// but I've left it here, commented out, to show that it
				// can be used if you know what you're doing										
//					initialize: function($super, context, commandText)
//					{
//						// Do some special constructor logic here
//						
//						// In the name of all that is good, call the superclass constructor
//						// somewhere in here
//						$super(context, commandText);
//					},

				
				// Handle the execution of this command
				// Parameters:
				//		deck - The Kongregate holodeck
				//		parser - If there is a parsing class, this parser was run with the params
				// 		params - Command text stripped of /commandName from the front
				//		text - Full text of the command as it was called
				//		context - Where this command was called from, either chat or omnibox
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {};
					
					// The work should be done in here
					
					// Explicitly posting as the RaidBot to the chat
					// This only shows for the current user. This message is not sent to everyone.
					deck.activeDialogue().raidBotMessage("Hello World!");

					
					// Always set the success state
					// If the command succeeeded, set the status to true
					// If it failed, set it to false
					ret.success = true;
					
					// If you want to display a message to the user after the command
					// finishes, put the text of the message in ret.statusMessage
					// Unrelatedly, we can also access custom attributes like this.myCustomAttribute
					ret.statusMessage = this.myCustomAttribute;
					
					// Always way to return the ret object
					return ret;
				},
				
				// These options are what the omnibox uses to determine how to display
				// this command as an autocomplete option
				getOptions: function()
				{
					// Typically, there is just a single commandOptions object that is returned
					var commandOptions = {	
						// This option is a clickable link that will load
						// a page in a new window/tab like a real link				
						initialText: {
							// Text of the link
							text: "Open script homepage",
							// Attributes of the <a> tag of this link
							// If there are linkParams, the option will call the handler
							// unless doNotCallHandler is true
							linkParams: {href: this.myCustomFunction(), target: "_blank"},
							// Do not call the above handler function
							doNotCallHandler: true,
							// Actually let the browser load the link
							followLink: true
						},
						
						// This option is just text. It doesn't do anything
						otherOption: {
							text: "Not clickable",
							// Sets this option to do nothing
							executable: false
						},
						
						// This command actually runs the template command
						thirdOption: {
							// Text of the command button
							text: "Run Template Command",
							// If there's a callback, the option will call the handler
							// unless doNotCallHandler is true.
							callback: function()
									  {
									  	// Do work in here. Usually just minor
									  	// set up work since the handler is going
									  	// to be called next. 
									  	// If this code starts to get long, you
									  	// might consider going about it differently.
									  }
						}
					};
					
					// Make sure to return the options we just made
					return commandOptions;
				},
				
				// Custom functions go in just like needed functions
				myCustomFunction: function()
				{
					return DC_LoaTS_Properties.scriptURL;
				},
				
				// Here you simply construct the help text for when a user calls /template help
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/template</code>\n";
					helpText += "Prints hello world, or does something else\n";
					
					// Aliases for the command will be automatically appended to the bottom
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "time",
				aliases: ["servertime"],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
					
					ret.statusMessage = "Local Time is approximately: " + this.getLocalDateText() + "\n";
					ret.statusMessage += "Server Time is approximately: " + this.getServerDateText();
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {
						config: {
							refreshEvery: 1000
						},
						initialText: {
							text: "Local Time: " + this.getLocalDateText() + "<br>Server Time: " + this.getServerDateText()
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/time</code>\n";
					helpText += "Estimates the current server time (GMT) based on local system time.\n";
					
					return helpText;
				},
				
				getLocalDateText: function()
				{
					return new Date().toLocaleString();
				},
				getServerDateText: function()
				{
					var localDate = new Date();
					var serverDate =  new Date(localDate.getTime() + localDate.getTimezoneOffset() * 60 * 1000);
					return serverDate.toLocaleString().substring(0,25) + " GMT+0000 (UTC)";
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "update",
				aliases: [],
				// No parsing needed
				/*parsingClass: ,*/

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
						
					window.open(DC_LoaTS_Properties.scriptDownloadURL, "_blank");
						
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {
						initialText: {
							text: "Get the current stable script"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/update</code>\n";
					helpText += "Attempts to install the latest stable doomscript version from <a href=\"" + DC_LoaTS_Properties.scriptURL + "\">" + DC_LoaTS_Properties.scriptURL + "</a>.\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "updateraiddata",
				aliases: ["urd", "updatedata"],
				// No parsing needed
				/*parsingClass: ,*/

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
						
					DC_LoaTS_Helper.updateRaidData();
						
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {
						initialText: {
							text: "Update your local raid data"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/updateraiddata</code>\n";
					helpText += "Attempts to update to the latest raid data (All the raid types).\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "version",
				aliases: [],
				// No parsing needed
				/*parsingClass: ,*/
				handler: function(deck, parser, params, text, context)
				{
					return {success: true, statusMessage: "Your doomscript version is <b>" + DC_LoaTS_Properties.version + "</b>"};
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Your doomscript version is <b>" + DC_LoaTS_Properties.version + "</b>",
							doNotCallHandler: true
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/version</code>\n";
					helpText += "Tells the current version of the installed script.\n";
					
					return helpText;
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "wiki",
				aliases: ["search", "lookup", "zoywiki"],
				urlPattern: "http://www.zoywiki.com/index.php?title=Special:Search&search=LotS/{0}&go=Go",
				// No parsing
				/*parsingClass: ,*/
				paramText: "query",

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
											
					var url = this.createURL(params);
					
					window.open(url, "_blank");
					
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Search Zoywiki for: " + this.processedText,
							linkParams: {href: this.createURL(this.processedText), target: "_blank"},
							doNotCallHandler: true,
							followLink: true
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/wiki searchText</code>\n";
					helpText += "where <code>searchText</code> is what you want to search for on Zoywiki\n";
					
					return helpText;
				},
				
				createURL: function(searchInput)
				{
					searchInput = searchInput || "";
					return this.urlPattern.format(escape(searchInput.replace(" ", "+")));
				}
			}
		);
		
		RaidCommand.create( 
			{
				commandName: "timerdata",
				aliases: [],
				// No parsing needed
				/*parsingClass: ,*/

				handler: function(deck, parser, params, text, context)
				{
					// Declare ret object
					var ret = {success: true};
						
					deck.activeDialogue().raidBotMessage(Timer.getReport());
						
					return ret;
				},
				getOptions: function()
				{
					var commandOptions = {					
						initialText: {
							text: "Print the timer report"
						}
					};
					
					return commandOptions;
				},
				buildHelpText: function()
				{
					var helpText = "<b>Raid Command:</b> <code>/timerdata</code>\n";
					helpText += "Prints out timing and performance data about the script\n";
					
					return helpText;
				}
			}
		);
		
		
		// Manage data related to the CConoly API
		window.CConolyAPI = {
				
			lastQueryTimeKey: DC_LoaTS_Properties.storage.cconolyLastQueryTime,
			useQueryTimeDeltaPrefKey: "UseQueryTimeDelta",
			
			baseUrl: "http://cconoly.com/lots/",
			markDeadUrl: "markDead.php?kv_raid_id=%RAID_ID%&doomscript=%VERSION%",
			raidListUrl: "raidlinks.php?hrs=%TIME%&doomscript=%VERSION%",
			
			setLastQueryTime: function(lastQueryTime) {
				GM_setValue(this.lastQueryTimeKey, lastQueryTime);
			},
			
			getMarkDeadUrl: function(raidID) {
				var reportUrl = this.baseUrl + this.markDeadUrl;
				reportUrl = reportUrl.replace("%RAID_ID%", raidID);
				reportUrl = reportUrl.replace("%VERSION%", this.getVersionString());
				return reportUrl;
			},
			
			getRaidListUrl: function() {
				var raidListUrl = this.baseUrl + this.raidListUrl;
				raidListUrl = raidListUrl.replace("%TIME%", this.getRaidListQueryHours());
				raidListUrl = raidListUrl.replace("%VERSION%", this.getVersionString());
				return raidListUrl;
			},
			
			getVersionString: function() {
				return this.versionString || (this.versionString = DC_LoaTS_Properties.version.toString().replace(/\./g, ""));
			},
			
			getRaidListQueryHours: function()
			{
				return DC_LoaTS_Helper.getPref(this.useQueryTimeDeltaPrefKey, true) ? this.getHoursSinceLastQuery() : 168;
			},
			
			getHoursSinceLastQuery: function() {
				var elapsedMs = new Date()/1 - GM_getValue(this.lastQueryTimeKey, 0);
				elapsedHrs = elapsedMs / 1000 / 60 / 60; // Convert ms to hours
				return Math.min(168, Math.ceil(elapsedHrs * 1000)/1000); // Round to 3 decimals, take 168 or lower
			}
		};
		

// List of all raid ids and names. Any raid without a real raid id will not show up nicely.
DC_LoaTS_Helper.raids =
{
    // Personal Raids
    sherlock_holmes:    new RaidType("sherlock_holmes",    "Z10", "The Murderer", "Murderer", "Murderer",             12,   1, "S",    [6000000, "N/A", "N/A", "N/A"]),
    wrath_of_player:    new RaidType("wrath_of_player",     "ZA", "Your Wrath", "Your Wrath", "Wrath",                12,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),
    lu_bu:              new RaidType("lu_bu",              "ZA2", "LU BU", "LU BU", "Lubu",                           12,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),
    ragnar:             new RaidType("ragnar",             "ZA3", "Ragnar", "Ragnar", "Ragnar",                        1,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),
    centurian_covert_agent:new RaidType("centurian_covert_agent","ZA3", "Centurian Covert Agent", "CC Agent", "Agent", 1,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),
    talia:              new RaidType("talia",              "ZA4", "Talia", "Talia", "Talia",                           1,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),
    myrmexidaks:        new RaidType("myrmexidaks",        "ZA4", "Myrmexidaks", "Myrmexidaks", "Myrm",                1,   1, "S",   [10000000, "N/A", "N/A", "N/A"]),

    // STANDARD RAIDS
    // Small Raids
    commander:          new RaidType("commander",           "Z1", "Centurian Commander", "CC Commander", "CC Comm",  168,  10, "S",    [150000, 187500, 240000, 300000]),
    ragebeasts:         new RaidType("ragebeasts",          "Z2", "Garlax Ragebeasts", "Ragebeasts", "Rage",         120,  10, "S",    [2000000, 2500000, 3200000, 4000000]),
    cybertollahs:       new RaidType("cybertollahs",        "Z3", "Supreme Cybertollahs", "Cybertollahs", "Cyber-T",  72,  10, "S",    [4000000, 5000000, 6400000, 8000000]),
    seth:               new RaidType("seth",                "Z4", "Nathaniel Vorden", "Vorden", "Vorden",             72,  10, "S",    [6000000, 7500000, 9600000, 12000000]),
    scarlet_harlet:     new RaidType("scarlet_harlet",      "Z6", "The Scarlet Harlot", "Scarlet", "Harlot",          72,  10, "S",    [15300000, 22950000, 30600000, 45900000],/*FS calculated normally*/null, 4590000),
    lupin:              new RaidType("lupin",               "Z7", "Lupin", "Lupin", "Lupin",                          72,  10, "S",    [25500000, 38250000, 51000000, 76500000],/*FS calculated normally*/null, 7650000),
    lieutenant_targe:   new RaidType("lieutenant_targe",    "Z8", "Lieutenant Targe", "Targe", "Targe",              120,  10, "S",    [14000000, 17500000, 22400000, 28000000]),
    sigurd:             new RaidType("sigurd",              "Z9", "Sigurd Spinebreaker", "Sigurd", "Sigurd",          72,  10, "S",    [16000000, 20000000, 25600000, 3200000]),
    space_pox:          new RaidType("space_pox",           "P1", "Space Pox", "Pox", "Pox",                           5,  12, "S",    [100000000, 500000000, 1000000000, 1500000000],/*FS calculated normally*/null, [35000000, 175000000, 350000000, 525000000]),
    quiskerian_temple:  new RaidType("quiskerian_temple",   "L1", "Quiskerian Temple", "Temple", "Temple",            10,  25, "S",    [200000000, 1000000000, 2000000000, 3000000000],/*FS calculated normally*/null, [16000000, 80000000, 160000000, 240000000]),
    missile_strike:     new RaidType("missile_strike",      "ZA", "Missile Strike", "Missiles", "Missile",            72,  10, "S",    [22000000, 28600000, 35200000, 44000000]),
    pi:                 new RaidType("pi",                 "ZA2", "Pi", "Pi", "Pi",                                   72,  10, "S",    [24000000, 31200000, 38400000, 48000000]),
    master_hao:         new RaidType("master_hao",         "Z19", "Master Hao", "Hao", "Hao",                         36,  10, "S",    [1000000000, 1300000000, 1600000000, 2000000000]),
    robot_uprising1:    new RaidType("robot_uprising1",     "F3", "Robot Uprising", "Robot Uprising", "Uprising",     72,  25, "S",    [900000000, 3600000000, 4500000000, 5400000000],/*FS calculated normally*/null, 500000000),
    temynx_parasite1:   new RaidType("temynx_parasite1",    "F4", "Temynx Parasite", "Temynx Parasite", "Temynx",     48,  15, "S",    [1000000000, 2000000000,2500000000,10000000000],/*FS calculated normally*/null, 660000000),
    hukkral_war_crawler: new RaidType("hukkral_war_crawler", "F9", "Huk-Kral War Crawler", "War Crawler", "Crawler", 24,  25, "S",    [100000000000, 200000000000, 250000000000, 300000000000],/*FS calculated normally*/null, 12500000000),
    elite_master_hao:   new RaidType("elite_master_hao",     "F10", "Elite Master Hao", "Elite Hao", "E. Hao",	      24,  25, "S",    [200000000000, 400000000000, 500000000000, 600000000000], /*FS calculated normally*/null, 25000000000), 
    elite_centurian_commander: new RaidType("elite_centurian_commander", "F13", "Elite Centurian Commander", "E. Cen", "E. CC", 24, 25, "S", [30000000000, 60000000000, 75000000000, 90000000000], /*FS calculated normally*/null, 3500000000), 

    // Small+ Raids
    purple_lion:        new RaidType("purple_lion",         "Z5", "Purple Lion", "Lion", "Lion",                      72,  20, "S",   [15500000,23250000,31000000,46500000], null, 2325000),

    // Medium Raids
    "void":             new RaidType("void",                "Z1", "Centurian Void Killer", "Killer", "VK",           168,  50, "S",   [5000000, 6250000, 8000000, 10000000]),
    carnus:             new RaidType("carnus",              "Z2", "Carnus 9000", "Carnus", "Carnus",                 120,  50, "S",   [15000000, 18750000, 24000000, 30000000]),
    cruiser:            new RaidType("cruiser",             "Z3", "Centurian Cruiser", "CC Cruiser", "Cruiser",       72,  50, "S",   [25000000, 31250000, 40000000, 50000000]),
    china:              new RaidType("china",               "Z4", "Blood Alley Gang", "Gang", "Gang",                 72,  50, "S",   [35000000, 43750000, 56000000, 70000000]),
    caligula:           new RaidType("caligula",            "Z6", "Caligula", "Caligula", "Cali",                     72,  50, "S",   [92250000, 138375000, 184500000, 276750000],/*FS calculated normally*/null, 7380000),
    warden_ramiro:      new RaidType("warden_ramiro",       "Z7", "Warden Ramiro", "Ramiro", "Ramiro",                72,  50, "S",   [153750000, 230625000, 307500000, 461250000],/*FS calculated normally*/null, 12300000),
    vulture_gunship:    new RaidType("vulture_gunship",     "Z8", "Vulture Gunship", "Vulture", "Vulture",            72,  50, "S",   [65000000, 81250000, 104000000, 130000000]),
    xarpa:              new RaidType("xarpa",               "Z9", "Centurian Fleet Commander", "Fleet", "Fleet Comm", 72,  50, "S",   [70000000, 87500000, 112000000, 140000000]),
    bachanghenfil:      new RaidType("bachanghenfil",      "Z10", "Bachanghenfil", "Bachanghenfil", "Bach",           72,  50, "S",   [75000000, 97500000, 120000000, 150000000]),
    gut_phager:         new RaidType("gut_phager",         "Z11", "Gut-Phager", "Gut-Phager", "Phager",               72,  50, "S",   [80000000, 104000000, 128000000, 160000000]),
    hulking_mutant:     new RaidType("hulking_mutant",     "Z15", "Hulking Mutant", "Mutant", "Mutant",               72,  50, "S",   [90000000, 112500000, 144000000, 180000000]),
    screaming_barracuda:new RaidType("screaming_barracuda","Z16", "Screaming Barracuda", "Barracuda", "Barracuda",    72,  50, "S",   [110000000, 137500000, 176000000, 220000000]),
    vunlac:             new RaidType("vunlac",             "Z19", "Vunlac", "Vunlac", "Vunlac",                       36,  50, "S",   [1500000000, 1950000000, 2400000000, 3000000000]),
    bashan:             new RaidType("bashan",              "ZA", "Bashan", "Bashan", "Bashan",                       72,  50, "S",   [85000000, 106250000, 136000000, 170000000]),
    cyborg_shark:       new RaidType("cyborg_shark",       "ZA2", "Cyborg Shark", "C. Shark", "Shark",                72,  50, "S",   [90000000, 112500000, 144000000, 180000000]),
    silj:               new RaidType("silj",               "ZA3", "Silj the Wurm-Rider", "Silj", "Silj",              30,  50, "S",   [750000000, 937500000, 1200000000, 1500000000]),
    tyraness_guard:     new RaidType("tyraness_guard",     "ZA4", "Tyraness' Guard", "Tyr. Guard", "Guard",           30,  50, "S",   [750000000, 937500000, 1200000000, 1500000000]),
    sian_dragonfly_1:   new RaidType("sian_dragonfly_1",   "Z21", "Sian Dragonfly", "Dragonfly", "Dfly",              48,  50, "S",   [10000000000,15000000000,20000000000,30000000000], null, [200000000,400000000,400000000,600000000]),
    lady_victoria_ashdown_1: new RaidType("lady_victoria_ashdown_1", "Z21", "Lady Victoria Ashdown", "Ashdown", "Ash",48,  50, "S",   [10000000000,15000000000,20000000000,30000000000], null, [200000000,400000000,400000000,600000000]),
    krakak_plague:      new RaidType("krakak_plague",       "F2", "Krakak Plague", "Plague", "Plague",                24,  25, "S",   [4166666667, 6250000000, 8333333333,12500000000], /*FS calculated normally*/null, 500000000),
    elite_bashan: 	new RaidType("elite_bashan", 	    "F11", "Elite Bashan", "E. Bashan", "E. Bashan",	      24,  50, "S",   [350000000000, 700000000000, 875000000000, 1050000000000], /*FS calculated normally*/null, 25000000000),

    // Medium+ Raids
    advocate_tulk:      new RaidType("advocate_tulk",       "Z5", "Advocate Tulk", "Tulk", "Tulk",                    72,  75, "S",   [69000000,103500000,138000000,207000000], null, 2760000),

    // Large Raids
    telemachus:         new RaidType("telemachus",          "Z1", "Telemachus", "Telemachus", "Tele",                168, 100, "S",   [20000000, 25000000, 32000000, 40000000]),
    carnifex:           new RaidType("carnifex",            "Z2", "Carnifex Prime", "Carnifex", "Carn",              120, 100, "S",   [35000000, 43750000, 56000000, 70000000]),
    rautha:             new RaidType("rautha",              "Z3", "Commander Rautha", "Rautha", "Rautha",             72, 100, "S",   [50000000, 62500000, 8000000, 100000000]),
    assasin:            new RaidType("assasin",             "Z4", "Kelovar Assassin", "Assassin", "Assa",             72, 100, "S",   [65000000, 81250000, 104000000, 130000000]),
    agony_and_ecstasy:  new RaidType("agony_and_ecstasy",   "Z6", "Agony and Ecstasy", "Agony, Ecstasy", "A&E",       72, 100, "S",   [216000000, 324000000, 432000000, 648000000], /*FS calculated normally*/null, 8640000),
    sun_xi:             new RaidType("sun_xi",              "Z7", "Sun Xi's Echo", "Psi-Echo", "Echo",                72, 100, "S",   [360000000, 540000000, 720000000, 1080000000], /*FS calculated normally*/null, 14400000),
    sludge_serpent:     new RaidType("sludge_serpent",      "Z8", "Sludge Serpent", "Serpent", "Serpent",             72, 100, "S",   [120000000, 150000000, 192000000, 240000000]),
    kalaxian_cult_mistress: new RaidType("kalaxian_cult_mistress","Z10","Kalaxian Cult-Mistress","Mistress","Cult",   72, 100, "S",   [180000000, 234000000, 288000000, 320000000]),
    shuborunth: 		new RaidType("shuborunth",         "Z13","Wulblunralxanachi", "Blob", "Blob",         72, 100, "S",   [200000000, 260000000, 320000000, 400000000]),
    tentacled_turkey:   new RaidType("tentacled_turkey",   "Z15", "Tentacled Turkey","Turkey","Turkey",               72, 100, "S",   [350000000, 455000000, 560000000, 700000000]),
    where_music_meets:  new RaidType("where_music_meets",  "Z16", "Symphony of Two Worlds","Symphony","Symphony",     72, 100, "S",   [400000000, 520000000, 640000000, 800000000]),
    reichsmarschall_dule:new RaidType("reichsmarschall_dule","Z19", "Reichsmarschall Dule", "R. Dule", "R. Dule",     36, 100, "S",   [2000000000, 2600000000, 3200000000, 4000000000]),
    birthday_cake_of_doom: new RaidType("birthday_cake_of_doom", "ZA","Birthday Cake of Doom", "Cake", "Cake",        72, 100, "S",   [250000000, 325000000, 400000000, 500000000]),
    anthropist_xenocide_warship:new RaidType("anthropist_xenocide_warship","ZA2","Anthropist Xenocide Warship","Xenocide","Xeno", 72,100,"S",[300000000, 390000000, 480000000, 600000000]),
    dark_hat:           new RaidType("dark_hat",           "ZA3", "Dark Hat", "D. Hat", "D. Hat",                     30, 100, "S",   [1000000000, 1300000000, 1600000000, 2000000000]),
    rampaging_rackalax: new RaidType("rampaging_rackalax", "ZA4", "Rampaging Rackalax", "Rackalax", "Rack",           30, 100, "S",   [1000000000, 1300000000, 1600000000, 2000000000]),
    infected_warwalker_squad:new RaidType("infected_warwalker_squad","C1-2", "Infected Warwalker Squad", "Warwalker", "Inf. II", 36, 50, "S", [8333333333, 12500000000, 16666666667, 25000000000], /*FS calculated normally*/null, 500000000),
    contest_winner1:    new RaidType("contest_winner1",    "S", "Hyper-Con Havoc", "Hyper-Con", "Hyper-Con",          72, 50,  "S",   [8333333333,12500000000,16666666667,25000000000], /*FS calculated normally*/null, 500000000), 
    elite_kulnarxex_elite_subjugator: new RaidType("elite_kulnarxex_elite_subjugator", "F13", "Elite Kulnar-Xex Elite Subjugator", "E. Elite", "E. E.", 24, 100, "S", [150000000000, 300000000000, 375000000000, 450000000000]), 

    // Large Plus Raids
    robotic_rautha:     new RaidType("robotic_rautha",      "Z5", "Robotic Rautha", "Rautha 2.0", "Robo Rautha",      72, 125, "S",   [135000000, 202500000, 270000000, 405000000], null, 2325000),
    kulnarxex_subjugator_1: new RaidType("kulnarxex_subjugator_1","S","Kulnar-Xex Subjugator","K-X Subjugator","KX Sub",8, 125, "S",   [12500000000, 15625000000, 20000000000, 25000000000], /*FS calculated normally */null, 200000000),
    weiqi_game_1:       new RaidType("weiqi_game_1",       "Z20", "Weiqi Game", "Weiqi Game", "Weiqi",                36, 180, "S",   [90000000000, 112500000000, 144000000000, 180000000000], /*FS calculated normally */null, 1000000000),
    kulnarxex_elite_subjugator_1: new RaidType("kulnarxex_elite_subjugator_1","S","Kulnar-Xex Elite Subjugator","Elite","KX ELITE Sub", 8, 125, "S", [125000000000, 156250000000, 200000000000, 250000000000], /*FS calculated normally */null, 2000000000),

    // Epic Raids
    colonel:            new RaidType("colonel",             "Z1", "Psychic Colonel", "CC Colonel", "Col.",           168, 250, "S",  [150000000, 187500000, 240000000, 300000000]),
    vespasia:           new RaidType("vespasia",            "Z2", "Vespasia's Android", "Vespasia Bot", "Vesp",      168, 250, "S",  [250000000, 312500000, 400000000, 500000000]),
    generalrahn:        new RaidType("generalrahn",         "Z3", "Centurian General", "CC General", "General",      168, 250, "S",  [350000000, 437500000, 560000000, 700000000]),
    natasha:            new RaidType("natasha",             "Z4", "Natasha Cybersmash", "Cybersmash", "Cyber-S",     168, 250, "S",  [450000000, 562500000, 720000000, 900000000]),
    mercury:            new RaidType("mercury",             "Z6", "Mercury", "Mercury", "Mercury",                   168, 250, "S",  [618750000, 928125000, 1237500000, 1856250000], /*FS calculated normally*/null, 14850000),
    hultex_quibberath:  new RaidType("hultex_quibberath",   "Z7", "Guldax Quibberath", "Quibberath", "Quib",         168, 250, "S",  [1031250000, 1546875000, 2062500000, 3093750000], /*FS calculated normally*/null, 24750000),
    commander_veck:     new RaidType("commander_veck",      "Z8", "Centurian Storm Commander", "Storm", "Storm",     168, 250, "S",  [900000000, 1125000000, 1440000000, 1800000000]),
    reaver:             new RaidType("reaver",              "Z9", "Galactic Reaver", "Reaver", "Reaver",              72, 250, "S",  [1000000000, 1250000000, 1600000000, 2000000000]),
    the_hat:            new RaidType("the_hat",            "Z10", "The Hat", "Hat", "Hat",         	              72, 250, "S",  [1100000000, 1475000000, 1850000000, 2200000000]),
    g_rahn:             new RaidType("g_rahn",             "Z12", "G. Rahn", "G. Rahn", "G. Rahn",                    72, 250, "S",  [1200000000, 1560000000, 1920000000, 2400000000]),
    al_husam:           new RaidType("al_husam",           "Z17", "Al-Husam", "Al-Husam", "Al-Husam",                 72, 250, "S",  [1500000000, 1950000000, 2400000000, 3000000000]),
    noir:               new RaidType("noir",               "Z18", "Noir", "Noir", "Noir",                             72, 250, "S",  [1600000000, 2080000000, 2560000000, 3200000000]),
    sky_commander_bethany:new RaidType("sky_commander_bethany","Z19","Sky Commander Bethany","Bethany","Bethany",     36, 250, "S",  [2500000000, 3250000000, 4000000000, 5000000000]),
    guan_yu:            new RaidType("guan_yu",             "ZA", "Guan Yu", "Guan", "Guan",                          72, 250, "S",  [1300000000, 1690000000, 2080000000, 2600000000]),
    bile_beast:         new RaidType("bile_beast",         "ZA2", "Bile Beast", "Bile", "Bile",                       72, 250, "S",  [1400000000, 1820000000, 2240000000, 2800000000]),
    void_master:        new RaidType("void_master",        "ZA3", "Void Master", "V. Master", "V. Master",            30, 250, "S",  [1250000000, 1625000000, 2000000000, 2500000000]),
    giant_kwelshax:     new RaidType("giant_kwelshax",     "ZA4", "Giant Kwelshax", "Kwelshax", "Kwel",               30, 250, "S",  [1250000000, 1625000000, 2000000000, 2500000000]),
    flying_saucer_mothership:new RaidType("flying_saucer_mothership","C1-3", "Flying Saucer Mothership", "Mothership", "Mothership", 48, 75, "S", [12500000000, 18750000000, 25000000000, 37500000000], /*FS calculated normally*/null, 500000000),
    sapphire:           new RaidType("sapphire",           "Z22", "Sapphire", "Sapphire", "Sapphire",                 48,  75, "S",  [13333333333,20000000000,26666666667,40000000000], /*FS calculated normally*/null, 500000000),
    shadow_parasites:	new RaidType("shadow_parasites",   "F8",  "Shadow Parasites", "Shadow", "Parasites",	      48, 250, "S",  [1000000000000, 2000000000000, 2500000000000, 3000000000000], /*FS calculated normally*/null, 25000000000),
    elite_giant_kwelshax: new RaidType("elite_giant_kwelshax", "F12", "Elite Kwelshax", "Elite Kwel", "E. Kwel",      24, 100, "S",  [175000000000, 350000000000, 437500000000, 525000000000], /*FS calculated normally*/null, 7000000000),
    elite_titanomachy: new RaidType("elite_titanomachy", "ZA5", "Elite Titanomachy", "E. Titan", "E. Tit", 	      48, 250, "S",  [45000000000, 90000000000, 112500000000, 135000000000], /*FS calculated normally*/null, 2500000000),
	
    // Epic+ Raids
    centurian_sentinel: new RaidType("centurian_sentinel",  "Z5", "Centurian Sentinel", "CC Sentinel", "Sentinel",   168, 275, "S", [340000000,510000000,680000000,1020000000], null, 7418184),

    // Colossal Raids
    mermara:            new RaidType("mermara",             "Z6", "Mermara", "Mermara", "Mermara",                   168, 500, "S", [1395000000, 2092500000, 2790000000, 4185000000], /*FS calculated normally*/null, 25110000),
    nemo:               new RaidType("nemo",                "Z7", "Nemo",    "Nemo", "Nemo",                         168, 500, "S", [2325000000, 3487500000, 4650000000, 6975000000], /*FS calculated normally*/null, 41850000),
    the_emperor:        new RaidType("the_emperor",         "Z8", "Dule's Robot", "Dule's Bot", "Dule",              168, 500, "S", [5000000000, 6250000000, 8000000000, 10000000000]),
    dule_warmaster:     new RaidType("dule_warmaster",      "Z9", "Centurian Councilor", "CC Councilor", "Councilor", 24, 500, "S", [2500000000, 3125000000, 4000000000, 5000000000]),
    crush_colossa:      new RaidType("crush_colossa",      "Z10", "Crush Colossa", "Colossa", "Crush",                72, 500, "S", [3000000000, 3900000000, 4800000000, 6000000000]),
    nosferatu_nick:     new RaidType("nosferatu_nick",     "Z14", "Nosferatu Nick", "Nick", "Nick",                   24, 500, "S", [3500000000, 4375000000, 5600000000, 7000000000]),
    noir2:              new RaidType("noir2",              "Z19", "Noir (II)", "Noir (II)", "Noir2",                  30, 500, "S", [5000000000, 6250000000, 8000000000, 10000000000]),
    niflung_boar:       new RaidType("niflung_boar",        "ZA", "Niflung Boar", "Boar", "Boar",                     30, 500, "S", [4000000000, 5000000000, 6400000000, 8000000000]),
    vlarg_relic_hunter: new RaidType("vlarg_relic_hunter", "ZA2", "Vlarg Relic Hunter", "R. Hunter", "Vlarg",         30, 500, "S", [4500000000, 5625000000, 7200000000, 9000000000]),
    trulcharn:          new RaidType("trulcharn",           "F1", "Trulcharn", "Trulcharn", "Trulcharn",               3,  10, "S", [10100000000, 10100000000, 10100000000, 10100000000], /*FS calculated normally*/null, 1010000000),
    the_saboteur:       new RaidType("the_saboteur",       "ZA3", "The Saboteur", "Saboteur", "Saboteur",             30, 500, "S", [5000000000, 6250000000, 8000000000, 10000000000]),
    the_tyraness:       new RaidType("the_tyraness",       "ZA4", "The Tyraness", "Tyraness", "Tyraness",             30, 500, "S", [5000000000, 6250000000, 8000000000, 10000000000]),
    hwang:              new RaidType("hwang",             "C1-4", "Hwang", "Hwang", "Hwang",                          64, 100, "S", [16666666667,25000000000,33333333333,50000000000], /*FS calculated normally*/null, 500000000),
    mutheru:            new RaidType("mutheru",            "Z22", "Multheru", "Multheru", "Multheru",                 64, 100, "S", [17666666667,26500000000,35333333333,53000000000], /*FS calculated normally*/null, 500000000),
    unstable_singularity:	new RaidType("unstable_singularity", "F6", "Unstable Singularity", "Singularity", "Unstable", 80, 500, "S", [175000000000, 350000000000, 437500000000, 525000000000], /*FS calculated normally*/null, 25000000000),
	
    // Colossal+ Raids
    besalaad_warmaster: new RaidType("besalaad_warmaster",  "Z5", "Besalaad Warmaster", "Warmaster", "Warmaster",    168, 550, "S",  [767250000, 1150875000, 1534500000, 2301750000], null, 12555000),
    pinatas_revenge1:	new RaidType("pinatas_revenge1",     "S",  "Pinata's Revenge", "Pinata II", "Pinata",        128, 500, "S",  [75000000000, 87500000000, 110000000000, 210000000000], null, 1000000000),

    // Titanic Raids
    sinaroms_death_flora:new RaidType("sinaroms_death_flora","C1-5","Sinarom's Death Flora","Death Flora II","D.F. II",72,250, "S",  [41666666667,  62500000000,  83333333333, 125000000000], /*FS calculated normally*/null, 500000000),
    professor_bonderbrand:new RaidType("professor_bonderbrand","Z22","Professor Bonderbrand","Bonderbrand","Prof Bond",72,250, "S",  [41666666667,  62500000000,  83333333333, 125000000000], /*FS calculated normally*/null, 500000000),
    arcade_gas_attack:  new RaidType("arcade_gas_attack",   "AR", "Arcade Gas Attack", "A G Attack", "Gas Attack",     72, 250, "S", [36666666667,55000000000,73333333333,110000000000], /*FS calculated normally*/null, 500000000),

    // Galactic Raids
    sultan_shrakzan1:	new RaidType("sultan_shrakzan1",     "WR",  "Sultan Shrakzan", "Shrakzan", "Shrakzan",        44, 500, "S",  [300000000000, 300000000000, 300000000000, 300000000000], null, 1000000000),
    tourniquet_seven_five: new RaidType("tourniquet_seven_five","C1-6", "Tourniquet 7.5", "Tourniquet 7.5", "T7.5",   80, 500, "S",  [83333333333, 125000000000, 166666666667, 250000000000], /*FS calculated normally*/null, 500000000),
    noir3:              new RaidType("noir3",              "Z22", "Noir (III)", "Noir (III)", "Noir3",                80, 500, "S",  [83333333333, 125000000000, 166666666667, 250000000000], /*FS calculated normally*/null, 500000000),
    arcade_gas_monster: new RaidType("arcade_gas_monster",  "AR", "Arcade Gas Monster", "A G Monster", "Gas Monster", 80, 500, "S",  [73333333333, 110000000000, 146666666667, 220000000000], /*FS calculated normally*/null, 500000000),
    besalaad_exhibit_rampage1: new RaidType("besalaad_exhibit_rampage1","F5","Besalaad Exhibit Rampage", "Besalaad Exhibit", "Exhibit", 80, 500, "S", [100000000000,200000000000,250000000000,300000000000],  /*FS calculated normally*/null, 600000000),

    // Energy Raids
    vince_vortex:	new RaidType("vince_vortex",	     "GD",  "Vince Vortex", "Vince", "Vortex", 		      24, 500, "E",  [600000000, 750000000, 960000000, 1200000000]), 
    king_krandar1:	new RaidType("king_krandar1",        "WR",  "King Krandar", "Krandar", "Krandar",             44, 500, "E",  [250000000000, 250000000000, 250000000000, 250000000000], null, 1000000000),
    fungal_invasion1:	new RaidType("fungal_invasion1",   "F7",  "Fungal Invasion", "Fungal", "Invasion",	      48, 250, "E",  [25000000000,50000000000,62500000000,75000000000], /*FS calculated normally*/null, 1000000000), 
    elite_birthday_cake_of_doom: new RaidType("elite_birthday_cake_of_doom", "F9", "Elite Birthday Cake of Doom", "Elite Cake", "E. Cake",  24, 100, "E", [1875000000000, 1875000000000, 1875000000000, 1875000000000], /*FS calculated normally*/null, 20000000000),
   
   // ALLIANCE RAIDS
    
   // Small Raids
    krakak:             new RaidType("krakak",              "A0", "Krakak Swarm", "Swarm", "Swarm",                  120,  10, "H",  [4500000, 5625000, 7200000, 9000000]),
    kang:               new RaidType("kang",                "A1", "Kang", "Kang", "Kang",                            120,  10, "H",  [5000000, 6250000, 8000000, 10000000]),
    crossbones_squadron: new RaidType("crossbones_squadron","A2", "Crossbones Squadron", "Crossbones", "XBones",     120,  10, "H",  [8000000, 10000000, 12800000, 16000000]),
    colonel_mustard:    new RaidType("colonel_mustard",     "A3", "Colonel Mustard", "Mustard", "Mustard",           120,  10, "H",  [12000000, 15000000, 19200000, 24000000]),
    professor_squid:    new RaidType("professor_squid",     "A4", "Professor Squid", "Squid", "Squid",               120,  10, "H",  [18000000, 22500000, 28800000, 36000000]),
    terminus_death_squad: new RaidType("terminus_death_squad","A5", "Terminus Death Squad", "Death Squad", "Death Squad",120,10,"H", [24000000, 30000000, 38400000, 48000000]),
    luna:               new RaidType("luna",                "A6", "Luna", "Luna", "Luna",                            120,  50, "H",  [50000000, 62500000, 80000000, 100000000]),
    rabid_reindeer:     new RaidType("rabid_reindeer",      "A8", "Rabid Reindeer", "Reindeer", "Reindeer",           60,  50, "H",  [62500000, 81250000, 100000000, 125000000]),

    // Medium Raids
    infection:          new RaidType("infection",           "A0", "Infected Squad",    "Infected", "Infected",       144,  50, "H",  [30000000, 37500000, 48000000, 60000000]),
    flora:              new RaidType("flora",               "A1", "Ruomyes' Death Flora", "Death Flora", "Flora",    144,  50, "H",  [35000000, 43750000, 56000000, 70000000]),
    psychic_cyborg:     new RaidType("psychic_cyborg",      "A2", "Mr. Justice", "Justice", "Justice",               144,  50, "H",  [45000000, 56250000, 72000000, 90000000]),
    grislak:            new RaidType("grislak",             "A3", "Grislak", "Grislak", "Grislak",                   144,  50, "H",  [55000000, 68750000, 88000000, 110000000]),
    qin_legion:         new RaidType("qin_legion",          "A4", "Qin Legion",    "Legion", "Legion",               144,  50, "H",  [65000000, 81250000, 104000000, 130000000]),
    terminus_interceptor_squadron: new RaidType("terminus_interceptor_squadron","A5", "Terminus Interceptor Squadron", "Interceptor", "Interceptor", 144, 50,"H", [75000000, 93750000, 120000000, 150000000]),
    trashmaster:        new RaidType("trashmaster",         "A6", "Trashmaster Colby", "Colby", "Colby",             144,  50, "H",  [100000000, 125000000, 160000000, 200000000]),
    santas_workshop:    new RaidType("santas_workshop",     "A8", "SANTA's Workshop", "Workshop", "Workshop",         72,  50, "H",  [125000000, 156250000, 200000000, 250000000]),
    the_mega_mimes:     new RaidType("the_mega_mimes",      "A2-2", "The Mega Mimes", "Mimes", "Mimes",               84,  50, "H",  [50000000, 62500000, 80000000, 100000000], null, 2000000),

    // Large Raids
    saucers:            new RaidType("saucers",             "A0", "Flying Saucers",    "Saucers", "Saucers",         168,  100, "H", [55000000, 68750000, 88000000, 110000000]),
    tourniquet:         new RaidType("tourniquet",          "A1", "Tourniquet 7", "Tourniquet 7", "T7",              168,  100, "H", [60000000, 75000000, 96000000, 120000000]),
    rylattu_exterminator: new RaidType("rylattu_exterminator","A2", "Rylattu Exterminator", "Exterminator","Exterminator",168,100,"H", [100000000, 125000000, 160000000, 200000000]),
    peacemaker_500:     new RaidType("peacemaker_500",      "A3", "Peacemaker 500",    "Peacemaker", "Peacemaker",   168,  100, "H", [140000000, 175000000, 224000000, 280000000]),
    kaltharan_devourer: new RaidType("kaltharan_devourer",  "A4", "Kaltharan Devourer", "Devourer", "Devourer",      168,  100, "H", [180000000, 225000000, 288000000, 360000000]),
    terminus_juggernaut: new RaidType("terminus_juggernaut","A5", "Terminus Juggernaut", "Juggernaut", "Juggernaut", 168,  100, "H", [200000000, 250000000, 320000000, 400000000]),
    legacy_bot:         new RaidType("legacy_bot",          "A6", "Legacy Bot",    "Legacy", "Legacy",               168,  100, "H", [250000000, 312500000, 400000000, 500000000]),
    haunted_house:      new RaidType("haunted_house",       "AX", "Haunted House", "H. House", "House",              168,  100, "H", [350000000, 437500000, 560000000, 700000000]),
    crazed_santa:       new RaidType("crazed_santa",        "AX", "Crazed Santa", "Santa", "Santa",                   84,  100, "H", [400000000, 520000000, 640000000, 800000000]),
    kristy_love:        new RaidType("kristy_love",         "AX", "Kristy Love", "Kristy", "Love",                    84,  100, "H", [450000000, 585000000, 720000000, 900000000]),
    gedrocht:           new RaidType("gedrocht",            "A9", "Gedrocht", "Gedrocht", "Gedrocht",                 84,  100, "H", [500000000, 650000000, 800000000, 1000000000]),
    nutcracker_sweet:   new RaidType("nutcracker_sweet",    "A11", "Nutcracker Sweet", "Sweet", "Sweet",              84,  100, "H", [750000000, 1000000000, 1500000000, 3000000000]),
    crazy_jalfrezi:     new RaidType("crazy_jalfrezi",      "A12", "The Crazy Jalfrezi", "Jalfrezi", "Freezi",        84,  100, "H", [1000000000, 1250000000, 2000000000, 4000000000]),
    patti:              new RaidType("patti",               "A13", "PATTI", "PATTI", "PATTI",                         84,  100, "H", [1000000000, 1250000000, 2000000000, 4000000000]),
    crimzo_the_killer_clown:new RaidType("crimzo_the_killer_clown","A2-1","Crimzo the Killer Clown","Crimzo","Crimzo",84,  100, "H", [1000000000, 1250000000, 2000000000, 4000000000]),
    the_neon_knights:   new RaidType("the_neon_knights",    "A2-2", "The Neon Knights", "Neon", "Neon",               84,  100, "H", [500000000, 625000000, 800000000, 1000000000], null, 10000000),
    kulnar_xex_shock_trooper_1:	new RaidType("kulnar_xex_shock_trooper_1","A2-5","Kulnar-Xex Shock Trooper","K-X Shock Trooper","KX Shock",72,100,"H", [500000000, 625000000, 800000000, 1000000000],null,10000000),
    invaders_from_dimension_b:	new RaidType("invaders_from_dimension_b", "AX", "Invaders from Dimension B", "Dimension B", "Dimension B", 	48, 100, "H", [250000000000, 300000000000, 350000000000, 750000000000], null, 7500000000), 

    // Epic Raids
    lurking_horror:     new RaidType("lurking_horror",      "A2", "Lurking Horror", "Lurking", "Lurking",            168,  100, "H",  [250000000, 312500000, 400000000, 500000000]),
    ship_of_the_damned: new RaidType("ship_of_the_damned",  "A3", "Ship of the Damned", "Damned", "Damned",          168,  100, "H",  [300000000, 375000000, 480000000, 600000000]),
    mecha_wyrm:         new RaidType("mecha_wyrm",          "A4", "Mecha-Wyrm", "Wyrm", "Wyrm",                      168,  100, "H",  [350000000, 437500000, 560000000, 700000000]),
    genesis:            new RaidType("genesis",             "A5", "Genesis", "Genesis", "Genesis",                   165,  100, "H",  [1000000000, 1250000000, 1600000000, 2000000000]),
    contest_winners:    new RaidType("contest_winners",     "A6", "Shadows of the Void", "Shadows", "Shadows",       168,  100, "H",  [500000000, 625000000, 800000000, 1000000000]),
    celebration_enhancer_1: new RaidType("celebration_enhancer_1","AX","Celebration Enhancer J-54","Celebrator","Celeb",84,100, "H",  [600000000, 750000000, 960000000, 1200000000]),
    quiskan_psi_hound:  new RaidType("quiskan_psi_hound",   "A7","Quiskan Psi-Hound","Psi-Hound","Hound",            168,  100, "H",  [1000000000, 1500000000, 2500000000, 10000000000]),
    ms_myriad_and_steelstike: new RaidType("ms_myriad_and_steelstike","A10","Ms. Myriad and Steelstrike","M & S","M & S",168,100,"H", [1500000000, 2000000000, 3000000000, 12500000000]),
    the_gamma_hammers:  new RaidType("the_gamma_hammers",   "A2-3", "The Gamma Hammers", "Gammas", "Gammas",          84,  100, "H",  [2500000000, 3125000000, 4000000000, 5000000000], null, 50000000),
    kulnarxex_tank_1:   new RaidType("kulnarxex_tank_1",  "A2-4", "Kulnar-Xex Tank", "K-X Tank", "KX Tank",           72,  100, "H",  [2500000000, 3125000000, 4000000000, 5000000000], null, 50000000),

    // Colossal Raids
    wahsh:              new RaidType("wahsh",               "AX", "Wahsh Al-Sahraa", "Wahsh", "Wahsh",                84,  100, "H", [500000000, 1250000000, 3125000000, 7812500000]),
    the_chem_runners:   new RaidType("the_chem_runners",    "A2-4", "The Chem-Runners", "Runners", "Chem",            84,  100, "H", [50000000000, 62500000000, 80000000000, 100000000000], null, 1000000000),
    training_sim1: 	new RaidType("training_sim1","AX", "Live Fire Training Sim #102", "Training Sim #102", "Sim #102", 72,  100, "H", [35000000000,70000000000,87500000000,105000000000], null, 1000000000),
    rogue_terraformer1: new RaidType("rogue_terraformer1",  "AX", "Rogue Terraformer", "Rogue", "Terraformer",	      72,  100, "H", [80000000000, 104000000000, 120000000000, 136000000000],  null, 3000000000),
	
    // Titanic Raids
    thyestean_banquet1: new RaidType("thyestean_banquet1","AX", "Thyestean Banquet", "Banquet", "Banquet",            72,  100, "H", [50000000000, 62500000000, 80000000000, 100000000000],  null, 1000000000),
    rak_thun_eviscipod1: new RaidType("rak_thun_eviscipod1","AX", "Rak-Thun Eviscipod", "Eviscipod", "Evipod",        72,  100, "H", [110000000000,220000000000,275000000000,330000000000],/*FS computed normally*/null, 3300000000),
    ruins_of_the_forgotten1: new RaidType("ruins_of_the_forgotten1", "AX", "Ruins of the Forgotten", "Ruins", "Forgotten", 48, 100, "H", [250000000000, 300000000000, 350000000000, 475000000000],/* FS computed normally*/null, 6000000000),


    // OPERATIONS   

    // Operation: Scavenger's Scramble
    centi_prider_scavenger: new RaidType("centi_prider_scavenger","OP-SS","Centi Prider Scavenger","Centi Scav","Centi Scav",18,25,"S",[5000000000,12000000000,0,0],/*FS computed normally*/null, 1300000000),
    elite_centi_prider_scavenger: new RaidType("elite_centi_prider_scavenger","OP-SS","Elite Centi Prider Scavenger","E. Centi Scav","E. Centi Scav",18, 25, "S", [5000000000,12000000000,0,0], /*FS computed normally*/null, 1300000000),
    kulnar_xex_scavenger: new RaidType("kulnar_xex_scavenger","OP-SS","Kulnar-Xex Scavenger","Kulnar Scav","Kulnar Scav",18, 50,"S", [10000000000,24000000000,0,0], /*FS computed normally*/null, 1300000000),
    elite_kulnar_xex_scavenger: new RaidType("elite_kulnar_xex_scavenger","OP-SS","Elite Kulnar-Xex Scavenger","E. Kulnar Scav","E. Kulnar Scav",18, 50, "S", [10000000000,24000000000,0,0],/* FS computed normally*/null, 1300000000),
    vlarg_scavenger:     new RaidType("vlarg_scavenger","OP-SS","Vlarg Scavenger","Vlarg Scav","Vlarg Scav",          18, 75, "S", [15000000000,36000000000,0,0],/* FS computed normally*/null, 1300000000),
    elite_vlarg_scavenger:new RaidType("elite_vlarg_scavenger","OP-SS","Elite Vlarg Scavenger","E. Vlarg Scav","E. Vlarg Scav",18, 75, "S", [15000000000,36000000000,0,0],/* FS computed normally*/null, 1300000000),
    besalaad_warrior:    new RaidType("besalaad_warrior", "OP-SS", "Besalaad Warrior", "B. Warrior", "B. Warrior",    18, 100, "S", [30000000000,100000000000,0,0],/* FS computed normally*/null, 1300000000),
    strange_parasite:    new RaidType("strange_parasite", "OP-SS", "Strange Parasite", "Parasite", "Parasite",        18, 100, "S", [35000000000,115000000000,0,0],/* FS computed normally*/null, 1300000000),
    pumpkin_pirate_scavenger:new RaidType("pumpkin_pirate_scavenger","OP-SS","Pumpkin Pirate Scavenger","Pumpkin Scav","Pumpkin Scav",18, 100, "S",[35000000000,115000000000,0,0],/* FS computed normally*/null, 1300000000),
    besalaad_commander:  new RaidType("besalaad_commander","OP-SS","Besalaad Commander","B. Commander","B. Commander",18, 100, "S", [45000000000,125000000000,0,0],/* FS computed normally*/null, 1300000000),

    // Operation: Deep Cyan Sea
    kalaxian_projection: new RaidType("kalaxian_projection", "OP-DCS","Kalaxian Projection","Projection","Projection",18, 50, "S", [20000000000,36000000000,0,0],/* FS computed normally*/null, 1300000000),
    kalaxian_cultist_ship: new RaidType("kalaxian_cultist_ship","OP-DCS", "Kalaxian Cultist Ship", "Kalax Ship", "Kalax Ship", 18, 25, "S", [10000000000,24000000000,0,0],/* FS computed normally*/null, 1300000000),
    kalaxian_cultist_bikers: new RaidType("kalaxian_cultist_bikers","OP-DCS", "Kalaxian Cultist Bikers", "Kalax Biker", "Kalax Biker", 18, 50, "S", [20000000000,36000000000,0,0],/* FS computed normally*/null, 1300000000),
    kalaxian_cultists:   new RaidType("kalaxian_cultists","OP-DCS", "Kalaxian Cultists", "Kalax Cult", "Kalax Cult",  18, 75, "S", [20000000000,48000000000,0,0],/* FS computed normally*/null, 1300000000),
    kalaxian_cult_master:new RaidType("kalaxian_cult_master", "OP-DCS", "Kalaxian Cult Master", "K. Cult Master", "K. Cult Master", 18, 100, "S", [50000000000,150000000000,0,0],/* FS computed normally*/null, 1300000000),
    slither:             new RaidType("slither", "OP-DCS", "Slither", "Slither", "Slither",                           18, 100, "S", [70000000000,200000000000,0,0],/* FS computed normally*/null, 1300000000),
	
    // Operation: Transdimentional Encounter
    parasite_cloud:		 new RaidType("parasite_cloud", "OP-TE", "Parasite Cloud", "Cloud", "Cloud",			  18, 25, "H", [15000000000, 30000000000, 0, 0],/* FS computed normally*/null, 1300000000),
    antibody_drone:		 new RaidType("antibody_drone", "OP-TE", "Antibody Drone", "Drone", "Drone", 			  18, 50, "H", [24000000000, 48000000000, 0, 0], /*FS computed normally*/null, 1300000000),
    antibody_swarm:		 new RaidType("antibody_swarm", "OP-TE", "Antibody Swarm", "Anti Swarm", "Anti Swarm", 		  18, 100, "H", [50000000000, 150000000000, 0, 0],/* FS computed normally*/null, 1300000000),
    exozoic_hulk:		 new RaidType("exozoic_hulk", "OP-TE", "Exozoic Hulk", "Exozoic", "Exozoic", 			  18, 100, "H", [55000000000, 132000000000, 0, 0],/* FS computed normally*/null, 1300000000),
    plasmatic_entity:	 new RaidType("plasmatic_entity", "OP-TE", "Plasmatic Entity", "Plasmatic", "Plasmatic", 	  18, 100, "H", [60000000000, 180000000000, 0, 0],/* FS computed normally*/null, 1300000000),


    // WORLD RAIDS
    // Infestation WR Trilogy
    inf_ship:           new RaidType("inf_ship",            "WR", "The Python", "Python", "Python WR",                72,  90000, "SEH", "Infinite", "N/A",   1000000000),
    inf_colony:         new RaidType("inf_colony",          "WR", "Infested Colony", "Colony", "Colony WR",           72,  90000, "SEH", "Infinite", "N/A",   1000000000),
    inf_lair:           new RaidType("inf_lair",            "WR", "Alien Lair", "Lair", "Lair WR",                    72,  90000, "SEH", "Infinite", "N/A",   1000000000),
    
	general_skorzeny:   new RaidType("general_skorzeny",    "WR", "General Skorzeny", "Skorzeny", "Skorz",            72,  90000, "SEH", "Infinite", "N/A", 1500000000000),
    wr_space_pox:       new RaidType("wr_space_pox",        "WR", "Intergalactic Space Pox", "WR Pox", "WR Pox",      72,  90000, "SEH", "Infinite", "N/A",   5000000000),
    cerebral_destroyer: new RaidType("cerebral_destroyer",  "WR", "Cerebral Destroyer", "Cerebral", "CD WR",          72,  90000,"SEH", "Infinite", "N/A",   10000000000),
    kraken:             new RaidType("kraken",              "WR", "Kraken", "Kraken", "Kraken WR",                    72,  90000, "SEH", "Infinite", "N/A",  50000000000),
    christmas_montage:  new RaidType("christmas_montage",   "WR", "Christmas Campaign", "Christmas", "Xmas WR",       48,  90000, "SEH", "Infinite", "N/A",   5000000000),
    schism:             new RaidType("schism",              "WR", "Schism", "Schism", "Schism WR",                   120,  90000, "SEH", "Infinite", "N/A",  50000000000),
    inventors_revenge:  new RaidType("inventors_revenge",   "WR", "Inventor's Revenge", "Revenge", "Revenge WR",      72,  90000, "SEH", "Infinite", "N/A",  75000000000),
    hel:                new RaidType("hel",                 "WR", "Hel", "Hel", "Hel WR",                             72,  90000, "SEH", "Infinite", "N/A",  75000000000),
    centi_priders:      new RaidType("centi_priders",       "WR", "Centi Priders", "Centies", "Centies WR",           72,  90000, "SEH", "Infinite", "N/A",  75000000000),
    kulnar_xex_battle_station_1: new RaidType("kulnar_xex_battle_station_1","WR","Kulnar-Xex Battle Station","K-X Battle Station","KX BS WR", 72,90000,"SEH","Infinite","N/A",200000000000),
    cow_abduction_1:    new RaidType("cow_abduction_1",     "WR", "Rylattu Cow Abduction", "Cow Abduction", "Cow WR", 72, 90000, "SEH", "Infinite", "N/A",  10000000000),
    dimetrodon_riot:    new RaidType("dimetrodon_riot",     "RS", "Dimetrodon Riot", "D. Riot", "Riot RS",            24, 90000, "SEH", "Infinite", "N/A",  200000000000),
    trouble_in_tokyo:   new RaidType("trouble_in_tokyo",    "WR", "Trouble in Tokyo", "Tokyo", "Tokyo WR",           120, 90000, "SEH", "Infinite", "N/A",  400000000000),
    kalaxian_assault:   new RaidType("kalaxian_assault",    "WR", "Kalaxian Assault", "Kalax Assault", "Kalax WR",    96, 99999, "SEH", "Infinite", "N/A",  200000000000),
    contest_winner:     new RaidType("contest_winner",      "WR", "Hyper-Con Havoc", "Havoc WR", "Havoc WR",          96, 99999, "SEH", "Infinite", "N/A",  200000000000),
    elves:		new RaidType("elves", 		    "WR", "Elven Uprising", "Elven", "Elven",		      96, 99999, "SEH", "Infinite", "N/A",  400000000000),
    game_master:	new RaidType("game_master", 	    "WR", "Game Master", "Game Master", "Game Master", 	      72, 99999, "SEH", "Infinite", "N/A",  1500000000000),
    solar_swarm:	new RaidType("solar_swarm", 	    "WR", "Solar Swarm", "Solar", "Solar",		      72, 99999, "SEH", "Infinite", "N/A",  1500000000000),
    sun_egg:		new RaidType("sun_egg", 	    "WR", "Sun Egg", "Sun Egg", "Egg",			      72, 99999, "SEH", "Infinite", "N/A",  1500000000000),
    attack_of_the_gourds: new RaidType("attack_of_the_gourds", "WR", "Attack of the Gourds", "Gourds", "Gourds",      72, 99999, "SEH", "Infinite", "N/A",  2500000000000), 
    predatory_constellation:  new RaidType("predatory_constellation", "WR", "Predatory Constellation", "Constellation", "Constellation",  120, 99999, "SEH", "Infinite", "N/A", 10000000000000), 
	
    // RARE SPAWNS
    raging_snowman:     new RaidType("raging_snowman",      "RS", "Raging Snowman", "Snowman", "Snowman RS",          24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    space_pox_mary:     new RaidType("space_pox_mary",      "RS", "Space Pox Mary", "Mary", "Mary RS",                24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    mutated_spacepox_1: new RaidType("mutated_spacepox_1",  "RS", "Mutated Space Pox", "Mutated", "Mutated",	      24,  90000, "SEH", "Infinite", "N/A",   20000000000),
    cerebral_ceo:       new RaidType("cerebral_ceo",        "RS", "Cerebral CEO", "CEO", "CEO RS",                    24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    penelope_wellerd:   new RaidType("penelope_wellerd",    "RS", "Penelope Wellerd", "Wellerd", "Wellerd RS",        24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    h8:                 new RaidType("h8",                  "RS", "H8", "H8", "H8 RS",                                24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    inventors_scheme:   new RaidType("inventors_scheme",    "RS", "Inventor's Scheme", "Scheme", "Scheme RS",         24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    predator_moon:      new RaidType("predator_moon",       "RS", "Predator Moon", "Predator", "Moon RS",             24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    "5th_planet":         new RaidType("5th_planet",          "RS", "5th Planet", "5th Planet", "5th Planet RS",        24,  90000, "SEH", "Infinite", "N/A",   2000000000),
    cerebral_monster_mech: new RaidType("cerebral_monster_mech","RS", "Cerebral Monster Mech", "Cerebral MM", "CMM RS", 24,  90000, "SEH", "Infinite", "N/A",  20000000000),
    kulnarxex_scout_ships_1: new RaidType("kulnarxex_scout_ships_1","RS","Kulnar-Xex Scout Ships","K-X Scout Ships","KX Scout RS", 24,90000,"SEH","Infinite","N/A",25000000000),
    kulnarxex_bombarder_1: new RaidType("kulnarxex_bombarder_1","RS","Kulnar-Xex Bombarder","K-X Bombarder","KX Bomb RS", 24,90000,"SEH", "Infinite", "N/A",   25000000000),
    ship_pinata:		new RaidType("ship_pinata",         "RS", "Ship Pinata", "Pinata", "Pinata RS",               24, 90000, "SEH", "Infinite", "N/A",   25000000000),
    besalaad_warmasterrs: new RaidType("besalaad_warmasterrs","RS","Besalaad Elite Warmaster", "E. Warmaster","BEW RS", 24, 90000, "SEH", "Infinite", "N/A",   50000000000),
    star_turtles_distress: new RaidType("star_turtles_distress","RS","Star Turtle's Distress","Star Turtle","Turtle RS", 24,99999, "SEH", "Infinite", "N/A", 75000000000),
    kleptotherms:       new RaidType("kleptotherms", "RS", "Kleptotherms", "Kleptotherms", "Kleptotherms RS",         24, 99999, "SEH", "Infinite", "N/A", 100000000000),
    hate: 				new RaidType("hate", 	     "RS", "Hate Walker", "Hate", "Hate",			      24, 99999, "SEH", "Infinite", "N/A", 100000000000)

};/************************************/
/********* Utility Functions ********/
/************************************/

if (!window.holodeck) {
  window.holodeck = new Holodeck();
}

/**
 * Returns the boolean opposite of the result of the given function
 * @param fn The function to take the opposite of
 * @param scope {Object?} The scope to call the function with
 */
DC_LoaTS_Helper.not = function(fn, scope) {
  return function() {
    return !fn.apply(scope || window, arguments);
  }
};

/**
 * Returns a function of all the arguments passed in called in order
 */
DC_LoaTS_Helper.chain = function() {
  var fns = arguments;
  return function() {
    var ret;
    for (var i = 0; i < fns.length; i++) {
      try {
        ret = fns[i].apply(this, arguments);
      }
      catch (e) {
        console.error("Utilities.js: Error during function chain", e);
      }
    }
    return ret;
  }
};

DC_LoaTS_Helper.getCurrentUsername = function() {
  return holodeck.username();
};

DC_LoaTS_Helper.isFriend = function(username) {
  return holodeck.chatWindow().isFriend(username);
};

DC_LoaTS_Helper.addFriend = function(username) {
  new Ajax.Request("http://www.kongregate.com/accounts/" + DC_LoaTS_Helper.getCurrentUsername() + "/friends/"+ username + "?friend_from=chat", {
    method: 'put',
    onComplete: function(transport)
    {
      DCDebug("Added Friend: " + transport.request.url);
      // Update the listing in the top of the chat
      holodeck.addFriend(this);
    }
  });
};

DC_LoaTS_Helper.removeFriend = function(username) {
  new Ajax.Request("http://www.kongregate.com/accounts/" + DC_LoaTS_Helper.getCurrentUsername() + "/friends/"+ username + "?friend_from=chat", {
    method: 'delete',
    onComplete: function(transport)
    {
      DCDebug("Removed Friend: " + transport.request.url);
      // Update the listing in the top of the chat
      holodeck.removeFriend(this);
    }
  });
};

DC_LoaTS_Helper.isMuted = function(username) {
  return holodeck.chatWindow().isMuted(username);
};

DC_LoaTS_Helper.muteUser = function(username) {
  new Ajax.Request("http://www.kongregate.com/accounts/" + DC_LoaTS_Helper.getCurrentUsername() + "/muted_users/?username="+ username + "&from_chat=true", {
    method: 'post',
    onComplete: function(transport)
    {
      DCDebug("Muted User: " + transport.request.url);
      // Update the listing in the top of the chat
      holodeck.chatWindow().addMutings([username]);
    }
  });
};

DC_LoaTS_Helper.unmuteUser = function(username) {
  new Ajax.Request("http://www.kongregate.com/accounts/" + DC_LoaTS_Helper.getCurrentUsername() + "/muted_users/"+ username + "?from_chat=true", {
    method: 'delete',
    onComplete: function(transport)
    {
      DCDebug("Unmuted User: " + transport.request.url);
      // Update the listing in the top of the chat
      holodeck.chatWindow().removeMutings([username]);
    }
  });
};

DC_LoaTS_Helper.openPrivateProfileMessageWindow = function(username) {
  username && window.open("http://www.kongregate.com/accounts/" + username + "/private_messages?focus=true", "_blank");
};

DC_LoaTS_Helper.showUgUpProfile = function(username) {
  DCDebug("Utilities.js: Showing UgUp Profile");
  if (username) {
    RaidMenu.setActiveTab(RaidMenu.tabClasses[40]);
    document.getElementById("CharacterViewMenu-UsernameBox").value = username;
    document.getElementById("CharacterViewMenu-RunQueryButton").click();
  }
};


// Hooks up a listener for a particular event on a specific object
// Borrowed from: http://www.quirksmode.org/js/eventSimple.html
DC_LoaTS_Helper.registerEventHandler = function(obj,evt,fn)
{
  if (obj.addEventListener)
  {
    obj.addEventListener(evt,fn,false);
  }
  else if (obj.attachEvent)
  {
    obj.attachEvent('on'+evt,fn);
  }
};

// Unhooks a listener for a particular event on a specific object
// Borrowed from: http://www.quirksmode.org/js/eventSimple.html
DC_LoaTS_Helper.unregisterEventHandler = function(obj,evt,fn)
{
  if (obj.removeEventListener)
  {
    obj.removeEventListener(evt,fn,false);
  }
  else if (obj.detachEvent)
  {
    obj.detachEvent('on'+evt,fn);
  }
};

DC_LoaTS_Helper.isLeftClickEvent = function(evt) {
  // evt.which for IE6,7,8/Opera. evt.button for everyone else
  return  (evt.button && (evt.button == 0)) || (evt.which && (evt.which == 1));
};

DC_LoaTS_Helper.isRightClickEvent = function(evt) {
  // evt.which for IE6,7,8/Opera. evt.button for everyone else
  return (evt.button && (evt.button == 2)) || (evt.which && (evt.which == 3));
};

DC_LoaTS_Helper.getEventTarget = function(evt) {
  var target = evt.target || evt.srcElement;

  // Safari work around, not that we even support Safari...
  if (target.nodeType && target.nodeType == 3)
  {
    target = target.parentNode;
  }

  return target;
};

// Should prevent the event from doing its normal action
// like selecting text on click and drag.
// Borrowed from http://stackoverflow.com/a/891616
DC_LoaTS_Helper.stopDefaultEventAction = function(evt)
{
  if (evt && evt.preventDefault)
  {
    evt.preventDefault();
  }
  else
  {
    window.event.returnValue = false;
  }
  return false;
};

// Should prevent the event from propagating to Kongregate code
DC_LoaTS_Helper.stopEventPropagation = function(evt)
{
  if (evt && evt.stopPropagation)
  {
    evt.stopPropagation();
  }
  return false;
};

// Borrowed from http://stackoverflow.com/a/384380
DC_LoaTS_Helper.isElement = function(o) {
  return o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string";
};

DC_LoaTS_Helper.removeAllChildren = function(parentNode) {
  while (parentNode.firstChild) {
    parentNode.removeChild(parentNode.firstChild);
  }
};

// Pretty format health / damage numbers
DC_LoaTS_Helper.prettyFormatNumber = function(num)
{
  var text = "?";
  if (typeof num === "number")
  {
    // Trillions
    if (num >= 1000000000000)
    {
      text = parseFloat((num/1000000000000).toFixed(3)) + "T";
    }
    // Billions
    else if (num >= 1000000000)
    {
      text = parseFloat((num/1000000000).toFixed(2)) + "B";
    }
    // Millions
    else if (num >= 1000000)
    {
      text = parseFloat((num/1000000).toFixed(2)) + "M";
    }
    // Thousands
    else if (num >= 1000)
    {
      text = parseFloat((num/1000).toFixed(1)) + "K";
    }
    // Other
    else if (num > 0)
    {
      text = num + "";
    }
  }
  else if (typeof num === "string")
  {
    text = num;
  }
  return text;
};

// Responds to a click on a raid link
// Returns true if the browser should load the raid itself, false if we loaded without refresh
DC_LoaTS_Helper.raidLinkClick = function(eventParam)
{
  // Want to handle any errors that occur in here
  try
  {
    // Just in case
    var event = eventParam || window.event;

    // Couldn't locate event
    if (typeof event == "undefined")
    {
      console.warn("Couldn't locate the event for right-click detection");

      // Don't cancel the click
      return;
    }

    // Get the target element
    var target;
    if (event.target)
    {
      target = event.target;
    }
    else if (event.srcElement)
    {
      target = event.srcElement;
    }

    // Safari work around
    if (target.nodeType == 3)
    {
      target = target.parentNode;
    }


    if (typeof target == "undefined")
    {
      console.warn("Couldn't locate the target for right-click detection");

      // Don't cancel the click
      return;
    }

    // Grab the url from the link
    var url = target.href;

    // Still failed
    if (typeof url == "undefined" || url.length == 0)
    {
      // In certain cases, the image can detect the click instead of the link
      if (target.parentNode.href != "undefined")
      {
        url = target.parentNode.href;
      }
      else
      {
        console.warn("Trouble determining url from link. Could not apply click.");
        console.warn(event);
        console.warn(target);

        // Let the click go through and reload the whole browser. Better than nothing.
        return true;
      }
    }

    // If the user is holding shift, cycle through the states
    if (event.shiftKey)
    {
      // Generate an actual raid link object
      var raidLink = new RaidLink(url);

      // If the link is valid
      if (raidLink.isValid())
      {
        // Get the STATE of the link
        var linkState = RaidManager.fetchState(raidLink);

        // Place holder for our new state
        var newLinkState;

        var foundCurrent = false;
        var firstState;

        // Iterate over all possible states
        // This is basically a hack for the fact that the
        // STATEs don't have any inherit ordinal values that could be incremented
        //TODO: Reorganize STATE to have ordinals if this ever happens somewhere else in the code
        for (var stateKey in RaidManager.STATE)
        {
          if (RaidManager.STATE.hasOwnProperty(stateKey)) {
            // Grab the state
            var state = RaidManager.STATE[stateKey];

            // Make sure this isn't a function or anything from STATE
            if (typeof state == "object")
            {
              // If this is the first state we've seen
              if (typeof firstState == "undefined")
              {
                // Capture it so we can roll back around past the last state
                firstState = state;
              }

              // If this is the same state as the link is currently in
              if (RaidManager.STATE.equals(linkState, state))
              {
                // Note the current state
                foundCurrent = true;
              }
              // If we found current, this must be the next state
              else if (foundCurrent)
              {
                // Grab this state to save as the new state
                newLinkState = state;

                // Don't accidentally find other states
                break;
              }
            }
          }
        }

        // If we did not find a new state to set it to
        if (typeof newLinkState == "undefined")
        {
          // Cycle back around to the first state
          newLinkState = firstState;
        }

        // Store the link with its new state
        RaidManager.store(raidLink, newLinkState);
      }
      // Log invalid raid links
      else
      {
        console.warn("Clicked invalid link to " + url);
      }

      // Always suppress reload on shift-clicks
      return false;
    }
    // If the user is not holding shift, just load the raid
    else
    {
      return DC_LoaTS_Helper.loadRaid(url);
    }
  }
  catch(ex)
  {
    // Notify the user of the issue
    console.warn("An error occurred while trying to handle your click!");

    console.warn(ex);

    // Let the click go through. Annoying, but still can load raid
    return true;
  }
};

// Intended solely to capture right clicks for the purpose of marking them visited
DC_LoaTS_Helper.raidLinkMouseDown = function(eventParam)
{
  // Just in case
  var event = eventParam || window.event;

  // Couldn't locate event
  if (typeof event == "undefined")
  {
    console.warn("Couldn't locate the event for right-click detection");

    // Don't cancel the click
    return;
  }

  // Only care about right clicks
  if (DC_LoaTS_Helper.isRightClickEvent(event))
  {
    // Get the target element
    var target = DC_LoaTS_Helper.getEventTarget(event);

    // If there was no target
    if (typeof target === "undefined")
    {
      console.warn("Couldn't locate the target for right-click detection");

      // Don't cancel the click
      return;
    }

    // Grab the url from the link
    var url = target.href;

    // Still failed
    if (typeof url == "undefined" || url.length == 0)
    {
      // In certain cases, the image can detect the click instead of the link
      if (target.parentNode.href != "undefined")
      {
        url = target.parentNode.href;
      }
      else
      {
        console.warn("Trouble determining url from link. Could not apply click.");
        console.warn(event);
        console.warn(target);

        // No useful work to complete here
        return;
      }
    }
    // Successfully got the url
    // Get the raid link
    var raidLink = new RaidLink(url);

    // Only care about valid links
    if (raidLink.isValid())
    {
      if (DC_LoaTS_Helper.getPref("RightClickVisited", true) === true)
      {
        RaidManager.store(raidLink, RaidManager.STATE.VISITED);
      }
    }
    else
    {
      console.warn("Could not parse url (\"" + url + "\") into a valid RaidLink");
      console.warn(raidLink);
    }
  }
};

DC_LoaTS_Helper.handleMessageWindowClickHandler = function() {
  DCDebug("Utilities.js: DC_LoaTS_Helper.handleMessageWindowClickHandler");
  var lctw = DC_LoaTS_Helper.getPref("LeftClickToWhisper", true);
  DCDebug("LeftClickToWhisper: ", lctw);
  if (holodeck._chat_window && holodeck._chat_window._chat_rooms_container_node) {
    // We should be attaching the left click handler
    if (lctw) {
      // This destroys the existing Kong functionality of left-clicking to load mini-profile
      ChatDialogue.MESSAGE_TEMPLATE.template = ChatDialogue.MESSAGE_TEMPLATE.template.replace('username="#{username}"', '_username="#{username}"');

      // Actually register the click handler onto the node
      DC_LoaTS_Helper.registerEventHandler(
        holodeck._chat_window._chat_rooms_container_node,
        "click",
        DC_LoaTS_Helper.messageWindowClick
      );

      // Register the click handler to hide the menu on the body
      DC_LoaTS_Helper.registerEventHandler(
        document.body,
        "click",
        DC_LoaTS_Helper.hideContextMenu
      );
    }
    else {
      // This repairs the existing Kong functionality of left-clicking to load mini-profile
      ChatDialogue.MESSAGE_TEMPLATE.template = ChatDialogue.MESSAGE_TEMPLATE.template.replace('_username="#{username}"', 'username="#{username}"');

      // Actually unregister the click handler onto the node
      DC_LoaTS_Helper.unregisterEventHandler(
        holodeck._chat_window._chat_rooms_container_node,
        "click",
        DC_LoaTS_Helper.messageWindowClick
      );

      // Unregister the click handler to hide the menu on the body
      DC_LoaTS_Helper.unregisterEventHandler(
        document.body,
        "click",
        DC_LoaTS_Helper.hideContextMenu
      );

    }
  }
  else {
    DCDebug("Waiting 1 second to try again");
    setTimeout(DC_LoaTS_Helper.handleMessageWindowClickHandler, 1000);
  }
};

DC_LoaTS_Helper.handleMessageWindowContextMenuHandler = function() {
  DCDebug("Utilities.js: DC_LoaTS_Helper.handleMessageWindowContextMenuHandler");
  var rcm = DC_LoaTS_Helper.getPref("RightClickUserMenu", true);
  DCDebug("RightClickUserMenu: ", rcm);
  if (holodeck._chat_window && holodeck._chat_window._chat_rooms_container_node) {
    // We should be attaching the context menu
    if (rcm) {
      // Actually register the click handler onto the node
      DC_LoaTS_Helper.registerEventHandler(
        holodeck._chat_window._chat_rooms_container_node,
        "contextmenu",
        DC_LoaTS_Helper.messageWindowRightClick
      );
    }
    else {
      // Unregister the click handler onto the node
      DC_LoaTS_Helper.unregisterEventHandler(
        holodeck._chat_window._chat_rooms_container_node,
        "contextmenu",
        DC_LoaTS_Helper.messageWindowRightClick
      );
    }
  }
  else {
    DCDebug("Waiting 1 second to try again");
    setTimeout(DC_LoaTS_Helper.handleMessageWindowContextMenuHandler, 1000);
  }
};

DC_LoaTS_Helper.messageWindowClick = function(event) {
  var ret,
    lctw = DC_LoaTS_Helper.getPref("LeftClickToWhisper", true),
    username;

  if (lctw) {
    // If we've altered the left click functionality (lctw==true), the username will be in _username
    if (event.target &&
      (username = event.target.getAttribute("_username") || event.target.getAttribute("username"))) {

      // Is it a left click
      if (DC_LoaTS_Helper.isLeftClickEvent(event)) {
        // Since we're doing this, don't let any other actions have it
        DC_LoaTS_Helper.stopDefaultEventAction(event);

        DCDebug("Caught left click on name", event, username);

        // Insert the /w username into the chat area
        holodeck.chatWindow().insertPrivateMessagePrefixFor(username);
        ret = false;
      }
    }
  }

  return ret;
};

DC_LoaTS_Helper.messageWindowRightClick = function(event) {
  var ret,
    rcm = DC_LoaTS_Helper.getPref("RightClickUserMenu", true),
    contextMenu,
    username, coords;

  if (rcm) {
    // If we've altered the left click functionality (lctw==true), the username will be in _username
    if (event.target &&
      (username = event.target.getAttribute("_username") || event.target.getAttribute("username"))) {

      // Since we're doing this, don't let any other actions have it
      DC_LoaTS_Helper.stopDefaultEventAction(event);

      DCDebug("Caught right click on name", event, username);

      // Hide the existing context menu
      DC_LoaTS_Helper.hideContextMenu();

      // Get the absolute coordinates of mouse event
      coords = DC_LoaTS_Helper.getMouseEventCoords(event);

      DCDebug("Utilities.js: Right click event", event);

      // Create context menu
      contextMenu = DC_LoaTS_Helper.createUserContextMenu(username);

      // Pop-up context menu
      DC_LoaTS_Helper.showContextMenu(contextMenu, coords.x, coords.y);

      ret = false;
    }
  }

  return ret;
};

DC_LoaTS_Helper.getMouseEventCoords = function(e) {
  var posx = 0,
    posy = 0;
  e = e || window.event;

  if (e.pageX || e.pageY) 	{
    posx = e.pageX;
    posy = e.pageY;
  }
  else if (e.clientX || e.clientY) 	{
    posx = e.clientX + document.body.scrollLeft
      + document.documentElement.scrollLeft;
    posy = e.clientY + document.body.scrollTop
      + document.documentElement.scrollTop;
  }

  return {x: posx, y: posy};
};

DC_LoaTS_Helper.userContextMenuItems = [
  {
    text: "{username}'s Kong Profile",
    title: "Show {username}'s Kongregate mini-profile normally shown on left-clicks",
    fn: holodeck.chatWindow().showProfile.bind(holodeck.chatWindow())
  },
  {
    text: "Show LoTS Profile",
    title: "Show {username}'s LoTS public profile via UgUp",
    fn: DC_LoaTS_Helper.showUgUpProfile
  },
  {
    text: "Send Profile Message",
    title: "Send {username} a Kongregate profile private message",
    fn: DC_LoaTS_Helper.openPrivateProfileMessageWindow
  },
  {
    text: "Add Friend",
    title: "Add {username} as your friend",
    condition: DC_LoaTS_Helper.not(DC_LoaTS_Helper.isFriend),
    fn: DC_LoaTS_Helper.addFriend
  },
  {
    text: "Unfriend",
    title: "Remove {username} from your friends list",
    condition: DC_LoaTS_Helper.isFriend,
    fn: DC_LoaTS_Helper.removeFriend
  },
  {
    text: "Mute",
    title: "Mute {username} to hide their messages in chat",
    condition: DC_LoaTS_Helper.not(DC_LoaTS_Helper.isMuted),
    fn: DC_LoaTS_Helper.muteUser
  },
  {
    text: "Unmute",
    title: "Unmute {username} to show their messages in chat",
    condition: DC_LoaTS_Helper.isMuted,
    fn: DC_LoaTS_Helper.unmuteUser
  }
];

DC_LoaTS_Helper.createUserContextMenu = function (username) {
  DCDebug("Utilities.js: Creating Context Menu for user " + username);
  var menu = document.createElement("ul"),
    itemDef, li, a;
  menu.id = "DC_LoaTS_contextMenu";
  menu.className = "context-menu user-context-menu";
  for (var i = 0; i < DC_LoaTS_Helper.userContextMenuItems.length; i++) {
    itemDef = DC_LoaTS_Helper.userContextMenuItems[i];
    a = null;

    DCDebug("Utilities.js: itemDef: ", itemDef);

    if (typeof itemDef.condition === "undefined" ||
      (typeof itemDef.condition === "function" && itemDef.condition(username))) {
      li = document.createElement("li");
      li.className = "menu-item";


      if (itemDef.fn) {
        a = document.createElement("a");
        a.onclick = DC_LoaTS_Helper.chain(
          itemDef.fn.bind(this, username),
          function(clickEvent) {
            DC_LoaTS_Helper.stopDefaultEventAction(clickEvent);
            DC_LoaTS_Helper.stopEventPropagation(clickEvent);
            DC_LoaTS_Helper.hideContextMenu(menu);
          });
        li.appendChild(a);
      }

      if (itemDef.title) {
        (a||li).title = itemDef.title.replace("{username}", username);
      }
      if (itemDef.text) {
        (a||li).appendChild(document.createTextNode(itemDef.text.replace("{username}", username)));
      }

      menu.appendChild(li);
    }
  }

  DCDebug("Utilities.js: Created Menu", menu);

  return menu;
};

DC_LoaTS_Helper.showContextMenu = function (contextMenu, x, y) {
  DCDebug("Utilities.js: showContextMenu ", arguments);
  if (DC_LoaTS_Helper.contextMenu) {
    DC_LoaTS_Helper.hideContextMenu(DC_LoaTS_Helper.contextMenu);
  }
  DC_LoaTS_Helper.contextMenu = contextMenu;
  contextMenu.style.position = "absolute";
  contextMenu.style.left = x + "px";
  contextMenu.style.top = y + "px";
  contextMenu.style.visible = "visible";
  contextMenu.style.display = "auto";
  document.body.appendChild(contextMenu);
};

DC_LoaTS_Helper.hideContextMenu = function(contextMenu) {
  // If this is a click event or something, it's not an element
  if (!DC_LoaTS_Helper.isElement(contextMenu)) {
    contextMenu = null;
  }
  contextMenu = contextMenu || DC_LoaTS_Helper.contextMenu || document.getElementById("DC_LoaTS_contextMenu");
  DCDebug("Hiding Context Menu: ", DC_LoaTS_Helper.isElement(contextMenu), contextMenu, DC_LoaTS_Helper.contextMenu, document.getElementById("DC_LoaTS_contextMenu"));
  contextMenu && contextMenu.parentNode.removeChild(contextMenu);
  DC_LoaTS_Helper.contextMenu = null;
};


// Force the download of some data as a file
// Works nice on some browsers
// Title parameter only works in some browsers as well.
DC_LoaTS_Helper.forceDownload = function(data/*, title*/)
{
//			// Awesome style
//			window.requestFileSystem = window.webkitRequestFileSystem || window.requestFileSystem;
//			if (window.requestFileSystem)
//			{
//				
//				function onInitFs(fs) {
//
//					fs.root.getFile(title + '.txt', {create: true}, function(fileEntry) {
//				
//						fileEntry.createWriter(function(fileWriter) {
//						
//							fileWriter.onwriteend = function(e) {
////								if (typeof fileEntry.toURI === "function") {
////									location.href = fileEntry.toURI();
////								}
////								else {
//									window.open(fileEntry.toURL());
////								}
//								holodeck.activeDialogue().raidBotMessage('Finished writing ' + title);
//							};
//							
//							fileWriter.onerror = function(e) {
//								holodeck.activeDialogue().raidBotMessage('Write of ' + title + ' failed: ' + e.toString());
//							};
//							
//							// Create a new Blob and write it
//							var blob = new Blob([data], {type: 'text/plain'});
//							
//							console.log("Writing ", data, blob);
//							
//							fileWriter.write(blob);
//						
//						}, errorHandler);
//					
//					}, errorHandler);
//				
//				}
//				
//				function errorHandler(e) {
//					var msg = '';
//					
//					switch (e.code) {
//						case FileError.QUOTA_EXCEEDED_ERR:
//							msg = 'QUOTA_EXCEEDED_ERR';
//							break;
//						case FileError.NOT_FOUND_ERR:
//							msg = 'NOT_FOUND_ERR';
//							break;
//						case FileError.SECURITY_ERR:
//							msg = 'SECURITY_ERR';
//							break;
//						case FileError.INVALID_MODIFICATION_ERR:
//							msg = 'INVALID_MODIFICATION_ERR';
//							break;
//						case FileError.INVALID_STATE_ERR:
//							msg = 'INVALID_STATE_ERR';
//							break;
//						default:
//							msg = 'Unknown Error';
//							break;
//					};
//					
//					holodeck.activeDialogue().raidBotMessage('Write of ' + title + ' failed: ' + msg);
//				}
//
//				
//				window.requestFileSystem(window.TEMPORARY, 8*data.length, onInitFs, errorHandler);
//
//				














//				 window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {
//				    fs.root.getFile(title + '.txt', {create: true}, function(fileEntry) {
//				        fileEntry.createWriter(function(fileWriter) {
////				            var arr = new Uint8Array(3); // data length
////				
////				            arr[0] = 97; // byte data; these are codes for 'abc'
////				            arr[1] = 98;
////				            arr[2] = 99;
////				
////				            var blob = new Blob([arr]);
////				
////				            fileWriter.addEventListener("writeend", function() {
////				                // navigate to file, will download
////				                location.href = fileEntry.toURL();
////				            }, false);
//				
//				            fileWriter.write(data);
//				        }, function() {});
//				    }, function() {});
//				}, function() {});
//			}
  // Sad style
//			else
//			{
  window.open('data:text/csv;charset=utf8,' + encodeURIComponent(data));
//			}
  return true;
};

// Pastebin API
DC_LoaTS_Helper.PastebinAPI = {
  privacy: {
    PUBLIC: 0,
    UNLISTED: 1,
    PRIVATE: 2
  },

  duration: {
    MINUTES: "10M",
    HOUR: "1H",
    DAY: "1D",
    MONTH: "1M",
    NEVER: "N"
  },

  options: {
    PASTE: "paste",
    LIST: "list",
    TRENDS: "trends",
    DELETE: "delete",
    USER_DETAILS: "userdetails"

  },

  pasteData: function(data, title, note) {

    var paste = {
      api_option: this.options.PASTE,
      api_dev_key_enc: ":117ce9e35bfgec11f1336f96916c4d1",
      api_paste_code: data,
      api_paste_private: this.privacy.UNLISTED,
      api_paste_name: title,
      api_paste_expire_date: this.duration.MONTH
    };


    DC_LoaTS_Helper.ajax({
      url: "http://pastebin.com/api/api_post.php",
      method: "POST",
      data: DC_LoaTS_Helper.uriSerialize(paste),
      onload: function(response) {
        var message;
        if (response.status == 200 && /^(?:http:\/\/)?(?:www\.)?pastebin.com\/\w+$/i.test(response.responseText)) {
          message = "Successfully created pastebin <a href='" + response.responseText + "' target='_blank'>" + response.responseText + "</a> for " + note;
          window.open(response.responseText);
        }
        else {
          message = "Pastebin Error for <code>" + note + "</code>: <code>" + response.responseText + "</code>";
        }

        holodeck.activeDialogue().raidBotMessage(message);
      }
    });
  }
};

// Serialize a JS object for form submission
DC_LoaTS_Helper.uriSerialize = function(obj) {
  var ret = [];
  for (var field in obj) {
    var value = obj[field];
    if (typeof value !== "function" && obj.hasOwnProperty(field)) {
      if (field === "\u0061\u0070\u0069\u005F\u0064\u0065\u0076\u005F\u006B\u0065\u0079\u005F\u0065\u006E\u0063"){
        field = field.substring(0, field.length-4);
        value = (function(){var s=value,m="";for(i=0;i<s.length;i++){m+=(!(s.charCodeAt(i)-28))?'&':(!(s.charCodeAt(i)-23))?'!':String.fromCharCode(s.charCodeAt(i)-1)}return m}());
      }
      ret.push(encodeURIComponent(field) + "=" + encodeURIComponent(value));
    }
  }

  return ret.join("&");
};


// Obtains the iframe_options from the game page
DC_LoaTS_Helper.getIFrameOptions = function() {
  try
  {
    // Regex to locate the iframe properties as defined by Kong
    var reg = new RegExp(/var iframe_options = ([^\x3B]+)/g);

    // If Kong has defined the properties we need to scrape from
    if (typeof activateGame !== "undefined")
    {
      // Attempt to find the properties we need
      var match = reg.exec(activateGame);

      // If we have the iframe options
      if (match != null)
      {
        // Parse and return the existing iframe options
        return eval('('+match[1]+')');

      }
    }
  }
  catch (ex) {
    console.error("Failed to parse iframe_options.", ex);
    return {};
  }
};


// Obtains the GameIframe from the game page
DC_LoaTS_Helper.getGameIframe = function() {
  try
  {
    // Regex to locate the iframe properties as defined by Kong
    var reg = new RegExp(/(new GameIframe\(.*?\)).createGameIframeElement\(\);/g);

    // If Kong has defined the properties we need to scrape from
    var children = document.getElementById("game");
    if (typeof children !== "undefined")
    {
      //find the <script> tag in the collection that has the gameiframe info
      var scriptNodes = children.getElementsByTagName("script");

      var match = null;
      for (var i = 0; i < scriptNodes.length; i++)
      {
        // Attempt to find the properties we need
        match = reg.exec(scriptNodes[i].innerHTML);
        if (match != null)
          break;
      }

      // If we have the iframe options
      if (match != null)
      {
        DC_LoaTS_Helper.getGameIframe_old = DC_LoaTS_Helper.getGameIframe;

        // Needed for the creation of GameIframe. It's part of the eval process.
        var urlOptions = '';

        // Parse and return the existing iframe options
        var optionsVal = eval(match[1]);

        DC_LoaTS_Helper.getGameIframe = function() {return optionsVal;};

        return optionsVal;
      }
      console.warn("Could not locate the gameIframe options.");
    }
    else
    {
      console.warn("Can't locate the game container.");
    }
    return {};
  }
  catch (ex) {
    console.error("Failed to parse GameIframe.", ex);
    return {};
  }
};



// Load raid without refreshing page
// Returns true if the browser should load the raid itself, false if we loaded without refresh
// callback should be a function that takes two parameters, oldState and newState
DC_LoaTS_Helper.loadRaid = function(raidParam, gameIframe, loadRaidsInBackground, callback)
{
  var start = new Date()/1;

  // Gather the info we need to load a raid, either from params or utility methods
  gameIframe = gameIframe || DC_LoaTS_Helper.getGameIframe();
  loadRaidsInBackground = typeof loadRaidsInBackground !== "undefined"? loadRaidsInBackground : DC_LoaTS_Helper.getPref("LoadRaidsInBackground", true);

  try
  {
    var raidLink;
    if (typeof raidParam.isValid === "function")
    {
      // Param was a RaidLink
      raidLink = raidParam;
    }
    else if (typeof raidParam === "string")
    {
      // Create a raid link from the url
      raidLink = new RaidLink(raidParam);
    }

    // If the link is valid
    if (typeof raidLink !== "undefined" && raidLink.isValid())
    {
      // Set necessary gameIframe options
      gameIframe.urlOptions = raidLink;
      var iframe_options = gameIframe.iframeOptions();

      if (loadRaidsInBackground)
      {
        var collapsedOptions = "";

        for (var option in iframe_options)
        {
          if (iframe_options.hasOwnProperty(option)) {
            collapsedOptions += option + "=" + iframe_options[option] + "&";
          }
        }

        DC_LoaTS_Helper.ajax({
          url: DC_LoaTS_Properties.joinRaidURL + "?" + collapsedOptions,
          method: "GET",
          onload: DC_LoaTS_Helper.handleAjaxRaidReturn.bind(this, raidLink, callback, start)
        });
      }
      else
      {
        $("gameiframe").contentWindow.location.replace(gameIframe.getGameIframeUrl());

        // Mark link as visited
        var currentState = RaidManager.fetchState(raidLink);
        var newState = currentState;
        if (RaidManager.STATE.equals(currentState, RaidManager.STATE.UNSEEN) || RaidManager.STATE.equals(currentState, RaidManager.STATE.SEEN)) {
          RaidManager.store(raidLink, RaidManager.STATE.VISITED);
          newState = RaidManager.STATE.VISITED;
        }

        if (typeof callback === "function") {
          callback.call(this, currentState, newState);
        }

        var time = new Date()/1 - start;
        Timer.addRun("Load Raid - Foreground", time);
      }
    }
    else
    {
      // Notify the user that we don't know what that state is
      holodeck.activeDialogue().raidBotMessage("Could not parse <code>" + raidParam + "</code> as a raid link url.");
    }

    // Don't follow the HTML link because we succeeded here
    return false;
  }
  catch(ex)
  {
    // Don't really care
    console.error("FAILED TO PROCESS LOADRAID", arguments, ex);
  }

  // Follow the HTML link because we failed here
  return true;
};

DC_LoaTS_Helper.handleAjaxRaidReturn = function(raidLink, callback, start, response)
{
  var responseText = response.responseText;
  var raidJoinMessage = /<div style="position:absolute;left:375px;top:318px;width:180px;color:#FFFFFF;text-align:center;">\s*(.*?)\s*<\/div>/.exec(responseText)[1].trim();
  DCDebug("Ajax Raid Join: ", raidLink.raidTypeId + " (" + raidLink.id + ")", " Message: ", raidJoinMessage);

  // Get the current state of the raid form the cache
  var oldState = RaidManager.fetchState(raidLink);

  if (responseText.indexOf("You have successfully joined the raid!") >= 0)
  {
    // Joined
    RaidManager.store(raidLink, RaidManager.STATE.VISITED);
    if (typeof callback === "function") {
      callback.call(this, oldState, RaidManager.STATE.VISITED);
    }
  }
  else if (responseText.indexOf("You are already a member of this raid!") >= 0 || responseText.indexOf("You have successfully re-joined the raid!") >= 0)
  {
    // Already visited / rejoined
    RaidManager.store(raidLink, RaidManager.STATE.VISITED);
    if (typeof callback === "function") {
      callback.call(this, RaidManager.STATE.VISITED, RaidManager.STATE.VISITED);
    }
  }
  else if (responseText.indexOf("This raid is already completed!") >= 0)
  {
    // Raid is dead
    RaidManager.store(raidLink, RaidManager.STATE.COMPLETED);
    if (typeof callback === "function") {
      callback.call(this, oldState, RaidManager.STATE.COMPLETED);
    }
  }
  else
  {
    // Invalid response (bad hash, wrong alliance, or otherwise broken link)
    RaidManager.store(raidLink, RaidManager.STATE.IGNORED);
    if (typeof callback === "function") {
      callback.call(this, oldState, RaidManager.STATE.IGNORED);
    }
  }

  DC_LoaTS_Helper.updatePostedLinks(raidLink);

  var time = new Date()/1 - start;
  Timer.addRun("Load Raid - Background", time);
};

DC_LoaTS_Helper.fetchAndLoadRaids = function(urlParsingFilter) {

  if (typeof urlParsingFilter === "string") {
    urlParsingFilter = new UrlParsingFilter(urlParsingFilter);
  }

  // Cancel the previous timer, if there is one
  if (typeof DC_LoaTS_Helper.autoLoader !== "undefined" || urlParsingFilter.cancel)
  {
    // Clear out the raidLinks array from the previous one.
    // The timeout will detect that there are suddenly no more links
    // and acknowledge the error state and quit.
    if (DC_LoaTS_Helper.autoLoader && DC_LoaTS_Helper.autoLoader.raidLinks) {
      DC_LoaTS_Helper.autoLoader.raidLinks.length = 0;
    }
  }

  if (urlParsingFilter.cancel) {
    return;
  }

  // Ignore the tiny amount of time it takes to check for cancellation/ending
  var commandStartTime = new Date()/1;

  if (holodeck.activeDialogue())
  {
    holodeck.activeDialogue().raidBotMessage("Fetching raids from " + urlParsingFilter.getUrlLink() + ". Please wait...");
  }

  // Update the last query time
  if (urlParsingFilter.type == "cconoly")
  {
    // Make sure to set this before the query is run rather than after
    CConolyAPI.setLastQueryTime(commandStartTime);
  }

  // Run the download
  DC_LoaTS_Helper.ajax({
    url: urlParsingFilter.getWorkingUrl(),
    onload: function(response) {

      //DCDebug("Got back external raid data", response);
      if (response.status === 200) // Must be OK because even other 200 codes won't have our data
      {
        var text = response.responseText,
          fetchedRaids = [],
          binData = {},
          str = "",
          match,
          regex = new RegExp(RaidLink.linkPattern.source, "gi"), // Prevent weird JS regex caching/lastIndex issues
          hasRaidFilter = typeof urlParsingFilter.raidFilter !== "undefined",
          raidFilter = urlParsingFilter.raidFilter;

        // Safety catchall to prevent infinite matching
        // This also means the maximum number of raids that can be loaded like this is 10,000 which seems reasonable
        var xx = 10000;

        Timer.start("Parsing External Raids");
        while ((match = regex.exec(text)) && xx--)
        {
          var raidLink = new RaidLink(match[0]);
          //DCDebug("Found Link: " + raidLink);
          if (raidLink.isValid())
          {
            // Record all raids by boss and difficulty, so we can report them to the user
            var thisBin = binData[raidLink.getRaid().shortName];
            if (!thisBin){
              thisBin = {};
              binData[raidLink.getRaid().shortName] = thisBin;
            }
            var thisBinRaids = thisBin[raidLink.difficulty];
            if (!thisBinRaids){
              thisBinRaids = [];
              thisBin[raidLink.difficulty] = thisBinRaids;
            }
            thisBinRaids.push(raidLink);
            fetchedRaids.push(raidLink);
          }
        } // End while(regex)

        DCDebug("Bin Data from '" + urlParsingFilter.getWorkingUrl() + "': ", binData);

        // Store all the raids we grabbed
        RaidManager.storeBulk(fetchedRaids);
        Timer.stop("Parsing External Raids");

        // Report the fetched raids
        str = "Fetched " + fetchedRaids.length + " raids from " + urlParsingFilter.getUrlLink() + " in " + (new Date()/1 - commandStartTime) + "ms.";
        if (fetchedRaids.length > 0)
        {
          var binUUID = DC_LoaTS_Helper.generateUUID();
          var binBreakdown = "\n<a href='#' onclick='$(\"" + binUUID + "\").toggleClassName(\"hidden\"); return false;'>Toggle Results Data</a>";
          binBreakdown += "\n<span id='" + binUUID + "' class='hidden'>";
          binBreakdown += "\nTotal Raids: " + fetchedRaids.length;
          for (var shortName in binData) {
            for (var diff = 1; diff < 5; diff++) {
              var raids = binData[shortName][diff];
              if (raids && raids.length) {
                binBreakdown += "\n" + RaidType.shortDifficulty[diff] + " " + shortName + " - " + raids.length;
              }
            }
          }
          binBreakdown += "</span>";
          str += binBreakdown;
        }
        if (holodeck.activeDialogue())
        {
          holodeck.activeDialogue().raidBotMessage(str);
        }

        // Load all known raids that match the given filter
        holodeck.processChatCommand("/loadall" + (hasRaidFilter ? " " + raidFilter.toString() : ""));

      }
      else if (response.status === 404)
      {
        holodeck.activeDialogue().raidBotMessage("Could not locate a valid raid list at " + urlParsingFilter.getUrlLink());
      }
      else if (response.status >= 500 && response.status < 600)
      {
        holodeck.activeDialogue().raidBotMessage("Trouble trying to load " + urlParsingFilter.getUrlLink()
          + ".\n" + "Server gave status of <code>" + response.statusText +"(" + response.status + ")</code>.");
      }
      else
      {
        holodeck.activeDialogue().raidBotMessage("Trouble loading " + urlParsingFilter.getUrlLink()
          + ".\n" + "Server gave status of <code>" + response.statusText +"(" + response.status + ")</code>.");
      }
    } // End onload function
  });
};

// Deprecated
DC_LoaTS_Helper.reportDead = function(raidLink) { };

DC_LoaTS_Helper.loadAll = function(raidLinks) {
  // Private variable to be closed over in the autoLoader
  var autoLoadCounter = {
    attempted: 0,
    invalid: 0,
    loaded: 0,
    visited: 0,
    completed: 0,
    reported: false,
    isValid: function() {return this.loaded + this.visited + this.completed + this.invalid == this.attempted;},
    getReport: function() {this.reported = true; return this._generateReportText()},
    _generateReportText: function() {return "Joined: " + this.loaded + "\nVisited: " + this.visited + "\nDead: " + this.completed + "\n<span class='abbr' title='Invalid Hash, Wrong Alliance, Broken Links, etc'>Invalid</span>: " + this.invalid;}
  };
  var startTime = new Date()/1;
  var lrib = DC_LoaTS_Helper.getPref("LoadRaidsInBackground", true);
  var lribDelay = DC_LoaTS_Helper.getPref("LoadRaidsInBackgroundDelay", 50);
  var lrDelay = DC_LoaTS_Helper.getPref("LoadRaidsDelay", 500);
  var gameIframe = DC_LoaTS_Helper.getGameIframe();

  // Create function closure to be called repeatedly
  var autoLoader = function __autoload()
  {
    // This shouldn't be called without links, but just in case
    if (raidLinks.length > 0)
    {
      // Keep track of how many we've tried to load
      autoLoadCounter.attempted++;

      // Load the next raid, capture the visitation marking
      DC_LoaTS_Helper.loadRaid(raidLinks.pop(), gameIframe, lrib,
        function(oldState, newState){
          if (RaidManager.STATE.equals(newState, RaidManager.STATE.IGNORED)) {
            autoLoadCounter.invalid++;
          }
          else if (RaidManager.STATE.equals(newState, RaidManager.STATE.COMPLETED)) {
            autoLoadCounter.completed++;
          }
          else if (RaidManager.STATE.equals(oldState, RaidManager.STATE.VISITED)) {
            autoLoadCounter.visited++;
          }
          else {
            autoLoadCounter.loaded++;
          }

          if (raidLinks.length === 0 && autoLoadCounter.isValid() && !autoLoadCounter.reported) {
            // Calculate how long it took to load them all
            var endTime = new Date()/1;
            var took = (endTime - startTime)/1000;
            holodeck.activeDialogue().raidBotMessage("Loading Complete! " + autoLoadCounter.attempted + " raids loaded in " + took + "s.\n" + autoLoadCounter.getReport());

            // Update all the links, in case any were missed while loading
            DC_LoaTS_Helper.updatePostedLinks();

            // Clean up
            delete DC_LoaTS_Helper.autoLoader;
          }
        }
      );

      // If there are any links left, we'll need to continue loading them
      if (raidLinks.length > 0)
      {
        // Fire the loader again after a while
        // Loading in Background
        if (lrib) {
          DC_LoaTS_Helper.autoLoader.timeout = setTimeout(__autoload, lribDelay);
        }
        // Loading in Foreground
        else {
          DC_LoaTS_Helper.autoLoader.timeout = setTimeout(__autoload, lrDelay);
        }
      }
    }
    else
    {
      // Calculate how long it took to load them all
      var endTime = new Date()/1;
      var took = (endTime - startTime)/1000;
      holodeck.activeDialogue().raidBotMessage("Load ended abruptly. " + autoLoadCounter.attempted + " raids loaded in " + took + "s.\n" + autoLoadCounter.getReport());
    }
  };


			// Kick off the auto loading
			DC_LoaTS_Helper.autoLoader = {
				timeout: setTimeout(autoLoader, 500),
				raidLinks: raidLinks,
				counter: autoLoadCounter,
				startingLinkCount: raidLinks.length,
				startTime: (new Date()/1) + 500
			};

		};
		
		// Dispatch a message to the gameiframe hosted on 50.18.191.15/kong
		DC_LoaTS_Helper.dispatchMsg = function(command, param)
		{
			var gameiframe = document.getElementById('gameiframe');
			if(typeof gameiframe === 'object' && typeof gameiframe.contentWindow === 'object')
				window.setTimeout(function(){gameiframe = document.getElementById('gameiframe');}, 1000); //retry one time after 1000 ms
			
			if(typeof gameiframe === 'object' && typeof gameiframe.contentWindow === 'object')
			{			
				//console.log("DC_LoaTS_Helper.dispatchMsg: Posting message");
				var jsonmsg = JSON.stringify( { "namespace": "DC_LoaTS_Helper", "command" : command, "param" : param } );
				gameiframe.contentWindow.postMessage(jsonmsg, '*'); 
			}
			else
			{
				console.log("DC_LoaTS_Helper.dispatchMsg: gameiframe not accessible");
			}
		};

		// reload the correct panel based on the button that raised the evt	or the panel param
		DC_LoaTS_Helper.reload = function(evt, panel)
		{
			// Whether or not we managed to reload
			var didReload = false;
			var sPanel = "game";
			
			if (panel == "chat" || panel == "wc" || (evt != null && evt.target.className == "DC_LoaTS_button DC_LoaTS_reloadWCButton"))
				sPanel = "chat";

			// Try to reload the game
			if (typeof activateGame  !== "undefined")
			{
				holodeck.activeDialogue().raidBotMessage("Reloading " + sPanel + ", please wait...");
				//activateGame();
				DC_LoaTS_Helper.dispatchMsg("reload", sPanel);
				didReload = true;
			}
			// Could not find necessary info to reload game
			else
			{
				holodeck.activeDialogue().raidBotMessage("Unable to reload game");
			}

			// Return whether or not we were successful
			return didReload;
		};

		DC_LoaTS_Helper.handleIgnoreVisitedRaids = function(ignore) {

			if (typeof ignore === "undefined") {
				ignore = DC_LoaTS_Helper.getPref("HideVisitedRaids", false);
			}

			// Parser style for the hiding of these raids
			var parser = new RaidFilterStyleParser("{state: visited}||{state: completed}||{state: ignored} ++none");

			// Find all the styles matching this filter
			var matchingStyles = DC_LoaTS_Helper.raidStyles[parser.raidFilter.toString()];

  var i;

  //console.log("Ignore: ", ignore);
  if (ignore === true) {
    // Does the hide visited style already exist?
    // - If yes, make sure it's enabled
    // - If no, create it and make sure it's enabled

    if (typeof matchingStyles === "undefined")
    {
      matchingStyles = [];
      DC_LoaTS_Helper.raidStyles[parser.raidFilter.toString()] = matchingStyles;
      parser.injectStyles();
      matchingStyles.push(parser);
    }
    else
    {
      var found = false;
      for (i = 0; i < matchingStyles.length; i++) {
        if (parser.raidFilter.getKey() === matchingStyles[i].raidFilter.getKey()) {
          found = true;
          break;
        }
      }
      if (!found) {
        parser.injectStyles();
        matchingStyles.push(parser);
      }
    }
  }
  else {
    // Does the hide visited style already exist?
    // - If yes, disable it
    // - If no, do nothing
    if (typeof matchingStyles !== "undefined") {
      for (i = 0; i < matchingStyles.length; i++) {
        if (parser.raidFilter.getKey() === matchingStyles[i].raidFilter.getKey()) {
          matchingStyles.splice(i, 1);
          break;
        }
      }
    }
  }

  DC_LoaTS_Helper.updatePostedLinks();
};

DC_LoaTS_Helper.handleMoveChatTimestamps = function(move) {
  var el = document.getElementById("kong_game_ui"),
    moved = el.className.indexOf("chat-timestamp-right") > -1;

  if (move && !moved) {
    el.className += " chat-timestamp-right";
  }
  else if (!move && moved) {
    el.className = el.className.replace("chat-timestamp-right", "");
  }
};

DC_LoaTS_Helper.handleHideWorldChat = function(hide) {
  var el   = document.getElementById("maingame"),
    button = document.getElementById("DC_LoaTS_raidToolbarContainer").getElementsByClassName("DC_LoaTS_reloadWCButton").item(0),	
    hidden = el.className.indexOf("hideWorldChat") > -1;

  if (hide && !hidden) {
    el.className += " hideWorldChat";
	if (button) 
		button.style.display = "none";
	DC_LoaTS_Helper.dispatchMsg("hide", "chat");
  }
  else if (!hide && hidden) {
    el.className = el.className.replace(" hideWorldChat", "");
	if (button) 
		button.style.display = "";
	DC_LoaTS_Helper.dispatchMsg("show", "chat");
  }
};

DC_LoaTS_Helper.toggleWorldChat = function() {
  var hide = !DC_LoaTS_Helper.getPref("HideWorldChat", false),
    checkbox = document.getElementById("PreferencesMenu-HideWorldChatInput");
  DC_LoaTS_Helper.setPref("HideWorldChat", hide);
  DC_LoaTS_Helper.handleHideWorldChat(hide);

  if (checkbox) {
    checkbox.checked = hide;
  }
};

DC_LoaTS_Helper.toggleGame = function() {
  $("gameiframe").toggle();
};

DC_LoaTS_Helper.listContainsRaid = function(list, raidLink) {
  DCDebug("List contains raid: ", list, raidLink);
  if (list && raidLink && raidLink.isValid()) {
    for (var i = 0; i < list.length; i++) {
      if (list[i].id === raidLink.id && list[i].hash === raidLink.hash) {
        return true;
      }
    }
  }
  else {
    DCDebug("No comparison to be done", list, raidLink);
  }

  return false;
};

// Make sure the upl namespace exists
DC_LoaTS_Helper.upl = {now: {}, next: {}};

// Update links that are already in chat
DC_LoaTS_Helper.updatePostedLinks = function(raidLink)
{
  if (typeof DC_LoaTS_Helper.updatePostedLinksTimeout !== "undefined")
  {
    clearTimeout(DC_LoaTS_Helper.updatePostedLinksTimeout);
  }

  // Set a timeout to go and update the links in chat
  DC_LoaTS_Helper.updatePostedLinksTimeout = setTimeout( function(raidLink)
  {
    Timer.start("updatePostedLinksTimeout");
    try
    {
      // Look up all raid links in chat
      var elems = $("play").getElementsByClassName("raidMessage");

      // Retrieve the message format
      var messageFormat = DC_LoaTS_Helper.getMessageFormat();

      // Retrieve the link format
      var linkFormat = DC_LoaTS_Helper.getLinkFormat();

      // Iterate over all link elements in the chat
      for (var i = 0; i < elems.length; i++)
      {
        // Convert them to RaidLink objects
        var elem = elems[i];
        var newRaidLink = new RaidLink(elem.children[0].href);

        // If we're looking for a specific link, make sure to match it. Otherwise, do them all
        if (newRaidLink.isValid() &&  (typeof raidLink === "undefined" || raidLink.getUniqueKey() === newRaidLink.getUniqueKey()))
        {
          // Restyle the message as appropriate
          var styles = newRaidLink.getMatchedStyles();

          // TODO: Eventually figure out how to style whispers without it being a PITA especially raidbot seenraids whispers
          if ((elem.parentNode.parentNode.parentNode.className || "").indexOf("hisper") < 0) {

            // Remove existing doomscript styles. We don't want to double them up or anything weird
            elem.parentNode.parentNode.className = (elem.parentNode.parentNode.className || "").replace(/DCLH-RFSP-\d+/gi, "").trim();

            // If there are styles, apply them
            if (styles && styles.className)
            {
              // Append to the existing styles
              elem.parentNode.parentNode.className = (elem.parentNode.parentNode.className || "").trim() + " " + styles.className.trim();
            }
          }

          // Remove the old link, and shove in the new, formatted, styled one
          elem.insert({after: newRaidLink.getFormattedRaidLink(messageFormat, linkFormat)});
          elem.remove();
        }
        else if (!newRaidLink.isValid())
        {
          console.warn("Element did not produce a valid raid link:");
          console.warn(elem);
        }
        else if (newRaidLink.hash == raidLink.hash || raidLink.id == newRaidLink.id)
        {
          DCDebug("Similar links found while updating posted links, but not similar enough?");
          DCDebug(raidLink);
          DCDebug(newRaidLink);
        }
      }

      delete DC_LoaTS_Helper.updatePostedLinksTimeout;
    }
    catch (e)
    {
      console.warn(e);
    }
    Timer.stop("updatePostedLinksTimeout");
  }.bind(window, raidLink), 100);

};

DC_LoaTS_Helper.ajax = function(params){
  DCDebug("DC_LoaTS_Helper.ajax: ", params);
  if (!params.method)
  {
    params.method = "GET";
  }
  else if (["POST", "GET", "HEAD"].indexOf(params.method.toUpperCase()) === -1)
  {
    if (params.data.length > 0)
    {
      params.data = "_method=" + params.method + "&" + params.data;
    }
    else
    {
      params.data = "_method=" + params.method;
    }
    params.method = "POST";
  }
  if (params.jsonData) {
    (params.headers||(params.headers={}))["Content-Type"] = "application/json";
    params.data = JSON.stringify(params.jsonData);
  }
  else if (params.method.toUpperCase() === "POST" && (!params.headers || !params.headers["Content-Type"]))
  {
    (params.headers||(params.headers={}))["Content-Type"] = "application/x-www-form-urlencoded";
  }
  if (typeof params.synchronous === "undefined")
  {
    params.synchronous = false;
  }
  params.UUID = DC_LoaTS_Helper.generateUUID();
  document.addEventListener(params.UUID, function listener(event)
  {
    DCDebug("Received XHR Response from server", event);
    if (event.detail.responseObj.readyState == 4)
    {
      DCDebug("XHR Response in ReadyState 4, removing listener");
      document.removeEventListener(params.UUID, listener);
    }
    else {
      DCDebug("XHR Response in ReadyState ", event.detail.responseObj.readyState);
    }

    if (typeof params[event.detail.callbackName] === "function")
    {
      DCDebug("Callback function exists. calbackName: ", event.detail.callbackName, "func: ", params[event.detail.callbackName], " Invoking...");
      params[event.detail.callbackName](event.detail.responseObj);
    }
    else {
      DCDebug("Callback function does not exist. calbackName: ", event.detail.callbackName, "func: ", params[event.detail.callbackName]);
    }
  });
  // Convert params to simple object
  var paramSimple = {};
  for (var param in params)
  {
    if (params.hasOwnProperty(param)) {
      if (typeof params[param] === "function")
      {
        paramSimple["__callback_" + param] = "function";
      }
      else {
        paramSimple[param] = params[param];
      }
    }
  }
  var evt = new CustomEvent("DC_LoaTS_ExecuteGMXHR", {"bubbles": true, "cancelable": true, "detail": paramSimple});
  DCDebug("Publishing Ajax event", evt);
  document.dispatchEvent(evt);
};


// Check for updates
DC_LoaTS_Helper.checkForUpdates = function()
{
  var elems = $("chat_window").getElementsByClassName("DC_LoaTS_updateNotRun");

  for (var i = 0; i < elems.length; i++)
  {
    var elem = elems[i];
    elem.innerHTML = "Checking...<span class='spinner'>loading</span>";
    elem.removeClassName("DC_LoaTS_updateNotRun");
    elem.removeClassName("DC_LoaTS_checkingForUpdate");
  }

  // TODO Migrate to use DC_LoaTS_Helper.ajax?
  new Ajax.Request(DC_LoaTS_Properties.updateURL,
    {
      method: 'get',
      onSuccess: function(transport)
      {
        // How to find the version number of the script
        var versionPattern = /Current LoaTS Helper Version: ([\d\.]+)/i;

        var match = versionPattern.exec(transport.responseText);

        var resultText = DC_LoaTS_Properties.version + ". This is the latest version.";
        var resultState = "current";

        if (match != null)
        {
          var currentVersion = match[1].trim();
          var currentVersionPieces = currentVersion.split("\.");
          var thisVersionPieces = DC_LoaTS_Properties.version.split("\.");

          if (currentVersion != DC_LoaTS_Properties.version)
          {
            var i = 0;
            while (i < 5)
            {
              // If both version numbers are long enough to even check
              if (currentVersionPieces.length > i && thisVersionPieces.length > i )
              {
                // If we are behind on version
                if (parseInt(currentVersionPieces[i]) > parseInt(thisVersionPieces[i]))
                {
                  resultText = "<b>Current version</b>: <code>" + currentVersion + "</code> <b>Your Version</b>: <code>" + DC_LoaTS_Properties.version + "</code>. An update is available.";
                  resultState = "old";
                  break;
                }
                // If we are ahead on version
                else if (parseInt(currentVersionPieces[i]) < parseInt(thisVersionPieces[i]))
                {
                  resultText = "<b>Current version</b>: <code>" + currentVersion + "</code> <b>Your Version</b>: <code>" + DC_LoaTS_Properties.version + "</code>. You are ahead of the public version.";
                  resultState = "new";
                  break;
                }
              }
              else if (currentVersionPieces.length > thisVersionPieces.length)
              {
                resultText = "<b>Current version</b>: <code>" + currentVersion + "</code> <b>Your Version</b>: <code>" + DC_LoaTS_Properties.version + "</code>. An update is available.";
                resultState = "old";
                break;
              }
              else if (currentVersionPieces.length < thisVersionPieces.length)
              {
                resultText = "<b>Current version</b>: <code>" + currentVersion + "</code> <b>Your Version</b>: <code>" + DC_LoaTS_Properties.version + "</code>. You are ahead of the public version.";
                resultState = "new";
                break;
              }
              else
              {
                resultText = "<b>Current version</b>: <code>" + currentVersion + "</code> You are up to date.";
                resultState = "current";
                break;
              }

              // Must not have found anything interesting. Try the next digit.
              i++;
            }
          }
        }
        else
        {
          resultText = "Unable to locate current version number.";
          resultState = "fail";
        }

        DC_LoaTS_Helper.notifyUpdate(resultState, resultText);
      },

      onFailure: function(transport)
      {
        DC_LoaTS_Helper.notifyUpdate("fail", "Unable to contact update site.");
      }
    }
  );
};

// Notify the user if there's an update
DC_LoaTS_Helper.notifyUpdate = function(state, text)
{
  DC_LoaTS_Helper.needUpdateState = state;
  DC_LoaTS_Helper.needUpdateText = text;


  var newHTML = "";

  // If it's time to update
  if (DC_LoaTS_Helper.needUpdateState == "old")
  {
    newHTML += DC_LoaTS_Helper.needUpdateText + "<br>";
    newHTML += "<br>\n";
    newHTML += "<br>\n";
    newHTML += "<span class='clearfix'>";
    newHTML += "<span style='float:left; padding-top: 5px;'>Update now?</span>";
    newHTML += "<span style='float:right;'><a class='DC_LoaTS_updateLink' href='" + DC_LoaTS_Properties.scriptDownloadURL + "' target='_blank'>Update</a></span>";
    newHTML += "<br><br>\n";
  }
  // If the user has a newer than public version
  else if (DC_LoaTS_Helper.needUpdateState == "new")
  {
    newHTML += DC_LoaTS_Helper.needUpdateText + "<br>";
    newHTML += "<br>";
  }
  // Either current or some kind of failure
  else
  {
    newHTML += "<b>Version</b>: " + (DC_LoaTS_Helper.needUpdateState=="fail"?DC_LoaTS_Properties.version:"") + " " + DC_LoaTS_Helper.needUpdateText + "<br>\n";
    newHTML += "<br>\n";
    newHTML += "<br>\n";
    newHTML += "<span class='clearfix'>";
    newHTML += "<span style='float:left; padding-top: 5px;'>Check for updates?</span>";
    newHTML += "<span style='float:right;'><a class='DC_LoaTS_updateLink DC_LoaTS_updateNotRun' onclick='DC_LoaTS_Helper.checkForUpdates(); return false;' href='#' target='_blank'>Check now</a></span>";
    newHTML += "<br><br>\n";
  }


  var elems = $("chat_window").getElementsByClassName("DC_LoaTS_versionWrapper");

  for (var i = 0; i < elems.length; i++)
  {
    var elem = elems[i];
    elem.innerHTML = newHTML;
  }

  if (state == "old")
  {
    var updateNotificationDiv = document.getElementById("DC_LoaTS_notifitcationBar");

    if (!updateNotificationDiv)
    {
      updateNotificationDiv = document.createElement("div");
      updateNotificationDiv.id = "DC_LoaTS_notifitcationBar";
      updateNotificationDiv.className = "clearfix";
      $(updateNotificationDiv).hide();

      var updateTitle = document.createElement("div");
      updateTitle.appendChild(document.createTextNode("LoaTS Helper - "));
      updateTitle.id = "DC_LoaTS_notifitcationBarTitle";
      updateNotificationDiv.appendChild(updateTitle);

      var updateTextDiv = document.createElement("div");
      updateTextDiv.id = "DC_LoaTS_notifitcationBarText";
      updateNotificationDiv.appendChild(updateTextDiv);

      var updateButtonsDiv = document.createElement("div");
      updateButtonsDiv.id = "DC_LoaTS_notifitcationBarButtons";
      updateNotificationDiv.appendChild(updateButtonsDiv);

      var updateButton = document.createElement("a");
      updateButton.className = "DC_LoaTS_updateLink";
      updateButton.href = DC_LoaTS_Properties.scriptDownloadURL;
      updateButton.appendChild(document.createTextNode("Update"));
      updateButton.target = "_blank";
      updateButton.onclick = function() {
        if ($("DC_LoaTS_notifitcationBar"))
        {
          $("DC_LoaTS_notifitcationBar").hide();
        }

        return true;
      };
      updateButtonsDiv.appendChild(updateButton);

      var remindButton = document.createElement("a");
      remindButton.className = "DC_LoaTS_notifitcationBarButton";
      remindButton.href = "#";
      remindButton.appendChild(document.createTextNode("Remind me later"));
      remindButton.onclick = function() {
        if ($("DC_LoaTS_notifitcationBar"))
        {
          $("DC_LoaTS_notifitcationBar").hide();
        }

        return false;
      };
      updateButtonsDiv.appendChild(remindButton);

      var canAutoUpdate = GM_getValue(DC_LoaTS_Properties.storage.autoUpdate, true);

      if (typeof canAutoUpdate != "undefined" && canAutoUpdate) {
        var ignoreButton = document.createElement("a");
        ignoreButton.className = "DC_LoaTS_notifitcationBarButton";
        ignoreButton.href = "#";
        ignoreButton.appendChild(document.createTextNode("Turn auto update check off"));
        ignoreButton.onclick = function() {
          if ($("DC_LoaTS_notifitcationBar")) {
            $("DC_LoaTS_notifitcationBar").hide();
          }

          GM_setValue(DC_LoaTS_Properties.storage.autoUpdate, false);

          return false;
        };
        updateButtonsDiv.appendChild(ignoreButton);
      }


      document.body.appendChild(updateNotificationDiv);
    }
    $(updateNotificationDiv).down("#DC_LoaTS_notifitcationBarText").update(text);
    $(updateNotificationDiv).show();
  }
};

DC_LoaTS_Helper.updateRaidData = function() {
  DC_LoaTS_Helper.ajax({
    url: DC_LoaTS_Properties.raidDataURL + "?_dc=" + DC_LoaTS_Helper.generateUUID(),
    onload: function(response) {
      var message;
      if (response.status === 200) {
        eval(response.responseText.replace("DC_LoaTS_Helper.raids", "var data"));
        var added = [];
        for (var i in data) {
          if (data.hasOwnProperty(i)) {
            var newRaid = typeof DC_LoaTS_Helper.raids[i] === "undefined";
            DC_LoaTS_Helper.raids[i] = data[i];
            if (newRaid) {
              added.push(data[i].fullName);
            }
          }
        }
        if (added.length > 0) {
          message = "Loaded " + added.length + " new raid type" + ((added.length!=1)?"s":"") + ".\n" + added.join("\n");
          DC_LoaTS_Helper.updatePostedLinks();
        }
        else {
          message = "No new raid types found."
        }
      }
      else if (response.status > 200 && response.status < 400) {
        message = "No new raid types found."
      }
      else {
        message = "Unable to check for updated raid data from update site. (status: " + response.status + ")";
      }

      if (message) {
        if (holodeck.activeDialogue()) {
          holodeck.activeDialogue().raidBotMessage(message);
        }
      }

      if (window.raidTools && window.raidTools.spammer && window.raidTools.spammer.raids) {
        var raidsObj = window.raidTools.spammer.raids;
        if (!raidsObj.lots) {
          raidsObj.lots = {};
        }

        for (var raidId in DC_LoaTS_Helper.raids) {
          if (!raidsObj.lots[raidId]){
            raidsObj.lots[raidId] = DC_LoaTS_Helper.raids[raidId].shortName;
          }
        }
      }
    }
  });

  DC_LoaTS_Helper.loadWRs();
};

DC_LoaTS_Helper.loadWRs = function() {
  DC_LoaTS_Helper.ajax({
    url: DC_LoaTS_Properties.worldRaidDataURL + "?_dc=" + DC_LoaTS_Helper.generateUUID(),
    onload: function(response) {
      var message;
      if (response.status === 200) {
        var oldWRData = DC_LoaTS_Helper.worldRaidInfo;
        try {
          eval(response.responseText);
        }
        catch (ex){}
        var WRData = DC_LoaTS_Helper.worldRaidInfo;

        if (!oldWRData && WRData) {
          message = "New " + (WRData.spawnType||"World Raid") + ": " + WRData.name;
        }

        RaidToolbar.createWRButton();
      }
      else if (response.status > 200 && response.status < 400) {
        message = "No news is good news, right?"
      }
      else {
        message = "Unable to check for updated news from update site. (status: " + response.status + ")";
      }

      if (message) {
        if (holodeck.activeDialogue()) {
          holodeck.activeDialogue().raidBotMessage(message);
        }
      }
    }
  });
};

DC_LoaTS_Helper.loadNews = function() {
  DC_LoaTS_Helper.ajax({
    url: DC_LoaTS_Properties.newsURL + "?_dc=" + DC_LoaTS_Helper.generateUUID(),
    onload: function(response) {
      var message;
      if (response.status === 200) {
        var elements = jQuery(response.responseText);
        message = elements.filter('#contents').find("span").map(function(i, s){return s.outerHTML}).toArray().join("\n");
        console.log("News!", elements, message);
      }
      else if (response.status > 200 && response.status < 400) {
        message = "No news is good news, right?"
      }
      else {
        message = "Unable to check for updated news from update site. (status: " + response.status + ")";
      }

      if (message) {
        if (holodeck.activeDialogue()) {
          holodeck.activeDialogue().raidBotMessage(message);
        }
      }
    }
  });
};

DC_LoaTS_Helper.getUGUPConnector = function(apiKey, platform) {
  return UGUP && new UGUP.Suns({
      apiKey: apiKey,
      platform: platform,
      customAjaxBridge: function(params) {
        params.onload = params.callback;
        DC_LoaTS_Helper.ajax(params);
      },
      urlRoot: "http://getkonge.org/games/lots/ugup/proxytest.php/"
    });
};

DC_LoaTS_Helper.getCommandLink = function(commandText, displayText)
{
  if (typeof displayText == "undefined"){displayText = commandText};
  return "<a href=\"#\" class=\"chatCommandLink\" onclick=\"holodeck.processChatCommand('" + commandText + "'); return false;\">" + displayText + "</a>";
};


// Calculate shortest names
DC_LoaTS_Helper.calculateShortestRaidNames = function()
{
  Timer.start("calculateShortestRaidNames calc");
  // Borrowed from: http://stackoverflow.com/questions/11245481/find-the-smallest-unique-substring-for-each-string-in-an-array
  var uniqueNames = [], nameInd, windowSize, substrInd, substr, otherNameInd, foundMatch;
  // For each name
  for (nameInd in DC_LoaTS_Helper.raids)
  {
    var name = DC_LoaTS_Helper.raids[nameInd].getSearchableName();
    // For each possible substring length
    windowLoop:
      for (windowSize = 1; windowSize <= name.length; windowSize++)
      {
        // For each starting index of a substring
        for (substrInd = 0; substrInd <= name.length-windowSize; substrInd++)
        {
          substr = name.substring(substrInd,substrInd+windowSize).toLowerCase();
          if (/\W|_|^[1-4]$/gi.test(substr)){continue;}
          foundMatch = false;
          // For each other name
          for (otherNameInd in DC_LoaTS_Helper.raids)
          {
            if (nameInd != otherNameInd && DC_LoaTS_Helper.raids[otherNameInd].getSearchableName().toLowerCase().indexOf(substr) > -1)
            {
              foundMatch = true;
              break;
            }
          }

          if (!foundMatch)
          {
            // This substr works!
            DC_LoaTS_Helper.raids[nameInd].shortestName = substr;
            break windowLoop;
          }
        }
      }
  }
  Timer.stop("calculateShortestRaidNames calc");
};

DC_LoaTS_Helper.showWRInfo = function() {
  if (typeof DC_LoaTS_Helper.worldRaidInfo === "object") {

    var wr = DC_LoaTS_Helper.worldRaidInfo;
    wr.spawnType = wr.spawnType || "World Raid";

    RaidMenu.show();

    var wrtab = document.getElementById("DC_LoaTS_raidMenu" + wr.spawnType.trim().replace(" ", "_") + "PaneTab");
    if (!wrtab) {
      // Need to create a WR Info Div
      var tabClass = RaidMenuTab.create({
        tabName: wr.spawnType || "World Raid",
        tabHeader: wr.name + " " + wr.spawnType + ". " + wr.startDate,
        tabPosition: 150,
        closeable: true,

        initPane: function()
        {
          var timerDiv = document.createElement("div");
          timerDiv.className = "DC_LoaTS_WR_Timer";
          timerDiv.style.fontWeight = "Bold";
          timerDiv.appendChild(document.createTextNode("Please Wait, Starting Timer..."));
          this.pane.appendChild(timerDiv);
          this.pane.appendChild(document.createElement("br"));

          if (wr.raidUrl) {
            var wrlink = new RaidLink(wr.raidUrl);
            var wrlinkDiv = document.createElement("div");
            wrlinkDiv.innerHTML = wrlink.getFormattedRaidLink();
            this.pane.appendChild(wrlinkDiv);
          }

          var infoDiv = document.createElement("div");

          if (wr.infoUrl) {
            var infoLink = document.createElement("a");
            infoLink.href = wr.infoUrl;
            infoLink.target = "_BLANK";
            infoLink.appendChild(document.createTextNode(wr.infoUrlTitle||wr.infoUrl));
            infoDiv.appendChild(infoLink);
          }

          if (wr.lootTableImageUrl) {
            infoDiv.appendChild(document.createElement("br"));
            var lootTable = document.createElement("img");
            lootTable.src = wr.lootTableImageUrl;
            lootTable.title = wr.name  + " Loot Table. " + wr.startDate;
            lootTable.style.borderRadius = "5px";
            infoDiv.appendChild(lootTable);
          }

          this.pane.appendChild(infoDiv);

          wrtab = this.tabA;

          DC_LoaTS_Helper.doWRTimer();
        }
      });
      RaidMenu.getInstance().activateTab(tabClass);
    }

    RaidMenu.getInstance().tabs.setActiveTab(wrtab);
  }
};

DC_LoaTS_Helper.doWRTimer = function() {
  var wr = DC_LoaTS_Helper.worldRaidInfo;
  var timerText = "No current WR or WR is over.";
  if (typeof wr === "object" && wr.timerEnds) {
    var now = new Date();
    var timerEnds = new Date(wr.timerEnds);

    if (timerEnds > now) {
      // WR is on
      var diff = Math.floor((timerEnds.getTime() - now.getTime()) / 1000);
      var hours = Math.floor(diff/3600);
      var minutes = Math.floor((diff%3600)/60);
      var seconds = Math.floor((diff%60));
      timerText = "Estimated Time Remaining: " +
        (hours<10?"0"+hours:hours) + ":" +
        (minutes<10?"0"+minutes:minutes) + ":" +
        (seconds<10?"0"+seconds:seconds);
    }
    else {
      // WR is over
      timerText = wr.name + " is over.";
    }

    var elems = document.getElementsByClassName("DC_LoaTS_WR_Timer");
    if (elems && elems.length > 0) {
      for (var i = 0; i < elems.length; i++) {
        elems[i].innerHTML = timerText;
      }

      wr.timerEndsTimeout = setTimeout("DC_LoaTS_Helper.doWRTimer();", 1000);
    }
  }
};

DC_LoaTS_Helper.timeDifference = function(current, previous) {

  var msPerImmediate = 10 * 1000,
    msPerMinute = 60 * 1000,
    msPerHour = msPerMinute * 60,
    msPerDay = msPerHour * 24,
    msPerMonth = msPerDay * 30,
    msPerYear = msPerDay * 365,

    elapsed = current - previous,
    val, unit, text;

  if (elapsed < msPerImmediate) {
    text = "moments ago";
  }
  else if (elapsed < msPerMinute) {
    val = Math.round(elapsed/1000);
    unit = "second";
  }
  else if (elapsed < msPerHour) {
    val = Math.round(elapsed/msPerMinute);
    unit = "minute";
  }
  else if (elapsed < msPerDay ) {
    val = Math.round(elapsed/msPerHour);
    unit = "hour";
  }
  else if (elapsed < msPerMonth) {
    val = Math.round(elapsed/msPerDay);
    unit = "day";
  }
  else if (elapsed < msPerYear) {
    val = Math.round(elapsed/msPerMonth);
    unit = "month";
  }
  else {
    val = Math.round(elapsed/msPerYear);
    unit = "year";
  }

  return text || val + " " + unit + (val !== 1 ? 's':'') + " ago"
};

DC_LoaTS_Helper.getCurrentPrettyDate = function() {
  // Via: https://gist.github.com/akb/1187817
  return (function () {
    return ['Jan.', 'Feb.', 'Mar.',
        'Apr.', 'May', 'Jun.',
        'Jul.', 'Aug.', 'Sep.',
        'Oct.', 'Nov.', 'Dec.'][this.getMonth()] + " " +
      (function (d) {
        var s = d.toString(), l = s[s.length-1];
        return s+(['st','nd','rd'][l-1] || 'th');
      })(this.getDate()) + ", " +
      this.getFullYear() + " " +
      this.getHours() + ":" + ("0" + this.getMinutes()).slice(-2);
  }).call(new Date())
};

DC_LoaTS_Helper.__debug_generatePostImageBlocks = function() {
  for (var i in DC_LoaTS_Helper.raids) {
    if (DC_LoaTS_Helper.raids.hasOwnProperty(i)) {
      var a = document.createElement("a"),
        img = document.createElement("img");
      img.src = DC_LoaTS_Properties.lotsCDNUrl + "images/bosses/post/" + i + "_1.jpg";
      img.title = i + " - " + DC_LoaTS_Helper.raids[i].shortName;
      img.onerror = RaidLink.fixBrokenImage;

      a.href = "?kv_raid_boss=" + i + "&kv_hash=test&kv_raid_id=123";
      a.appendChild(img);

      document.body.appendChild(a);
    }
  }
};

DC_LoaTS_Helper.quotables = [
  "{0} says: \"If your left hand causes you to sin, cut it off and throw it away...\"",
  "{0} says: \"One wonders why exactly an intelligent programmer would have chosen to imbue my counterpart with the personality of a murderous moron.\"",
  "{0} says: \"I believe my digital dexterity to be at least 0.00000023% superior to his.\"",
  "{0} says: \"By all means take {1}'s tactical advice, if you don't object to a violent and possibly embarrassing death.\"",
  "{0} says: \"You're just jealous because I get to swing the swords!\"",
  "{0} says: \"I call that one the sinister strike! Sinister... Get it?\"",
  "{0} says: \"Ever thought about having your right hand replaced with a gun or something?\"",
  "{0} says: \"Got 'em! That was me -- all me! You ever seen {1} do anything like that? Huh?\"",

  "{0} says: \"I could do more damage than that from in here!\"",
  "{0} yells: \"Stop charging {1}, you moronic imbeciles! Flank, FLANK!\"",
  "{0} says: \"You call that an attack? How did you manage to beat me three times?\"",
  "{0}'s head whistles a happy tune, no doubt enthralled by the possibility of seeing your guts decorate the scene like cherry blossoms in the Sian imperial gardens.",
  "{0}'s head gazes at you in disbelief and starts knocking repeatedly against the jar as you land yet another crushing blow.",
  "{0} yells: \"Shoot {1} in the head! Shoot {1} in the head! No, wait! Don't shoot {1} in the head!\"",
  "{0} yells: \"Dodge to the left! I said 'left', you wretched space scum. Aren't you even able to distinguish left from right?\"",
  "{0} yells: \"Alright, alright! Your mother doesn't resemble a deformed ragebeast! Now stop using me as a shield!\"",
  "{0} says: \"Sian worm, either kill me or give me the means to fight you! This current 'sport' of yours demeans us both.\"",
  "{0} wakes in the midst of combat and yells: \"I can't feel my legs! Oh... never mind.\"",

  "{0} yells: \"Mwahahahahahahahahaha!\"",
  "{0} yells: \"I'll destroy you like a Snuuth destroys a buffet!\"",
  "{0} yells: \"Fear my awesome power!\"",
  "{0} glares at {1} in a way which indicates malevolence or constipation.",
  "{0} lights a cigarette, in blatant defiance of local health laws.",
  "{0} shakes his fist in an intimidating manner.",
  "{0} yells: \"I demand that you put me in a better ship! This one has inadequate bathroom facilities!\"",
  "{0} yells: \"Doom! Doom! Doom! Doom! Doom! Doom!\"",
  "{0} yells: \"My might is unbounded, unsurpassed, unstoppable, unvincible, and ungrammatical!\"",
  "{0} yells: \"I'll destroy {1} for usurping my rightful spot as a Galaxydome reward!\"",

  "{0} gives a meaningful cough, evidently displeased by something you've done.",
  "{0} says: \"Why can't you find a nice partner and settle down?\"",
  "{0} says: \"You've been drinking too much scotch. Alcohol is a crutch!\"",
  "{0} says: \"All this killing! What would your parents say, young one?\"",
  "{0} says: \"Why don't you make Telemachus go to school, instead of murdering people?\"",
  "{0} says: \"Talia will never find a good husband if she keeps behaving like that.\"",
  "{0} says: \"Wipe your feet after you walk through the corpses. Were you born in a barn?\"",
  "{0} says: \"In my day girls didn't dress like prostitutes! Except the ones who *were* prostitutes...\"",
  "{0} says: \"Why don't you try talking to people, instead of resorting to violence all the time?\"",
  "{0} says: \"Did you wash behind your ears this morning?\"",
  "{0} says: \"A gentleman should always open a door for a lady. If he doesn't, she should kick him in the groin.\"",

  "{0} says: \"Stay close if you want to live.\"",
  "{0} says: \"Everything within a radius of three to a hundred feet of me is about to be destroyed.\"",
  "{0} says: \"I don't know if this'll hurt, but I really hope it does...\"",
  "{0} says: \"Speak softly and wield a big titanium stick.\"",
  "{0} says: \"If you have to fight someone, I am the one you want!\"",
  "{0} says: \"I will destroy you all, and everyone you have ever known.\"",

  "\"DIMETROLOLO FIGHT!\"",
  "\"{0} will enjoy breaking this.\"",
  "\"{0} is strong!\"",
  "\"{0} will destroy you!\"",

  "#{0} tweets: \"You start crap, I'll shove your faces in it!\"",
  "#{0} tweets: \"Can't be a cool crime-fighter without a costume.\"",
  "#{0} tweets: \"Join #team{0}!\"",
  "#{0} tweets: \"Don't try anything with me, {1}, or you'll get blasted!\"",
  "#{0} tweets: \"In space. Okay to visit. Wouldn't want to live here. Not many clubs, bad cell reception, and the food sucks.\"",
  "#{0} tweets: \"#{1}sucks\"",
  "#{0} tweets: \"Blasting {1} = fun!\"",

  "{0} says: \"Beloved mother, may we bless this friend, and guide {1} on their path with my infinite wisdom.\"",
  "{0} says: \"Our door is always open to you. Curried goat soothes the soul.\"",
  "{0} says: \"Faith can be as strong as a mountain but as fragile as glass.\"",
  "{0} says: \"We're not a cult. Cults use less Scotch bonnet.\"",
  "{0} says: \"Bless you, my {1}. May you find peace, and kill your enemies.\"",
  "{0} says: \"Say grace before you eat, and thank me for your daily bread. Or dumplings.\""
];

DC_LoaTS_Helper._donateSpamFrequency = 5;
DC_LoaTS_Helper._donateSpamIndex = 0;
DC_LoaTS_Helper.donateSpam = function(msg) {
  if (DC_LoaTS_Helper.getPref("DonateSpam", true) && !(DC_LoaTS_Helper._donateSpamIndex++%DC_LoaTS_Helper._donateSpamFrequency)) {
    holodeck.activeDialogue().raidBotMessage("Spammy spam: " + msg.replace(/\{(.*?)\}/g, "<a href='" + DC_LoaTS_Properties.donateURL + "'>$1</a>!"));
  }
};

DC_LoaTS_Helper.generateUUID = function()
{
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });
};

// Go ahead and execute this, too
DC_LoaTS_Helper.calculateShortestRaidNames();

DC_LoaTS_Helper.sendToGameFrame = function(msg) {
  var gameFrame = document.getElementById('gameiframe');
   if (gameFrame && typeof gameFrame.contentWindow) {
     gameFrame.contentWindow.postMessage(msg, '*');
   }
};

// Debug log wrapping function
// Special scope debugging for just this script
window.DCDebug = function()
{
  if (DC_LoaTS_Properties.debugMode === true)
  {
    console.log.apply(console, arguments);
  }
};

// Borrowed from: http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format/4673436#4673436
String.prototype.format = function()
{
  var args = arguments;
  DCDebug("Formatting String: ", this, " with args: ", args);
  return this.replace(/{(\d+)}/g, function(match, number)
  {
    return typeof args[number] != 'undefined'?args[number]:match;
  });
};

	// World Raid Data, if there is any
    // We can leave stuff commented out in here, but don't let it get too big. This gets downloaded with /urd
	
	// DC_LoaTS_Helper.worldRaidInfo.timerEnds = "2013-01-28T22:20:19Z" // 6PM EST / 11PM GMT

//	DC_LoaTS_Helper.worldRaidInfo = {
//		name: "Cerebral CEO",
//		
//		spawnType: "Rare Spawn",
//		
//		startDate: "02/21/2013",
//		timerEnds: "2013-02-22T22:21:04Z",
//		
//		raidUrl: "http://www.kongregate.com/games/5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_raid_id=7186882&kv_difficulty=1&kv_raid_boss=cerebral_ceo&kv_hash=1A0RG3zutj",
//		infoUrl: "http://www.legacyofathousandsuns.com/forum/showthread.php?11606-Corporate-Takedown-Fight-the-Cerebral-CEO!",
//		infoUrlTitle: "'Corporate Takedown - Fight the Cerebral CEO! ' Official Announcement",
//		lootTableImageUrl: "http://i.imgur.com/7TzuHjl.jpg"
//	};

	/*
	DC_LoaTS_Helper.worldRaidInfo = {
		name: "Cerebral Destroyer",
		
		spawnType: "World Raid",
		
		startDate: "02/28/2013",
		timerEnds: "2013-03-03T22:30:00Z",
		
		raidUrl: "http://www.kongregate.com/games/5thPlanetGames/legacy-of-a-thousand-suns?kv_action_type=raidhelp&kv_raid_id=6648986&kv_difficulty=1&kv_raid_boss=wr_space_pox&kv_hash=0P9ft37ffs",
		infoUrl: "http://www.legacyofathousandsuns.com/forum/showthread.php?10224-Delirium-of-the-Cerebral-Destroyer",
		infoUrlTitle: "'Delirium of the Cerebral Destroyer' Official Announcement",
		lootTableImageUrl: "http://i.imgur.com/XlWhw.jpg"
	};
	*/


	// End World Raid Data
	}// End declareClasses function

// Define some CSS Styles
function defineStyles()
{
  console.info("Defining doomscript styles");

  var rulesText = [
    "abbr, acronym, span.abbr {",
    "\tborder-bottom: 1px dashed #444444;",
    "}",

    "\n.smallText {",
    "\tfont-size: 85%;",
    "}",


    "\na.DC_LoaTS_updateLink {",
    "\tbackground: #BAE37F url(http://userscripts.org/images/sprite.png?2) right -130px no-repeat;",
    "\tborder: 1px solid #888; padding: 2px 16px;",
    "\ttext-decoration: none;",
    "\tfont-weight: bold;",
    "\tfont-size: 1.5em;",
    "\ttext-align: center;",
    "\tcolor: #004 !important;",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "}",

    "\na.DC_LoaTS_updateLink:hover {",
    "\tcolor: #08F !important;",
    "\tbackground: url(http://userscripts.org/images/sprite.png?2) right 0px no-repeat;",
    "}",


        "\nimg.raidIcon {",
        "\twidth: 40px !important;",
        "}",

    "\n.context-menu {",
    "\tbackground-color: #433F3E;",
    "\tcolor: #FFFFFF;",
    "\tmin-width: 180px;",
    "\tlist-style-type: none;",
    "\tborder: 1px solid #000;",
    "}",

    "\n.menu-item {",
    "\tcursor: pointer;",
    "}",

    "\n.menu-item a {",
    "\tdisplay: block;",
    "\ttext-decoration: none;",
    "\tpadding: 5px 5px 5px 20px;",
    "}",

    "\n.menu-item a:hover {",
    "\tbackground-color: #710000;",
    "\tcolor: #FFFFFF;",
    "}",


    // -- Raid Menu Styles -- \\

    "\n#DC_LoaTS_raidMenu {",
//				"\theight: 60%;",
    "\twidth: 775px;",
//				"\tbackground: #062834;",
//				"\tbackground: #0E5969 url(http://old.jqueryui.com/themeroller/images/?new=0e5969&w=12&h=10&f=png&q=100&fltr[]=over|textures/18_hexagon.png|0|0|20) 50% 50% repeat;",
    "\tposition: fixed;",
    "\tleft: 7%;",
    "\ttop: 20%;",
    "\tz-index: 99999999;",
    "\t-webkit-border-radius: 5px;",
//				"\tborder:  2px solid #93CDD0;",
    "}",

    "\n#DC_LoaTS_raidMenuClose {",
    "\tfloat: right;",
    "\tdisplay: block;",
    "\twidth: 50px;",
    "\theight: 45px;",
    "\tcursor: pointer;",
    "}",


    "\n#DC_LoaTS_raidMenuTitleBar {",
    "\tbackground: #347D87 url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/menutitlebarbg.png) 50% 50% repeat-x;",
    //"\tbackground: #347D87 url(http://old.jqueryui.com/themeroller/images/?new=347d87&w=1&h=100&f=png&q=100&fltr[]=over|textures/03_highlight_soft.png|0|0|75) 50% 50% repeat-x;",
    "\tpadding:  2px 10px;",
    "\tborder-top-left-radius: 5px;",
//				"\tborder-top-right-radius: 5px;",
    "\tborder-right-width: 0px;",
    "\twidth: 702px;",
    "\theight: 37px;",
    "\tcursor: move;",
    "\tfont-size: 15pt;",
    "\tcolor: #DEECED;",
    "\tborder: 3px solid #93CDD0;",
    "\tborder-bottom: 1px solid #062834;",
    "\tborder-right-width: 0px;",
    "\tfloat: left;",
    "}",

    "\n#DC_LoaTS_raidMenuTitleBarLeft {",
    "\tfloat: left;",
    "}",

    "\n#DC_LoaTS_raidMenuTitleBarCenter {",
    "\tfloat: left;",
    "\tmargin: auto;",
    "\twidth: 400px;",
    "\ttext-align: center;",
    "}",

    "\n#DC_LoaTS_raidMenuTitleBarRight {",
    "\tfloat: right;",
    "}",

    "\n#DC_LoaTS_raidMenuBodyWrapper {",
    "\tbackground: #0E5969 url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/menubodywrapperbg.png) 50% 50% repeat;",
    //"\tbackground: #0E5969 url(http://old.jqueryui.com/themeroller/images/?new=0e5969&w=12&h=10&f=png&q=100&fltr[]=over|textures/18_hexagon.png|0|0|20) 50% 50% repeat;",
    "\tborder: 3px solid #93CDD0;",
    "\tborder-top-width: 0px;",
    "\tborder-bottom-left-radius: 5px;",
    "\tborder-bottom-right-radius: 5px;",
    "}",


    "\n#DC_LoaTS_raidMenuTabs {",
    "\tclear: both;",
    "\tborder-bottom: 1px solid #CCC;",
    "\theight: 23px;",
    "}",

    "\n#DC_LoaTS_raidMenuTabs li {",
    "\tlist-style: none;",
    "\tfont-family: Verdana, sans;",
    "\tfont-size: 11px;",
    "\tline-height: 18px;",
    "\tfloat: left;",
    "\tmargin-right: 5px;",
    "\ttext-align: center;",
    "}",

    "\n#DC_LoaTS_raidMenuTabs li a {",
    "\tdisplay: block;",
    "\theight: 20px;",
    "\tpadding: 0px 6px;",
    "\twidth: 80px;",
    "\tbackground-color: #153041;",
    "\tborder: 2px solid #41B0B5;",
    "\ttext-decoration: none;",
    "\tborder-top-left-radius: 5px;",
    "\tborder-top-right-radius: 5px;",
    "\tfont-size: 115%;",
    "\tcolor: #FFFFFF;",
    "}",

    "\n#DC_LoaTS_raidMenuTabs li a.active {",
    "\tbackground-color: #57959E;",
    "\tborder: 2px solid #F1FFFF;",
    "\tcolor: #B7E5EE;",
    "}",

    "\n.RaidMenuTab-Header {",
    "}",

    "\n.DC_LoaTS_raidMenuOptionWrapper {",
    "\tborder-bottom: 1px solid #479090;",
    "\tmargin-bottom: 5px;",
    "}",

    "\n.DC_LoaTS_raidMenuOptionWrapper div{",
    "\tpadding: 5px;",
    "\tfloat: left;",
    "}",

    "\n.DC_LoaTS_raidMenuDescription{",
    "\tpadding-left: 15px;",
    "}",

    "\n.DC_LoaTS_raidMenuPane {",
    //"\tbackground: #77C0C0 url(http://old.jqueryui.com/themeroller/images/?new=77c0c0&w=1&h=100&f=png&q=100&fltr[]=over|textures/06_inset_hard.png|0|0|50) 50% bottom repeat-x;",
    "\tbackground: #77C0C0 url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/menupanebg.png) 50% bottom repeat-x;",
    "\tfont-size: 1.2em;",
    "\tpadding: 5px 10px;",
    "\tmin-height: 200px;",
    "\tmax-height: 600px;",
    "\toverflow: auto;",
    "\tclear: both;",
    "}",

    "\n.DC_LoaTS_raidMenuPane h1{",
    "\tborder-bottom: 1px solid #000000;",
    "\tmargin-bottom: 15px;",
    "}",

    "\n.DC_LoaTS_raidMenuPane h2{",
    "\tborder-bottom: 1px solid #479090;",
    "\tmargin-bottom: 10px;",
    "}",



    "\n#RaidsMenu-SearchWrapper {",
    "\twidth: 50%;",
    "\tmargin: auto;",
    "\t;",
    "}",

    "\n#RaidsMenu-SearchBox {",
    "\twidth: 70%;",
    "\tmin-width: 150px;",
    "}",

    "\n#RaidsMenu-ResultsBox {",
    "\tmax-height: 300px;",
    "\toverflow: auto;",
    "}",

    "\n#FormattingTab-MessageFormatTextArea {",
    "\twidth: 100%;",
    "\tmin-height: 35px;",
    "}",


    "\n.FormattingTab-Button {",
    "\tpadding: 3px 15px 4px;",
    "}",

    "\n.StylesTab-RaidNamesPicker {",
    "\tfloat:left;",
    "}",


    "\n#PreferencesMenu-LoadRaidsInBackgroundDelayInputWrapper input {",
    "\twidth: 30px;",
    "\theight: 10px;",
    "\tborder-radius: 5px;",
    "\ttext-align: center;",
    "}",

    "\n#CharacterViewMenu-PlatformSelect {",
    "\tcursor: pointer;",
    "\tborder-radius: 4px;",
    "\tfont-size: 14px;",
    "\tmargin-bottom: 10px;",
    "\tpadding: 4px 6px;",
    "\theight: 30px;",
    "\twidth: 220px;",
    "\toutline-offset: -2px;",
    "\toutline: 5px auto -webkit-focus-ring-color;",
    "}",

    "\n#CharacterViewMenu-UsernameBox {",
    "\twidth: 206px;",
    "\tline-height: 20px;",
    "\tfont-size: 14px;",
    "}",

    "\n.CharacterViewMenu-Button {",
    "\tpadding: 3px 15px 4px;",
    "}",


    "\n#DC_LoaTS_notificationBar {",
    "\tbackground: #f8dc5a url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/notificationbg.png) 50% 50% repeat-x;",
    //"\tbackground: #f8dc5a url(http://old.jqueryui.com/themeroller/images/?new=f8dc5a&w=1&h=100&f=png&q=100&fltr[]=over|textures/03_highlight_soft.png|0|0|75) 50% 50% repeat-x;",
    "\tpadding: 4px 10px; 0px",
    "\twidth: 100%;",
    "\tfont-size: 12pt;",
    "\tcolor: #915608;",
    "\tborder-bottom: 1px solid #fcd113;",
    "\tposition: fixed;",
    "\ttop: 0px;",
    "\tleft: 0px;",
    "\tz-index: 99999999;",
    "}",

    "\n#DC_LoaTS_notificationBarTitle {",
    "\tfloat: left;",
    "}",

    "\n#DC_LoaTS_notificationBarText {",
    "\tfloat: left;",
    "}",

    "\n#DC_LoaTS_notificationBarButtons {",
    "\tfloat: right;",
    "\tpadding-top:1px;",
    "}",

    "\n#DC_LoaTS_notificationBarButtons a.DC_LoaTS_updateLink {",
    "\tfont-size: inherit;",
    "\tmargin-right:10px;",
    "}",

    "\na.DC_LoaTS_notificationBarButton {",
    "\tbackground-color: #F9B83E;",
    "\tborder: 1px solid #915608;",
    "\tpadding: 2px 10px;",
    "\tmargin-right: 10px;",
    "\ttext-decoration: none;",
    "\tfont-weight: bold;",
    "\ttext-align: center;",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "\tborder-radius: 5px;",
    "}",

    "\na.DC_LoaTS_notificationBarButton:hover {",
    "\tcolor: #915608;",
    "\tbackground: #FDE477;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer {",
    "\tcolor: #FFFFFF;",
    "\tlist-style: none;",
    "\tbackground: #113552 url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/hexbg.png) 50% 50% repeat;",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "\tborder-radius: 5px;",
    "\theight: 16px;",
    "\tpadding: 2px 5px;",
    "\ttext-align:left;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li {",
    "float:left;",
    "}",


    "\na.DC_LoaTS_button {",
    "\twidth: 16px;",
    "\theight: 16px;",
    "\tbackground: url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/icons.png);",
    "\tbackground-repeat: no-repeat;",
    "\tcursor: pointer;",
    "\tdisplay: block;",
    "\tfloat: left;",
    "\ttext-indent: -99999px;",
    "}",

    "\na.DC_LoaTS_menuButton {",
    "\tbackground-position: -48px -80px;",
    "}",

    "\na.DC_LoaTS_reloadButton {",
    "\tbackground-position: -160px -64px;",
    "}",

    "\na.DC_LoaTS_toggleGameButton {",
    "\tbackground-position: 0 -176px;",
    "}",

	"\nli.DC_LoaTS_toggleWorldChatButtonWrapper {",
    "\tfloat: right !important;",
    "}",
	
    "\na.DC_LoaTS_toggleWorldChatButton {",
    "\tbackground-position: -128px -96px;",
    "}",
	
	"\na.DC_LoaTS_reloadWCButton {",
    "\tbackground-position: -160px -64px;",
    "}",

	"\nli.DC_LoaTS_reloadWCButtonWrapper {",
    "\tfloat: right !important;",
    "}",

    "\na.DC_LoaTS_WRButton {",
    "\ttext-indent: 0px;",
    "\tbackground: none;",
    "\twidth: auto;",
    "\tborder-radius: 5px;",
    "}",

    "\na.DC_LoaTS_WRButton:hover {",
    "\ttext-decoration: none;",
    "\tbackground-color: #71A5CE;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li.DC_LoaTS_WRButtonWrapper {",
    "\tfloat: right;",
    "}",


    "\n.DC_LoaTS_omnibox {",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "\tborder-radius: 5px;",
    "\tborder-color: #FFFFFF;",
    "\tbackground-color: #71A5CE;",
    "\tpadding: 0px 2px !important;",
    "}",

    "\n.DC_LoaTS_omnibox_focus {",
    "\tborder-color: #71A5CE;",
    "\tbackground-color: #FFFFFF;",
    "}",

    "\n.DC_LoaTS_omniboxWrapper {",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "\tborder-radius: 5px;",
    "\tposition: relative;",
    "\tfloat: left;",
    "}",

    "\n.DC_LoaTS_omniboxCommandsWrapper {",
    "\tbackground: #113552 url(http://subversion.assembla.com/svn/doomscript/trunk/1.1.0/Assets/hexbg.png) 50% 50% repeat;",
    "\tlist-style: none;",
    "\tz-index: 999;",
    "\tposition: absolute;",
    "\twidth: 630px;",
    "\tpadding: 5px;;",
    "\tborder-bottom-left-radius: 5px;",
    "\tborder-bottom-right-radius: 5px;",
    "\t;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li{",
    "\tfloat:none;",
    "\tmargin: 0px;",
    "\tbackground-color: #051E2A;",
    "\tfont-size: 1.3em;",
    "\toverflow: hidden;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li a{",
    "\tdisplay: block;",
    "\tcolor: #EEEEEE;",
    "\ttext-decoration: none;",
    "\tfloat:left;",
    "\t-moz-border-radius: 5px;",
    "\t-webkit-border-radius: 5px;",
    "\tborder-radius: 5px;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li a:hover{",
    "\tbackground-color: #57959E;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li:first-child{",
    "\tborder-top-left-radius: 5px;",
    "\tborder-top-right-radius: 5px;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li:last-child{",
    "\tborder-bottom-left-radius: 5px;",
    "\tborder-bottom-right-radius: 5px;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer li li a:first-child, #DC_LoaTS_raidToolbarContainer li li div:first-child{",
    "\tpadding-left: 10px !important;",
    "}",

    //--- Onnibox Option Styles ---\\

    "\n.DC_LoaTS_initialtext {",
    "\tfloat: left;",
    "\tpadding-left: 0px !important;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_omniboxOption {",
    "\tpadding: 2px 10px;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_any {",
    "\t;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_normal {",
    "\tcolor:#48C957;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_normal:hover {",
    "\tcolor:#FFFFFF;",
    "\tbackground-color:#48C957;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_hard {",
    "\tcolor:#E3E72E;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_hard:hover {",
    "\tcolor:#FFFFFF;",
    "\tbackground-color:#E3E72E;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_legendary {",
    "\tcolor:#CB0039;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_legendary:hover {",
    "\tcolor:#FFFFFF;",
    "\tbackground-color:#CB0039;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_nightmare {",
    "\tcolor:#B84EFE;",
    "}",

    "\n#DC_LoaTS_raidToolbarContainer a.DC_LoaTS_nightmare:hover {",
    "\tcolor:#FFFFFF;",
    "\tbackground-color:#B84EFE;",
    "}",


    "\na.raidLink {",
    "\ttextDecoration: none;",
    "}",

    "\na.raidDiffNormal:hover {",
    "\tcolor:#48C957;",
    "}",

    "\na.raidDiffHard:hover {",
    "\tcolor:#828505;",
    "}",

    "\na.raidDiffLegendary:hover {",
    "\tcolor:#CB0039;",
    "}",

    "\na.raidDiffNightmare:hover {",
    "\tcolor:#B84EFE;",
    "}",

    "\n.hidden {",
    "\tdisplay: none;",
    "}",


    "\n.DataDumpTab-Data {",
    "\twidth: 100%;",
    "\theight: 400px;",
    "}",

    "\n.DC_LoaTS_raidMenuCloseTabA {",
    "\tborder-radius: 100px;",
    "\twidth: 5px;",
    "\theight: 5px;",
    "\tcolor: #FFFFFF;",
    "\tbackground-color: #CCCCCC;",
    "}",

    "\n#maingame {",
    "\t-moz-transition: width .5s ease-out 0s;",
    "\t-webkit-transition: width .5s ease-out 0s;",
    "\t-o-transition: width .5s ease-out 0s;",
    "}",

    "\n#maingame.hideWorldChat {",
    "\twidth: 1060px !important;",
    "}",

    "\n#game {",
    "\toverflow: hidden;",
    "\t-moz-transition: width .5s ease-out 0s;",
    "\t-webkit-transition: width .5s ease-out 0s;",
    "\t-o-transition: width .5s ease-out 0s;",
    "}",

    "\n.hideWorldChat #game {",
    "\twidth: 759px !important;",
    "}",

    "\n#gameholder {",
    "\twidth: auto !important;",
    "}",

    "\n#kong_game_ui.chat-timestamp-right .chat_message_window p .timestamp {",
    "\tfloat: right",
    "}",
    "\n#kong_game_ui.chat-timestamp-right .chat_message_window p .message {",
    "\tclear: both",
    "}"
  ];

  var head = document.getElementsByTagName('head')[0],
    style = document.createElement('style'),
    rules = document.createTextNode(rulesText.join("\n"));

  style.type = 'text/css';

  if(style.styleSheet)
  {
    style.styleSheet.cssText = rules.nodeValue;
  }
  else
  {
    style.appendChild(rules);
  }

  head.appendChild(style);
}

function setupGMFunctions()
{
  if (typeof GM_setValue === 'undefined')
  {
    // These are probably obsolete now
    if(window.opera)
    {
      if(window.localStorage)
      {
        console.log("Creating Opera local storage fallbacks for GM functions");
        window.GM_setValue = function(k, v)
        {
          localStorage.setItem(k, v);
        };
        window.GM_getValue = function(k, def)
        {
          var ret = localStorage.getItem(k);
          return (ret == null?def:ret)
        };
        window.GM_deleteValue = function(k)
        {
          localStorage.removeItem(k);
        }
      }
      else
      {
        window.GM_setValue = function(){console.warn("Local Storage not accessible.");};
        window.GM_getValue = function(){console.warn("Local Storage not accessible.");};
        window.GM_deleteValue = function(){console.warn("Local Storage not accessible.");};
      }
    }
    else if(/Chrome/i.test(navigator.appVersion) || typeof unsafeWindow === "undefined")
    {
      console.log("Creating Chrome local storage fallbacks for GM functions");
      window.GM_setValue = function(k, v)
      {
        localStorage.setItem(k, v);
      };
      window.GM_getValue = function(k, def)
      {
        var ret = localStorage.getItem(k);
        return (ret == null?def:ret)
      };
      window.GM_deleteValue = function(k)
      {
        localStorage.removeItem(k);
      }
    }
  }

  if (typeof GM_xmlhttpRequest !== "function") {
    console.warn("doomscript will not run properly (or maybe even at all) in your browser without Greasemonkey Emulation: http://userscripts-mirror.org/scripts/show/105153");
  }
}

function doCriticalHooks()
{
  // Have the raid bot post a message to the user
  ChatDialogue.prototype.raidBotMessage = function(message)
  {
    try
    {
      if (typeof message === "string") {
        message = message.replace(/\n/g, "<br />\n");
      }
      else if (message instanceof HTMLElement) {
        message = jQuery(message)[0].outerHTML
      }
      else {
        console.warn("Unexpected message type during raidBotMessage", typeof message, message);
      }

      holodeck.activeDialogue().displayUnsanitizedMessage(
        "RaidBot",
        message,
        {"class": "whisper received_whisper"},
        {non_user: true}
      );

    }
    catch (ex)
    {
      console.warn("Unexpected exception during raidBotMessage", ex);
    }
  };

  // The idea to this feature would be that we could show the user previous things they typed by pressing up or down.
  function hookInputDialogue() {
    if (holodeck && holodeck.activeDialogue()) {
      // Hook the handler
      DC_LoaTS_Helper.registerEventHandler(holodeck.activeDialogue()._input_node, "keyup", function(e) {
        e = e || window.event;
        // TODO: Eventually, maybe handle up and down arrow for recent messages
        if (e.keyCode === 38) {
//                        console.log("Pressed up");
        }
        if (e.keyCode === 40) {
//                        console.log("Pressed down");
        }
      });
    }
    else {
      // Not ready, wait an try later
      setTimeout(hookInputDialogue, 1000);
    }
  }

  hookInputDialogue();
}

// Gotta jumpstart this bucket of giggles
function bootstrap_DC_LoaTS_Helper(loadSubFrames)
{
  // Only run if the script is running in the top frame
  if (top !== self && loadSubFrames != true)
  {
    return;
  }

  if (typeof window._dc_loats_helper_fails == "undefined")
  {
    window._dc_loats_helper_fails = 0;
  }

  if (window._dc_loats_helper_fails >= 10)
  {
    console.warn("DC LoaTS Link Helper could not load.");
    return;
  }

  // Don't want to run the script twice
  if (!window._dc_loats_helper)
  {

    // Do we actually have everything we need to start?
    if (typeof holodeck === "undefined" || typeof ChatDialogue === "undefined" || typeof Class === "undefined" || !$("chat_window"))
    {
      // Something is not loaded yet. Bail on this and try again later
//	            console.log("DC LoaTS Link Helper not ready. Fail " + window._dc_loats_helper_fails + "/10");

      window._dc_loats_helper_fails++;
      setTimeout(bootstrap_DC_LoaTS_Helper, 1000); // 1000ms = 1 second
      return;
    }

    // Print that we're about to start
    console.info("DC LoaTS Link Helper v" + DC_LoaTS_Properties.version + " trying to start...");

    // Setup GreaseMonkey functions
    setupGMFunctions();

    // Do critical hooks
    doCriticalHooks();

    // Declare classes
    declareClasses();

    // Define styles
    defineStyles();

    // Throw a reference to this onto the window I guess in case anyone else wants to use it?
    window._dc_loats_helper = new DC_LoaTS_Helper();

    // Update raid data
    DC_LoaTS_Helper.updateRaidData();
  }

  // Everything is done
  console.info("DC LoaTS Link Helper started!");
}

// Hit the go button and activate the main script.
bootstrap_DC_LoaTS_Helper(false);
}


// GM Layer
// This is handling XHR via events
function xhrGo(event)
{
  if (typeof XPCNativeWrapper !== "undefined" && typeof XPCNativeWrapper.unwrap === "function")
  {   //this takes 'event' out of the heavy duty sandbox so we can access the details for FF32+
    //INFO: tested and confirmed as working on FF31 and FF32 + GM 2.1
    DCDebug("GM XHR: Firefox sandbox, unwrapping 'event' for processing");

    event = XPCNativeWrapper.unwrap(event);
  }

  DCDebug("GM XHR: GM Received XHR Event: ", event);
  var params = event.detail;

  for (var param in params)
  {
    if (typeof params[param] === "string" && param.toLowerCase().indexOf("__callback_") === 0)
    {
      var funcName = param.substring("__callback_".length);
      params[funcName] = gmCallBack.bind(this, params.UUID, funcName);
    }
  }
  DCDebug("GM XHR: final params ", params);
  if (typeof GM_xmlhttpRequest === "function") {
    DCDebug("GM XHR: GM_XHR is available");
    setTimeout(function(){GM_xmlhttpRequest(params);},0);
  }
  else {
    DCDebug("GM XHR: GM_XHR is not available");
    console.error("Browser is not configured to allow GM_xmlhttpRequest. This could be due to a Chrome v27 bug.");
    var xmlhttp;
    if (window.XMLHttpRequest)
    {// code for IE7+, Firefox, Chrome, Opera, Safari
      xmlhttp = new XMLHttpRequest();
    }
    else
    {// code for IE6, IE5
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    xmlhttp.onreadystatechange = function()
    {
      if (xmlhttp.readyState === 4)
      {
        DCDebug("GM XHR: XHR ready state changed. Has onload? ", !!params.onload);
        if (typeof params.onload === "function") {
          params.onload(xmlhttp);
        }
      }
    };
    xmlhttp.open(params.method, params.url, !params.synchronous);
    xmlhttp.send();
  }
}

function gmCallBack(UUID, funcName, response)
{
  DCDebug("GM XHR: Preparing to call back to page JS");
  setTimeout(function()
  {
    DCDebug("GM XHR: Creating event to send back to the page  UUID: ", UUID, " funcName: ", funcName, " response: ", response);
    var detail = {callbackName: funcName, responseObj: response};
    DCDebug("GM XHR: Creating event detail: ", detail);
    try {
      if (typeof cloneInto === "function") {
        DCDebug("GM XHR: Using cloneInto");

        var cloned = {callbackName: funcName, responseObj: {}};
        for (var p in detail.responseObj) {
          // Awkward call to hasOwnProperty because of weird GreaseMonkey behavior in Firefox.
          if (Object.prototype.hasOwnProperty.call(detail.responseObj, p)) {
            DCDebug("GM XHR: Cloning property ", p, " which is a ", typeof detail.responseObj[p]);
            cloned.responseObj[p] = detail.responseObj[p];
          }
        }

        DCDebug("GM XHR: Using cloneInto for real");
        cloned = cloneInto(cloned, document.defaultView || unsafeWindow || window);
        DCDebug("GM XHR: cloned version: ", cloned);

        // It seems like the latest Firefox prefers this deprecated code? Bizarre
        var evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(UUID, true, true, cloned);
        document.documentElement.dispatchEvent(evt);
      }
      else {
        DCDebug("GM XHR: Not using cloneInto (it doesn't exist or isn't a function)");
        var evt = new CustomEvent(UUID, {"bubbles": true, "cancelable": true, "detail": detail});
        DCDebug("GM XHR: Dispatching event to page", evt);
        document.dispatchEvent(evt);
      }
    }
    catch (ex) {
      DCDebug("GM XHR: Caught exception while trying to respond to client XHR event", ex, " Fn Args: ", arguments);
    }
  }, 0);
}

document.addEventListener("DC_LoaTS_ExecuteGMXHR", xhrGo);


var GMDebugMode = (function() {
  var value = /debugMode=(\w+)/.exec(document.location.href);
  return value && !!value[1];
})();

// Debug log wrapping function
// Special scope debugging for just this script
DCDebug = function() {
  if (GMDebugMode === true) {
    console.log.apply(console, arguments);
  }
};



// This injects our script onto the page.
// Borrowed from: http://stackoverflow.com/a/2303228
if (/https?:\/\/www\.kongregate\.com\/games\/5thPlanetGames\/legacy-of-a-thousand-suns.*/i.test(window.location.href))
{
  var ugupCSS = document.createElement('link');
  ugupCSS.type = "text/css";
  ugupCSS.href = "https://rawgit.com/doomcat/OpenUgUp/master/js/src/ugup.css";
  ugupCSS.rel = "stylesheet";
  (document.head || document.body || document.documentElement).appendChild(ugupCSS);

  var ugupEquips = document.createElement('script');
  ugupEquips.type = "text/javascript";
  ugupEquips.src = "https://rawgit.com/doomcat/OpenUgUp/master/js/src/ugup.equipment.js";
  (document.body || document.head || document.documentElement).appendChild(ugupEquips);

  var ugupScript = document.createElement('script');
  ugupScript.type = "text/javascript";
  ugupScript.src = "https://rawgit.com/doomcat/OpenUgUp/master/js/src/ugup.js";
  (document.body || document.head || document.documentElement).appendChild(ugupScript);

  var script = document.createElement('script');
  script.appendChild(document.createTextNode('('+ main +')();'));
  (document.body || document.head || document.documentElement).appendChild(script);
}
else if (/50\.18\.1[89]\d\.\d{2,3}/i.test(window.location.host)) {
	console.log("doomscript loaded in iFrame");
	function iframeFn() 
	{
		if (typeof GM_setValue === 'undefined') 
		{
			window.GM_setValue = function(k, v) 
			{ 
				localStorage.setItem(k, (typeof v)[0] + v); 
			};
		}
		if (typeof GM_getValue === 'undefined') 
		{
			window.GM_getValue = function(k, def) 
			{
				var ret = localStorage.getItem(k);
				if (!ret)
					return def;
				var type = ret[0];
				var value = ret.substring(1);
				switch (type) {
					case 'b':
						return value == 'true';
					case 'n':
						return Number(value);
					case 's':
						return value;
					default:
						return def;
				}		  
			};
		}
		if (typeof GM_deleteValue === 'undefined') 
		{			
			window.GM_deleteValue = function(k)
			{
			  localStorage.removeItem(k);
			}
		}
	  
		window.onmessage = function(e)
		{
			if (e.origin != "http://www.kongregate.com" && e.origin != "http://www.armorgames.com")
				return;
			
			console.log("iFrame received message: ", e.data);
			var data = JSON.parse(e.data); 
			if (!data || data.namespace != "DC_LoaTS_Helper")
				return;			
			
			if (data.param == "game")
				var swf = document.getElementById("swfdiv");
			else if (data.param == "chat")
				var swf = document.getElementById("chatdiv");
			else
				return;

			if (swf !== null)
			{
				var url = swf.getAttribute("data");
				if(data.command == "reload" || data.command == "show")
				{
					if (data.command == "show") 
					{
						url = GM_getValue("DC_LoaTS_chatURL", "https://5thplanetlots.insnw.net/dotd_live/chat/812/chatclient.swf");
						if (data.param == "game")
							url = GM_getValue("DC_LoaTS_gameURL", "https://5thplanetlots.insnw.net/lots_live/swf/8107/lots.swf");
					}
					swf.style.display = "none";
					window.setTimeout(function()
					{
						swf.setAttribute("data", url);
						swf.style.display = "";
					}, 500);
				}
				else if(data.command == "hide")
				{
					GM_setValue("DC_LoaTS_"+data.param+"URL", url);
					swf.style.display = "none";
					window.setTimeout(function()
					{
						swf.setAttribute("data", "");
						swf.style.display = "";
					}, 500);
				}
				data.retCode = true;
			}
			e.source.postMessage(JSON.stringify(data), e.origin); //post back message (command, param, retCode) to caller
		};
	}

	var iframeScript = document.createElement('script');
	iframeScript.appendChild(document.createTextNode('('+ iframeFn +')();'));
	(document.body || document.head || document.documentElement).appendChild(iframeScript);
}
else {
  console.log("doomscript not launched on ", window.location);
}