code:
#define AUTOFIRE_MOUSEUP 0
#define AUTOFIRE_MOUSEDOWN 1

/datum/component/automatic_fire
	var/client/clicker
	var/mob/living/shooter
	var/atom/target
	var/turf/target_loc //For dealing with locking on targets due to BYOND engine limitations (the mouse input only happening when mouse moves).
	var/autofire_stat = AUTOFIRE_STAT_IDLE
	var/mouse_parameters
	var/autofire_shot_delay = 0.3 SECONDS //Time between individual shots.
	var/mouse_status = AUTOFIRE_MOUSEUP //This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.

	COOLDOWN_DECLARE(next_shot_cd)

/datum/component/automatic_fire/Initialize(_autofire_shot_delay)
	. = ..()
	if(!isgun(parent))
		return COMPONENT_INCOMPATIBLE
	var/obj/item/gun = parent
	RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/wake_up)
	if(_autofire_shot_delay)
		autofire_shot_delay = _autofire_shot_delay
	if(autofire_stat == AUTOFIRE_STAT_IDLE && ismob(gun.loc))
		var/mob/user = gun.loc
		wake_up(src, user)

/datum/component/automatic_fire/Destroy()
	autofire_off()
	return ..()

/datum/component/automatic_fire/process(delta_time)
	if(autofire_stat != AUTOFIRE_STAT_FIRING)
		STOP_PROCESSING(SSprojectiles, src)
		return
	process_shot()

/datum/component/automatic_fire/proc/wake_up(datum/source, mob/user, slot)
	SIGNAL_HANDLER

	if(autofire_stat == AUTOFIRE_STAT_ALERT)
		return //We've updated the firemode. No need for more.
	if(autofire_stat == AUTOFIRE_STAT_FIRING)
		stop_autofiring() //Let's stop shooting to avoid issues.
		return
	if(iscarbon(user))
		var/mob/living/carbon/arizona_ranger = user
		if(arizona_ranger.is_holding(parent))
			autofire_on(arizona_ranger.client)

// There is a gun and there is a user wielding it. The component now waits for the mouse click.
/datum/component/automatic_fire/proc/autofire_on(client/usercli)
	SIGNAL_HANDLER

	if(autofire_stat != AUTOFIRE_STAT_IDLE)
		return
	autofire_stat = AUTOFIRE_STAT_ALERT
	if(!QDELETED(usercli))
		clicker = usercli
		shooter = clicker.mob
		RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, .proc/on_mouse_down)
	if(!QDELETED(shooter))
		RegisterSignal(shooter, COMSIG_MOB_LOGOUT, .proc/autofire_off)
		UnregisterSignal(shooter, COMSIG_MOB_LOGIN)
	RegisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED), .proc/autofire_off)
	parent.RegisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, /obj/item/gun/.proc/autofire_bypass_check)
	parent.RegisterSignal(parent, COMSIG_AUTOFIRE_SHOT, /obj/item/gun/.proc/do_autofire)

/datum/component/automatic_fire/proc/autofire_off(datum/source)
	SIGNAL_HANDLER
	if(autofire_stat == AUTOFIRE_STAT_IDLE)
		return
	if(autofire_stat == AUTOFIRE_STAT_FIRING)
		stop_autofiring()

	autofire_stat = AUTOFIRE_STAT_IDLE

	if(!QDELETED(clicker))
		UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDOWN, COMSIG_CLIENT_MOUSEUP, COMSIG_CLIENT_MOUSEDRAG))
	mouse_status = AUTOFIRE_MOUSEUP //In regards to the component there's no click anymore to care about.
	clicker = null
	if(!QDELETED(shooter))
		RegisterSignal(shooter, COMSIG_MOB_LOGIN, .proc/on_client_login)
		UnregisterSignal(shooter, COMSIG_MOB_LOGOUT)
	UnregisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED))
	shooter = null
	parent.UnregisterSignal(parent, COMSIG_AUTOFIRE_SHOT)
	parent.UnregisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN)

/datum/component/automatic_fire/proc/on_client_login(mob/source)
	SIGNAL_HANDLER
	if(!source.client)
		return
	if(source.is_holding(parent))
		autofire_on(source.client)

/datum/component/automatic_fire/proc/on_mouse_down(client/source, atom/_target, turf/location, control, params)
	SIGNAL_HANDLER
	var/list/modifiers = params2list(params) //If they're shift+clicking, for example, let's not have them accidentally shoot.

	if(LAZYACCESS(modifiers, SHIFT_CLICK))
		return
	if(LAZYACCESS(modifiers, CTRL_CLICK))
		return
	if(LAZYACCESS(modifiers, MIDDLE_CLICK))
		return
	if(LAZYACCESS(modifiers, RIGHT_CLICK))
		return
	if(LAZYACCESS(modifiers, ALT_CLICK))
		return
	if(source.mob.throw_mode)
		return
	if(!isturf(source.mob.loc)) //No firing inside lockers and stuff.
		return
	if(get_dist(source.mob, _target) < 2) //Adjacent clicking.
		return

	if(isnull(location)) //Clicking on a screen object.
		if(_target.plane != CLICKCATCHER_PLANE) //The clickcatcher is a special case. We want the click to trigger then, under it.
			return //If we click and drag on our worn backpack, for example, we want it to open instead.
		_target = params_to_turf(modifiers["screen-loc"], get_turf(source.eye), source)
		if(!_target)
			CRASH("Failed to get the turf under clickcatcher")

	if(SEND_SIGNAL(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, source, _target, location, control, params) & COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS)
		return

	source.click_intercept_time = world.time //From this point onwards Click() will no longer be triggered.

	if(autofire_stat == (AUTOFIRE_STAT_IDLE))
		CRASH("on_mouse_down() called with [autofire_stat] autofire_stat")
	if(autofire_stat == AUTOFIRE_STAT_FIRING)
		stop_autofiring() //This can happen if we click and hold and then alt+tab, printscreen or other such action. MouseUp won't be called then and it will keep autofiring.

	target = _target
	target_loc = get_turf(target)
	mouse_parameters = params
	INVOKE_ASYNC(src, .proc/start_autofiring)

//Dakka-dakka
/datum/component/automatic_fire/proc/start_autofiring()
	if(autofire_stat == AUTOFIRE_STAT_FIRING)
		return
	autofire_stat = AUTOFIRE_STAT_FIRING

	clicker.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
	clicker.mouse_pointer_icon = clicker.mouse_override_icon

	if(mouse_status == AUTOFIRE_MOUSEUP) //See mouse_status definition for the reason for this.
		RegisterSignal(clicker, COMSIG_CLIENT_MOUSEUP, .proc/on_mouse_up)
		mouse_status = AUTOFIRE_MOUSEDOWN

	RegisterSignal(shooter, COMSIG_MOB_SWAP_HANDS, .proc/stop_autofiring)

	if(isgun(parent))
		var/obj/item/gun/shoota = parent
		if(!shoota.on_autofire_start(shooter)) //This is needed because the minigun has a do_after before firing and signals are async.
			stop_autofiring()
			return
	if(autofire_stat != AUTOFIRE_STAT_FIRING)
		return //Things may have changed while on_autofire_start() was being processed, due to do_after's sleep.

	if(!process_shot()) //First shot is processed instantly.
		return //If it fails, such as when the gun is empty, then there's no need to schedule a second shot.

	START_PROCESSING(SSprojectiles, src)
	RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, .proc/on_mouse_drag)

/datum/component/automatic_fire/proc/on_mouse_up(datum/source, atom/object, turf/location, control, params)
	SIGNAL_HANDLER
	UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEUP)
	mouse_status = AUTOFIRE_MOUSEUP
	if(autofire_stat == AUTOFIRE_STAT_FIRING)
		stop_autofiring()
	return COMPONENT_CLIENT_MOUSEUP_INTERCEPT

/datum/component/automatic_fire/proc/stop_autofiring(datum/source, atom/object, turf/location, control, params)
	SIGNAL_HANDLER
	if(autofire_stat != AUTOFIRE_STAT_FIRING)
		return
	STOP_PROCESSING(SSprojectiles, src)
	autofire_stat = AUTOFIRE_STAT_ALERT
	if(clicker)
		clicker.mouse_override_icon = null
		clicker.mouse_pointer_icon = clicker.mouse_override_icon
		UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG)
	if(!QDELETED(shooter))
		UnregisterSignal(shooter, COMSIG_MOB_SWAP_HANDS)
	target = null
	target_loc = null
	mouse_parameters = null

/datum/component/automatic_fire/proc/on_mouse_drag(client/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
	SIGNAL_HANDLER
	if(isnull(over_location)) //This happens when the mouse is over an inventory or screen object, or on entering deep darkness, for example.
		var/list/modifiers = params2list(params)
		var/new_target = params_to_turf(modifiers["screen-loc"], get_turf(source.eye), source)
		mouse_parameters = params
		if(!new_target)
			if(QDELETED(target)) //No new target acquired, and old one was deleted, get us out of here.
				stop_autofiring()
				CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]. Old target was incidentally QDELETED.")
			target = get_turf(target) //If previous target wasn't a turf, let's turn it into one to avoid locking onto a potentially moving target.
			target_loc = target
			CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]")
		target = new_target
		target_loc = new_target
		return
	target = over_object
	target_loc = get_turf(over_object)
	mouse_parameters = params

/datum/component/automatic_fire/proc/process_shot()
	if(autofire_stat != AUTOFIRE_STAT_FIRING)
		return FALSE
	if(!COOLDOWN_FINISHED(src, next_shot_cd))
		return TRUE
	if(QDELETED(target) || get_turf(target) != target_loc) //Target moved or got destroyed since we last aimed.
		target = target_loc //So we keep firing on the emptied tile until we move our mouse and find a new target.
	if(get_dist(shooter, target) <= 0)
		target = get_step(shooter, shooter.dir) //Shoot in the direction faced if the mouse is on the same tile as we are.
		target_loc = target
	else if(!in_view_range(shooter, target))
		stop_autofiring() //Elvis has left the building.
		return FALSE
	shooter.face_atom(target)
	COOLDOWN_START(src, next_shot_cd, autofire_shot_delay)
	if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
		return TRUE
	stop_autofiring()
	return FALSE

// Gun procs.

/obj/item/gun/proc/on_autofire_start(mob/living/shooter)
	if(semicd || shooter.stat || !can_trigger_gun(shooter))
		return FALSE
	if(!can_shoot())
		shoot_with_empty_chamber(shooter)
		return FALSE
	var/obj/item/bodypart/other_hand = shooter.has_hand_for_held_index(shooter.get_inactive_hand_index())
	if(weapon_weight == WEAPON_HEAVY && (shooter.get_inactive_held_item() || !other_hand))
		to_chat(shooter, span_warning("You need two hands to fire [src]!"))
		return FALSE
	return TRUE

/obj/item/gun/proc/autofire_bypass_check(datum/source, client/clicker, atom/target, turf/location, control, params)
	SIGNAL_HANDLER
	if(clicker.mob.get_active_held_item() != src)
		return COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS

/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, params)
	SIGNAL_HANDLER
	if(semicd || shooter.stat)
		return NONE
	if(!can_shoot())
		shoot_with_empty_chamber(shooter)
		return NONE
	INVOKE_ASYNC(src, .proc/do_autofire_shot, source, target, shooter, params)
	return COMPONENT_AUTOFIRE_SHOT_SUCCESS //All is well, we can continue shooting.

/obj/item/gun/proc/do_autofire_shot(datum/source, atom/target, mob/living/shooter, params)
	var/obj/item/gun/akimbo_gun = shooter.get_inactive_held_item()
	var/bonus_spread = 0
	if(istype(akimbo_gun) && weapon_weight < WEAPON_MEDIUM)
		if(akimbo_gun.weapon_weight < WEAPON_MEDIUM && akimbo_gun.can_trigger_gun(shooter))
			bonus_spread = dual_wield_spread
			addtimer(CALLBACK(akimbo_gun, /obj/item/gun.proc/process_fire, target, shooter, TRUE, params, null, bonus_spread), 1)
	process_fire(target, shooter, TRUE, params, null, bonus_spread)

#undef AUTOFIRE_MOUSEUP
#undef AUTOFIRE_MOUSEDOWN


code:
#define PROCESS_COMPONENT "component"
#define PROCESS_MACHINE "machine"
#define PROCESS_OBJ "obj"

/// A component to leak gas over time from damaged objects with gas storage
/datum/component/gas_leaker
	/// Keeps track of what type we were attached to so we don't need to istype every process
	var/process_type

	/// The percent of max integrity that we start leaking. From 0 to 1
	var/integrity_leak_percent

	/// The rate at which gas leaks, you probably want this *very* low. From 0 to 1
	var/leak_rate

	/// Mirror of the machine var signifying whether this is live in the air subsystem
	var/atmos_processing = FALSE

/datum/component/gas_leaker/Initialize(integrity_leak_percent=0.9, leak_rate=1)
	. = ..()
	if(istype(parent, /obj/machinery/atmospherics/components))
		process_type = PROCESS_COMPONENT
	else if(istype(parent, /obj/machinery))
		process_type = PROCESS_MACHINE
	else if(isobj(parent))
		process_type = PROCESS_OBJ
	else
		return COMPONENT_INCOMPATIBLE

	src.integrity_leak_percent = integrity_leak_percent
	src.leak_rate = leak_rate

/datum/component/gas_leaker/Destroy(force, silent)
	SSair.stop_processing_machine(src)
	return ..()

/datum/component/gas_leaker/RegisterWithParent()
	. = ..()
	RegisterSignal(parent, COMSIG_ATOM_TAKE_DAMAGE, .proc/start_processing)

/datum/component/gas_leaker/UnregisterFromParent()
	. = ..()
	UnregisterSignal(parent, COMSIG_ATOM_TAKE_DAMAGE)

/datum/component/gas_leaker/proc/process_atmos()
	. = PROCESS_KILL
	switch(process_type)
		if(PROCESS_OBJ)
			. = process_obj(parent)
		if(PROCESS_MACHINE)
			. = process_machine(parent)
		if(PROCESS_COMPONENT)
			. = process_component(parent)

/datum/component/gas_leaker/proc/start_processing()
	SIGNAL_HANDLER
	// Hello fellow atmospherics machines, I too am definitely an atmos machine like you!
	// This component needs to tick at the same rate as the atmos system
	SSair.start_processing_machine(src)

/datum/component/gas_leaker/proc/process_obj(obj/master, list/airs=list())
	airs += master.return_air()
	return process_leak(master, airs)

/datum/component/gas_leaker/proc/process_machine(obj/machinery/master, list/airs=list())
	if(master.machine_stat & BROKEN)
		return PROCESS_KILL
	return process_obj(master, airs)

/datum/component/gas_leaker/proc/process_component(obj/machinery/atmospherics/components/master, list/airs=list())
	airs += master.airs
	return process_machine(master, airs)

/datum/component/gas_leaker/proc/process_leak(obj/master, list/airs)
	var/current_integrity = master.get_integrity()
	if(current_integrity > master.max_integrity * integrity_leak_percent)
		return PROCESS_KILL
	var/turf/location = get_turf(master)
	var/true_rate = (1 - (current_integrity / master.max_integrity)) * leak_rate
	for(var/datum/gas_mixture/mix as anything in airs)
		var/pressure = mix.return_pressure()
		if(mix.release_gas_to(location.return_air(), pressure, true_rate))
			location.air_update_turf(FALSE, FALSE)

#undef PROCESS_OBJ
#undef PROCESS_MACHINE
#undef PROCESS_COMPONENT

repo_name: AnalWerewolf/sundown

license: mit


code:
///Global GPS_list. All  GPS components get saved in here for easy reference.
GLOBAL_LIST_EMPTY(GPS_list)
///GPS component. Atoms that have this show up on gps. Pretty simple stuff.
/datum/component/gps
	var/gpstag = "COM0"
	var/tracking = TRUE
	var/emped = FALSE

/datum/component/gps/Initialize(_gpstag = "COM0")
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE
	gpstag = _gpstag
	GLOB.GPS_list += src

/datum/component/gps/Destroy()
	GLOB.GPS_list -= src
	return ..()

///GPS component subtype. Only gps/item's can be used to open the UI.
/datum/component/gps/item
	var/updating = TRUE //Automatic updating of GPS list. Can be set to manual by user.
	var/global_mode = TRUE //If disabled, only GPS signals of the same Z level are shown
	/// UI state of GPS, altering when it can be used.
	var/datum/ui_state/state = null

/datum/component/gps/item/Initialize(_gpstag = "COM0", emp_proof = FALSE, state = null)
	. = ..()
	if(. == COMPONENT_INCOMPATIBLE || !isitem(parent))
		return COMPONENT_INCOMPATIBLE

	if(isnull(state))
		state = GLOB.default_state
	src.state = state

	var/atom/A = parent
	A.add_overlay("working")
	A.name = "[initial(A.name)] ([gpstag])"
	RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, .proc/interact)
	if(!emp_proof)
		RegisterSignal(parent, COMSIG_ATOM_EMP_ACT, .proc/on_emp_act)
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
	RegisterSignal(parent, COMSIG_CLICK_ALT, .proc/on_AltClick)

///Called on COMSIG_ITEM_ATTACK_SELF
/datum/component/gps/item/proc/interact(datum/source, mob/user)
	SIGNAL_HANDLER

	if(user)
		INVOKE_ASYNC(src, .proc/ui_interact, user)

///Called on COMSIG_PARENT_EXAMINE
/datum/component/gps/item/proc/on_examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	examine_list += span_notice("Alt-click to switch it [tracking ? "off":"on"].")

///Called on COMSIG_ATOM_EMP_ACT
/datum/component/gps/item/proc/on_emp_act(datum/source, severity)
	SIGNAL_HANDLER

	emped = TRUE
	var/atom/A = parent
	A.cut_overlay("working")
	A.add_overlay("emp")
	addtimer(CALLBACK(src, .proc/reboot), 300, TIMER_UNIQUE|TIMER_OVERRIDE) //if a new EMP happens, remove the old timer so it doesn't reactivate early
	SStgui.close_uis(src) //Close the UI control if it is open.

///Restarts the GPS after getting turned off by an EMP.
/datum/component/gps/item/proc/reboot()
	emped = FALSE
	var/atom/A = parent
	A.cut_overlay("emp")
	A.add_overlay("working")

///Calls toggletracking
/datum/component/gps/item/proc/on_AltClick(datum/source, mob/user)
	SIGNAL_HANDLER

	toggletracking(user)

///Toggles the tracking for the gps
/datum/component/gps/item/proc/toggletracking(mob/user)
	if(!user.canUseTopic(parent, BE_CLOSE))
		return //user not valid to use gps
	if(emped)
		to_chat(user, span_warning("It's busted!"))
		return
	var/atom/A = parent
	if(tracking)
		A.cut_overlay("working")
		to_chat(user, span_notice("[parent] is no longer tracking, or visible to other GPS devices."))
		tracking = FALSE
	else
		A.add_overlay("working")
		to_chat(user, span_notice("[parent] is now tracking, and visible to other GPS devices."))
		tracking = TRUE

/datum/component/gps/item/ui_interact(mob/user, datum/tgui/ui)
	if(emped)
		to_chat(user, span_hear("[parent] fizzles weakly."))
		return
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "Gps")
		ui.open()
	ui.set_autoupdate(updating)

/datum/component/gps/item/ui_state(mob/user)
	return state

/datum/component/gps/item/ui_data(mob/user)
	var/list/data = list()
	data["power"] = tracking
	data["tag"] = gpstag
	data["updating"] = updating
	data["globalmode"] = global_mode
	if(!tracking || emped) //Do not bother scanning if the GPS is off or EMPed
		return data

	var/turf/curr = get_turf(parent)
	data["currentArea"] = "[get_area_name(curr, TRUE)]"
	data["currentCoords"] = "[curr.x], [curr.y], [curr.z]"

	var/list/signals = list()
	data["signals"] = list()

	for(var/gps in GLOB.GPS_list)
		var/datum/component/gps/G = gps
		if(G.emped || !G.tracking || G == src)
			continue
		var/turf/pos = get_turf(G.parent)
		if(!pos || !global_mode && pos.z != curr.z)
			continue
		var/list/signal = list()
		signal["entrytag"] = G.gpstag //Name or 'tag' of the GPS
		signal["coords"] = "[pos.x], [pos.y], [pos.z]"
		if(pos.z == curr.z) //Distance/Direction calculations for same z-level only
			signal["dist"] = max(get_dist(curr, pos), 0) //Distance between the src and remote GPS turfs
			signal["degrees"] = round(get_angle(curr, pos)) //0-360 degree directional bearing, for more precision.
		signals += list(signal) //Add this signal to the list of signals
	data["signals"] = signals
	return data

/datum/component/gps/item/ui_act(action, params)
	. = ..()
	if(.)
		return

	switch(action)
		if("rename")
			var/atom/parentasatom = parent
			var/a = stripped_input(usr, "Please enter desired tag.", parentasatom.name, gpstag, 20)

			if (!a)
				return

			gpstag = a
			. = TRUE
			log_game("[key_name(usr)] renamed [parentasatom] to \"global positioning system ([gpstag])\".")
			parentasatom.name = "global positioning system ([gpstag])"

		if("power")
			toggletracking(usr)
			. = TRUE
		if("updating")
			updating = !updating
			. = TRUE
		if("globalmode")
			global_mode = !global_mode
			. = TRUE


code:
/datum/component/grillable
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS // So you can change grill results with various cookstuffs
	///Result atom type of grilling this object
	var/atom/cook_result
	///Amount of time required to cook the food
	var/required_cook_time = 2 MINUTES
	///Is this a positive grill result?
	var/positive_result = TRUE

	///Time spent cooking so far
	var/current_cook_time = 0

	///Are we currently grilling?
	var/currently_grilling = FALSE

	///Do we use the large steam sprite?
	var/use_large_steam_sprite = FALSE

/datum/component/grillable/Initialize(cook_result, required_cook_time, positive_result, use_large_steam_sprite)
	. = ..()
	if(!isitem(parent)) //Only items support grilling at the moment
		return COMPONENT_INCOMPATIBLE

	src.cook_result = cook_result
	src.required_cook_time = required_cook_time
	src.positive_result = positive_result
	src.use_large_steam_sprite = use_large_steam_sprite

	RegisterSignal(parent, COMSIG_ITEM_GRILLED, .proc/OnGrill)
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/OnExamine)

// Inherit the new values passed to the component
/datum/component/grillable/InheritComponent(datum/component/grillable/new_comp, original, cook_result, required_cook_time, positive_result, use_large_steam_sprite)
	if(!original)
		return
	if(cook_result)
		src.cook_result = cook_result
	if(required_cook_time)
		src.required_cook_time = required_cook_time
	if(positive_result)
		src.positive_result = positive_result
	if(use_large_steam_sprite)
		src.use_large_steam_sprite = use_large_steam_sprite

///Ran every time an item is grilled by something
/datum/component/grillable/proc/OnGrill(datum/source, atom/used_grill, delta_time = 1)
	SIGNAL_HANDLER

	. = COMPONENT_HANDLED_GRILLING

	current_cook_time += delta_time * 10 //turn it into ds
	if(current_cook_time >= required_cook_time)
		FinishGrilling(used_grill)
	else if(!currently_grilling) //We havn't started grilling yet
		StartGrilling(used_grill)

///Ran when an object starts grilling on something
/datum/component/grillable/proc/StartGrilling(atom/grill_source)
	currently_grilling = TRUE
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/OnMoved)
	RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/AddGrilledItemOverlay)

	var/atom/A = parent
	A.update_appearance()

///Ran when an object finished grilling
/datum/component/grillable/proc/FinishGrilling(atom/grill_source)

	var/atom/original_object = parent
	var/atom/grilled_result = new cook_result(original_object.loc)

	if(original_object.custom_materials)
		grilled_result.set_custom_materials(original_object.custom_materials, 1)

	grilled_result.pixel_x = original_object.pixel_x
	grilled_result.pixel_y = original_object.pixel_y

	grill_source.visible_message("[parent] turns into \a [grilled_result]!")
	SEND_SIGNAL(parent, COMSIG_GRILL_COMPLETED, grilled_result)
	currently_grilling = FALSE
	qdel(parent)

///Ran when an object almost finishes grilling
/datum/component/grillable/proc/OnExamine(atom/A, mob/user, list/examine_list)
	SIGNAL_HANDLER

	if(!current_cook_time) //Not grilled yet
		if(positive_result)
			examine_list += span_notice("[parent] can be grilled into \a [initial(cook_result.name)].")
		return

	if(positive_result)
		if(current_cook_time <= required_cook_time * 0.75)
			examine_list += span_notice("[parent] probably needs to be cooked a bit longer!")
		else if(current_cook_time <= required_cook_time)
			examine_list += span_notice("[parent] seems to be almost finished cooking!")
	else
		examine_list += span_danger("[parent] should probably not be cooked for much longer!")

///Ran when an object moves from the grill
/datum/component/grillable/proc/OnMoved(atom/A, atom/OldLoc, Dir, Forced)
	SIGNAL_HANDLER
	currently_grilling = FALSE
	UnregisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS)
	UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
	A.update_appearance()

/datum/component/grillable/proc/AddGrilledItemOverlay(datum/source, list/overlays)
	SIGNAL_HANDLER

	overlays += mutable_appearance('icons/effects/steam.dmi', "[use_large_steam_sprite ? "steam_triple" : "steam_single"]", ABOVE_OBJ_LAYER)


code:
/// How many tiles around the target the shooter can roam without losing their shot
#define GUNPOINT_SHOOTER_STRAY_RANGE 2
/// How long it takes from the gunpoint is initiated to reach stage 2
#define GUNPOINT_DELAY_STAGE_2 2.5 SECONDS
/// How long it takes from stage 2 starting to move up to stage 3
#define GUNPOINT_DELAY_STAGE_3 7.5 SECONDS
/// If the projectile doesn't have a wound_bonus of CANT_WOUND, we add (this * the stage mult) to their wound_bonus and bare_wound_bonus upon triggering
#define GUNPOINT_BASE_WOUND_BONUS 5
/// How much the damage and wound bonus mod is multiplied when you're on stage 1
#define GUNPOINT_MULT_STAGE_1 1
/// As above, for stage 2
#define GUNPOINT_MULT_STAGE_2 2
/// As above, for stage 3
#define GUNPOINT_MULT_STAGE_3 2.5

/datum/component/gunpoint
	dupe_mode = COMPONENT_DUPE_UNIQUE

	var/mob/living/target
	var/obj/item/gun/weapon

	var/stage = 1
	var/damage_mult = GUNPOINT_MULT_STAGE_1

	var/point_of_no_return = FALSE

// *extremely bad russian accent* no!
/datum/component/gunpoint/Initialize(mob/living/targ, obj/item/gun/wep)
	if(!isliving(parent))
		return COMPONENT_INCOMPATIBLE

	var/mob/living/shooter = parent
	target = targ
	weapon = wep
	RegisterSignal(targ, list(COMSIG_MOB_ATTACK_HAND, COMSIG_MOB_ITEM_ATTACK, COMSIG_MOVABLE_MOVED, COMSIG_MOB_FIRED_GUN), .proc/trigger_reaction)

	RegisterSignal(weapon, list(COMSIG_ITEM_DROPPED, COMSIG_ITEM_EQUIPPED), .proc/cancel)

	shooter.visible_message(span_danger("[shooter] aims [weapon] point blank at [target]!"), \
		span_danger("You aim [weapon] point blank at [target]!"), ignored_mobs = target)
	to_chat(target, span_userdanger("[shooter] aims [weapon] point blank at you!"))
	add_memory_in_range(target, 7, MEMORY_GUNPOINT, list(DETAIL_PROTAGONIST = target, DETAIL_DEUTERAGONIST = shooter, DETAIL_WHAT_BY = weapon), story_value = STORY_VALUE_OKAY, memory_flags = MEMORY_CHECK_BLINDNESS)

	shooter.apply_status_effect(STATUS_EFFECT_HOLDUP, shooter)
	target.apply_status_effect(STATUS_EFFECT_HELDUP, shooter)

	if(istype(weapon, /obj/item/gun/ballistic/rocketlauncher) && weapon.chambered)
		if(target.stat == CONSCIOUS && IS_NUKE_OP(shooter) && !IS_NUKE_OP(target) && (locate(/obj/item/disk/nuclear) in target.get_contents()) && shooter.client)
			shooter.client.give_award(/datum/award/achievement/misc/rocket_holdup, shooter)

	target.do_alert_animation()
	target.playsound_local(target.loc, 'sound/machines/chime.ogg', 50, TRUE)
	SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, "gunpoint", /datum/mood_event/gunpoint)

	addtimer(CALLBACK(src, .proc/update_stage, 2), GUNPOINT_DELAY_STAGE_2)

/datum/component/gunpoint/Destroy(force, silent)
	var/mob/living/shooter = parent
	shooter.remove_status_effect(STATUS_EFFECT_HOLDUP)
	target.remove_status_effect(STATUS_EFFECT_HELDUP, shooter)
	SEND_SIGNAL(target, COMSIG_CLEAR_MOOD_EVENT, "gunpoint")
	return ..()

/datum/component/gunpoint/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/check_deescalate)
	RegisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE, .proc/flinch)
	RegisterSignal(parent, COMSIG_MOB_ATTACK_HAND, .proc/check_shove)
	RegisterSignal(parent, list(COMSIG_LIVING_START_PULL, COMSIG_MOVABLE_BUMP), .proc/check_bump)

/datum/component/gunpoint/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
	UnregisterSignal(parent, COMSIG_MOB_APPLY_DAMAGE)
	UnregisterSignal(parent, COMSIG_MOB_ATTACK_HAND)
	UnregisterSignal(parent, list(COMSIG_LIVING_START_PULL, COMSIG_MOVABLE_BUMP))

///If the shooter bumps the target, cancel the holdup to avoid cheesing and forcing the charged shot
/datum/component/gunpoint/proc/check_bump(atom/B, atom/A)
	SIGNAL_HANDLER

	if(A != target)
		return
	var/mob/living/shooter = parent
	shooter.visible_message(span_danger("[shooter] bumps into [target] and fumbles [shooter.p_their()] aim!"), \
		span_danger("You bump into [target] and fumble your aim!"), ignored_mobs = target)
	to_chat(target, span_userdanger("[shooter] bumps into you and fumbles [shooter.p_their()] aim!"))
	qdel(src)

///If the shooter shoves or grabs the target, cancel the holdup to avoid cheesing and forcing the charged shot
/datum/component/gunpoint/proc/check_shove(mob/living/carbon/shooter, mob/shooter_again, mob/living/T, datum/martial_art/attacker_style, modifiers)
	SIGNAL_HANDLER
	/* SEPTIC EDIT REMOVAL
	if(T != target || LAZYACCESS(modifiers, RIGHT_CLICK))
		return
	*/
	//SEPTIC EDIT BEGIN
	if(T != target || (ishuman(shooter) ? (shooter.a_intent == INTENT_DISARM || shooter.a_intent == INTENT_GRAB) : (shooter.combat_mode)))
		return
	//SEPTIC EDIT END
	shooter.visible_message(span_danger("[shooter] bumps into [target] and fumbles [shooter.p_their()] aim!"), \
		span_danger("You bump into [target] and fumble your aim!"), ignored_mobs = target)
	to_chat(target, span_userdanger("[shooter] bumps into you and fumbles [shooter.p_their()] aim!"))
	qdel(src)

///Update the damage multiplier for whatever stage we're entering into
/datum/component/gunpoint/proc/update_stage(new_stage)
	stage = new_stage
	if(stage == 2)
		to_chat(parent, span_danger("You steady [weapon] on [target]."))
		to_chat(target, span_userdanger("[parent] has steadied [weapon] on you!"))
		damage_mult = GUNPOINT_MULT_STAGE_2
		addtimer(CALLBACK(src, .proc/update_stage, 3), GUNPOINT_DELAY_STAGE_3)
	else if(stage == 3)
		to_chat(parent, span_danger("You have fully steadied [weapon] on [target]."))
		to_chat(target, span_userdanger("[parent] has fully steadied [weapon] on you!"))
		damage_mult = GUNPOINT_MULT_STAGE_3

///Cancel the holdup if the shooter moves out of sight or out of range of the target
/datum/component/gunpoint/proc/check_deescalate()
	SIGNAL_HANDLER

	if(!can_see(parent, target, GUNPOINT_SHOOTER_STRAY_RANGE - 1))
		cancel()

///Bang bang, we're firing a charged shot off
/datum/component/gunpoint/proc/trigger_reaction()
	SIGNAL_HANDLER
	INVOKE_ASYNC(src, .proc/async_trigger_reaction)

/datum/component/gunpoint/proc/async_trigger_reaction()
	var/mob/living/shooter = parent
	shooter.remove_status_effect(STATUS_EFFECT_HOLDUP) // try doing these before the trigger gets pulled since the target (or shooter even) may not exist after pulling the trigger, dig?
	target.remove_status_effect(STATUS_EFFECT_HELDUP, shooter)
	SEND_SIGNAL(target, COMSIG_CLEAR_MOOD_EVENT, "gunpoint")

	if(point_of_no_return)
		return
	point_of_no_return = TRUE

	if(!weapon.can_shoot() || !weapon.can_trigger_gun(shooter) || (weapon.weapon_weight == WEAPON_HEAVY && shooter.get_inactive_held_item()))
		shooter.visible_message(span_danger("[shooter] fumbles [weapon]!"), \
			span_danger("You fumble [weapon] and fail to fire at [target]!"), ignored_mobs = target)
		to_chat(target, span_userdanger("[shooter] fumbles [weapon] and fails to fire at you!"))
		qdel(src)
		return

	if(weapon.chambered && weapon.chambered.loaded_projectile)
		weapon.chambered.loaded_projectile.damage *= damage_mult
		if(weapon.chambered.loaded_projectile.wound_bonus != CANT_WOUND)
			weapon.chambered.loaded_projectile.wound_bonus += damage_mult * GUNPOINT_BASE_WOUND_BONUS
			weapon.chambered.loaded_projectile.bare_wound_bonus += damage_mult * GUNPOINT_BASE_WOUND_BONUS

	if(weapon.check_botched(shooter))
		return

	var/fired = weapon.process_fire(target, shooter)
	if(!fired && weapon.chambered?.loaded_projectile)
		weapon.chambered.loaded_projectile.damage /= damage_mult
		if(weapon.chambered.loaded_projectile.wound_bonus != CANT_WOUND)
			weapon.chambered.loaded_projectile.wound_bonus -= damage_mult * GUNPOINT_BASE_WOUND_BONUS
			weapon.chambered.loaded_projectile.bare_wound_bonus -= damage_mult * GUNPOINT_BASE_WOUND_BONUS

	qdel(src)

///Shooter canceled their shot, either by dropping/equipping their weapon, leaving sight/range, or clicking on the alert
/datum/component/gunpoint/proc/cancel()
	SIGNAL_HANDLER

	var/mob/living/shooter = parent
	shooter.visible_message(span_danger("[shooter] breaks [shooter.p_their()] aim on [target]!"), \
		span_danger("You are no longer aiming [weapon] at [target]."), ignored_mobs = target)
	to_chat(target, span_userdanger("[shooter] breaks [shooter.p_their()] aim on you!"))
	qdel(src)

///If the shooter is hit by an attack, they have a 50% chance to flinch and fire. If it hit the arm holding the trigger, it's an 80% chance to fire instead
/datum/component/gunpoint/proc/flinch(attacker, damage, damagetype, def_zone)
	SIGNAL_HANDLER

	var/mob/living/shooter = parent
	if(attacker == shooter)
		return // somehow this wasn't checked for months but no one tried punching themselves to initiate the shot, amazing

	var/flinch_chance = 50
	var/gun_hand = LEFT_HANDS

	if(shooter.held_items[RIGHT_HANDS] == weapon)
		gun_hand = RIGHT_HANDS

	if((def_zone == BODY_ZONE_L_ARM && gun_hand == LEFT_HANDS) || (def_zone == BODY_ZONE_R_ARM && gun_hand == RIGHT_HANDS))
		flinch_chance = 80

	if(prob(flinch_chance))
		shooter.visible_message(span_danger("[shooter] flinches!"), \
			span_danger("You flinch!"))
		INVOKE_ASYNC(src, .proc/trigger_reaction)

#undef GUNPOINT_SHOOTER_STRAY_RANGE
#undef GUNPOINT_DELAY_STAGE_2
#undef GUNPOINT_DELAY_STAGE_3
#undef GUNPOINT_BASE_WOUND_BONUS
#undef GUNPOINT_MULT_STAGE_1
#undef GUNPOINT_MULT_STAGE_2
#undef GUNPOINT_MULT_STAGE_3


code:
/datum/component/heirloom
	var/datum/mind/owner
	var/family_name

/datum/component/heirloom/Initialize(new_owner, new_family_name)
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

	owner = new_owner
	family_name = new_family_name

	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/examine)

/datum/component/heirloom/proc/examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	if(user.mind == owner)
		examine_list += span_notice("It is your precious [family_name] family heirloom. Keep it safe!")
	else if(isobserver(user))
		examine_list += span_notice("It is the [family_name] family heirloom, belonging to [owner].")
	else
		var/datum/antagonist/obsessed/creeper = user.mind.has_antag_datum(/datum/antagonist/obsessed)
		if(creeper && creeper.trauma.obsession == owner)
			examine_list += span_nicegreen("This must be [owner]'s family heirloom! It smells just like them...")


code:
/** Holder Loving Component
 *
 * This component is assigned to an [/obj/item], and also keeps track of a [holder].
 * The [parent] is 'bound' to [holder]. [parent] will be kept either directly
 * inside [holder], or in the inventory of a [/mob] that is itself holding [holder].
 *
 * If [parent] is placed in a [loc] that is not [holder] or [holder].[loc]
 * (if it's a mob), it is placed back inside [holder].
 *
 * This is intended for items that are a 'part' of another item.
 *
 * It can also delete [parent] when [holder] is deleted.
 *
 */
/datum/component/holderloving
	can_transfer = TRUE
	/** Item that parent is bound to.
	 * We try to keep parent either directly in holder, or in holder's loc if loc is a mob,
	 * and warp parent into holder if they go anywhere else.
	 */
	var/atom/holder
	/// If parent is deleted when the holder gets deleted
	var/del_parent_with_holder = FALSE

/datum/component/holderloving/Initialize(holder, del_parent_with_holder)
	if(!isitem(parent) || !holder)
		return COMPONENT_INCOMPATIBLE
	src.holder = holder
	if(del_parent_with_holder)
		src.del_parent_with_holder = del_parent_with_holder

/datum/component/holderloving/RegisterWithParent()
	RegisterSignal(holder, COMSIG_MOVABLE_MOVED, .proc/check_my_loc)
	RegisterSignal(holder, COMSIG_PARENT_QDELETING, .proc/holder_deleting)
	RegisterSignal(parent, list(
		COMSIG_ITEM_DROPPED,
		COMSIG_ITEM_EQUIPPED,
		COMSIG_STORAGE_ENTERED,
		COMSIG_STORAGE_EXITED,
	), .proc/check_my_loc)

/datum/component/holderloving/UnregisterFromParent()
	UnregisterSignal(holder, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING))
	UnregisterSignal(parent, list(
		COMSIG_ITEM_DROPPED,
		COMSIG_ITEM_EQUIPPED,
		COMSIG_STORAGE_ENTERED,
		COMSIG_STORAGE_EXITED,
	))

/datum/component/holderloving/PostTransfer()
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE

/datum/component/holderloving/InheritComponent(datum/component/holderloving/friend, i_am_original, list/arguments)
	if(i_am_original)
		holder = friend.holder

/datum/component/holderloving/proc/check_valid_loc(atom/location)
	return (location == holder || ( location == holder.loc && ismob(holder.loc) ))

/datum/component/holderloving/proc/holder_deleting(datum/source, force)
	SIGNAL_HANDLER
	if(del_parent_with_holder)
		qdel(parent)
	else
		qdel(src)

/datum/component/holderloving/proc/check_my_loc(datum/source)
	SIGNAL_HANDLER
	var/obj/item/item_parent = parent
	if(!check_valid_loc(item_parent.loc))
		item_parent.forceMove(holder)


code:
/datum/component/igniter
	var/fire_stacks

/datum/component/igniter/Initialize(fire_stacks=1)
	if(!isitem(parent) && !ishostile(parent) && !isgun(parent) && !ismachinery(parent) && !isstructure(parent))
		return COMPONENT_INCOMPATIBLE

	src.fire_stacks = fire_stacks

/datum/component/igniter/RegisterWithParent()
	if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc
		RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit)
	else if(isitem(parent))
		RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK, .proc/item_afterattack)
	else if(ishostile(parent))
		RegisterSignal(parent, COMSIG_HOSTILE_POST_ATTACKINGTARGET, .proc/hostile_attackingtarget)

/datum/component/igniter/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT))

/datum/component/igniter/proc/item_afterattack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters)
	SIGNAL_HANDLER

	if(!proximity_flag)
		return
	do_igniter(target)

/datum/component/igniter/proc/hostile_attackingtarget(mob/living/simple_animal/hostile/attacker, atom/target, success)
	SIGNAL_HANDLER

	if(!success)
		return
	do_igniter(target)

/datum/component/igniter/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle)
	SIGNAL_HANDLER

	do_igniter(target)

/datum/component/igniter/proc/do_igniter(atom/target)
	if(isliving(target))
		var/mob/living/L = target
		L.adjust_fire_stacks(fire_stacks)
		L.IgniteMob()


code:
/datum/component/infective
	var/list/datum/disease/diseases //make sure these are the static, non-processing versions!
	var/expire_time
	var/required_clean_types = CLEAN_TYPE_DISEASE

/datum/component/infective/Initialize(list/datum/disease/_diseases, expire_in)
	if(islist(_diseases))
		diseases = _diseases
	else
		diseases = list(_diseases)
	if(expire_in)
		expire_time = world.time + expire_in
		QDEL_IN(src, expire_in)

	if(!ismovable(parent))
		return COMPONENT_INCOMPATIBLE

	var/static/list/disease_connections = list(
		COMSIG_ATOM_ENTERED = .proc/try_infect_crossed,
	)
	AddComponent(/datum/component/connect_loc_behalf, parent, disease_connections)

	RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/clean)
	RegisterSignal(parent, COMSIG_MOVABLE_BUCKLE, .proc/try_infect_buckle)
	RegisterSignal(parent, COMSIG_MOVABLE_BUMP, .proc/try_infect_collide)
	RegisterSignal(parent, COMSIG_MOVABLE_IMPACT_ZONE, .proc/try_infect_impact_zone)
	if(isitem(parent))
		RegisterSignal(parent, COMSIG_ITEM_ATTACK_ZONE, .proc/try_infect_attack_zone)
		RegisterSignal(parent, COMSIG_ITEM_ATTACK, .proc/try_infect_attack)
		RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/try_infect_equipped)
		RegisterSignal(parent, COMSIG_FOOD_EATEN, .proc/try_infect_eat)
		if(istype(parent, /obj/item/reagent_containers/food/drinks))
			RegisterSignal(parent, COMSIG_DRINK_DRANK, .proc/try_infect_drink)
		else if(istype(parent, /obj/item/reagent_containers/glass))
			RegisterSignal(parent, COMSIG_GLASS_DRANK, .proc/try_infect_drink)
	else if(istype(parent, /obj/effect/decal/cleanable/blood/gibs))
		RegisterSignal(parent, COMSIG_GIBS_STREAK, .proc/try_infect_streak)

/datum/component/infective/proc/try_infect_eat(datum/source, mob/living/eater, mob/living/feeder)
	SIGNAL_HANDLER

	for(var/V in diseases)
		eater.ForceContractDisease(V)
	try_infect(feeder, BODY_ZONE_L_ARM)

/datum/component/infective/proc/try_infect_drink(datum/source, mob/living/drinker, mob/living/feeder)
	SIGNAL_HANDLER

	for(var/disease in diseases)
		drinker.ForceContractDisease(disease)
	var/appendage_zone = feeder.held_items.Find(source)
	appendage_zone = appendage_zone == 0 ? BODY_ZONE_CHEST : appendage_zone % 2 ? BODY_ZONE_R_ARM : BODY_ZONE_L_ARM
	try_infect(feeder, appendage_zone)

/datum/component/infective/proc/clean(datum/source, clean_types)
	SIGNAL_HANDLER

	. = NONE
	if(clean_types & required_clean_types)
		qdel(src)
		return COMPONENT_CLEANED

/datum/component/infective/proc/try_infect_buckle(datum/source, mob/M, force)
	SIGNAL_HANDLER

	if(isliving(M))
		try_infect(M)

/datum/component/infective/proc/try_infect_collide(datum/source, atom/A)
	SIGNAL_HANDLER

	var/atom/movable/P = parent
	if(P.throwing)
		//this will be handled by try_infect_impact_zone()
		return
	if(isliving(A))
		try_infect(A)

/datum/component/infective/proc/try_infect_impact_zone(datum/source, mob/living/target, hit_zone)
	SIGNAL_HANDLER

	try_infect(target, hit_zone)

/datum/component/infective/proc/try_infect_attack_zone(datum/source, mob/living/carbon/target, mob/living/user, hit_zone)
	SIGNAL_HANDLER

	try_infect(user, BODY_ZONE_L_ARM)
	try_infect(target, hit_zone)

/datum/component/infective/proc/try_infect_attack(datum/source, mob/living/target, mob/living/user)
	SIGNAL_HANDLER
	if(!iscarbon(target)) //this case will be handled by try_infect_attack_zone
		try_infect(target)
	try_infect(user, BODY_ZONE_L_ARM)

/datum/component/infective/proc/try_infect_equipped(datum/source, mob/living/L, slot)
	SIGNAL_HANDLER

	var/old_permeability
	if(isitem(parent))
		//if you are putting an infective item on, it obviously will not protect you, so set its permeability high enough that it will never block ContactContractDisease()
		var/obj/item/I = parent
		old_permeability = I.permeability_coefficient
		I.permeability_coefficient = 1.01

	try_infect(L, slot2body_zone(slot))

	if(isitem(parent))
		var/obj/item/I = parent
		I.permeability_coefficient = old_permeability

/datum/component/infective/proc/try_infect_crossed(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER

	if(isliving(arrived))
		try_infect(arrived, BODY_ZONE_PRECISE_L_FOOT)

/datum/component/infective/proc/try_infect_streak(datum/source, list/directions, list/output_diseases)
	SIGNAL_HANDLER

	output_diseases |= diseases

/datum/component/infective/proc/try_infect(mob/living/L, target_zone)
	for(var/V in diseases)
		L.ContactContractDisease(V, target_zone)


code:
/// You can only hold whitelisted items
/datum/component/itempicky
	can_transfer = TRUE
	/// Typecache of items you can hold
	var/whitelist
	/// Message shown if you try to pick up an item not in the whitelist
	var/message = "You don't like %TARGET, why would you hold it?"

/datum/component/itempicky/Initialize(whitelist, message)
	if(!ismob(parent))
		return COMPONENT_INCOMPATIBLE
	src.whitelist = whitelist
	if(message)
		src.message = message

/datum/component/itempicky/RegisterWithParent()
	RegisterSignal(parent, COMSIG_LIVING_TRY_PUT_IN_HAND, .proc/particularly)

/datum/component/itempicky/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_LIVING_TRY_PUT_IN_HAND)

/datum/component/itempicky/PostTransfer()
	if(!ismob(parent))
		return COMPONENT_INCOMPATIBLE

/datum/component/itempicky/InheritComponent(datum/component/itempicky/friend, i_am_original, list/arguments)
	if(i_am_original)
		whitelist = friend.whitelist
		message = friend.message

/datum/component/itempicky/proc/particularly(datum/source, obj/item/pickingup)
	SIGNAL_HANDLER
	if(!is_type_in_typecache(pickingup, whitelist))
		to_chat(source, span_warning("[replacetext(message, "%TARGET", pickingup)]"))
		return COMPONENT_LIVING_CANT_PUT_IN_HAND


code:
/datum/component/jousting
	var/current_direction = NONE
	var/max_tile_charge = 5
	var/min_tile_charge = 2 //tiles before this code gets into effect.
	var/current_tile_charge = 0
	var/movement_reset_tolerance = 3 //deciseconds
	var/unmounted_damage_boost_per_tile = 0
	var/unmounted_knockdown_chance_per_tile = 0
	var/unmounted_knockdown_time = 0
	var/mounted_damage_boost_per_tile = 2
	var/mounted_knockdown_chance_per_tile = 20
	var/mounted_knockdown_time = 20
	var/requires_mob_riding = TRUE //whether this only works if the attacker is riding a mob, rather than anything they can buckle to.
	var/requires_mount = TRUE //kinda defeats the point of jousting if you're not mounted but whatever.
	var/mob/current_holder
	var/current_timerid

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

/datum/component/jousting/proc/on_equip(datum/source, mob/user, slot)
	SIGNAL_HANDLER

	RegisterSignal(user, COMSIG_MOVABLE_MOVED, .proc/mob_move, TRUE)
	current_holder = user

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

	UnregisterSignal(user, COMSIG_MOVABLE_MOVED)
	current_holder = null
	current_direction = NONE
	current_tile_charge = 0

/datum/component/jousting/proc/on_attack(datum/source, mob/living/target, mob/user)
	SIGNAL_HANDLER

	if(user != current_holder)
		return
	var/current = current_tile_charge
	var/obj/item/I = parent
	var/target_buckled = target.buckled ? TRUE : FALSE //we don't need the reference of what they're buckled to, just whether they are.
	if((requires_mount && ((requires_mob_riding && !ismob(user.buckled)) || (!user.buckled))) || !current_direction || (current_tile_charge < min_tile_charge))
		return
	var/turf/target_turf = get_step(user, current_direction)
	if(target in range(1, target_turf))
		var/knockdown_chance = (target_buckled? mounted_knockdown_chance_per_tile : unmounted_knockdown_chance_per_tile) * current
		var/knockdown_time = (target_buckled? mounted_knockdown_time : unmounted_knockdown_time)
		var/damage = (target_buckled? mounted_damage_boost_per_tile : unmounted_damage_boost_per_tile) * current
		var/sharp = I.get_sharpness()
		var/msg
		if(damage)
			msg += "[user] [sharp? "impales" : "slams into"] [target] [sharp? "on" : "with"] their [parent]"
			target.apply_damage(damage, BRUTE, user.zone_selected, 0)
		if(prob(knockdown_chance))
			msg += " and knocks [target] [target_buckled? "off of [target.buckled]" : "down"]"
			if(target_buckled)
				target.buckled.unbuckle_mob(target)
			target.Paralyze(knockdown_time)
		if(length(msg))
			user.visible_message(span_danger("[msg]!"))

/datum/component/jousting/proc/mob_move(datum/source, newloc, dir)
	SIGNAL_HANDLER

	if(!current_holder || (requires_mount && ((requires_mob_riding && !ismob(current_holder.buckled)) || (!current_holder.buckled))))
		return
	if(dir != current_direction)
		current_tile_charge = 0
		current_direction = dir
	if(current_tile_charge < max_tile_charge)
		current_tile_charge++
	if(current_timerid)
		deltimer(current_timerid)
	current_timerid = addtimer(CALLBACK(src, .proc/reset_charge), movement_reset_tolerance, TIMER_STOPPABLE)

/datum/component/jousting/proc/reset_charge()
	current_tile_charge = 0


code:
///Items with these will have a chance to get knocked off when disarming or being knocked down
/datum/component/knockoff
	///Chance to knockoff
	var/knockoff_chance = 100
	///Aiming for these zones will cause the knockoff, null means all zones allowed
	var/list/target_zones
	///Can be only knocked off from these slots, null means all slots allowed
	var/list/slots_knockoffable

/datum/component/knockoff/Initialize(knockoff_chance,zone_override,slots_knockoffable)
	if(!isitem(parent))
		return COMPONENT_INCOMPATIBLE
	RegisterSignal(parent, COMSIG_ITEM_EQUIPPED,.proc/OnEquipped)
	RegisterSignal(parent, COMSIG_ITEM_DROPPED,.proc/OnDropped)

	src.knockoff_chance = knockoff_chance

	if(zone_override)
		target_zones = zone_override

	if(slots_knockoffable)
		src.slots_knockoffable = slots_knockoffable

///Tries to knockoff the item when disarmed
/datum/component/knockoff/proc/Knockoff(mob/living/carbon/human/wearer,mob/living/attacker,zone)
	SIGNAL_HANDLER

	var/obj/item/item = parent
	if(!istype(wearer))
		return
	if(target_zones && !(zone in target_zones))
		return
	if(!prob(knockoff_chance))
		return
	if(!wearer.dropItemToGround(item))
		return
	wearer.visible_message(span_warning("[attacker] knocks off [wearer]'s [item.name]!"),span_userdanger("[attacker] knocks off your [item.name]!"))

///Tries to knockoff the item when user is knocked down
/datum/component/knockoff/proc/Knockoff_knockdown(mob/living/carbon/human/wearer,amount)
	SIGNAL_HANDLER

	if(amount <= 0)
		return

	var/obj/item/item = parent
	if(!istype(wearer))
		return
	if(!prob(knockoff_chance))
		return
	if(!wearer.dropItemToGround(item))
		return
	wearer.visible_message(span_warning("[wearer]'s [item.name] get[item.p_s()] knocked off!"),span_userdanger("Your [item.name] [item.p_were()] knocked off!"))

/datum/component/knockoff/proc/OnEquipped(datum/source, mob/living/carbon/human/H,slot)
	SIGNAL_HANDLER
	if(!istype(H))
		return
	if(slots_knockoffable && !(slot in slots_knockoffable))
		UnregisterSignal(H, COMSIG_HUMAN_DISARM_HIT)
		UnregisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN)
		return
	RegisterSignal(H, COMSIG_HUMAN_DISARM_HIT, .proc/Knockoff, TRUE)
	RegisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN, .proc/Knockoff_knockdown, TRUE)

/datum/component/knockoff/proc/OnDropped(datum/source, mob/living/M)
	SIGNAL_HANDLER

	UnregisterSignal(M, COMSIG_HUMAN_DISARM_HIT)
	UnregisterSignal(M, COMSIG_LIVING_STATUS_KNOCKDOWN)


code:
/**
	The label component.

	This component is used to manage labels applied by the hand labeler.

	Atoms can only have one instance of this component, and therefore only one label at a time.
	This is to avoid having names like "Backpack (label1) (label2) (label3)". This is annoying and abnoxious to read.

	When a player clicks the atom with a hand labeler to apply a label, this component gets applied to it.
	If the labeler is off, the component will be removed from it, and the label will be removed from its name.
 */
/datum/component/label
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	/// The name of the label the player is applying to the parent.
	var/label_name

/datum/component/label/Initialize(_label_name)
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	label_name = _label_name
	apply_label()

/datum/component/label/RegisterWithParent()
	RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/OnAttackby)
	RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/Examine)

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

/**
	This proc will fire after the parent is hit by a hand labeler which is trying to apply another label.
	Since the parent already has a label, it will remove the old one from the parent's name, and apply the new one.
*/
/datum/component/label/InheritComponent(datum/component/label/new_comp , i_am_original, _label_name)
	remove_label()
	if(new_comp)
		label_name = new_comp.label_name
	else
		label_name = _label_name
	apply_label()

/**
	This proc will trigger when any object is used to attack the parent.

	If the attacking object is not a hand labeler, it will return.
	If the attacking object is a hand labeler it will restore the name of the parent to what it was before this component was added to it, and the component will be deleted.

	Arguments:
	* source: The parent.
	* attacker: The object that is hitting the parent.
	* user: The mob who is wielding the attacking object.
*/
/datum/component/label/proc/OnAttackby(datum/source, obj/item/attacker, mob/user)
	SIGNAL_HANDLER

	// If the attacking object is not a hand labeler or its mode is 1 (has a label ready to apply), return.
	// The hand labeler should be off (mode is 0), in order to remove a label.
	var/obj/item/hand_labeler/labeler = attacker
	if(!istype(labeler) || labeler.mode)
		return

	remove_label()
	playsound(parent, 'sound/items/poster_ripped.ogg', 20, TRUE)
	to_chat(user, span_warning("You remove the label from [parent]."))
	qdel(src) // Remove the component from the object.

/**
	This proc will trigger when someone examines the parent.
	It will attach the text found in the body of the proc to the `examine_list` and display it to the player examining the parent.

	Arguments:
	* source: The parent.
	* user: The mob exmaining the parent.
	* examine_list: The current list of text getting passed from the parent's normal examine() proc.
*/
/datum/component/label/proc/Examine(datum/source, mob/user, list/examine_list)
	SIGNAL_HANDLER

	examine_list += span_notice("It has a label with some words written on it. Use a hand labeler to remove it.")

/// Applies a label to the name of the parent in the format of: "parent_name (label)"
/datum/component/label/proc/apply_label()
	var/atom/owner = parent
	owner.name += " ([label_name])"

/// Removes the label from the parent's name
/datum/component/label/proc/remove_label()
	var/atom/owner = parent
	owner.name = replacetext(owner.name, "([label_name])", "") // Remove the label text from the parent's name, wherever it's located.
	owner.name = trim(owner.name) // Shave off any white space from the beginning or end of the parent's name.


code:
/**
 * Makes anything it attaches to capable of removing something's ability to produce light until it is destroyed
 *
 * The permanent version of this is [/datum/element/light_eater]
 */
/datum/component/light_eater
	dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
	/// Tracks things this light eater has eaten
	var/list/eaten_lights

/datum/component/light_eater/Initialize(list/_eaten)
	if(!isatom(parent) && !istype(parent, /datum/reagent))
		return COMPONENT_INCOMPATIBLE

	. = ..()
	if(!LAZYLEN(_eaten))
		return

	LAZYINITLIST(eaten_lights)
	var/list/cached_eaten_lights = eaten_lights
	for(var/morsel in _eaten)
		LAZYSET(cached_eaten_lights, morsel, TRUE)
		RegisterSignal(morsel, COMSIG_PARENT_QDELETING, .proc/deref_eaten_light)

/datum/component/light_eater/Destroy(force, silent)
	for(var/light in eaten_lights)
		var/atom/eaten_light = light
		eaten_light.RemoveElement(/datum/element/light_eaten)
		UnregisterSignal(eaten_light, COMSIG_PARENT_QDELETING)
	eaten_lights = null
	return ..()

/datum/component/light_eater/RegisterWithParent()
	. = ..()
	RegisterSignal(parent, COMSIG_LIGHT_EATER_DEVOUR, .proc/on_devour)
	parent.AddElement(/datum/element/light_eater)

/datum/component/light_eater/UnregisterFromParent()
	. = ..()
	parent.RemoveElement(/datum/element/light_eater)
	UnregisterSignal(parent, COMSIG_LIGHT_EATER_DEVOUR)

/datum/component/light_eater/InheritComponent(datum/component/C, i_am_original, list/_eaten)
	. = ..()
	if(!LAZYLEN(_eaten))
		return

	LAZYINITLIST(eaten_lights)
	var/list/cached_eaten_lights = eaten_lights
	for(var/morsel in _eaten)
		RegisterSignal(morsel, COMSIG_PARENT_QDELETING, .proc/deref_eaten_light)
		LAZYSET(cached_eaten_lights, morsel, TRUE)

/// Handles storing references to lights eaten by the light eater.
/datum/component/light_eater/proc/on_devour(datum/source, atom/morsel)
	SIGNAL_HANDLER
	LAZYSET(eaten_lights, morsel, TRUE)
	RegisterSignal(morsel, COMSIG_PARENT_QDELETING, .proc/deref_eaten_light)
	return NONE

/// Handles dereferencing deleted lights.
/datum/component/light_eater/proc/deref_eaten_light(atom/eaten_light, force)
	SIGNAL_HANDLER
	UnregisterSignal(eaten_light, COMSIG_PARENT_QDELETING)
	LAZYREMOVE(eaten_lights, eaten_light)
	return NONE


code:
#define LOCKON_AIMING_MAX_CURSOR_RADIUS 7
#define LOCKON_IGNORE_RESULT "ignore_my_result"
#define LOCKON_RANGING_BREAK_CHECK if(current_ranging_id != this_id){return LOCKON_IGNORE_RESULT}

/datum/component/lockon_aiming
	dupe_mode = COMPONENT_DUPE_ALLOWED
	var/lock_icon = 'icons/mob/cameramob.dmi'
	var/lock_icon_state = "marker"
	var/mutable_appearance/lock_appearance
	var/list/image/lock_images
	var/list/target_typecache
	var/list/immune_weakrefs //list(weakref = TRUE)
	var/mob_stat_check = TRUE //if a potential target is a mob make sure it's conscious!
	var/lock_amount = 1
	var/lock_cursor_range = 5
	var/list/locked_weakrefs
	var/update_disabled = FALSE
	var/current_ranging_id = 0
	var/list/last_location
	var/datum/callback/on_lock
	var/datum/callback/can_target_callback

/datum/component/lockon_aiming/Initialize(range, list/typecache, amount, list/immune, datum/callback/when_locked, icon, icon_state, datum/callback/target_callback)
	if(!ismob(parent))
		return COMPONENT_INCOMPATIBLE
	if(target_callback)
		can_target_callback = target_callback
	else
		can_target_callback = CALLBACK(src, .proc/can_target)
	if(range)
		lock_cursor_range = range
	if(typecache)
		target_typecache = typecache
	if(amount)
		lock_amount = amount
	immune_weakrefs = list(WEAKREF(parent) = TRUE) //Manually take this out if you want..
	if(immune)
		for(var/i in immune)
			if(isweakref(i))
				immune_weakrefs[i] = TRUE
			else if(isatom(i))
				immune_weakrefs[WEAKREF(i)] = TRUE
	if(when_locked)
		on_lock = when_locked
	if(icon)
		lock_icon = icon
	if(icon_state)
		lock_icon_state = icon_state
	generate_lock_visuals()
	START_PROCESSING(SSfastprocess, src)

/datum/component/lockon_aiming/Destroy()
	clear_visuals()
	STOP_PROCESSING(SSfastprocess, src)
	return ..()

/datum/component/lockon_aiming/proc/show_visuals()
	LAZYINITLIST(lock_images)
	var/mob/M = parent
	if(!M.client)
		return
	for(var/i in locked_weakrefs)
		var/datum/weakref/R = i
		var/atom/A = R.resolve()
		if(!A)
			continue //It'll be cleared by processing.
		var/image/I = new
		I.appearance = lock_appearance
		I.loc = A
		M.client.images |= I
		lock_images |= I

/datum/component/lockon_aiming/proc/clear_visuals()
	var/mob/M = parent
	if(!M.client)
		return
	if(!lock_images)
		return
	for(var/i in lock_images)
		M.client.images -= i
		qdel(i)
	lock_images.Cut()

/datum/component/lockon_aiming/proc/refresh_visuals()
	clear_visuals()
	show_visuals()

/datum/component/lockon_aiming/proc/generate_lock_visuals()
	lock_appearance = mutable_appearance(icon = lock_icon, icon_state = lock_icon_state, layer = FLOAT_LAYER)

/datum/component/lockon_aiming/proc/unlock_all(refresh_vis = TRUE)
	LAZYCLEARLIST(locked_weakrefs)
	if(refresh_vis)
		refresh_visuals()

/datum/component/lockon_aiming/proc/unlock(atom/A, refresh_vis = TRUE)
	if(!A.weak_reference)
		return
	LAZYREMOVE(locked_weakrefs, A.weak_reference)
	if(refresh_vis)
		refresh_visuals()

/datum/component/lockon_aiming/proc/lock(atom/A, refresh_vis = TRUE)
	LAZYOR(locked_weakrefs, WEAKREF(A))
	if(refresh_vis)
		refresh_visuals()

/datum/component/lockon_aiming/proc/add_immune_atom(atom/A)
	var/datum/weakref/R = WEAKREF(A)
	if(immune_weakrefs && (immune_weakrefs[R]))
		return
	LAZYSET(immune_weakrefs, R, TRUE)

/datum/component/lockon_aiming/proc/remove_immune_atom(atom/A)
	if(!A.weak_reference || !immune_weakrefs) //if A doesn't have a weakref how did it get on the immunity list?
		return
	LAZYREMOVE(immune_weakrefs, A.weak_reference)

/datum/component/lockon_aiming/process()
	if(update_disabled)
		return
	if(!last_location)
		return
	var/changed = FALSE
	for(var/i in locked_weakrefs)
		var/datum/weakref/R = i
		if(istype(R))
			var/atom/thing = R.resolve()
			if(!istype(thing) || (get_dist(thing, locate(last_location[1], last_location[2], last_location[3])) > lock_cursor_range))
				unlock(R)
				changed = TRUE
		else
			unlock(R)
			changed = TRUE
	if(changed)
		autolock()

/datum/component/lockon_aiming/proc/autolock()
	var/mob/M = parent
	if(!M.client)
		return FALSE
	var/datum/position/current = mouse_absolute_datum_map_position_from_client(M.client)
	var/turf/target = current.return_turf()
	var/list/atom/targets = get_nearest(target, target_typecache, lock_amount, lock_cursor_range)
	if(targets == LOCKON_IGNORE_RESULT)
		return
	unlock_all(FALSE)
	for(var/i in targets)
		if(immune_weakrefs[WEAKREF(i)])
			continue
		lock(i, FALSE)
	refresh_visuals()
	on_lock.Invoke(locked_weakrefs)

/datum/component/lockon_aiming/proc/can_target(atom/A)
	var/mob/M = A
	return is_type_in_typecache(A, target_typecache) && !(ismob(A) && mob_stat_check && M.stat != CONSCIOUS) && !immune_weakrefs[WEAKREF(A)]

/datum/component/lockon_aiming/proc/get_nearest(turf/T, list/typecache, amount, range)
	current_ranging_id++
	var/this_id = current_ranging_id
	var/list/L = list()
	var/turf/center = get_turf(T)
	if(amount < 1 || range < 0 || !istype(center) || !islist(typecache))
		return
	if(range == 0)
		return typecache_filter_list(T.contents + T, typecache)
	var/x = 0
	var/y = 0
	var/cd = 0
	while(cd <= range)
		x = center.x - cd + 1
		y = center.y + cd
		LOCKON_RANGING_BREAK_CHECK
		for(x in x to center.x + cd)
			T = locate(x, y, center.z)
			if(T)
				L |= special_list_filter(T.contents, can_target_callback)
				if(L.len >= amount)
					L.Cut(amount+1)
					return L
		LOCKON_RANGING_BREAK_CHECK
		y = center.y + cd - 1
		x = center.x + cd
		for(y in center.y - cd to y)
			T = locate(x, y, center.z)
			if(T)
				L |= special_list_filter(T.contents, can_target_callback)
				if(L.len >= amount)
					L.Cut(amount+1)
					return L
		LOCKON_RANGING_BREAK_CHECK
		y = center.y - cd
		x = center.x + cd - 1
		for(x in center.x - cd to x)
			T = locate(x, y, center.z)
			if(T)
				L |= special_list_filter(T.contents, can_target_callback)
				if(L.len >= amount)
					L.Cut(amount+1)
					return L
		LOCKON_RANGING_BREAK_CHECK
		y = center.y - cd + 1
		x = center.x - cd
		for(y in y to center.y + cd)
			T = locate(x, y, center.z)
			if(T)
				L |= special_list_filter(T.contents, can_target_callback)
				if(L.len >= amount)
					L.Cut(amount+1)
					return L
		LOCKON_RANGING_BREAK_CHECK
		cd++
		CHECK_TICK


code:
/datum/component/manual_blinking
	dupe_mode = COMPONENT_DUPE_UNIQUE

	var/obj/item/organ/eyes/E
	var/warn_grace = FALSE
	var/warn_dying = FALSE
	var/last_blink
	var/check_every = 20 SECONDS
	var/grace_period = 6 SECONDS
	var/damage_rate = 1 // organ damage taken per tick
	var/list/valid_emotes = list(/datum/emote/living/carbon/blink, /datum/emote/living/carbon/blink_r)

/datum/component/manual_blinking/Initialize()
	if(!iscarbon(parent))
		return COMPONENT_INCOMPATIBLE

	var/mob/living/carbon/C = parent
	E = C.getorganslot(ORGAN_SLOT_EYES)

	if(E)
		START_PROCESSING(SSdcs, src)
		last_blink = world.time
		to_chat(C, span_notice("You suddenly realize you're blinking manually."))

/datum/component/manual_blinking/Destroy(force, silent)
	E = null
	STOP_PROCESSING(SSdcs, src)
	to_chat(parent, span_notice("You revert back to automatic blinking."))
	return ..()

/datum/component/manual_blinking/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOB_EMOTE, .proc/check_emote)
	RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, .proc/check_added_organ)
	RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, .proc/check_removed_organ)
	RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/restart)
	RegisterSignal(parent, COMSIG_LIVING_DEATH, .proc/pause)

/datum/component/manual_blinking/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOB_EMOTE)
	UnregisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN)
	UnregisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN)
	UnregisterSignal(parent, COMSIG_LIVING_REVIVE)
	UnregisterSignal(parent, COMSIG_LIVING_DEATH)

/datum/component/manual_blinking/proc/restart()
	SIGNAL_HANDLER

	START_PROCESSING(SSdcs, src)

/datum/component/manual_blinking/proc/pause()
	SIGNAL_HANDLER

	STOP_PROCESSING(SSdcs, src)

/datum/component/manual_blinking/process()
	var/mob/living/carbon/C = parent

	if(world.time > (last_blink + check_every + grace_period))
		if(!warn_dying)
			to_chat(C, span_userdanger("Your eyes begin to wither, you need to blink!"))
			warn_dying = TRUE

		E.applyOrganDamage(damage_rate)
	else if(world.time > (last_blink + check_every))
		if(!warn_grace)
			to_chat(C, span_danger("You feel a need to blink!"))
			warn_grace = TRUE

/datum/component/manual_blinking/proc/check_added_organ(mob/who_cares, obj/item/organ/O)
	SIGNAL_HANDLER

	var/obj/item/organ/eyes/new_eyes = O

	if(istype(new_eyes,/obj/item/organ/eyes))
		E = new_eyes
		START_PROCESSING(SSdcs, src)

/datum/component/manual_blinking/proc/check_removed_organ(mob/who_cares, obj/item/organ/O)
	SIGNAL_HANDLER

	var/obj/item/organ/eyes/bye_beyes = O // oh come on, that's pretty good

	if(istype(bye_beyes, /obj/item/organ/eyes))
		E = null
		STOP_PROCESSING(SSdcs, src)

/datum/component/manual_blinking/proc/check_emote(mob/living/carbon/user, datum/emote/emote)
	SIGNAL_HANDLER

	if(emote.type in valid_emotes)
		warn_grace = FALSE
		warn_dying = FALSE
		last_blink = world.time


code:
/datum/component/manual_breathing
	dupe_mode = COMPONENT_DUPE_UNIQUE

	var/obj/item/organ/lungs/L
	var/warn_grace = FALSE
	var/warn_dying = FALSE
	var/last_breath
	var/check_every = 12 SECONDS
	var/grace_period = 6 SECONDS
	var/damage_rate = 1 // organ damage taken per tick
	var/datum/emote/next_breath_type = /datum/emote/living/inhale

/datum/component/manual_breathing/Initialize()
	if(!iscarbon(parent))
		return COMPONENT_INCOMPATIBLE

	var/mob/living/carbon/C = parent
	L = C.getorganslot(ORGAN_SLOT_LUNGS)

	if(L)
		START_PROCESSING(SSdcs, src)
		last_breath = world.time
		to_chat(C, span_notice("You suddenly realize you're breathing manually."))

/datum/component/manual_breathing/Destroy(force, silent)
	L = null
	STOP_PROCESSING(SSdcs, src)
	to_chat(parent, span_notice("You revert back to automatic breathing."))
	return ..()

/datum/component/manual_breathing/RegisterWithParent()
	RegisterSignal(parent, COMSIG_MOB_EMOTE, .proc/check_emote)
	RegisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN, .proc/check_added_organ)
	RegisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN, .proc/check_removed_organ)
	RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/restart)
	RegisterSignal(parent, COMSIG_LIVING_DEATH, .proc/pause)

/datum/component/manual_breathing/UnregisterFromParent()
	UnregisterSignal(parent, COMSIG_MOB_EMOTE)
	UnregisterSignal(parent, COMSIG_CARBON_GAIN_ORGAN)
	UnregisterSignal(parent, COMSIG_CARBON_LOSE_ORGAN)
	UnregisterSignal(parent, COMSIG_LIVING_REVIVE)
	UnregisterSignal(parent, COMSIG_LIVING_DEATH)

/datum/component/manual_breathing/proc/restart()
	SIGNAL_HANDLER

	START_PROCESSING(SSdcs, src)

/datum/component/manual_breathing/proc/pause()
	SIGNAL_HANDLER

	STOP_PROCESSING(SSdcs, src)

/datum/component/manual_breathing/process()
	var/mob/living/carbon/C = parent

	var/next_text = initial(next_breath_type.key)
	if(world.time > (last_breath + check_every + grace_period))
		if(!warn_dying)
			to_chat(C, span_userdanger("You begin to suffocate, you need to [next_text]!"))
			warn_dying = TRUE

		L.applyOrganDamage(damage_rate)
		C.losebreath += 0.8
	else if(world.time > (last_breath + check_every))
		if(!warn_grace)
			to_chat(C, span_danger("You feel a need to [next_text]!"))
			warn_grace = TRUE

/datum/component/manual_breathing/proc/check_added_organ(mob/who_cares, obj/item/organ/O)
	SIGNAL_HANDLER

	var/obj/item/organ/eyes/new_lungs = O

	if(istype(new_lungs,/obj/item/organ/lungs))
		L = new_lungs
		START_PROCESSING(SSdcs, src)

/datum/component/manual_breathing/proc/check_removed_organ(mob/who_cares, obj/item/organ/O)
	SIGNAL_HANDLER

	var/obj/item/organ/lungs/old_lungs = O

	if(istype(old_lungs, /obj/item/organ/lungs))
		L = null
		STOP_PROCESSING(SSdcs, src)

/datum/component/manual_breathing/proc/check_emote(mob/living/carbon/user, datum/emote/emote)
	SIGNAL_HANDLER

	if(emote.type == next_breath_type)
		if(next_breath_type == /datum/emote/living/inhale)
			next_breath_type = /datum/emote/living/exhale
		else
			next_breath_type = /datum/emote/living/inhale

		warn_grace = FALSE
		warn_dying = FALSE
		last_breath = world.time

		var/mob/living/carbon/C = parent
		C.losebreath = max(0, C.losebreath - 0.4)


code:
/*!
	This datum should be used for handling mineral contents of machines and whatever else is supposed to hold minerals and make use of them.

	Variables:
		amount - raw amount of the mineral this container is holding, calculated by the defined value MINERAL_MATERIAL_AMOUNT=2000.
		max_amount - max raw amount of mineral this container can hold.
		sheet_type - type of the mineral sheet the container handles, used for output.
		parent - object that this container is being used by, used for output.
		MAX_STACK_SIZE - size of a stack of mineral sheets. Constant.
*/

/datum/component/material_container
	/// The total amount of materials this material container contains
	var/total_amount = 0
	/// The maximum amount of materials this material container can contain
	var/max_amount
	/// Map of material ref -> amount
	var/list/materials //Map of key = material ref | Value = amount
	/// The list of materials that this material container can accept
	var/list/allowed_materials
	/// The typecache of things that this material container can accept
	var/list/allowed_item_typecache
	/// The last main material that was inserted into this container
	var/last_inserted_id
	/// Whether or not this material container allows specific amounts from sheets to be inserted
	var/precise_insertion = FALSE
	/// A callback for checking wheter we can insert a material into this container
	var/datum/callback/insertion_check
	/// A callback invoked before materials are inserted into this container
	var/datum/callback/precondition
	/// A callback invoked after materials are inserted into this container
	var/datum/callback/after_insert
	/// The material container flags. See __DEFINES/materials.dm.
	var/mat_container_flags

/// Sets up the proper signals and fills the list of materials with the appropriate references.
/datum/component/material_container/Initialize(list/init_mats, max_amt = 0, _mat_container_flags=NONE, list/allowed_mats=init_mats, list/allowed_items, datum/callback/_insertion_check, datum/callback/_precondition, datum/callback/_after_insert)
	if(!isatom(parent))
		return COMPONENT_INCOMPATIBLE

	materials = list()
	max_amount = max(0, max_amt)
	mat_container_flags = _mat_container_flags

	allowed_materials = allowed_mats || list()
	if(allowed_items)
		if(ispath(allowed_items) && allowed_items == /obj/item/stack)
			allowed_item_typecache = GLOB.typecache_stack
		else
			allowed_item_typecache = typecacheof(allowed_items)

	insertion_check = _insertion_check
	precondition = _precondition
	after_insert = _after_insert

	for(var/mat in init_mats) //Make the assoc list material reference -> amount
		var/mat_ref = GET_MATERIAL_REF(mat)
		if(isnull(mat_ref))
			continue
		var/mat_amt = init_mats[mat]
		if(isnull(mat_amt))
			mat_amt = 0
		materials[mat_ref] += mat_amt

/datum/component/material_container/Destroy(force, silent)
	materials = null
	allowed_materials = null
	if(insertion_check)
		QDEL_NULL(insertion_check)
	if(precondition)
		QDEL_NULL(precondition)
	if(after_insert)
		QDEL_NULL(after_insert)
	return ..()

/datum/component/material_container/RegisterWithParent()
	. = ..()

	if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
		RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
	if(mat_container_flags & MATCONTAINER_EXAMINE)
		RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)

/datum/component/material_container/vv_edit_var(var_name, var_value)
	var/old_flags = mat_container_flags
	. = ..()
	if(var_name == NAMEOF(src, mat_container_flags) && parent)
		if(!(old_flags & MATCONTAINER_EXAMINE) && mat_container_flags & MATCONTAINER_EXAMINE)
			RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
		else if(old_flags & MATCONTAINER_EXAMINE && !(mat_container_flags & MATCONTAINER_EXAMINE))
			UnregisterSignal(parent, COMSIG_PARENT_EXAMINE)

		if(old_flags & MATCONTAINER_NO_INSERT && !(mat_container_flags & MATCONTAINER_NO_INSERT))
			RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
		else if(!(old_flags & MATCONTAINER_NO_INSERT) && mat_container_flags & MATCONTAINER_NO_INSERT)
			UnregisterSignal(parent, COMSIG_PARENT_ATTACKBY)

/datum/component/material_container/proc/on_examine(datum/source, mob/user, list/examine_texts)
	SIGNAL_HANDLER

	for(var/I in materials)
		var/datum/material/M = I
		var/amt = materials[I]
		if(amt)
			examine_texts += span_notice("It has [amt] units of [lowertext(M.name)] stored.")

/// Proc that allows players to fill the parent with mats
/datum/component/material_container/proc/on_attackby(datum/source, obj/item/I, mob/living/user)
	SIGNAL_HANDLER

	var/list/tc = allowed_item_typecache
	/* SEPTIC EDIT REMOVAL
	if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
		return
	*/
	//SEPTIC EDIT BEGIN
	if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && (user.a_intent == INTENT_HARM))
		return
	//SEPTIC EDIT END
	if(I.item_flags & ABSTRACT)
		return
	if((I.flags_1 & HOLOGRAM_1) || (I.item_flags & NO_MAT_REDEMPTION) || (tc && !is_type_in_typecache(I, tc)))
		if(!(mat_container_flags & MATCONTAINER_SILENT))
			to_chat(user, span_warning("[parent] won't accept [I]!"))
		return
	. = COMPONENT_NO_AFTERATTACK
	var/datum/callback/pc = precondition
	if(pc && !pc.Invoke(user))
		return
	var/material_amount = get_item_material_amount(I, mat_container_flags)
	if(!material_amount)
		to_chat(user, span_warning("[I] does not contain sufficient materials to be accepted by [parent]."))
		return
	if(!has_space(material_amount))
		to_chat(user, span_warning("[parent] is full. Please remove materials from [parent] in order to insert more."))
		return
	user_insert(I, user, mat_container_flags)

/// Proc used for when player inserts materials
/datum/component/material_container/proc/user_insert(obj/item/I, mob/living/user, breakdown_flags = mat_container_flags)
	set waitfor = FALSE
	var/requested_amount
	var/active_held = user.get_active_held_item()  // differs from I when using TK
	if(istype(I, /obj/item/stack) && precise_insertion)
		var/atom/current_parent = parent
		var/obj/item/stack/S = I
		requested_amount = input(user, "How much do you want to insert?", "Inserting [S.singular_name]s") as num|null
		if(isnull(requested_amount) || (requested_amount <= 0))
			return
		if(QDELETED(I) || QDELETED(user) || QDELETED(src) || parent != current_parent || user.physical_can_use_topic(current_parent) < UI_INTERACTIVE || user.get_active_held_item() != active_held)
			return
	if(!user.temporarilyRemoveItemFromInventory(I))
		to_chat(user, span_warning("[I] is stuck to you and cannot be placed into [parent]."))
		return
	var/inserted = insert_item(I, stack_amt = requested_amount, breakdown_flags= mat_container_flags)
	if(inserted)
		to_chat(user, span_notice("You insert a material total of [inserted] into [parent]."))
		qdel(I)
		if(after_insert)
			after_insert.Invoke(I, last_inserted_id, inserted)
	else if(I == active_held)
		user.put_in_active_hand(I)

/// Proc specifically for inserting items, returns the amount of materials entered.
/datum/component/material_container/proc/insert_item(obj/item/I, multiplier = 1, stack_amt, breakdown_flags = mat_container_flags)
	if(QDELETED(I))
		return FALSE

	multiplier = CEILING(multiplier, 0.01)

	var/material_amount = get_item_material_amount(I, breakdown_flags)
	if(!material_amount || !has_space(material_amount))
		return FALSE

	last_inserted_id = insert_item_materials(I, multiplier, breakdown_flags)
	return material_amount

/**
 * Inserts the relevant materials from an item into this material container.
 *
 * Arguments:
 * - [source][/obj/item]: The source of the materials we are inserting.
 * - multiplier: The multiplier for the materials being inserted.
 * - breakdown_flags: The breakdown bitflags that will be used to retrieve the materials from the source
 */
/datum/component/material_container/proc/insert_item_materials(obj/item/source, multiplier = 1, breakdown_flags = mat_container_flags)
	var/primary_mat
	var/max_mat_value = 0
	var/list/item_materials = source.get_material_composition(breakdown_flags)
	for(var/MAT in item_materials)
		if(!can_hold_material(MAT))
			continue
		materials[MAT] += item_materials[MAT] * multiplier
		total_amount += item_materials[MAT] * multiplier
		if(item_materials[MAT] > max_mat_value)
			max_mat_value = item_materials[MAT]
			primary_mat = MAT

	return primary_mat

/**
 * The default check for whether we can add materials to this material container.
 *
 * Arguments:
 * - [mat][/atom/material]: The material we are checking for insertability.
 */
/datum/component/material_container/proc/can_hold_material(datum/material/mat)
	if(mat in allowed_materials)
		return TRUE
	if(istype(mat) && ((mat.id in allowed_materials) || (mat.type in allowed_materials)))
		allowed_materials += mat // This could get messy with passing lists by ref... but if you're doing that the list expansion is probably being taken care of elsewhere anyway...
		return TRUE
	if(insertion_check?.Invoke(mat))
		allowed_materials += mat
		return TRUE
	return FALSE

/// For inserting an amount of material
/datum/component/material_container/proc/insert_amount_mat(amt, datum/material/mat)
	if(amt <= 0 || !has_space(amt))
		return 0

	var/total_amount_saved = total_amount
	if(mat)
		if(!istype(mat))
			mat = GET_MATERIAL_REF(mat)
		materials[mat] += amt
	else
		var/num_materials = length(materials)
		if(!num_materials)
			return 0

		amt /= num_materials
		for(var/i in materials)
			materials[i] += amt
			total_amount += amt
	return (total_amount - total_amount_saved)

/// Uses an amount of a specific material, effectively removing it.
/datum/component/material_container/proc/use_amount_mat(amt, datum/material/mat)
	if(!istype(mat))
		mat = GET_MATERIAL_REF(mat)

	if(!mat)
		return 0
	var/amount = materials[mat]
	if(amount < amt)
		return 0

	materials[mat] -= amt
	total_amount -= amt
	return amt

/// Proc for transfering materials to another container.
/datum/component/material_container/proc/transer_amt_to(datum/component/material_container/T, amt, datum/material/mat)
	if(!istype(mat))
		mat = GET_MATERIAL_REF(mat)
	if((amt==0)||(!T)||(!mat))
		return FALSE
	if(amt<0)
		return T.transer_amt_to(src, -amt, mat)
	var/tr = min(amt, materials[mat], T.can_insert_amount_mat(amt, mat))
	if(tr)
		use_amount_mat(tr, mat)
		T.insert_amount_mat(tr, mat)
		return tr
	return FALSE

/// Proc for checking if there is room in the component, returning the amount or else the amount lacking.
/datum/component/material_container/proc/can_insert_amount_mat(amt, datum/material/mat)
	if(!amt || !mat)
		return 0

	if((total_amount + amt) <= max_amount)
		return amt
	else
		return (max_amount - total_amount)

/// For consuming a dictionary of materials. mats is the map of materials to use and the corresponding amounts, example: list(M/datum/material/glass =100, datum/material/iron=200)
/datum/component/material_container/proc/use_materials(list/mats, multiplier=1)
	if(!mats || !length(mats))
		return FALSE

	var/list/mats_to_remove = list() //Assoc list MAT | AMOUNT

	for(var/x in mats) //Loop through all required materials
		var/datum/material/req_mat = x
		if(!istype(req_mat))
			req_mat = GET_MATERIAL_REF(req_mat) //Get the ref if necesary
		if(!materials[req_mat]) //Do we have the resource?
			return FALSE //Can't afford it
		var/amount_required = mats[x] * multiplier
		if(!(materials[req_mat] >= amount_required)) // do we have enough of the resource?
			return FALSE //Can't afford it
		mats_to_remove[req_mat] += amount_required //Add it to the assoc list of things to remove
		continue

	var/total_amount_save = total_amount

	for(var/i in mats_to_remove)
		total_amount_save -= use_amount_mat(mats_to_remove[i], i)

	return total_amount_save - total_amount

/// For spawning mineral sheets at a specific location. Used by machines to output sheets.
/datum/component/material_container/proc/retrieve_sheets(sheet_amt, datum/material/M, atom/target = null)
	if(!M.sheet_type)
		return 0 //Add greyscale sheet handling here later
	if(sheet_amt <= 0)
		return 0

	if(!target)
		var/atom/parent_atom = parent
		target = parent_atom.drop_location()
	if(materials[M] < (sheet_amt * MINERAL_MATERIAL_AMOUNT))
		sheet_amt = round(materials[M] / MINERAL_MATERIAL_AMOUNT)
	var/count = 0
	while(sheet_amt > MAX_STACK_SIZE)
		new M.sheet_type(target, MAX_STACK_SIZE, null, list((M) = MINERAL_MATERIAL_AMOUNT))
		count += MAX_STACK_SIZE
		use_amount_mat(sheet_amt * MINERAL_MATERIAL_AMOUNT, M)
		sheet_amt -= MAX_STACK_SIZE
	if(sheet_amt >= 1)
		new M.sheet_type(target, sheet_amt, null, list((M) = MINERAL_MATERIAL_AMOUNT))
		count += sheet_amt
		use_amount_mat(sheet_amt * MINERAL_MATERIAL_AMOUNT, M)
	return count

/// Proc to get all the materials and dump them as sheets
/datum/component/material_container/proc/retrieve_all(target = null)
	var/result = 0
	for(var/MAT in materials)
		var/amount = materials[MAT]
		result += retrieve_sheets(amount2sheet(amount), MAT, target)
	return result

/// Proc that returns TRUE if the container has space
/datum/component/material_container/proc/has_space(amt = 0)
	return (total_amount + amt) <= max_amount

/// Checks if its possible to afford a certain amount of materials. Takes a dictionary of materials.
/datum/component/material_container/proc/has_materials(list/mats, multiplier=1)
	if(!mats || !mats.len)
		return FALSE

	for(var/x in mats) //Loop through all required materials
		var/datum/material/req_mat = x
		if(!istype(req_mat))
			if(ispath(req_mat)) //Is this an actual material, or is it a category?
				req_mat = GET_MATERIAL_REF(req_mat) //Get the ref

			else // Its a category. (For example MAT_CATEGORY_RIGID)
				if(!has_enough_of_category(req_mat, mats[x], multiplier)) //Do we have enough of this category?
					return FALSE
				else
					continue

		if(!has_enough_of_material(req_mat, mats[x], multiplier))//Not a category, so just check the normal way
			return FALSE

	return TRUE

/// Returns all the categories in a recipe.
/datum/component/material_container/proc/get_categories(list/mats)
	var/list/categories = list()
	for(var/x in mats) //Loop through all required materials
		if(!istext(x)) //This means its not a category
			continue
		categories += x
	return categories

/// Returns TRUE if you have enough of the specified material.
/datum/component/material_container/proc/has_enough_of_material(datum/material/req_mat, amount, multiplier=1)
	if(!materials[req_mat]) //Do we have the resource?
		return FALSE //Can't afford it
	var/amount_required = amount * multiplier
	if(materials[req_mat] >= amount_required) // do we have enough of the resource?
		return TRUE
	return FALSE //Can't afford it

/// Returns TRUE if you have enough of a specified material category (Which could be multiple materials)
/datum/component/material_container/proc/has_enough_of_category(category, amount, multiplier=1)
	for(var/i in SSmaterials.materials_by_category[category])
		var/datum/material/mat = i
		if(materials[mat] >= amount) //we have enough
			return TRUE
	return FALSE

/// Turns a material amount into the amount of sheets it should output
/datum/component/material_container/proc/amount2sheet(amt)
	if(amt >= MINERAL_MATERIAL_AMOUNT)
		return round(amt / MINERAL_MATERIAL_AMOUNT)
	return FALSE

/// Turns an amount of sheets into the amount of material amount it should output
/datum/component/material_container/proc/sheet2amount(sheet_amt)
	if(sheet_amt > 0)
		return sheet_amt * MINERAL_MATERIAL_AMOUNT
	return FALSE

///returns the amount of material relevant to this container; if this container does not support glass, any glass in 'I' will not be taken into account
/datum/component/material_container/proc/get_item_material_amount(obj/item/I, breakdown_flags = mat_container_flags)
	if(!istype(I) || !I.custom_materials)
		return 0
	var/material_amount = 0
	var/list/item_materials = I.get_material_composition(breakdown_flags)
	for(var/MAT in item_materials)
		if(!can_hold_material(MAT))
			continue
		material_amount += item_materials[MAT]
	return material_amount

/// Returns the amount of a specific material in this container.
/datum/component/material_container/proc/get_material_amount(datum/material/mat)
	if(!istype(mat))
		mat = GET_MATERIAL_REF(mat)
	return materials[mat]

/// List format is list(material_name = list(amount = ..., ref = ..., etc.))
/datum/component/material_container/ui_data(mob/user)
	var/list/data = list()

	for(var/datum/material/material as anything in materials)
		var/amount = materials[material]

		data += list(list(
			"name" = material.name,
			"ref" = REF(material),
			"amount" = amount,
			"sheets" = round(amount / MINERAL_MATERIAL_AMOUNT),
			"removable" = amount >= MINERAL_MATERIAL_AMOUNT,
		))

	return data


code:
/datum/component/mirage_border
	can_transfer = TRUE
	var/obj/effect/abstract/mirage_holder/holder

/datum/component/mirage_border/Initialize(turf/target, direction, range=world.view)
	if(!isturf(parent))
		return COMPONENT_INCOMPATIBLE
	if(!target || !istype(target) || !direction)
		. = COMPONENT_INCOMPATIBLE
		CRASH("[type] improperly instanced with the following args: target=\[[target]\], direction=\[[direction]\], range=\[[range]\]")

	holder = new(parent)

	var/x = target.x
	var/y = target.y
	var/z = target.z
	var/turf/southwest = locate(clamp(x - (direction & WEST ? range : 0), 1, world.maxx), clamp(y - (direction & SOUTH ? range : 0), 1, world.maxy), clamp(z, 1, world.maxz))
	var/turf/northeast = locate(clamp(x + (direction & EAST ? range : 0), 1, world.maxx), clamp(y + (direction & NORTH ? range : 0), 1, world.maxy), clamp(z, 1, world.maxz))
	//holder.vis_contents += block(southwest, northeast) // This doesnt work because of beta bug memes
	for(var/i in block(southwest, northeast))
		holder.vis_contents += i
	if(direction & SOUTH)
		holder.pixel_y -= world.icon_size * range
	if(direction & WEST)
		holder.pixel_x -= world.icon_size * range

/datum/component/mirage_border/Destroy()
	QDEL_NULL(holder)
	return ..()

/datum/component/mirage_border/PreTransfer()
	holder.moveToNullspace()

/datum/component/mirage_border/PostTransfer()
	if(!isturf(parent))
		return COMPONENT_INCOMPATIBLE
	holder.forceMove(parent)

/obj/effect/abstract/mirage_holder
	name = "Mirage holder"
	anchored = TRUE
	mouse_opacity = MOUSE_OPACITY_TRANSPARENT


code:
/datum/component/mirv
	var/projectile_type
	var/radius // shoots a projectile for every turf on this radius from the hit target
	var/override_projectile_range

/datum/component/mirv/Initialize(projectile_type, radius=1, override_projectile_range)
	if(!isgun(parent) && !ismachinery(parent) && !isstructure(parent) && !isgrenade(parent))
		return COMPONENT_INCOMPATIBLE

	src.projectile_type = projectile_type
	src.radius = radius
	src.override_projectile_range = override_projectile_range

	if(isgrenade(parent))
		parent.AddComponent(/datum/component/pellet_cloud, projectile_type=projectile_type)

/datum/component/mirv/RegisterWithParent()
	if(ismachinery(parent) || isstructure(parent) || isgun(parent)) // turrets, etc
		RegisterSignal(parent, COMSIG_PROJECTILE_ON_HIT, .proc/projectile_hit)

/datum/component/mirv/UnregisterFromParent()
	UnregisterSignal(parent, list(COMSIG_PROJECTILE_ON_HIT))

/datum/component/mirv/proc/projectile_hit(atom/fired_from, atom/movable/firer, atom/target, Angle)
	SIGNAL_HANDLER

	INVOKE_ASYNC(src, .proc/do_shrapnel, firer, target)

/datum/component/mirv/proc/do_shrapnel(mob/firer, atom/target)
	if(radius < 1)
		return
	var/turf/target_turf = get_turf(target)
	for(var/turf/shootat_turf in RANGE_TURFS(radius, target) - RANGE_TURFS(radius-1, target))

		var/obj/projectile/P = new projectile_type(target_turf)
		//Shooting Code:
		P.range = radius+1
		if(override_projectile_range)
			P.range = override_projectile_range
		P.preparePixelProjectile(shootat_turf, target)
		P.firer = firer // don't hit ourself that would be really annoying
		P.impacted = list(target = TRUE) // don't hit the target we hit already with the flak
		P.fire()