code:
/// Fires an absurd amount of bullets at the target
/datum/smite/berforate
	name = ":B:erforate"

	/// Determines how fucked the target is
	var/hatred

/datum/smite/berforate/configure(client/user)
	var/static/list/how_fucked_is_this_dude = list("A little", "A lot", "So fucking much", "FUCK THIS DUDE")
	hatred = input(user, "How much do you hate this guy?") in how_fucked_is_this_dude

/datum/smite/berforate/effect(client/user, mob/living/target)
	. = ..()
	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return

	var/repetitions
	var/shots_per_limb_per_rep = 2
	var/damage
	switch (hatred)
		if ("A little")
			repetitions = 1
			damage = 5
		if ("A lot")
			repetitions = 2
			damage = 8
		if ("So fucking much")
			repetitions = 3
			damage = 10
		if ("FUCK THIS DUDE")
			repetitions = 4
			damage = 10

	var/mob/living/carbon/dude = target
	var/list/open_adj_turfs = get_adjacent_open_turfs(dude)
	var/list/wound_bonuses = list(15, 70, 110, 250)

	var/delay_per_shot = 1
	var/delay_counter = 1

	dude.Immobilize(5 SECONDS)
	for (var/wound_bonus_rep in 1 to repetitions)
		for (var/_limb in dude.bodyparts)
			var/obj/item/bodypart/limb = _limb
			var/shots_this_limb = 0
			for (var/_iter_turf in shuffle(open_adj_turfs))
				var/turf/iter_turf = _iter_turf
				addtimer(CALLBACK(GLOBAL_PROC, .proc/firing_squad, dude, iter_turf, limb.body_zone, wound_bonuses[wound_bonus_rep], damage), delay_counter)
				delay_counter += delay_per_shot
				shots_this_limb += 1
				if (shots_this_limb > shots_per_limb_per_rep)
					break


code:
/// Slashes up the target
/datum/smite/bloodless
	name = ":B:loodless"
/* SEPTIC EDIT REMOVAL
/datum/smite/bloodless/effect(client/user, mob/living/target)
	. = ..()
	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return
	var/mob/living/carbon/carbon_target = target
	for(var/_limb in carbon_target.bodyparts)
		var/obj/item/bodypart/limb = _limb
		var/type_wound = pick(list(/datum/wound/slash/severe, /datum/wound/slash/moderate))
		limb.force_wound_upwards(type_wound, smited = TRUE)
		type_wound = pick(list(/datum/wound/slash/critical, /datum/wound/slash/severe, /datum/wound/slash/moderate))
		limb.force_wound_upwards(type_wound, smited = TRUE)
		type_wound = pick(list(/datum/wound/slash/critical, /datum/wound/slash/severe))
		limb.force_wound_upwards(type_wound, smited = TRUE)
*/

repo_name: AnalWerewolf/sundown

license: mit


code:
/// Gives the target critically bad wounds
/datum/smite/boneless
	name = ":B:oneless"

/datum/smite/boneless/effect(client/user, mob/living/target)
	. = ..()

	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return

	var/mob/living/carbon/carbon_target = target
	for(var/_limb in carbon_target.bodyparts)
		var/obj/item/bodypart/limb = _limb
		var/type_wound = pick(list(/datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/critical, /datum/wound/blunt/severe, /datum/wound/blunt/moderate))
		limb.force_wound_upwards(type_wound, smited = TRUE)


code:
/// Inflicts crippling brain damage on the target
/datum/smite/brain_damage
	name = "Brain damage"

/datum/smite/brain_damage/effect(client/user, mob/living/target)
	. = ..()
	target.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 1, BRAIN_DAMAGE_DEATH - 1)


code:
#define BREADIFY_TIME (5 SECONDS)

/// Turns the target into bread
/datum/smite/bread
	name = "Bread"

/datum/smite/bread/effect(client/user, mob/living/target)
	. = ..()
	var/mutable_appearance/bread_appearance = mutable_appearance('icons/obj/food/burgerbread.dmi', "bread")
	var/mutable_appearance/transform_scanline = mutable_appearance('icons/effects/effects.dmi', "transform_effect")
	target.transformation_animation(bread_appearance,time = BREADIFY_TIME, transform_overlay=transform_scanline, reset_after=TRUE)
	addtimer(CALLBACK(GLOBAL_PROC, .proc/breadify, target), BREADIFY_TIME)

#undef BREADIFY_TIME


code:
#define BSA_CHANCE_TO_BREAK_TILE_TO_PLATING 80
#define BSA_MAX_DAMAGE 99
#define BSA_PARALYZE_TIME (40 SECONDS)
#define BSA_STUTTER_TIME 20

/// Fires the BSA at the target
/datum/smite/bsa
	name = "Bluespace Artillery Device"

/datum/smite/bsa/effect(client/user, mob/living/target)
	. = ..()

	explosion(target.loc, explosion_cause = src)

	var/turf/open/floor/target_turf = get_turf(target)
	if (istype(target_turf))
		if (prob(BSA_CHANCE_TO_BREAK_TILE_TO_PLATING))
			target_turf.break_tile_to_plating()
		else
			target_turf.break_tile()

	if (target.health <= 1)
		target.gib(
			/* no_brain = */ TRUE,
			/* no_organs = */ TRUE,
		)
	else
		target.adjustBruteLoss(min(BSA_MAX_DAMAGE, target.health - 1))
		target.Paralyze(BSA_PARALYZE_TIME)
		target.stuttering = BSA_STUTTER_TIME

#undef BSA_CHANCE_TO_BREAK_TILE_TO_PLATING
#undef BSA_MAX_DAMAGE
#undef BSA_PARALYZE_TIME
#undef BSA_STUTTER_TIME


code:
/// Docks the target's pay
/datum/smite/dock_pay
	name = "Dock Pay"

/datum/smite/dock_pay/effect(client/user, mob/living/target)
	. = ..()
	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return
	var/mob/living/carbon/dude = target
	var/obj/item/card/id/card = dude.get_idcard(TRUE)
	if (!card)
		to_chat(user, span_warning("[dude] does not have an ID card on!"), confidential = TRUE)
		return
	if (!card.registered_account)
		to_chat(user, span_warning("[dude] does not have an ID card with an account!"), confidential = TRUE)
		return
	if (card.registered_account.account_balance == 0)
		to_chat(user,  span_warning("ID Card lacks any funds. No pay to dock."))
		return
	var/new_cost = input("How much pay are we docking? Current balance: [card.registered_account.account_balance] credits.", "BUDGET CUTS") as num|null
	if (!new_cost)
		return
	if (!(card.registered_account.has_money(new_cost)))
		to_chat(user,  span_warning("ID Card lacked funds. Emptying account."))
		card.registered_account.bank_card_talk("[new_cost] credits deducted from your account based on performance review.")
		card.registered_account.account_balance = 0
	else
		card.registered_account.account_balance = card.registered_account.account_balance - new_cost
		card.registered_account.bank_card_talk("[new_cost] credits deducted from your account based on performance review.")
	SEND_SOUND(target, 'sound/machines/buzz-sigh.ogg')


code:
/// Sends the target a fake adminhelp sound
/datum/smite/fake_bwoink
	name = "Fake bwoink"

/datum/smite/fake_bwoink/effect(client/user, mob/living/target)
	. = ..()
	SEND_SOUND(target, 'sound/effects/adminhelp.ogg')


code:
/// Fattens the target
/datum/smite/fat
	name = "Fatten up"

/datum/smite/fat/effect(client/user, mob/living/target)
	. = ..()
	target.set_nutrition(NUTRITION_LEVEL_FAT * 2)


code:
/// Throws a fireball down at the target
/datum/smite/fireball
	name = "Fireball"

/datum/smite/fireball/effect(client/user, mob/living/target)
	. = ..()
	new /obj/effect/temp_visual/target(get_turf(target))


code:
/// Gibs the target
/datum/smite/gib
	name = "Gib"

/datum/smite/gib/effect(client/user, mob/living/target)
	. = ..()
	target.gib(/* no_brain = */ FALSE)


code:
/**
 * Custom imaginary friend.
 *
 * Allows the admin to select the ckey to put into the imaginary friend and whether the imaginary friend looks like the
 * ckey's character.
 *
 * Is not tied to the brain trauma and can be used on all mobs, technically. Including cyborgs and simple/basic mobs.
 *
 * Warranty void if used on AI eyes or other imaginary friends. Please smite responsibly.
 **/
/datum/smite/custom_imaginary_friend
	name = "Imaginary Friend (Special)"
	var/client/friend_candidate_client
	var/random_appearance

/datum/smite/custom_imaginary_friend/configure(client/user)
	friend_candidate_client = tgui_input_list(user, "Pick the player to put in control.", "New Imaginary Friend", sort_list(GLOB.clients))

	if(QDELETED(friend_candidate_client))
		to_chat(user, span_notice("Selected player no longer has a client, aborting."))
		return FALSE

	if(isliving(friend_candidate_client.mob) && (tgui_alert(user, "This player already has a living mob ([friend_candidate_client.mob]). Do you still want to turn them into an Imaginary Friend?", "Remove player from mob?", list("Do it!", "Cancel")) == "Cancel"))
		return

	if(QDELETED(friend_candidate_client))
		to_chat(user, span_notice("Selected player no longer has a client, aborting."))
		return FALSE

	if(friend_candidate_client.prefs)
		random_appearance = tgui_alert(user, "Do you want the imaginary friend to look like and be named after [friend_candidate_client]'s current preferences ([friend_candidate_client.prefs.read_preference(/datum/preference/name/real_name)])?", "Imaginary Friend Appearance?", list("Look-a-like", "Random")) == "Random"
	else
		random_appearance = TRUE

/datum/smite/custom_imaginary_friend/effect(client/user, mob/living/target)
	. = ..()

	if(QDELETED(target))
		to_chat(user, span_warning("The target mob no longer exists, aborting."))
		return

	if(QDELETED(friend_candidate_client))
		to_chat(user, span_warning("Imaginary friend candidate no longer has a client, aborting."))
		return

	if(isliving(friend_candidate_client.mob))
		friend_candidate_client.mob.ghostize(can_reenter_corpse = TRUE)

	var/mob/camera/imaginary_friend/friend_mob

	if(random_appearance)
		friend_mob = new /mob/camera/imaginary_friend(get_turf(target), target)
	else
		friend_mob = new /mob/camera/imaginary_friend(get_turf(target), target, friend_candidate_client.prefs)

	friend_mob.key = friend_candidate_client.key


code:
/// "Fully immerses" the player, making them manually breathe and blink
/datum/smite/immerse
	name = "Fully Immerse"

/datum/smite/immerse/effect(client/user, mob/living/target)
	. = ..()
	immerse_player(target)
	SEND_SOUND(target, sound('sound/voice/roleplay.ogg'))
	to_chat(target, span_boldnotice("Please roleplay appropriately, okay?"))


code:
/// Ties the target's shoes
/datum/smite/knot_shoes
	name = "Knot Shoes"

/datum/smite/knot_shoes/effect(client/user, mob/living/target)
	. = ..()
	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return
	var/mob/living/carbon/dude = target
	var/obj/item/clothing/shoes/sick_kicks = dude.shoes
	if (!sick_kicks?.can_be_tied)
		to_chat(user, span_warning("[dude] does not have knottable shoes!"), confidential = TRUE)
		return
	sick_kicks.adjust_laces(SHOES_KNOTTED)


code:
/// Strikes the target with a lightning bolt
/datum/smite/lightning
	name = "Lightning bolt"

/datum/smite/lightning/effect(client/user, mob/living/target)
	. = ..()
	lightningbolt(target)
	to_chat(target, span_userdanger("The gods have punished you for your sins!"), confidential = TRUE)

///this is the actual bolt effect and damage, made into its own proc because it is used elsewhere
/proc/lightningbolt(mob/living/user)
	var/turf/lightning_source = get_step(get_step(user, NORTH), NORTH)
	lightning_source.Beam(user, icon_state="lightning[rand(1,12)]", time = 5)
	user.adjustFireLoss(LIGHTNING_BOLT_DAMAGE)
	playsound(get_turf(user), 'sound/magic/lightningbolt.ogg', 50, TRUE)
	if(ishuman(user))
		var/mob/living/carbon/human/human_target = user
		human_target.electrocution_animation(LIGHTNING_BOLT_ELECTROCUTION_ANIMATION_LENGTH)


code:
/// Rips off all the limbs of the target
/datum/smite/nugget
	name = "Nugget"

/datum/smite/nugget/effect(client/user, mob/living/target)
	. = ..()

	if (!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return

	var/mob/living/carbon/carbon_target = target
	var/timer = 2 SECONDS
	for (var/_limb in carbon_target.bodyparts)
		var/obj/item/bodypart/limb = _limb
		if (limb.body_part == HEAD || limb.body_part == CHEST)
			continue
		addtimer(CALLBACK(limb, /obj/item/bodypart/.proc/dismember), timer)
		addtimer(CALLBACK(GLOBAL_PROC, .proc/playsound, carbon_target, 'sound/effects/cartoon_pop.ogg', 70), timer)
		addtimer(CALLBACK(carbon_target, /mob/living/.proc/spin, 4, 1), timer - 0.4 SECONDS)
		timer += 2 SECONDS


code:
/// "Immerses" the player, making them unable to use some OOC terms IC
/datum/smite/ocky_icky
	name = "Ocky icky phobia"

/datum/smite/ocky_icky/effect(client/user, mob/living/target)
	. = ..()
	var/mob/living/carbon/ocker = target
	ocker.gain_trauma(/datum/brain_trauma/mild/phobia/ocky_icky, TRAUMA_RESILIENCE_LOBOTOMY)


code:
/// Turns the user into a sliding puzzle
/datum/smite/puzzle
	name = "Puzzle"

/datum/smite/puzzle/effect(client/user, mob/living/target)
	. = ..()
	if(!puzzle_imprison(target))
		to_chat(user, span_warning("Imprisonment failed!"), confidential = TRUE)


code:
/// Throw an immovable rod at the target
/datum/smite/rod
	name = "Immovable Rod"
	var/force_looping = FALSE

/datum/smite/rod/configure(client/user)
	var/loop_input = tgui_alert(usr,"Would you like this rod to force-loop across space z-levels?", "Loopy McLoopface", list("Yes", "No"))

	force_looping = (loop_input == "Yes")

/datum/smite/rod/effect(client/user, mob/living/target)
	. = ..()
	var/turf/target_turf = get_turf(target)
	var/startside = pick(GLOB.cardinals)
	var/turf/start_turf = spaceDebrisStartLoc(startside, target_turf.z)
	var/turf/end_turf = spaceDebrisFinishLoc(startside, target_turf.z)
	new /obj/effect/immovablerod(start_turf, end_turf, target, force_looping)


code:
/// Gives the target fake scars
/datum/smite/scarify
	name = "Scarify"

/datum/smite/scarify/effect(client/user, mob/living/target)
	. = ..()
	if(!iscarbon(target))
		to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE)
		return
	var/mob/living/carbon/dude = target
	dude.generate_fake_scars(rand(1, 4))
	to_chat(dude, span_warning("You feel your body grow jaded and torn..."))


code:
/// A smite, used by admins to punish players, or for their own amusement
/datum/smite
	/// The name of the smite, shown in the menu
	var/name

	/// Should this smite write to logs?
	var/should_log = TRUE

/// Called once after either choosing the option to smite a player, or when selected in smite build mode.
/// Use this to prompt the user configuration options.
/// Return FALSE if the smite should not be used.
/datum/smite/proc/configure(client/user)

/// The effect of the smite, make sure to call this in your own smites
/datum/smite/proc/effect(client/user, mob/living/target)
	if (should_log)
		user.punish_log(target, name)


code:
#define SUPPLY_POD_FIRE_RANGE

/// Throws a supply pod at the target, with no item inside
/datum/smite/supply_pod
	name = "Supply Pod"

	// punish_log() is handled by the centcom_podlauncher datum
	should_log = FALSE

/datum/smite/supply_pod/effect(client/user, mob/living/target)
	. = ..()
	var/datum/centcom_podlauncher/plaunch = new(user)
	plaunch.specificTarget = target
	plaunch.launchChoice = 0
	plaunch.damageChoice = 1
	plaunch.explosionChoice = 1
	plaunch.temp_pod.damage = 40 // bring the mother fuckin ruckus
	plaunch.temp_pod.explosionSize = list(0, 0, 0, SUPPLY_POD_FIRE_RANGE)
	plaunch.temp_pod.effectStun = TRUE

#undef SUPPLY_POD_FIRE_RANGE


code:
#define SUPPLY_POD_QUICK_DAMAGE 40
#define SUPPLY_POD_QUICK_FIRE_RANGE 2

/// Quickly throws a supply pod at the target, optionally with an item
/datum/smite/supply_pod_quick
	name = "Supply Pod (Quick)"

	/// What is sent down with the pod
	var/target_path

/datum/smite/supply_pod_quick/configure(client/user)
	var/attempted_target_path = input(
		user,
		"Enter typepath of an atom you'd like to send with the pod (type \"empty\" to send an empty pod):",
		"Typepath",
		"/obj/item/food/grown/harebell",
	) as null|text

	if (isnull(attempted_target_path)) //The user pressed "Cancel"
		return FALSE

	if (attempted_target_path == "empty")
		target_path = null
		return

	// if you didn't type empty, we want to load the pod with a delivery
	var/delivery = text2path(attempted_target_path)
	if(!ispath(delivery))
		delivery = pick_closest_path(attempted_target_path)
		if(!delivery)
			tgui_alert(user, "ERROR: Incorrect / improper path given.")
			return FALSE
	target_path = delivery

/datum/smite/supply_pod_quick/effect(client/user, mob/living/target)
	. = ..()
	podspawn(list(
		"target" = get_turf(target),
		"path" = /obj/structure/closet/supplypod/centcompod,
		"style" = STYLE_CENTCOM,
		"spawn" = target_path,
		"damage" = SUPPLY_POD_QUICK_DAMAGE,
		"explosionSize" = list(0, 0, 0, SUPPLY_POD_QUICK_FIRE_RANGE),
		"effectStun" = TRUE
	))

#undef SUPPLY_POD_QUICK_DAMAGE
#undef SUPPLY_POD_QUICK_FIRE_RANGE


code:
#define SOUND_EMITTER_LOCAL "local" //Plays the sound like a normal heard sound
#define SOUND_EMITTER_DIRECT "direct" //Plays the sound directly to hearers regardless of pressure/proximity/et cetera

#define SOUND_EMITTER_RADIUS "radius" //Plays the sound to everyone in a radius
#define SOUND_EMITTER_ZLEVEL "zlevel" //Plays the sound to everyone on the z-level
#define SOUND_EMITTER_GLOBAL "global" //Plays the sound to everyone in the game world

//Admin sound emitters with highly customizable functions!
/obj/effect/sound_emitter
	name = "sound emitter"
	desc = "Emits sounds, presumably."
	icon = 'icons/effects/effects.dmi'
	icon_state = "shield2"
	invisibility = INVISIBILITY_OBSERVER
	anchored = TRUE
	density = FALSE
	opacity = FALSE
	alpha = 175
	var/sound_file //The sound file the emitter plays
	var/sound_volume = 50 //The volume the sound file is played at
	var/play_radius = 3 //Any mobs within this many tiles will hear the sounds played if it's using the appropriate mode
	var/motus_operandi = SOUND_EMITTER_LOCAL //The mode this sound emitter is using
	var/emitter_range = SOUND_EMITTER_ZLEVEL //The range this emitter's sound is heard at; this isn't a number, but a string (see the defines above)

/obj/effect/sound_emitter/Destroy(force)
	if(!force)
		return QDEL_HINT_LETMELIVE
	. = ..()

/obj/effect/sound_emitter/singularity_act()
	return

/obj/effect/sound_emitter/singularity_pull()
	return

/obj/effect/sound_emitter/examine(mob/user)
	. = ..()
	if(!isobserver(user))
		return
	. += "[span_boldnotice("Sound File:")] [sound_file ? sound_file : "None chosen"]"
	. += span_boldnotice("Mode: [motus_operandi]")
	. += span_boldnotice("Range: [emitter_range]")
	. += "Sound is playing at [sound_volume]% volume."
	if(user.client.holder)
		. += "Alt-click it to quickly activate it!"

//ATTACK GHOST IGNORING PARENT RETURN VALUE
/obj/effect/sound_emitter/attack_ghost(mob/user)
	if(!check_rights_for(user.client, R_SOUND))
		examine(user)
		return
	edit_emitter(user)

/obj/effect/sound_emitter/AltClick(mob/user)
	if(check_rights_for(user.client, R_SOUND))
		activate(user)
		to_chat(user, span_notice("Sound emitter activated."), confidential = TRUE)

/obj/effect/sound_emitter/proc/edit_emitter(mob/user)
	var/dat = ""
	dat += "Label: [maptext ? maptext : "No label set!"]
"
	dat += "
"
	dat += "Sound File: [sound_file ? sound_file : "No file chosen!"]
"
	dat += "Volume: [sound_volume]%
"
	dat += "
"
	dat += "Mode: [motus_operandi]
"
	if(motus_operandi != SOUND_EMITTER_LOCAL)
		dat += "Range: [emitter_range][emitter_range == SOUND_EMITTER_RADIUS ? "[play_radius]-tile radius" : ""]
"
	dat += "
"
	dat += "Play Sound (interrupts other sound emitter sounds)"
	var/datum/browser/popup = new(user, "emitter", "", 500, 600)
	popup.set_content(dat)
	popup.open()

/obj/effect/sound_emitter/Topic(href, href_list)
	..()
	if(!ismob(usr) || !usr.client || !check_rights_for(usr.client, R_SOUND))
		return
	var/mob/user = usr
	if(href_list["edit_label"])
		var/new_label = stripped_input(user, "Choose a new label.", "Sound Emitter")
		if(!new_label)
			return
		maptext = MAPTEXT(new_label)
		to_chat(user, span_notice("Label set to [maptext]."), confidential = TRUE)
	if(href_list["edit_sound_file"])
		var/new_file = input(user, "Choose a sound file.", "Sound Emitter") as null|sound
		if(!new_file)
			return
		sound_file = new_file
		to_chat(user, span_notice("New sound file set to [sound_file]."), confidential = TRUE)
	if(href_list["edit_volume"])
		var/new_volume = input(user, "Choose a volume.", "Sound Emitter", sound_volume) as null|num
		if(isnull(new_volume))
			return
		new_volume = clamp(new_volume, 0, 100)
		sound_volume = new_volume
		to_chat(user, span_notice("Volume set to [sound_volume]%."), confidential = TRUE)
	if(href_list["edit_mode"])
		var/new_mode
		var/mode_list = list("Local (normal sound)" = SOUND_EMITTER_LOCAL, "Direct (not affected by environment/location)" = SOUND_EMITTER_DIRECT)
		new_mode = input(user, "Choose a new mode.", "Sound Emitter") as null|anything in mode_list
		if(!new_mode)
			return
		motus_operandi = mode_list[new_mode]
		to_chat(user, span_notice("Mode set to [motus_operandi]."), confidential = TRUE)
	if(href_list["edit_range"])
		var/new_range
		var/range_list = list("Radius (all mobs within a radius)" = SOUND_EMITTER_RADIUS, "Z-Level (all mobs on the same z)" = SOUND_EMITTER_ZLEVEL, "Global (all players)" = SOUND_EMITTER_GLOBAL)
		new_range = input(user, "Choose a new range.", "Sound Emitter") as null|anything in range_list
		if(!new_range)
			return
		emitter_range = range_list[new_range]
		to_chat(user, span_notice("Range set to [emitter_range]."), confidential = TRUE)
	if(href_list["edit_radius"])
		var/new_radius = input(user, "Choose a radius.", "Sound Emitter", sound_volume) as null|num
		if(isnull(new_radius))
			return
		new_radius = clamp(new_radius, 0, 127)
		play_radius = new_radius
		to_chat(user, span_notice("Audible radius set to [play_radius]."), confidential = TRUE)
	if(href_list["play"])
		activate(user)
	edit_emitter(user) //Refresh the UI to see our changes

/obj/effect/sound_emitter/proc/activate(mob/user)
	var/list/hearing_mobs = list()
	if(motus_operandi == SOUND_EMITTER_LOCAL)
		playsound(src, sound_file, sound_volume, FALSE)
		return
	switch(emitter_range)
		if(SOUND_EMITTER_RADIUS)
			for(var/mob/M in GLOB.player_list)
				if(get_dist(src, M) <= play_radius)
					hearing_mobs += M
		if(SOUND_EMITTER_ZLEVEL)
			for(var/mob/M in GLOB.player_list)
				if(M.z == z)
					hearing_mobs += M
		if(SOUND_EMITTER_GLOBAL)
			hearing_mobs = GLOB.player_list.Copy()
	for(var/mob/M in hearing_mobs)
		if(M.client.prefs.toggles & SOUND_MIDI)
			M.playsound_local(M, sound_file, sound_volume, FALSE, channel = CHANNEL_ADMIN, pressure_affected = FALSE)
	if(user)
		log_admin("[ADMIN_LOOKUPFLW(user)] activated a sound emitter with file \"[sound_file]\" at [AREACOORD(src)]")
	flick("shield1", src)

#undef SOUND_EMITTER_LOCAL
#undef SOUND_EMITTER_DIRECT
#undef SOUND_EMITTER_RADIUS
#undef SOUND_EMITTER_ZLEVEL
#undef SOUND_EMITTER_GLOBAL


code:
#define MAX_ADMINBANS_PER_ADMIN 1
#define MAX_ADMINBANS_PER_HEADMIN 3

//checks client ban cache or DB ban table if ckey is banned from one or more roles
//doesn't return any details, use only for if statements
/proc/is_banned_from(player_ckey, list/roles)
	if(!player_ckey)
		return
	var/client/player_client = GLOB.directory[player_ckey]
	if(player_client)
		var/list/ban_cache = player_client.ban_cache || build_ban_cache(player_client)
		if(!islist(ban_cache))
			return // Disconnected while building the list.
		if(islist(roles))
			for(var/role in roles)
				if(role in ban_cache)
					return TRUE //they're banned from at least one role, no need to keep checking
		else if(roles in ban_cache)
			return TRUE
	else
		var/values = list(
			"player_ckey" = player_ckey,
			"must_apply_to_admins" = !!(GLOB.admin_datums[player_ckey] || GLOB.deadmins[player_ckey]),
		)
		var/sql_roles
		if(islist(roles))
			var/list/sql_roles_list = list()
			for (var/i in 1 to roles.len)
				values["role[i]"] = roles[i]
				sql_roles_list += ":role[i]"
			sql_roles = sql_roles_list.Join(", ")
		else
			values["role"] = roles
			sql_roles = ":role"
		var/datum/db_query/query_check_ban = SSdbcore.NewQuery({"
			SELECT 1
			FROM [format_table_name("ban")]
			WHERE
				ckey = :player_ckey AND
				role IN ([sql_roles]) AND
				unbanned_datetime IS NULL AND
				(expiration_time IS NULL OR expiration_time > NOW())
				AND (NOT :must_apply_to_admins OR applies_to_admins = 1)
		"}, values)
		if(!query_check_ban.warn_execute())
			qdel(query_check_ban)
			return
		if(query_check_ban.NextRow())
			qdel(query_check_ban)
			return TRUE
		qdel(query_check_ban)

//checks DB ban table if a ckey, ip and/or cid is banned from a specific role
//returns an associative nested list of each matching row's ban id, bantime, ban round id, expiration time, ban duration, applies to admins, reason, key, ip, cid and banning admin's key in that order
/proc/is_banned_from_with_details(player_ckey, player_ip, player_cid, role)
	if(!player_ckey && !player_ip && !player_cid)
		return
	var/datum/db_query/query_check_ban = SSdbcore.NewQuery({"
		SELECT
			id,
			bantime,
			round_id,
			expiration_time,
			TIMESTAMPDIFF(MINUTE, bantime, expiration_time),
			applies_to_admins,
			reason,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey), ckey),
			INET_NTOA(ip),
			computerid,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey), a_ckey)
		FROM [format_table_name("ban")]
		WHERE role = :role
			AND (ckey = :ckey OR ip = INET_ATON(:ip) OR computerid = :computerid)
			AND unbanned_datetime IS NULL
			AND (expiration_time IS NULL OR expiration_time > NOW())
		ORDER BY bantime DESC
	"}, list("role" = role, "ckey" = player_ckey, "ip" = player_ip, "computerid" = player_cid))
	if(!query_check_ban.warn_execute())
		qdel(query_check_ban)
		return
	. = list()
	while(query_check_ban.NextRow())
		. += list(list("id" = query_check_ban.item[1], "bantime" = query_check_ban.item[2], "round_id" = query_check_ban.item[3], "expiration_time" = query_check_ban.item[4], "duration" = query_check_ban.item[5], "applies_to_admins" = query_check_ban.item[6], "reason" = query_check_ban.item[7], "key" = query_check_ban.item[8], "ip" = query_check_ban.item[9], "computerid" = query_check_ban.item[10], "admin_key" = query_check_ban.item[11]))
	qdel(query_check_ban)

/proc/build_ban_cache(client/player_client)
	if(!SSdbcore.Connect())
		return
	if(QDELETED(player_client))
		return
	var/ckey = player_client.ckey
	var/list/ban_cache = list()
	var/is_admin = FALSE
	if(GLOB.admin_datums[ckey] || GLOB.deadmins[ckey])
		is_admin = TRUE
	var/datum/db_query/query_build_ban_cache = SSdbcore.NewQuery(
		"SELECT role, applies_to_admins FROM [format_table_name("ban")] WHERE ckey = :ckey AND unbanned_datetime IS NULL AND (expiration_time IS NULL OR expiration_time > NOW())",
		list("ckey" = ckey)
	)
	if(!query_build_ban_cache.warn_execute())
		qdel(query_build_ban_cache)
		return
	while(query_build_ban_cache.NextRow())
		if(is_admin && !text2num(query_build_ban_cache.item[2]))
			continue
		ban_cache[query_build_ban_cache.item[1]] = TRUE
	qdel(query_build_ban_cache)
	if(QDELETED(player_client)) // Disconnected while working with the DB.
		return
	player_client.ban_cache = ban_cache
	return ban_cache

/datum/admins/proc/ban_panel(player_key, player_ip, player_cid, role, duration = 1440, applies_to_admins, reason, edit_id, page, admin_key)
	var/panel_height = 620
	if(edit_id)
		panel_height = 240
	var/datum/browser/panel = new(usr, "banpanel", "Banning Panel", 910, panel_height)
	panel.add_stylesheet("admin_panelscss", 'html/admin/admin_panels.css')
	panel.add_stylesheet("banpanelcss", 'html/admin/banpanel.css')
	var/tgui_fancy = usr.client.prefs.read_preference(/datum/preference/toggle/tgui_fancy)
	if(tgui_fancy) //some browsers (IE8) have trouble with unsupported css3 elements and DOM methods that break the panel's functionality, so we won't load those if a user is in no frills tgui mode since that's for similar compatability support
		panel.add_stylesheet("admin_panelscss3", 'html/admin/admin_panels_css3.css')
		panel.add_script("banpaneljs", 'html/admin/banpanel.js')
	var/list/output = list("<form method='get' action='?src=[REF(src)]'>[HrefTokenFormField()]")
	output += {"<input type='hidden' name='src' value='[REF(src)]'>
	Key:
	<input type='checkbox' id='keycheck' name='keycheck' value='1'[player_key ? " checked": ""]>

	<input type='text' name='keytext' size='26' value='[player_key]'>
	IP:
	<input type='checkbox' id='ipcheck' name='ipcheck' value='1'[isnull(duration) ? " checked" : ""]>

	<input type='text' name='iptext' size='18' value='[player_ip]'>
	CID:
	<input type='checkbox' id='cidcheck' name='cidcheck' value='1' checked>

	<input type='text' name='cidtext' size='14' value='[player_cid]'>

	Use IP and CID from last connection of key
	<input type='checkbox' id='lastconn' name='lastconn' value='1' [(isnull(duration) && !player_ip) || (!player_cid) ? " checked": ""]>

	Applies to Admins
	<input type='checkbox' id='applyadmins' name='applyadmins' value='1'[applies_to_admins ? " checked": ""]>

	<input type='submit' value='Submit'>

			Duration type

			Permanent
			<input type='radio' id='permanent' name='radioduration' value='permanent'[isnull(duration) ? " checked" : ""]>

			Temporary
			<input type='radio' id='temporary' name='radioduration' value='temporary'[duration ? " checked" : ""]>

			<input type='text' name='duration' size='7' value='[duration]'>

				<select name='intervaltype'>
					<option value='SECOND'>Seconds</option>
					<option value='MINUTE' selected>Minutes</option>
					<option value='HOUR'>Hours</option>
					<option value='DAY'>Days</option>
					<option value='WEEK'>Weeks</option>
					<option value='MONTH'>Months</option>
					<option value='YEAR'>Years</option>
				</select>

			Ban type

			Server
			<input type='radio' id='server' name='radioban' value='server'[role == "Server" ? " checked" : ""][edit_id ? " disabled" : ""]>

			Role
			<input type='radio' id='role' name='radioban' value='role'[role == "Server" ? "" : " checked"][edit_id ? " disabled" : ""]>

			Severity

			None
			<input type='radio' id='none' name='radioseverity' value='none'[edit_id ? " disabled" : ""]>

			Medium
			<input type='radio' id='medium' name='radioseverity' value='medium'[edit_id ? " disabled" : ""]>

			Minor
			<input type='radio' id='minor' name='radioseverity' value='minor'[edit_id ? " disabled" : ""]>

			High
			<input type='radio' id='high' name='radioseverity' value='high'[edit_id ? " disabled" : ""]>

			Reason

			<textarea class='reason' name='reason'>[reason]</textarea>

	"}

	if(edit_id)
		output += {"Mirror edits to matching bans
		<input type='checkbox' id='mirroredit' name='mirroredit' value='1'>

		<input type='hidden' name='editid' value='[edit_id]'>
		<input type='hidden' name='oldkey' value='[player_key]'>
		<input type='hidden' name='oldip' value='[player_ip]'>
		<input type='hidden' name='oldcid' value='[player_cid]'>
		<input type='hidden' name='oldapplies' value='[applies_to_admins]'>
		<input type='hidden' name='oldduration' value='[duration]'>
		<input type='hidden' name='oldreason' value='[reason]'>
		<input type='hidden' name='page' value='[page]'>
		<input type='hidden' name='adminkey' value='[admin_key]'>
		<input type='hidden' name='role' value='[role]'>

		When ticked, edits here will also affect bans created with matching ckey, IP, CID and time. Use this to edit all role bans which were made at the same time.
		"}
	else
		output += "<input type='hidden' name='roleban_delimiter' value='1'>"
		//there's not always a client to use the bancache of so to avoid many individual queries from using is_banned_form we'll build a cache to use here
		var/banned_from = list()
		if(player_key)
			var/datum/db_query/query_get_banned_roles = SSdbcore.NewQuery({"
				SELECT role
				FROM [format_table_name("ban")]
				WHERE
					ckey = :player_ckey AND
					role <> 'server'
					AND unbanned_datetime IS NULL
					AND (expiration_time IS NULL OR expiration_time > NOW())
			"}, list("player_ckey" = ckey(player_key)))
			if(!query_get_banned_roles.warn_execute())
				qdel(query_get_banned_roles)
				return
			while(query_get_banned_roles.NextRow())
				banned_from += query_get_banned_roles.item[1]
			qdel(query_get_banned_roles)
		var/break_counter = 0
		output += "
"

		for(var/datum/job_department/department as anything in SSjob.joinable_departments)
			var/label_class = department.label_class
			var/department_name = department.department_name
			output += "
[tgui_fancy ? "<input type='checkbox' name='[label_class]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""] \
			[department_name]
"
			for(var/datum/job/job_datum as anything in department.department_jobs)
				if(break_counter > 0 && (break_counter % 3 == 0))
					output += "
"
				break_counter++
				var/job_name = job_datum.title
				if(length(job_datum.departments_list) > 1) //This job is in multiple departments, so we need to check all the boxes.
					// Clicking this will also toggle all the other boxes, minus this one.
					var/department_index = job_datum.departments_list.Find(department.type)
					if(!department_index)
						stack_trace("Failed to find a department index for [department.type] in the departments_list of [job_datum.type]")
					output += {"[job_name]
						<input type='checkbox' id='[job_name]_[department_index]' name='[job_name]' class='[label_class]' value='1'[tgui_fancy ? " onClick='toggle_other_checkboxes(this, \"[length(job_datum.departments_list)]\", \"[department_index]\")'" : ""]>

						"}
				else
					output += {"[job_name]
							<input type='checkbox' name='[job_name]' class='[label_class]' value='1'>

							"}
			output += "

"
			break_counter = 0

		var/list/other_job_lists = list(
			"Abstract" = list("Appearance", "Emote", "Deadchat", "OOC"),
			)
		for(var/department in other_job_lists)
			output += "
[tgui_fancy ? "<input type='checkbox' name='[department]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""][department]
"
			break_counter = 0
			for(var/job in other_job_lists[department])
				if(break_counter > 0 && (break_counter % 3 == 0))
					output += "
"
				output += {"[job]
							<input type='checkbox' name='[job]' class='[department]' value='1'>

				"}
				break_counter++
			output += "

"
		var/list/long_job_lists = list(
			"Ghost and Other Roles" = list(
				ROLE_PAI,
				ROLE_BRAINWASHED,
				ROLE_DEATHSQUAD,
				ROLE_DRONE,
				ROLE_LAVALAND,
				ROLE_MIND_TRANSFER,
				ROLE_POSIBRAIN,
				ROLE_SENTIENCE,
			),
			"Antagonist Positions" = list(
				ROLE_ABDUCTOR,
				ROLE_ALIEN,
				ROLE_BLOB,
				ROLE_BROTHER,
				ROLE_CHANGELING,
				ROLE_CULTIST,
				ROLE_FAMILIES,
				ROLE_HERETIC,
				ROLE_HIVE,
				ROLE_INTERNAL_AFFAIRS,
				ROLE_MALF,
				ROLE_MONKEY,
				ROLE_NINJA,
				ROLE_OPERATIVE,
				ROLE_OVERTHROW,
				ROLE_REV,
				ROLE_REVENANT,
				ROLE_REV_HEAD,
				ROLE_SENTIENT_DISEASE,
				ROLE_SPIDER,
				ROLE_SWARMER,
				ROLE_SYNDICATE,
				ROLE_TRAITOR,
				ROLE_WIZARD,
			),
		)
		for(var/department in long_job_lists)
			output += "
[tgui_fancy ? "<input type='checkbox' name='[department]' class='hidden' onClick='header_click_all_checkboxes(this)'>" : ""][department]
"
			break_counter = 0
			for(var/job in long_job_lists[department])
				if(break_counter > 0 && (break_counter % 10 == 0))
					output += "
"
				output += {"[job]
							<input type='checkbox' name='[job]' class='[department]' value='1'>

				"}
				break_counter++
			output += "

"
		output += "
"
	output += "</form>"
	panel.set_content(output.Join())
	panel.open()

/datum/admins/proc/ban_parse_href(list/href_list)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/list/error_state = list()
	var/player_key
	var/ip_check = FALSE
	var/player_ip
	var/cid_check = FALSE
	var/player_cid
	var/use_last_connection = FALSE
	var/applies_to_admins = FALSE
	var/duration
	var/interval
	var/severity
	var/reason
	var/mirror_edit
	var/edit_id
	var/old_key
	var/old_ip
	var/old_cid
	var/old_applies
	var/page
	var/admin_key
	var/list/changes = list()
	var/list/roles_to_ban = list()
	if(href_list["keycheck"])
		player_key = href_list["keytext"]
		if(!player_key)
			error_state += "Key was ticked but none was provided."
	if(href_list["ipcheck"])
		ip_check = TRUE
	if(href_list["cidcheck"])
		cid_check = TRUE
	if(href_list["lastconn"])
		if(player_key)
			use_last_connection = TRUE
	else
		if(ip_check)
			player_ip = href_list["iptext"]
			if(!player_ip && !use_last_connection)
				error_state += "IP was ticked but none was provided."
		if(cid_check)
			player_cid = href_list["cidtext"]
			if(!player_cid && !use_last_connection)
				error_state += "CID was ticked but none was provided."
	if(!use_last_connection && !player_ip && !player_cid && !player_key)
		error_state += "At least a key, IP or CID must be provided."
	if(use_last_connection && !ip_check && !cid_check)
		error_state += "Use last connection was ticked, but neither IP nor CID was."
	if(href_list["applyadmins"])
		applies_to_admins = TRUE
	switch(href_list["radioduration"])
		if("permanent")
			duration = null
		if("temporary")
			duration = href_list["duration"]
			interval = href_list["intervaltype"]
			if(!duration)
				error_state += "Temporary ban was selected but no duration was provided."
		else
			error_state += "No duration was selected."
	reason = href_list["reason"]
	if(!reason)
		error_state += "No reason was provided."
	if(href_list["editid"])
		edit_id = href_list["editid"]
		if(href_list["mirroredit"])
			mirror_edit = TRUE
		old_key = href_list["oldkey"]
		old_ip = href_list["oldip"]
		old_cid = href_list["oldcid"]
		page = href_list["page"]
		admin_key = href_list["adminkey"]
		if(player_key != old_key)
			changes += list("Key" = "[old_key] to [player_key]")
		if(player_ip != old_ip)
			changes += list("IP" = "[old_ip] to [player_ip]")
		if(player_cid != old_cid)
			changes += list("CID" = "[old_cid] to [player_cid]")
		old_applies = text2num(href_list["oldapplies"])
		if(applies_to_admins != old_applies)
			changes += list("Applies to admins" = "[old_applies] to [applies_to_admins]")
		if(duration != href_list["oldduration"])
			changes += list("Duration" = "[href_list["oldduration"]] MINUTE to [duration] [interval]")
		if(reason != href_list["oldreason"])
			changes += list("Reason" = "[href_list["oldreason"]]
to
[reason]")
		if(!changes.len)
			error_state += "No changes were detected."
		roles_to_ban += href_list["role"]
	else
		severity = href_list["radioseverity"]
		if(!severity)
			error_state += "No severity was selected."
		switch(href_list["radioban"])
			if("server")
				roles_to_ban += "Server"
			if("role")
				href_list.Remove("Command", "Security", "Engineering", "Medical", "Science", "Supply", "Silicon", "Abstract", "Service", "Ghost and Other Roles", "Antagonist Positions") //remove the role banner hidden input values
				if(href_list[href_list.len] == "roleban_delimiter")
					error_state += "Role ban was selected but no roles to ban were selected."
				else
					var/delimiter_pos = href_list.Find("roleban_delimiter")
					href_list.Cut(1, delimiter_pos+1)//remove every list element before and including roleban_delimiter so we have a list of only the roles to ban
					for(var/key in href_list) //flatten into a list of only unique keys
						roles_to_ban |= key
			else
				error_state += "No ban type was selected."
	if(error_state.len)
		to_chat(usr, span_danger("Ban not [edit_id ? "edited" : "created"] because the following errors were present:\n[error_state.Join("\n")]"), confidential = TRUE)
		return
	if(edit_id)
		edit_ban(edit_id, player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, reason, mirror_edit, old_key, old_ip, old_cid, old_applies, page, admin_key, changes, roles_to_ban[1] == "Server")
	else
		create_ban(player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, severity, reason, roles_to_ban)

/datum/admins/proc/create_ban(player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, severity, reason, list/roles_to_ban)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/player_ckey = ckey(player_key)
	if(player_ckey)
		var/datum/db_query/query_create_ban_get_player = SSdbcore.NewQuery({"
			SELECT byond_key, INET_NTOA(ip), computerid FROM [format_table_name("player")] WHERE ckey = :player_ckey
		"}, list("player_ckey" = player_ckey))
		if(!query_create_ban_get_player.warn_execute())
			qdel(query_create_ban_get_player)
			return
		if(query_create_ban_get_player.NextRow())
			player_key = query_create_ban_get_player.item[1]
			if(use_last_connection)
				if(ip_check)
					player_ip = query_create_ban_get_player.item[2]
				if(cid_check)
					player_cid = query_create_ban_get_player.item[3]
		else
			if(use_last_connection)
				if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, unable to use IP and CID from last connection. Are you sure you want to create a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
					qdel(query_create_ban_get_player)
					return
			else
				if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, are you sure you want to create a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
					qdel(query_create_ban_get_player)
					return
		qdel(query_create_ban_get_player)
	var/admin_ckey = usr.client.ckey
	if(applies_to_admins && !can_place_additional_admin_ban(admin_ckey))
		return
	var/admin_ip = usr.client.address
	var/admin_cid = usr.client.computer_id
	duration = text2num(duration)
	if (!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR")))
		interval = "MINUTE"
	var/time_message = "[duration] [lowertext(interval)]" //no DisplayTimeText because our duration is of variable interval type
	if(duration > 1) //pluralize the interval if necessary
		time_message += "s"
	var/is_server_ban = (roles_to_ban[1] == "Server")
	var/note_reason = "Banned from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"] [isnull(duration) ? "permanently" : "for [time_message]"] - [reason]"
	var/list/clients_online = GLOB.clients.Copy()
	var/list/admins_online = list()
	for(var/client/C in clients_online)
		if(C.holder) //deadmins aren't included since they wouldn't show up on adminwho
			admins_online += C
	var/who = clients_online.Join(", ")
	var/adminwho = admins_online.Join(", ")
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)

	var/special_columns = list(
		"bantime" = "NOW()",
		"server_ip" = "INET_ATON(?)",
		"ip" = "INET_ATON(?)",
		"a_ip" = "INET_ATON(?)",
		"expiration_time" = "IF(? IS NULL, NULL, NOW() + INTERVAL ? [interval])"
	)
	var/sql_ban = list()
	for(var/role in roles_to_ban)
		sql_ban += list(list(
			"server_ip" = world.internet_address || 0,
			"server_port" = world.port,
			"round_id" = GLOB.round_id,
			"role" = role,
			"expiration_time" = duration,
			"applies_to_admins" = applies_to_admins,
			"reason" = reason,
			"ckey" = player_ckey || null,
			"ip" = player_ip || null,
			"computerid" = player_cid || null,
			"a_ckey" = admin_ckey,
			"a_ip" = admin_ip || null,
			"a_computerid" = admin_cid,
			"who" = who,
			"adminwho" = adminwho,
		))
	if(!SSdbcore.MassInsert(format_table_name("ban"), sql_ban, warn = TRUE, special_columns = special_columns))
		return
	var/target = ban_target_string(player_key, player_ip, player_cid)
	var/msg = "has created a [isnull(duration) ? "permanent" : "temporary [time_message]"] [applies_to_admins ? "admin " : ""][is_server_ban ? "server ban" : "role ban from [roles_to_ban.len] roles"] for [target]."
	log_admin_private("[kn] [msg][is_server_ban ? "" : " Roles: [roles_to_ban.Join(", ")]"] Reason: [reason]")
	message_admins("[kna] [msg][is_server_ban ? "" : " Roles: [roles_to_ban.Join("\n")]"]\nReason: [reason]")
	if(applies_to_admins)
		send2adminchat("BAN ALERT","[kn] [msg]")
	if(player_ckey)
		create_message("note", player_ckey, admin_ckey, note_reason, null, null, 0, 0, null, 0, severity)

	var/player_ban_notification = span_boldannounce("You have been [applies_to_admins ? "admin " : ""]banned by [usr.client.key] from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"].\nReason: [reason]
[span_danger("This ban is [isnull(duration) ? "permanent." : "temporary, it will be removed in [time_message]."] The round ID is [GLOB.round_id].")]")
	var/other_ban_notification = span_boldannounce("Another player sharing your IP or CID has been banned by [usr.client.key] from [is_server_ban ? "the server" : " Roles: [roles_to_ban.Join(", ")]"].\nReason: [reason]
[span_danger("This ban is [isnull(duration) ? "permanent." : "temporary, it will be removed in [time_message]."] The round ID is [GLOB.round_id].")]")

	notify_all_banned_players(player_ckey, player_ip, player_cid, player_ban_notification, other_ban_notification, is_server_ban, applies_to_admins)

	var/datum/admin_help/linked_ahelp_ticket = admin_ticket_log(player_ckey, "[kna] [msg]")

	if(is_server_ban && linked_ahelp_ticket)
		linked_ahelp_ticket.Resolve()

/datum/admins/proc/unban_panel(player_key, admin_key, player_ip, player_cid, page = 0)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/datum/browser/unban_panel = new(usr, "unbanpanel", "Unbanning Panel", 850, 600)
	unban_panel.add_stylesheet("unbanpanelcss", 'html/admin/unbanpanel.css')
	var/list/output = list("
")
	output += {"<form method='get' action='?src=[REF(src)]'>[HrefTokenFormField()]
	<input type='hidden' name='src' value='[REF(src)]'>
	Key:<input type='text' name='searchunbankey' size='18' value='[player_key]'>
	Admin Key:<input type='text' name='searchunbanadminkey' size='18' value='[admin_key]'>
	IP:<input type='text' name='searchunbanip' size='12' value='[player_ip]'>
	CID:<input type='text' name='searchunbancid' size='10' value='[player_cid]'>
	<input type='submit' value='Search'>
	</form>

	"}
	if(player_key || admin_key || player_ip || player_cid)
		var/bancount = 0
		var/bansperpage = 10
		page = text2num(page)
		var/datum/db_query/query_unban_count_bans = SSdbcore.NewQuery({"
			SELECT COUNT(id)
			FROM [format_table_name("ban")]
			WHERE
				(:player_key IS NULL OR ckey = :player_key) AND
				(:admin_key IS NULL OR a_ckey = :admin_key) AND
				(:player_ip IS NULL OR ip = INET_ATON(:player_ip)) AND
				(:player_cid IS NULL OR computerid = :player_cid)
		"}, list(
			"player_key" = ckey(player_key),
			"admin_key" = ckey(admin_key),
			"player_ip" = player_ip || null,
			"player_cid" = player_cid || null,
		))
		if(!query_unban_count_bans.warn_execute())
			qdel(query_unban_count_bans)
			return
		if(query_unban_count_bans.NextRow())
			bancount = text2num(query_unban_count_bans.item[1])
		qdel(query_unban_count_bans)
		if(bancount > bansperpage)
			output += "Page: "
			var/pagecount = 1
			var/list/pagelist = list()
			while(bancount > 0)
				pagelist += "[pagecount == page ? "\[[pagecount]\]" : "\[[pagecount]\]"]"
				bancount -= bansperpage
				pagecount++
			output += pagelist.Join(" | ")
		var/datum/db_query/query_unban_search_bans = SSdbcore.NewQuery({"
			SELECT
				id,
				bantime,
				round_id,
				role,
				expiration_time,
				TIMESTAMPDIFF(MINUTE, bantime, expiration_time),
				IF(expiration_time < NOW(), 1, NULL),
				applies_to_admins,
				reason,
				IFNULL((
					SELECT byond_key
					FROM [format_table_name("player")]
					WHERE [format_table_name("player")].ckey = [format_table_name("ban")].ckey
				), ckey),
				INET_NTOA(ip),
				computerid,
				IFNULL((
					SELECT byond_key
					FROM [format_table_name("player")]
					WHERE [format_table_name("player")].ckey = [format_table_name("ban")].a_ckey
				), a_ckey),
				IF(edits IS NOT NULL, 1, NULL),
				unbanned_datetime,
				IFNULL((
					SELECT byond_key
					FROM [format_table_name("player")]
					WHERE [format_table_name("player")].ckey = [format_table_name("ban")].unbanned_ckey
				), unbanned_ckey),
				unbanned_round_id
			FROM [format_table_name("ban")]
			WHERE
				(:player_key IS NULL OR ckey = :player_key) AND
				(:admin_key IS NULL OR a_ckey = :admin_key) AND
				(:player_ip IS NULL OR ip = INET_ATON(:player_ip)) AND
				(:player_cid IS NULL OR computerid = :player_cid)
			ORDER BY id DESC
			LIMIT :skip, :take
		"}, list(
			"player_key" = ckey(player_key),
			"admin_key" = ckey(admin_key),
			"player_ip" = player_ip || null,
			"player_cid" = player_cid || null,
			"skip" = bansperpage * page,
			"take" = bansperpage,
		))
		if(!query_unban_search_bans.warn_execute())
			qdel(query_unban_search_bans)
			return
		while(query_unban_search_bans.NextRow())
			var/ban_id = query_unban_search_bans.item[1]
			var/ban_datetime = query_unban_search_bans.item[2]
			var/ban_round_id  = query_unban_search_bans.item[3]
			var/role = query_unban_search_bans.item[4]
			var/expiration_time = query_unban_search_bans.item[5]
			//we don't cast duration as num because if the duration is large enough to be converted to scientific notation by byond then the + character gets lost when passed through href causing SQL to interpret '4.321e 007' as '4'
			var/duration = query_unban_search_bans.item[6]
			var/expired = query_unban_search_bans.item[7]
			var/applies_to_admins = text2num(query_unban_search_bans.item[8])
			var/reason = query_unban_search_bans.item[9]
			var/banned_player_key = query_unban_search_bans.item[10]
			var/banned_player_ip = query_unban_search_bans.item[11]
			var/banned_player_cid = query_unban_search_bans.item[12]
			var/banning_admin_key = query_unban_search_bans.item[13]
			var/edits = query_unban_search_bans.item[14]
			var/unban_datetime = query_unban_search_bans.item[15]
			var/unban_key = query_unban_search_bans.item[16]
			var/unban_round_id = query_unban_search_bans.item[17]
			var/target = ban_target_string(banned_player_key, banned_player_ip, banned_player_cid)

			output += "

[target][applies_to_admins ? " ADMIN" : ""] banned by [banning_admin_key] from [role] on [ban_datetime] during round #[ban_round_id].
"
			if(!expiration_time)
				output += "Permanent ban."
			else
				output += "Duration of [DisplayTimeText(text2num(duration) MINUTES)], [expired ? "expired" : "expires"] on [expiration_time]."
			if(unban_datetime)
				output += "
Unbanned by [unban_key] on [unban_datetime] during round #[unban_round_id]."
			output += "

[reason]

"

			var/un_or_reban_href
			if(unban_datetime)
				un_or_reban_href = "Reban"
			else
				un_or_reban_href = "Unban"
			output += "Edit
[un_or_reban_href]"

			if(edits)
				output += "
Edit log"
			output += "

"
		qdel(query_unban_search_bans)
		output += "
"
	unban_panel.set_content(jointext(output, ""))
	unban_panel.open()

/datum/admins/proc/unban(ban_id, player_key, player_ip, player_cid, role, page, admin_key)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/target = ban_target_string(player_key, player_ip, player_cid)
	// Make sure the only input that doesn't early return is "Yes" - This is the only situation in which we want the unban to proceed.
	if(tgui_alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", list("Yes", "No")) != "Yes")
		return
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/change_message = "[usr.client.key] unbanned [target] from [role] on [SQLtime()] during round #[GLOB.round_id]
"
	var/datum/db_query/query_unban = SSdbcore.NewQuery({"
		UPDATE [format_table_name("ban")] SET
			unbanned_datetime = NOW(),
			unbanned_ckey = :admin_ckey,
			unbanned_ip = INET_ATON(:admin_ip),
			unbanned_computerid = :admin_cid,
			unbanned_round_id = :round_id,
			edits = CONCAT(IFNULL(edits,''), :change_message)
		WHERE id = :ban_id
	"}, list("ban_id" = ban_id, "admin_ckey" = usr.client.ckey, "admin_ip" = usr.client.address, "admin_cid" = usr.client.computer_id, "round_id" = GLOB.round_id, "change_message" = change_message))
	if(!query_unban.warn_execute())
		qdel(query_unban)
		return
	qdel(query_unban)
	log_admin_private("[kn] has unbanned [target] from [role].")
	message_admins("[kna] has unbanned [target] from [role].")
	var/client/C = GLOB.directory[player_key]
	if(C)
		build_ban_cache(C)
		to_chat(C, span_boldannounce("[usr.client.key] has removed a ban from [role] for your key."), confidential = TRUE)
	for(var/client/i in GLOB.clients - C)
		if(i.address == player_ip || i.computer_id == player_cid)
			build_ban_cache(i)
			to_chat(i, span_boldannounce("[usr.client.key] has removed a ban from [role] for your IP or CID."), confidential = TRUE)
	unban_panel(player_key, admin_key, player_ip, player_cid, page)

/// Sometimes an admin did not intend to unban a player. This proc undoes an unbanning operation by setting the unbanned_ keys in the DB back to null.
/datum/admins/proc/reban(ban_id, applies_to_admins, player_key, player_ip, player_cid, role, page, admin_key)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return

	var/target = ban_target_string(player_key, player_ip, player_cid)
	// Make sure the only input that doesn't early return is "Yes" - This is the only situation in which we want the unban to proceed.
	if(tgui_alert(usr, "Please confirm undoing of unban of [target] from [role].", "Reban confirmation", list("Yes", "No")) != "Yes")
		return

	if(applies_to_admins && !can_place_additional_admin_ban(usr.client.ckey))
		return

	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/change_message = "[usr.client.key] re-activated ban of [target] from [role] on [SQLtime()] during round #[GLOB.round_id]
"
	var/datum/db_query/query_reban = SSdbcore.NewQuery({"
		UPDATE [format_table_name("ban")] SET
			unbanned_datetime = NULL,
			unbanned_ckey = NULL,
			unbanned_ip = NULL,
			unbanned_computerid = NULL,
			unbanned_round_id = NULL,
			edits = CONCAT(IFNULL(edits,''), :change_message)
		WHERE id = :ban_id
	"}, list("change_message" = change_message, "ban_id" = ban_id))
	if(!query_reban.warn_execute())
		qdel(query_reban)
		return
	qdel(query_reban)
	log_admin_private("[kn] has rebanned [target] from [role].")
	message_admins("[kna] has rebanned [target] from [role].")

	var/banned_player_message = span_boldannounce("[usr.client.key] has re-activated a removed ban from [role] for your key.")
	var/banned_other_message = span_boldannounce("[usr.client.key] has re-activated a removed ban from [role] for your IP or CID.")
	var/kick_banned_players = (role == "Server")

	notify_all_banned_players(ckey(player_key), player_ip, player_cid, banned_player_message, banned_other_message, kick_banned_players, applies_to_admins)
	unban_panel(player_key, admin_key, player_ip, player_cid, page)

/datum/admins/proc/edit_ban(ban_id, player_key, ip_check, player_ip, cid_check, player_cid, use_last_connection, applies_to_admins, duration, interval, reason, mirror_edit, old_key, old_ip, old_cid, old_applies, admin_key, page, list/changes, is_server_ban)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/player_ckey = ckey(player_key)
	var/bantime
	if(player_ckey)
		var/datum/db_query/query_edit_ban_get_player = SSdbcore.NewQuery({"
			SELECT
				byond_key,
				(SELECT bantime FROM [format_table_name("ban")] WHERE id = :ban_id),
				ip,
				computerid
			FROM [format_table_name("player")]
			WHERE ckey = :player_ckey
		"}, list("player_ckey" = player_ckey, "ban_id" = ban_id))
		if(!query_edit_ban_get_player.warn_execute())
			qdel(query_edit_ban_get_player)
			return
		if(query_edit_ban_get_player.NextRow())
			player_key = query_edit_ban_get_player.item[1]
			bantime = query_edit_ban_get_player.item[2]
			if(use_last_connection)
				if(ip_check)
					player_ip = query_edit_ban_get_player.item[3]
				if(cid_check)
					player_cid = query_edit_ban_get_player.item[4]
		else
			if(use_last_connection)
				if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, unable to use IP and CID from last connection. Are you sure you want to edit a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
					qdel(query_edit_ban_get_player)
					return
			else
				if(tgui_alert(usr, "[player_key]/([player_ckey]) has not been seen before, are you sure you want to edit a ban for them?", "Unknown key", list("Yes", "No", "Cancel")) != "Yes")
					qdel(query_edit_ban_get_player)
					return
		qdel(query_edit_ban_get_player)

	if(applies_to_admins && (applies_to_admins != old_applies) && !can_place_additional_admin_ban(usr.client.ckey))
		return

	if (!(interval in list("SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR")))
		interval = "MINUTE"

	var/list/changes_text = list()
	var/list/changes_keys = list()
	for(var/i in changes)
		changes_text += "[i]: [changes[i]]"
		changes_keys += i
	var/change_message = "[usr.client.key] edited the following [jointext(changes_text, ", ")]
"

	var/list/arguments = list(
		"duration" = duration || null,
		"reason" = reason,
		"applies_to_admins" = applies_to_admins,
		"ckey" = player_ckey || null,
		"ip" = player_ip || null,
		"cid" = player_cid || null,
		"change_message" = change_message,
	)
	var/where
	if(text2num(mirror_edit))
		var/list/wherelist = list("bantime = '[bantime]'")
		if(old_key)
			wherelist += "ckey = :old_ckey"
			arguments["old_ckey"] = ckey(old_key)
		if(old_ip)
			wherelist += "ip = INET_ATON(:old_ip)"
			arguments["old_ip"] = old_ip || null
		if(old_cid)
			wherelist += "computerid = :old_cid"
			arguments["old_cid"] = old_cid
		where = wherelist.Join(" AND ")
	else
		where = "id = :ban_id"
		arguments["ban_id"] = ban_id

	var/datum/db_query/query_edit_ban = SSdbcore.NewQuery({"
		UPDATE [format_table_name("ban")]
		SET
			expiration_time = IF(:duration IS NULL, NULL, bantime + INTERVAL :duration [interval]),
			applies_to_admins = :applies_to_admins,
			reason = :reason,
			ckey = :ckey,
			ip = INET_ATON(:ip),
			computerid = :cid,
			edits = CONCAT(IFNULL(edits,''), :change_message)
		WHERE [where]
	"}, arguments)
	if(!query_edit_ban.warn_execute())
		qdel(query_edit_ban)
		return
	qdel(query_edit_ban)

	var/changes_keys_text = jointext(changes_keys, ", ")
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	log_admin_private("[kn] has edited the [changes_keys_text] of a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"].") //if a ban doesn't have a key it must have an ip and/or a cid to have reached this point normally
	message_admins("[kna] has edited the [changes_keys_text] of a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"].")
	if(changes["Applies to admins"])
		send2adminchat("BAN ALERT","[kn] has edited a ban for [old_key ? "[old_key]" : "[old_ip]-[old_cid]"] to [applies_to_admins ? "" : "not"]affect admins")

	var/player_edit_message = span_boldannounce("[usr.client.key] has edited the [changes_keys_text] of a ban for your key.")
	var/other_edit_message = span_boldannounce("[usr.client.key] has edited the [changes_keys_text] of a ban for your IP or CID.")

	var/kick_banned_players = (is_server_ban && (changes["Key"] || changes["IP"] || changes["CID"]))

	notify_all_banned_players(player_ckey, player_ip, player_cid, player_edit_message, other_edit_message, kick_banned_players, applies_to_admins)

	unban_panel(player_key, null, null, null, page)

/datum/admins/proc/ban_log(ban_id)
	if(!check_rights(R_BAN))
		return
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/datum/db_query/query_get_ban_edits = SSdbcore.NewQuery({"
		SELECT edits FROM [format_table_name("ban")] WHERE id = :ban_id
	"}, list("ban_id" = ban_id))
	if(!query_get_ban_edits.warn_execute())
		qdel(query_get_ban_edits)
		return
	if(query_get_ban_edits.NextRow())
		var/edits = query_get_ban_edits.item[1]
		var/datum/browser/edit_log = new(usr, "baneditlog", "Ban edit log")
		edit_log.set_content(edits)
		edit_log.open()
	qdel(query_get_ban_edits)

/datum/admins/proc/ban_target_string(player_key, player_ip, player_cid)
	. = list()
	if(player_key)
		. += player_key
	else
		if(player_ip)
			. += player_ip
		else
			. += "NULL"
		if(player_cid)
			. += player_cid
		else
			. += "NULL"
	. = jointext(., "/")

/**
 * Checks if the admin can place an additional admin ban.
 *
 * Returns FALSE if the query fails to execute.
 * Returns FALSE and notifies the admin in chat if they are at their max number of admin bans already.
 * Returns TRUE if an admin can place an additional admin ban.
 *
 * Arguments:
 * * admin_ckey - The ckey of the admin who is trying to place an admin ban.
 */
/datum/admins/proc/can_place_additional_admin_ban(admin_ckey)
	var/datum/db_query/query_check_adminban_count = SSdbcore.NewQuery({"
		SELECT COUNT(DISTINCT bantime)
		FROM [format_table_name("ban")]
		WHERE
			a_ckey = :admin_ckey AND
			applies_to_admins = 1 AND
			unbanned_datetime IS NULL AND
			(expiration_time IS NULL OR expiration_time > NOW())
	"}, list("admin_ckey" = admin_ckey))
	if(!query_check_adminban_count.warn_execute()) //count distinct bantime to treat rolebans made at the same time as one ban
		qdel(query_check_adminban_count)
		return FALSE
	if(query_check_adminban_count.NextRow())
		var/adminban_count = text2num(query_check_adminban_count.item[1])
		var/max_adminbans = MAX_ADMINBANS_PER_ADMIN
		if(check_rights(R_PERMISSIONS, show_msg = FALSE) && (rank.can_edit_rights & R_EVERYTHING) == R_EVERYTHING) //edit rights are a more effective way to check hierarchical rank since many non-headmins have R_PERMISSIONS now
			max_adminbans = MAX_ADMINBANS_PER_HEADMIN
		if(adminban_count >= max_adminbans)
			to_chat(usr, span_danger("You've already logged [max_adminbans] admin ban(s) or more. Do not abuse this function!"), confidential = TRUE)
			qdel(query_check_adminban_count)
			return FALSE
	qdel(query_check_adminban_count)
	return TRUE

/**
 * Notifies all banned players about their ban and gives them a link to appeal from the config. If it was a server ban, it also kicks them.
 *
 * If the banned player's ckey has a linked client on the server, it notifies them of the ban details and kicks them if it was a server ban.
 * If there is anyone else sharing the banned player's IP or CID, it notifies them of the ban details and kicks them if it was a server ban.
 *
 * Builds the ban cache for every client impacted by the ban.
 *
 * Arguments:
 * * banned_player_ckey - The ckey of the banned player.
 * * banned_player_ip - IP address of the banned player.
 * * banned_player_cid - CID of the banned player.
 * * banned_player_message - The message to show to only the specifically banned player.
 * * banned_other_message - The message to show to any other players who share the banned player's IP or CID.
 * * kick_banned_players - TRUE if we want to kick affected players, FALSE otherwise. This should generally only be TRUE for server bans.
 * * applies_to_admins - TRUE if this ban applies to admins and we may need to kick them, FALSE otherwise.
 */
/datum/admins/proc/notify_all_banned_players(banned_player_ckey, banned_player_ip, banned_player_cid, banned_player_message, banned_other_message, kick_banned_players, applies_to_admins)
	var/client/player_client = GLOB.directory[banned_player_ckey]

	var/appeal_url = "No ban appeal url set!"
	appeal_url = CONFIG_GET(string/banappeals)

	var/is_admin = FALSE
	if(player_client)
		build_ban_cache(player_client)
		to_chat(player_client, span_boldannounce("[banned_player_message]
To appeal this ban go to [appeal_url]"), confidential = TRUE)
		to_chat(player_client, "<video controls width=\"250\" autoplay ><source src=\"https://www.tgstation13.download/byond/ban.mp4\" type=\"video/mp4\">Sorry, your browser does not support embedded videos</video>");
		if(GLOB.admin_datums[player_client.ckey] || GLOB.deadmins[player_client.ckey])
			is_admin = TRUE
		if(kick_banned_players && (!is_admin || (is_admin && applies_to_admins)))
			qdel(player_client)

	for(var/client/other_player_client in GLOB.clients - player_client)
		if(other_player_client.address == banned_player_ip || other_player_client.computer_id == banned_player_cid)
			build_ban_cache(other_player_client)
			to_chat(other_player_client, span_boldannounce("[banned_other_message]
To appeal this ban go to [appeal_url]"), confidential = TRUE)
			to_chat(other_player_client, "<video controls width=\"250\" autoplay ><source src=\"https://www.tgstation13.download/byond/ban.mp4\" type=\"video/mp4\">Sorry, your browser does not support embedded videos</video>");
			if(GLOB.admin_datums[other_player_client.ckey] || GLOB.deadmins[other_player_client.ckey])
				is_admin = TRUE
			if(kick_banned_players && (!is_admin || (is_admin && applies_to_admins)))
				qdel(other_player_client)


code:
/proc/create_message(type, target_key, admin_ckey, text, timestamp, server, secret, logged = 1, browse, expiry, note_severity)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	if(!type)
		return
	var/target_ckey = ckey(target_key)
	if(!target_key && (type == "note" || type == "message" || type == "watchlist entry"))
		var/new_key = input(usr,"Who would you like to create a [type] for?","Enter a key or ckey",null) as null|text
		if(!new_key)
			return
		var/new_ckey = ckey(new_key)
		var/datum/db_query/query_find_ckey = SSdbcore.NewQuery(
			"SELECT ckey FROM [format_table_name("player")] WHERE ckey = :ckey",
			list("ckey" = new_ckey)
		)
		if(!query_find_ckey.warn_execute())
			qdel(query_find_ckey)
			return
		if(!query_find_ckey.NextRow())
			if(tgui_alert(usr, "[new_key]/([new_ckey]) has not been seen before, are you sure you want to create a [type] for them?", "Unknown ckey", list("Yes", "No", "Cancel")) != "Yes")
				qdel(query_find_ckey)
				return
		qdel(query_find_ckey)
		target_ckey = new_ckey
		target_key = new_key
	if(QDELETED(usr))
		return
	if(!target_key)
		target_key = target_ckey
	if(!admin_ckey)
		admin_ckey = usr.ckey
		if(!admin_ckey)
			return
	if(!target_ckey)
		target_ckey = admin_ckey
	if(!text)
		text = input(usr,"Write your [type]","Create [type]") as null|message
		if(!text)
			return
	if(!server)
		var/ssqlname = CONFIG_GET(string/serversqlname)
		if (ssqlname)
			server = ssqlname
	if(isnull(secret))
		switch(tgui_alert(usr,"Hide note from being viewed by players?", "Secret note?",list("Yes","No","Cancel")))
			if("Yes")
				secret = 1
			if("No")
				secret = 0
			else
				return
	if(isnull(expiry))
		if(tgui_alert(usr, "Set an expiry time? Expired messages are hidden like deleted ones.", "Expiry time?", list("Yes", "No", "Cancel")) == "Yes")
			var/expire_time = input("Set expiry time for [type] 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 current time for obvious reasons.", "Set expiry time", SQLtime()) as null|text
			if(!expire_time)
				return
			var/datum/db_query/query_validate_expire_time = SSdbcore.NewQuery(
				"SELECT IF(STR_TO_DATE(:expire_time,'%Y-%c-%d %T') > NOW(), STR_TO_DATE(:expire_time,'%Y-%c-%d %T'), 0)",
				list("expire_time" = expire_time)
			)
			if(!query_validate_expire_time.warn_execute())
				qdel(query_validate_expire_time)
				return
			if(query_validate_expire_time.NextRow())
				var/checktime = text2num(query_validate_expire_time.item[1])
				if(!checktime)
					to_chat(usr, "Datetime entered is improperly formatted or not later than current server time.", confidential = TRUE)
					qdel(query_validate_expire_time)
					return
				expiry = query_validate_expire_time.item[1]
			qdel(query_validate_expire_time)
	if(type == "note" && isnull(note_severity))
		note_severity = input("Set the severity of the note.", "Severity", null, null) as null|anything in list("High", "Medium", "Minor", "None")
		if(!note_severity)
			return
	var/list/parameters = list(
		"type" = type,
		"target_ckey" = target_ckey,
		"admin_ckey" = admin_ckey,
		"text" = text,
		"server" = server,
		"internet_address" = world.internet_address || "0",
		"port" = "[world.port]",
		"round_id" = GLOB.round_id,
		"secret" = secret,
		"expiry" = expiry || null,
		"note_severity" = note_severity,
	)
	if(timestamp)
		parameters["timestamp"] = timestamp
	var/datum/db_query/query_create_message = SSdbcore.NewQuery({"
		INSERT INTO [format_table_name("messages")] (type, targetckey, adminckey, text, timestamp, server, server_ip, server_port, round_id, secret, expire_timestamp, severity, playtime)
		VALUES (:type, :target_ckey, :admin_ckey, :text, [timestamp? ":timestamp" : "Now()"], :server, INET_ATON(:internet_address), :port, :round_id, :secret, :expiry, :note_severity, (SELECT `minutes` FROM [format_table_name("role_time")] WHERE `ckey` = :target_ckey AND `job` = 'Living'))
	"}, parameters)
	var/pm = "[key_name(usr)] has created a [type][(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""]: [text]"
	var/header = "[key_name_admin(usr)] has created a [type][(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""]"
	if(!query_create_message.warn_execute())
		qdel(query_create_message)
		return
	qdel(query_create_message)
	if(logged)
		log_admin_private(pm)
		message_admins("[header]:
[text]")
		admin_ticket_log(target_ckey, "[header]")
		admin_ticket_log(target_ckey, text)
		if(browse)
			browse_messages("[type]")
		else
			browse_messages(target_ckey = target_ckey, agegate = TRUE)

/proc/delete_message(message_id, logged = 1, browse)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	message_id = text2num(message_id)
	if(!message_id)
		return
	var/type
	var/target_key
	var/text
	var/user_key_name = key_name(usr)
	var/user_name_admin = key_name_admin(usr)
	var/deleted_by_ckey = usr.ckey
	var/datum/db_query/query_find_del_message = SSdbcore.NewQuery(
		"SELECT type, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey), text FROM [format_table_name("messages")] WHERE id = :id AND deleted = 0",
		list("id" = message_id)
	)
	if(!query_find_del_message.warn_execute())
		qdel(query_find_del_message)
		return
	if(query_find_del_message.NextRow())
		type = query_find_del_message.item[1]
		target_key = query_find_del_message.item[2]
		text = query_find_del_message.item[3]
	qdel(query_find_del_message)
	var/datum/db_query/query_del_message = SSdbcore.NewQuery(
		"UPDATE [format_table_name("messages")] SET deleted = 1, deleted_ckey = :deleted_ckey WHERE id = :id",
		list("deleted_ckey" = deleted_by_ckey, "id" = message_id)
	)
	if(!query_del_message.warn_execute())
		qdel(query_del_message)
		return
	qdel(query_del_message)
	if(logged)
		var/m1 = "[user_key_name] has deleted a [type][(type == "note" || type == "message" || type == "watchlist entry") ? " for" : " made by"] [target_key]: [text]"
		var/m2 = "[user_name_admin] has deleted a [type][(type == "note" || type == "message" || type == "watchlist entry") ? " for" : " made by"] [target_key]:
[text]"
		log_admin_private(m1)
		message_admins(m2)
		if(browse)
			browse_messages("[type]")
		else
			browse_messages(target_ckey = ckey(target_key), agegate = TRUE)

/proc/edit_message(message_id, browse)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	message_id = text2num(message_id)
	if(!message_id)
		return
	var/editor_ckey = usr.ckey
	var/editor_key = usr.key
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/datum/db_query/query_find_edit_message = SSdbcore.NewQuery({"
		SELECT
			type,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), targetckey),
			text
		FROM [format_table_name("messages")]
		WHERE id = :id AND deleted = 0
	"}, list("id" = message_id))
	if(!query_find_edit_message.warn_execute())
		qdel(query_find_edit_message)
		return
	if(query_find_edit_message.NextRow())
		var/type = query_find_edit_message.item[1]
		var/target_key = query_find_edit_message.item[2]
		var/admin_key = query_find_edit_message.item[3]
		var/old_text = query_find_edit_message.item[4]
		var/new_text = input("Input new [type]", "New [type]", "[old_text]") as null|message
		if(!new_text)
			qdel(query_find_edit_message)
			return
		var/edit_text = "Edited by [editor_key] on [SQLtime()] from
[old_text]
to
[new_text]
"
		var/datum/db_query/query_edit_message = SSdbcore.NewQuery({"
			UPDATE [format_table_name("messages")]
			SET text = :text, lasteditor = :lasteditor, edits = CONCAT(IFNULL(edits,''),:edit_text)
			WHERE id = :id AND deleted = 0
		"}, list("text" = new_text, "lasteditor" = editor_ckey, "edit_text" = edit_text, "id" = message_id))
		if(!query_edit_message.warn_execute())
			qdel(query_edit_message)
			return
		qdel(query_edit_message)
		log_admin_private("[kn] has edited a [type] [(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""] made by [admin_key] from [old_text] to [new_text]")
		message_admins("[kna] has edited a [type] [(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""] made by [admin_key] from
[old_text]
to
[new_text]")
		if(browse)
			browse_messages("[type]")
		else
			browse_messages(target_ckey = ckey(target_key), agegate = TRUE)
	qdel(query_find_edit_message)

/proc/edit_message_expiry(message_id, browse)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	message_id = text2num(message_id)
	if(!message_id)
		return
	var/editor_ckey = usr.ckey
	var/editor_key = usr.key
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/datum/db_query/query_find_edit_expiry_message = SSdbcore.NewQuery({"
		SELECT
			type,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
			expire_timestamp
		FROM [format_table_name("messages")]
		WHERE id = :id AND deleted = 0
	"}, list("id" = message_id))
	if(!query_find_edit_expiry_message.warn_execute())
		qdel(query_find_edit_expiry_message)
		return
	if(query_find_edit_expiry_message.NextRow())
		var/type = query_find_edit_expiry_message.item[1]
		var/target_key = query_find_edit_expiry_message.item[2]
		var/admin_key = query_find_edit_expiry_message.item[3]
		var/old_expiry = query_find_edit_expiry_message.item[4]
		var/new_expiry
		var/expire_time = input("Set expiry time for [type] 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 current time for obvious reasons. Enter -1 to remove expiry time.", "Set expiry time", old_expiry) as null|text
		if(!expire_time)
			qdel(query_find_edit_expiry_message)
			return
		if(expire_time == "-1")
			new_expiry = "non-expiring"
		else
			var/datum/db_query/query_validate_expire_time_edit = SSdbcore.NewQuery({"
				SELECT IF(STR_TO_DATE(:expire_time,'%Y-%c-%d %T') > NOW(), STR_TO_DATE(:expire_time,'%Y-%c-%d %T'), 0)
			"}, list("expire_time" = expire_time))
			if(!query_validate_expire_time_edit.warn_execute())
				qdel(query_validate_expire_time_edit)
				qdel(query_find_edit_expiry_message)
				return
			if(query_validate_expire_time_edit.NextRow())
				var/checktime = text2num(query_validate_expire_time_edit.item[1])
				if(!checktime)
					to_chat(usr, "Datetime entered is improperly formatted or not later than current server time.", confidential = TRUE)
					qdel(query_validate_expire_time_edit)
					qdel(query_find_edit_expiry_message)
					return
				new_expiry = query_validate_expire_time_edit.item[1]
			qdel(query_validate_expire_time_edit)
		var/edit_text = "Expiration time edited by [editor_key] on [SQLtime()] from [old_expiry] to [new_expiry]
"
		var/datum/db_query/query_edit_message_expiry = SSdbcore.NewQuery({"
			UPDATE [format_table_name("messages")]
			SET expire_timestamp = :expire_time, lasteditor = :lasteditor, edits = CONCAT(IFNULL(edits,''),:edit_text)
			WHERE id = :id AND deleted = 0
		"}, list("expire_time" = (expire_time == "-1" ? null : new_expiry), "lasteditor" = editor_ckey, "edit_text" = edit_text, "id" = message_id))
		if(!query_edit_message_expiry.warn_execute())
			qdel(query_edit_message_expiry)
			qdel(query_find_edit_expiry_message)
			return
		qdel(query_edit_message_expiry)
		log_admin_private("[kn] has edited the expiration time of a [type] [(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""] made by [admin_key] from [old_expiry] to [new_expiry]")
		message_admins("[kna] has edited the expiration time of a [type] [(type == "note" || type == "message" || type == "watchlist entry") ? " for [target_key]" : ""] made by [admin_key] from [old_expiry] to [new_expiry]")
		if(browse)
			browse_messages("[type]")
		else
			browse_messages(target_ckey = ckey(target_key), agegate = TRUE)
	qdel(query_find_edit_expiry_message)

/proc/edit_message_severity(message_id)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	message_id = text2num(message_id)
	if(!message_id)
		return
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/datum/db_query/query_find_edit_note_severity = SSdbcore.NewQuery({"
		SELECT
			type,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
			severity
		FROM [format_table_name("messages")]
		WHERE id = :id AND deleted = 0
	"}, list("id" = message_id))
	if(!query_find_edit_note_severity.warn_execute())
		qdel(query_find_edit_note_severity)
		return
	if(query_find_edit_note_severity.NextRow())
		var/type = query_find_edit_note_severity.item[1]
		var/target_key = query_find_edit_note_severity.item[2]
		var/admin_key = query_find_edit_note_severity.item[3]
		var/old_severity = query_find_edit_note_severity.item[4]
		if(!old_severity)
			old_severity = "NA"
		var/editor_key = usr.key
		var/editor_ckey = usr.ckey
		var/new_severity = input("Set the severity of the note.", "Severity", null, null) as null|anything in list("high", "medium", "minor", "none") //lowercase for edit log consistency
		if(!new_severity)
			qdel(query_find_edit_note_severity)
			return
		new_severity = new_severity
		var/edit_text = "Note severity edited by [editor_key] on [SQLtime()] from [old_severity] to [new_severity]
"
		var/datum/db_query/query_edit_note_severity = SSdbcore.NewQuery({"
			UPDATE [format_table_name("messages")]
			SET severity = :severity, lasteditor = :lasteditor, edits = CONCAT(IFNULL(edits,''),:edit_text)
			WHERE id = :id AND deleted = 0
		"}, list("severity" = new_severity, "lasteditor" = editor_ckey, "edit_text" = edit_text, "id" = message_id))
		if(!query_edit_note_severity.warn_execute(async = TRUE))
			qdel(query_edit_note_severity)
			qdel(qdel(query_find_edit_note_severity))
			return
		qdel(query_edit_note_severity)
		log_admin_private("[kn] has edited the severity of a [type] for [target_key] made by [admin_key] from [old_severity] to [new_severity]")
		message_admins("[kna] has edited the severity time of a [type] for [target_key] made by [admin_key] from [old_severity] to [new_severity]")
		browse_messages(target_ckey = ckey(target_key), agegate = TRUE)
	qdel(query_find_edit_note_severity)

/proc/toggle_message_secrecy(message_id)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	message_id = text2num(message_id)
	if(!message_id)
		return
	var/editor_ckey = usr.ckey
	var/editor_key = usr.key
	var/kn = key_name(usr)
	var/kna = key_name_admin(usr)
	var/datum/db_query/query_find_message_secret = SSdbcore.NewQuery({"
		SELECT
			type,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), targetckey),
			secret
		FROM [format_table_name("messages")]
		WHERE id = :id AND deleted = 0
	"}, list("id" = message_id))
	if(!query_find_message_secret.warn_execute())
		qdel(query_find_message_secret)
		return
	if(query_find_message_secret.NextRow())
		var/type = query_find_message_secret.item[1]
		var/target_key = query_find_message_secret.item[2]
		var/admin_key = query_find_message_secret.item[3]
		var/secret = text2num(query_find_message_secret.item[4])
		var/edit_text = "Made [secret ? "not secret" : "secret"] by [editor_key] on [SQLtime()]
"
		var/datum/db_query/query_message_secret = SSdbcore.NewQuery({"
			UPDATE [format_table_name("messages")]
			SET secret = NOT secret, lasteditor = :lasteditor, edits = CONCAT(IFNULL(edits,''),:edit_text)
			WHERE id = :id
		"}, list("lasteditor" = editor_ckey, "edit_text" = edit_text, "id" = message_id))
		if(!query_message_secret.warn_execute())
			qdel(query_find_message_secret)
			qdel(query_message_secret)
			return
		qdel(query_message_secret)
		log_admin_private("[kn] has toggled [target_key]'s [type] made by [admin_key] to [secret ? "not secret" : "secret"]")
		message_admins("[kna] has toggled [target_key]'s [type] made by [admin_key] to [secret ? "not secret" : "secret"]")
		browse_messages(target_ckey = ckey(target_key), agegate = TRUE)
	qdel(query_find_message_secret)

/proc/browse_messages(type, target_ckey, index, linkless = FALSE, filter, agegate = FALSE)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	var/list/output = list()
	var/ruler = "
"
	var/list/navbar = list("All#")
	for(var/letter in GLOB.alphabet)
		navbar += "[letter]"
	navbar += "MemosWatchlist"
	navbar += "
<form method='GET' name='search' action='?'>\
	<input type='hidden' name='_src_' value='holder'>\
	[HrefTokenFormField()]\
	<input type='text' name='searchmessages' value='[index]'>\
	<input type='submit' value='Search'></form>"
	if(!linkless)
		output = navbar
	if(type == "memo" || type == "watchlist entry")
		if(type == "memo")
			output += "
Admin memos
"
			output += "Add memo"
		else if(type == "watchlist entry")
			output += "
Watchlist entries
"
			output += "Add watchlist entry"
			if(filter)
				output += "Unfilter clients"
			else
				output += "Filter offline clients"
		output += ruler
		var/datum/db_query/query_get_type_messages = SSdbcore.NewQuery({"
			SELECT
				id,
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
				targetckey,
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
				text,
				timestamp,
				server,
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor),
				expire_timestamp,
				playtime,
				round_id
			FROM [format_table_name("messages")]
			WHERE type = :type AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
		"}, list("type" = type))
		if(!query_get_type_messages.warn_execute())
			qdel(query_get_type_messages)
			return
		while(query_get_type_messages.NextRow())
			if(QDELETED(usr))
				return
			var/id = query_get_type_messages.item[1]
			var/t_key = query_get_type_messages.item[2]
			var/t_ckey = query_get_type_messages.item[3]
			if(type == "watchlist entry" && filter && !(t_ckey in GLOB.directory))
				continue
			var/admin_key = query_get_type_messages.item[4]
			var/text = query_get_type_messages.item[5]
			var/timestamp = query_get_type_messages.item[6]
			var/server = query_get_type_messages.item[7]
			var/editor_key = query_get_type_messages.item[8]
			var/expire_timestamp = query_get_type_messages.item[9]
			var/playtime = query_get_type_messages.item[10]
			var/round_id = query_get_type_messages.item[11]
			output += ""
			if(type == "watchlist entry")
				output += "[t_key] | "
			output += "[timestamp] | [server] | Round [round_id] | [admin_key]"
			if(type == "watchlist entry")
				output += " | [get_exp_format(text2num(playtime))] Living Playtime"
			if(expire_timestamp)
				output += " | Expires [expire_timestamp]"
			output += ""
			output += " Change Expiry Time"
			output += " Delete"
			output += " Edit"
			if(editor_key)
				output += " Last edit by [editor_key] (Click here to see edit log)"
			output += "
[text]
"
		qdel(query_get_type_messages)
	if(target_ckey)
		var/target_key
		var/datum/db_query/query_get_messages = SSdbcore.NewQuery({"
			SELECT
				type,
				secret,
				id,
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
				text,
				timestamp,
				server,
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor),
				DATEDIFF(NOW(), timestamp),
				IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
				expire_timestamp,
				severity,
				playtime,
				round_id
			FROM [format_table_name("messages")]
			WHERE type <> 'memo' AND targetckey = :targetckey AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
			ORDER BY timestamp DESC
		"}, list("targetckey" = target_ckey))
		if(!query_get_messages.warn_execute())
			qdel(query_get_messages)
			return
		var/list/messagedata = list()
		var/list/watchdata = list()
		var/list/notedata = list()
		var/skipped = 0
		while(query_get_messages.NextRow())
			if(QDELETED(usr))
				return
			type = query_get_messages.item[1]
			if(type == "memo")
				continue
			var/secret = text2num(query_get_messages.item[2])
			if(linkless && secret)
				continue
			var/id = query_get_messages.item[3]
			var/admin_key = query_get_messages.item[4]
			var/text = query_get_messages.item[5]
			var/timestamp = query_get_messages.item[6]
			var/server = query_get_messages.item[7]
			var/editor_key = query_get_messages.item[8]
			var/age = text2num(query_get_messages.item[9])
			target_key = query_get_messages.item[10]
			var/expire_timestamp = query_get_messages.item[11]
			var/severity = query_get_messages.item[12]
			var/playtime = query_get_messages.item[13]
			var/round_id = query_get_messages.item[14]
			var/alphatext = ""
			var/nsd = CONFIG_GET(number/note_stale_days)
			var/nfd = CONFIG_GET(number/note_fresh_days)
			if (agegate && type == "note" && isnum(nsd) && isnum(nfd) && nsd > nfd)
				var/alpha = clamp(100 - (age - nfd) * (85 / (nsd - nfd)), 15, 100)
				if (alpha < 100)
					if (alpha <= 15)
						if (skipped)
							skipped++
							continue
						alpha = 10
						skipped = TRUE
					alphatext = "filter: alpha(opacity=[alpha]); opacity: [alpha/100];"
			var/list/data = list("

")
			if(severity)
				data += " "
			data += "[timestamp] | [server] | Round [round_id] | [admin_key][secret ? " | - Secret" : ""] | [get_exp_format(text2num(playtime))] Living Playtime"
			if(expire_timestamp)
				data += " | Expires [expire_timestamp]"
			data += "
"
			if(!linkless)
				if(type == "note")
					if(severity)
						data += "[severity=="none" ? "No" : "[capitalize(severity)]"] Severity"
					else
						data += "N/A Severity"
				data += " Change Expiry Time"
				data += " Delete"
				if(type == "note")
					data += " [secret ? "Secret" : "Not secret"]"
				if(type == "message sent")
					data += " Message has been sent"
					if(editor_key)
						data += "|"
				else
					data += " Edit"
				if(editor_key)
					data += " Last edit by [editor_key] (Click here to see edit log)"
			data += "
"
			data += "
[text]

"
			switch(type)
				if("message")
					messagedata += data
				if("message sent")
					messagedata += data
				if("watchlist entry")
					watchdata += data
				if("note")
					notedata += data
		qdel(query_get_messages)
		if(!target_key)
			var/datum/db_query/query_get_message_key = SSdbcore.NewQuery({"
				SELECT byond_key FROM [format_table_name("player")] WHERE ckey = :ckey
			"}, list("ckey" = target_ckey))
			if(!query_get_message_key.warn_execute())
				qdel(query_get_message_key)
				return
			if(query_get_message_key.NextRow())
				target_key = query_get_message_key.item[1]
			qdel(query_get_message_key)
		output += "
[target_key]
"
		if(!linkless)
			output += "Add note"
			output += " Add message"
			output += " Add to watchlist"
			output += " Refresh page"
		else
			output += " Refresh page"
		output += ruler
		if(messagedata)
			output += "
Messages
"
			output += messagedata
		if(watchdata)
			output += "
Watchlist
"
			output += watchdata
		if(notedata)
			output += "
Notes
"
			output += notedata
			if(!linkless)
				if (agegate)
					if (skipped) //the first skipped message is still shown so that we can put this link over it.
						output += "Show [skipped] hidden messages"
					else
						output += "Show All"
				else
					output += "Hide Old"
	if(index)
		var/search
		output += "Add messageAdd watchlist entryAdd note"
		output += ruler
		switch(index)
			if(1)
				search = "^."
			if(2)
				search = "^\[^\[:alpha:\]\]"
			else
				search = "^[index]"
		var/datum/db_query/query_list_messages = SSdbcore.NewQuery({"
			SELECT DISTINCT
				targetckey,
				(SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey)
			FROM [format_table_name("messages")]
			WHERE type <> 'memo'
				AND targetckey REGEXP :search
				AND deleted = 0
				AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
			ORDER BY targetckey
		"}, list("search" = search))
		if(!query_list_messages.warn_execute())
			qdel(query_list_messages)
			return
		while(query_list_messages.NextRow())
			if(QDELETED(usr))
				return
			var/index_ckey = query_list_messages.item[1]
			var/index_key = query_list_messages.item[2]
			if(!index_key)
				index_key = index_ckey
			output += "[index_key]
"
		qdel(query_list_messages)
	else if(!type && !target_ckey && !index)
		output += "Add messageAdd watchlist entryAdd note"
		output += ruler
	var/datum/browser/browser = new(usr, "Note panel", "Manage player notes", 1000, 500)
	var/datum/asset/notes_assets = get_asset_datum(/datum/asset/simple/notes)
	notes_assets.send(usr.client)
	browser.set_content(jointext(output, ""))
	browser.open()

/proc/get_message_output(type, target_ckey, show_secret = TRUE, after_timestamp)
	if(!SSdbcore.Connect())
		to_chat(usr, span_danger("Failed to establish database connection."), confidential = TRUE)
		return
	if(!type)
		return
	var/output
	var/list/parameters = list(
		"targetckey" = target_ckey,
		"type" = type,
	)
	if(after_timestamp)
		parameters["after_timestamp"] = after_timestamp
	var/datum/db_query/query_get_message_output = SSdbcore.NewQuery({"
		SELECT
			id,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
			text,
			timestamp,
			IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor)
		FROM [format_table_name("messages")]
		WHERE type = :type
		AND deleted = 0
		AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
		AND (type = 'memo' OR targetckey = :targetckey)
		[after_timestamp? "AND timestamp > :after_timestamp": ""]
		[!show_secret? "AND secret = 0": ""]
	"}, parameters)
	if(!query_get_message_output.warn_execute())
		qdel(query_get_message_output)
		return
	while(query_get_message_output.NextRow())
		var/message_id = query_get_message_output.item[1]
		var/admin_key = query_get_message_output.item[2]
		var/text = query_get_message_output.item[3]
		var/timestamp = query_get_message_output.item[4]
		var/editor_key = query_get_message_output.item[5]
		switch(type)
			if("message")
				output += "Admin message left by [span_prefix("[admin_key]")] on [timestamp]"
				output += "
[text]
"
				var/datum/db_query/query_message_read = SSdbcore.NewQuery(
					"UPDATE [format_table_name("messages")] SET type = 'message sent' WHERE id = :id",
					list("id" = message_id)
				)
				if(!query_message_read.warn_execute())
					qdel(query_get_message_output)
					qdel(query_message_read)
					return
				qdel(query_message_read)
			if("note")
				output += "Note left by [span_prefix("[admin_key]")] on [timestamp]"
				output += "
[text]
"
			if("watchlist entry")
				message_admins("Notice: [key_name_admin(target_ckey)] has been on the watchlist since [timestamp] and has just connected - Reason: [text]")
				send2tgs_adminless_only("Watchlist", "[key_name(target_ckey)] is on the watchlist and has just connected - Reason: [text]")
			if("memo")
				output += "[span_memo("Memo by [admin_key]")] on [timestamp]"
				if(editor_key)
					output += "
[span_memoedit("Last edit by [editor_key] (Click here to see edit log)")]"
				output += "
[text]
"
	qdel(query_get_message_output)
	return output

#define NOTESFILE "data/player_notes.sav"
//if the AUTOCONVERT_NOTES is turned on, anytime a player connects this will be run to try and add all their notes to the databas
/proc/convert_notes_sql(ckey)
	if(!fexists(NOTESFILE))
		return

	var/savefile/notesfile = new(NOTESFILE)
	if(!notesfile)
		log_game("Error: Cannot access [NOTESFILE]")
		return
	notesfile.cd = "/[ckey]"
	while(!notesfile.eof)
		var/notetext
		notesfile >> notetext
		var/server
		var/ssqlname = CONFIG_GET(string/serversqlname)
		if (ssqlname)
			server = ssqlname
		var/regex/note = new("^(\\d{2}-\\w{3}-\\d{4}) \\| (.+) ~(\\w+)$", "i")
		note.Find(notetext)
		var/timestamp = note.group[1]
		notetext = note.group[2]
		var/admin_ckey = note.group[3]
		var/datum/db_query/query_convert_time = SSdbcore.NewQuery("SELECT ADDTIME(STR_TO_DATE(:timestamp,'%d-%b-%Y'), '0')", list("timestamp" = timestamp))
		if(!query_convert_time.Execute())
			qdel(query_convert_time)
			return
		if(query_convert_time.NextRow())
			timestamp = query_convert_time.item[1]
		qdel(query_convert_time)
		if(ckey && notetext && timestamp && admin_ckey && server)
			create_message("note", ckey, admin_ckey, notetext, timestamp, server, 1, 0, null, 0, 0)
	notesfile.cd = "/"
	notesfile.dir.Remove(ckey)

/*alternatively this proc can be run once to pass through every note and attempt to convert it before deleting the file, if done then AUTOCONVERT_NOTES should be turned off
this proc can take several minutes to execute fully if converting and cause DD to hang if converting a lot of notes; it's not advised to do so while a server is live
/proc/mass_convert_notes()
	to_chat(world, "Beginning mass note conversion", confidential = TRUE)
	var/savefile/notesfile = new(NOTESFILE)
	if(!notesfile)
		log_game("Error: Cannot access [NOTESFILE]")
		return
	notesfile.cd = "/"
	for(var/ckey in notesfile.dir)
		convert_notes_sql(ckey)
	to_chat(world, "Deleting NOTESFILE", confidential = TRUE)
	fdel(NOTESFILE)
	to_chat(world, "Finished mass note conversion, remember to turn off AUTOCONVERT_NOTES", confidential = TRUE)*/
#undef NOTESFILE