code:
/**********************Mining drone**********************/
#define MINEDRONE_COLLECT 1
#define MINEDRONE_ATTACK 2

/mob/living/simple_animal/hostile/mining_drone
	name = "\improper Nanotrasen minebot"
	desc = "The instructions printed on the side read: This is a small robot used to support miners, can be set to search and collect loose ore, or to help fend off wildlife."
	gender = NEUTER
	icon = 'icons/mob/aibots.dmi'
	icon_state = "mining_drone"
	icon_living = "mining_drone"
	status_flags = CANSTUN|CANKNOCKDOWN|CANPUSH
	mouse_opacity = MOUSE_OPACITY_ICON
	faction = list("neutral")
	combat_mode = TRUE
	atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
	minbodytemp = 0
	move_to_delay = 10
	health = 125
	maxHealth = 125
	melee_damage_lower = 15
	melee_damage_upper = 15
	obj_damage = 10
	environment_smash = ENVIRONMENT_SMASH_NONE
	check_friendly_fire = TRUE
	stop_automated_movement_when_pulled = TRUE
	attack_verb_continuous = "drills"
	attack_verb_simple = "drill"
	attack_sound = 'sound/weapons/circsawhit.ogg'
	sentience_type = SENTIENCE_MINEBOT
	speak_emote = list("states")
	wanted_objects = list(/obj/item/stack/ore/diamond, /obj/item/stack/ore/gold, /obj/item/stack/ore/silver,
						  /obj/item/stack/ore/plasma, /obj/item/stack/ore/uranium, /obj/item/stack/ore/iron,
						  /obj/item/stack/ore/bananium, /obj/item/stack/ore/titanium)
	healable = 0
	loot = list(/obj/effect/decal/cleanable/robot_debris)
	del_on_death = TRUE
	light_system = MOVABLE_LIGHT
	light_range = 6
	light_on = FALSE
	var/mode = MINEDRONE_COLLECT
	var/obj/item/gun/energy/kinetic_accelerator/minebot/stored_gun

/mob/living/simple_animal/hostile/mining_drone/Initialize(mapload)
	. = ..()

	AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)

	stored_gun = new(src)
	var/datum/action/innate/minedrone/toggle_light/toggle_light_action = new()
	toggle_light_action.Grant(src)
	var/datum/action/innate/minedrone/toggle_meson_vision/toggle_meson_vision_action = new()
	toggle_meson_vision_action.Grant(src)
	var/datum/action/innate/minedrone/toggle_mode/toggle_mode_action = new()
	toggle_mode_action.Grant(src)
	var/datum/action/innate/minedrone/dump_ore/dump_ore_action = new()
	dump_ore_action.Grant(src)
	var/obj/item/implant/radio/mining/imp = new(src)
	imp.implant(src)

	access_card = new /obj/item/card/id/advanced/gold(src)
	SSid_access.apply_trim_to_card(access_card, /datum/id_trim/job/shaft_miner)

	SetCollectBehavior()

/mob/living/simple_animal/hostile/mining_drone/Destroy()
	for (var/datum/action/innate/minedrone/action in actions)
		qdel(action)
	return ..()

/mob/living/simple_animal/hostile/mining_drone/sentience_act()
	..()
	check_friendly_fire = 0

/mob/living/simple_animal/hostile/mining_drone/examine(mob/user)
	. = ..()
	var/t_He = p_they(TRUE)
	var/t_him = p_them()
	var/t_s = p_s()
	if(health < maxHealth)
		if(health >= maxHealth * 0.5)
			. += span_warning("[t_He] look[t_s] slightly dented.")
		else
			. += span_boldwarning("[t_He] look[t_s] severely dented!")
	. += {"Using a mining scanner on [t_him] will instruct [t_him] to drop stored ore. [max(0, LAZYLEN(contents) - 1)] Stored Ore\n
	Field repairs can be done with a welder."}
	if(stored_gun?.max_mod_capacity)
		. += "[stored_gun.get_remaining_mod_capacity()]% mod capacity remaining."
		for(var/obj/item/borg/upgrade/modkit/modkit as anything in stored_gun.modkits)
			. += span_notice("There is \a [modkit] installed, using [modkit.cost]% capacity.")

/mob/living/simple_animal/hostile/mining_drone/welder_act(mob/living/user, obj/item/welder)
	..()
	. = TRUE
	if(mode == MINEDRONE_ATTACK)
		to_chat(user, span_warning("[src] can't be repaired while in attack mode!"))
		return

	if(maxHealth == health)
		to_chat(user, span_info("[src] is at full integrity."))
		return

	if(welder.use_tool(src, user, 0, volume=40))
		adjustBruteLoss(-15)
		to_chat(user, span_info("You repair some of the armor on [src]."))

/mob/living/simple_animal/hostile/mining_drone/attackby(obj/item/item_used, mob/user, params)
	if(istype(item_used, /obj/item/mining_scanner) || istype(item_used, /obj/item/t_scanner/adv_mining_scanner))
		to_chat(user, span_info("You instruct [src] to drop any collected ore."))
		DropOre()
		return
	if(item_used.tool_behaviour == TOOL_CROWBAR || istype(item_used, /obj/item/borg/upgrade/modkit))
		item_used.melee_attack_chain(user, stored_gun, params)
		return
	..()

/mob/living/simple_animal/hostile/mining_drone/death()
	DropOre()
	if(stored_gun)
		for(var/obj/item/borg/upgrade/modkit/modkit as anything in stored_gun.modkits)
			modkit.uninstall(stored_gun)
	deathmessage = "blows apart!"
	..()

/mob/living/simple_animal/hostile/mining_drone/attack_hand(mob/living/carbon/human/user, list/modifiers)
	. = ..()
	if(.)
		return
	//if(!user.combat_mode) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	if(IS_HELP_INTENT(user, modifiers))
	//SEPTIC EDIT END
		toggle_mode()
		switch(mode)
			if(MINEDRONE_COLLECT)
				to_chat(user, span_info("[src] has been set to search and store loose ore."))
			if(MINEDRONE_ATTACK)
				to_chat(user, span_info("[src] has been set to attack hostile wildlife."))
		return

/mob/living/simple_animal/hostile/mining_drone/CanAllowThrough(atom/movable/mover, border_dir)
	. = ..()
	if(istype(mover, /obj/projectile/kinetic))
		var/obj/projectile/kinetic/projectile = mover
		if(projectile.kinetic_gun)
			if (locate(/obj/item/borg/upgrade/modkit/minebot_passthrough) in projectile.kinetic_gun.modkits)
				return TRUE
	else if(istype(mover, /obj/projectile/destabilizer))
		return TRUE

/mob/living/simple_animal/hostile/mining_drone/proc/SetCollectBehavior()
	mode = MINEDRONE_COLLECT
	vision_range = 9
	search_objects = 2
	wander = TRUE
	ranged = FALSE
	minimum_distance = 1
	retreat_distance = null
	icon_state = "mining_drone"
	to_chat(src, span_info("You are set to collect mode. You can now collect loose ore."))

/mob/living/simple_animal/hostile/mining_drone/proc/SetOffenseBehavior()
	mode = MINEDRONE_ATTACK
	vision_range = 7
	search_objects = 0
	wander = FALSE
	ranged = TRUE
	retreat_distance = 2
	minimum_distance = 1
	icon_state = "mining_drone_offense"
	to_chat(src, span_info("You are set to attack mode. You can now attack from range."))

/mob/living/simple_animal/hostile/mining_drone/AttackingTarget()
	if(istype(target, /obj/item/stack/ore) && mode == MINEDRONE_COLLECT)
		CollectOre()
		return
	if(isliving(target))
		SetOffenseBehavior()
	return ..()

/mob/living/simple_animal/hostile/mining_drone/OpenFire(atom/target)
	if(CheckFriendlyFire(target))
		return
	stored_gun.afterattack(target, src) //of the possible options to allow minebots to have KA mods, would you believe this is the best?

/mob/living/simple_animal/hostile/mining_drone/proc/CollectOre()
	for(var/obj/item/stack/ore/O in range(1, src))
		O.forceMove(src)

/mob/living/simple_animal/hostile/mining_drone/proc/DropOre(message = 1)
	if(!contents.len)
		if(message)
			to_chat(src, span_warning("You attempt to dump your stored ore, but you have none!"))
		return
	if(message)
		to_chat(src, span_notice("You dump your stored ore."))
	for(var/obj/item/stack/ore/O in contents)
		O.forceMove(drop_location())

/mob/living/simple_animal/hostile/mining_drone/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
	if(mode != MINEDRONE_ATTACK && amount > 0)
		SetOffenseBehavior()
	. = ..()

/datum/action/innate/minedrone/toggle_meson_vision
	name = "Toggle Meson Vision"
	button_icon_state = "meson"

/datum/action/innate/minedrone/toggle_meson_vision/Activate()
	var/mob/living/simple_animal/hostile/mining_drone/user = owner
	if(user.sight & SEE_TURFS)
		user.sight &= ~SEE_TURFS
		user.lighting_alpha = initial(user.lighting_alpha)
	else
		user.sight |= SEE_TURFS
		user.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE

	user.sync_lighting_plane_alpha()

	to_chat(user, span_notice("You toggle your meson vision [(user.sight & SEE_TURFS) ? "on" : "off"]."))

/mob/living/simple_animal/hostile/mining_drone/proc/toggle_mode()
	switch(mode)
		if(MINEDRONE_ATTACK)
			SetCollectBehavior()
		else
			SetOffenseBehavior()

//Actions for sentient minebots

/datum/action/innate/minedrone
	check_flags = AB_CHECK_CONSCIOUS
	icon_icon = 'icons/mob/actions/actions_mecha.dmi'
	background_icon_state = "bg_default"

/datum/action/innate/minedrone/toggle_light
	name = "Toggle Light"
	button_icon_state = "mech_lights_off"

/datum/action/innate/minedrone/toggle_light/Activate()
	var/mob/living/simple_animal/hostile/mining_drone/user = owner
	user.set_light_on(!user.light_on)
	to_chat(user, span_notice("You toggle your light [user.light_on ? "on" : "off"]."))

/datum/action/innate/minedrone/toggle_mode
	name = "Toggle Mode"
	button_icon_state = "mech_cycle_equip_off"

/datum/action/innate/minedrone/toggle_mode/Activate()
	var/mob/living/simple_animal/hostile/mining_drone/user = owner
	user.toggle_mode()

/datum/action/innate/minedrone/dump_ore
	name = "Dump Ore"
	button_icon_state = "mech_eject"

/datum/action/innate/minedrone/dump_ore/Activate()
	var/mob/living/simple_animal/hostile/mining_drone/user = owner
	user.DropOre()

/**********************Minebot Upgrades**********************/

//Melee

/obj/item/mine_bot_upgrade
	name = "minebot melee upgrade"
	desc = "A minebot upgrade."
	icon_state = "door_electronics"
	icon = 'icons/obj/module.dmi'

/obj/item/mine_bot_upgrade/afterattack(mob/living/simple_animal/hostile/mining_drone/minebot, mob/user, proximity)
	. = ..()
	if(!istype(minebot) || !proximity)
		return
	upgrade_bot(minebot, user)

/obj/item/mine_bot_upgrade/proc/upgrade_bot(mob/living/simple_animal/hostile/mining_drone/minebot, mob/user)
	if(minebot.melee_damage_upper != initial(minebot.melee_damage_upper))
		to_chat(user, span_warning("[minebot] already has a combat upgrade installed!"))
		return
	minebot.melee_damage_lower += 7
	minebot.melee_damage_upper += 7
	to_chat(user, "You increase the close-quarter combat abilities of [minebot].")
	qdel(src)

//Health

/obj/item/mine_bot_upgrade/health
	name = "minebot armor upgrade"

/obj/item/mine_bot_upgrade/health/upgrade_bot(mob/living/simple_animal/hostile/mining_drone/minebot, mob/user)
	if(minebot.maxHealth != initial(minebot.maxHealth))
		to_chat(user, span_warning("[minebot] already has reinforced armor!"))
		return
	minebot.maxHealth += 45
	minebot.updatehealth()
	to_chat(user, "You reinforce the armor of [minebot].")
	qdel(src)

//AI

/obj/item/slimepotion/slime/sentience/mining
	name = "minebot AI upgrade"
	desc = "Can be used to grant sentience to minebots. It's incompatible with minebot armor and melee upgrades, and will override them."
	icon_state = "door_electronics"
	icon = 'icons/obj/module.dmi'
	sentience_type = SENTIENCE_MINEBOT
	var/base_health_add = 5 //sentient minebots are penalized for beign sentient; they have their stats reset to normal plus these values
	var/base_damage_add = 1 //this thus disables other minebot upgrades
	var/base_speed_add = 1
	var/base_cooldown_add = 10 //base cooldown isn't reset to normal, it's just added on, since it's not practical to disable the cooldown module

/obj/item/slimepotion/slime/sentience/mining/after_success(mob/living/user, mob/living/simple_animal/simple_mob)
	if(!istype(simple_mob, /mob/living/simple_animal/hostile/mining_drone))
		return
	var/mob/living/simple_animal/hostile/mining_drone/minebot = simple_mob
	minebot.maxHealth = initial(minebot.maxHealth) + base_health_add
	minebot.melee_damage_lower = initial(minebot.melee_damage_lower) + base_damage_add
	minebot.melee_damage_upper = initial(minebot.melee_damage_upper) + base_damage_add
	minebot.move_to_delay = initial(minebot.move_to_delay) + base_speed_add
	minebot.stored_gun?.overheat_time += base_cooldown_add

#undef MINEDRONE_COLLECT
#undef MINEDRONE_ATTACK


code:
/*****************************Money bag********************************/

/obj/item/storage/bag/money
	name = "money bag"
	icon_state = "moneybag"
	worn_icon_state = "moneybag"
	force = 10
	throwforce = 0
	resistance_flags = FLAMMABLE
	max_integrity = 100
	w_class = WEIGHT_CLASS_BULKY

/obj/item/storage/bag/money/Initialize(mapload)
	. = ..()
	if(prob(20))
		icon_state = "moneybagalt"
	var/datum/component/storage/STR = GetComponent(/datum/component/storage)
	STR.max_w_class = WEIGHT_CLASS_NORMAL
	STR.max_items = 40
	STR.max_combined_w_class = 40
	STR.set_holdable(list(/obj/item/coin, /obj/item/stack/spacecash, /obj/item/holochip))

/obj/item/storage/bag/money/vault/PopulateContents()
	new /obj/item/coin/silver(src)
	new /obj/item/coin/silver(src)
	new /obj/item/coin/silver(src)
	new /obj/item/coin/silver(src)
	new /obj/item/coin/gold(src)
	new /obj/item/coin/gold(src)
	new /obj/item/coin/adamantine(src)

///Used in the dutchmen pirate shuttle.
/obj/item/storage/bag/money/dutchmen/PopulateContents()
	for(var/iteration in 1 to 9)
		new /obj/item/coin/silver/doubloon(src)
	for(var/iteration in 1 to 9)
		new /obj/item/coin/gold/doubloon(src)
	new /obj/item/coin/adamantine/doubloon(src)

repo_name: AnalWerewolf/sundown

license: mit


code:
#define GIBTONITE_QUALITY_HIGH 3
#define GIBTONITE_QUALITY_MEDIUM 2
#define GIBTONITE_QUALITY_LOW 1

#define ORESTACK_OVERLAYS_MAX 10

/**********************Mineral ores**************************/

/obj/item/stack/ore
	name = "rock"
	icon = 'icons/obj/mining.dmi'
	icon_state = "ore"
	inhand_icon_state = "ore"
	full_w_class = WEIGHT_CLASS_BULKY
	singular_name = "ore chunk"
	material_flags = MATERIAL_EFFECTS
	var/points = 0 //How many points this ore gets you from the ore redemption machine
	var/refined_type = null //What this ore defaults to being refined into
	var/mine_experience = 5 //How much experience do you get for mining this ore?
	novariants = TRUE // Ore stacks handle their icon updates themselves to keep the illusion that there's more going
	var/list/stack_overlays
	var/scan_state = "" //Used by mineral turfs for their scan overlay.
	var/spreadChance = 0 //Also used by mineral turfs for spreading veins

/obj/item/stack/ore/update_overlays()
	. = ..()
	var/difference = min(ORESTACK_OVERLAYS_MAX, amount) - (LAZYLEN(stack_overlays)+1)
	if(!difference)
		return

	if(difference < 0 && LAZYLEN(stack_overlays)) //amount < stack_overlays, remove excess.
		if(LAZYLEN(stack_overlays)-difference <= 0)
			stack_overlays = null
			return
		stack_overlays.len += difference

	else //amount > stack_overlays, add some.
		for(var/i in 1 to difference)
			var/mutable_appearance/newore = mutable_appearance(icon, icon_state)
			newore.pixel_x = rand(-8,8)
			newore.pixel_y = rand(-8,8)
			LAZYADD(stack_overlays, newore)

	if(stack_overlays)
		. += stack_overlays

/obj/item/stack/ore/welder_act(mob/living/user, obj/item/I)
	..()
	if(!refined_type)
		return TRUE

	if(I.use_tool(src, user, 0, volume=50, amount=15))
		new refined_type(drop_location())
		use(1)

	return TRUE

/obj/item/stack/ore/fire_act(exposed_temperature, exposed_volume)
	. = ..()
	if(isnull(refined_type))
		return
	else
		var/probability = (rand(0,100))/100
		var/burn_value = probability*amount
		var/amountrefined = round(burn_value, 1)
		if(amountrefined < 1)
			qdel(src)
		else
			new refined_type(drop_location(),amountrefined)
			qdel(src)

/obj/item/stack/ore/uranium
	name = "uranium ore"
	icon_state = "Uranium ore"
	inhand_icon_state = "Uranium ore"
	singular_name = "uranium ore chunk"
	points = 30
	material_flags = NONE
	mats_per_unit = list(/datum/material/uranium=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/uranium
	mine_experience = 6
	scan_state = "rock_Uranium"
	spreadChance = 5
	merge_type = /obj/item/stack/ore/uranium

/obj/item/stack/ore/iron
	name = "iron ore"
	icon_state = "Iron ore"
	inhand_icon_state = "Iron ore"
	singular_name = "iron ore chunk"
	points = 1
	mats_per_unit = list(/datum/material/iron=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/iron
	mine_experience = 1
	scan_state = "rock_Iron"
	spreadChance = 20
	merge_type = /obj/item/stack/ore/iron

/obj/item/stack/ore/glass
	name = "sand pile"
	icon_state = "Glass ore"
	inhand_icon_state = "Glass ore"
	singular_name = "sand pile"
	points = 1
	mats_per_unit = list(/datum/material/glass=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/glass
	w_class = WEIGHT_CLASS_TINY
	mine_experience = 0 //its sand
	merge_type = /obj/item/stack/ore/glass

GLOBAL_LIST_INIT(sand_recipes, list(\
		new /datum/stack_recipe("sandstone", /obj/item/stack/sheet/mineral/sandstone, 1, 1, 50),\
		new /datum/stack_recipe("aesthetic volcanic floor tile", /obj/item/stack/tile/basalt, 2, 1, 50)\
))

/obj/item/stack/ore/glass/get_main_recipes()
	. = ..()
	. += GLOB.sand_recipes

/obj/item/stack/ore/glass/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
	if(..() || !ishuman(hit_atom))
		return
	var/mob/living/carbon/human/C = hit_atom
	if(C.is_eyes_covered())
		C.visible_message(span_danger("[C]'s eye protection blocks the sand!"), span_warning("Your eye protection blocks the sand!"))
		return
	C.adjust_blurriness(6)
	C.adjustStaminaLoss(15)//the pain from your eyes burning does stamina damage
	C.add_confusion(5)
	to_chat(C, span_userdanger("\The [src] gets into your eyes! The pain, it burns!"))
	qdel(src)

/obj/item/stack/ore/glass/ex_act(severity, target)
	if(severity)
		qdel(src)

/obj/item/stack/ore/glass/basalt
	name = "volcanic ash"
	icon_state = "volcanic_sand"
	inhand_icon_state = "volcanic_sand"
	singular_name = "volcanic ash pile"
	mine_experience = 0
	merge_type = /obj/item/stack/ore/glass/basalt

/obj/item/stack/ore/plasma
	name = "plasma ore"
	icon_state = "Plasma ore"
	inhand_icon_state = "Plasma ore"
	singular_name = "plasma ore chunk"
	points = 15
	mats_per_unit = list(/datum/material/plasma=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/plasma
	mine_experience = 5
	scan_state = "rock_Plasma"
	spreadChance = 8
	merge_type = /obj/item/stack/ore/plasma

/obj/item/stack/ore/plasma/welder_act(mob/living/user, obj/item/I)
	to_chat(user, span_warning("You can't hit a high enough temperature to smelt [src] properly!"))
	return TRUE

/obj/item/stack/ore/silver
	name = "silver ore"
	icon_state = "Silver ore"
	inhand_icon_state = "Silver ore"
	singular_name = "silver ore chunk"
	points = 16
	mine_experience = 3
	mats_per_unit = list(/datum/material/silver=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/silver
	scan_state = "rock_Silver"
	spreadChance = 5
	merge_type = /obj/item/stack/ore/silver

/obj/item/stack/ore/gold
	name = "gold ore"
	icon_state = "Gold ore"
	inhand_icon_state = "Gold ore"
	singular_name = "gold ore chunk"
	points = 18
	mine_experience = 5
	mats_per_unit = list(/datum/material/gold=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/gold
	scan_state = "rock_Gold"
	spreadChance = 5
	merge_type = /obj/item/stack/ore/gold

/obj/item/stack/ore/diamond
	name = "diamond ore"
	icon_state = "Diamond ore"
	inhand_icon_state = "Diamond ore"
	singular_name = "diamond ore chunk"
	points = 50
	mats_per_unit = list(/datum/material/diamond=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/diamond
	mine_experience = 10
	scan_state = "rock_Diamond"
	merge_type = /obj/item/stack/ore/diamond

/obj/item/stack/ore/bananium
	name = "bananium ore"
	icon_state = "Bananium ore"
	inhand_icon_state = "Bananium ore"
	singular_name = "bananium ore chunk"
	points = 60
	mats_per_unit = list(/datum/material/bananium=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/bananium
	mine_experience = 15
	scan_state = "rock_Bananium"
	merge_type = /obj/item/stack/ore/bananium

/obj/item/stack/ore/titanium
	name = "titanium ore"
	icon_state = "Titanium ore"
	inhand_icon_state = "Titanium ore"
	singular_name = "titanium ore chunk"
	points = 50
	mats_per_unit = list(/datum/material/titanium=MINERAL_MATERIAL_AMOUNT)
	refined_type = /obj/item/stack/sheet/mineral/titanium
	mine_experience = 3
	scan_state = "rock_Titanium"
	spreadChance = 5
	merge_type = /obj/item/stack/ore/titanium

/obj/item/stack/ore/slag
	name = "slag"
	desc = "Completely useless."
	icon_state = "slag"
	inhand_icon_state = "slag"
	singular_name = "slag chunk"
	merge_type = /obj/item/stack/ore/slag

/obj/item/gibtonite
	name = "gibtonite ore"
	desc = "Extremely explosive if struck with mining equipment, Gibtonite is often used by miners to speed up their work by using it as a mining charge. This material is illegal to possess by unauthorized personnel under space law."
	icon = 'icons/obj/mining.dmi'
	icon_state = "Gibtonite ore"
	inhand_icon_state = "Gibtonite ore"
	w_class = WEIGHT_CLASS_BULKY
	throw_range = 0
	var/primed = FALSE
	var/det_time = 100
	var/quality = GIBTONITE_QUALITY_LOW //How pure this gibtonite is, determines the explosion produced by it and is derived from the det_time of the rock wall it was taken from, higher value = better
	var/attacher = "UNKNOWN"
	var/det_timer

/obj/item/gibtonite/ComponentInitialize()
	. = ..()
	AddComponent(/datum/component/two_handed, require_twohands=TRUE)

/obj/item/gibtonite/Destroy()
	qdel(wires)
	wires = null
	return ..()

/obj/item/gibtonite/attackby(obj/item/I, mob/user, params)
	if(!wires && istype(I, /obj/item/assembly/igniter))
		user.visible_message(span_notice("[user] attaches [I] to [src]."), span_notice("You attach [I] to [src]."))
		wires = new /datum/wires/explosive/gibtonite(src)
		attacher = key_name(user)
		qdel(I)
		add_overlay("Gibtonite_igniter")
		return

	if(wires && !primed)
		if(is_wire_tool(I))
			wires.interact(user)
			return

	if(I.tool_behaviour == TOOL_MINING || istype(I, /obj/item/resonator) || I.force >= 10)
		GibtoniteReaction(user)
		return
	if(primed)
		if(istype(I, /obj/item/mining_scanner) || istype(I, /obj/item/t_scanner/adv_mining_scanner) || I.tool_behaviour == TOOL_MULTITOOL)
			primed = FALSE
			if(det_timer)
				deltimer(det_timer)
			user.visible_message(span_notice("The chain reaction stopped! ...The ore's quality looks diminished."), span_notice("You stopped the chain reaction. ...The ore's quality looks diminished."))
			icon_state = "Gibtonite ore"
			quality = GIBTONITE_QUALITY_LOW
			return
	..()

/obj/item/gibtonite/attack_self(user)
	if(wires)
		wires.interact(user)
	else
		..()

/obj/item/gibtonite/bullet_act(obj/projectile/P)
	GibtoniteReaction(P.firer)
	. = ..()

/obj/item/gibtonite/ex_act()
	GibtoniteReaction(null, 1)

/obj/item/gibtonite/proc/GibtoniteReaction(mob/user, triggered_by = 0)
	if(!primed)
		primed = TRUE
		playsound(src,'sound/effects/hit_on_shattered_glass.ogg',50,TRUE)
		icon_state = "Gibtonite active"
		var/notify_admins = FALSE
		if(z != 5)//Only annoy the admins ingame if we're triggered off the mining zlevel
			notify_admins = TRUE

		if(triggered_by == 1)
			log_bomber(null, "An explosion has primed a", src, "for detonation", notify_admins)
		else if(triggered_by == 2)
			var/turf/bombturf = get_turf(src)
			if(notify_admins)
				message_admins("A signal has triggered a [name] to detonate at [ADMIN_VERBOSEJMP(bombturf)]. Igniter attacher: [ADMIN_LOOKUPFLW(attacher)]")
			var/bomb_message = "A signal has primed a [name] for detonation at [AREACOORD(bombturf)]. Igniter attacher: [key_name(attacher)]."
			log_game(bomb_message)
			GLOB.bombers += bomb_message
		else
			user.visible_message(span_warning("[user] strikes \the [src], causing a chain reaction!"), span_danger("You strike \the [src], causing a chain reaction."))
			log_bomber(user, "has primed a", src, "for detonation", notify_admins)
		det_timer = addtimer(CALLBACK(src, .proc/detonate, notify_admins), det_time, TIMER_STOPPABLE)

/obj/item/gibtonite/proc/detonate(notify_admins)
	if(primed)
		switch(quality)
			if(GIBTONITE_QUALITY_HIGH)
				explosion(src, devastation_range = 2, heavy_impact_range = 4, light_impact_range = 9, adminlog = notify_admins)
			if(GIBTONITE_QUALITY_MEDIUM)
				explosion(src, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 5, adminlog = notify_admins)
			if(GIBTONITE_QUALITY_LOW)
				explosion(src, heavy_impact_range = 1, light_impact_range = 3, adminlog = notify_admins)
		qdel(src)

/obj/item/stack/ore/Initialize(mapload, new_amount, merge = TRUE, list/mat_override=null, mat_amt=1)
	. = ..()
	pixel_x = base_pixel_x + rand(0, 16) - 8
	pixel_y = base_pixel_y + rand(0, 8) - 8

/obj/item/stack/ore/ex_act(severity, target)
	if(severity >= EXPLODE_DEVASTATE)
		qdel(src)

/*****************************Coin********************************/

// The coin's value is a value of it's materials.
// Yes, the gold standard makes a come-back!
// This is the only way to make coins that are possible to produce on station actually worth anything.
/obj/item/coin
	icon = 'icons/obj/economy.dmi'
	name = "coin"
	icon_state = "coin"
	flags_1 = CONDUCT_1
	force = 1
	throwforce = 2
	w_class = WEIGHT_CLASS_TINY
	custom_materials = list(/datum/material/iron = 400)
	material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS
	var/string_attached
	var/list/sideslist = list("heads","tails")
	var/cooldown = 0
	var/value
	var/coinflip
	item_flags = NO_MAT_REDEMPTION //You know, it's kind of a problem that money is worth more extrinsicly than intrinsically in this universe.

/obj/item/coin/Initialize(mapload)
	. = ..()
	coinflip = pick(sideslist)
	icon_state = "coin_[coinflip]"
	pixel_x = base_pixel_x + rand(0, 16) - 8
	pixel_y = base_pixel_y + rand(0, 8) - 8

/obj/item/coin/set_custom_materials(list/materials, multiplier = 1)
	. = ..()
	value = 0
	for(var/i in custom_materials)
		var/datum/material/M = i
		value += M.value_per_unit * custom_materials[M]

/obj/item/coin/get_item_credit_value()
	return value

/obj/item/coin/suicide_act(mob/living/user)
	user.visible_message(span_suicide("[user] contemplates suicide with \the [src]!"))
	if (!attack_self(user))
		user.visible_message(span_suicide("[user] couldn't flip \the [src]!"))
		return SHAME
	addtimer(CALLBACK(src, .proc/manual_suicide, user), 10)//10 = time takes for flip animation
	return MANUAL_SUICIDE_NONLETHAL

/obj/item/coin/proc/manual_suicide(mob/living/user)
	var/index = sideslist.Find(coinflip)
	if (index==2)//tails
		user.visible_message(span_suicide("\the [src] lands on [coinflip]! [user] promptly falls over, dead!"))
		user.adjustOxyLoss(200)
		user.death(0)
		user.set_suicide(TRUE)
		user.suicide_log()
	else
		user.visible_message(span_suicide("\the [src] lands on [coinflip]! [user] keeps on living!"))

/obj/item/coin/examine(mob/user)
	. = ..()
	. += span_info("It's worth [value] credit\s.")

/obj/item/coin/attackby(obj/item/W, mob/user, params)
	if(istype(W, /obj/item/stack/cable_coil))
		var/obj/item/stack/cable_coil/CC = W
		if(string_attached)
			to_chat(user, span_warning("There already is a string attached to this coin!"))
			return

		if (CC.use(1))
			add_overlay("coin_string_overlay")
			string_attached = 1
			to_chat(user, span_notice("You attach a string to the coin."))
		else
			to_chat(user, span_warning("You need one length of cable to attach a string to the coin!"))
			return
	else
		..()

/obj/item/coin/wirecutter_act(mob/living/user, obj/item/I)
	..()
	if(!string_attached)
		return TRUE

	new /obj/item/stack/cable_coil(drop_location(), 1)
	overlays = list()
	string_attached = null
	to_chat(user, span_notice("You detach the string from the coin."))
	return TRUE

/obj/item/coin/attack_self(mob/user)
	if(cooldown < world.time)
		if(string_attached) //does the coin have a wire attached
			to_chat(user, span_warning("The coin won't flip very well with something attached!") )
			return FALSE//do not flip the coin
		cooldown = world.time + 15
		flick("coin_[coinflip]_flip", src)
		coinflip = pick(sideslist)
		icon_state = "coin_[coinflip]"
		playsound(user.loc, 'sound/items/coinflip.ogg', 50, TRUE)
		var/oldloc = loc
		sleep(15)
		if(loc == oldloc && user && !user.incapacitated())
			user.visible_message(span_notice("[user] flips [src]. It lands on [coinflip]."), \
				span_notice("You flip [src]. It lands on [coinflip]."), \
				span_hear("You hear the clattering of loose change."))
	return TRUE//did the coin flip? useful for suicide_act

/obj/item/coin/gold
	custom_materials = list(/datum/material/gold = 400)

/obj/item/coin/silver
	custom_materials = list(/datum/material/silver = 400)

/obj/item/coin/diamond
	custom_materials = list(/datum/material/diamond = 400)

/obj/item/coin/plasma
	custom_materials = list(/datum/material/plasma = 400)

/obj/item/coin/uranium
	custom_materials = list(/datum/material/uranium = 400)

/obj/item/coin/titanium
	custom_materials = list(/datum/material/titanium = 400)

/obj/item/coin/bananium
	custom_materials = list(/datum/material/bananium = 400)

/obj/item/coin/adamantine
	custom_materials = list(/datum/material/adamantine = 400)

/obj/item/coin/mythril
	custom_materials = list(/datum/material/mythril = 400)

/obj/item/coin/plastic
	custom_materials = list(/datum/material/plastic = 400)

/obj/item/coin/runite
	custom_materials = list(/datum/material/runite = 400)

/obj/item/coin/twoheaded
	desc = "Hey, this coin's the same on both sides!"
	sideslist = list("heads")

/obj/item/coin/antagtoken
	name = "antag token"
	desc = "A novelty coin that helps the heart know what hard evidence cannot prove."
	icon_state = "coin_valid"
	custom_materials = list(/datum/material/plastic = 400)
	sideslist = list("valid", "salad")
	material_flags = NONE

/obj/item/coin/iron

/obj/item/coin/gold/debug
	custom_materials = list(/datum/material/gold = 400)
	desc = "If you got this somehow, be aware that it will dust you. Almost certainly."

/obj/item/coin/gold/debug/attack_self(mob/user)
	if(cooldown < world.time)
		if(string_attached) //does the coin have a wire attached
			to_chat(user, span_warning("The coin won't flip very well with something attached!") )
			return FALSE//do not flip the coin
		cooldown = world.time + 15
		flick("coin_[coinflip]_flip", src)
		coinflip = pick(sideslist)
		icon_state = "coin_[coinflip]"
		playsound(user.loc, 'sound/items/coinflip.ogg', 50, TRUE)
		var/oldloc = loc
		sleep(15)
		if(loc == oldloc && user && !user.incapacitated())
			user.visible_message(span_notice("[user] flips [src]. It lands on [coinflip]."), \
				span_notice("You flip [src]. It lands on [coinflip]."), \
				span_hear("You hear the clattering of loose change."))
		SSeconomy.fire()
		to_chat(user,"[SSeconomy.inflation_value()] is the inflation value.")
	return TRUE//did the coin flip? useful for suicide_act

///Coins used in the dutchmen money bag.
/obj/item/coin/silver/doubloon
	name = "doubloon"

/obj/item/coin/gold/doubloon
	name = "doubloon"

/obj/item/coin/adamantine/doubloon
	name = "doubloon"

#undef ORESTACK_OVERLAYS_MAX


code:
/**********************Ore box**************************/

/obj/structure/ore_box
	icon = 'icons/obj/mining.dmi'
	icon_state = "orebox"
	name = "ore box"
	desc = "A heavy wooden box, which can be filled with a lot of ores."
	density = TRUE
	pressure_resistance = 5*ONE_ATMOSPHERE

/obj/structure/ore_box/attackby(obj/item/W, mob/user, params)
	if (istype(W, /obj/item/stack/ore))
		user.transferItemToLoc(W, src)
	else if(SEND_SIGNAL(W, COMSIG_CONTAINS_STORAGE))
		SEND_SIGNAL(W, COMSIG_TRY_STORAGE_TAKE_TYPE, /obj/item/stack/ore, src)
		to_chat(user, span_notice("You empty the ore in [W] into \the [src]."))
	else
		return ..()

/obj/structure/ore_box/ComponentInitialize()
	. = ..()
	AddElement(/datum/element/rad_insulation, 0.01) //please datum mats no more cancer

/obj/structure/ore_box/crowbar_act(mob/living/user, obj/item/I)
	if(I.use_tool(src, user, 50, volume=50))
		user.visible_message(span_notice("[user] pries \the [src] apart."),
			span_notice("You pry apart \the [src]."),
			span_hear("You hear splitting wood."))
		deconstruct(TRUE, user)
	return TRUE

/obj/structure/ore_box/examine(mob/living/user)
	if(Adjacent(user) && istype(user))
		ui_interact(user)
	. = ..()

/obj/structure/ore_box/attack_hand(mob/user, list/modifiers)
	. = ..()
	if(.)
		return
	if(Adjacent(user))
		ui_interact(user)

/obj/structure/ore_box/attack_robot(mob/user)
	if(Adjacent(user))
		ui_interact(user)

/obj/structure/ore_box/proc/dump_box_contents()
	var/drop = drop_location()
	for(var/obj/item/stack/ore/O in src)
		if(QDELETED(O))
			continue
		if(QDELETED(src))
			break
		O.forceMove(drop)
		if(TICK_CHECK)
			stoplag()
			drop = drop_location()

/obj/structure/ore_box/ui_interact(mob/user, datum/tgui/ui)
	ui = SStgui.try_update_ui(user, src, ui)
	if(!ui)
		ui = new(user, src, "OreBox", name)
		ui.open()

/obj/structure/ore_box/ui_data()
	var/contents = list()
	for(var/obj/item/stack/ore/O in src)
		contents[O.type] += O.amount

	var/data = list()
	data["materials"] = list()
	for(var/type in contents)
		var/obj/item/stack/ore/O = type
		var/name = initial(O.name)
		data["materials"] += list(list("name" = name, "amount" = contents[type], "id" = type))

	return data

/obj/structure/ore_box/ui_act(action, params)
	. = ..()
	if(.)
		return
	if(!Adjacent(usr))
		return
	add_fingerprint(usr)
	usr.set_machine(src)
	switch(action)
		if("removeall")
			dump_box_contents()
			to_chat(usr, span_notice("You open the release hatch on the box.."))

/obj/structure/ore_box/deconstruct(disassembled = TRUE, mob/user)
	var/obj/item/stack/sheet/mineral/wood/WD = new (loc, 4)
	if(user && !QDELETED(WD))
		WD.add_fingerprint(user)
	dump_box_contents()
	qdel(src)

/// Special override for notify_contents = FALSE.
/obj/structure/ore_box/on_changed_z_level(turf/old_turf, turf/new_turf, notify_contents = FALSE)
	return ..()


code:
/datum/map_template/shelter
	var/shelter_id
	var/description
	var/list/blacklisted_turfs
	var/list/whitelisted_turfs
	var/list/banned_areas
	var/list/banned_objects

/datum/map_template/shelter/New()
	. = ..()
	blacklisted_turfs = typecacheof(/turf/closed)
	whitelisted_turfs = list()
	banned_areas = typecacheof(/area/shuttle)
	banned_objects = list()

/datum/map_template/shelter/proc/check_deploy(turf/deploy_location)
	var/affected = get_affected_turfs(deploy_location, centered=TRUE)
	for(var/turf/T in affected)
		var/area/A = get_area(T)
		if(is_type_in_typecache(A, banned_areas))
			return SHELTER_DEPLOY_BAD_AREA

		var/banned = is_type_in_typecache(T, blacklisted_turfs)
		var/permitted = is_type_in_typecache(T, whitelisted_turfs)
		if(banned && !permitted)
			return SHELTER_DEPLOY_BAD_TURFS

		for(var/obj/O in T)
			if((O.density && O.anchored) || is_type_in_typecache(O, banned_objects))
				return SHELTER_DEPLOY_ANCHORED_OBJECTS

	// Check if the shelter sticks out of map borders
	var/shelter_origin_x = deploy_location.x - round(width/2)
	if(shelter_origin_x <= 1 || shelter_origin_x+width > world.maxx)
		return SHELTER_DEPLOY_OUTSIDE_MAP
	var/shelter_origin_y = deploy_location.y - round(height/2)
	if(shelter_origin_y <= 1 || shelter_origin_y+height > world.maxy)
		return SHELTER_DEPLOY_OUTSIDE_MAP

	return SHELTER_DEPLOY_ALLOWED

/datum/map_template/shelter/alpha
	name = "Shelter Alpha"
	shelter_id = "shelter_alpha"
	description = "A cosy self-contained pressurized shelter, with \
		built-in navigation, entertainment, medical facilities and a \
		sleeping area! Order now, and we'll throw in a TINY FAN, \
		absolutely free!"
	mappath = "_maps/templates/shelter_1.dmm"

/datum/map_template/shelter/alpha/New()
	. = ..()
	whitelisted_turfs = typecacheof(/turf/closed/mineral)
	banned_objects = typecacheof(/obj/structure/stone_tile)

/datum/map_template/shelter/beta
	name = "Shelter Beta"
	shelter_id = "shelter_beta"
	description = "An extremely luxurious shelter, containing all \
		the amenities of home, including carpeted floors, hot and cold \
		running water, a gourmet three course meal, cooking facilities, \
		and a deluxe companion to keep you from getting lonely during \
		an ash storm."
	mappath = "_maps/templates/shelter_2.dmm"

/datum/map_template/shelter/beta/New()
	. = ..()
	whitelisted_turfs = typecacheof(/turf/closed/mineral)
	banned_objects = typecacheof(/obj/structure/stone_tile)

/datum/map_template/shelter/charlie
	name = "Shelter Charlie"
	shelter_id = "shelter_charlie"
	description = "A luxury elite bar which holds an entire bar \
		along with two vending machines, tables, and a restroom that \
		also has a sink. This isn't a survival capsule and so you can \
		expect that this won't save you if you're bleeding out to \
		death."
	mappath = "_maps/templates/shelter_3.dmm"

/datum/map_template/shelter/charlie/New()
	. = ..()
	whitelisted_turfs = typecacheof(/turf/closed/mineral)
	banned_objects = typecacheof(/obj/structure/stone_tile)


code:
// Camera mob, used by AI camera and blob.
/mob/camera
	name = "camera mob"
	density = FALSE
	move_force = INFINITY
	move_resist = INFINITY
	status_flags = GODMODE  // You can't damage it.
	mouse_opacity = MOUSE_OPACITY_TRANSPARENT
	see_in_dark = 7
	invisibility = INVISIBILITY_ABSTRACT // No one can see us
	sight = SEE_SELF
	move_on_shuttle = FALSE

/mob/camera/Initialize(mapload)
	. = ..()
	SSpoints_of_interest.make_point_of_interest(src)

/mob/camera/experience_pressure_difference()
	return

/mob/camera/canUseStorage()
	return FALSE

/mob/camera/emote(act, m_type=1, message = null, intentional = FALSE, force_silence = FALSE)
	return FALSE


code:
/datum/crew_manifest

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

/datum/crew_manifest/ui_status(mob/user, datum/ui_state/state)
	return (isnewplayer(user) || isobserver(user) || isAI(user) || ispAI(user)) ? UI_INTERACTIVE : UI_CLOSE

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

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

/datum/crew_manifest/ui_data(mob/user)
	var/list/positions = list()
	for(var/datum/job_department/department as anything in SSjob.joinable_departments)
		var/open = 0
		var/list/exceptions = list()
		for(var/datum/job/job as anything in department.department_jobs)
			if(job.total_positions == -1)
				exceptions += job.title
				continue
			var/open_slots = job.total_positions - job.current_positions
			if(open_slots < 1)
				continue
			open += open_slots
		positions[department.department_name] = list("exceptions" = exceptions, "open" = open)

	return list(
		"manifest" = GLOB.data_core.get_manifest(),
		"positions" = positions
	)


code:
//Dead mobs can exist whenever. This is needful

INITIALIZE_IMMEDIATE(/mob/dead)

/mob/dead
	sight = SEE_TURFS | SEE_MOBS | SEE_OBJS | SEE_SELF
	move_resist = INFINITY
	throwforce = 0

/mob/dead/Initialize(mapload)
	SHOULD_CALL_PARENT(FALSE)
	if(flags_1 & INITIALIZED_1)
		stack_trace("Warning: [src]([type]) initialized multiple times!")
	flags_1 |= INITIALIZED_1
	tag = "mob_[next_mob_id++]"
	add_to_mob_list()

	prepare_huds()

	if(length(CONFIG_GET(keyed_list/cross_server)))
		add_verb(src, /mob/dead/proc/server_hop)
	set_focus(src)
	become_hearing_sensitive()
	return INITIALIZE_HINT_NORMAL

/mob/dead/canUseStorage()
	return FALSE

/mob/dead/abstract_move(atom/destination)
	var/turf/old_turf = get_turf(src)
	var/turf/new_turf = get_turf(destination)
	if (old_turf?.z != new_turf?.z)
		on_changed_z_level(old_turf, new_turf)
	return ..()

/mob/dead/get_status_tab_items()
	. = ..()
	. += ""

	if(SSticker.HasRoundStarted())
		return

	var/time_remaining = SSticker.GetTimeLeft()
	if(time_remaining > 0)
		. += "Time To Start: [round(time_remaining/10)]s"
	else if(time_remaining == -10)
		. += "Time To Start: DELAYED"
	else
		. += "Time To Start: SOON"

	. += "Players: [SSticker.totalPlayers]"
	if(client.holder)
		. += "Players Ready: [SSticker.totalPlayersReady]"

/mob/dead/proc/server_hop()
	set category = "OOC"
	set name = "Server Hop!"
	set desc= "Jump to the other server"
	if(notransform)
		return
	var/list/our_id = CONFIG_GET(string/cross_comms_name)
	var/list/csa = CONFIG_GET(keyed_list/cross_server) - our_id
	var/pick
	switch(csa.len)
		if(0)
			remove_verb(src, /mob/dead/proc/server_hop)
			to_chat(src, span_notice("Server Hop has been disabled."))
		if(1)
			pick = csa[1]
		else
			pick = input(src, "Pick a server to jump to", "Server Hop") as null|anything in csa

	if(!pick)
		return

	var/addr = csa[pick]

	if(tgui_alert(usr, "Jump to server [pick] ([addr])?", "Server Hop", list("Yes", "No")) != "Yes")
		return

	var/client/C = client
	to_chat(C, span_notice("Sending you to [pick]."))
	new /atom/movable/screen/splash(null, C)

	notransform = TRUE
	sleep(29) //let the animation play
	notransform = FALSE

	if(!C)
		return

	winset(src, null, "command=.options") //other wise the user never knows if byond is downloading resources

	C << link("[addr]")

/mob/dead/proc/update_z(new_z) // 1+ to register, null to unregister
	if (registered_z != new_z)
		if (registered_z)
			SSmobs.dead_players_by_zlevel[registered_z] -= src
		if (client)
			if (new_z)
				SSmobs.dead_players_by_zlevel[new_z] += src
			registered_z = new_z
		else
			registered_z = null

/mob/dead/Login()
	. = ..()
	if(!. || !client)
		return FALSE
	var/turf/T = get_turf(src)
	if (isturf(T))
		update_z(T.z)

/mob/dead/auto_deadmin_on_login()
	return

/mob/dead/Logout()
	update_z(null)
	return ..()

/mob/dead/on_changed_z_level(turf/old_turf, turf/new_turf)
	..()
	update_z(new_turf?.z)


code:
/mob/dead/new_player/Login()
	if(!client)
		return

	if(CONFIG_GET(flag/use_exp_tracking))
		client.set_exp_from_db()
		client.set_db_player_flags()
	if(!mind)
		mind = new /datum/mind(key)
		mind.active = TRUE
		mind.set_current(src)

	. = ..()
	if(!. || !client)
		return FALSE

	var/motd = global.config.motd
	if(motd)
		to_chat(src, "
[motd]
", handle_whitespace=FALSE)

	if(GLOB.admin_notice)
		to_chat(src, span_notice("Admin Notice:\n \t [GLOB.admin_notice]"))

	var/spc = CONFIG_GET(number/soft_popcap)
	if(spc && living_player_count() >= spc)
		to_chat(src, span_notice("Server Notice:\n \t [CONFIG_GET(string/soft_popcap_message)]"))

	sight |= SEE_TURFS

	client.playtitlemusic()

	var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/lobby)
	asset_datum.send(client)

	// Check if user should be added to interview queue
	if (!client.holder && CONFIG_GET(flag/panic_bunker) && CONFIG_GET(flag/panic_bunker_interview) && !(client.ckey in GLOB.interviews.approved_ckeys))
		var/required_living_minutes = CONFIG_GET(number/panic_bunker_living)
		var/living_minutes = client.get_exp_living(TRUE)
		if (required_living_minutes >= living_minutes)
			client.interviewee = TRUE
			register_for_interview()
			return

	if(SSticker.current_state < GAME_STATE_SETTING_UP)
		var/tl = SSticker.GetTimeLeft()
		to_chat(src, "Please set up your character and select \"Ready\". The game will start [tl > 0 ? "in about [DisplayTimeText(tl)]" : "soon"].")


code:
/mob/dead/new_player/Logout()
	ready = 0
	..()
	if(!spawning)//Here so that if they are spawning and log out, the other procs can play out and they will have a mob to come back to.
		key = null//We null their key before deleting the mob, so they are properly kicked out.
		QDEL_NULL(mind) //Clean out mind, yes this fucking sucks
		qdel(src)
	return


code:
/mob/dead/new_player
	flags_1 = NONE
	invisibility = INVISIBILITY_ABSTRACT
	density = FALSE
	stat = DEAD
	hud_type = /datum/hud/new_player
	hud_possible = list()

	var/ready = FALSE
	/// Referenced when you want to delete the new_player later on in the code.
	var/spawning = FALSE
	/// For instant transfer once the round is set up
	var/mob/living/new_character
	///Used to make sure someone doesn't get spammed with messages if they're ineligible for roles.
	var/ineligible_for_roles = FALSE

/mob/dead/new_player/Initialize(mapload)
	if(client && SSticker.state == GAME_STATE_STARTUP)
		var/atom/movable/screen/splash/S = new(null, client, TRUE, TRUE)
		S.Fade(TRUE)

	if(length(GLOB.newplayer_start))
		forceMove(pick(GLOB.newplayer_start))
	else
		forceMove(locate(1,1,1))

	ComponentInitialize()

	. = ..()

	GLOB.new_player_list += src

/mob/dead/new_player/Destroy()
	GLOB.new_player_list -= src

	return ..()

/mob/dead/new_player/prepare_huds()
	return

/mob/dead/new_player/Topic(href, href_list[])
	if(src != usr)
		return

	if(!client)
		return

	if(client.interviewee)
		return FALSE

	if(href_list["late_join"]) //This still exists for queue messages in chat
		if(!SSticker?.IsRoundInProgress())
			to_chat(usr, span_boldwarning("The round is either not ready, or has already finished..."))
			return
		LateChoices()
		return

	if(href_list["SelectedJob"])
		if(!SSticker?.IsRoundInProgress())
			to_chat(usr, span_danger("The round is either not ready, or has already finished..."))
			return

		if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
			to_chat(usr, span_notice("There is an administrative lock on entering the game!"))
			return

		//Determines Relevent Population Cap
		var/relevant_cap
		var/hpc = CONFIG_GET(number/hard_popcap)
		var/epc = CONFIG_GET(number/extreme_popcap)
		if(hpc && epc)
			relevant_cap = min(hpc, epc)
		else
			relevant_cap = max(hpc, epc)

		if(SSticker.queued_players.len && !(ckey(key) in GLOB.admin_datums))
			if((living_player_count() >= relevant_cap) || (src != SSticker.queued_players[1]))
				to_chat(usr, span_warning("Server is full."))
				return

		AttemptLateSpawn(href_list["SelectedJob"])
		return

	if(href_list["viewpoll"])
		var/datum/poll_question/poll = locate(href_list["viewpoll"]) in GLOB.polls
		poll_player(poll)

	if(href_list["votepollref"])
		var/datum/poll_question/poll = locate(href_list["votepollref"]) in GLOB.polls
		vote_on_poll_handler(poll, href_list)

//When you cop out of the round (NB: this HAS A SLEEP FOR PLAYER INPUT IN IT)
/mob/dead/new_player/proc/make_me_an_observer()
	if(QDELETED(src) || !src.client)
		ready = PLAYER_NOT_READY
		return FALSE

	var/less_input_message
	if(SSlag_switch.measures[DISABLE_DEAD_KEYLOOP])
		less_input_message = " - Notice: Observer freelook is currently disabled."
	var/this_is_like_playing_right = alert(usr, "Are you sure you wish to observe? You will not be able to play this round![less_input_message]","Player Setup", "Yes", "No")

	if(QDELETED(src) || !src.client || this_is_like_playing_right != "Yes")
		ready = PLAYER_NOT_READY
		src << browse(null, "window=playersetup") //closes the player setup window
		return FALSE

	var/mob/dead/observer/observer = new()
	spawning = TRUE

	observer.started_as_observer = TRUE
	var/obj/effect/landmark/observer_start/O = locate(/obj/effect/landmark/observer_start) in GLOB.landmarks_list
	to_chat(src, span_notice("Now teleporting."))
	if (O)
		observer.forceMove(O.loc)
	else
		to_chat(src, span_notice("Teleporting failed. Ahelp an admin please"))
		stack_trace("There's no freaking observer landmark available on this map or you're making observers before the map is initialised")
	observer.key = key
	observer.client = client
	observer.set_ghost_appearance()
	if(observer.client && observer.client.prefs)
		observer.real_name = observer.client.prefs.read_preference(/datum/preference/name/real_name)
		observer.name = observer.real_name
		observer.client.init_verbs()
	observer.update_appearance()
	observer.stop_sound_channel(CHANNEL_LOBBYMUSIC)
	deadchat_broadcast(" has observed.", "[observer.real_name]", follow_target = observer, turf_target = get_turf(observer), message_type = DEADCHAT_DEATHRATTLE)
	QDEL_NULL(mind)
	qdel(src)
	return TRUE

/proc/get_job_unavailable_error_message(retval, jobtitle)
	switch(retval)
		if(JOB_AVAILABLE)
			return "[jobtitle] is available."
		if(JOB_UNAVAILABLE_GENERIC)
			return "[jobtitle] is unavailable."
		if(JOB_UNAVAILABLE_BANNED)
			return "You are currently banned from [jobtitle]."
		if(JOB_UNAVAILABLE_PLAYTIME)
			return "You do not have enough relevant playtime for [jobtitle]."
		if(JOB_UNAVAILABLE_ACCOUNTAGE)
			return "Your account is not old enough for [jobtitle]."
		if(JOB_UNAVAILABLE_SLOTFULL)
			return "[jobtitle] is already filled to capacity."
		//SEPTIC EDIT BEGIN
		if(JOB_UNAVAILABLE_QUIRK)
			return "[jobtitle] is restricted from your quirks."
		if(JOB_UNAVAILABLE_SPECIES)
			return "[jobtitle] is restricted from your species."
		if(JOB_UNAVAILABLE_LANGUAGE)
			return "You don't have the required languages for [jobtitle]."
		//SEPTIC EDIT END
	return "Error: Unknown job availability."

/mob/dead/new_player/proc/IsJobUnavailable(rank, latejoin = FALSE)
	var/datum/job/job = SSjob.GetJob(rank)
	if(!(job.job_flags & JOB_NEW_PLAYER_JOINABLE))
		return JOB_UNAVAILABLE_GENERIC
	if((job.current_positions >= job.total_positions) && job.total_positions != -1)
		if(is_assistant_job(job))
			if(isnum(client.player_age) && client.player_age <= 14) //Newbies can always be assistants
				return JOB_AVAILABLE
			for(var/datum/job/other_job as anything in SSjob.joinable_occupations)
				if(other_job.current_positions < other_job.total_positions && other_job != job)
					return JOB_UNAVAILABLE_SLOTFULL
		else
			return JOB_UNAVAILABLE_SLOTFULL
	if(is_banned_from(ckey, rank))
		return JOB_UNAVAILABLE_BANNED
	if(QDELETED(src))
		return JOB_UNAVAILABLE_GENERIC
	if(!job.player_old_enough(client))
		return JOB_UNAVAILABLE_ACCOUNTAGE
	if(job.required_playtime_remaining(client))
		return JOB_UNAVAILABLE_PLAYTIME
	if(latejoin && !job.special_check_latejoin(client))
		return JOB_UNAVAILABLE_GENERIC
	return JOB_AVAILABLE

/mob/dead/new_player/proc/AttemptLateSpawn(rank)
	var/error = IsJobUnavailable(rank)
	if(error != JOB_AVAILABLE)
		tgui_alert(usr, get_job_unavailable_error_message(error, rank))
		return FALSE

	if(SSshuttle.arrivals)
		if(SSshuttle.arrivals.damaged && CONFIG_GET(flag/arrivals_shuttle_require_safe_latejoin))
			src << tgui_alert(usr,"The arrivals shuttle is currently malfunctioning! You cannot join.")
			return FALSE

		if(CONFIG_GET(flag/arrivals_shuttle_require_undocked))
			SSshuttle.arrivals.RequireUndocked(src)

	//Remove the player from the join queue if he was in one and reset the timer
	SSticker.queued_players -= src
	SSticker.queue_delay = 4

	var/datum/job/job = SSjob.GetJob(rank)

	SSjob.AssignRole(src, job, TRUE)

	mind.late_joiner = TRUE
	var/atom/destination = mind.assigned_role.get_latejoin_spawn_point()
	if(!destination)
		CRASH("Failed to find a latejoin spawn point.")
	var/mob/living/character = create_character(destination)
	if(!character)
		CRASH("Failed to create a character for latejoin.")
	transfer_character()

	SSjob.EquipRank(character, job, character.client)
	job.after_latejoin_spawn(character)

	#define IS_NOT_CAPTAIN 0
	#define IS_ACTING_CAPTAIN 1
	#define IS_FULL_CAPTAIN 2
	var/is_captain = IS_NOT_CAPTAIN
	// If we already have a captain, are they a "Captain" rank and are we allowing multiple of them to be assigned?
	if(is_captain_job(job))
		is_captain = IS_FULL_CAPTAIN
	// If we don't have an assigned cap yet, check if this person qualifies for some from of captaincy.
	else if(!SSjob.assigned_captain && ishuman(character) && SSjob.chain_of_command[rank] && !is_banned_from(ckey, list("Captain")))
		is_captain = IS_ACTING_CAPTAIN
	if(is_captain != IS_NOT_CAPTAIN)
		minor_announce(job.get_captaincy_announcement(character))
		SSjob.promote_to_captain(character, is_captain == IS_ACTING_CAPTAIN)
	#undef IS_NOT_CAPTAIN
	#undef IS_ACTING_CAPTAIN
	#undef IS_FULL_CAPTAIN

	SSticker.minds += character.mind
	character.client.init_verbs() // init verbs for the late join
	var/mob/living/carbon/human/humanc
	if(ishuman(character))
		humanc = character //Let's retypecast the var to be human,

	if(humanc) //These procs all expect humans
		GLOB.data_core.manifest_inject(humanc)
		if(SSshuttle.arrivals)
			SSshuttle.arrivals.QueueAnnounce(humanc, rank)
		else
			announce_arrival(humanc, rank)
		AddEmploymentContract(humanc)

		humanc.increment_scar_slot()
		humanc.load_persistent_scars()

		if(GLOB.curse_of_madness_triggered)
			give_madness(humanc, GLOB.curse_of_madness_triggered)

	GLOB.joined_player_list += character.ckey

	if(CONFIG_GET(flag/allow_latejoin_antagonists) && humanc) //Borgs aren't allowed to be antags. Will need to be tweaked if we get true latejoin ais.
		if(SSshuttle.emergency)
			switch(SSshuttle.emergency.mode)
				if(SHUTTLE_RECALL, SHUTTLE_IDLE)
					SSticker.mode.make_antag_chance(humanc)
				if(SHUTTLE_CALL)
					if(SSshuttle.emergency.timeLeft(1) > initial(SSshuttle.emergencyCallTime)*0.5)
						SSticker.mode.make_antag_chance(humanc)

	if((job.job_flags & JOB_ASSIGN_QUIRKS) && humanc && CONFIG_GET(flag/roundstart_traits))
		SSquirks.AssignQuirks(humanc, humanc.client)

	log_manifest(character.mind.key,character.mind,character,latejoin = TRUE)

	SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CREWMEMBER_JOINED, character, rank)

/mob/dead/new_player/proc/AddEmploymentContract(mob/living/carbon/human/employee)
	//TODO:  figure out a way to exclude wizards/nukeops/demons from this.
	for(var/C in GLOB.employmentCabinets)
		var/obj/structure/filingcabinet/employment/employmentCabinet = C
		if(!employmentCabinet.virgin)
			employmentCabinet.addFile(employee)

/mob/dead/new_player/proc/LateChoices()
	var/list/dat = list()
	if(SSlag_switch.measures[DISABLE_NON_OBSJOBS])
		dat += "
Only Observers may join at this time.

"
	dat += "
Round Duration: [DisplayTimeText(world.time - SSticker.round_start_time)]
"
	if(SSshuttle.emergency)
		switch(SSshuttle.emergency.mode)
			if(SHUTTLE_ESCAPE)
				dat += "
The station has been evacuated.

"
			if(SHUTTLE_CALL)
				if(!SSshuttle.canRecall())
					dat += "
The station is currently undergoing evacuation procedures.

"
	for(var/datum/job/prioritized_job in SSjob.prioritized_jobs)
		if(prioritized_job.current_positions >= prioritized_job.total_positions)
			SSjob.prioritized_jobs -= prioritized_job
	dat += "

"
	var/column_counter = 0

	for(var/datum/job_department/department as anything in SSjob.joinable_departments)
		var/department_color = department.latejoin_color
		dat += "<fieldset style='width: 185px; border: 2px solid [department_color]; display: inline'>"
		dat += "<legend align='center' style='color: [department_color]'>[department.department_name]</legend>"
		var/list/dept_data = list()
		for(var/datum/job/job_datum as anything in department.department_jobs)
			if(IsJobUnavailable(job_datum.title, TRUE) != JOB_AVAILABLE)
				continue
			var/command_bold = ""
			if(job_datum.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
				command_bold = " command"
			if(job_datum in SSjob.prioritized_jobs)
				dept_data += "[job_datum.title] ([job_datum.current_positions])"
			else
				dept_data += "[job_datum.title] ([job_datum.current_positions])"
		if(!length(dept_data))
			dept_data += "No positions open."
		dat += dept_data.Join()
		dat += "</fieldset>
"
		column_counter++
		if(column_counter > 0 && (column_counter % 3 == 0))
			dat += "	"
	dat += "

"
	dat += "

"
	var/datum/browser/popup = new(src, "latechoices", "Choose Profession", 680, 580)
	popup.add_stylesheet("playeroptions", 'html/browser/playeroptions.css')
	popup.set_content(jointext(dat, ""))
	popup.open(FALSE) // 0 is passed to open so that it doesn't use the onclose() proc

/// Creates, assigns and returns the new_character to spawn as. Assumes a valid mind.assigned_role exists.
/mob/dead/new_player/proc/create_character(atom/destination)
	spawning = TRUE

	mind.active = FALSE //we wish to transfer the key manually
	var/mob/living/spawning_mob = mind.assigned_role.get_spawn_mob(client, destination)
	if(QDELETED(src) || !client)
		return // Disconnected while checking for the appearance ban.
	if(!isAI(spawning_mob)) // Unfortunately there's still snowflake AI code out there.
		mind.original_character_slot_index = client.prefs.default_slot
		mind.transfer_to(spawning_mob) //won't transfer key since the mind is not active
		mind.set_original_character(spawning_mob)
	client.init_verbs()
	. = spawning_mob
	new_character = .

/mob/dead/new_player/proc/transfer_character()
	. = new_character
	if(!.)
		return
	new_character.key = key //Manually transfer the key to log them in,
	new_character.stop_sound_channel(CHANNEL_LOBBYMUSIC)
	var/area/joined_area = get_area(new_character.loc)
	if(joined_area)
		joined_area.on_joining_game(new_character)
	new_character = null
	qdel(src)

/mob/dead/new_player/proc/ViewManifest()
	if(!client)
		return
	if(world.time < client.crew_manifest_delay)
		return
	client.crew_manifest_delay = world.time + (1 SECONDS)

	if(!GLOB.crew_manifest_tgui)
		GLOB.crew_manifest_tgui = new /datum/crew_manifest(src)

	GLOB.crew_manifest_tgui.ui_interact(src)

/mob/dead/new_player/Move()
	return 0

// Used to make sure that a player has a valid job preference setup, used to knock players out of eligibility for anything if their prefs don't make sense.
// A "valid job preference setup" in this situation means at least having one job set to low, or not having "return to lobby" enabled
// Prevents "antag rolling" by setting antag prefs on, all jobs to never, and "return to lobby if preferences not available"
// Doing so would previously allow you to roll for antag, then send you back to lobby if you didn't get an antag role
// This also does some admin notification and logging as well, as well as some extra logic to make sure things don't go wrong
/mob/dead/new_player/proc/check_preferences()
	if(!client)
		return FALSE //Not sure how this would get run without the mob having a client, but let's just be safe.
	if(client.prefs.read_preference(/datum/preference/choiced/jobless_role) != RETURNTOLOBBY)
		return TRUE
	// If they have antags enabled, they're potentially doing this on purpose instead of by accident. Notify admins if so.
	var/has_antags = FALSE
	if(client.prefs.be_special.len > 0)
		has_antags = TRUE
	if(client.prefs.job_preferences.len == 0)
		if(!ineligible_for_roles)
			to_chat(src, span_danger("You have no jobs enabled, along with return to lobby if job is unavailable. This makes you ineligible for any round start role, please update your job preferences."))
		ineligible_for_roles = TRUE
		ready = PLAYER_NOT_READY
		if(has_antags)
			log_admin("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. The player has been forcefully returned to the lobby.")
			message_admins("[src.ckey] has no jobs enabled, return to lobby if job is unavailable enabled and [client.prefs.be_special.len] antag preferences enabled. This is an old antag rolling technique. The player has been asked to update their job preferences and has been forcefully returned to the lobby.")
		return FALSE //This is the only case someone should actually be completely blocked from antag rolling as well
	return TRUE

/**
 * Prepares a client for the interview system, and provides them with a new interview
 *
 * This proc will both prepare the user by removing all verbs from them, as well as
 * giving them the interview form and forcing it to appear.
 */
/mob/dead/new_player/proc/register_for_interview()
	// First we detain them by removing all the verbs they have on client
	for (var/v in client.verbs)
		var/procpath/verb_path = v
		if (!(verb_path in GLOB.stat_panel_verbs))
			remove_verb(client, verb_path)

	// Then remove those on their mob as well
	for (var/v in verbs)
		var/procpath/verb_path = v
		if (!(verb_path in GLOB.stat_panel_verbs))
			remove_verb(src, verb_path)

	// Then we create the interview form and show it to the client
	var/datum/interview/I = GLOB.interviews.interview_for_client(client)
	if (I)
		I.ui_interact(src)

	// Add verb for re-opening the interview panel, and re-init the verbs for the stat panel
	add_verb(src, /mob/dead/new_player/proc/open_interview)


code:
///All currently running polls held as datums
GLOBAL_LIST_EMPTY(polls)
GLOBAL_PROTECT(polls)

///All poll option datums of running polls
GLOBAL_LIST_EMPTY(poll_options)
GLOBAL_PROTECT(poll_options)

/**
 * Shows a list of currently running polls a player can vote/has voted on
 *
 */
/mob/dead/new_player/proc/handle_player_polling()
	var/list/output = list("
Player polls

")
	var/rs = REF(src)
	for(var/p in GLOB.polls)
		var/datum/poll_question/poll = p
		if((poll.admin_only && !client.holder) || poll.future_poll)
			continue
		output += "
[poll.question]
"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpolllist;size=500x300")

/**
 * Redirects a player to the correct poll window based on poll type.
 *
 */
/mob/dead/new_player/proc/poll_player(datum/poll_question/poll)
	if(!poll)
		return
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	switch(poll.poll_type)
		if(POLLTYPE_OPTION)
			poll_player_option(poll)
		if(POLLTYPE_TEXT)
			poll_player_text(poll)
		if(POLLTYPE_RATING)
			poll_player_rating(poll)
		if(POLLTYPE_MULTI)
			poll_player_multi(poll)
		if(POLLTYPE_IRV)
			poll_player_irv(poll)

/**
 * Shows voting window for an option type poll, listing its options and relevant details.
 *
 * If already voted on, the option a player voted for is pre-selected.
 *
 */
/mob/dead/new_player/proc/poll_player_option(datum/poll_question/poll)
	var/datum/db_query/query_option_get_voted = SSdbcore.NewQuery({"
		SELECT optionid FROM [format_table_name("poll_vote")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = poll.poll_id, "ckey" = ckey))
	if(!query_option_get_voted.warn_execute())
		qdel(query_option_get_voted)
		return
	var/voted_option_id = 0
	if(query_option_get_voted.NextRow())
		voted_option_id = text2num(query_option_get_voted.item[1])
	qdel(query_option_get_voted)
	var/list/output = list("
Player poll
Question: [poll.question]
")
	if(poll.subtitle)
		output += "[poll.subtitle]
"
	output += "Poll runs from [poll.start_datetime] until [poll.end_datetime]
"
	if(poll.allow_revoting)
		output += "Revoting is enabled."
	if(!voted_option_id || poll.allow_revoting)
		output += {"<form action='?src=[REF(src)]' method='get'>
		<input type='hidden' name='src' value='[REF(src)]'>
		<input type='hidden' name='votepollref' value='[REF(poll)]'>
		"}
	output += "

"
	for(var/o in poll.options)
		var/datum/poll_option/option = o
		output += "<input type='radio' name='voteoptionref' value='[REF(option)]'"
		if(voted_option_id && !poll.allow_revoting)
			output += " disabled"
		if(voted_option_id == option.option_id)
			output += " selected"
		output += ">[option.text]
"
	output += "

"
	if(!voted_option_id || poll.allow_revoting)
		output += "
<input type='submit' value='Vote'></form>"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpoll;size=500x250")

/**
 * Shows voting window for a text response type poll, listing its relevant details.
 *
 * If already responded to, the saved response of a player is shown.
 *
 */
/mob/dead/new_player/proc/poll_player_text(datum/poll_question/poll)
	var/datum/db_query/query_text_get_replytext = SSdbcore.NewQuery({"
		SELECT replytext FROM [format_table_name("poll_textreply")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = poll.poll_id, "ckey" = ckey))
	if(!query_text_get_replytext.warn_execute())
		qdel(query_text_get_replytext)
		return
	var/reply_text = ""
	if(query_text_get_replytext.NextRow())
		reply_text = query_text_get_replytext.item[1]
	qdel(query_text_get_replytext)
	var/list/output = list("
Player poll
Question: [poll.question]
")
	if(poll.subtitle)
		output += "[poll.subtitle]
"
	output += "Feedback gathering runs from [poll.start_datetime] until [poll.end_datetime]
"
	if(poll.allow_revoting)
		output += "Revoting is enabled."
	if(!reply_text || poll.allow_revoting)
		output += {"<form action='?src=[REF(src)]' method='get'>
		<input type='hidden' name='src' value='[REF(src)]'>
		<input type='hidden' name='votepollref' value='[REF(poll)]'>
		Please provide feedback below. You can use any letters of the English alphabet, numbers and the symbols: . , ! ? : ; -

		<textarea name='replytext' cols='50' rows='14'>[reply_text]</textarea>

<input type='submit' value='Submit'></form>
		"}
	else
		output += "[reply_text]"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpoll;size=500x500")

/**
 * Shows voting window for a rating type poll, listing its options and relevant details.
 *
 * If already voted on, the options a player voted for are pre-selected.
 *
 */
/mob/dead/new_player/proc/poll_player_rating(datum/poll_question/poll)
	var/datum/db_query/query_rating_get_votes = SSdbcore.NewQuery({"
		SELECT optionid, rating FROM [format_table_name("poll_vote")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = poll.poll_id, "ckey" = ckey))
	if(!query_rating_get_votes.warn_execute())
		qdel(query_rating_get_votes)
		return
	var/list/voted_ratings = list()
	while(query_rating_get_votes.NextRow())
		voted_ratings += list("[query_rating_get_votes.item[1]]" = query_rating_get_votes.item[2])
	qdel(query_rating_get_votes)
	var/list/output = list("
Player poll
Question: [poll.question]
")
	if(poll.subtitle)
		output += "[poll.subtitle]
"
	output += "Poll runs from [poll.start_datetime] until [poll.end_datetime]
"
	if(poll.allow_revoting)
		output += "Revoting is enabled."
	if(!length(voted_ratings) || poll.allow_revoting)
		output += {"<form action='?src=[REF(src)]' method='get'>
		<input type='hidden' name='src' value='[REF(src)]'>
		<input type='hidden' name='votepollref' value='[REF(poll)]'>
		"}
	for(var/o in poll.options)
		var/datum/poll_option/option = o
		var/mid_val = round((option.max_val + option.min_val) / 2)
		var/selected_rating = text2num(voted_ratings["[option.option_id]"])
		output += "
[option.text]: <select name='[REF(option)]'"
		if(length(voted_ratings) && !poll.allow_revoting)
			output += " disabled"
		output += ">"
		for(var/rating in option.min_val to option.max_val)
			output += "<option value='[rating]'"
			if(selected_rating == rating)
				output += " selected"
			output += ">[rating]"
			if(option.desc_min && rating == option.min_val)
				output += " ([option.desc_min])"
			else if(option.desc_mid && rating == mid_val)
				output += " ([option.desc_mid])"
			else if(option.desc_max && rating == option.max_val)
				output += " ([option.desc_max])"
			output += "</option>"
		output += "</select>"
	if(!length(voted_ratings) || poll.allow_revoting)
		output += "
<input type='submit' value='Submit'></form>"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpoll;size=500x500")

/**
 * Shows voting window for a multiple choice type poll, listing its options and relevant details.
 *
 * If already voted on, the options a player voted for are pre-selected.
 *
 */
/mob/dead/new_player/proc/poll_player_multi(datum/poll_question/poll)
	var/datum/db_query/query_multi_get_votes = SSdbcore.NewQuery({"
		SELECT optionid FROM [format_table_name("poll_vote")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = poll.poll_id, "ckey" = ckey))
	if(!query_multi_get_votes.warn_execute())
		qdel(query_multi_get_votes)
		return
	var/list/voted_for = list()
	while(query_multi_get_votes.NextRow())
		voted_for += text2num(query_multi_get_votes.item[1])
	qdel(query_multi_get_votes)
	var/list/output = list("
Player poll
Question: [poll.question]
")
	if(poll.subtitle)
		output += "[poll.subtitle]
"
	output += "You can select up to [poll.options_allowed] options. If you select more, the first [poll.options_allowed] will be saved.
Poll runs from [poll.start_datetime] until [poll.end_datetime]
"
	if(poll.allow_revoting)
		output += "Revoting is enabled."
	if(!length(voted_for) || poll.allow_revoting)
		output += {"<form action='?src=[REF(src)]' method='get'>
		<input type='hidden' name='src' value='[REF(src)]'>
		<input type='hidden' name='votepollref' value='[REF(poll)]'>
		"}
	output += "

"
	for(var/o in poll.options)
		var/datum/poll_option/option = o
		output += "<input type='checkbox' name='[REF(option)]' value='[option.option_id]'"
		if(length(voted_for) && !poll.allow_revoting)
			output += " disabled"
		if(option.option_id in voted_for)
			output += " checked"
		output += ">[option.text]
"
	output += "

"
	if(!length(voted_for) || poll.allow_revoting)
		output += "
<input type='submit' value='Vote'></form>"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpoll;size=500x300")

/**
 * Shows voting window for an IRV type poll, listing its options and relevant details.
 *
 * If already voted on, the options are sorted how a player voted for them, otherwise they are randomly shuffled.
 *
 */
/mob/dead/new_player/proc/poll_player_irv(datum/poll_question/poll)
	var/datum/asset/irv_assets = get_asset_datum(/datum/asset/group/irv)
	irv_assets.send(src)
	var/datum/db_query/query_irv_get_votes = SSdbcore.NewQuery({"
		SELECT optionid FROM [format_table_name("poll_vote")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = poll.poll_id, "ckey" = ckey))
	if(!query_irv_get_votes.warn_execute())
		qdel(query_irv_get_votes)
		return
	var/list/voted_for = list()
	while(query_irv_get_votes.NextRow())
		voted_for += text2num(query_irv_get_votes.item[1])
	qdel(query_irv_get_votes)
	var/list/prepared_options = list()
	//if they've already voted we use the order they voted in plus a shuffle of any options they haven't voted for, if any
	if(length(voted_for))
		var/list/option_copy = poll.options.Copy()
		for(var/vote_id in voted_for)
			for(var/o in option_copy)
				var/datum/poll_option/option = o
				if(option.option_id == vote_id)
					prepared_options += option
					option_copy -= option
		prepared_options += shuffle(option_copy)
	//otherwise just shuffle the options
	else
		prepared_options = shuffle(poll.options)
	var/list/output = list({"<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>

Player poll
Question: [poll.question]
"})
	if(poll.subtitle)
		output += "[poll.subtitle]
"
	output += "Poll runs from [poll.start_datetime] until [poll.end_datetime]
"
	if(poll.allow_revoting)
		output += "Revoting is enabled."
	output += "Please sort the options in the order of most preferred to least preferred

"
	if(!length(voted_for) || poll.allow_revoting)
		output += {"<form action='?src=[REF(src)]' method='POST'>
		<input type='hidden' name='src' value='[REF(src)]'>
		<input type='hidden' name='votepollref' value='[REF(poll)]'>
		<input type='hidden' name='IRVdata' id='IRVdata'>
		"}
	output += "
Most Preferred
"
	for(var/o in prepared_options)
		var/datum/poll_option/option = o
		output += "
 [option.text]
\n"
	output += "
Least Preferred
"
	if(!length(voted_for) || poll.allow_revoting)
		output += "
<input type='submit' value='Vote'></form>"
	output += "
"
	src << browse(jointext(output, ""),"window=playerpoll;size=500x500")

/**
 * Runs some poll validation before a vote is processed.
 *
 * Checks a player is who they claim to be and that a poll is actually still running.
 * Also loads the vote_id to pass onto single-option and text polls.
 * Increments the vote count when successful.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_handler(datum/poll_question/poll, href_list)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(!poll || !href_list)
		return
	if(IsAdminAdvancedProcCall())
		log_game("[key_name(usr)] attempted to rig the vote by voting as [key]")
		message_admins("[key_name_admin(usr)] attempted to rig the vote by voting as [key]")
		to_chat(usr, span_danger("You don't seem to be [key]."))
		to_chat(src, span_danger("Something went horribly wrong processing your vote. Please contact an administrator, they should have gotten a message about this"))
		return
	var/admin_rank
	if(client.holder)
		admin_rank = client.holder.rank.name
	else
		if(poll.admin_only)
			return
		else
			admin_rank = "Player"
	var/table = "poll_vote"
	if(poll.poll_type == POLLTYPE_TEXT)
		table = "poll_textreply"
	var/sql_poll_id = poll.poll_id
	var/vote_id //only used for option and text polls to save needing another query
	var/datum/db_query/query_validate_poll_vote = SSdbcore.NewQuery({"
		SELECT
			(SELECT id FROM [format_table_name(table)] WHERE ckey = :ckey AND pollid = :pollid AND deleted = 0 LIMIT 1)
		FROM [format_table_name("poll_question")]
		WHERE NOW() BETWEEN starttime AND endtime AND deleted = 0 AND id = :pollid
	"}, list("ckey" = ckey, "pollid" = sql_poll_id))
	if(!query_validate_poll_vote.warn_execute())
		qdel(query_validate_poll_vote)
		return
	//triple state return: no row returned if poll isn't running, null if no vote found, otherwise returns the vote id
	if(query_validate_poll_vote.NextRow())
		vote_id = text2num(query_validate_poll_vote.item[1])
		if(vote_id && !poll.allow_revoting)
			to_chat(usr, span_danger("Poll revoting is disabled and you've already replied to this poll."))
			qdel(query_validate_poll_vote)
			return
	else
		to_chat(usr, span_danger("Selected poll is not open."))
		qdel(query_validate_poll_vote)
		return
	qdel(query_validate_poll_vote)
	var/vote_success = FALSE
	switch(poll.poll_type)
		if(POLLTYPE_OPTION)
			vote_success = vote_on_poll_option(poll, href_list, admin_rank, sql_poll_id, vote_id)
		if(POLLTYPE_TEXT)
			vote_success = vote_on_poll_text(href_list, admin_rank, sql_poll_id, vote_id)
		if(POLLTYPE_RATING)
			vote_success = vote_on_poll_rating(poll, href_list, admin_rank, sql_poll_id)
		if(POLLTYPE_MULTI)
			vote_success = vote_on_poll_multi(poll, href_list, admin_rank, sql_poll_id)
		if(POLLTYPE_IRV)
			vote_success = vote_on_poll_irv(poll, href_list, admin_rank, sql_poll_id)
	if(vote_success)
		if(!vote_id)
			poll.poll_votes++
		to_chat(usr, span_notice("Vote successful."))

/**
 * Processes vote form data and saves results to the database for an option type poll.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_option(datum/poll_question/poll, href_list, admin_rank, sql_poll_id, vote_id)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(IsAdminAdvancedProcCall())
		return
	var/datum/poll_option/option = locate(href_list["voteoptionref"]) in poll.options
	if(!option)
		to_chat(src, span_danger("No option was selected."))
		return
	var/datum/db_query/query_vote_option = SSdbcore.NewQuery({"
		INSERT INTO [format_table_name("poll_vote")] (id, datetime, pollid, optionid, ckey, ip, adminrank)
		VALUES (:vote_id, NOW(), :poll_id, :option_id, :ckey, INET_ATON(:ip), :admin_rank)
		ON DUPLICATE KEY UPDATE datetime = NOW(), optionid = :option_id, ip = INET_ATON(:ip), adminrank = :admin_rank
	"}, list(
		"vote_id" = vote_id,
		"poll_id" = sql_poll_id,
		"option_id" = option.option_id,
		"ckey" = ckey,
		"ip" = client.address,
		"admin_rank" = admin_rank,
	))
	if(!query_vote_option.warn_execute())
		qdel(query_vote_option)
		return
	qdel(query_vote_option)
	return TRUE

/**
 * Processes response form data and saves results to the database for a text response type poll.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_text(href_list, admin_rank, sql_poll_id, vote_id)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(IsAdminAdvancedProcCall())
		return
	var/reply_text = href_list["replytext"]
	if(!reply_text || (length(reply_text) > 2048))
		to_chat(src, span_danger("The text you entered was blank or too long. Please correct the text and submit again."))
		return
	var/datum/db_query/query_vote_text = SSdbcore.NewQuery({"
		INSERT INTO [format_table_name("poll_textreply")] (id, datetime, pollid, ckey, ip, replytext, adminrank)
		VALUES (:vote_id, NOW(), :poll_id, :ckey, INET_ATON(:ip), :reply_text, :admin_rank)
		ON DUPLICATE KEY UPDATE datetime = NOW(), ip = INET_ATON(:ip), replytext = :reply_text, adminrank = :admin_rank
	"}, list(
		"vote_id" = vote_id,
		"poll_id" = sql_poll_id,
		"ckey" = ckey,
		"ip" = client.address,
		"reply_text" = reply_text,
		"admin_rank" = admin_rank,
	))
	if(!query_vote_text.warn_execute())
		qdel(query_vote_text)
		return
	qdel(query_vote_text)
	return TRUE

/**
 * Processes vote form data and saves results to the database for a rating type poll.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_rating(datum/poll_question/poll, list/href_list, admin_rank, sql_poll_id)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(IsAdminAdvancedProcCall())
		return
	var/list/votes = list()
	var/datum/db_query/query_get_rating_votes = SSdbcore.NewQuery({"
		SELECT id, optionid FROM [format_table_name("poll_vote")]
		WHERE pollid = :pollid AND ckey = :ckey AND deleted = 0
	"}, list("pollid" = sql_poll_id, "ckey" = ckey))
	if(!query_get_rating_votes.warn_execute())
		qdel(query_get_rating_votes)
		return
	while(query_get_rating_votes.NextRow())
		votes += list("[query_get_rating_votes.item[2]]" = text2num(query_get_rating_votes.item[1]))
	qdel(query_get_rating_votes)
	href_list.Cut(1,3) //first two values aren't options

	var/special_columns = list(
		"datetime" = "NOW()",
		"ip" = "INET_ATON(?)",
	)

	var/sql_votes = list()
	for(var/h in href_list)
		var/datum/poll_option/option = locate(h) in poll.options
		sql_votes += list(list(
			"id" = votes["[option.option_id]"],
			"pollid" = sql_poll_id,
			"optionid" = option.option_id,
			"ckey" = ckey,
			"ip" = client.address,
			"adminrank" = admin_rank,
			"rating" = href_list[h]
		))
	SSdbcore.MassInsert(format_table_name("poll_vote"), sql_votes, duplicate_key = TRUE, special_columns = special_columns)
	return TRUE

/**
 * Processes vote form data and saves results to the database for a multiple choice type poll.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_multi(datum/poll_question/poll, list/href_list, admin_rank, sql_poll_id)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(IsAdminAdvancedProcCall())
		return
	if(length(href_list) > 2)
		href_list.Cut(1,3) //first two values aren't options
	else
		to_chat(src, span_danger("No options were selected."))

	var/special_columns = list(
		"datetime" = "NOW()",
		"ip" = "INET_ATON(?)",
	)

	var/sql_votes = list()
	var/vote_count = 0
	for(var/h in href_list)
		if(vote_count == poll.options_allowed)
			to_chat(src, span_danger("Allowed option count exceeded, only the first [poll.options_allowed] selected options have been saved."))
			break
		vote_count++
		var/datum/poll_option/option = locate(h) in poll.options
		sql_votes += list(list(
			"pollid" = sql_poll_id,
			"optionid" = option.option_id,
			"ckey" = ckey,
			"ip" = client.address,
			"adminrank" = admin_rank
		))
	/*with revoting and poll editing possible there can be an edge case where a poll is changed to allow less multiple choice options than a user has already voted on
	rather than trying to calculate which options should be updated and which deleted, we just delete all of a user's votes and re-insert as needed*/
	var/datum/db_query/query_delete_multi_votes = SSdbcore.NewQuery({"
		UPDATE [format_table_name("poll_vote")] SET deleted = 1 WHERE pollid = :pollid AND ckey = :ckey
	"}, list("pollid" = sql_poll_id, "ckey" = ckey))
	if(!query_delete_multi_votes.warn_execute())
		qdel(query_delete_multi_votes)
		return
	qdel(query_delete_multi_votes)
	SSdbcore.MassInsert(format_table_name("poll_vote"), sql_votes, special_columns = special_columns)
	return TRUE

/**
 * Processes vote form data and saves results to the database for an IRV type poll.
 *
 */
/mob/dead/new_player/proc/vote_on_poll_irv(datum/poll_question/poll, list/href_list, admin_rank, sql_poll_id)
	if(!SSdbcore.Connect())
		to_chat(src, span_danger("Failed to establish database connection."))
		return
	if(IsAdminAdvancedProcCall())
		return
	var/list/votelist = splittext(href_list["IRVdata"], ",")
	if(!length(votelist))
		to_chat(src, span_danger("No ordering data found. Please try again or contact an administrator."))

	var/list/special_columns = list(
		"datetime" = "NOW()",
		"ip" = "INET_ATON(?)",
	)

	var/list/sql_votes = list()
	var/list/option_copy = poll.options.Copy()
	for(var/o in votelist)
		var/datum/poll_option/option = locate(o) in option_copy
		if (!option)
			to_chat(src, span_warning("invalid votes were trimmed from your ballot, please revote ."))
		sql_votes += list(list(
			"pollid" = sql_poll_id,
			"optionid" = option.option_id,
			"ckey" = ckey,
			"ip" = client.address,
			"adminrank" = admin_rank
		))
	//IRV results are calculated based on id order, we delete all of a user's votes to avoid potential errors caused by revoting and option editing
	var/datum/db_query/query_delete_irv_votes = SSdbcore.NewQuery({"
		UPDATE [format_table_name("poll_vote")] SET deleted = 1 WHERE pollid = :pollid AND ckey = :ckey
	"}, list("pollid" = sql_poll_id, "ckey" = ckey))
	if(!query_delete_irv_votes.warn_execute())
		qdel(query_delete_irv_votes)
		return
	qdel(query_delete_irv_votes)
	SSdbcore.MassInsert(format_table_name("poll_vote"), sql_votes, special_columns = special_columns)
	return TRUE


code:
/// Fully randomizes everything in the character.
/datum/preferences/proc/randomise_appearance_prefs(randomize_flags = ALL)
	for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
		if (!preference.included_in_randomization_flags(randomize_flags))
			continue

		if (preference.is_randomizable())
			write_preference(preference, preference.create_random_value(src))

/// Randomizes the character according to preferences.
/datum/preferences/proc/apply_character_randomization_prefs(antag_override = FALSE)
	switch (read_preference(/datum/preference/choiced/random_body))
		if (RANDOM_ANTAG_ONLY)
			if (!antag_override)
				return

		if (RANDOM_DISABLED)
			return

	for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
		if (should_randomize(preference, antag_override))
			write_preference(preference, preference.create_random_value(src))

///Setup the random hardcore quirks and give the character the new score prize.
/datum/preferences/proc/hardcore_random_setup(mob/living/carbon/human/character)
	var/next_hardcore_score = select_hardcore_quirks()
	character.hardcore_survival_score = next_hardcore_score ** 1.2  //30 points would be about 60 score

/**
 * Goes through all quirks that can be used in hardcore mode and select some based on a random budget.
 * Returns the new value to be gained with this setup, plus the previously earned score.
 **/
/datum/preferences/proc/select_hardcore_quirks()
	. = 0

	var/quirk_budget = rand(8, 35)

	all_quirks = list() //empty it out

	var/list/available_hardcore_quirks = SSquirks.hardcore_quirks.Copy()

	while(quirk_budget > 0)
		for(var/i in available_hardcore_quirks) //Remove from available quirks if its too expensive.
			var/datum/quirk/available_quirk = i
			if(available_hardcore_quirks[available_quirk] > quirk_budget)
				available_hardcore_quirks -= available_quirk

		if(!available_hardcore_quirks.len)
			break

		var/datum/quirk/picked_quirk = pick(available_hardcore_quirks)

		var/picked_quirk_blacklisted = FALSE
		for(var/bl in SSquirks.quirk_blacklist) //Check if the quirk is blacklisted with our current quirks. quirk_blacklist is a list of lists.
			var/list/blacklist = bl
			if(!(picked_quirk in blacklist))
				continue
			for(var/iterator_quirk in all_quirks) //Go through all the quirks we've already selected to see if theres a blacklist match
				if((iterator_quirk in blacklist) && !(iterator_quirk == picked_quirk)) //two quirks have lined up in the list of the list of quirks that conflict with each other, so return (see quirks.dm for more details)
					picked_quirk_blacklisted = TRUE
					break
			if(picked_quirk_blacklisted)
				break

		if(picked_quirk_blacklisted)
			available_hardcore_quirks -= picked_quirk
			continue

		if(initial(picked_quirk.mood_quirk) && CONFIG_GET(flag/disable_human_mood)) //check for moodlet quirks
			available_hardcore_quirks -= picked_quirk
			continue

		all_quirks += initial(picked_quirk.name)
		quirk_budget -= available_hardcore_quirks[picked_quirk]
		. += available_hardcore_quirks[picked_quirk]
		available_hardcore_quirks -= picked_quirk

/// Returns what job is marked as highest
/datum/preferences/proc/get_highest_priority_job()
	var/datum/job/preview_job
	var/highest_pref = 0

	for(var/job in job_preferences)
		if(job_preferences[job] > highest_pref)
			preview_job = SSjob.GetJob(job)
			highest_pref = job_preferences[job]

	return preview_job

/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin)
	var/datum/job/preview_job = get_highest_priority_job()

	if(preview_job)
		// Silicons only need a very basic preview since there is no customization for them.
		if (istype(preview_job,/datum/job/ai))
			return image('icons/mob/ai.dmi', icon_state = resolve_ai_icon(read_preference(/datum/preference/choiced/ai_core_display)), dir = SOUTH)
		if (istype(preview_job,/datum/job/cyborg))
			return image('icons/mob/robots.dmi', icon_state = "robot", dir = SOUTH)

	// Set up the dummy for its photoshoot
	apply_prefs_to(mannequin, TRUE)

	if(preview_job)
		mannequin.job = preview_job.title
		mannequin.dress_up_as_job(preview_job, TRUE)

	COMPILE_OVERLAYS(mannequin)
	return mannequin.appearance