code:
/client/var/inquisitive_ghost = 1
/mob/dead/observer/verb/toggle_inquisition() // warning: unexpected inquisition
	set name = "Toggle Inquisitiveness"
	set desc = "Sets whether your ghost examines everything on click by default"
	set category = "Ghost"
	if(!client) return
	client.inquisitive_ghost = !client.inquisitive_ghost
	if(client.inquisitive_ghost)
		src << "You will now examine everything you click on."
	else
		src << "You will no longer examine things you click on."

/mob/dead/observer/DblClickOn(var/atom/A, var/params)
	if(client.buildmode)
		build_click(src, client.buildmode, params, A)
		return
	if(can_reenter_corpse && mind && mind.current)
		if(A == mind.current || (mind.current in A)) // double click your corpse or whatever holds it
			reenter_corpse()						// (cloning scanner, body bag, closet, mech, etc)
			return									// seems legit.

	// Things you might plausibly want to follow
	if((ismob(A) && A != src) || istype(A,/obj/machinery/bot) || istype(A,/obj/singularity)|| istype(A, /obj/machinery/power/supermatter)) //why isn't this OOP
		ManualFollow(A)

	// Otherwise jump
	else if(A.loc)
		loc = get_turf(A)

/mob/dead/observer/ClickOn(var/atom/A, var/params)
	if(client.buildmode)
		build_click(src, client.buildmode, params, A)
		return

	var/list/modifiers = params2list(params)
	if(modifiers["middle"])
		MiddleClickOn(A)
		return
	if(modifiers["shift"])
		ShiftClickOn(A)
		return
	if(modifiers["alt"])
		AltClickOn(A)
		return
	if(modifiers["ctrl"])
		CtrlClickOn(A)
		return

	if(world.time <= next_move)
		return
	// You are responsible for checking config.ghost_interaction when you override this function
	// Not all of them require checking, see below
	A.attack_ghost(src)

// Oh by the way this didn't work with old click code which is why clicking shit didn't spam you
/atom/proc/attack_ghost(mob/dead/observer/user as mob)
	if(user.client && user.client.inquisitive_ghost)
		user.examinate(src)
	return

// ---------------------------------------
// And here are some good things for free:
// Now you can click through portals, wormholes, gateways, and teleporters while observing. -Sayu

/obj/machinery/teleport/hub/attack_ghost(mob/user as mob)
	var/atom/l = loc
	var/obj/machinery/computer/teleporter/com = locate(/obj/machinery/computer/teleporter, locate(l.x - 2, l.y, l.z))
	if(com && com.locked)
		user.loc = get_turf(com.locked)

/obj/effect/portal/attack_ghost(mob/user as mob)
	if(target)
		user.loc = get_turf(target)

/obj/machinery/gateway/centerstation/attack_ghost(mob/user as mob)
	if(awaygate)
		user.loc = awaygate.loc
	else
		user << "[src] has no destination."

/obj/machinery/gateway/centeraway/attack_ghost(mob/user as mob)
	if(stationgate)
		user.loc = stationgate.loc
	else
		user << "[src] has no destination."

/obj/item/weapon/storage/attack_ghost(mob/user as mob)
	orient2hud(user)
	show_to(user)

// -------------------------------------------
// This was supposed to be used by adminghosts
// I think it is a *terrible* idea
// but I'm leaving it here anyway
// commented out, of course.
/*
/atom/proc/attack_admin(mob/user as mob)
	if(!user || !user.client || !user.client.holder)
		return
	attack_hand(user)

*/


code:
/*
	Humans:
	Adds an exception for gloves, to allow special glove types like the ninja ones.

	Otherwise pretty standard.
*/
/mob/living/carbon/human/UnarmedAttack(var/atom/A, var/proximity)
	var/obj/item/clothing/gloves/G = gloves // not typecast specifically enough in defines

	// Special glove functions:
	// If the gloves do anything, have them return 1 to stop
	// normal attack_hand() here.
	if(proximity && istype(G) && G.Touch(A,1))
		return

	var/override = 0

	for(var/datum/mutation/human/HM in dna.mutations)
		override += HM.on_attack_hand(src, A)

	if(override)	return

	A.attack_hand(src)

/atom/proc/attack_hand(mob/user as mob)
	return

/*
/mob/living/carbon/human/RestrainedClickOn(var/atom/A) ---carbons will handle this
	return
*/

/mob/living/carbon/RestrainedClickOn(var/atom/A)
	return 0

/mob/living/carbon/human/RangedAttack(var/atom/A)
	if(gloves)
		var/obj/item/clothing/gloves/G = gloves
		if(istype(G) && G.Touch(A,0)) // for magic gloves
			return

	for(var/datum/mutation/human/HM in dna.mutations)
		HM.on_ranged_attack(src, A)

/*
	Animals & All Unspecified
*/
/mob/living/UnarmedAttack(var/atom/A)
	A.attack_animal(src)

/mob/living/captive_brain/UnarmedAttack(var/atom/A)
	return

/mob/living/simple_animal/hostile/UnarmedAttack(var/atom/A)
	target = A
	AttackingTarget()

/atom/proc/attack_animal(mob/user as mob)
	return
/mob/living/RestrainedClickOn(var/atom/A)
	return

/*
	Monkeys
*/
/mob/living/carbon/monkey/UnarmedAttack(var/atom/A)
	A.attack_paw(src)
/atom/proc/attack_paw(mob/user as mob)
	return

/*
	Monkey RestrainedClickOn() was apparently the
	one and only use of all of the restrained click code
	(except to stop you from doing things while handcuffed);
	moving it here instead of various hand_p's has simplified
	things considerably
*/
/mob/living/carbon/monkey/RestrainedClickOn(var/atom/A)
	if(..())
		return
	if(a_intent != "harm" || !ismob(A)) return
	if(is_muzzled())
		return
	var/mob/living/carbon/ML = A
	var/dam_zone = pick("chest", "l_hand", "r_hand", "l_leg", "r_leg")
	var/obj/item/organ/limb/affecting = null
	if(ishuman(ML)) // why the hell is this not more general
		affecting = ML:get_organ(ran_zone(dam_zone))
	var/armor = ML.run_armor_check(affecting, "melee")
	if(prob(75))
		ML.apply_damage(rand(1,3), BRUTE, affecting, armor)
		ML.visible_message("[name] bites [ML]!", \
						"[name] bites [ML]!")
		if(armor >= 2) return
		for(var/datum/disease/D in viruses)
			ML.ForceContractDisease(D)
	else
		ML.visible_message("[src] has attempted to bite [ML]!")

/*
	Aliens
	Defaults to same as monkey in most places
*/
/mob/living/carbon/alien/UnarmedAttack(var/atom/A)
	A.attack_alien(src)
/atom/proc/attack_alien(mob/user as mob)
	attack_paw(user)
	return
/mob/living/carbon/alien/RestrainedClickOn(var/atom/A)
	return

// Babby aliens
/mob/living/carbon/alien/larva/UnarmedAttack(var/atom/A)
	A.attack_larva(src)
/atom/proc/attack_larva(mob/user as mob)
	return

/*
	Slimes
	Nothing happening here
*/
/mob/living/carbon/slime/UnarmedAttack(var/atom/A)
	A.attack_slime(src)
/atom/proc/attack_slime(mob/user as mob)
	return
/mob/living/carbon/slime/RestrainedClickOn(var/atom/A)
	return

/*
	New Players:
	Have no reason to click on anything at all.
*/
/mob/new_player/ClickOn()
	return

license: mit


code:
// Blob Overmind Controls

/mob/camera/blob/CtrlClickOn(var/atom/A) // Expand blob
	var/turf/T = get_turf(A)
	if(T)
		expand_blob(T)

/mob/camera/blob/MiddleClickOn(var/atom/A) // Rally spores
	var/turf/T = get_turf(A)
	if(T)
		rally_spores(T)

/mob/camera/blob/AltClickOn(var/atom/A) // Create a shield
	var/turf/T = get_turf(A)
	if(T)
		create_shield(T)


code:
/*
	Telekinesis

	This needs more thinking out, but I might as well.
*/
var/const/tk_maxrange = 15

/*
	Telekinetic attack:

	By default, emulate the user's unarmed attack
*/
/atom/proc/attack_tk(mob/user)
	if(user.stat) return
	user.UnarmedAttack(src,0) // attack_hand, attack_paw, etc
	return

/*
	This is similar to item attack_self, but applies to anything
	that you can grab with a telekinetic grab.

	It is used for manipulating things at range, for example, opening and closing closets.
	There are not a lot of defaults at this time, add more where appropriate.
*/
/atom/proc/attack_self_tk(mob/user)
	return

/obj/item/attack_self_tk(mob/user)
	attack_self(user)

/obj/attack_tk(mob/user)
	if(user.stat) return
	if(anchored)
		..()
		return

	var/obj/item/tk_grab/O = new(src)
	user.put_in_active_hand(O)
	O.host = user
	O.focus_object(src)
	return

/obj/item/attack_tk(mob/user)
	if(user.stat) return
	var/obj/item/tk_grab/O = new(src)
	user.put_in_active_hand(O)
	O.host = user
	O.focus_object(src)
	return

/mob/attack_tk(mob/user)
	return // needs more thinking about

/*
	TK Grab Item (the workhorse of old TK)

	* If you have not grabbed something, do a normal tk attack
	* If you have something, throw it at the target.  If it is already adjacent, do a normal attackby()
	* If you click what you are holding, or attack_self(), do an attack_self_tk() on it.
	* Deletes itself if it is ever not in your hand, or if you should have no access to TK.
*/
/obj/item/tk_grab
	name = "Telekinetic Grab"
	desc = "Magic"
	icon = 'icons/obj/magic.dmi'//Needs sprites
	icon_state = "2"
	flags = NOBLUDGEON | ABSTRACT
	//item_state = null
	w_class = 10.0
	layer = 20

	var/last_throw = 0
	var/atom/movable/focus = null
	var/mob/living/host = null

/obj/item/tk_grab/dropped(mob/user as mob)
	if(focus && user && loc != user && loc != user.loc) // drop_item() gets called when you tk-attack a table/closet with an item
		if(focus.Adjacent(loc))
			focus.loc = loc

	qdel(src)
	return

//stops TK grabs being equipped anywhere but into hands
/obj/item/tk_grab/equipped(var/mob/user, var/slot)
	if( (slot == slot_l_hand) || (slot== slot_r_hand) )	return
	qdel(src)
	return

/obj/item/tk_grab/attack_self(mob/user as mob)
	if(focus)
		focus.attack_self_tk(user)

/obj/item/tk_grab/afterattack(atom/target, mob/living/carbon/user, proximity, params)//TODO: go over this
	if(!target || !user)	return
	if(last_throw+3 > world.time)	return
	if(!host || host != user)
		qdel(src)
		return
	if(!(user.dna.check_mutation(TK)))
		qdel(src)
		return
	if(isobj(target) && !isturf(target.loc))
		return

	if(!tkMaxRangeCheck(user, target, focus))
		return

	if(!focus)
		focus_object(target, user)
		return

	if(target == focus)
		target.attack_self_tk(user)
		return // todo: something like attack_self not laden with assumptions inherent to attack_self

	if(!istype(target, /turf) && istype(focus,/obj/item) && target.Adjacent(focus))
		var/obj/item/I = focus
		var/resolved = target.attackby(I, user, params)
		if(!resolved && target && I)
			I.afterattack(target,user,1) // for splashing with beakers

	else
		apply_focus_overlay()
		focus.throw_at(target, 10, 1)
		last_throw = world.time
	return

/proc/tkMaxRangeCheck(mob/user, atom/target, atom/focus)
	var/d = get_dist(user, target)
	if(focus)
		d = max(d,get_dist(user,focus)) // whichever is further
	if(d > tk_maxrange)
		user << "Your mind won't reach that far."
		return 0
	return 1

/obj/item/tk_grab/attack(mob/living/M as mob, mob/living/user as mob, def_zone)
	return

/obj/item/tk_grab/proc/focus_object(var/obj/target, var/mob/living/user)
	if(!istype(target,/obj))	return//Cant throw non objects atm might let it do mobs later
	if(target.anchored || !isturf(target.loc))
		qdel(src)
		return
	focus = target
	update_icon()
	apply_focus_overlay()
	return

/obj/item/tk_grab/proc/apply_focus_overlay()
	if(!focus)	return
	var/obj/effect/overlay/O = new /obj/effect/overlay(locate(focus.x,focus.y,focus.z))
	O.name = "sparkles"
	O.anchored = 1
	O.density = 0
	O.layer = FLY_LAYER
	O.dir = pick(cardinal)
	O.icon = 'icons/effects/effects.dmi'
	O.icon_state = "nothing"
	flick("empdisable",O)
	spawn(5)
		qdel(O)

/obj/item/tk_grab/update_icon()
	overlays.Cut()
	if(focus && focus.icon && focus.icon_state)
		overlays += icon(focus.icon,focus.icon_state)
	return

obj/item/tk_grab/suicide_act(mob/user)
	user.visible_message("[user] is using \his telekinesis to choke \himself! It looks like \he's trying to commit suicide.")
	return (OXYLOSS)

/*Not quite done likely needs to use something thats not get_step_to
/obj/item/tk_grab/proc/check_path()
	var/turf/ref = get_turf(src.loc)
	var/turf/target = get_turf(focus.loc)
	if(!ref || !target)	return 0
	var/distance = get_dist(ref, target)
	if(distance >= 10)	return 0
	for(var/i = 1 to distance)
		ref = get_step_to(ref, target, 0)
	if(ref != target)	return 0
	return 1
*/

//equip_to_slot_or_del(obj/item/W, slot, qdel_on_fail = 1)
/*
		if(istype(user, /mob/living/carbon))
			if(user:mutations & TK && get_dist(source, user) <= 7)
				if(user:get_active_hand())	return 0
				var/X = source:x
				var/Y = source:y
				var/Z = source:z

*/


code:
/*
	Modified DynamicAreaLighting for TGstation - Coded by Carnwennan

	This is TG's 'new' lighting system. It's basically a heavily modified combination of Forum_Account's and
	ShadowDarke's respective lighting libraries. Credits, where due, to them.

	Like sd_DAL (what we used to use), it changes the shading overlays of areas by splitting each type of area into sub-areas
	by using the var/tag variable and moving turfs into the contents list of the correct sub-area. This method is
	much less costly than using overlays or objects.

	Unlike sd_DAL however it uses a queueing system. Everytime we call a change to opacity or luminosity
	(through SetOpacity() or SetLuminosity()) we are  simply updating variables and scheduling certain lights/turfs for an
	update. Actual updates are handled periodically by the SSlighting subsystem. This carries additional overheads, however it
	means that each thing is changed only once per SSlighting.wait deciseconds. Allowing for greater control
	over how much priority we'd like lighting updates to have.

	UPDATE: we no longer postpone lighting updates by editting variables, there is now a subsystem/proc/postpone() procedure.
			So you would do SSlighting.postpone()

	Unlike our old system there are hardcoded maximum luminositys (different for certain atoms).
	This is to cap the cost of creating lighting effects.
	(without this, an atom with luminosity of 20 would have to update 41^2 turfs!) :s

	Also, in order for the queueing system to work, each light remembers the effect it casts on each turf. This is going to
	have larger memory requirements than our previous system but it's easily worth the hassle for the greater control we
	gain. It also reduces cost of removing lighting effects by a lot!

	Known Issues/TODO:
		Shuttles still do not have support for dynamic lighting (I hope to fix this at some point)
		No directional lighting support. (prototype looked ugly)
*/

#define USE_CIRCULAR_LIGHTING		//comment this out to use old square lighting effects.

#define LIGHTING_LAYER 10									//Drawing layer for lighting overlays
#define LIGHTING_ICON 'icons/effects/ss13_dark_alpha6.dmi'	//Icon used for lighting shading effects
/*
#define LIGHTING_ICON 'icons/effects/ss13_dark_alpha6.dmi'
#define LIGHTING_LAYER 10									//Drawing layer for lighting overlays
*/

datum/light_source
	var/atom/owner
	var/changed = 1
	var/list/effect = list()
	var/__x = 0		// x coordinate at last update
	var/__y = 0		// y coordinate at last update
	var/__z = 0		// z coordinate at last update

	var/_l_color // do not use directly, only used as reference for updating
	var/col_r
	var/col_g
	var/col_b

/datum/light_source/New(atom/A)
	if(!istype(A))
		CRASH("The first argument to the light object's constructor must be the atom that is the light source. Expected atom, received '[A]' instead.")
	..()
	owner = A
	readrgb(owner.l_color)
	__x = owner.x
	__y = owner.y
	__z = owner.z
	// the lighting object maintains a list of all light sources
	SSlighting.lights += src

//Check a light to see if its effect needs reprocessing. If it does, remove any old effect and create a new one
/datum/light_source/proc/check()
	if(!owner)
		remove_effect()
		return 1	//causes it to be removed from our list of lights. The garbage collector will then destroy it.
		// check to see if we've moved since last update
	if(owner.x != __x || owner.y != __y || owner.z != __z)
		__x = owner.x
		__y = owner.y
		__z = owner.z
		changed = 1

	if (owner.l_color != _l_color)
		changed = 1

	if(changed)
		changed = 0
		remove_effect()
		return add_effect()
	return 0

/datum/light_source/proc/remove_effect()
	// before we apply the effect we remove the light's current effect.
	for(var/turf/T in effect)	// negate the effect of this light source
		T.update_lumcount(-effect[T], col_r, col_g, col_b, 1)
	effect.len = 0					// clear the effect list

/datum/light_source/proc/add_effect()
	// only do this if the light is turned on and is on the map
	if(owner.loc && owner.luminosity > 0)
		readrgb(owner.l_color)
		effect = list()
		for(var/turf/T in view(owner.get_light_range(), get_turf(owner)))
			var/delta_lumen = lum(T)
			if(delta_lumen > 0)
				effect[T] = delta_lumen
				T.update_lumcount(delta_lumen, col_r, col_g, col_b, 0)

		return 0
	else
		owner.light = null
		return 1	//cause the light to be removed from the lights list and garbage collected once it's no
						//longer referenced by the queue

/datum/light_source/proc/lum(turf/A)
	if (owner.trueLuminosity < 1)
		return 0
	var/dist
	if(!A)
		dist = 0
	else
#ifdef USE_CIRCULAR_LIGHTING
		dist = cheap_hypotenuse(A.x, A.y, __x, __y)
#else
		dist = max(abs(A.x - __x), abs(A.y - __y))
#endif
	if (owner.trueLuminosity > 100) // This will never happen... right?
		return sqrt(owner.trueLuminosity) - dist
	else
		return sqrtTable[owner.trueLuminosity] - dist

/datum/light_source/proc/readrgb(const/col)
	/*if(!configuration.colour_lights)
		col_r = 255
		col_g = 255
		col_g = 255 */
//	else
	_l_color = col

	if(col)
		col_r = GetRedPart(col)
		col_g = GetGreenPart(col)
		col_b = GetBluePart(col)
	else
		col_r = null

/atom
	var/datum/light_source/light
	var/trueLuminosity = 0  // Typically 'luminosity' squared.  The builtin luminosity must remain linear.
	                        // We may read it, but NEVER set it directly.
	var/l_color

//Turfs with opacity when they are constructed will trigger nearby lights to update
//Turfs and atoms with luminosity when they are constructed will create a light_source automatically
/turf/New()
	..()
	if(luminosity)
		if(light)	WARNING("[type] - Don't set lights up manually during New(), We do it automatically.")
		trueLuminosity = luminosity * luminosity
		light = new(src)

//Movable atoms with opacity when they are constructed will trigger nearby lights to update
//Movable atoms with luminosity when they are constructed will create a light_source automatically
atom/movable/New()
	..()
	if(opacity)
		if(isturf(loc))
			if(loc:lighting_lumcount > 1)
				UpdateAffectingLights()
	if(luminosity)
		if(light)	WARNING("[type] - Don't set lights up manually during New(), We do it automatically.")
		trueLuminosity = luminosity * luminosity
		light = new(src)
//Objects with opacity will trigger nearby lights to update at next lighting process.
/atom/movable/Destroy()
	if(opacity)
		UpdateAffectingLights()
	return ..()

//Sets our luminosity.
//If we have no light it will create one.
//If we are setting luminosity to 0 the light will be cleaned up by the controller and garbage collected once all its
//queues are complete.
//if we have a light already it is merely updated, rather than making a new one.
atom/proc/SetLuminosity(new_luminosity, trueLum = FALSE)
	if(new_luminosity < 0)
		new_luminosity = 0
	if(!trueLum)
		new_luminosity *= new_luminosity
	if(light)
		if(trueLuminosity != new_luminosity)	//non-luminous lights are removed from the lights list in add_effect()
			light.changed = 1
	else
		if(new_luminosity)
			light = new(src)
	trueLuminosity = new_luminosity
	if (trueLuminosity < 1)
		luminosity = 0
	else if (trueLuminosity <= 100)
		luminosity = sqrtTable[trueLuminosity]
	else
		luminosity = sqrt(trueLuminosity)

atom/proc/AddLuminosity(delta_luminosity)
	if(delta_luminosity > 0)
		SetLuminosity(trueLuminosity + delta_luminosity*delta_luminosity, TRUE)
	else if(delta_luminosity < 0)
		SetLuminosity(trueLuminosity - delta_luminosity*delta_luminosity, TRUE)

area/SetLuminosity(new_luminosity)			//we don't want dynamic lighting for areas
	luminosity = !!new_luminosity
	trueLuminosity = luminosity

//change our opacity (defaults to toggle), and then update all lights that affect us.
/atom/proc/SetOpacity(new_opacity)
	if(new_opacity == null)
		new_opacity = !opacity			//default = toggle opacity
	else if(opacity == new_opacity)
		return 0						//opacity hasn't changed! don't bother doing anything
	opacity = new_opacity				//update opacity, the below procs now call light updates.
	return 1

/turf/SetOpacity(new_opacity)
	if(..()==1)							//only bother if opacity changed
		if(lighting_lumcount)			//only bother with an update if our turf is currently affected by a light
			UpdateAffectingLights()

/atom/movable/SetOpacity(new_opacity)
	if(..()==1)							//only bother if opacity changed
		if(isturf(loc))					//only bother with an update if we're on a turf
			var/turf/T = loc
			if(T.lighting_lumcount)		//only bother with an update if our turf is currently affected by a light
				UpdateAffectingLights()

turf
	var/lighting_lumcount = 0
	var/lighting_changed = 0
	var/color_lighting_lumcount = 0

	var/lumcount_r = 0
	var/lumcount_g = 0
	var/lumcount_b = 0
	var/light_col_sources = 0

/turf/space
	lighting_lumcount = 4		//starlight
/*
/turf/proc/update_lumcount(amount)
	lighting_lumcount += amount
	if(!lighting_changed)
		SSlighting.changed_turfs += src
		lighting_changed = 1
*/
/turf/proc/update_lumcount(amount, col_r, col_g, col_b, removing = 0)
	lighting_lumcount += amount

	if(!isnull(col_r)) //col_r is the "key" var, if it's null so will the rest
		if(removing)
			light_col_sources--
			lumcount_r -= col_r
			lumcount_g -= col_g
			lumcount_b -= col_b
		else
			light_col_sources++
			lumcount_r += col_r
			lumcount_g += col_g
			lumcount_b += col_b

		if(light_col_sources)
			var/r_avg = Clamp(round(lumcount_r / light_col_sources, 16) + 15, 0, 255)
			var/g_avg = Clamp(round(lumcount_g / light_col_sources, 16) + 15, 0, 255)
			var/b_avg = Clamp(round(lumcount_b / light_col_sources, 16) + 15, 0, 255)
			l_color = rgb(r_avg, g_avg, b_avg)
		else
			l_color = null

		color_lighting_lumcount = max(color_lighting_lumcount + amount, 0) // Minimum of 0.

	if(!lighting_changed)
		SSlighting.changed_turfs += src
		lighting_changed = 1
/*

/area/proc/lighting_tag(level)
	return tagbase + "sd_L[level]"
*/
/turf/proc/lighting_tag(const/level)
	var/area/A = loc
	return A.tagbase + "sd_L[level]"

/turf/proc/build_lighting_area(const/tag, const/level, const/color_light)
	var/area/Area = loc
	var/area/A = new Area.type()    // create area if it wasn't found
	// replicate vars
	for(var/V in Area.vars)
		switch(V)
			if ("contents","lighting_overlay", "color_overlay", "overlays")
				continue
			else
				if(issaved(Area.vars[V])) A.vars[V] = Area.vars[V]

	A.tag = tag
	A.lighting_subarea = 1
	A.lighting_space = 0 // in case it was copied from a space subarea

	if (l_color != A.l_color)
		A.l_color = l_color
		//color_light = min(max(round(color_lighting_lumcount, 1), 0), lighting_controller.lighting_states)
		//world << "[color_light] [color_lighting_lumcount]"

	A.SetLightLevel(level, color_light)
	Area.related += A
	return A

/*
/area/proc/build_lighting_area(tag, level)
	var/area/A = locate(tag)	// find an appropriate area
	if(A)
		return A
	A = new type()    // create area if it wasn't found
	// replicate vars
	for(var/V in vars)
		switch(V)
			if("contents","last_light","overlays")	continue
			else
				if(issaved(vars[V])) A.vars[V] = vars[V]

	A.tag = tag
	A.lighting_subarea = 1
	A.lighting_space = 0 // in case it was copied from a space subarea
	A.SetLightLevel(level)

	related += A
	return A
*/

turf/proc/shift_to_subarea()
	lighting_changed = 0
	var/area/Area = loc
	if(!istype(Area) || !Area.lighting_use_dynamic) return
	var/level = Clamp(round(lighting_lumcount, 1), 0, SSlighting.lighting_states)
	var/new_tag = lighting_tag(level)
	// pomf - If we have a lighting color that is not null, apply the new tag to seperate the areas.
	if (l_color)
		// pomf - We append the (rounded!) color lighting lumcount so we can have colored lights.
		new_tag += "[l_color][Clamp(round(color_lighting_lumcount, 1), 0, SSlighting.lighting_states)]"

	if(Area.tag != new_tag)	//skip if already in this area
		var/area/A = locate(new_tag)	// find an appropriate area
		var/color_light = Clamp(round(color_lighting_lumcount, 1), 0, SSlighting.lighting_states)

		if(!A)
			A = build_lighting_area(new_tag, level, color_light)
		else if(l_color != A.l_color)
			A.l_color = l_color
			A.SetLightLevel(level, color_light)

		A.contents += src	// move the turf into the area
	universe.OnTurfTick(src)

// Dedicated lighting sublevel for space turfs
// helps us depower things in space, remove space fire alarms,
// and evens out space lighting
turf/space/lighting_tag(var/level)
	var/area/A = loc
	return A.tagbase + "sd_L_space"
turf/space/build_lighting_area(var/tag,var/level)
	var/area/A = ..(tag,4)
	A.lighting_space = 1
	A.SetLightLevel(4)
	A.icon_state = null
	return A

/area
	var/lighting_use_dynamic = 1	//Turn this flag off to prevent sd_DynamicAreaLighting from affecting this area
	var/image/lighting_overlay		//tracks the darkness image of the area for easy removal
	var/lighting_subarea = 0		//tracks whether we're a lighting sub-area
	var/lighting_space = 0			// true for space-only lighting subareas
	var/tagbase
	var/image/color_overlay //Tracks the color image.

/area/proc/SetLightLevel(light, color_light = 0)
	if(!src) return
	if(light <= 0)
		light = 0
		luminosity = 0
	else
		if(light > SSlighting.lighting_states)
			light = SSlighting.lighting_states
		luminosity = 1

	if(lighting_overlay)
		overlays -= lighting_overlay
		lighting_overlay.icon_state = "[light]"
	else
		lighting_overlay = image(LIGHTING_ICON,,num2text(light),LIGHTING_LAYER)

	if (color_overlay)
		overlays.Remove(color_overlay)
		color_overlay.icon_state = "white"
	else
		if (l_color)
			color_overlay = image('icons/obj/weapons.dmi', ,"white", 10.1)

	if (istype(color_overlay))
		color_overlay.color = l_color

		if (1)
			switch (color_light)
				if (6)
					color_overlay.alpha = 180
				if (5)
					color_overlay.alpha = 140
				if (4)
					color_overlay.alpha = 120
				if (3)
					color_overlay.alpha = 80
				if (2)
					color_overlay.alpha = 60
				if (1)
					color_overlay.alpha = 20
				if (-INFINITY to 0)
					//world << "Zero or below, [color_light]."
					color_overlay.alpha = 0
				else
					//world << "Setting the alpha to max... color_light [color_light]."
					color_overlay.alpha = 180

		color_overlay.blend_mode = BLEND_ADD

		if (color_overlay.color)
			overlays.Add(color_overlay)

	if (isnull(color_overlay))
		overlays.Add(lighting_overlay)
	else if (light < 6)
		overlays.Add(lighting_overlay)

/area/proc/SetDynamicLighting()
	src.lighting_use_dynamic = 1
	for(var/turf/T in src.contents)
		T.update_lumcount(0)

/area/proc/InitializeLighting()	//TODO: could probably improve this bit ~Carn
	tagbase = "[type]"
	if(!tag) tag = tagbase
	if(!lighting_use_dynamic)
		if(!lighting_subarea)	// see if this is a lighting subarea already
		//show the dark overlay so areas, not yet in a lighting subarea, won't be bright as day and look silly.
			SetLightLevel(4)

/*
/area/proc/SetLightLevel(light)
	if(!src) return
	if(light <= 1)
		light = 1
		luminosity = 0
	else
		if(light > SSlighting.lighting_images.len)
			light = SSlighting.lighting_images.len
		luminosity = 1

	if(last_light != light)
		if(last_light)
			overlays -= SSlighting.lighting_images[last_light]
		overlays += SSlighting.lighting_images[light]
		last_light = light

/area/proc/SetDynamicLighting()
	lighting_use_dynamic = 1
	for(var/turf/T in contents)
		T.update_lumcount(0)

/area/proc/InitializeLighting()	//TODO: could probably improve this bit ~Carn
	tagbase = "[type]"
	if(!tag) tag = tagbase
	if(!lighting_use_dynamic)
		if(!lighting_subarea)	// see if this is a lighting subarea already
		//show the dark overlay so areas, not yet in a lighting subarea, won't be bright as day and look silly.
			SetLightLevel(4)
*/
#undef USE_CIRCULAR_LIGHTING

//set the changed status of all lights which could have possibly lit this atom.
//We don't need to worry about lights which lit us but moved away, since they will have change status set already
//This proc can cause lots of lights to be updated. :(
/*
/atom/proc/UpdateAffectingLights()

/atom/movable/UpdateAffectingLights()
	if(isturf(loc))
		loc.UpdateAffectingLights()

/turf/UpdateAffectingLights()
	if(affecting_lights)
		for(var/thing in affecting_lights)
			thing:changed = 1			//force it to update at next process()
*/

#define LIGHTING_MAX_LUMINOSITY_STATIC	8	//Maximum luminosity to reduce lag.
#define LIGHTING_MAX_LUMINOSITY_MOBILE	5	//Moving objects have a lower max luminosity since these update more often. (lag reduction)
#define LIGHTING_MAX_LUMINOSITY_MOB		5
#define LIGHTING_MAX_LUMINOSITY_TURF	1	//turfs have a severely shortened range to protect from inevitable floor-lighttile spam.

//caps luminosity effects max-range based on what type the light's owner is.
/atom/proc/UpdateAffectingLights()
	for(var/atom/A in oview(LIGHTING_MAX_LUMINOSITY_STATIC-1,src))
		if(A.light)
			A.light.changed = 1			//force it to update at next process()

/atom/proc/get_light_range()
	return min(luminosity, LIGHTING_MAX_LUMINOSITY_STATIC)

/atom/movable/get_light_range()
	return min(luminosity, LIGHTING_MAX_LUMINOSITY_MOBILE)

/mob/get_light_range()
	return min(luminosity, LIGHTING_MAX_LUMINOSITY_MOB)

/obj/machinery/light/get_light_range()
	return min(luminosity, LIGHTING_MAX_LUMINOSITY_STATIC)

/turf/get_light_range()
	return min(luminosity, LIGHTING_MAX_LUMINOSITY_TURF)

#undef LIGHTING_MAX_LUMINOSITY_STATIC
#undef LIGHTING_MAX_LUMINOSITY_MOBILE
#undef LIGHTING_MAX_LUMINOSITY_MOB
#undef LIGHTING_MAX_LUMINOSITY_TURF


code:
//Configuraton defines //TODO: Move all yes/no switches into bitflags

//Used by jobs_have_maint_access
#define ASSISTANTS_HAVE_MAINT_ACCESS 1
#define SECURITY_HAS_MAINT_ACCESS 2
#define EVERYONE_HAS_MAINT_ACCESS 4

/datum/configuration
	var/server_name = null				// server name (the name of the game window)
	var/station_name = null				// station name (the name of the station in-game)
	var/server_suffix = 0				// generate numeric suffix based on server port
	var/lobby_countdown = 120			// In between round countdown.

	var/log_ooc = 0						// log OOC channel
	var/log_access = 0					// log login/logout
	var/log_say = 0						// log client say
	var/log_admin = 0					// log admin actions
	var/log_game = 0					// log game events
	var/log_vote = 0					// log voting
	var/log_whisper = 0					// log client whisper
	var/log_prayer = 0					// log prayers
	var/log_law = 0						// log lawchanges
	var/log_emote = 0					// log emotes
	var/log_attack = 0					// log attack messages
	var/log_adminchat = 0				// log admin chat messages
	var/log_pda = 0						// log pda messages
	var/log_hrefs = 0					// logs all links clicked in-game. Could be used for debugging and tracking down exploits
	var/sql_enabled = 0					// for sql switching
	var/allow_admin_ooccolor = 0		// Allows admins with relevant permissions to have their own ooc colour
	var/allow_vote_restart = 0 			// allow votes to restart
	var/allow_vote_mode = 0				// allow votes to change mode
	var/vote_delay = 6000				// minimum time between voting sessions (deciseconds, 10 minute default)
	var/vote_period = 600				// length of voting period (deciseconds, default 1 minute)
	var/vote_no_default = 0				// vote does not default to nochange/norestart (tbi)
	var/vote_no_dead = 0				// dead people can't vote (tbi)
	var/del_new_on_log = 1				// del's new players if they log before they spawn in
	var/allow_Metadata = 0				// Metadata is supported.
	var/popup_admin_pm = 0				//adminPMs to non-admins show in a pop-up 'reply' window when set to 1.
	var/fps = 10
	var/Tickcomp = 0
	var/allow_holidays = 0				//toggles whether holiday-specific content should be used

	var/hostedby = null
	var/respawn = 1
	var/guest_jobban = 1
	var/usewhitelist = 0
	var/kick_inactive = 0				//force disconnect for inactive players
	var/load_jobs_from_txt = 0
	var/automute_on = 0					//enables automuting/spam prevention
	var/minimal_access_threshold = 0	//If the number of players is larger than this threshold, minimal access will be turned on.
	var/jobs_have_minimal_access = 0	//determines whether jobs use minimal access or expanded access.
	var/jobs_have_maint_access = 0 		//Who gets maint access?  See defines above.
	var/sec_start_brig = 0				//makes sec start in brig or dept sec posts

	var/server
	var/banappeals
	var/wikiurl = "http://www.tgstation13.org/wiki" // Default wiki link.
	var/forumurl = "http://tgstation13.org/phpBB/index.php" //default forums
	var/rulesurl = "http://www.tgstation13.org/wiki/Rules" // default rules
	var/githuburl = "https://www.github.com/tgstation/-tg-station" //default github

	var/forbid_singulo_possession = 0
	var/useircbot = 0

	var/admin_legacy_system = 0	//Defines whether the server uses the legacy admin system with admins.txt or the SQL system. Config option in config.txt
	var/ban_legacy_system = 0	//Defines whether the server uses the legacy banning system with the files in /data or the SQL system. Config option in config.txt
	var/use_age_restriction_for_jobs = 0 //Do jobs use account age restrictions? --requires database
	var/see_own_notes = 0 //Can players see their own admin notes (read-only)? Config option in config.txt

	//Population cap vars
	var/soft_popcap				= 0
	var/hard_popcap				= 0
	var/extreme_popcap			= 0
	var/soft_popcap_message		= "Be warned that the server is currently serving a high number of users, consider using alternative game servers."
	var/hard_popcap_message		= "The server is currently serving a high number of users, You cannot currently join. You may wait for the number of living crew to decline, observe, or find alternative servers."
	var/extreme_popcap_message	= "The server is currently serving a high number of users, find alternative servers."

	//game_options.txt configs
	var/force_random_names = 0
	var/list/mode_names = list()
	var/list/modes = list()				// allowed modes
	var/list/votable_modes = list()		// votable modes
	var/list/probabilities = list()		// relative probability of each mode

	var/humans_need_surnames = 0
	var/allow_random_events = 0			// enables random events mid-round when set to 1
	var/allow_ai = 0					// allow ai job
	var/panic_bunker = 0				// prevents new people it hasn't seen before from connecting
	var/notify_new_player_age = 0		// how long do we notify admins of a new player
	var/irc_first_connection_alert = 0	// do we notify the irc channel when somebody is connecting for the first time?

	var/traitor_scaling_coeff = 6		//how much does the amount of players get divided by to determine traitors
	var/changeling_scaling_coeff = 6	//how much does the amount of players get divided by to determine changelings
	var/security_scaling_coeff = 8		//how much does the amount of players get divided by to determine open security officer positions
	var/abductor_scaling_coeff = 12 	//how many players per abductor team

	var/traitor_objectives_amount = 2
	var/protect_roles_from_antagonist = 0 //If security and such can be traitor/cult/other
	var/protect_assistant_from_antagonist = 0 //If assistants can be traitor/cult/other
	var/enforce_human_authority = 0		//If non-human species are barred from joining as a head of staff
	var/allow_latejoin_antagonists = 0 	// If late-joining players can be traitor/changeling
	var/continuous_round_rev = 0		// Gamemodes which end instantly will instead keep on going until the round ends by escape shuttle or nuke.
	var/continuous_round_gang = 0
	var/continuous_round_wiz = 0
	var/continuous_round_malf = 0
	var/continuous_round_blob = 0
	var/midround_antag_time_check = 60  // How late (in minutes) you want the midround antag system to stay on, setting this to 0 will disable the system
	var/midround_antag_life_check = 0.7 // A ratio of how many people need to be alive in order for the round not to immediately end in midround antagonist
	var/shuttle_refuel_delay = 12000
	var/show_game_type_odds = 0			//if set this allows players to see the odds of each roundtype on the get revision screen
	var/mutant_races = 0				//players can choose their mutant race before joining the game
	var/mutant_colors = 0

	var/alert_desc_green = "All threats to the station have passed. Security may not have weapons visible, privacy laws are once again fully enforced."
	var/alert_desc_blue_upto = "The station has received reliable information about possible hostile activity on the station. Security staff may have weapons visible, random searches are permitted."
	var/alert_desc_blue_downto = "The immediate threat has passed. Security may no longer have weapons drawn at all times, but may continue to have them visible. Random searches are still allowed."
	var/alert_desc_red_upto = "There is an immediate serious threat to the station. Security may have weapons unholstered at all times. Random searches are allowed and advised."
	var/alert_desc_red_downto = "The self-destruct mechanism has been deactivated, there is still however an immediate serious threat to the station. Security may have weapons unholstered at all times, random searches are allowed and advised."
	var/alert_desc_delta = "The station's self-destruct mechanism has been engaged. All crew are instructed to obey all instructions given by heads of staff. Any violations of these orders can be punished by death. This is not a drill."

	var/health_threshold_crit = 0
	var/health_threshold_dead = -100

	var/revival_pod_plants = 1
	var/revival_cloning = 1
	var/revival_brain_life = -1

	var/rename_cyborg = 0
	var/ooc_during_round = 0
	var/emojis = 0

	//Used for modifying movement speed for mobs.
	//Unversal modifiers
	var/run_speed = 0
	var/walk_speed = 0

	//Mob specific modifiers. NOTE: These will affect different mob types in different ways
	var/human_delay = 0
	var/robot_delay = 0
	var/monkey_delay = 0
	var/alien_delay = 0
	var/slime_delay = 0
	var/animal_delay = 0

	var/gateway_delay = 18000 //How long the gateway takes before it activates. Default is half an hour.
	var/ghost_interaction = 0

	var/silent_ai = 0
	var/silent_borg = 0
	var/reactionary_explosions = 1 //If we use reactionary explosions, explosions that react to walls and doors

	var/sandbox_autoclose = 0 // close the sandbox panel after spawning an item, potentially reducing griff

	var/default_laws = 0 //Controls what laws the AI spawns with.
	var/silicon_max_law_amount = 12
	var/hook_round_end = 0

	var/assistant_cap = -1

	var/starlight = 0
	var/grey_assistants = 0

	var/colour_lights //gay

	var/aggressive_changelog = 0

/datum/configuration/New()
	var/list/L = typesof(/datum/game_mode) - /datum/game_mode
	for(var/T in L)
		// I wish I didn't have to instance the game modes in order to look up
		// their information, but it is the only way (at least that I know of).
		var/datum/game_mode/M = new T()

		if(M.config_tag)
			if(!(M.config_tag in modes))		// ensure each mode is added only once
				diary << "Adding game mode [M.name] ([M.config_tag]) to configuration."
				modes += M.config_tag
				mode_names[M.config_tag] = M.name
				probabilities[M.config_tag] = M.probability
				if(M.votable)
					votable_modes += M.config_tag
		del(M)
	votable_modes += "secret"

/datum/configuration/proc/load(filename, type = "config") //the type can also be game_options, in which case it uses a different switch. not making it separate to not copypaste code - Urist
	var/list/Lines = file2list(filename)

	for(var/t in Lines)
		if(!t)	continue

		t = trim(t)
		if(length(t) == 0)
			continue
		else if(copytext(t, 1, 2) == "#")
			continue

		var/pos = findtext(t, " ")
		var/name = null
		var/value = null

		if(pos)
			name = lowertext(copytext(t, 1, pos))
			value = copytext(t, pos + 1)
		else
			name = lowertext(t)

		if(!name)
			continue

		if(type == "config")
			switch(name)
				if("admin_legacy_system")
					config.admin_legacy_system = 1
				if("ban_legacy_system")
					config.ban_legacy_system = 1
				if("use_age_restriction_for_jobs")
					config.use_age_restriction_for_jobs = 1
				if("lobby_countdown")
					config.lobby_countdown = text2num(value)
				if("log_ooc")
					config.log_ooc = 1
				if("log_access")
					config.log_access = 1
				if("log_say")
					config.log_say = 1
				if("log_admin")
					config.log_admin = 1
				if("log_prayer")
					config.log_prayer = 1
				if("log_law")
					config.log_law = 1
				if("log_game")
					config.log_game = 1
				if("log_vote")
					config.log_vote = 1
				if("log_whisper")
					config.log_whisper = 1
				if("log_attack")
					config.log_attack = 1
				if("log_emote")
					config.log_emote = 1
				if("log_adminchat")
					config.log_adminchat = 1
				if("log_pda")
					config.log_pda = 1
				if("log_hrefs")
					config.log_hrefs = 1
				if("allow_admin_ooccolor")
					config.allow_admin_ooccolor = 1
				if("allow_vote_restart")
					config.allow_vote_restart = 1
				if("allow_vote_mode")
					config.allow_vote_mode = 1
				if("no_dead_vote")
					config.vote_no_dead = 1
				if("default_no_vote")
					config.vote_no_default = 1
				if("vote_delay")
					config.vote_delay = text2num(value)
				if("vote_period")
					config.vote_period = text2num(value)
				if("norespawn")
					config.respawn = 0
				if("servername")
					config.server_name = value
				if("stationname")
					config.station_name = value
				if("serversuffix")
					config.server_suffix = 1
				if("hostedby")
					config.hostedby = value
				if("server")
					config.server = value
				if("banappeals")
					config.banappeals = value
				if("wikiurl")
					config.wikiurl = value
				if("forumurl")
					config.forumurl = value
				if("rulesurl")
					config.rulesurl = value
				if("githuburl")
					config.githuburl = value
				if("guest_jobban")
					config.guest_jobban = 1
				if("guest_ban")
					guests_allowed = 0
				if("usewhitelist")
					config.usewhitelist = 1
				if("allow_metadata")
					config.allow_Metadata = 1
				if("kick_inactive")
					if(value < 1)
						value = INACTIVITY_KICK
					config.kick_inactive = value
				if("load_jobs_from_txt")
					load_jobs_from_txt = 1
				if("forbid_singulo_possession")
					forbid_singulo_possession = 1
				if("popup_admin_pm")
					config.popup_admin_pm = 1
				if("allow_holidays")
					config.allow_holidays = 1
				if("useircbot")
					useircbot = 1
				if("ticklag")
					var/ticklag = text2num(value)
					if(ticklag > 0)
						fps = 10 / ticklag
				if("fps")
					fps = text2num(value)
				if("tickcomp")
					Tickcomp = 1
				if("automute_on")
					automute_on = 1
				if("comms_key")
					global.comms_key = value
					if(value != "default_pwd" && length(value) > 6) //It's the default value or less than 6 characters long, warn badmins
						global.comms_allowed = 1
				if("see_own_notes")
					config.see_own_notes = 1
				if("hook_round_end")
					config.hook_round_end = 1
				if("soft_popcap")
					config.soft_popcap = text2num(value)
				if("hard_popcap")
					config.hard_popcap = text2num(value)
				if("extreme_popcap")
					config.extreme_popcap = text2num(value)
				if("soft_popcap_message")
					config.soft_popcap_message = value
				if("hard_popcap_message")
					config.hard_popcap_message = value
				if("extreme_popcap_message")
					config.extreme_popcap_message = value
				if("panic_bunker")
					config.panic_bunker = 1
				if("notify_new_player_age")
					config.notify_new_player_age = text2num(value)
				if("irc_first_connection_alert")
					config.irc_first_connection_alert = 1
				if("aggressive_changelog")
					config.aggressive_changelog = 1
				if("reactionary_explosions")
					config.reactionary_explosions	= 1
				else
					diary << "Unknown setting in configuration: '[name]'"

		else if(type == "game_options")
			switch(name)
				if("health_threshold_crit")
					config.health_threshold_crit	= text2num(value)
				if("health_threshold_dead")
					config.health_threshold_dead	= text2num(value)
				if("revival_pod_plants")
					config.revival_pod_plants		= text2num(value)
				if("revival_cloning")
					config.revival_cloning			= text2num(value)
				if("revival_brain_life")
					config.revival_brain_life		= text2num(value)
				if("rename_cyborg")
					config.rename_cyborg			= 1
				if("ooc_during_round")
					config.ooc_during_round			= 1
				if("emojis")
					config.emojis					= 1
				if("run_delay")
					config.run_speed				= text2num(value)
				if("walk_delay")
					config.walk_speed				= text2num(value)
				if("human_delay")
					config.human_delay				= text2num(value)
				if("robot_delay")
					config.robot_delay				= text2num(value)
				if("monkey_delay")
					config.monkey_delay				= text2num(value)
				if("alien_delay")
					config.alien_delay				= text2num(value)
				if("slime_delay")
					config.slime_delay				= text2num(value)
				if("animal_delay")
					config.animal_delay				= text2num(value)
				if("alert_red_upto")
					config.alert_desc_red_upto		= value
				if("alert_red_downto")
					config.alert_desc_red_downto	= value
				if("alert_blue_downto")
					config.alert_desc_blue_downto	= value
				if("alert_blue_upto")
					config.alert_desc_blue_upto		= value
				if("alert_green")
					config.alert_desc_green			= value
				if("alert_delta")
					config.alert_desc_delta			= value
				if("assistants_have_maint_access")
					config.jobs_have_maint_access	|= ASSISTANTS_HAVE_MAINT_ACCESS
				if("security_has_maint_access")
					config.jobs_have_maint_access	|= SECURITY_HAS_MAINT_ACCESS
				if("everyone_has_maint_access")
					config.jobs_have_maint_access	|= EVERYONE_HAS_MAINT_ACCESS
				if("sec_start_brig")
					config.sec_start_brig			= 1
				if("gateway_delay")
					config.gateway_delay			= text2num(value)
				if("continuous_round_rev")
					config.continuous_round_rev		= 1
				if("continuous_round_gang")
					config.continuous_round_gang	= 1
				if("continuous_round_wiz")
					config.continuous_round_wiz		= 1
				if("continuous_round_malf")
					config.continuous_round_malf	= 1
				if("continuous_round_blob")
					config.continuous_round_blob	= 1
				if("midround_antag_time_check")
					config.midround_antag_time_check = text2num(value)
				if("midround_antag_life_check")
					config.midround_antag_life_check = text2num(value)
				if("shuttle_refuel_delay")
					config.shuttle_refuel_delay     = text2num(value)
				if("show_game_type_odds")
					config.show_game_type_odds		= 1
				if("ghost_interaction")
					config.ghost_interaction		= 1
				if("traitor_scaling_coeff")
					config.traitor_scaling_coeff	= text2num(value)
				if("changeling_scaling_coeff")
					config.changeling_scaling_coeff	= text2num(value)
				if("security_scaling_coeff")
					config.security_scaling_coeff	= text2num(value)
				if("abductor_scaling_coeff")
					config.abductor_scaling_coeff	= text2num(value)
				if("traitor_objectives_amount")
					config.traitor_objectives_amount = text2num(value)
				if("probability")
					var/prob_pos = findtext(value, " ")
					var/prob_name = null
					var/prob_value = null

					if(prob_pos)
						prob_name = lowertext(copytext(value, 1, prob_pos))
						prob_value = copytext(value, prob_pos + 1)
						if(prob_name in config.modes)
							config.probabilities[prob_name] = text2num(prob_value)
						else
							diary << "Unknown game mode probability configuration definition: [prob_name]."
					else
						diary << "Incorrect probability configuration definition: [prob_name]  [prob_value]."

				if("protect_roles_from_antagonist")
					config.protect_roles_from_antagonist	= 1
				if("protect_assistant_from_antagonist")
					config.protect_assistant_from_antagonist	= 1
				if("enforce_human_authority")
					config.enforce_human_authority	= 1
				if("allow_latejoin_antagonists")
					config.allow_latejoin_antagonists	= 1
				if("allow_random_events")
					config.allow_random_events		= 1
				if("minimal_access_threshold")
					config.minimal_access_threshold	= text2num(value)
				if("jobs_have_minimal_access")
					config.jobs_have_minimal_access	= 1
				if("humans_need_surnames")
					humans_need_surnames			= 1
				if("force_random_names")
					config.force_random_names		= 1
				if("allow_ai")
					config.allow_ai					= 1
				if("silent_ai")
					config.silent_ai 				= 1
				if("silent_borg")
					config.silent_borg				= 1
				if("sandbox_autoclose")
					config.sandbox_autoclose		= 1
				if("default_laws")
					config.default_laws				= text2num(value)
				if("silicon_max_law_amount")
					config.silicon_max_law_amount	= text2num(value)
				if("join_with_mutant_race")
					config.mutant_races				= 1
				if("mutant_colors")
					config.mutant_colors			= 1
				if("assistant_cap")
					config.assistant_cap			= text2num(value)
				if("starlight")
					config.starlight			= 1
				if("grey_assistants")
					config.grey_assistants			= 1
				else
					diary << "Unknown setting in configuration: '[name]'"

	fps = round(fps)
	if(fps <= 0)
		fps = initial(fps)

/datum/configuration/proc/loadsql(filename)
	var/list/Lines = file2list(filename)
	for(var/t in Lines)
		if(!t)	continue

		t = trim(t)
		if(length(t) == 0)
			continue
		else if(copytext(t, 1, 2) == "#")
			continue

		var/pos = findtext(t, " ")
		var/name = null
		var/value = null

		if(pos)
			name = lowertext(copytext(t, 1, pos))
			value = copytext(t, pos + 1)
		else
			name = lowertext(t)

		if(!name)
			continue

		switch(name)
			if("sql_enabled")
				config.sql_enabled = 1
			if("address")
				sqladdress = value
			if("port")
				sqlport = value
			if("feedback_database")
				sqlfdbkdb = value
			if("feedback_login")
				sqlfdbklogin = value
			if("feedback_password")
				sqlfdbkpass = value
			if("feedback_tableprefix")
				sqlfdbktableprefix = value
			else
				diary << "Unknown setting in configuration: '[name]'"

/datum/configuration/proc/pick_mode(mode_name)
	// I wish I didn't have to instance the game modes in order to look up
	// their information, but it is the only way (at least that I know of).
	for(var/T in (typesof(/datum/game_mode) - /datum/game_mode))
		var/datum/game_mode/M = new T()
		if(M.config_tag && M.config_tag == mode_name)
			return M
		del(M)
	return new /datum/game_mode/extended()

/datum/configuration/proc/get_runnable_modes()
	var/list/datum/game_mode/runnable_modes = new
	for(var/T in (typesof(/datum/game_mode) - /datum/game_mode))
		var/datum/game_mode/M = new T()
		//world << "DEBUG: [T], tag=[M.config_tag], prob=[probabilities[M.config_tag]]"
		if(!(M.config_tag in modes))
			del(M)
			continue
		if(probabilities[M.config_tag]<=0)
			del(M)
			continue
		if(M.can_start())
			runnable_modes[M] = probabilities[M.config_tag]
			//world << "DEBUG: runnable_mode\[[runnable_modes.len]\] = [M.config_tag]"
	return runnable_modes


code:
var/datum/controller/failsafe/Failsafe

/datum/controller/failsafe // This thing pretty much just keeps poking the master controller
	var/processing_interval = 100	//poke the MC every 10 seconds - set to 0 to disable

	var/MC_iteration = 0
	var/MC_defcon = 0			//alert level. For every poke that fails this is raised by 1. When it reaches 5 the MC is replaced with a new one. (effectively killing any master_controller.process() and starting a new one)

/datum/controller/failsafe/New()
	//There can be only one failsafe. Out with the old in with the new (that way we can restart the Failsafe by spawning a new one)
	if(Failsafe != src)
		if(istype(Failsafe))
			qdel(Failsafe)
	Failsafe = src
	Failsafe.process()

/datum/controller/failsafe/process()
	spawn(0)
		while(1)	//more efficient than recursivly calling ourself over and over. background = 1 ensures we do not trigger an infinite loop
			if(!master_controller)		new /datum/controller/game_controller()	//replace the missing master_controller! This should never happen.

			if(processing_interval > 0)
				if(master_controller.processing_interval > 0)	//only poke if these overrides aren't in effect
					if(MC_iteration == master_controller.iteration)	//master_controller hasn't finished processing in the defined interval
						switch(MC_defcon)
							if(0 to 3)
								++MC_defcon
							if(4)
								admins << "Warning. The Master Controller has not fired in the last [MC_defcon*processing_interval] ticks. Automatic restart in [processing_interval] ticks."
								MC_defcon = 5
							if(5)
								admins << "Warning. The Master Controller has still not fired within the last [MC_defcon*processing_interval] ticks. Killing and restarting..."
								new /datum/controller/game_controller()	//replace the old master_controller (hence killing the old one's process)
								master_controller.process()				//Start it rolling again
								MC_defcon = 0
					else
						MC_defcon = 0
						MC_iteration = master_controller.iteration
				sleep(processing_interval)
			else
				MC_defcon = 0
				sleep(100)


code:
//simplified MC that is designed to fail when procs 'break'. When it fails it's just replaced with a new one.
//It ensures master_controller.process() is never doubled up by killing the MC (hence terminating any of its sleeping procs)

//Update: all core-systems are now placed inside subsystem datums. This makes them highly configurable and easy to work with.

var/global/datum/controller/game_controller/master_controller = new()

/datum/controller/game_controller
	var/processing_interval = 1	//The minimum length of time between MC ticks (in deciseconds). The highest this can be without affecting schedules, is the GCD of all subsystem var/wait. Set to 0 to disable all processing.
	var/iteration = 0
	var/cost = 0
	var/last_thing_processed
	var/SSCostPerSecond = 0
	var/list/subsystems = list()

/datum/controller/game_controller/New()
	//There can be only one master_controller. Out with the old and in with the new.
	if(master_controller != src)
		if(istype(master_controller))
			Recover()
			master_controller.Del()
		else
			init_subtypes(/datum/subsystem, subsystems)

		master_controller = src
	calculateGCD()

/*
Calculate the longest number of ticks the MC can wait between each cycle without causing subsystems to not fire on schedule
*/
/datum/controller/game_controller/proc/calculateGCD()
	var/GCD
	for(var/datum/subsystem/SS in subsystems)
		if(SS.wait)
			GCD = Gcd(round(SS.wait*10), GCD)
	GCD = round(GCD)
	if(GCD < world.tick_lag*10)
		GCD = world.tick_lag*10
	processing_interval = GCD/10

/datum/controller/game_controller/proc/setup(zlevel)
	if (zlevel && zlevel > 0 && zlevel <= world.maxz)
		for(var/datum/subsystem/S in subsystems)
			S.Initialize(world.timeofday, zlevel)
			sleep(-1)
		return
	world << "Initializing Subsystems..."

	//sort subsystems by priority, so they initialize in the correct order
	sortTim(subsystems, /proc/cmp_subsystem_priority)

	createRandomZlevel()	//gate system
	setup_map_transitions()
	SetupXenoarch()
	for(var/i=0, i<max_secret_rooms, i++)
		make_mining_asteroid_secret()

	//Eventually all this other setup stuff should be contained in subsystems and done in subsystem.Initialize()
	for(var/datum/subsystem/S in subsystems)
		S.Initialize(world.timeofday, zlevel)
		sleep(-1)
	for(var/datum/subsystem/S in subsystems)
		S.AfterInitialize(zlevel)

	world << "Initializations complete"
	world.log << "Initializations complete"

	world.sleep_offline = 1
	world.fps = config.fps

	sleep(-1)

	process()

//used for smoothing out the cost values so they don't fluctuate wildly
#define MC_AVERAGE(average, current) (0.8*(average) + 0.2*(current))

/datum/controller/game_controller/process()
	if(!Failsafe)	new /datum/controller/failsafe()
	spawn(0)
		var/timer = world.time
		for(var/datum/subsystem/SS in subsystems)
			timer += processing_interval
			SS.next_fire = timer

		var/start_time

		while(1)	//far more efficient than recursively calling ourself
			if(processing_interval > 0)
				++iteration

				var/SubSystemRan = 0

				start_time = world.timeofday

				for(var/datum/subsystem/SS in subsystems)
					if(SS.can_fire > 0)
						if(SS.next_fire <= world.time)
							SubSystemRan = 1
							timer = world.timeofday
							last_thing_processed = SS.type
							SS.last_fire = world.time
							SS.fire()
							SS.cost = MC_AVERAGE(SS.cost, world.timeofday - timer)
							if (SS.dynamic_wait)
								var/oldwait = SS.wait
								var/GlobalCostDelta = (SSCostPerSecond-(SS.cost/SS.wait))/(SS.wait/10)-1
								var/NewWait = MC_AVERAGE(oldwait,(SS.cost-1.5+GlobalCostDelta)*SS.dwait_delta)
								SS.wait = Clamp(round(NewWait,0.1),SS.dwait_lower,SS.dwait_upper)
								if (oldwait != SS.wait)
									calculateGCD()
							SS.next_fire += SS.wait
							++SS.times_fired

							sleep(-1)

				cost = MC_AVERAGE(cost, world.timeofday - start_time)
				if (SubSystemRan)
					calculateSScost()

				sleep(processing_interval)
			else
				sleep(50)

/datum/controller/game_controller/proc/calculateSScost()
	var/newcost = 0
	for(var/datum/subsystem/SS in subsystems)
		if (!SS.can_fire)
			continue
		newcost += SS.cost/(SS.wait/10)
	SSCostPerSecond = MC_AVERAGE(SSCostPerSecond,newcost)

#undef MC_AVERAGE

/datum/controller/game_controller/proc/roundHasStarted()
	for(var/datum/subsystem/SS in subsystems)
		SS.can_fire = 1
		SS.next_fire = world.time + rand(0,SS.wait)

/datum/controller/game_controller/proc/Recover()
	var/msg = "## DEBUG: [time2text(world.timeofday)] MC restarted. Reports:\n"
	for(var/varname in master_controller.vars)
		switch(varname)
			if("tag","bestF","type","parent_type","vars")	continue
			else
				var/varval = master_controller.vars[varname]
				if(istype(varval,/datum))
					var/datum/D = varval
					msg += "\t [varname] = [D.type]\n"
				else
					msg += "\t [varname] = [varval]\n"
	world.log << msg

	subsystems = master_controller.subsystems


code:
var/datum/subsystem/air/SSair

/datum/subsystem/air
	name = "Air"
	priority = 20
	wait = 5
	dynamic_wait = 1
	dwait_lower = 5
	dwait_upper = 50

	var/cost_turfs = 0
	var/cost_groups = 0
	var/cost_highpressure = 0
	var/cost_hotspots = 0
	var/cost_superconductivity = 0
	var/cost_pipenets = 0
	var/cost_atmos_machinery = 0

	var/obj/effect/overlay/plasma_overlay			//overlay for plasma
	var/obj/effect/overlay/sleeptoxin_overlay		//overlay for sleeptoxin

	var/list/excited_groups = list()
	var/list/active_turfs = list()
	var/list/hotspots = list()
	var/list/networks = list()
	var/list/obj/machinery/atmos_machinery = list()

	//Special functions lists
	var/list/turf/simulated/active_super_conductivity = list()
	var/list/turf/simulated/high_pressure_delta = list()

/datum/subsystem/air/New()
	NEW_SS_GLOBAL(SSair)

	plasma_overlay	= new /obj/effect/overlay{icon='icons/effects/tile_effects.dmi';mouse_opacity=0;layer=5;icon_state="plasma"}()
	sleeptoxin_overlay	= new /obj/effect/overlay{icon='icons/effects/tile_effects.dmi';mouse_opacity=0;layer=5;icon_state="sleeping_agent"}()

/datum/subsystem/air/stat_entry(msg)
	msg += "C:{"
	msg += "AT:[round(cost_turfs,0.01)]|"
	msg += "EG:[round(cost_groups,0.01)]|"
	msg += "HP:[round(cost_highpressure,0.01)]|"
	msg += "HS:[round(cost_hotspots,0.01)]|"
	msg += "SC:[round(cost_superconductivity,0.01)]|"
	msg += "PN:[round(cost_pipenets,0.01)]|"
	msg += "AM:[round(cost_atmos_machinery,0.01)]"
	msg += "} "
	msg +=  "AT:[active_turfs.len]|"
	msg +=  "EG:[excited_groups.len]|"
	msg +=  "HS:[hotspots.len]|"
	msg +=  "AS:[active_super_conductivity.len]"
	..(msg)

/datum/subsystem/air/Initialize(timeofday, zlevel)
	setup_atmos_machinery(zlevel)
	..()

/datum/subsystem/air/AfterInitialize(zlevel)
	setup_allturfs(zlevel)

#define MC_AVERAGE(average, current) (0.8*(average) + 0.2*(current))
/datum/subsystem/air/fire()
	var/timer = world.timeofday
	process_pipenets()
	cost_pipenets = MC_AVERAGE(cost_pipenets, (world.timeofday - timer))

	timer = world.timeofday
	process_atmos_machinery()
	cost_atmos_machinery = MC_AVERAGE(cost_atmos_machinery, (world.timeofday - timer))

	timer = world.timeofday
	process_active_turfs()
	cost_turfs = MC_AVERAGE(cost_turfs, (world.timeofday - timer))

	timer = world.timeofday
	process_excited_groups()
	cost_groups = MC_AVERAGE(cost_groups, (world.timeofday - timer))

	timer = world.timeofday
	process_high_pressure_delta()
	cost_highpressure = MC_AVERAGE(cost_highpressure, (world.timeofday - timer))

	timer = world.timeofday
	process_hotspots()
	cost_hotspots = MC_AVERAGE(cost_hotspots, (world.timeofday - timer))

	timer = world.timeofday
	process_super_conductivity()
	cost_superconductivity = MC_AVERAGE(cost_superconductivity, (world.timeofday - timer))

#undef MC_AVERAGE

/datum/subsystem/air/proc/process_pipenets()
	var/i=1
	for(var/thing in networks)
		if(thing)
			thing:process()
			++i
			continue
		networks.Cut(i, i+1)

/datum/subsystem/air/proc/process_atmos_machinery()
	var/seconds = wait * 0.1
	for(var/obj/machinery/M in atmos_machinery)
		if(M && (M.process_atmos(seconds) != PROCESS_KILL))
			continue
		atmos_machinery.Remove(M)

/datum/subsystem/air/proc/process_super_conductivity()
	for(var/turf/simulated/T in active_super_conductivity)
		T.super_conduct()

/datum/subsystem/air/proc/process_hotspots()
	for(var/obj/effect/hotspot/H in hotspots)
		H.process()

/datum/subsystem/air/proc/process_high_pressure_delta()
	for(var/turf/T in high_pressure_delta)
		T.high_pressure_movements()
		T.pressure_difference = 0
	high_pressure_delta.len = 0

/datum/subsystem/air/proc/process_active_turfs()
	for(var/turf/simulated/T in active_turfs)
		T.process_cell()

/datum/subsystem/air/proc/remove_from_active(var/turf/simulated/T)
	if(istype(T))
		T.excited = 0
		active_turfs -= T
		if(T.excited_group)
			T.excited_group.garbage_collect()

/datum/subsystem/air/proc/add_to_active(var/turf/simulated/T, var/blockchanges = 1)
	if(istype(T) && T.air)
		T.excited = 1
		active_turfs |= T
		if(blockchanges && T.excited_group)
			T.excited_group.garbage_collect()
	else
		for(var/direction in cardinal)
			if(!(T.atmos_adjacent_turfs & direction))
				continue
			var/turf/simulated/S = get_step(T, direction)
			if(istype(S))
				add_to_active(S)

/datum/subsystem/air/proc/process_excited_groups()
	for(var/datum/excited_group/EG in excited_groups)
		EG.breakdown_cooldown ++
		if(EG.breakdown_cooldown == 10)
			EG.self_breakdown()
			return
		if(EG.breakdown_cooldown > 20)
			EG.dismantle()

/datum/subsystem/air/proc/setup_allturfs(z_level)
	active_turfs.Cut()
	var/z_start = 1
	var/z_finish = world.maxz
	if(1 <= z_level && z_level <= world.maxz)
		z_level = round(z_level)
		z_start = z_level
		z_finish = z_level

	var/list/turfs_to_init = block(locate(1, 1, z_start), locate(world.maxx, world.maxy, z_finish))

	for(var/turf/simulated/T in turfs_to_init)
		T.CalculateAdjacentTurfs()
		if(!T.blocks_air)
			if(T.air.check_tile_graphic())
				T.update_visuals(T.air)
			for(var/direction in cardinal)
				if(!(T.atmos_adjacent_turfs & direction))
					continue
				var/turf/enemy_tile = get_step(T, direction)
				if(istype(enemy_tile,/turf/simulated/))
					var/turf/simulated/enemy_simulated = enemy_tile
					if(!T.air.compare(enemy_simulated.air))
						T.excited = 1
						active_turfs |= T
						break
				else
					if(!T.air.check_turf_total(enemy_tile))
						T.excited = 1
						active_turfs |= T
	if(active_turfs.len)
		warning("There are [active_turfs.len] active turfs at roundstart, this is a mapping error caused by a difference of the air between the adjacent turfs.")

/datum/subsystem/air/proc/setup_atmos_machinery(z_level)
	for (var/obj/machinery/atmospherics/AM in atmos_machinery)
		if (z_level && AM.z != z_level)
			continue
		AM.atmosinit()


code:
var/datum/subsystem/bots/SSbot

/datum/subsystem/bots
	name = "Bots"
	priority = 8

	var/list/processing = list()

/datum/subsystem/bots/New()
	NEW_SS_GLOBAL(SSbot)

/datum/subsystem/bots/stat_entry(msg)
	..("P:[processing.len]")

/datum/subsystem/bots/fire()
	var/seconds = wait * 0.1
	var/i=1
	for(var/thing in processing)
		if(thing && !thing:gc_destroyed)
			spawn(-1)
				thing:bot_process(seconds)
			++i
			continue
		processing.Cut(i, i+1)


code:
var/datum/subsystem/diseases/SSdisease

/datum/subsystem/diseases
	name = "Diseases"
	priority = 7

	var/list/processing = list()

/datum/subsystem/diseases/New()
	NEW_SS_GLOBAL(SSdisease)

/datum/subsystem/diseases/stat_entry(msg)
	..("P:[processing.len]")

/datum/subsystem/diseases/fire()
	var/i=1
	for(var/thing in processing)
		if(thing)
			thing:process()
			++i
			continue
		processing.Cut(i,i+1)


code:
var/datum/subsystem/events/SSevent

/datum/subsystem/events
	name = "Events"
	priority = 6

	var/list/control = list()	//list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
	var/list/running = list()	//list of all existing /datum/round_event

	var/scheduled = 0			//The next world.time that a naturally occuring random event can be selected.
	var/frequency_lower = 3000	//5 minutes lower bound.
	var/frequency_upper = 9000	//15 minutes upper bound. Basically an event will happen every 5 to 15 minutes.

	var/list/holidays			//List of all holidays occuring today or null if no holidays
	var/wizardmode = 0

/datum/subsystem/events/New()
	NEW_SS_GLOBAL(SSevent)

/datum/subsystem/events/Initialize(time, zlevel)
	if (zlevel)
		return ..()
	for(var/type in typesof(/datum/round_event_control))
		var/datum/round_event_control/E = new type()
		if(!E.typepath)
			continue				//don't want this one! leave it for the garbage collector
		if(E.wizardevent && !wizardmode)
			E.weight = 0
		control += E				//add it to the list of all events (controls)
	reschedule()
	getHoliday()
	..()

/datum/subsystem/events/fire()
	checkEvent()
	var/i=1
	for(var/thing in running)
		if(thing)
			thing:process()
			++i
			continue
		running.Cut(i,i+1)

//checks if we should select a random event yet, and reschedules if necessary
/datum/subsystem/events/proc/checkEvent()
	if(scheduled <= world.time)
		spawnEvent()
		reschedule()

//decides which world.time we should select another random event at.
/datum/subsystem/events/proc/reschedule()
	scheduled = world.time + rand(frequency_lower, max(frequency_lower,frequency_upper))

//selects a random event based on whether it can occur and it's 'weight'(probability)
/datum/subsystem/events/proc/spawnEvent()
	if(!config.allow_random_events)
//		var/datum/round_event_control/E = locate(/datum/round_event_control/dust) in control
//		if(E)	E.runEvent()
		return

	var/sum_of_weights = 0
	for(var/datum/round_event_control/E in control)
		if(E.occurrences >= E.max_occurrences)	continue
		if(E.earliest_start >= world.time)		continue
		if(E.holidayID)
			if(!holidays || !holidays[E.holidayID])			continue
		if(E.weight < 0)						//for round-start events etc.
			if(E.runEvent() == PROCESS_KILL)
				E.max_occurrences = 0
				continue
			if (E.alertadmins)
				message_admins("Random Event triggering: [E.name] ([E.typepath])")
			log_game("Random Event triggering: [E.name] ([E.typepath])")
			return
		sum_of_weights += E.weight

	sum_of_weights = rand(0,sum_of_weights)	//reusing this variable. It now represents the 'weight' we want to select

	for(var/datum/round_event_control/E in control)
		if(E.occurrences >= E.max_occurrences)	continue
		if(E.earliest_start >= world.time)		continue
		if(E.holidayID)
			if(!holidays || !holidays[E.holidayID])			continue
		sum_of_weights -= E.weight

		if(sum_of_weights <= 0)				//we've hit our goal
			if(E.runEvent() == PROCESS_KILL)//we couldn't run this event for some reason, set its max_occurrences to 0
				E.max_occurrences = 0
				continue
			if (E.alertadmins)
				message_admins("Random Event triggering: [E.name] ([E.typepath])")
			log_game("Random Event triggering: [E.name] ([E.typepath])")
			return

/datum/round_event/proc/findEventArea() //Here's a nice proc to use to find an area for your event to land in!
	var/list/safe_areas = list(
	/area/turret_protected/ai,
	/area/turret_protected/ai_upload,
	/area/engine,
	/area/solar,
	/area/holodeck,
	/area/shuttle
	)

	//These are needed because /area/engine has to be removed from the list, but we still want these areas to get fucked up.
	var/list/danger_areas = list(
	/area/engine/break_room,
	/area/engine/chiefs_office)

	//Need to locate() as it's just a list of paths.
	return locate(pick((the_station_areas - safe_areas) + danger_areas))

//allows a client to trigger an event
//aka Badmin Central
/client/proc/forceEvent()
	set name = "Trigger Event"
	set category = "Fun"

	if(!holder ||!check_rights(R_FUN))
		return

	holder.forceEvent()

/datum/admins/proc/forceEvent()
	var/dat 	= ""
	var/normal 	= ""
	var/magic 	= ""
	var/holiday = ""
	for(var/datum/round_event_control/E in SSevent.control)
		dat = "
[E]"
		if(E.holidayID)
			holiday	+= dat
		else if(E.wizardevent)
			magic 	+= dat
		else
			normal 	+= dat

	dat = normal + "
" + magic + "
" + holiday

	var/datum/browser/popup = new(usr, "forceevent", "Force Random Event", 300, 750)
	popup.set_content(dat)
	popup.open()

/*
//////////////
// HOLIDAYS //
//////////////
//Uncommenting ALLOW_HOLIDAYS in config.txt will enable holidays

//It's easy to add stuff. Just add a holiday datum in code/modules/holiday/holidays.dm
//You can then check if it's a special day in any code in the game by doing if(SSevent.holidays["Groundhog Day"])

//You can also make holiday random events easily thanks to Pete/Gia's system.
//simply make a random event normally, then assign it a holidayID string which matches the holiday's name.
//Anything with a holidayID, which isn't in the holidays list, will never occur.

//Please, Don't spam stuff up with stupid stuff (key example being april-fools Pooh/ERP/etc),
//And don't forget: CHECK YOUR CODE!!!! We don't want any zero-day bugs which happen only on holidays and never get found/fixed!

//////////////////////////////////////////////////////////////////////////////////////////////////////////
//ALSO, MOST IMPORTANTLY: Don't add stupid stuff! Discuss bonus content with Project-Heads first please!//
//////////////////////////////////////////////////////////////////////////////////////////////////////////
*/

//sets up the holidays and holidays list
/datum/subsystem/events/proc/getHoliday()
	if(!config.allow_holidays)	return		// Holiday stuff was not enabled in the config!

	var/YY = text2num(time2text(world.timeofday, "YY")) 	// get the current year
	var/MM = text2num(time2text(world.timeofday, "MM")) 	// get the current month
	var/DD = text2num(time2text(world.timeofday, "DD")) 	// get the current day

	for(var/H in typesof(/datum/holiday) - /datum/holiday)
		var/datum/holiday/holiday = new H()
		if(holiday.shouldCelebrate(DD, MM, YY))
			holiday.celebrate()
			if(!holidays)
				holidays = list()
			holidays[holiday.name] = holiday

	if(holidays)
		holidays = shuffle(holidays)
		world.update_status()

/datum/subsystem/events/proc/toggleWizardmode()
	wizardmode = !wizardmode
	for(var/datum/round_event_control/E in SSevent.control)
		E.weight = initial(E.weight)
		if((E.wizardevent && !wizardmode) || (!E.wizardevent && wizardmode))
			E.weight = 0
	message_admins("Summon Events has been [wizardmode ? "enabled, events will occur every [SSevent.frequency_lower / 600] to [SSevent.frequency_upper / 600] minutes" : "disabled"]!")
	log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")

/datum/subsystem/events/proc/resetFrequency()
	frequency_lower = initial(frequency_lower)
	frequency_upper = initial(frequency_upper)


code:
var/datum/subsystem/garbage_collector/SSgarbage

/datum/subsystem/garbage_collector
	name = "Garbage"
	can_fire = 1
	priority = -1
	dynamic_wait = 1
	dwait_delta = 5

	var/collection_timeout = 300// deciseconds to wait to let running procs finish before we just say fuck it and force del() the object
	var/max_run_time = 1		// how long, in deciseconds, can we run before waiting for the next tick
	var/delslasttick = 0		// number of del()'s we've done this tick
	var/gcedlasttick = 0		// number of things that gc'ed last tick
	var/totaldels = 0
	var/totalgcs = 0

	var/list/queue = list() 	// list of refID's of things that should be garbage collected
								// refID's are associated with the time at which they time out and need to be manually del()
								// we do this so we aren't constantly locating them and preventing them from being gc'd

	var/list/didntgc = list()	// list of all types that have failed to GC associated with the number of times that's happened.
								// the types are stored as strings

/datum/subsystem/garbage_collector/New()
	NEW_SS_GLOBAL(SSgarbage)

/datum/subsystem/garbage_collector/stat_entry(msg)
	msg += "Q:[queue.len]|D:[delslasttick]|G:[gcedlasttick]|"
	msg += "GR:"
	if (!(delslasttick+gcedlasttick))
		msg += "n/a|"
	else
		msg += "[round((gcedlasttick/(delslasttick+gcedlasttick))*100, 0.01)]%|"

	msg += "TD:[totaldels]|TG:[totalgcs]|"
	if (!(totaldels+totalgcs))
		msg += "n/a|"
	else
		msg += "TGR:[round((totalgcs/(totaldels+totalgcs))*100, 0.01)]%"
	..(msg)

/datum/subsystem/garbage_collector/fire()
	delslasttick = 0
	gcedlasttick = 0
	var/time_to_stop = world.timeofday + max_run_time
	var/time_to_kill = world.time - collection_timeout // Anything qdel() but not GC'd BEFORE this time needs to be manually del()

	while(queue.len && world.timeofday < time_to_stop)
		var/refID = queue[1]
		if (!refID)
			queue.Cut(1, 2)
			continue

		var/GCd_at_time = queue[refID]
		if(GCd_at_time > time_to_kill)
			break // Everything else is newer, skip them

		var/atom/A
		if (!istext(refID))
			del(A)
		else
			A = locate(refID)
			if (A && A.gc_destroyed == GCd_at_time) // So if something else coincidently gets the same ref, it's not deleted by mistake
				// Something's still referring to the qdel'd object.  Kill it.
				testing("GC: -- \ref[A] | [A.type] was unable to be GC'd and was deleted --")
				didntgc["[A.type]"]++
				del(A)
				++delslasttick
				++totaldels
			else
				++gcedlasttick
				++totalgcs

		queue.Cut(1, 2)

/datum/subsystem/garbage_collector/proc/Queue(datum/A, harddel = 0)
	if (!istype(A) || !isnull(A.gc_destroyed))
		return
	A.gc_destroyed = world.time
	queue -= "\ref[A]" // Removing any previous references that were GC'd so that the current object will be at the end of the list.
	queue["\ref[A]"] = world.time

/datum/subsystem/garbage_collector/proc/HardQueue(datum/A, harddel = 0)
	if (!istype(A) || !isnull(A.gc_destroyed))
		return
	A.gc_destroyed = world.time
	queue -= A // Removing any previous references that were GC'd so that the current object will be at the end of the list.
	queue[A] = world.time

// Should be treated as a replacement for the 'del' keyword.
// Datums passed to this will be given a chance to clean up references to allow the GC to collect them.
/proc/qdel(var/datum/A)
	if (!A)
		return
	if (!istype(A))
		//warning("qdel() passed object of type [A.type]. qdel() can only handle /datum types.")
		del(A)
	else if (isnull(A.gc_destroyed))
		// Let our friend know they're about to get fucked up.
		var/hint = A.Destroy()
		if (!A)
			return
		switch (hint)
			if (QDEL_HINT_QUEUE)		//qdel should queue the object for deletion.
				SSgarbage.Queue(A)
			if (QDEL_HINT_LETMELIVE)	//qdel should let the object live after calling destory.
				return
			if (QDEL_HINT_IWILLGC)		//functionally the same as the above. qdel should assume the object will gc on its own, and not check it.
				return
			if (QDEL_HINT_HARDDEL)		//qdel should assume this object won't gc, and queue a hard delete using a hard reference to save time from the locate()
				SSgarbage.HardQueue(A)
			if (QDEL_HINT_HARDDEL_NOW)	//qdel should assume this object won't gc, and hard del it post haste.
				del(A)
			if (QDEL_HINT_PUTINPOOL)	//qdel will put this object in the pool.
				PlaceInPool(A,0)
			else
				SSgarbage.Queue(A)

// Default implementation of clean-up code.
// This should be overridden to remove all references pointing to the object being destroyed.
// Return true if the the GC controller should allow the object to continue existing. (Useful if pooling objects.)
/datum/proc/Destroy()
	//del(src)
	tag = null
	return QDEL_HINT_QUEUE

/datum/var/gc_destroyed //Time when this object was destroyed.

#ifdef TESTING
/client/var/running_find_references

/atom/verb/find_references()
	set category = "Debug"
	set name = "Find References"
	set background = 1
	set src in world

	if(!usr || !usr.client)
		return

	if(usr.client.running_find_references)
		testing("CANCELLED search for references to a [usr.client.running_find_references].")
		usr.client.running_find_references = null
		return

	if(alert("Running this will create a lot of lag until it finishes.  You can cancel it by running it again.  Would you like to begin the search?", "Find References", "Yes", "No") == "No")
		return

	// Remove this object from the list of things to be auto-deleted.
	if(garbage)
		garbage.destroyed -= "\ref[src]"

	usr.client.running_find_references = type
	testing("Beginning search for references to a [type].")
	var/list/things = list()
	for(var/client/thing)
		things += thing
	for(var/datum/thing)
		things += thing
	for(var/atom/thing)
		things += thing
	testing("Collected list of things in search for references to a [type]. ([things.len] Thing\s)")
	for(var/datum/thing in things)
		if(!usr.client.running_find_references) return
		for(var/varname in thing.vars)
			var/variable = thing.vars[varname]
			if(variable == src)
				testing("Found [src.type] \ref[src] in [thing.type]'s [varname] var.")
			else if(islist(variable))
				if(src in variable)
					testing("Found [src.type] \ref[src] in [thing.type]'s [varname] list var.")
	testing("Completed search for references to a [type].")
	usr.client.running_find_references = null

/client/verb/purge_all_destroyed_objects()
	set category = "Debug"
	if(garbage)
		while(garbage.destroyed.len)
			var/datum/o = locate(garbage.destroyed[1])
			if(istype(o) && o.gc_destroyed)
				del(o)
				garbage.dels++
			garbage.destroyed.Cut(1, 2)
#endif


code:
var/datum/subsystem/job/SSjob

/datum/subsystem/job
	name = "Jobs"
	priority = 5

	var/list/occupations = list()		//List of all jobs
	var/list/unassigned = list()		//Players who need jobs
	var/list/job_debug = list()			//Debug info
	var/initial_players_to_assign = 0 	//used for checking against population caps

/datum/subsystem/job/New()
	NEW_SS_GLOBAL(SSjob)

/datum/subsystem/job/Initialize(timeofday, zlevel)
	if (zlevel)
		return ..()
	SetupOccupations()
	LoadJobs("config/jobs.txt")
	..()

/datum/subsystem/job/proc/SetupOccupations(faction = "Station")
	occupations = list()
	var/list/all_jobs = typesof(/datum/job)
	if(!all_jobs.len)
		world << "Error setting up jobs, no job datums found"
		return 0

	for(var/J in all_jobs)
		var/datum/job/job = new J()
		if(!job)	continue
		if(job.faction != faction)	continue
		if(!job.config_check()) continue
		occupations += job

	return 1

/datum/subsystem/job/proc/Debug(text)
	if(!Debug2)	return 0
	job_debug.Add(text)
	return 1

/datum/subsystem/job/proc/GetJob(rank)
	if(!rank)	return null
	for(var/datum/job/J in occupations)
		if(!J)	continue
		if(J.title == rank)	return J
	return null

/datum/subsystem/job/proc/AssignRole(mob/new_player/player, rank, latejoin=0)
	Debug("Running AR, Player: [player], Rank: [rank], LJ: [latejoin]")
	if(player && player.mind && rank)
		var/datum/job/job = GetJob(rank)
		if(!job)	return 0
		if(jobban_isbanned(player, rank))	return 0
		if(!job.player_old_enough(player.client)) return 0
		var/position_limit = job.total_positions
		if(!latejoin)
			position_limit = job.spawn_positions
		Debug("Player: [player] is now Rank: [rank], JCP:[job.current_positions], JPL:[position_limit]")
		player.mind.assigned_role = rank
		unassigned -= player
		job.current_positions++
		return 1
	Debug("AR has failed, Player: [player], Rank: [rank]")
	return 0

/datum/subsystem/job/proc/FindOccupationCandidates(datum/job/job, level, flag)
	Debug("Running FOC, Job: [job], Level: [level], Flag: [flag]")
	var/list/candidates = list()
	for(var/mob/new_player/player in unassigned)
		if(jobban_isbanned(player, job.title))
			Debug("FOC isbanned failed, Player: [player]")
			continue
		if(!job.player_old_enough(player.client))
			Debug("FOC player not old enough, Player: [player]")
			continue
		if(flag && (!player.client.prefs.be_special & flag))
			Debug("FOC flag failed, Player: [player], Flag: [flag], ")
			continue
		if(player.mind && job.title in player.mind.restricted_roles)
			Debug("FOC incompatible with antagonist role, Player: [player]")
			continue
		if(config.enforce_human_authority && (job.title in command_positions) && player.client.prefs.pref_species.id != "human")
			Debug("FOC non-human failed, Player: [player]")
			continue
		if(player.client.prefs.GetJobDepartment(job, level) & job.flag)
			Debug("FOC pass, Player: [player], Level:[level]")
			candidates += player
	return candidates

/datum/subsystem/job/proc/GiveRandomJob(mob/new_player/player)
	Debug("GRJ Giving random job, Player: [player]")
	for(var/datum/job/job in shuffle(occupations))
		if(!job)
			continue

		if(istype(job, GetJob("Assistant"))) // We don't want to give him assistant, that's boring!
			continue

		if(job in command_positions) //If you want a command position, select it!
			continue

		if(jobban_isbanned(player, job.title))
			Debug("GRJ isbanned failed, Player: [player], Job: [job.title]")
			continue

		if(!job.player_old_enough(player.client))
			Debug("GRJ player not old enough, Player: [player]")
			continue

		if(player.mind && job.title in player.mind.restricted_roles)
			Debug("GRJ incompatible with antagonist role, Player: [player], Job: [job.title]")
			continue

		if(config.enforce_human_authority && (job.title in command_positions) && player.client.prefs.pref_species.id != "human")
			Debug("GRJ non-human failed, Player: [player]")
			continue

		if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1)
			Debug("GRJ Random job given, Player: [player], Job: [job]")
			AssignRole(player, job.title)
			unassigned -= player
			break

/datum/subsystem/job/proc/ResetOccupations()
	for(var/mob/new_player/player in player_list)
		if((player) && (player.mind))
			player.mind.assigned_role = null
			player.mind.special_role = null
	SetupOccupations()
	unassigned = list()
	return

//This proc is called before the level loop of DivideOccupations() and will try to select a head, ignoring ALL non-head preferences for every level until
//it locates a head or runs out of levels to check
//This is basically to ensure that there's atleast a few heads in the round
/datum/subsystem/job/proc/FillHeadPosition()
	for(var/level = 1 to 3)
		for(var/command_position in command_positions)
			var/datum/job/job = GetJob(command_position)
			if(!job)	continue
			if((job.current_positions >= job.total_positions) && job.total_positions != -1)	continue
			var/list/candidates = FindOccupationCandidates(job, level)
			if(!candidates.len)	continue
			var/mob/new_player/candidate = pick(candidates)
			if(AssignRole(candidate, command_position))
				return 1
	return 0

//This proc is called at the start of the level loop of DivideOccupations() and will cause head jobs to be checked before any other jobs of the same level
//This is also to ensure we get as many heads as possible
/datum/subsystem/job/proc/CheckHeadPositions(level)
	for(var/command_position in command_positions)
		var/datum/job/job = GetJob(command_position)
		if(!job)	continue
		if((job.current_positions >= job.total_positions) && job.total_positions != -1)	continue
		var/list/candidates = FindOccupationCandidates(job, level)
		if(!candidates.len)	continue
		var/mob/new_player/candidate = pick(candidates)
		AssignRole(candidate, command_position)
	return

/datum/subsystem/job/proc/FillAIPosition()
	var/ai_selected = 0
	var/datum/job/job = GetJob("AI")
	if(!job)	return 0
	if(ticker.mode.name == "AI malfunction")	// malf. AIs are pre-selected before jobs
		for (var/datum/mind/mAI in ticker.mode.malf_ai)
			AssignRole(mAI.current, "AI")
			ai_selected++
		if(ai_selected)	return 1
		return 0

	for(var/i = job.total_positions, i > 0, i--)
		for(var/level = 1 to 3)
			var/list/candidates = list()
			candidates = FindOccupationCandidates(job, level)
			if(candidates.len)
				var/mob/new_player/candidate = pick(candidates)
				if(AssignRole(candidate, "AI"))
					ai_selected++
					break
	if(ai_selected)	return 1
	return 0

/** Proc DivideOccupations
 *  fills var "assigned_role" for all ready players.
 *  This proc must not have any side effect besides of modifying "assigned_role".
 **/
/datum/subsystem/job/proc/DivideOccupations()
	//Setup new player list and get the jobs list
	Debug("Running DO")
	SetupOccupations()

	//Holder for Triumvirate is stored in the ticker, this just processes it
	if(ticker)
		for(var/datum/job/ai/A in occupations)
			if(ticker.triai)
				A.spawn_positions = 3

	//Get the players who are ready
	for(var/mob/new_player/player in player_list)
		if(player.ready && player.mind && !player.mind.assigned_role)
			unassigned += player

	initial_players_to_assign = unassigned.len

	Debug("DO, Len: [unassigned.len]")
	if(unassigned.len == 0)	return 0

	//Scale number of open security officer slots to population
	setup_officer_positions()

	//Jobs will have fewer access permissions if the number of players exceeds the threshold defined in game_options.txt
	if(config.minimal_access_threshold)
		if(config.minimal_access_threshold > unassigned.len)
			config.jobs_have_minimal_access = 0
		else
			config.jobs_have_minimal_access = 1

	//Shuffle players and jobs
	unassigned = shuffle(unassigned)

	HandleFeedbackGathering()

	//People who wants to be assistants, sure, go on.
	Debug("DO, Running Assistant Check 1")
	var/datum/job/assist = new /datum/job/assistant()
	var/list/assistant_candidates = FindOccupationCandidates(assist, 3)
	Debug("AC1, Candidates: [assistant_candidates.len]")
	for(var/mob/new_player/player in assistant_candidates)
		Debug("AC1 pass, Player: [player]")
		AssignRole(player, "Assistant")
		assistant_candidates -= player
	Debug("DO, AC1 end")

	//Select one head
	Debug("DO, Running Head Check")
	FillHeadPosition()
	Debug("DO, Head Check end")

	//Check for an AI
	Debug("DO, Running AI Check")
	FillAIPosition()
	Debug("DO, AI Check end")

	//Other jobs are now checked
	Debug("DO, Running Standard Check")

	// New job giving system by Donkie
	// This will cause lots of more loops, but since it's only done once it shouldn't really matter much at all.
	// Hopefully this will add more randomness and fairness to job giving.

	// Loop through all levels from high to low
	var/list/shuffledoccupations = shuffle(occupations)
	for(var/level = 1 to 3)
		//Check the head jobs first each level
		CheckHeadPositions(level)

		// Loop through all unassigned players
		for(var/mob/new_player/player in unassigned)
			if(PopcapReached())
				RejectPlayer(player)

			// Loop through all jobs
			for(var/datum/job/job in shuffledoccupations) // SHUFFLE ME BABY
				if(!job)
					continue

				if(jobban_isbanned(player, job.title))
					Debug("DO isbanned failed, Player: [player], Job:[job.title]")
					continue

				if(!job.player_old_enough(player.client))
					Debug("DO player not old enough, Player: [player], Job:[job.title]")
					continue

				if(player.mind && job.title in player.mind.restricted_roles)
					Debug("DO incompatible with antagonist role, Player: [player], Job:[job.title]")
					continue

				if(config.enforce_human_authority && (job.title in command_positions) && player.client.prefs.pref_species.id != "human")
					Debug("DO non-human failed, Player: [player], Job:[job.title]")
					continue

				// If the player wants that job on this level, then try give it to him.
				if(player.client.prefs.GetJobDepartment(job, level) & job.flag)

					// If the job isn't filled
					if((job.current_positions < job.spawn_positions) || job.spawn_positions == -1)
						Debug("DO pass, Player: [player], Level:[level], Job:[job.title]")
						AssignRole(player, job.title)
						unassigned -= player
						break

	// Hand out random jobs to the people who didn't get any in the last check
	// Also makes sure that they got their preference correct
	for(var/mob/new_player/player in unassigned)
		if(PopcapReached())
			RejectPlayer(player)
		else if(jobban_isbanned(player, "Assistant"))
			GiveRandomJob(player) //you get to roll for random before everyone else just to be sure you don't get assistant. you're so speshul

	for(var/mob/new_player/player in unassigned)
		if(PopcapReached())
			RejectPlayer(player)
		else if(player.client.prefs.userandomjob)
			GiveRandomJob(player)

	Debug("DO, Standard Check end")

	Debug("DO, Running AC2")

	// For those who wanted to be assistant if their preferences were filled, here you go.
	for(var/mob/new_player/player in unassigned)
		if(PopcapReached())
			RejectPlayer(player)
		Debug("AC2 Assistant located, Player: [player]")
		AssignRole(player, "Assistant")
	return 1

//Gives the player the stuff he should have with his rank
/datum/subsystem/job/proc/EquipRank(mob/living/H, rank, joined_late=0)
	var/datum/job/job = GetJob(rank)

	H.job = rank

	//If we joined at roundstart we should be positioned at our workstation
	if(!joined_late)
		var/obj/S = null
		for(var/obj/effect/landmark/start/sloc in start_landmarks_list)
			if(sloc.name != rank)	continue
			if(locate(/mob/living) in sloc.loc)	continue
			S = sloc
			break
		if(!S) //if there isn't a spawnpoint send them to latejoin, if there's no latejoin go yell at your mapper
			S = pick(latejoin)
		if(istype(S, /obj/effect/landmark) && istype(S.loc, /turf))
			H.loc = S.loc

	if(H.mind)
		H.mind.assigned_role = rank

	if(job)
		var/new_mob = job.equip(H)
		if(ismob(new_mob))
			H = new_mob
		job.apply_fingerprints(H)

	H << "You are the [rank]."
	H << "As the [rank] you answer directly to [job.supervisors]. Special circumstances may change this."
	H << "To speak on your departments radio, use the :h button. To see others, look closely at your headset."
	if(job.req_admin_notify)
		H << "You are playing a job that is important for Game Progression. If you have to disconnect, please notify the admins via adminhelp."
	if(config.minimal_access_threshold)
		H << "As this station was initially staffed with a [config.jobs_have_minimal_access ? "full crew, only your job's necessities" : "skeleton crew, additional access may"] have been added to your ID card."

	H.update_hud() 	// Tmp fix for Github issue 1006. TODO: make all procs in update_icons.dm do client.screen |= equipment no matter what.
	return 1

/datum/subsystem/job/proc/setup_officer_positions()
	var/officer_positions = 5 //Number of open security officer positions at round start

	if(config.security_scaling_coeff > 0)
		officer_positions = min(12, max(5, round(unassigned.len/config.security_scaling_coeff))) //Scale between 5 and 12 officers
		var/datum/job/J = SSjob.GetJob("Security Officer")
		if(J  || J.spawn_positions > 0)
			Debug("Setting open security officer positions to [officer_positions]")
			J.total_positions = officer_positions
			J.spawn_positions = officer_positions
	for(var/i=officer_positions-5, i>0, i--) //Spawn some extra eqipment lockers if we have more than 5 officers
		if(secequipment.len)
			var/spawnloc = secequipment[1]
			new /obj/structure/closet/secure_closet/security(spawnloc)
			secequipment -= spawnloc
		else //We ran out of spare locker spawns!
			break

/datum/subsystem/job/proc/LoadJobs(jobsfile) //ran during round setup, reads info from jobs.txt -- Urist
	if(!config.load_jobs_from_txt)
		return 0

	var/list/jobEntries = file2list(jobsfile)

	for(var/job in jobEntries)
		if(!job)
			continue

		job = trim(job)
		if (!length(job))
			continue

		var/pos = findtext(job, "=")
		var/name = null
		var/value = null

		if(pos)
			name = copytext(job, 1, pos)
			value = copytext(job, pos + 1)
		else
			continue

		if(name && value)
			var/datum/job/J = GetJob(name)
			if(!J)	continue
			J.total_positions = text2num(value)
			J.spawn_positions = text2num(value)
			if(name == "AI" || name == "Cyborg")//I dont like this here but it will do for now
				J.total_positions = 0

	return 1

/datum/subsystem/job/proc/HandleFeedbackGathering()
	for(var/datum/job/job in occupations)
		var/tmp_str = "|[job.title]|"

		var/level1 = 0 //high
		var/level2 = 0 //medium
		var/level3 = 0 //low
		var/level4 = 0 //never
		var/level5 = 0 //banned
		var/level6 = 0 //account too young
		for(var/mob/new_player/player in player_list)
			if(!(player.ready && player.mind && !player.mind.assigned_role))
				continue //This player is not ready
			if(jobban_isbanned(player, job.title))
				level5++
				continue
			if(!job.player_old_enough(player.client))
				level6++
				continue
			if(player.client.prefs.GetJobDepartment(job, 1) & job.flag)
				level1++
			else if(player.client.prefs.GetJobDepartment(job, 2) & job.flag)
				level2++
			else if(player.client.prefs.GetJobDepartment(job, 3) & job.flag)
				level3++
			else level4++ //not selected

		tmp_str += "HIGH=[level1]|MEDIUM=[level2]|LOW=[level3]|NEVER=[level4]|BANNED=[level5]|YOUNG=[level6]|-"
		feedback_add_details("job_preferences",tmp_str)

/datum/subsystem/job/proc/PopcapReached()
	if(config.hard_popcap || config.extreme_popcap)
		var/relevent_cap = max(config.hard_popcap, config.extreme_popcap)
		if((initial_players_to_assign - unassigned.len) >= relevent_cap)
			return 1
	return 0

/datum/subsystem/job/proc/RejectPlayer(var/mob/new_player/player)
	if(player.mind && player.mind.special_role)
		return
	Debug("Popcap overflow Check observer located, Player: [player]")
	player << "You have failed to qualify for any job you desired."
	unassigned -= player
	player.ready = 0


code:
var/datum/subsystem/lighting/SSlighting

#define MC_AVERAGE(average, current) (0.8*(average) + 0.2*(current))

/datum/subsystem/lighting
	name = "Lighting"
	wait = LIGHTING_INTERVAL
	priority = 1
	dynamic_wait = 1
	dwait_delta = 1

//	var/list/lighting_images = list()		//replaces lighting_states (use lighting_images.len) ~carn
//	var/list/lights = list()				//list of all datum/light_source
	var/lights_workload = 0					//stats on the largest number of lights (max lights.len)
//	var/lighting_states = 6
//	var/list/changed_turfs = list()			//list of all turfs which need moving to a new lighting subarea
//	var/changed_turfs_workload = 0			//stats on the largest number of turfs changed (max changed_turfs.len)

/datum/subsystem/lighting/New()
	NEW_SS_GLOBAL(SSlighting)

	//cache lighting images
//	if(!lighting_images.len)
//		for(var/icon_state in icon_states(LIGHTING_ICON))
//			lighting_images += image(LIGHTING_ICON, null, icon_state, LIGHTING_LAYER)

	return ..()

/datum/subsystem/lighting/stat_entry()
	stat(name, "[round(cost,0.001)]ds L:[round(lights_workload,1)]")

//Workhorse of lighting. It cycles through each light to see which ones need their effects updating. It updates their
//effects and then processes every turf in the queue, moving the turfs to the corresponing lighting sub-area.
//All queue lists prune themselves, which will cause lights with no luminosity to be garbage collected (cheaper and safer
//than deleting them).
//By using queues we are ensuring we don't perform more updates than are necessary
/datum/subsystem/lighting/fire()

	lights_workload = MC_AVERAGE(lights_workload, lighting_update_lights.len)
	for(var/datum/light_source/L in lighting_update_lights)
		if(L.needs_update)
			if(L.destroyed || L.check() || L.force_update)
				L.remove_lum()
			if(!L.destroyed)
				L.apply_lum()
			L.force_update = 0
			L.needs_update = 0

//		scheck()

	lighting_update_lights.len = 0

	for(var/atom/movable/lighting_overlay/O in lighting_update_overlays)
		if(O.needs_update)
			O.update_overlay()
			O.needs_update = 0

//		scheck()

	lighting_update_overlays.len = 0

/*
	lights_workload = MC_AVERAGE(lights_workload, lighting_update_lights.len)
	var/i=1
	for(var/thing in lights)
		if(thing && !thing:check())	//yes, cry that I'm using the : operator, it's much faster looping like this. And this gets called a lot. Dealwithit.
			++i
			continue
		lights.Cut(i, i+1)

	changed_turfs_workload = MC_AVERAGE(changed_turfs_workload, changed_turfs.len)
	for(var/thing in changed_turfs)
		if(thing && thing:lighting_changed)
			thing:shift_to_subarea()
	changed_turfs.Cut()

*/
//same as above except it attempts to shift ALL turfs in the world regardless of lighting_changed status
//Does not loop. Should be run prior to process() being called for the first time.
//Note: if we get additional z-levels at runtime (e.g. if the gateway thin ever gets finished) we can initialize specific
//z-levels with the z_level argument
/datum/subsystem/lighting/Initialize(timeofday, z_level)

	create_lighting_overlays()

	if(z_level)
		create_lighting_overlays(z_level)

	if(config.starlight)  //As if!
		set background = 1
		for(var/turf/space/S in world)
			S.update_starlight()

	..()

//Used to strip valid information from an existing instance and transfer it to the replacement. i.e. when a crash occurs
//It works by using spawn(-1) to transfer the data, if there is a runtime the data does not get transfered but the loop
//does not crash
/datum/subsystem/lighting/Recover()
	var/msg = "## DEBUG: [time2text(world.timeofday)] [name] subsystem restarted. Reports:\n"
	for(var/varname in SSlighting.vars)
		switch(varname)
			if("tag","bestF","type","parent_type","vars")	continue
			else
				var/varval1 = SSlighting.vars[varname]
				var/varval2 = vars[varname]
				if(istype(varval1,/list))
					varval1 = "/list([length(varval1)])"
					varval2 = "/list([length(varval2)])"
				msg += "\t [varname] = [varval1] -> [varval2]\n"
	world.log << msg


code:
var/datum/subsystem/machines/SSmachine

/datum/subsystem/machines
	name = "Machines"
	priority = 9

	var/list/processing = list()
	var/list/powernets = list()

/datum/subsystem/machines/Initialize()
	makepowernets()
	fire()
	..()

/datum/subsystem/machines/proc/makepowernets(zlevel)
	for(var/datum/powernet/PN in powernets)
		qdel(PN)
	powernets.Cut()

	for(var/obj/structure/cable/PC in cable_list)
		if(!PC.powernet)
			var/datum/powernet/NewPN = new()
			NewPN.add_cable(PC)
			propagate_network(PC,PC.powernet)

/datum/subsystem/machines/New()
	NEW_SS_GLOBAL(SSmachine)

/datum/subsystem/machines/stat_entry()
	..("M:[processing.len]|PN:[powernets.len]")

/datum/subsystem/machines/fire()
	for(var/datum/powernet/Powernet in powernets)
		Powernet.reset() //reset the power state.

	var/seconds = wait * 0.1
	for(var/thing in processing)
		if(thing && (thing:process(seconds) != PROCESS_KILL))
			if(thing:use_power)
				thing:auto_use_power() //add back the power state
			continue
		processing.Remove(thing)


code:
var/datum/subsystem/mobs/SSmob

/datum/subsystem/mobs
	name = "Mobs"
	priority = 4

/datum/subsystem/mobs/New()
	NEW_SS_GLOBAL(SSmob)

/datum/subsystem/mobs/stat_entry()
	..("P:[mob_list.len]")

/datum/subsystem/mobs/fire()
	var/seconds = wait * 0.1
	for(var/thing in mob_list)
		if(thing)
			thing:Life(seconds)
			continue
		mob_list.Remove(thing)


code:
var/datum/subsystem/nano/SSnano

/datum/subsystem/nano
	name = "NanoUI"
	can_fire = 1
	wait = 5
	priority = 16

	var/list/open_uis = list()			//a list of current open /nanoui UIs, grouped by src_object and ui_key
	var/list/processing_uis = list()	//a list of current open /nanoui UIs, not grouped, for use in processing

	//List of asset filenames to be sent to the client on user login
	var/list/asset_files = list()

/datum/subsystem/nano/New()
	NEW_SS_GLOBAL(SSnano)

	//Generate list of files to send to client for nano UI's
	var/list/nano_asset_dirs = list(\
		"nano/css/",\
		"nano/images/",\
		"nano/js/",\
		"nano/templates/"\
	)
	var/list/filenames = null
	for (var/path in nano_asset_dirs)
		filenames = flist(path)
		for(var/filename in filenames)
			//Ignore directories
			if(copytext(filename, length(filename)) != "/")
				if(fexists(path + filename))
					asset_files.Add(fcopy_rsc(path + filename))

/datum/subsystem/nano/stat_entry()
	..("P:[processing_uis.len]")

/datum/subsystem/nano/fire()
	var/i=1
	for(var/thing in SSnano.processing_uis)
		if(thing)
			var/datum/nanoui/ui = thing
			if(ui.src_object && ui.user)
				ui.process()
				++i
				continue
		processing_uis.Cut(i, i+1)