code:
//This is the lowest supported version, anything below this is completely obsolete and the entire savefile will be wiped.
#define SAVEFILE_VERSION_MIN 32

//This is the current version, anything below this will attempt to update (if it's not obsolete)
// You do not need to raise this if you are adding new values that have sane defaults.
// Only raise this value when changing the meaning/format/name/layout of an existing value
// where you would want the updater procs below to run
#define SAVEFILE_VERSION_MAX 40

/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
	This proc checks if the current directory of the savefile S needs updating
	It is to be used by the load_character and load_preferences procs.
	(S.cd=="/" is preferences, S.cd=="/character[integer]" is a character slot, etc)
	if the current directory's version is below SAVEFILE_VERSION_MIN it will simply wipe everything in that directory
	(if we're at root "/" then it'll just wipe the entire savefile, for instance.)
	if its version is below SAVEFILE_VERSION_MAX but above the minimum, it will load data but later call the
	respective update_preferences() or update_character() proc.
	Those procs allow coders to specify format changes so users do not lose their setups and have to redo them again.
	Failing all that, the standard sanity checks are performed. They simply check the data is suitable, reverting to
	initial() values if necessary.
*/
/datum/preferences/proc/savefile_needs_update(savefile/S)
	var/savefile_version
	READ_FILE(S["version"], savefile_version)

	if(savefile_version < SAVEFILE_VERSION_MIN)
		S.dir.Cut()
		return -2
	if(savefile_version < SAVEFILE_VERSION_MAX)
		return savefile_version
	return -1

//should these procs get fairly long
//just increase SAVEFILE_VERSION_MIN so it's not as far behind
//SAVEFILE_VERSION_MAX and then delete any obsolete if clauses
//from these procs.
//This only really meant to avoid annoying frequent players
//if your savefile is 3 months out of date, then 'tough shit'.

/datum/preferences/proc/update_preferences(current_version, savefile/S)
	if(current_version < 33)
		toggles |= SOUND_ENDOFROUND

	if(current_version < 34)
		auto_fit_viewport = TRUE

	if(current_version < 35) //makes old keybinds compatible with #52040, sets the new default
		var/newkey = FALSE
		for(var/list/key in key_bindings)
			for(var/bind in key)
				if(bind == "quick_equipbelt")
					key -= "quick_equipbelt"
					key |= "quick_equip_belt"

				if(bind == "bag_equip")
					key -= "bag_equip"
					key |= "quick_equip_bag"

				if(bind == "quick_equip_suit_storage")
					newkey = TRUE
		if(!newkey && !key_bindings["ShiftQ"])
			key_bindings["ShiftQ"] = list("quick_equip_suit_storage")

	if(current_version < 36)
		if(key_bindings["ShiftQ"] == "quick_equip_suit_storage")
			key_bindings["ShiftQ"] = list("quick_equip_suit_storage")

	if(current_version < 37)
		if(clientfps == 0)
			clientfps = -1

	if (current_version < 38)
		var/found_block_movement = FALSE

		for (var/list/key in key_bindings)
			for (var/bind in key)
				if (bind == "block_movement")
					found_block_movement = TRUE
					break
			if (found_block_movement)
				break

		if (!found_block_movement)
			LAZYADD(key_bindings["Ctrl"], "block_movement")

	if (current_version < 39)
		LAZYADD(key_bindings["F"], "toggle_combat_mode")
		LAZYADD(key_bindings["4"], "toggle_combat_mode")
	if (current_version < 40)
		LAZYADD(key_bindings["Space"], "hold_throw_mode")

/datum/preferences/proc/update_character(current_version, savefile/S)
	return

/// checks through keybindings for outdated unbound keys and updates them
/datum/preferences/proc/check_keybindings()
	if(!parent)
		return
	var/list/user_binds = list()
	for (var/key in key_bindings)
		for(var/kb_name in key_bindings[key])
			user_binds[kb_name] += list(key)
	var/list/notadded = list()
	for (var/name in GLOB.keybindings_by_name)
		var/datum/keybinding/kb = GLOB.keybindings_by_name[name]
		if(length(user_binds[kb.name]))
			continue // key is unbound and or bound to something
		var/addedbind = FALSE
		if(hotkeys)
			for(var/hotkeytobind in kb.classic_keys)
				if(!length(key_bindings[hotkeytobind]))
					LAZYADD(key_bindings[hotkeytobind], kb.name)
					addedbind = TRUE
		else
			for(var/classickeytobind in kb.classic_keys)
				if(!length(key_bindings[classickeytobind]))
					LAZYADD(key_bindings[classickeytobind], kb.name)
					addedbind = TRUE
		if(!addedbind)
			notadded += kb
	if(length(notadded))
		addtimer(CALLBACK(src, .proc/announce_conflict, notadded), 5 SECONDS)

/datum/preferences/proc/announce_conflict(list/notadded)
	to_chat(parent, "KEYBINDING CONFLICT\n\
					There are new keybindings that default to keys you've already bound. These will be unbound.")
	for(var/item in notadded)
		var/datum/keybinding/conflicted = item
		to_chat(parent, "[conflicted.category]: [conflicted.full_name] needs updating")
		LAZYADD(key_bindings["Unbound"], conflicted.name) // set it to unbound to prevent this from opening up again in the future

/datum/preferences/proc/load_path(ckey,filename="preferences.sav")
	if(!ckey)
		return
	path = "data/player_saves/[ckey[1]]/[ckey]/[filename]"

/datum/preferences/proc/load_preferences()
	if(!path)
		return FALSE
	if(!fexists(path))
		return FALSE

	var/savefile/S = new /savefile(path)
	if(!S)
		return FALSE
	S.cd = "/"

	var/needs_update = savefile_needs_update(S)
	if(needs_update == -2) //fatal, can't load any data
		var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
		if (fexists(bacpath))
			fdel(bacpath) //only keep 1 version of backup
		fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
		return FALSE

	//general preferences
	READ_FILE(S["asaycolor"], asaycolor)
	READ_FILE(S["brief_outfit"], brief_outfit)
	READ_FILE(S["ooccolor"], ooccolor)
	READ_FILE(S["screentip_color"], screentip_color)
	READ_FILE(S["lastchangelog"], lastchangelog)
	READ_FILE(S["UI_style"], UI_style)
	READ_FILE(S["hotkeys"], hotkeys)
	READ_FILE(S["chat_on_map"], chat_on_map)
	READ_FILE(S["max_chat_length"], max_chat_length)
	READ_FILE(S["see_chat_non_mob"] , see_chat_non_mob)
	READ_FILE(S["see_rc_emotes"] , see_rc_emotes)
	READ_FILE(S["broadcast_login_logout"] , broadcast_login_logout)

	READ_FILE(S["announcement_volume"], announcement_volume)

	READ_FILE(S["tgui_fancy"], tgui_fancy)
	READ_FILE(S["tgui_lock"], tgui_lock)
	READ_FILE(S["buttons_locked"], buttons_locked)
	READ_FILE(S["windowflash"], windowflashing)
	READ_FILE(S["be_special"] , be_special)

	READ_FILE(S["default_slot"], default_slot)
	READ_FILE(S["chat_toggles"], chat_toggles)
	READ_FILE(S["toggles"], toggles)
	READ_FILE(S["septictoggles"], septictoggles)
	READ_FILE(S["ghost_form"], ghost_form)
	READ_FILE(S["ghost_orbit"], ghost_orbit)
	READ_FILE(S["ghost_accs"], ghost_accs)
	READ_FILE(S["ghost_others"], ghost_others)
	READ_FILE(S["preferred_map"], preferred_map)
	READ_FILE(S["ignoring"], ignoring)
	READ_FILE(S["ghost_hud"], ghost_hud)
	READ_FILE(S["inquisitive_ghost"], inquisitive_ghost)
	READ_FILE(S["uses_glasses_colour"], uses_glasses_colour)
	READ_FILE(S["clientfps"], clientfps)
	READ_FILE(S["parallax"], parallax)
	READ_FILE(S["ambientocclusion"], ambientocclusion)
	READ_FILE(S["screentip_pref"], screentip_pref)
	READ_FILE(S["itemoutline_pref"], itemoutline_pref)
	READ_FILE(S["auto_fit_viewport"], auto_fit_viewport)
	READ_FILE(S["widescreenpref"], widescreenpref)
	READ_FILE(S["pixel_size"], pixel_size)
	READ_FILE(S["scaling_method"], scaling_method)
	READ_FILE(S["menuoptions"], menuoptions)
	READ_FILE(S["enable_tips"], enable_tips)
	READ_FILE(S["tip_delay"], tip_delay)
	READ_FILE(S["pda_style"], pda_style)
	READ_FILE(S["pda_color"], pda_color)

	// Custom hotkeys
	READ_FILE(S["key_bindings"], key_bindings)
	check_keybindings()
	// hearted
	READ_FILE(S["hearted_until"], hearted_until)
	if(hearted_until > world.realtime)
		hearted = TRUE
	//favorite outfits
	READ_FILE(S["favorite_outfits"], favorite_outfits)

	var/list/parsed_favs = list()
	for(var/typetext in favorite_outfits)
		var/datum/outfit/path = text2path(typetext)
		if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore
			parsed_favs += path
	favorite_outfits = uniqueList(parsed_favs)

	//try to fix any outdated data if necessary
	if(needs_update >= 0)
		var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
		if (fexists(bacpath))
			fdel(bacpath) //only keep 1 version of backup
		fcopy(S, bacpath) //byond helpfully lets you use a savefile for the first arg.
		update_preferences(needs_update, S) //needs_update = savefile_version if we need an update (positive integer)

	//Sanitize
	asaycolor = sanitize_ooccolor(sanitize_hexcolor(asaycolor, 6, TRUE, initial(asaycolor)))
	ooccolor = sanitize_ooccolor(sanitize_hexcolor(ooccolor, 6, TRUE, initial(ooccolor)))
	screentip_color = sanitize_ooccolor(sanitize_hexcolor(screentip_color, 6, TRUE, initial(screentip_color)))
	lastchangelog = sanitize_text(lastchangelog, initial(lastchangelog))
	UI_style = sanitize_inlist(UI_style, GLOB.available_ui_styles, GLOB.available_ui_styles[1])
	hotkeys = sanitize_integer(hotkeys, FALSE, TRUE, initial(hotkeys))
	chat_on_map = sanitize_integer(chat_on_map, FALSE, TRUE, initial(chat_on_map))
	max_chat_length = sanitize_integer(max_chat_length, 1, CHAT_MESSAGE_MAX_LENGTH, initial(max_chat_length))
	see_chat_non_mob = sanitize_integer(see_chat_non_mob, FALSE, TRUE, initial(see_chat_non_mob))
	see_rc_emotes = sanitize_integer(see_rc_emotes, FALSE, TRUE, initial(see_rc_emotes))
	broadcast_login_logout = sanitize_integer(broadcast_login_logout, FALSE, TRUE, initial(broadcast_login_logout))
	tgui_fancy = sanitize_integer(tgui_fancy, FALSE, TRUE, initial(tgui_fancy))
	tgui_lock = sanitize_integer(tgui_lock, FALSE, TRUE, initial(tgui_lock))
	buttons_locked = sanitize_integer(buttons_locked, FALSE, TRUE, initial(buttons_locked))
	windowflashing = sanitize_integer(windowflashing, FALSE, TRUE, initial(windowflashing))
	default_slot = sanitize_integer(default_slot, 1, max_save_slots, initial(default_slot))
	toggles = sanitize_integer(toggles, 0, (2**24)-1, initial(toggles))
	clientfps = sanitize_integer(clientfps, 0, 1000, 40)
	parallax = sanitize_integer(parallax, PARALLAX_INSANE, PARALLAX_DISABLE, null)
	ambientocclusion = sanitize_integer(ambientocclusion, FALSE, TRUE, initial(ambientocclusion))
	screentip_pref	= sanitize_integer(screentip_pref, FALSE, TRUE, initial(screentip_pref))
	itemoutline_pref = sanitize_integer(itemoutline_pref, FALSE, TRUE, initial(itemoutline_pref))
	auto_fit_viewport = sanitize_integer(auto_fit_viewport, FALSE, TRUE, initial(auto_fit_viewport))
	widescreenpref = sanitize_integer(widescreenpref, FALSE, TRUE, initial(widescreenpref))
	pixel_size = sanitize_float(pixel_size, PIXEL_SCALING_AUTO, PIXEL_SCALING_3X, 0.5, initial(pixel_size))
	scaling_method = sanitize_text(scaling_method, initial(scaling_method))
	ghost_form = sanitize_inlist(ghost_form, GLOB.ghost_forms, initial(ghost_form))
	ghost_orbit = sanitize_inlist(ghost_orbit, GLOB.ghost_orbits, initial(ghost_orbit))
	ghost_accs = sanitize_inlist(ghost_accs, GLOB.ghost_accs_options, GHOST_ACCS_DEFAULT_OPTION)
	ghost_others = sanitize_inlist(ghost_others, GLOB.ghost_others_options, GHOST_OTHERS_DEFAULT_OPTION)
	menuoptions = SANITIZE_LIST(menuoptions)
	be_special = SANITIZE_LIST(be_special)
	brief_outfit = sanitize_inlist(brief_outfit, subtypesof(/datum/outfit), null)
	pda_style = sanitize_inlist(pda_style, GLOB.pda_styles, initial(pda_style))
	pda_color = sanitize_hexcolor(pda_color, 6, TRUE, initial(pda_color))
	key_bindings = sanitize_keybindings(key_bindings)
	favorite_outfits = SANITIZE_LIST(favorite_outfits)
	announcement_volume = sanitize_integer(announcement_volume, 1, 100, initial(announcement_volume))

	if(needs_update >= 0) //save the updated version
		var/old_default_slot = default_slot
		var/old_max_save_slots = max_save_slots

		for (var/slot in S.dir) //but first, update all current character slots.
			if (copytext(slot, 1, 10) != "character")
				continue
			var/slotnum = text2num(copytext(slot, 10))
			if (!slotnum)
				continue
			max_save_slots = max(max_save_slots, slotnum) //so we can still update byond member slots after they lose memeber status
			default_slot = slotnum
			if (load_character())
				save_character()
		default_slot = old_default_slot
		max_save_slots = old_max_save_slots
		save_preferences()

	return TRUE

/datum/preferences/proc/save_preferences()
	if(!path)
		return FALSE
	var/savefile/S = new /savefile(path)
	if(!S)
		return FALSE
	S.cd = "/"

	WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //updates (or failing that the sanity checks) will ensure data is not invalid at load. Assume up-to-date

	//general preferences
	WRITE_FILE(S["asaycolor"], asaycolor)

	WRITE_FILE(S["ooccolor"], ooccolor)
	WRITE_FILE(S["screentip_color"], screentip_color)
	WRITE_FILE(S["lastchangelog"], lastchangelog)
	WRITE_FILE(S["UI_style"], UI_style)
	WRITE_FILE(S["hotkeys"], hotkeys)
	WRITE_FILE(S["chat_on_map"], chat_on_map)
	WRITE_FILE(S["max_chat_length"], max_chat_length)
	WRITE_FILE(S["see_chat_non_mob"], see_chat_non_mob)
	WRITE_FILE(S["see_rc_emotes"], see_rc_emotes)
	WRITE_FILE(S["broadcast_login_logout"], broadcast_login_logout)
	WRITE_FILE(S["tgui_fancy"], tgui_fancy)
	WRITE_FILE(S["tgui_lock"], tgui_lock)
	WRITE_FILE(S["buttons_locked"], buttons_locked)
	WRITE_FILE(S["windowflash"], windowflashing)
	WRITE_FILE(S["be_special"], be_special)
	WRITE_FILE(S["default_slot"], default_slot)
	WRITE_FILE(S["toggles"], toggles)
	WRITE_FILE(S["chat_toggles"], chat_toggles)
	WRITE_FILE(S["septictoggles"], septictoggles)
	WRITE_FILE(S["ghost_form"], ghost_form)
	WRITE_FILE(S["ghost_orbit"], ghost_orbit)
	WRITE_FILE(S["ghost_accs"], ghost_accs)
	WRITE_FILE(S["ghost_others"], ghost_others)
	WRITE_FILE(S["preferred_map"], preferred_map)
	WRITE_FILE(S["ignoring"], ignoring)
	WRITE_FILE(S["ghost_hud"], ghost_hud)
	WRITE_FILE(S["inquisitive_ghost"], inquisitive_ghost)
	WRITE_FILE(S["uses_glasses_colour"], uses_glasses_colour)
	WRITE_FILE(S["clientfps"], clientfps)
	WRITE_FILE(S["parallax"], parallax)
	WRITE_FILE(S["ambientocclusion"], ambientocclusion)
	WRITE_FILE(S["screentip_pref"], screentip_pref)
	WRITE_FILE(S["itemoutline_pref"], itemoutline_pref)
	WRITE_FILE(S["auto_fit_viewport"], auto_fit_viewport)
	WRITE_FILE(S["widescreenpref"], widescreenpref)
	WRITE_FILE(S["pixel_size"], pixel_size)
	WRITE_FILE(S["scaling_method"], scaling_method)
	WRITE_FILE(S["menuoptions"], menuoptions)
	WRITE_FILE(S["enable_tips"], enable_tips)
	WRITE_FILE(S["tip_delay"], tip_delay)
	WRITE_FILE(S["pda_style"], pda_style)
	WRITE_FILE(S["pda_color"], pda_color)
	WRITE_FILE(S["key_bindings"], key_bindings)
	WRITE_FILE(S["hearted_until"], (hearted_until > world.realtime ? hearted_until : null))
	WRITE_FILE(S["favorite_outfits"], favorite_outfits)
	WRITE_FILE(S["announcement_volume"], announcement_volume)
	return TRUE

/datum/preferences/proc/load_character(slot)
	if(!path)
		return FALSE
	if(!fexists(path))
		return FALSE
	var/savefile/S = new /savefile(path)
	if(!S)
		return FALSE
	S.cd = "/"
	if(!slot)
		slot = default_slot
	slot = sanitize_integer(slot, 1, max_save_slots, initial(default_slot))
	if(slot != default_slot)
		default_slot = slot
		WRITE_FILE(S["default_slot"] , slot)

	S.cd = "/character[slot]"
	var/needs_update = savefile_needs_update(S)
	if(needs_update == -2) //fatal, can't load any data
		return FALSE

	//Species
	var/species_id
	READ_FILE(S["species"], species_id)
	if(species_id)
		var/newtype = GLOB.species_list[species_id]
		if(newtype)
			pref_species = new newtype

	/*if(!S["features["mcolor"]"] || S["features["mcolor"]"] == "#000")
		WRITE_FILE(S["features["mcolor"]"]	, "#FFF")

	if(!S["feature_ethcolor"] || S["feature_ethcolor"] == "#000")
		WRITE_FILE(S["feature_ethcolor"]	, "9c3030")*/

	//Character
	READ_FILE(S["real_name"], real_name)
	READ_FILE(S["gender"], gender)
	READ_FILE(S["genitals"], genitals)
	READ_FILE(S["body_type"], body_type)
	READ_FILE(S["age"], age)
	READ_FILE(S["hair_color"], hair_color)
	READ_FILE(S["facial_hair_color"], facial_hair_color)
	READ_FILE(S["left_eye_color"], left_eye_color)
	READ_FILE(S["right_eye_color"], right_eye_color)
	READ_FILE(S["skin_tone"], skin_tone)
	READ_FILE(S["hairstyle_name"], hairstyle)
	READ_FILE(S["facial_style_name"], facial_hairstyle)
	READ_FILE(S["backpack"], backpack)
	READ_FILE(S["uplink_loc"], uplink_spawn_loc)
	READ_FILE(S["playtime_reward_cloak"], playtime_reward_cloak)
	READ_FILE(S["phobia"], phobia)
	READ_FILE(S["randomise"],  randomise)
	/*READ_FILE(S["feature_mcolor"], features["mcolor"])
	READ_FILE(S["feature_ethcolor"], features["ethcolor"])
	READ_FILE(S["feature_lizard_tail"], features["tail_lizard"])
	READ_FILE(S["feature_lizard_snout"], features["snout"])
	READ_FILE(S["feature_lizard_horns"], features["horns"])
	READ_FILE(S["feature_lizard_frills"], features["frills"])
	READ_FILE(S["feature_lizard_spines"], features["spines"])
	READ_FILE(S["feature_lizard_body_markings"], features["body_markings"])
	READ_FILE(S["feature_lizard_legs"], features["legs"])
	READ_FILE(S["feature_moth_wings"], features["moth_wings"])
	READ_FILE(S["feature_moth_antennae"], features["moth_antennae"])
	READ_FILE(S["feature_moth_markings"], features["moth_markings"])*/
	READ_FILE(S["persistent_scars"] , persistent_scars)
	/*if(!CONFIG_GET(flag/join_with_mutant_humans))
		features["tail_human"] = "none"
		features["ears"] = "none"
	else
		READ_FILE(S["feature_human_tail"], features["tail_human"])
		READ_FILE(S["feature_human_ears"], features["ears"])*/

	//Custom names
	for(var/custom_name_id in GLOB.preferences_custom_names)
		var/savefile_slot_name = custom_name_id + "_name" //TODO remove this
		READ_FILE(S[savefile_slot_name], custom_names[custom_name_id])

	READ_FILE(S["preferred_ai_core_display"], preferred_ai_core_display)
	READ_FILE(S["prefered_security_department"], prefered_security_department)

	//Jobs
	READ_FILE(S["joblessrole"], joblessrole)
	//Load prefs
	READ_FILE(S["job_preferences"], job_preferences)

	//Quirks
	READ_FILE(S["all_quirks"], all_quirks)

	//try to fix any outdated data if necessary
	//preference updating will handle saving the updated data for us.
	if(needs_update >= 0)
		update_character(needs_update, S) //needs_update == savefile_version if we need an update (positive integer)

	//Sanitize
	real_name = reject_bad_name(real_name)
	gender = sanitize_gender(gender)
	genitals = sanitize_inlist(genitals, GLOB.genital_sets, (gender == FEMALE ? "Female" : "Male"))
	body_type = sanitize_gender(body_type, FALSE, FALSE, gender)
	if(!real_name)
		real_name = random_unique_name(gender)

	for(var/custom_name_id in GLOB.preferences_custom_names)
		var/namedata = GLOB.preferences_custom_names[custom_name_id]
		custom_names[custom_name_id] = reject_bad_name(custom_names[custom_name_id],namedata["allow_numbers"])
		if(!custom_names[custom_name_id])
			custom_names[custom_name_id] = get_default_name(custom_name_id)

	/*if(!features["mcolor"] || features["mcolor"] == "#000")
		features["mcolor"] = pick("FFF","7F7F7F", "7FFF7F", "7F7FFF", "FF7F7F", "7FFFFF", "FF7FFF", "FFFF7F")

	if(!features["ethcolor"] || features["ethcolor"] == "#000")
		features["ethcolor"] = GLOB.color_list_ethereal[pick(GLOB.color_list_ethereal)]*/

	randomise = SANITIZE_LIST(randomise)

	if(gender == MALE)
		hairstyle = sanitize_inlist(hairstyle, GLOB.hairstyles_male_list)
		facial_hairstyle = sanitize_inlist(facial_hairstyle, GLOB.facial_hairstyles_male_list)
		underwear = sanitize_inlist(underwear, GLOB.underwear_m)
		undershirt = sanitize_inlist(undershirt, GLOB.undershirt_m)
	else if(gender == FEMALE)
		hairstyle = sanitize_inlist(hairstyle, GLOB.hairstyles_female_list)
		facial_hairstyle = sanitize_inlist(facial_hairstyle, GLOB.facial_hairstyles_female_list)
		underwear = sanitize_inlist(underwear, GLOB.underwear_f)
		undershirt = sanitize_inlist(undershirt, GLOB.undershirt_f)
	else
		hairstyle = sanitize_inlist(hairstyle, GLOB.hairstyles_list)
		facial_hairstyle = sanitize_inlist(facial_hairstyle, GLOB.facial_hairstyles_list)
		underwear = sanitize_inlist(underwear, GLOB.underwear_list)
		undershirt = sanitize_inlist(undershirt, GLOB.undershirt_list)

	socks = sanitize_inlist(socks, GLOB.socks_list)
	age = sanitize_integer(age, AGE_MIN, AGE_MAX, initial(age))
	hair_color = sanitize_hexcolor(hair_color, 6, FALSE)
	facial_hair_color = sanitize_hexcolor(facial_hair_color, 6, FALSE)
	underwear_color = sanitize_hexcolor(underwear_color, 6, FALSE)
	left_eye_color = sanitize_hexcolor(left_eye_color, 6, FALSE)
	right_eye_color = sanitize_hexcolor(right_eye_color, 6, FALSE)
	skin_tone = sanitize_inlist(skin_tone, GLOB.skin_tones)
	backpack = sanitize_inlist(backpack, GLOB.backpacklist, initial(backpack))
	uplink_spawn_loc = sanitize_inlist(uplink_spawn_loc, GLOB.uplink_spawn_loc_list, initial(uplink_spawn_loc))
	playtime_reward_cloak = sanitize_integer(playtime_reward_cloak)

	persistent_scars = sanitize_integer(persistent_scars)

	joblessrole = sanitize_integer(joblessrole, 1, 3, initial(joblessrole))
	//Validate job prefs
	for(var/j in job_preferences)
		if(job_preferences[j] != JP_LOW && job_preferences[j] != JP_MEDIUM && job_preferences[j] != JP_HIGH)
			job_preferences -= j

	all_quirks = SANITIZE_LIST(all_quirks)
	validate_quirks()

	//All the new skyrat related stuff here
	READ_FILE(S["features"], features)
	READ_FILE(S["mutant_bodyparts"], mutant_bodyparts)
	READ_FILE(S["body_markings"], body_markings)

	READ_FILE(S["loadout"], loadout)

	READ_FILE(S["general_record"], general_record)
	READ_FILE(S["security_record"], security_record)
	READ_FILE(S["medical_record"], medical_record)
	READ_FILE(S["background_info"], background_info)
	READ_FILE(S["exploitable_info"], exploitable_info)

	READ_FILE(S["mismatched_customization"] , mismatched_customization)
	READ_FILE(S["allow_advanced_colors"] , allow_advanced_colors)

	READ_FILE(S["augments"] , augments)
	READ_FILE(S["augment_limb_styles"] , augment_limb_styles)

	READ_FILE(S["undershirt_color"], undershirt_color)
	undershirt_color = sanitize_hexcolor(undershirt_color, 6, FALSE)

	READ_FILE(S["socks_color"], socks_color)
	socks_color	= sanitize_hexcolor(socks_color, 6, FALSE)

	READ_FILE(S["foodlikes"], foodlikes)
	READ_FILE(S["fooddislikes"], fooddislikes)

	foodlikes = SANITIZE_LIST(foodlikes)
	fooddislikes = SANITIZE_LIST(fooddislikes)
	features = SANITIZE_LIST(features)
	mutant_bodyparts = SANITIZE_LIST(mutant_bodyparts)
	body_markings = SANITIZE_LIST(body_markings)

	loadout = SANITIZE_LIST(loadout)
	//LOADOUT POINT VALIDATION
	//Here we calculate if we truly can use the loaded loadout
	loadout_points = initial_loadout_points()
	var/accumulated_cost = 0
	for(var/path in loadout)
		var/datum/loadout_item/LI = GLOB.loadout_items[path]
		if(!LI)
			loadout -= path
			continue
		loadout[path] = LI.get_valid_information(loadout[path])//Savefile validation
		accumulated_cost += LI.cost
	if(accumulated_cost > loadout_points) //Not enough points, reset loadout
		loadout = list()
	else
		loadout_points -= accumulated_cost //We got enough points, subtract the cost

	general_record = sanitize_text(general_record)
	security_record = sanitize_text(security_record)
	medical_record = sanitize_text(medical_record)
	background_info = sanitize_text(background_info)
	exploitable_info = sanitize_text(exploitable_info)

	mismatched_customization = sanitize_integer(mismatched_customization, FALSE, TRUE, initial(mismatched_customization))
	allow_advanced_colors = sanitize_integer(allow_advanced_colors, FALSE, TRUE, initial(allow_advanced_colors))

	//Validating mandatory features
	for(var/key in MANDATORY_FEATURE_LIST)
		if(!features[key])
			features[key] = MANDATORY_FEATURE_LIST[key]

	//validating body markings
	for(var/zone in body_markings)
		for(var/name in body_markings[zone])
			if(!(name in GLOB.body_markings_per_limb[zone]))
				body_markings[zone] -= name

	augments = SANITIZE_LIST(augments)
	//validating augments
	for(var/aug_slot in augments)
		var/datum/augment_item/aug = GLOB.augment_items[augments[aug_slot]]
		if(!aug)
			augments -= aug_slot
	augment_limb_styles = SANITIZE_LIST(augment_limb_styles)
	//validating limb styles
	for(var/limb_slot in augment_limb_styles)
		if(!GLOB.robotic_styles_list[augment_limb_styles[limb_slot]])
			augment_limb_styles -= limb_slot

	validate_species_parts()

	READ_FILE(S["pref_birthsign"], pref_birthsign)

	READ_FILE(S["languages"], languages)
	var/do_get_common_later = FALSE
	if(isnull(languages))
		do_get_common_later = TRUE
	languages = SANITIZE_LIST(languages)
	READ_FILE(S["handedness"], handedness)
	handedness = sanitize_integer(handedness)
	READ_FILE(S["blood_type"], blood_type)
	blood_type = sanitize_inlist(blood_type, get_safe_blood("AB+") | "Random", "Random")

	if(!pref_birthsign || !GLOB.culture_birthsigns[pref_birthsign])
		pref_birthsign = pref_species.culture_birthsigns[1]

	validate_languages()
	if(do_get_common_later)
		try_get_common_language()

	needs_update = TRUE
	return TRUE

/datum/preferences/proc/save_character()
	if(!path)
		return FALSE
	var/savefile/S = new /savefile(path)
	if(!S)
		return FALSE
	S.cd = "/character[default_slot]"

	WRITE_FILE(S["version"] , SAVEFILE_VERSION_MAX) //load_character will sanitize any bad data, so assume up-to-date.)

	//Character
	WRITE_FILE(S["real_name"] , real_name)
	WRITE_FILE(S["gender"] , gender)
	WRITE_FILE(S["genitals"], genitals)
	WRITE_FILE(S["body_type"] , body_type)
	WRITE_FILE(S["age"] , age)
	WRITE_FILE(S["hair_color"] , hair_color)
	WRITE_FILE(S["facial_hair_color"] , facial_hair_color)
	WRITE_FILE(S["left_eye_color"] , left_eye_color)
	WRITE_FILE(S["right_eye_color"] , right_eye_color)
	WRITE_FILE(S["skin_tone"] , skin_tone)
	WRITE_FILE(S["hairstyle_name"] , hairstyle)
	WRITE_FILE(S["facial_style_name"] , facial_hairstyle)
	WRITE_FILE(S["underwear"] , underwear)
	WRITE_FILE(S["underwear_color"] , underwear_color)
	WRITE_FILE(S["undershirt"] , undershirt)
	WRITE_FILE(S["socks"] , socks)
	WRITE_FILE(S["backpack"] , backpack)
	WRITE_FILE(S["uplink_loc"] , uplink_spawn_loc)
	WRITE_FILE(S["playtime_reward_cloak"] , playtime_reward_cloak)
	WRITE_FILE(S["phobia"], phobia)
	WRITE_FILE(S["randomise"] , randomise)
	WRITE_FILE(S["species"] , pref_species.id)
	/*WRITE_FILE(S["feature_mcolor"] , features["mcolor"])
	WRITE_FILE(S["feature_ethcolor"] , features["ethcolor"])
	WRITE_FILE(S["feature_lizard_tail"] , features["tail_lizard"])
	WRITE_FILE(S["feature_human_tail"] , features["tail_human"])
	WRITE_FILE(S["feature_lizard_snout"] , features["snout"])
	WRITE_FILE(S["feature_lizard_horns"] , features["horns"])
	WRITE_FILE(S["feature_human_ears"] , features["ears"])
	WRITE_FILE(S["feature_lizard_frills"] , features["frills"])
	WRITE_FILE(S["feature_lizard_spines"] , features["spines"])
	WRITE_FILE(S["feature_lizard_body_markings"] , features["body_markings"])
	WRITE_FILE(S["feature_lizard_legs"] , features["legs"])
	WRITE_FILE(S["feature_moth_wings"] , features["moth_wings"])
	WRITE_FILE(S["feature_moth_antennae"] , features["moth_antennae"])
	WRITE_FILE(S["feature_moth_markings"] , features["moth_markings"])*/
	WRITE_FILE(S["persistent_scars"] , persistent_scars)
	WRITE_FILE(S["foodlikes"] , foodlikes)
	WRITE_FILE(S["fooddislikes"] , fooddislikes)

	//Custom names
	for(var/custom_name_id in GLOB.preferences_custom_names)
		var/savefile_slot_name = custom_name_id + "_name" //TODO remove this
		WRITE_FILE(S[savefile_slot_name],custom_names[custom_name_id])

	WRITE_FILE(S["preferred_ai_core_display"] ,  preferred_ai_core_display)
	WRITE_FILE(S["prefered_security_department"] , prefered_security_department)

	//Jobs
	WRITE_FILE(S["joblessrole"] , joblessrole)
	//Write prefs
	WRITE_FILE(S["job_preferences"] , job_preferences)

	//Quirks
	WRITE_FILE(S["all_quirks"] , all_quirks)

	//All the new skyrat related stuff here
	WRITE_FILE(S["features"] , features)
	WRITE_FILE(S["mutant_bodyparts"] , mutant_bodyparts)
	WRITE_FILE(S["body_markings"] , body_markings)

	WRITE_FILE(S["loadout"] , loadout)

	WRITE_FILE(S["general_record"] , general_record)
	WRITE_FILE(S["security_record"] , security_record)
	WRITE_FILE(S["medical_record"] , medical_record)
	WRITE_FILE(S["background_info"] , background_info)
	WRITE_FILE(S["exploitable_info"] , exploitable_info)

	WRITE_FILE(S["mismatched_customization"] , mismatched_customization)
	WRITE_FILE(S["allow_advanced_colors"] , allow_advanced_colors)

	WRITE_FILE(S["augments"] , augments)
	WRITE_FILE(S["augment_limb_styles"] , augment_limb_styles)

	WRITE_FILE(S["undershirt_color"], undershirt_color)
	WRITE_FILE(S["socks_color"], socks_color)

	WRITE_FILE(S["pref_birthsign"] , pref_birthsign)

	WRITE_FILE(S["languages"], languages)
	WRITE_FILE(S["handedness"], handedness)
	WRITE_FILE(S["blood_type"], blood_type)

	return TRUE

/proc/sanitize_keybindings(value)
	var/list/base_bindings = sanitize_islist(value,list())
	for(var/key in base_bindings)
		base_bindings[key] = base_bindings[key] & GLOB.keybindings_by_name
		if(!length(base_bindings[key]))
			base_bindings -= key
	return base_bindings

#undef SAVEFILE_VERSION_MAX
#undef SAVEFILE_VERSION_MIN

#ifdef TESTING
//DEBUG
//Some crude tools for testing savefiles
//path is the savefile path
/client/verb/savefile_export(path as text)
	var/savefile/S = new /savefile(path)
	S.ExportText("/",file("[path].txt"))
//path is the savefile path
/client/verb/savefile_import(path as text)
	var/savefile/S = new /savefile(path)
	S.ImportText("/",file("[path].txt"))

#endif

repo_name: AnalWerewolf/sundown

license: mit


code:
TOGGLE_CHECKBOX(/datum/verbs/menu/settings, toggle_fullscreen)()
	set name = "Toggle Fullscreen"
	set category = "Preferences"
	set desc = "Toggles fullscreen."
	usr.client.prefs.septictoggles ^= TOGGLE_FULLSCREEN
	usr.client.prefs.save_preferences()
	to_chat(src, "Fullscreen [usr.client.prefs.septictoggles & TOGGLE_FULLSCREEN ? "enabled" : "disabled"].")
	usr.client.fullscreen()
	SSblackbox.record_feedback("nested tally", "fullscreen toggle", 1, list("Toggle Fullscreen", "[usr.client.prefs.septictoggles & TOGGLE_FULLSCREEN ? "Enabled" : "Disabled"]"))
/datum/verbs/menu/settings/toggle_fullscreen/Get_checked(client/C)
	return C.prefs.septictoggles & TOGGLE_FULLSCREEN


code:
#define POLITICAL_VERSION_MAX 1
#define POLITICAL_VERSION_MIN 1

/datum/political_compass
	var/path
	var/savefile_version = POLITICAL_VERSION_MAX
	var/classification = ""
	var/client/owner
	// 0 always equals center
	/// Left/right
	var/right_axis = 0
	/// Lib/auth
	var/auth_axis = 0
	/// Progressive/conservative
	var/prog_axis = 0
	/// List of questions associated with given answer - Not global
	var/list/questions_answered = list()

	var/static/max_rightaxis = 16
	var/static/min_rightaxis = -16
	var/static/max_authaxis = 15
	var/static/min_authaxis = -15
	var/static/max_progaxis = 16
	var/static/min_progaxis = -16
	/// List of questions associated with possible answers
	var/static/list/list/all_questions = list(
		"Freedom of business is the best practical way a society can prosper." = list("Agree", "Disagree"),
		"Charity is a better way of helping those in need than social welfare." = list("Agree", "Disagree"),
		"Wages are always fair, as employers know best what a worker's labour is worth." = list("Agree", "Disagree"),
		"It is human nature to be greedy." = list("Agree", "Disagree"),
		"\"Exploitation\" is an outdated term, as the struggles of 1800s capitalism doesn't exist anymore." = list("Agree", "Disagree"),
		"Communism is an ideal that can never work in practice." = list("Agree", "Disagree"),
		"Taxation of the wealthy is a bad idea, society would be better off without it." = list("Agree", "Disagree"),
		"The harder you work, the more you progress up the social ladder." = list("Agree", "Disagree"),
		"Organisations and corporations cannot be trusted and need regulating by the government." = list("Agree", "Disagree"),
		"A government that provides for everyone is an inherently good idea." = list("Agree", "Disagree"),
		"The current welfare system should be expanded to further combat inequality." = list("Agree", "Disagree"),
		"Land should not be a commodity to be bought and sold." = list("Agree", "Disagree"),
		"All industry and the bank should be nationalised." = list("Agree", "Disagree"),
		"Class is the primary division of society." = list("Agree", "Disagree"),
		"Economic inequality is too high in the world." = list("Agree", "Disagree"),
		"Sometimes it is right that the government may spy on its citizens to combat extremists and terrorists." = list("Agree", "Disagree"),
		"Authority figures, if morally correct, are a good thing for society." = list("Agree", "Disagree"),
		"Strength is necessary for any government to succeed." = list("Agree", "Disagree"),
		"Only the government can fairly and effectively regulate organisations." = list("Agree", "Disagree"),
		"Society requires structure and bureaucracy in order to function." = list("Agree", "Disagree"),
		"Mandatory IDs should be used to ensure public safety." = list("Agree", "Disagree"),
		"In times of crisis, safety becomes more important than civil liberties." = list("Agree", "Disagree"),
		"If you have nothing to hide, you have nothing to fear." = list("Agree", "Disagree"),
		"The government should be less involved in the day to day life of its citizens." = list("Agree", "Disagree"),
		"Without democracy, a society is nothing." = list("Agree", "Disagree"),
		"Jury nullification should be legal." = list("Agree", "Disagree"),
		"The smaller the government, the freer the people." = list("Agree", "Disagree"),
		"The government should, at most, provide emergency services and law enforcement." = list("Agree", "Disagree"),
		"The police was not made to protect the people, but to uphold the status-quo by force." = list("Agree", "Disagree"),
		"State schools are a bad idea because our state shouldn't be influencing our children." = list("Agree", "Disagree"),
		"Two consenting individuals should be able to do whatever they want with each other, even if it makes me uncomfortable." = list("Agree", "Disagree"),
		"An individual's body is their own property, and they should be able to do anything they desire to it." = list("Agree", "Disagree"),
		"A person should be able to worship whomever or whatever they want." = list("Agree", "Disagree"),
		"Nudism is perfectly natural." = list("Agree", "Disagree"),
		"Animals deserve certain universal rights." = list("Agree", "Disagree"),
		"Gender is a social construct, not a natural state of affairs." = list("Agree", "Disagree"),
		"Laws based on cultural values, rather than ethical ones, aren't justice." = list("Agree", "Disagree"),
		"Autonomy of body extends even to minors, the mentally ill, and serious criminals." = list("Agree", "Disagree"),
		"Homosexuality is against my values." = list("Agree", "Disagree"),
		"Transgender individuals should not be able to adopt children." = list("Agree", "Disagree"),
		"Drugs are harmful and should be banned." = list("Agree", "Disagree"),
		"The death penalty should exist for certain crimes." = list("Agree", "Disagree"),
		"Victimless crimes should still be punished." = list("Agree", "Disagree"),
		"One cannot be moral without religion." = list("Agree", "Disagree"),
		"Parents should hold absolute power over their children, as they are older and more experienced." = list("Agree", "Disagree"),
		"Multiculturalism is bad." = list("Agree", "Disagree"),
	)
	/// List of questions associated with answer valus (axis:value)
	var/static/list/list/questions_values = list(
		"Freedom of business is the best practical way a society can prosper." = list("right_axis1" =  1, "right_axis2" = -1),
		"Charity is a better way of helping those in need than social welfare." = list("right_axis1" =  1, "right_axis2" = -1),
		"Wages are always fair, as employers know best what a worker's labour is worth." = list("right_axis1" =  1, "right_axis2" = -1),
		"It is human nature to be greedy." = list("right_axis1" =  1, "right_axis2" = -1),
		"\"Exploitation\" is an outdated term, as the struggles of 1800s capitalism doesn't exist anymore." = list("right_axis1" =  1, "right_axis2" = -1),
		"Communism is an ideal that can never work in practice." = list("right_axis1" =  1, "right_axis2" = -1),
		"Taxation of the wealthy is a bad idea, society would be better off without it." = list("right_axis1" =  1, "right_axis2" = -1),
		"The harder you work, the more you progress up the social ladder." = list("right_axis1" =  1, "right_axis2" = -1),
		"Organisations and corporations cannot be trusted and need regulating by the government." = list("right_axis2" = -1, "right_axis1" =  1),
		"A government that provides for everyone is an inherently good idea." = list("right_axis2" = -1, "right_axis1" =  1),
		"The current welfare system should be expanded to further combat inequality." = list("right_axis2" = -1, "right_axis1" =  1),
		"Land should not be a commodity to be bought and sold." = list("right_axis2" = -1, "right_axis1" =  1),
		"All industry and the bank should be nationalised." = list("right_axis2" = -1, "right_axis1" =  1),
		"Class is the primary division of society." = list("right_axis2" = -1, "right_axis1" =  1),
		"Economic inequality is too high in the world." = list("right_axis2" = -1, "right_axis1" =  1),
		"Sometimes it is right that the government may spy on its citizens to combat extremists and terrorists." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"Authority figures, if morally correct, are a good thing for society." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"Strength is necessary for any government to succeed." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"Only the government can fairly and effectively regulate organisations." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"Society requires structure and bureaucracy in order to function." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"Mandatory IDs should be used to ensure public safety." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"In times of crisis, safety becomes more important than civil liberties." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"If you have nothing to hide, you have nothing to fear." = list("auth_axis2" = 1, "auth_axis1" = -1),
		"The government should be less involved in the day to day life of its citizens." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"Without democracy, a society is nothing." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"Jury nullification should be legal." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"The smaller the government, the freer the people." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"The government should, at most, provide emergency services and law enforcement." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"The police was not made to protect the people, but to uphold the status-quo by force." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"State schools are a bad idea because our state shouldn't be influencing our children." = list("auth_axis1" = -1, "auth_axis2" = 1),
		"Two consenting individuals should be able to do whatever they want with each other, even if it makes me uncomfortable." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"An individual's body is their own property, and they should be able to do anything they desire to it." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"A person should be able to worship whomever or whatever they want." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Nudism is perfectly natural." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Animals deserve certain universal rights." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Gender is a social construct, not a natural state of affairs." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Laws based on cultural values, rather than ethical ones, aren't justice." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Autonomy of body extends even to minors, the mentally ill, and serious criminals." = list("prog_axis2" = 1, "prog_axis1" = -1),
		"Homosexuality is against my values." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"Transgender individuals should not be able to adopt children." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"Drugs are harmful and should be banned." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"The death penalty should exist for certain crimes." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"Victimless crimes should still be punished." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"One cannot be moral without religion." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"Parents should hold absolute power over their children, as they are older and more experienced." = list("prog_axis1" = -1, "prog_axis2" = 1),
		"Multiculturalism is bad." = list("prog_axis1" = -1, "prog_axis2" = 1),
	)

/datum/political_compass/proc/calculate()
	right_axis = 0
	auth_axis = 0
	prog_axis = 0
	for(var/question in questions_answered)
		var/list/qlist = all_questions[question]
		if(!LAZYLEN(qlist))
			continue
		var/answer = questions_answered[question]
		if(!answer)
			continue
		var/aindex = qlist.Find(answer)
		if(!aindex)
			continue
		var/list/values = questions_values[question]
		if(!LAZYLEN(values))
			continue
		var/axis = LAZYACCESS(values, aindex)
		if(!axis)
			continue
		var/value = LAZYACCESS(values, axis)
		if(!value)
			continue
		switch(axis)
			if("right_axis1", "right_axis2")
				right_axis += value
			if("auth_axis1", "auth_axis2")
				auth_axis += value
			if("prog_axis1", "prog_axis2")
				prog_axis += value

/datum/political_compass/proc/get_result()
	if(!length(questions_answered))
		return "Apolitical Retard"
	var/progressive = "Centrist"
	var/authoritarian = "Centrist"
	var/rightist = "Centrist"
	var/final = ""
	switch(prog_axis/max_progaxis * 100)
		if(-20 to 20)
			progressive = "Centrist"
		if(-100 to -20)
			progressive = "Traditional"
		if(20 to 100)
			progressive = "Progressive"
	switch(auth_axis/max_authaxis * 100)
		if(-20 to 20)
			authoritarian = "Centrist"
		if(-100 to -20)
			authoritarian = "Libertarian"
		if(20 to 100)
			authoritarian = "Authoritarian"
	switch(right_axis/max_rightaxis * 100)
		if(-20 to 20)
			rightist = "Centrist"
		if(-100 to -20)
			rightist = "Left"
		if(20 to 100)
			rightist = "Right"
	final = "[progressive] [authoritarian] [rightist]"
	if(final == "Centrist Centrist Centrist")
		final = "Enlightened Centrist"
	return final

/datum/political_compass/proc/get_flag()
	if(!length(questions_answered))
		return
	if(isnull(classification))
		return "truecenter"
	var/authoritarian = "center"
	var/rightist = "center"
	var/icon_state = ""
	switch(auth_axis/max_authaxis * 100)
		if(-20 to 20)
			authoritarian = "center"
		if(-100 to -20)
			authoritarian = "lib"
		if(20 to 100)
			authoritarian = "auth"
	switch(right_axis/max_rightaxis * 100)
		if(-20 to 20)
			rightist = "center"
		if(-100 to -20)
			rightist = "left"
		if(20 to 100)
			rightist = "right"
	if((rightist == "center") && (authoritarian == "center"))
		return "truecenter"
	icon_state = "[authoritarian][rightist]"
	return icon_state

/datum/political_compass/proc/get_color()
	if(!length(questions_answered))
		return
	if(isnull(classification))
		return COLOR_VERY_LIGHT_GRAY
	var/flag = get_flag()
	switch(flag)
		if("libright")
			return COLOR_VIVID_YELLOW
		if("libleft")
			return COLOR_VIBRANT_LIME
		if("authright")
			return COLOR_DARK_CYAN
		if("authleft")
			return COLOR_MOSTLY_PURE_RED
		if("centerright")
			return COLOR_BLUE
		if("centerleft")
			return COLOR_GREEN
		if("authcenter")
			return COLOR_CYAN
		if("libcenter")
			return COLOR_VIOLET
		if("truecenter")
			return COLOR_VERY_LIGHT_GRAY
		else
			return COLOR_VERY_LIGHT_GRAY

/datum/political_compass/proc/load_path()
	if(!owner?.ckey)
		return
	var/ckey = owner.ckey
	path = "data/player_saves/[ckey[1]]/[ckey]/political.sav"

/datum/political_compass/proc/load_save()
	if(!path)
		return FALSE
	if(!fexists(path))
		return FALSE
	var/savefile/save = new /savefile(path)
	if(!save)
		return FALSE
	save.cd = "/"

	var/needs_update = savefile_needs_update(save)
	if(needs_update == -2) //fatal, can't load any data
		var/bacpath = "[path].updatebac" //todo: if the savefile version is higher then the server, check the backup, and give the player a prompt to load the backup
		if (fexists(bacpath))
			fdel(bacpath) //only keep 1 version of backup
		fcopy(save, bacpath) //byond helpfully lets you use a savefile for the first arg.
		return FALSE
	READ_FILE(save["questions_answered"], questions_answered)
	questions_answered = SANITIZE_LIST(questions_answered)
	if(needs_update >= 0)
		update_savefile(savefile_version, save)

/datum/political_compass/proc/save_save()
	if(!path)
		return FALSE
	var/savefile/save = new /savefile(path)
	if(!save)
		return FALSE
	save.cd = "/"

	WRITE_FILE(save["savefile_version"], POLITICAL_VERSION_MAX)
	WRITE_FILE(save["questions_answered"], questions_answered)

/datum/political_compass/proc/savefile_needs_update(savefile/save)
	var/savefile_version
	READ_FILE(save["savefile_version"], savefile_version)

	if(savefile_version < POLITICAL_VERSION_MIN)
		save.dir.Cut()
		return -2
	if(savefile_version < POLITICAL_VERSION_MAX)
		return savefile_version
	return -1

/datum/political_compass/proc/update_savefile(savefile_version = POLITICAL_VERSION_MAX, savefile/save)
	return

#undef POLITICAL_VERSION_MAX
#undef POLITICAL_VERSION_MIN


code:
/datum/political_compass/ui_interact(mob/user, datum/tgui/ui)
	. = ..()
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "PoliticalCompass")
		ui.open()

/datum/political_compass/ui_state(mob/user)
	return GLOB.always_state

/datum/political_compass/ui_data(mob/user)
	var/list/data = list()
	var/list/questions = list()
	for(var/question in all_questions)
		var/list/this_question = list()
		this_question["question"] = question
		var/qid = all_questions.Find(question)
		this_question["qid"] = qid
		var/list/unparsed_answers = all_questions[question]
		var/list/answers = list()
		for(var/answer in all_questions[question])
			var/list/this_answer = list()
			this_answer["answer"] = answer
			var/aid = unparsed_answers.Find(answer)
			this_answer["aid"] = aid
			this_answer["selected"] = (questions_answered[question] == answer ? TRUE : FALSE)

			answers += list(this_answer)
		this_question["answers"] = answers
		this_question["unanswered"] = !questions_answered[question]

		questions += list(this_question)
	data["questions"] = questions
	data["final_result"] = get_result()
	data["final_color"] = get_color()
	var/flag = get_flag()
	if(flag)
		var/icon/iconflag = icon('modular_septic/icons/ui_icons/chat/ooc.dmi', get_flag())
		data["flagicon64"] = icon2base64(iconflag)
	return data

/datum/political_compass/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	. = ..()
	switch(action)
		if("reset")
			reset()
			. = TRUE
		if("answerquestion")
			var/question = LAZYACCESS(all_questions, params["question_id"])
			var/answer = LAZYACCESS(all_questions[question], params["answer_id"])
			if(answer)
				questions_answered[question] = answer
			update_and_save()
			. = TRUE
		if("removeanswer")
			var/question = LAZYACCESS(all_questions, params["question_id"])
			questions_answered -= question
			update_and_save()
			. = TRUE

/datum/political_compass/ui_close(mob/user)
	. = ..()
	update_and_save()

/datum/political_compass/proc/reset()
	classification = ""
	right_axis = 0
	auth_axis = 0
	prog_axis = 0
	questions_answered = list()
	save_save()

/datum/political_compass/proc/update()
	calculate()
	classification = get_result()

/datum/political_compass/proc/update_and_save()
	update()
	save_save()


code:
/datum/preferences
	/// Supporting table, default is 5:
	/// Byond supporter: +5
	/// Patreon supporter: +5
	max_save_slots = 5
	/// Gives us our donator tier, if we are a donator
	var/donator_rank
	/// Preview type
	var/character_preview_type = PREVIEW_PREF_JOB
	/// Associative list, keyed by language typepath, pointing to LANGUAGE_UNDERSTOOD, or LANGUAGE_SPOKEN, for whether we understand or speak the language
	var/list/languages = list()
	/// List of chosen augmentations - It's an associative list with key name of the slot, pointing to a typepath of an augment datum
	var/list/augments = list()
	/// List of chosen preferred styles for limb replacements - Associative list, augment slot to augment style
	var/list/augment_styles = list()
	/// A list of all body markings
	var/list/list/body_markings = list()
	/// Our currently chosen birthsign type
	var/datum/cultural_info/birthsign/birthsign

/datum/preferences/New(client/C)
	. = ..()
	max_save_slots = 5
	if(C.IsByondMember())
		max_save_slots += 5
	var/is_donator = C.is_donator()
	if(is_donator)
		SSdonators.add_donator(C.ckey, is_donator)
		donator_rank = is_donator
		max_save_slots += 5

/datum/preferences/ui_static_data(mob/user)
	var/list/data = ..()
	data["donator_rank"] = donator_rank
	data["maximum_customization_points"] = MAXIMUM_CUSTOMIZATION_POINTS
	return data

/datum/preferences/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	. = ..()
	if(.)
		return

	switch(action)
		if("set_preference")
			var/requested_preference_key = params["preference"]
			var/value = params["value"]
			for(var/datum/preference_middleware/preference_middleware as anything in middleware)
				if(preference_middleware.post_set_preference(usr, requested_preference_key, value))
					return TRUE
		if("rotateleft")
			character_preview_view.dir = turn(character_preview_view.dir, 90)
			return TRUE
		if("rotateright")
			character_preview_view.dir = turn(character_preview_view.dir, -90)
			return TRUE
		if("character_preview_update")
			return TRUE
		if("set_tricolor_preference")
			var/requested_preference_key = params["preference"]
			var/requested_preference_index = params["preferenceindex"]

			var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
			if(isnull(requested_preference))
				return FALSE

			if(!istype(requested_preference, /datum/preference/tri_color) )
				return FALSE

			var/list/color_list = read_preference(requested_preference.type)
			var/default_value = sanitize_hexcolor(color_list[requested_preference_index], 6, TRUE)

			// Yielding
			var/new_color = input(
				usr,
				"Select new color",
				null,
				default_value || COLOR_WHITE,
			) as color | null

			if(!new_color)
				return FALSE

			var/list/new_color_list = list(
				(requested_preference_index == 1 ? new_color : color_list[1]),
				(requested_preference_index == 2 ? new_color : color_list[2]),
				(requested_preference_index == 3 ? new_color : color_list[3])
			)

			if(!update_preference(requested_preference, new_color_list))
				return FALSE

			return TRUE
		if("set_tricolor_default_colors")
			var/requested_preference_key = params["preference"]

			var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
			if(isnull(requested_preference))
				return FALSE

			if(!istype(requested_preference, /datum/preference/tri_color))
				return FALSE

			var/list/new_color_list = requested_preference.create_informed_default_value(src)
			if(!update_preference(requested_preference, new_color_list))
				return FALSE

			return TRUE
		if("set_tricolor_mutant_colors")
			var/requested_preference_key = params["preference"]

			var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
			if(isnull(requested_preference))
				return FALSE

			if(!istype(requested_preference, /datum/preference/tri_color))
				return FALSE

			var/list/new_color_list = read_preference(/datum/preference/tri_color/mutant_colors)
			if(!update_preference(requested_preference, new_color_list))
				return FALSE

			return TRUE
		if("set_color_default_colors")
			var/requested_preference_key = params["preference"]

			var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
			if(isnull(requested_preference))
				return FALSE

			if(!istype(requested_preference, /datum/preference/color))
				return FALSE

			var/new_color = requested_preference.create_informed_default_value(src)
			if(!update_preference(requested_preference, new_color))
				return FALSE

			return TRUE
		if("set_color_mutant_colors")
			var/requested_preference_key = params["preference"]

			var/datum/preference/requested_preference = GLOB.preference_entries_by_key[requested_preference_key]
			if(isnull(requested_preference))
				return FALSE

			if(!istype(requested_preference, /datum/preference/color))
				return FALSE

			var/list/new_color_list = read_preference(/datum/preference/tri_color/mutant_colors)
			if(!update_preference(requested_preference, new_color_list[1]))
				return FALSE

			return TRUE

/datum/preferences/apply_all_client_preferences()
	for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
		if (preference.savefile_identifier != PREFERENCE_PLAYER)
			continue

		value_cache -= preference.type
		var/read_value = read_preference(preference.type)
		if(preference.should_apply_to_client(parent, read_value, src))
			preference.apply_to_client(parent, read_value, src)

/datum/preferences/apply_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE)
	character.dna.features = MANDATORY_FEATURE_LIST
	character.underwear = "Nude"
	character.socks = "Nude"
	character.undershirt = "Nude"
	var/has_bad_preference = FALSE
	for(var/datum/preference/preference as anything in get_preferences_in_priority_order())
		if(preference.savefile_identifier != PREFERENCE_CHARACTER)
			continue

		var/read_value = read_preference(preference.type)
		if(preference.should_apply_to_human(character, read_value, src))
			preference.apply_to_human(character, read_value, src)
			if(!has_bad_preference && preference.is_bad_preference(parent, read_value, src))
				has_bad_preference = TRUE

	character.dna.real_name = character.real_name
	character.regenerate_limbs(FALSE)
	character.assimilate_dna_to_species()
	character.dna.species.regenerate_organs(character, character.dna.species, TRUE)
	character.build_all_organs_from_dna()

	//Extremely snowflake, i know
	var/genitals_type = read_preference(/datum/preference/choiced/genitals)
	if(GLOB.genital_sets[genitals_type][ORGAN_SLOT_WOMB])
		var/obj/item/organ/genital/womb/womb = new()
		womb.Insert(character, FALSE, FALSE)

	//Apply markings/augments after everything else is done
	for(var/datum/preference_middleware/preference_middleware as anything in middleware)
		preference_middleware.apply_to_human(character, src)

	character.dna.update_body_size()
	if(icon_updates)
		character.update_body()
		character.update_hair()
		character.update_body_parts()
		character.update_mutations_overlay()
	if(has_bad_preference)
		character.gain_trauma(pick(/datum/brain_trauma/mild/stuttering, \
								/datum/brain_trauma/mild/dumbness, \
								/datum/brain_trauma/mild/speech_impediment, \
								/datum/brain_trauma/mild/expressive_aphasia))

/datum/preferences/update_preference(datum/preference/preference, preference_value)
	if(!preference.is_accessible(src))
		return FALSE

	var/new_value = preference.deserialize(preference_value, src)
	var/success = preference.write(null, new_value)

	if(!success)
		return FALSE

	recently_updated_keys |= preference.type
	value_cache[preference.type] = new_value

	if(preference.savefile_identifier == PREFERENCE_PLAYER)
		preference.apply_to_client_updated(parent, read_preference(preference.type), src)
	else
		character_preview_view?.update_body()

	if(preference.is_bad_preference(parent, new_value, src))
		var/chat_warning = preference.bad_preference_warning(parent, new_value, src)
		if(chat_warning)
			to_chat(parent, chat_warning)

	return TRUE

/datum/preferences/GetQuirkBalance()
	var/bal = MAXIMUM_CUSTOMIZATION_POINTS
	for(var/quirk_type in all_quirks)
		var/datum/quirk/quirk = SSquirks.quirks[quirk_type]
		bal -= initial(quirk.value)
	for(var/augment_slot in augments)
		var/augment_type = augments[augment_slot]
		var/datum/augment_item/augment_item = GLOB.augment_item_datums[augment_type]
		bal -= initial(augment_item.value)
	for(var/language_type in languages)
		bal -= (languages[language_type] == LANGUAGE_SPOKEN ? LANGUAGE_COST_SPOKEN : LANGUAGE_COST_UNDERSTOOD)
	return bal

/datum/preferences/proc/get_features()
	var/list/features = list()

	var/list/mutant_colors = read_preference(/datum/preference/tri_color/mutant_colors)
	var/uses_skintones = read_preference(/datum/preference/toggle/skin_tone)
	var/skin_tone = read_preference(/datum/preference/choiced/skin_tone)

	features["mcolor"] = LAZYACCESS(mutant_colors, 1)
	features["mcolor2"] = LAZYACCESS(mutant_colors, 2)
	features["mcolor3"] = LAZYACCESS(mutant_colors, 3)
	features["uses_skintones"] = uses_skintones
	features["skin_tone"] = skin_tone
	features["skin_color"] = skintone2hex(skin_tone)

	return features

/datum/preferences/proc/read_preference_if_accessible(preference_type)
	var/datum/preference/preference_entry = GLOB.preference_entries[preference_type]
	if (isnull(preference_entry))
		var/extra_info = ""

		// Current initializing subsystem is important to know because it might be a problem with
		// things running pre-assets-initialization.
		if (!isnull(Master.current_initializing_subsystem))
			extra_info = "Info was attempted to be retrieved while [Master.current_initializing_subsystem] was initializing."

		CRASH("Preference type `[preference_type]` is invalid! [extra_info]")
	if(preference_entry.is_accessible(src))
		return read_preference(preference_type)


code:
// Use skintones comes before mutant colors
#define PREFERENCE_PRIORITY_USE_SKINTONES 6
// Mutant colors come before mutant parts
#define PREFERENCE_PRIORITY_MUTANT_COLORS 7
// Mutant parts should be almost the last thing to mess with
#define PREFERENCE_PRIORITY_MUTANT_PART 8
// Genital colors come after other mutant colors
#define PREFERENCE_PRIORITY_GENITAL_COLOR 9
// Genital customization is last, after creating the genitals
#define PREFERENCE_PRIORITY_GENITAL_PART 10

// 6 hex colors
/datum/preference/color/is_valid(value)
	return findtext(sanitize_hexcolor(value, 6, TRUE), GLOB.is_color)

/datum/preference/color/deserialize(input, datum/preferences/preferences)
	return sanitize_hexcolor(input, 6, TRUE)

/datum/preference/color/create_default_value()
	return random_color()

// Triple colors
/datum/preference/tri_color
	abstract_type = /datum/preference/tri_color

/datum/preference/tri_color/create_default_value()
	return list("#FFFFFF", "#FFFFFF", "#FFFFFF")

/datum/preference/tri_color/is_valid(value)
	. = TRUE
	var/list/value_list = value
	if(!istype(value_list))
		return FALSE
	if(LAZYLEN(value) != 3)
		return FALSE
	if(!(findtext(sanitize_hexcolor(value[1], 6, TRUE), GLOB.is_color) && findtext(sanitize_hexcolor(value[2], 6, TRUE), GLOB.is_color) && findtext(sanitize_hexcolor(value[3], 6, TRUE), GLOB.is_color)))
		return FALSE

/datum/preference/tri_color/deserialize(input, datum/preferences/preferences)
	var/list/input_colors = input
	return list(sanitize_hexcolor(input_colors[1], 6, TRUE), sanitize_hexcolor(input_colors[2], 6, TRUE), sanitize_hexcolor(input_colors[3], 6, TRUE))

// Text input
/datum/preference/text
	abstract_type = /datum/preference/text

/datum/preference/text/deserialize(input, datum/preferences/preferences)
	return STRIP_HTML_SIMPLE(input, MAX_FLAVOR_LEN)

/datum/preference/text/create_default_value()
	return ""

/datum/preference/text/is_valid(value)
	return istext(value)

// Mismatched parts support
/datum/preference/is_accessible(datum/preferences/preferences)
	SHOULD_CALL_PARENT(TRUE)

	if(!isnull(relevant_mutant_bodypart) || !isnull(relevant_species_trait))
		var/species_type = preferences.read_preference(/datum/preference/choiced/species)
		var/datum/species/species = new species_type
		if(!(savefile_key in species.get_features()) && !preferences.read_preference(/datum/preference/toggle/mismatched_parts))
			qdel(species)
			return FALSE
		qdel(species)

	if(!should_show_on_page(preferences.current_window))
		return FALSE

	return TRUE

// Adds preferences argument for ease of use
/datum/preference/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences)
	SHOULD_CALL_PARENT(FALSE)
	CRASH("`apply_to_human()` was not implemented for [type]!")

/datum/preference/apply_to_client(client/client, value, datum/preferences/preferences)
	SHOULD_CALL_PARENT(FALSE)

/datum/preference/apply_to_client_updated(client/client, value, datum/preferences/preferences)
	apply_to_client(client, value, preferences)

/datum/preference/proc/should_apply_to_client(client/client, value, datum/preferences/preferences)
	if(savefile_identifier != PREFERENCE_CHARACTER)
		return TRUE
	return FALSE

/datum/preference/proc/should_apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences)
	if(savefile_identifier != PREFERENCE_PLAYER)
		return is_accessible(preferences)
	return FALSE

/datum/preference/proc/is_bad_preference(client/client, value, datum/preferences/preferences)
	return FALSE

/datum/preference/proc/bad_preference_warning(client/client, value, datum/preferences/preferences)
	return div_infobox(span_animatedpain("Uh oh this is a bad preference. You sure?"))


code:
/datum/preference/choiced/body_type
	category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
	priority = PREFERENCE_PRIORITY_BODY_TYPE
	savefile_key = "body_type"
	savefile_identifier = PREFERENCE_CHARACTER

/datum/preference/choiced/body_type/init_possible_values()
	return list("Masculine", "Feminine")

/datum/preference/choiced/body_type/create_informed_default_value(datum/preferences/preferences)
	var/gender = preferences?.read_preference(/datum/preference/choiced/gender)
	if(gender)
		if(gender == FEMALE)
			return "Feminine"
		else
			return "Masculine"
	return pick(get_choices())

/datum/preference/choiced/body_type/apply_to_human(mob/living/carbon/human/target, value)
	target.body_type = (value == "Masculine" ? MALE : FEMALE)


code:
/datum/preference/choiced/ethereal_color/is_accessible(datum/preferences/preferences)
	. = ..()
	if(!.)
		return
	return (preferences?.read_preference(/datum/preference/choiced/species) == /datum/species/ethereal)


code:
/datum/preference/color/eye_color
	savefile_key = "left_eye_color"
	savefile_identifier = PREFERENCE_CHARACTER
	category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
	relevant_species_trait = EYECOLOR

/datum/preference/color/eye_color/create_default_value()
	return random_color()

/datum/preference/color/eye_color/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences)
	target.left_eye_color = value
	for(var/obj/item/organ/eyes/eyes_organ in target.internal_organs)
		if(eyes_organ.side != LEFT_SIDE)
			continue
		if(!initial(eyes_organ.eye_color))
			eyes_organ.eye_color = value
		eyes_organ.old_eye_color = value

/datum/preference/color/eye_color/right
	savefile_key = "right_eye_color"
	savefile_identifier = PREFERENCE_CHARACTER
	category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
	relevant_species_trait = EYECOLOR

/datum/preference/color/eye_color/right/create_default_value()
	return random_color()

/datum/preference/color/eye_color/right/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences)
	target.right_eye_color = value
	for(var/obj/item/organ/eyes/eyes_organ in target.internal_organs)
		if(eyes_organ.side != RIGHT_SIDE)
			continue
		if(!initial(eyes_organ.eye_color))
			eyes_organ.eye_color = value
		eyes_organ.old_eye_color = value


code:
/datum/preference/toggle/filmgrain
	category = PREFERENCE_CATEGORY_GAME_PREFERENCES
	savefile_key = "filmgrainpref"
	savefile_identifier = PREFERENCE_PLAYER
	default_value = TRUE

// This gets applied when the hud gets created
/datum/preference/toggle/filmgrain/apply_to_client(client/client, value)
	return


code:
/datum/preference/toggle/fullscreen
	category = PREFERENCE_CATEGORY_GAME_PREFERENCES
	savefile_key = "fullscreenpref"
	savefile_identifier = PREFERENCE_PLAYER
	default_value = TRUE

/datum/preference/toggle/fullscreen/apply_to_client(client/client, value)
	client.do_fullscreen(value)


code:
/// Genitals preference
/datum/preference/choiced/genitals
	savefile_identifier = PREFERENCE_CHARACTER
	savefile_key = "genitals"
	priority = PREFERENCE_PRIORITY_MUTANT_PART
	can_randomize = FALSE

/datum/preference/choiced/genitals/create_informed_default_value(datum/preferences/preferences)
	var/gender = preferences.read_preference(/datum/preference/choiced/gender)
	switch(gender)
		if(MALE)
			return GENITALS_MALE
		if(FEMALE)
			return GENITALS_FEMALE
		else
			return GENITALS_MALE

/datum/preference/choiced/genitals/is_accessible(datum/preferences/preferences)
	. = ..()
	if(!.)
		return
	var/species_type = preferences.read_preference(/datum/preference/choiced/species)
	var/datum/species/species = new species_type()
	if(AGENDER in species.species_traits)
		. = FALSE
	var/list/genitals_possible = list()
	genitals_possible |= species.default_genitals_male
	genitals_possible |= species.default_genitals_female
	if(!LAZYLEN(genitals_possible))
		. = FALSE
	qdel(species)

/datum/preference/choiced/genitals/init_possible_values()
	. = list()
	for(var/thing in GLOB.genital_sets)
		. += thing

/datum/preference/choiced/genitals/apply_to_human(mob/living/carbon/human/target, value, datum/preferences/preferences)
	target.genitals = value


code:
/datum/preference/choiced/handedness
	category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
	priority = PREFERENCE_PRIORITY_BODY_TYPE
	savefile_key = "handedness"
	savefile_identifier = PREFERENCE_CHARACTER

/datum/preference/choiced/handedness/init_possible_values()
	return list("Right Handed", "Left Handed", "Poorly Ambidextrous")

/datum/preference/choiced/handedness/create_default_value()
	return "Right Handed"

/datum/preference/choiced/handedness/apply_to_human(mob/living/carbon/human/target, value)
	target.handed_flags = unparse_handedness(value)


code:
/proc/possible_values_for_sprite_accessory_list(list/sprite_accessories, direction = SOUTH)
	var/list/possible_values = list()
	for(var/key in sprite_accessories)
		var/datum/sprite_accessory/sprite_accessory = sprite_accessories[key]
		if(istype(sprite_accessory) && \
			sprite_accessory.icon && \
			sprite_accessory.icon_state && \
			(sprite_accessory.icon_state != "none") )
			var/icon/final_icon
			if(!istype(sprite_accessory, /datum/sprite_accessory/hair) && !istype(sprite_accessory, /datum/sprite_accessory/facial_hair))
				var/list/icon_states = icon_states(sprite_accessory.icon)
				for(var/layer in sprite_accessory.relevant_layers)
					var/final_icon_state = ""
					if(sprite_accessory.gender_specific)
						final_icon_state = "male_"
					else
						final_icon_state = "m_"
					final_icon_state += "[sprite_accessory.key]_[sprite_accessory.icon_state]"
					var/append_text = ""
					switch(layer)
						if(BODY_FRONT_LAYER)
							append_text = "FRONT"
						if(BODY_ADJ_LAYER)
							append_text = "ADJ"
						if(BODY_BEHIND_LAYER)
							append_text = "BEHIND"
					if(append_text)
						final_icon_state += "_[append_text]"
					if(final_icon_state in icon_states)
						if(!final_icon)
							final_icon = icon(sprite_accessory.icon, final_icon_state, direction)
						else
							final_icon.Blend(icon(sprite_accessory.icon, final_icon_state, direction), ICON_OVERLAY)
			else
				final_icon = icon(sprite_accessory.icon, sprite_accessory.icon_state, direction)
			// This means it didn't have a proper final icon state
			if(!final_icon)
				final_icon = icon('modular_septic/icons/mob/human/sprite_accessory/mutant_bodyparts.dmi', "none", direction)
			possible_values[key] = final_icon
		else
			// This means it didn't have an icon state at all
			possible_values[key] = icon('modular_septic/icons/mob/human/sprite_accessory/mutant_bodyparts.dmi', "none", direction)
	return possible_values

/proc/generate_possible_values_for_sprite_accessories_on_head(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/head_icon = icon(DEFAULT_BODYPART_ICON_ORGANIC, "human_head_m", direction)
	head_icon.Blend("#[skintone2hex("caucasian1")]", ICON_MULTIPLY)

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(head_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(10, 22, 22, 32)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values

/proc/generate_possible_values_for_sprite_accessories_on_moth_head(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/head_icon = icon('modular_septic/icons/mob/human/species/insect/moth_parts.dmi', "moth_head_m", direction)

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(head_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(10, 22, 22, 32)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values

/proc/generate_possible_values_for_sprite_accessories_on_chest(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/chest_icon = icon(DEFAULT_BODYPART_ICON_ORGANIC, "human_chest_m")

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(chest_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(6, 9, 27, 24)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values

/proc/generate_possible_values_for_sprite_accessories_on_moth_chest(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/chest_icon = icon('modular_septic/icons/mob/human/species/insect/moth_parts.dmi', "moth_chest_m", direction)

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(chest_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(6, 9, 27, 24)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values

/proc/generate_possible_values_for_sprite_accessories_on_groin(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/groin_icon = icon(DEFAULT_BODYPART_ICON_ORGANIC, "human_groin_m", direction)
	for(var/icon_state in list("human_r_leg", "human_r_foot", "human_l_leg", "human_l_foot"))
		groin_icon.Blend(icon(DEFAULT_BODYPART_ICON_ORGANIC, icon_state, direction), ICON_OVERLAY)
	groin_icon.Blend("#[skintone2hex("caucasian1")]", ICON_MULTIPLY)

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(groin_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(8, 1, 24, 12)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values

/proc/generate_possible_values_for_sprite_accessories_on_groin_with_tail(accessories, direction = SOUTH)
	var/list/values = possible_values_for_sprite_accessory_list(accessories, direction)

	var/icon/groin_icon = icon(DEFAULT_BODYPART_ICON_ORGANIC, "human_groin_m", direction)
	for(var/icon_state in list("human_r_leg", "human_r_foot", "human_l_leg", "human_l_foot"))
		groin_icon.Blend(icon(DEFAULT_BODYPART_ICON_ORGANIC, icon_state, direction), ICON_OVERLAY)
	groin_icon.Blend("#[skintone2hex("caucasian1")]", ICON_MULTIPLY)
	var/datum/sprite_accessory/tails/lizard/smooth/lizard_tail = GLOB.sprite_accessories["tail"]["Smooth"]
	var/icon/tail_icon = icon(lizard_tail.icon, "m_tail_smooth_FRONT")
	var/tail_preview_color = lizard_tail.get_preview_color()
	//Color matrix
	if(islist(tail_preview_color))
		tail_icon.MapColors(
				tail_preview_color[1],
				tail_preview_color[2],
				tail_preview_color[3]
				)
	//Not color matrix
	else
		tail_icon.Blend(tail_preview_color, ICON_MULTIPLY)
	groin_icon.Blend(tail_icon, ICON_OVERLAY)

	for(var/key in values)
		var/datum/sprite_accessory/accessory = accessories[key]
		if(!istype(accessory) || !accessory.icon_state || (accessory.icon_state == "none"))
			continue

		var/icon/final_icon = new(groin_icon)

		var/icon/accessory_icon = values[key]
		var/accessory_preview_color = accessory.get_preview_color()
		//Color matrix
		if(islist(accessory_preview_color))
			accessory_icon.MapColors(
				accessory_preview_color[1],
				accessory_preview_color[2],
				accessory_preview_color[3]
				)
		//Not color matrix
		else
			accessory_icon.Blend(accessory_preview_color, ICON_MULTIPLY)

		if(accessory_icon)
			final_icon.Blend(accessory_icon, ICON_OVERLAY)

		final_icon.Crop(9, 1, 23, 12)
		final_icon.Scale(32, 32)

		values[key] = final_icon

	return values


code:
/// Called after every update_preference, returns TRUE if this handled it
/datum/preference_middleware/proc/post_set_preference(mob/user, preference, value)
	return FALSE

/// Called when applying preferences to the mob
/datum/preference_middleware/proc/apply_to_human(mob/living/carbon/human/target, datum/preferences/preferences)
	SHOULD_NOT_SLEEP(TRUE)
	SHOULD_CALL_PARENT(FALSE)
	return


code:
/// Middleware to handle augmentations
/datum/preference_middleware/augments
	action_delegations = list(
		"add_augment" = .proc/add_augment,
		"remove_augment" = .proc/remove_augment,
		"set_augment_style" = .proc/set_augment_style,
	)

/datum/preference_middleware/augments/New(datum/preferences)
	. = ..()
	// Run this at least once to verify if everything is valid here
	if(src.preferences)
		get_ui_data()

/datum/preference_middleware/augments/on_new_character(mob/user)
	. = ..()
	// Run this at least once to verify if everything is valid here
	get_ui_data()

/datum/preference_middleware/augments/get_ui_data(mob/user)
	var/list/data = list()

	// How?
	if(!islist(preferences.augments))
		preferences.augments = list()
	if(!islist(preferences.augment_styles))
		preferences.augment_styles = list()

	var/augment_balance = preferences.GetQuirkBalance()
	// Something is fucked
	if(augment_balance < 0)
		preferences.augments = list()
		preferences.augment_styles = list()
		augment_balance = preferences.GetQuirkBalance()
	var/list/unselected_augments = list()
	for(var/category in GLOB.augment_categories_to_slots_to_items)
		var/list/this_category = list()
		var/list/this_category_slots = list()

		this_category["name"] = category

		for(var/slot in GLOB.augment_categories_to_slots_to_items[category])
			var/list/this_slot = list()
			var/list/this_slot_items = list()

			this_slot["name"] = slot

			for(var/item_type in GLOB.augment_categories_to_slots_to_items[category][slot])
				if(preferences.augments[slot] == item_type)
					continue
				var/list/this_item = list()

				var/datum/augment_item/augment_item = GLOB.augment_item_datums[item_type]
				this_item["category"] = augment_item.category
				this_item["slot"] = augment_item.slot
				this_item["name"] = augment_item.name
				this_item["description"] = augment_item.description
				this_item["can_use_styles"] = augment_item.uses_robotic_styles
				this_item["value"] = augment_item.value
				this_item["cant_buy"] = (LAZYACCESS(preferences.augments, slot) ? "Slot occupied!" : ((augment_balance-augment_item.value < 0) ? "Not enough points!" : "") )

				this_slot_items += list(this_item)

			this_slot["items"] = this_slot_items
			this_category_slots += list(this_slot)

		this_category["slots"] = this_category_slots
		unselected_augments += list(this_category)
	var/list/our_augments = list()
	for(var/slot in preferences.augments)
		var/list/this_slot = list()
		var/list/this_slot_item = list()
		var/datum/augment_item/augment_item = GLOB.augment_item_datums[preferences.augments[slot]]

		this_slot_item["category"] = augment_item.category
		this_slot_item["slot"] = augment_item.slot
		this_slot_item["name"] = augment_item.name
		this_slot_item["description"] = augment_item.description
		this_slot_item["can_use_styles"] = augment_item.uses_robotic_styles
		this_slot_item["value"] = augment_item.value
		this_slot_item["cant_buy"] = ((augment_balance+augment_item.value < 0) ? "Not enough points!" : "") //we already bought it, so this means we cannot remove it

		this_slot["name"] = slot
		this_slot["item"] = this_slot_item

		our_augments += list(this_slot)
	var/list/unselected_augments_styles = list()
	for(var/style in GLOB.robotic_styles_list)
		unselected_augments_styles += style
	var/list/our_augment_styles = list()
	for(var/slot in preferences.augment_styles)
		our_augment_styles[slot] = preferences.augment_styles[slot]
	data["selected_augments"] = our_augments
	data["selected_augments_styles"] = our_augment_styles
	data["unselected_augments"] = unselected_augments
	data["unselected_augments_styles"] = unselected_augments_styles
	data["augment_balance"] = augment_balance
	return data

/datum/preference_middleware/augments/apply_to_human(mob/living/carbon/human/target, datum/preferences/preferences)
	for(var/slot in preferences.augments)
		var/augment_type = preferences.augments[slot]
		var/datum/augment_item/augment_item = GLOB.augment_item_datums[augment_type]
		augment_item.apply_to_human(target, FALSE, preferences)

/datum/preference_middleware/augments/proc/add_augment(list/params, mob/user)
	var/augment_category = params["augment_category"]
	var/augment_slot = params["augment_slot"]
	var/augment_name = params["augment_item"]
	var/augment_balance = preferences.GetQuirkBalance()
	for(var/augment_type in GLOB.augment_categories_to_slots_to_items[augment_category][augment_slot])
		var/datum/augment_item/augment_item = GLOB.augment_item_datums[augment_type]
		if(augment_item.name == augment_name)
			if((augment_balance-augment_item.value) >= 0)
				preferences.augments[augment_slot] = augment_type
	return TRUE

/datum/preference_middleware/augments/proc/remove_augment(list/params, mob/user)
	var/augment_slot = params["augment_slot"]
	var/augment_balance = preferences.GetQuirkBalance()
	for(var/slot in preferences.augments)
		if(slot == augment_slot)
			var/datum/augment_item/augment_item = GLOB.augment_item_datums[preferences.augments[slot]]
			if((augment_balance+augment_item.value) >= 0)
				preferences.augments -= augment_slot
				preferences.augment_styles -= augment_slot
				return TRUE

/datum/preference_middleware/augments/proc/set_augment_style(list/params, mob/user)
	var/augment_slot = params["augment_slot"]
	var/augment_style = params["augment_style"]
	if(augment_style == "None")
		preferences.augment_styles -= augment_slot
	else
		preferences.augment_styles[augment_slot] = augment_style
	return TRUE


code:
/// Middleware to handle birthsign
/datum/preference_middleware/birthsign
	action_delegations = list(
		"select_birthsign" = .proc/select_birthsign,
	)

/datum/preference_middleware/birthsign/New(datum/preferences)
	. = ..()
	if(!LAZYLEN(GLOB.species_to_available_birthsigns))
		for(var/species_type in subtypesof(/datum/species))
			var/datum/species/species = new species_type()
			GLOB.species_to_available_birthsigns[species_type] = species.available_birthsigns.Copy()
			qdel(species)
	// Run this at least once to verify if everything is valid here
	if(src.preferences)
		get_ui_data()

/datum/preference_middleware/birthsign/on_new_character(mob/user)
	. = ..()
	// Run this at least once to verify if everything is valid here
	get_ui_data()

/datum/preference_middleware/birthsign/get_ui_assets()
	return list(get_asset_datum(/datum/asset/spritesheet/birthsigns))

/datum/preference_middleware/birthsign/get_ui_data(mob/user)
	var/list/data = list()

	// How?
	if(!ispath(preferences.birthsign))
		preferences.birthsign = /datum/cultural_info/birthsign/starless

	var/species_type = preferences.read_preference(/datum/preference/choiced/species)
	// Checking if this species can have this birthsign
	if(!LAZYLEN(GLOB.species_to_available_birthsigns[species_type]) || !(preferences.birthsign in GLOB.species_to_available_birthsigns[species_type]))
		preferences.birthsign = /datum/cultural_info/birthsign/starless

	var/birthsign_balance = preferences.GetQuirkBalance()
	// Something is fucked
	if(birthsign_balance < 0)
		preferences.birthsign = /datum/cultural_info/birthsign/starless
		birthsign_balance = preferences.GetQuirkBalance()

	var/list/selected_birthsign = list()
	var/datum/cultural_info/birthsign/birthsign_datum = GLOB.culture_birthsigns[preferences.birthsign]
	if(birthsign_datum)
		selected_birthsign["name"] = birthsign_datum.name
		selected_birthsign["description"] = birthsign_datum.description
		selected_birthsign["icon"] = sanitize_css_class_name(birthsign_datum.name)
		selected_birthsign["patron_saint"] = birthsign_datum.patron_saint
		selected_birthsign["favored_profession"] = birthsign_datum.favored_profession
		selected_birthsign["value"] = birthsign_datum.value
		selected_birthsign["cant_buy"] = null

		data["selected_birthsign"] = selected_birthsign
	var/list/unselected_birthsigns = list()
	for(var/birthsign_path in (GLOB.species_to_available_birthsigns[species_type]-birthsign_datum.type))
		var/list/this_birthsign = list()
		var/datum/cultural_info/birthsign/unselected_birthsign_datum = GLOB.culture_birthsigns[birthsign_path]

		this_birthsign["name"] = unselected_birthsign_datum.name
		this_birthsign["description"] = unselected_birthsign_datum.description
		this_birthsign["icon"] = sanitize_css_class_name(unselected_birthsign_datum.name)
		this_birthsign["patron_saint"] = unselected_birthsign_datum.patron_saint
		this_birthsign["favored_profession"] = unselected_birthsign_datum.favored_profession
		this_birthsign["value"] = unselected_birthsign_datum.value
		this_birthsign["cant_buy"] = (birthsign_balance < unselected_birthsign_datum.value ? "Not enough points!" : null)

		unselected_birthsigns += list(this_birthsign)
	data["unselected_birthsigns"] = unselected_birthsigns
	data["birthsign_balance"] = birthsign_balance

	return data

/datum/preference_middleware/birthsign/proc/select_birthsign(list/params, mob/user)
	var/birthsign_name = params["birthsign_name"]
	var/birthsign_balance = preferences.GetQuirkBalance()
	var/datum/cultural_info/birthsign/birthsign_datum = GLOB.culture_birthsigns[preferences.birthsign]
	if(birthsign_datum?.value)
		birthsign_balance += birthsign_datum.value
	birthsign_datum = null
	for(var/birthsign_path in GLOB.culture_birthsigns)
		var/datum/cultural_info/birthsign/possible_sign = GLOB.culture_birthsigns[birthsign_path]
		if(birthsign_name == possible_sign.name)
			birthsign_datum = possible_sign
			break
	if(birthsign_datum && ((birthsign_balance-birthsign_datum.value) >= 0))
		preferences.birthsign = birthsign_datum.type
		return TRUE


code:
/// Middleware to handle languages
/datum/preference_middleware/languages
	action_delegations = list(
		"give_language" = .proc/give_language,
		"remove_language" = .proc/remove_language,
	)

/datum/preference_middleware/languages/New(datum/preferences)
	. = ..()
	// Run this at least once to verify if everything is valid here
	if(src.preferences)
		get_ui_data()

/datum/preference_middleware/languages/on_new_character(mob/user)
	. = ..()
	// Run this at least once to verify if everything is valid here
	get_ui_data()

/datum/preference_middleware/languages/get_ui_assets()
	return list(get_asset_datum(/datum/asset/spritesheet/languages))

/datum/preference_middleware/languages/get_ui_data(mob/user)
	var/list/data = list()

	// How?
	if(!islist(preferences.languages))
		preferences.languages = list()

	var/species_type = preferences.read_preference(/datum/preference/choiced/species)
	// Checking if this species can have these languages
	for(var/language_type in preferences.languages)
		// Invalid language
		if(!(language_type in LAZYACCESS(GLOB.species_to_learnable_languages, species_type)))
			preferences.languages -= language_type

	// Ensuring that the necessary languages are present
	for(var/language_type in LAZYACCESS(GLOB.species_to_necessary_languages, species_type))
		var/current_understanding = LAZYACCESS(preferences.languages, language_type)
		var/necessary_understanding = LAZYACCESSASSOC(GLOB.species_to_necessary_languages, species_type, language_type)
		if(!current_understanding || (necessary_understanding && (current_understanding < necessary_understanding)))
			preferences.languages[language_type] = GLOB.species_to_necessary_languages[species_type][language_type]

	var/language_balance = preferences.GetQuirkBalance()

	// Too litle languages or too many languages
	if(!LAZYLEN(preferences.languages) || (language_balance < 0))
		// Try the default languages first
		preferences.languages = list()
		for(var/language_type in LAZYACCESS(GLOB.species_to_default_languages, species_type))
			preferences.languages[language_type] = LAZYACCESSASSOC(GLOB.species_to_default_languages, species_type, language_type)
		language_balance = preferences.GetQuirkBalance()
		// Necessary ones second
		if(!LAZYLEN(preferences.languages) || (language_balance < 0))
			preferences.languages = list()
			for(var/language_type in LAZYACCESS(GLOB.species_to_necessary_languages, species_type))
				preferences.languages[language_type] = GLOB.species_to_necessary_languages[species_type][language_type]
		language_balance = preferences.GetQuirkBalance()
		// First learnable one lastly
		if(!LAZYLEN(preferences.languages) || (language_balance < 0))
			preferences.languages = list()
			for(var/language_type in LAZYACCESS(GLOB.species_to_learnable_languages, species_type))
				preferences.languages[language_type] = LAZYACCESSASSOC(GLOB.species_to_learnable_languages, species_type, language_type)
				break
		language_balance = preferences.GetQuirkBalance()
		// At this point, give up, literally everything that could fail did
		if(!LAZYLEN(preferences.languages) || (language_balance < 0))
			preferences.languages[/datum/language/common] = LANGUAGE_SPOKEN

	language_balance = preferences.GetQuirkBalance()

	var/list/selected_languages = list()
	var/list/unselected_languages = list()
	for(var/language_type in LAZYACCESS(GLOB.species_to_learnable_languages, species_type))
		var/datum/language/language = GLOB.language_datum_instances[language_type]
		if(language.secret)
			continue
		if(preferences.languages[language.type])
			var/list/this_language = list()
			var/already_taken_cost = (preferences.languages[language_type] == LANGUAGE_SPOKEN ? LANGUAGE_COST_SPOKEN : LANGUAGE_COST_UNDERSTOOD)

			this_language["name"] = language.name
			this_language["description"] = language.desc
			this_language["understand_type"] = preferences.languages[language.type]
			this_language["icon"] = sanitize_css_class_name(language.name)
			this_language["value_understand"] = LANGUAGE_COST_UNDERSTOOD
			this_language["value_speak"] = LANGUAGE_COST_SPOKEN
			this_language["cant_learn_understand"] = ((language_balance+already_taken_cost < LANGUAGE_COST_UNDERSTOOD) ? \
												"Not enough points!" : \
												((GLOB.species_to_necessary_languages[species_type][language_type] > LANGUAGE_UNDERSTOOD) ? \
												"Must be spoken!" : \
												((GLOB.species_to_learnable_languages[species_type][language_type] < LANGUAGE_UNDERSTOOD) ? \
												"Cannot understand this language!" : null))
												)
			this_language["cant_learn_speak"] = ((language_balance+already_taken_cost < LANGUAGE_COST_SPOKEN) ? \
												"Not enough points!" : \
												((GLOB.species_to_necessary_languages[species_type][language_type] > LANGUAGE_SPOKEN) ? \
												"Must be more than spoken!" : \
												((GLOB.species_to_learnable_languages[species_type][language_type] < LANGUAGE_SPOKEN) ? \
												"Cannot speak this language!" : null))
												)
			this_language["cant_forget"] = (GLOB.species_to_necessary_languages[species_type][language_type] ? \
										"This is a required language!" : \
										(LAZYLEN(preferences.languages) <= 1 ? "You need at least one language!" : null)
										)
			selected_languages += list(this_language)
		else
			var/list/this_language = list()

			this_language["name"] = language.name
			this_language["description"] = language.desc
			this_language["understand_type"] = LANGUAGE_NOT_UNDERSTOOD
			this_language["icon"] = sanitize_css_class_name(language.name)
			this_language["value_understand"] = LANGUAGE_COST_UNDERSTOOD
			this_language["value_speak"] = LANGUAGE_COST_SPOKEN
			this_language["cant_learn_understand"] = ((language_balance < LANGUAGE_COST_UNDERSTOOD) ? \
												"Not enough points!" : \
												((GLOB.species_to_necessary_languages[species_type][language_type] > LANGUAGE_UNDERSTOOD) ? \
												"Must be spoken!" : \
												((GLOB.species_to_learnable_languages[species_type][language_type] < LANGUAGE_UNDERSTOOD) ? \
												"Cannot understand this language!" : null))
												)
			this_language["cant_learn_speak"] = ((language_balance < LANGUAGE_COST_SPOKEN) ? \
												"Not enough points!" : \
												((GLOB.species_to_necessary_languages[species_type][language_type] > LANGUAGE_SPOKEN) ? \
												"Must be more than spoken!" : \
												((GLOB.species_to_learnable_languages[species_type][language_type] < LANGUAGE_SPOKEN) ? \
												"Cannot speak this language!" : null))
												)
			this_language["cant_forget"] = (GLOB.species_to_necessary_languages[species_type][language_type] ? \
										"This is a required language!" : \
										(LAZYLEN(preferences.languages) <= 1 ? "You need at least one language!" : null)
										)

			unselected_languages += list(this_language)

	data["language_balance"] = language_balance
	data["selected_languages"] = selected_languages
	data["unselected_languages"] = unselected_languages

	return data

/datum/preference_middleware/languages/post_set_preference(mob/user, preference, value)
	if(preference == "species")
		preferences.languages = list()
		var/species_type = preferences.read_preference(/datum/preference/choiced/species)
		var/datum/species/species = new species_type()
		for(var/language_type in species.default_languages)
			preferences.languages[language_type] = species.default_languages[language_type]
		qdel(species)
	return ..()

/datum/preference_middleware/languages/apply_to_human(mob/living/carbon/human/target, datum/preferences/preferences)
	target.language_holder.understood_languages.Cut()
	target.language_holder.spoken_languages.Cut()
	for(var/language_type in preferences.languages)
		if(LAZYACCESS(preferences.languages, language_type) >= LANGUAGE_UNDERSTOOD)
			target.language_holder.understood_languages[language_type] = list(LANGUAGE_ATOM)
		if(LAZYACCESS(preferences.languages, language_type) >= LANGUAGE_SPOKEN)
			target.language_holder.spoken_languages[language_type] = list(LANGUAGE_ATOM)

/datum/preference_middleware/languages/proc/give_language(list/params, mob/user)
	var/language_name = params["language_name"]
	var/understand_type = params["understand_type"]
	var/understand_cost = 0
	switch(understand_type)
		if(LANGUAGE_UNDERSTOOD)
			understand_cost = LANGUAGE_COST_UNDERSTOOD
		if(LANGUAGE_SPOKEN)
			understand_cost = LANGUAGE_COST_SPOKEN
	var/already_understood_type = params["already_understood_type"]
	var/already_understood_cost = 0
	switch(already_understood_type)
		if(LANGUAGE_UNDERSTOOD)
			already_understood_cost = LANGUAGE_COST_UNDERSTOOD
		if(LANGUAGE_SPOKEN)
			already_understood_cost = LANGUAGE_COST_SPOKEN
	var/language_balance = params["language_balance"]+already_understood_cost
	// too many languages
	if(preferences.languages && ((language_balance-understand_cost) < 0))
		return TRUE
	var/datum/language/language = GLOB.name_to_language_datum[language_name]
	preferences.languages[language.type] = understand_type
	return TRUE

/datum/preference_middleware/languages/proc/remove_language(list/params, mob/user)
	var/language_name = params["language_name"]
	var/datum/language/language = GLOB.name_to_language_datum[language_name]
	preferences.languages -= language.type
	return TRUE