code:
/obj/item/skillchip
	name = "skillchip"
	desc = "This biochip integrates with user's brain to enable mastery of specific skill. Consult certified Nanotrasen neurosurgeon before use."

	icon = 'icons/obj/card.dmi'
	icon_state = "data_3"
	custom_price = PAYCHECK_MEDIUM * 3
	w_class = WEIGHT_CLASS_SMALL

	/// Traits automatically granted by this chip, optional. Lazylist.
	var/list/auto_traits
	/// Skill name shown on UI
	var/skill_name
	/// Skill description shown on UI
	var/skill_description
	/// Category string. Used alongside SKILLCHIP_RESTRICTED_CATEGORIES flag to make a chip incompatible with chips from another category.
	var/chip_category = SKILLCHIP_CATEGORY_GENERAL
	/// List of any incompatible categories.
	var/list/incompatibility_list
	/// Fontawesome icon show on UI, list of possible icons https://fontawesome.com/icons?d=gallery&m=free
	var/skill_icon = "brain"
	/// Message shown when activating the chip
	var/activate_message
	/// Message shown when deactivating the chip
	var/deactivate_message
	//If set to FALSE, trying to extract the chip will destroy it instead
	var/removable
	/// How complex the skillchip is. Brains can only handle so much complexity at once and skillchips will start to deactivate when the brain's complexity limit is exceeded.
	var/complexity = 1
	/// How many slots taken up in the brain by this chip. Max brain slots are hard set and should not be changed at all.
	var/slot_use = 1
	/// Variable for flags. DANGEROUS - Child types overwrite flags instead of adding to them. If you change this, make sure all child types have the appropriate flags set too.
	var/skillchip_flags = NONE
	/// Cooldown before the skillchip can be extracted after it has been implanted.
	var/cooldown = 5 MINUTES
	/// Cooldown for chip actions.
	COOLDOWN_DECLARE(chip_cooldown)
	/// Used to determine if this is an abstract type or not. If this is meant to be an abstract type, set it to the type's path. Will be overridden by subsequent abstract parents. See /datum/action/item_action/chameleon/change/skillchip/initialize_disguises()
	var/abstract_parent_type = /obj/item/skillchip
	/// Set to TRUE when the skill chip's effects are applied. Set to FALSE when they're not.
	var/active = FALSE
	/// Brain that holds this skillchip.
	var/obj/item/organ/brain/holding_brain

/obj/item/skillchip/Initialize(mapload, is_removable = TRUE)
	. = ..()
	removable = is_removable

/**
 * Activates the skillchip, if possible.
 *
 * Returns a message containing the reason if activation is not possible.
 * Arguments:
 * * silent - Boolean. Whether or not an activation message should be shown to the user.
 * * force - Boolean. Whether or not to just force de-activation if it would be prevented for any reason.
 */
/obj/item/skillchip/proc/try_activate_skillchip(silent = FALSE, force = FALSE)
	// Should not happen. Holding brain is destroyed and the chip hasn't had its state set appropriately.
	if(QDELETED(holding_brain))
		stack_trace("Skillchip's owner is null or qdeleted brain.")
		return "Skillchip cannot detect viable brain."

	// Also should not happen. We're somehow activating skillchips in a bodyless brain.
	if(QDELETED(holding_brain.owner))
		stack_trace("Skillchip's brain has no owner, owner is null or owner qdeleted.")
		return "Skillchip cannot detect viable body."

	// We have a holding brain, the holding brain has an owner. If we're forcing this, do it hard and fast.
	if(force)
		on_activate(holding_brain.owner, silent)
		return

	// Is the chip still experiencing a cooldown period?
	if(!COOLDOWN_FINISHED(src, chip_cooldown))
		return "Skillchip is still recharging for [COOLDOWN_TIMELEFT(src, chip_cooldown) * 0.1]s"

	// So, we have a brain and that brain has a body. Let's start checking for incompatibility.
	var/activate_msg = has_activate_incompatibility(holding_brain)

	// If there's an activate_msg it means we can't activate for some reason. Return the feedback message.
	if(activate_msg)
		return activate_msg

	// Either there's no incompatibility or we're forcing the activation. We're good to go!
	on_activate(holding_brain.owner, silent)

/**
 * Deactivates the skillchip, if possible.
 *
 * Returns a message containing the reason if deactivation is not possible.
 * Arguments:
 * * silent - Boolean. Whether or not an activation message should be shown to the user.
 * * force - Boolean. Whether or not to just force de-activation if it would be prevented for any reason.
 */
/obj/item/skillchip/proc/try_deactivate_skillchip(silent = FALSE, force = FALSE)
	if(!active)
		return "Skillchip is not active."

	// Should not happen. Holding brain is destroyed and the chip hasn't had its state set appropriately.
	if(QDELETED(holding_brain))
		stack_trace("Skillchip's owner is null or qdeleted brain.")
		return "Skillchip cannot detect viable brain."

	// Also should not happen. We're somehow deactivating skillchips in a bodyless brain.
	if(QDELETED(holding_brain.owner))
		active = FALSE
		stack_trace("Skillchip's brain has no owner, owner is null or owner qdeleted.")
		return "Skillchip cannot detect viable body."

	// We have a holding brain, the holding brain has an owner. If we're forcing this, do it hard and fast.
	if(force)
		on_deactivate(holding_brain.owner, silent)
		return

	// Is the chip still experiencing a cooldown period?
	if(!COOLDOWN_FINISHED(src, chip_cooldown))
		return "Skillchip is still recharging for [COOLDOWN_TIMELEFT(src, chip_cooldown) * 0.1]s"

	// We're good to go. Deactive this chip.
	on_deactivate(holding_brain.owner, silent)

/**
 * Called when a skillchip is inserted in a user's brain.
 *
 * Arguments:
 * * owner_brain - The brain that this skillchip was implanted in to.
 */
/obj/item/skillchip/proc/on_implant(obj/item/organ/brain/owner_brain)
	if(holding_brain)
		CRASH("Skillchip is trying to be implanted into [owner_brain], but it's already implanted in [holding_brain]")

	holding_brain = owner_brain

/**
 * Called when a skillchip is activated.
 *
 * Arguments:
 * * user - The user to apply skillchip effects to.
 * * silent - Boolean. Whether or not an activation message should be shown to the user.
 */
/obj/item/skillchip/proc/on_activate(mob/living/carbon/user, silent=FALSE)
	if(!silent && activate_message)
		to_chat(user, activate_message)

	for(var/trait in auto_traits)
		ADD_TRAIT(user, trait, SKILLCHIP_TRAIT)

	active = TRUE

	COOLDOWN_START(src, chip_cooldown, cooldown)

/**
 * Called when a skillchip is removed from the user's brain.
 *
 * Always deactivates the skillchip.
 * Arguments:
 * * user - The user to remove skillchip effects from.
 * * silent - Boolean. Whether or not a deactivation message should be shown to the user.
 */
/obj/item/skillchip/proc/on_removal(silent=FALSE)
	if(active)
		try_deactivate_skillchip(silent, TRUE)

	COOLDOWN_RESET(src, chip_cooldown)

	holding_brain = null

/**
 * Called when a skillchip is deactivated.
 *
 * Arguments:
 * * user - The user to remove skillchip effects from.
 * * silent - Boolean. Whether or not a deactivation message should be shown to the user.
 */
/obj/item/skillchip/proc/on_deactivate(mob/living/carbon/user, silent=FALSE)
	if(!silent && deactivate_message)
		to_chat(user, deactivate_message)

	for(var/trait in auto_traits)
		REMOVE_TRAIT(user, trait, SKILLCHIP_TRAIT)

	active = FALSE

	COOLDOWN_START(src, chip_cooldown, cooldown)

/**
 * Checks whether a given skillchip has an incompatibility with a brain that should render it impossible
 * to activate.
 *
 * Returns a string with an explanation if the chip is not activatable. FALSE otherwise.
 * Arguments:
 * * skillchip - The skillchip you're intending to activate. Does not activate the chip.
 */
/obj/item/skillchip/proc/has_activate_incompatibility(obj/item/organ/brain/brain)
	if(QDELETED(brain))
		return "No brain detected."

	// Check if there's enough complexity usage left to activate the skillchip.
	var/max_complexity = brain.get_max_skillchip_complexity()
	var/new_complexity = brain.get_used_skillchip_complexity() + get_complexity()
	if(new_complexity > max_complexity)
		return "Skillchip is too complex to activate: [new_complexity] total out of [max_complexity] max complexity."

	return FALSE

/**
 * Checks for skillchip incompatibility with another chip.
 *
 * Does *this* skillchip have incompatibility with the skillchip in the args?
 * Override this with any snowflake chip-vs-chip incompatibility checks.
 * Returns a string with an incompatibility explanation if the chip is not compatible, returns FALSE
 * if it is compatible.
 * Arguments:
 * * skillchip - The skillchip to test for incompatability.
 */
/obj/item/skillchip/proc/has_skillchip_incompatibility(obj/item/skillchip/skillchip)
	// Only allow multiple copies of a type if SKILLCHIP_ALLOWS_MULTIPLE flag is set
	if(!(skillchip_flags & SKILLCHIP_ALLOWS_MULTIPLE) && (skillchip.type == type))
		return "Duplicate chip detected: [skillchip.name]"

	// Prevent implanting multiple chips of the same category.
	if((skillchip_flags & SKILLCHIP_RESTRICTED_CATEGORIES) && (skillchip.chip_category in incompatibility_list))
		return "Incompatible with implanted [skillchip.chip_category] chip [skillchip.name]."

	return FALSE

/**
 * Performs a full sweep of checks that dictate if this chip can be implanted in a given target.
 *
 * Override this with any snowflake chip checks. An example of which would be checking if a target is
 * mindshielded if you've got a special security skillchip.
 * Returns a string with an incompatibility explanation if the chip is not compatible, returns FALSE
 * if it is compatible.
 * Arguments:
 * * target - The mob to check for implantability with.
 */
/obj/item/skillchip/proc/has_mob_incompatibility(mob/living/carbon/target)
	// No carbon/carbon of incorrect type
	if(!istype(target))
		return "Incompatible lifeform detected."

	// No brain
	var/obj/item/organ/brain/brain = target.getorganslot(ORGAN_SLOT_BRAIN)
	if(QDELETED(brain))
		return "No brain detected."

	// Check brain incompatibility. This also performs skillchip-to-skillchip incompatibility checks.
	var/brain_message = has_brain_incompatibility(brain)
	if(brain_message)
		return brain_message

	return FALSE

/**
 * Performs a full sweep of checks that dictate if this chip can be implanted in a given brain.
 *
 * Override this with any snowflake chip checks.
 * Returns TRUE if the chip is fully compatible, FALSE otherwise.
 * Arguments:
 * * brain - The brain to check for implantability with.
 */
/obj/item/skillchip/proc/has_brain_incompatibility(obj/item/organ/brain/brain)
	if(!istype(brain))
		stack_trace("Attempted to check incompatibility with invalid brain object [brain].")
		return "Incompatible brain."

	var/chip_message

	// Slot capacity check!
	var/max_slots = brain.get_max_skillchip_slots()
	var/used_slots = brain.get_used_skillchip_slots()

	if(used_slots + slot_use > max_slots)
		return "Not enough free slots. You have [max_slots - used_slots] free and need [slot_use]."

	// Check if this chip is incompatible with any other chips in the brain.
	for(var/skillchip in brain.skillchips)
		chip_message = has_skillchip_incompatibility(skillchip)
		if(chip_message)
			return chip_message

	return FALSE

/**
 * Returns whether the chip is on cooldown. Chips ordinarily go on cooldown when activated.
 *
 * This does not mean the chip should be impossible to do anything with.
 * It's up to each individual piece of code to decide what it does with the result of this proc.
 *
 * Returns TRUE if the chip's extraction cooldown hasn't yet passed.
 */
/obj/item/skillchip/proc/is_on_cooldown()
	return !COOLDOWN_FINISHED(src, chip_cooldown)

/**
 * Returns whether the chip is active.
 *
 * Intended to be overriden.
 * Returns TRUE if the chip is active.
 */
/obj/item/skillchip/proc/is_active()
	return active

/**
 * Returns the chip's complexity.
 *
 * Intended to be overriden.
 */
/obj/item/skillchip/proc/get_complexity()
	return complexity

/**
 * Returns a list of basic chip info. Used by the skill station.
 */
/obj/item/skillchip/proc/get_chip_data()
	return list(
		"name" = skill_name,
		"icon" = skill_icon,
		"desc" = skill_description,
		"complexity" = get_complexity(),
		"slot_use" = slot_use,
		"removable" = removable,
		"ref" = REF(src),
		"active" = is_active(),
		"active_error" = has_activate_incompatibility(holding_brain),
		"cooldown" = COOLDOWN_TIMELEFT(src, chip_cooldown),
		"actionable" = is_on_cooldown())

/**
 * Gets key metadata from this skillchip in an assoc list.
 *
 * If you override this proc, don't forget to also override set_metadata, which takes the output of
 * this proc and uses it to set the metadata.
 * Does not copy over any owner or brain status. Handle that externally.
 */
/obj/item/skillchip/proc/get_metadata()
	var/list/metadata = list()
	metadata["type"] = type
	metadata["chip_cooldown"] = chip_cooldown
	metadata["active"] = active
	metadata["removable"] = removable

	return metadata

/**
 * Sets key metadata for this skillchip from an assoc list.
 *
 * Best used with the output from get_metadata() of another chip.
 * If you override this proc, don't forget to also override get_metadata, which is where you should
 * usually get the assoc list that feeds into this proc.
 * Does not set any owner or brain status. Handle that externally.
 * Arguments:
 * metadata - Ideally the output of another chip's get_metadata proc. Assoc list of metadata.
 */
/obj/item/skillchip/proc/set_metadata(list/metadata)
	var/active_msg
	// Start by trying to activate.
	active = metadata["active"]
	if(active)
		active_msg = try_activate_skillchip(FALSE, TRUE)

	// Whether it worked or not, set the rest of the metadata and then return any activate message.
	chip_cooldown = metadata["chip_cooldown"]
	removable = metadata["removable"]

	return active_msg

/obj/item/skillchip/basketweaving
	name = "Basketsoft 3000 skillchip"
	desc = "Underwater edition."
	auto_traits = list(TRAIT_UNDERWATER_BASKETWEAVING_KNOWLEDGE)
	skill_name = "Underwater Basketweaving"
	skill_description = "Master intricate art of using twine to create perfect baskets while submerged."
	skill_icon = "shopping-basket"
	activate_message = "You're one with the twine and the sea."
	deactivate_message = "Higher mysteries of underwater basketweaving leave your mind."

/obj/item/skillchip/wine_taster
	name = "WINE skillchip"
	desc = "Wine.Is.Not.Equal version 5."
	auto_traits = list(TRAIT_WINE_TASTER)
	skill_name = "Wine Tasting"
	skill_description = "Recognize wine vintage from taste alone. Never again lack an opinion when presented with an unknown drink."
	skill_icon = "wine-bottle"
	activate_message = "You recall wine taste."
	deactivate_message = "Your memories of wine evaporate."

/obj/item/skillchip/bonsai
	name = "Hedge 3 skillchip"
	auto_traits = list(TRAIT_BONSAI)
	skill_name = "Hedgetrimming"
	skill_description = "Trim hedges and potted plants into marvelous new shapes with any old knife. Not applicable to plastic plants."
	skill_icon = "spa"
	activate_message = "Your mind is filled with plant arrangments."
	deactivate_message = "You can't remember what a hedge looks like anymore."

/obj/item/skillchip/useless_adapter
	name = "Skillchip adapter"
	skill_name = "Useless adapter"
	skill_description = "Allows you to insert another skillchip into this adapter after it has been inserted into your brain..."
	skill_icon = "plug"
	activate_message = "You can now activate another chip through this adapter, but you're not sure why you did this..."
	deactivate_message = "You no longer have the useless skillchip adapter."
	skillchip_flags = SKILLCHIP_ALLOWS_MULTIPLE
	// Literally does nothing.
	complexity = 0
	slot_use = 0

/obj/item/skillchip/light_remover
	name = "N16H7M4R3 skillchip"
	auto_traits = list(TRAIT_LIGHTBULB_REMOVER)
	skill_name = "Lightbulb Removing"
	skill_description = "Stop failing taking out lightbulbs today, no gloves needed!"
	skill_icon = "lightbulb"
	activate_message = "Your feel like your pain receptors are less sensitive to hot objects."
	deactivate_message = "You feel like hot objects could stop you again..."

/obj/item/skillchip/disk_verifier
	name = "K33P-TH4T-D15K skillchip"
	auto_traits = list(TRAIT_DISK_VERIFIER)
	skill_name = "Nuclear Disk Verification"
	skill_description = "Nuclear authentication disks have an extremely long serial number for verification. This skillchip stores that number, which allows the user to automatically spot forgeries."
	skill_icon = "save"
	activate_message = "You feel your mind automatically verifying long serial numbers on disk shaped objects."
	deactivate_message = "The innate recognition of absurdly long disk-related serial numbers fades from your mind."

/obj/item/skillchip/entrails_reader
	name = "3NTR41LS skillchip"
	auto_traits = list(TRAIT_ENTRAILS_READER)
	skill_name = "Entrails Reader"
	skill_description = "Be able to learn about a person's life, by looking at their internal organs. Not to be confused with looking into the future."
	skill_icon = "lungs"
	activate_message = "You feel that you know a lot about interpreting organs."
	deactivate_message = "Knowledge of liver damage, heart strain and lung scars fades from your mind."

/obj/item/skillchip/brainwashing
	name = "suspicious skillchip"
	auto_traits = list(TRAIT_BRAINWASHING)
	skill_name = "Brainwashing"
	skill_description = "WARNING: The integrity of this chip is compromised. Please discard this skillchip."
	skill_icon = "soap"
	activate_message = span_notice("...But all at once it comes to you... something involving putting a brain in a washing machine?")
	deactivate_message = span_warning("All knowledge of the secret brainwashing technique is GONE.")

/obj/item/skillchip/brainwashing/examine(mob/user)
	. = ..()
	. += span_warning("It seems to have been corroded over time, putting this in your head may not be the best idea...")

/obj/item/skillchip/brainwashing/on_activate(mob/living/carbon/user, silent = FALSE)
	to_chat(user, span_danger("You get a pounding headache as the chip sends corrupt memories into your head!"))
	user.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
	. = ..()


code:
/**
 * Internal atom that copies an appearance on to the blocker plane
 *
 * Copies an appearance vis render_target and render_source on to the emissive blocking plane.
 * This means that the atom in question will block any emissive sprites.
 * This should only be used internally. If you are directly creating more of these, you're
 * almost guaranteed to be doing something wrong.
 */
/atom/movable/emissive_blocker
	name = "emissive blocker"
	plane = EMISSIVE_PLANE
	layer = FLOAT_LAYER
	mouse_opacity = MOUSE_OPACITY_TRANSPARENT
	//Why?
	//render_targets copy the transform of the target as well, but vis_contents also applies the transform
	//to what's in it. Applying RESET_TRANSFORM here makes vis_contents not apply the transform.
	//Since only render_target handles transform we don't get any applied transform "stacking"
	appearance_flags = EMISSIVE_APPEARANCE_FLAGS

/atom/movable/emissive_blocker/Initialize(mapload, source)
	. = ..()
	verbs.Cut() //Cargo culting from lighting object, this maybe affects memory usage?

	render_source = source
	color = GLOB.em_block_color

/atom/movable/emissive_blocker/ex_act(severity)
	return FALSE

/atom/movable/emissive_blocker/singularity_act()
	return

/atom/movable/emissive_blocker/singularity_pull()
	return

/atom/movable/emissive_blocker/blob_act()
	return

/atom/movable/emissive_blocker/on_changed_z_level(turf/old_turf, turf/new_turf)
	return

//Prevents people from moving these after creation, because they shouldn't be.
/atom/movable/emissive_blocker/forceMove(atom/destination, no_tp=FALSE, harderforce = FALSE)
	if(harderforce)
		return ..()

repo_name: AnalWerewolf/sundown

license: mit


code:
/area
	luminosity = 1
	///The mutable appearance we underlay to show light
	var/mutable_appearance/lighting_effect = null
	///Whether this area has a currently active base lighting, bool
	var/area_has_base_lighting = FALSE
	///alpha 0-255 of lighting_effect and thus baselighting intensity
	var/base_lighting_alpha = 0
	///The colour of the light acting on this area
	var/base_lighting_color = COLOR_WHITE

/area/proc/set_base_lighting(new_base_lighting_color = -1, new_alpha = -1)
	if(base_lighting_alpha == new_alpha && base_lighting_color == new_base_lighting_color)
		return FALSE
	if(new_alpha != -1)
		base_lighting_alpha = new_alpha
	if(new_base_lighting_color != -1)
		base_lighting_color = new_base_lighting_color
	update_base_lighting()
	return TRUE

/area/vv_edit_var(var_name, var_value)
	switch(var_name)
		if("base_lighting_color")
			set_base_lighting(new_base_lighting_color = var_value)
			return TRUE
		if("base_lighting_alpha")
			set_base_lighting(new_alpha = var_value)
			return TRUE
		if("static_lighting")
			if(!static_lighting)
				create_area_lighting_objects()
			else
				remove_area_lighting_objects()

	return ..()

/area/proc/update_base_lighting()
	if(!area_has_base_lighting && (!base_lighting_alpha || !base_lighting_color))
		return

	if(!area_has_base_lighting)
		add_base_lighting()
		return
	remove_base_lighting()
	if(base_lighting_alpha && base_lighting_color)
		add_base_lighting()

/area/proc/remove_base_lighting()
	for(var/turf/T in src)
		T.cut_overlay(lighting_effect)
	QDEL_NULL(lighting_effect)
	area_has_base_lighting = FALSE

/area/proc/add_base_lighting()
	lighting_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
	lighting_effect.plane = LIGHTING_PLANE
	lighting_effect.layer = LIGHTING_PRIMARY_LAYER
	lighting_effect.blend_mode = BLEND_ADD
	lighting_effect.alpha = base_lighting_alpha
	lighting_effect.color = base_lighting_color
	lighting_effect.appearance_flags = RESET_TRANSFORM | RESET_ALPHA | RESET_COLOR
	for(var/turf/T in src)
		T.add_overlay(lighting_effect)
		T.luminosity = 1
	area_has_base_lighting = TRUE


code:
// The proc you should always use to set the light of this atom.
// Nonesensical value for l_color default, so we can detect if it gets set to null.
#define NONSENSICAL_VALUE -99999
/atom/proc/set_light(l_range, l_power, l_color = NONSENSICAL_VALUE, l_on)
	if(l_range > 0 && l_range < MINIMUM_USEFUL_LIGHT_RANGE)
		l_range = MINIMUM_USEFUL_LIGHT_RANGE //Brings the range up to 1.4, which is just barely brighter than the soft lighting that surrounds players.

	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT, l_range, l_power, l_color, l_on) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return

	if(!isnull(l_power))
		set_light_power(l_power)

	if(!isnull(l_range))
		set_light_range(l_range)

	if(l_color != NONSENSICAL_VALUE)
		set_light_color(l_color)

	if(!isnull(l_on))
		set_light_on(l_on)

	update_light()

#undef NONSENSICAL_VALUE

// Will update the light (duh).
// Creates or destroys it if needed, makes it update values, makes sure it's got the correct source turf...
/atom/proc/update_light()
	set waitfor = FALSE
	if (QDELETED(src))
		return

	if(light_system != STATIC_LIGHT)
		CRASH("update_light() for [src] with following light_system value: [light_system]")

	if (!light_power || !light_range || !light_on) // We won't emit light anyways, destroy the light source.
		QDEL_NULL(light)
	else
		if (!ismovable(loc)) // We choose what atom should be the top atom of the light here.
			. = src
		else
			. = loc

		if (light) // Update the light or create it if it does not exist.
			light.update(.)
		else
			light = new/datum/light_source(src, .)

/**
 * Updates the atom's opacity value.
 *
 * This exists to act as a hook for associated behavior.
 * It notifies (potentially) affected light sources so they can update (if needed).
 */
/atom/proc/set_opacity(new_opacity)
	if (new_opacity == opacity)
		return
	SEND_SIGNAL(src, COMSIG_ATOM_SET_OPACITY, new_opacity)
	. = opacity
	opacity = new_opacity

/atom/movable/set_opacity(new_opacity)
	. = ..()
	if(isnull(.) || !isturf(loc))
		return

	if(opacity)
		AddElement(/datum/element/light_blocking)
	else
		RemoveElement(/datum/element/light_blocking)

/turf/set_opacity(new_opacity)
	. = ..()
	if(isnull(.))
		return
	recalculate_directional_opacity()

/atom/movable/Moved(atom/OldLoc, Dir)
	. = ..()
	for (var/datum/light_source/light as anything in light_sources) // Cycle through the light sources on this atom and tell them to update.
		light.source_atom.update_light()

/atom/proc/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION)
	return

/turf/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION)
	if(!_duration)
		stack_trace("Lighting FX obj created on a turf without a duration")
	new /obj/effect/dummy/lighting_obj (src, _range, _power, _color, _duration)

/obj/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION)
	if(!_duration)
		stack_trace("Lighting FX obj created on a obj without a duration")
	new /obj/effect/dummy/lighting_obj (get_turf(src), _range, _power, _color, _duration)

/mob/living/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION)
	mob_light(_range, _power, _color, _duration)

/mob/living/proc/mob_light(_range, _power, _color, _duration)
	var/obj/effect/dummy/lighting_obj/moblight/mob_light_obj = new (src, _range, _power, _color, _duration)
	return mob_light_obj

/// Setter for the light power of this atom.
/atom/proc/set_light_power(new_power)
	if(new_power == light_power)
		return
	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_POWER, new_power) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return
	. = light_power
	light_power = new_power
	SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_POWER, .)

/// Setter for the light range of this atom.
/atom/proc/set_light_range(new_range)
	if(new_range == light_range)
		return
	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_RANGE, new_range) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return
	. = light_range
	light_range = new_range
	SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_RANGE, .)

/// Setter for the light color of this atom.
/atom/proc/set_light_color(new_color)
	if(new_color == light_color)
		return
	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_COLOR, new_color) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return
	. = light_color
	light_color = new_color
	SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .)

/// Setter for whether or not this atom's light is on.
/atom/proc/set_light_on(new_value)
	if(new_value == light_on)
		return
	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_ON, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return
	. = light_on
	light_on = new_value
	SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .)

/// Setter for the light flags of this atom.
/atom/proc/set_light_flags(new_value)
	if(new_value == light_flags)
		return
	if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_FLAGS, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE)
		return
	. = light_flags
	light_flags = new_value
	SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .)


code:
// Because we can control each corner of every lighting object.
// And corners get shared between multiple turfs (unless you're on the corners of the map, then 1 corner doesn't).
// For the record: these should never ever ever be deleted, even if the turf doesn't have dynamic lighting.

/datum/lighting_corner
	var/list/datum/light_source/affecting // Light sources affecting us.

	var/x = 0
	var/y = 0

	var/turf/master_NE
	var/turf/master_SE
	var/turf/master_SW
	var/turf/master_NW

	//"raw" color values, changed by update_lumcount()
	var/lum_r = 0
	var/lum_g = 0
	var/lum_b = 0

	//true color values, guaranteed to be between 0 and 1
	var/cache_r = LIGHTING_SOFT_THRESHOLD
	var/cache_g = LIGHTING_SOFT_THRESHOLD
	var/cache_b = LIGHTING_SOFT_THRESHOLD

	///the maximum of lum_r, lum_g, and lum_b. if this is > 1 then the three cached color values are divided by this
	var/largest_color_luminosity = 0

	///whether we are to be added to SSlighting's corners_queue list for an update
	var/needs_update = FALSE

/datum/lighting_corner/New(turf/new_turf, diagonal)
	. = ..()
	save_master(new_turf, turn(diagonal, 180))

	var/vertical   = diagonal & ~(diagonal - 1) // The horizontal directions (4 and 8) are bigger than the vertical ones (1 and 2), so we can reliably say the lsb is the horizontal direction.
	var/horizontal = diagonal & ~vertical       // Now that we know the horizontal one we can get the vertical one.

	x = new_turf.x + (horizontal == EAST  ? 0.5 : -0.5)
	y = new_turf.y + (vertical   == NORTH ? 0.5 : -0.5)

	// My initial plan was to make this loop through a list of all the dirs (horizontal, vertical, diagonal).
	// Issue being that the only way I could think of doing it was very messy, slow and honestly overengineered.
	// So we'll have this hardcode instead.
	var/turf/new_master_turf

	// Diagonal one is easy.
	new_master_turf = get_step(new_turf, diagonal)
	if (new_master_turf) // In case we're on the map's border.
		save_master(new_master_turf, diagonal)

	// Now the horizontal one.
	new_master_turf = get_step(new_turf, horizontal)
	if (new_master_turf) // Ditto.
		save_master(new_master_turf, ((new_master_turf.x > x) ? EAST : WEST) | ((new_master_turf.y > y) ? NORTH : SOUTH)) // Get the dir based on coordinates.

	// And finally the vertical one.
	new_master_turf = get_step(new_turf, vertical)
	if (new_master_turf)
		save_master(new_master_turf, ((new_master_turf.x > x) ? EAST : WEST) | ((new_master_turf.y > y) ? NORTH : SOUTH)) // Get the dir based on coordinates.

/datum/lighting_corner/proc/save_master(turf/master, dir)
	switch (dir)
		if (NORTHEAST)
			master_NE = master
			master.lighting_corner_SW = src
		if (SOUTHEAST)
			master_SE = master
			master.lighting_corner_NW = src
		if (SOUTHWEST)
			master_SW = master
			master.lighting_corner_NE = src
		if (NORTHWEST)
			master_NW = master
			master.lighting_corner_SE = src

/datum/lighting_corner/proc/self_destruct_if_idle()
	if (!LAZYLEN(affecting))
		qdel(src, force = TRUE)

/datum/lighting_corner/proc/vis_update()
	for (var/datum/light_source/light_source as anything in affecting)
		light_source.vis_update()

/datum/lighting_corner/proc/full_update()
	for (var/datum/light_source/light_source as anything in affecting)
		light_source.recalc_corner(src)

// God that was a mess, now to do the rest of the corner code! Hooray!
/datum/lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b)
	if (!(delta_r || delta_g || delta_b)) // 0 is falsey ok
		return

	lum_r += delta_r
	lum_g += delta_g
	lum_b += delta_b

	if (!needs_update)
		needs_update = TRUE
		SSlighting.corners_queue += src

/datum/lighting_corner/proc/update_objects()
	// Cache these values ahead of time so 4 individual lighting objects don't all calculate them individually.
	var/lum_r = src.lum_r
	var/lum_g = src.lum_g
	var/lum_b = src.lum_b
	var/largest_color_luminosity = max(lum_r, lum_g, lum_b) // Scale it so one of them is the strongest lum, if it is above 1.
	. = 1 // factor
	if (largest_color_luminosity > 1)
		. = 1 / largest_color_luminosity

	#if LIGHTING_SOFT_THRESHOLD != 0
	else if (largest_color_luminosity < LIGHTING_SOFT_THRESHOLD)
		. = 0 // 0 means soft lighting.

	cache_r  = round(lum_r * ., LIGHTING_ROUND_VALUE) || LIGHTING_SOFT_THRESHOLD
	cache_g  = round(lum_g * ., LIGHTING_ROUND_VALUE) || LIGHTING_SOFT_THRESHOLD
	cache_b  = round(lum_b * ., LIGHTING_ROUND_VALUE) || LIGHTING_SOFT_THRESHOLD
	#else
	cache_r  = round(lum_r * ., LIGHTING_ROUND_VALUE)
	cache_g  = round(lum_g * ., LIGHTING_ROUND_VALUE)
	cache_b  = round(lum_b * ., LIGHTING_ROUND_VALUE)
	#endif

	src.largest_color_luminosity = round(largest_color_luminosity, LIGHTING_ROUND_VALUE)

	var/datum/lighting_object/lighting_object = master_NE?.lighting_object
	if (lighting_object && !lighting_object.needs_update)
		lighting_object.needs_update = TRUE
		SSlighting.objects_queue += lighting_object

	lighting_object = master_SE?.lighting_object
	if (lighting_object && !lighting_object.needs_update)
		lighting_object.needs_update = TRUE
		SSlighting.objects_queue += lighting_object

	lighting_object = master_SW?.lighting_object
	if (lighting_object && !lighting_object.needs_update)
		lighting_object.needs_update = TRUE
		SSlighting.objects_queue += lighting_object

	lighting_object = master_NW?.lighting_object
	if (lighting_object && !lighting_object.needs_update)
		lighting_object.needs_update = TRUE
		SSlighting.objects_queue += lighting_object

	self_destruct_if_idle()

/datum/lighting_corner/dummy/New()
	return

/datum/lighting_corner/Destroy(force)
	if (!force)
		return QDEL_HINT_LETMELIVE

	for (var/datum/light_source/light_source as anything in affecting)
		LAZYREMOVE(light_source.effect_str, src)
	affecting = null

	if (master_NE)
		master_NE.lighting_corner_SW = null
		master_NE.lighting_corners_initialised = FALSE
	if (master_SE)
		master_SE.lighting_corner_NW = null
		master_SE.lighting_corners_initialised = FALSE
	if (master_SW)
		master_SW.lighting_corner_NE = null
		master_SW.lighting_corners_initialised = FALSE
	if (master_NW)
		master_NW.lighting_corner_SE = null
		master_NW.lighting_corners_initialised = FALSE
	if (needs_update)
		SSlighting.corners_queue -= src

	return ..()


code:
/datum/lighting_object
	///the underlay we are currently applying to our turf to apply light
	var/mutable_appearance/current_underlay

	///whether we are already in the SSlighting.objects_queue list
	var/needs_update = FALSE

	///the turf that our light is applied to
	var/turf/affected_turf

/datum/lighting_object/New(turf/source)
	if(!isturf(source))
		qdel(src, force=TRUE)
		stack_trace("a lighting object was assigned to [source], a non turf! ")
		return
	. = ..()

	current_underlay = mutable_appearance(LIGHTING_ICON, "transparent", source.z, LIGHTING_PLANE, 255, RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM)

	affected_turf = source
	if (affected_turf.lighting_object)
		qdel(affected_turf.lighting_object, force = TRUE)
		stack_trace("a lighting object was assigned to a turf that already had a lighting object!")

	affected_turf.lighting_object = src
	affected_turf.luminosity = 0

	for(var/turf/open/space/space_tile in RANGE_TURFS(1, affected_turf))
		space_tile.update_starlight()

	needs_update = TRUE
	SSlighting.objects_queue += src

/datum/lighting_object/Destroy(force)
	if (!force)
		return QDEL_HINT_LETMELIVE
	SSlighting.objects_queue -= src
	if (isturf(affected_turf))
		affected_turf.lighting_object = null
		affected_turf.luminosity = 1
		affected_turf.underlays -= current_underlay
	affected_turf = null
	return ..()

/datum/lighting_object/proc/update()

	// To the future coder who sees this and thinks
	// "Why didn't he just use a loop?"
	// Well my man, it's because the loop performed like shit.
	// And there's no way to improve it because
	// without a loop you can make the list all at once which is the fastest you're gonna get.
	// Oh it's also shorter line wise.
	// Including with these comments.

	var/static/datum/lighting_corner/dummy/dummy_lighting_corner = new

	var/datum/lighting_corner/red_corner = affected_turf.lighting_corner_SW || dummy_lighting_corner
	var/datum/lighting_corner/green_corner = affected_turf.lighting_corner_SE || dummy_lighting_corner
	var/datum/lighting_corner/blue_corner = affected_turf.lighting_corner_NW || dummy_lighting_corner
	var/datum/lighting_corner/alpha_corner = affected_turf.lighting_corner_NE || dummy_lighting_corner

	var/max = max(red_corner.largest_color_luminosity, green_corner.largest_color_luminosity, blue_corner.largest_color_luminosity, alpha_corner.largest_color_luminosity)

	var/rr = red_corner.cache_r
	var/rg = red_corner.cache_g
	var/rb = red_corner.cache_b

	var/gr = green_corner.cache_r
	var/gg = green_corner.cache_g
	var/gb = green_corner.cache_b

	var/br = blue_corner.cache_r
	var/bg = blue_corner.cache_g
	var/bb = blue_corner.cache_b

	var/ar = alpha_corner.cache_r
	var/ag = alpha_corner.cache_g
	var/ab = alpha_corner.cache_b

	#if LIGHTING_SOFT_THRESHOLD != 0
	var/set_luminosity = max > LIGHTING_SOFT_THRESHOLD
	#else
	// Because of floating points™?, it won't even be a flat 0.
	// This number is mostly arbitrary.
	var/set_luminosity = max > 1e-6
	#endif

	if((rr & gr & br & ar) && (rg + gg + bg + ag + rb + gb + bb + ab == 8))
		//anything that passes the first case is very likely to pass the second, and addition is a little faster in this case
		affected_turf.underlays -= current_underlay
		current_underlay.icon_state = "transparent"
		current_underlay.color = null
		affected_turf.underlays += current_underlay
	else if(!set_luminosity)
		affected_turf.underlays -= current_underlay
		current_underlay.icon_state = "dark"
		current_underlay.color = null
		affected_turf.underlays += current_underlay
	else
		affected_turf.underlays -= current_underlay
		current_underlay.icon_state = null
		current_underlay.color = list(
			rr, rg, rb, 00,
			gr, gg, gb, 00,
			br, bg, bb, 00,
			ar, ag, ab, 00,
			00, 00, 00, 01
		)

		affected_turf.underlays += current_underlay

	affected_turf.luminosity = set_luminosity


code:
/proc/create_all_lighting_objects()
	for(var/area/A in world)
		if(!A.static_lighting)
			continue

		for(var/turf/T in A)
			if(T.always_lit)
				continue
			new/datum/lighting_object(T)
			CHECK_TICK
		CHECK_TICK


code:
// This is where the fun begins.
// These are the main datums that emit light.

/datum/light_source
	///The atom we're emitting light from (for example a mob if we're from a flashlight that's being held).
	var/atom/top_atom
	///The atom that we belong to.
	var/atom/source_atom

	///The turf under the source atom.
	var/turf/source_turf
	///The turf the top_atom appears to over.
	var/turf/pixel_turf
	///Intensity of the emitter light.
	var/light_power
	/// The range of the emitted light.
	var/light_range
	/// The colour of the light, string, decomposed by parse_light_color()
	var/light_color

	// Variables for keeping track of the colour.
	var/lum_r
	var/lum_g
	var/lum_b

	// The lumcount values used to apply the light.
	var/tmp/applied_lum_r
	var/tmp/applied_lum_g
	var/tmp/applied_lum_b

	/// List used to store how much we're affecting corners.
	var/list/datum/lighting_corner/effect_str

	/// Whether we have applied our light yet or not.
	var/applied = FALSE

	/// whether we are to be added to SSlighting's sources_queue list for an update
	var/needs_update = LIGHTING_NO_UPDATE

/datum/light_source/New(atom/owner, atom/top)
	source_atom = owner // Set our new owner.
	LAZYADD(source_atom.light_sources, src)
	top_atom = top
	if (top_atom != source_atom)
		LAZYADD(top_atom.light_sources, src)

	source_turf = top_atom
	pixel_turf = get_turf_pixel(top_atom) || source_turf

	light_power = source_atom.light_power
	light_range = source_atom.light_range
	light_color = source_atom.light_color

	PARSE_LIGHT_COLOR(src)

	update()

/datum/light_source/Destroy(force)
	remove_lum()
	if (source_atom)
		LAZYREMOVE(source_atom.light_sources, src)

	if (top_atom)
		LAZYREMOVE(top_atom.light_sources, src)

	if (needs_update)
		SSlighting.sources_queue -= src

	top_atom = null
	source_atom = null
	source_turf = null
	pixel_turf = null

	return ..()

// Yes this doesn't align correctly on anything other than 4 width tabs.
// If you want it to go switch everybody to elastic tab stops.
// Actually that'd be great if you could!
#define EFFECT_UPDATE(level)                \
	if (needs_update == LIGHTING_NO_UPDATE) \
		SSlighting.sources_queue += src; \
	if (needs_update < level)               \
		needs_update            = level;    \

// This proc will cause the light source to update the top atom, and add itself to the update queue.
/datum/light_source/proc/update(atom/new_top_atom)
	// This top atom is different.
	if (new_top_atom && new_top_atom != top_atom)
		if(top_atom != source_atom && top_atom.light_sources) // Remove ourselves from the light sources of that top atom.
			LAZYREMOVE(top_atom.light_sources, src)

		top_atom = new_top_atom

		if (top_atom != source_atom)
			LAZYADD(top_atom.light_sources, src) // Add ourselves to the light sources of our new top atom.

	EFFECT_UPDATE(LIGHTING_CHECK_UPDATE)

// Will force an update without checking if it's actually needed.
/datum/light_source/proc/force_update()
	EFFECT_UPDATE(LIGHTING_FORCE_UPDATE)

// Will cause the light source to recalculate turfs that were removed or added to visibility only.
/datum/light_source/proc/vis_update()
	EFFECT_UPDATE(LIGHTING_VIS_UPDATE)

// Macro that applies light to a new corner.
// It is a macro in the interest of speed, yet not having to copy paste it.
// If you're wondering what's with the backslashes, the backslashes cause BYOND to not automatically end the line.
// As such this all gets counted as a single line.
// The braces and semicolons are there to be able to do this on a single line.
#define LUM_FALLOFF(C, T) (1 - CLAMP01(sqrt((C.x - T.x) ** 2 + (C.y - T.y) ** 2 + LIGHTING_HEIGHT) / max(1, light_range)))

#define APPLY_CORNER(C)                          \
	. = LUM_FALLOFF(C, pixel_turf);              \
	. *= light_power;                            \
	var/OLD = effect_str[C];                     \
	                                             \
	C.update_lumcount                            \
	(                                            \
		(. * lum_r) - (OLD * applied_lum_r),     \
		(. * lum_g) - (OLD * applied_lum_g),     \
		(. * lum_b) - (OLD * applied_lum_b)      \
	);                                           \

#define REMOVE_CORNER(C)                         \
	. = -effect_str[C];                          \
	C.update_lumcount                            \
	(                                            \
		. * applied_lum_r,                       \
		. * applied_lum_g,                       \
		. * applied_lum_b                        \
	);

/// This is the define used to calculate falloff.
/datum/light_source/proc/remove_lum()
	applied = FALSE
	for (var/datum/lighting_corner/corner as anything in effect_str)
		REMOVE_CORNER(corner)
		LAZYREMOVE(corner.affecting, src)

	effect_str = null

/datum/light_source/proc/recalc_corner(datum/lighting_corner/corner)
	LAZYINITLIST(effect_str)
	if (effect_str[corner]) // Already have one.
		REMOVE_CORNER(corner)
		effect_str[corner] = 0

	APPLY_CORNER(corner)
	effect_str[corner] = .

/datum/light_source/proc/update_corners()
	var/update = FALSE
	var/atom/source_atom = src.source_atom

	if (QDELETED(source_atom))
		qdel(src)
		return

	if (source_atom.light_power != light_power)
		light_power = source_atom.light_power
		update = TRUE

	if (source_atom.light_range != light_range)
		light_range = source_atom.light_range
		update = TRUE

	if (!top_atom)
		top_atom = source_atom
		update = TRUE

	if (!light_range || !light_power)
		qdel(src)
		return

	if (isturf(top_atom))
		if (source_turf != top_atom)
			source_turf = top_atom
			pixel_turf = source_turf
			update = TRUE
	else if (top_atom.loc != source_turf)
		source_turf = top_atom.loc
		pixel_turf = get_turf_pixel(top_atom)
		update = TRUE
	else
		var/pixel_loc = get_turf_pixel(top_atom)
		if (pixel_loc != pixel_turf)
			pixel_turf = pixel_loc
			update = TRUE

	if (!isturf(source_turf))
		if (applied)
			remove_lum()
		return

	if (light_range && light_power && !applied)
		update = TRUE

	if (source_atom.light_color != light_color)
		light_color = source_atom.light_color
		PARSE_LIGHT_COLOR(src)
		update = TRUE

	else if (applied_lum_r != lum_r || applied_lum_g != lum_g || applied_lum_b != lum_b)
		update = TRUE

	if (update)
		needs_update = LIGHTING_CHECK_UPDATE
		applied = TRUE
	else if (needs_update == LIGHTING_CHECK_UPDATE)
		return //nothing's changed

	var/list/datum/lighting_corner/corners = list()
	var/list/turf/turfs = list()

	if (source_turf)
		var/oldlum = source_turf.luminosity
		source_turf.luminosity = CEILING(light_range, 1)
		for(var/turf/T in view(CEILING(light_range, 1), source_turf))
			if(!IS_OPAQUE_TURF(T))
				if (!T.lighting_corners_initialised)
					T.generate_missing_corners()
				corners[T.lighting_corner_NE] = 0
				corners[T.lighting_corner_SE] = 0
				corners[T.lighting_corner_SW] = 0
				corners[T.lighting_corner_NW] = 0
			turfs += T
		source_turf.luminosity = oldlum

	var/list/datum/lighting_corner/new_corners = (corners - effect_str)
	LAZYINITLIST(effect_str)
	if (needs_update == LIGHTING_VIS_UPDATE)
		for (var/datum/lighting_corner/corner as anything in new_corners)
			APPLY_CORNER(corner)
			if (. != 0)
				LAZYADD(corner.affecting, src)
				effect_str[corner] = .
	else
		for (var/datum/lighting_corner/corner as anything in new_corners)
			APPLY_CORNER(corner)
			if (. != 0)
				LAZYADD(corner.affecting, src)
				effect_str[corner] = .

		for (var/datum/lighting_corner/corner as anything in corners - new_corners) // Existing corners
			APPLY_CORNER(corner)
			if (. != 0)
				effect_str[corner] = .
			else
				LAZYREMOVE(corner.affecting, src)
				effect_str -= corner

	var/list/datum/lighting_corner/gone_corners = effect_str - corners
	for (var/datum/lighting_corner/corner as anything in gone_corners)
		REMOVE_CORNER(corner)
		LAZYREMOVE(corner.affecting, src)
	effect_str -= gone_corners

	applied_lum_r = lum_r
	applied_lum_g = lum_g
	applied_lum_b = lum_b

	UNSETEMPTY(effect_str)

#undef EFFECT_UPDATE
#undef LUM_FALLOFF
#undef REMOVE_CORNER
#undef APPLY_CORNER


code:
// Causes any affecting light sources to be queued for a visibility update, for example a door got opened.
/turf/proc/reconsider_lights()
	lighting_corner_NE?.vis_update()
	lighting_corner_SE?.vis_update()
	lighting_corner_SW?.vis_update()
	lighting_corner_NW?.vis_update()

/turf/proc/lighting_clear_overlay()
	if (lighting_object)
		qdel(lighting_object, force=TRUE)

// Builds a lighting object for us, but only if our area is dynamic.
/turf/proc/lighting_build_overlay()
	if (lighting_object)
		qdel(lighting_object, force=TRUE) //Shitty fix for lighting objects persisting after death

	new/datum/lighting_object(src)

// Used to get a scaled lumcount.
/turf/proc/get_lumcount(minlum = 0, maxlum = 1)
	if (!lighting_object)
		return 1

	var/totallums = 0
	var/datum/lighting_corner/L
	L = lighting_corner_NE
	if (L)
		totallums += L.lum_r + L.lum_b + L.lum_g
	L = lighting_corner_SE
	if (L)
		totallums += L.lum_r + L.lum_b + L.lum_g
	L = lighting_corner_SW
	if (L)
		totallums += L.lum_r + L.lum_b + L.lum_g
	L = lighting_corner_NW
	if (L)
		totallums += L.lum_r + L.lum_b + L.lum_g

	totallums /= 12 // 4 corners, each with 3 channels, get the average.

	totallums = (totallums - minlum) / (maxlum - minlum)

	totallums += dynamic_lumcount

	return CLAMP01(totallums)

// Returns a boolean whether the turf is on soft lighting.
// Soft lighting being the threshold at which point the overlay considers
// itself as too dark to allow sight and see_in_dark becomes useful.
// So basically if this returns true the tile is unlit black.
/turf/proc/is_softly_lit()
	if (!lighting_object)
		return FALSE

	return !(luminosity || dynamic_lumcount)

///Proc to add movable sources of opacity on the turf and let it handle lighting code.
/turf/proc/add_opacity_source(atom/movable/new_source)
	LAZYADD(opacity_sources, new_source)
	if(opacity)
		return
	recalculate_directional_opacity()

///Proc to remove movable sources of opacity on the turf and let it handle lighting code.
/turf/proc/remove_opacity_source(atom/movable/old_source)
	LAZYREMOVE(opacity_sources, old_source)
	if(opacity) //Still opaque, no need to worry on updating.
		return
	recalculate_directional_opacity()

///Calculate on which directions this turfs block view.
/turf/proc/recalculate_directional_opacity()
	. = directional_opacity
	if(opacity)
		directional_opacity = ALL_CARDINALS
		if(. != directional_opacity)
			reconsider_lights()
		return
	directional_opacity = NONE
	for(var/atom/movable/opacity_source as anything in opacity_sources)
		if(opacity_source.flags_1 & ON_BORDER_1)
			directional_opacity |= opacity_source.dir
		else //If fulltile and opaque, then the whole tile blocks view, no need to continue checking.
			directional_opacity = ALL_CARDINALS
			break
	if(. != directional_opacity && (. == ALL_CARDINALS || directional_opacity == ALL_CARDINALS))
		reconsider_lights() //The lighting system only cares whether the tile is fully concealed from all directions or not.

/turf/proc/change_area(area/old_area, area/new_area)
	if(SSlighting.initialized)
		if (new_area.static_lighting != old_area.static_lighting)
			if (new_area.static_lighting)
				lighting_build_overlay()
			else
				lighting_clear_overlay()
	//Inherit overlay of new area
	if(old_area.lighting_effect)
		cut_overlay(old_area.lighting_effect)
	if(new_area.lighting_effect)
		add_overlay(new_area.lighting_effect)

/turf/proc/generate_missing_corners()
	if (!lighting_corner_NE)
		lighting_corner_NE = new/datum/lighting_corner(src, NORTH|EAST)

	if (!lighting_corner_SE)
		lighting_corner_SE = new/datum/lighting_corner(src, SOUTH|EAST)

	if (!lighting_corner_SW)
		lighting_corner_SW = new/datum/lighting_corner(src, SOUTH|WEST)

	if (!lighting_corner_NW)
		lighting_corner_NW = new/datum/lighting_corner(src, NORTH|WEST)

	lighting_corners_initialised = TRUE


code:
GLOBAL_DATUM_INIT(fullbright_overlay, /mutable_appearance, create_fullbright_overlay())

/proc/create_fullbright_overlay()
	var/mutable_appearance/lighting_effect = mutable_appearance('icons/effects/alphacolors.dmi', "white")
	lighting_effect.plane = LIGHTING_PLANE
	lighting_effect.layer = LIGHTING_PRIMARY_LAYER
	lighting_effect.blend_mode = BLEND_ADD
	return lighting_effect

/area
	///Whether this area allows static lighting and thus loads the lighting objects
	var/static_lighting = TRUE

//Non static lighting areas.
//Any lighting area that wont support static lights.
//These areas will NOT have corners generated.

///regenerates lighting objects for turfs in this area, primary use is VV changes
/area/proc/create_area_lighting_objects()
	for(var/turf/T in src)
		if(T.always_lit)
			continue
		T.lighting_build_overlay()
		CHECK_TICK

///Removes lighting objects from turfs in this area if we have them, primary use is VV changes
/area/proc/remove_area_lighting_objects()
	for(var/turf/T in src)
		if(T.always_lit)
			continue
		T.lighting_clear_overlay()
		CHECK_TICK


code:
///how many people can play mafia without issues (running out of spawns, procs not expecting more than this amount of people, etc)
#define MAFIA_MAX_PLAYER_COUNT 12

#define MAFIA_TEAM_TOWN "town"
#define MAFIA_TEAM_MAFIA "mafia"
#define MAFIA_TEAM_SOLO "solo"

//types of town roles for random setup gen
/// Add this if you don't want a role to be a choice in the selection
#define TOWN_OVERFLOW "overflow"
/// roles that learn info about others in the game (chaplain, detective, psych)
#define TOWN_INVEST "invest"
/// roles that keep other roles safe (doctor, sec officer, and weirdly enough lawyer counts)
#define TOWN_PROTECT "protect"
/// roles that are only there to kill bad guys.
#define TOWN_KILLING "killing"
/// roles that don't fit into anything else (hop)
#define TOWN_SUPPORT "support"

//other types (mafia team, neutrals)
/// normal vote kill changelings
#define MAFIA_REGULAR "regular"
/// every other changeling role that has extra abilities
#define MAFIA_SPECIAL "special"
/// role that wins solo that nobody likes
#define NEUTRAL_KILL "kill"
/// role that upsets the game aka obsessed, usually worse for town than mafia but they can vote against mafia
#define NEUTRAL_DISRUPT "disrupt"

//role flags (special status of roles like detection immune)
///to all forms of detection, shows themselves as an assistant.
#define ROLE_UNDETECTABLE (1<<0)
///has the ability to kill at night and thus, blocks the game from ending with other teams alive.
#define ROLE_CAN_KILL (1<<1)
///can only be one in a randomly generated game
#define ROLE_UNIQUE (1<<2)
///role is public to all other players in the game.
#define ROLE_REVEALED (1<<3)
///can not be defended, protected, or any other form of protection. all kills succeed no matter what.
#define ROLE_VULNERABLE (1<<4)
///cannot perform any actions that night, preselected actions fail
#define ROLE_ROLEBLOCKED (1<<5)

#define MAFIA_PHASE_SETUP 1
#define MAFIA_PHASE_DAY 2
#define MAFIA_PHASE_VOTING 3
#define MAFIA_PHASE_JUDGEMENT 4
#define MAFIA_PHASE_NIGHT 5
#define MAFIA_PHASE_VICTORY_LAP 6

#define MAFIA_ALIVE 1
#define MAFIA_DEAD 2

#define COMSIG_MAFIA_ON_VISIT "mafia_onvisit"
#define MAFIA_VISIT_INTERRUPTED 1

#define COMSIG_MAFIA_ON_KILL "mafia_onkill"
#define MAFIA_PREVENT_KILL 1

//in order of events + game end

/// when the shutters fall, before the 45 second wait and night event resolution
#define COMSIG_MAFIA_SUNDOWN "sundown"
/// after the 45 second wait, for actions that must go first
#define COMSIG_MAFIA_NIGHT_START "night_start"
/// most night actions now resolve
#define COMSIG_MAFIA_NIGHT_ACTION_PHASE "night_actions"
/// now killing happens from the roles that do that. the reason this is post action phase is to ensure doctors can protect and lawyers can block
#define COMSIG_MAFIA_NIGHT_KILL_PHASE "night_kill"
/// now undoing states like protection, actions that must happen last, etc. right before shutters raise and the day begins
#define COMSIG_MAFIA_NIGHT_END "night_end"

/// signal sent to roles when the game is confirmed ending
#define COMSIG_MAFIA_GAME_END "game_end"

/// list of ghosts who want to play mafia, every time someone enters the list it checks to see if enough are in
GLOBAL_LIST_EMPTY(mafia_signup)
/// list of ghosts who want to play mafia that have since disconnected. They are kept in the lobby, but not counted for starting a game.
GLOBAL_LIST_EMPTY(mafia_bad_signup)
/// the current global mafia game running.
GLOBAL_VAR(mafia_game)


code:
/**
 * The mafia controller handles the mafia minigame in progress.
 * It is first created when the first ghost signs up to play.
 */
/datum/mafia_controller
	///list of observers that should get game updates.
	var/list/spectators = list()
	///all roles in the game, dead or alive. check their game status if you only want living or dead.
	var/list/all_roles = list()
	///exists to speed up role retrieval, it's a dict. `player_role_lookup[player ckey]` will give you the role they play
	var/list/player_role_lookup = list()
	///what part of the game you're playing in. day phases, night phases, judgement phases, etc.
	var/phase = MAFIA_PHASE_SETUP
	///how long the game has gone on for, changes with every sunrise. day one, night one, day two, etc.
	var/turn = 0
	///if enabled, the game has fallen under half pop and is sped up
	var/speed_up = FALSE
	///for debugging and testing a full game, or adminbuse. If this is not empty, it will use this as a setup. clears when game is over
	var/list/custom_setup = list()
	///first day has no voting, and thus is shorter
	var/first_day_phase_period = 20 SECONDS
	///talk with others about the last night
	var/day_phase_period = 1 MINUTES
	///vote someone to get put on trial
	var/voting_phase_period = 30 SECONDS
	///defend yourself! don't get lynched! sometimes skipped if nobody votes.
	var/judgement_phase_period = 30 SECONDS
	///guilty or innocent, we want a bit of time for players to process the outcome of the vote
	var/judgement_lynch_period = 5 SECONDS
	///mafia talk at night and pick someone to kill, some town roles use their actions, etc etc.
	var/night_phase_period = 45 SECONDS
	///like the lynch period, players need to see what the other players in the game's roles were
	var/victory_lap_period = 20 SECONDS

	///template picked when the game starts. used for the name and desc reading
	var/datum/map_template/mafia/current_map
	///map generation tool that deletes the current map after the game finishes
	var/datum/map_generator/massdelete/map_deleter

	///Readable list of roles in current game, sent to the tgui panel for roles list > list("Psychologist x1", "Clown x2")
	var/list/current_setup_text

	///starting outfit for all mafia players. it's just a grey jumpsuit.
	var/player_outfit = /datum/outfit/mafia

	///spawn points for players, each one has a house
	var/list/landmarks = list()
	///town center for when people get put on trial
	var/town_center_landmark

	///group voting on one person, like putting people to trial or choosing who to kill as mafia
	var/list/votes = list()
	///and these (judgement_innocent_votes, judgement_abstain_votes and judgement_guilty_votes) are the judgement phase votes, aka people sorting themselves into guilty and innocent, and "eh, i don't really care" lists. whichever has more inno or guilty wins!
	var/list/judgement_abstain_votes = list()
	var/list/judgement_innocent_votes = list()
	var/list/judgement_guilty_votes = list()
	///current role on trial for the judgement phase, will die if guilty is greater than innocent
	var/datum/mafia_role/on_trial

	///current timer for phase
	var/next_phase_timer

	///used for debugging in testing (doesn't put people out of the game, some other shit i forgot, who knows just don't set this in live) honestly kinda deprecated
	var/debug = FALSE

/datum/mafia_controller/New()
	. = ..()
	GLOB.mafia_game = src
	map_deleter = new

/datum/mafia_controller/Destroy(force, ...)
	. = ..()
	GLOB.mafia_game = null
	end_game()
	qdel(map_deleter)

/**
 * Triggers at beginning of the game when there is a confirmed list of valid, ready players.
 * Creates a 100% ready game that has NOT started (no players in bodies)
 * Followed by start game
 *
 * Does the following:
 * * Picks map, and loads it
 * * Grabs landmarks if it is the first time it's loading
 * * Sets up the role list
 * * Puts players in each role randomly
 * Arguments:
 * * setup_list: list of all the datum setups (fancy list of roles) that would work for the game
 * * ready_players: list of filtered, sane players (so not playing or disconnected) for the game to put into roles
 */
/datum/mafia_controller/proc/prepare_game(setup_list,ready_players)

	var/list/possible_maps = subtypesof(/datum/map_template/mafia)
	var/turf/spawn_area = get_turf(locate(/obj/effect/landmark/mafia_game_area) in GLOB.landmarks_list)

	current_map = pick(possible_maps)
	current_map = new current_map

	if(!spawn_area)
		CRASH("No spawn area detected for Mafia!")
	var/list/bounds = current_map.load(spawn_area)
	if(!bounds)
		CRASH("Loading mafia map failed!")
	map_deleter.defineRegion(spawn_area, locate(spawn_area.x + 23,spawn_area.y + 23,spawn_area.z), replace = TRUE) //so we're ready to mass delete when round ends

	if(!landmarks.len)//we grab town center when we grab landmarks, if there is none (the first game signed up for let's grab them post load)
		for(var/obj/effect/landmark/mafia/possible_spawn in GLOB.landmarks_list)
			if(istype(possible_spawn, /obj/effect/landmark/mafia/town_center))
				town_center_landmark = possible_spawn
			else
				landmarks += possible_spawn

	current_setup_text = list()
	for(var/rtype in setup_list)
		for(var/i in 1 to setup_list[rtype])
			all_roles += new rtype(src)
		var/datum/mafia_role/rp = rtype
		current_setup_text += "[initial(rp.name)] x[setup_list[rtype]]"
	var/list/spawnpoints = landmarks.Copy()
	for(var/datum/mafia_role/role in all_roles)
		role.assigned_landmark = pick_n_take(spawnpoints)
		if(!debug)
			role.player_key = pick_n_take(ready_players)
		else
			role.player_key = pop(ready_players)

/datum/mafia_controller/proc/send_message(msg,team)
	for(var/datum/mafia_role/R in all_roles)
		if(team && R.team != team)
			continue
		to_chat(R.body,msg)
	var/team_suffix = team ? "([uppertext(team)] CHAT)" : ""
	for(var/M in GLOB.dead_mob_list)
		var/mob/spectator = M
		if(spectator.ckey in spectators) //was in current game, or spectatin' (won't send to living)
			var/link = FOLLOW_LINK(M, town_center_landmark)
			to_chat(M, "[link] MAFIA: [msg] [team_suffix]")

/**
 * The game by this point is now all set up, and so we can put people in their bodies and start the first phase.
 *
 * Does the following:
 * * Creates bodies for all of the roles with the first proc
 * * Starts the first day manually (so no timer) with the second proc
 */
/datum/mafia_controller/proc/start_game()
	create_bodies()
	start_day()

/**
 * How every day starts.
 *
 * What players do in this phase:
 * * If day one, just a small starting period to see who is in the game and check role, leading to the night phase.
 * * Otherwise, it's a longer period used to discuss events that happened during the night, leading to the voting phase.
 */
/datum/mafia_controller/proc/start_day()
	turn += 1
	phase = MAFIA_PHASE_DAY
	if(!check_victory())
		if(!speed_up)//lets check if the game should be sped up, if not already.
			var/living_players = 0
			for(var/i in all_roles)
				var/datum/mafia_role/player = i
				if(player.game_status == MAFIA_ALIVE)
					living_players += 1
			if(living_players < all_roles.len / 2)
				speed_up = TRUE
				send_message("With only [living_players] living players left, the game timers have been sped up.")
				day_phase_period /= 2
				voting_phase_period /= 2
				judgement_phase_period /= 2
				judgement_lynch_period /= 2
				night_phase_period /= 2
		if(turn == 1)
			send_message(span_notice("The selected map is [current_map.name]!
[current_map.description]"))
			send_message("Day [turn] started! There is no voting on the first day. Say hello to everybody!")
			next_phase_timer = addtimer(CALLBACK(src,.proc/check_trial, FALSE),first_day_phase_period,TIMER_STOPPABLE) //no voting period = no votes = instant night
		else
			send_message("Day [turn] started! Voting will start in 1 minute.")
			next_phase_timer = addtimer(CALLBACK(src,.proc/start_voting_phase),day_phase_period,TIMER_STOPPABLE)

	SStgui.update_uis(src)

/**
 * Players have finished the discussion period, and now must put up someone to the chopping block.
 *
 * What players do in this phase:
 * * Vote on which player to put up for lynching, leading to the judgement phase.
 * * If no votes are case, the judgement phase is skipped, leading to the night phase.
 */
/datum/mafia_controller/proc/start_voting_phase()
	phase = MAFIA_PHASE_VOTING
	next_phase_timer = addtimer(CALLBACK(src, .proc/check_trial, TRUE),voting_phase_period,TIMER_STOPPABLE) //be verbose!
	send_message("Voting started! Vote for who you want to see on trial today.")
	SStgui.update_uis(src)

/**
 * Players have voted someone up, and now the person must defend themselves while the town votes innocent or guilty.
 *
 * What players do in this phase:
 * * Vote innocent or guilty, if they are not on trial.
 * * Defend themselves and wait for judgement, if they are.
 * * Leads to the lynch phase.
 * Arguments:
 * * verbose: boolean, announces whether there were votes or not. after judgement it goes back here with no voting period to end the day.
 */
/datum/mafia_controller/proc/check_trial(verbose = TRUE)
	var/datum/mafia_role/loser = get_vote_winner("Day")//, majority_of_town = TRUE)
	var/loser_votes = get_vote_count(loser,"Day")
	if(loser)
		if(loser_votes > 12)
			award_role(/datum/award/achievement/mafia/universally_hated, loser)
		send_message("[loser.body.real_name] wins the day vote, Listen to their defense and vote \"INNOCENT\" or \"GUILTY\"!")
		//refresh the lists
		judgement_abstain_votes = list()
		judgement_innocent_votes = list()
		judgement_guilty_votes = list()
		for(var/i in all_roles)
			var/datum/mafia_role/abstainee = i
			if(abstainee.game_status == MAFIA_ALIVE && abstainee != loser)
				judgement_abstain_votes += abstainee
		on_trial = loser
		on_trial.body.forceMove(get_turf(town_center_landmark))
		phase = MAFIA_PHASE_JUDGEMENT
		next_phase_timer = addtimer(CALLBACK(src, .proc/lynch),judgement_phase_period,TIMER_STOPPABLE)
		reset_votes("Day")
	else
		if(verbose)
			send_message("Not enough people have voted to put someone on trial, nobody will be lynched today.")
		if(!check_victory())
			lockdown()
	SStgui.update_uis(src)

/**
 * Players have voted innocent or guilty on the person on trial, and that person is now killed or returned home.
 *
 * What players do in this phase:
 * * r/watchpeopledie
 * * If the accused is killed, their true role is revealed to the rest of the players.
 */
/datum/mafia_controller/proc/lynch()
	for(var/i in judgement_innocent_votes)
		var/datum/mafia_role/role = i
		send_message(span_green("[role.body.real_name] voted innocent."))
	for(var/ii in judgement_abstain_votes)
		var/datum/mafia_role/role = ii
		send_message(span_comradio("[role.body.real_name] abstained."))
	for(var/iii in judgement_guilty_votes)
		var/datum/mafia_role/role = iii
		send_message(span_red("[role.body.real_name] voted guilty."))
	if(judgement_guilty_votes.len > judgement_innocent_votes.len) //strictly need majority guilty to lynch
		send_message(span_red("Guilty wins majority, [on_trial.body.real_name] has been lynched."))
		on_trial.kill(src,lynch = TRUE)
		addtimer(CALLBACK(src, .proc/send_home, on_trial),judgement_lynch_period)
	else
		send_message(span_green("Innocent wins majority, [on_trial.body.real_name] has been spared."))
		on_trial.body.forceMove(get_turf(on_trial.assigned_landmark))
	on_trial = null
	//day votes are already cleared, so this will skip the trial and check victory/lockdown/whatever else
	next_phase_timer = addtimer(CALLBACK(src, .proc/check_trial, FALSE),judgement_lynch_period,TIMER_STOPPABLE)// small pause to see the guy dead, no verbosity since we already did this

/**
 * Teenie helper proc to move players back to their home.
 * Used in the above, but also used in the debug button "send all players home"
 * Arguments:
 * * role: mafia role that is getting sent back to the game.
 */
/datum/mafia_controller/proc/send_home(datum/mafia_role/role)
	role.body.forceMove(get_turf(role.assigned_landmark))

/**
 * Checks to see if a faction (or solo antagonist) has won.
 *
 * Calculates in this order:
 * * counts up town, mafia, and solo
 * * solos can count as town members for the purposes of mafia winning
 * * sends the amount of living people to the solo antagonists, and see if they won OR block the victory of the teams
 * * checks if solos won from above, then if town, then if mafia
 * * starts the end of the game if a faction won
 * * returns TRUE if someone won the game, halting other procs from continuing in the case of a victory
 */
/datum/mafia_controller/proc/check_victory()
	//needed for achievements
	var/list/total_town = list()
	var/list/total_mafia = list()

	//voting power of town + solos (since they don't want mafia to overpower)
	var/anti_mafia_power = 0
	//voting power of mafia (greater than anti mafia power + team end not blocked = mafia victory)
	var/alive_mafia = 0
	var/list/solos_to_ask = list() //need to ask after because first round is counting team sizes
	var/list/total_victors = list() //if this list gets filled with anyone, they win. list because side antags can with with people
	var/blocked_victory = FALSE //if a solo antagonist is stopping the town or mafia from finishing the game.
	var/town_can_kill = FALSE //Town has a killing role and it cannot allow mafia to win

	///PHASE ONE: TALLY UP ALL NUMBERS OF PEOPLE STILL ALIVE

	for(var/datum/mafia_role/R in all_roles)
		switch(R.team)
			if(MAFIA_TEAM_MAFIA)
				total_mafia += R
				if(R.game_status == MAFIA_ALIVE)
					alive_mafia += R.vote_potential
			if(MAFIA_TEAM_TOWN)
				total_town += R
				if(R.game_status == MAFIA_ALIVE)
					anti_mafia_power += R.vote_potential
				if(R.role_flags & ROLE_CAN_KILL) //the game cannot autoresolve with killing roles (unless a solo wins anyways, like traitors who are immune)
					town_can_kill = TRUE
			if(MAFIA_TEAM_SOLO)
				if(R.game_status == MAFIA_ALIVE)
					anti_mafia_power += R.vote_potential
					solos_to_ask += R

	///PHASE TWO: SEND STATS TO SOLO ANTAGS, SEE IF THEY WON OR TEAMS CANNOT WIN

	for(var/datum/mafia_role/solo in solos_to_ask)
		if(solo.check_total_victory(anti_mafia_power, alive_mafia))
			total_victors += solo
		if(solo.block_team_victory(anti_mafia_power, alive_mafia))
			blocked_victory = TRUE

	//solo victories!
	var/solo_end = FALSE
	for(var/datum/mafia_role/winner in total_victors)
		send_message("!! [uppertext(winner.name)] VICTORY !!")
		award_role(winner.winner_award, winner)
		solo_end = TRUE
	if(solo_end)
		start_the_end()
		return TRUE
	if(blocked_victory)
		return FALSE
	if(alive_mafia == 0)
		for(var/datum/mafia_role/townie in total_town)
			award_role(townie.winner_award, townie)
		start_the_end("!! TOWN VICTORY !!")
		return TRUE
	else if(alive_mafia >= anti_mafia_power && !town_can_kill)
		start_the_end("!! MAFIA VICTORY !!")
		for(var/datum/mafia_role/changeling in total_mafia)
			award_role(changeling.winner_award, changeling)
		return TRUE

/**
 * Lets the game award roles with all their checks and sanity, prevents achievements given out for debug games
 *
 * Arguments:
 * * award: path of the award
 * * role: mafia_role datum to reward.
 */
/datum/mafia_controller/proc/award_role(award, datum/mafia_role/rewarded)
	if(custom_setup.len)
		return
	var/client/role_client = GLOB.directory[rewarded.player_key]
	role_client?.give_award(award, rewarded.body)

/**
 * The end of the game is in two procs, because we want a bit of time for players to see eachothers roles.
 * Because of how check_victory works, the game is halted in other places by this point.
 *
 * What players do in this phase:
 * * See everyone's role postgame
 * * See who won the game
 * Arguments:
 * * message: string, if non-null it sends it to all players. used to announce team victories while solos are handled in check victory
 */
/datum/mafia_controller/proc/start_the_end(message)
	SEND_SIGNAL(src,COMSIG_MAFIA_GAME_END)
	if(message)
		send_message(message)
	for(var/datum/mafia_role/R in all_roles)
		R.reveal_role(src)
	phase = MAFIA_PHASE_VICTORY_LAP
	next_phase_timer = addtimer(CALLBACK(src,.proc/end_game),victory_lap_period,TIMER_STOPPABLE)

/**
 * Cleans up the game, resetting variables back to the beginning and removing the map with the generator.
 */
/datum/mafia_controller/proc/end_game()
	map_deleter.generate() //remove the map, it will be loaded at the start of the next one
	QDEL_LIST(all_roles)
	current_setup_text = null
	custom_setup = list()
	turn = 0
	votes = list()

	day_phase_period = initial(day_phase_period)
	voting_phase_period = initial(voting_phase_period)
	judgement_phase_period = initial(judgement_phase_period)
	judgement_lynch_period = initial(judgement_lynch_period)
	night_phase_period = initial(night_phase_period)

	//map gen does not deal with landmarks
	QDEL_LIST(landmarks)
	QDEL_NULL(town_center_landmark)
	phase = MAFIA_PHASE_SETUP

/**
 * After the voting and judgement phases, the game goes to night shutting the windows and beginning night with a proc.
 */
/datum/mafia_controller/proc/lockdown()
	toggle_night_curtains(close=TRUE)
	start_night()

/**
 * Shuts poddoors attached to mafia.
 * Arguments:
 * * close: boolean, the state you want the curtains in.
 */
/datum/mafia_controller/proc/toggle_night_curtains(close)
	for(var/obj/machinery/door/poddoor/D in GLOB.machines) //I really dislike pathing of these
		if(D.id != "mafia") //so as to not trigger shutters on station, lol
			continue
		if(close)
			INVOKE_ASYNC(D, /obj/machinery/door/poddoor.proc/close)
		else
			INVOKE_ASYNC(D, /obj/machinery/door/poddoor.proc/open)

/**
 * The actual start of night for players. Mostly info is given at the start of the night as the end of the night is when votes and actions are submitted and tried.
 *
 * What players do in this phase:
 * * Mafia are told to begin voting on who to kill
 * * Powers that are picked during the day announce themselves right now
 */
/datum/mafia_controller/proc/start_night()
	phase = MAFIA_PHASE_NIGHT
	send_message("Night [turn] started! Lockdown will end in 45 seconds.")
	SEND_SIGNAL(src,COMSIG_MAFIA_SUNDOWN)
	next_phase_timer = addtimer(CALLBACK(src, .proc/resolve_night),night_phase_period,TIMER_STOPPABLE)
	SStgui.update_uis(src)

/**
 * The end of the night, and a series of signals for the order of events on a night.
 *
 * Order of events, and what they mean:
 * * Start of resolve (NIGHT_START) is for activating night abilities that MUST go first
 * * Action phase (NIGHT_ACTION_PHASE) is for non-lethal day abilities
 * * Mafia then tallies votes and kills the highest voted person (note: one random voter visits that person for the purposes of roleblocking)
 * * Killing phase (NIGHT_KILL_PHASE) is for lethal night abilities
 * * End of resolve (NIGHT_END) is for cleaning up abilities that went off and i guess doing some that must go last
 * * Finally opens the curtains and calls the start of day phase, completing the cycle until check victory returns TRUE
 */
/datum/mafia_controller/proc/resolve_night()
	SEND_SIGNAL(src,COMSIG_MAFIA_NIGHT_START)
	SEND_SIGNAL(src,COMSIG_MAFIA_NIGHT_ACTION_PHASE)
	//resolve mafia kill, todo unsnowflake this
	var/datum/mafia_role/victim = get_vote_winner("Mafia")
	if(victim)
		var/datum/mafia_role/killer = get_random_voter("Mafia")
		if(!victim.can_action(src, killer, "changeling murder"))
			send_message(span_danger("[killer.body.real_name] was unable to attack [victim.body.real_name] tonight!"),MAFIA_TEAM_MAFIA)
		else
			send_message(span_danger("[killer.body.real_name] has attacked [victim.body.real_name]!"),MAFIA_TEAM_MAFIA)
			if(victim.kill(src,killer,lynch=FALSE))
				to_chat(victim.body, span_userdanger("You have been killed by a Changeling!"))
	reset_votes("Mafia")
	SEND_SIGNAL(src,COMSIG_MAFIA_NIGHT_KILL_PHASE)
	SEND_SIGNAL(src,COMSIG_MAFIA_NIGHT_END)
	toggle_night_curtains(close=FALSE)
	start_day()
	SStgui.update_uis(src)

/**
 * Proc that goes off when players vote for something with their mafia panel.
 *
 * If teams, it hides the tally overlay and only sends the vote messages to the team that is voting
 * Arguments:
 * * voter: the mafia role that is trying to vote for...
 * * target: the mafia role that is getting voted for
 * * vote_type: type of vote submitted (is this the day vote? is this the mafia night vote?)
 * * teams: see mafia team defines for what to put in, makes the messages only send to a specific team (so mafia night votes only sending messages to mafia at night)
 */
/datum/mafia_controller/proc/vote_for(datum/mafia_role/voter,datum/mafia_role/target,vote_type, teams)
	if(!votes[vote_type])
		votes[vote_type] = list()
	var/old_vote = votes[vote_type][voter]
	if(old_vote && old_vote == target)
		votes[vote_type] -= voter
	else
		votes[vote_type][voter] = target
	if(old_vote && old_vote == target)
		send_message(span_notice("[voter.body.real_name] retracts their vote for [target.body.real_name]!"), team = teams)
	else
		send_message(span_notice("[voter.body.real_name] voted for [target.body.real_name]!"),team = teams)
	if(!teams)
		target.body.update_appearance() //Update the vote display if it's a public vote
		var/datum/mafia_role/old = old_vote
		if(old)
			old.body.update_appearance()

/**
 * Clears out the votes of a certain type (day votes, mafia kill votes) while leaving others untouched
 */
/datum/mafia_controller/proc/reset_votes(vote_type)
	var/list/bodies_to_update = list()
	for(var/vote in votes[vote_type])
		var/datum/mafia_role/R = votes[vote_type][vote]
		bodies_to_update += R.body
	votes[vote_type] = list()
	for(var/mob/M in bodies_to_update)
		M.update_appearance()

/**
 * Returns how many people voted for the role, in whatever vote (day vote, night kill vote)
 * Arguments:
 * * role: the mafia role the proc tries to get the amount of votes for
 * * vote_type: the vote type (getting how many day votes were for the role, or mafia night votes for the role)
 */
/datum/mafia_controller/proc/get_vote_count(role,vote_type)
	. = 0
	for(var/v in votes[vote_type])
		var/datum/mafia_role/votee = v
		if(votes[vote_type][votee] == role)
			. += votee.vote_power

/**
 * Returns whichever role got the most votes, in whatever vote (day vote, night kill vote)
 * returns null if no votes
 * Arguments:
 * * vote_type: the vote type (getting the role that got the most day votes, or the role that got the most mafia votes)
 */
/datum/mafia_controller/proc/get_vote_winner(vote_type)
	var/list/tally = list()
	for(var/votee in votes[vote_type])
		if(!tally[votes[vote_type][votee]])
			tally[votes[vote_type][votee]] = 1
		else
			tally[votes[vote_type][votee]] += 1
	sortTim(tally,/proc/cmp_numeric_dsc,associative=TRUE)
	return length(tally) ? tally[1] : null

/**
 * Returns a random person who voted for whatever vote (day vote, night kill vote)
 * Arguments:
 * * vote_type: vote type (getting a random day voter, or mafia night voter)
 */
/datum/mafia_controller/proc/get_random_voter(vote_type)
	if(length(votes[vote_type]))
		return pick(votes[vote_type])

/**
 * Adds mutable appearances to people who get publicly voted on (so not night votes) showing how many people are picking them
 * Arguments:
 * * source: the body of the role getting the overlays
 * * overlay_list: signal var passing the overlay list of the mob
 */
/datum/mafia_controller/proc/display_votes(atom/source, list/overlay_list)
	SIGNAL_HANDLER

	if(phase != MAFIA_PHASE_VOTING)
		return
	var/v = get_vote_count(player_role_lookup[source],"Day")
	var/mutable_appearance/MA = mutable_appearance('icons/obj/mafia.dmi',"vote_[v > 12 ? "over_12" : v]")
	overlay_list += MA

/**
 * Called when the game is setting up, AFTER map is loaded but BEFORE the phase timers start. Creates and places each role's body and gives the correct player key
 *
 * Notably:
 * * Toggles godmode so the mafia players cannot kill themselves
 * * Adds signals for voting overlays, see display_votes proc
 * * gives mafia panel
 * * sends the greeting text (goals, role name, etc)
 */
/datum/mafia_controller/proc/create_bodies()
	for(var/datum/mafia_role/role in all_roles)
		var/mob/living/carbon/human/H = new(get_turf(role.assigned_landmark))
		ADD_TRAIT(H, TRAIT_NOFIRE, MAFIA_TRAIT)
		ADD_TRAIT(H, TRAIT_NOBREATH, MAFIA_TRAIT)
		ADD_TRAIT(H, TRAIT_CANNOT_CRYSTALIZE, MAFIA_TRAIT)
		H.equipOutfit(player_outfit)
		H.status_flags |= GODMODE
		RegisterSignal(H,COMSIG_ATOM_UPDATE_OVERLAYS,.proc/display_votes)
		var/datum/action/innate/mafia_panel/mafia_panel = new(null,src)
		mafia_panel.Grant(H)
		var/client/player_client = GLOB.directory[role.player_key]
		if(player_client)
			player_client.prefs.safe_transfer_prefs_to(H, is_antag = TRUE)
		role.body = H
		player_role_lookup[H] = role
		H.key = role.player_key
		role.greet()

/datum/mafia_controller/ui_data(mob/user)
	. = ..()
	switch(phase)
		if(MAFIA_PHASE_DAY,MAFIA_PHASE_VOTING,MAFIA_PHASE_JUDGEMENT)
			.["phase"] = "Day [turn]"
		if(MAFIA_PHASE_NIGHT)
			.["phase"] = "Night [turn]"
		else
			.["phase"] = "No Game"
	if(user.client?.holder)
		.["admin_controls"] = TRUE //show admin buttons to start/setup/stop
	if(phase == MAFIA_PHASE_JUDGEMENT)
		.["judgement_phase"] = TRUE //show judgement section
	else
		.["judgement_phase"] = FALSE
	var/datum/mafia_role/user_role = player_role_lookup[user]
	if(user_role)
		.["roleinfo"] = list("role" = user_role.name,"desc" = user_role.desc, "action_log" = user_role.role_notes, "hud_icon" = user_role.hud_icon, "revealed_icon" = user_role.revealed_icon)
		var/actions = list()
		for(var/action in user_role.actions)
			if(user_role.validate_action_target(src,action,null))
				actions += action
		.["actions"] = actions
		.["role_theme"] = user_role.special_theme
	else
		var/list/lobby_data = list()
		for(var/key in GLOB.mafia_signup + GLOB.mafia_bad_signup)
			var/list/lobby_member = list()
			lobby_member["name"] = key
			lobby_member["status"] = "Ready"
			if(key in GLOB.mafia_bad_signup)
				lobby_member["status"] = "Disconnected"
			lobby_member["spectating"] = "Ghost"
			if(key in spectators)
				lobby_member["spectating"] = "Spectator"
			lobby_data += list(lobby_member)
		.["lobbydata"] = lobby_data
	var/list/player_data = list()
	for(var/datum/mafia_role/R in all_roles)
		var/list/player_info = list()
		var/list/actions = list()
		if(user_role) //not observer
			for(var/action in user_role.targeted_actions)
				if(user_role.validate_action_target(src,action,R))
					actions += action
			//Awful snowflake, could use generalizing
			if(phase == MAFIA_PHASE_VOTING)
				player_info["votes"] = get_vote_count(R,"Day")
				if(R.game_status == MAFIA_ALIVE && R != user_role)
					actions += "Vote"
			if(phase == MAFIA_PHASE_NIGHT && user_role.team == MAFIA_TEAM_MAFIA && R.game_status == MAFIA_ALIVE && R.team != MAFIA_TEAM_MAFIA)
				actions += "Kill Vote"
		player_info["name"] = R.body.real_name
		player_info["ref"] = REF(R)
		player_info["actions"] = actions
		player_info["alive"] = R.game_status == MAFIA_ALIVE
		player_data += list(player_info)
	.["players"] = player_data
	.["timeleft"] = next_phase_timer ? timeleft(next_phase_timer) : 0

	//Not sure on this, should this info be visible
	.["all_roles"] = current_setup_text

/datum/mafia_controller/ui_assets(mob/user)
	return list(
		get_asset_datum(/datum/asset/spritesheet/mafia),
	)

/datum/mafia_controller/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	. = ..()
	if(.)
		return
	var/datum/mafia_role/user_role = player_role_lookup[usr]
	//Admin actions
	if(usr.client?.holder)
		switch(action)
			if("new_game")
				end_game()
				basic_setup()
			if("nuke")
				end_game()
				qdel(src)
			if("next_phase")
				var/datum/timedevent/timer = SStimer.timer_id_dict[next_phase_timer]
				if(!timer.spent)
					var/datum/callback/tc = timer.callBack
					deltimer(next_phase_timer)
					tc.InvokeAsync()
				return TRUE
			if("players_home")
				var/list/failed = list()
				for(var/datum/mafia_role/player in all_roles)
					if(!player.body)
						failed += player
						continue
					player.body.forceMove(get_turf(player.assigned_landmark))
				if(failed.len)
					to_chat(usr, "List of players who no longer had a body (if you see this, the game is runtiming anyway so just hit \"New Game\" to end it)")
					for(var/i in failed)
						var/datum/mafia_role/fail = i
						to_chat(usr, fail.player_key)
			if("debug_setup")
				var/list/debug_setup = list()
				var/list/rolelist_dict = list()
				var/done = FALSE
				for(var/p in typesof(/datum/mafia_role))
					var/datum/mafia_role/path = p
					rolelist_dict[initial(path.name) + " ([uppertext(initial(path.team))])"] = path
				rolelist_dict = list("CANCEL", "FINISH") + rolelist_dict
				while(!done)
					to_chat(usr, "You have a total player count of [assoc_value_sum(debug_setup)] in this setup.")
					var/chosen_role_name = input(usr,"Select a role!","Custom Setup Creation",rolelist_dict[1]) as null|anything in rolelist_dict
					if(chosen_role_name == "CANCEL")
						return
					if(chosen_role_name == "FINISH")
						break
					var/found_path = rolelist_dict[chosen_role_name]
					var/role_count = input(usr,"How many? Zero to cancel.","Custom Setup Creation",0) as null|num
					if(role_count > 0)
						debug_setup[found_path] = role_count
				custom_setup = debug_setup
				try_autostart()//don't worry, this fails if there's a game in progress
			if("cancel_setup")
				custom_setup = list()
	switch(action) //both living and dead
		if("mf_lookup")
			var/role_lookup = params["atype"]
			var/datum/mafia_role/helper
			for(var/datum/mafia_role/role in all_roles)
				if(role_lookup == role.name)
					helper = role
					break
			helper.show_help(usr)
	if(!user_role)//just the dead
		var/client/C = ui.user.client
		switch(action)
			if("mf_signup")
				if(!SSticker.HasRoundStarted())
					to_chat(usr, span_warning("Wait for the round to start."))
					return
				if(GLOB.mafia_signup[C.ckey])
					GLOB.mafia_signup -= C.ckey
					to_chat(usr, span_notice("You unregister from Mafia."))
					return TRUE
				else
					GLOB.mafia_signup[C.ckey] = C
					to_chat(usr, span_notice("You sign up for Mafia."))
				if(phase == MAFIA_PHASE_SETUP)
					check_signups()
					try_autostart()
				return TRUE
			if("mf_spectate")
				if(C.ckey in spectators)
					to_chat(usr, span_notice("You will no longer get messages from the game."))
					spectators -= C.ckey
				else
					to_chat(usr, span_notice("You will now get messages from the game."))
					spectators += C.ckey
				return TRUE
	if(user_role && user_role.game_status == MAFIA_DEAD)
		return
	//User actions (just living)
	switch(action)
		if("mf_action")
			if(!user_role.actions.Find(params["atype"]))
				return
			user_role.handle_action(src,params["atype"],null)
			return TRUE //vals for self-ui update
		if("mf_targ_action")
			var/datum/mafia_role/target = locate(params["target"]) in all_roles
			if(!istype(target))
				return
			switch(params["atype"])
				if("Vote")
					if(phase != MAFIA_PHASE_VOTING)
						return
					vote_for(user_role,target,vote_type="Day")
				if("Kill Vote")
					if(phase != MAFIA_PHASE_NIGHT || user_role.team != MAFIA_TEAM_MAFIA)
						return
					vote_for(user_role,target,"Mafia", MAFIA_TEAM_MAFIA)
					to_chat(user_role.body,"You will vote for [target.body.real_name] for tonights killing.")
				else
					if(!user_role.targeted_actions.Find(params["atype"]))
						return
					if(!user_role.validate_action_target(src,params["atype"],target))
						return
					user_role.handle_action(src,params["atype"],target)
			return TRUE
	if(user_role != on_trial)
		switch(action)
			if("vote_abstain")
				if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_abstain_votes))
					return
				to_chat(user_role.body,"You have decided to abstain.")
				judgement_innocent_votes -= user_role
				judgement_guilty_votes -= user_role
				judgement_abstain_votes += user_role
			if("vote_innocent")
				if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_innocent_votes))
					return
				to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as INNOCENT!")
				judgement_abstain_votes -= user_role//no fakers, and...
				judgement_guilty_votes -= user_role//no radical centrism
				judgement_innocent_votes += user_role
			if("vote_guilty")
				if(phase != MAFIA_PHASE_JUDGEMENT || (user_role in judgement_guilty_votes))
					return
				to_chat(user_role.body,"Your vote on [on_trial.body.real_name] submitted as GUILTY!")
				judgement_abstain_votes -= user_role//no fakers, and...
				judgement_innocent_votes -= user_role//no radical centrism
				judgement_guilty_votes += user_role

/datum/mafia_controller/ui_state(mob/user)
	return GLOB.always_state

/datum/mafia_controller/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, null)
	if(!ui)
		ui = new(user, src, "MafiaPanel")
		ui.set_autoupdate(FALSE)
		ui.open()

/proc/assoc_value_sum(list/L)
	. = 0
	for(var/key in L)
		. += L[key]

/**
 * Returns a semirandom setup with 12 roles. balance not guaranteed!
 *
 * please check the variables at the top of the proc to see how much of each role types it picks
 */
/datum/mafia_controller/proc/generate_random_setup()
	var/invests_left = 2
	var/protects_left = 2
	var/killings_left = 1
	var/supports_left = 2

	var/mafiareg_left = 2
	var/mafiaspe_left = 1

	// if there is one killing role, there will be less disruptors
	var/neutral_killing_role = prob(50)

	var/list/random_setup = list()
	var/list/unique_roles_added = list()
	for(var/i in 1 to MAFIA_MAX_PLAYER_COUNT) //should match the number of roles to add
		if(invests_left)
			add_setup_role(random_setup, unique_roles_added, TOWN_INVEST)
			invests_left--
		else if(protects_left)
			add_setup_role(random_setup, unique_roles_added, TOWN_PROTECT)
			protects_left--
		else if(killings_left)
			add_setup_role(random_setup, unique_roles_added, TOWN_KILLING)
			killings_left--
		else if(supports_left)
			add_setup_role(random_setup, unique_roles_added, TOWN_SUPPORT)
			supports_left--
		else if(mafiareg_left)
			add_setup_role(random_setup, unique_roles_added, MAFIA_REGULAR)
			mafiareg_left--
		else if(mafiaspe_left)
			add_setup_role(random_setup, unique_roles_added, MAFIA_SPECIAL)
			mafiaspe_left--
		else if(neutral_killing_role)
			add_setup_role(random_setup, unique_roles_added, NEUTRAL_KILL)
			neutral_killing_role--
		else
			add_setup_role(random_setup, unique_roles_added, NEUTRAL_DISRUPT)
	debug = random_setup
	return random_setup

/**
 * Helper proc that adds a random role of a type to a setup. if it doesn't exist in the setup, it adds the path to the list and otherwise bumps the path in the list up one. unique roles can only get added once.
 */
/datum/mafia_controller/proc/add_setup_role(setup_list, banned_roles, wanted_role_type)
	var/list/role_type_paths = list()
	for(var/path in typesof(/datum/mafia_role))
		var/datum/mafia_role/instance = path
		if(initial(instance.role_type) == wanted_role_type && !(path in banned_roles))
			role_type_paths += instance

	var/mafia_path = pick(role_type_paths)
	var/datum/mafia_role/mafia_path_type = mafia_path
	var/found_role
	for(var/searched_path in setup_list)
		var/datum/mafia_role/searched_path_type = searched_path
		if(initial(mafia_path_type.name) == initial(searched_path_type.name))
			found_role = searched_path
			break
	if(found_role)
		setup_list[found_role] += 1
		return
	setup_list[mafia_path] = 1
	if(initial(mafia_path_type.role_flags) & ROLE_UNIQUE) //check to see if we should no longer consider this okay to add to the game
		banned_roles += mafia_path

/**
 * Called when enough players have signed up to fill a setup. DOESN'T NECESSARILY MEAN THE GAME WILL START.
 *
 * Checks for a custom setup, if so gets the required players from that and if not it sets the player requirement to MAFIA_MAX_PLAYER_COUNT and generates one IF basic setup starts a game.
 * Checks if everyone signed up is an observer, and is still connected. If people aren't, they're removed from the list.
 * If there aren't enough players post sanity, it aborts. otherwise, it selects enough people for the game and starts preparing the game for real.
 */
/datum/mafia_controller/proc/basic_setup()
	var/req_players
	var/list/setup = custom_setup
	if(!setup.len)
		req_players = MAFIA_MAX_PLAYER_COUNT
	else
		req_players = assoc_value_sum(setup)

	//final list for all the players who will be in this game
	var/list/filtered_keys = list()
	//cuts invalid players from signups (disconnected/not a ghost)
	var/list/possible_keys = list()
	for(var/key in GLOB.mafia_signup)
		if(GLOB.directory[key])
			var/client/C = GLOB.directory[key]
			if(isobserver(C.mob))
				possible_keys += key
				continue
		GLOB.mafia_signup -= key //not valid to play when we checked so remove them from signups

	//if there were not enough players, don't start. we already trimmed the list to now hold only valid signups
	if(length(possible_keys) < req_players)
		return

	//if there were too many players, still start but only make filtered keys as big as it needs to be (cut excess)
	//also removes people who do get into final player list from the signup so they have to sign up again when game ends
	for(var/i in 1 to req_players)
		var/chosen_key = pick_n_take(possible_keys)
		filtered_keys += chosen_key
		GLOB.mafia_signup -= chosen_key
	//small message about not getting into this game for clarity on why they didn't get in
	for(var/unpicked in possible_keys)
		var/client/unpicked_client = GLOB.directory[unpicked]
		to_chat(unpicked_client, span_danger("Sorry, the starting mafia game has too many players and you were not picked."))
		to_chat(unpicked_client, span_warning("You're still signed up, getting messages from the current round, and have another chance to join when the one starting now finishes."))

	if(!setup.len) //don't actually have one yet, so generate a max player random setup. it's good to do this here instead of above so it doesn't generate one every time a game could possibly start.
		setup = generate_random_setup()
	prepare_game(setup,filtered_keys)
	start_game()

/**
 * Called when someone signs up, and sees if there are enough people in the signup list to begin.
 *
 * Only checks if everyone is actually valid to start (still connected and an observer) if there are enough players (basic_setup)
 */
/datum/mafia_controller/proc/try_autostart()
	if(phase != MAFIA_PHASE_SETUP || !(GLOB.ghost_role_flags & GHOSTROLE_MINIGAME))
		return
	if(GLOB.mafia_signup.len >= MAFIA_MAX_PLAYER_COUNT || custom_setup.len)//enough people to try and make something (or debug mode)
		basic_setup()

/**
 * Filters inactive player into a different list until they reconnect, and removes players who are no longer ghosts.
 *
 * If a disconnected player gets a non-ghost mob and reconnects, they will be first put back into mafia_signup then filtered by that.
 */
/datum/mafia_controller/proc/check_signups()
	for(var/bad_key in GLOB.mafia_bad_signup)
		if(GLOB.directory[bad_key])//they have reconnected if we can search their key and get a client
			GLOB.mafia_bad_signup -= bad_key
			GLOB.mafia_signup += bad_key
	for(var/key in GLOB.mafia_signup)
		var/client/C = GLOB.directory[key]
		if(!C)//vice versa but in a variable we use later
			GLOB.mafia_signup -= key
			GLOB.mafia_bad_signup += key
		if(!isobserver(C.mob))
			//they are back to playing the game, remove them from the signups
			GLOB.mafia_signup -= key

/datum/action/innate/mafia_panel
	name = "Mafia Panel"
	desc = "Use this to play."
	icon_icon = 'icons/obj/mafia.dmi'
	button_icon_state = "board"
	var/datum/mafia_controller/parent

/datum/action/innate/mafia_panel/New(Target,mf)
	. = ..()
	parent = mf

/datum/action/innate/mafia_panel/Activate()
	parent.ui_interact(owner)

/**
 * Creates the global datum for playing mafia games, destroys the last if that's required and returns the new.
 */
/proc/create_mafia_game()
	if(GLOB.mafia_game)
		QDEL_NULL(GLOB.mafia_game)
	var/datum/mafia_controller/MF = new()
	return MF


code:
/obj/effect/landmark/mafia_game_area //locations where mafia will be loaded by the datum
	name = "Mafia Area Spawn"
	var/game_id = "mafia"

/obj/effect/landmark/mafia
	name = "Mafia Player Spawn"
	var/game_id = "mafia"

/obj/effect/landmark/mafia/town_center
	name = "Mafia Town Center"

//for ghosts/admins
/obj/mafia_game_board
	name = "Mafia Game Board"
	icon = 'icons/obj/mafia.dmi'
	icon_state = "board"
	anchored = TRUE
	var/game_id = "mafia"
	var/datum/mafia_controller/MF

/obj/mafia_game_board/attack_ghost(mob/user)
	. = ..()
	if(!MF)
		MF = GLOB.mafia_game
	if(!MF)
		MF = create_mafia_game()
	MF.ui_interact(user)

/area/mafia
	name = "Mafia Minigame"
	icon_state = "mafia"
	static_lighting = FALSE

	base_lighting_alpha = 255
	requires_power = FALSE
	has_gravity = STANDARD_GRAVITY
	flags_1 = NONE
	area_flags = BLOCK_SUICIDE | UNIQUE_AREA

/datum/map_template/mafia
	var/description = ""

/datum/map_template/mafia/summerball
	name = "Summerball 2020"
	description = "The original, the OG. The 2020 Summer ball was where mafia came from, with this map."
	mappath = "_maps/map_files/Mafia/mafia_ball.dmm"

/datum/map_template/mafia/syndicate
	name = "Syndicate Megastation"
	description = "Yes, it's a very confusing day at the Megastation. Will the syndicate conflict resolution operatives succeed?"
	mappath = "_maps/map_files/Mafia/mafia_syndie.dmm"

/datum/map_template/mafia/lavaland
	name = "Lavaland Excursion"
	description = "The station has no idea what's going down on lavaland right now, we got changelings... traitors, and worst of all... lawyers roleblocking you every night."
	mappath = "_maps/map_files/Mafia/mafia_lavaland.dmm"

/datum/map_template/mafia/ufo
	name = "Alien Mothership"
	description = "The haunted ghost UFO tour has gone south and now it's up to our fine townies and scare seekers to kill the actual real alien changelings..."
	mappath = "_maps/map_files/Mafia/mafia_ayylmao.dmm"

/datum/map_template/mafia/spider_clan
	name = "Spider Clan Kidnapping"
	description = "New and improved spider clan kidnappings are a lot less boring and have a lot more lynching. Damn westaboos!"
	mappath = "_maps/map_files/Mafia/mafia_spiderclan.dmm"

/datum/map_template/mafia/snowy
	name = "Snowdin"
	description = "Based off of the icey moon map of the same name, the guy who reworked it pretty much did it for nothing since away missions are disabled but at least he'll get this...?"
	mappath = "_maps/map_files/Mafia/mafia_snow.dmm"

/datum/map_template/mafia/gothic
	name = "Vampire's Castle"
	description = "Vampires and changelings clash to find out who's the superior bloodsucking monster in this creepy castle map."
	mappath = "_maps/map_files/Mafia/mafia_gothic.dmm"