code:
var/global/BSACooldown = 0

////////////////////////////////
/proc/message_admins(msg)
	msg = "ADMIN LOG: [msg]"
	admins << msg

///////////////////////////////////////////////////////////////////////////////////////////////Panels

/datum/admins/proc/show_player_panel(mob/M in mob_list)
	set category = "Admin"
	set name = "Show Player Panel"
	set desc="Edit player (respawn, ban, heal, etc)"

	if(!check_rights())
		return

	if(!M)
		usr << "You seem to be selecting a mob that doesn't exist anymore."
		return

	var/body = "<title>Options for [M.key]</title>"
	body += "Options panel for [M]"
	if(M.client)
		body += " played by [M.client] "
		body += "\[[M.client.holder ? M.client.holder.rank : "Player"]\]"

	if(istype(M, /mob/new_player))
		body += " Hasn't Entered Game "
	else
		body += " \[Heal\] "

	body += "

\[ "
	body += "VV - "
	body += "TP - "
	body += "PM - "
	body += "SM - "
	body += "FLW\]
"

	body += "Mob type = [M.type]

"

	body += "Kick | "
	body += "Ban | "
	body += "Jobban | "
	body += "Identity Ban | "
	if(jobban_isbanned(M, "OOC"))
		body+= "OOCBan | "
	else
		body+= "OOCBan | "
	if(jobban_isbanned(M, "emote"))
		body+= "EmoteBan | "
	else
		body+= "Emoteban | "

	body += "Notes | "
	if(M.client)
		if(M.client.check_watchlist(M.client.ckey))
			body += "Remove from Watchlist | "
			body += "Edit Watchlist reason "
		else
			body += "Add to Watchlist "

		body += "| Prison | "
		body += "\ Send back to Lobby | "
		var/muted = M.client.prefs.muted
		body += "
Mute:  "
		body += "\[IC | "
		body += "OOC | "
		body += "PRAY | "
		body += "ADMINHELP | "
		body += "DEADCHAT\]"
		body += "(toggle all)"

	body += "

"
	body += "Jump to | "
	body += "Get | "
	body += "Send To"

	body += "

"
	body += "Traitor panel | "
	body += "Narrate to | "
	body += "Subtle message"

	if (M.client)
		if(!istype(M, /mob/new_player))
			body += "

"
			body += "Transformation:"
			body += "
"

			//Human
			if(ishuman(M))
				body += "Human | "
			else
				body += "Humanize | "

			//Monkey
			if(ismonkey(M))
				body += "Monkeyized | "
			else
				body += "Monkeyize | "

			//Corgi
			if(iscorgi(M))
				body += "Corgized | "
			else
				body += "Corgize | "

			//AI / Cyborg
			if(isAI(M))
				body += "Is an AI "
			else if(ishuman(M))
				body += "Make AI | "
				body += "Make Robot | "
				body += "Make Alien | "
				body += "Make Slime | "
				body += "Make Blob | "

			//Simple Animals
			if(isanimal(M))
				body += "Re-Animalize | "
			else
				body += "Animalize | "

			body += "

"
			body += "Rudimentary transformation:
These transformations only create a new mob type and copy stuff over. They do not take into account MMIs and similar mob-specific things. The buttons in 'Transformations' are preferred, when possible.
"
			body += "Observer | "
			body += "\[ Alien: Drone, "
			body += "Hunter, "
			body += "Sentinel, "
			body += "Praetorian, "
			body += "Queen, "
			body += "Larva \] "
			body += "Human "
			body += "\[ slime: Baby, "
			body += "Adult \] "
			body += "Monkey | "
			body += "Cyborg | "
			body += "Cat | "
			body += "Runtime | "
			body += "Corgi | "
			body += "Ian | "
			body += "Crab | "
			body += "Coffee | "
			//body += "Parrot | "
			//body += "Poly | "
			body += "\[ Construct: Juggernaut , "
			body += "Artificer , "
			body += "Wraith \] "
			body += "Shade"
			body += "
"

	if (M.client)
		body += "

"
		body += "Other actions:"
		body += "
"
		body += "Forcesay | "
		body += "Thunderdome 1 | "
		body += "Thunderdome 2 | "
		body += "Thunderdome Admin | "
		body += "Thunderdome Observer | "

	body += "
"
	body += ""

	usr << browse(body, "window=adminplayeropts-\ref[M];size=550x515")
	feedback_add_details("admin_verb","SPP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/access_news_network() //MARKER
	set category = "Fun"
	set name = "Access Newscaster Network"
	set desc = "Allows you to view, add and edit news feeds."

	if (!istype(src,/datum/admins))
		src = usr.client.holder
	if (!istype(src,/datum/admins))
		usr << "Error: you are not an admin!"
		return
	var/dat
	dat = text("<TITLE>Admin Newscaster</TITLE>
Admin Newscaster Unit
")

	switch(admincaster_screen)
		if(0)
			dat += "Welcome to the admin newscaster.
 Here you can add, edit and censor every newspiece on the network."
			dat += "
Feed channels and stories entered through here will be uneditable and handled as official news by the rest of the units."
			dat += "
Note that this panel allows full freedom over the news network, there are no constrictions except the few basic ones. Don't break things!"
			if(news_network.wanted_issue.active)
				dat+= "
Read Wanted Issue"
			dat+= "

Create Feed Channel"
			dat+= "
View Feed Channels"
			dat+= "
Submit new Feed story"
			dat+= "

Exit"
			var/wanted_already = 0
			if(news_network.wanted_issue.active)
				wanted_already = 1
			dat+="
Feed Security functions:
"
			dat+="
[(wanted_already) ? ("Manage") : ("Publish")] \"Wanted\" Issue"
			dat+="
Censor Feed Stories"
			dat+="
Mark Feed Channel with Nanotrasen D-Notice (disables and locks the channel."
			dat+="

The newscaster recognises you as:
 [src.admin_signature]"
		if(1)
			dat+= "Station Feed Channels
"
			if( isemptylist(news_network.network_channels) )
				dat+="No active channels found..."
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in news_network.network_channels)
					if(CHANNEL.is_admin_channel)
						dat+="[CHANNEL.channel_name]
"
					else
						dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ()]
"
			dat+="

Refresh"
			dat+="
Back"
		if(2)
			dat+="Creating new Feed Channel..."
			dat+="
Channel Name: [src.admincaster_feed_channel.channel_name]
"
			dat+="Channel Author: [src.admin_signature]
"
			dat+="Will Accept Public Feeds: [(src.admincaster_feed_channel.locked) ? ("NO") : ("YES")]

"
			dat+="
Submit

Cancel
"
		if(3)
			dat+="Creating new Feed Message..."
			dat+="
Receiving Channel: [src.admincaster_feed_channel.channel_name]
" //MARK
			dat+="Message Author: [src.admin_signature]
"
			dat+="Message Body: [src.admincaster_feed_message.returnBody(-1)]
"
			dat+="
Submit

Cancel
"
		if(4)
			dat+="Feed story successfully submitted to [src.admincaster_feed_channel.channel_name].

"
			dat+="
Return
"
		if(5)
			dat+="Feed Channel [src.admincaster_feed_channel.channel_name] created successfully.

"
			dat+="
Return
"
		if(6)
			dat+="ERROR: Could not submit Feed story to Network.

"
			if(src.admincaster_feed_channel.channel_name=="")
				dat+="Invalid receiving channel name.
"
			if(src.admincaster_feed_message.returnBody(-1) == "" || src.admincaster_feed_message.returnBody(-1) == "\[REDACTED\]")
				dat+="Invalid message body.
"
			dat+="
Return
"
		if(7)
			dat+="ERROR: Could not submit Feed Channel to Network.

"
			if(src.admincaster_feed_channel.channel_name =="" || src.admincaster_feed_channel.channel_name == "\[REDACTED\]")
				dat+="Invalid channel name.
"
			var/check = 0
			for(var/datum/newscaster/feed_channel/FC in news_network.network_channels)
				if(FC.channel_name == src.admincaster_feed_channel.channel_name)
					check = 1
					break
			if(check)
				dat+="Channel name already in use.
"
			dat+="
Return
"
		if(9)
			dat+="[admincaster_feed_channel.channel_name]: \[created by: [admincaster_feed_channel.returnAuthor(-1)]\]
"
			if(src.admincaster_feed_channel.censored)
				dat+="ATTENTION: This channel has been deemed as threatening to the welfare of the station, and marked with a Nanotrasen D-Notice.
"
				dat+="No further feed story additions are allowed while the D-Notice is in effect.

"
			else
				if( isemptylist(src.admincaster_feed_channel.messages) )
					dat+="No feed messages found in channel...
"
				else
					var/i = 0
					for(var/datum/newscaster/feed_message/MESSAGE in src.admincaster_feed_channel.messages)
						i++
						dat+="-[MESSAGE.returnBody(-1)]
"
						if(MESSAGE.img)
							usr << browse_rsc(MESSAGE.img, "tmp_photo[i].png")
							dat+="

"
						dat+="\[Story by [MESSAGE.returnAuthor(-1)]\]
"
						dat+="[MESSAGE.comments.len] comment[MESSAGE.comments.len > 1 ? "s" : ""]:
"
						for(var/datum/newscaster/feed_comment/comment in MESSAGE.comments)
							dat+="[comment.body]
[comment.author] [comment.time_stamp]
"
						dat+="
"
			dat+="

Refresh"
			dat+="
Back"
		if(10)
			dat+="Nanotrasen Feed Censorship Tool
"
			dat+="NOTE: Due to the nature of news Feeds, total deletion of a Feed Story is not possible.
"
			dat+="Keep in mind that users attempting to view a censored feed will instead see the \[REDACTED\] tag above it."
			dat+="
Select Feed channel to get Stories from:
"
			if(isemptylist(news_network.network_channels))
				dat+="No feed channels found active...
"
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in news_network.network_channels)
					dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ()]
"
			dat+="
Cancel"
		if(11)
			dat+="Nanotrasen D-Notice Handler
"
			dat+="A D-Notice is to be bestowed upon the channel if the handling Authority deems it as harmful for the station's"
			dat+="morale, integrity or disciplinary behaviour. A D-Notice will render a channel unable to be updated by anyone, without deleting any feed"
			dat+="stories it might contain at the time. You can lift a D-Notice if you have the required access at any time.
"
			if(isemptylist(news_network.network_channels))
				dat+="No feed channels found active...
"
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in news_network.network_channels)
					dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? ("***") : ()]
"

			dat+="
Back"
		if(12)
			dat+="[src.admincaster_feed_channel.channel_name]: \[ created by: [src.admincaster_feed_channel.returnAuthor(-1)] \]
"
			dat+="[(src.admincaster_feed_channel.authorCensor) ? ("Undo Author censorship") : ("Censor channel Author")]
"

			if( isemptylist(src.admincaster_feed_channel.messages) )
				dat+="No feed messages found in channel...
"
			else
				for(var/datum/newscaster/feed_message/MESSAGE in src.admincaster_feed_channel.messages)
					dat+="-[MESSAGE.returnBody(-1)]
\[Story by [MESSAGE.returnAuthor(-1)]\]
"
					dat+="[(MESSAGE.bodyCensor) ? ("Undo story censorship") : ("Censor story")]  -  [(MESSAGE.authorCensor) ? ("Undo Author Censorship") : ("Censor message Author")]
"
					dat+="[MESSAGE.comments.len] comment[MESSAGE.comments.len > 1 ? "s" : ""]: [MESSAGE.locked ? "Unlock" : "Lock"]
"
					for(var/datum/newscaster/feed_comment/comment in MESSAGE.comments)
						dat+="[comment.body] X
[comment.author] [comment.time_stamp]
"
			dat+="
Back"
		if(13)
			dat+="[src.admincaster_feed_channel.channel_name]: \[ created by: [src.admincaster_feed_channel.returnAuthor(-1)] \]
"
			dat+="Channel messages listed below. If you deem them dangerous to the station, you can Bestow a D-Notice upon the channel.
"
			if(src.admincaster_feed_channel.censored)
				dat+="ATTENTION: This channel has been deemed as threatening to the welfare of the station, and marked with a Nanotrasen D-Notice.
"
				dat+="No further feed story additions are allowed while the D-Notice is in effect.

"
			else
				if( isemptylist(src.admincaster_feed_channel.messages) )
					dat+="No feed messages found in channel...
"
				else
					for(var/datum/newscaster/feed_message/MESSAGE in src.admincaster_feed_channel.messages)
						dat+="-[MESSAGE.returnBody(-1)]
\[Story by [MESSAGE.returnAuthor(-1)]\]
"
			dat+="
Back"
		if(14)
			dat+="Wanted Issue Handler:"
			var/wanted_already = 0
			var/end_param = 1
			if(news_network.wanted_issue.active)
				wanted_already = 1
				end_param = 2
			if(wanted_already)
				dat+="
A wanted issue is already in Feed Circulation. You can edit or cancel it below."
			dat+="
"
			dat+="Criminal Name: [src.admincaster_wanted_message.criminal]
"
			dat+="Description: [src.admincaster_wanted_message.body]
"
			if(wanted_already)
				dat+="Wanted Issue created by:[news_network.wanted_issue.scannedUser]
"
			else
				dat+="Wanted Issue will be created under prosecutor:[src.admin_signature]
"
			dat+="
[(wanted_already) ? ("Edit Issue") : ("Submit")]"
			if(wanted_already)
				dat+="
Take down Issue"
			dat+="
Cancel"
		if(15)
			dat+="Wanted issue for [src.admincaster_wanted_message.criminal] is now in Network Circulation.

"
			dat+="
Return
"
		if(16)
			dat+="ERROR: Wanted Issue rejected by Network.

"
			if(src.admincaster_wanted_message.criminal =="" || src.admincaster_wanted_message.criminal == "\[REDACTED\]")
				dat+="Invalid name for person wanted.
"
			if(src.admincaster_wanted_message.body == "" || src.admincaster_wanted_message.body == "\[REDACTED\]")
				dat+="Invalid description.
"
			dat+="
Return
"
		if(17)
			dat+="Wanted Issue successfully deleted from Circulation
"
			dat+="
Return
"
		if(18)
			dat+="-- STATIONWIDE WANTED ISSUE --
\[Submitted by: [news_network.wanted_issue.scannedUser]\]
"
			dat+="Criminal: [news_network.wanted_issue.criminal]
"
			dat+="Description: [news_network.wanted_issue.body]
"
			dat+="Photo:: "
			if(news_network.wanted_issue.img)
				usr << browse_rsc(news_network.wanted_issue.img, "tmp_photow.png")
				dat+="
"
			else
				dat+="None"
			dat+="
Back
"
		if(19)
			dat+="Wanted issue for [src.admincaster_wanted_message.criminal] successfully edited.

"
			dat+="
Return
"
		else
			dat+="I'm sorry to break your immersion. This shit's bugged. Report this bug to Agouri, polyxenitopalidou@gmail.com"

	//world << "Channelname: [src.admincaster_feed_channel.channel_name] [src.admincaster_feed_channel.author]"
	//world << "Msg: [src.admincaster_feed_message.author] [src.admincaster_feed_message.body]"
	usr << browse(dat, "window=admincaster_main;size=400x600")
	onclose(usr, "admincaster_main")

/datum/admins/proc/Game()
	if(!check_rights(0))
		return

	var/dat = {"
		Game Panel
\n
		Change Game Mode

		"}
	if(master_mode == "secret")
		dat += "(Force Secret Mode)
"

	dat += {"

		Create Object

		Quick Create Object

		Create Turf

		Create Mob

		"}

	if(marked_datum && istype(marked_datum, /atom))
		dat += "Duplicate Marked Datum
"

	usr << browse(dat, "window=admin2;size=210x200")
	return

/////////////////////////////////////////////////////////////////////////////////////////////////admins2.dm merge
//i.e. buttons/verbs

/datum/admins/proc/restart()
	set category = "Server"
	set name = "Hard Restart"
	set desc="Restarts the world immediately"
	if (!usr.client.holder)
		return
	var/confirm = alert("Restart the game world?", "Restart", "Yes", "Cancel")
	if(confirm == "Cancel")
		return
	if(confirm == "Yes")
		ticker.delay_end = 0
		feedback_add_details("admin_verb","R") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
		world.Reboot("Initiated by [usr.client.holder.fakekey ? "Admin" : usr.key].", "end_error", "admin reboot - by [usr.key] [usr.client.holder.fakekey ? "(stealth)" : ""]", 10)

/datum/admins/proc/end_round()
	set category = "Server"
	set name = "End Round"
	set desc = "Attempts to produce a round end report and then restart the server organically."

	if (!usr.client.holder)
		return
	var/confirm = alert("End the round and  restart the game world?", "End Round", "Yes", "Cancel")
	if(confirm == "Cancel")
		return
	if(confirm == "Yes")
		ticker.force_ending = 1
		feedback_add_details("admin_verb","ER") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/announce()
	set category = "Special Verbs"
	set name = "Announce"
	set desc="Announce your desires to the world"
	if(!check_rights(0))
		return

	var/message = input("Global message to send:", "Admin Announce", null, null)  as message
	if(message)
		if(!check_rights(R_SERVER,0))
			message = adminscrub(message,500)
		world << "[usr.client.holder.fakekey ? "Administrator" : usr.key] Announces:\n \t [message]"
		log_admin("Announce: [key_name(usr)] : [message]")
	feedback_add_details("admin_verb","A") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/set_admin_notice()
	set category = "Special Verbs"
	set name = "Set Admin Notice"
	set desc ="Set an announcement that appears to everyone who joins the server. Only lasts this round"
	if(!check_rights(0))
		return

	var/new_admin_notice = input(src,"Set a public notice for this round. Everyone who joins the server will see it.\n(Leaving it blank will delete the current notice):","Set Notice",admin_notice) as message|null
	if(new_admin_notice == null)
		return
	if(new_admin_notice == admin_notice)
		return
	if(new_admin_notice == "")
		message_admins("[key_name(usr)] removed the admin notice.")
		log_admin("[key_name(usr)] removed the admin notice:\n[admin_notice]")
	else
		message_admins("[key_name(usr)] set the admin notice.")
		log_admin("[key_name(usr)] set the admin notice:\n[new_admin_notice]")
		world << "Admin Notice:\n \t [new_admin_notice]"
	feedback_add_details("admin_verb","SAN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	admin_notice = new_admin_notice
	return

/datum/admins/proc/toggleooc()
	set category = "Server"
	set desc="Toggle dis bitch"
	set name="Toggle OOC"
	toggle_ooc()
	log_admin("[key_name(usr)] toggled OOC.")
	message_admins("[key_name_admin(usr)] toggled OOC.")
	feedback_add_details("admin_verb","TOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/toggleoocdead()
	set category = "Server"
	set desc="Toggle dis bitch"
	set name="Toggle Dead OOC"
	dooc_allowed = !( dooc_allowed )

	log_admin("[key_name(usr)] toggled OOC.")
	message_admins("[key_name_admin(usr)] toggled Dead OOC.")
	feedback_add_details("admin_verb","TDOOC") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/*
/datum/admins/proc/toggletraitorscaling()
	set category = "Server"
	set desc="Toggle traitor scaling"
	set name="Toggle Traitor Scaling"
	traitor_scaling = !traitor_scaling
	log_admin("[key_name(usr)] toggled Traitor Scaling to [traitor_scaling].")
	message_admins("[key_name_admin(usr)] toggled Traitor Scaling [traitor_scaling ? "on" : "off"].")
	feedback_add_details("admin_verb","TTS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
*/
/datum/admins/proc/startnow()
	set category = "Server"
	set desc="Start the round RIGHT NOW"
	set name="Start Now"
	if(ticker.current_state == GAME_STATE_PREGAME)
		ticker.can_fire = 1
		ticker.timeLeft = 0
		log_admin("[usr.key] has started the game.")
		message_admins("[usr.key] has started the game.")
		feedback_add_details("admin_verb","SN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
		return 1
	else if (ticker.current_state == GAME_STATE_STARTUP)
		usr << "Error: Start Now: Game is in startup, please wait until it has finished."
	else
		usr << "Error: Start Now: Game has already started."

	return 0

/datum/admins/proc/toggleenter()
	set category = "Server"
	set desc="People can't enter"
	set name="Toggle Entering"
	enter_allowed = !( enter_allowed )
	if (!( enter_allowed ))
		world << "New players may no longer enter the game."
	else
		world << "New players may now enter the game."
	log_admin("[key_name(usr)] toggled new player game entering.")
	message_admins("[key_name_admin(usr)] toggled new player game entering.")
	world.update_status()
	feedback_add_details("admin_verb","TE") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/toggleAI()
	set category = "Server"
	set desc="People can't be AI"
	set name="Toggle AI"
	config.allow_ai = !( config.allow_ai )
	if (!( config.allow_ai ))
		world << "The AI job is no longer chooseable."
	else
		world << "The AI job is chooseable now."
	log_admin("[key_name(usr)] toggled AI allowed.")
	world.update_status()
	feedback_add_details("admin_verb","TAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/toggleaban()
	set category = "Server"
	set desc="Respawn basically"
	set name="Toggle Respawn"
	abandon_allowed = !( abandon_allowed )
	if (abandon_allowed)
		world << "You may now respawn."
	else
		world << "You may no longer respawn :("
	message_admins("[key_name_admin(usr)] toggled respawn to [abandon_allowed ? "On" : "Off"].")
	log_admin("[key_name(usr)] toggled respawn to [abandon_allowed ? "On" : "Off"].")
	world.update_status()
	feedback_add_details("admin_verb","TR") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/delay()
	set category = "Server"
	set desc="Delay the game start"
	set name="Delay pre-game"

	var/newtime = input("Set a new time in seconds. Set -1 for indefinite delay.","Set Delay",round(ticker.timeLeft/10)) as num|null
	if(ticker.current_state > GAME_STATE_PREGAME)
		return alert("Too late... The game has already started!")
	if(newtime)
		ticker.timeLeft = newtime * 10
		if(newtime < 0)
			world << "The game start has been delayed."
			log_admin("[key_name(usr)] delayed the round start.")
		else
			world << "The game will start in [newtime] seconds."
			world << 'sound/ai/attention.ogg'
			log_admin("[key_name(usr)] set the pre-game delay to [newtime] seconds.")
		feedback_add_details("admin_verb","DELAY") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/unprison(mob/M in mob_list)
	set category = "Admin"
	set name = "Unprison"
	if (M.z == ZLEVEL_CENTCOM)
		M.loc = pick(latejoin)
		message_admins("[key_name_admin(usr)] has unprisoned [key_name_admin(M)]")
		log_admin("[key_name(usr)] has unprisoned [key_name(M)]")
	else
		alert("[M.name] is not prisoned.")
	feedback_add_details("admin_verb","UP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

////////////////////////////////////////////////////////////////////////////////////////////////ADMIN HELPER PROCS

/*
/datum/admins/proc/get_sab_desc(var/target)
	switch(target)
		if(1)
			return "Destroy at least 70% of the plasma canisters on the station"
		if(2)
			return "Destroy the AI"
		if(3)
			var/count = 0
			for(var/mob/living/carbon/monkey/Monkey in world)
				if(Monkey.z == 1)
					count++
			return "Kill all [count] of the monkeys on the station"
		if(4)
			return "Cut power to at least 80% of the station"
		else
			return "Error: Invalid sabotage target: [target]"
*/
/datum/admins/proc/spawn_atom(object as text)
	set category = "Debug"
	set desc = "(atom path) Spawn an atom"
	set name = "Spawn"

	if(!check_rights(R_SPAWN))
		return

	var/chosen = pick_closest_path(object)
	if(!chosen)
		return
	if(ispath(chosen,/turf))
		var/turf/T = get_turf(usr.loc)
		T.ChangeTurf(chosen)
	else
		new chosen(usr.loc)

	log_admin("[key_name(usr)] spawned [chosen] at ([usr.x],[usr.y],[usr.z])")
	feedback_add_details("admin_verb","SA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/show_traitor_panel(mob/M in mob_list)
	set category = "Admin"
	set desc = "Edit mobs's memory and role"
	set name = "Show Traitor Panel"

	if(!istype(M))
		usr << "This can only be used on instances of type /mob"
		return
	if(!M.mind)
		usr << "This mob has no mind!"
		return

	M.mind.edit_memory()
	feedback_add_details("admin_verb","STP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/toggletintedweldhelmets()
	set category = "Debug"
	set desc="Reduces view range when wearing welding helmets"
	set name="Toggle tinted welding helmes"
	tinted_weldhelh = !( tinted_weldhelh )
	if (tinted_weldhelh)
		world << "The tinted_weldhelh has been enabled!"
	else
		world << "The tinted_weldhelh has been disabled!"
	log_admin("[key_name(usr)] toggled tinted_weldhelh.")
	message_admins("[key_name_admin(usr)] toggled tinted_weldhelh.")
	feedback_add_details("admin_verb","TTWH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/toggleguests()
	set category = "Server"
	set desc="Guests can't enter"
	set name="Toggle guests"
	guests_allowed = !( guests_allowed )
	if (!( guests_allowed ))
		world << "Guests may no longer enter the game."
	else
		world << "Guests may now enter the game."
	log_admin("[key_name(usr)] toggled guests game entering [guests_allowed?"":"dis"]allowed.")
	message_admins("[key_name_admin(usr)] toggled guests game entering [guests_allowed?"":"dis"]allowed.")
	feedback_add_details("admin_verb","TGU") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/output_ai_laws()
	var/ai_number = 0
	for(var/mob/living/silicon/S in mob_list)
		ai_number++
		if(isAI(S))
			usr << "AI [key_name(S, usr)]'s laws:"
		else if(isrobot(S))
			var/mob/living/silicon/robot/R = S
			usr << "CYBORG [key_name(S, usr)] [R.connected_ai?"(Slaved to: [R.connected_ai])":"(Independant)"]: laws:"
		else if (ispAI(S))
			usr << "pAI [key_name(S, usr)]'s laws:"
		else
			usr << "SOMETHING SILICON [key_name(S, usr)]'s laws:"

		if (S.laws == null)
			usr << "[key_name(S, usr)]'s laws are null?? Contact a coder."
		else
			S.laws.show_laws(usr)
	if(!ai_number)
		usr << "No AIs located" //Just so you know the thing is actually working and not just ignoring you.

/datum/admins/proc/manage_free_slots()
	if(!check_rights())
		return
	var/dat = "<title>Manage Free Slots</title>"
	var/count = 0

	if(ticker && !ticker.mode)
		alert(usr, "You cannot manage jobs before the round starts!")
		return

	if(SSjob)
		for(var/datum/job/job in SSjob.occupations)
			count++
			var/J_title = html_encode(job.title)
			var/J_opPos = html_encode(job.total_positions - (job.total_positions - job.current_positions))
			var/J_totPos = html_encode(job.total_positions)
			if(job.total_positions < 0)
				dat += "[J_title]: [J_opPos]   (unlimited)"
			else
				dat += "[J_title]: [J_opPos]/[J_totPos]"

			if(job.title == "AI" || job.title == "Cyborg")
				dat += "   (Cannot Late Join)
"
				continue
			if(job.total_positions >= 0)
				dat += "   Add  |  "
				if(job.total_positions > job.current_positions)
					dat += "Remove  |  "
				else
					dat += "Remove  |  "
				dat += "Unlimit"
			else
				dat += "   Limit"
			dat += "
"

	dat += ""
	var/winheight = 100 + (count * 20)
	winheight = min(winheight, 690)
	usr << browse(dat, "window=players;size=375x[winheight]")

//
//
//ALL DONE
//*********************************************************************************************************
//TO-DO:
//
//

//RIP ferry snowflakes

//Kicks all the clients currently in the lobby. The second parameter (kick_only_afk) determins if an is_afk() check is ran, or if all clients are kicked
//defaults to kicking everyone (afk + non afk clients in the lobby)
//returns a list of ckeys of the kicked clients
/proc/kick_clients_in_lobby(message, kick_only_afk = 0)
	var/list/kicked_client_names = list()
	for(var/client/C in clients)
		if(istype(C.mob, /mob/new_player))
			if(kick_only_afk && !C.is_afk())	//Ignore clients who are not afk
				continue
			if(message)
				C << message
			kicked_client_names.Add("[C.ckey]")
			del(C)
	return kicked_client_names

//returns 1 to let the dragdrop code know we are trapping this event
//returns 0 if we don't plan to trap the event
/datum/admins/proc/cmd_ghost_drag(mob/dead/observer/frommob, mob/living/tomob)

	//this is the exact two check rights checks required to edit a ckey with vv.
	if (!check_rights(R_VAREDIT,0) || !check_rights(R_SPAWN|R_DEBUG,0))
		return 0

	if (!frommob.ckey)
		return 0

	var/question = ""
	if (tomob.ckey)
		question = "This mob already has a user ([tomob.key]) in control of it! "
	question += "Are you sure you want to place [frommob.name]([frommob.key]) in control of [tomob.name]?"

	var/ask = alert(question, "Place ghost in control of mob?", "Yes", "No")
	if (ask != "Yes")
		return 1

	if (!frommob || !tomob) //make sure the mobs don't go away while we waited for a response
		return 1

	tomob.ghostize(0)

	message_admins("[key_name_admin(usr)] has put [frommob.ckey] in control of [tomob.name].")
	log_admin("[key_name(usr)] stuffed [frommob.ckey] into [tomob.name].")
	feedback_add_details("admin_verb","CGD")

	tomob.ckey = frommob.ckey
	qdel(frommob)

	return 1

/client/proc/adminGreet(logout)
	if(ticker && ticker.current_state == GAME_STATE_PLAYING)
		var/string
		if(logout && config && config.announce_admin_logout)
			string = pick(
				"Admin logout: [key_name(src)]")
		else if(!logout && config && config.announce_admin_login && (prefs.toggles & ANNOUNCE_LOGIN))
			string = pick(
				"Admin login: [key_name(src)]")
		if(string)
			message_admins("[string]")


code:
//By Carnwennan

//This system was made as an alternative to all the in-game lists and variables used to log stuff in-game.
//lists and variables are great. However, they have several major flaws:
//Firstly, they use memory. TGstation has one of the highest memory usage of all the ss13 branches.
//Secondly, they are usually stored in an object. This means that they aren't centralised. It also means that
//the data is lost when the object is deleted! This is especially annoying for things like the singulo engine!
#define INVESTIGATE_DIR "data/investigate/"

//SYSTEM
/proc/investigate_subject2file(subject)
	return file("[INVESTIGATE_DIR][subject].html")

/proc/investigate_reset()
	if(fdel(INVESTIGATE_DIR))
		return 1
	return 0

/atom/proc/investigate_log(message, subject)
	if(!message)
		return
	var/F = investigate_subject2file(subject)
	if(!F)
		return
	F << "[time_stamp()] \ref[src] ([x],[y],[z]) || [src] [message]
"

//ADMINVERBS
/client/proc/investigate_show( subject in list("hrefs","notes","watchlist","singulo","wires","telesci", "gravity", "records", "cargo", "supermatter", "atmos", "experimentor", "kudzu") )
	set name = "Investigate"
	set category = "Admin"
	if(!holder)
		return
	switch(subject)
		if("singulo", "wires", "telesci", "gravity", "records", "cargo", "supermatter", "atmos", "kudzu")			//general one-round-only stuff
			var/F = investigate_subject2file(subject)
			if(!F)
				src << "Error: admin_investigate: [INVESTIGATE_DIR][subject] is an invalid path or cannot be accessed."
				return
			src << browse(F,"window=investigate[subject];size=800x300")

		if("hrefs")				//persistant logs and stuff
			if(config && config.log_hrefs)
				if(href_logfile)
					src << browse(href_logfile,"window=investigate[subject];size=800x300")
				else
					src << "Error: admin_investigate: No href logfile found."
					return
			else
				src << "Error: admin_investigate: Href Logging is not on."
				return
		if("notes")
			show_note()
		if("watchlist")
			watchlist_show()

license: mit


code:
/client/proc/admin_memo()
	set name = "Memo"
	set category = "Server"
	if(!check_rights(0))
		return
	if(!dbcon.IsConnected())
		src << "Failed to establish database connection."
		return
	var/memotask = input(usr,"Choose task.","Memo") in list("Show","Write","Edit","Remove")
	if(!memotask)
		return
	admin_memo_output(memotask)

/client/proc/admin_memo_output(task)
	if(!task)
		return
	if(!dbcon.IsConnected())
		src << "Failed to establish database connection."
		return
	var/sql_ckey = sanitizeSQL(src.ckey)
	switch(task)
		if("Write")
			var/DBQuery/query_memocheck = dbcon.NewQuery("SELECT ckey FROM [format_table_name("memo")] WHERE ckey = '[sql_ckey]'")
			if(!query_memocheck.Execute())
				var/err = query_memocheck.ErrorMsg()
				log_game("SQL ERROR obtaining ckey from memo table. Error : \[[err]\]\n")
				return
			if(query_memocheck.NextRow())
				src << "You already have set a memo."
				return
			var/memotext = input(src,"Write your Memo","Memo") as message
			if(!memotext)
				return
			memotext = sanitizeSQL(memotext)
			var/timestamp = SQLtime()
			var/DBQuery/query_memoadd = dbcon.NewQuery("INSERT INTO [format_table_name("memo")] (ckey, memotext, timestamp) VALUES ('[sql_ckey]', '[memotext]', '[timestamp]')")
			if(!query_memoadd.Execute())
				var/err = query_memoadd.ErrorMsg()
				log_game("SQL ERROR adding new memo. Error : \[[err]\]\n")
				return
			log_admin("[key_name(src)] has set a memo: [memotext]")
			message_admins("[key_name_admin(src)] has set a memo:
[memotext]")
		if("Edit")
			var/DBQuery/query_memolist = dbcon.NewQuery("SELECT ckey FROM [format_table_name("memo")]")
			if(!query_memolist.Execute())
				var/err = query_memolist.ErrorMsg()
				log_game("SQL ERROR obtaining ckey from memo table. Error : \[[err]\]\n")
				return
			var/list/memolist = list()
			while(query_memolist.NextRow())
				var/lkey = query_memolist.item[1]
				memolist += "[lkey]"
			if(!memolist.len)
				src << "No memos found in database."
				return
			var/target_ckey = input(src, "Select whose memo to edit", "Select memo") as null|anything in memolist
			if(!target_ckey)
				return
			var/target_sql_ckey = sanitizeSQL(target_ckey)
			var/DBQuery/query_memofind = dbcon.NewQuery("SELECT memotext FROM [format_table_name("memo")] WHERE ckey = '[target_sql_ckey]'")
			if(!query_memofind.Execute())
				var/err = query_memofind.ErrorMsg()
				log_game("SQL ERROR obtaining memotext from memo table. Error : \[[err]\]\n")
				return
			if(query_memofind.NextRow())
				var/old_memo = query_memofind.item[1]
				var/new_memo = input("Input new memo", "New Memo", "[old_memo]", null) as message
				if(!new_memo)
					return
				new_memo = sanitizeSQL(new_memo)
				var/edit_text = "Edited by [sql_ckey] on [SQLtime()] from
[old_memo]
to
[new_memo]
"
				edit_text = sanitizeSQL(edit_text)
				var/DBQuery/update_query = dbcon.NewQuery("UPDATE [format_table_name("memo")] SET memotext = '[new_memo]', last_editor = '[sql_ckey]', edits = CONCAT(IFNULL(edits,''),'[edit_text]') WHERE ckey = '[target_sql_ckey]'")
				if(!update_query.Execute())
					var/err = update_query.ErrorMsg()
					log_game("SQL ERROR editing memo. Error : \[[err]\]\n")
					return
				if(target_sql_ckey == sql_ckey)
					log_admin("[key_name(src)] has edited their memo from [old_memo] to [new_memo]")
					message_admins("[key_name_admin(src)] has edited their memo from
[old_memo]
to
[new_memo]")
				else
					log_admin("[key_name(src)] has edited [target_sql_ckey]'s memo from [old_memo] to [new_memo]")
					message_admins("[key_name_admin(src)] has edited [target_sql_ckey]'s memo from
[old_memo]
to
[new_memo]")
		if("Show")
			var/DBQuery/query_memoshow = dbcon.NewQuery("SELECT ckey, memotext, timestamp, last_editor FROM [format_table_name("memo")]")
			if(!query_memoshow.Execute())
				var/err = query_memoshow.ErrorMsg()
				log_game("SQL ERROR obtaining ckey, memotext, timestamp, last_editor from memo table. Error : \[[err]\]\n")
				return
			var/output = null
			while(query_memoshow.NextRow())
				var/ckey = query_memoshow.item[1]
				var/memotext = query_memoshow.item[2]
				var/timestamp = query_memoshow.item[3]
				var/last_editor = query_memoshow.item[4]
				output += "Memo by [ckey] on [timestamp]"
				if(last_editor)
					output += "
Last edit by [last_editor] (Click here to see edit log)"
				output += "
[memotext]
"
			if(!output)
				src << "No memos found in database."
				return
			src << output
		if("Remove")
			var/DBQuery/query_memodellist = dbcon.NewQuery("SELECT ckey FROM [format_table_name("memo")]")
			if(!query_memodellist.Execute())
				var/err = query_memodellist.ErrorMsg()
				log_game("SQL ERROR obtaining ckey from memo table. Error : \[[err]\]\n")
				return
			var/list/memolist = list()
			while(query_memodellist.NextRow())
				var/ckey = query_memodellist.item[1]
				memolist += "[ckey]"
			if(!memolist.len)
				src << "No memos found in database."
				return
			var/target_ckey = input(src, "Select whose memo to delete", "Select memo") as null|anything in memolist
			if(!target_ckey)
				return
			var/target_sql_ckey = sanitizeSQL(target_ckey)
			var/DBQuery/query_memodel = dbcon.NewQuery("DELETE FROM [format_table_name("memo")] WHERE ckey = '[target_sql_ckey]'")
			if(!query_memodel.Execute())
				var/err = query_memodel.ErrorMsg()
				log_game("SQL ERROR removing memo. Error : \[[err]\]\n")
				return
			if(target_sql_ckey == sql_ckey)
				log_admin("[key_name(src)] has removed their memo.")
				message_admins("[key_name_admin(src)] has removed their memo.")
			else
				log_admin("[key_name(src)] has removed [target_sql_ckey]'s memo.")
				message_admins("[key_name_admin(src)] has removed [target_sql_ckey]'s memo.")


code:
var/list/admin_ranks = list()								//list of all admin_rank datums

/datum/admin_rank
	var/name = "NoRank"
	var/rights = 0
	var/list/adds
	var/list/subs

/datum/admin_rank/New(init_name, init_rights, list/init_adds, list/init_subs)
	name = init_name
	switch(name)
		if("Removed",null,"")
			spawn(0)
				qdel(src)
			throw EXCEPTION("invalid admin-rank name")
			return
	if(init_rights)
		rights = init_rights
	if(!init_adds)
		init_adds = list()
	if(!init_subs)
		init_subs = list()
	adds = init_adds
	subs = init_subs

/proc/admin_keyword_to_flag(word, previous_rights=0)
	var/flag = 0
	switch(ckey(word))
		if("buildmode","build")
			flag = R_BUILDMODE
		if("admin")
			flag = R_ADMIN
		if("ban")
			flag = R_BAN
		if("fun")
			flag = R_FUN
		if("server")
			flag = R_SERVER
		if("debug")
			flag = R_DEBUG
		if("permissions","rights")
			flag = R_PERMISSIONS
		if("possess")
			flag = R_POSSESS
		if("stealth")
			flag = R_STEALTH
		if("rejuv","rejuvinate")
			flag = R_REJUVINATE
		if("varedit")
			flag = R_VAREDIT
		if("everything","host","all")
			flag = 65535
		if("sound","sounds")
			flag = R_SOUNDS
		if("spawn","create")
			flag = R_SPAWN
		if("@","prev")
			flag = previous_rights
	return flag

/proc/admin_keyword_to_path(word) //use this with verb keywords eg +/client/proc/blah
	return text2path(copytext(word, 2, findtext(word, " ", 2, 0)))

// Adds/removes rights to this admin_rank
/datum/admin_rank/proc/process_keyword(word, previous_rights=0)
	var/flag = admin_keyword_to_flag(word, previous_rights)
	if(flag)
		switch(text2ascii(word,1))
			if(43)
				rights |= flag	//+
			if(45)
				rights &= ~flag	//-
	else
		//isn't a keyword so maybe it's a verbpath?
		var/path = admin_keyword_to_path(word)
		if(path)
			switch(text2ascii(word,1))
				if(43)
					if(!subs.Remove(path))
						adds += path	//+
				if(45)
					if(!adds.Remove(path))
						subs += path	//-

// Checks for (keyword-formatted) rights on this admin
/datum/admins/proc/check_keyword(word)
	var/flag = admin_keyword_to_flag(word)
	if(flag)
		return ((rank.rights & flag) == flag) //true only if right has everything in flag
	else
		var/path = admin_keyword_to_path(word)
		for(var/i in owner.verbs) //this needs to be a foreach loop for some reason. in operator and verbs.Find() don't work
			if(i == path)
				return 1
		return 0

//load our rank - > rights associations
/proc/load_admin_ranks()
	admin_ranks.Cut()

	if(config.admin_legacy_system)
		var/previous_rights = 0
		//load text from file and process each line seperately
		for(var/line in file2list("config/admin_ranks.txt"))
			if(!line)
				continue
			if(findtextEx(line,"#",1,2))
				continue

			var/next = findtext(line, "=")
			var/datum/admin_rank/R = new(ckeyEx(copytext(line, 1, next)))
			if(!R)
				continue
			admin_ranks += R

			var/prev = findchar(line, "+-", next, 0)
			while(prev)
				next = findchar(line, "+-", prev + 1, 0)
				R.process_keyword(copytext(line, prev, next), previous_rights)
				prev = next

			previous_rights = R.rights
	else
		establish_db_connection()
		if(!dbcon.IsConnected())
			world.log << "Failed to connect to database in load_admin_ranks(). Reverting to legacy system."
			diary << "Failed to connect to database in load_admin_ranks(). Reverting to legacy system."
			config.admin_legacy_system = 1
			load_admin_ranks()
			return

		var/DBQuery/query = dbcon.NewQuery("SELECT rank, flags FROM [format_table_name("admin_ranks")]")
		query.Execute()
		while(query.NextRow())
			var/rank_name = ckeyEx(query.item[1])
			var/flags = query.item[2]
			if(istext(flags))
				flags = text2num(flags)
			var/datum/admin_rank/R = new(rank_name, flags)
			if(!R)
				continue
			admin_ranks += R

	#ifdef TESTING
	var/msg = "Permission Sets Built:\n"
	for(var/datum/admin_rank/R in admin_ranks)
		msg += "\t[R.name]"
		var/rights = rights2text(R.rights,"\n\t\t",R.adds,R.subs)
		if(rights)
			msg += "\t\t[rights]\n"
	testing(msg)
	#endif

/proc/load_admins(target = null)
	//clear the datums references
	if(!target)
		admin_datums.Cut()
		for(var/client/C in admins)
			C.remove_admin_verbs()
			C.holder = null
		admins.Cut()
		load_admin_ranks()

	var/list/rank_names = list()
	for(var/datum/admin_rank/R in admin_ranks)
		rank_names[R.name] = R

	if(config.admin_legacy_system)
		//load text from file
		var/list/lines = file2list("config/admins.txt")

		//process each line seperately
		for(var/line in lines)
			if(!length(line))
				continue
			if(findtextEx(line, "#", 1, 2))
				continue

			var/list/entry = splittext(line, "=")
			if(entry.len < 2)
				continue

			var/ckey = ckey(entry[1])
			var/rank = ckeyEx(entry[2])
			if(!ckey || !rank || (target && ckey != target))
				continue

			var/datum/admins/D = new(rank_names[rank], ckey)	//create the admin datum and store it for later use
			if(!D)
				continue									//will occur if an invalid rank is provided
			D.associate(directory[ckey])	//find the client for a ckey if they are connected and associate them with the new admin datum
	else
		establish_db_connection()
		if(!dbcon.IsConnected())
			world.log << "Failed to connect to database in load_admins(). Reverting to legacy system."
			diary << "Failed to connect to database in load_admins(). Reverting to legacy system."
			config.admin_legacy_system = 1
			load_admins()
			return

		var/DBQuery/query = dbcon.NewQuery("SELECT ckey, rank FROM [format_table_name("admin")]")
		query.Execute()
		while(query.NextRow())
			var/ckey = ckey(query.item[1])
			var/rank = ckeyEx(query.item[2])
			if(target && ckey != target)
				continue

			if(rank_names[rank] == null)
				WARNING("Admin rank ([rank]) does not exist.")
				continue

			var/datum/admins/D = new(rank_names[rank], ckey)				//create the admin datum and store it for later use
			if(!D)
				continue									//will occur if an invalid rank is provided
			D.associate(directory[ckey])	//find the client for a ckey if they are connected and associate them with the new admin datum

	#ifdef TESTING
	var/msg = "Admins Built:\n"
	for(var/ckey in admin_datums)
		var/datum/admins/D = admin_datums[ckey]
		msg += "\t[ckey] - [D.rank.name]\n"
	testing(msg)
	#endif

#ifdef TESTING
/client/verb/changerank(newrank in admin_ranks)
	if(holder)
		holder.rank = newrank
	else
		holder = new /datum/admins(newrank, ckey)
	remove_admin_verbs()
	holder.associate(src)

/client/verb/changerights(newrights as num)
	if(holder)
		holder.rank.rights = newrights
	else
		holder = new /datum/admins("testing", newrights, ckey)
	remove_admin_verbs()
	holder.associate(src)
#endif

/datum/admins/proc/edit_rights_topic(list/href_list)
	if(!check_rights(R_PERMISSIONS))
		message_admins("[key_name_admin(usr)] attempted to edit the admin permissions without sufficient rights.")
		log_admin("[key_name(usr)] attempted to edit the admin permissions without sufficient rights.")
		return

	var/adm_ckey
	var/task = href_list["editrights"]
	switch(task)
		if("add")
			var/new_ckey = ckey(input(usr,"New admin's ckey","Admin ckey", null) as text|null)
			if(!new_ckey)
				return
			if(new_ckey in admin_datums)
				usr << "Error: Topic 'editrights': [new_ckey] is already an admin"
				return
			adm_ckey = new_ckey
			task = "rank"
		else
			adm_ckey = ckey(href_list["ckey"])
			if(!adm_ckey)
				usr << "Error: Topic 'editrights': No valid ckey"
				return

	var/datum/admins/D = admin_datums[adm_ckey]

	switch(task)
		if("remove")
			if(alert("Are you sure you want to remove [adm_ckey]?","Message","Yes","Cancel") == "Yes")
				if(!D)
					return
				if(!check_if_greater_rights_than_holder(D))
					message_admins("[key_name_admin(usr)] attempted to remove [adm_ckey] from the admins list without sufficient rights.")
					log_admin("[key_name(usr)] attempted to remove [adm_ckey] from the admins list without sufficient rights.")
					return
				admin_datums -= adm_ckey
				D.disassociate()

				updateranktodb(adm_ckey, "player")
				message_admins("[key_name_admin(usr)] removed [adm_ckey] from the admins list")
				log_admin("[key_name(usr)] removed [adm_ckey] from the admins list")
				log_admin_rank_modification(adm_ckey, "Removed")

		if("rank")
			var/datum/admin_rank/R

			var/list/rank_names = list("*New Rank*")
			for(R in admin_ranks)
				rank_names[R.name] = R

			var/new_rank = input("Please select a rank", "New rank", null, null) as null|anything in rank_names

			switch(new_rank)
				if(null)
					return
				if("*New Rank*")
					new_rank = ckeyEx(input("Please input a new rank", "New custom rank", null, null) as null|text)
					if(!new_rank)
						return

			if(D)
				if(!check_if_greater_rights_than_holder(D))
					message_admins("[key_name_admin(usr)] attempted to change the rank of [adm_ckey] to [new_rank] without sufficient rights.")
					log_admin("[key_name(usr)] attempted to change the rank of [adm_ckey] to [new_rank] without sufficient rights.")
					return

			R = rank_names[new_rank]
			if(!R)	//rank with that name doesn't exist yet - make it
				if(D)
					R = new(new_rank, D.rank.rights, D.rank.adds, D.rank.subs)	//duplicate our previous admin_rank but with a new name
				else
					R = new(new_rank)							//blank new admin_rank
				admin_ranks += R

			if(D)	//they were previously an admin
				D.disassociate()	//existing admin needs to be disassociated
				D.rank = R			//set the admin_rank as our rank
			else
				D = new(R,adm_ckey)	//new admin

			var/client/C = directory[adm_ckey]	//find the client with the specified ckey (if they are logged in)
			D.associate(C)						//link up with the client and add verbs

			updateranktodb(adm_ckey, new_rank)
			message_admins("[key_name_admin(usr)] edited the admin rank of [adm_ckey] to [new_rank]")
			log_admin("[key_name(usr)] edited the admin rank of [adm_ckey] to [new_rank]")
			log_admin_rank_modification(adm_ckey, new_rank)

		if("permissions")
			if(!D)
				return	//they're not an admin!

			var/keyword = input("Input permission keyword (one at a time):\ne.g. +BAN or -FUN or +/client/proc/someverb", "Permission toggle", null, null) as null|text
			if(!keyword)
				return

			if(!check_keyword(keyword) || !check_if_greater_rights_than_holder(D))
				message_admins("[key_name_admin(usr)] attempted to give [adm_ckey] the keyword [keyword] without sufficient rights.")
				log_admin("[key_name(usr)] attempted to give [adm_ckey] the keyword [keyword] without sufficient rights.")
				return

			D.disassociate()

			if(!findtext(D.rank.name, "([adm_ckey])"))	//not a modified subrank, need to duplicate the admin_rank datum to prevent modifying others too
				D.rank = new("[D.rank.name]([adm_ckey])", D.rank.rights, D.rank.adds, D.rank.subs)	//duplicate our previous admin_rank but with a new name
				//we don't add this clone to the admin_ranks list, as it is unique to that ckey

			D.rank.process_keyword(keyword)

			var/client/C = directory[adm_ckey]	//find the client with the specified ckey (if they are logged in)
			D.associate(C)						//link up with the client and add verbs

			message_admins("[key_name(usr)] added keyword [keyword] to permission of [adm_ckey]")
			log_admin("[key_name(usr)] added keyword [keyword] to permission of [adm_ckey]")
			log_admin_permission_modification(adm_ckey, D.rank.rights)

	edit_admin_permissions()

/datum/admins/proc/updateranktodb(ckey,newrank)
	establish_db_connection()
	if (!dbcon.IsConnected())
		return
	var/sql_ckey = sanitizeSQL(ckey)
	var/sql_admin_rank = sanitizeSQL(newrank)

	var/DBQuery/query_update = dbcon.NewQuery("UPDATE [format_table_name("player")] SET lastadminrank = '[sql_admin_rank]' WHERE ckey = '[sql_ckey]'")
	query_update.Execute()


code:
//admin verb groups - They can overlap if you so wish. Only one of each verb will exist in the verbs list regardless
var/list/admin_verbs_default = list(
	/client/proc/toggleadminhelpsound,	/*toggles whether we hear a sound when adminhelps/PMs are used*/
	/client/proc/toggleannouncelogin, /*toggles if an admin's login is announced during a round*/
	/client/proc/deadmin,				/*destroys our own admin datum so we can play as a regular player*/
	/client/proc/cmd_admin_say,			/*admin-only ooc chat*/
	/client/proc/hide_verbs,			/*hides all our adminverbs*/
	/client/proc/hide_most_verbs,		/*hides all our hideable adminverbs*/
	/client/proc/debug_variables,		/*allows us to -see- the variables of any instance in the game. +VAREDIT needed to modify*/
	/client/proc/admin_memo,			/*admin memo system. show/delete/write. +SERVER needed to delete admin memos of others*/
	/client/proc/deadchat,				/*toggles deadchat on/off*/
	/client/proc/dsay,					/*talk in deadchat using our ckey/fakekey*/
	/client/proc/toggleprayers,			/*toggles prayers on/off*/
	/client/verb/toggleprayersounds,	/*Toggles prayer sounds (HALLELUJAH!)*/
	/client/proc/toggle_hear_radio,		/*toggles whether we hear the radio*/
	/client/proc/investigate_show,		/*various admintools for investigation. Such as a singulo grief-log*/
	/client/proc/secrets,
	/client/proc/reload_admins,
	/client/proc/reestablish_db_connection,/*reattempt a connection to the database*/
	/client/proc/cmd_admin_pm_context,	/*right-click adminPM interface*/
	/client/proc/cmd_admin_pm_panel,		/*admin-pm list*/
	/client/proc/stop_sounds
	)
var/list/admin_verbs_admin = list(
	/client/proc/player_panel_new,		/*shows an interface for all players, with links to various panels*/
	/client/proc/invisimin,				/*allows our mob to go invisible/visible*/
//	/datum/admins/proc/show_traitor_panel,	/*interface which shows a mob's mind*/ -Removed due to rare practical use. Moved to debug verbs ~Errorage
	/datum/admins/proc/show_player_panel,	/*shows an interface for individual players, with various links (links require additional flags*/
	/client/proc/game_panel,			/*game panel, allows to change game-mode etc*/
	/client/proc/check_ai_laws,			/*shows AI and borg laws*/
	/datum/admins/proc/toggleooc,		/*toggles ooc on/off for everyone*/
	/datum/admins/proc/toggleoocdead,	/*toggles ooc on/off for everyone who is dead*/
	/datum/admins/proc/toggleenter,		/*toggles whether people can join the current game*/
	/datum/admins/proc/toggleguests,	/*toggles whether guests can join the current game*/
	/datum/admins/proc/announce,		/*priority announce something to all clients.*/
	/datum/admins/proc/set_admin_notice,/*announcement all clients see when joining the server.*/
	/client/proc/admin_ghost,			/*allows us to ghost/reenter body at will*/
	/client/proc/toggle_view_range,		/*changes how far we can see*/
	/datum/admins/proc/view_txt_log,	/*shows the server log (diary) for today*/
	/datum/admins/proc/view_atk_log,	/*shows the server combat-log, doesn't do anything presently*/
	/client/proc/cmd_admin_subtle_message,	/*send an message to somebody as a 'voice in their head'*/
	/client/proc/cmd_admin_delete,		/*delete an instance/object/mob/etc*/
	/client/proc/cmd_admin_check_contents,	/*displays the contents of an instance*/
	/client/proc/check_antagonists,		/*shows all antags*/
	/datum/admins/proc/access_news_network,	/*allows access of newscasters*/
	/client/proc/giveruntimelog,		/*allows us to give access to runtime logs to somebody*/
	/client/proc/getruntimelog,			/*allows us to access runtime logs to somebody*/
	/client/proc/getserverlog,			/*allows us to fetch server logs (diary) for other days*/
	/client/proc/jumptocoord,			/*we ghost and jump to a coordinate*/
	/client/proc/Getmob,				/*teleports a mob to our location*/
	/client/proc/Getkey,				/*teleports a mob with a certain ckey to our location*/
//	/client/proc/sendmob,				/*sends a mob somewhere*/ -Removed due to it needing two sorting procs to work, which were executed every time an admin right-clicked. ~Errorage
	/client/proc/jumptoarea,
	/client/proc/jumptokey,				/*allows us to jump to the location of a mob with a certain ckey*/
	/client/proc/jumptomob,				/*allows us to jump to a specific mob*/
	/client/proc/jumptoturf,			/*allows us to jump to a specific turf*/
	/client/proc/admin_call_shuttle,	/*allows us to call the emergency shuttle*/
	/client/proc/admin_cancel_shuttle,	/*allows us to cancel the emergency shuttle, sending it back to centcom*/
	/client/proc/cmd_admin_direct_narrate,	/*send text directly to a player with no padding. Useful for narratives and fluff-text*/
	/client/proc/cmd_admin_world_narrate,	/*sends text to all players with no padding*/
	/client/proc/cmd_admin_local_narrate,	/*sends text to all mobs within view of atom*/
	/client/proc/cmd_admin_create_centcom_report,
	/client/proc/cmd_change_command_name,
	/client/proc/toggle_antag_hud, 	/*toggle display of the admin antag hud*/
	/client/proc/toggle_AI_interact, /*toggle admin ability to interact with machines as an AI*/
	/client/proc/customiseSNPC, /* Customise any interactive crewmembers in the world */
	/client/proc/resetSNPC, /* Resets any interactive crewmembers in the world */
	/client/proc/toggleSNPC /* Toggles an npc's processing mode */
	)
var/list/admin_verbs_ban = list(
	/client/proc/unban_panel,
	/client/proc/DB_ban_panel,
	/client/proc/stickybanpanel
	)
var/list/admin_verbs_sounds = list(
	/client/proc/play_local_sound,
	/client/proc/play_sound,
	/client/proc/set_round_end_sound,
	)
var/list/admin_verbs_fun = list(
	/client/proc/cmd_admin_dress,
	/client/proc/cmd_admin_gib_self,
	/client/proc/drop_bomb,
	/client/proc/cinematic,
	/client/proc/one_click_antag,
	/client/proc/send_space_ninja,
	/client/proc/cmd_admin_add_freeform_ai_law,
	/client/proc/cmd_admin_add_random_ai_law,
	/client/proc/object_say,
	/client/proc/toggle_random_events,
	/client/proc/set_ooc,
	/client/proc/reset_ooc,
	/client/proc/forceEvent,
	/client/proc/bluespace_artillery,
	/client/proc/admin_change_sec_level,
	/client/proc/toggle_nuke
	)
var/list/admin_verbs_spawn = list(
	/datum/admins/proc/spawn_atom,		/*allows us to spawn instances*/
	/client/proc/respawn_character
	)
var/list/admin_verbs_server = list(
	/datum/admins/proc/startnow,
	/datum/admins/proc/restart,
	/datum/admins/proc/end_round,
	/datum/admins/proc/delay,
	/datum/admins/proc/toggleaban,
	/client/proc/toggle_log_hrefs,
	/client/proc/everyone_random,
	/datum/admins/proc/toggleAI,
	/client/proc/cmd_admin_delete,		/*delete an instance/object/mob/etc*/
	/client/proc/cmd_debug_del_all,
	/client/proc/toggle_random_events,
#if SERVERTOOLS
	/client/proc/forcerandomrotate,
	/client/proc/adminchangemap,
#endif
	/client/proc/panicbunker

	)
var/list/admin_verbs_debug = list(
	/client/proc/restart_controller,
	/client/proc/cmd_admin_list_open_jobs,
	/client/proc/Debug2,
	/client/proc/cmd_debug_make_powernets,
	/client/proc/cmd_debug_mob_lists,
	/client/proc/cmd_admin_delete,
	/client/proc/cmd_debug_del_all,
	/client/proc/restart_controller,
	/client/proc/enable_debug_verbs,
	/client/proc/callproc,
	/client/proc/callproc_datum,
	/client/proc/SDQL2_query,
	/client/proc/test_movable_UI,
	/client/proc/test_snap_UI,
	/client/proc/debugNatureMapGenerator,
	/client/proc/check_bomb_impacts,
	/proc/machine_upgrade,
	/client/proc/populate_world,
	/client/proc/cmd_display_del_log,
	/client/proc/reset_latejoin_spawns,
	/client/proc/create_outfits,
	/client/proc/debug_huds,
	/client/proc/map_template_load,
	/client/proc/map_template_upload
	)
var/list/admin_verbs_possess = list(
	/proc/possess,
	/proc/release
	)
var/list/admin_verbs_permissions = list(
	/client/proc/edit_admin_permissions,
	/client/proc/create_poll
	)
var/list/admin_verbs_rejuv = list(
	/client/proc/respawn_character
	)

//verbs which can be hidden - needs work
var/list/admin_verbs_hideable = list(
	/client/proc/set_ooc,
	/client/proc/reset_ooc,
	/client/proc/deadmin,
	/client/proc/deadchat,
	/client/proc/toggleprayers,
	/client/proc/toggle_hear_radio,
	/datum/admins/proc/show_traitor_panel,
	/datum/admins/proc/toggleenter,
	/datum/admins/proc/toggleguests,
	/datum/admins/proc/announce,
	/datum/admins/proc/set_admin_notice,
	/client/proc/admin_ghost,
	/client/proc/toggle_view_range,
	/datum/admins/proc/view_txt_log,
	/datum/admins/proc/view_atk_log,
	/client/proc/cmd_admin_subtle_message,
	/client/proc/cmd_admin_check_contents,
	/datum/admins/proc/access_news_network,
	/client/proc/admin_call_shuttle,
	/client/proc/admin_cancel_shuttle,
	/client/proc/cmd_admin_direct_narrate,
	/client/proc/cmd_admin_world_narrate,
	/client/proc/cmd_admin_local_narrate,
	/client/proc/play_local_sound,
	/client/proc/play_sound,
	/client/proc/set_round_end_sound,
	/client/proc/cmd_admin_dress,
	/client/proc/cmd_admin_gib_self,
	/client/proc/drop_bomb,
	/client/proc/cinematic,
	/client/proc/send_space_ninja,
	/client/proc/cmd_admin_add_freeform_ai_law,
	/client/proc/cmd_admin_add_random_ai_law,
	/client/proc/cmd_admin_create_centcom_report,
	/client/proc/cmd_change_command_name,
	/client/proc/object_say,
	/client/proc/toggle_random_events,
	/client/proc/cmd_admin_add_random_ai_law,
	/datum/admins/proc/startnow,
	/datum/admins/proc/restart,
	/datum/admins/proc/delay,
	/datum/admins/proc/toggleaban,
	/client/proc/toggle_log_hrefs,
	/client/proc/everyone_random,
	/datum/admins/proc/toggleAI,
	/client/proc/restart_controller,
	/client/proc/cmd_admin_list_open_jobs,
	/client/proc/callproc,
	/client/proc/callproc_datum,
	/client/proc/Debug2,
	/client/proc/reload_admins,
	/client/proc/cmd_debug_make_powernets,
	/client/proc/startSinglo,
	/client/proc/cmd_debug_mob_lists,
	/client/proc/cmd_debug_del_all,
	/client/proc/enable_debug_verbs,
	/proc/possess,
	/proc/release,
	/client/proc/reload_admins,
	/client/proc/panicbunker,
	/client/proc/admin_change_sec_level,
	/client/proc/toggle_nuke,
	/client/proc/cmd_display_del_log,
	/client/proc/toggle_antag_hud,
	/client/proc/debug_huds,
	/client/proc/customiseSNPC,
	/client/proc/resetSNPC,
	/client/proc/toggleSNPC
	)

/client/proc/add_admin_verbs()
	if(holder)
		control_freak = CONTROL_FREAK_SKIN | CONTROL_FREAK_MACROS

		var/rights = holder.rank.rights
		verbs += admin_verbs_default
		if(rights & R_BUILDMODE)
			verbs += /client/proc/togglebuildmodeself
		if(rights & R_ADMIN)
			verbs += admin_verbs_admin
		if(rights & R_BAN)
			verbs += admin_verbs_ban
		if(rights & R_FUN)
			verbs += admin_verbs_fun
		if(rights & R_SERVER)
			verbs += admin_verbs_server
		if(rights & R_DEBUG)
			verbs += admin_verbs_debug
		if(rights & R_POSSESS)
			verbs += admin_verbs_possess
		if(rights & R_PERMISSIONS)
			verbs += admin_verbs_permissions
		if(rights & R_STEALTH)
			verbs += /client/proc/stealth
		if(rights & R_REJUVINATE)
			verbs += admin_verbs_rejuv
		if(rights & R_SOUNDS)
			verbs += admin_verbs_sounds
		if(rights & R_SPAWN)
			verbs += admin_verbs_spawn

		for(var/path in holder.rank.adds)
			verbs += path
		for(var/path in holder.rank.subs)
			verbs -= path

/client/proc/remove_admin_verbs()
	verbs.Remove(
		admin_verbs_default,
		/client/proc/togglebuildmodeself,
		admin_verbs_admin,
		admin_verbs_ban,
		admin_verbs_fun,
		admin_verbs_server,
		admin_verbs_debug,
		admin_verbs_possess,
		admin_verbs_permissions,
		/client/proc/stealth,
		admin_verbs_rejuv,
		admin_verbs_sounds,
		admin_verbs_spawn,
		/*Debug verbs added by "show debug verbs"*/
		/client/proc/Cell,
		/client/proc/do_not_use_these,
		/client/proc/camera_view,
		/client/proc/sec_camera_report,
		/client/proc/intercom_view,
		/client/proc/air_status,
		/client/proc/atmosscan,
		/client/proc/powerdebug,
		/client/proc/count_objects_on_z_level,
		/client/proc/count_objects_all,
		/client/proc/cmd_assume_direct_control,
		/client/proc/startSinglo,
		/client/proc/fps,
		/client/proc/cmd_admin_grantfullaccess,
		/client/proc/cmd_admin_areatest,
		/client/proc/readmin
		)
	if(holder)
		verbs.Remove(holder.rank.adds)

/client/proc/hide_most_verbs()//Allows you to keep some functionality while hiding some verbs
	set name = "Adminverbs - Hide Most"
	set category = "Admin"

	verbs.Remove(/client/proc/hide_most_verbs, admin_verbs_hideable)
	verbs += /client/proc/show_verbs

	src << "Most of your adminverbs have been hidden."
	feedback_add_details("admin_verb","HMV") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/hide_verbs()
	set name = "Adminverbs - Hide All"
	set category = "Admin"

	remove_admin_verbs()
	verbs += /client/proc/show_verbs

	src << "Almost all of your adminverbs have been hidden."
	feedback_add_details("admin_verb","TAVVH") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/show_verbs()
	set name = "Adminverbs - Show"
	set category = "Admin"

	verbs -= /client/proc/show_verbs
	add_admin_verbs()

	src << "All of your adminverbs are now visible."
	feedback_add_details("admin_verb","TAVVS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/admin_ghost()
	set category = "Admin"
	set name = "Aghost"
	if(!holder)
		return
	if(istype(mob,/mob/dead/observer))
		//re-enter
		var/mob/dead/observer/ghost = mob
		if(!ghost.mind || !ghost.mind.current) //won't do anything if there is no body
			return
		if(!ghost.can_reenter_corpse)
			log_admin("[key_name(usr)] re-entered corpse")
			message_admins("[key_name_admin(usr)] re-entered corpse")
		ghost.can_reenter_corpse = 1 //force re-entering even when otherwise not possible
		ghost.reenter_corpse()
		feedback_add_details("admin_verb","P") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	else if(istype(mob,/mob/new_player))
		src << "Error: Aghost: Can't admin-ghost whilst in the lobby. Join or Observe first."
	else
		//ghostize
		log_admin("[key_name(usr)] admin ghosted.")
		message_admins("[key_name_admin(usr)] admin ghosted.")
		var/mob/body = mob
		body.ghostize(1)
		if(body && !body.key)
			body.key = "@[key]"	//Haaaaaaaack. But the people have spoken. If it breaks; blame adminbus
		feedback_add_details("admin_verb","O") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/invisimin()
	set name = "Invisimin"
	set category = "Admin"
	set desc = "Toggles ghost-like invisibility (Don't abuse this)"
	if(holder && mob)
		if(mob.invisibility == INVISIBILITY_OBSERVER)
			mob.invisibility = initial(mob.invisibility)
			mob << "Invisimin off. Invisibility reset."
		else
			mob.invisibility = INVISIBILITY_OBSERVER
			mob << "Invisimin on. You are now as invisible as a ghost."

/client/proc/player_panel_new()
	set name = "Player Panel"
	set category = "Admin"
	if(holder)
		holder.player_panel_new()
	feedback_add_details("admin_verb","PPN") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/check_antagonists()
	set name = "Check Antagonists"
	set category = "Admin"
	if(holder)
		holder.check_antagonists()
		log_admin("[key_name(usr)] checked antagonists.")	//for tsar~
		if(!isobserver(usr))
			message_admins("[key_name_admin(usr)] checked antagonists.")
	feedback_add_details("admin_verb","CHA") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/unban_panel()
	set name = "Unban Panel"
	set category = "Admin"
	if(holder)
		if(config.ban_legacy_system)
			holder.unbanpanel()
		else
			holder.DB_ban_panel()
	feedback_add_details("admin_verb","UBP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/game_panel()
	set name = "Game Panel"
	set category = "Admin"
	if(holder)
		holder.Game()
	feedback_add_details("admin_verb","GP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/secrets()
	set name = "Secrets"
	set category = "Admin"
	if (holder)
		holder.Secrets()
	feedback_add_details("admin_verb","S") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	return

/client/proc/findStealthKey(txt)
	if(txt)
		for(var/P in stealthminID)
			if(stealthminID[P] == txt)
				return P
	txt = stealthminID[ckey]
	return txt

/client/proc/createStealthKey()
	var/num = (rand(0,1000))
	var/i = 0
	while(i == 0)
		i = 1
		for(var/P in stealthminID)
			if(num == stealthminID[P])
				num++
				i = 0
	stealthminID["[ckey]"] = "@[num2text(num)]"

/client/proc/stealth()
	set category = "Admin"
	set name = "Stealth Mode"
	if(holder)
		if(holder.fakekey)
			holder.fakekey = null
			if(isobserver(mob))
				mob.invisibility = initial(mob.invisibility)
				mob.alpha = initial(mob.alpha)
				mob.name = initial(mob.name)
				mob.mouse_opacity = initial(mob.mouse_opacity)
		else
			var/new_key = ckeyEx(input("Enter your desired display name.", "Fake Key", key) as text|null)
			if(!new_key)
				return
			if(length(new_key) >= 26)
				new_key = copytext(new_key, 1, 26)
			holder.fakekey = new_key
			createStealthKey()
			if(isobserver(mob))
				mob.invisibility = INVISIBILITY_MAXIMUM + 1 //JUST IN CASE
				mob.alpha = 0 //JUUUUST IN CASE
				mob.name = " "
				mob.mouse_opacity = 0
		log_admin("[key_name(usr)] has turned stealth mode [holder.fakekey ? "ON" : "OFF"]")
		message_admins("[key_name_admin(usr)] has turned stealth mode [holder.fakekey ? "ON" : "OFF"]")
	feedback_add_details("admin_verb","SM") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/drop_bomb()
	set category = "Special Verbs"
	set name = "Drop Bomb"
	set desc = "Cause an explosion of varying strength at your location."

	var/list/choices = list("Small Bomb", "Medium Bomb", "Big Bomb", "Custom Bomb")
	var/choice = input("What size explosion would you like to produce?") in choices
	var/turf/epicenter = mob.loc
	switch(choice)
		if(null)
			return 0
		if("Small Bomb")
			explosion(epicenter, 1, 2, 3, 3)
		if("Medium Bomb")
			explosion(epicenter, 2, 3, 4, 4)
		if("Big Bomb")
			explosion(epicenter, 3, 5, 7, 5)
		if("Custom Bomb")
			var/devastation_range = input("Devastation range (in tiles):") as null|num
			if(devastation_range == null)
				return
			var/heavy_impact_range = input("Heavy impact range (in tiles):") as null|num
			if(heavy_impact_range == null)
				return
			var/light_impact_range = input("Light impact range (in tiles):") as null|num
			if(light_impact_range == null)
				return
			var/flash_range = input("Flash range (in tiles):") as null|num
			if(flash_range == null)
				return
			epicenter = mob.loc //We need to reupdate as they may have moved again
			explosion(epicenter, devastation_range, heavy_impact_range, light_impact_range, flash_range)
	message_admins("[ckey] creating an admin explosion at [epicenter.loc].")
	feedback_add_details("admin_verb","DB") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/give_spell(mob/T in mob_list)
	set category = "Fun"
	set name = "Give Spell"
	set desc = "Gives a spell to a mob."

	var/list/spell_list = list()
	var/type_length = length("/obj/effect/proc_holder/spell") + 2
	for(var/A in spells)
		spell_list[copytext("[A]", type_length)] = A
	var/obj/effect/proc_holder/spell/S = input("Choose the spell to give to that guy", "ABRAKADABRA") as null|anything in spell_list
	if(!S)
		return
	S = spell_list[S]
	feedback_add_details("admin_verb","GS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	log_admin("[key_name(usr)] gave [key_name(T)] the spell [S].")
	message_admins("[key_name_admin(usr)] gave [key_name(T)] the spell [S].")
	if(T.mind)
		T.mind.AddSpell(new S)
	else
		T.AddSpell(new S)
		message_admins("Spells given to mindless mobs will not be transferred in mindswap or cloning!")

/client/proc/give_disease(mob/T in mob_list)
	set category = "Fun"
	set name = "Give Disease"
	set desc = "Gives a Disease to a mob."
	var/datum/disease/D = input("Choose the disease to give to that guy", "ACHOO") as null|anything in diseases
	if(!D) return
	T.ForceContractDisease(new D)
	feedback_add_details("admin_verb","GD") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	log_admin("[key_name(usr)] gave [key_name(T)] the disease [D].")
	message_admins("[key_name_admin(usr)] gave [key_name(T)] the disease [D].")

/client/proc/object_say(obj/O in world)
	set category = "Special Verbs"
	set name = "OSay"
	set desc = "Makes an object say something."
	var/message = input(usr, "What do you want the message to be?", "Make Sound") as text | null
	if(!message)
		return
	var/templanguages = O.languages
	O.languages |= ALL
	O.say(message)
	O.languages = templanguages
	log_admin("[key_name(usr)] made [O] at [O.x], [O.y], [O.z] say \"[message]\"")
	message_admins("[key_name_admin(usr)] made [O] at [O.x], [O.y], [O.z]. say \"[message]\"")
	feedback_add_details("admin_verb","OS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
/client/proc/togglebuildmodeself()
	set name = "Toggle Build Mode Self"
	set category = "Special Verbs"
	if(src.mob)
		togglebuildmode(src.mob)
	feedback_add_details("admin_verb","TBMS") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/toggle_log_hrefs()
	set name = "Toggle href logging"
	set category = "Server"
	if(!holder)
		return
	if(config)
		if(config.log_hrefs)
			config.log_hrefs = 0
			src << "Stopped logging hrefs"
		else
			config.log_hrefs = 1
			src << "Started logging hrefs"

/client/proc/check_ai_laws()
	set name = "Check AI Laws"
	set category = "Admin"
	if(holder)
		src.holder.output_ai_laws()

/client/proc/deadmin()
	set name = "Deadmin"
	set category = "Admin"
	set desc = "Shed your admin powers."

	if(!holder)
		return

	holder.disassociate()
	qdel(holder)

	deadmins += ckey
	admin_datums -= ckey
	verbs += /client/proc/readmin

	src << "You are now a normal player."
	log_admin("[src] deadmined themself.")
	message_admins("[src] deadmined themself.")
	feedback_add_details("admin_verb","DAS")

/client/proc/readmin()
	set name = "Readmin"
	set category = "Admin"
	set desc = "Regain your admin powers."

	load_admins(ckey)

	if(!holder) // Something went wrong...
		return

	deadmins -= ckey
	verbs -= /client/proc/readmin

	src << "You are now an admin."
	message_admins("[src] re-adminned themselves.")
	log_admin("[src] re-adminned themselves.")
	feedback_add_details("admin_verb","RAS")

/client/proc/populate_world(amount = 50 as num)
	set name = "Populate World"
	set category = "Debug"
	set desc = "(\"Amount of mobs to create\") Populate the world with test mobs."

	if (amount > 0)
		var/area/area
		var/list/candidates
		var/turf/open/floor/tile
		var/j,k
		var/mob/living/carbon/human/mob

		for (var/i = 1 to amount)
			j = 100

			do
				area = pick(the_station_areas)

				if (area)

					candidates = get_area_turfs(area)

					if (candidates.len)
						k = 100

						do
							tile = pick(candidates)
						while ((!tile || !istype(tile)) && --k > 0)

						if (tile)
							mob = new/mob/living/carbon/human/interactive(tile)

							testing("Spawned test mob with name \"[mob.name]\" at [tile.x],[tile.y],[tile.z]")
			while (!area && --j > 0)

/client/proc/toggle_AI_interact()
 	set name = "Toggle Admin AI Interact"
 	set category = "Admin"
 	set desc = "Allows you to interact with most machines as an AI would as a ghost"

 	AI_Interact = !AI_Interact
 	log_admin("[key_name(usr)] has [AI_Interact ? "activated" : "deactivated"] Admin AI Interact")
 	message_admins("[key_name_admin(usr)] has [AI_Interact ? "activated" : "deactivated"] their AI interaction")


code:
//ban people from using custom names and appearances. that'll show 'em.

var/appearanceban_runonce	//Updates legacy bans with new info
var/appearance_keylist[0]	//to store the keys

/proc/appearance_fullban(mob/M, reason)
	if (!M || !M.key) return
	appearance_keylist.Add(text("[M.ckey] ## [reason]"))
	appearance_savebanfile()

/proc/appearance_client_fullban(ckey)
	if (!ckey) return
	appearance_keylist.Add(text("[ckey]"))
	appearance_savebanfile()

//returns a reason if M is banned, returns 0 otherwise
/proc/appearance_isbanned(mob/M)
	if(M)
		for(var/s in appearance_keylist)
			if(findtext(s, "[M.ckey]") == 1)
				var/startpos = findtext(s, "## ") + 3
				if(startpos && startpos < length(s))
					var/text = copytext(s, startpos, 0)
					if(text)
						return text
				return "Reason Unspecified"
	return 0

/*
DEBUG
/mob/verb/list_all_appearances()
	set name = "list all appearances"

	for(var/s in appearance_keylist)
		world << s

/mob/verb/reload_appearances()
	set name = "reload appearances"

	appearance_loadbanfile()
*/

/proc/appearance_loadbanfile()
	if(config.ban_legacy_system)
		var/savefile/S=new("data/appearance_full.ban")
		S["keys[0]"] >> appearance_keylist
		log_admin("Loading appearance_rank")
		S["runonce"] >> appearanceban_runonce

		if (!length(appearance_keylist))
			appearance_keylist=list()
			log_admin("appearance_keylist was empty")
	else
		if(!establish_db_connection())
			world.log << "Database connection failed. Reverting to the legacy ban system."
			diary << "Database connection failed. Reverting to the legacy ban system."
			config.ban_legacy_system = 1
			appearance_loadbanfile()
			return

		//appearance bans
		var/DBQuery/query = dbcon.NewQuery("SELECT ckey FROM [format_table_name("ban")] WHERE bantype = 'APPEARANCE_PERMABAN' AND NOT unbanned = 1")
		query.Execute()

		while(query.NextRow())
			var/ckey = query.item[1]

			appearance_keylist.Add("[ckey]")

/proc/appearance_savebanfile()
	var/savefile/S=new("data/appearance_full.ban")
	S["keys[0]"] << appearance_keylist

/proc/appearance_unban(mob/M)
	appearance_remove("[M.ckey]")
	appearance_savebanfile()

/proc/appearance_updatelegacybans()
	if(!appearanceban_runonce)
		log_admin("Updating appearancefile!")
		// Updates bans.. Or fixes them. Either way.
		for(var/T in appearance_keylist)
			if(!T)
				continue
		appearanceban_runonce++	//don't run this update again

/proc/appearance_remove(X)
	for (var/i = 1; i <= length(appearance_keylist); i++)
		if( findtext(appearance_keylist[i], "[X]") )
			appearance_keylist.Remove(appearance_keylist[i])
			appearance_savebanfile()
			return 1
	return 0

/*
/proc/DB_ban_isappearancebanned(var/playerckey)
	establish_db_connection()
	if(!dbcon.IsConnected())
		return

	var/sqlplayerckey = sanitizeSQL(ckey(playerckey))

	var/DBQuery/query = dbcon.NewQuery("SELECT id FROM [format_table_name("ban")] WHERE CKEY = '[sqlplayerckey]' AND ((bantype = 'APPEARANCE_PERMABAN') OR (bantype = 'APPEARANCE_TEMPBAN' AND expiration_time > Now())) AND unbanned != 1")
	query.Execute()
	while(query.NextRow())
		return 1
	return 0
*/


code:
//returns a reason if M is banned from rank, returns 0 otherwise
/proc/jobban_isbanned(mob/M, rank)
	if(!M || !istype(M) || !M.ckey)
		return 0

	if(!M.client) //no cache. fallback to a DBQuery
		var/DBQuery/query = dbcon.NewQuery("SELECT reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(M.ckey)]' AND job = '[sanitizeSQL(rank)]' AND (bantype = 'JOB_PERMABAN'  OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)")
		if(!query.Execute())
			log_game("SQL ERROR obtaining jobbans. Error : \[[query.ErrorMsg()]\]\n")
			return
		if(query.NextRow())
			var/reason = query.item[1]
			return reason ? reason : 1 //we don't want to return "" if there is no ban reason, as that would evaluate to false
		else
			return 0

	if(!M.client.jobbancache)
		jobban_buildcache(M.client)

	if(rank in M.client.jobbancache)
		var/reason = M.client.jobbancache[rank]
		return (reason) ? reason : 1 //see above for why we need to do this
	return 0

/proc/jobban_buildcache(client/C)
	if(C && istype(C))
		C.jobbancache = list()
		var/DBQuery/query = dbcon.NewQuery("SELECT job, reason FROM [format_table_name("ban")] WHERE ckey = '[sanitizeSQL(C.ckey)]' AND (bantype = 'JOB_PERMABAN'  OR (bantype = 'JOB_TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)")
		if(!query.Execute())
			log_game("SQL ERROR obtaining jobbans. Error : \[[query.ErrorMsg()]\]\n")
			return
		while(query.NextRow())
			C.jobbancache[query.item[1]] = query.item[2]

/proc/ban_unban_log_save(var/formatted_log)
	text2file(formatted_log,"data/ban_unban_log.txt")


code:
/var/create_mob_html = null
/datum/admins/proc/create_mob(mob/user)
	if (!create_mob_html)
		var/mobjs = null
		mobjs = jointext(typesof(/mob), ";")
		create_mob_html = file2text('html/create_object.html')
		create_mob_html = replacetext(create_mob_html, "null /* object types */", "\"[mobjs]\"")

	user << browse(replacetext(create_mob_html, "/* ref src */", "\ref[src]"), "window=create_mob;size=425x475")

/proc/randomize_human(mob/living/carbon/human/H)
	H.gender = pick(MALE, FEMALE)
	H.real_name = random_unique_name(H.gender)
	H.name = H.real_name
	H.underwear = random_underwear(H.gender)
	H.skin_tone = random_skin_tone()
	H.hair_style = random_hair_style(H.gender)
	H.facial_hair_style = random_facial_hair_style(H.gender)
	H.hair_color = random_short_color()
	H.facial_hair_color = H.hair_color
	H.eye_color = random_eye_color()
	H.dna.blood_type = random_blood_type()
	H.update_body()
	H.update_hair()


code:
var/create_object_html = null
var/list/create_object_forms = list(
	/obj, /obj/structure, /obj/machinery, /obj/effect,
	/obj/item, /obj/item/clothing, /obj/item/stack, /obj/item/device,
	/obj/item/weapon, /obj/item/weapon/reagent_containers, /obj/item/weapon/gun)

/datum/admins/proc/create_object(mob/user)
	if (!create_object_html)
		var/objectjs = null
		objectjs = jointext(typesof(/obj), ";")
		create_object_html = file2text('html/create_object.html')
		create_object_html = replacetext(create_object_html, "null /* object types */", "\"[objectjs]\"")

	user << browse(replacetext(create_object_html, "/* ref src */", "\ref[src]"), "window=create_object;size=425x475")

/datum/admins/proc/quick_create_object(mob/user)
	var/path = input("Select the path of the object you wish to create.", "Path", /obj) in create_object_forms
	var/html_form = create_object_forms[path]

	if (!html_form)
		var/objectjs = jointext(typesof(path), ";")
		html_form = file2text('html/create_object.html')
		html_form = replacetext(html_form, "null /* object types */", "\"[objectjs]\"")
		create_object_forms[path] = html_form

	user << browse(replacetext(html_form, "/* ref src */", "\ref[src]"), "window=qco[path];size=425x475")


code:
/client/proc/create_poll()
	set name = "Create Poll"
	set category = "Special Verbs"
	if(!check_rights(R_PERMISSIONS))
		return
	if(!dbcon.IsConnected())
		src << "Failed to establish database connection."
		return
	var/returned = create_poll_function()
	if(returned)
		var/DBQuery/query_check_option = dbcon.NewQuery("SELECT id FROM [format_table_name("poll_option")] WHERE pollid = [returned]")
		if(!query_check_option.Execute())
			var/err = query_check_option.ErrorMsg()
			log_game("SQL ERROR obtaining id from poll_option table. Error : \[[err]\]\n")
			return
		if(query_check_option.NextRow())
			var/DBQuery/query_log_get = dbcon.NewQuery("SELECT polltype, question, adminonly FROM [format_table_name("poll_question")] WHERE id = [returned]")
			if(!query_log_get.Execute())
				var/err = query_log_get.ErrorMsg()
				log_game("SQL ERROR obtaining polltype, question, adminonly from poll_question table. Error : \[[err]\]\n")
				return
			if(query_log_get.NextRow())
				var/polltype = query_log_get.item[1]
				var/question = query_log_get.item[2]
				var/adminonly = text2num(query_log_get.item[3])
				log_admin("[key_name(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"] - Question: [question]")
				message_admins("[key_name_admin(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"]
Question: [question]")
		else
			src << "Poll question created without any options, poll will be deleted."
			var/DBQuery/query_del_poll = dbcon.NewQuery("DELETE FROM [format_table_name("poll_question")] WHERE id = [returned]")
			if(!query_del_poll.Execute())
				var/err = query_del_poll.ErrorMsg()
				log_game("SQL ERROR deleting poll question [returned]. Error : \[[err]\]\n")
				return

/client/proc/create_poll_function()
	var/polltype = input("Choose poll type.","Poll Type") in list("Single Option","Text Reply","Rating","Multiple Choice")
	var/choice_amount = 0
	switch(polltype)
		if("Single Option")
			polltype = POLLTYPE_OPTION
		if("Text Reply")
			polltype = POLLTYPE_TEXT
		if("Rating")
			polltype = POLLTYPE_RATING
		if("Multiple Choice")
			polltype = POLLTYPE_MULTI
			choice_amount = input("How many choices should be allowed?","Select choice amount") as num|null
			if(!choice_amount)
				return
	var/starttime = SQLtime()
	var/endtime = input("Set end time for poll as format YYYY-MM-DD HH:MM:SS. All times in server time. HH:MM:SS is optional and 24-hour. Must be later than starting time for obvious reasons.", "Set end time", SQLtime()) as text
	if(!endtime)
		return
	endtime = sanitizeSQL(endtime)
	var/DBQuery/query_validate_time = dbcon.NewQuery("SELECT STR_TO_DATE('[endtime]','%Y-%c-%d %T')")
	if(!query_validate_time.Execute())
		var/err = query_validate_time.ErrorMsg()
		log_game("SQL ERROR validating endtime. Error : \[[err]\]\n")
		return
	if(query_validate_time.NextRow())
		endtime = query_validate_time.item[1]
		if(!endtime)
			src << "Datetime entered is invalid."
			return
	var/DBQuery/query_time_later = dbcon.NewQuery("SELECT TIMESTAMP('[endtime]') < NOW()")
	if(!query_time_later.Execute())
		var/err = query_time_later.ErrorMsg()
		log_game("SQL ERROR comparing endtime to NOW(). Error : \[[err]\]\n")
		return
	if(query_time_later.NextRow())
		var/checklate = text2num(query_time_later.item[1])
		if(checklate)
			src << "Datetime entered is not later than current server time."
			return
	var/adminonly
	switch(alert("Admin only poll?",,"Yes","No","Cancel"))
		if("Yes")
			adminonly = 1
		if("No")
			adminonly = 0
		else
			return
	var/sql_ckey = sanitizeSQL(ckey)
	var/question = input("Write your question","Question") as message|null
	if(!question)
		return
	question = sanitizeSQL(question)
	var/DBQuery/query_polladd_question = dbcon.NewQuery("INSERT INTO [format_table_name("poll_question")] (polltype, starttime, endtime, question, adminonly, multiplechoiceoptions, createdby_ckey, createdby_ip) VALUES ('[polltype]', '[starttime]', '[endtime]', '[question]', '[adminonly]', '[choice_amount]', '[sql_ckey]', '[address]')")
	if(!query_polladd_question.Execute())
		var/err = query_polladd_question.ErrorMsg()
		log_game("SQL ERROR adding new poll question to table. Error : \[[err]\]\n")
		return
	if(polltype == POLLTYPE_TEXT)
		log_admin("[key_name(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"] - Question: [question]")
		message_admins("[key_name_admin(usr)] has created a new server poll. Poll type: [polltype] - Admin Only: [adminonly ? "Yes" : "No"]
Question: [question]")
		return
	var/pollid = 0
	var/DBQuery/query_get_id = dbcon.NewQuery("SELECT id FROM [format_table_name("poll_question")] WHERE question = '[question]' AND starttime = '[starttime]' AND endtime = '[endtime]' AND createdby_ckey = '[sql_ckey]' AND createdby_ip = '[address]'")
	if(!query_get_id.Execute())
		var/err = query_get_id.ErrorMsg()
		log_game("SQL ERROR obtaining id from poll_question table. Error : \[[err]\]\n")
		return
	if(query_get_id.NextRow())
		pollid = query_get_id.item[1]
	var/add_option = 1
	while(add_option)
		var/option = input("Write your option","Option") as message|null
		if(!option)
			return pollid
		option = sanitizeSQL(option)
		var/percentagecalc
		switch(alert("Calculate option results as percentage?",,"Yes","No","Cancel"))
			if("Yes")
				percentagecalc = 1
			if("No")
				percentagecalc = 0
			else
				return pollid
		var/minval = 0
		var/maxval = 0
		var/descmin = ""
		var/descmid = ""
		var/descmax = ""
		if(polltype == POLLTYPE_RATING)
			minval = input("Set minimum rating value.","Minimum rating") as num|null
			if(!minval)
				return pollid
			maxval = input("Set maximum rating value.","Maximum rating") as num|null
			if(!maxval)
				return pollid
			if(minval >= maxval)
				src << "Minimum rating value can't be more than maximum rating value"
				return pollid
			descmin = input("Optional: Set description for minimum rating","Minimum rating description") as message|null
			if(descmin)
				descmin = sanitizeSQL(descmin)
			else if(descmin == null)
				return pollid
			descmid = input("Optional: Set description for median rating","Median rating description") as message|null
			if(descmid)
				descmid = sanitizeSQL(descmid)
			else if(descmid == null)
				return pollid
			descmax = input("Optional: Set description for maximum rating","Maximum rating description") as message|null
			if(descmax)
				descmax = sanitizeSQL(descmax)
			else if(descmax == null)
				return pollid
		var/DBQuery/query_polladd_option = dbcon.NewQuery("INSERT INTO [format_table_name("poll_option")] (pollid, text, percentagecalc, minval, maxval, descmin, descmid, descmax) VALUES ('[pollid]', '[option]', '[percentagecalc]', '[minval]', '[maxval]', '[descmin]', '[descmid]', '[descmax]')")
		if(!query_polladd_option.Execute())
			var/err = query_polladd_option.ErrorMsg()
			log_game("SQL ERROR adding new poll option to table. Error : \[[err]\]\n")
			return pollid
		switch(alert(" ",,"Add option","Finish"))
			if("Add option")
				add_option = 1
			if("Finish")
				add_option = 0
	return pollid


code:
/var/create_turf_html = null
/datum/admins/proc/create_turf(mob/user)
	if (!create_turf_html)
		var/turfjs = null
		turfjs = jointext(typesof(/turf), ";")
		create_turf_html = file2text('html/create_object.html')
		create_turf_html = replacetext(create_turf_html, "null /* object types */", "\"[turfjs]\"")

	user << browse(replacetext(create_turf_html, "/* ref src */", "\ref[src]"), "window=create_turf;size=425x475")


code:
var/list/admin_datums = list()

/datum/admins
	var/datum/admin_rank/rank

	var/client/owner	= null
	var/fakekey			= null

	var/datum/marked_datum

	var/admincaster_screen = 0	//TODO: remove all these 5 variables, they are completly unacceptable
	var/datum/newscaster/feed_message/admincaster_feed_message = new /datum/newscaster/feed_message
	var/datum/newscaster/wanted_message/admincaster_wanted_message = new /datum/newscaster/wanted_message
	var/datum/newscaster/feed_channel/admincaster_feed_channel = new /datum/newscaster/feed_channel
	var/admin_signature

/datum/admins/New(datum/admin_rank/R, ckey)
	if(!ckey)
		spawn(0)
			del(src)
		throw EXCEPTION("Admin datum created without a ckey")
		return
	if(!istype(R))
		spawn(0)
			del(src)
		throw EXCEPTION("Admin datum created without a rank")
		return
	rank = R
	admin_signature = "Nanotrasen Officer #[rand(0,9)][rand(0,9)][rand(0,9)]"
	admin_datums[ckey] = src

/datum/admins/proc/associate(client/C)
	if(istype(C))
		owner = C
		owner.holder = src
		owner.add_admin_verbs()	//TODO
		owner.verbs -= /client/proc/readmin
		admins |= C

/datum/admins/proc/disassociate()
	if(owner)
		admins -= owner
		owner.remove_admin_verbs()
		owner.holder = null
		owner = null

/datum/admins/proc/check_if_greater_rights_than_holder(datum/admins/other)
	if(!other)
		return 1 //they have no rights
	if(rank.rights == 65535)
		return 1 //we have all the rights
	if(src == other)
		return 1 //you always have more rights than yourself
	if(rank.rights != other.rank.rights)
		if( (rank.rights & other.rank.rights) == other.rank.rights )
			return 1 //we have all the rights they have and more
	return 0

/*
checks if usr is an admin with at least ONE of the flags in rights_required. (Note, they don't need all the flags)
if rights_required == 0, then it simply checks if they are an admin.
if it doesn't return 1 and show_msg=1 it will prints a message explaining why the check has failed
generally it would be used like so:

proc/admin_proc()
	if(!check_rights(R_ADMIN)) return
	world << "you have enough rights!"

NOTE: it checks usr! not src! So if you're checking somebody's rank in a proc which they did not call
you will have to do something like if(client.rights & R_ADMIN) yourself.
*/
/proc/check_rights(rights_required, show_msg=1)
	if(usr && usr.client)
		if (check_rights_for(usr.client, rights_required))
			return 1
		else
			if(show_msg)
				usr << "Error: You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")]."
	return 0

//probably a bit iffy - will hopefully figure out a better solution
/proc/check_if_greater_rights_than(client/other)
	if(usr && usr.client)
		if(usr.client.holder)
			if(!other || !other.holder)
				return 1
			return usr.client.holder.check_if_greater_rights_than_holder(other.holder)
	return 0

//This proc checks whether subject has at least ONE of the rights specified in rights_required.
/proc/check_rights_for(client/subject, rights_required)
	if(subject && subject.holder && subject.holder.rank)
		if(rights_required && !(rights_required & subject.holder.rank.rights))
			return 0
		return 1
	return 0


code:
/client/proc/edit_admin_permissions()
	set category = "Admin"
	set name = "Permissions Panel"
	set desc = "Edit admin permissions"
	if(!check_rights(R_PERMISSIONS))
		return
	usr.client.holder.edit_admin_permissions()

/datum/admins/proc/edit_admin_permissions()
	if(!check_rights(R_PERMISSIONS))
		return

	var/output = {"<!DOCTYPE html>

<title>Permissions Panel</title>

<link rel='stylesheet' type='text/css' href='panels.css'>

CKEY \[+\]
RANK
PERMISSIONS
VERB-OVERRIDES

"}

	for(var/adm_ckey in admin_datums)
		var/datum/admins/D = admin_datums[adm_ckey]
		if(!D)
			continue

		var/rights = rights2text(D.rank.rights," ")
		if(!rights)	rights = "*none*"

		output += "
"
		output += "[adm_ckey] \[-\]	"
		output += "[D.rank.name]	"
		output += "[rights]	"
		output += "[rights2text(0," ",D.rank.adds,D.rank.subs)]	"
		output += "
"

	output += {"

Search: <input type='text' id='filter' value='' style='width:70%;' onkeyup='updateSearch();'>

"}

	usr << browse(output,"window=editrights;size=900x650")

/datum/admins/proc/log_admin_rank_modification(adm_ckey, new_rank)
	if(config.admin_legacy_system)
		return

	if(!usr.client)
		return

	if (!check_rights(R_PERMISSIONS))
		return

	establish_db_connection()

	if(!dbcon.IsConnected())
		usr << "Failed to establish database connection."
		return

	if(!adm_ckey || !new_rank)
		return

	adm_ckey = ckey(adm_ckey)

	if(!adm_ckey)
		return

	if(!istext(adm_ckey) || !istext(new_rank))
		return

	var/DBQuery/select_query = dbcon.NewQuery("SELECT id FROM [format_table_name("admin")] WHERE ckey = '[adm_ckey]'")
	select_query.Execute()

	var/new_admin = 1
	var/admin_id
	while(select_query.NextRow())
		new_admin = 0
		admin_id = text2num(select_query.item[1])

	if(new_admin)
		var/DBQuery/insert_query = dbcon.NewQuery("INSERT INTO `[format_table_name("admin")]` (`id`, `ckey`, `rank`, `level`, `flags`) VALUES (null, '[adm_ckey]', '[new_rank]', -1, 0)")
		insert_query.Execute()
		var/DBQuery/log_query = dbcon.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Added new admin [adm_ckey] to rank [new_rank]');")
		log_query.Execute()
		usr << "New admin added."
	else
		if(!isnull(admin_id) && isnum(admin_id))
			var/DBQuery/insert_query = dbcon.NewQuery("UPDATE `[format_table_name("admin")]` SET rank = '[new_rank]' WHERE id = [admin_id]")
			insert_query.Execute()
			var/DBQuery/log_query = dbcon.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Edited the rank of [adm_ckey] to [new_rank]');")
			log_query.Execute()
			usr << "Admin rank changed."

/datum/admins/proc/log_admin_permission_modification(adm_ckey, new_permission)
	if(config.admin_legacy_system)
		return
	if(!usr.client)
		return
	if(check_rights(R_PERMISSIONS))
		return

	establish_db_connection()
	if(!dbcon.IsConnected())
		usr << "Failed to establish database connection."
		return

	if(!adm_ckey || !istext(adm_ckey) || !isnum(new_permission))
		return

	var/DBQuery/select_query = dbcon.NewQuery("SELECT id, flags FROM [format_table_name("admin")] WHERE ckey = '[adm_ckey]'")
	select_query.Execute()

	var/admin_id
	while(select_query.NextRow())
		admin_id = text2num(select_query.item[1])

	if(!admin_id)
		return

	var/DBQuery/insert_query = dbcon.NewQuery("UPDATE `[format_table_name("admin")]` SET flags = [new_permission] WHERE id = [admin_id]")
	insert_query.Execute()
	var/DBQuery/log_query = dbcon.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Edit permission [rights2text(new_permission)] (flag = [new_permission]) to admin [adm_ckey]');")
	log_query.Execute()