code:
/*
 * Transforming weapon component. For weapons that swap between states.
 * For example: Energy swords, cleaving saws, switch blades.
 *
 * Used to easily make an item that can be attack_self'd to gain force or change mode.
 *
 * Only values passed on initialize will update when the item is activated (except the icon_state).
 * The icon_state of the item will swap between "[icon_state]" and "[icon_state]_on".
 */
/datum/component/transforming
	/// Whether the weapon is transformed
	var/active = FALSE
	/// Cooldown on transforming this item back and forth
	var/transform_cooldown_time
	/// Force of the weapon when active
	var/force_on
	/// Throwforce of the weapon when active
	var/throwforce_on
	/// Throw speed of the weapon when active
	var/throw_speed_on
	/// Weight class of the weapon when active
	var/w_class_on
	/// The sharpness of the weapon when active
	var/sharpness_on
	/// Hitsound played when active
	var/hitsound_on
	/// List of the original continuous attack verbs the item has.
	var/list/attack_verb_continuous_off
	/// List of the original simple attack verbs the item has.
	var/list/attack_verb_simple_off
	/// List of continuous attack verbs used when the weapon is enabled
	var/list/attack_verb_continuous_on
	/// List of simple attack verbs used when the weapon is enabled
	var/list/attack_verb_simple_on
	/// Whether clumsy people need to succeed an RNG check to turn it on without hurting themselves
	var/clumsy_check
	/// If we get sharpened with a whetstone, save the bonus here for later use if we un/redeploy
	var/sharpened_bonus = 0
	/// Cooldown in between transforms
	COOLDOWN_DECLARE(transform_cooldown)

/datum/component/transforming/Initialize(
		start_transformed = FALSE,
		transform_cooldown_time = 0 SECONDS,
		force_on = 0,
		throwforce_on = 0,
		throw_speed_on = 2,
		sharpness_on = NONE,
		hitsound_on = 'sound/weapons/blade1.ogg',
		w_class_on = WEIGHT_CLASS_BULKY,
		clumsy_check = TRUE,
		list/attack_verb_continuous_on,
		list/attack_verb_simple_on,
		)

	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

	var/obj/item/item_parent = parent

	src.transform_cooldown_time = transform_cooldown_time
	src.force_on = force_on
	src.throwforce_on = throwforce_on
	src.throw_speed_on = throw_speed_on
	src.sharpness_on = sharpness_on
	src.hitsound_on = hitsound_on
	src.w_class_on = w_class_on
	src.clumsy_check = clumsy_check

	if(attack_verb_continuous_on)
		src.attack_verb_continuous_on = attack_verb_continuous_on
		attack_verb_continuous_off = item_parent.attack_verb_continuous
	if(attack_verb_simple_on)
		src.attack_verb_simple_on = attack_verb_simple_on
		attack_verb_simple_off = item_parent.attack_verb_simple

	if(start_transformed)
		toggle_active(parent)

/datum/component/transforming/RegisterWithParent()
	var/obj/item/item_parent = parent

	RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, .proc/on_attack_self)
	if(item_parent.sharpness || sharpness_on)
		RegisterSignal(parent, COMSIG_ITEM_SHARPEN_ACT, .proc/on_sharpen)

/datum/component/transforming/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_SHARPEN_ACT))

/*
 * Called on [COMSIG_ITEM_ATTACK_SELF].
 *
 * Check if we can transform our weapon, and if so, call [do_transform].
 * Sends signal [COMSIG_TRANSFORMING_PRE_TRANSFORM], and stops the transform action if it returns [COMPONENT_BLOCK_TRANSFORM].
 * And, if [do_transform] was successful, do a clumsy effect from [clumsy_transform_effect].
 *
 * source - source of the signal, the item being transformed / parent
 * user - the mob transforming the weapon
 */
/datum/component/transforming/proc/on_attack_self(obj/item/source, mob/user)
	SIGNAL_HANDLER

	if(!COOLDOWN_FINISHED(src, transform_cooldown))
		to_chat(user, span_warning("Wait a bit before trying to use [source] again!"))
		return

	if(SEND_SIGNAL(source, COMSIG_TRANSFORMING_PRE_TRANSFORM, user, active) & COMPONENT_BLOCK_TRANSFORM)
		return

	if(do_transform(source, user))
		clumsy_transform_effect(user)
		. = COMPONENT_CANCEL_ATTACK_CHAIN

/*
 * Transform the weapon into its alternate form, calling [toggle_active].
 *
 * Sends signal [COMSIG_TRANSFORMING_ON_TRANSFORM], and calls [default_transform_message] if it does not return [COMPONENT_NO_DEFAULT_MESSAGE].
 * Also starts the [transform_cooldown] if we have a set [transform_cooldown_time].
 *
 * source - the item being transformed / parent
 * user - the mob transforming the item
 *
 * returns TRUE.
 */
/datum/component/transforming/proc/do_transform(obj/item/source, mob/user)
	toggle_active(source)
	if(!(SEND_SIGNAL(source, COMSIG_TRANSFORMING_ON_TRANSFORM, user, active) & COMPONENT_NO_DEFAULT_MESSAGE))
		default_transform_message(source, user)

	if(isnum(transform_cooldown_time))
		COOLDOWN_START(src, transform_cooldown, transform_cooldown_time)
	if(user)
		source.add_fingerprint(user)
	return TRUE

/*
 * The default feedback message and sound effect for an item transforming.
 *
 * source - the item being transformed / parent
 * user - the mob transforming the item
 */
/datum/component/transforming/proc/default_transform_message(obj/item/source, mob/user)
	source.balloon_alert(user, "[active ? "enabled" : "disabled"] [source]")
	playsound(user ? user : source.loc, 'sound/weapons/batonextend.ogg', 50, TRUE)

/*
 * Toggle active between true and false, and call
 * either set_active or set_inactive depending on whichever state is toggled.
 *
 * source - the item being transformed / parent
 */
/datum/component/transforming/proc/toggle_active(obj/item/source)
	active = !active
	if(active)
		set_active(source)
	else
		set_inactive(source)

/*
 * Set our transformed item into its active state.
 * Updates all the values that were passed from init and the icon_state.
 *
 * source - the item being transformed / parent
 */
/datum/component/transforming/proc/set_active(obj/item/source)
	if(sharpness_on)
		source.sharpness = sharpness_on
	if(force_on)
		source.force = force_on + (source.sharpness ? sharpened_bonus : 0)
	if(throwforce_on)
		source.throwforce = throwforce_on + (source.sharpness ? sharpened_bonus : 0)
	if(throw_speed_on)
		source.throw_speed = throw_speed_on

	if(LAZYLEN(attack_verb_continuous_on))
		source.attack_verb_continuous = attack_verb_continuous_on
	if(LAZYLEN(attack_verb_simple_on))
		source.attack_verb_simple = attack_verb_simple_on

	source.hitsound = hitsound_on
	source.w_class = w_class_on
	source.icon_state = "[source.icon_state]_on"

/*
 * Set our transformed item into its inactive state.
 * Updates all the values back to the item's initial values.
 *
 * source - the item being un-transformed / parent
 */
/datum/component/transforming/proc/set_inactive(obj/item/source)
	if(sharpness_on)
		source.sharpness = initial(source.sharpness)
	if(force_on)
		source.force = initial(source.force) + (source.sharpness ? sharpened_bonus : 0)
	if(throwforce_on)
		source.throwforce = initial(source.throwforce) + (source.sharpness ? sharpened_bonus : 0)
	if(throw_speed_on)
		source.throw_speed = initial(source.throw_speed)

	if(LAZYLEN(attack_verb_continuous_on))
		source.attack_verb_continuous = attack_verb_continuous_off
	if(LAZYLEN(attack_verb_simple_off))
		source.attack_verb_simple = attack_verb_simple_off

	source.hitsound = initial(source.hitsound)
	source.w_class = initial(source.w_class)
	source.icon_state = initial(source.icon_state)

/*
 * If [clumsy_check] is set to TRUE, attempt to cause a side effect for clumsy people activating this item.
 * Called after the transform is done, meaning [active] var has already updated.
 *
 * user - the clumsy mob, transforming our item (parent)
 *
 * Returns TRUE if side effects happened, FALSE otherwise
 */
/datum/component/transforming/proc/clumsy_transform_effect(mob/living/user)
	if(!clumsy_check)
		return FALSE

	if(!HAS_TRAIT(user, TRAIT_CLUMSY))
		return FALSE

	if(active && prob(50))
		var/hurt_self_verb_simple = LAZYLEN(attack_verb_simple_on) ? pick(attack_verb_simple_on) : "hit"
		var/hurt_self_verb_continuous = LAZYLEN(attack_verb_continuous_on) ? pick(attack_verb_continuous_on) : "hits"
		user.visible_message(
			span_warning("[user] triggers [parent] while holding it backwards and [hurt_self_verb_continuous] themself, like a doofus!"),
			span_warning("You trigger [parent] while holding it backwards and [hurt_self_verb_simple] yourself, like a doofus!")
			)
		user.take_bodypart_damage(10)
		return TRUE
	return FALSE

/*
 * Called on [COMSIG_ITEM_SHARPEN_ACT].
 * We need to track our sharpened bonus here, so we correctly apply and unapply it
 * if our item's sharpness state changes from transforming.
 *
 * source - the item being sharpened / parent
 * increment - the amount of force added
 * max - the maximum force that the item can be adjusted to.
 *
 * Does not return naturally [COMPONENT_BLOCK_SHARPEN_APPLIED] as this is only to track our sharpened bonus between transformation.
 */
/datum/component/transforming/proc/on_sharpen(obj/item/source, increment, max)
	SIGNAL_HANDLER

	if(sharpened_bonus)
		return COMPONENT_BLOCK_SHARPEN_ALREADY
	if(force_on + increment > max)
		return COMPONENT_BLOCK_SHARPEN_MAXED
	sharpened_bonus = increment


code:
///makes this file more legible
#define IS_OPEN(parent) isgroundlessturf(parent)
///distance a trapdoor will accept a link request.
#define TRAPDOOR_LINKING_SEARCH_RANGE 7

/**
 * ## trapdoor component!
 *
 * component attached to floors to turn them into trapdoors, a constructable trap that when signalled drops people to the level below.
 * assembly code at the bottom of this file
 */
/datum/component/trapdoor
	///assembly tied to this trapdoor
	var/obj/item/assembly/trapdoor/assembly
	///path of the turf this should change into when the assembly is pulsed. needed for openspace trapdoors knowing what to turn back into
	var/trapdoor_turf_path

/datum/component/trapdoor/Initialize(starts_open, trapdoor_turf_path, assembly)
	if(!isopenturf(parent))
		return COMPONENT_INCOMPATIBLE

	src.assembly = assembly
	if(IS_OPEN(parent))
		openspace_trapdoor_setup(trapdoor_turf_path, assembly)
	else
		tile_trapdoor_setup(trapdoor_turf_path, assembly)

	if(starts_open)
		try_opening()

///initializing as an opened trapdoor, we need to trust that we were given the data by a closed trapdoor
/datum/component/trapdoor/proc/openspace_trapdoor_setup(trapdoor_turf_path)
	src.trapdoor_turf_path = trapdoor_turf_path

///initializing as a closed trapdoor, we need to take data from the tile we're on to give it to the open state to store
/datum/component/trapdoor/proc/tile_trapdoor_setup(trapdoor_turf_path)
	src.trapdoor_turf_path = parent.type
	if(assembly && assembly.stored_decals.len)
		reapply_all_decals()

/datum/component/trapdoor/RegisterWithParent()
	. = ..()
	RegisterSignal(parent, COMSIG_TURF_CHANGE, .proc/turf_changed_pre)
	if(!src.assembly)
		RegisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK, .proc/on_link_requested)
	else
		RegisterSignal(assembly, COMSIG_ASSEMBLY_PULSED, .proc/toggle_trapdoor)

/datum/component/trapdoor/UnregisterFromParent()
	. = ..()
	UnregisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK)
	UnregisterSignal(assembly, COMSIG_ASSEMBLY_PULSED)
	UnregisterSignal(parent, COMSIG_TURF_CHANGE)

/datum/component/trapdoor/proc/decal_detached(datum/source, description, cleanable, directional, pic)
	SIGNAL_HANDLER
	///so it adds the list to the list, not appending it to the end. thank you byond, very cool.
	assembly.stored_decals += list(list(description, cleanable, directional, pic))

/**
 * ## reapply_all_decals
 *
 * changing turfs does not bring over decals, so we must perform a little bit of element reapplication.
 */
/datum/component/trapdoor/proc/reapply_all_decals()
	for(var/list/element_data as anything in assembly.stored_decals)
		apply_decal(element_data[1], element_data[2], element_data[3], element_data[4])
	assembly.stored_decals = list()

/// small proc that takes passed arguments and drops it into a new element
/datum/component/trapdoor/proc/apply_decal(description, cleanable, directional, pic)
	parent.AddElement(/datum/element/decal, _description = description, _cleanable = cleanable, _dir = directional, _pic = pic)

///called by linking remotes to tie an assembly to the trapdoor
/datum/component/trapdoor/proc/on_link_requested(datum/source, obj/item/assembly/trapdoor/assembly)
	SIGNAL_HANDLER
	if(get_dist(parent, assembly) > TRAPDOOR_LINKING_SEARCH_RANGE)
		return
	. = LINKED_UP
	src.assembly = assembly
	assembly.linked = TRUE
	UnregisterSignal(SSdcs, COMSIG_GLOB_TRAPDOOR_LINK)
	RegisterSignal(assembly, COMSIG_ASSEMBLY_PULSED, .proc/toggle_trapdoor)

///signal called by our assembly being pulsed
/datum/component/trapdoor/proc/toggle_trapdoor(datum/source)
	SIGNAL_HANDLER
	if(!IS_OPEN(parent))
		try_opening()
	else
		try_closing()

///signal called by turf changing
/datum/component/trapdoor/proc/turf_changed_pre(datum/source, path, new_baseturfs, flags, post_change_callbacks)
	SIGNAL_HANDLER
	var/turf/open/dying_trapdoor = parent
	if((!IS_OPEN(dying_trapdoor) && !IS_OPEN(path)) || path == /turf/open/floor/plating) //not a process of the trapdoor, so this trapdoor has been destroyed
		dying_trapdoor.visible_message(span_warning("The trapdoor mechanism in [dying_trapdoor] is broken!"))
		if(assembly)
			assembly.linked = FALSE
			assembly.stored_decals.Cut()
			assembly = null
		return
	post_change_callbacks += CALLBACK(assembly, /obj/item/assembly/trapdoor.proc/carry_over_trapdoor, trapdoor_turf_path)

/**
 * ## carry_over_trapdoor
 *
 * applies the trapdoor to the new turf (created by the last trapdoor)
 * apparently callbacks with arguments on invoke and the callback itself have the callback args go first. interesting!
 */
/obj/item/assembly/trapdoor/proc/carry_over_trapdoor(trapdoor_turf_path, turf/new_turf)
	new_turf.AddComponent(/datum/component/trapdoor, FALSE, trapdoor_turf_path, src)

/**
 * ## try_opening
 *
 * small proc for opening the turf into openspace
 * there are no checks for opening a trapdoor, but closed has some
 */
/datum/component/trapdoor/proc/try_opening()
	var/turf/open/trapdoor_turf = parent
	///we want to save this turf's decals as they were right before deletion, so this is the point where we begin listening
	if(assembly)
		RegisterSignal(parent, COMSIG_TURF_DECAL_DETACHED, .proc/decal_detached)
	playsound(trapdoor_turf, 'sound/machines/trapdoor/trapdoor_open.ogg', 50)
	trapdoor_turf.visible_message(span_warning("[trapdoor_turf] swings open!"))
	trapdoor_turf.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR)

/**
 * ## try_closing
 *
 * small proc for closing the turf back into what it should be
 * trapdoor can be blocked by building things on the openspace turf
 */
/datum/component/trapdoor/proc/try_closing()
	var/turf/open/trapdoor_turf = parent
	var/obj/structure/lattice/blocking = locate() in trapdoor_turf.contents
	if(blocking)
		trapdoor_turf.visible_message(span_warning("The trapdoor mechanism in [trapdoor_turf] tries to shut, but is jammed by [blocking]!"))
		return
	playsound(trapdoor_turf, 'sound/machines/trapdoor/trapdoor_shut.ogg', 50)
	trapdoor_turf.visible_message(span_warning("The trapdoor mechanism in [trapdoor_turf] swings shut!"))
	trapdoor_turf.ChangeTurf(trapdoor_turf_path, flags = CHANGETURF_INHERIT_AIR)

#undef IS_OPEN

/obj/item/assembly/trapdoor
	name = "trapdoor controller"
	desc = "A sinister-looking controller for a trapdoor."
	icon_state = "trapdoor"
	///if the trapdoor isn't linked it will try to link on pulse, this shouldn't be spammable
	COOLDOWN_DECLARE(search_cooldown)
	///trapdoor link cooldown time here!
	var/search_cooldown_time = 10 SECONDS
	///if true, a trapdoor in the world has a reference to this assembly and is listening for when it is pulsed.
	var/linked = FALSE
	/**
	* list of lists that are arguments for readding decals when the linked trapdoor comes back. pain.
	*
	* we are storing this data FOR the trapdoor component we are linked to. kinda like a multitool.
	* format: list(list(element's description, element's cleanable, element's directional, element's pic))
	* the list will be filled with all the data of the deleting elements (when ChangeTurf is called) only when the trapdoor begins to open.
	* so any other case the elements will be changed but not recorded.
	*/
	var/list/stored_decals = list()

/obj/item/assembly/trapdoor/pulsed(radio)
	. = ..()
	if(linked)
		return
	if(!COOLDOWN_FINISHED(src, search_cooldown))
		visible_message(span_warning("[src] cannot attempt another trapdoor linkup so soon!"))
		return
	attempt_link_up()
	COOLDOWN_START(src, search_cooldown, search_cooldown_time)

/obj/item/assembly/trapdoor/proc/attempt_link_up()
	var/turf/assembly_turf = get_turf(src)
	if(!COOLDOWN_FINISHED(src, search_cooldown))
		var/timeleft = DisplayTimeText(COOLDOWN_TIMELEFT(src, search_cooldown))
		assembly_turf.visible_message(span_warning("[src] is on cooldown! Please wait [timeleft]."), vision_distance = SAMETILE_MESSAGE_RANGE)
		return
	if(SEND_GLOBAL_SIGNAL(COMSIG_GLOB_TRAPDOOR_LINK, src) & LINKED_UP)
		playsound(assembly_turf, 'sound/machines/chime.ogg', 50, TRUE)
		assembly_turf.visible_message("[src] has linked up to a nearby trapdoor! \
		You may now use it to check where the trapdoor is... be careful!", vision_distance = SAMETILE_MESSAGE_RANGE)
	else
		playsound(assembly_turf, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
		assembly_turf.visible_message(span_warning("[src] has failed to find a trapdoor nearby to link to."), vision_distance = SAMETILE_MESSAGE_RANGE)

/**
 * ## trapdoor remotes!
 *
 * Item that accepts the assembly for the internals and helps link/activate it.
 * This base type is an empty shell that needs the assembly added to it first to work.
 */
/obj/item/trapdoor_remote
	name = "trapdoor remote"
	desc = "A small machine that interfaces with a trapdoor controller for easy use."
	icon = 'icons/obj/device.dmi'
	icon_state = "trapdoor_remote"
	COOLDOWN_DECLARE(trapdoor_cooldown)
	var/trapdoor_cooldown_time = 2 SECONDS
	var/obj/item/assembly/trapdoor/internals

/obj/item/trapdoor_remote/examine(mob/user)
	. = ..()
	if(!internals)
		. += span_warning("[src] has no internals! It needs a trapdoor controller to function.")
		return
	. += span_notice("The internals can be removed with a screwdriver.")
	if(!internals.linked)
		. += span_warning("[src] is not linked to a trapdoor.")
		return
	. += span_notice("[src] is linked to a trapdoor.")
	if(!COOLDOWN_FINISHED(src, trapdoor_cooldown))
		. += span_warning("It is on a short cooldown.")

/obj/item/trapdoor_remote/screwdriver_act(mob/living/user, obj/item/tool)
	. = ..()
	if(!internals)
		to_chat(user, span_warning("[src] has no internals!"))
		return
	to_chat(user, span_notice("You pop [internals] out of [src]."))
	internals.forceMove(get_turf(src))
	internals = null

/obj/item/trapdoor_remote/attackby(obj/item/assembly/trapdoor/assembly, mob/living/user, params)
	. = ..()
	if(. || !istype(assembly))
		return
	if(internals)
		to_chat(user, span_warning("[src] already has internals!"))
		return
	to_chat(user, span_notice("You add [assembly] to [src]."))
	internals = assembly
	assembly.forceMove(src)

/obj/item/trapdoor_remote/attack_self(mob/user, modifiers)
	. = ..()
	if(.)
		return
	if(!internals)
		to_chat(user, span_warning("[src] has no internals!"))
		return
	if(!internals.linked)
		to_chat(user, span_notice("You activate [src]."))
		internals.pulsed()
		return
	if(!COOLDOWN_FINISHED(src, trapdoor_cooldown))
		to_chat(user, span_warning("[src] is on a short cooldown."))
		return
	to_chat(user, span_notice("You activate [src]."))
	playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
	icon_state = "trapdoor_pressed"
	addtimer(VARSET_CALLBACK(src, icon_state, initial(icon_state)), trapdoor_cooldown_time)
	COOLDOWN_START(src, trapdoor_cooldown, trapdoor_cooldown_time)
	internals.pulsed()

#undef TRAPDOOR_LINKING_SEARCH_RANGE

///subtype with internals already included. If you're giving a department a roundstart trapdoor, this is what you want
/obj/item/trapdoor_remote/preloaded

/obj/item/trapdoor_remote/preloaded/Initialize(mapload)
	. = ..()
	internals = new(src)

repo_name: AnalWerewolf/sundown

license: mit


code:
/**
 * Two Handed Component
 *
 * When applied to an item it will make it two handed
 *
 */
/datum/component/two_handed
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS // Only one of the component can exist on an item
	var/wielded = FALSE /// Are we holding the two handed item properly
	var/force_multiplier = 0 /// The multiplier applied to force when wielded, does not work with force_wielded, and force_unwielded
	var/force_wielded = 0 /// The force of the item when weilded
	var/force_unwielded = 0 /// The force of the item when unweilded
	var/wieldsound = FALSE /// Play sound when wielded
	var/unwieldsound = FALSE /// Play sound when unwielded
	var/attacksound = FALSE /// Play sound on attack when wielded
	var/require_twohands = FALSE /// Does it have to be held in both hands
	var/icon_wielded = FALSE /// The icon that will be used when wielded
	var/obj/item/offhand/offhand_item = null /// Reference to the offhand created for the item
	var/sharpened_increase = 0 /// The amount of increase recived from sharpening the item
/**

 * Two Handed component
 *
 * vars:
 * * require_twohands (optional) Does the item need both hands to be carried
 * * wieldsound (optional) The sound to play when wielded
 * * unwieldsound (optional) The sound to play when unwielded
 * * attacksound (optional) The sound to play when wielded and attacking
 * * force_multiplier (optional) The force multiplier when wielded, do not use with force_wielded, and force_unwielded
 * * force_wielded (optional) The force setting when the item is wielded, do not use with force_multiplier
 * * force_unwielded (optional) The force setting when the item is unwielded, do not use with force_multiplier
 * * icon_wielded (optional) The icon to be used when wielded
 */
/datum/component/two_handed/Initialize(require_twohands=FALSE, wieldsound=FALSE, unwieldsound=FALSE, attacksound=FALSE, \
										force_multiplier=0, force_wielded=0, force_unwielded=0, icon_wielded=FALSE)
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

	src.require_twohands = require_twohands
	src.wieldsound = wieldsound
	src.unwieldsound = unwieldsound
	src.attacksound = attacksound
	src.force_multiplier = force_multiplier
	src.force_wielded = force_wielded
	src.force_unwielded = force_unwielded
	src.icon_wielded = icon_wielded

	if(require_twohands)
		ADD_TRAIT(parent, TRAIT_NEEDS_TWO_HANDS, ABSTRACT_ITEM_TRAIT)

// Inherit the new values passed to the component
/datum/component/two_handed/InheritComponent(datum/component/two_handed/new_comp, original, require_twohands, wieldsound, unwieldsound, \
											force_multiplier, force_wielded, force_unwielded, icon_wielded)
	if(!original)
		return
	if(require_twohands)
		src.require_twohands = require_twohands
	if(wieldsound)
		src.wieldsound = wieldsound
	if(unwieldsound)
		src.unwieldsound = unwieldsound
	if(attacksound)
		src.attacksound = attacksound
	if(force_multiplier)
		src.force_multiplier = force_multiplier
	if(force_wielded)
		src.force_wielded = force_wielded
	if(force_unwielded)
		src.force_unwielded = force_unwielded
	if(icon_wielded)
		src.icon_wielded = icon_wielded

// register signals withthe parent item
/datum/component/two_handed/RegisterWithParent()
	RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_drop)
	RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, .proc/on_attack_self)
	RegisterSignal(parent, COMSIG_ITEM_ATTACK, .proc/on_attack)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, .proc/on_update_icon)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(parent, COMSIG_ITEM_SHARPEN_ACT, .proc/on_sharpen)

// Remove all siginals registered to the parent item
/datum/component/two_handed/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED,
								COMSIG_ITEM_DROPPED,
								COMSIG_ITEM_ATTACK_SELF,
								COMSIG_ITEM_ATTACK,
								COMSIG_ATOM_UPDATE_ICON,
								COMSIG_MOVABLE_MOVED,
								COMSIG_ITEM_SHARPEN_ACT))

/// Triggered on equip of the item containing the component
/datum/component/two_handed/proc/on_equip(datum/source, mob/user, slot)
	SIGNAL_HANDLER

	if(require_twohands && slot == ITEM_SLOT_HANDS) // force equip the item
		wield(user)
	if(!user.is_holding(parent) && wielded && !require_twohands)
		unwield(user)

/// Triggered on drop of item containing the component
/datum/component/two_handed/proc/on_drop(datum/source, mob/user)
	SIGNAL_HANDLER

	if(require_twohands) //Don't let the item fall to the ground and cause bugs if it's actually being equipped on another slot.
		unwield(user, FALSE, FALSE)
	if(wielded)
		unwield(user)
	if(source == offhand_item && !QDELETED(source))
		offhand_item = null
		qdel(source)

/// Triggered on destroy of the component's offhand
/datum/component/two_handed/proc/on_destroy(datum/source)
	SIGNAL_HANDLER
	offhand_item = null

/// Triggered on attack self of the item containing the component
/datum/component/two_handed/proc/on_attack_self(datum/source, mob/user)
	SIGNAL_HANDLER

	if(wielded)
		unwield(user)
	else if(user.is_holding(parent))
		wield(user)

/**
 * Wield the two handed item in both hands
 *
 * vars:
 * * user The mob/living/carbon that is wielding the item
 */
/datum/component/two_handed/proc/wield(mob/living/carbon/user)
	if(wielded)
		return
	if(ismonkey(user))
		if(require_twohands)
			to_chat(user, span_notice("[parent] is too heavy and cumbersome for you to carry!"))
			user.dropItemToGround(parent, force=TRUE)
		else
			to_chat(user, span_notice("It's too heavy for you to wield fully."))
		return
	if(user.get_inactive_held_item())
		if(require_twohands)
			to_chat(user, span_notice("[parent] is too cumbersome to carry in one hand!"))
			user.dropItemToGround(parent, force=TRUE)
		else
			to_chat(user, span_warning("You need your other hand to be empty!"))
		return
	if(user.usable_hands < 2)
		if(require_twohands)
			user.dropItemToGround(parent, force=TRUE)
		to_chat(user, span_warning("You don't have enough intact hands."))
		return

	// wield update status
	if(SEND_SIGNAL(parent, COMSIG_TWOHANDED_WIELD, user) & COMPONENT_TWOHANDED_BLOCK_WIELD)
		return // blocked wield from item
	wielded = TRUE
	ADD_TRAIT(parent,TRAIT_WIELDED,src)
	RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, .proc/on_swap_hands)

	// update item stats and name
	var/obj/item/parent_item = parent
	if(force_multiplier)
		parent_item.force *= force_multiplier
	else if(force_wielded)
		parent_item.force = force_wielded
	if(sharpened_increase)
		parent_item.force += sharpened_increase
	/* SEPTIC EDIT REMOVAL
	parent_item.name = "[parent_item.name] (Wielded)"
	*/
	//SEPTIC EDIT BEGIN
	parent_item.name = "wielded [parent_item.name]"
	//SEPTIC EDIT END
	parent_item.update_appearance()

	if(iscyborg(user))
		to_chat(user, span_notice("You dedicate your module to [parent]."))
	else
		to_chat(user, span_notice("You grab [parent] with both hands."))

	// Play sound if one is set
	if(wieldsound)
		playsound(parent_item.loc, wieldsound, 50, TRUE)

	// Let's reserve the other hand
	offhand_item = new(user)
	offhand_item.name = "[parent_item.name] - offhand"
	offhand_item.desc = "Your second grip on [parent_item]."
	offhand_item.wielded = TRUE
	RegisterSignal(offhand_item, COMSIG_ITEM_DROPPED, .proc/on_drop)
	RegisterSignal(offhand_item, COMSIG_PARENT_QDELETING, .proc/on_destroy)
	user.put_in_inactive_hand(offhand_item)

/**
 * Unwield the two handed item
 *
 * vars:
 * * user The mob/living/carbon that is unwielding the item
 * * show_message (option) show a message to chat on unwield
 * * can_drop (option) whether 'dropItemToGround' can be called or not.
 */
/datum/component/two_handed/proc/unwield(mob/living/carbon/user, show_message=TRUE, can_drop = TRUE)
	if(!wielded)
		return

	// wield update status
	wielded = FALSE
	UnregisterSignal(user, COMSIG_MOB_SWAP_HANDS)
	SEND_SIGNAL(parent, COMSIG_TWOHANDED_UNWIELD, user)
	REMOVE_TRAIT(parent,TRAIT_WIELDED,src)

	// update item stats
	var/obj/item/parent_item = parent
	if(sharpened_increase)
		parent_item.force -= sharpened_increase
	if(force_multiplier)
		parent_item.force /= force_multiplier
	else if(force_unwielded)
		parent_item.force = force_unwielded

	// update the items name to remove the wielded status
	/* SEPTIC EDIT REMOVAL
	var/sf = findtext(parent_item.name, " (Wielded)", -10) // 10 == length(" (Wielded)")
	if(sf)
		parent_item.name = copytext(parent_item.name, 1, sf)
	else
		parent_item.name = "[initial(parent_item.name)]"
	*/
	//SEPTIC EDIT BEGIN
	var/sf = findtext(parent_item.name, "wielded ", 1, 9)
	if(sf)
		parent_item.name = copytext(parent_item.name, 9) // 9 == length("wielded ") + 1
	else
		parent_item.name = "[initial(parent_item.name)]"
	//SEPTIC EDIT END

	// Update icons
	parent_item.update_appearance()

	if(istype(user)) // tk showed that we might not have a mob here
		if(user.get_item_by_slot(ITEM_SLOT_BACK) == parent)
			user.update_inv_back()
		else
			user.update_inv_hands()

		// if the item requires two handed drop the item on unwield
		if(require_twohands && can_drop)
			user.dropItemToGround(parent, force=TRUE)

		// Show message if requested
		if(show_message)
			if(iscyborg(user))
				to_chat(user, span_notice("You free up your module."))
			else if(require_twohands)
				to_chat(user, span_notice("You drop [parent]."))
			else
				to_chat(user, span_notice("You are now carrying [parent] with one hand."))

	// Play sound if set
	if(unwieldsound)
		playsound(parent_item.loc, unwieldsound, 50, TRUE)

	// Remove the object in the offhand
	if(offhand_item)
		UnregisterSignal(offhand_item, list(COMSIG_ITEM_DROPPED, COMSIG_PARENT_QDELETING))
		qdel(offhand_item)
	// Clear any old refrence to an item that should be gone now
	offhand_item = null

/**
 * on_attack triggers on attack with the parent item
 */
/datum/component/two_handed/proc/on_attack(obj/item/source, mob/living/target, mob/living/user)
	SIGNAL_HANDLER
	if(wielded && attacksound)
		var/obj/item/parent_item = parent
		playsound(parent_item.loc, attacksound, 50, TRUE)

/**
 * on_update_icon triggers on call to update parent items icon
 *
 * Updates the icon using icon_wielded if set
 */
/datum/component/two_handed/proc/on_update_icon(obj/item/source)
	SIGNAL_HANDLER
	if(!wielded)
		return NONE
	if(!icon_wielded)
		return NONE
	source.icon_state = icon_wielded
	return COMSIG_ATOM_NO_UPDATE_ICON_STATE

/**
 * on_moved Triggers on item moved
 */
/datum/component/two_handed/proc/on_moved(datum/source, mob/user, dir)
	SIGNAL_HANDLER

	unwield(user)

/**
 * on_swap_hands Triggers on swapping hands, blocks swap if the other hand is busy
 */
/datum/component/two_handed/proc/on_swap_hands(mob/user, obj/item/held_item)
	SIGNAL_HANDLER

	if(!held_item)
		return
	if(held_item == parent)
		return COMPONENT_BLOCK_SWAP

/**
 * on_sharpen Triggers on usage of a sharpening stone on the item
 */
/datum/component/two_handed/proc/on_sharpen(obj/item/item, amount, max_amount)
	SIGNAL_HANDLER

	if(!item)
		return COMPONENT_BLOCK_SHARPEN_BLOCKED
	if(sharpened_increase)
		return COMPONENT_BLOCK_SHARPEN_ALREADY
	var/wielded_val = 0
	if(force_multiplier)
		var/obj/item/parent_item = parent
		if(wielded)
			wielded_val = parent_item.force
		else
			wielded_val = parent_item.force * force_multiplier
	else
		wielded_val = force_wielded
	if(wielded_val > max_amount)
		return COMPONENT_BLOCK_SHARPEN_MAXED
	sharpened_increase = min(amount, (max_amount - wielded_val))
	return COMPONENT_BLOCK_SHARPEN_APPLIED

/**
 * The offhand dummy item for two handed items
 *
 */
/obj/item/offhand
	name = "offhand"
	icon_state = "offhand"
	w_class = WEIGHT_CLASS_HUGE
	item_flags = ABSTRACT
	resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
	var/wielded = FALSE // Off Hand tracking of wielded status

/obj/item/offhand/Initialize(mapload)
	. = ..()
	ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)

/obj/item/offhand/Destroy()
	wielded = FALSE
	return ..()

/obj/item/offhand/equipped(mob/user, slot)
	. = ..()
	if(wielded && !user.is_holding(src) && !QDELETED(src))
		qdel(src)


code:
/**
 * Udder component; for farm animals to generate milk.
 *
 * Used for cows, goats, gutlunches. neat!
 */
/datum/component/udder
	///abstract item for managing reagents (further down in this file)
	var/obj/item/udder/udder
	///optional proc to callback to when the udder is milked
	var/datum/callback/on_milk_callback

//udder_type and reagent_produced_typepath are typepaths, not reference
/datum/component/udder/Initialize(udder_type = /obj/item/udder, datum/callback/on_milk_callback, datum/callback/on_generate_callback, reagent_produced_typepath = /datum/reagent/consumable/milk)
	if(!isliving(parent)) //technically is possible to drop this on carbons... but you wouldn't do that to me, would you?
		return COMPONENT_INCOMPATIBLE
	udder = new udder_type(null, parent, on_generate_callback, reagent_produced_typepath)
	src.on_milk_callback = on_milk_callback

/datum/component/udder/RegisterWithParent()
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)

/datum/component/udder/UnregisterFromParent()
	QDEL_NULL(udder)
	UnregisterSignal(parent, list(COMSIG_PARENT_EXAMINE, COMSIG_PARENT_ATTACKBY))

///signal called on parent being examined
/datum/component/udder/proc/on_examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	var/mob/living/milked = parent
	if(milked.stat != CONSCIOUS)
		return //come on now

	var/udder_filled_percentage = PERCENT(udder.reagents.total_volume / udder.reagents.maximum_volume)
	switch(udder_filled_percentage)
		if(0 to 10)
			examine_list += span_notice("[parent]'s [udder] is dry.")
		if(11 to 99)
			examine_list += span_notice("[parent]'s [udder] can be milked if you have something to contain it.")
		if(100)
			examine_list += span_notice("[parent]'s [udder] is round and full, and can be milked if you have something to contain it.")

///signal called on parent being attacked with an item
/datum/component/udder/proc/on_attackby(datum/source, obj/item/milking_tool, mob/user)
	SIGNAL_HANDLER

	var/mob/living/milked = parent
	if(milked.stat == CONSCIOUS && istype(milking_tool, /obj/item/reagent_containers/glass))
		udder.milk(milking_tool, user)
		if(on_milk_callback)
			on_milk_callback.Invoke(udder.reagents.total_volume, udder.reagents.maximum_volume)
		return COMPONENT_NO_AFTERATTACK

/**
 * # udder item
 *
 * Abstract item that is held in nullspace and manages reagents. Created by udder component.
 * While perhaps reagents created by udder component COULD be managed in the mob, it would be somewhat finnicky and I actually like the abstract udders.
 */
/obj/item/udder
	name = "udder"
	///typepath of reagent produced by the udder
	var/reagent_produced_typepath = /datum/reagent/consumable/milk
	///how much the udder holds
	var/size = 50
	///mob that has the udder component
	var/mob/living/udder_mob
	///optional proc to callback to when the udder generates milk
	var/datum/callback/on_generate_callback

/obj/item/udder/Initialize(mapload, udder_mob, on_generate_callback, reagent_produced_typepath = /datum/reagent/consumable/milk)
	src.udder_mob = udder_mob
	src.on_generate_callback = on_generate_callback
	create_reagents(size, REAGENT_HOLDER_ALIVE)
	src.reagent_produced_typepath = reagent_produced_typepath
	initial_conditions()
	. = ..()

/obj/item/udder/Destroy()
	. = ..()
	STOP_PROCESSING(SSobj, src)
	udder_mob = null

/obj/item/udder/process(delta_time)
	if(udder_mob.stat != DEAD)
		generate() //callback is on generate() itself as sometimes generate does not add new reagents, or is not called via process

/**
 * Proc called on creation separate from the reagent datum creation to allow for signalled milk generation instead of processing milk generation
 * also useful for changing initial amounts in reagent holder (cows start with milk, gutlunches start empty)
 */
/obj/item/udder/proc/initial_conditions()
	reagents.add_reagent(reagent_produced_typepath, 20)
	START_PROCESSING(SSobj, src)

/**
 * Proc called every 2 seconds from SSMobs to add whatever reagent the udder is generating.
 */
/obj/item/udder/proc/generate()
	if(prob(5))
		reagents.add_reagent(reagent_produced_typepath, rand(5, 10))
		if(on_generate_callback)
			on_generate_callback.Invoke(reagents.total_volume, reagents.maximum_volume)

/**
 * Proc called from attacking the component parent with the correct item, moves reagents into the glass basically.
 *
 * Arguments:
 * * obj/item/reagent_containers/glass/milk_holder - what we are trying to transfer the reagents to
 * * mob/user - who is trying to do this
 */
/obj/item/udder/proc/milk(obj/item/reagent_containers/glass/milk_holder, mob/user)
	if(milk_holder.reagents.total_volume >= milk_holder.volume)
		to_chat(user, span_warning("[milk_holder] is full."))
		return
	var/transfered = reagents.trans_to(milk_holder, rand(5,10))
	if(transfered)
		user.visible_message(span_notice("[user] milks [src] using \the [milk_holder]."), span_notice("You milk [src] using \the [milk_holder]."))
	else
		to_chat(user, span_warning("The udder is dry. Wait a bit longer..."))

/**
 * # gutlunch udder subtype
 *
 * Used by gutlunches, and generates healing reagents instead of milk on eating gibs instead of a process. Starts empty!
 * Female gutlunches (ahem, guthens if you will) make babies when their udder is full under processing, instead of milk generation
 */
/obj/item/udder/gutlunch
	name = "nutrient sac"

/obj/item/udder/gutlunch/initial_conditions()
	if(!udder_mob)
		return
	if(udder_mob.gender == FEMALE)
		START_PROCESSING(SSobj, src)
	RegisterSignal(udder_mob, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, .proc/on_mob_attacking)

/obj/item/udder/gutlunch/process(delta_time)
	var/mob/living/simple_animal/hostile/asteroid/gutlunch/gutlunch = udder_mob
	if(reagents.total_volume != reagents.maximum_volume)
		return
	if(gutlunch.make_babies())
		reagents.clear_reagents()
		//usually this would be a callback but this is a specifically gutlunch feature so fuck it, gutlunch specific proccall
		gutlunch.regenerate_icons(reagents.total_volume, reagents.maximum_volume)

/**
 * signal called on parent attacking an atom
*/
/obj/item/udder/gutlunch/proc/on_mob_attacking(mob/living/simple_animal/hostile/gutlunch, atom/target)
	SIGNAL_HANDLER

	if(is_type_in_typecache(target, gutlunch.wanted_objects)) //we eats
		generate()
		gutlunch.visible_message(span_notice("[udder_mob] slurps up [target]."))
		qdel(target)
	return COMPONENT_HOSTILE_NO_ATTACK //there is no longer a target to attack

/obj/item/udder/gutlunch/generate()
	var/made_something = FALSE
	if(prob(60))
		reagents.add_reagent(/datum/reagent/consumable/cream, rand(2, 5))
		made_something = TRUE
	if(prob(45))
		reagents.add_reagent(/datum/reagent/medicine/salglu_solution, rand(2,5))
		made_something = TRUE
	if(made_something && on_generate_callback)
		on_generate_callback.Invoke(reagents.total_volume, reagents.maximum_volume)


code:
/datum/component/unbreakable
	COOLDOWN_DECLARE(surge_cooldown)

/datum/component/unbreakable/Initialize()
	if(!ishuman(parent))
		return COMPONENT_INCOMPATIBLE
	ADD_TRAIT(parent, TRAIT_UNBREAKABLE, INNATE_TRAIT)

/datum/component/unbreakable/Destroy(force, silent)
	REMOVE_TRAIT(parent, TRAIT_UNBREAKABLE, INNATE_TRAIT)
	return ..()

/datum/component/unbreakable/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOB_STATCHANGE, .proc/surge)

/datum/component/unbreakable/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOB_STATCHANGE)

/datum/component/unbreakable/proc/surge(mob/living/carbon/human/surged, new_stat)
	SIGNAL_HANDLER
	if(new_stat < SOFT_CRIT || new_stat >= DEAD)
		return
	if(!COOLDOWN_FINISHED(src, surge_cooldown))
		return
	COOLDOWN_START(src, surge_cooldown, 1 MINUTES)
	surged.balloon_alert(surged, "you refuse to give up!")//breaks balloon alert conventions by using a "!" for a fail message but that's okay because it's a pretty awesome moment
	surged.heal_overall_damage(15, 15, 0, BODYPART_ORGANIC)
	if(surged.reagents.get_reagent_amount(/datum/reagent/medicine/ephedrine) < 20)
		surged.reagents.add_reagent(/datum/reagent/medicine/ephedrine, 10)
	if(surged.reagents.get_reagent_amount(/datum/reagent/medicine/epinephrine) < 20)
		surged.reagents.add_reagent(/datum/reagent/medicine/epinephrine, 10)


code:
#define PEN_ROTATIONS 2

/**
 * Uplinks
 *
 * All /obj/item(s) have a hidden_uplink var. By default it's null. Give the item one with 'new(src') (it must be in it's contents). Then add 'uses.'
 * Use whatever conditionals you want to check that the user has an uplink, and then call interact() on their uplink.
 * You might also want the uplink menu to open if active. Check if the uplink is 'active' and then interact() with it.
**/
/datum/component/uplink
	dupe_mode = COMPONENT_DUPE_UNIQUE
	var/name = "syndicate uplink"
	var/active = FALSE
	var/lockable = TRUE
	var/locked = TRUE
	var/allow_restricted = TRUE
	var/telecrystals
	var/selected_cat
	var/owner = null
	var/uplink_flag
	var/datum/uplink_purchase_log/purchase_log
	var/list/uplink_items
	var/hidden_crystals = 0
	var/unlock_note
	var/unlock_code
	var/failsafe_code
	var/compact_mode = FALSE
	var/debug = FALSE
	///Instructions on how to access the uplink based on location
	var/unlock_text
	var/list/previous_attempts

/datum/component/uplink/Initialize(_owner, _lockable = TRUE, _enabled = FALSE, uplink_flag = UPLINK_TRAITORS, starting_tc = TELECRYSTALS_DEFAULT)
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/OnAttackBy)
	RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, .proc/interact)
	if(istype(parent, /obj/item/implant))
		RegisterSignal(parent, COMSIG_IMPLANT_ACTIVATED, .proc/implant_activation)
		RegisterSignal(parent, COMSIG_IMPLANT_IMPLANTING, .proc/implanting)
		RegisterSignal(parent, COMSIG_IMPLANT_OTHER, .proc/old_implant)
		RegisterSignal(parent, COMSIG_IMPLANT_EXISTING_UPLINK, .proc/new_implant)
	else if(istype(parent, /obj/item/pda))
		RegisterSignal(parent, COMSIG_PDA_CHANGE_RINGTONE, .proc/new_ringtone)
		RegisterSignal(parent, COMSIG_PDA_CHECK_DETONATE, .proc/check_detonate)
	else if(istype(parent, /obj/item/radio))
		RegisterSignal(parent, COMSIG_RADIO_NEW_FREQUENCY, .proc/new_frequency)
	else if(istype(parent, /obj/item/pen))
		RegisterSignal(parent, COMSIG_PEN_ROTATED, .proc/pen_rotation)
	if(_owner)
		owner = _owner
		LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
		if(GLOB.uplink_purchase_logs_by_key[owner])
			purchase_log = GLOB.uplink_purchase_logs_by_key[owner]
		else
			purchase_log = new(owner, src)
	lockable = _lockable
	active = _enabled
	src.uplink_flag = uplink_flag
	update_items()
	telecrystals = starting_tc
	if(!lockable)
		active = TRUE
		locked = FALSE

	previous_attempts = list()

/datum/component/uplink/InheritComponent(datum/component/uplink/U)
	lockable |= U.lockable
	active |= U.active
	uplink_flag |= U.uplink_flag
	telecrystals += U.telecrystals
	if(purchase_log && U.purchase_log)
		purchase_log.MergeWithAndDel(U.purchase_log)

/datum/component/uplink/Destroy()
	purchase_log = null
	return ..()

/datum/component/uplink/proc/update_items(user)
	var/updated_items
	updated_items = get_uplink_items(uplink_flag, TRUE, allow_restricted)
	update_sales(updated_items)
	update_special_equipment(user, updated_items)
	uplink_items = updated_items

/datum/component/uplink/proc/update_sales(updated_items)
	var/discount_categories = list("Discounted Gear", "Discounted Team Gear", "Limited Stock Team Gear")
	if (uplink_items == null)
		return
	for (var/category in discount_categories) // Makes sure discounted items aren't renewed or replaced
		if (uplink_items[category] != null && updated_items[category] != null)
			updated_items[category] = uplink_items[category]

/datum/component/uplink/proc/update_special_equipment(mob/user, updated_items)
	if(!user?.mind?.failed_special_equipment)
		return
	for(var/obj/item/equipment_path as anything in user.mind.failed_special_equipment)
		var/datum/uplink_item/special_equipment/equipment_uplink_item = new
		if(!updated_items[equipment_uplink_item.category])
			updated_items[equipment_uplink_item.category] = list()
		var/list/name_words = splittext(initial(equipment_path.name), " ")
		var/capitalized_name
		for(var/i in 1 to name_words.len)
			name_words[i] = capitalize(name_words[i])
		capitalized_name = name_words.Join(" ")
		equipment_uplink_item.item = equipment_path
		equipment_uplink_item.name = capitalized_name
		equipment_uplink_item.desc = initial(equipment_path.desc)
		updated_items[equipment_uplink_item.category][equipment_uplink_item.name] = equipment_uplink_item

/datum/component/uplink/proc/LoadTC(mob/user, obj/item/stack/telecrystal/TC, silent = FALSE)
	if(!silent)
		to_chat(user, span_notice("You slot [TC] into [parent] and charge its internal uplink."))
	var/amt = TC.amount
	telecrystals += amt
	TC.use(amt)
	log_uplink("[key_name(user)] loaded [amt] telecrystals into [parent]'s uplink")

/datum/component/uplink/proc/OnAttackBy(datum/source, obj/item/I, mob/user)
	SIGNAL_HANDLER

	if(!active)
		return //no hitting everyone/everything just to try to slot tcs in!
	if(istype(I, /obj/item/stack/telecrystal))
		LoadTC(user, I)
	for(var/category in uplink_items)
		for(var/item in uplink_items[category])
			var/datum/uplink_item/UI = uplink_items[category][item]
			var/path = UI.refund_path || UI.item
			var/cost = UI.refund_amount || UI.cost
			if(I.type == path && UI.refundable && I.check_uplink_validity())
				telecrystals += cost
				log_uplink("[key_name(user)] refunded [UI] for [cost] telecrystals using [parent]'s uplink")
				if(purchase_log)
					purchase_log.total_spent -= cost
				to_chat(user, span_notice("[I] refunded."))
				qdel(I)
				return

/datum/component/uplink/proc/interact(datum/source, mob/user)
	SIGNAL_HANDLER

	if(locked)
		return
	active = TRUE
	update_items(user)
	if(user)
		INVOKE_ASYNC(src, .proc/ui_interact, user)
	// an unlocked uplink blocks also opening the PDA or headset menu
	return COMPONENT_CANCEL_ATTACK_CHAIN

/datum/component/uplink/ui_state(mob/user)
	return GLOB.inventory_state

/datum/component/uplink/ui_interact(mob/user, datum/tgui/ui)
	active = TRUE
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "Uplink", name)
		// This UI is only ever opened by one person,
		// and never is updated outside of user input.
		ui.set_autoupdate(FALSE)
		ui.open()

/datum/component/uplink/ui_data(mob/user)
	if(!user.mind)
		return
	var/list/data = list()
	data["telecrystals"] = telecrystals
	data["lockable"] = lockable
	data["compactMode"] = compact_mode
	return data

/datum/component/uplink/ui_static_data(mob/user)
	var/list/data = list()
	data["categories"] = list()
	for(var/category in uplink_items)
		var/list/cat = list(
			"name" = category,
			"items" = (category == selected_cat ? list() : null))
		for(var/item in uplink_items[category])
			var/datum/uplink_item/I = uplink_items[category][item]
			if(I.limited_stock == 0)
				continue
			if(length(I.restricted_roles))
				if(!debug && !(user.mind.assigned_role.title in I.restricted_roles))
					continue
			if(I.restricted_species)
				if(ishuman(user))
					var/is_inaccessible = TRUE
					var/mob/living/carbon/human/H = user
					for(var/F in I.restricted_species)
						if(F == H.dna.species.id || debug)
							is_inaccessible = FALSE
							break
					if(is_inaccessible)
						continue
			cat["items"] += list(list(
				"name" = I.name,
				"cost" = I.cost,
				"desc" = I.desc,
			))
		data["categories"] += list(cat)
	return data

/datum/component/uplink/ui_act(action, params)
	. = ..()
	if(.)
		return
	if(!active)
		return
	switch(action)
		if("buy")
			var/item_name = params["name"]
			var/list/buyable_items = list()
			for(var/category in uplink_items)
				buyable_items += uplink_items[category]
			if(item_name in buyable_items)
				var/datum/uplink_item/I = buyable_items[item_name]
				MakePurchase(usr, I)
				return TRUE
		if("lock")
			active = FALSE
			locked = TRUE
			telecrystals += hidden_crystals
			hidden_crystals = 0
			SStgui.close_uis(src)
		if("select")
			selected_cat = params["category"]
			return TRUE
		if("compact_toggle")
			compact_mode = !compact_mode
			return TRUE

/datum/component/uplink/proc/MakePurchase(mob/user, datum/uplink_item/U)
	if(!istype(U))
		return
	if (!user || user.incapacitated())
		return

	if(telecrystals  0)
		U.limited_stock -= 1

	SSblackbox.record_feedback("nested tally", "traitor_uplink_items_bought", 1, list("[initial(U.name)]", "[U.cost]"))
	return TRUE

// Implant signal responses

/datum/component/uplink/proc/implant_activation()
	SIGNAL_HANDLER

	var/obj/item/implant/implant = parent
	locked = FALSE
	interact(null, implant.imp_in)

/datum/component/uplink/proc/implanting(datum/source, list/arguments)
	SIGNAL_HANDLER

	var/mob/user = arguments[2]
	owner = user?.key
	if(owner && !purchase_log)
		LAZYINITLIST(GLOB.uplink_purchase_logs_by_key)
		if(GLOB.uplink_purchase_logs_by_key[owner])
			purchase_log = GLOB.uplink_purchase_logs_by_key[owner]
		else
			purchase_log = new(owner, src)

/datum/component/uplink/proc/old_implant(datum/source, list/arguments, obj/item/implant/new_implant)
	SIGNAL_HANDLER

	// It kinda has to be weird like this until implants are components
	return SEND_SIGNAL(new_implant, COMSIG_IMPLANT_EXISTING_UPLINK, src)

/datum/component/uplink/proc/new_implant(datum/source, datum/component/uplink/uplink)
	SIGNAL_HANDLER

	uplink.telecrystals += telecrystals
	return COMPONENT_DELETE_NEW_IMPLANT

// PDA signal responses

/datum/component/uplink/proc/new_ringtone(datum/source, mob/living/user, new_ring_text)
	SIGNAL_HANDLER

	var/obj/item/pda/master = parent
	if(trim(lowertext(new_ring_text)) != trim(lowertext(unlock_code)))
		if(trim(lowertext(new_ring_text)) == trim(lowertext(failsafe_code)))
			failsafe(user)
			return COMPONENT_STOP_RINGTONE_CHANGE
		return
	locked = FALSE
	interact(null, user)
	to_chat(user, span_hear("The PDA softly beeps."))
	user << browse(null, "window=pda")
	master.mode = 0
	return COMPONENT_STOP_RINGTONE_CHANGE

/datum/component/uplink/proc/check_detonate()
	SIGNAL_HANDLER

	return COMPONENT_PDA_NO_DETONATE

// Radio signal responses

/datum/component/uplink/proc/new_frequency(datum/source, list/arguments)
	SIGNAL_HANDLER

	var/obj/item/radio/master = parent
	var/frequency = arguments[1]
	if(frequency != unlock_code)
		if(frequency == failsafe_code)
			failsafe(master.loc)
		return
	locked = FALSE
	if(ismob(master.loc))
		interact(null, master.loc)

// Pen signal responses

/datum/component/uplink/proc/pen_rotation(datum/source, degrees, mob/living/carbon/user)
	SIGNAL_HANDLER

	var/obj/item/pen/master = parent
	previous_attempts += degrees
	if(length(previous_attempts) > PEN_ROTATIONS)
		popleft(previous_attempts)

	if(compare_list(previous_attempts, unlock_code))
		locked = FALSE
		previous_attempts.Cut()
		master.degrees = 0
		interact(null, user)
		to_chat(user, span_warning("Your pen makes a clicking noise, before quickly rotating back to 0 degrees!"))

	else if(compare_list(previous_attempts, failsafe_code))
		failsafe(user)

/datum/component/uplink/proc/setup_unlock_code()
	unlock_code = generate_code()
	var/obj/item/P = parent
	if(istype(parent,/obj/item/pda))
		unlock_note = "Uplink Passcode: [unlock_code] ([P.name])."
	else if(istype(parent,/obj/item/radio))
		unlock_note = "Radio Frequency: [format_frequency(unlock_code)] ([P.name])."
	else if(istype(parent,/obj/item/pen))
		unlock_note = "Uplink Degrees: [english_list(unlock_code)] ([P.name])."

/datum/component/uplink/proc/generate_code()
	if(istype(parent,/obj/item/pda))
		return "[rand(100,999)] [pick(GLOB.phonetic_alphabet)]"
	else if(istype(parent,/obj/item/radio))
		return return_unused_frequency()
	else if(istype(parent,/obj/item/pen))
		var/list/L = list()
		for(var/i in 1 to PEN_ROTATIONS)
			L += rand(1, 360)
		return L

/datum/component/uplink/proc/failsafe(mob/living/carbon/user)
	if(!parent)
		return
	var/turf/T = get_turf(parent)
	if(!T)
		return
	message_admins("[ADMIN_LOOKUPFLW(user)] has triggered an uplink failsafe explosion at [AREACOORD(T)] The owner of the uplink was [ADMIN_LOOKUPFLW(owner)].")
	log_game("[key_name(user)] triggered an uplink failsafe explosion. The owner of the uplink was [key_name(owner)].")
	explosion(parent, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 3)
	qdel(parent) //Alternatively could brick the uplink.


code:
/// Opens up a USB port that can be connected to by circuits, creating registerable circuit components
/datum/component/usb_port
	/// The component types to create when something plugs in
	var/list/circuit_component_types

	/// The currently connected circuit
	var/obj/item/integrated_circuit/attached_circuit

	/// The currently connected USB cable
	var/datum/weakref/usb_cable_ref

	/// The components inside the parent
	var/list/obj/item/circuit_component/circuit_components

	/// The beam connecting the USB cable to the machine
	var/datum/beam/usb_cable_beam

	/// The current physical object that the beam is connected to and listens to.
	var/atom/movable/physical_object

/datum/component/usb_port/Initialize(list/circuit_component_types)
	if (!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	circuit_components = list()

	set_circuit_components(circuit_component_types)

/datum/component/usb_port/proc/set_circuit_components(list/components)
	var/should_register = FALSE
	if(length(circuit_components))
		UnregisterFromParent()
		should_register = TRUE
		QDEL_LIST(circuit_components)

	for(var/circuit_component in components)
		var/obj/item/circuit_component/component = circuit_component
		if(ispath(circuit_component))
			component = new circuit_component(null)
		RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_SAVE, .proc/save_component)
		circuit_components += component

	if(should_register)
		RegisterWithParent()

/datum/component/usb_port/RegisterWithParent()
	RegisterSignal(parent, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, .proc/on_atom_usb_cable_try_attach)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
	RegisterSignal(parent, COMSIG_MOVABLE_CIRCUIT_LOADED, .proc/on_load)

	for(var/obj/item/circuit_component/component as anything in circuit_components)
		component.register_usb_parent(parent)

/datum/component/usb_port/UnregisterFromParent()
	UnregisterSignal(parent, list(
		COMSIG_ATOM_USB_CABLE_TRY_ATTACH,
		COMSIG_MOVABLE_MOVED,
		COMSIG_PARENT_EXAMINE,
		COMSIG_MOVABLE_CIRCUIT_LOADED,
	))

	for(var/obj/item/circuit_component/component as anything in circuit_components)
		component.unregister_usb_parent(parent)

	unregister_circuit_signals()
	unregister_physical_signals()
	attached_circuit = null

/datum/component/usb_port/proc/save_component(datum/source, list/objects)
	SIGNAL_HANDLER
	objects += parent

/datum/component/usb_port/proc/on_load(datum/source, obj/item/integrated_circuit/circuit, list/components)
	SIGNAL_HANDLER
	var/list/components_in_list = list()
	for(var/obj/item/circuit_component/component as anything in components)
		components_in_list += component.type

	for(var/obj/item/circuit_component/component as anything in circuit_components)
		if(component.type in components_in_list)
			continue
		components += component.type
	set_circuit_components(components)
	var/obj/item/usb_cable/cable = new(circuit.drop_location())
	cable.attached_circuit = circuit

	on_atom_usb_cable_try_attach(src, cable, null)

/datum/component/usb_port/Destroy()
	QDEL_LIST(circuit_components)
	QDEL_NULL(usb_cable_beam)

	attached_circuit = null
	usb_cable_ref = null

	return ..()

/datum/component/usb_port/proc/unregister_circuit_signals()
	if (isnull(attached_circuit))
		return

	UnregisterSignal(attached_circuit, list(
		COMSIG_CIRCUIT_SHELL_REMOVED,
		COMSIG_PARENT_QDELETING,
		COMSIG_CIRCUIT_SET_SHELL,
	))

/datum/component/usb_port/proc/unregister_physical_signals()
	if (isnull(physical_object))
		return

	UnregisterSignal(physical_object, list(
		COMSIG_MOVABLE_MOVED,
		COMSIG_PARENT_EXAMINE,
	))

/datum/component/usb_port/proc/attach_circuit_components(obj/item/integrated_circuit/circuitboard)
	for(var/obj/item/circuit_component/component as anything in circuit_components)
		circuitboard.add_component(component)
		RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_REMOVED, .proc/on_circuit_component_removed)

/datum/component/usb_port/proc/on_examine(datum/source, mob/user, list/examine_text)
	SIGNAL_HANDLER

	if (isnull(attached_circuit))
		examine_text += span_notice("There is a USB port on the front.")
	else
		examine_text += span_notice("[attached_circuit.shell || attached_circuit] is connected to [parent.p_them()] by a USB port.")

/datum/component/usb_port/proc/on_examine_shell(datum/source, mob/user, list/examine_text)
	SIGNAL_HANDLER

	examine_text += span_notice("[source.p_they(TRUE)] [source.p_are()] attached to [parent] with a USB cable.")

/datum/component/usb_port/proc/on_atom_usb_cable_try_attach(datum/source, obj/item/usb_cable/connecting_cable, mob/user)
	SIGNAL_HANDLER

	var/atom/atom_parent = parent

	if (!isnull(attached_circuit))
		if(user)
			atom_parent.balloon_alert(user, "usb already connected")
		return COMSIG_CANCEL_USB_CABLE_ATTACK

	if (isnull(connecting_cable.attached_circuit))
		if(user)
			connecting_cable.balloon_alert(user, "connect to a shell first")
		return COMSIG_CANCEL_USB_CABLE_ATTACK

	if (!IN_GIVEN_RANGE(connecting_cable.attached_circuit, parent, USB_CABLE_MAX_RANGE))
		if(user)
			connecting_cable.balloon_alert(user, "too far away")
		return COMSIG_CANCEL_USB_CABLE_ATTACK

	usb_cable_ref = WEAKREF(connecting_cable)
	attached_circuit = connecting_cable.attached_circuit

	connecting_cable.forceMove(attached_circuit)
	attach_circuit_components(attached_circuit)
	if(user)
		attached_circuit.interact(user)

	var/new_physical_object = attached_circuit.shell
	if(!new_physical_object)
		new_physical_object = attached_circuit

	RegisterSignal(attached_circuit, COMSIG_CIRCUIT_SHELL_REMOVED, .proc/on_circuit_shell_removed)
	RegisterSignal(attached_circuit, COMSIG_PARENT_QDELETING, .proc/on_circuit_deleting)
	RegisterSignal(attached_circuit, COMSIG_CIRCUIT_SET_SHELL, .proc/on_set_shell)
	set_physical_object(new_physical_object)

	return COMSIG_USB_CABLE_ATTACHED

/datum/component/usb_port/proc/set_physical_object(atom/movable/new_physical_object)
	if(physical_object)
		unregister_physical_signals()
	if(usb_cable_beam)
		QDEL_NULL(usb_cable_beam)

	var/atom/atom_parent = parent
	usb_cable_beam = atom_parent.Beam(new_physical_object, "usb_cable_beam", 'icons/obj/wiremod.dmi')

	RegisterSignal(new_physical_object, COMSIG_MOVABLE_MOVED, .proc/on_moved)
	RegisterSignal(new_physical_object, COMSIG_PARENT_EXAMINE, .proc/on_examine_shell)
	physical_object = new_physical_object

// Adds support for loading circuits without shells but with usb cables, or loading circuits with shells because the shells might not load first.
/datum/component/usb_port/proc/on_set_shell(datum/source, atom/movable/new_shell)
	SIGNAL_HANDLER
	set_physical_object(new_shell)

/datum/component/usb_port/proc/on_moved()
	SIGNAL_HANDLER

	if (isnull(attached_circuit))
		return

	if (IN_GIVEN_RANGE(attached_circuit, parent, USB_CABLE_MAX_RANGE))
		return

	detach()

/datum/component/usb_port/proc/on_circuit_deleting()
	SIGNAL_HANDLER
	detach()
	qdel(usb_cable_ref)

/datum/component/usb_port/proc/on_circuit_component_removed(datum/source)
	SIGNAL_HANDLER
	detach()

/datum/component/usb_port/proc/on_circuit_shell_removed()
	SIGNAL_HANDLER
	detach()

/datum/component/usb_port/proc/detach()
	var/obj/item/usb_cable/usb_cable = usb_cable_ref?.resolve()
	if (isnull(usb_cable))
		return

	for(var/obj/item/circuit_component/component as anything in circuit_components)
		UnregisterSignal(component, COMSIG_CIRCUIT_COMPONENT_REMOVED)
		attached_circuit.remove_component(component)
		component.moveToNullspace()

	unregister_circuit_signals()
	unregister_physical_signals()

	var/atom/atom_parent = parent
	usb_cable.forceMove(atom_parent.drop_location())
	usb_cable.balloon_alert_to_viewers("snap")

	physical_object = null
	attached_circuit = null
	usb_cable_ref = null

	QDEL_NULL(usb_cable_beam)


code:
/**
 * # Vacuum Component
 *
 * Adds a vacuum functionality to an atom, requires a trashbag to be linked
 * using signals
 *
 */
/datum/component/vacuum
	/// The linked trash bag to vacuum trash into
	var/obj/item/storage/bag/trash/vacuum_bag

/datum/component/vacuum/Initialize(obj/item/storage/bag/trash/connected_bag = null)
	. = ..()
	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE
	if (connected_bag)
		attach_bag(null, connected_bag)
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/suck)
	RegisterSignal(parent, COMSIG_VACUUM_BAG_ATTACH, .proc/attach_bag)
	RegisterSignal(parent, COMSIG_VACUUM_BAG_DETACH, .proc/detach_bag)

/**
 * Called when parent moves, deligates vacuuming functionality
 *
 * Arguments:
 * * suckee - The source of the signal
 */
/datum/component/vacuum/proc/suck(datum/suckee)
	SIGNAL_HANDLER

	// get tile to suck on
	var/atom/movable/AM = suckee
	var/turf/tile = AM.loc
	if (!isturf(tile))
		return

	// no bag attached, don't bother
	if (!vacuum_bag)
		return

	// suck the things
	INVOKE_ASYNC(src, .proc/suck_items, tile)

/**
 * Sucks up items as possible from a provided turf into the connected trash bag
 *
 * Arguments:
 * * tile - The tile upon which to vacuum up items
 */
/datum/component/vacuum/proc/suck_items(turf/tile)
	var/sucked = FALSE
	for (var/potential_item in tile)
		if (!isitem(potential_item))
			continue
		var/obj/item/item = potential_item
		if (vacuum_bag?.attackby(item))
			sucked = TRUE // track that we successfully sucked up something

	// if we did indeed suck up something, play a funny noise
	if (sucked)
		playsound(parent, "rustle", 50, TRUE, -5)

/**
 * Handler for when a new trash bag is attached
 *
 * Arguments:
 * * source - The source of the signal
 * * new_bag - The new bag being installed
 */
/datum/component/vacuum/proc/attach_bag(datum/source, obj/item/storage/bag/trash/new_bag)
	SIGNAL_HANDLER

	vacuum_bag = new_bag
	RegisterSignal(new_bag, COMSIG_PARENT_QDELETING, .proc/detach_bag)

/**
 * Handler for when a trash bag is detached
 *
 * Arguments:
 * * source - The source of the signal
 */
/datum/component/vacuum/proc/detach_bag(datum/source)
	SIGNAL_HANDLER
	if (vacuum_bag) // null check to avoid runtime on bag being deleted then sending detach as a result from parent
		UnregisterSignal(vacuum_bag, COMSIG_PARENT_QDELETING)
		vacuum_bag = null


code:
// A dummy parent type used for easily making components that target an item's wearer rather than the item itself.

/datum/component/wearertargeting
	var/list/valid_slots = list()
	var/list/signals = list()
	var/proctype = .proc/pass
	var/mobtype = /mob/living

/datum/component/wearertargeting/Initialize()
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE
	RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_drop)

/datum/component/wearertargeting/proc/on_equip(datum/source, mob/equipper, slot)
	SIGNAL_HANDLER

	if((slot in valid_slots) && istype(equipper, mobtype))
		RegisterSignal(equipper, signals, proctype, TRUE)
	else
		UnregisterSignal(equipper, signals)

/datum/component/wearertargeting/proc/on_drop(datum/source, mob/user)
	SIGNAL_HANDLER

	UnregisterSignal(user, signals)


code:
/datum/component/wet_floor
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	can_transfer = TRUE
	var/highest_strength = TURF_DRY
	var/lube_flags = NONE //why do we have this?
	var/list/time_left_list //In deciseconds.
	var/static/mutable_appearance/permafrost_overlay = mutable_appearance('icons/effects/water.dmi', "ice_floor")
	var/static/mutable_appearance/ice_overlay = mutable_appearance('icons/turf/overlays.dmi', "snowfloor")
	var/static/mutable_appearance/water_overlay = mutable_appearance('icons/effects/water.dmi', "wet_floor_static")
	var/static/mutable_appearance/generic_turf_overlay = mutable_appearance('icons/effects/water.dmi', "wet_static")
	var/current_overlay
	var/permanent = FALSE
	var/last_process = 0

/datum/component/wet_floor/InheritComponent(datum/newcomp, orig, strength, duration_minimum, duration_add, duration_maximum, _permanent)
	if(!newcomp) //We are getting passed the arguments of a would-be new component, but not a new component
		add_wet(arglist(args.Copy(3)))
	else //We are being passed in a full blown component
		var/datum/component/wet_floor/WF = newcomp //Lets make an assumption
		if(WF.gc()) //See if it's even valid, still. Also does LAZYLEN and stuff for us.
			CRASH("Wet floor component tried to inherit another, but the other was able to garbage collect while being inherited! What a waste of time!")
		for(var/i in WF.time_left_list)
			add_wet(text2num(i), WF.time_left_list[i])

/datum/component/wet_floor/Initialize(strength, duration_minimum, duration_add, duration_maximum, _permanent = FALSE)
	if(!isopenturf(parent))
		return COMPONENT_INCOMPATIBLE
	add_wet(strength, duration_minimum, duration_add, duration_maximum)
	permanent = _permanent
	if(!permanent)
		START_PROCESSING(SSwet_floors, src)
	addtimer(CALLBACK(src, .proc/gc, TRUE), 1) //GC after initialization.
	last_process = world.time

/datum/component/wet_floor/RegisterWithParent()
	RegisterSignal(parent, COMSIG_TURF_IS_WET, .proc/is_wet)
	RegisterSignal(parent, COMSIG_TURF_MAKE_DRY, .proc/dry)

/datum/component/wet_floor/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_TURF_IS_WET, COMSIG_TURF_MAKE_DRY))

/datum/component/wet_floor/Destroy()
	STOP_PROCESSING(SSwet_floors, src)
	var/turf/T = parent
	qdel(T.GetComponent(/datum/component/slippery))
	if(istype(T)) //If this is false there is so many things wrong with it.
		T.cut_overlay(current_overlay)
	else
		stack_trace("Warning: Wet floor component wasn't on a turf when being destroyed! This is really bad!")
	return ..()

/datum/component/wet_floor/proc/update_overlay()
	var/intended
	if(!istype(parent, /turf/open/floor))
		intended = generic_turf_overlay
	else
		switch(highest_strength)
			if(TURF_WET_PERMAFROST)
				intended = permafrost_overlay
			if(TURF_WET_ICE)
				intended = ice_overlay
			else
				intended = water_overlay
	if(current_overlay != intended)
		var/turf/T = parent
		T.cut_overlay(current_overlay)
		T.add_overlay(intended)
		current_overlay = intended

/datum/component/wet_floor/proc/AfterSlip(mob/living/L)
	if(highest_strength == TURF_WET_LUBE)
		L.set_confusion(max(L.get_confusion(), 8))

/datum/component/wet_floor/proc/update_flags()
	var/intensity
	lube_flags = NONE
	switch(highest_strength)
		if(TURF_WET_WATER)
			intensity = 60
			lube_flags = NO_SLIP_WHEN_WALKING
		if(TURF_WET_LUBE)
			intensity = 80
			lube_flags = SLIDE | GALOSHES_DONT_HELP
		if(TURF_WET_ICE)
			intensity = 120
			lube_flags = SLIDE | GALOSHES_DONT_HELP
		if(TURF_WET_PERMAFROST)
			intensity = 120
			lube_flags = SLIDE_ICE | GALOSHES_DONT_HELP
		if(TURF_WET_SUPERLUBE)
			intensity = 120
			lube_flags = SLIDE | GALOSHES_DONT_HELP | SLIP_WHEN_CRAWLING
		else
			qdel(parent.GetComponent(/datum/component/slippery))
			return

	parent.LoadComponent(/datum/component/slippery, intensity, lube_flags, CALLBACK(src, .proc/AfterSlip))

/datum/component/wet_floor/proc/dry(datum/source, strength = TURF_WET_WATER, immediate = FALSE, duration_decrease = INFINITY)
	SIGNAL_HANDLER

	for(var/i in time_left_list)
		if(text2num(i) <= strength)
			time_left_list[i] = max(0, time_left_list[i] - duration_decrease)
	if(immediate)
		check()

/datum/component/wet_floor/proc/max_time_left()
	. = 0
	for(var/i in time_left_list)
		. = max(., time_left_list[i])

/datum/component/wet_floor/process()
	var/turf/open/T = parent
	var/diff = world.time - last_process
	var/decrease = 0
	var/t = T.GetTemperature()
	switch(t)
		if(-INFINITY to T0C)
			add_wet(TURF_WET_ICE, max_time_left()) //Water freezes into ice!
		if(T0C to T0C + 100)
			decrease = ((T.air.temperature - T0C) / SSwet_floors.temperature_coeff) * (diff / SSwet_floors.time_ratio)
		if(T0C + 100 to INFINITY)
			decrease = INFINITY
	decrease = max(0, decrease)
	if((is_wet() & TURF_WET_ICE) && t > T0C) //Ice melts into water!
		for(var/obj/O in T.contents)
			if(O.obj_flags & FROZEN)
				O.make_unfrozen()
		add_wet(TURF_WET_WATER, max_time_left())
		dry(null, TURF_WET_ICE)
	dry(null, ALL, FALSE, decrease)
	check()
	last_process = world.time

/datum/component/wet_floor/proc/update_strength()
	highest_strength = 0 //Not bitflag.
	for(var/i in time_left_list)
		highest_strength = max(highest_strength, text2num(i))

/datum/component/wet_floor/proc/is_wet()
	SIGNAL_HANDLER

	. = 0
	for(var/i in time_left_list)
		. |= text2num(i)

/datum/component/wet_floor/PreTransfer()
	var/turf/O = parent
	O.cut_overlay(current_overlay)
	//That turf is no longer slippery, we're out of here
	//Slippery components don't transfer due to callbacks
	qdel(O.GetComponent(/datum/component/slippery))

/datum/component/wet_floor/PostTransfer()
	if(!isopenturf(parent))
		return COMPONENT_INCOMPATIBLE
	var/turf/T = parent
	T.add_overlay(current_overlay)
	//Make sure to add/update any slippery component on the new turf (update_flags calls LoadComponent)
	update_flags()

	//NB it's possible we get deleted after this, due to inherit

/datum/component/wet_floor/proc/add_wet(type, duration_minimum = 0, duration_add = 0, duration_maximum = MAXIMUM_WET_TIME, _permanent = FALSE)
	var/static/list/allowed_types = list(TURF_WET_WATER, TURF_WET_LUBE, TURF_WET_ICE, TURF_WET_PERMAFROST, TURF_WET_SUPERLUBE)
	if(duration_minimum <= 0 || !type)
		return FALSE
	if(type in allowed_types)
		return _do_add_wet(type, duration_minimum, duration_add, duration_maximum)
	else
		. = NONE
		for(var/i in allowed_types)
			if(!(type & i))
				continue
			. |= _do_add_wet(i, duration_minimum, duration_add, duration_maximum)
	if(_permanent)
		permanent = TRUE
		STOP_PROCESSING(SSwet_floors, src)

/datum/component/wet_floor/proc/_do_add_wet(type, duration_minimum, duration_add, duration_maximum)
	var/time = 0
	if(LAZYACCESS(time_left_list, "[type]"))
		time = clamp(LAZYACCESS(time_left_list, "[type]") + duration_add, duration_minimum, duration_maximum)
	else
		time = min(duration_minimum, duration_maximum)
	LAZYSET(time_left_list, "[type]", time)
	check(TRUE)
	return TRUE

/datum/component/wet_floor/proc/gc(on_init = FALSE)
	if(!LAZYLEN(time_left_list))
		if(on_init)
			var/turf/T = parent
			stack_trace("Warning: Wet floor component gc'd right after initialization! What a waste of time and CPU! Type = [T? T.type : "ERROR - NO PARENT"], Location = [istype(T)? AREACOORD(T) : "ERROR - INVALID PARENT"].")
		qdel(src)
		return TRUE
	return FALSE

/datum/component/wet_floor/proc/check(force_update = FALSE)
	var/changed = FALSE
	for(var/i in time_left_list)
		if(time_left_list[i] <= 0)
			time_left_list -= i
			changed = TRUE
	if(changed || force_update)
		update_strength()
		update_overlay()
		update_flags()
		gc()


code:
/datum/action/innate/dash
	name = "Dash"
	desc = "Teleport to the targeted location."
	icon_icon = 'icons/mob/actions/actions_items.dmi'
	button_icon_state = "jetboot"
	var/current_charges = 1
	var/max_charges = 1
	var/charge_rate = 250
	var/obj/item/dashing_item
	var/dash_sound = 'sound/magic/blink.ogg'
	var/recharge_sound = 'sound/magic/charge.ogg'
	var/beam_effect = "blur"
	var/phasein = /obj/effect/temp_visual/dir_setting/ninja/phase
	var/phaseout = /obj/effect/temp_visual/dir_setting/ninja/phase/out

/datum/action/innate/dash/Grant(mob/user, obj/dasher)
	. = ..()
	dashing_item = dasher

/datum/action/innate/dash/Destroy()
	dashing_item = null
	return ..()

/datum/action/innate/dash/IsAvailable()
	if(current_charges > 0)
		return TRUE
	else
		return FALSE

/datum/action/innate/dash/Activate()
	dashing_item.attack_self(owner) //Used to toggle dash behavior in the dashing item

/datum/action/innate/dash/proc/Teleport(mob/user, atom/target)
	if(!IsAvailable())
		return
	var/turf/target_turf = get_turf(target)
	/* SEPTIC EDIT REMOVAL
	if(target in view(user.client.view, user))
	*/
	//SEPTIC EDIT BEGIN
	if(target in user.fov_view(user.client.view))
	//SEPTIC EDIT END
		var/obj/spot1 = new phaseout(get_turf(user), user.dir)
		user.forceMove(target_turf)
		playsound(target_turf, dash_sound, 25, TRUE)
		var/obj/spot2 = new phasein(get_turf(user), user.dir)
		spot1.Beam(spot2,beam_effect,time=2 SECONDS)
		current_charges--
		owner.update_action_buttons_icon()
		addtimer(CALLBACK(src, .proc/charge), charge_rate)

/datum/action/innate/dash/proc/charge()
	current_charges = clamp(current_charges + 1, 0, max_charges)
	if(recharge_sound)
		playsound(dashing_item, recharge_sound, 50, TRUE)

	if(!owner)
		return
	owner.update_action_buttons_icon()
	dashing_item.balloon_alert(owner, "[current_charges]/[max_charges] dash charges")


code:
GLOBAL_DATUM_INIT(data_core, /datum/datacore, new)

//TODO: someone please get rid of this shit
/datum/datacore
	var/list/medical = list()
	var/medicalPrintCount = 0
	var/list/general = list()
	var/list/security = list()
	var/securityPrintCount = 0
	var/securityCrimeCounter = 0
	///This list tracks characters spawned in the world and cannot be modified in-game. Currently referenced by respawn_character().
	var/list/locked = list()

/datum/data
	var/name = "data"

/datum/data/record
	name = "record"
	var/list/fields = list()

/datum/data/record/Destroy()
	if(src in GLOB.data_core.medical)
		GLOB.data_core.medical -= src
	if(src in GLOB.data_core.security)
		GLOB.data_core.security -= src
	if(src in GLOB.data_core.general)
		GLOB.data_core.general -= src
	if(src in GLOB.data_core.locked)
		GLOB.data_core.locked -= src
	. = ..()

/datum/data/crime
	name = "crime"
	var/crimeName = ""
	var/crimeDetails = ""
	var/author = ""
	var/time = ""
	var/fine = 0
	var/paid = 0
	var/dataId = 0

/datum/datacore/proc/createCrimeEntry(cname = "", cdetails = "", author = "", time = "", fine = 0)
	var/datum/data/crime/c = new /datum/data/crime
	c.crimeName = cname
	c.crimeDetails = cdetails
	c.author = author
	c.time = time
	c.fine = fine
	c.paid = 0
	c.dataId = ++securityCrimeCounter
	return c

/datum/datacore/proc/addCitation(id = "", datum/data/crime/crime)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["citation"]
			crimes |= crime
			return

/datum/datacore/proc/removeCitation(id, cDataId)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["citation"]
			for(var/datum/data/crime/crime in crimes)
				if(crime.dataId == text2num(cDataId))
					crimes -= crime
					return

/datum/datacore/proc/payCitation(id, cDataId, amount)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["citation"]
			for(var/datum/data/crime/crime in crimes)
				if(crime.dataId == text2num(cDataId))
					crime.paid = crime.paid + amount
					var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_SEC)
					D.adjust_money(amount)
					return

/**
 * Adds crime to security record.
 *
 * Is used to add single crime to someone's security record.
 * Arguments:
 * * id - record id.
 * * datum/data/crime/crime - premade array containing every variable, usually created by createCrimeEntry.
 */
/datum/datacore/proc/addCrime(id = "", datum/data/crime/crime)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["crim"]
			crimes |= crime
			return

/**
 * Deletes crime from security record.
 *
 * Is used to delete single crime to someone's security record.
 * Arguments:
 * * id - record id.
 * * cDataId - id of already existing crime.
 */
/datum/datacore/proc/removeCrime(id, cDataId)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["crim"]
			for(var/datum/data/crime/crime in crimes)
				if(crime.dataId == text2num(cDataId))
					crimes -= crime
					return

/**
 * Adds details to a crime.
 *
 * Is used to add or replace details to already existing crime.
 * Arguments:
 * * id - record id.
 * * cDataId - id of already existing crime.
 * * details - data you want to add.
 */
/datum/datacore/proc/addCrimeDetails(id, cDataId, details)
	for(var/datum/data/record/R in security)
		if(R.fields["id"] == id)
			var/list/crimes = R.fields["crim"]
			for(var/datum/data/crime/crime in crimes)
				if(crime.dataId == text2num(cDataId))
					crime.crimeDetails = details
					return

/datum/datacore/proc/manifest()
	for(var/i in GLOB.new_player_list)
		var/mob/dead/new_player/N = i
		if(N.new_character)
			log_manifest(N.ckey,N.new_character.mind,N.new_character)
		if(ishuman(N.new_character))
			manifest_inject(N.new_character, N.client)
		CHECK_TICK

/datum/datacore/proc/manifest_modify(name, assignment)
	var/datum/data/record/foundrecord = find_record("name", name, GLOB.data_core.general)
	if(foundrecord)
		foundrecord.fields["rank"] = assignment

/datum/datacore/proc/get_manifest()
	// First we build up the order in which we want the departments to appear in.
	var/list/manifest_out = list()
	for(var/datum/job_department/department as anything in SSjob.joinable_departments)
		manifest_out[department.department_name] = list()
	manifest_out[DEPARTMENT_UNASSIGNED] = list()

	var/list/departments_by_type = SSjob.joinable_departments_by_type
	for(var/datum/data/record/record as anything in GLOB.data_core.general)
		var/name = record.fields["name"]
		var/rank = record.fields["rank"]
		var/datum/job/job = SSjob.GetJob(rank)
		if(!job || !(job.job_flags & JOB_CREW_MANIFEST) || !LAZYLEN(job.departments_list)) // In case an unlawful custom rank is added.
			var/list/misc_list = manifest_out[DEPARTMENT_UNASSIGNED]
			misc_list[++misc_list.len] = list(
				"name" = name,
				"rank" = rank,
				)
			continue
		for(var/department_type as anything in job.departments_list)
			var/datum/job_department/department = departments_by_type[department_type]
			if(!department)
				stack_trace("get_manifest() failed to get job department for [department_type] of [job.type]")
				continue
			var/list/entry = list(
				"name" = name,
				"rank" = rank,
				)
			var/list/department_list = manifest_out[department.department_name]
			if(istype(job, department.department_head))
				department_list.Insert(1, null)
				department_list[1] = entry
			else
				department_list[++department_list.len] = entry

	// Trim the empty categories.
	for (var/department in manifest_out)
		if(!length(manifest_out[department]))
			manifest_out -= department

	return manifest_out

/datum/datacore/proc/get_manifest_html(monochrome = FALSE)
	var/list/manifest = get_manifest()
	var/dat = {"

Name	Rank

	"}
	for(var/department in manifest)
		var/list/entries = manifest[department]
		dat += "
[department]
"
		//JUST
		var/even = FALSE
		for(var/entry in entries)
			var/list/entry_list = entry
			dat += "
[entry_list["name"]]	[entry_list["rank"]]
"
			even = !even

	dat += "
"
	dat = replacetext(dat, "\n", "")
	dat = replacetext(dat, "\t", "")
	return dat

/datum/datacore/proc/manifest_inject(mob/living/carbon/human/H, client/C)
	set waitfor = FALSE
	var/static/list/show_directions = list(SOUTH, WEST)
	if(H.mind?.assigned_role.job_flags & JOB_CREW_MANIFEST)
		var/assignment = H.mind.assigned_role.title

		var/static/record_id_num = 1001
		var/id = num2hex(record_id_num++,6)
		if(!C)
			C = H.client
		var/image = get_id_photo(H, C, show_directions)
		var/datum/picture/pf = new
		var/datum/picture/ps = new
		pf.picture_name = "[H]"
		ps.picture_name = "[H]"
		pf.picture_desc = "This is [H]."
		ps.picture_desc = "This is [H]."
		pf.picture_image = icon(image, dir = SOUTH)
		ps.picture_image = icon(image, dir = WEST)
		var/obj/item/photo/photo_front = new(null, pf)
		var/obj/item/photo/photo_side = new(null, ps)

		//These records should ~really~ be merged or something
		//General Record
		var/datum/data/record/G = new()
		G.fields["id"] = id
		G.fields["name"] = H.real_name
		G.fields["rank"] = assignment
		G.fields["age"] = H.age
		G.fields["species"] = H.dna.species.name
		G.fields["fingerprint"] = md5(H.dna.unique_identity)
		G.fields["p_stat"] = "Active"
		G.fields["m_stat"] = "Stable"
		G.fields["gender"] = H.gender
		if(H.gender == "male")
			G.fields["gender"]  = "Male"
		else if(H.gender == "female")
			G.fields["gender"]  = "Female"
		else
			G.fields["gender"]  = "Other"
		G.fields["photo_front"] = photo_front
		G.fields["photo_side"] = photo_side
		general += G

		//Medical Record
		var/datum/data/record/M = new()
		M.fields["id"] = id
		M.fields["name"] = H.real_name
		M.fields["blood_type"] = H.dna.blood_type
		M.fields["b_dna"] = H.dna.unique_enzymes
		M.fields["mi_dis"] = H.get_quirk_string(!medical, CAT_QUIRK_MINOR_DISABILITY)
		M.fields["mi_dis_d"] = H.get_quirk_string(medical, CAT_QUIRK_MINOR_DISABILITY)
		M.fields["ma_dis"] = H.get_quirk_string(!medical, CAT_QUIRK_MAJOR_DISABILITY)
		M.fields["ma_dis_d"] = H.get_quirk_string(medical, CAT_QUIRK_MAJOR_DISABILITY)
		M.fields["cdi"] = "None"
		M.fields["cdi_d"] = "No diseases have been diagnosed at the moment."
		M.fields["notes"] = H.get_quirk_string(!medical, CAT_QUIRK_NOTES)
		M.fields["notes_d"] = H.get_quirk_string(medical, CAT_QUIRK_NOTES)
		medical += M

		//Security Record
		var/datum/data/record/S = new()
		S.fields["id"] = id
		S.fields["name"] = H.real_name
		S.fields["criminal"] = "None"
		S.fields["citation"] = list()
		S.fields["crim"] = list()
		S.fields["notes"] = "No notes."
		security += S

		//Locked Record
		var/datum/data/record/L = new()
		L.fields["id"] = md5("[H.real_name][assignment]") //surely this should just be id, like the others?
		L.fields["name"] = H.real_name
		L.fields["rank"] = assignment
		L.fields["age"] = H.age
		L.fields["gender"] = H.gender
		if(H.gender == "male")
			G.fields["gender"]  = "Male"
		else if(H.gender == "female")
			G.fields["gender"]  = "Female"
		else
			G.fields["gender"]  = "Other"
		L.fields["blood_type"] = H.dna.blood_type
		L.fields["b_dna"] = H.dna.unique_enzymes
		L.fields["identity"] = H.dna.unique_identity
		L.fields["species"] = H.dna.species.type
		L.fields["features"] = H.dna.features
		L.fields["image"] = image
		L.fields["mindref"] = H.mind
		locked += L
	return

/datum/datacore/proc/get_id_photo(mob/living/carbon/human/H, client/C, show_directions = list(SOUTH))
	var/datum/job/J = H.mind.assigned_role
	var/datum/preferences/P
	if(!C)
		C = H.client
	if(C)
		P = C.prefs
	return get_flat_human_icon(null, J, P, DUMMY_HUMAN_SLOT_MANIFEST, show_directions)


code:
/**
 * The absolute base class for everything
 *
 * A datum instantiated has no physical world prescence, use an atom if you want something
 * that actually lives in the world
 *
 * Be very mindful about adding variables to this class, they are inherited by every single
 * thing in the entire game, and so you can easily cause memory usage to rise a lot with careless
 * use of variables at this level
 */
/datum
	/**
	  * Tick count time when this object was destroyed.
	  *
	  * If this is non zero then the object has been garbage collected and is awaiting either
	  * a hard del by the GC subsystme, or to be autocollected (if it has no references)
	  */
	var/gc_destroyed

	/// Active timers with this datum as the target
	var/list/active_timers
	/// Status traits attached to this datum
	var/list/status_traits

	/**
	  * Components attached to this datum
	  *
	  * Lazy associated list in the structure of `type:component/list of components`
	  */
	var/list/datum_components
	/**
	  * Any datum registered to receive signals from this datum is in this list
	  *
	  * Lazy associated list in the structure of `signal:registree/list of registrees`
	  */
	var/list/comp_lookup
	/// Lazy associated list in the structure of `signals:proctype` that are run when the datum receives that signal
	var/list/list/datum/callback/signal_procs

	/// Datum level flags
	var/datum_flags = NONE

	/// A weak reference to another datum
	var/datum/weakref/weak_reference

	/*
	* Lazy associative list of currently active cooldowns.
	*
	* cooldowns [ COOLDOWN_INDEX ] = add_timer()
	* add_timer() returns the truthy value of -1 when not stoppable, and else a truthy numeric index
	*/
	var/list/cooldowns

#ifdef REFERENCE_TRACKING
	var/running_find_references
	var/last_find_references = 0
	#ifdef REFERENCE_TRACKING_DEBUG
	///Stores info about where refs are found, used for sanity checks and testing
	var/list/found_refs
	#endif
#endif

#ifdef DATUMVAR_DEBUGGING_MODE
	var/list/cached_vars
#endif

/**
 * Called when a href for this datum is clicked
 *
 * Sends a [COMSIG_TOPIC] signal
 */
/datum/Topic(href, href_list[])
	..()
	SEND_SIGNAL(src, COMSIG_TOPIC, usr, href_list)

/**
 * Default implementation of clean-up code.
 *
 * This should be overridden to remove all references pointing to the object being destroyed, if
 * you do override it, make sure to call the parent and return it's return value by default
 *
 * Return an appropriate [QDEL_HINT][QDEL_HINT_QUEUE] to modify handling of your deletion;
 * in most cases this is [QDEL_HINT_QUEUE].
 *
 * The base case is responsible for doing the following
 * * Erasing timers pointing to this datum
 * * Erasing compenents on this datum
 * * Notifying datums listening to signals from this datum that we are going away
 *
 * Returns [QDEL_HINT_QUEUE]
 */
/datum/proc/Destroy(force=FALSE, ...)
	SHOULD_CALL_PARENT(TRUE)
	SHOULD_NOT_SLEEP(TRUE)
	tag = null
	datum_flags &= ~DF_USE_TAG //In case something tries to REF us
	weak_reference = null //ensure prompt GCing of weakref.

	var/list/timers = active_timers
	active_timers = null
	for(var/thing in timers)
		var/datum/timedevent/timer = thing
		if (timer.spent && !(timer.flags & TIMER_DELETE_ME))
			continue
		qdel(timer)

	#ifdef REFERENCE_TRACKING
	#ifdef REFERENCE_TRACKING_DEBUG
	found_refs = null
	#endif
	#endif

	//BEGIN: ECS SHIT
	var/list/dc = datum_components
	if(dc)
		var/all_components = dc[/datum/component]
		if(length(all_components))
			for(var/I in all_components)
				var/datum/component/C = I
				qdel(C, FALSE, TRUE)
		else
			var/datum/component/C = all_components
			qdel(C, FALSE, TRUE)
		dc.Cut()

	clear_signal_refs()
	//END: ECS SHIT

	return QDEL_HINT_QUEUE

///Only override this if you know what you're doing. You do not know what you're doing
///This is a threat
/datum/proc/clear_signal_refs()
	var/list/lookup = comp_lookup
	if(lookup)
		for(var/sig in lookup)
			var/list/comps = lookup[sig]
			if(length(comps))
				for(var/i in comps)
					var/datum/component/comp = i
					comp.UnregisterSignal(src, sig)
			else
				var/datum/component/comp = comps
				comp.UnregisterSignal(src, sig)
		comp_lookup = lookup = null

	for(var/target in signal_procs)
		UnregisterSignal(target, signal_procs[target])

#ifdef DATUMVAR_DEBUGGING_MODE
/datum/proc/save_vars()
	cached_vars = list()
	for(var/i in vars)
		if(i == "cached_vars")
			continue
		cached_vars[i] = vars[i]

/datum/proc/check_changed_vars()
	. = list()
	for(var/i in vars)
		if(i == "cached_vars")
			continue
		if(cached_vars[i] != vars[i])
			.[i] = list(cached_vars[i], vars[i])

/datum/proc/txt_changed_vars()
	var/list/l = check_changed_vars()
	var/t = "[src]([REF(src)]) changed vars:"
	for(var/i in l)
		t += "\"[i]\" \[[l[i][1]]\] --> \[[l[i][2]]\] "
	t += "."

/datum/proc/to_chat_check_changed_vars(target = world)
	to_chat(target, txt_changed_vars())
#endif

///Return a LIST for serialize_datum to encode! Not the actual json!
/datum/proc/serialize_list(list/options)
	CRASH("Attempted to serialize datum [src] of type [type] without serialize_list being implemented!")

///Accepts a LIST from deserialize_datum. Should return src or another datum.
/datum/proc/deserialize_list(json, list/options)
	CRASH("Attempted to deserialize datum [src] of type [type] without deserialize_list being implemented!")

///Serializes into JSON. Does not encode type.
/datum/proc/serialize_json(list/options)
	. = serialize_list(options)
	if(!islist(.))
		. = null
	else
		. = json_encode(.)

///Deserializes from JSON. Does not parse type.
/datum/proc/deserialize_json(list/input, list/options)
	var/list/jsonlist = json_decode(input)
	. = deserialize_list(jsonlist)
	if(!istype(., /datum))
		. = null

///Convert a datum into a json blob
/proc/json_serialize_datum(datum/D, list/options)
	if(!istype(D))
		return
	var/list/jsonlist = D.serialize_list(options)
	if(islist(jsonlist))
		jsonlist["DATUM_TYPE"] = D.type
	return json_encode(jsonlist)

/// Convert a list of json to datum
/proc/json_deserialize_datum(list/jsonlist, list/options, target_type, strict_target_type = FALSE)
	if(!islist(jsonlist))
		if(!istext(jsonlist))
			CRASH("Invalid JSON")
		jsonlist = json_decode(jsonlist)
		if(!islist(jsonlist))
			CRASH("Invalid JSON")
	if(!jsonlist["DATUM_TYPE"])
		return
	if(!ispath(jsonlist["DATUM_TYPE"]))
		if(!istext(jsonlist["DATUM_TYPE"]))
			return
		jsonlist["DATUM_TYPE"] = text2path(jsonlist["DATUM_TYPE"])
		if(!ispath(jsonlist["DATUM_TYPE"]))
			return
	if(target_type)
		if(!ispath(target_type))
			return
		if(strict_target_type)
			if(target_type != jsonlist["DATUM_TYPE"])
				return
		else if(!ispath(jsonlist["DATUM_TYPE"], target_type))
			return
	var/typeofdatum = jsonlist["DATUM_TYPE"] //BYOND won't directly read if this is just put in the line below, and will instead runtime because it thinks you're trying to make a new list?
	var/datum/D = new typeofdatum
	var/datum/returned = D.deserialize_list(jsonlist, options)
	if(!istype(returned, /datum))
		qdel(D)
	else
		return returned

/**
 * Callback called by a timer to end an associative-list-indexed cooldown.
 *
 * Arguments:
 * * source - datum storing the cooldown
 * * index - string index storing the cooldown on the cooldowns associative list
 *
 * This sends a signal reporting the cooldown end.
 */
/proc/end_cooldown(datum/source, index)
	if(QDELETED(source))
		return
	SEND_SIGNAL(source, COMSIG_CD_STOP(index))
	TIMER_COOLDOWN_END(source, index)

/**
 * Proc used by stoppable timers to end a cooldown before the time has ran out.
 *
 * Arguments:
 * * source - datum storing the cooldown
 * * index - string index storing the cooldown on the cooldowns associative list
 *
 * This sends a signal reporting the cooldown end, passing the time left as an argument.
 */
/proc/reset_cooldown(datum/source, index)
	if(QDELETED(source))
		return
	SEND_SIGNAL(source, COMSIG_CD_RESET(index), S_TIMER_COOLDOWN_TIMELEFT(source, index))
	TIMER_COOLDOWN_END(source, index)


code:
/datum/proc/CanProcCall(procname)
	return TRUE

/datum/proc/can_vv_get(var_name)
	return TRUE

/datum/proc/vv_edit_var(var_name, var_value) //called whenever a var is edited
	if(var_name == NAMEOF(src, vars))
		return FALSE
	vars[var_name] = var_value
	datum_flags |= DF_VAR_EDITED
	return TRUE

/datum/proc/vv_get_var(var_name)
	switch(var_name)
		if ("vars")
			return debug_variable(var_name, list(), 0, src)
	return debug_variable(var_name, vars[var_name], 0, src)

/datum/proc/can_vv_mark()
	return TRUE

//please call . = ..() first and append to the result, that way parent items are always at the top and child items are further down
//add separaters by doing . += "---"
/datum/proc/vv_get_dropdown()
	. = list()
	VV_DROPDOWN_OPTION("", "---")
	VV_DROPDOWN_OPTION(VV_HK_CALLPROC, "Call Proc")
	VV_DROPDOWN_OPTION(VV_HK_MARK, "Mark Object")
	VV_DROPDOWN_OPTION(VV_HK_DELETE, "Delete")
	VV_DROPDOWN_OPTION(VV_HK_EXPOSE, "Show VV To Player")
	VV_DROPDOWN_OPTION(VV_HK_ADDCOMPONENT, "Add Component/Element")
	VV_DROPDOWN_OPTION(VV_HK_REMOVECOMPONENT, "Remove Component/Element")
	VV_DROPDOWN_OPTION(VV_HK_MASS_REMOVECOMPONENT, "Mass Remove Component/Element")
	VV_DROPDOWN_OPTION(VV_HK_MODIFY_TRAITS, "Modify Traits")

//This proc is only called if everything topic-wise is verified. The only verifications that should happen here is things like permission checks!
//href_list is a reference, modifying it in these procs WILL change the rest of the proc in topic.dm of admin/view_variables!
//This proc is for "high level" actions like admin heal/set species/etc/etc. The low level debugging things should go in admin/view_variables/topic_basic.dm incase this runtimes.
/datum/proc/vv_do_topic(list/href_list)
	if(!usr || !usr.client || !usr.client.holder || !check_rights(NONE))
		return FALSE //This is VV, not to be called by anything else.
	if(SEND_SIGNAL(src, COMSIG_VV_TOPIC, usr, href_list) & COMPONENT_VV_HANDLED)
		return FALSE
	if(href_list[VV_HK_MODIFY_TRAITS])
		usr.client.holder.modify_traits(src)
	return TRUE

/datum/proc/vv_get_header()
	. = list()
	if(("name" in vars) && !isatom(src))
		. += "[vars["name"]]
"


code:
/mob/living/proc/HasDisease(datum/disease/D)
	for(var/thing in diseases)
		var/datum/disease/DD = thing
		if(D.IsSame(DD))
			return TRUE
	return FALSE

/mob/living/proc/CanContractDisease(datum/disease/D)
	if(stat == DEAD && !D.process_dead)
		return FALSE

	if(D.GetDiseaseID() in disease_resistances)
		return FALSE

	if(HasDisease(D))
		return FALSE

	if(!(D.infectable_biotypes & mob_biotypes))
		return FALSE

	if(!D.is_viable_mobtype(type))
		return FALSE

	return TRUE

/mob/living/proc/ContactContractDisease(datum/disease/D)
	if(!CanContractDisease(D))
		return FALSE
	D.try_infect(src)

/mob/living/carbon/ContactContractDisease(datum/disease/D, target_zone)
	if(!CanContractDisease(D))
		return FALSE

	var/obj/item/clothing/Cl = null
	var/passed = TRUE

	var/head_ch = 80
	var/body_ch = 100
	var/hands_ch = 35
	var/feet_ch = 15

	if(prob(15/D.permeability_mod))
		return

	if(satiety>0 && prob(satiety/10)) // positive satiety makes it harder to contract the disease.
		return

	//Lefts and rights do not matter for arms and legs, they both run the same checks
	if(!target_zone)
		target_zone = pick(head_ch;BODY_ZONE_HEAD,body_ch;BODY_ZONE_CHEST,hands_ch;BODY_ZONE_L_ARM,feet_ch;BODY_ZONE_L_LEG)
	else
		target_zone = check_zone(target_zone)

	if(ismonkey(src))
		var/mob/living/carbon/human/M = src
		switch(target_zone)
			if(BODY_ZONE_HEAD)
				if(M.wear_mask && isobj(M.wear_mask))
					Cl = M.wear_mask
					passed = prob((Cl.permeability_coefficient*100) - 1)

	else if(ishuman(src))
		var/mob/living/carbon/human/H = src

		switch(target_zone)
			if(BODY_ZONE_HEAD)
				if(isobj(H.head) && !istype(H.head, /obj/item/paper))
					Cl = H.head
					passed = prob((Cl.permeability_coefficient*100) - 1)
				if(passed && isobj(H.wear_mask))
					Cl = H.wear_mask
					passed = prob((Cl.permeability_coefficient*100) - 1)
				if(passed && isobj(H.wear_neck))
					Cl = H.wear_neck
					passed = prob((Cl.permeability_coefficient*100) - 1)
			if(BODY_ZONE_CHEST)
				if(isobj(H.wear_suit))
					Cl = H.wear_suit
					passed = prob((Cl.permeability_coefficient*100) - 1)
				if(passed && isobj(ITEM_SLOT_ICLOTHING))
					Cl = ITEM_SLOT_ICLOTHING
					passed = prob((Cl.permeability_coefficient*100) - 1)
			if(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)
				if(isobj(H.wear_suit) && H.wear_suit.body_parts_covered&HANDS)
					Cl = H.wear_suit
					passed = prob((Cl.permeability_coefficient*100) - 1)

				if(passed && isobj(H.gloves))
					Cl = H.gloves
					passed = prob((Cl.permeability_coefficient*100) - 1)
			if(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
				if(isobj(H.wear_suit) && H.wear_suit.body_parts_covered&FEET)
					Cl = H.wear_suit
					passed = prob((Cl.permeability_coefficient*100) - 1)

				if(passed && isobj(H.shoes))
					Cl = H.shoes
					passed = prob((Cl.permeability_coefficient*100) - 1)

	if(passed)
		D.try_infect(src)

/mob/living/proc/AirborneContractDisease(datum/disease/D, force_spread)
	if( ((D.spread_flags & DISEASE_SPREAD_AIRBORNE) || force_spread) && prob((50*D.permeability_mod) - 1))
		ForceContractDisease(D)

/mob/living/carbon/AirborneContractDisease(datum/disease/D, force_spread)
	if(internal)
		return
	if(HAS_TRAIT(src, TRAIT_NOBREATH))
		return
	..()

//Proc to use when you 100% want to try to infect someone (ignoreing protective clothing and such), as long as they aren't immune
/mob/living/proc/ForceContractDisease(datum/disease/D, make_copy = TRUE, del_on_fail = FALSE)
	if(!CanContractDisease(D))
		if(del_on_fail)
			qdel(D)
		return FALSE
	if(!D.try_infect(src, make_copy))
		if(del_on_fail)
			qdel(D)
		return FALSE
	return TRUE

/mob/living/carbon/human/CanContractDisease(datum/disease/D)
	if(dna)
		if(HAS_TRAIT(src, TRAIT_VIRUSIMMUNE) && !D.bypasses_immunity)
			return FALSE

	for(var/thing in D.required_organs)
		if(!((locate(thing) in bodyparts) || (locate(thing) in internal_organs)))
			return FALSE
	return ..()

/mob/living/proc/CanSpreadAirborneDisease()
	return !is_mouth_covered()

/mob/living/carbon/CanSpreadAirborneDisease()
	return !((head && (head.flags_cover & HEADCOVERSMOUTH) && (head.armor.getRating(BIO) >= 25)) || (wear_mask && (wear_mask.flags_cover & MASKCOVERSMOUTH) && (wear_mask.armor.getRating(BIO) >= 25)))