diff --git a/code/datums/chat_message.dm b/code/datums/chat_message.dm
index ecba4221fc..3a95ba1db5 100644
--- a/code/datums/chat_message.dm
+++ b/code/datums/chat_message.dm
@@ -78,12 +78,11 @@ var/list/runechat_image_cache = list()
if(timer_delete)
deltimer(timer_delete)
timer_delete = null
- if(!QDELETED(owned_by))
+ if(istype(owned_by, /client)) // hopefully the PARENT_QDELETING on client should beat this if it's a disconnect
UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING)
- LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
+ if(owned_by.seen_messages)
+ LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
owned_by.images.Remove(message)
- if(!QDELETED(message_loc))
- UnregisterSignal(message_loc, COMSIG_PARENT_QDELETING)
owned_by = null
message_loc = null
message = null
@@ -108,7 +107,7 @@ var/list/runechat_image_cache = list()
// Register client who owns this message
owned_by = owner.client
- RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(qdel_self))
+ RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(unregister_qdel_self)) // this should only call owned_by if the client is destroyed
var/extra_length = owned_by.is_preference_enabled(/datum/client_preference/runechat_long_messages)
var/maxlen = extra_length ? CHAT_MESSAGE_EXT_LENGTH : CHAT_MESSAGE_LENGTH
@@ -217,6 +216,10 @@ var/list/runechat_image_cache = list()
spawn(lifespan - CHAT_MESSAGE_EOL_FADE)
end_of_life()
+/datum/chatmessage/proc/unregister_qdel_self() // this should only call owned_by if the client is destroyed
+ UnregisterSignal(owned_by, COMSIG_PARENT_QDELETING)
+ owned_by = null
+ qdel_self()
/**
* Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion
*/
diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm
index 5ce7fcbc58..38b48ee76a 100644
--- a/code/game/machinery/cryopod.dm
+++ b/code/game/machinery/cryopod.dm
@@ -473,70 +473,74 @@
SStranscore.leave_round(to_despawn)
//VOREStation Edit End - Resleeving.
- //Handle job slot/tater cleanup.
- var/job = to_despawn.mind.assigned_role
- job_master.FreeRole(job)
- to_despawn.mind.assigned_role = null
+ // Everything below should only be applicable to a cliented living/carbon/human.
+ // All living/carbon/humans should have minds.
- if(to_despawn.mind.objectives.len)
- qdel(to_despawn.mind.objectives)
- to_despawn.mind.special_role = null
+ //Handle job slot/tater cleanup.
+ var/job = to_despawn.mind.assigned_role
+ job_master.FreeRole(job)
+ to_despawn.mind.assigned_role = null
- //else
- //if(ticker.mode.name == "AutoTraitor")
- //var/datum/game_mode/traitor/autotraitor/current_mode = ticker.mode
- //current_mode.possible_traitors.Remove(to_despawn)
+ if(to_despawn.mind.objectives.len)
+ qdel(to_despawn.mind.objectives)
+ to_despawn.mind.special_role = null
- // Delete them from datacore.
+ //else
+ //if(ticker.mode.name == "AutoTraitor")
+ //var/datum/game_mode/traitor/autotraitor/current_mode = ticker.mode
+ //current_mode.possible_traitors.Remove(to_despawn)
- if(PDA_Manifest.len)
- PDA_Manifest.Cut()
- for(var/datum/data/record/R in data_core.medical)
- if((R.fields["name"] == to_despawn.real_name))
- qdel(R)
- for(var/datum/data/record/T in data_core.security)
- if((T.fields["name"] == to_despawn.real_name))
- qdel(T)
- for(var/datum/data/record/G in data_core.general)
- if((G.fields["name"] == to_despawn.real_name))
- qdel(G)
+ // Delete them from datacore.
- // Also check the hidden version of each datacore, if they're an offmap role.
- var/datum/job/J = SSjob.get_job(job)
- if(J?.offmap_spawn)
- for(var/datum/data/record/R in data_core.hidden_general)
+ if(PDA_Manifest.len)
+ PDA_Manifest.Cut()
+ for(var/datum/data/record/R in data_core.medical)
if((R.fields["name"] == to_despawn.real_name))
qdel(R)
- for(var/datum/data/record/T in data_core.hidden_security)
+ for(var/datum/data/record/T in data_core.security)
if((T.fields["name"] == to_despawn.real_name))
qdel(T)
- for(var/datum/data/record/G in data_core.hidden_medical)
+ for(var/datum/data/record/G in data_core.general)
if((G.fields["name"] == to_despawn.real_name))
qdel(G)
- icon_state = base_icon_state
+ // Also check the hidden version of each datacore, if they're an offmap role.
+ var/datum/job/J = SSjob.get_job(job)
+ if(J?.offmap_spawn)
+ for(var/datum/data/record/R in data_core.hidden_general)
+ if((R.fields["name"] == to_despawn.real_name))
+ qdel(R)
+ for(var/datum/data/record/T in data_core.hidden_security)
+ if((T.fields["name"] == to_despawn.real_name))
+ qdel(T)
+ for(var/datum/data/record/G in data_core.hidden_medical)
+ if((G.fields["name"] == to_despawn.real_name))
+ qdel(G)
- //TODO: Check objectives/mode, update new targets if this mob is the target, spawn new antags?
+ icon_state = base_icon_state
+
+ //TODO: Check objectives/mode, update new targets if this mob is the target, spawn new antags?
- //Make an announcement and log the person entering storage.
- control_computer.frozen_crew += "[to_despawn.real_name], [to_despawn.mind.role_alt_title] - [stationtime2text()]"
- control_computer._admin_logs += "[key_name(to_despawn)] ([to_despawn.mind.role_alt_title]) at [stationtime2text()]"
- log_and_message_admins("[key_name(to_despawn)] ([to_despawn.mind.role_alt_title]) entered cryostorage.")
+ //Make an announcement and log the person entering storage.
+ control_computer.frozen_crew += "[to_despawn.real_name], [to_despawn.mind.role_alt_title] - [stationtime2text()]"
+ control_computer._admin_logs += "[key_name(to_despawn)] ([to_despawn.mind.role_alt_title]) at [stationtime2text()]"
+ log_and_message_admins("[key_name(to_despawn)] ([to_despawn.mind.role_alt_title]) entered cryostorage.")
- //VOREStation Edit Start
- var/depart_announce = TRUE
- var/departing_job = to_despawn.mind.role_alt_title
+ //VOREStation Edit Start
+ var/depart_announce = TRUE
+ var/departing_job = to_despawn.mind.role_alt_title
- if(istype(to_despawn, /mob/living/dominated_brain))
- depart_announce = FALSE
+ if(istype(to_despawn, /mob/living/dominated_brain))
+ depart_announce = FALSE
- if(depart_announce)
- announce.autosay("[to_despawn.real_name][departing_job ? ", [departing_job], " : " "][on_store_message]", "[on_store_name]", announce_channel, using_map.get_map_levels(z, TRUE, om_range = DEFAULT_OVERMAP_RANGE))
- visible_message("\The [initial(name)] [on_store_visible_message_1] [to_despawn.real_name] [on_store_visible_message_2]", 3)
+ if(depart_announce)
+ announce.autosay("[to_despawn.real_name][departing_job ? ", [departing_job], " : " "][on_store_message]", "[on_store_name]", announce_channel, using_map.get_map_levels(z, TRUE, om_range = DEFAULT_OVERMAP_RANGE))
+ visible_message("\The [initial(name)] [on_store_visible_message_1] [to_despawn.real_name] [on_store_visible_message_2]", 3)
- //VOREStation Edit End
+
+ //VOREStation Edit End
//VOREStation Edit begin: Dont delete mobs-in-mobs
if(to_despawn.client && to_despawn.stat<2)
diff --git a/code/game/objects/items/devices/text_to_speech.dm b/code/game/objects/items/devices/text_to_speech.dm
index 749bb65173..d2fa720e6a 100644
--- a/code/game/objects/items/devices/text_to_speech.dm
+++ b/code/game/objects/items/devices/text_to_speech.dm
@@ -26,7 +26,7 @@
if(message)
audible_message("[icon2html(src, user.client)] \The [src.name] states, \"[message]\"", runemessage = "synthesized speech")
if(ismob(loc))
- loc.audible_message("", runemessage = "\[TTS Voice\] [message]")
+ loc.runechat_message("\[TTS Voice\] [message]")
/obj/item/device/text_to_speech/AltClick(mob/user) // QOL Change
attack_self(user)
diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm
index 0f053b73ee..f3b7c76106 100644
--- a/code/modules/client/client procs.dm
+++ b/code/modules/client/client procs.dm
@@ -271,19 +271,30 @@
//DISCONNECT//
//////////////
/client/Del()
+ if(!gc_destroyed)
+ gc_destroyed = world.time
+ if (!QDELING(src))
+ stack_trace("Client does not purport to be QDELING, this is going to cause bugs in other places!")
+
+ GLOB.ahelp_tickets.ClientLogout(src)
+ GLOB.mhelp_tickets.ClientLogout(src)
+
+ // Yes this is the same as what's found in qdel(). Yes it does need to be here
+ // Get off my back
+ SEND_SIGNAL(src, COMSIG_PARENT_QDELETING, TRUE)
+ Destroy() //Clean up signals and timers.
+ return ..()
+
+/client/Destroy()
if(holder)
holder.owner = null
GLOB.admins -= src
if (mentorholder)
mentorholder.owner = null
GLOB.mentors -= src
- GLOB.ahelp_tickets.ClientLogout(src)
- GLOB.mhelp_tickets.ClientLogout(src)
GLOB.directory -= ckey
GLOB.clients -= src
- return ..()
-/client/Destroy()
..()
return QDEL_HINT_HARDDEL_NOW
diff --git a/code/modules/client/preference_setup/general/03_body.dm b/code/modules/client/preference_setup/general/03_body.dm
index 6d35219a1d..71a442415a 100644
--- a/code/modules/client/preference_setup/general/03_body.dm
+++ b/code/modules/client/preference_setup/general/03_body.dm
@@ -418,7 +418,7 @@ var/global/list/valid_bloodtypes = list("A+", "A-", "B+", "B-", "AB+", "AB-", "O
for(var/BP in mark_datum.body_parts)
var/obj/item/organ/external/O = character.organs_by_name[BP]
- if(O)
+ if(O && islist(O.markings) && islist(pref.body_markings[M]) && islist(pref.body_markings[M][BP]))
O.markings[M] = list("color" = pref.body_markings[M][BP]["color"], "datum" = mark_datum, "priority" = priority, "on" = pref.body_markings[M][BP]["on"])
character.markings_len = priority
diff --git a/code/modules/client/preference_setup/global/02_settings.dm b/code/modules/client/preference_setup/global/02_settings.dm
index fcdb85b843..71ff5d0a09 100644
--- a/code/modules/client/preference_setup/global/02_settings.dm
+++ b/code/modules/client/preference_setup/global/02_settings.dm
@@ -91,7 +91,7 @@
preference = list(preference)
for(var/p in preference)
var/datum/client_preference/cp = get_client_preference(p)
- if(!prefs || !cp || !(cp.key in prefs.preferences_enabled))
+ if(!prefs || !cp || !istype(cp, /datum/client_preference) || !(cp.key in prefs.preferences_enabled))
return FALSE
return TRUE
diff --git a/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm b/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
index f336a45b86..37ef332a14 100644
--- a/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
@@ -124,6 +124,8 @@ I think I covered everything.
var/small_icon = 'icons/mob/bigdragon_small.dmi'
var/small_icon_state = "dragon_small"
var/flames
+ var/firebreathtimer
+ var/chargetimer
tame_items = list(
/obj/item/weapon/coin/gold = 100,
@@ -263,6 +265,10 @@ I think I covered everything.
/mob/living/simple_mob/vore/bigdragon/runechat_y_offset(width, height)
return (..()*size_multiplier) + 40
+/mob/living/simple_mob/vore/bigdragon/death()
+ . = ..()
+ canceltimers()
+
///
/// Verbs
///
@@ -766,10 +772,14 @@ I think I covered everything.
do_windup_animation(A, charge_warmup)
//callbacks are more reliable than byond's process scheduler
- addtimer(CALLBACK(src, PROC_REF(chargeend), A), charge_warmup)
+ chargetimer = addtimer(CALLBACK(src, PROC_REF(chargeend), A), charge_warmup, TIMER_STOPPABLE)
/mob/living/simple_mob/vore/bigdragon/proc/chargeend(var/atom/A, var/explicit = 0, var/gentle = 0)
+ //make sure our target still exists and is on a turf
+ if(QDELETED(A) || !isturf(get_turf(A)))
+ set_AI_busy(FALSE)
+ return
status_flags |= LEAPING
flying = 1 //So we can thunk into things
hovering = 1 // So we don't hurt ourselves running off cliffs
@@ -804,10 +814,14 @@ I think I covered everything.
set_AI_busy(TRUE)
flames = 1
build_icons()
- addtimer(CALLBACK(src, PROC_REF(firebreathend), A), charge_warmup)
+ firebreathtimer = addtimer(CALLBACK(src, PROC_REF(firebreathend), A), charge_warmup, TIMER_STOPPABLE)
playsound(src, "sound/magic/Fireball.ogg", 50, 1)
/mob/living/simple_mob/vore/bigdragon/proc/firebreathend(var/atom/A)
+ //make sure our target still exists and is on a turf
+ if(QDELETED(A) || !isturf(get_turf(A)))
+ set_AI_busy(FALSE)
+ return
var/obj/item/projectile/P = new /obj/item/projectile/bullet/dragon(get_turf(src))
src.visible_message("\The [src] spews fire at \the [A]!")
playsound(src, "sound/weapons/Flamer.ogg", 50, 1)
@@ -891,6 +905,9 @@ I think I covered everything.
vore_selected = gut2 //Just incase it eats someone right after being tamed
ai_holder = new /datum/ai_holder/simple_mob/healbelly/retaliate/dragon(src)
+ //Cancel any charges or firebreaths winding up
+ canceltimers()
+
/datum/ai_holder/simple_mob/healbelly
intelligence_level = 3
can_breakthrough = 0
@@ -1036,6 +1053,17 @@ I think I covered everything.
vore_selected = gut1
D.give_target(attacker)
+/mob/living/simple_mob/vore/bigdragon/proc/canceltimers()
+ //Cancel any charges or firebreaths winding up
+ if(firebreathtimer)
+ deltimer(firebreathtimer)
+ firebreathtimer = null
+ if(chargetimer)
+ deltimer(chargetimer)
+ chargetimer = null
+ //re-enable the AI
+ set_AI_busy(FALSE)
+
//Smack people it warns
/datum/ai_holder/simple_mob/healbelly/retaliate/dragon/proc/dissuade(var/chump)
if(chump in check_trajectory(chump, holder, pass_flags = PASSTABLE))
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 9efeca0e58..643ad28105 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1220,12 +1220,24 @@
return 0
//Exploitable Info Update
+/obj
+ var/datum/weakref/exploit_for //if this obj is an exploit for somebody, this points to them
/mob/proc/amend_exploitable(var/obj/item/I)
if(istype(I))
exploit_addons |= I
var/exploitmsg = html_decode("\n" + "Has " + I.name + ".")
exploit_record += exploitmsg
+ I.exploit_for = WEAKREF(src)
+
+
+/obj/Destroy()
+ if(exploit_for)
+ var/mob/exploited = exploit_for.resolve()
+ exploited?.exploit_addons -= src
+ exploit_for = null
+ . = ..()
+
/obj/Destroy()
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 2dbe340951..42dcae7dcc 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -89,7 +89,7 @@
* return bool - TRUE if a new pooled window is opened, FALSE in all other situations including if a new pooled window didn't open because one already exists.
*/
/datum/tgui/proc/open()
- if(!user.client)
+ if(!user?.client)
return FALSE
if(window)
return FALSE
@@ -214,7 +214,7 @@
* optional force bool Send an update even if UI is not interactive.
*/
/datum/tgui/proc/send_full_update(custom_data, force)
- if(!user.client || !initialized || closing)
+ if(!user?.client || !initialized || closing)
return
//if(!COOLDOWN_FINISHED(src, refresh_cooldown))
//refreshing = TRUE