code:
SUBSYSTEM_DEF(networks)
	name = "Networks"
	priority = FIRE_PRIORITY_NETWORKS
	wait = 5
	flags = SS_KEEP_TIMING
	init_order = INIT_ORDER_NETWORKS

	var/list/relays = list()
	/// Legacy ntnet lookup for software.  Should be changed latter so don't rely on this
	/// being here.
	var/datum/ntnet/station_root/station_network
	var/datum/ntnet/station_root/syndie_network
	var/list/network_initialize_queue = list()
	/// all interfaces by their hardware address.
	/// Do NOT use to verify a reciver_id is valid, use the network.root_devices for that
	var/list/interfaces_by_hardware_id = list()
	/// Area to network, network to area list
	/// This is an associated list to quickly find an area using either its id or network
	/// Used mainly to make sure all area id's are unique even if a mapper uses the same
	/// area many times
	var/list/area_network_lookup = list()

	/// List of networks using their fully qualified network name.  Used for quick lookups
	/// of networks for sending packets
	var/list/networks = list()
	/// List of the root networks starting at their root names.  Used to find and/or build
	/// network tress
	var/list/root_networks = list()

	// Why not list?  Because its a Copy() every time we add a packet, and thats stupid.
	var/datum/netdata/first = null // start of the queue.  Pulled off in fire.
	var/datum/netdata/last = null // end of the queue.  pushed on by transmit
	var/packet_count = 0
	// packet stats
	var/count_broadcasts_packets = 0 // count of broadcast packets sent
	var/count_failed_packets = 0 // count of message fails
	var/count_good_packets = 0
	// Logs moved here
	// Amount of logs the system tries to keep in memory. Keep below 999 to prevent byond from acting weirdly.
	// High values make displaying logs much laggier.
	var/setting_maxlogcount = 100
	var/list/logs = list()

	/// Random name search to make sure we have unique names.
	/// DO NOT REMOVE NAMES HERE UNLESS YOU KNOW WHAT YOUR DOING
	var/list/used_names = list()

/// You shouldn't need to do this.  But mapping is async and there is no guarantee that Initialize
/// will run before these networks are dynamically created.  So its here.
/datum/controller/subsystem/networks/PreInit()
	/// Limbo network needs to be made at boot up for all error devices
	new/datum/ntnet(LIMBO_NETWORK_ROOT)
	station_network = new(STATION_NETWORK_ROOT)
	syndie_network = new(SYNDICATE_NETWORK_ROOT)
	/// As well as the station network incase something funny goes during startup
	new/datum/ntnet(CENTCOM_NETWORK_ROOT)

/datum/controller/subsystem/networks/stat_entry(msg)
	msg = "NET: QUEUE([packet_count]) FAILS([count_failed_packets]) BROADCAST([count_broadcasts_packets])"
	return ..()

/datum/controller/subsystem/networks/Initialize()
	station_network.register_map_supremecy() // sigh
	assign_areas_root_ids(GLOB.sortedAreas) // setup area names before Initialize
	station_network.build_software_lists()
	syndie_network.build_software_lists()

	// At round start, fix the network_id's so the station root is on them
	initialized = TRUE
	// Now when the objects Initialize they will join the right network
	return ..()

/*
 * Process incoming queued packet and return NAK/ACK signals
 *
 * This should only be called when you want the target object to process the NAK/ACK signal, usually
 * during fire.  At this point data.receiver_id has already been converted if it was a broadcast but
 * is undefined in this function.
 * Arguments:
 * * receiver_id - text hardware id for the target device
 * * data - packet to be sent
 */

/datum/controller/subsystem/networks/proc/_process_packet(receiver_id, datum/netdata/data)
	/// Used only for sending NAK/ACK and error reply's
	var/datum/component/ntnet_interface/sending_interface = interfaces_by_hardware_id[data.sender_id]

	/// Check if the network_id is valid and if not send an error and return
	var/datum/ntnet/target_network = networks[data.network_id]
	if(!target_network)
		count_failed_packets++
		add_log("Bad target network '[data.network_id]'", null, data.sender_id)
		if(!QDELETED(sending_interface))
			SEND_SIGNAL(sending_interface.parent, COMSIG_COMPONENT_NTNET_NAK, data , NETWORK_ERROR_BAD_NETWORK)
		return

	/// Check if the receiver_id is in the network.  If not send an error and return
	var/datum/component/ntnet_interface/target_interface = target_network.root_devices[receiver_id]
	if(QDELETED(target_interface))
		count_failed_packets++
		add_log("Bad target device '[receiver_id]'", target_network, data.sender_id)
		if(!QDELETED(sending_interface))
			SEND_SIGNAL(sending_interface.parent, COMSIG_COMPONENT_NTNET_NAK, data,  NETWORK_ERROR_BAD_RECEIVER_ID)
		return

	// Check if we care about permissions.  If we do check if we are allowed the message to be processed
	if(data.passkey) // got to check permissions
		var/obj/O = target_interface.parent
		if(O)
			if(!O.check_access_ntnet(data.passkey))
				count_failed_packets++
				add_log("Access denied to ([receiver_id]) from ([data.network_id])", target_network, data.sender_id)
				if(!QDELETED(sending_interface))
					SEND_SIGNAL(sending_interface.parent, COMSIG_COMPONENT_NTNET_NAK, data, NETWORK_ERROR_UNAUTHORIZED)
				return
		else
			add_log("A access key message was sent to a non-device", target_network, data.sender_id)
			if(!QDELETED(sending_interface))
				SEND_SIGNAL(sending_interface.parent, COMSIG_COMPONENT_NTNET_NAK, data, NETWORK_ERROR_UNAUTHORIZED)

	SEND_SIGNAL(target_interface.parent, COMSIG_COMPONENT_NTNET_RECEIVE, data)
	// All is good, send the packet then send an ACK to the sender
	if(!QDELETED(sending_interface))
		SEND_SIGNAL(sending_interface.parent, COMSIG_COMPONENT_NTNET_ACK, data)
	count_good_packets++

/// Helper define to make sure we pop the packet and qdel it
#define POP_PACKET(CURRENT) first = CURRENT.next;  packet_count--; if(!first) { last = null; packet_count = 0; }; qdel(CURRENT);

/datum/controller/subsystem/networks/fire(resumed = 0)
	var/datum/netdata/current
	var/datum/component/ntnet_interface/target_interface
	while(first)
		current = first
		/// Check if we are a list.  If so process the list
		if(islist(current.receiver_id)) // are we a broadcast list
			var/list/receivers = current.receiver_id
			var/receiver_id = receivers[receivers.len] // pop it
			receivers.len--
			_process_packet(receiver_id, current)
			if(receivers.len == 0) // pop it if done
				count_broadcasts_packets++
				POP_PACKET(current)
		else // else set up a broadcast or send a single targete
			// check if we are sending to a network or to a single target
			target_interface = interfaces_by_hardware_id[current.receiver_id]
			if(target_interface) // a single sender id
				_process_packet(current.receiver_id, current) // single target
				POP_PACKET(current)
			else // ok so lets find the network to send it too
				var/datum/ntnet/net = networks[current.network_id] // get the sending network
				net = net?.networks[current.receiver_id] // find the target network to broadcast
				if(net) // we found it
					current.receiver_id = net.collect_interfaces() // make a list of all the sending targets
				else
					// We got an error, the network is bad so send a NAK
					target_interface = interfaces_by_hardware_id[current.sender_id]
					if(!QDELETED(target_interface))
						SEND_SIGNAL(target_interface.parent, COMSIG_COMPONENT_NTNET_NAK, current , NETWORK_ERROR_BAD_NETWORK)
					POP_PACKET(current) // and get rid of it
		if (MC_TICK_CHECK)
			return

#undef POP_PACKET

/*
 * Main function to queue a packet.  As long as we have valid receiver_id and network_id we will take it
 *
 * Main queuing function for any message sent.  if the data.receiver_id is null, then it will be broadcasted
 * error checking is only done during the process this just throws it on the queue.
 * Arguments:
 * * data - packet to be sent
 */
/datum/controller/subsystem/networks/proc/transmit(datum/netdata/data)
	data.next = null // sanity check

	if(!last)
		first = last = data
	else
		last.next = data
		last = data
	packet_count++
	// We do error checking when the packet is sent
	return NETWORK_ERROR_OK

/datum/controller/subsystem/networks/proc/check_relay_operation(zlevel=0) //can be expanded later but right now it's true/false.
	for(var/i in relays)
		var/obj/machinery/ntnet_relay/n = i
		if(zlevel && n.z != zlevel)
			continue
		if(n.is_operational)
			return TRUE
	return FALSE

/datum/controller/subsystem/networks/proc/log_data_transfer( datum/netdata/data)
	logs += "[station_time_timestamp()] - [data.generate_netlog()]"
	if(logs.len > setting_maxlogcount)
		logs = logs.Copy(logs.len - setting_maxlogcount, 0)

/**
 * Records a message into the station logging system for the network
 *
 * This CAN be read in station by personal so do not use it for game debugging
 * during fire.  At this point data.receiver_id has already been converted if it was a broadcast but
 * is undefined in this function.  It is also dumped to normal logs but remember players can read/intercept
 * these messages
 * Arguments:
 * * log_string - message to log
 * * network - optional, It can be a ntnet or just the text equivalent
 * * hardware_id = optional, text, will look it up and return with the parent.name as well
 */
/datum/controller/subsystem/networks/proc/add_log(log_string, network = null , hardware_id = null)
	set waitfor = FALSE // so process keeps running
	var/list/log_text = list()
	log_text += "\[[station_time_timestamp()]\]"
	if(network)
		var/datum/ntnet/net = network
		if(!istype(net))
			net = networks[network]
		if(net) // bad network?
			log_text += "{[net.network_id]}"
		else // bad network?
			log_text += "{[network] *BAD*}"

	if(hardware_id)
		var/datum/component/ntnet_interface/conn = interfaces_by_hardware_id[hardware_id]
		if(conn)
			log_text += " ([hardware_id])[conn.parent]"
		else
			log_text += " ([hardware_id])*BAD ID*"
	else
		log_text += "*SYSTEM*"
	log_text += " - "
	log_text += log_string
	log_string = log_text.Join()

	logs.Add(log_string)
	//log_telecomms("NetLog: [log_string]") // causes runtime on startup humm

	// We have too many logs, remove the oldest entries until we get into the limit
	if(logs.len > setting_maxlogcount)
		logs = logs.Copy(logs.len-setting_maxlogcount,0)

/**
 * Removes all station logs for the current game
 */
/datum/controller/subsystem/networks/proc/purge_logs()
	logs = list()
	add_log("-!- LOGS DELETED BY SYSTEM OPERATOR -!-")

/**
 * Updates the maximum amount of logs and purges those that go beyond that number
 *
 * Shouldn't been needed to be run by players but maybe admins need it?
 * Arguments:
 * * lognumber - new setting_maxlogcount count
 */
/datum/controller/subsystem/networks/proc/update_max_log_count(lognumber)
	if(!lognumber)
		return FALSE
	// Trim the value if necessary
	lognumber = max(MIN_NTNET_LOGS, min(lognumber, MAX_NTNET_LOGS))
	setting_maxlogcount = lognumber
	add_log("Configuration Updated. Now keeping [setting_maxlogcount] logs in system memory.")

/**
 * Gives an area a root and a network_area_id
 *
 * When a device is added to the network on map load, it needs to know where it is.
 * So that it is added to that ruins/base's network instead of the general station network
 * This way people on the station cannot just hack Charlie's doors and visa versa.  All area's
 * "should" have this information and if not one is created from existing map tags or
 * ruin template id's.  This SHOULD run before the Initialize of a atom, or the root will not
 * be put in the object.area
 *
 * An example on what the area.network_root_id does/
 * Before Init: obj.network_id = "ATMOS.SCRUBBER"  area.network_root_id="SS13_STATION" area.network_area_id = "BRIDGE"
 * After Init: obj.network_id = "SS13_STATION.ATMOS.SCRUBBER" also obj.network_id = "SS13_STATION.AREA.BRIDGE"
 *
 * Arguments:
 * * area - Area to modify the root id.
 * * template - optional, map_template of that area
 */
/datum/controller/subsystem/networks/proc/lookup_area_root_id(area/A, datum/map_template/M=null)
	/// Check if the area is valid and if it doesn't have a network root id.
	if(!istype(A) || A.network_root_id != null)
		return

	/// If we are a ruin or a shuttle, we get our own network
	if(M)
		/// if we have a template, try to get the network id from the template
		if(M.station_id && M.station_id != LIMBO_NETWORK_ROOT) // check if the template specifies it
			A.network_root_id = simple_network_name_fix(M.station_id)
		else if(istype(M, /datum/map_template/shuttle))  // if not, then check if its a shuttle type
			var/datum/map_template/shuttle/T = M // we are a shuttle so use shuttle id
			A.network_root_id = simple_network_name_fix(T.shuttle_id)
		else if(istype(M,/datum/map_template/ruin)) // if not again, check if its a ruin type
			var/datum/map_template/ruin/R = M
			A.network_root_id = simple_network_name_fix(R.id)

	if(!A.network_root_id) // not assigned?  Then lets use some defaults
		// Anything in Centcom is completely isolated
		// Special case for holodecks.
		if(istype(A,/area/holodeck))
			A.network_root_id =  "HOLODECK" // isolated from the station network
		else if(SSmapping.level_trait(A.z, ZTRAIT_CENTCOM))
			A.network_root_id =  CENTCOM_NETWORK_ROOT
		// Otherwise the default is the station
		else
			A.network_root_id =  STATION_NETWORK_ROOT

/datum/controller/subsystem/networks/proc/assign_area_network_id(area/A, datum/map_template/M=null)
	if(!istype(A))
		return
	if(!A.network_root_id)
		lookup_area_root_id(A, M)
		// finally  set the network area id, bit copy paste from area Initialize
		// This is done in case we have more than one area type, each area instance has its own network name
	if(!A.network_area_id)
		A.network_area_id = A.network_root_id + ".AREA." + simple_network_name_fix(A.name) // Make the string
		if(!(A.area_flags & UNIQUE_AREA)) // if we aren't a unique area, make sure our name is different
			A.network_area_id = SSnetworks.assign_random_name(5, A.network_area_id + "_") // tack on some garbage incase there are two area types

/datum/controller/subsystem/networks/proc/assign_areas_root_ids(list/areas, datum/map_template/M=null)
	for(var/area/A in areas)
		assign_area_network_id(A, M)

/**
 * Converts a list of string's into a full network_id
 *
 * Converts a list of individual branches into a proper network id.  Validates
 * individual parts to make sure they are clean.
 *
 * ex. list("A","B","C") -> A.B.C
 *
 * Arguments:
 * * tree - List of strings
 */
/datum/controller/subsystem/networks/proc/network_list_to_string(list/tree)
#ifdef DEBUG_NETWORKS
	ASSERT(tree && tree.len > 0) // this should be obvious but JUST in case.
	for(var/part in tree)
		if(!verify_network_name(part) || findtext(name,".")!=0) // and no stray dots
			stack_trace("network_list_to_string: Cannot create network with ([part]) of ([tree.Join(".")])")
			break
#endif
	return tree.Join(".")

/**
 * Converts string into a list of network branches
 *
 * Converts a a proper network id into a list of the individual branches
 *
 * ex.  A.B.C -> list("A","B","C")
 *
 * Arguments:
 * * tree - List of strings
 */
/datum/controller/subsystem/networks/proc/network_string_to_list(name)
#ifdef DEBUG_NETWORKS
	if(!verify_network_name(name))
		stack_trace("network_string_to_list: [name] IS INVALID")
#endif
	return splittext(name,".") // should we do a splittext_char?  I doubt we really need unicode in network names

/**
 * Hard creates a network. Helper function for create_network_simple and create_network
 *
 * Hard creates a using a list of branches and returns.  No error checking as it should
 * of been done before this call
 *
 * Arguments:
 * * network_tree - list,text List of branches of network
 */
/datum/controller/subsystem/networks/proc/_hard_create_network(list/network_tree)
	var/network_name_part = network_tree[1]
	var/network_id = network_name_part
	var/datum/ntnet/parent = root_networks[network_name_part]
	var/datum/ntnet/network
	if(!parent) // we have no network root?  Must be mapload of a ruin or such
		parent = new(network_name_part)

	// go up the branches, creating nodes
	for(var/i in 2 to network_tree.len)
		network_name_part = network_tree[i]
		network = parent.children[network_name_part]
		network_id += "." + network_name_part
		if(!network)
			network = new(network_id, network_name_part, parent)

		parent = network
	return network

/**
 * Creates or finds a network anywhere in the world using a fully qualified name
 *
 * This is the simple case finding of a network in the world.  It must take a full
 * qualified network name and it will either return an existing network or build
 * a new one from scratch.  We must be able to create names on the fly as there is
 * no way for the map loader to tell us ahead of time what networks to create or
 * use for any maps or templates.  So this thing will throw silent mapping errors
 * and log them, but will always return a network for something.
 *
 * Arguments:
 * * network_id - text, Fully qualified network name
 */
/datum/controller/subsystem/networks/proc/create_network_simple(network_id)

	var/datum/ntnet/network = networks[network_id]
	if(network!=null)
		return network // don't worry about it

	/// Checks to make sure the network is valid.  We log BOTH to mapping and telecoms
	/// so if your checking for network errors you can find it in mapping to (because its their fault!)
	if(!verify_network_name(network_id))
		log_mapping("create_network_simple: [network_id] IS INVALID, replacing with LIMBO")
		log_telecomms("create_network_simple: [network_id] IS INVALID, replacing with LIMBO")
		return networks[LIMBO_NETWORK_ROOT]

	var/list/network_tree = network_string_to_list(network_id)
	if(!network_tree || network_tree.len == 0)
		log_mapping("create_network_simple: [network_id] IS INVALID, replacing with LIMBO")
		log_telecomms("create_network_simple: [network_id] IS INVALID, replacing with LIMBO")
		return networks[LIMBO_NETWORK_ROOT]

	network = _hard_create_network(network_tree)
#ifdef DEBUG_NETWORKS
	if(!network)
		CRASH("NETWORK CANNOT BE NULL")
#endif
	log_telecomms("create_network_simple:  created final [network.network_id]")
	return network // and we are done!

/**
 * Creates or finds a network anywhere in the world using bits of text
 *
 * This works the same as create_network_simple however it allows the addition
 * of qualified network names.  So you can call it with a root_id and a sub
 * network.  However this function WILL return null if it cannot be created
 * so it should be used with error checking is involved.
 *
 * ex. create_network("ROOT_NETWORK", "ATMOS.SCRUBBERS") -> ROOT_NETWORK.ATMOS.SCRUBBERS
 *
 * Arguments:
 * * tree - List of string
 */
/datum/controller/subsystem/networks/proc/create_network(...)
	var/list/network_tree = list()

	for(var/i in 1 to args.len)
		var/part = args[i]
#ifdef DEBUG_NETWORKS
		if(!part || !istext(part))
			/// stack trace here because this is a bad error
			stack_trace("create_network: We only take text on [part] index [i]")
			return null
#endif
		network_tree += network_string_to_list(part)

	var/datum/ntnet/network =  _hard_create_network(network_tree)
	log_telecomms("create_network:  created final [network.network_id]")
	return network

/**
 * Generate a hardware id for devices.
 *
 * Creates a 32 bit hardware id for network devices.  This is random so masking
 * the number won't make routing "easier" (Think Ethernet)  It does check if
 * an existing device has the number but will NOT assign it as thats
 * up to the collar
 *
 * Returns (string) The generated name
 */
/datum/controller/subsystem/networks/proc/get_next_HID()
	do
		var/string = md5("[num2text(rand(HID_RESTRICTED_END, 999999999), 12)]")
		if(!string)
			log_runtime("Could not generagea m5 hash from address, problem with md5?")
			return //errored
		. = "[copytext_char(string, 1, 9)]" //16 ^ 8 possibilities I think.
	while(interfaces_by_hardware_id[.])

/**
 * Generate a name devices
 *
 * Creates a randomly generated tag or name for devices or anything really
 * it keeps track of a special list that makes sure no name is used more than
 * once
 *
 * args:
 * * len (int)(Optional) Default=5 The length of the name
 * * prefix (string)(Optional) static text in front of the random name
 * * postfix (string)(Optional) static text in back of the random name
 * Returns (string) The generated name
 */
/datum/controller/subsystem/networks/proc/assign_random_name(len=5, prefix="", postfix="")
	var/static/valid_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
	var/list/new_name = list()
	var/text
	// machine id's should be fun random chars hinting at a larger world
	do
		new_name.Cut()
		new_name += prefix
		for(var/i = 1 to len)
			new_name += valid_chars[rand(1,length(valid_chars))]
		new_name += postfix
		text = new_name.Join()
	while(used_names[text])
	used_names[text] = TRUE
	return text


code:
PROCESSING_SUBSYSTEM_DEF(obj)
	name = "Objects"
	priority = FIRE_PRIORITY_OBJ
	flags = SS_NO_INIT
	wait = 2 SECONDS

repo_name: AnalWerewolf/sundown

license: mit


code:
//Used to process objects.

SUBSYSTEM_DEF(processing)
	name = "Processing"
	priority = FIRE_PRIORITY_PROCESS
	flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT
	wait = 1 SECONDS

	var/stat_tag = "P" //Used for logging
	var/list/processing = list()
	var/list/currentrun = list()

/datum/controller/subsystem/processing/stat_entry(msg)
	msg = "[stat_tag]:[length(processing)]"
	return ..()

/datum/controller/subsystem/processing/fire(resumed = FALSE)
	if (!resumed)
		currentrun = processing.Copy()
	//cache for sanic speed (lists are references anyways)
	var/list/current_run = currentrun

	while(current_run.len)
		var/datum/thing = current_run[current_run.len]
		current_run.len--
		if(QDELETED(thing))
			processing -= thing
		else if(thing.process(wait * 0.1) == PROCESS_KILL)
			// fully stop so that a future START_PROCESSING will work
			STOP_PROCESSING(src, thing)
		if (MC_TICK_CHECK)
			return

/**
 * This proc is called on a datum on every "cycle" if it is being processed by a subsystem. The time between each cycle is determined by the subsystem's "wait" setting.
 * You can start and stop processing a datum using the START_PROCESSING and STOP_PROCESSING defines.
 *
 * Since the wait setting of a subsystem can be changed at any time, it is important that any rate-of-change that you implement in this proc is multiplied by the delta_time that is sent as a parameter,
 * Additionally, any "prob" you use in this proc should instead use the DT_PROB define to make sure that the final probability per second stays the same even if the subsystem's wait is altered.
 * Examples where this must be considered:
 * - Implementing a cooldown timer, use `mytimer -= delta_time`, not `mytimer -= 1`. This way, `mytimer` will always have the unit of seconds
 * - Damaging a mob, do `L.adjustFireLoss(20 * delta_time)`, not `L.adjustFireLoss(20)`. This way, the damage per second stays constant even if the wait of the subsystem is changed
 * - Probability of something happening, do `if(DT_PROB(25, delta_time))`, not `if(prob(25))`. This way, if the subsystem wait is e.g. lowered, there won't be a higher chance of this event happening per second
 *
 * If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list
 */
/datum/proc/process(delta_time)
	set waitfor = FALSE
	return PROCESS_KILL


code:
PROCESSING_SUBSYSTEM_DEF(projectiles)
	name = "Projectiles"
	wait = 1
	stat_tag = "PP"
	flags = SS_NO_INIT|SS_TICKER
	var/global_max_tick_moves = 10
	var/global_pixel_speed = 2
	var/global_iterations_per_move = 16

/datum/controller/subsystem/processing/projectiles/proc/set_pixel_speed(new_speed)
	global_pixel_speed = new_speed
	for(var/i in processing)
		var/obj/projectile/P = i
		if(istype(P)) //there's non projectiles on this too.
			P.set_pixel_speed(new_speed)

/datum/controller/subsystem/processing/projectiles/vv_edit_var(var_name, var_value)
	switch(var_name)
		if(NAMEOF(src, global_pixel_speed))
			set_pixel_speed(var_value)
			return TRUE
		else
			return ..()


code:
#define EXP_ASSIGN_WAYFINDER 1200
#define RANDOM_QUIRK_BONUS 3
#define MINIMUM_RANDOM_QUIRKS 3
//Used to process and handle roundstart quirks
// - Quirk strings are used for faster checking in code
// - Quirk datums are stored and hold different effects, as well as being a vector for applying trait string
PROCESSING_SUBSYSTEM_DEF(quirks)
	name = "Quirks"
	init_order = INIT_ORDER_QUIRKS
	flags = SS_BACKGROUND
	runlevels = RUNLEVEL_GAME
	wait = 1 SECONDS

	var/list/quirks = list() //Assoc. list of all roundstart quirk datum types; "name" = /path/
	var/list/quirk_points = list() //Assoc. list of quirk names and their "point cost"; positive numbers are good traits, and negative ones are bad
	///An assoc list of quirks that can be obtained as a hardcore character, and their hardcore value.
	var/list/hardcore_quirks = list()

	/// A list of quirks that can not be used with each other. Format: list(quirk1,quirk2),list(quirk3,quirk4)
	var/static/list/quirk_blacklist = list(
		list("Blind","Nearsighted"),
		list("Jolly","Depression","Apathetic","Hypersensitive"),
		list("Ageusia","Vegetarian","Deviant Tastes"),
		list("Ananas Affinity","Ananas Aversion"),
		list("Alcohol Tolerance","Light Drinker"),
		list("Clown Enjoyer","Mime Fan"),
		list("Bad Touch", "Friendly"),
		list("Extrovert", "Introvert"),
	)

/datum/controller/subsystem/processing/quirks/Initialize(timeofday)
	get_quirks()
	return ..()

/// Returns the list of possible quirks
/datum/controller/subsystem/processing/quirks/proc/get_quirks()
	RETURN_TYPE(/list)
	if (!quirks.len)
		SetupQuirks()

	return quirks

/datum/controller/subsystem/processing/quirks/proc/SetupQuirks()
	// Sort by Positive, Negative, Neutral; and then by name
	var/list/quirk_list = sort_list(subtypesof(/datum/quirk), /proc/cmp_quirk_asc)

	for(var/type in quirk_list)
		var/datum/quirk/quirk_type = type

		if(initial(quirk_type.abstract_parent_type) == type)
			continue

		quirks[initial(quirk_type.name)] = quirk_type
		quirk_points[initial(quirk_type.name)] = initial(quirk_type.value)

		var/hardcore_value = initial(quirk_type.hardcore_value)

		if(!hardcore_value)
			continue
		hardcore_quirks[quirk_type] += hardcore_value

/datum/controller/subsystem/processing/quirks/proc/AssignQuirks(mob/living/user, client/cli)
	var/badquirk = FALSE
	for(var/quirk_name in cli.prefs.all_quirks)
		var/datum/quirk/Q = quirks[quirk_name]
		if(Q)
			if(user.add_quirk(Q))
				SSblackbox.record_feedback("nested tally", "quirks_taken", 1, list("[quirk_name]"))
		else
			stack_trace("Invalid quirk \"[quirk_name]\" in client [cli.ckey] preferences")
			cli.prefs.all_quirks -= quirk_name
			badquirk = TRUE
	if(badquirk)
		cli.prefs.save_character()

	/* SEPTIC EDIT REMOVAL
	// Assign wayfinding pinpointer granting quirk if they're new
	if(cli.get_exp_living(TRUE) < EXP_ASSIGN_WAYFINDER && !user.has_quirk(/datum/quirk/item_quirk/needswayfinder))
		var/datum/quirk/wayfinder = /datum/quirk/item_quirk/needswayfinder
		if(user.add_quirk(wayfinder))
			SSblackbox.record_feedback("nested tally", "quirks_taken", 1, list(initial(wayfinder.name)))

	*/
/*
 *Randomises the quirks for a specified mob
 */
/datum/controller/subsystem/processing/quirks/proc/randomise_quirks(mob/living/user)
	var/bonus_quirks = max((length(user.quirks) + rand(-RANDOM_QUIRK_BONUS, RANDOM_QUIRK_BONUS)), MINIMUM_RANDOM_QUIRKS)
	var/added_quirk_count = 0 //How many we've added
	var/list/quirks_to_add = list() //Quirks we're adding
	var/good_count = 0 //Maximum of 6 good perks
	var/score //What point score we're at
	///Cached list of possible quirks
	var/list/possible_quirks = quirks.Copy()
	//Create a random list of stuff to start with
	while(bonus_quirks > added_quirk_count)
		var/quirk = pick(possible_quirks) //quirk is a string
		if(quirk in quirk_blacklist) //prevent blacklisted
			possible_quirks -= quirk
			continue
		if(quirk_points[quirk] > 0)
			good_count++
		score += quirk_points[quirk]
		quirks_to_add += quirk
		possible_quirks -= quirk
		added_quirk_count++

	//But lets make sure we're balanced
	while(score > 0)
		if(!length(possible_quirks))//Lets not get stuck
			break
		var/quirk = pick(quirks)
		if(quirk in quirk_blacklist) //prevent blacklisted
			possible_quirks -= quirk
			continue
		if(!quirk_points[quirk] < 0)//negative only
			possible_quirks -= quirk
			continue
		good_count++
		score += quirk_points[quirk]
		quirks_to_add += quirk

	//And have benefits too
	while(score < 0 && good_count <= MAX_QUIRKS)
		if(!length(possible_quirks))//Lets not get stuck
			break
		var/quirk = pick(quirks)
		if(quirk in quirk_blacklist) //prevent blacklisted
			possible_quirks -= quirk
			continue
		if(!quirk_points[quirk] > 0) //positive only
			possible_quirks -= quirk
			continue
		good_count++
		score += quirk_points[quirk]
		quirks_to_add += quirk

	for(var/datum/quirk/quirk as anything in user.quirks)
		if(quirk.name in quirks_to_add) //Don't delete ones we keep
			quirks_to_add -= quirk.name //Already there, no need to add.
			continue
		user.remove_quirk(quirk.type) //these quirks are objects

	for(var/datum/quirk/quirk as anything in quirks_to_add)
		user.add_quirk(quirks[quirk]) //these are typepaths converted from string

/// Takes a list of quirk names and returns a new list of quirks that would
/// be valid.
/// If no changes need to be made, will return the same list.
/// Expects all quirk names to be unique, but makes no other expectations.
/datum/controller/subsystem/processing/quirks/proc/filter_invalid_quirks(list/quirks)
	var/list/new_quirks = list()
	var/list/positive_quirks = list()
	var/balance = 0

	var/list/all_quirks = get_quirks()

	for (var/quirk_name in quirks)
		var/datum/quirk/quirk = all_quirks[quirk_name]
		if (isnull(quirk))
			continue

		if (initial(quirk.mood_quirk) && CONFIG_GET(flag/disable_human_mood))
			continue

		var/blacklisted = FALSE

		for (var/list/blacklist as anything in quirk_blacklist)
			if (!(quirk in blacklist))
				continue

			for (var/other_quirk in blacklist)
				if (other_quirk in new_quirks)
					blacklisted = TRUE
					break

			if (blacklisted)
				break

		if (blacklisted)
			continue

		var/value = initial(quirk.value)
		if (value > 0)
			if (positive_quirks.len == MAX_QUIRKS)
				continue

			positive_quirks[quirk_name] = value

		balance += value
		new_quirks += quirk_name

	if (balance > 0)
		var/balance_left_to_remove = balance

		for (var/positive_quirk in positive_quirks)
			var/value = positive_quirks[positive_quirk]
			balance_left_to_remove -= value
			new_quirks -= positive_quirk

			if (balance_left_to_remove <= 0)
				break

	// It is guaranteed that if no quirks are invalid, you can simply check through `==`
	if (new_quirks.len == quirks.len)
		return quirks

	return new_quirks

#undef RANDOM_QUIRK_BONUS
#undef MINIMUM_RANDOM_QUIRKS


code:
//Used for active reactions in reagents/equilibrium datums

PROCESSING_SUBSYSTEM_DEF(reagents)
	name = "Reagents"
	init_order = INIT_ORDER_REAGENTS
	priority = FIRE_PRIORITY_REAGENTS
	wait = 0.25 SECONDS //You might think that rate_up_lim has to be set to half, but since everything is normalised around delta_time, it automatically adjusts it to be per second. Magic!
	flags = SS_KEEP_TIMING
	runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
	///What time was it when we last ticked
	var/previous_world_time = 0

/datum/controller/subsystem/processing/reagents/Initialize()
	. = ..()
	//So our first step isn't insane
	previous_world_time = world.time
	///Blacklists these reagents from being added to the master list. the exact type only. Children are not blacklisted.
	GLOB.fake_reagent_blacklist = list(/datum/reagent/medicine/c2, /datum/reagent/medicine, /datum/reagent/reaction_agent)
	//Build GLOB lists - see holder.dm
	build_chemical_reactions_lists()
	return

/datum/controller/subsystem/processing/reagents/fire(resumed = FALSE)
	if (!resumed)
		currentrun = processing.Copy()
	//cache for sanic speed (lists are references anyways)
	var/list/current_run = currentrun

	//Attempt to realtime reactions in a way that doesn't make them overtly dangerous
	var/delta_realtime = (world.time - previous_world_time)/10 //normalise to s from ds
	previous_world_time = world.time

	while(current_run.len)
		var/datum/thing = current_run[current_run.len]
		current_run.len--
		if(QDELETED(thing))
			stack_trace("Found qdeleted thing in [type], in the current_run list.")
			processing -= thing
		else if(thing.process(delta_realtime) == PROCESS_KILL) //we are realtime
			// fully stop so that a future START_PROCESSING will work
			STOP_PROCESSING(src, thing)
		if (MC_TICK_CHECK)
			return


code:
PROCESSING_SUBSYSTEM_DEF(station)
	name = "Station"
	init_order = INIT_ORDER_STATION
	flags = SS_BACKGROUND
	runlevels = RUNLEVEL_GAME
	wait = 5 SECONDS

	///A list of currently active station traits
	var/list/station_traits = list()
	///Assoc list of trait type || assoc list of traits with weighted value. Used for picking traits from a specific category.
	var/list/selectable_traits_by_types = list(STATION_TRAIT_POSITIVE = list(), STATION_TRAIT_NEUTRAL = list(), STATION_TRAIT_NEGATIVE = list())
	///Currently active announcer. Starts as a type but gets initialized after traits are selected
	var/datum/centcom_announcer/announcer = /datum/centcom_announcer/default

/datum/controller/subsystem/processing/station/Initialize(timeofday)

	//If doing unit tests we don't do none of that trait shit ya know?
	#ifndef UNIT_TESTS
	SetupTraits()
	#endif

	announcer = new announcer() //Initialize the station's announcer datum

	return ..()

///Rolls for the amount of traits and adds them to the traits list
/datum/controller/subsystem/processing/station/proc/SetupTraits()
	if (fexists(FUTURE_STATION_TRAITS_FILE))
		var/forced_traits_contents = file2text(FUTURE_STATION_TRAITS_FILE)
		fdel(FUTURE_STATION_TRAITS_FILE)

		var/list/forced_traits_text_paths = json_decode(forced_traits_contents)
		forced_traits_text_paths = SANITIZE_LIST(forced_traits_text_paths)

		for (var/trait_text_path in forced_traits_text_paths)
			var/station_trait_path = text2path(trait_text_path)
			if (!ispath(station_trait_path, /datum/station_trait) || station_trait_path == /datum/station_trait)
				var/message = "Invalid station trait path [station_trait_path] was requested in the future station traits!"
				log_game(message)
				message_admins(message)
				continue

			setup_trait(station_trait_path)

		return

	for(var/i in subtypesof(/datum/station_trait))
		var/datum/station_trait/trait_typepath = i

		// If forced, (probably debugging), just set it up now, keep it out of the pool.
		if(initial(trait_typepath.force))
			setup_trait(trait_typepath)
			continue

		if(initial(trait_typepath.trait_flags) & STATION_TRAIT_ABSTRACT)
			continue //Dont add abstract ones to it
		selectable_traits_by_types[initial(trait_typepath.trait_type)][trait_typepath] = initial(trait_typepath.weight)

	var/positive_trait_count = pick(20;0, 5;1, 1;2)
	var/neutral_trait_count = pick(10;0, 10;1, 3;2)
	var/negative_trait_count = pick(20;0, 5;1, 1;2)

	pick_traits(STATION_TRAIT_POSITIVE, positive_trait_count)
	pick_traits(STATION_TRAIT_NEUTRAL, neutral_trait_count)
	pick_traits(STATION_TRAIT_NEGATIVE, negative_trait_count)

///Picks traits of a specific category (e.g. bad or good) and a specified amount, then initializes them and adds them to the list of traits.
/datum/controller/subsystem/processing/station/proc/pick_traits(trait_sign, amount)
	if(!amount)
		return
	for(var/iterator in 1 to amount)
		var/datum/station_trait/trait_type = pick_weight(selectable_traits_by_types[trait_sign]) //Rolls from the table for the specific trait type
		setup_trait(trait_type)

///Creates a given trait of a specific type, while also removing any blacklisted ones from the future pool.
/datum/controller/subsystem/processing/station/proc/setup_trait(datum/station_trait/trait_type)
	var/datum/station_trait/trait_instance = new trait_type()
	station_traits += trait_instance
	if(!trait_instance.blacklist)
		return
	for(var/i in trait_instance.blacklist)
		var/datum/station_trait/trait_to_remove = i
		selectable_traits_by_types[initial(trait_to_remove.trait_type)] -= trait_to_remove


code:
PROCESSING_SUBSYSTEM_DEF(tramprocess)
	name = "Tram Process"
	wait = 1
	/// only used on maps with trams, so only enabled by such.
	can_fire = FALSE


code:
PROCESSING_SUBSYSTEM_DEF(wet_floors)
	name = "Wet floors"
	priority = FIRE_PRIORITY_WET_FLOORS
	wait = 10
	stat_tag = "WFP" //Used for logging
	var/temperature_coeff = 2
	var/time_ratio = 1.5 SECONDS


code:
#define PROFILER_FILENAME "profiler.json"
#define SENDMAPS_FILENAME "sendmaps.json"
GLOBAL_REAL_VAR(world_init_maptick_profiler) = world.Profile(PROFILE_RESTART, type = "sendmaps")

SUBSYSTEM_DEF(profiler)
	name = "Profiler"
	init_order = INIT_ORDER_PROFILER
	runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
	wait = 3000
	var/fetch_cost = 0
	var/write_cost = 0

/datum/controller/subsystem/profiler/stat_entry(msg)
	msg += "F:[round(fetch_cost,1)]ms"
	msg += "|W:[round(write_cost,1)]ms"
	return msg

/datum/controller/subsystem/profiler/Initialize()
	if(CONFIG_GET(flag/auto_profile))
		StartProfiling()
	else
		StopProfiling() //Stop the early start profiler
	return ..()

/datum/controller/subsystem/profiler/fire()
	if(CONFIG_GET(flag/auto_profile))
		DumpFile()

/datum/controller/subsystem/profiler/Shutdown()
	if(CONFIG_GET(flag/auto_profile))
		DumpFile()
		world.Profile(PROFILE_CLEAR, type = "sendmaps")
	return ..()

/datum/controller/subsystem/profiler/proc/StartProfiling()
	world.Profile(PROFILE_START)
	world.Profile(PROFILE_START, type = "sendmaps")

/datum/controller/subsystem/profiler/proc/StopProfiling()
	world.Profile(PROFILE_STOP)
	world.Profile(PROFILE_STOP, type = "sendmaps")

/datum/controller/subsystem/profiler/proc/DumpFile()
	var/timer = TICK_USAGE_REAL
	var/current_profile_data = world.Profile(PROFILE_REFRESH, format = "json")
	var/current_sendmaps_data = world.Profile(PROFILE_REFRESH, type = "sendmaps", format="json")
	fetch_cost = MC_AVERAGE(fetch_cost, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))
	CHECK_TICK

	if(!length(current_profile_data)) //Would be nice to have explicit proc to check this
		stack_trace("Warning, profiling stopped manually before dump.")
	var/prof_file = file("[GLOB.log_directory]/[PROFILER_FILENAME]")
	if(fexists(prof_file))
		fdel(prof_file)
	if(!length(current_sendmaps_data)) //Would be nice to have explicit proc to check this
		stack_trace("Warning, sendmaps profiling stopped manually before dump.")
	var/sendmaps_file = file("[GLOB.log_directory]/[SENDMAPS_FILENAME]")
	if(fexists(sendmaps_file))
		fdel(sendmaps_file)

	timer = TICK_USAGE_REAL
	WRITE_FILE(prof_file, current_profile_data)
	WRITE_FILE(sendmaps_file, current_sendmaps_data)
	write_cost = MC_AVERAGE(write_cost, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer))


code:
PROCESSING_SUBSYSTEM_DEF(radiation)
	name = "Radiation"
	flags = SS_NO_INIT | SS_BACKGROUND
	wait = 1 SECONDS

	var/list/warned_atoms = list()

/datum/controller/subsystem/processing/radiation/proc/warn(datum/component/radioactive/contamination)
	if(!contamination || QDELETED(contamination))
		return
	var/ref = REF(contamination.parent)
	if(warned_atoms[ref])
		return
	warned_atoms[ref] = TRUE
	var/atom/master = contamination.parent
	SSblackbox.record_feedback("tally", "contaminated", 1, master.type)
	var/msg = "has become contaminated with enough radiation to contaminate other objects. || Source: [contamination.source] || Strength: [contamination.strength]"
	master.investigate_log(msg, INVESTIGATE_RADIATION)


code:
SUBSYSTEM_DEF(radio)
	name = "Radio"
	flags = SS_NO_FIRE|SS_NO_INIT

	var/list/datum/radio_frequency/frequencies = list()
	var/list/saymodes = list()

/datum/controller/subsystem/radio/PreInit(timeofday)
	for(var/_SM in subtypesof(/datum/saymode))
		var/datum/saymode/SM = new _SM()
		saymodes[SM.key] = SM
	return ..()

/datum/controller/subsystem/radio/proc/add_object(obj/device, new_frequency as num, filter = null as text|null)
	var/f_text = num2text(new_frequency)
	var/datum/radio_frequency/frequency = frequencies[f_text]
	if(!frequency)
		frequencies[f_text] = frequency = new(new_frequency)
	frequency.add_listener(device, filter)
	return frequency

/datum/controller/subsystem/radio/proc/remove_object(obj/device, old_frequency)
	var/f_text = num2text(old_frequency)
	var/datum/radio_frequency/frequency = frequencies[f_text]
	if(frequency)
		frequency.remove_listener(device)
		// let's don't delete frequencies in case a non-listener keeps a reference
	return 1

/datum/controller/subsystem/radio/proc/return_frequency(new_frequency as num)
	var/f_text = num2text(new_frequency)
	var/datum/radio_frequency/frequency = frequencies[f_text]
	if(!frequency)
		frequencies[f_text] = frequency = new(new_frequency)
	return frequency


code:
SUBSYSTEM_DEF(research)
	name = "Research"
	priority = FIRE_PRIORITY_RESEARCH
	wait = 10
	init_order = INIT_ORDER_RESEARCH
	//TECHWEB STATIC
	var/list/techweb_nodes = list() //associative id = node datum
	var/list/techweb_designs = list() //associative id = node datum
	var/list/datum/techweb/techwebs = list()
	var/datum/techweb/science/science_tech
	var/datum/techweb/admin/admin_tech
	var/datum/techweb_node/error_node/error_node //These two are what you get if a node/design is deleted and somehow still stored in a console.
	var/datum/design/error_design/error_design

	//ERROR LOGGING
	var/list/invalid_design_ids = list() //associative id = number of times
	var/list/invalid_node_ids = list() //associative id = number of times
	var/list/invalid_node_boost = list() //associative id = error message

	var/list/obj/machinery/rnd/server/servers = list()

	var/list/techweb_nodes_starting = list() //associative id = TRUE
	var/list/techweb_categories = list() //category name = list(node.id = TRUE)
	var/list/techweb_boost_items = list() //associative double-layer path = list(id = list(point_type = point_discount))
	var/list/techweb_nodes_hidden = list() //Node ids that should be hidden by default.
	var/list/techweb_nodes_experimental = list() //Node ids that are exclusive to the BEPIS.
	var/list/techweb_point_items = list( //path = list(point type = value)
	/obj/item/assembly/signaler/anomaly = list(TECHWEB_POINT_TYPE_GENERIC = 10000)
	)
	var/list/errored_datums = list()
	var/list/point_types = list() //typecache style type = TRUE list
	//----------------------------------------------
	var/list/single_server_income = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_SINGLE_SERVER_INCOME)
	var/last_income
	//^^^^^^^^ ALL OF THESE ARE PER SECOND! ^^^^^^^^

	/// A list of all master servers. If none of these have a source code HDD, research point generation is lowered.
	var/list/obj/machinery/rnd/server/master/master_servers = list()
	/// The multiplier to research points when no source code HDD is present.
	var/no_source_code_income_modifier = 0.5

	//Aiming for 1.5 hours to max R&D
	//[88nodes * 5000points/node] / [1.5hr * 90min/hr * 60s/min]
	//Around 450000 points max???

	/// The global list of raw anomaly types that have been refined, for hard limits.
	var/list/created_anomaly_types = list()
	/// The hard limits of cores created for each anomaly type. For faster code lookup without switch statements.
	var/list/anomaly_hard_limit_by_type = list(
	ANOMALY_CORE_BLUESPACE = MAX_CORES_BLUESPACE,
	ANOMALY_CORE_PYRO = MAX_CORES_PYRO,
	ANOMALY_CORE_GRAVITATIONAL = MAX_CORES_GRAVITATIONAL,
	ANOMALY_CORE_VORTEX = MAX_CORES_VORTEX,
	ANOMALY_CORE_FLUX = MAX_CORES_FLUX
	)

/datum/controller/subsystem/research/Initialize()
	point_types = TECHWEB_POINT_TYPE_LIST_ASSOCIATIVE_NAMES
	initialize_all_techweb_designs()
	initialize_all_techweb_nodes()
	science_tech = new /datum/techweb/science
	admin_tech = new /datum/techweb/admin
	autosort_categories()
	error_design = new
	error_node = new
	return ..()

/datum/controller/subsystem/research/fire()
	var/list/bitcoins = list()
	for(var/obj/machinery/rnd/server/miner as anything in servers)
		if(miner.working)
			bitcoins = single_server_income.Copy()
			break //Just need one to work.

	// Check if any master server has a source code HDD in it or if all master servers have just been plain old blown up.
	// Start by assuming no source code, then set the modifier to 1 if we find one.
	var/bitcoin_multiplier = no_source_code_income_modifier
	for(var/obj/machinery/rnd/server/master/master_server as anything in master_servers)
		if(master_server.source_code_hdd)
			bitcoin_multiplier = 1
			break

	if (!isnull(last_income))
		var/income_time_difference = world.time - last_income
		science_tech.last_bitcoins = bitcoins  // Doesn't take tick drift into account
		for(var/i in bitcoins)
			bitcoins[i] *= (income_time_difference / 10) * bitcoin_multiplier
		science_tech.add_point_list(bitcoins)
	last_income = world.time

/datum/controller/subsystem/research/proc/calculate_server_coefficient() //Diminishing returns.
	var/amt = servers.len
	if(!amt)
		return 0
	var/coeff = 100
	coeff = sqrt(coeff / amt)
	return coeff

/datum/controller/subsystem/research/proc/autosort_categories()
	for(var/i in techweb_nodes)
		var/datum/techweb_node/I = techweb_nodes[i]
		if(techweb_categories[I.category])
			techweb_categories[I.category][I.id] = TRUE
		else
			techweb_categories[I.category] = list(I.id = TRUE)

/datum/controller/subsystem/research/proc/techweb_node_by_id(id)
	return techweb_nodes[id] || error_node

/datum/controller/subsystem/research/proc/techweb_design_by_id(id)
	return techweb_designs[id] || error_design

/datum/controller/subsystem/research/proc/on_design_deletion(datum/design/D)
	for(var/i in techweb_nodes)
		var/datum/techweb_node/TN = techwebs[i]
		TN.on_design_deletion(TN)
	for(var/i in techwebs)
		var/datum/techweb/T = i
		T.recalculate_nodes(TRUE)

/datum/controller/subsystem/research/proc/on_node_deletion(datum/techweb_node/TN)
	for(var/i in techweb_nodes)
		var/datum/techweb_node/TN2 = techwebs[i]
		TN2.on_node_deletion(TN)
	for(var/i in techwebs)
		var/datum/techweb/T = i
		T.recalculate_nodes(TRUE)

/datum/controller/subsystem/research/proc/initialize_all_techweb_nodes(clearall = FALSE)
	if(islist(techweb_nodes) && clearall)
		QDEL_LIST(techweb_nodes)
	if(islist(techweb_nodes_starting && clearall))
		techweb_nodes_starting.Cut()
	var/list/returned = list()
	for(var/path in subtypesof(/datum/techweb_node))
		var/datum/techweb_node/TN = path
		if(isnull(initial(TN.id)))
			continue
		TN = new path
		if(returned[initial(TN.id)])
			stack_trace("WARNING: Techweb node ID clash with ID [initial(TN.id)] detected! Path: [path]")
			errored_datums[TN] = initial(TN.id)
			continue
		returned[initial(TN.id)] = TN
		if(TN.starting_node)
			techweb_nodes_starting[TN.id] = TRUE
	for(var/id in techweb_nodes)
		var/datum/techweb_node/TN = techweb_nodes[id]
		TN.Initialize()
	techweb_nodes = returned
	if (!verify_techweb_nodes()) //Verify all nodes have ids and such.
		stack_trace("Invalid techweb nodes detected")
	calculate_techweb_nodes()
	calculate_techweb_boost_list()
	if (!verify_techweb_nodes()) //Verify nodes and designs have been crosslinked properly.
		CRASH("Invalid techweb nodes detected")

/datum/controller/subsystem/research/proc/initialize_all_techweb_designs(clearall = FALSE)
	if(islist(techweb_designs) && clearall)
		QDEL_LIST(techweb_designs)
	var/list/returned = list()
	for(var/path in subtypesof(/datum/design))
		var/datum/design/DN = path
		if(isnull(initial(DN.id)))
			stack_trace("WARNING: Design with null ID detected. Build path: [initial(DN.build_path)]")
			continue
		else if(initial(DN.id) == DESIGN_ID_IGNORE)
			continue
		DN = new path
		if(returned[initial(DN.id)])
			stack_trace("WARNING: Design ID clash with ID [initial(DN.id)] detected! Path: [path]")
			errored_datums[DN] = initial(DN.id)
			continue
		DN.InitializeMaterials() //Initialize the materials in the design
		returned[initial(DN.id)] = DN
	techweb_designs = returned
	verify_techweb_designs()

/datum/controller/subsystem/research/proc/verify_techweb_nodes()
	. = TRUE
	for(var/n in techweb_nodes)
		var/datum/techweb_node/N = techweb_nodes[n]
		if(!istype(N))
			WARNING("Invalid research node with ID [n] detected and removed.")
			techweb_nodes -= n
			research_node_id_error(n)
			. = FALSE
		for(var/p in N.prereq_ids)
			var/datum/techweb_node/P = techweb_nodes[p]
			if(!istype(P))
				WARNING("Invalid research prerequisite node with ID [p] detected in node [N.display_name]\[[N.id]\] removed.")
				N.prereq_ids  -= p
				research_node_id_error(p)
				. = FALSE
		for(var/d in N.design_ids)
			var/datum/design/D = techweb_designs[d]
			if(!istype(D))
				WARNING("Invalid research design with ID [d] detected in node [N.display_name]\[[N.id]\] removed.")
				N.design_ids -= d
				design_id_error(d)
				. = FALSE
		for(var/u in N.unlock_ids)
			var/datum/techweb_node/U = techweb_nodes[u]
			if(!istype(U))
				WARNING("Invalid research unlock node with ID [u] detected in node [N.display_name]\[[N.id]\] removed.")
				N.unlock_ids -= u
				research_node_id_error(u)
				. = FALSE
		for(var/p in N.boost_item_paths)
			if(!ispath(p))
				N.boost_item_paths -= p
				WARNING("[p] is not a valid path.")
				node_boost_error(N.id, "[p] is not a valid path.")
				. = FALSE
			var/list/points = N.boost_item_paths[p]
			if(islist(points))
				for(var/i in points)
					if(!isnum(points[i]))
						WARNING("[points[i]] is not a valid number.")
						node_boost_error(N.id, "[points[i]] is not a valid number.")
						. = FALSE
					else if(!point_types[i])
						WARNING("[i] is not a valid point type.")
						node_boost_error(N.id, "[i] is not a valid point type.")
						. = FALSE
			else if(!isnull(points))
				N.boost_item_paths -= p
				node_boost_error(N.id, "No valid list.")
				WARNING("No valid list.")
				. = FALSE
		CHECK_TICK

/datum/controller/subsystem/research/proc/verify_techweb_designs()
	for(var/d in techweb_designs)
		var/datum/design/D = techweb_designs[d]
		if(!istype(D))
			stack_trace("WARNING: Invalid research design with ID [d] detected and removed.")
			techweb_designs -= d
		CHECK_TICK

/datum/controller/subsystem/research/proc/research_node_id_error(id)
	if(invalid_node_ids[id])
		invalid_node_ids[id]++
	else
		invalid_node_ids[id] = 1

/datum/controller/subsystem/research/proc/design_id_error(id)
	if(invalid_design_ids[id])
		invalid_design_ids[id]++
	else
		invalid_design_ids[id] = 1

/datum/controller/subsystem/research/proc/calculate_techweb_nodes()
	for(var/design_id in techweb_designs)
		var/datum/design/D = techweb_designs[design_id]
		D.unlocked_by.Cut()
	for(var/node_id in techweb_nodes)
		var/datum/techweb_node/node = techweb_nodes[node_id]
		node.unlock_ids = list()
		for(var/i in node.design_ids)
			var/datum/design/D = techweb_designs[i]
			node.design_ids[i] = TRUE
			D.unlocked_by += node.id
		if(node.hidden)
			techweb_nodes_hidden[node.id] = TRUE
		if(node.experimental)
			techweb_nodes_experimental[node.id] = TRUE
		CHECK_TICK
	generate_techweb_unlock_linking()

/datum/controller/subsystem/research/proc/generate_techweb_unlock_linking()
	for(var/node_id in techweb_nodes) //Clear all unlock links to avoid duplication.
		var/datum/techweb_node/node = techweb_nodes[node_id]
		node.unlock_ids = list()
	for(var/node_id in techweb_nodes)
		var/datum/techweb_node/node = techweb_nodes[node_id]
		for(var/prereq_id in node.prereq_ids)
			var/datum/techweb_node/prereq_node = techweb_node_by_id(prereq_id)
			prereq_node.unlock_ids[node.id] = node

/datum/controller/subsystem/research/proc/calculate_techweb_boost_list(clearall = FALSE)
	if(clearall)
		techweb_boost_items = list()
	for(var/node_id in techweb_nodes)
		var/datum/techweb_node/node = techweb_nodes[node_id]
		for(var/path in node.boost_item_paths)
			if(!ispath(path))
				continue
			if(length(techweb_boost_items[path]))
				techweb_boost_items[path][node.id] = node.boost_item_paths[path]
			else
				techweb_boost_items[path] = list(node.id = node.boost_item_paths[path])
		CHECK_TICK


code:
/*!
This subsystem exists to serve as a holder for important info for the restaurant system for chef and bartender.
*/

SUBSYSTEM_DEF(restaurant)
	name = "Restaurant"
	wait = 20 SECONDS //Roll for new guests but don't do it too fast.
	init_order = INIT_ORDER_RESTAURANT
	flags = SS_NO_FIRE
	///All venues that exist, assoc list of type - reference
	var/list/all_venues = list()
	///All customer data datums that exist, assoc list of type - reference
	var/list/all_customers = list()
	///Caches appearances of food, assoc list where key is the type of food, and value is the appearance. Used so we don't have to keep creating new food. Gets filled whenever a new food that hasn't been ordered gets ordered for the first time.
	var/list/food_appearance_cache = list()

/datum/controller/subsystem/restaurant/Initialize(timeofday)
	. = ..()
	for(var/key in subtypesof(/datum/venue))
		all_venues[key] = new key()
	for(var/key in subtypesof(/datum/customer_data))
		all_customers[key] = new key()


code:
/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth)
#define BUCKET_LEN (world.fps * 1 * 30)
/// Helper for getting the correct bucket for a given chatmessage
#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN)
/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue
#define BUCKET_LIMIT (world.time + TICKS2DS(min(BUCKET_LEN - (SSrunechat.practical_offset - DS2TICKS(world.time - SSrunechat.head_offset)) - 1, BUCKET_LEN - 1)))

/**
 * # Runechat Subsystem
 *
 * Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled
 * after or adapted from the timer subsystem.
 *
 * Note that this has the same structure for storing and queueing messages as the timer subsystem does
 * for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head
 * of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket.
 */
SUBSYSTEM_DEF(runechat)
	name = "Runechat"
	flags = SS_TICKER | SS_NO_INIT
	wait = 1
	priority = FIRE_PRIORITY_RUNECHAT

	/// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
	var/head_offset = 0
	/// Index of the first non-empty bucket
	var/practical_offset = 1
	/// world.tick_lag the bucket was designed for
	var/bucket_resolution = 0
	/// How many messages are in the buckets
	var/bucket_count = 0
	/// List of buckets, each bucket holds every message that has to be killed that byond tick
	var/list/bucket_list = list()
	/// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets
	var/list/datum/chatmessage/second_queue = list()

/datum/controller/subsystem/runechat/PreInit()
	bucket_list.len = BUCKET_LEN
	head_offset = world.time
	bucket_resolution = world.tick_lag

/datum/controller/subsystem/runechat/stat_entry(msg)
	msg = "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]"
	return msg

/datum/controller/subsystem/runechat/fire(resumed = FALSE)
	// Store local references to datum vars as it is faster to access them this way
	var/list/bucket_list = src.bucket_list

	if (MC_TICK_CHECK)
		return

	// Check for when we need to loop the buckets, this occurs when
	// the head_offset is approaching BUCKET_LEN ticks in the past
	if (practical_offset > BUCKET_LEN)
		head_offset += TICKS2DS(BUCKET_LEN)
		practical_offset = 1
		resumed = FALSE

	// Check for when we have to reset buckets, typically from auto-reset
	if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution))
		reset_buckets()
		bucket_list = src.bucket_list
		resumed = FALSE
	// Store a reference to the 'working' chatmessage so that we can resume if the MC
	// has us stop mid-way through processing
	var/static/datum/chatmessage/cm
	if (!resumed)
		cm = null

	// Iterate through each bucket starting from the practical offset
	while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time)
		var/datum/chatmessage/bucket_head = bucket_list[practical_offset]
		if (!cm || !bucket_head || cm == bucket_head)
			bucket_head = bucket_list[practical_offset]
			cm = bucket_head

		while (cm)
			// If the chatmessage hasn't yet had its life ended then do that now
			var/datum/chatmessage/next = cm.next
			if (!cm.eol_complete)
				cm.end_of_life()
			else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion)
				qdel(cm)

			if (MC_TICK_CHECK)
				return

			// Break once we've processed the entire bucket
			cm = next
			if (cm == bucket_head)
				break

		// Empty the bucket, check if anything in the secondary queue should be shifted to this bucket
		bucket_list[practical_offset++] = null
		var/i = 0
		for (i in 1 to length(second_queue))
			cm = second_queue[i]
			if (cm.scheduled_destruction >= BUCKET_LIMIT)
				i--
				break

			// Transfer the message into the bucket, performing necessary circular doubly-linked list operations
			bucket_count++
			var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction))
			var/datum/timedevent/head = bucket_list[bucket_pos]
			if (!head)
				bucket_list[bucket_pos] = cm
				cm.next = null
				cm.prev = null
				continue

			if (!head.prev)
				head.prev = head
			cm.next = head
			cm.prev = head.prev
			cm.next.prev = cm
			cm.prev.next = cm
		if (i)
			second_queue.Cut(1, i + 1)
		cm = null

/datum/controller/subsystem/runechat/Recover()
	bucket_list |= SSrunechat.bucket_list
	second_queue |= SSrunechat.second_queue

/datum/controller/subsystem/runechat/proc/reset_buckets()
	bucket_list.len = BUCKET_LEN
	head_offset = world.time
	bucket_resolution = world.tick_lag

/**
 * Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue
 *
 * This will also account for a chatmessage already being registered, and in which case
 * the position will be updated to remove it from the previous location if necessary
 *
 * Arguments:
 * * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time
 */
/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0)
	// Get local references from subsystem as they are faster to access than the datum references
	var/list/bucket_list = SSrunechat.bucket_list
	var/list/second_queue = SSrunechat.second_queue

	// When necessary, de-list the chatmessage from its previous position
	if (new_sched_destruction)
		if (scheduled_destruction >= BUCKET_LIMIT)
			second_queue -= src
		else
			SSrunechat.bucket_count--
			var/bucket_pos = BUCKET_POS(scheduled_destruction)
			if (bucket_pos > 0)
				var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
				if (bucket_head == src)
					bucket_list[bucket_pos] = next
			if (prev != next)
				prev.next = next
				next.prev = prev
			else
				prev?.next = null
				next?.prev = null
			prev = next = null
		scheduled_destruction = new_sched_destruction

	// Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event
	scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag)

	// Handle insertion into the secondary queue if the required time is outside our tracked amounts
	if (scheduled_destruction >= BUCKET_LIMIT)
		BINARY_INSERT(src, SSrunechat.second_queue, /datum/chatmessage, src, scheduled_destruction, COMPARE_KEY)
		return

	// Get bucket position and a local reference to the datum var, it's faster to access this way
	var/bucket_pos = BUCKET_POS(scheduled_destruction)

	// Get the bucket head for that bucket, increment the bucket count
	var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
	SSrunechat.bucket_count++

	// If there is no existing head of this bucket, we can set this message to be that head
	if (!bucket_head)
		bucket_list[bucket_pos] = src
		return

	// Otherwise it's a simple insertion into the circularly doubly-linked list
	if (!bucket_head.prev)
		bucket_head.prev = bucket_head
	next = bucket_head
	prev = bucket_head.prev
	next.prev = src
	prev.next = src

/**
 * Removes this chatmessage datum from the runechat subsystem
 */
/datum/chatmessage/proc/leave_subsystem()
	// Attempt to find the bucket that contains this chat message
	var/bucket_pos = BUCKET_POS(scheduled_destruction)

	// Get local references to the subsystem's vars, faster than accessing on the datum
	var/list/bucket_list = SSrunechat.bucket_list
	var/list/second_queue = SSrunechat.second_queue

	// Attempt to get the head of the bucket
	var/datum/chatmessage/bucket_head
	if (bucket_pos > 0)
		bucket_head = bucket_list[bucket_pos]

	// Decrement the number of messages in buckets if the message is
	// the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits
	// into an existing bucket, or is otherwise not present in the secondary queue
	if(bucket_head == src)
		bucket_list[bucket_pos] = next
		SSrunechat.bucket_count--
	else if(scheduled_destruction < BUCKET_LIMIT)
		SSrunechat.bucket_count--
	else
		var/l = length(second_queue)
		second_queue -= src
		if(l == length(second_queue))
			SSrunechat.bucket_count--

	// Remove the message from the bucket, ensuring to maintain
	// the integrity of the bucket's list if relevant
	if(prev != next)
		prev.next = next
		next.prev = prev
	else
		prev?.next = null
		next?.prev = null
	prev = next = null

#undef BUCKET_LEN
#undef BUCKET_POS
#undef BUCKET_LIMIT


code:
SUBSYSTEM_DEF(security_level)
	name = "Security Level"
	flags = SS_NO_FIRE
	/// Currently set security level
	var/current_level = SEC_LEVEL_GREEN

/**
 * Sets a new security level as our current level
 *
 * Arguments:
 * * new_level The new security level that will become our current level
 */
/datum/controller/subsystem/security_level/proc/set_level(new_level)
	SSsecurity_level.current_level = new_level
	SEND_SIGNAL(src, COMSIG_SECURITY_LEVEL_CHANGED, new_level)
	SSnightshift.check_nightshift()
	SSblackbox.record_feedback("tally", "security_level_changes", 1, get_security_level())


code:
#define PING_BUFFER_TIME 25

SUBSYSTEM_DEF(server_maint)
	name = "Server Tasks"
	wait = 6
	flags = SS_POST_FIRE_TIMING
	priority = FIRE_PRIORITY_SERVER_MAINT
	init_order = INIT_ORDER_SERVER_MAINT
	runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
	var/list/currentrun
	///Associated list of list names to lists to clear of nulls
	var/list/lists_to_clear
	///Delay between list clearings in ticks
	var/delay = 5
	var/cleanup_ticker = 0

/datum/controller/subsystem/server_maint/PreInit()
	world.hub_password = "" //quickly! before the hubbies see us.

/datum/controller/subsystem/server_maint/Initialize(timeofday)
	if (CONFIG_GET(flag/hub))
		world.update_hub_visibility(TRUE)
	//Keep in mind, because of how delay works adding a list here makes each list take wait * delay more time to clear
	//Do it for stuff that's properly important, and shouldn't have null checks inside its other uses
	lists_to_clear = list(
		"player_list" = GLOB.player_list,
		"mob_list" = GLOB.mob_list,
		"alive_mob_list" = GLOB.alive_mob_list,
		"suicided_mob_list" = GLOB.suicided_mob_list,
		"dead_mob_list" = GLOB.dead_mob_list,
		"keyloop_list" = GLOB.keyloop_list, //A null here will cause new clients to be unable to move. totally unacceptable
	)
	return ..()

/datum/controller/subsystem/server_maint/fire(resumed = FALSE)
	if(!resumed)
		if(list_clear_nulls(GLOB.clients))
			log_world("Found a null in clients list!")
		src.currentrun = GLOB.clients.Copy()

		var/position_in_loop = (cleanup_ticker / delay) + 1	 //Index at 1, thanks byond

		if(!(position_in_loop % 1)) //If it's a whole number
			var/listname = lists_to_clear[position_in_loop]
			if(list_clear_nulls(lists_to_clear[listname]))
				log_world("Found a null in [listname]!")

		cleanup_ticker++

		var/amount_to_work = length(lists_to_clear)
		if(cleanup_ticker >= amount_to_work * delay) //If we've already done a loop, reset
			cleanup_ticker = 0

	var/list/currentrun = src.currentrun
	var/round_started = SSticker.HasRoundStarted()

	var/kick_inactive = CONFIG_GET(flag/kick_inactive)
	var/afk_period
	if(kick_inactive)
		afk_period = CONFIG_GET(number/afk_period)
	for(var/I in currentrun)
		var/client/C = I
		//handle kicking inactive players
		if(round_started && kick_inactive && !C.holder && C.is_afk(afk_period))
			var/cmob = C.mob
			if (!isnewplayer(cmob) || !SSticker.queued_players.Find(cmob))
				log_access("AFK: [key_name(C)]")
				to_chat(C, span_userdanger("You have been inactive for more than [DisplayTimeText(afk_period)] and have been disconnected.
You may reconnect via the button in the file menu or by clicking here to reconnect."))
				QDEL_IN(C, 1) //to ensure they get our message before getting disconnected
				continue

		if (!(!C || world.time - C.connection_time < PING_BUFFER_TIME || C.inactivity >= (wait-1)))
			winset(C, null, "command=.update_ping+[num2text(world.time+world.tick_lag*TICK_USAGE_REAL/100, 32)]")

		if (MC_TICK_CHECK) //one day, when ss13 has 1000 people per server, you guys are gonna be glad I added this tick check
			return

/datum/controller/subsystem/server_maint/Shutdown()
	kick_clients_in_lobby(span_boldannounce("The round came to an end with you in the lobby."), TRUE) //second parameter ensures only afk clients are kicked
	var/server = CONFIG_GET(string/server)
	for(var/thing in GLOB.clients)
		if(!thing)
			continue
		var/client/C = thing
		C?.tgui_panel?.send_roundrestart()
		if(server) //if you set a server location in config.txt, it sends you there instead of trying to reconnect to the same world address. -- NeoFite
			C << link("byond://[server]")
	var/datum/tgs_version/tgsversion = world.TgsVersion()
	if(tgsversion)
		SSblackbox.record_feedback("text", "server_tools", 1, tgsversion.raw_parameter)

/datum/controller/subsystem/server_maint/proc/UpdateHubStatus()
	if(!CONFIG_GET(flag/hub) || !CONFIG_GET(number/max_hub_pop))
		return FALSE //no point, hub / auto hub controls are disabled

	var/max_pop = CONFIG_GET(number/max_hub_pop)

	if(GLOB.clients.len > max_pop)
		world.update_hub_visibility(FALSE)
	else
		world.update_hub_visibility(TRUE)
#undef PING_BUFFER_TIME