diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 9609f19c..cff9c7ff 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -302,6 +302,12 @@
#define COMSIG_MOB_ATTACK_RANGED "mob_attack_ranged"
///from base of /mob/throw_item(): (atom/target)
#define COMSIG_MOB_THROW "mob_throw"
+#define COMSIG_MOB_KEY_CHANGE "mob_key_change" //from base of /mob/transfer_ckey(): (new_character, old_character)
+#define COMSIG_MOB_PRE_PLAYER_CHANGE "mob_pre_player_change" //sent to the target mob from base of /mob/transfer_ckey() and /mind/transfer_to(): (our_character, their_character)
+#define COMSIG_MOB_GHOSTIZE "mob_ghostize" //from base of mob/Ghostize(): (can_reenter_corpse, special, penalize)
+ #define COMPONENT_BLOCK_GHOSTING (1<<0)
+ #define COMPONENT_DO_NOT_PENALIZE_GHOSTING (1<<1)
+ #define COMPONENT_FREE_GHOSTING (1<<2)
///from base of /mob/verb/examinate(): (atom/target)
#define COMSIG_MOB_EXAMINATE "mob_examinate"
///from base of /mob/update_sight(): ()
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index a1869b99..35f0482a 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -503,4 +503,6 @@ GLOBAL_LIST_INIT(pda_reskins, list(PDA_SKIN_CLASSIC = 'icons/obj/pda.dmi', PDA_S
#define FALL_STOP_INTERCEPTING (1<<2) //Used in situations where halting the whole "intercept" loop would be better, like supermatter dusting (and thus deleting) the atom.
//Misc text define. Does 4 spaces. Used as a makeshift tabulator.
-#define FOURSPACES " "
\ No newline at end of file
+#define FOURSPACES " "
+
+#define CANT_REENTER_ROUND -1
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 23ecbf0c..cc7f5d5d 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -442,12 +442,8 @@
candidates -= M
/proc/pollGhostCandidates(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE)
- var/list/candidates = list()
-
- for(var/mob/dead/observer/G in GLOB.player_list)
- if(G.can_reenter_round)
- candidates += G
-
+ var/datum/element/ghost_role_eligibility/eligibility = SSdcs.GetElement(/datum/element/ghost_role_eligibility)
+ var/list/candidates = eligibility.get_all_ghost_role_eligible()
return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates)
/proc/pollCandidates(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, list/group = null)
@@ -510,7 +506,7 @@
G_found.client.prefs.copy_to(new_character)
new_character.dna.update_dna_identity()
- new_character.key = G_found.key
+ G_found.transfer_ckey(new_character, FALSE)
return new_character
diff --git a/code/_globalvars/misc.dm b/code/_globalvars/misc.dm
index 899ccbe7..38147262 100644
--- a/code/_globalvars/misc.dm
+++ b/code/_globalvars/misc.dm
@@ -17,6 +17,8 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE) //BSA unlocked by head ID swipes
GLOBAL_LIST_EMPTY(player_details) // ckey -> /datum/player_details
+GLOBAL_LIST_EMPTY(clientless_round_timeouts) // ckey -> time that ckey can rejoin round
+
// All religion stuff
GLOBAL_VAR(religion)
GLOBAL_VAR(deity)
@@ -24,4 +26,4 @@ GLOBAL_VAR(bible_name)
GLOBAL_VAR(bible_icon_state)
GLOBAL_VAR(bible_item_state)
GLOBAL_VAR(holy_weapon_type)
-GLOBAL_VAR(holy_armor_type)
\ No newline at end of file
+GLOBAL_VAR(holy_armor_type)
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index a75d17fa..b487dc3d 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -132,6 +132,14 @@
min_val = 0
max_val = 1
+/datum/config_entry/number/suicide_reenter_round_timer
+ config_entry_value = 30
+ min_val = 0
+
+/datum/config_entry/number/roundstart_suicide_time_limit
+ config_entry_value = 30
+ min_val = 0
+
/datum/config_entry/number/shuttle_refuel_delay
config_entry_value = 12000
min_val = 0
diff --git a/code/datums/action.dm b/code/datums/action.dm
index cd0d31e0..66e837c7 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -141,6 +141,17 @@
current_button.add_overlay(mutable_appearance(icon_icon, button_icon_state))
current_button.button_icon_state = button_icon_state
+/datum/action/ghost
+ icon_icon = 'icons/mob/mob.dmi'
+ button_icon_state = "ghost"
+ name = "Ghostize"
+ desc = "Turn into a ghost and freely come back to your body."
+
+/datum/action/ghost/Trigger()
+ if(!..())
+ return 0
+ var/mob/M = target
+ M.ghostize(1)
//Presets for item actions
/datum/action/item_action
diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm
index f12835d2..1a5b5148 100644
--- a/code/datums/elements/_element.dm
+++ b/code/datums/elements/_element.dm
@@ -5,7 +5,7 @@
if(type == /datum/element)
return ELEMENT_INCOMPATIBLE
if(element_flags & ELEMENT_DETACH)
- RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/Detach)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/Detach, override = TRUE)
/datum/element/proc/Detach(datum/source, force)
UnregisterSignal(source, COMSIG_PARENT_QDELETING)
@@ -24,6 +24,10 @@
if(ele.Attach(arglist(args)) == ELEMENT_INCOMPATIBLE)
CRASH("Incompatible [eletype] assigned to a [type]! args: [json_encode(args)]")
-/datum/proc/RemoveElement(eletype)
- var/datum/element/ele = SSdcs.GetElement(eletype)
+/**
+ * Finds the singleton for the element type given and detaches it from src
+ * You only need additional arguments beyond the type if you're using ELEMENT_BESPOKE
+ */
+/datum/proc/RemoveElement(eletype, ...)
+ var/datum/element/ele = SSdcs.GetElement(arglist(args))
ele.Detach(src)
diff --git a/code/datums/elements/ghost_role_eligibility.dm b/code/datums/elements/ghost_role_eligibility.dm
new file mode 100644
index 00000000..8ecb579b
--- /dev/null
+++ b/code/datums/elements/ghost_role_eligibility.dm
@@ -0,0 +1,54 @@
+/datum/element/ghost_role_eligibility
+ element_flags = ELEMENT_DETACH
+ var/list/timeouts = list()
+ var/list/mob/eligible_mobs = list()
+
+/datum/element/ghost_role_eligibility/Attach(datum/target,penalize = FALSE)
+ . = ..()
+ if(!ismob(target))
+ return ELEMENT_INCOMPATIBLE
+ var/mob/M = target
+ if(!(M in eligible_mobs))
+ eligible_mobs += M
+ if(penalize) //penalizing them from making a ghost role / midround antag comeback right away.
+ var/penalty = CONFIG_GET(number/suicide_reenter_round_timer) MINUTES
+ var/roundstart_quit_limit = CONFIG_GET(number/roundstart_suicide_time_limit) MINUTES
+ if(world.time < roundstart_quit_limit) //add up the time difference to their antag rolling penalty if they quit before half a (ingame) hour even passed.
+ penalty += roundstart_quit_limit - world.time
+ if(penalty)
+ penalty += world.realtime
+ if(penalty - SSshuttle.realtimeofstart > SSshuttle.auto_call + SSshuttle.emergencyCallTime + SSshuttle.emergencyDockTime + SSshuttle.emergencyEscapeTime)
+ penalty = CANT_REENTER_ROUND
+ if(!(M.ckey in timeouts))
+ timeouts += M.ckey
+ timeouts[M.ckey] = 0
+ timeouts[M.ckey] = max(timeouts[M.ckey],penalty)
+
+/datum/element/ghost_role_eligibility/Detach(mob/M)
+ . = ..()
+ if(M in eligible_mobs)
+ eligible_mobs -= M
+
+/datum/element/ghost_role_eligibility/proc/get_all_ghost_role_eligible(silent = FALSE)
+ var/list/candidates = list()
+ for(var/m in eligible_mobs)
+ var/mob/M = m
+ if(M.can_reenter_round(TRUE))
+ candidates += M
+ return candidates
+
+/mob/proc/can_reenter_round(silent = FALSE)
+ var/datum/element/ghost_role_eligibility/eli = SSdcs.GetElement(/datum/element/ghost_role_eligibility)
+ return eli.can_reenter_round(src,silent)
+
+/datum/element/ghost_role_eligibility/proc/can_reenter_round(var/mob/M,silent = FALSE)
+ if(!(M in eligible_mobs))
+ return FALSE
+ if(!(M.ckey in timeouts))
+ return TRUE
+ var/timeout = timeouts[M.ckey]
+ if(timeout != CANT_REENTER_ROUND && timeout <= world.realtime)
+ return TRUE
+ if(!silent && M.client)
+ to_chat(M, "You are unable to reenter the round[timeout != CANT_REENTER_ROUND ? " yet. Your ghost role blacklist will expire in [DisplayTimeText(timeout - world.realtime)]" : ""].")
+ return FALSE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 25e26856..7bcd6c8a 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -17,6 +17,7 @@
var/list/living_antags = list()
var/list/dead_players = list()
var/list/list_observers = list()
+ var/list/ghost_eligible = list()
/datum/dynamic_ruleset/midround/from_ghosts
weight = 0
@@ -32,10 +33,11 @@
// So for example you can get the list of all current dead players with var/list/dead_players = candidates[CURRENT_DEAD_PLAYERS]
// Make sure to properly typecheck the mobs in those lists, as the dead_players list could contain ghosts, or dead players still in their bodies.
// We're still gonna trim the obvious (mobs without clients, jobbanned players, etc)
- living_players = trim_list(mode.current_players[CURRENT_LIVING_PLAYERS])
- living_antags = trim_list(mode.current_players[CURRENT_LIVING_ANTAGS])
- dead_players = trim_list(mode.current_players[CURRENT_DEAD_PLAYERS])
- list_observers = trim_list(mode.current_players[CURRENT_OBSERVERS])
+ living_players = trim_list(mode.current_players[CURRENT_LIVING_PLAYERS])
+ living_antags = trim_list(mode.current_players[CURRENT_LIVING_ANTAGS])
+ list_observers = trim_list(mode.current_players[CURRENT_OBSERVERS])
+ var/datum/element/ghost_role_eligibility/eligibility = SSdcs.GetElement(/datum/element/ghost_role_eligibility)
+ ghost_eligible = trim_list(eligibility.get_all_ghost_role_eligible())
/datum/dynamic_ruleset/midround/proc/trim_list(list/L = list())
var/list/trimmed_list = L.Copy()
@@ -65,6 +67,25 @@
continue
return trimmed_list
+/datum/dynamic_ruleset/midround/from_ghosts/trim_list(list/L = list())
+ var/list/trimmed_list = L.Copy()
+ for(var/mob/M in trimmed_list)
+ if (!M.client) // Are they connected?
+ trimmed_list.Remove(M)
+ continue
+ if(!mode.check_age(M.client, minimum_required_age))
+ trimmed_list.Remove(M)
+ continue
+ if(antag_flag_override)
+ if(!(antag_flag_override in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag_override))
+ trimmed_list.Remove(M)
+ continue
+ else
+ if(!(antag_flag in M.client.prefs.be_special) || jobban_isbanned(M.ckey, antag_flag))
+ trimmed_list.Remove(M)
+ continue
+ return trimmed_list
+
// You can then for example prompt dead players in execute() to join as strike teams or whatever
// Or autotator someone
@@ -85,15 +106,16 @@
return FALSE
return TRUE
-/datum/dynamic_ruleset/midround/from_ghosts/execute()
- var/list/possible_candidates = list()
- possible_candidates.Add(dead_players)
- possible_candidates.Add(list_observers)
- send_applications(possible_candidates)
- if(assigned.len > 0)
- return TRUE
- else
+/datum/dynamic_ruleset/midround/from_ghosts/ready(forced = FALSE)
+ if (required_candidates > ghost_eligible.len)
+ SSblackbox.record_feedback("tally","dynamic",1,"Times rulesets rejected due to not enough ghosts")
return FALSE
+ return ..()
+
+
+/datum/dynamic_ruleset/midround/from_ghosts/execute()
+ var/application_successful = send_applications(ghost_eligible)
+ return assigned.len > 0 && application_successful
/// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset.
/datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list())
@@ -596,4 +618,4 @@
#undef ABDUCTOR_MAX_TEAMS
-#undef REVENANT_SPAWN_THRESHOLD
\ No newline at end of file
+#undef REVENANT_SPAWN_THRESHOLD
diff --git a/code/game/objects/items/holy_weapons.dm b/code/game/objects/items/holy_weapons.dm
index 040cd53c..01b20d42 100644
--- a/code/game/objects/items/holy_weapons.dm
+++ b/code/game/objects/items/holy_weapons.dm
@@ -490,10 +490,10 @@
possessed = TRUE
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as the spirit of [user.real_name]'s blade?", ROLE_PAI, null, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE)
+ var/list/mob/candidates = pollGhostCandidates("Do you want to play as the spirit of [user.real_name]'s blade?", ROLE_PAI, null, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE)
if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
+ var/mob/C = pick(candidates)
var/mob/living/simple_animal/shade/S = new(src)
S.real_name = name
S.name = name
diff --git a/code/modules/admin/verbs/one_click_antag.dm b/code/modules/admin/verbs/one_click_antag.dm
index 9fdb0ea2..39dac328 100644
--- a/code/modules/admin/verbs/one_click_antag.dm
+++ b/code/modules/admin/verbs/one_click_antag.dm
@@ -138,9 +138,9 @@
/datum/admins/proc/makeWizard()
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for the position of a Wizard Foundation 'diplomat'?", ROLE_WIZARD, null)
+ var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for the position of a Wizard Foundation 'diplomat'?", ROLE_WIZARD, null)
- var/mob/dead/observer/selected = pick_n_take(candidates)
+ var/mob/selected = pick_n_take(candidates)
var/mob/living/carbon/human/new_character = makeBody(selected)
new_character.mind.make_Wizard()
@@ -215,9 +215,9 @@
/datum/admins/proc/makeNukeTeam()
var/datum/game_mode/nuclear/temp = new
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for a nuke team being sent in?", ROLE_OPERATIVE, temp)
- var/list/mob/dead/observer/chosen = list()
- var/mob/dead/observer/theghost = null
+ var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for a nuke team being sent in?", ROLE_OPERATIVE, temp)
+ var/list/mob/chosen = list()
+ var/mob/theghost = null
if(candidates.len)
var/numagents = 5
@@ -379,7 +379,7 @@
ertemplate.enforce_human = prefs["enforce_human"]["value"] == "Yes" ? TRUE : FALSE
ertemplate.opendoors = prefs["open_armory"]["value"] == "Yes" ? TRUE : FALSE
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you wish to be considered for [ertemplate.polldesc] ?", "deathsquad", null)
+ var/list/mob/candidates = pollGhostCandidates("Do you wish to be considered for [ertemplate.polldesc] ?", "deathsquad", null)
var/teamSpawned = FALSE
if(candidates.len > 0)
@@ -405,7 +405,7 @@
numagents--
continue // This guy's unlucky, not enough spawn points, we skip him.
var/spawnloc = spawnpoints[numagents]
- var/mob/dead/observer/chosen_candidate = pick(candidates)
+ var/mob/chosen_candidate = pick(candidates)
candidates -= chosen_candidate
if(!chosen_candidate.key)
continue
diff --git a/code/modules/antagonists/blob/blob/powers.dm b/code/modules/antagonists/blob/blob/powers.dm
index 9e915ee0..76c9c6f4 100644
--- a/code/modules/antagonists/blob/blob/powers.dm
+++ b/code/modules/antagonists/blob/blob/powers.dm
@@ -156,7 +156,7 @@
if(!can_buy(40))
return
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as a [blob_reagent_datum.name] blobbernaut?", ROLE_BLOB, null, ROLE_BLOB, 50) //players must answer rapidly
+ var/list/mob/candidates = pollGhostCandidates("Do you want to play as a [blob_reagent_datum.name] blobbernaut?", ROLE_BLOB, null, ROLE_BLOB, 50) //players must answer rapidly
if(LAZYLEN(candidates)) //if we got at least one candidate, they're a blobbernaut now.
B.max_integrity = initial(B.max_integrity) * 0.25 //factories that produced a blobbernaut have much lower health
B.obj_integrity = min(B.obj_integrity, B.max_integrity)
@@ -171,8 +171,8 @@
blobber.update_icons()
blobber.adjustHealth(blobber.maxHealth * 0.5)
blob_mobs += blobber
- var/mob/dead/observer/C = pick(candidates)
- blobber.key = C.key
+ var/mob/C = pick(candidates)
+ C.transfer_ckey(blobber)
SEND_SOUND(blobber, sound('sound/effects/blobattack.ogg'))
SEND_SOUND(blobber, sound('sound/effects/attackblob.ogg'))
to_chat(blobber, "You are a blobbernaut!")
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 3dd76c75..53170805 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -83,4 +83,4 @@
var/keysend_tripped = FALSE
/// Messages currently seen by this client
- var/list/seen_messages
\ No newline at end of file
+ var/list/seen_messages
diff --git a/code/modules/events/holiday/xmas.dm b/code/modules/events/holiday/xmas.dm
index a76c75dd..bc8be48e 100644
--- a/code/modules/events/holiday/xmas.dm
+++ b/code/modules/events/holiday/xmas.dm
@@ -75,7 +75,7 @@
/datum/round_event/santa/start()
var/list/candidates = pollGhostCandidates("Santa is coming to town! Do you want to be Santa?", poll_time=150)
if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
+ var/mob/C = pick(candidates)
santa = new /mob/living/carbon/human(pick(GLOB.blobstart))
santa.key = C.key
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index d42c5e05..0c05f720 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -135,7 +135,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
AA.onNewMob(src)
. = ..()
-
+ AddElement(/datum/element/ghost_role_eligibility)
grant_all_languages()
/mob/dead/observer/get_photo_description(obj/item/camera/camera)
@@ -261,17 +261,21 @@ Transfer_mind is there to check if mob is being deleted/not going to have a body
Works together with spawning an observer, noted above.
*/
-/mob/proc/ghostize(can_reenter_corpse = 1)
- if(key)
- if(!cmptext(copytext(key,1,2),"@")) // Skip aghosts.
- stop_sound_channel(CHANNEL_HEARTBEAT) //Stop heartbeat sounds because You Are A Ghost Now
- var/mob/dead/observer/ghost = new(src) // Transfer safety to observer spawning proc.
- SStgui.on_transfer(src, ghost) // Transfer NanoUIs.
- ghost.can_reenter_corpse = can_reenter_corpse
- ghost.can_reenter_round = (can_reenter_corpse && !suiciding)
- ghost.key = key
- ghost.client.lastrespawn = world.time + 1800 SECONDS
- return ghost
+/mob/proc/ghostize(can_reenter_corpse = TRUE, special = FALSE, penalize = FALSE)
+ penalize = suiciding || penalize // suicide squad.
+ if(!key || cmptext(copytext(key,1,2),"@") || (SEND_SIGNAL(src, COMSIG_MOB_GHOSTIZE, can_reenter_corpse, special, penalize) & COMPONENT_BLOCK_GHOSTING))
+ return //mob has no key, is an aghost or some component hijacked.
+ stop_sound_channel(CHANNEL_HEARTBEAT) //Stop heartbeat sounds because You Are A Ghost Now
+ var/mob/dead/observer/ghost = new(src) // Transfer safety to observer spawning proc.
+ SStgui.on_transfer(src, ghost) // Transfer NanoUIs.
+ ghost.can_reenter_corpse = can_reenter_corpse
+ if (client && client.prefs && client.prefs.auto_ooc)
+ if (!(client.prefs.chat_toggles & CHAT_OOC))
+ client.prefs.chat_toggles ^= CHAT_OOC
+ transfer_ckey(ghost, FALSE)
+ ghost.AddElement(/datum/element/ghost_role_eligibility,penalize) // technically already run earlier, but this adds the penalty
+ // needs to be done AFTER the ckey transfer, too
+ return ghost
/*
This is the proc mobs get to turn into a ghost. Forked from ghostize due to compatibility issues.
@@ -317,7 +321,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
var/response = alert(src, "Are you -sure- you want to ghost?\n(You are alive. If you ghost whilst still alive you won't be able to re-enter this round! You can't change your mind so choose wisely!!)","Are you sure you want to ghost?","Ghost","Stay in body")
if(response != "Ghost")
return
- ghostize(0)
+ ghostize(0, penalize = TRUE)
/mob/dead/observer/Move(NewLoc, direct)
if(updatedir)
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
index 5aec56b1..20d33a4b 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ b/code/modules/mob/living/simple_animal/guardian/guardian.dm
@@ -423,9 +423,9 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
var/mob/living/simple_animal/hostile/guardian/G = input(src, "Pick the guardian you wish to reset", "Guardian Reset") as null|anything in guardians
if(G)
to_chat(src, "You attempt to reset [G.real_name]'s personality...")
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as [src.real_name]'s [G.real_name]?", ROLE_PAI, null, FALSE, 100)
+ var/list/mob/candidates = pollGhostCandidates("Do you want to play as [src.real_name]'s [G.real_name]?", ROLE_PAI, null, FALSE, 100)
if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
+ var/mob/C = pick(candidates)
to_chat(G, "Your user reset you, and your body was taken over by a ghost. Looks like they weren't happy with your performance.")
to_chat(src, "Your [G.real_name] has been successfully reset.")
message_admins("[key_name_admin(C)] has taken control of ([key_name_admin(G)])")
@@ -497,10 +497,10 @@ GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
return
used = TRUE
to_chat(user, "[use_message]")
- var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as the [mob_name] of [user.real_name]?", ROLE_PAI, null, FALSE, 100, POLL_IGNORE_HOLOPARASITE)
+ var/list/mob/candidates = pollGhostCandidates("Do you want to play as the [mob_name] of [user.real_name]?", ROLE_PAI, null, FALSE, 100, POLL_IGNORE_HOLOPARASITE)
if(LAZYLEN(candidates))
- var/mob/dead/observer/C = pick(candidates)
+ var/mob/C = pick(candidates)
spawn_guardian(user, C.key)
else
to_chat(user, "[failure_message]")
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 1e6be0b3..4e50551f 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -459,7 +459,21 @@
// M.Login() //wat
return
-
+/mob/proc/transfer_ckey(mob/new_mob, send_signal = TRUE)
+ if(!new_mob || (!ckey && new_mob.ckey))
+ CRASH("transfer_ckey() called [new_mob ? "on ckey-less mob with a player mob as target" : "without a valid mob target"]!")
+ if(!ckey)
+ return
+ SEND_SIGNAL(new_mob, COMSIG_MOB_PRE_PLAYER_CHANGE, new_mob, src)
+ if (client && client.prefs && client.prefs.auto_ooc)
+ if (client.prefs.chat_toggles & CHAT_OOC && isliving(new_mob))
+ client.prefs.chat_toggles ^= CHAT_OOC
+ if (!(client.prefs.chat_toggles & CHAT_OOC) && isdead(new_mob))
+ client.prefs.chat_toggles ^= CHAT_OOC
+ new_mob.ckey = ckey
+ if(send_signal)
+ SEND_SIGNAL(src, COMSIG_MOB_KEY_CHANGE, new_mob, src)
+ return TRUE
/mob/verb/cancel_camera()
set name = "Cancel Camera View"
diff --git a/config/game_options.txt b/config/game_options.txt
index 5e635669..c97a0bfe 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -478,6 +478,18 @@ MIDROUND_ANTAG_TIME_CHECK 60
## A ratio of living to total crew members, the lower this is, the more people will have to die in order for midround antag to be skipped
MIDROUND_ANTAG_LIFE_CHECK 0.7
+## A "timeout", in real-time minutes, applied upon suicide, cryosleep or ghosting whilst alive,
+## during which the player shouldn't be able to come back into the round through
+## midround playable roles or mob spawners.
+## Set to 0 to completely disable it.
+SUICIDE_REENTER_ROUND_TIMER 30
+
+## A world time threshold, in minutes, under which the player receives
+## an extra timeout, purposely similar to the above one (and also stacks with),
+## equal to the difference between the current world.time and this threshold.
+## Both configs are indipendent from each other, disabling one won't affect the other.
+ROUNDSTART_SUICIDE_TIME_LIMIT 30
+
##Limit Spell Choices##
## Uncomment to disallow wizards from using certain spells that may be too chaotic/fun for your playerbase
diff --git a/modular_citadel/code/modules/client/preferences.dm b/modular_citadel/code/modules/client/preferences.dm
index a8a60db3..827b797c 100644
--- a/modular_citadel/code/modules/client/preferences.dm
+++ b/modular_citadel/code/modules/client/preferences.dm
@@ -15,6 +15,7 @@
var/arousable = TRUE
var/widescreenpref = TRUE
var/autostand = TRUE
+ var/auto_ooc = FALSE
var/lewdchem = TRUE
//vore prefs
diff --git a/tgstation.dme b/tgstation.dme
index c65c76ec..2e00b4a8 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -456,6 +456,7 @@
#include "code\datums\elements\_element.dm"
#include "code\datums\elements\cleaning.dm"
#include "code\datums\elements\earhealing.dm"
+#include "code\datums\elements\ghost_role_eligibility.dm"
#include "code\datums\helper_datums\events.dm"
#include "code\datums\helper_datums\getrev.dm"
#include "code\datums\helper_datums\icon_snapshot.dm"