code:
/*!Language holders will either exist in an atom/movable or a mind. Creation of language holders happens
automatically when they are needed, for example when something tries to speak.
Where a mind is available, the mind language holder will be the one "in charge". The mind holder
will update its languages based on the atom holder, and will get updated as part of
transformations and other events that cause new languages to become available.

Every language holder has three lists of languages (and sources for each of them):
- understood_languages
- spoken_languages
- blocked_languages

Understood languages let you understand them, spoken languages lets you speak them
(if your tongue is compatible), and blocked languages will let you do neither no matter
what the source of the language is.

Language holders are designed to mostly only ever require the use the helpers in atom/movable
to achieve your goals, but it is also possible to work on them directly if needed. Any adding
and removing of languages and sources should only happen through the procs, as directly changing
these will mess something up somewhere down the line.

All atom movables have the initial_language_holder var which allows you to set the default language
holder to create. For example, /datum/language_holder/alien will give you xenocommon and a block for
galactic common. Human species also have a default language holder var that will be updated on
species change, initial_species_holder.

Key procs
* [grant_language](atom/movable.html#proc/grant_language)
* [remove_language](atom/movable.html#proc/remove_language)
* [add_blocked_language](atom/movable.html#proc/add_blocked_language)
* [remove_blocked_language](atom/movable.html#proc/remove_blocked_language)
* [grant_all_languages](atom/movable.html#proc/grant_all_languages)
* [remove_all_languages](atom/movable.html#proc/remove_all_languages)
* [has_language](atom/movable.html#proc/has_language)
* [can_speak_language](atom/movable.html#proc/can_speak_language)
* [get_selected_language](atom/movable.html#proc/get_selected_language)
* [update_atom_languages](atom/movable.html#proc/update_atom_languages)
*/

/datum/language_holder
	/// Understood languages.
	var/list/understood_languages = list(/datum/language/common = list(LANGUAGE_MIND))
	/// A list of languages that can be spoken. Tongue organ may also set limits beyond this list.
	var/list/spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM))
	/// A list of blocked languages. Used to prevent understanding and speaking certain languages, ie for certain mobs, mutations etc.
	var/list/blocked_languages = list()
	/// If true, overrides tongue limitations.
	var/omnitongue = FALSE
	/// Handles displaying the language menu UI.
	var/datum/language_menu/language_menu
	/// Currently spoken language
	var/selected_language
	/// Tracks the entity that owns the holder.
	var/atom/owner

/// Initializes, and copies in the languages from the current atom if available.
/datum/language_holder/New(atom/_owner)
	if(_owner && QDELETED(_owner))
		CRASH("Langauge holder added to a qdeleting thing, what the fuck \ref[_owner]")
	owner = _owner
	if(istype(owner, /datum/mind))
		var/datum/mind/M = owner
		if(M.current)
			update_atom_languages(M.current)
	get_selected_language()

/datum/language_holder/Destroy()
	QDEL_NULL(language_menu)
	owner = null
	return ..()

/// Grants the supplied language.
/datum/language_holder/proc/grant_language(language, understood = TRUE, spoken = TRUE, source = LANGUAGE_MIND)
	if(understood)
		if(!understood_languages[language])
			understood_languages[language] = list()
		understood_languages[language] |= source
		. = TRUE
	if(spoken)
		if(!spoken_languages[language])
			spoken_languages[language] = list()
		spoken_languages[language] |= source
		. = TRUE

/// Grants every language to understood and spoken, and gives omnitongue.
/datum/language_holder/proc/grant_all_languages(understood = TRUE, spoken = TRUE, grant_omnitongue = TRUE, source = LANGUAGE_MIND)
	for(var/language in GLOB.all_languages)
		grant_language(language, understood, spoken, source)
	if(grant_omnitongue) // Overrides tongue limitations.
		omnitongue = TRUE
	return TRUE

/// Removes a single language or source, removing all sources returns the pre-removal state of the language.
/datum/language_holder/proc/remove_language(language, understood = TRUE, spoken = TRUE, source = LANGUAGE_ALL)
	if(understood && understood_languages[language])
		if(source == LANGUAGE_ALL)
			understood_languages -= language
		else
			understood_languages[language] -= source
			if(!length(understood_languages[language]))
				understood_languages -= language
		. = TRUE

	if(spoken && spoken_languages[language])
		if(source == LANGUAGE_ALL)
			spoken_languages -= language
		else
			spoken_languages[language] -= source
			if(!length(spoken_languages[language]))
				spoken_languages -= language
		. = TRUE

/// Removes every language and optionally sets omnitongue false. If a non default source is supplied, only removes that source.
/datum/language_holder/proc/remove_all_languages(source = LANGUAGE_ALL, remove_omnitongue = FALSE)
	for(var/language in GLOB.all_languages)
		remove_language(language, TRUE, TRUE, source)
	if(remove_omnitongue)
		omnitongue = FALSE
	return TRUE

/// Adds a single language or list of languages to the blocked language list.
/datum/language_holder/proc/add_blocked_language(languages, source = LANGUAGE_MIND)
	if(!islist(languages))
		languages = list(languages)
	for(var/language in languages)
		if(!blocked_languages[language])
			blocked_languages[language] = list()
		blocked_languages[language] |= source
	return TRUE

/// Removes a single language or list of languages from the blocked language list.
/datum/language_holder/proc/remove_blocked_language(languages, source = LANGUAGE_MIND)
	if(!islist(languages))
		languages = list(languages)
	for(var/language in languages)
		if(blocked_languages[language])
			if(source == LANGUAGE_ALL)
				blocked_languages -= language
			else
				blocked_languages[language] -= source
				if(!length(blocked_languages[language]))
					blocked_languages -= language
	return TRUE

/// Checks if you have the language. If spoken is true, only checks if you can speak the language.
/datum/language_holder/proc/has_language(language, spoken = FALSE)
	if(language in blocked_languages)
		return FALSE
	if(spoken)
		return language in spoken_languages
	return language in understood_languages

/// Checks if you can speak the language. Tongue limitations should be supplied as an argument.
/datum/language_holder/proc/can_speak_language(language)
	var/atom/movable/ouratom = get_atom()
	var/tongue = ouratom.could_speak_language(language)
	if((omnitongue || tongue) && has_language(language, TRUE))
		return TRUE
	return FALSE

/// Returns selected language if it can be spoken, or decides, sets and returns a new selected language if possible.
/datum/language_holder/proc/get_selected_language()
	if(selected_language && can_speak_language(selected_language))
		return selected_language
	selected_language = null
	var/highest_priority
	for(var/lang in spoken_languages)
		var/datum/language/language = lang
		var/priority = initial(language.default_priority)
		if((!highest_priority || (priority > highest_priority)) && !(language in blocked_languages))
			if(can_speak_language(language))
				selected_language = language
				highest_priority = priority
	return selected_language

/// Gets a random understood language, useful for hallucinations and such.
/datum/language_holder/proc/get_random_understood_language()
	return pick(understood_languages)

/// Gets a random spoken language, useful for forced speech and such.
/datum/language_holder/proc/get_random_spoken_language()
	return pick(spoken_languages)

/// Opens a language menu reading from the language holder.
/datum/language_holder/proc/open_language_menu(mob/user)
	if(!language_menu)
		language_menu = new (src)
	language_menu.ui_interact(user)

/// Gets the atom, since we some times need to check if the tongue has limitations.
/datum/language_holder/proc/get_atom()
	if(owner)
		if(istype(owner, /datum/mind))
			var/datum/mind/M = owner
			return M.current
		return owner
	return FALSE

/// Empties out the atom specific languages and updates them according to the supplied atoms language holder.
/datum/language_holder/proc/update_atom_languages(atom/movable/thing)
	var/datum/language_holder/from_atom = thing.get_language_holder(FALSE) //Gets the atoms language holder
	if(from_atom == src) //This could happen if called on an atom without a mind.
		return FALSE
	for(var/language in understood_languages)
		remove_language(language, TRUE, FALSE, LANGUAGE_ATOM)
	for(var/language in spoken_languages)
		remove_language(language, FALSE, TRUE, LANGUAGE_ATOM)
	for(var/language in blocked_languages)
		remove_blocked_language(language, LANGUAGE_ATOM)

	copy_languages(from_atom)
	get_selected_language()
	return TRUE

/// Copies all languages from the supplied atom/language holder. Source should be overridden when you
/// do not want the language overwritten by later atom updates or want to avoid blocked languages.
/datum/language_holder/proc/copy_languages(datum/language_holder/from_holder, source_override)
	if(source_override) //No blocked languages here, for now only used by ling absorb.
		for(var/language in from_holder.understood_languages)
			grant_language(language, TRUE, FALSE, source_override)
		for(var/language in from_holder.spoken_languages)
			grant_language(language, FALSE, TRUE, source_override)
	else
		for(var/language in from_holder.understood_languages)
			grant_language(language, TRUE, FALSE, from_holder.understood_languages[language])
		for(var/language in from_holder.spoken_languages)
			grant_language(language, FALSE, TRUE, from_holder.spoken_languages[language])
		for(var/language in from_holder.blocked_languages)
			add_blocked_language(language, from_holder.blocked_languages[language])
	return TRUE

//************************************************
//*        Specific language holders              *
//*      Use atom language sources only.           *
//************************************************/

/datum/language_holder/alien
	understood_languages = list(/datum/language/xenocommon = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/xenocommon = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/construct
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/narsie = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/narsie = list(LANGUAGE_ATOM))

/datum/language_holder/drone
	understood_languages = list(/datum/language/drone = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/drone = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/drone/syndicate
	blocked_languages = list()

/datum/language_holder/jelly
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/slime = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/slime = list(LANGUAGE_ATOM))

/datum/language_holder/lightbringer
	understood_languages = list(/datum/language/slime = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/slime = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/lizard
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/draconic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/draconic = list(LANGUAGE_ATOM))

/datum/language_holder/lizard/ash
	selected_language = /datum/language/draconic

/datum/language_holder/lizard/silver
	understood_languages = list(/datum/language/uncommon = list(LANGUAGE_ATOM),
								/datum/language/draconic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/uncommon = list(LANGUAGE_ATOM),
							/datum/language/draconic = list(LANGUAGE_ATOM))
	selected_language = /datum/language/uncommon

/datum/language_holder/monkey
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/monkey = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/monkey = list(LANGUAGE_ATOM))

/datum/language_holder/mushroom
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/mushroom = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/mushroom = list(LANGUAGE_ATOM))

/datum/language_holder/slime
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/slime = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/slime = list(LANGUAGE_ATOM))

/datum/language_holder/swarmer
	understood_languages = list(/datum/language/swarmer = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/swarmer = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/venus
	understood_languages = list(/datum/language/sylvan = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/sylvan = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/spider
	understood_languages = list(/datum/language/buzzwords = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/buzzwords = list(LANGUAGE_ATOM))
	blocked_languages = list(/datum/language/common = list(LANGUAGE_ATOM))

/datum/language_holder/synthetic
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/uncommon = list(LANGUAGE_ATOM),
								/datum/language/machine = list(LANGUAGE_ATOM),
								/datum/language/draconic = list(LANGUAGE_ATOM),
								/datum/language/moffic = list(LANGUAGE_ATOM),
								/datum/language/calcic = list(LANGUAGE_ATOM),
								/datum/language/voltaic = list(LANGUAGE_ATOM),
								/datum/language/nekomimetic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/uncommon = list(LANGUAGE_ATOM),
							/datum/language/machine = list(LANGUAGE_ATOM),
							/datum/language/draconic = list(LANGUAGE_ATOM),
							/datum/language/moffic = list(LANGUAGE_ATOM),
							/datum/language/calcic = list(LANGUAGE_ATOM),
							/datum/language/voltaic = list(LANGUAGE_ATOM),
							/datum/language/nekomimetic = list(LANGUAGE_ATOM))

/datum/language_holder/moth
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/moffic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/moffic = list(LANGUAGE_ATOM))

/datum/language_holder/skeleton
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/calcic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/calcic = list(LANGUAGE_ATOM))

/datum/language_holder/ethereal
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/voltaic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/voltaic = list(LANGUAGE_ATOM))

/datum/language_holder/golem
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/terrum = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/terrum = list(LANGUAGE_ATOM))

/datum/language_holder/golem/bone
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/terrum = list(LANGUAGE_ATOM),
								/datum/language/calcic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/terrum = list(LANGUAGE_ATOM),
							/datum/language/calcic = list(LANGUAGE_ATOM))

/datum/language_holder/golem/runic
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/terrum = list(LANGUAGE_ATOM),
								/datum/language/narsie = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/terrum = list(LANGUAGE_ATOM),
							/datum/language/narsie = list(LANGUAGE_ATOM))

/datum/language_holder/fly
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/buzzwords = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/buzzwords = list(LANGUAGE_ATOM))

/datum/language_holder/plant
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/sylvan = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/sylvan = list(LANGUAGE_ATOM))

/datum/language_holder/felinid
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/nekomimetic = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/nekomimetic = list(LANGUAGE_ATOM))

/datum/language_holder/shadowpeople
	understood_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
								/datum/language/shadowtongue = list(LANGUAGE_ATOM))
	spoken_languages = list(/datum/language/common = list(LANGUAGE_ATOM),
							/datum/language/shadowtongue = list(LANGUAGE_ATOM))

/datum/language_holder/empty
	understood_languages = list()
	spoken_languages = list()

/datum/language_holder/universal/New()
	..()
	grant_all_languages()


code:
/obj/item/language_manual
	icon = 'icons/obj/library.dmi'
	icon_state = "book2"
	/// Number of charges the book has, limits the number of times it can be used.
	var/charges = 1
	/// Path to a language datum that the book teaches.
	var/datum/language/language = /datum/language/common
	/// Flavour text to display when the language is successfully learned.
	var/flavour_text = "suddenly your mind is filled with codewords and responses"

/obj/item/language_manual/attack_self(mob/living/user)
	if(!isliving(user))
		return

	if(user.has_language(language))
		to_chat(user, span_boldwarning("You start skimming through [src], but you already know [initial(language.name)]."))
		return

	to_chat(user, span_boldannounce("You start skimming through [src], and [flavour_text]."))
	user.grant_language(language, TRUE, TRUE, LANGUAGE_MIND)

	use_charge(user)

/obj/item/language_manual/attack(mob/living/M, mob/living/user)
	if(!istype(M) || !istype(user))
		return
	if(M == user)
		attack_self(user)
		return

	playsound(loc, "punch", 25, TRUE, -1)

	if(M.stat == DEAD)
		M.visible_message(span_danger("[user] smacks [M]'s lifeless corpse with [src]."), span_userdanger("[user] smacks your lifeless corpse with [src]."), span_hear("You hear smacking."))
	else if(M.has_language(language))
		M.visible_message(span_danger("[user] beats [M] over the head with [src]!"), span_userdanger("[user] beats you over the head with [src]!"), span_hear("You hear smacking."))
	else
		M.visible_message(span_notice("[user] teaches [M] by beating [M.p_them()] over the head with [src]!"), span_boldnotice("As [user] hits you with [src], [flavour_text]."), span_hear("You hear smacking."))
		M.grant_language(language, TRUE, TRUE, LANGUAGE_MIND)
		use_charge(user)

/obj/item/language_manual/proc/use_charge(mob/user)
	charges--
	if(!charges)
		var/turf/T = get_turf(src)
		T.visible_message(span_warning("The cover and contents of [src] start shifting and changing!"))

		qdel(src)
		var/obj/item/book/manual/random/book = new(T)
		user.put_in_active_hand(book)

/obj/item/language_manual/codespeak_manual
	name = "codespeak manual"
	desc = "The book's cover reads: \"Codespeak(tm) - Secure your communication with metaphors so elaborate, they seem randomly generated!\""
	language = /datum/language/codespeak
	flavour_text = "suddenly your mind is filled with codewords and responses"

/obj/item/language_manual/codespeak_manual/unlimited
	name = "deluxe codespeak manual"
	charges = INFINITY

/obj/item/language_manual/roundstart_species

/obj/item/language_manual/roundstart_species/Initialize(mapload)
	. = ..()
	language = pick( \
		/datum/language/voltaic, \
		/datum/language/nekomimetic, \
		/datum/language/draconic, \
		/datum/language/moffic, \
		/datum/language/calcic \
	)
	name = "[initial(language.name)] manual"
	desc = "The book's cover reads: \"[initial(language.name)] for Xenos - Learn common galactic tongues in seconds.\""
	flavour_text = "you feel empowered with a mastery over [initial(language.name)]"

/obj/item/language_manual/roundstart_species/unlimited
	charges = INFINITY

/obj/item/language_manual/roundstart_species/unlimited/Initialize(mapload)
	. = ..()
	name = "deluxe [initial(language.name)] manual"

/obj/item/language_manual/roundstart_species/five
	charges = 5

/obj/item/language_manual/roundstart_species/five/Initialize(mapload)
	. = ..()
	name = "extended [initial(language.name)] manual"

// So drones can teach borgs and AI dronespeak. For best effect, combine with mother drone lawset.
/obj/item/language_manual/dronespeak_manual
	name = "dronespeak manual"
	desc = "The book's cover reads: \"Understanding Dronespeak - An exercise in futility.\" The book is written entirely in binary, non-silicons probably won't understand it."
	language = /datum/language/drone
	flavour_text = "suddenly the drone chittering makes sense"
	charges = INFINITY

/obj/item/language_manual/dronespeak_manual/attack(mob/living/M, mob/living/user)
	// If they are not drone or silicon, we don't want them to learn this language.
	if(!(isdrone(M) || issilicon(M)))
		M.visible_message(span_danger("[user] beats [M] over the head with [src]!"), span_userdanger("[user] beats you over the head with [src]!"), span_hear("You hear smacking."))
		return

	return ..()

repo_name: AnalWerewolf/sundown

license: mit


code:
/datum/language_menu
	var/datum/language_holder/language_holder

/datum/language_menu/New(_language_holder)
	language_holder = _language_holder

/datum/language_menu/Destroy()
	language_holder = null
	. = ..()

/datum/language_menu/ui_state(mob/user)
	return GLOB.language_menu_state

/datum/language_menu/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "LanguageMenu")
		ui.open()

/datum/language_menu/ui_data(mob/user)
	var/list/data = list()

	var/atom/movable/AM = language_holder.get_atom()
	if(isliving(AM))
		data["is_living"] = TRUE
	else
		data["is_living"] = FALSE

	data["languages"] = list()
	for(var/lang in GLOB.all_languages)
		var/result = language_holder.has_language(lang) || language_holder.has_language(lang, TRUE)
		if(!result)
			continue
		var/datum/language/language = lang
		var/list/L = list()

		L["name"] = initial(language.name)
		L["desc"] = initial(language.desc)
		L["key"] = initial(language.key)
		L["is_default"] = (language == language_holder.selected_language)
		if(AM)
			L["can_speak"] = AM.can_speak_language(language)
			L["can_understand"] = AM.has_language(language)

		data["languages"] += list(L)

	if(check_rights_for(user.client, R_ADMIN) || isobserver(AM))
		data["admin_mode"] = TRUE
		data["omnitongue"] = language_holder.omnitongue

		data["unknown_languages"] = list()
		for(var/lang in GLOB.all_languages)
			if(language_holder.has_language(lang) || language_holder.has_language(lang, TRUE))
				continue
			var/datum/language/language = lang
			var/list/L = list()

			L["name"] = initial(language.name)
			L["desc"] = initial(language.desc)
			L["key"] = initial(language.key)

			data["unknown_languages"] += list(L)
	return data

/datum/language_menu/ui_act(action, params)
	. = ..()
	if(.)
		return
	var/mob/user = usr
	var/atom/movable/AM = language_holder.get_atom()

	var/language_name = params["language_name"]
	var/datum/language/language_datum
	for(var/lang in GLOB.all_languages)
		var/datum/language/language = lang
		if(language_name == initial(language.name))
			language_datum = language
	var/is_admin = check_rights_for(user.client, R_ADMIN)

	switch(action)
		if("select_default")
			if(language_datum && AM.can_speak_language(language_datum))
				language_holder.selected_language = language_datum
				. = TRUE
		if("grant_language")
			if((is_admin || isobserver(AM)) && language_datum)
				var/list/choices = list("Only Spoken", "Only Understood", "Both")
				var/choice = input(user,"How do you want to add this language?","[language_datum]",null) as null|anything in choices
				var/spoken = FALSE
				var/understood = FALSE
				switch(choice)
					if("Only Spoken")
						spoken = TRUE
					if("Only Understood")
						understood = TRUE
					if("Both")
						spoken = TRUE
						understood = TRUE
				if(language_holder.blocked_languages && language_holder.blocked_languages[language_datum])
					choice = tgui_alert(user, "Do you want to lift the blockage that's also preventing the language to be spoken or understood?", "[language_datum]", list("Yes", "No"))
					if(choice == "Yes")
						language_holder.remove_blocked_language(language_datum, LANGUAGE_ALL)
				language_holder.grant_language(language_datum, understood, spoken)
				if(is_admin)
					message_admins("[key_name_admin(user)] granted the [language_name] language to [key_name_admin(AM)].")
					log_admin("[key_name(user)] granted the language [language_name] to [key_name(AM)].")
				. = TRUE
		if("remove_language")
			if((is_admin || isobserver(AM)) && language_datum)
				var/list/choices = list("Only Spoken", "Only Understood", "Both")
				var/choice = input(user,"Which part do you wish to remove?","[language_datum]",null) as null|anything in choices
				var/spoken = FALSE
				var/understood = FALSE
				switch(choice)
					if("Only Spoken")
						spoken = TRUE
					if("Only Understood")
						understood = TRUE
					if("Both")
						spoken = TRUE
						understood = TRUE
				language_holder.remove_language(language_datum, understood, spoken)
				if(is_admin)
					message_admins("[key_name_admin(user)] removed the [language_name] language to [key_name_admin(AM)].")
					log_admin("[key_name(user)] removed the language [language_name] to [key_name(AM)].")
				. = TRUE
		if("toggle_omnitongue")
			if(is_admin || isobserver(AM))
				language_holder.omnitongue = !language_holder.omnitongue
				if(is_admin)
					message_admins("[key_name_admin(user)] [language_holder.omnitongue ? "enabled" : "disabled"] the ability to speak all languages (that they know) of [key_name_admin(AM)].")
					log_admin("[key_name(user)] [language_holder.omnitongue ? "enabled" : "disabled"] the ability to speak all languages (that_they know) of [key_name(AM)].")
				. = TRUE


code:
/datum/language/machine
	name = "Encoded Audio Language"
	desc = "An efficient language of encoded tones developed by synthetics and cyborgs."
	spans = list(SPAN_ROBOT)
	key = "6"
	flags = NO_STUTTER
	syllables = list("beep","beep","beep","beep","beep","boop","boop","boop","bop","bop","dee","dee","doo","doo","hiss","hss","buzz","buzz","bzz","ksssh","keey","wurr","wahh","tzzz")
	space_chance = 10
	default_priority = 90

	icon_state = "eal"

/datum/language/machine/get_random_name()
	if(prob(70))
		return "[pick(GLOB.posibrain_names)]-[rand(100, 999)]"
	return pick(GLOB.ai_names)


code:
/datum/language/moffic
	name = "Moffic"
	desc = "The language of the Mothpeople borders on complete unintelligibility."
	key = "m"
	space_chance = 10
	syllables = list(
		"år", "i", "går", "sek", "mo", "ff", "ok", "gj", "ø", "gå", "la", "le",
		"lit", "ygg", "van", "dår", "næ", "møt", "idd", "hvo", "ja", "på", "han",
		"så", "ån", "det", "att", "nå", "gö", "bra", "int", "tyc", "om", "när",
		"två", "må", "dag", "sjä", "vii", "vuo", "eil", "tun", "käyt", "teh", "vä",
		"hei", "huo", "suo", "ää", "ten", "ja", "heu", "stu", "uhr", "kön", "we", "hön"
	)
	icon_state = "moth"
	default_priority = 90

// Fuck guest accounts, and fuck language testing.


code:
/datum/language/monkey
	name = "Chimpanzee"
	desc = "Ook ook ook."
	key = "1"
	space_chance = 100
	syllables = list("oop", "aak", "chee", "eek")
	default_priority = 80

	icon_state = "animal"


code:
/datum/language/mushroom
	name = "Mushroom"
	desc = "A language that consists of the sound of periodic gusts of spore-filled air being released."
	key = "y"
	sentence_chance = 0
	default_priority = 80
	syllables = list("poof", "pff", "pFfF", "piff", "puff", "pooof", "pfffff", "piffpiff", "puffpuff", "poofpoof", "pifpafpofpuf")


code:
/datum/language/narsie
	name = "Nar'Sian"
	desc = "The ancient, blood-soaked, impossibly complex language of Nar'Sian cultists."
	key = "n"
	sentence_chance = 8
	space_chance = 95 //very high due to the potential length of each syllable
	var/static/list/base_syllables = list(
		"h", "v", "c", "e", "g", "d", "r", "n", "h", "o", "p",
		"ra", "so", "at", "il", "ta", "gh", "sh", "ya", "te", "sh", "ol", "ma", "om", "ig", "ni", "in",
		"sha", "mir", "sas", "mah", "zar", "tok", "lyr", "nqa", "nap", "olt", "val", "qha",
		"fwe", "ath", "yro", "eth", "gal", "gib", "bar", "jin", "kla", "atu", "kal", "lig",
		"yoka", "drak", "loso", "arta", "weyh", "ines", "toth", "fara", "amar", "nyag", "eske", "reth", "dedo", "btoh", "nikt", "neth",
		"kanas", "garis", "uloft", "tarat", "khari", "thnor", "rekka", "ragga", "rfikk", "harfr", "andid", "ethra", "dedol", "totum",
		"ntrath", "keriam"
	) //the list of syllables we'll combine with itself to get a larger list of syllables
	syllables = list(
		"sha", "mir", "sas", "mah", "hra", "zar", "tok", "lyr", "nqa", "nap", "olt", "val",
		"yam", "qha", "fel", "det", "fwe", "mah", "erl", "ath", "yro", "eth", "gal", "mud",
		"gib", "bar", "tea", "fuu", "jin", "kla", "atu", "kal", "lig",
		"yoka", "drak", "loso", "arta", "weyh", "ines", "toth", "fara", "amar", "nyag", "eske", "reth", "dedo", "btoh", "nikt", "neth", "abis",
		"kanas", "garis", "uloft", "tarat", "khari", "thnor", "rekka", "ragga", "rfikk", "harfr", "andid", "ethra", "dedol", "totum",
		"verbot", "pleggh", "ntrath", "barhah", "pasnar", "keriam", "usinar", "savrae", "amutan", "tannin", "remium", "barada",
		"forbici"
	) //the base syllables, which include a few rare ones that won't appear in the mixed syllables
	icon_state = "narsie"
	default_priority = 10

/datum/language/narsie/New()
	for(var/syllable in base_syllables) //we only do this once, since there's only ever a single one of each language datum.
		for(var/target_syllable in base_syllables)
			if(syllable != target_syllable) //don't combine with yourself
				if(length(syllable) + length(target_syllable) > 8) //if the resulting syllable would be very long, don't put anything between it
					syllables += "[syllable][target_syllable]"
				else if(prob(80)) //we'll be minutely different each round.
					syllables += "[syllable]'[target_syllable]"
				else if(prob(25)) //5% chance of - instead of '
					syllables += "[syllable]-[target_syllable]"
				else //15% chance of no ' or - at all
					syllables += "[syllable][target_syllable]"
	..()


code:
/datum/language/nekomimetic
	name = "Nekomimetic"
	desc = "To the casual observer, this langauge is an incomprehensible mess of broken Japanese. To the felinids, it's somehow comprehensible."
	key = "f"
	space_chance = 70
	syllables = list(
		"neko", "nyan", "mimi", "moe", "mofu", "fuwa", "kyaa", "kawaii", "poka", "munya",
		"puni", "munyu", "ufufu", "uhuhu", "icha", "doki", "kyun", "kusu", "nya", "nyaa",
		"desu", "kis", "ama", "chuu", "baka", "hewo", "boop", "gato", "kit", "sune", "yori",
		"sou", "baka", "chan", "san", "kun", "mahou", "yatta", "suki", "usagi", "domo", "ori",
		"uwa", "zaazaa", "shiku", "puru", "ira", "heto", "etto"
	)
	icon_state = "neko"
	default_priority = 90


code:
/datum/language/piratespeak
	name = "Piratespeak"
	desc = "The language of space pirates."
	key = "p"
	space_chance = 100
	default_priority = 90
	syllables = list(
	"arr","ahoy","rum","aye","blimey","booty","bucko","grog","treasure",
	"me","scallywag","landlubber","poopdeck","ye","avast",
	"shiver","timbers","matey","swashbuckler"
	)
	icon_state = "pirate"


code:
// You n'wah!
// many thanks to https://casualscrolls.fandom.com/wiki/Dunmeri_language, for providing this list of syllables
/datum/language/shadowtongue
	name = "Shadowtongue"
	desc = "What a grand and intoxicating innocence."
	key = "x"
	space_chance = 50
	syllables = list(
		"er", "sint", "en", "et", "nor", "bahr", "sint", "un", "ku'elm", "lakor", "eri",
		"noj", "dashilu", "as", "ot", "lih", "morh", "ghinu", "kin", "sha", "marik", "jibu",
		"sudas", "fut", "kol", "bivi", "pohim", "devohr", "ru", "huirf", "neiris", "sut",
		"devehr", "iru", "gher", "gan", "ujil", "lacor", "bahris", "ghar", "alnef", "wah",
		"khurdhar", "bar", "et", "ilu", "dash", "diru", "noj", "de", "damjulan", "luvahr",
		"telshahr", "tifur", "enhi", "am", "bahr", "nei", "neibahri", "n'chow", "n'wah",
		"s'wit", "b'vehk", "f'lah", "muth", "sera", "sedura", "bal", "dun"
	)
	icon_state = "shadow"
	default_priority = 90


code:
/datum/language/slime
	name = "Slime"
	desc = "A melodic and complex language spoken by slimes. Some of the notes are inaudible to humans."
	key = "k"
	syllables = list("qr","qrr","xuq","qil","quum","xuqm","vol","xrim","zaoo","qu-uu","qix","qoo","zix","*","!")
	default_priority = 70

	icon_state = "slime"


code:
/datum/language/swarmer
	name = "Swarmer"
	desc = "A language only consisting of musical notes."
	spans = list(SPAN_ROBOT)
	key = "s"
	flags = NO_STUTTER
	space_chance = 100
	sentence_chance = 0
	default_priority = 60

	icon_state = "swarmer"

	// since various flats and sharps are the same,
	// all non-accidental notes are doubled in the list
	/* The list with unicode symbols for the accents.
	syllables = list(
					"C", "C",
					"C♯", "D♭",
					"D", "D",
					"D♯", "E♭",
					"E", "E",
					"F", "F",
					"F♯", "G♭",
					"G", "G",
					"G♯", "A♭",
					"A", "A",
					"A♯", "B♭",
					"B", "B")
	*/
	syllables = list(
					"C", "C",
					"C#", "Db",
					"D", "D",
					"D#", "Eb",
					"E", "E",
					"F", "F",
					"F#", "Gb",
					"G", "G",
					"G#", "Ab",
					"A", "A",
					"A#", "Bb",
					"B", "B")


code:
// The language of the podpeople. Yes, it's a shameless ripoff of elvish.
/datum/language/sylvan
	name = "Sylvan"
	desc = "A complicated, ancient language spoken by sentient plants."
	key = "h"
	space_chance = 20
	syllables = list(
		"fii", "sii", "rii", "rel", "maa", "ala", "san", "tol", "tok", "dia", "eres",
		"fal", "tis", "bis", "qel", "aras", "losk", "rasa", "eob", "hil", "tanl", "aere",
		"fer", "bal", "pii", "dala", "ban", "foe", "doa", "cii", "uis", "mel", "wex",
		"incas", "int", "elc", "ent", "aws", "qip", "nas", "vil", "jens", "dila", "fa",
		"la", "re", "do", "ji", "ae", "so", "qe", "ce", "na", "mo", "ha", "yu"
	)
	icon_state = "plant"
	default_priority = 90


code:
/datum/language/terrum
	name = "Terrum"
	desc = "The language of the golems. Sounds similar to old-earth Hebrew."
	key = "g"
	space_chance = 40
	syllables = list(
		"sha", "vu", "nah", "ha", "yom", "ma", "cha", "ar", "et", "mol", "lua",
		"ch", "na", "sh", "ni", "yah", "bes", "ol", "hish", "ev", "la", "ot", "la",
		"khe", "tza", "chak", "hak", "hin", "hok", "lir", "tov", "yef", "yfe",
		"cho", "ar", "kas", "kal", "ra", "lom", "im", "'", "'", "'", "'", "bok",
		"erev", "shlo", "lo", "ta", "im", "yom"
	)
	icon_state = "golem"
	default_priority = 90


code:
/datum/language/uncommon
	name = "Galactic Uncommon"
	desc = "The second-most spoken Human language."
	key = "!"
	flags = TONGUELESS_SPEECH
	space_chance = 50
	syllables = list(
"ba", "be", "bo", "ca", "ce", "co", "da", "de", "do",
"fa", "fe", "fo", "ga", "ge", "go", "ha", "he", "ho",
"ja", "je", "jo", "ka", "ke", "ko", "la", "le", "lo",
"ma", "me", "mo", "na", "ne", "no", "ra", "re", "ro",
"sa", "se", "so", "ta", "te", "to", "va", "ve", "vo",
"xa", "xe", "xo", "ya", "ye", "yo", "za", "ze", "zo"
	)
	icon_state = "galuncom"
	default_priority = 90


code:
// One of these languages will actually work, I'm certain of it.
/datum/language/voltaic
	name = "Voltaic"
	desc = "A sparky language made by manipulating electrical discharge."
	key = "v"
	space_chance = 20
	syllables = list(
		"bzzt", "skrrt", "zzp", "mmm", "hzz", "tk", "shz", "k", "z",
		"bzt", "zzt", "skzt", "skzz", "hmmt", "zrrt", "hzzt", "hz",
		"vzt", "zt", "vz", "zip", "tzp", "lzzt", "dzzt", "zdt", "kzt",
		"zzzz", "mzz"
	)
	icon_state = "volt"
	default_priority = 90


code:
/datum/language/xenocommon
	name = "Xenomorph"
	desc = "The common tongue of the xenomorphs."
	key = "4"
	syllables = list("sss","sSs","SSS")
	default_priority = 50

	icon_state = "xeno"


code:
/obj/item/book/codex_gigas
	name = "\improper Codex Gigas"
	desc = "A book documenting the nature of devils."
	icon_state ="demonomicon"
	lefthand_file = 'icons/mob/inhands/misc/books_lefthand.dmi'
	righthand_file = 'icons/mob/inhands/misc/books_righthand.dmi'
	throw_speed = 1
	throw_range = 10
	resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
	author = "Forces beyond your comprehension"
	unique = 1
	title = "the Codex Gigas"


code:
#define BOOKCASE_UNANCHORED 0
#define BOOKCASE_ANCHORED 1
#define BOOKCASE_FINISHED 2

/* Library Items
 *
 * Contains:
 * Bookcase
 * Book
 * Barcode Scanner
 */

/*
 * Bookcase
 */

/obj/structure/bookcase
	name = "bookcase"
	icon = 'icons/obj/library.dmi'
	icon_state = "bookempty"
	desc = "A great place for storing knowledge."
	anchored = FALSE
	density = TRUE
	opacity = FALSE
	resistance_flags = FLAMMABLE
	max_integrity = 200
	armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, RAD = 0, FIRE = 50, ACID = 0)
	var/state = BOOKCASE_UNANCHORED
	/// When enabled, books_to_load number of random books will be generated for this bookcase when first interacted with.
	var/load_random_books = FALSE
	/// The category of books to pick from when populating random books.
	var/random_category = null
	/// How many random books to generate.
	var/books_to_load = 0

/obj/structure/bookcase/examine(mob/user)
	. = ..()
	if(!anchored)
		. += span_notice("The bolts on the bottom are unsecured.")
	else
		. += span_notice("It's secured in place with bolts.")
	switch(state)
		if(BOOKCASE_UNANCHORED)
			. += span_notice("There's a small crack visible on the back panel.")
		if(BOOKCASE_ANCHORED)
			. += span_notice("There's space inside for a wooden shelf.")
		if(BOOKCASE_FINISHED)
			. += span_notice("There's a small crack visible on the shelf.")

/obj/structure/bookcase/Initialize(mapload)
	. = ..()
	if(!mapload)
		return
	set_anchored(TRUE)
	state = BOOKCASE_FINISHED
	for(var/obj/item/I in loc)
		if(!isbook(I))
			continue
		I.forceMove(src)
	update_appearance()

/obj/structure/bookcase/set_anchored(anchorvalue)
	. = ..()
	if(isnull(.))
		return
	state = anchorvalue
	if(!anchorvalue) //in case we were vareditted or uprooted by a hostile mob, ensure we drop all our books instead of having them disappear till we're rebuild.
		var/atom/Tsec = drop_location()
		for(var/obj/I in contents)
			if(!isbook(I))
				continue
			I.forceMove(Tsec)
	update_appearance()

/obj/structure/bookcase/attackby(obj/item/I, mob/user, params)
	switch(state)
		if(BOOKCASE_UNANCHORED)
			if(I.tool_behaviour == TOOL_WRENCH)
				if(I.use_tool(src, user, 20, volume=50))
					to_chat(user, span_notice("You wrench the frame into place."))
					set_anchored(TRUE)
			else if(I.tool_behaviour == TOOL_CROWBAR)
				if(I.use_tool(src, user, 20, volume=50))
					to_chat(user, span_notice("You pry the frame apart."))
					deconstruct(TRUE)

		if(BOOKCASE_ANCHORED)
			if(istype(I, /obj/item/stack/sheet/mineral/wood))
				var/obj/item/stack/sheet/mineral/wood/W = I
				if(W.get_amount() >= 2)
					W.use(2)
					to_chat(user, span_notice("You add a shelf."))
					state = BOOKCASE_FINISHED
					update_appearance()
			else if(I.tool_behaviour == TOOL_WRENCH)
				I.play_tool_sound(src, 100)
				to_chat(user, span_notice("You unwrench the frame."))
				set_anchored(FALSE)

		if(BOOKCASE_FINISHED)
			var/datum/component/storage/STR = I.GetComponent(/datum/component/storage)
			if(isbook(I))
				if(!user.transferItemToLoc(I, src))
					return
				update_appearance()
			else if(STR)
				for(var/obj/item/T in I.contents)
					if(istype(T, /obj/item/book) || istype(T, /obj/item/spellbook))
						STR.remove_from_storage(T, src)
				to_chat(user, span_notice("You empty \the [I] into \the [src]."))
				update_appearance()
			else if(istype(I, /obj/item/pen))
				if(!user.is_literate())
					to_chat(user, span_notice("You scribble illegibly on the side of [src]!"))
					return
				var/newname = stripped_input(user, "What would you like to title this bookshelf?")
				if(!user.canUseTopic(src, BE_CLOSE))
					return
				if(!newname)
					return
				else
					name = "bookcase ([sanitize(newname)])"
			else if(I.tool_behaviour == TOOL_CROWBAR)
				if(contents.len)
					to_chat(user, span_warning("You need to remove the books first!"))
				else
					I.play_tool_sound(src, 100)
					to_chat(user, span_notice("You pry the shelf out."))
					new /obj/item/stack/sheet/mineral/wood(drop_location(), 2)
					state = BOOKCASE_ANCHORED
					update_appearance()
			else
				return ..()

/obj/structure/bookcase/attack_hand(mob/living/user, list/modifiers)
	. = ..()
	if(.)
		return
	if(!istype(user))
		return
	if(load_random_books)
		create_random_books(books_to_load, src, FALSE, random_category)
		load_random_books = FALSE
	if(contents.len)
		var/obj/item/book/choice = input(user, "Which book would you like to remove from the shelf?") as null|obj in sort_names(contents.Copy())
		if(choice)
			if(!(user.mobility_flags & MOBILITY_USE) || user.stat != CONSCIOUS || HAS_TRAIT(user, TRAIT_HANDS_BLOCKED) || !in_range(loc, user))
				return
			if(ishuman(user))
				if(!user.get_active_held_item())
					user.put_in_hands(choice)
			else
				choice.forceMove(drop_location())
			update_appearance()

/obj/structure/bookcase/deconstruct(disassembled = TRUE)
	var/atom/Tsec = drop_location()
	new /obj/item/stack/sheet/mineral/wood(Tsec, 4)
	for(var/obj/item/I in contents)
		if(!isbook(I))
			continue
		I.forceMove(Tsec)
	return ..()

/obj/structure/bookcase/update_icon_state()
	if(state == BOOKCASE_UNANCHORED || state == BOOKCASE_ANCHORED)
		icon_state = "bookempty"
		return ..()
	var/amount = contents.len
	if(load_random_books)
		amount += books_to_load
	icon_state = "book-[clamp(amount, 0, 5)]"
	return ..()

/obj/structure/bookcase/manuals/engineering
	name = "engineering manuals bookcase"

/obj/structure/bookcase/manuals/engineering/Initialize(mapload)
	. = ..()
	new /obj/item/book/manual/wiki/engineering_construction(src)
	new /obj/item/book/manual/wiki/engineering_hacking(src)
	new /obj/item/book/manual/wiki/engineering_guide(src)
	new /obj/item/book/manual/wiki/robotics_cyborgs(src)
	update_appearance()

/obj/structure/bookcase/manuals/research_and_development
	name = "\improper R&D manuals bookcase"

/obj/structure/bookcase/manuals/research_and_development/Initialize(mapload)
	. = ..()
	new /obj/item/book/manual/wiki/research_and_development(src)
	update_appearance()

/*
 * Book
 */
/obj/item/book
	name = "book"
	icon = 'icons/obj/library.dmi'
	icon_state ="book"
	worn_icon_state = "book"
	desc = "Crack it open, inhale the musk of its pages, and learn something new."
	throw_speed = 1
	throw_range = 5
	w_class = WEIGHT_CLASS_NORMAL  //upped to three because books are, y'know, pretty big. (and you could hide them inside eachother recursively forever)
	attack_verb_continuous = list("bashes", "whacks", "educates")
	attack_verb_simple = list("bash", "whack", "educate")
	resistance_flags = FLAMMABLE
	drop_sound = 'sound/items/handling/book_drop.ogg'
	pickup_sound =  'sound/items/handling/book_pickup.ogg'
	var/dat //Actual page content
	var/due_date = 0 //Game time in 1/10th seconds
	var/author //Who wrote the thing, can be changed by pen or PC. It is not automatically assigned
	var/unique = FALSE //false - Normal book, true - Should not be treated as normal book, unable to be copied, unable to be modified
	var/title //The real name of the book.
	var/window_size = null // Specific window size for the book, i.e: "1920x1080", Size x Width

/obj/item/book/attack_self(mob/user)
	if(!user.can_read(src))
		return
	user.visible_message(span_notice("[user] opens a book titled \"[title]\" and begins reading intently."))
	SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "book_nerd", /datum/mood_event/book_nerd)
	on_read(user)

/obj/item/book/proc/on_read(mob/user)
	if(dat)
		user << browse("<meta charset=UTF-8><TT>Penned by [author].</TT>
" + "[dat]", "window=book[window_size != null ? ";size=[window_size]" : ""]")
		onclose(user, "book")
	else
		to_chat(user, span_notice("This book is completely blank!"))

/obj/item/book/attackby(obj/item/I, mob/user, params)
	if(istype(I, /obj/item/pen))
		if(user.is_blind())
			to_chat(user, span_warning("As you are trying to write on the book, you suddenly feel very stupid!"))
			return
		if(unique)
			to_chat(user, span_warning("These pages don't seem to take the ink well! Looks like you can't modify it."))
			return
		var/literate = user.is_literate()
		if(!literate)
			to_chat(user, span_notice("You scribble illegibly on the cover of [src]!"))
			return
		var/choice = tgui_input_list(usr, "What would you like to change?",,list("Title", "Contents", "Author", "Cancel"))
		if(!user.canUseTopic(src, BE_CLOSE, literate))
			return
		switch(choice)
			if("Title")
				var/newtitle = reject_bad_text(stripped_input(user, "Write a new title:"))
				if(!user.canUseTopic(src, BE_CLOSE, literate))
					return
				if (length_char(newtitle) > 30)
					to_chat(user, span_warning("That title won't fit on the cover!"))
					return
				if(!newtitle)
					to_chat(user, span_warning("That title is invalid."))
					return
				else
					name = newtitle
					title = newtitle
			if("Contents")
				var/content = stripped_input(user, "Write your book's contents (HTML NOT allowed):","","",8192)
				if(!user.canUseTopic(src, BE_CLOSE, literate))
					return
				if(!content)
					to_chat(user, span_warning("The content is invalid."))
					return
				else
					dat += content
			if("Author")
				var/newauthor = stripped_input(user, "Write the author's name:")
				if(!user.canUseTopic(src, BE_CLOSE, literate))
					return
				if(!newauthor)
					to_chat(user, span_warning("The name is invalid."))
					return
				else
					author = newauthor
			else
				return

	else if(istype(I, /obj/item/barcodescanner))
		var/obj/item/barcodescanner/scanner = I
		if(!scanner.computer)
			to_chat(user, span_alert("[I]'s screen flashes: 'No associated computer found!'"))
		else
			switch(scanner.mode)
				if(0)
					scanner.book = src
					to_chat(user, span_notice("[I]'s screen flashes: 'Book stored in buffer.'"))
				if(1)
					scanner.book = src
					scanner.computer.buffer_book = name
					to_chat(user, span_notice("[I]'s screen flashes: 'Book stored in buffer. Book title stored in associated computer buffer.'"))
				if(2)
					scanner.book = src
					for(var/datum/borrowbook/b in scanner.computer.checkouts)
						if(b.bookname == name)
							scanner.computer.checkouts.Remove(b)
							to_chat(user, span_notice("[I]'s screen flashes: 'Book stored in buffer. Book has been checked in.'"))
							return
					to_chat(user, span_notice("[I]'s screen flashes: 'Book stored in buffer. No active check-out record found for current title.'"))
				if(3)
					scanner.book = src
					for(var/obj/item/book in scanner.computer.inventory)
						if(book == src)
							to_chat(user, span_alert("[I]'s screen flashes: 'Book stored in buffer. Title already present in inventory, aborting to avoid duplicate entry.'"))
							return
					scanner.computer.inventory.Add(src)
					to_chat(user, span_notice("[I]'s screen flashes: 'Book stored in buffer. Title added to general inventory.'"))

	else if((istype(I, /obj/item/knife) || I.tool_behaviour == TOOL_WIRECUTTER) && !(flags_1 & HOLOGRAM_1))
		to_chat(user, span_notice("You begin to carve out [title]..."))
		if(do_after(user, 30, target = src))
			to_chat(user, span_notice("You carve out the pages from [title]! You didn't want to read it anyway."))
			var/obj/item/storage/book/B = new
			B.name = src.name
			B.title = src.title
			B.icon_state = src.icon_state
			if(user.is_holding(src))
				qdel(src)
				user.put_in_hands(B)
				return
			else
				B.forceMove(drop_location())
				qdel(src)
				return
		return
	else
		..()

/*
 * Barcode Scanner
 */
/obj/item/barcodescanner
	name = "barcode scanner"
	icon = 'icons/obj/library.dmi'
	icon_state ="scanner"
	desc = "A fabulous tool if you need to scan a barcode."
	throw_speed = 3
	throw_range = 5
	w_class = WEIGHT_CLASS_TINY
	var/obj/machinery/computer/bookmanagement/computer //Associated computer - Modes 1 to 3 use this
	var/obj/item/book/book //Currently scanned book
	var/mode = 0 //0 - Scan only, 1 - Scan and Set Buffer, 2 - Scan and Attempt to Check In, 3 - Scan and Attempt to Add to Inventory

/obj/item/barcodescanner/attack_self(mob/user)
	mode += 1
	if(mode > 3)
		mode = 0
	to_chat(user, "[src] Status Display:")
	var/modedesc
	switch(mode)
		if(0)
			modedesc = "Scan book to local buffer."
		if(1)
			modedesc = "Scan book to local buffer and set associated computer buffer to match."
		if(2)
			modedesc = "Scan book to local buffer, attempt to check in scanned book."
		if(3)
			modedesc = "Scan book to local buffer, attempt to add book to general inventory."
		else
			modedesc = "ERROR"
	to_chat(user, " - Mode [mode] : [modedesc]")
	if(computer)
		to_chat(user, "Computer has been associated with this unit.")
	else
		to_chat(user, "No associated computer found. Only local scans will function properly.")
	to_chat(user, "\n")

#undef BOOKCASE_UNANCHORED
#undef BOOKCASE_ANCHORED
#undef BOOKCASE_FINISHED


code:
/* Library Machines
 *
 * Contains:
 * Borrowbook datum
 * Library Public Computer
 * Cachedbook datum
 * Library Computer
 * Library Scanner
 * Book Binder
 */

/*
 * Library Public Computer
 */
/obj/machinery/computer/libraryconsole
	name = "library visitor console"
	icon_state = "oldcomp"
	icon_screen = "library"
	icon_keyboard = null
	circuit = /obj/item/circuitboard/computer/libraryconsole
	desc = "Checked out books MUST be returned on time."
	var/screenstate = 0
	var/title
	var/category = "Any"
	var/author
	var/search_page = 0
	COOLDOWN_DECLARE(library_visitor_topic_cooldown)

/obj/machinery/computer/libraryconsole/ui_interact(mob/user)
	. = ..()
	var/list/dat = list() // <META HTTP-EQUIV='Refresh' CONTENT='10'>
	switch(screenstate)
		if(0)
			dat += "
Search Settings

"
			dat += "Filter by Title: [title]
"
			dat += "Filter by Category: [category]
"
			dat += "Filter by Author: [author]
"
			dat += "\[Start Search\]
"
		if(1)
			if (!SSdbcore.Connect())
				dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance.
"
			else if(QDELETED(user))
				return
			else
				dat += "
"
				dat += "
AUTHOR	TITLE	CATEGORY	SS13BN
"
				var/SQLsearch = "isnull(deleted) AND "
				if(category == "Any")
					SQLsearch += "author LIKE '%[author]%' AND title LIKE '%[title]%'"
				else
					SQLsearch += "author LIKE '%[author]%' AND title LIKE '%[title]%' AND category='[category]'"
				var/bookcount = 0
				var/booksperpage = 20
				var/datum/db_query/query_library_count_books = SSdbcore.NewQuery({"
					SELECT COUNT(id) FROM [format_table_name("library")]
					WHERE isnull(deleted)
						AND author LIKE CONCAT('%',:author,'%')
						AND title LIKE CONCAT('%',:title,'%')
						AND (:category = 'Any' OR category = :category)
				"}, list("author" = author, "title" = title, "category" = category))
				if(!query_library_count_books.warn_execute())
					qdel(query_library_count_books)
					return
				if(query_library_count_books.NextRow())
					bookcount = text2num(query_library_count_books.item[1])
				qdel(query_library_count_books)
				if(bookcount > booksperpage)
					dat += "Page: "
					var/pagecount = 1
					var/list/pagelist = list()
					while(bookcount > 0)
						pagelist += "[pagecount == search_page + 1 ? "\[[pagecount]\]" : "\[[pagecount]\]"]"
						bookcount -= booksperpage
						pagecount++
					dat += pagelist.Join(" | ")
				search_page = text2num(search_page)
				var/datum/db_query/query_library_list_books = SSdbcore.NewQuery({"
					SELECT author, title, category, id
					FROM [format_table_name("library")]
					WHERE isnull(deleted)
						AND author LIKE CONCAT('%',:author,'%')
						AND title LIKE CONCAT('%',:title,'%')
						AND (:category = 'Any' OR category = :category)
					LIMIT :skip, :take
				"}, list("author" = author, "title" = title, "category" = category, "skip" = booksperpage * search_page, "take" = booksperpage))
				if(!query_library_list_books.Execute())
					dat += "ERROR: Unable to retrieve book listings. Please contact your system administrator for assistance.
"
				else
					while(query_library_list_books.NextRow())
						var/author = query_library_list_books.item[1]
						var/title = query_library_list_books.item[2]
						var/category = query_library_list_books.item[3]
						var/id = query_library_list_books.item[4]
						dat += "
[author]	[title]	[category]	[id]
"
				qdel(query_library_list_books)
				if(QDELETED(user))
					return
				dat += "

"
			dat += "\[Go Back\]
"
	var/datum/browser/popup = new(user, "publiclibrary", name, 600, 400)
	popup.set_content(jointext(dat, ""))
	popup.open()

/obj/machinery/computer/libraryconsole/Topic(href, href_list)
	if(!COOLDOWN_FINISHED(src, library_visitor_topic_cooldown))
		return
	COOLDOWN_START(src, library_visitor_topic_cooldown, 1 SECONDS)
	. = ..()
	if(.)
		usr << browse(null, "window=publiclibrary")
		onclose(usr, "publiclibrary")
		return

	if(href_list["settitle"])
		var/newtitle = input("Enter a title to search for:") as text|null
		if(newtitle)
			title = sanitize(newtitle)
		else
			title = null
	if(href_list["setcategory"])
		var/newcategory = tgui_input_list(usr, "Choose a category to search for:",, list("Any", "Fiction", "Non-Fiction", "Adult", "Reference", "Religion"))
		if(newcategory)
			category = sanitize(newcategory)
		else
			category = "Any"
	if(href_list["setauthor"])
		var/newauthor = input("Enter an author to search for:") as text|null
		if(newauthor)
			author = sanitize(newauthor)
		else
			author = null
	if(href_list["search"])
		screenstate = 1

	if(href_list["bookpagecount"])
		search_page = text2num(href_list["bookpagecount"])

	if(href_list["back"])
		screenstate = 0

	src.add_fingerprint(usr)
	src.updateUsrDialog()
	return

/*
 * Borrowbook datum
 */
/datum/borrowbook // Datum used to keep track of who has borrowed what when and for how long.
	var/bookname
	var/mobname
	var/getdate
	var/duedate

#define PRINTER_COOLDOWN 60

/*
 * Library Computer
 * After 860 days, it's finally a buildable computer.
 */
// TODO: Make this an actual /obj/machinery/computer that can be crafted from circuit boards and such
// It is August 22nd, 2012... This TODO has already been here for months.. I wonder how long it'll last before someone does something about it.
// It's December 25th, 2014, and this is STILL here, and it's STILL relevant. Kill me
/obj/machinery/computer/bookmanagement
	name = "book inventory management console"
	desc = "Librarian's command station."
	verb_say = "beeps"
	verb_ask = "beeps"
	verb_exclaim = "beeps"
	pass_flags = PASSTABLE

	icon_state = "oldcomp"
	icon_screen = "library"
	icon_keyboard = null
	circuit = /obj/item/circuitboard/computer/libraryconsole

	var/screenstate = 0 // 0 - Main Menu, 1 - Inventory, 2 - Checked Out, 3 - Check Out a Book

	var/arcanecheckout = 0
	var/buffer_book
	var/buffer_mob
	var/upload_category = "Fiction"
	var/list/checkouts = list()
	var/list/inventory = list()
	var/checkoutperiod = 5 // In minutes
	var/obj/machinery/libraryscanner/scanner // Book scanner that will be used when uploading books to the Archive
	var/page = 1 //current page of the external archives
	var/printer_cooldown = 0
	COOLDOWN_DECLARE(library_console_topic_cooldown)

/obj/machinery/computer/bookmanagement/Initialize(mapload)
	. = ..()
	if(circuit)
		circuit.name = "Book Inventory Management Console (Machine Board)"
		circuit.build_path = /obj/machinery/computer/bookmanagement

/obj/machinery/computer/bookmanagement/ui_interact(mob/user)
	. = ..()
	var/dat = "" // <META HTTP-EQUIV='Refresh' CONTENT='10'>
	switch(screenstate)
		if(0)
			// Main Menu
			dat += "1. View General Inventory
"
			dat += "2. View Checked Out Inventory
"
			dat += "3. Check out a Book
"
			dat += "4. Connect to External Archive
"
			dat += "5. Upload New Title to Archive
"
			dat += "6. Upload Scanned Title to Newscaster
"
			dat += "7. Print Corporate Materials
"
			if(obj_flags & EMAGGED)
				dat += "8. Access the Forbidden Lore Vault
"
			if(src.arcanecheckout)
				print_forbidden_lore(user)
				src.arcanecheckout = 0
		if(1)
			// Inventory
			dat += "
Inventory

"
			for(var/obj/item/book/b in inventory)
				dat += "[b.name] (Delete)
"
			dat += "(Return to main menu)
"
		if(2)
			// Checked Out
			dat += "
Checked Out Books

"
			for(var/datum/borrowbook/b in checkouts)
				var/timetaken = world.time - b.getdate
				timetaken /= 600
				timetaken = round(timetaken)
				var/timedue = b.duedate - world.time
				timedue /= 600
				if(timedue <= 0)
					timedue = "(OVERDUE) [timedue]"
				else
					timedue = round(timedue)
				dat += "\"[b.bookname]\", Checked out to: [b.mobname]
--- Taken: [timetaken] minutes ago, Due: in [timedue] minutes
"
				dat += "(Check In)

"
			dat += "(Return to main menu)
"
		if(3)
			// Check Out a Book
			dat += "
Check Out a Book

"
			dat += "Book: [src.buffer_book] "
			dat += "\[Edit\]
"
			dat += "Recipient: [src.buffer_mob] "
			dat += "\[Edit\]
"
			dat += "Checkout Date : [world.time/600]
"
			dat += "Due Date: [(world.time + checkoutperiod)/600]
"
			dat += "(Checkout Period: [checkoutperiod] minutes) (+/-)"
			dat += "(Commit Entry)
"
			dat += "(Return to main menu)
"
		if(4)
			dat += "
External Archive
"
			if(!SSdbcore.Connect())
				dat += "ERROR: Unable to contact External Archive. Please contact your system administrator for assistance."
			else
				var/booksperpage = 50
				var/pagecount
				var/datum/db_query/query_library_count_books = SSdbcore.NewQuery("SELECT COUNT(id) FROM [format_table_name("library")] WHERE isnull(deleted)")
				if(!query_library_count_books.Execute())
					qdel(query_library_count_books)
					return
				if(query_library_count_books.NextRow())
					pagecount = CEILING(text2num(query_library_count_books.item[1]) / booksperpage, 1)
				qdel(query_library_count_books)
				var/list/booklist = list()
				var/datum/db_query/query_library_get_books = SSdbcore.NewQuery({"
					SELECT id, author, title, category
					FROM [format_table_name("library")]
					WHERE isnull(deleted)
					LIMIT :skip, :take
				"}, list("skip" = booksperpage * (page - 1), "take" = booksperpage))
				if(!query_library_get_books.Execute())
					qdel(query_library_get_books)
					return
				while(query_library_get_books.NextRow())
					booklist += "
[query_library_get_books.item[2]]	[query_library_get_books.item[3]]	[query_library_get_books.item[4]]	\[Order\]
\n"
				dat += "(Order book by SS13BN)

"
				dat += "
"
				dat += "
AUTHOR	TITLE	CATEGORY
"
				dat += jointext(booklist, "")
				dat += "
<<<<	 	 	 >>>>
"
				dat += "
"
				qdel(query_library_get_books)
			dat += "
(Return to main menu)
"
		if(5)
			dat += "
Upload a New Title
"
			if(!scanner)
				scanner = findscanner(9)
			if(!scanner)
				dat += "No scanner found within wireless network range.
"
			else if(!scanner.cache)
				dat += "No data found in scanner memory.
"
			else
				dat += "<TT>Data marked for upload...</TT>
"
				dat += "<TT>Title: </TT>[scanner.cache.name]
"
				if(!scanner.cache.author)
					scanner.cache.author = "Anonymous"
				dat += "<TT>Author: </TT>[scanner.cache.author]
"
				dat += "<TT>Category: </TT>[upload_category]
"
				dat += "\[Upload\]
"
			dat += "(Return to main menu)
"
		if(6)
			dat += "
Post Title to Newscaster
"
			if(!scanner)
				scanner = findscanner(9)
			if(!scanner)
				dat += "No scanner found within wireless network range.
"
			else if(!scanner.cache)
				dat += "No data found in scanner memory.
"
			else
				dat += "<TT>Post [scanner.cache.name] to station newscasters?</TT>"
				dat += "\[Post\]
"
			dat += "(Return to main menu)
"
		if(7)
			dat += "
NTGanda(tm) Universal Printing Module
"
			dat += "What would you like to print?
"
			dat += "\[Bible\]
"
			dat += "\[Poster\]
"
			dat += "(Return to main menu)
"
		if(8)
			dat += "
Accessing Forbidden Lore Vault v 1.3
"
			dat += "Are you absolutely sure you want to proceed? EldritchRelics Inc. takes no responsibilities for loss of sanity resulting from this action.
"
			dat += "Yes.
"
			dat += "No.
"

	var/datum/browser/popup = new(user, "library", name, 600, 400)
	popup.set_content(dat)
	popup.open()

/obj/machinery/computer/bookmanagement/proc/findscanner(viewrange)
	for(var/obj/machinery/libraryscanner/S in range(viewrange, get_turf(src)))
		return S
	return null

/obj/machinery/computer/bookmanagement/proc/print_forbidden_lore(mob/user)
	new /obj/item/melee/cultblade/dagger(get_turf(src))
	to_chat(user, span_warning("Your sanity barely endures the seconds spent in the vault's browsing window. The only thing to remind you of this when you stop browsing is a sinister dagger sitting on the desk. You don't even remember where it came from..."))
	user.visible_message(span_warning("[user] stares at the blank screen for a few moments, [user.p_their()] expression frozen in fear. When [user.p_they()] finally awaken[user.p_s()] from it, [user.p_they()] look[user.p_s()] a lot older."), 2)

/obj/machinery/computer/bookmanagement/attackby(obj/item/W, mob/user, params)
	if(istype(W, /obj/item/barcodescanner))
		var/obj/item/barcodescanner/scanner = W
		scanner.computer = src
		to_chat(user, span_notice("[scanner]'s associated machine has been set to [src]."))
		audible_message(span_hear("[src] lets out a low, short blip."))
	else
		return ..()

/obj/machinery/computer/bookmanagement/emag_act(mob/user)
	if(density && !(obj_flags & EMAGGED))
		obj_flags |= EMAGGED

/obj/machinery/computer/bookmanagement/Topic(href, href_list)
	if(!COOLDOWN_FINISHED(src, library_console_topic_cooldown))
		return
	COOLDOWN_START(src, library_console_topic_cooldown, 1 SECONDS)
	if(..())
		usr << browse(null, "window=library")
		onclose(usr, "library")
		return
	if(href_list["page"] && screenstate == 4)
		page = text2num(href_list["page"])
	if(href_list["switchscreen"])
		switch(href_list["switchscreen"])
			if("0")
				screenstate = 0
			if("1")
				screenstate = 1
			if("2")
				screenstate = 2
			if("3")
				screenstate = 3
			if("4")
				screenstate = 4
			if("5")
				screenstate = 5
			if("6")
				screenstate = 6
			if("7")
				screenstate = 7
			if("8")
				screenstate = 8
	if(href_list["arccheckout"])
		if(obj_flags & EMAGGED)
			src.arcanecheckout = 1
		src.screenstate = 0
	if(href_list["increasetime"])
		checkoutperiod += 1
	if(href_list["decreasetime"])
		checkoutperiod -= 1
		if(checkoutperiod < 1)
			checkoutperiod = 1
	if(href_list["editbook"])
		buffer_book = stripped_input(usr, "Enter the book's title:", max_length = 45)
	if(href_list["editmob"])
		buffer_mob = stripped_input(usr, "Enter the recipient's name:", max_length = MAX_NAME_LEN)
	if(href_list["checkout"])
		var/datum/borrowbook/b = new /datum/borrowbook
		b.bookname = sanitize(buffer_book)
		b.mobname = sanitize(buffer_mob)
		b.getdate = world.time
		b.duedate = world.time + (checkoutperiod * 600)
		checkouts.Add(b)
	if(href_list["checkin"])
		var/datum/borrowbook/b = locate(href_list["checkin"]) in checkouts
		if(b && istype(b))
			checkouts.Remove(b)
	if(href_list["delbook"])
		var/obj/item/book/b = locate(href_list["delbook"]) in inventory
		if(b && istype(b))
			inventory.Remove(b)
	if(href_list["setauthor"])
		var/newauthor = stripped_input(usr, "Enter the author's name: ", max_length = 45)
		if(newauthor)
			scanner.cache.author = newauthor
	if(href_list["setcategory"])
		var/newcategory = tgui_input_list(usr, "Choose a category: ",, list("Fiction", "Non-Fiction", "Adult", "Reference", "Religion","Technical"))
		if(newcategory)
			upload_category = newcategory
	if(href_list["upload"])
		if(scanner)
			if(scanner.cache)
				var/choice = tgui_alert(usr, "Are you certain you wish to upload this title to the Archive?",, list("Confirm", "Abort"))
				if(choice == "Confirm")
					if (!SSdbcore.Connect())
						tgui_alert(usr,"Connection to Archive has been severed. Aborting.")
					else
						var/msg = "[key_name(usr)] has uploaded the book titled [scanner.cache.name], [length(scanner.cache.dat)] signs"
						var/datum/db_query/query_library_upload = SSdbcore.NewQuery({"
							INSERT INTO [format_table_name("library")] (author, title, content, category, ckey, datetime, round_id_created)
							VALUES (:author, :title, :content, :category, :ckey, Now(), :round_id)
						"}, list("title" = scanner.cache.name, "author" = scanner.cache.author, "content" = scanner.cache.dat, "category" = upload_category, "ckey" = usr.ckey, "round_id" = GLOB.round_id))
						if(!query_library_upload.Execute())
							qdel(query_library_upload)
							tgui_alert(usr,"Database error encountered uploading to Archive")
							return
						else
							log_game(msg)
							qdel(query_library_upload)
							tgui_alert(usr,"Upload Complete. Uploaded title will be unavailable for printing for a short period")
	if(href_list["newspost"])
		if(!GLOB.news_network)
			tgui_alert(usr,"No news network found on station. Aborting.")
		var/channelexists = 0
		for(var/datum/newscaster/feed_channel/FC in GLOB.news_network.network_channels)
			if(FC.channel_name == "Nanotrasen Book Club")
				channelexists = 1
				break
		if(!channelexists)
			GLOB.news_network.CreateFeedChannel("Nanotrasen Book Club", "Library", null)
		GLOB.news_network.SubmitArticle(scanner.cache.dat, "[scanner.cache.name]", "Nanotrasen Book Club", null)
		tgui_alert(usr,"Upload complete. Your uploaded title is now available on station newscasters.")
	if(href_list["orderbyid"])
		if(printer_cooldown > world.time)
			say("Printer unavailable. Please allow a short time before attempting to print.")
		else
			var/orderid = input("Enter your order:") as num|null
			if(orderid)
				if(isnum(orderid) && ISINTEGER(orderid))
					href_list["targetid"] = num2text(orderid)

	if(href_list["targetid"])
		var/id = href_list["targetid"]
		if (!SSdbcore.Connect())
			tgui_alert(usr,"Connection to Archive has been severed. Aborting.")
		if(printer_cooldown > world.time)
			say("Printer unavailable. Please allow a short time before attempting to print.")
		else
			var/datum/db_query/query_library_print = SSdbcore.NewQuery(
				"SELECT * FROM [format_table_name("library")] WHERE id=:id AND isnull(deleted)",
				list("id" = id)
			)
			if(!query_library_print.Execute())
				qdel(query_library_print)
				say("PRINTER ERROR! Failed to print document (0x0000000F)")
				return
			printer_cooldown = world.time + PRINTER_COOLDOWN
			while(query_library_print.NextRow())
				var/author = query_library_print.item[2]
				var/title = query_library_print.item[3]
				var/content = query_library_print.item[4]
				if(!QDELETED(src))
					var/obj/item/book/B = new(get_turf(src))
					B.name = "Book: [title]"
					B.title = title
					B.author = author
					B.dat = content
					B.icon_state = "book[rand(1,8)]"
					visible_message(span_notice("[src]'s printer hums as it produces a completely bound book. How did it do that?"))
				break
			qdel(query_library_print)
	if(href_list["printbible"])
		if(printer_cooldown < world.time)
			var/obj/item/storage/book/bible/B = new /obj/item/storage/book/bible(src.loc)
			if(GLOB.bible_icon_state && GLOB.bible_inhand_icon_state)
				B.icon_state = GLOB.bible_icon_state
				B.inhand_icon_state = GLOB.bible_inhand_icon_state
				B.name = GLOB.bible_name
				B.deity_name = GLOB.deity
			printer_cooldown = world.time + PRINTER_COOLDOWN
		else
			say("Printer currently unavailable, please wait a moment.")
	if(href_list["printposter"])
		if(printer_cooldown < world.time)
			new /obj/item/poster/random_official(src.loc)
			printer_cooldown = world.time + PRINTER_COOLDOWN
		else
			say("Printer currently unavailable, please wait a moment.")
	add_fingerprint(usr)
	updateUsrDialog()

/*
 * Library Scanner
 */
/obj/machinery/libraryscanner
	name = "scanner control interface"
	icon = 'icons/obj/library.dmi'
	icon_state = "bigscanner"
	desc = "It servers the purpose of scanning stuff."
	density = TRUE
	var/obj/item/book/cache // Last scanned book

/obj/machinery/libraryscanner/attackby(obj/O, mob/user, params)
	if(istype(O, /obj/item/book))
		if(!user.transferItemToLoc(O, src))
			return
	else
		return ..()

/obj/machinery/libraryscanner/attack_hand(mob/user, list/modifiers)
	. = ..()
	if(.)
		return
	usr.set_machine(src)
	var/dat = "" // <META HTTP-EQUIV='Refresh' CONTENT='10'>
	if(cache)
		dat += "Data stored in memory.
"
	else
		dat += "No data stored in memory.
"
	dat += "\[Scan\]"
	if(cache)
		dat += "       \[Clear Memory\]

\[Remove Book\]"
	else
		dat += "
"
	var/datum/browser/popup = new(user, "scanner", name, 600, 400)
	popup.set_content(dat)
	popup.open()

/obj/machinery/libraryscanner/Topic(href, href_list)
	if(..())
		usr << browse(null, "window=scanner")
		onclose(usr, "scanner")
		return

	if(href_list["scan"])
		for(var/obj/item/book/B in contents)
			cache = B
			break
	if(href_list["clear"])
		cache = null
	if(href_list["eject"])
		for(var/obj/item/book/B in contents)
			B.forceMove(drop_location())
	src.add_fingerprint(usr)
	src.updateUsrDialog()
	return

/*
 * Book binder
 */
/obj/machinery/bookbinder
	name = "book binder"
	icon = 'icons/obj/library.dmi'
	icon_state = "binder"
	desc = "Only intended for binding paper products."
	density = TRUE
	var/busy = FALSE

/obj/machinery/bookbinder/attackby(obj/O, mob/user, params)
	if(istype(O, /obj/item/paper))
		bind_book(user, O)
	else if(default_unfasten_wrench(user, O))
		return 1
	else
		return ..()

/obj/machinery/bookbinder/proc/bind_book(mob/user, obj/item/paper/P)
	if(machine_stat)
		return
	if(busy)
		to_chat(user, span_warning("The book binder is busy. Please wait for completion of previous operation."))
		return
	if(!user.transferItemToLoc(P, src))
		return
	user.visible_message(span_notice("[user] loads some paper into [src]."), span_notice("You load some paper into [src]."))
	audible_message(span_hear("[src] begins to hum as it warms up its printing drums."))
	busy = TRUE
	sleep(rand(200,400))
	busy = FALSE
	if(P)
		if(!machine_stat)
			visible_message(span_notice("[src] whirs as it prints and binds a new book."))
			var/obj/item/book/B = new(src.loc)
			B.dat = P.info
			B.name = "Print Job #" + "[rand(100, 999)]"
			B.icon_state = "book[rand(1,7)]"
			qdel(P)
		else
			P.forceMove(drop_location())


code:
/obj/item/book/manual/random
	icon_state = "random_book"

/obj/item/book/manual/random/Initialize(mapload)
	..()
	var/static/banned_books = list(/obj/item/book/manual/random, /obj/item/book/manual/nuclear, /obj/item/book/manual/wiki)
	var/newtype = pick(subtypesof(/obj/item/book/manual) - banned_books)
	new newtype(loc)
	return INITIALIZE_HINT_QDEL

/obj/item/book/random
	icon_state = "random_book"
	/// The category of books to pick from when creating this book.
	var/random_category = null
	/// If this book has already been 'generated' yet.
	var/random_loaded = FALSE

/obj/item/book/random/Initialize(mapload)
	. = ..()
	icon_state = "book[rand(1,8)]"

/obj/item/book/random/attack_self()
	if(!random_loaded)
		create_random_books(1, loc, TRUE, random_category, src)
		random_loaded = TRUE
	return ..()

/obj/structure/bookcase/random
	load_random_books = TRUE
	books_to_load = 2
	icon_state = "random_bookcase"

/obj/structure/bookcase/random/Initialize(mapload)
	. = ..()
	if(books_to_load && isnum(books_to_load))
		books_to_load += pick(-1,-1,0,1,1)
	update_appearance()

/proc/create_random_books(amount, location, fail_loud = FALSE, category = null, obj/item/book/existing_book)
	. = list()
	if(!isnum(amount) || amount<1)
		return
	if (!SSdbcore.Connect())
		if(existing_book && (fail_loud || prob(5)))
			existing_book.author = "???"
			existing_book.title = "Strange book"
			existing_book.name = "Strange book"
			existing_book.dat = "There once was a book from Nantucket
But the database failed us, so f*$! it.
I tried to be good to you
Now this is an I.O.U
If you're feeling entitled, well, stuff it!

~"
		return
	if(prob(25))
		category = null
	var/datum/db_query/query_get_random_books = SSdbcore.NewQuery({"
		SELECT author, title, content
		FROM [format_table_name("library")]
		WHERE isnull(deleted) AND (:category IS NULL OR category = :category)
		ORDER BY rand() LIMIT :limit
	"}, list("category" = category, "limit" = amount))
	if(query_get_random_books.Execute())
		while(query_get_random_books.NextRow())
			var/obj/item/book/B
			B = existing_book ? existing_book : new(location)
			B.author = query_get_random_books.item[1]
			B.title = query_get_random_books.item[2]
			B.dat = query_get_random_books.item[3]
			B.name = "Book: [B.title]"
			if(!existing_book)
				B.icon_state= "book[rand(1,8)]"
	qdel(query_get_random_books)

/obj/structure/bookcase/random/fiction
	name = "bookcase (Fiction)"
	random_category = "Fiction"
/obj/structure/bookcase/random/nonfiction
	name = "bookcase (Non-Fiction)"
	random_category = "Non-fiction"
/obj/structure/bookcase/random/religion
	name = "bookcase (Religion)"
	random_category = "Religion"
/obj/structure/bookcase/random/adult
	name = "bookcase (Adult)"
	random_category = "Adult"

/obj/structure/bookcase/random/reference
	name = "bookcase (Reference)"
	random_category = "Reference"
	var/ref_book_prob = 20

/obj/structure/bookcase/random/reference/Initialize(mapload)
	. = ..()
	while(books_to_load > 0 && prob(ref_book_prob))
		books_to_load--
		new /obj/item/book/manual/random(src)


code:
/obj/item/skillchip/job
	skillchip_flags = SKILLCHIP_RESTRICTED_CATEGORIES
	chip_category = SKILLCHIP_CATEGORY_JOB
	incompatibility_list = list(SKILLCHIP_CATEGORY_JOB)
	abstract_parent_type = /obj/item/skillchip/job
	slot_use = 2


code:
/obj/item/skillchip/job/chef
	name = "B0RK-X3 skillchip"  // bork bork bork
	desc = "This biochip faintly smells of garlic, which is odd for something that is normally wedged inside a user's brain. Consult a dietician before use."
	skill_name = "Close Quarters Cooking"
	skill_description = "A specialised form of self defence, developed by skilled sous-chef de cuisines. No man fights harder than a chef to defend his kitchen."
	skill_icon = "utensils"
	activate_message = "You can visualize how to defend your kitchen with martial arts."
	deactivate_message = "You forget how to control your muscles to execute kicks, slams and restraints while in a kitchen environment."
	var/datum/martial_art/cqc/under_siege/style

/obj/item/skillchip/job/chef/Initialize(mapload)
	. = ..()
	style = new
	style.refresh_valid_areas()

/obj/item/skillchip/job/chef/on_activate(mob/living/carbon/user, silent = FALSE)
	. = ..()
	style.teach(user, make_temporary = TRUE)

/obj/item/skillchip/job/chef/on_deactivate(mob/living/carbon/user, silent = FALSE)
	style.remove(user)
	return ..()


code:
/obj/item/skillchip/job/psychology
	name = "HYPERG1G4 skillchip"
	desc = "Learn to bend the abyss to your will."
	auto_traits = list(TRAIT_SUPERMATTER_SOOTHER, TRAIT_SUPERMATTER_MADNESS_IMMUNE)
	skill_name = "Supermatter Cognition Theory"
	skill_description = "Understand the correct mental patterns to keep in mind around matter in a hyperfractal state, causing immunity to visions and making the matter in question \"calmer\"."
	skill_icon = "spa"
	activate_message = "You start thinking in patterns that will render you immune to visions from, and act as a calming influence for, matter in a hyperfractal state."
	deactivate_message = "Your thoughts become more disordered and jumbled. You are no longer immune to the abyss."


code:
/obj/item/skillchip/job/research_director
	name = "R.D.S.P.L.X. skillchip"
	desc = "Knowledge of how to solve the ancient conumdrum; what happens when an unstoppable force meets an immovable object."
	auto_traits = list(TRAIT_ROD_SUPLEX)
	skill_name = "True Strength"
	skill_description = "The knowledge and strength to resolve the most ancient conumdrum; what happens when an unstoppable force meets an immovable object."
	skill_icon = "dumbbell"
	activate_message = "You realise if you apply the correct force, at the correct angle, it is possible to make the immovable permanently movable."
	deactivate_message = "You forget how to permanently anchor a paradoxical object."


code:
/obj/item/skillchip/job/roboticist
	name = "Cyborg C1-RCU-1T skillchip"
	desc = "A roboticist's second best friend."
	auto_traits = list(TRAIT_KNOW_CYBORG_WIRES)
	skill_name = "Cyborg Circuitry"
	skill_description = "Recognise cyborg wire layouts and understand their functionality at a glance."
	skill_icon = "sitemap"
	activate_message = "You suddenly comprehend the secrets behind cyborg circuitry."
	deactivate_message = "Cyborg circuitry stops making sense as images of coloured wires fade from your mind."


code:
/obj/item/skillchip/job/engineer
	name = "Engineering C1-RCU-1T skillchip"
	desc = "Endorsed by Poly."
	auto_traits = list(TRAIT_KNOW_ENGI_WIRES)
	skill_name = "Engineering Circuitry"
	skill_description = "Recognise airlock and APC wire layouts and understand their functionality at a glance."
	skill_icon = "sitemap"
	activate_message = "You suddenly comprehend the secrets behind airlock and APC circuitry."
	deactivate_message = "Airlock and APC circuitry stops making sense as images of coloured wires fade from your mind."


code:
#define SKILLCHIP_IMPLANT_TIME 15 SECONDS
#define SKILLCHIP_REMOVAL_TIME 15 SECONDS

/obj/machinery/skill_station
	name = "\improper Skillsoft station"
	desc = "Learn skills with only minimal chance for brain damage."

	icon = 'icons/obj/machines/implantchair.dmi'
	icon_state = "implantchair"
	occupant_typecache = list(/mob/living/carbon) //todo make occupant_typecache per type
	state_open = TRUE
	interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND //Don't call ui_interac by default - we only want that when inside
	circuit = /obj/item/circuitboard/machine/skill_station
	/// Currently implanting/removing
	var/working = FALSE
	/// Timer until implanting/removing finishes.
	var/work_timer
	/// What we're implanting
	var/obj/item/skillchip/inserted_skillchip

/obj/machinery/skill_station/Initialize(mapload)
	. = ..()
	update_appearance()

//Only usable by the person inside
/obj/machinery/skill_station/ui_state(mob/user)
	return GLOB.contained_state

/obj/machinery/skill_station/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "SkillStation", name)
		ui.open()

/obj/machinery/skill_station/update_icon_state()
	icon_state = initial(icon_state)
	if(state_open)
		icon_state += "_open"
	if(occupant)
		icon_state += "_occupied"
	return ..()

/obj/machinery/skill_station/update_overlays()
	. = ..()
	if(working)
		. += "working"

/obj/machinery/skill_station/relaymove(mob/living/user, direction)
	open_machine()

/obj/machinery/skill_station/open_machine()
	. = ..()
	interrupt_operation()

/obj/machinery/skill_station/Exited(atom/movable/gone, direction)
	. = ..()
	if(gone == inserted_skillchip)
		inserted_skillchip = null
		interrupt_operation()

/obj/machinery/skill_station/power_change()
	. = ..()
	if(working)
		interrupt_operation()

/obj/machinery/skill_station/close_machine(atom/movable/target)
	. = ..()
	if(occupant)
		ui_interact(occupant)

/obj/machinery/skill_station/proc/interrupt_operation()
	working = FALSE
	if(work_timer)
		deltimer(work_timer)
		work_timer = null
	update_appearance()

/obj/machinery/skill_station/interact(mob/user)
	. = ..()
	if(user == occupant)
		ui_interact(user)
	else
		toggle_open()

/obj/machinery/skill_station/attackby(obj/item/I, mob/living/user, params)
	if(istype(I,/obj/item/skillchip))
		if(inserted_skillchip)
			to_chat(user,span_notice("There's already a skillchip inside."))
			return
		if(!user.transferItemToLoc(I, src))
			return
		inserted_skillchip = I
		return
	return ..()

/obj/machinery/skill_station/dump_contents()
	. = ..()
	inserted_skillchip = null

/obj/machinery/skill_station/dump_inventory_contents(list/subset = null)
	// Don't drop the skillchip, it's directly inserted into the machine.
	// dump_contents() will drop everything including the skillchip as an alternative to this.
	subset = contents - inserted_skillchip
	return ..()

/obj/machinery/skill_station/proc/toggle_open(mob/user)
	state_open ? close_machine() : open_machine()

// Functions below do not validate occupant exists - should be handled outer wrappers.
/// Start implanting.
/obj/machinery/skill_station/proc/start_implanting()
	var/mob/living/carbon/carbon_occupant = occupant

	if(inserted_skillchip.has_mob_incompatibility(carbon_occupant))
		CRASH("Unusual error - [usr] attempted to start implanting of [inserted_skillchip] when the interface state should not have allowed it.")

	working = TRUE
	work_timer = addtimer(CALLBACK(src,.proc/implant),SKILLCHIP_IMPLANT_TIME,TIMER_STOPPABLE)
	update_appearance()

/// Finish implanting.
/obj/machinery/skill_station/proc/implant()
	working = FALSE
	work_timer = null
	var/mob/living/carbon/carbon_occupant = occupant
	var/implant_msg = carbon_occupant.implant_skillchip(inserted_skillchip, FALSE)
	if(implant_msg)
		to_chat(carbon_occupant,span_notice("Operation failed! [implant_msg]"))
	else
		to_chat(carbon_occupant,span_notice("Operation complete!"))
		inserted_skillchip = null

	update_appearance()

/// Start removal.
/obj/machinery/skill_station/proc/start_removal(obj/item/skillchip/to_be_removed)
	if(!to_be_removed)
		return

	if(to_be_removed.is_on_cooldown())
		to_chat(occupant, span_notice("DANGER! Operation cannot be completed, removal is unsafe."))
		CRASH("Unusual error - [usr] attempted to start removal of [to_be_removed] when the interface state should not have allowed it.")

	working = TRUE
	work_timer = addtimer(CALLBACK(src,.proc/remove_skillchip,to_be_removed),SKILLCHIP_REMOVAL_TIME,TIMER_STOPPABLE)
	update_appearance()

/// Finish removal.
/obj/machinery/skill_station/proc/remove_skillchip(obj/item/skillchip/to_be_removed)
	working = FALSE
	work_timer = null
	update_appearance()

	var/mob/living/carbon/carbon_occupant = occupant

	if(to_be_removed.is_on_cooldown())
		to_chat(carbon_occupant,span_notice("Safety mechanisms activated! Skillchip cannot be safely removed."))
		return

	if(!istype(carbon_occupant))
		to_chat(carbon_occupant,span_notice("Occupant does not appear to be a carbon-based lifeform!"))
		return

	if(!carbon_occupant.remove_skillchip(to_be_removed))
		to_chat(carbon_occupant,span_notice("Failed to remove skillchip!"))
		return

	if(to_be_removed.removable)
		carbon_occupant.put_in_hands(to_be_removed)
	else
		qdel(to_be_removed)

	to_chat(carbon_occupant, span_notice("Operation complete!"))

/obj/machinery/skill_station/proc/toggle_chip_active(obj/item/skillchip/to_be_toggled)
	var/mob/living/carbon/carbon_occupant = occupant

	if(to_be_toggled.is_on_cooldown())
		to_chat(carbon_occupant,span_notice("Safety mechanisms activated! Skillchip cannot be safely modified."))
		return

	if(!istype(carbon_occupant))
		to_chat(carbon_occupant,span_notice("Occupant does not appear to be a carbon-based lifeform!"))
		return

	if(to_be_toggled.is_active())
		var/active_msg = to_be_toggled.try_deactivate_skillchip(FALSE, FALSE)
		if(active_msg)
			to_chat(carbon_occupant,span_notice("Failed to deactivate skillchip! [active_msg]"))
		return

	// This code will fire when to_be_toggled.active is FALSE
	var/active_msg = to_be_toggled.try_activate_skillchip(FALSE, FALSE)
	if(active_msg)
		to_chat(carbon_occupant,span_notice("Failed to activate skillchip! [active_msg]"))

/obj/machinery/skill_station/ui_data(mob/user)
	. = ..()
	.["working"] = working
	.["timeleft"] = work_timer ? timeleft(work_timer) : null
	var/mob/living/carbon/carbon_occupant = occupant

	.["skillchip_ready"] = inserted_skillchip ? TRUE : FALSE
	if(inserted_skillchip)
		// This is safe, incompatibility check can accept a null or invalid mob.
		var/incompatibility_check = inserted_skillchip.has_mob_incompatibility(carbon_occupant)
		// Grab chip data. We do this because of special chips like Chameleon that may want to
		// spoof their information.
		var/list/inserted_chip_data = inserted_skillchip.get_chip_data()
		if(incompatibility_check)
			.["implantable"] = FALSE
			.["implantable_reason"] = incompatibility_check
		else
			.["implantable"] = TRUE
			.["implantable_reason"] = null
		.["skill_name"] = inserted_chip_data["name"]
		.["skill_desc"] = inserted_chip_data["desc"]
		.["skill_icon"] = inserted_chip_data["icon"]
		.["complexity"] = inserted_chip_data["complexity"]
		.["slot_use"] = inserted_chip_data["slot_use"]

	// If there's no occupant, we don't need to worry about what skillchips are in their brain.
	if(!carbon_occupant)
		.["error"] = "No valid occupant detected. Please consult nearest medical practitioner."
		.["current"] = null
		.["complexity_used"] = null
		.["complexity_max"] = null
		.["slots_used"] = null
		.["slots_max"] = null
		return

	var/obj/item/organ/brain/occupant_brain = carbon_occupant.getorganslot(ORGAN_SLOT_BRAIN)

	// If there's no brain, we don't need to worry either.
	if(QDELETED(occupant_brain))
		.["error"] = "Brain not detected. Please consult nearest medical practitioner."
		.["current"] = null
		.["complexity_used"] = null
		.["complexity_max"] = null
		.["slots_used"] = null
		.["slots_max"] = null
		return

	.["complexity_used"] = occupant_brain.get_used_skillchip_complexity()
	.["complexity_max"] = occupant_brain.get_max_skillchip_complexity()
	.["slots_used"] = occupant_brain.get_used_skillchip_slots()
	.["slots_max"] = occupant_brain.get_max_skillchip_slots()

	// If we got here, we have both an occupant and it has a brain, so we can check for skillchips.
	var/list/current_skills = list()
	for(var/obj/item/skillchip/skill_chip in occupant_brain.skillchips)
		current_skills += list(skill_chip.get_chip_data())
	.["current"] = current_skills

/obj/machinery/skill_station/ui_act(action, list/params)
	. = ..()
	if(.)
		return
	if(usr != occupant)
		return
	switch(action)
		if("implant")
			if(working)
				stack_trace("[usr] tried to start skillchip implanting when [src] was in an invalid state.")
				return TRUE
			if(occupant && inserted_skillchip)
				start_implanting()
			return TRUE
		if("remove")
			if(working)
				stack_trace("[usr] tried to start skillchip removal when [src] was in an invalid state.")
				return TRUE
			var/chipref = params["ref"]
			var/mob/living/carbon/carbon_occupant = occupant
			var/obj/item/organ/brain/occupant_brain = carbon_occupant.getorganslot(ORGAN_SLOT_BRAIN)
			if(QDELETED(carbon_occupant) || QDELETED(occupant_brain))
				return TRUE
			var/obj/item/skillchip/to_be_removed = locate(chipref) in occupant_brain.skillchips
			if(!to_be_removed)
				return TRUE
			start_removal(to_be_removed)
			return TRUE
		if("eject")
			if(working)
				stack_trace("[usr] tried to toggle skillchip activation when [src] was in an invalid state.")
				return TRUE
			if(inserted_skillchip)
				to_chat(occupant,span_notice("You eject the skillchip."))
				var/mob/living/carbon/human/H = occupant
				H.put_in_hands(inserted_skillchip)
				inserted_skillchip = null
				return TRUE
		if("toggle_activate")
			var/chipref = params["ref"]
			// Check if the machine is already working. If it is, this act should not have sent.
			if(working)
				stack_trace("[usr] tried to toggle skillchip activation when [src] was in an invalid state.")
				return TRUE
			var/mob/living/carbon/carbon_occupant = occupant
			var/obj/item/organ/brain/occupant_brain = carbon_occupant.getorganslot(ORGAN_SLOT_BRAIN)
			if(QDELETED(carbon_occupant) || QDELETED(occupant_brain))
				return TRUE
			var/obj/item/skillchip/to_be_removed = locate(chipref) in occupant_brain.skillchips
			if(!to_be_removed)
				return TRUE
			toggle_chip_active(to_be_removed)
			return TRUE