code:
GLOBAL_LIST_INIT_TYPED(sdql_spells, /obj/effect/proc_holder/spell, list())

/client/proc/cmd_sdql_spell_menu(target in GLOB.mob_list)
	set name = "Give/Edit SDQL spell"
	set hidden = TRUE
	if(CONFIG_GET(flag/sdql_spells))
		var/datum/give_sdql_spell/ui = new(usr, target)
		ui.ui_interact(usr)
	else
		to_chat(usr, span_warning("SDQL spells are disabled."))

/datum/give_sdql_spell
	var/client/user
	var/mob/living/target_mob
	var/obj/effect/proc_holder/spell/target_spell
	var/spell_type
	var/list/saved_vars = list("query" = "", "suppress_message_admins" = FALSE)
	var/list/list_vars = list()
	var/list/parse_result = null
	var/alert

	//This list contains all the vars that it should be okay to edit from the menu
	var/static/list/editable_spell_vars = list(
		"action_background_icon_state",
		"action_icon_state",
		"action_icon",
		"active_msg",
		"aim_assist",
		"antimagic_allowed",
		"base_icon_state",
		"centcom_cancast",
		"charge_max",
		"charge_type",
		"clothes_req",
		"cone_level",
		"cult_req",
		"deactive_msg",
		"desc",
		"drawmessage",
		"dropmessage",
		"hand_path",
		"hand_var_overrides",
		"holder_var_amount",
		"holder_var_type",
		"human_req",
		"include_user",
		"inner_radius",
		"invocation_emote_self",
		"invocation_type",
		"invocation",
		"max_targets",
		"message",
		"name",
		"nonabstract_req",
		"overlay_icon_state",
		"overlay_icon",
		"overlay_lifespan",
		"overlay",
		"phase_allowed",
		"player_lock",
		"projectile_type",
		"projectile_amount",
		"projectile_var_overrides",
		"projectiles_per_fire",
		"random_target_priority",
		"random_target",
		"range",
		"ranged_mousepointer",
		"respect_density",
		"selection_type",
		"self_castable",
		"smoke_amt",
		"smoke_spread",
		"sound",
		"sparks_amt",
		"sparks_spread",
		"stat_allowed",
		"still_recharging_msg",
		"target_ignore_prev",
	)

	//If a spell creates a datum with vars it overrides, this list should contain an association with the variable containing the path of the created datum.
	var/static/list/special_list_vars = list(
		"projectile_var_overrides" = "projectile_type",
		"hand_var_overrides" = "hand_path",
	)

	var/static/list/special_var_lists = list(
		"projectile_type" = "projectile_var_overrides",
		"hand_path" = "hand_var_overrides",
	)

	var/static/list/enum_vars = list(
		"invocation_type" = list("none", "whisper", "shout", "emote"),
		"selection_type" = list("view", "range"),
		"smoke_spread" = list(0, 1, 2, 3),
		"random_target_priority" = list(0, 1),
	)

	//base64 representations of any icons that may need to be displayed
	var/action_icon_base64
	var/projectile_icon_base64
	var/hand_icon_base64
	var/overlay_icon_base64
	var/mouse_icon_base64

/datum/give_sdql_spell/New(user, target)
	if(!CONFIG_GET(flag/sdql_spells))
		to_chat(user, span_warning("SDQL spells are disabled."))
		qdel(src)
		return
	src.user = CLIENT_FROM_VAR(user)

	if(istype(target, /obj/effect/proc_holder/spell))
		target_spell = target
		var/mob/living/spell_owner = target_spell.owner.resolve()
		if(spell_owner)
			target_mob = spell_owner
		else
			to_chat(user, span_warning("[target_spell] does not have an owner, or its owner was qdelled. This REALLY shouldn't happen."))
			qdel(src)
			return
	else if(isliving(target))
		target_mob = target
	else
		to_chat(user, span_warning("Invalid target."))
		qdel(src)
		return
	if(target_spell)
		load_vars_from(target_spell)

/datum/give_sdql_spell/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "SDQLSpellMenu", "Give SDQL Spell")
		ui.open()
		ui.set_autoupdate(FALSE)

/datum/give_sdql_spell/ui_state(mob/user)
	return GLOB.admin_state

/datum/give_sdql_spell/ui_status(mob/user, datum/ui_state/state)
	if(QDELETED(target_mob))
		return UI_CLOSE
	return ..()

/datum/give_sdql_spell/ui_close(mob/user)
	qdel(src)

#define SANITIZE_NULLIFY 0
#define SANITIZE_STRINGIFY 1

/datum/give_sdql_spell/ui_data(mob/user, params)
	var/list/data = list()
	if(target_spell)
		data["type"] = copytext("[target_spell.type]", 31, -5)
		data["fixed_type"] = TRUE
	else
		data["type"] = spell_type
		data["fixed_type"] = FALSE
	data["saved_vars"] = saved_vars
	data["list_vars"] = json_sanitize_list_vars(list_vars, SANITIZE_STRINGIFY)
	if(parse_result)
		data["parse_errors"] = parse_result["parse_errors"]
		data["parsed_type"] = parse_result["type"]
		data["parsed_vars"] = parse_result["vars"]
		data["parsed_list_vars"] = json_sanitize_list_vars(parse_result["list_vars"], SANITIZE_STRINGIFY)
	else
		data["parse_errors"] = null
		data["parsed_type"] = null
		data["parsed_vars"] = null
		data["parsed_list_vars"] = null
	data["action_icon"] = action_icon_base64
	data["projectile_icon"] = projectile_icon_base64
	data["hand_icon"] = hand_icon_base64
	data["overlay_icon"] = overlay_icon_base64
	data["mouse_icon"] = mouse_icon_base64
	data["alert"] = alert
	alert = ""
	return data

/datum/give_sdql_spell/ui_static_data(mob/user)
	return list(
		"types" = list("aimed", "aoe_turf", "cone", "cone/staggered", "pointed", "self", "targeted", "targeted/touch"),
		"tooltips" = list(
			"query" = "The SDQL query that is executed. Certain keywords are specific to SDQL spell queries.\n\
				$type\n\
				USER is replaced with a reference to the user of the spell.\n\
				TARGETS_AND_USER is replaced with the combined references from TARGETS and USER.\n\
				SOURCE is replaced with a reference to this spell, allowing you to refer to and edit variables within it.\n\
				SCRATCHPAD is a list used to store variables between individual queries within the same cast or between multiple casts.\n\
				NOTE: The SDQL keywords usr and marked do not work.",
			"query_aimed" = "TARGETS is replaced with a list containing a reference to the atom hit by the fired projectile.",
			"query_aoe_turf" = "TARGETS is replaced with a list containing references to every atom in the spell's area of effect.",
			"query_cone" = "TARGETS is replaced with a list containing references to every atom in the cone produced by the spell.",
			"query_cone/staggered" = "The query will be executed once for every level of the cone produced by the spell.\n\
				TARGETS is replaced with a list containing references to every atom in the given level of the cone.",
			"query_pointed" = "TARGETS is replaced with a list containing a reference to the targeted atom.",
			"query_self" = "TARGETS is null.",
			"query_targeted" = "TARGETS is replaced with a list containing a reference(s) to the targeted mob(s).",
			"query_targeted_touch" = "TARGETS is replaced with a list containing a reference to the atom hit with the touch attack.",
			"suppress_message_admins" = "If this is true, the spell will not print out its query to admins' chat panels.\n\
				The query will still be output to the game log.",
			"charge_type" = "How the spell's charge works. This affects how charge_max is used.\n\
				When set to \"recharge\", charge_max is the time in deciseconds between casts of the spell.\n\
				When set to \"charges\", the user can only use the spell a number of times equal to charge_max.\n\
				When set to \"holder_var\", charge_max is not used. holder_var_type and holder_var_amount are used instead.\n",
			"holder_var_type" = "When charge_type is set to \"holder_var\", this is the name of the var that is modified each time the spell is cast.\n\
				If this is set to \"bruteloss\", \"fireloss\", \"toxloss\", or \"oxyloss\", the user will take the corresponding damage.\n\
				If this is set to \"stun\", \"knockdown\", \"paralyze\", \"immobilize\", or \"unconscious\", the user will suffer the corresponding status effect.\n\
				If this is set to anything else, the variable with the appropriate name will be modified.",
			"holder_var_amount" = "The amount of damage taken, the duration of status effect inflicted, or the change made to any other variable.",
			"clothes_req" = "Whether the user has to be wearing wizard robes to cast the spell.",
			"cult_req" = "Whether the user has to be wearing cult robes to cast the spell.",
			"human_req" = "Whether the user has to be a human to cast the spell. Redundant when clothes_req is true.",
			"nonabstract_req" = "If this is true, the spell cannot be cast by brains and pAIs.",
			"stat_allowed" = "Whether the spell can be cast if the user is unconscious or dead.",
			"phase_allowed" = "Whether the spell can be cast while the user is jaunting or bloodcrawling.",
			"antimagic_allowed" = "Whether the spell can be cast while the user is affected by anti-magic effects.",
			"invocation_type" = "How the spell is invoked.\n\
				When set to \"whisper\", the user whispers the invocation, as if with the whisper verb.\n\
				When set to \"shout\", the user says the invocation, as if with the say verb.\n\
				When set to \"emote\", a visible message is produced.",
			"invocation" = "What the user says, whispers, or emotes when using the spell.",
			"invocation_emote_self" = "What the user sees in their own chat when they use the spell.",
			"selection_type" = "Whether the spell can target any mob in range, or only visible mobs in range.",
			"range" = "The spell's range, in tiles.",
			"message" = "What mobs affected by the spell see in their chat.\n\
				Keep in mind, just because a mob is affected by the spell doesn't mean the query will have any effect on them.",
			"player_lock" = "If false, simple mobs can use the spell.",
			"overlay" = "Whether an overlay is drawn atop atoms affectecd by the spell.\n\
				Keep in mind, just because an atom is affected by the spell doesn't mean the query will have any effect on it.",
			"overlay_lifetime" = "The amount of time in deciseconds the overlay will persist.",
			"sparks_spread" = "Whether the spell produces sparks when cast.",
			"smoke_spread" = "The kind of smoke, if any, the spell produces when cast.",
			"centcom_cancast" = "If true, the spell can be cast on the centcom Z-level.",
			"max_targets" = "The maximum number of mobs the spell can target.",
			"target_ignore_prev" = "If false, the same mob can be targeted multiple times.",
			"include_user" = "If true, the user can target themselves with the spell.",
			"random_target" = "If true, the spell will target a random mob(s) in range.",
			"random_target_priority" = "Whether the spell will target random mobs in range or the closest mobs in range.",
			"inner_radius" = "If this is a non-negative number, the spell will not affect atoms within that many tiles of the user.",
			"ranged_mousepointer" = "The icon used for the mouse when aiming the spell.",
			"deactive_mesg" = "The message the user sees when canceling the spell.",
			"active_msg" = "The message the user sees when activating the spell.",
			"projectile_amount" = "The maximum number of projectiles the user can fire with each cast of the spell.",
			"projectiles_per_fire" = "The amount of projectiles fired with each click of the mouse.",
			"projectile_var_overrides" = "The fired projectiles will have the appropriate variables overridden by the corresponding values in this associative list.\n\
				You should probably set \"name\", \"icon\", and \"icon_state\".\n\
				Refer to code/modules/projectiles/projectile.dm to see what other vars you can override.",
			"cone_level" = "How many tiles out the cone will extend.",
			"respect_density" = "If true, the cone produced by the spell is blocked by walls.",
			"self_castable" = "If true, the user can cast the spell on themselves.",
			"aim_assist" = "If true, the spell has turf-based aim assist.",
			"drawmessage" = "The message the user sees when activating the spell.",
			"dropmessage" = "The message the user sees when canceling the spell.",
			"hand_var_overrides" = "The touch attack will have the appropriate variables overridden by the corresponding values in this associative list.\n\
				You should probably set \"name\", \"desc\", \"catchphrase\", \"on_use_sound\" \"icon\", \"icon_state\", and \"inhand_icon_state\".\n\
				Refer to code/modules/spells/spell_types/godhand.dm to see what other vars you can override.",
			"scratchpad" = "This list can be used to store variables between individual queries within the same cast or between casts.\n\
				You can declare variables from this menu for convenience. To access this list in a query, use the identifier \"SOURCE.scratchpad\".\n\
				Refer to the _list procs defined in code/modules/admin/verbs/SDQL2/SDQL_2_wrappers.dm for information on how to modify and edit list vars from within a query.",
		),
	)

#define LIST_VAR_FLAGS_TYPED 1
#define LIST_VAR_FLAGS_NAMED 2

/datum/give_sdql_spell/proc/load_vars_from(obj/effect/proc_holder/spell/sample)
	var/datum/component/sdql_executor/executor = sample.GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[sample]'s SDQL executor component went missing!")
	saved_vars["query"] = executor.query
	saved_vars["suppress_message_admins"] = executor.suppress_message_admins
	load_list_var(executor.scratchpad, "scratchpad")
	for(var/V in sample.vars&editable_spell_vars)
		if(islist(sample.vars[V]))
			if(special_list_vars[V])
				var/list/saved_overrides = executor.saved_overrides[V]
				if(saved_overrides)
					list_vars[V] = saved_overrides.Copy()
				icon_needs_updating("[V]/icon")
		else
			saved_vars[V] = sample.vars[V]
			icon_needs_updating(V)

/datum/give_sdql_spell/proc/load_list_var(list/L, list_name)
	list_vars[list_name] = list()
	for(var/V in L)
		if(islist(L[V]))
			list_vars[list_name][V] = list("type" = "list", "value" = null)
			list_vars |= load_list_var(L[V], "[list_name]/[V]")
		else if(isnum(L[V]))
			list_vars[list_name][V] = list("type" = "num", "value" = L[V])
		else if(ispath(L[V]))
			list_vars[list_name][V] = list("type" = "path", "value" = L[V])
		else if(isicon(L[V]))
			list_vars[list_name][V] = list("type" = "icon", "value" = L[V])
		else if(istext(L[V]) || isfile(L[V]))
			list_vars[list_name][V] = list("type" = "string", "value" = L[V])
		else if(istype(L[V], /datum))
			list_vars[list_name][V] = list("type" = "ref", "value" = L[V])
		else if(isnull(L[V]))
			list_vars[list_name][V] = list("type" = "num", "value" = 0)
			alert = "Could not determine the type for [list_name]/[V]! Be sure to set it correctly, or you may cause unnecessary runtimes!"

/datum/give_sdql_spell/ui_act(action, params, datum/tgui/ui)
	if(..())
		return
	. = TRUE
	switch(action)
		if("type")
			if(!target_spell)
				spell_type = params["path"]
				load_sample()
		if("variable")
			var/V = params["name"]
			if(V == "holder_var_type")
				if(!holder_var_validate(params["value"]))
					return
			saved_vars[V] = params["value"]
			icon_needs_updating(V)
		if("bool_variable")
			saved_vars[params["name"]] = !saved_vars[params["name"]]
		if("path_variable")
			var/new_path = tgui_input_list(user, "Select type.", "Add SDQL Spell", typesof(text2path(params["root_path"])))
			if(new_path)
				saved_vars[params["name"]] = new_path
				var/datum/sample = new new_path
				var/list/overrides = list_vars[special_var_lists[params["name"]]]
				overrides = overrides&sample.vars
				qdel(sample)
				icon_needs_updating(params["name"])
		if("list_variable_add")
			if(!list_vars[params["list"]])
				list_vars[params["list"]] = list()
			if(special_list_vars[params["list"]])
				var/path = saved_vars[special_list_vars[params["list"]]]
				var/datum/sample = new path
				var/list/choosable_vars = map_var_list(sample.vars-list_vars[params["list"]], sample)
				var/chosen_var = tgui_input_list(user, "Select variable to add.", "Add SDQL Spell", sort_list(choosable_vars))
				if(chosen_var)
					if(islist(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "list", "value" = null, "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
						list_vars["[params["list"]]/[choosable_vars[chosen_var]]"] = list()
					else if(isnum(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
					else if(ispath(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "path", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
					else if(isicon(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "icon", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
					else if(istext(sample.vars[choosable_vars[chosen_var]]) || isfile(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "string", "value" = sample.vars[choosable_vars[chosen_var]], "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
					else if(istype(sample.vars[choosable_vars[chosen_var]], /datum))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "ref", "value" = null, "flags" = LIST_VAR_FLAGS_TYPED|LIST_VAR_FLAGS_NAMED)
						alert = "[params["list"]]/[choosable_vars[chosen_var]] is a reference! Be sure to set it correctly, or you may cause unnecessary runtimes!"
					else if(isnull(sample.vars[choosable_vars[chosen_var]]))
						list_vars[params["list"]][choosable_vars[chosen_var]] = list("type" = "num", "value" = 0, "flags" = LIST_VAR_FLAGS_NAMED)
						alert = "Could not determine the type for [params["list"]]/[choosable_vars[chosen_var]]! Be sure to set it correctly, or you may cause unnecessary runtimes!"
					else
						alert = "[params["list"]]/[choosable_vars[chosen_var]] is not of a supported type!"
					icon_needs_updating("[params["list"]]/[choosable_vars[chosen_var]]")
				qdel(sample)
			else
				if(!list_vars[params["list"]]["new_var"])
					list_vars[params["list"]] += list("new_var" = list("type" = "num", "value" = 0, "flags" = 0))
				else
					alert = "Rename or remove [params["list"]]/new_var before attempting to add another variable to this list!"
		if("list_variable_remove")
			remove_list_var(params["list"], params["name"])
		if("list_variable_rename")
			rename_list_var(params["list"], params["name"], params["new_name"])
		if("list_variable_change_type")
			change_list_var_type(params["list"], params["name"], params["value"])
		if("list_variable_change_value")
			set_list_var(params["list"], params["name"], params["value"])
			icon_needs_updating("[params["list"]]/[params["name"]]")
		if("list_variable_change_bool")
			toggle_list_var(params["list"], params["name"])
		if("list_variable_set_ref")
			set_list_ref_var(params["list"], params["name"])
		if("save")
			var/f = file("data/TempSpellUpload")
			fdel(f)
			WRITE_FILE(f, json_encode(list("type" = spell_type, "vars" = saved_vars, "list_vars" = json_sanitize_list_vars(list_vars))))
			user << ftp(f,"[replacetext_char(saved_vars["name"], " ", "_")].json")
		if("load")
			var/spell_file = input("Pick spell json file:", "File") as null|file
			if(!spell_file)
				return
			var/filedata = file2text(spell_file)
			var/json = json_decode(filedata)
			if(!json)
				alert = "JSON decode error!"
				return
			parse_result = load_from_json(json)
			var/list/parse_errors = parse_result["parse_errors"]
			if(!parse_errors.len)
				finalize_load()
		if("close_error")
			parse_result = null
		if("load_despite_error")
			finalize_load()
		if("confirm")
			if(target_spell)
				reassign_vars(target_spell)
				target_spell.action.UpdateButtonIcon()
				log_admin("[key_name(user)] edited the SDQL spell \"[target_spell]\" owned by [key_name(target_mob)].")
			else
				var/new_spell = give_spell()
				log_admin("[key_name(user)] gave the SDQL spell \"[new_spell]\" to [key_name(target_mob)].")
			ui.close()

/datum/give_sdql_spell/proc/load_sample()
	var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql")
	var/datum/sample = new path
	if(spell_type)
		load_vars_from(sample)
	qdel(sample)

/datum/give_sdql_spell/proc/finalize_load()
	spell_type = parse_result["type"]
	load_sample()
	saved_vars = parse_result["vars"] | saved_vars
	list_vars = parse_result["list_vars"] | list_vars
	parse_result = null
	icon_needs_updating("everything")

//Change all references in the list vars, either to null (for saving) or to their string representation (for display)
/datum/give_sdql_spell/proc/json_sanitize_list_vars(list/list_vars, mode = SANITIZE_NULLIFY)
	var/list/temp_list_vars = deep_copy_list(list_vars)
	for(var/V in temp_list_vars)
		var/list/L = temp_list_vars[V]
		for(var/W in L)
			if(temp_list_vars[V][W]["type"] == "ref")
				switch(mode)
					if(SANITIZE_NULLIFY)
						temp_list_vars[V][W]["value"] = null
					if(SANITIZE_STRINGIFY)
						if(temp_list_vars[V][W]["value"])
							temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
						else
							temp_list_vars[V][W]["value"] = "null"
	return temp_list_vars

#undef SANITIZE_NULLIFY
#undef SANITIZE_STRINGIFY

/datum/give_sdql_spell/proc/load_from_json(json)
	var/list/result_vars = list()
	var/list/result_list_vars = list()
	var/list/parse_errors = list()
	. = list("type" = "",
			"vars" = result_vars,
			"list_vars" = result_list_vars,
			"parse_errors" = parse_errors)
	if(!json["type"])
		parse_errors += "The \"type\" property is missing from the json file"
		return
	var/temp_type = json["type"]
	var/datum/D = text2path("/obj/effect/proc_holder/spell/[temp_type]/sdql")
	if(!ispath(D))
		parse_errors += "[temp_type] is not a valid SDQL spell type"
		return
	if(target_spell)
		if(!istype(target_spell, D))
			parse_errors += "You cannot change the type of an existing spell"
	if(!json["vars"])
		parse_errors += "The \"vars\" property is missing from the json file"
	if(!islist(json["vars"]))
		parse_errors += "The \"vars\" property must be a json object"
	if(!json["list_vars"])
		parse_errors += "The \"list_vars\" property is missing from the json file"
	if(!islist(json["list_vars"]))
		parse_errors += "The \"list_vars\" property must be a json object"
	if(parse_errors.len)
		return
	.["type"] = temp_type
	var/list/temp_vars = json["vars"]
	var/list/temp_list_vars = json["list_vars"]
	D = new D
	for(var/V in temp_vars)
		if(!istext(V))
			parse_errors += "JSON property names must be text ([V] is not text)"
			continue
		if(V == "query")
			if(!istext(temp_vars[V]))
				parse_errors += "The value of \"query\" must be text"
				continue
			result_vars[V] = temp_vars[V]
			continue
		if(V == "suppress_message_admins")
			if(!isnum(temp_vars[V]))
				parse_errors += "The value of \"suppress_message_admins\" must be a number"
				continue
			result_vars[V] = !!temp_vars[V]
			continue
		if(!(V in editable_spell_vars))
			parse_errors += "\"[V]\" is not an editable variable"
			continue
		if(!(V in D.vars)) //D.vars[V] can runtime unlike V in D.vars
			parse_errors += "Spells of type \"[temp_type]\" have no such var [V]"
			continue
		if(islist(D.vars[V]))
			parse_errors += "[D.type]/[V] is a list; vars.[V] should be in the \"list_vars\" property"
			continue
		if(istext(D.vars[V]))
			if(!istext(temp_vars[V]))
				parse_errors += "[D.type]/[V] is text; vars.[V] has been converted to text"
				temp_vars[V] = "[temp_vars[V]]"
				continue
			if(V=="holder_var_type")
				var/potential_alert = holder_var_validate(temp_vars[V], TRUE)
				if(potential_alert)
					parse_errors += potential_alert
					continue
		if(isicon(D.vars[V]))
			if(!istext(temp_vars[V]))
				parse_errors += "[D.type]/[V] is an icon; vars.[V] has been converted to text"
				temp_vars[V] = "[temp_vars[V]]"
			if(!fexists(temp_vars[V]))
				parse_errors += "[D.type]/[V] is an icon; no such file [temp_vars[V]] exists on the server"
				continue
		if(ispath(D.vars[V]))
			if(!istext(temp_vars[V]))
				parse_errors += "[D.type]/[V] is a path; vars.[V] has been converted to text"
				temp_vars[V] = "[temp_vars[V]]"
			var/path = text2path(temp_vars[V])
			if(!path)
				parse_errors += "[D.type]/[V] is a path; vars.[V] ([temp_vars[V]]) does not correspond to an existing type"
				continue
			if(!ispath(path, D.vars[V]))
				parse_errors += "[D.type]/[V] is a path; vars.[V] ([path]) is not derived from [D.vars[V]]"
				continue
		if(isnum(D.vars[V]))
			if(!isnum(temp_vars[V]))
				parse_errors += "[D.type]/[V] is a number; vars.[V] should be a number"
				continue
		if(enum_vars[V])
			var/list/enum = enum_vars[V]
			if(!enum.Find(temp_vars[V]))
				parse_errors += "[D.type]/[V] is an enumeration; vars.[V] should be one of: [english_list(enum, and_text = " or ")]"
				continue
		result_vars[V] = temp_vars[V]
	for(var/V in temp_list_vars)
		if(!istext(V))
			parse_errors += "JSON property names must be text ([V] is not text)"
			continue
		if(!islist(temp_list_vars[V]))
			parse_errors += "list_vars.[V] should be a json object"
			continue
		if(special_list_vars[V] && (V in D.vars))
			var/sample_path = D.vars[special_list_vars[V]]
			var/temp_path
			if(temp_vars[special_list_vars[V]])
				temp_path = temp_vars[special_list_vars[V]]
			temp_path = text2path(temp_path)
			if(!temp_path)
				parse_errors += "[D.type]/[special_list_vars[V]] is a path; vars.[temp_vars[special_list_vars[V]]] (temp_vars[special_list_vars[V]]) does not correspond to an existing type"
			else if(!ispath(temp_path, D.vars[special_list_vars[V]]))
				parse_errors += "[D.type]/[special_list_vars[V]] is a path; vars.[special_list_vars[V]] ([temp_path]) is not derived from [D.vars[special_list_vars[V]]]"
			else
				sample_path = temp_path
			result_list_vars[V] = list()
			var/datum/sample = new sample_path
			for(var/W in temp_list_vars[V])
				if(!istext(W))
					parse_errors += "JSON property names must be text ([W] in list_vars.[V] is not text)"
					continue
				if(!(W in sample.vars))
					parse_errors += "[sample.type] has no such var \"[W]\""
					continue
				if(!(islist(temp_list_vars[V][W]) && istext(temp_list_vars[V][W]["type"]) && (istext(temp_list_vars[V][W]["value"]) || isnum(temp_list_vars[V][W]["value"]) || isnull(temp_list_vars[V][W]["value"])) && isnum(temp_list_vars[V][W]["flags"])))
					parse_errors += "[V]/[W] is not of the form {type: string, value: num|string|null, flags: number}"
					continue
				if(!(temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_NAMED))
					parse_errors += "[V]/[W] did not have the LIST_VAR_FLAGS_NAMED flag set; it has been set"
					temp_list_vars[V][W]["flags"] |= LIST_VAR_FLAGS_NAMED
				if(temp_list_vars[V][W]["flags"] & ~(LIST_VAR_FLAGS_NAMED | LIST_VAR_FLAGS_TYPED))
					parse_errors += "[V]/[W] has unused bit flags set; they have been unset"
					temp_list_vars[V][W]["flags"] &= LIST_VAR_FLAGS_NAMED | LIST_VAR_FLAGS_TYPED
				if(!(temp_list_vars[V][W]["flags"] & LIST_VAR_FLAGS_TYPED))
					if(isnull(sample.vars[W]))
						continue
					parse_errors += "[sample.type]/[W] is not null; it has had the LIST_VAR_FLAGS_TYPED flag set"
					temp_list_vars[V][W]["flags"] |= LIST_VAR_FLAGS_TYPED
					if(islist(sample.vars[W]))
						temp_list_vars[V][W]["type"] = "list"
					else if(isnum(sample.vars[W]))
						temp_list_vars[V][W]["type"] = "num"
					else if(istext(sample.vars[W]))
						temp_list_vars[V][W]["type"] = "string"
					else if(ispath(sample.vars[W]))
						temp_list_vars[V][W]["type"] = "path"
					else if(isicon(sample.vars[W]))
						temp_list_vars[V][W]["type"] = "icon"
					else if(istype(sample.vars[W], /datum))
						temp_list_vars[V][W]["type"] = "ref"
						temp_list_vars[V][W]["value"] = null
					else
						parse_errors += "[sample.type]/[W] is not of a supported type"
						continue
				if(islist(sample.vars[W]))
					if(temp_list_vars[V][W]["type"] != "list")
						parse_errors += "[sample.type]/[W] is a list; list_vars.[V].[W].type has been converted to \"list\""
						temp_list_vars[V][W]["type"] = "list"
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
					if(!temp_list_vars[temp_list_vars[V][W]["value"]])
						parse_errors += "list_vars.[V].[W].type is \"list\"; there is no property of list_vars whose name is list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]])"
						continue
				else if(isnum(sample.vars[W]))
					if(temp_list_vars[V][W]["type"] != "num")
						parse_errors += "[sample.type]/[W] is a number; list_vars.[V].[W].type has been converted to \"num\""
						temp_list_vars[V][W]["type"] = "num"
					if(!isnum(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"num\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) should be a number"
						continue
				else if(istext(sample.vars[W]))
					if(temp_list_vars[V][W]["type"] != "string")
						parse_errors += "[sample.type]/[W] is text; list_vars.[V].[W].type has been converted to \"string\""
						temp_list_vars[V][W]["type"] = "string"
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
				else if(ispath(sample.vars[W]))
					if(temp_list_vars[V][W]["type"] != "path")
						parse_errors += "[sample.type]/[W] is a path; list_vars.[V].[W].type has been converted to \"path\""
						temp_list_vars[V][W]["type"] = "path"
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
					temp_path = text2path(temp_list_vars[V][W]["value"])
					if(!ispath(temp_path))
						parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) does not correspond to an existing type"
						continue
					if(!ispath(temp_path, sample.vars[W]))
						parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) is not derived from [sample.vars[W]]"
						continue
				else if(isicon(sample.vars[W]))
					if(temp_list_vars[V][W]["type"] != "icon")
						parse_errors += "[sample.type]/[W] is an icon; list_vars.[V].[W].type has been converted to \"icon\""
						temp_list_vars[V][W]["type"] = "icon"
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"icon\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
					if(!fexists(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"icon\"; no such file \"[temp_list_vars[V][W]["value"]]\" exists on the server"
						continue
				else if(istype(sample.vars[W], /datum))
					if(temp_list_vars[V][W]["type"] != "ref")
						parse_errors += "[sample.type]/[W] is a datum reference; list_vars.[V].[W].type has been converted to \"ref\""
						alert = "Reference vars are not assigned on load from file. Be sure to set them correctly."
						temp_list_vars[V][W]["type"] = "ref"
						temp_list_vars[V][W]["value"] = null
				result_list_vars[V][W] = temp_list_vars[V][W]
				qdel(sample)
		else
			result_list_vars[V] = list()
			for(var/W in temp_list_vars[V])
				if(temp_list_vars[V][W]["flags"])
					parse_errors += "list_vars.[V].[W] has unnecessary flags set; they have been unset"
					temp_list_vars[V][W]["flags"] = 0
				if(temp_list_vars[V][W]["type"] == "list")
					if(temp_list_vars[V][W]["value"])
						parse_errors += "list_vars.[V].[W].type is \"list\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been nulled, as it is not used"
						temp_list_vars[V][W]["value"] = null
					if(!temp_list_vars["[V]/[W]"])
						parse_errors += "list_vars.[V].[W].type is \"list\"; there is no property of list_vars whose name is \"[V]/[W]\""
						continue
				if(temp_list_vars[V][W]["type"] == "num")
					if(!isnum(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"num\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) should be a number"
						continue
				if(temp_list_vars[V][W]["type"] == "string")
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"string\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
				if(temp_list_vars[V][W]["type"] == "path")
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
					var/temp_path = text2path(temp_list_vars[V][W]["value"])
					if(!ispath(temp_path))
						parse_errors += "list_vars.[W].[W].type is \"path\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) does not correspond to an existing type"
						continue
				if(temp_list_vars[V][W]["type"] == "icon")
					if(!istext(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"icon\"; list_vars.[V].[W].value ([temp_list_vars[V][W]["value"]]) has been converted to text"
						temp_list_vars[V][W]["value"] = "[temp_list_vars[V][W]["value"]]"
					if(!fexists(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"icon\"; no such file \"[temp_list_vars[V][W]["value"]]\" exists on the server"
						continue
				if(temp_list_vars[V][W]["type"] == "ref")
					if(!isnull(temp_list_vars[V][W]["value"]))
						parse_errors += "list_vars.[V].[W].type is \"ref\"; list_vars.[V].[W].value has been nulled out"
						temp_list_vars[V][W]["value"] = null
					else
						alert = "Reference vars are not assigned on load from file. Be sure to set them correctly."
				result_list_vars[V][W] = temp_list_vars[V][W]
	qdel(D)

#undef LIST_VAR_FLAGS_TYPED
#undef LIST_VAR_FLAGS_NAMED

/datum/give_sdql_spell/proc/map_var_list(list/L, datum/D)
	var/list/ret = list()
	for(var/V in L)
		if(D.vars[V])
			ret["[V] = [string_rep(D.vars[V])]"] = V
	return ret

/datum/give_sdql_spell/proc/string_rep(V)
	if(istext(V) || isfile(V) || isicon(V))
		return "\"[V]\""
	else if(isnull(V))
		return "null"
	else
		return "[V]"

/datum/give_sdql_spell/proc/holder_var_validate(V, return_alert = FALSE)
	switch(V)
		if("bruteloss", "fireloss", "toxloss", "oxyloss", "stun", "knockdown", "paralyze", "unconscious")
			if(return_alert)
				return ""
			return TRUE
		else
			if(target_mob.vars[V])
				if(!isnum(target_mob.vars[V]))
					var/new_alert = "[target_mob.type]/[V] is not a number!"
					if(return_alert)
						return new_alert
					alert = new_alert
					return FALSE
				else
					return return_alert ? "" : TRUE
			else
				var/new_alert = "[target_mob.type] has no such variable [V]!"
				if(return_alert)
					return new_alert
				alert = new_alert
				return FALSE

/datum/give_sdql_spell/proc/icon_needs_updating(var_name)
	switch(var_name)
		if("action_icon", "action_icon_state", "action_background_icon_state")
			var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
			var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"])
			var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"])
			out_image.overlays += image(overlay_icon)
			out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE))
			action_icon_base64 = icon2base64(out_icon)

		if("projectile_var_overrides/icon", "projectile_var_overrides/icon_state")
			var/atom/A = /obj/projectile
			var/icon = initial(A.icon)
			var/icon_state = initial(A.icon_state)
			if(list_vars["projectile_var_overrides"]?["icon"])
				icon = list_vars["projectile_var_overrides"]["icon"]["value"]
			if(list_vars["projectile_var_overrides"]?["icon_state"])
				icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"]
			var/icon/out_icon = icon(icon, icon_state, frame = 1)
			projectile_icon_base64 = icon2base64(out_icon)

		if("hand_var_overrides/icon", "hand_var_overrides/icon_state")
			var/atom/A = /obj/item/melee/touch_attack
			var/icon = initial(A.icon)
			var/icon_state = initial(A.icon_state)
			if(list_vars["hand_var_overrides"]?["icon"])
				icon = list_vars["hand_var_overrides"]["icon"]["value"]
			if(list_vars["hand_var_overrides"]?["icon_state"])
				icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"]
			var/icon/out_icon = icon(icon, icon_state, frame = 1)
			hand_icon_base64 = icon2base64(out_icon)

		if("overlay", "overlay_icon", "overlay_icon_state")
			var/icon/out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1)
			overlay_icon_base64 = icon2base64(out_icon)

		if("ranged_mousepointer")
			var/icon/out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1)
			mouse_icon_base64 = icon2base64(out_icon)

		if("everything")
			var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
			var/image/out_image = image('icons/mob/actions/backgrounds.dmi', null, saved_vars["action_background_icon_state"])
			var/overlay_icon = icon(saved_vars["action_icon"], saved_vars["action_icon_state"])
			out_image.overlays += image(overlay_icon)
			out_icon.Insert(getFlatIcon(out_image, no_anim = TRUE))
			action_icon_base64 = icon2base64(out_icon)
			if(list_vars["projectile_var_overrides"])
				var/atom/A = saved_vars["projectile_type"]
				var/icon = initial(A.icon)
				var/icon_state = initial(A.icon_state)
				if(list_vars["projectile_var_overrides"]?["icon"])
					icon = list_vars["projectile_var_overrides"]["icon"]["value"]
				if(list_vars["projectile_var_overrides"]?["icon_state"])
					icon_state = list_vars["projectile_var_overrides"]["icon_state"]["value"]
				out_icon = icon(icon, icon_state, frame = 1)
				projectile_icon_base64 = icon2base64(out_icon)
			if(list_vars["hand_var_overrides"])
				var/atom/A = saved_vars["hand_path"]
				var/icon = initial(A.icon)
				var/icon_state = initial(A.icon_state)
				if(list_vars["hand_var_overrides"]?["icon"])
					icon = list_vars["hand_var_overrides"]["icon"]["value"]
				if(list_vars["hand_var_overrides"]?["icon_state"])
					icon_state = list_vars["hand_var_overrides"]["icon_state"]["value"]
				out_icon = icon(icon, icon_state, frame = 1)
				hand_icon_base64 = icon2base64(out_icon)
			out_icon = icon(saved_vars["overlay_icon"], saved_vars["overlay_icon_state"], frame = 1)
			overlay_icon_base64 = icon2base64(out_icon)
			out_icon = icon(saved_vars["ranged_mousepointer"], frame = 1)
			mouse_icon_base64 = icon2base64(out_icon)

/datum/give_sdql_spell/proc/toggle_list_var(list_name, list_var)
	if(list_vars[list_name]?[list_var])
		list_vars[list_name][list_var]["value"] = !list_vars[list_name][list_var]["value"]

/datum/give_sdql_spell/proc/set_list_var(list_name, list_var, value)
	if(list_vars[list_name]?[list_var])
		list_vars[list_name][list_var]["value"] = value

/datum/give_sdql_spell/proc/set_list_ref_var(list_name, list_var)
	if(list_vars[list_name]?[list_var])
		list_vars[list_name][list_var]["value"] = user.holder?.marked_datum

/datum/give_sdql_spell/proc/rename_list_var(list_name, list_var, new_name)
	if(!new_name)
		alert = "You can't give a list variable an empty string for a name!"
		return
	if(list_var == new_name)
		return
	if(list_vars[list_name])
		var/list/L = list_vars[list_name]
		var/ind = L.Find(list_var)
		if(ind)
			if(list_vars[list_name][new_name])
				alert = "There is already a variable named [new_name] in [list_name]!"
			else
				var/old_val = list_vars[list_name][list_var]
				list_vars[list_name][ind] = new_name
				list_vars[list_name][new_name] = old_val

/datum/give_sdql_spell/proc/change_list_var_type(list_name, list_var, var_type)
	if(list_vars[list_name]?[list_var])
		if(list_vars[list_name][list_var]["type"] == "list" && var_type != "list")
			purge_list_var("[list_name]/[list_var]")
		list_vars[list_name][list_var]["type"] = var_type
		switch(var_type)
			if("string", "path")
				list_vars[list_name][list_var]["value"] = ""
			if("bool", "num")
				list_vars[list_name][list_var]["value"] = 0
			if("list")
				list_vars[list_name][list_var]["value"] = null
				list_vars |= list("[list_name]/[list_var]" = list())

/datum/give_sdql_spell/proc/remove_list_var(list_name, list_var)
	if(list_vars[list_name])
		var/list/L = list_vars[list_name]
		var/ind = L.Find(list_var)
		if(ind)
			if(list_vars[list_name][list_var]["type"] == "list")
				purge_list_var("[list_name]/[list_var]")
			L.Cut(ind, ind+1)
			list_vars[list_name] = L

/datum/give_sdql_spell/proc/purge_list_var(list_name)
	var/ind = list_vars.Find(list_name)
	if(ind)
		for(var/V in list_vars[list_name])
			if(list_vars[list_name][V]["type"] == "list")
				purge_list_var("[list_name]/[V]")
		list_vars.Cut(ind, ind+1)

/datum/give_sdql_spell/proc/generate_list_var(list_name)
	if(!list_vars[list_name])
		return null
	var/list/ret = list()
	for(var/V in list_vars[list_name])
		if(list_vars[list_name][V]["type"] == "list")
			ret[V] = generate_list_var("[list_name]/[V]")
		else if(list_vars[list_name][V]["type"] == "path")
			ret[V] = text2path(list_vars[list_name][V]["value"])
		else if(list_vars[list_name][V]["type"] == "icon")
			ret[V] = icon(list_vars[list_name][V]["value"])
		else
			ret[V] = list_vars[list_name][V]["value"]
	return ret

/datum/give_sdql_spell/proc/give_spell()
	var/path = text2path("/obj/effect/proc_holder/spell/[spell_type]/sdql")
	var/obj/effect/proc_holder/spell/new_spell = new path(null, target_mob, user.ckey)
	GLOB.sdql_spells += new_spell
	reassign_vars(new_spell)
	new_spell.action.UpdateButtonIcon()
	if(target_mob.mind)
		target_mob.mind.AddSpell(new_spell)
	else
		target_mob.AddSpell(new_spell)
		to_chat(user, span_danger("Spells given to mindless mobs will not be transferred in mindswap or cloning!"))
	return new_spell

/datum/give_sdql_spell/proc/reassign_vars(obj/effect/proc_holder/spell/target)
	if(!target)
		CRASH("edit_spell must be called with a non_null target")
	var/datum/component/sdql_executor/executor = target.GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	for(var/V in saved_vars+list_vars)
		if(V == "query")
			executor.vv_edit_var("query", saved_vars["query"])
		else if(V == "suppress_message_admins")
			executor.vv_edit_var("suppress_message_admins", saved_vars["suppress_message_admins"])
		else if(V == "scratchpad")
			var/list/new_scratchpad = generate_list_var("scratchpad")
			if(new_scratchpad)
				executor.vv_edit_var("scratchpad", new_scratchpad)
		else if(target.vars[V])
			if(islist(target.vars[V]))
				if(special_list_vars[V])
					var/list/overrides_to_save = list_vars[V]
					executor.saved_overrides[V] = overrides_to_save.Copy()
				var/list/list_var = generate_list_var(V)
				if(list_var)
					target.vv_edit_var(V, list_var)
			else if(isicon(target.vars[V]))
				target.vv_edit_var(V, icon(saved_vars[V]))
			else
				target.vv_edit_var(V, saved_vars[V])


code:
/obj/effect/proc_holder/spell/aimed/sdql
	name = "Aimed SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
	projectile_type = /obj/projectile

/obj/effect/proc_holder/spell/aimed/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)
	RegisterSignal(src, COMSIG_PROJECTILE_ON_HIT, .proc/on_projectile_hit)

/obj/effect/proc_holder/spell/aimed/sdql/proc/on_projectile_hit(source, firer, target)
	SIGNAL_HANDLER
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	INVOKE_ASYNC(executor, /datum/component/sdql_executor/proc/execute, list(target), owner.resolve())

/obj/effect/proc_holder/spell/aoe_turf/sdql
	name = "AoE SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."

/obj/effect/proc_holder/spell/aoe_turf/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/aoe_turf/sdql/cast(list/targets, mob/user)
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(targets, user)

/obj/effect/proc_holder/spell/cone/sdql
	name = "Cone SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
	var/list/targets = list()

/obj/effect/proc_holder/spell/cone/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/cone/sdql/do_mob_cone_effect(mob/living/target_mob, level)
	targets |= target_mob

/obj/effect/proc_holder/spell/cone/sdql/do_obj_cone_effect(obj/target_obj, level)
	targets |= target_obj

/obj/effect/proc_holder/spell/cone/sdql/do_turf_cone_effect(turf/target_turf, level)
	targets |= target_turf

/obj/effect/proc_holder/spell/cone/sdql/cast(list/targets, mob/user)
	. = ..()
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(targets, user)
	targets = list()

/obj/effect/proc_holder/spell/cone/staggered/sdql
	name = "Staggered Cone SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
	var/list/targets = list()

/obj/effect/proc_holder/spell/cone/staggered/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/cone/staggered/sdql/do_mob_cone_effect(mob/living/target_mob, level)
	targets |= target_mob

/obj/effect/proc_holder/spell/cone/staggered/sdql/do_obj_cone_effect(obj/target_obj, level)
	targets |= target_obj

/obj/effect/proc_holder/spell/cone/staggered/sdql/do_turf_cone_effect(turf/target_turf, level)
	targets |= target_turf

/obj/effect/proc_holder/spell/cone/staggered/sdql/do_cone_effects(list/target_turf_list, level)
	. = ..()
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(target_turf_list, owner.resolve())
	targets = list()

/obj/effect/proc_holder/spell/pointed/sdql
	name = "Pointed SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."

/obj/effect/proc_holder/spell/pointed/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/pointed/sdql/cast(list/targets, mob/user)
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(targets, user)

/obj/effect/proc_holder/spell/self/sdql
	name = "Self SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."

/obj/effect/proc_holder/spell/self/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/self/sdql/cast(list/targets, mob/user)
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(targets, user)

/obj/effect/proc_holder/spell/targeted/sdql
	name = "Targeted SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."

/obj/effect/proc_holder/spell/targeted/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/targeted/sdql/cast(list/targets, mob/user)
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	executor.execute(targets, user)

/obj/effect/proc_holder/spell/targeted/touch/sdql
	name = "Touch SDQL Spell"
	desc = "If you are reading this outside of the \"Give SDQL Spell\" menu, tell the admin that gave this spell to you to use said menu."
	var/list/hand_var_overrides = list() //The touch attack has its vars changed to the ones put in this list.

/obj/effect/proc_holder/spell/targeted/touch/sdql/Initialize(mapload, new_owner, giver)
	. = ..()
	AddComponent(/datum/component/sdql_executor, giver)

/obj/effect/proc_holder/spell/targeted/touch/sdql/ChargeHand(mob/living/carbon/user)
	if(..())
		for(var/V in hand_var_overrides)
			if(attached_hand.vars[V])
				attached_hand.vv_edit_var(V, hand_var_overrides[V])
		RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, .proc/on_touch_attack)
		user.update_inv_hands()

/obj/effect/proc_holder/spell/targeted/touch/sdql/proc/on_touch_attack(source, target, user)
	SIGNAL_HANDLER
	var/datum/component/sdql_executor/executor = GetComponent(/datum/component/sdql_executor)
	if(!executor)
		CRASH("[src]'s SDQL executor component went missing!")
	INVOKE_ASYNC(executor, /datum/component/sdql_executor/proc/execute, list(target), user)

repo_name: AnalWerewolf/sundown

license: mit


code:
// Admin Tab - Admin Verbs

/client/proc/show_tip()
	set category = "Admin"
	set name = "Show Tip"
	set desc = "Sends a tip (that you specify) to all players. After all \
		you're the experienced player here."

	if(!check_rights(R_ADMIN))
		return

	var/input = input(usr, "Please specify your tip that you want to send to the players.", "Tip", "") as message|null
	if(!input)
		return

	if(!SSticker)
		return

	// If we've already tipped, then send it straight away.
	if(SSticker.tipped)
		send_tip_of_the_round(world, input)
	else
		SSticker.selected_tip = input

	message_admins("[key_name_admin(usr)] sent a tip of the round.")
	log_admin("[key_name(usr)] sent \"[input]\" as the Tip of the Round.")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Show Tip")

/datum/admins/proc/announce()
	set category = "Admin"
	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|null
	if(message)
		if(!check_rights(R_SERVER,0))
			message = adminscrub(message,500)
		to_chat(world, "[span_adminnotice("[usr.client.holder.fakekey ? "Administrator" : usr.key] Announces:")]\n \t [message]", confidential = TRUE)
		log_admin("Announce: [key_name(usr)] : [message]")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Announce") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/datum/admins/proc/unprison(mob/M in GLOB.mob_list)
	set category = "Admin"
	set name = "Unprison"
	if (is_centcom_level(M.z))
		SSjob.SendToLateJoin(M)
		message_admins("[key_name_admin(usr)] has unprisoned [key_name_admin(M)]")
		log_admin("[key_name(usr)] has unprisoned [key_name(M)]")
	else
		tgui_alert(usr,"[M.name] is not prisoned.")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Unprison") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_check_player_exp() //Allows admins to determine who the newer players are.
	set category = "Admin"
	set name = "Player Playtime"
	if(!check_rights(R_ADMIN))
		return

	if(!CONFIG_GET(flag/use_exp_tracking))
		to_chat(usr, span_warning("Tracking is disabled in the server configuration file."), confidential = TRUE)
		return

	var/list/msg = list()
	msg += "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'><title>Playtime Report</title>Playtime:

"
	var/list/clients_list_copy = GLOB.clients.Copy()
	sort_list(clients_list_copy)
	for(var/client/C in clients_list_copy)
		msg += "
 - [key_name_admin(C)]: " + C.get_exp_living() + "
"
	msg += "
"
	src << browse(msg.Join(), "window=Player_playtime_check")

/client/proc/trigger_centcom_recall()
	if(!check_rights(R_ADMIN))
		return
	var/message = pick(GLOB.admiral_messages)
	message = input("Enter message from the on-call admiral to be put in the recall report.", "Admiral Message", message) as text|null

	if(!message)
		return

	message_admins("[key_name_admin(usr)] triggered a CentCom recall, with the admiral message of: [message]")
	log_game("[key_name(usr)] triggered a CentCom recall, with the message of: [message]")
	SSshuttle.centcom_recall(SSshuttle.emergency.timer, message)

/datum/admins/proc/cmd_show_exp_panel(client/client_to_check)
	if(!check_rights(R_ADMIN))
		return
	if(!client_to_check)
		to_chat(usr, span_danger("ERROR: Client not found."), confidential = TRUE)
		return
	if(!CONFIG_GET(flag/use_exp_tracking))
		to_chat(usr, span_warning("Tracking is disabled in the server configuration file."), confidential = TRUE)
		return

	new /datum/job_report_menu(client_to_check, usr)

/datum/admins/proc/toggle_exempt_status(client/C)
	if(!check_rights(R_ADMIN))
		return
	if(!C)
		to_chat(usr, span_danger("ERROR: Client not found."), confidential = TRUE)
		return

	if(!C.set_db_player_flags())
		to_chat(usr, span_danger("ERROR: Unable read player flags from database. Please check logs."), confidential = TRUE)
	var/dbflags = C.prefs.db_flags
	var/newstate = FALSE
	if(dbflags & DB_FLAG_EXEMPT)
		newstate = FALSE
	else
		newstate = TRUE

	if(C.update_flag_db(DB_FLAG_EXEMPT, newstate))
		to_chat(usr, span_danger("ERROR: Unable to update player flags. Please check logs."), confidential = TRUE)
	else
		message_admins("[key_name_admin(usr)] has [newstate ? "activated" : "deactivated"] job exp exempt status on [key_name_admin(C)]")
		log_admin("[key_name(usr)] has [newstate ? "activated" : "deactivated"] job exp exempt status on [key_name(C)]")

/// Allow admin to add or remove traits of datum
/datum/admins/proc/modify_traits(datum/D)
	if(!D)
		return

	var/add_or_remove = input("Remove/Add?", "Trait Remove/Add") as null|anything in list("Add","Remove")
	if(!add_or_remove)
		return
	var/list/available_traits = list()

	switch(add_or_remove)
		if("Add")
			for(var/key in GLOB.traits_by_type)
				if(istype(D,key))
					available_traits += GLOB.traits_by_type[key]
		if("Remove")
			if(!GLOB.trait_name_map)
				GLOB.trait_name_map = generate_trait_name_map()
			for(var/trait in D.status_traits)
				var/name = GLOB.trait_name_map[trait] || trait
				available_traits[name] = trait

	var/chosen_trait = input("Select trait to modify", "Trait") as null|anything in sort_list(available_traits)
	if(!chosen_trait)
		return
	chosen_trait = available_traits[chosen_trait]

	var/source = "adminabuse"
	switch(add_or_remove)
		if("Add") //Not doing source choosing here intentionally to make this bit faster to use, you can always vv it.
			if(GLOB.movement_type_trait_to_flag[chosen_trait]) //include the required element.
				D.AddElement(/datum/element/movetype_handler)
			ADD_TRAIT(D,chosen_trait,source)
		if("Remove")
			var/specific = input("All or specific source ?", "Trait Remove/Add") as null|anything in list("All","Specific")
			if(!specific)
				return
			switch(specific)
				if("All")
					source = null
				if("Specific")
					source = input("Source to be removed","Trait Remove/Add") as null|anything in sort_list(D.status_traits[chosen_trait])
					if(!source)
						return
			REMOVE_TRAIT(D,chosen_trait,source)

///////////////////////////////////////////////////////////////////////////////////////////////

/client/proc/cmd_admin_drop_everything(mob/M in GLOB.mob_list)
	set category = null
	set name = "Drop Everything"
	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "Make [M] drop everything?", "Message", list("Yes", "No"))
	if(confirm != "Yes")
		return

	for(var/obj/item/W in M)
		if(!M.dropItemToGround(W))
			qdel(W)
			M.regenerate_icons()

	log_admin("[key_name(usr)] made [key_name(M)] drop everything!")
	var/msg = "[key_name_admin(usr)] made [ADMIN_LOOKUPFLW(M)] drop everything!"
	message_admins(msg)
	admin_ticket_log(M, msg)
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Drop Everything") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/proc/cmd_admin_mute(whom, mute_type, automute = 0)
	if(!whom)
		return

	var/muteunmute
	var/mute_string
	var/feedback_string
	switch(mute_type)
		if(MUTE_IC)
			mute_string = "IC (say and emote)"
			feedback_string = "IC"
		if(MUTE_OOC)
			mute_string = "OOC"
			feedback_string = "OOC"
		if(MUTE_PRAY)
			mute_string = "pray"
			feedback_string = "Pray"
		if(MUTE_ADMINHELP)
			mute_string = "adminhelp, admin PM and ASAY"
			feedback_string = "Adminhelp"
		if(MUTE_DEADCHAT)
			mute_string = "deadchat and DSAY"
			feedback_string = "Deadchat"
		if(MUTE_ALL)
			mute_string = "everything"
			feedback_string = "Everything"
		else
			return

	var/client/C
	if(istype(whom, /client))
		C = whom
	else if(istext(whom))
		C = GLOB.directory[whom]
	else
		return

	var/datum/preferences/P
	if(C)
		P = C.prefs
	else
		P = GLOB.preferences_datums[whom]
	if(!P)
		return

	if(automute)
		if(!CONFIG_GET(flag/automute_on))
			return
	else
		if(!check_rights())
			return

	if(automute)
		muteunmute = "auto-muted"
		P.muted |= mute_type
		log_admin("SPAM AUTOMUTE: [muteunmute] [key_name(whom)] from [mute_string]")
		message_admins("SPAM AUTOMUTE: [muteunmute] [key_name_admin(whom)] from [mute_string].")
		if(C)
			to_chat(C, "You have been [muteunmute] from [mute_string] by the SPAM AUTOMUTE system. Contact an admin.", confidential = TRUE)
		SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Auto Mute [feedback_string]", "1")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
		return

	if(P.muted & mute_type)
		muteunmute = "unmuted"
		P.muted &= ~mute_type
	else
		muteunmute = "muted"
		P.muted |= mute_type

	log_admin("[key_name(usr)] has [muteunmute] [key_name(whom)] from [mute_string]")
	message_admins("[key_name_admin(usr)] has [muteunmute] [key_name_admin(whom)] from [mute_string].")
	if(C)
		to_chat(C, "You have been [muteunmute] from [mute_string] by [key_name(usr, include_name = FALSE)].", confidential = TRUE)
	SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Mute [feedback_string]", "[P.muted & mute_type]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/proc/immerse_player(mob/living/carbon/target, toggle=TRUE, remove=FALSE)
	var/list/immersion_components = list(/datum/component/manual_breathing, /datum/component/manual_blinking)

	for(var/immersies in immersion_components)
		var/has_component = target.GetComponent(immersies)

		if(has_component && (toggle || remove))
			qdel(has_component)
		else if(toggle || !remove)
			target.AddComponent(immersies)

/proc/mass_immerse(remove=FALSE)
	for(var/mob/living/carbon/M in GLOB.mob_list)
		immerse_player(M, toggle=FALSE, remove=remove)


code:
// Admin Tab - Event Verbs

/client/proc/cmd_admin_subtle_message(mob/M in GLOB.mob_list)
	set category = "Admin.Events"
	set name = "Subtle Message"

	if(!ismob(M))
		return
	if(!check_rights(R_ADMIN))
		return

	message_admins("[key_name_admin(src)] has started answering [ADMIN_LOOKUPFLW(M)]'s prayer.")
	var/msg = input("Message:", text("Subtle PM to [M.key]")) as text|null

	if(!msg)
		message_admins("[key_name_admin(src)] decided not to answer [ADMIN_LOOKUPFLW(M)]'s prayer")
		return
	if(usr)
		if (usr.client)
			if(usr.client.holder)
				to_chat(M, "You hear a voice in your head... [msg]", confidential = TRUE)

	log_admin("SubtlePM: [key_name(usr)] -> [key_name(M)] : [msg]")
	msg = span_adminnotice(" SubtleMessage: [key_name_admin(usr)] -> [key_name_admin(M)] : [msg]")
	message_admins(msg)
	admin_ticket_log(M, msg)
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Subtle Message") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_headset_message(mob/M in GLOB.mob_list)
	set category = "Admin.Events"
	set name = "Headset Message"

	admin_headset_message(M)

/client/proc/admin_headset_message(mob/M in GLOB.mob_list, sender = null)
	var/mob/living/carbon/human/H = M

	if(!check_rights(R_ADMIN))
		return

	if(!istype(H))
		to_chat(usr, "This can only be used on instances of type /mob/living/carbon/human", confidential = TRUE)
		return
	if(!istype(H.ears, /obj/item/radio/headset))
		to_chat(usr, "The person you are trying to contact is not wearing a headset.", confidential = TRUE)
		return

	if (!sender)
		sender = input("Who is the message from?", "Sender") as null|anything in list(RADIO_CHANNEL_CENTCOM,RADIO_CHANNEL_SYNDICATE)
		if(!sender)
			return

	message_admins("[key_name_admin(src)] has started answering [key_name_admin(H)]'s [sender] request.")
	var/input = input("Please enter a message to reply to [key_name(H)] via their headset.","Outgoing message from [sender]", "") as text|null
	if(!input)
		message_admins("[key_name_admin(src)] decided not to answer [key_name_admin(H)]'s [sender] request.")
		return

	log_directed_talk(mob, H, input, LOG_ADMIN, "reply")
	message_admins("[key_name_admin(src)] replied to [key_name_admin(H)]'s [sender] message with: \"[input]\"")
	to_chat(H, span_hear("You hear something crackle in your ears for a moment before a voice speaks. \"Please stand by for a message from [sender == "Syndicate" ? "your benefactor" : "Central Command"]. Message as follows[sender == "Syndicate" ? ", agent." : ":"] [input]. Message ends.\""), confidential = TRUE)

	SSblackbox.record_feedback("tally", "admin_verb", 1, "Headset Message") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_world_narrate()
	set category = "Admin.Events"
	set name = "Global Narrate"

	if(!check_rights(R_ADMIN))
		return

	var/msg = input("Message:", text("Enter the text you wish to appear to everyone:")) as text|null

	if (!msg)
		return
	to_chat(world, "[msg]", confidential = TRUE)
	log_admin("GlobalNarrate: [key_name(usr)] : [msg]")
	message_admins(span_adminnotice("[key_name_admin(usr)] Sent a global narrate"))
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Global Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_local_narrate(atom/A)
	set category = "Admin.Events"
	set name = "Local Narrate"

	if(!check_rights(R_ADMIN))
		return
	if(!A)
		return
	var/range = input("Range:", "Narrate to mobs within how many tiles:", 7) as num|null
	if(!range)
		return
	var/msg = input("Message:", text("Enter the text you wish to appear to everyone within view:")) as text|null
	if (!msg)
		return
	for(var/mob/M in view(range,A))
		to_chat(M, msg, confidential = TRUE)

	log_admin("LocalNarrate: [key_name(usr)] at [AREACOORD(A)]: [msg]")
	message_admins(span_adminnotice(" LocalNarrate: [key_name_admin(usr)] at [ADMIN_VERBOSEJMP(A)]: [msg]
"))
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Local Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_direct_narrate(mob/M)
	set category = "Admin.Events"
	set name = "Direct Narrate"

	if(!check_rights(R_ADMIN))
		return

	if(!M)
		M = input("Direct narrate to whom?", "Active Players") as null|anything in GLOB.player_list

	if(!M)
		return

	var/msg = input("Message:", text("Enter the text you wish to appear to your target:")) as text|null

	if( !msg )
		return

	to_chat(M, msg, confidential = TRUE)
	log_admin("DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]")
	msg = span_adminnotice(" DirectNarrate: [key_name(usr)] to ([M.name]/[M.key]): [msg]
")
	message_admins(msg)
	admin_ticket_log(M, msg)
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Direct Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_add_freeform_ai_law()
	set category = "Admin.Events"
	set name = "Add Custom AI law"

	if(!check_rights(R_ADMIN))
		return

	var/input = input(usr, "Please enter anything you want the AI to do. Anything. Serious.", "What?", "") as text|null
	if(!input)
		return

	log_admin("Admin [key_name(usr)] has added a new AI law - [input]")
	message_admins("Admin [key_name_admin(usr)] has added a new AI law - [input]")

	var/show_log = tgui_alert(usr, "Show ion message?", "Message", list("Yes", "No"))
	var/announce_ion_laws = (show_log == "Yes" ? 100 : 0)

	var/datum/round_event/ion_storm/add_law_only/ion = new()
	ion.announceChance = announce_ion_laws
	ion.ionMessage = input

	SSblackbox.record_feedback("tally", "admin_verb", 1, "Add Custom AI Law") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/admin_call_shuttle()
	set category = "Admin.Events"
	set name = "Call Shuttle"

	if(EMERGENCY_AT_LEAST_DOCKED)
		return

	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "You sure?", "Confirm", list("Yes", "Yes (No Recall)", "No"))
	switch(confirm)
		if(null, "No")
			return
		if("Yes (No Recall)")
			SSshuttle.adminEmergencyNoRecall = TRUE
			SSshuttle.emergency.mode = SHUTTLE_IDLE

	SSshuttle.emergency.request()
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Call Shuttle") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	log_admin("[key_name(usr)] admin-called the emergency shuttle.")
	message_admins(span_adminnotice("[key_name_admin(usr)] admin-called the emergency shuttle[confirm == "Yes (No Recall)" ? " (non-recallable)" : ""]."))
	return

/client/proc/admin_cancel_shuttle()
	set category = "Admin.Events"
	set name = "Cancel Shuttle"
	if(!check_rights(0))
		return
	if(tgui_alert(usr, "You sure?", "Confirm", list("Yes", "No")) != "Yes")
		return

	if(SSshuttle.adminEmergencyNoRecall)
		SSshuttle.adminEmergencyNoRecall = FALSE

	if(EMERGENCY_AT_LEAST_DOCKED)
		return

	SSshuttle.emergency.cancel()
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Cancel Shuttle") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
	log_admin("[key_name(usr)] admin-recalled the emergency shuttle.")
	message_admins(span_adminnotice("[key_name_admin(usr)] admin-recalled the emergency shuttle."))

	return

/client/proc/admin_disable_shuttle()
	set category = "Admin.Events"
	set name = "Disable Shuttle"

	if(!check_rights(R_ADMIN))
		return

	if(SSshuttle.emergency.mode == SHUTTLE_DISABLED)
		to_chat(usr, span_warning("Error, shuttle is already disabled."))
		return

	if(tgui_alert(usr, "You sure?", "Confirm", list("Yes", "No")) != "Yes")
		return

	message_admins(span_adminnotice("[key_name_admin(usr)] disabled the shuttle."))

	SSshuttle.lastMode = SSshuttle.emergency.mode
	SSshuttle.lastCallTime = SSshuttle.emergency.timeLeft(1)
	SSshuttle.adminEmergencyNoRecall = TRUE
	SSshuttle.emergency.setTimer(0)
	SSshuttle.emergency.mode = SHUTTLE_DISABLED
	priority_announce("Warning: Emergency Shuttle uplink failure, shuttle disabled until further notice.", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')

/client/proc/admin_enable_shuttle()
	set category = "Admin.Events"
	set name = "Enable Shuttle"

	if(!check_rights(R_ADMIN))
		return

	if(SSshuttle.emergency.mode != SHUTTLE_DISABLED)
		to_chat(usr, span_warning("Error, shuttle not disabled."))
		return

	if(tgui_alert(usr, "You sure?", "Confirm", list("Yes", "No")) != "Yes")
		return

	message_admins(span_adminnotice("[key_name_admin(usr)] enabled the emergency shuttle."))
	SSshuttle.adminEmergencyNoRecall = FALSE
	SSshuttle.emergencyNoRecall = FALSE
	if(SSshuttle.lastMode == SHUTTLE_DISABLED) //If everything goes to shit, fix it.
		SSshuttle.lastMode = SHUTTLE_IDLE

	SSshuttle.emergency.mode = SSshuttle.lastMode
	if(SSshuttle.lastCallTime < 10 SECONDS && SSshuttle.lastMode != SHUTTLE_IDLE)
		SSshuttle.lastCallTime = 10 SECONDS //Make sure no insta departures.
	SSshuttle.emergency.setTimer(SSshuttle.lastCallTime)
	priority_announce("Warning: Emergency Shuttle uplink reestablished, shuttle enabled.", "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')

/client/proc/toggle_nuke(obj/machinery/nuclearbomb/N in GLOB.nuke_list)
	set category = "Admin.Events"
	set name = "Toggle Nuke"
	set popup_menu = FALSE
	if(!check_rights(R_DEBUG))
		return

	if(!N.timing)
		var/newtime = input(usr, "Set activation timer.", "Activate Nuke", "[N.timer_set]") as num|null
		if(!newtime)
			return
		N.timer_set = newtime
	N.set_safety()
	N.set_active()

	log_admin("[key_name(usr)] [N.timing ? "activated" : "deactivated"] a nuke at [AREACOORD(N)].")
	message_admins("[ADMIN_LOOKUPFLW(usr)] [N.timing ? "activated" : "deactivated"] a nuke at [ADMIN_VERBOSEJMP(N)].")
	SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Nuke", "[N.timing]")) //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 = "Admin.Events"
	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))
		to_chat(usr, "Error: you are not an admin!", confidential = TRUE)
		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(GLOB.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(GLOB.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( !length(GLOB.news_network.network_channels) )
				dat+="No active channels found..."
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.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 GLOB.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( !length(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(!length(GLOB.news_network.network_channels))
				dat+="No feed channels found active...
"
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.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(!length(GLOB.news_network.network_channels))
				dat+="No feed channels found active...
"
			else
				for(var/datum/newscaster/feed_channel/CHANNEL in GLOB.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( !length(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( !length(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(GLOB.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:[GLOB.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: [GLOB.news_network.wanted_issue.scannedUser]\]
"
			dat+="Criminal: [GLOB.news_network.wanted_issue.criminal]
"
			dat+="Description: [GLOB.news_network.wanted_issue.body]
"
			dat+="Photo:: "
			if(GLOB.news_network.wanted_issue.img)
				usr << browse_rsc(GLOB.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"

	usr << browse(dat, "window=admincaster_main;size=400x600")
	onclose(usr, "admincaster_main")

/client/proc/admin_change_sec_level()
	set category = "Admin.Events"
	set name = "Set Security Level"
	set desc = "Changes the security level. Announcement only, i.e. setting to Delta won't activate nuke"

	if(!check_rights(R_ADMIN))
		return

	var/level = input("Select security level to change to","Set Security Level") as null|anything in list("green","blue","red","delta")
	if(level)
		set_security_level(level)

		log_admin("[key_name(usr)] changed the security level to [level]")
		message_admins("[key_name_admin(usr)] changed the security level to [level]")
		SSblackbox.record_feedback("tally", "admin_verb", 1, "Set Security Level [capitalize(level)]") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/run_weather()
	set category = "Admin.Events"
	set name = "Run Weather"
	set desc = "Triggers a weather on the z-level you choose."

	if(!holder)
		return

	var/weather_type = input("Choose a weather", "Weather")  as null|anything in sort_list(subtypesof(/datum/weather), /proc/cmp_typepaths_asc)
	if(!weather_type)
		return

	var/turf/T = get_turf(mob)
	var/z_level = input("Z-Level to target?", "Z-Level", T?.z) as num|null
	if(!isnum(z_level))
		return

	SSweather.run_weather(weather_type, z_level)

	message_admins("[key_name_admin(usr)] started weather of type [weather_type] on the z-level [z_level].")
	log_admin("[key_name(usr)] started weather of type [weather_type] on the z-level [z_level].")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Run Weather")


code:
// Admin Tab - Fun Verbs

/client/proc/cmd_admin_explosion(atom/O as obj|mob|turf in world)
	set category = "Admin.Fun"
	set name = "Explosion"

	if(!check_rights(R_ADMIN))
		return

	var/devastation = input("Range of total devastation. -1 to none", text("Input"))  as num|null
	if(devastation == null)
		return
	var/heavy = input("Range of heavy impact. -1 to none", text("Input"))  as num|null
	if(heavy == null)
		return
	var/light = input("Range of light impact. -1 to none", text("Input"))  as num|null
	if(light == null)
		return
	var/flash = input("Range of flash. -1 to none", text("Input"))  as num|null
	if(flash == null)
		return
	var/flames = input("Range of flames. -1 to none", text("Input"))  as num|null
	if(flames == null)
		return

	if ((devastation != -1) || (heavy != -1) || (light != -1) || (flash != -1) || (flames != -1))
		if ((devastation > 20) || (heavy > 20) || (light > 20) || (flames > 20))
			if (tgui_alert(usr, "Are you sure you want to do this? It will laaag.", "Confirmation", list("Yes", "No")) == "No")
				return

		explosion(O, devastation, heavy, light, flames, flash, explosion_cause = mob)
		log_admin("[key_name(usr)] created an explosion ([devastation],[heavy],[light],[flames]) at [AREACOORD(O)]")
		message_admins("[key_name_admin(usr)] created an explosion ([devastation],[heavy],[light],[flames]) at [AREACOORD(O)]")
		SSblackbox.record_feedback("tally", "admin_verb", 1, "Explosion") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_emp(atom/O as obj|mob|turf in world)
	set category = "Admin.Fun"
	set name = "EM Pulse"

	if(!check_rights(R_ADMIN))
		return

	var/heavy = input("Range of heavy pulse.", text("Input"))  as num|null
	if(heavy == null)
		return
	var/light = input("Range of light pulse.", text("Input"))  as num|null
	if(light == null)
		return

	if (heavy || light)
		empulse(O, heavy, light)
		log_admin("[key_name(usr)] created an EM Pulse ([heavy],[light]) at [AREACOORD(O)]")
		message_admins("[key_name_admin(usr)] created an EM Pulse ([heavy],[light]) at [AREACOORD(O)]")
		SSblackbox.record_feedback("tally", "admin_verb", 1, "EM Pulse") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_gib(mob/victim in GLOB.mob_list)
	set category = "Admin.Fun"
	set name = "Gib"

	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "Drop a brain?", "Confirm", list("Yes", "No","Cancel"))
	if(confirm == "Cancel")
		return
	//Due to the delay here its easy for something to have happened to the mob
	if(!victim)
		return

	log_admin("[key_name(usr)] has gibbed [key_name(victim)]")
	message_admins("[key_name_admin(usr)] has gibbed [key_name_admin(victim)]")

	if(isobserver(victim))
		new /obj/effect/gibspawner/generic(get_turf(victim))
		return

	var/mob/living/living_victim = victim
	if (istype(living_victim))
		if(confirm == "Yes")
			living_victim.gib()
		else
			living_victim.gib(TRUE)

	SSblackbox.record_feedback("tally", "admin_verb", 1, "Gib") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/cmd_admin_gib_self()
	set name = "Gibself"
	set category = "Admin.Fun"

	var/confirm = tgui_alert(usr, "You sure?", "Confirm", list("Yes", "No"))
	if(confirm == "Yes")
		log_admin("[key_name(usr)] used gibself.")
		message_admins(span_adminnotice("[key_name_admin(usr)] used gibself."))
		SSblackbox.record_feedback("tally", "admin_verb", 1, "Gib Self") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

		var/mob/living/ourself = mob
		if (istype(ourself))
			ourself.gib(TRUE, TRUE, TRUE)

/client/proc/everyone_random()
	set category = "Admin.Fun"
	set name = "Make Everyone Random"
	set desc = "Make everyone have a random appearance. You can only use this before rounds!"

	if(SSticker.HasRoundStarted())
		to_chat(usr, "Nope you can't do this, the game's already started. This only works before rounds!", confidential = TRUE)
		return

	var/frn = CONFIG_GET(flag/force_random_names)
	if(frn)
		CONFIG_SET(flag/force_random_names, FALSE)
		message_admins("Admin [key_name_admin(usr)] has disabled \"Everyone is Special\" mode.")
		to_chat(usr, "Disabled.", confidential = TRUE)
		return

	var/notifyplayers = tgui_alert(usr, "Do you want to notify the players?", "Options", list("Yes", "No", "Cancel"))
	if(notifyplayers == "Cancel")
		return

	log_admin("Admin [key_name(src)] has forced the players to have random appearances.")
	message_admins("Admin [key_name_admin(usr)] has forced the players to have random appearances.")

	if(notifyplayers == "Yes")
		to_chat(world, span_adminnotice("Admin [usr.key] has forced the players to have completely random identities!"), confidential = TRUE)

	to_chat(usr, "<i>Remember: you can always disable the randomness by using the verb again, assuming the round hasn't started yet</i>.", confidential = TRUE)

	CONFIG_SET(flag/force_random_names, TRUE)
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Make Everyone Random") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!

/client/proc/mass_zombie_infection()
	set category = "Admin.Fun"
	set name = "Mass Zombie Infection"
	set desc = "Infects all humans with a latent organ that will zombify \
		them on death."

	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "Please confirm you want to add latent zombie organs in all humans?", "Confirm Zombies", list("Yes", "No"))
	if(confirm != "Yes")
		return

	for(var/i in GLOB.human_list)
		var/mob/living/carbon/human/H = i
		new /obj/item/organ/zombie_infection/nodamage(H)

	message_admins("[key_name_admin(usr)] added a latent zombie infection to all humans.")
	log_admin("[key_name(usr)] added a latent zombie infection to all humans.")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Mass Zombie Infection")

/client/proc/mass_zombie_cure()
	set category = "Admin.Fun"
	set name = "Mass Zombie Cure"
	set desc = "Removes the zombie infection from all humans, returning them to normal."
	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "Please confirm you want to cure all zombies?", "Confirm Zombie Cure", list("Yes", "No"))
	if(confirm != "Yes")
		return

	for(var/obj/item/organ/zombie_infection/nodamage/I in GLOB.zombie_infection_list)
		qdel(I)

	message_admins("[key_name_admin(usr)] cured all zombies.")
	log_admin("[key_name(usr)] cured all zombies.")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Mass Zombie Cure")

/client/proc/polymorph_all()
	set category = "Admin.Fun"
	set name = "Polymorph All"
	set desc = "Applies the effects of the bolt of change to every single mob."

	if(!check_rights(R_ADMIN))
		return

	var/confirm = tgui_alert(usr, "Please confirm you want polymorph all mobs?", "Confirm Polymorph", list("Yes", "No"))
	if(confirm != "Yes")
		return

	var/list/mobs = shuffle(GLOB.alive_mob_list.Copy()) // might change while iterating
	var/who_did_it = key_name_admin(usr)

	message_admins("[key_name_admin(usr)] started polymorphed all living mobs.")
	log_admin("[key_name(usr)] polymorphed all living mobs.")
	SSblackbox.record_feedback("tally", "admin_verb", 1, "Polymorph All")

	for(var/mob/living/M in mobs)
		CHECK_TICK

		if(!M)
			continue

		M.audible_message(span_hear("...wabbajack...wabbajack..."))
		playsound(M.loc, 'sound/magic/staff_change.ogg', 50, TRUE, -1)

		M.wabbajack()

	message_admins("Mass polymorph started by [who_did_it] is complete.")

/client/proc/smite(mob/living/target as mob)
	set category = "Admin.Fun"
	set name = "Smite"
	if(!check_rights(R_ADMIN) || !check_rights(R_FUN))
		return

	var/punishment = input("Choose a punishment", "DIVINE SMITING") as null|anything in GLOB.smites

	if(QDELETED(target) || !punishment)
		return

	var/smite_path = GLOB.smites[punishment]
	var/datum/smite/smite = new smite_path
	var/configuration_success = smite.configure(usr)
	if (configuration_success == FALSE)
		return
	smite.effect(src, target)

/proc/breadify(atom/movable/target)
	var/obj/item/food/bread/plain/bread = new(get_turf(target))
	target.forceMove(bread)

/**
 * firing_squad is a proc for the :B:erforate smite to shoot each individual bullet at them, so that we can add actual delays without sleep() nonsense
 *
 * Hilariously, if you drag someone away mid smite, the bullets will still chase after them from the original spot, possibly hitting other people. Too funny to fix imo
 *
 * Arguments:
 * * target- guy we're shooting obviously
 * * source_turf- where the bullet begins, preferably on a turf next to the target
 * * body_zone- which bodypart we're aiming for, if there is one there
 * * wound_bonus- the wounding power we're assigning to the bullet, since we don't care about the base one
 * * damage- the damage we're assigning to the bullet, since we don't care about the base one
 */
/proc/firing_squad(mob/living/carbon/target, turf/source_turf, body_zone, wound_bonus, damage)
	if(!target.get_bodypart(body_zone))
		return
	playsound(target, 'sound/weapons/gun/revolver/shot.ogg', 100)
	var/obj/projectile/bullet/smite/divine_wrath = new(source_turf)
	divine_wrath.damage = damage
	divine_wrath.wound_bonus = wound_bonus
	divine_wrath.original = target
	divine_wrath.def_zone = body_zone
	divine_wrath.spread = 0
	divine_wrath.preparePixelProjectile(target, source_turf)
	divine_wrath.fire()

/client/proc/punish_log(whom, punishment)
	var/msg = "[key_name_admin(src)] punished [key_name_admin(whom)] with [punishment]."
	message_admins(msg)
	admin_ticket_log(whom, msg)
	log_admin("[key_name(src)] punished [key_name(whom)] with [punishment].")