code:
/proc/random_blood_type()
	return pick(4;"O-", 36;"O+", 3;"A-", 28;"A+", 1;"B-", 20;"B+", 1;"AB-", 5;"AB+")

/proc/random_eye_color()
	switch(pick(20;"brown",20;"hazel",20;"grey",15;"blue",15;"green",1;"amber",1;"albino"))
		if("brown")
			return "630"
		if("hazel")
			return "542"
		if("grey")
			return pick("666","777","888","999","aaa","bbb","ccc")
		if("blue")
			return "36c"
		if("green")
			return "060"
		if("amber")
			return "fc0"
		if("albino")
			return pick("c","d","e","f") + pick("0","1","2","3","4","5","6","7","8","9") + pick("0","1","2","3","4","5","6","7","8","9")
		else
			return "000"

/proc/random_underwear(gender)
	if(!underwear_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/underwear, underwear_list, underwear_m, underwear_f)
	switch(gender)
		if(MALE)
			return pick(underwear_m)
		if(FEMALE)
			return pick(underwear_f)
		else
			return pick(underwear_list)

/proc/random_undershirt(gender)
	if(!undershirt_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/undershirt, undershirt_list, undershirt_m, undershirt_f)
	switch(gender)
		if(MALE)
			return pick(undershirt_m)
		if(FEMALE)
			return pick(undershirt_f)
		else
			return pick(undershirt_list)

/proc/random_socks()
	if(!socks_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/socks, socks_list)
	return pick(socks_list)

/proc/random_features()
	if(!tails_list_human.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/human, tails_list_human)
	if(!tails_list_lizard.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/tails/lizard, tails_list_lizard)
	if(!snouts_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/snouts, snouts_list)
	if(!horns_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/horns, horns_list)
	if(!ears_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/ears, horns_list)
	if(!frills_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/frills, frills_list)
	if(!spines_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/spines, spines_list)
	if(!body_markings_list.len)
		init_sprite_accessory_subtypes(/datum/sprite_accessory/body_markings, body_markings_list)

	//For now we will always return none for tail_human and ears.
	return(list("mcolor" = pick("FFFFFF","7F7F7F", "7FFF7F", "7F7FFF", "FF7F7F", "7FFFFF", "FF7FFF", "FFFF7F"), "tail_lizard" = pick(tails_list_lizard), "tail_human" = "None", "snout" = pick(snouts_list), "horns" = pick(horns_list), "ears" = "None", "frills" = pick(frills_list), "spines" = pick(spines_list), "body_markings" = pick(body_markings_list)))

/proc/random_hair_style(gender)
	switch(gender)
		if(MALE)
			return pick(hair_styles_male_list)
		if(FEMALE)
			return pick(hair_styles_female_list)
		else
			return pick(hair_styles_list)

/proc/random_facial_hair_style(gender)
	switch(gender)
		if(MALE)
			return pick(facial_hair_styles_male_list)
		if(FEMALE)
			return pick(facial_hair_styles_female_list)
		else
			return pick(facial_hair_styles_list)

/proc/random_unique_name(gender, attempts_to_find_unique_name=10)
	for(var/i=1, i<=attempts_to_find_unique_name, i++)
		if(gender==FEMALE)
			. = capitalize(pick(first_names_female)) + " " + capitalize(pick(last_names))
		else
			. = capitalize(pick(first_names_male)) + " " + capitalize(pick(last_names))

		if(i != attempts_to_find_unique_name && !findname(.))
			break

/proc/random_unique_lizard_name(gender, attempts_to_find_unique_name=10)
	for(var/i=1, i<=attempts_to_find_unique_name, i++)
		. = capitalize(lizard_name(gender))

		if(i != attempts_to_find_unique_name && !findname(.))
			break

/proc/random_skin_tone()
	return pick(skin_tones)

var/list/skin_tones = list(
	"albino",
	"caucasian1",
	"caucasian2",
	"caucasian3",
	"latino",
	"mediterranean",
	"asian1",
	"asian2",
	"arab",
	"indian",
	"african1",
	"african2"
	)

var/global/list/species_list[0]
var/global/list/roundstart_species[0]

/proc/age2agedescription(age)
	switch(age)
		if(0 to 1)
			return "infant"
		if(1 to 3)
			return "toddler"
		if(3 to 13)
			return "child"
		if(13 to 19)
			return "teenager"
		if(19 to 30)
			return "young adult"
		if(30 to 45)
			return "adult"
		if(45 to 60)
			return "middle-aged"
		if(60 to 70)
			return "aging"
		if(70 to INFINITY)
			return "elderly"
		else
			return "unknown"

/*
Proc for attack log creation, because really why not
1 argument is the actor
2 argument is the target of action
3 is the description of action(like punched, throwed, or any other verb)
4 should it make adminlog note or not
5 is the tool with which the action was made(usually item)					5 and 6 are very similar(5 have "by " before it, that it) and are separated just to keep things in a bit more in order
6 is additional information, anything that needs to be added
*/

/proc/add_logs(mob/user, mob/target, what_done, object=null, addition=null)
	var/newhealthtxt = ""
	var/coordinates = ""
	var/turf/attack_location = get_turf(target)
	if(attack_location)
		coordinates = "([attack_location.x],[attack_location.y],[attack_location.z])"
	if(target && isliving(target))
		var/mob/living/L = target
		newhealthtxt = " (NEWHP: [L.health])"
	if(user && ismob(user))
		user.attack_log += text("\[[time_stamp()]\] Has [what_done] [target ? "[target.name][(ismob(target) && target.ckey) ? "([target.ckey])" : ""]" : "NON-EXISTANT SUBJECT"][object ? " with [object]" : " "][addition][newhealthtxt][coordinates]")
		if(user.mind)
			user.mind.attack_log += text("\[[time_stamp()]\] [user ? "[user.name][(ismob(user) && user.ckey) ? "([user.ckey])" : ""]" : "NON-EXISTANT SUBJECT"] has [what_done] [target ? "[target.name][(ismob(target) && target.ckey) ? "([target.ckey])" : ""]" : "NON-EXISTANT SUBJECT"][object ? " with [object]" : " "][addition][newhealthtxt][coordinates]")
	if(target && ismob(target))
		target.attack_log += text("\[[time_stamp()]\] Has been [what_done] by [user ? "[user.name][(ismob(user) && user.ckey) ? "([user.ckey])" : ""]" : "NON-EXISTANT SUBJECT"][object ? " with [object]" : " "][addition][newhealthtxt][coordinates]")
		if(target.mind)
			target.mind.attack_log += text("\[[time_stamp()]\] [target ? "[target.name][(ismob(target) && target.ckey) ? "([target.ckey])" : ""]" : "NON-EXISTANT SUBJECT"] has been [what_done] by [user ? "[user.name][(ismob(user) && user.ckey) ? "([user.ckey])" : ""]" : "NON-EXISTANT SUBJECT"][object ? " with [object]" : " "][addition][newhealthtxt][coordinates]")
	log_attack("[user ? "[user.name][(ismob(user) && user.ckey) ? "([user.ckey])" : ""]" : "NON-EXISTANT SUBJECT"] [what_done] [target ? "[target.name][(ismob(target) && target.ckey)? "([target.ckey])" : ""]" : "NON-EXISTANT SUBJECT"][object ? " with [object]" : " "][addition][newhealthtxt][coordinates]")

/proc/do_mob(mob/user , mob/target, time = 30, uninterruptible = 0, progress = 1)
	if(!user || !target)
		return 0
	var/user_loc = user.loc

	var/drifting = 0
	if(!user.Process_Spacemove(0) && user.inertia_dir)
		drifting = 1

	var/target_loc = target.loc

	var/holding = user.get_active_hand()
	var/datum/progressbar/progbar
	if (progress)
		progbar = new(user, time, target)

	var/endtime = world.time+time
	var/starttime = world.time
	. = 1
	while (world.time < endtime)
		stoplag()
		if (progress)
			progbar.update(world.time - starttime)
		if(!user || !target)
			. = 0
			break
		if(uninterruptible)
			continue

		if(drifting && !user.inertia_dir)
			drifting = 0
			user_loc = user.loc

		if((!drifting && user.loc != user_loc) || target.loc != target_loc || user.get_active_hand() != holding || user.incapacitated() || user.lying )
			. = 0
			break
	if (progress)
		qdel(progbar)

/proc/do_after(mob/user, delay, needhand = 1, atom/target = null, progress = 1)
	if(!user)
		return 0
	var/atom/Tloc = null
	if(target)
		Tloc = target.loc

	var/atom/Uloc = user.loc

	var/drifting = 0
	if(!user.Process_Spacemove(0) && user.inertia_dir)
		drifting = 1

	var/holding = user.get_active_hand()

	var/holdingnull = 1 //User's hand started out empty, check for an empty hand
	if(holding)
		holdingnull = 0 //Users hand started holding something, check to see if it's still holding that

	var/datum/progressbar/progbar
	if (progress)
		progbar = new(user, delay, target)

	var/endtime = world.time + delay
	var/starttime = world.time
	. = 1
	while (world.time < endtime)
		stoplag()
		if (progress)
			progbar.update(world.time - starttime)

		if(drifting && !user.inertia_dir)
			drifting = 0
			Uloc = user.loc

		if(!user || user.stat || user.weakened || user.stunned  || (!drifting && user.loc != Uloc))
			. = 0
			break

		if(Tloc && (!target || Tloc != target.loc))
			. = 0
			break

		if(needhand)
			//This might seem like an odd check, but you can still need a hand even when it's empty
			//i.e the hand is used to pull some item/tool out of the construction
			if(!holdingnull)
				if(!holding)
					. = 0
					break
			if(user.get_active_hand() != holding)
				. = 0
				break
	if (progress)
		qdel(progbar)

/proc/do_after_mob(mob/user, var/list/targets, time = 30, uninterruptible = 0, progress = 1)
	if(!user || !targets)
		return 0
	if(!islist(targets))
		targets = list(targets)
	var/user_loc = user.loc

	var/drifting = 0
	if(!user.Process_Spacemove(0) && user.inertia_dir)
		drifting = 1

	var/list/originalloc = list()
	for(var/atom/target in targets)
		originalloc[target] = target.loc

	var/holding = user.get_active_hand()
	var/datum/progressbar/progbar
	if(progress)
		progbar = new(user, time, targets[1])

	var/endtime = world.time + time
	var/starttime = world.time
	. = 1
	mainloop:
		while(world.time < endtime)
			sleep(1)
			if(progress)
				progbar.update(world.time - starttime)
			if(!user || !targets)
				. = 0
				break
			if(uninterruptible)
				continue

			if(drifting && !user.inertia_dir)
				drifting = 0
				user_loc = user.loc

			for(var/atom/target in targets)
				if((!drifting && user_loc != user.loc) || originalloc[target] != target.loc || user.get_active_hand() != holding || user.incapacitated() || user.lying )
					. = 0
					break mainloop
	if(progbar)
		qdel(progbar)


code:
/proc/lizard_name(gender)
	if(gender == MALE)
		return "[pick(lizard_names_male)]-[pick(lizard_names_male)]"
	else
		return "[pick(lizard_names_female)]-[pick(lizard_names_female)]"

var/church_name = null
/proc/church_name()
	if (church_name)
		return church_name

	var/name = ""

	name += pick("Holy", "United", "First", "Second", "Last")

	if (prob(20))
		name += " Space"

	name += " " + pick("Church", "Cathedral", "Body", "Worshippers", "Movement", "Witnesses")
	name += " of [religion_name()]"

	return name

var/command_name = null
/proc/command_name()
	if (command_name)
		return command_name

	var/name = "Central Command"

	command_name = name
	return name

/proc/change_command_name(name)

	command_name = name

	return name

var/religion_name = null
/proc/religion_name()
	if (religion_name)
		return religion_name

	var/name = ""

	name += pick("bee", "science", "edu", "captain", "assistant", "monkey", "alien", "space", "unit", "sprocket", "gadget", "bomb", "revolution", "beyond", "station", "goon", "robot", "ivor", "hobnob")
	name += pick("ism", "ia", "ology", "istism", "ites", "ick", "ian", "ity")

	return capitalize(name)

/proc/station_name()
	if(station_name)
		return station_name

	if(config && config.station_name)
		station_name = config.station_name
	else
		station_name = new_station_name()

	if(config && config.server_name)
		world.name = "[config.server_name][config.server_name==station_name ? "" : ": [station_name]"]"
	else
		world.name = station_name

	return station_name

/proc/new_station_name()
	var/random = rand(1,5)
	var/name = ""
	var/new_station_name = ""

	//Rare: Pre-Prefix
	if (prob(10))
		name = pick("Imperium", "Heretical", "Cuban", "Psychic", "Elegant", "Common", "Uncommon", "Rare", "Unique", "Houseruled", "Religious", "Atheist", "Traditional", "Houseruled", "Mad", "Super", "Ultra", "Secret", "Top Secret", "Deep", "Death", "Zybourne", "Central", "Main", "Government", "Uoi", "Fat", "Automated", "Experimental", "Augmented")
		new_station_name = name + " "
		name = ""

	// Prefix
	for(var/holiday_name in SSevent.holidays)
		if(holiday_name == "Friday the 13th")
			random = 13
		var/datum/holiday/holiday = SSevent.holidays[holiday_name]
		name = holiday.getStationPrefix()
		//get normal name
	if(!name)
		name = pick("", "Stanford", "Dorf", "Alium", "Prefix", "Clowning", "Aegis", "Ishimura", "Scaredy", "Death-World", "Mime", "Honk", "Rogue", "MacRagge", "Ultrameens", "Safety", "Paranoia", "Explosive", "Neckbear", "Donk", "Muppet", "North", "West", "East", "South", "Slant-ways", "Widdershins", "Rimward", "Expensive", "Procreatory", "Imperial", "Unidentified", "Immoral", "Carp", "Ork", "Pete", "Control", "Nettle", "Aspie", "Class", "Crab", "Fist","Corrogated","Skeleton","Race", "Fatguy", "Gentleman", "Capitalist", "Communist", "Bear", "Beard", "Derp", "Space", "Spess", "Star", "Moon", "System", "Mining", "Neckbeard", "Research", "Supply", "Military", "Orbital", "Battle", "Science", "Asteroid", "Home", "Production", "Transport", "Delivery", "Extraplanetary", "Orbital", "Correctional", "Robot", "Hats", "Pizza")
	if(name)
		new_station_name += name + " "

	// Suffix
	name = pick("Station", "Fortress", "Frontier", "Suffix", "Death-trap", "Space-hulk", "Lab", "Hazard","Spess Junk", "Fishery", "No-Moon", "Tomb", "Crypt", "Hut", "Monkey", "Bomb", "Trade Post", "Fortress", "Village", "Town", "City", "Edition", "Hive", "Complex", "Base", "Facility", "Depot", "Outpost", "Installation", "Drydock", "Observatory", "Array", "Relay", "Monitor", "Platform", "Construct", "Hangar", "Prison", "Center", "Port", "Waystation", "Factory", "Waypoint", "Stopover", "Hub", "HQ", "Office", "Object", "Fortification", "Colony", "Planet-Cracker", "Roost", "Fat Camp")
	new_station_name += name + " "

	// ID Number
	switch(random)
		if(1)
			new_station_name += "[rand(1, 99)]"
		if(2)
			new_station_name += pick("Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta", "Iota", "Kappa", "Lambda", "Mu", "Nu", "Xi", "Omicron", "Pi", "Rho", "Sigma", "Tau", "Upsilon", "Phi", "Chi", "Psi", "Omega")
		if(3)
			new_station_name += pick("II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX")
		if(4)
			new_station_name += pick("Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu")
		if(5)
			new_station_name += pick("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen")
		if(13)
			new_station_name += pick("13","XIII","Thirteen")
	return new_station_name

var/syndicate_name = null
/proc/syndicate_name()
	if (syndicate_name)
		return syndicate_name

	var/name = ""

	// Prefix
	name += pick("Clandestine", "Prima", "Blue", "Zero-G", "Max", "Blasto", "Waffle", "North", "Omni", "Newton", "Cyber", "Bonk", "Gene", "Gib")

	// Suffix
	if (prob(80))
		name += " "

		// Full
		if (prob(60))
			name += pick("Syndicate", "Consortium", "Collective", "Corporation", "Group", "Holdings", "Biotech", "Industries", "Systems", "Products", "Chemicals", "Enterprises", "Family", "Creations", "International", "Intergalactic", "Interplanetary", "Foundation", "Positronics", "Hive")
		// Broken
		else
			name += pick("Syndi", "Corp", "Bio", "System", "Prod", "Chem", "Inter", "Hive")
			name += pick("", "-")
			name += pick("Tech", "Sun", "Co", "Tek", "X", "Inc", "Code")
	// Small
	else
		name += pick("-", "*", "")
		name += pick("Tech", "Sun", "Co", "Tek", "X", "Inc", "Gen", "Star", "Dyne", "Code", "Hive")

	syndicate_name = name
	return name

//Traitors and traitor silicons will get these. Revs will not.
var/syndicate_code_phrase//Code phrase for traitors.
var/syndicate_code_response//Code response for traitors.

	/*
	Should be expanded.
	How this works:
	Instead of "I'm looking for James Smith," the traitor would say "James Smith" as part of a conversation.
	Another traitor may then respond with: "They enjoy running through the void-filled vacuum of the derelict."
	The phrase should then have the words: James Smith.
	The response should then have the words: run, void, and derelict.
	This way assures that the code is suited to the conversation and is unpredicatable.
	Obviously, some people will be better at this than others but in theory, everyone should be able to do it and it only enhances roleplay.
	Can probably be done through "{ }" but I don't really see the practical benefit.
	One example of an earlier system is commented below.
	/N
	*/

/proc/generate_code_phrase()//Proc is used for phrase and response in master_controller.dm

	var/code_phrase = ""//What is returned when the proc finishes.
	var/words = pick(//How many words there will be. Minimum of two. 2, 4 and 5 have a lesser chance of being selected. 3 is the most likely.
		50; 2,
		200; 3,
		50; 4,
		25; 5
	)

	var/safety[] = list(1,2,3)//Tells the proc which options to remove later on.
	var/nouns[] = list("love","hate","anger","peace","pride","sympathy","bravery","loyalty","honesty","integrity","compassion","charity","success","courage","deceit","skill","beauty","brilliance","pain","misery","beliefs","dreams","justice","truth","faith","liberty","knowledge","thought","information","culture","trust","dedication","progress","education","hospitality","leisure","trouble","friendships", "relaxation")
	var/drinks[] = list("vodka and tonic","gin fizz","bahama mama","manhattan","black Russian","whiskey soda","long island tea","margarita","Irish coffee"," manly dwarf","Irish cream","doctor's delight","Beepksy Smash","tequila sunrise","brave bull","gargle blaster","bloody mary","whiskey cola","white Russian","vodka martini","martini","Cuba libre","kahlua","vodka","wine","moonshine")
	var/locations[] = teleportlocs.len ? teleportlocs : drinks//if null, defaults to drinks instead.

	var/names[] = list()
	for(var/datum/data/record/t in data_core.general)//Picks from crew manifest.
		names += t.fields["name"]

	var/maxwords = words//Extra var to check for duplicates.

	for(words,words>0,words--)//Randomly picks from one of the choices below.

		if(words==1&&(1 in safety)&&(2 in safety))//If there is only one word remaining and choice 1 or 2 have not been selected.
			safety = list(pick(1,2))//Select choice 1 or 2.
		else if(words==1&&maxwords==2)//Else if there is only one word remaining (and there were two originally), and 1 or 2 were chosen,
			safety = list(3)//Default to list 3

		switch(pick(safety))//Chance based on the safety list.
			if(1)//1 and 2 can only be selected once each to prevent more than two specific names/places/etc.
				switch(rand(1,2))//Mainly to add more options later.
					if(1)
						if(names.len&&prob(70))
							code_phrase += pick(names)
						else
							if(prob(10))
								code_phrase += pick(lizard_name(MALE),lizard_name(FEMALE))
							else
								code_phrase += pick(pick(first_names_male,first_names_female))
								code_phrase += " "
								code_phrase += pick(last_names)
					if(2)
						code_phrase += pick(get_all_jobs())//Returns a job.
				safety -= 1
			if(2)
				switch(rand(1,2))//Places or things.
					if(1)
						code_phrase += pick(drinks)
					if(2)
						code_phrase += pick(locations)
				safety -= 2
			if(3)
				switch(rand(1,3))//Nouns, adjectives, verbs. Can be selected more than once.
					if(1)
						code_phrase += pick(nouns)
					if(2)
						code_phrase += pick(adjectives)
					if(3)
						code_phrase += pick(verbs)
		if(words==1)
			code_phrase += "."
		else
			code_phrase += ", "

	return code_phrase

license: mit


code:
// Ensure the frequency is within bounds of what it should be sending/recieving at
/proc/sanitize_frequency(frequency, free = FALSE)
	. = round(frequency)
	if(free)
		. = Clamp(frequency, MIN_FREE_FREQ, MAX_FREE_FREQ)
	else
		. = Clamp(frequency, MIN_FREQ, MAX_FREQ)
	if(!(. % 2)) // Ensure the last digit is an odd number
		. += 1

// Format frequency by moving the decimal.
/proc/format_frequency(frequency)
	frequency = text2num(frequency)
	return "[round(frequency / 10)].[frequency % 10]"


code:
//general stuff
/proc/sanitize_integer(number, min=0, max=1, default=0)
	if(isnum(number))
		number = round(number)
		if(min <= number && number <= max)
			return number
	return default

/proc/sanitize_text(text, default="")
	if(istext(text))
		return text
	return default

/proc/sanitize_inlist(value, list/List, default)
	if(value in List)
		return value
	if(default)
		return default
	if(List && List.len)
		return pick(List)

//more specialised stuff
/proc/sanitize_gender(gender,neuter=0,plural=0, default="male")
	switch(gender)
		if(MALE, FEMALE)
			return gender
		if(NEUTER)
			if(neuter)
				return gender
			else
				return default
		if(PLURAL)
			if(plural)
				return gender
			else
				return default
	return default

/proc/sanitize_hexcolor(color, desired_format=3, include_crunch=0, default)
	var/crunch = include_crunch ? "#" : ""
	if(!istext(color))
		color = ""

	var/start = 1 + (text2ascii(color,1)==35)
	var/len = length(color)
	var/step_size = 1 + ((len+1)-start != desired_format)

	. = ""
	for(var/i=start, i<=len, i+=step_size)
		var/ascii = text2ascii(color,i)
		switch(ascii)
			if(48 to 57)
				. += ascii2text(ascii)		//numbers 0 to 9
			if(97 to 102)
				. += ascii2text(ascii)		//letters a to f
			if(65 to 70)
				. += ascii2text(ascii+32)	//letters A to F - translates to lowercase
			else
				break

	if(length(.) != desired_format)
		if(default)
			return default
		return crunch + repeat_string(desired_format, "0")

	return crunch + .

/proc/sanitize_ooccolor(color)
	var/list/HSL = rgb2hsl(hex2num(copytext(color,2,4)),hex2num(copytext(color,4,6)),hex2num(copytext(color,6,8)))
	HSL[3] = min(HSL[3],0.4)
	var/list/RGB = hsl2rgb(arglist(HSL))
	return "#[num2hex(RGB[1],2)][num2hex(RGB[2],2)][num2hex(RGB[3],2)]"


code:
//simple insertion sort - generally faster than merge for runs of 7 or smaller
/proc/sortInsert(list/L, cmp=/proc/cmp_numeric_asc, associative, fromIndex=1, toIndex=0)
	if(L && L.len >= 2)
		fromIndex = fromIndex % L.len
		toIndex = toIndex % (L.len+1)
		if(fromIndex <= 0)
			fromIndex += L.len
		if(toIndex <= 0)
			toIndex += L.len + 1

		sortInstance.L = L
		sortInstance.cmp = cmp
		sortInstance.associative = associative

		sortInstance.binarySort(fromIndex, toIndex, fromIndex)
	return L


code:
//merge-sort - gernerally faster than insert sort, for runs of 7 or larger
/proc/sortMerge(list/L, cmp=/proc/cmp_numeric_asc, associative, fromIndex=1, toIndex)
	if(L && L.len >= 2)
		fromIndex = fromIndex % L.len
		toIndex = toIndex % (L.len+1)
		if(fromIndex <= 0)
			fromIndex += L.len
		if(toIndex <= 0)
			toIndex += L.len + 1

		sortInstance.L = L
		sortInstance.cmp = cmp
		sortInstance.associative = associative
		sortInstance.mergeSort(fromIndex, toIndex)

	return L


code:
//TimSort interface
/proc/sortTim(list/L, cmp=/proc/cmp_numeric_asc, associative, fromIndex=1, toIndex=0)
	if(L && L.len >= 2)
		fromIndex = fromIndex % L.len
		toIndex = toIndex % (L.len+1)
		if(fromIndex <= 0)
			fromIndex += L.len
		if(toIndex <= 0)
			toIndex += L.len + 1

		sortInstance.L = L
		sortInstance.cmp = cmp
		sortInstance.associative = associative

		sortInstance.timSort(fromIndex, toIndex)

	return L


code:
	//These are macros used to reduce on proc calls
#define fetchElement(L, i) (associative) ? L[L[i]] : L[i]

	//Minimum sized sequence that will be merged. Anything smaller than this will use binary-insertion sort.
	//Should be a power of 2
#define MIN_MERGE 32

	//When we get into galloping mode, we stay there until both runs win less often than MIN_GALLOP consecutive times.
#define MIN_GALLOP 7

	//This is a global instance to allow much of this code to be reused. The interfaces are kept seperately
var/datum/sortInstance/sortInstance = new()
/datum/sortInstance
	//The array being sorted.
	var/list/L

	//The comparator proc-reference
	var/cmp = /proc/cmp_numeric_asc

	//whether we are sorting list keys (0: L[i]) or associated values (1: L[L[i]])
	var/associative = 0

	//This controls when we get *into* galloping mode.  It is initialized	to MIN_GALLOP.
	//The mergeLo and mergeHi methods nudge it higher for random data, and lower for highly structured data.
	var/minGallop = MIN_GALLOP

	//Stores information regarding runs yet to be merged.
	//Run i starts at runBase[i] and extends for runLen[i] elements.
	//runBase[i] + runLen[i] == runBase[i+1]
	//var/stackSize
	var/list/runBases = list()
	var/list/runLens = list()

	proc/timSort(start, end)
		runBases.Cut()
		runLens.Cut()

		var/remaining = end - start

		//If array is small, do a 'mini-TimSort' with no merges
		if(remaining < MIN_MERGE)
			var/initRunLen = countRunAndMakeAscending(start, end)
			binarySort(start, end, start+initRunLen)
			return

		//March over the array finding natural runs
		//Extend any short natural runs to runs of length minRun
		var/minRun = minRunLength(remaining)

		do
				//identify next run
			var/runLen = countRunAndMakeAscending(start, end)

				//if run is short, extend to min(minRun, remaining)
			if(runLen < minRun)
				var/force = (remaining <= minRun) ? remaining : minRun

				binarySort(start, start+force, start+runLen)
				runLen = force

				//add data about run to queue
			runBases.Add(start)
			runLens.Add(runLen)

				//maybe merge
			mergeCollapse()

				//Advance to find next run
			start += runLen
			remaining -= runLen

		while(remaining > 0)

			//Merge all remaining runs to complete sort
		//ASSERT(start == end)
		mergeForceCollapse();
		//ASSERT(runBases.len == 1)

			//reset minGallop, for successive calls
		minGallop = MIN_GALLOP

		return L

	/*
	Sorts the specified portion of the specified array using a binary
	insertion sort.  This is the best method for sorting small numbers
	of elements.  It requires O(n log n) compares, but O(n^2) data
	movement (worst case).

	If the initial part of the specified range is already sorted,
	this method can take advantage of it: the method assumes that the
	elements in range [lo,start) are already sorted

	lo		the index of the first element in the range to be sorted
	hi		the index after the last element in the range to be sorted
	start	the index of the first element in the range that is	not already known to be sorted
	*/
	proc/binarySort(lo, hi, start)
		//ASSERT(lo <= start && start <= hi)
		if(start <= lo)
			start = lo + 1

		for(,start < hi, ++start)
			var/pivot = fetchElement(L,start)

			//set left and right to the index where pivot belongs
			var/left = lo
			var/right = start
			//ASSERT(left <= right)

			//[lo, left) elements <= pivot < [right, start) elements
			//in other words, find where the pivot element should go using bisection search
			while(left < right)
				var/mid = (left + right) >> 1	//round((left+right)/2)
				if(call(cmp)(fetchElement(L,mid), pivot) > 0)
					right = mid
				else
					left = mid+1

			//ASSERT(left == right)
			moveElement(L, start, left)	//move pivot element to correct location in the sorted range

	/*
	Returns the length of the run beginning at the specified position and reverses the run if it is back-to-front

	A run is the longest ascending sequence with:
		a[lo] <= a[lo + 1] <= a[lo + 2] <= ...
	or the longest descending sequence with:
		a[lo] >  a[lo + 1] >  a[lo + 2] >  ...

	For its intended use in a stable mergesort, the strictness of the
	definition of "descending" is needed so that the call can safely
	reverse a descending sequence without violating stability.
	*/
	proc/countRunAndMakeAscending(lo, hi)
		//ASSERT(lo < hi)

		var/runHi = lo + 1
		if(runHi >= hi)
			return 1

		var/last = fetchElement(L,lo)
		var/current = fetchElement(L,runHi++)

		if(call(cmp)(current, last) < 0)
			while(runHi < hi)
				last = current
				current = fetchElement(L,runHi)
				if(call(cmp)(current, last) >= 0)
					break
				++runHi
			reverseRange(L, lo, runHi)
		else
			while(runHi < hi)
				last = current
				current = fetchElement(L,runHi)
				if(call(cmp)(current, last) < 0)
					break
				++runHi

		return runHi - lo

	//Returns the minimum acceptable run length for an array of the specified length.
	//Natural runs shorter than this will be extended with binarySort
	proc/minRunLength(n)
		//ASSERT(n >= 0)
		var/r = 0	//becomes 1 if any bits are shifted off
		while(n >= MIN_MERGE)
			r |= (n & 1)
			n >>= 1
		return n + r

	//Examines the stack of runs waiting to be merged and merges adjacent runs until the stack invariants are reestablished:
	//	runLen[i-3] > runLen[i-2] + runLen[i-1]
	//	runLen[i-2] > runLen[i-1]
	//This method is called each time a new run is pushed onto the stack.
	//So the invariants are guaranteed to hold for i<stackSize upon entry to the method
	proc/mergeCollapse()
		while(runBases.len >= 2)
			var/n = runBases.len - 1
			if(n > 1 && runLens[n-1] <= runLens[n] + runLens[n+1])
				if(runLens[n-1] < runLens[n+1])
					--n
				mergeAt(n)
			else if(runLens[n] <= runLens[n+1])
				mergeAt(n)
			else
				break	//Invariant is established

	//Merges all runs on the stack until only one remains.
	//Called only once, to finalise the sort
	proc/mergeForceCollapse()
		while(runBases.len >= 2)
			var/n = runBases.len - 1
			if(n > 1 && runLens[n-1] < runLens[n+1])
				--n
			mergeAt(n)

	//Merges the two consecutive runs at stack indices i and i+1
	//Run i must be the penultimate or antepenultimate run on the stack
	//In other words, i must be equal to stackSize-2 or stackSize-3
	proc/mergeAt(i)
		//ASSERT(runBases.len >= 2)
		//ASSERT(i >= 1)
		//ASSERT(i == runBases.len - 1 || i == runBases.len - 2)

		var/base1 = runBases[i]
		var/base2 = runBases[i+1]
		var/len1 = runLens[i]
		var/len2 = runLens[i+1]

		//ASSERT(len1 > 0 && len2 > 0)
		//ASSERT(base1 + len1 == base2)

		//Record the legth of the combined runs. If i is the 3rd last run now, also slide over the last run
		//(which isn't involved in this merge). The current run (i+1) goes away in any case.
		runLens[i] += runLens[i+1]
		runLens.Cut(i+1, i+2)
		runBases.Cut(i+1, i+2)

		//Find where the first element of run2 goes in run1.
		//Prior elements in run1 can be ignored (because they're already in place)
		var/k = gallopRight(fetchElement(L,base2), base1, len1, 0)
		//ASSERT(k >= 0)
		base1 += k
		len1 -= k
		if(len1 == 0)
			return

		//Find where the last element of run1 goes in run2.
		//Subsequent elements in run2 can be ignored (because they're already in place)
		len2 = gallopLeft(fetchElement(L,base1 + len1 - 1), base2, len2, len2-1)
		//ASSERT(len2 >= 0)
		if(len2 == 0)
			return

		//Merge remaining runs, using tmp array with min(len1, len2) elements
		if(len1 <= len2)
			mergeLo(base1, len1, base2, len2)
		else
			mergeHi(base1, len1, base2, len2)

	/*
		Locates the position to insert key within the specified sorted range
		If the range contains elements equal to key, this will return the index of the LEFTMOST of those elements

		key		the element to be inserted into the sorted range
		base	the index of the first element of the sorted range
		len		the length of the sorted range, must be greater than 0
		hint	the offset from base at which to begin the search, such that 0 <= hint < len; i.e. base <= hint < base+hint

		Returns the index at which to insert element 'key'
	*/
	proc/gallopLeft(key, base, len, hint)
		//ASSERT(len > 0 && hint >= 0 && hint < len)

		var/lastOffset = 0
		var/offset = 1
		if(call(cmp)(key, fetchElement(L,base+hint)) > 0)
			var/maxOffset = len - hint
			while(offset < maxOffset && call(cmp)(key, fetchElement(L,base+hint+offset)) > 0)
				lastOffset = offset
				offset = (offset << 1) + 1

			if(offset > maxOffset)
				offset = maxOffset

			lastOffset += hint
			offset += hint

		else
			var/maxOffset = hint + 1
			while(offset < maxOffset && call(cmp)(key, fetchElement(L,base+hint-offset)) <= 0)
				lastOffset = offset
				offset = (offset << 1) + 1

			if(offset > maxOffset)
				offset = maxOffset

			var/temp = lastOffset
			lastOffset = hint - offset
			offset = hint - temp

			//ASSERT(-1 <= lastOffset && lastOffset < offset && offset <= len)

		//Now L[base+lastOffset] < key <= L[base+offset], so key belongs somewhere to the right of lastOffset but no farther than
		//offset. Do a binary search with invariant L[base+lastOffset-1] < key <= L[base+offset]
		++lastOffset
		while(lastOffset < offset)
			var/m = lastOffset + ((offset - lastOffset) >> 1)

			if(call(cmp)(key, fetchElement(L,base+m)) > 0)
				lastOffset = m + 1
			else
				offset = m

		//ASSERT(lastOffset == offset)
		return offset

	/**
	 * Like gallopLeft, except that if the range contains an element equal to
	 * key, gallopRight returns the index after the rightmost equal element.
	 *
	 * @param key the key whose insertion point to search for
	 * @param a the array in which to search
	 * @param base the index of the first element in the range
	 * @param len the length of the range; must be > 0
	 * @param hint the index at which to begin the search, 0 <= hint < n.
	 *	 The closer hint is to the result, the faster this method will run.
	 * @param c the comparator used to order the range, and to search
	 * @return the int k,  0 <= k <= n such that a[b + k - 1] <= key < a[b + k]
	 */
	proc/gallopRight(key, base, len, hint)
		//ASSERT(len > 0 && hint >= 0 && hint < len)

		var/offset = 1
		var/lastOffset = 0
		if(call(cmp)(key, fetchElement(L,base+hint)) < 0)	//key <= L[base+hint]
			var/maxOffset = hint + 1	//therefore we want to insert somewhere in the range [base,base+hint] = [base+,base+(hint+1))
			while(offset < maxOffset && call(cmp)(key, fetchElement(L,base+hint-offset)) < 0)	//we are iterating backwards
				lastOffset = offset
				offset = (offset << 1) + 1	//1 3 7 15
				//if(offset <= 0)	//int overflow, not an issue here since we are using floats
				//	offset = maxOffset

			if(offset > maxOffset)
				offset = maxOffset

			var/temp = lastOffset
			lastOffset = hint - offset
			offset = hint - temp

		else	//key > L[base+hint]
			var/maxOffset = len - hint	//therefore we want to insert somewhere in the range (base+hint,base+len) = [base+hint+1, base+hint+(len-hint))
			while(offset < maxOffset && call(cmp)(key, fetchElement(L,base+hint+offset)) >= 0)
				lastOffset = offset
				offset = (offset << 1) + 1
				//if(offset <= 0)	//int overflow, not an issue here since we are using floats
				//	offset = maxOffset

			if(offset > maxOffset)
				offset = maxOffset

			lastOffset += hint
			offset += hint

		//ASSERT(-1 <= lastOffset && lastOffset < offset && offset <= len)

		++lastOffset
		while(lastOffset < offset)
			var/m = lastOffset + ((offset - lastOffset) >> 1)

			if(call(cmp)(key, fetchElement(L,base+m)) < 0)	//key <= L[base+m]
				offset = m
			else							//key > L[base+m]
				lastOffset = m + 1

		//ASSERT(lastOffset == offset)

		return offset

	//Merges two adjacent runs in-place in a stable fashion.
	//For performance this method should only be called when len1 <= len2!
	proc/mergeLo(base1, len1, base2, len2)
		//ASSERT(len1 > 0 && len2 > 0 && base1 + len1 == base2)

		var/cursor1 = base1
		var/cursor2 = base2

		//degenerate cases
		if(len2 == 1)
			moveElement(L, cursor2, cursor1)
			return

		if(len1 == 1)
			moveElement(L, cursor1, cursor2+len2)
			return

		//Move first element of second run
		moveElement(L, cursor2++, cursor1++)
		--len2

		outer:
			while(1)
				var/count1 = 0	//# of times in a row that first run won
				var/count2 = 0	//	"	"	"	"	"	"  second run won

				//do the straightfoward thin until one run starts winning consistently

				do
					//ASSERT(len1 > 1 && len2 > 0)
					if(call(cmp)(fetchElement(L,cursor2), fetchElement(L,cursor1)) < 0)
						moveElement(L, cursor2++, cursor1++)
						--len2

						++count2
						count1 = 0

						if(len2 == 0)
							break outer
					else
						++cursor1

						++count1
						count2 = 0

						if(--len1 == 1)
							break outer

				while((count1 | count2) < minGallop)

				//one run is winning consistently so galloping may provide huge benifits
				//so try galloping, until such time as the run is no longer consistently winning
				do
					//ASSERT(len1 > 1 && len2 > 0)

					count1 = gallopRight(fetchElement(L,cursor2), cursor1, len1, 0)
					if(count1)
						cursor1 += count1
						len1 -= count1

						if(len1 <= 1)
							break outer

					moveElement(L, cursor2, cursor1)
					++cursor2
					++cursor1
					if(--len2 == 0)
						break outer

					count2 = gallopLeft(fetchElement(L,cursor1), cursor2, len2, 0)
					if(count2)
						moveRange(L, cursor2, cursor1, count2)

						cursor2 += count2
						cursor1 += count2
						len2 -= count2

						if(len2 == 0)
							break outer

					++cursor1
					if(--len1 == 1)
						break outer

					--minGallop

				while((count1|count2) > MIN_GALLOP)

				if(minGallop < 0)
					minGallop = 0
				minGallop += 2;  // Penalize for leaving gallop mode

		if(len1 == 1)
			//ASSERT(len2 > 0)
			moveElement(L, cursor1, cursor2+len2)

		//else
			//ASSERT(len2 == 0)
			//ASSERT(len1 > 1)

	proc/mergeHi(base1, len1, base2, len2)
		//ASSERT(len1 > 0 && len2 > 0 && base1 + len1 == base2)

		var/cursor1 = base1 + len1 - 1	//start at end of sublists
		var/cursor2 = base2 + len2 - 1

		//degenerate cases
		if(len2 == 1)
			moveElement(L, base2, base1)
			return

		if(len1 == 1)
			moveElement(L, base1, cursor2+1)
			return

		moveElement(L, cursor1--, cursor2-- + 1)
		--len1

		outer:
			while(1)
				var/count1 = 0	//# of times in a row that first run won
				var/count2 = 0	//	"	"	"	"	"	"  second run won

				//do the straightfoward thing until one run starts winning consistently
				do
					//ASSERT(len1 > 0 && len2 > 1)
					if(call(cmp)(fetchElement(L,cursor2), fetchElement(L,cursor1)) < 0)
						moveElement(L, cursor1--, cursor2-- + 1)
						--len1

						++count1
						count2 = 0

						if(len1 == 0)
							break outer
					else
						--cursor2
						--len2

						++count2
						count1 = 0

						if(len2 == 1)
							break outer
				while((count1 | count2) < minGallop)

				//one run is winning consistently so galloping may provide huge benifits
				//so try galloping, until such time as the run is no longer consistently winning
				do
					//ASSERT(len1 > 0 && len2 > 1)

					count1 = len1 - gallopRight(fetchElement(L,cursor2), base1, len1, len1-1)	//should cursor1 be base1?
					if(count1)
						cursor1 -= count1

						moveRange(L, cursor1+1, cursor2+1, count1)	//cursor1+1 == cursor2 by definition

						cursor2 -= count1
						len1 -= count1

						if(len1 == 0)
							break outer

					--cursor2

					if(--len2 == 1)
						break outer

					count2 = len2 - gallopLeft(fetchElement(L,cursor1), cursor1+1, len2, len2-1)
					if(count2)
						cursor2 -= count2
						len2 -= count2

						if(len2 <= 1)
							break outer

					moveElement(L, cursor1--, cursor2-- + 1)
					--len1

					if(len1 == 0)
						break outer

					--minGallop
				while((count1|count2) > MIN_GALLOP)

				if(minGallop < 0)
					minGallop = 0
				minGallop += 2	// Penalize for leaving gallop mode

		if(len2 == 1)
			//ASSERT(len1 > 0)

			cursor1 -= len1
			moveRange(L, cursor1+1, cursor2+1, len1)

		//else
			//ASSERT(len1 == 0)
			//ASSERT(len2 > 0)

	proc/mergeSort(start, end)
		var/remaining = end - start

		//If array is small, do an insertion sort
		if(remaining < MIN_MERGE)
			//var/initRunLen = countRunAndMakeAscending(start, end)
			binarySort(start, end, start/*+initRunLen*/)
			return

		var/minRun = minRunLength(remaining)

		do
			var/runLen = (remaining <= minRun) ? remaining : minRun

			binarySort(start, start+runLen, start)

			//add data about run to queue
			runBases.Add(start)
			runLens.Add(runLen)

			//Advance to find next run
			start += runLen
			remaining -= runLen

		while(remaining > 0)

		while(runBases.len >= 2)
			var/n = runBases.len - 1
			if(n > 1 && runLens[n-1] <= runLens[n] + runLens[n+1])
				if(runLens[n-1] < runLens[n+1])
					--n
				mergeAt2(n)
			else if(runLens[n] <= runLens[n+1])
				mergeAt2(n)
			else
				break	//Invariant is established

		while(runBases.len >= 2)
			var/n = runBases.len - 1
			if(n > 1 && runLens[n-1] < runLens[n+1])
				--n
			mergeAt2(n)

		return L

	proc/mergeAt2(i)
		var/cursor1 = runBases[i]
		var/cursor2 = runBases[i+1]

		var/end1 = cursor1+runLens[i]
		var/end2 = cursor2+runLens[i+1]

		var/val1 = fetchElement(L,cursor1)
		var/val2 = fetchElement(L,cursor2)

		while(1)
			if(call(cmp)(val1,val2) < 0)
				if(++cursor1 >= end1)
					break
				val1 = fetchElement(L,cursor1)
			else
				moveElement(L,cursor2,cursor1)

				++cursor2
				if(++cursor2 >= end2)
					break
				++end1
				++cursor1
				//if(++cursor1 >= end1)
				//	break

				val2 = fetchElement(L,cursor2)

		//Record the legth of the combined runs. If i is the 3rd last run now, also slide over the last run
		//(which isn't involved in this merge). The current run (i+1) goes away in any case.
		runLens[i] += runLens[i+1]
		runLens.Cut(i+1, i+2)
		runBases.Cut(i+1, i+2)

#undef MIN_GALLOP
#undef MIN_MERGE

#undef fetchElement


code:
/*
 * Holds procs designed to help with filtering text
 * Contains groups:
 *			SQL sanitization/formating
 *			Text sanitization
 *			Text searches
 *			Text modification
 *			Misc
 */

/*
 * SQL sanitization
 */

// Run all strings to be used in an SQL query through this proc first to properly escape out injection attempts.
/proc/sanitizeSQL(t as text)
	var/sqltext = dbcon.Quote(t);
	return copytext(sqltext, 2, lentext(sqltext));//Quote() adds quotes around input, we already do that

/proc/format_table_name(table as text)
	return sqlfdbktableprefix + table

/*
 * Text sanitization
 */

//Simply removes < and > and limits the length of the message
/proc/strip_html_simple(t,limit=MAX_MESSAGE_LEN)
	var/list/strip_chars = list("<",">")
	t = copytext(t,1,limit)
	for(var/char in strip_chars)
		var/index = findtext(t, char)
		while(index)
			t = copytext(t, 1, index) + copytext(t, index+1)
			index = findtext(t, char)
	return t

//Removes a few problematic characters
/proc/sanitize_simple(t,list/repl_chars = list("\n"="#","\t"="#"))
	for(var/char in repl_chars)
		var/index = findtext(t, char)
		while(index)
			t = copytext(t, 1, index) + repl_chars[char] + copytext(t, index+1)
			index = findtext(t, char, index+1)
	return t

//Runs byond's sanitization proc along-side sanitize_simple
/proc/sanitize(t,list/repl_chars = null)
	return html_encode(sanitize_simple(t,repl_chars))

//Runs sanitize and strip_html_simple
//I believe strip_html_simple() is required to run first to prevent '<' from displaying as '<' after sanitize() calls byond's html_encode()
/proc/strip_html(t,limit=MAX_MESSAGE_LEN)
	return copytext((sanitize(strip_html_simple(t))),1,limit)

//Runs byond's sanitization proc along-side strip_html_simple
//I believe strip_html_simple() is required to run first to prevent '<' from displaying as '<' that html_encode() would cause
/proc/adminscrub(t,limit=MAX_MESSAGE_LEN)
	return copytext((html_encode(strip_html_simple(t))),1,limit)

//Returns null if there is any bad text in the string
/proc/reject_bad_text(text, max_length=512)
	if(length(text) > max_length)
		return			//message too long
	var/non_whitespace = 0
	for(var/i=1, i<=length(text), i++)
		switch(text2ascii(text,i))
			if(62,60,92,47)
				return			//rejects the text if it contains these bad characters: <, >, \ or /
			if(127 to 255)
				return			//rejects weird letters like �
			if(0 to 31)
				return			//more weird stuff
			if(32)
				continue		//whitespace
			else
				non_whitespace = 1
	if(non_whitespace)
		return text		//only accepts the text if it has some non-spaces

// Used to get a properly sanitized input, of max_length
/proc/stripped_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN)
	var/name = input(user, message, title, default) as text|null
	return trim(html_encode(name), max_length) //trim is "outside" because html_encode can expand single symbols into multiple symbols (such as turning < into <)

// Used to get a properly sanitized multiline input, of max_length
/proc/stripped_multiline_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN)
	var/name = input(user, message, title, default) as message|null
	return html_encode(trim(name, max_length))

//Filters out undesirable characters from names
/proc/reject_bad_name(t_in, allow_numbers=0, max_length=MAX_NAME_LEN)
	if(!t_in || length(t_in) > max_length)
		return //Rejects the input if it is null or if it is longer then the max length allowed

	var/number_of_alphanumeric	= 0
	var/last_char_group			= 0
	var/t_out = ""

	for(var/i=1, i<=length(t_in), i++)
		var/ascii_char = text2ascii(t_in,i)
		switch(ascii_char)
			// A  .. Z
			if(65 to 90)			//Uppercase Letters
				t_out += ascii2text(ascii_char)
				number_of_alphanumeric++
				last_char_group = 4

			// a  .. z
			if(97 to 122)			//Lowercase Letters
				if(last_char_group<2)
					t_out += ascii2text(ascii_char-32)	//Force uppercase first character
				else
					t_out += ascii2text(ascii_char)
				number_of_alphanumeric++
				last_char_group = 4

			// 0  .. 9
			if(48 to 57)			//Numbers
				if(!last_char_group)
					continue	//suppress at start of string
				if(!allow_numbers)
					continue
				t_out += ascii2text(ascii_char)
				number_of_alphanumeric++
				last_char_group = 3

			// '  -  .
			if(39,45,46)			//Common name punctuation
				if(!last_char_group)
					continue
				t_out += ascii2text(ascii_char)
				last_char_group = 2

			// ~   |   @  :  #  $  %  &  *  +
			if(126,124,64,58,35,36,37,38,42,43)			//Other symbols that we'll allow (mainly for AI)
				if(!last_char_group)
					continue	//suppress at start of string
				if(!allow_numbers)
					continue
				t_out += ascii2text(ascii_char)
				last_char_group = 2

			//Space
			if(32)
				if(last_char_group <= 1)
					continue	//suppress double-spaces and spaces at start of string
				t_out += ascii2text(ascii_char)
				last_char_group = 1
			else
				return

	if(number_of_alphanumeric < 2)
		return		//protects against tiny names like "A" and also names like "' ' ' ' ' ' ' '"

	if(last_char_group == 1)
		t_out = copytext(t_out,1,length(t_out))	//removes the last character (in this case a space)

	for(var/bad_name in list("space","floor","wall","r-wall","monkey","unknown","inactive ai"))	//prevents these common metagamey names
		if(cmptext(t_out,bad_name))
			return	//(not case sensitive)

	return t_out

//html_encode helper proc that returns the smallest non null of two numbers
//or 0 if they're both null (needed because of findtext returning 0 when a value is not present)
/proc/non_zero_min(a, b)
	if(!a)
		return b
	if(!b)
		return a
	return (a < b ? a : b)

/*
 * Text searches
 */

//Checks the beginning of a string for a specified sub-string
//Returns the position of the substring or 0 if it was not found
/proc/dd_hasprefix(text, prefix)
	var/start = 1
	var/end = length(prefix) + 1
	return findtext(text, prefix, start, end)

//Checks the beginning of a string for a specified sub-string. This proc is case sensitive
//Returns the position of the substring or 0 if it was not found
/proc/dd_hasprefix_case(text, prefix)
	var/start = 1
	var/end = length(prefix) + 1
	return findtextEx(text, prefix, start, end)

//Checks the end of a string for a specified substring.
//Returns the position of the substring or 0 if it was not found
/proc/dd_hassuffix(text, suffix)
	var/start = length(text) - length(suffix)
	if(start)
		return findtext(text, suffix, start, null)
	return

//Checks the end of a string for a specified substring. This proc is case sensitive
//Returns the position of the substring or 0 if it was not found
/proc/dd_hassuffix_case(text, suffix)
	var/start = length(text) - length(suffix)
	if(start)
		return findtextEx(text, suffix, start, null)

//Adds 'u' number of zeros ahead of the text 't'
/proc/add_zero(t, u)
	while (length(t) < u)
		t = "0[t]"
	return t

//Adds 'u' number of spaces ahead of the text 't'
/proc/add_lspace(t, u)
	while(length(t) < u)
		t = " [t]"
	return t

//Adds 'u' number of spaces behind the text 't'
/proc/add_tspace(t, u)
	while(length(t)  32)
			return copytext(text, i)
	return ""

//Returns a string with reserved characters and spaces after the last letter removed
/proc/trim_right(text)
	for (var/i = length(text), i > 0, i--)
		if (text2ascii(text, i) > 32)
			return copytext(text, 1, i + 1)

	return ""

//Returns a string with reserved characters and spaces before the first word and after the last word removed.
/proc/trim(text, max_length)
	if(max_length)
		text = copytext(text, 1, max_length)
	return trim_left(trim_right(text))

//Returns a string with the first element of the string capitalized.
/proc/capitalize(t as text)
	return uppertext(copytext(t, 1, 2)) + copytext(t, 2)

//Centers text by adding spaces to either side of the string.
/proc/dd_centertext(message, length)
	var/new_message = message
	var/size = length(message)
	var/delta = length - size
	if(size == length)
		return new_message
	if(size > length)
		return copytext(new_message, 1, length + 1)
	if(delta == 1)
		return new_message + " "
	if(delta % 2)
		new_message = " " + new_message
		delta--
	var/spaces = add_lspace("",delta/2-1)
	return spaces + new_message + spaces

//Limits the length of the text. Note: MAX_MESSAGE_LEN and MAX_NAME_LEN are widely used for this purpose
/proc/dd_limittext(message, length)
	var/size = length(message)
	if(size <= length)
		return message
	return copytext(message, 1, length + 1)

/proc/stringmerge(text,compare,replace = "*")
//This proc fills in all spaces with the "replace" var (* by default) with whatever
//is in the other string at the same spot (assuming it is not a replace char).
//This is used for fingerprints
	var/newtext = text
	if(lentext(text) != lentext(compare))
		return 0
	for(var/i = 1, i < lentext(text), i++)
		var/a = copytext(text,i,i+1)
		var/b = copytext(compare,i,i+1)
//if it isn't both the same letter, or if they are both the replacement character
//(no way to know what it was supposed to be)
		if(a != b)
			if(a == replace) //if A is the replacement char
				newtext = copytext(newtext,1,i) + b + copytext(newtext, i+1)
			else if(b == replace) //if B is the replacement char
				newtext = copytext(newtext,1,i) + a + copytext(newtext, i+1)
			else //The lists disagree, Uh-oh!
				return 0
	return newtext

/proc/stringpercent(text,character = "*")
//This proc returns the number of chars of the string that is the character
//This is used for detective work to determine fingerprint completion.
	if(!text || !character)
		return 0
	var/count = 0
	for(var/i = 1, i <= lentext(text), i++)
		var/a = copytext(text,i,i+1)
		if(a == character)
			count++
	return count

/proc/reverse_text(text = "")
	var/new_text = ""
	for(var/i = length(text); i > 0; i--)
		new_text += copytext(text, i, i+1)
	return new_text

var/list/zero_character_only = list("0")
var/list/hex_characters = list("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f")
var/list/alphabet = list("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z")
var/list/binary = list("0","1")
/proc/random_string(length, list/characters)
	. = ""
	for(var/i=1, i<=length, i++)
		. += pick(characters)

/proc/repeat_string(times, string="")
	. = ""
	for(var/i=1, i<=times, i++)
		. += string

/proc/random_short_color()
	return random_string(3, hex_characters)

/proc/random_color()
	return random_string(6, hex_characters)

/proc/add_zero2(t, u)
	var/temp1
	while (length(t)  u)
		temp1 = copytext(t,2,u+1)
	return temp1

//merges non-null characters (3rd argument) from "from" into "into". Returns result
//e.g. into = "Hello World"
//     from = "Seeya______"
//     returns"Seeya World"
//The returned text is always the same length as into
//This was coded to handle DNA gene-splicing.
/proc/merge_text(into, from, null_char="_")
	. = ""
	if(!istext(into))
		into = ""
	if(!istext(from))
		from = ""
	var/null_ascii = istext(null_char) ? text2ascii(null_char,1) : null_char

	var/previous = 0
	var/start = 1
	var/end = length(into) + 1

	for(var/i=1, i<end, i++)
		var/ascii = text2ascii(from, i)
		if(ascii == null_ascii)
			if(previous != 1)
				. += copytext(from, start, i)
				start = i
				previous = 1
		else
			if(previous != 0)
				. += copytext(into, start, i)
				start = i
				previous = 0

	if(previous == 0)
		. += copytext(from, start, end)
	else
		. += copytext(into, start, end)

//finds the first occurrence of one of the characters from needles argument inside haystack
//it may appear this can be optimised, but it really can't. findtext() is so much faster than anything you can do in byondcode.
//stupid byond :(
/proc/findchar(haystack, needles, start=1, end=0)
	var/temp
	var/len = length(needles)
	for(var/i=1, i<=len, i++)
		temp = findtextEx(haystack, ascii2text(text2ascii(needles,i)), start, end)	//Note: ascii2text(text2ascii) is faster than copytext()
		if(temp)
			end = temp
	return end

/proc/parsepencode(t, mob/user=null, signfont=SIGNFONT)
	if(length(t) < 1)		//No input means nothing needs to be parsed
		return

	t = replacetext(t, "\[center\]", "")
	t = replacetext(t, "\[/center\]", "")
	t = replacetext(t, "\[br\]", "
")
	t = replacetext(t, "\[b\]", "")
	t = replacetext(t, "\[/b\]", "")
	t = replacetext(t, "\[i\]", "")
	t = replacetext(t, "\[/i\]", "")
	t = replacetext(t, "\[u\]", "")
	t = replacetext(t, "\[/u\]", "")
	t = replacetext(t, "\[large\]", "")
	t = replacetext(t, "\[/large\]", "")
	if(user)
		t = replacetext(t, "\[sign\]", "[user.real_name]")
	else
		t = replacetext(t, "\[sign\]", "")
	t = replacetext(t, "\[field\]", "")

	t = replacetext(t, "\[*\]", "
")
	t = replacetext(t, "\[hr\]", "
")
	t = replacetext(t, "\[small\]", "")
	t = replacetext(t, "\[/small\]", "")
	t = replacetext(t, "\[list\]", "
")
	t = replacetext(t, "\[/list\]", "
")

	return t

/proc/char_split(t)
	. = list()
	for(var/x in 1 to length(t))
		. += copytext(t,x,x+1)


code:
//Returns the world time in english
/proc/worldtime2text()
	return gameTimestamp("hh:mm")

/proc/time_stamp(format = "hh:mm:ss")
	return time2text(world.timeofday, format)

/proc/gameTimestamp(format = "hh:mm:ss") // Get the game time in text
	return time2text(world.time - timezoneOffset + 432000, format)

/* Returns 1 if it is the selected month and day */
/proc/isDay(month, day)
	if(isnum(month) && isnum(day))
		var/MM = text2num(time2text(world.timeofday, "MM")) // get the current month
		var/DD = text2num(time2text(world.timeofday, "DD")) // get the current day
		if(month == MM && day == DD)
			return 1

		// Uncomment this out when debugging!
		//else
			//return 1

//returns timestamp in a sql and ISO 8601 friendly format
/proc/SQLtime()
	return time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")


code:
/*
 * Holds procs designed to change one type of value, into another.
 * Contains:
 *			hex2num & num2hex
 *			file2list
 *			angle2dir
 *			angle2text
 *			worldtime2text
 *			text2dir_extended & dir2text_short
 */

//Returns an integer given a hex input, supports negative values "-ff"
//skips preceding invalid characters
//breaks when hittin invalid characters thereafter
/proc/hex2num(hex)
	. = 0
	if(istext(hex))
		var/negative = 0
		var/len = length(hex)
		for(var/i=1, i<=len, i++)
			var/num = text2ascii(hex,i)
			switch(num)
				if(48 to 57)
					num -= 48	//0-9
				if(97 to 102)
					num -= 87	//a-f
				if(65 to 70)
					num -= 55	//A-F
				if(45)
					negative = 1//-
				else
					if(num)
						break
					else
						continue
			. *= 16
			. += num
		if(negative)
			. *= -1
	return .

//Returns the hex value of a decimal number
//len == length of returned string
//if len < 0 then the returned string will be as long as it needs to be to contain the data
//Only supports positive numbers
//if an invalid number is provided, it assumes num==0
//Note, unlike previous versions, this one works from low to high <-- that way
/proc/num2hex(num, len=2)
	if(!isnum(num))
		num = 0
	num = round(abs(num))
	. = ""
	var/i=0
	while(1)
		if(len<=0)
			if(!num)
				break
		else
			if(i>=len)
				break
		var/remainder = num/16
		num = round(remainder)
		remainder = (remainder - num) * 16
		switch(remainder)
			if(9,8,7,6,5,4,3,2,1)
				. = "[remainder]" + .
			if(10,11,12,13,14,15)
				. = ascii2text(remainder+87) + .
			else
				. = "0" + .
		i++
	return .

//Splits the text of a file at seperator and returns them in a list.
/proc/file2list(filename, seperator="\n")
	return splittext(return_file_text(filename),seperator)

//Turns a direction into text
/proc/dir2text(direction)
	switch(direction)
		if(1)
			return "north"
		if(2)
			return "south"
		if(4)
			return "east"
		if(8)
			return "west"
		if(5)
			return "northeast"
		if(6)
			return "southeast"
		if(9)
			return "northwest"
		if(10)
			return "southwest"
		else
	return

//Turns text into proper directions
/proc/text2dir(direction)
	switch(uppertext(direction))
		if("NORTH")
			return 1
		if("SOUTH")
			return 2
		if("EAST")
			return 4
		if("WEST")
			return 8
		if("NORTHEAST")
			return 5
		if("NORTHWEST")
			return 9
		if("SOUTHEAST")
			return 6
		if("SOUTHWEST")
			return 10
		else
	return

//Converts an angle (degrees) into an ss13 direction
/proc/angle2dir(degree)

	degree = SimplifyDegrees(degree)

	if(degree < 45)
		return NORTH
	if(degree < 90)
		return NORTHEAST
	if(degree < 135)
		return EAST
	if(degree < 180)
		return SOUTHEAST
	if(degree < 225)
		return SOUTH
	if(degree < 270)
		return SOUTHWEST
	if(degree < 315)
		return WEST
	return NORTH|WEST

//returns the north-zero clockwise angle in degrees, given a direction

/proc/dir2angle(D)
	switch(D)
		if(NORTH)
			return 0
		if(SOUTH)
			return 180
		if(EAST)
			return 90
		if(WEST)
			return 270
		if(NORTHEAST)
			return 45
		if(SOUTHEAST)
			return 135
		if(NORTHWEST)
			return 315
		if(SOUTHWEST)
			return 225
		else
			return null

//Returns the angle in english
/proc/angle2text(degree)
	return dir2text(angle2dir(degree))

//Converts a blend_mode constant to one acceptable to icon.Blend()
/proc/blendMode2iconMode(blend_mode)
	switch(blend_mode)
		if(BLEND_MULTIPLY)
			return ICON_MULTIPLY
		if(BLEND_ADD)
			return ICON_ADD
		if(BLEND_SUBTRACT)
			return ICON_SUBTRACT
		else
			return ICON_OVERLAY

//Converts a rights bitfield into a string
/proc/rights2text(rights, seperator="", list/adds, list/subs)
	if(rights & R_BUILDMODE)
		. += "[seperator]+BUILDMODE"
	if(rights & R_ADMIN)
		. += "[seperator]+ADMIN"
	if(rights & R_BAN)
		. += "[seperator]+BAN"
	if(rights & R_FUN)
		. += "[seperator]+FUN"
	if(rights & R_SERVER)
		. += "[seperator]+SERVER"
	if(rights & R_DEBUG)
		. += "[seperator]+DEBUG"
	if(rights & R_POSSESS)
		. += "[seperator]+POSSESS"
	if(rights & R_PERMISSIONS)
		. += "[seperator]+PERMISSIONS"
	if(rights & R_STEALTH)
		. += "[seperator]+STEALTH"
	if(rights & R_REJUVINATE)
		. += "[seperator]+REJUVINATE"
	if(rights & R_VAREDIT)
		. += "[seperator]+VAREDIT"
	if(rights & R_SOUNDS)
		. += "[seperator]+SOUND"
	if(rights & R_SPAWN)
		. += "[seperator]+SPAWN"

	for(var/verbpath in adds)
		. += "[seperator]+[verbpath]"
	for(var/verbpath in subs)
		. += "[seperator]-[verbpath]"
	return .

/proc/ui_style2icon(ui_style)
	switch(ui_style)
		if("Retro")
			return 'icons/mob/screen_retro.dmi'
		if("Plasmafire")
			return 'icons/mob/screen_plasmafire.dmi'
		if("Slimecore")
			return 'icons/mob/screen_slimecore.dmi'
		if("Operative")
			return 'icons/mob/screen_operative.dmi'
		else
			return 'icons/mob/screen_midnight.dmi'

//colour formats
/proc/rgb2hsl(red, green, blue)
	red /= 255;green /= 255;blue /= 255;
	var/max = max(red,green,blue)
	var/min = min(red,green,blue)
	var/range = max-min

	var/hue=0;var/saturation=0;var/lightness=0;
	lightness = (max + min)/2
	if(range != 0)
		if(lightness < 0.5)
			saturation = range/(max+min)
		else
			saturation = range/(2-max-min)

		var/dred = ((max-red)/(6*max)) + 0.5
		var/dgreen = ((max-green)/(6*max)) + 0.5
		var/dblue = ((max-blue)/(6*max)) + 0.5

		if(max==red)
			hue = dblue - dgreen
		else if(max==green)
			hue = dred - dblue + (1/3)
		else
			hue = dgreen - dred + (2/3)
		if(hue < 0)
			hue++
		else if(hue > 1)
			hue--

	return list(hue, saturation, lightness)

/proc/hsl2rgb(hue, saturation, lightness)
	var/red;var/green;var/blue;
	if(saturation == 0)
		red = lightness * 255
		green = red
		blue = red
	else
		var/a;var/b;
		if(lightness < 0.5)
			b = lightness*(1+saturation)
		else
			b = (lightness+saturation) - (saturation*lightness)
		a = 2*lightness - b

		red = round(255 * hue2rgb(a, b, hue+(1/3)))
		green = round(255 * hue2rgb(a, b, hue))
		blue = round(255 * hue2rgb(a, b, hue-(1/3)))

	return list(red, green, blue)

/proc/hue2rgb(a, b, hue)
	if(hue < 0)
		hue++
	else if(hue > 1)
		hue--
	if(6*hue < 1)
		return (a+(b-a)*6*hue)
	if(2*hue < 1)
		return b
	if(3*hue < 2)
		return (a+(b-a)*((2/3)-hue)*6)
	return a

// Very ugly, BYOND doesn't support unix time and rounding errors make it really hard to convert it to BYOND time.
// returns "YYYY-MM-DD" by default
/proc/unix2date(timestamp, seperator = "-")

	if(timestamp < 0)
		return 0 //Do not accept negative values

	var/year = 1970 //Unix Epoc begins 1970-01-01
	var/dayInSeconds = 86400 //60secs*60mins*24hours
	var/daysInYear = 365 //Non Leap Year
	var/daysInLYear = daysInYear + 1//Leap year
	var/days = round(timestamp / dayInSeconds) //Days passed since UNIX Epoc
	var/tmpDays = days + 1 //If passed (timestamp < dayInSeconds), it will return 0, so add 1
	var/monthsInDays = list() //Months will be in here ***Taken from the PHP source code***
	var/month = 1 //This will be the returned MONTH NUMBER.
	var/day //This will be the returned day number.

	while(tmpDays > daysInYear) //Start adding years to 1970
		year++
		if(isLeap(year))
			tmpDays -= daysInLYear
		else
			tmpDays -= daysInYear

	if(isLeap(year)) //The year is a leap year
		monthsInDays = list(-1,30,59,90,120,151,181,212,243,273,304,334)
	else
		monthsInDays = list(0,31,59,90,120,151,181,212,243,273,304,334)

	var/mDays = 0;
	var/monthIndex = 0;

	for(var/m in monthsInDays)
		monthIndex++
		if(tmpDays > m)
			mDays = m
			month = monthIndex

	day = tmpDays - mDays //Setup the date

	return "[year][seperator][((month < 10) ? "0[month]" : month)][seperator][((day < 10) ? "0[day]" : day)]"

/*
var/list/test_times = list("December" = 1323522004, "August" = 1123522004, "January" = 1011522004,
						   "Jan Leap" = 946684800, "Jan Normal" = 978307200, "New Years Eve" = 1009670400,
						   "New Years" = 1009836000, "New Years 2" = 1041372000, "New Years 3" = 1104530400,
						   "July Month End" = 744161003, "July Month End 12" = 1343777003, "End July" = 1091311200)
for(var/t in test_times)
	world.log << "TEST: [t] is [unix2date(test_times[t])]"
*/

/proc/isLeap(y)
	return ((y) % 4 == 0 && ((y) % 100 != 0 || (y) % 400 == 0))

// A copy of text2dir, extended to accept one and two letter
//  directions, and to clearly return 0 otherwise.
/proc/text2dir_extended(direction)
	switch(uppertext(direction))
		if("NORTH", "N")
			return 1
		if("SOUTH", "S")
			return 2
		if("EAST", "E")
			return 4
		if("WEST", "W")
			return 8
		if("NORTHEAST", "NE")
			return 5
		if("NORTHWEST", "NW")
			return 9
		if("SOUTHEAST", "SE")
			return 6
		if("SOUTHWEST", "SW")
			return 10
		else
	return 0

// A copy of dir2text, which returns the short one or two letter
//  directions used in tube icon states.
/proc/dir2text_short(direction)
	switch(direction)
		if(1)
			return "N"
		if(2)
			return "S"
		if(4)
			return "E"
		if(8)
			return "W"
		if(5)
			return "NE"
		if(6)
			return "SE"
		if(9)
			return "NW"
		if(10)
			return "SW"
		else
	return

//Turns a Body_parts_covered bitfield into a list of organ/limb names.
//(I challenge you to find a use for this)
/proc/body_parts_covered2organ_names(bpc)
	var/list/covered_parts = list()

	if(!bpc)
		return 0

	if(bpc & FULL_BODY)
		covered_parts |= list("l_arm","r_arm","head","chest","l_leg","r_leg")

	else
		if(bpc & HEAD)
			covered_parts |= list("head")
		if(bpc & CHEST)
			covered_parts |= list("chest")
		if(bpc & GROIN)
			covered_parts |= list("chest")

		if(bpc & ARMS)
			covered_parts |= list("l_arm","r_arm")
		else
			if(bpc & ARM_LEFT)
				covered_parts |= list("l_arm")
			if(bpc & ARM_RIGHT)
				covered_parts |= list("r_arm")

		if(bpc & HANDS)
			covered_parts |= list("l_arm","r_arm")
		else
			if(bpc & HAND_LEFT)
				covered_parts |= list("l_arm")
			if(bpc & HAND_RIGHT)
				covered_parts |= list("r_arm")

		if(bpc & LEGS)
			covered_parts |= list("l_leg","r_leg")
		else
			if(bpc & LEG_LEFT)
				covered_parts |= list("l_leg")
			if(bpc & LEG_RIGHT)
				covered_parts |= list("r_leg")

		if(bpc & FEET)
			covered_parts |= list("l_leg","r_leg")
		else
			if(bpc & FOOT_LEFT)
				covered_parts |= list("l_leg")
			if(bpc & FOOT_RIGHT)
				covered_parts |= list("r_leg")

	return covered_parts

//adapted from http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
/proc/heat2colour(temp)
	return rgb(heat2colour_r(temp), heat2colour_g(temp), heat2colour_b(temp))

/proc/heat2colour_r(temp)
	temp /= 100
	if(temp <= 66)
		. = 255
	else
		. = max(0, min(255, 329.698727446 * (temp - 60) ** -0.1332047592))

/proc/heat2colour_g(temp)
	temp /= 100
	if(temp <= 66)
		. = max(0, min(255, 99.4708025861 * log(temp) - 161.1195681661))
	else
		. = max(0, min(255, 288.1221685293 * ((temp - 60) ** -0.075148492)))

/proc/heat2colour_b(temp)
	temp /= 100
	if(temp >= 66)
		. = 255
	else
		if(temp <= 16)
			. = 0
		else
			. = max(0, min(255, 138.5177312231 * log(temp - 10) - 305.0447927307))

/proc/color2hex(color)	//web colors
	if(!color)
		return "#000000"

	switch(color)
		if("white")
			return "#FFFFFF"
		if("black")
			return "#000000"
		if("gray")
			return "#808080"
		if("brown")
			return "#A52A2A"
		if("red")
			return "#FF0000"
		if("darkred")
			return "#8B0000"
		if("crimson")
			return "#DC143C"
		if("orange")
			return "#FFA500"
		if("yellow")
			return "#FFFF00"
		if("green")
			return "#008000"
		if("lime")
			return "#00FF00"
		if("darkgreen")
			return "#006400"
		if("cyan")
			return "#00FFFF"
		if("blue")
			return "#0000FF"
		if("navy")
			return "#000080"
		if("teal")
			return "#008080"
		if("purple")
			return "#800080"
		if("indigo")
			return "#4B0082"
		else
			return "#FFFFFF"

//This is a weird one:
//It returns a list of all var names found in the string
//These vars must be in the [var_name] format
//It's only a proc because it's used in more than one place

//Takes a string and a datum
//The string is well, obviously the string being checked
//The datum is used as a source for var names, to check validity
//Otherwise every single word could technically be a variable!
/proc/string2listofvars(var/t_string, var/datum/var_source)
	if(!t_string || !var_source)
		return list()

	. = list()

	var/var_found = findtext(t_string,"\[") //Not the actual variables, just a generic "should we even bother" check
	if(var_found)
		//Find var names

		// "A dog said hi [name]!"
		// splittext() --> list("A dog said hi ","name]!"
		// jointext() --> "A dog said hi name]!"
		// splittext() --> list("A","dog","said","hi","name]!")

		t_string = replacetext(t_string,"\[","\[ ")//Necessary to resolve "word[var_name]" scenarios
		var/list/list_value = splittext(t_string,"\[")
		var/intermediate_stage = jointext(list_value, null)

		list_value = splittext(intermediate_stage," ")
		for(var/value in list_value)
			if(findtext(value,"]"))
				value = splittext(value,"]") //"name]!" --> list("name","!")
				for(var/A in value)
					if(var_source.vars.Find(A))
						. += A

//assumes format #RRGGBB #rrggbb
/proc/color_hex2num(A)
	if(!A)
		return 0
	var/R = hex2num(copytext(A,2,4))
	var/G = hex2num(copytext(A,4,6))
	var/B = hex2num(copytext(A,6,0))
	return R+G+B

//Converts a positive interger to its roman numeral equivilent. Ignores any decimals.
//Numbers over 3999 will display with extra "M"s (don't tell the Romans) and can get comically long, so be careful.
/proc/num2roman(A)
	var/list/values = list("M" = 1000, "CM" = 900, "D" = 500, "CD" = 400, "C" = 100, "XC" = 90, "L" = 50, "XL" = 40, "X" = 10, "IX" = 9, "V" = 5, "IV" = 4, "I" = 1)
	if(!A || !isnum(A))
		return 0
	while(A >= 1)
		for(var/i in values)
			if(A >= values[i])
				. += i
				A -= values[i]
				break