diff --git a/_maps/RandomZLevels/VR/yuma_VR.dmm b/_maps/RandomZLevels/VR/yuma_VR.dmm
index f0187d84769..d61ccfdff4c 100644
--- a/_maps/RandomZLevels/VR/yuma_VR.dmm
+++ b/_maps/RandomZLevels/VR/yuma_VR.dmm
@@ -3575,12 +3575,6 @@
},
/turf/open/floor/carpet/green,
/area/awaymission/vr/bos/casino)
-"xW" = (
-/obj/machinery/power/emitter/energycannon{
- dir = 1
- },
-/turf/open/floor/plating,
-/area/awaymission/vr/public/deathmatch)
"ya" = (
/turf/open/floor/wood_worn,
/area/awaymission/vr/den)
@@ -5005,10 +4999,6 @@
},
/turf/open/floor/plasteel/dark,
/area/awaymission/vr/bos/deathmatch)
-"Ky" = (
-/obj/machinery/power/emitter/energycannon,
-/turf/open/floor/plating,
-/area/awaymission/vr/bos/deathmatch)
"KE" = (
/obj/machinery/light/small,
/turf/open/floor/plating/tunnel,
@@ -5487,12 +5477,6 @@
"Oo" = (
/turf/open/floor/circuit/green/off,
/area/awaymission/vr/bos/deathmatch)
-"Op" = (
-/obj/machinery/power/emitter/energycannon{
- dir = 1
- },
-/turf/open/floor/plating,
-/area/awaymission/vr/bos/deathmatch)
"Ow" = (
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/f13/wood{
@@ -6597,10 +6581,6 @@
icon_state = "redrustyfull"
},
/area/awaymission/vr/bos/enclavebunker)
-"XA" = (
-/obj/machinery/power/emitter/energycannon,
-/turf/open/floor/plating,
-/area/awaymission/vr/public/deathmatch)
"XG" = (
/obj/effect/turf_decal/stripes/line{
dir = 10
@@ -44110,11 +44090,11 @@ sT
Je
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
Jf
@@ -44153,11 +44133,11 @@ Nf
OJ
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
bz
@@ -45138,11 +45118,11 @@ sT
Rt
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
hM
@@ -45181,11 +45161,11 @@ Nf
Eo
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
gb
@@ -46166,11 +46146,11 @@ sT
Je
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
WP
@@ -46209,11 +46189,11 @@ Nf
OJ
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
fK
@@ -47451,11 +47431,11 @@ Rk
EF
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
hM
@@ -47494,11 +47474,11 @@ lv
lw
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
gb
@@ -48712,11 +48692,11 @@ hM
WP
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
Je
@@ -48736,11 +48716,11 @@ RL
Je
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
WP
@@ -48755,11 +48735,11 @@ gb
fK
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
OJ
@@ -48779,11 +48759,11 @@ fI
OJ
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
fK
@@ -49740,11 +49720,11 @@ Jf
hM
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
NC
@@ -49764,11 +49744,11 @@ RL
NC
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
hM
@@ -49783,11 +49763,11 @@ bz
gb
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
tO
@@ -49807,11 +49787,11 @@ fI
tO
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
gb
@@ -50768,11 +50748,11 @@ hM
WP
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
Je
@@ -50792,11 +50772,11 @@ RL
Je
WP
Je
-Ky
+ui
ZS
ui
ui
-Op
+ui
EF
EF
WP
@@ -50811,11 +50791,11 @@ gb
fK
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
OJ
@@ -50835,11 +50815,11 @@ fI
OJ
fK
OJ
-XA
+xR
fz
xR
xR
-xW
+xR
lw
lw
fK
@@ -52053,11 +52033,11 @@ Jf
hM
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
EF
EF
@@ -52096,11 +52076,11 @@ bz
gb
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
lw
lw
@@ -53338,11 +53318,11 @@ hM
WP
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
Je
@@ -53381,11 +53361,11 @@ gb
fK
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
OJ
@@ -54366,11 +54346,11 @@ hM
hM
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
rC
@@ -54409,11 +54389,11 @@ gb
gb
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
Rm
@@ -55394,11 +55374,11 @@ hM
Jf
EF
EF
-Ky
+ui
ZS
ui
ui
-Op
+ui
Je
WP
Je
@@ -55437,11 +55417,11 @@ gb
bz
lw
lw
-XA
+xR
fz
xR
xR
-xW
+xR
OJ
fK
OJ
diff --git a/_maps/map_files/Tipton/Tipton-Dungeon.dmm b/_maps/map_files/Tipton/Tipton-Dungeon.dmm
index 1df6e26d668..6fb19dbe6ee 100644
--- a/_maps/map_files/Tipton/Tipton-Dungeon.dmm
+++ b/_maps/map_files/Tipton/Tipton-Dungeon.dmm
@@ -9545,19 +9545,6 @@
},
/turf/open/floor/plasteel/f13/vault_floor/floor,
/area/f13/vault/dormitory)
-"kTK" = (
-/obj/docking_port/stationary{
- area_type = /area/f13;
- dir = 2;
- dwidth = 1;
- height = 1;
- id = "North_Ground";
- name = "Ground";
- roundstart_template = /datum/map_template/shuttle/northbunker/elevator;
- width = 4
- },
-/turf/open/floor/plasteel/elevatorshaft,
-/area/f13/bunker/bunkertwo)
"kTV" = (
/obj/machinery/light{
bulb_colour = "#BC8F8F";
@@ -106602,7 +106589,7 @@ epd
epd
epd
rZT
-kTK
+orH
orH
orH
oLw
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index e7b8d24cbe5..3675077d5da 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -134,7 +134,7 @@ SUBSYSTEM_DEF(statpanels)
if(length(turfitems) < 30) // only create images for the first 30 items on the turf, for performance reasons
if(!(REF(turf_content) in cached_images))
cached_images += REF(turf_content)
- turf_content.RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/atom, remove_from_cache)) // we reset cache if anything in it gets deleted
+ RegisterSignal(turf_content, COMSIG_PARENT_QDELETING, PROC_REF(remove_from_cache), override = TRUE)
if(ismob(turf_content) || length(turf_content.overlays) > 2)
turfitems[++turfitems.len] = list("[turf_content.name]", REF(turf_content), costly_icon2html(turf_content, target, sourceonly=TRUE))
else
@@ -170,9 +170,9 @@ SUBSYSTEM_DEF(statpanels)
mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", "\ref[GLOB.cameranet]")
mc_data_encoded = url_encode(json_encode(mc_data))
-/atom/proc/remove_from_cache()
+/datum/controller/subsystem/statpanels/proc/remove_from_cache(atom/source)
SIGNAL_HANDLER
- SSstatpanels.cached_images -= REF(src)
+ cached_images -= REF(source)
/// verbs that send information from the browser UI
/client/verb/set_tab(tab as text|null)
diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm
index ca92797e040..f3d3cef4354 100644
--- a/code/datums/traits/negative.dm
+++ b/code/datums/traits/negative.dm
@@ -766,17 +766,18 @@ Edit: TK~ This is the dumbest fucking shit I've ever seen in my life. This isn
mood_change = -3
timeout = 0
-/datum/quirk/masked_mook/on_spawn()
- . = ..()
- var/mob/living/carbon/human/H = quirk_holder
- var/list/obj/item/clothing/masks = subtypesof(/obj/item/clothing/mask)
- var/obj/item/clothing/mask/chosen = pick(masks)
- var/list/ciggies = subtypesof(/obj/item/clothing/mask/cigarette)
- while(chosen in ciggies)
- chosen = pick(masks)
- var/obj/item/clothing/mask/gas = new chosen(get_turf(quirk_holder))
- H.equip_to_slot(gas, SLOT_WEAR_MASK)
- H.regenerate_icons()
+// this is the code that gives the player a mask on spawn. If somebody takes the trait, they're responsible for getting their own mask in the first place, not getting it given to them.
+// /datum/quirk/masked_mook/on_spawn()
+// . = ..()
+// var/mob/living/carbon/human/H = quirk_holder
+// var/list/obj/item/clothing/masks = subtypesof(/obj/item/clothing/mask)
+// var/obj/item/clothing/mask/chosen = pick(masks)
+// var/list/ciggies = subtypesof(/obj/item/clothing/mask/cigarette)
+// while(chosen in ciggies)
+// chosen = pick(masks)
+// var/obj/item/clothing/mask/gas = new chosen(get_turf(quirk_holder))
+// H.equip_to_slot(gas, SLOT_WEAR_MASK)
+// H.regenerate_icons()
/datum/quirk/paper_skin
name = "Paper Skin"
diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm
index 39537ee818a..26b0307e89a 100644
--- a/code/game/machinery/autolathe.dm
+++ b/code/game/machinery/autolathe.dm
@@ -74,9 +74,9 @@
/obj/machinery/autolathe/Destroy()
QDEL_NULL(wires)
- QDEL_NULL(stored_research) // Clean up techweb datum
- QDEL_NULL(being_built) // Clear current design
- QDEL_LIST(matching_designs) // Clear design list
+ QDEL_NULL(stored_research)
+ being_built = null // or just omit - it's likely already null
+ matching_designs = null // clear references, don't delete contents
return ..()
/obj/machinery/autolathe/ui_interact(mob/user)
@@ -772,8 +772,6 @@
/obj/machinery/autolathe/ammo/can_build(datum/design/D, amount = 1)
if(!D)
return FALSE
- if(!islist(D.category)) // Changed from !D.category
- return FALSE
if("Handloaded Ammo" in D.category)
return ..()
if("Handmade Magazines" in D.category)
diff --git a/code/game/machinery/transformer.dm b/code/game/machinery/transformer.dm
index fd929ce61ce..d76be6e0613 100644
--- a/code/game/machinery/transformer.dm
+++ b/code/game/machinery/transformer.dm
@@ -31,9 +31,11 @@
if(cooldown && (hasSiliconAccessInArea(user) || isobserver(user)))
. += "It will be ready in [DisplayTimeText(cooldown_timer - world.time)]."
+// Fix transformer to clear masterAI reference
/obj/machinery/transformer/Destroy()
QDEL_NULL(countdown)
- . = ..()
+ masterAI = null
+ return ..()
/obj/machinery/transformer/power_change()
..()
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index 3663b5a0fc7..af6699a6f42 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -7,6 +7,70 @@
density = FALSE
max_integrity = 15
+// New procs to replace spawn() chains
+/obj/structure/spider/spiderling/proc/vent_travel_stage1(datum/weakref/ourref, obj/machinery/atmospherics/components/unary/vent_pump/exit_vent, obj/machinery/atmospherics/components/unary/vent_pump/start_vent)
+ var/obj/structure/spider/spiderling/us = ourref?.resolve()
+ if(!us || us.being_deleted || QDELETED(us))
+ return
+
+ us.forceMove(exit_vent)
+ var/travel_time = round(get_dist(us.loc, exit_vent.loc) / 2)
+
+ addtimer(CALLBACK(us, PROC_REF(vent_travel_stage2), ourref, exit_vent, start_vent, travel_time), travel_time)
+
+/obj/structure/spider/spiderling/proc/vent_travel_stage2(datum/weakref/ourref, obj/machinery/atmospherics/components/unary/vent_pump/exit_vent, obj/machinery/atmospherics/components/unary/vent_pump/start_vent, travel_time)
+ var/obj/structure/spider/spiderling/us = ourref?.resolve()
+ if(!us || us.being_deleted || QDELETED(us))
+ return
+
+ if(!exit_vent || exit_vent.welded)
+ if(start_vent && !QDELETED(start_vent))
+ us.forceMove(start_vent)
+ us.entry_vent = null
+ return
+ if(prob(50))
+ us.audible_message(span_italic("You hear something scampering through the ventilation ducts."))
+
+ addtimer(CALLBACK(us, PROC_REF(vent_travel_stage3), ourref, exit_vent, start_vent), travel_time)
+
+/obj/structure/spider/spiderling/proc/vent_travel_stage3(datum/weakref/ourref, obj/machinery/atmospherics/components/unary/vent_pump/exit_vent, obj/machinery/atmospherics/components/unary/vent_pump/start_vent)
+ var/obj/structure/spider/spiderling/us = ourref?.resolve()
+ if(!us || us.being_deleted || QDELETED(us))
+ return
+
+ if(!exit_vent || exit_vent.welded)
+ if(start_vent && !QDELETED(start_vent))
+ us.forceMove(start_vent)
+ us.entry_vent = null
+ return
+
+ us.forceMove(exit_vent.loc)
+ us.entry_vent = null
+ var/area/new_area = get_area(us.loc)
+ if(new_area)
+ new_area.Entered(us)
+
+/obj/structure/spider/spiderling/proc/grow_into_spider()
+ if(being_deleted || QDELETED(src))
+ return
+
+ if(!grow_as)
+ if(prob(3))
+ grow_as = pick(/mob/living/simple_animal/hostile/poison/giant_spider/tarantula, /mob/living/simple_animal/hostile/poison/giant_spider/hunter/viper, /mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife)
+ else
+ grow_as = pick(/mob/living/simple_animal/hostile/poison/giant_spider, /mob/living/simple_animal/hostile/poison/giant_spider/hunter, /mob/living/simple_animal/hostile/poison/giant_spider/nurse)
+
+ var/mob/living/simple_animal/hostile/poison/giant_spider/S = new grow_as(src.loc)
+ S.poison_per_bite = poison_per_bite
+ S.poison_type = poison_type
+ S.faction = faction.Copy()
+ S.directive = directive
+
+ if(player_spiders)
+ S.playable_spider = TRUE
+ notify_ghosts("Spider [S.name] can be controlled", null, enter_link="(Click to play)", source=S, action=NOTIFY_ATTACK, ignore_key = POLL_IGNORE_SPIDER, ignore_dnr_observers = TRUE)
+
+ qdel(src)
/obj/structure/spider/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
@@ -92,6 +156,16 @@
START_PROCESSING(SSobj, src)
. = ..()
+/obj/structure/spider/eggcluster/Destroy()
+ // Stop processing FIRST
+ STOP_PROCESSING(SSobj, src)
+
+ // Clear all references
+ poison_type = null
+ directive = null
+ faction = null
+
+ return ..()
/obj/structure/spider/eggcluster/process()
amount_grown += rand(0,2)
if(amount_grown >= 100)
@@ -118,20 +192,37 @@
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent
var/travelling_in_vent = 0
var/player_spiders = 0
- var/directive = "" //Message from the mother
+ var/directive = ""
var/poison_type = "toxin"
var/poison_per_bite = 5
var/list/faction = list("spiders")
+ var/being_deleted = FALSE
+ // Track active callbacks so we can cancel them
+ var/list/active_callbacks = list()
attack_hand_speed = CLICK_CD_MELEE
attack_hand_is_action = TRUE
/obj/structure/spider/spiderling/Destroy()
- // Break parent references
- RemoveComponentByType(/datum/component/swarming)
+ // Set flag FIRST
+ being_deleted = TRUE
+
+ // CRITICAL: Stop processing IMMEDIATELY
+ STOP_PROCESSING(SSobj, src)
+
+ // Remove component BEFORE clearing references
+ var/datum/component/swarming/swarm = GetComponent(/datum/component/swarming)
+ if(swarm)
+ // Let the component clean itself up properly
+ qdel(swarm)
+
+ // Clear all references
entry_vent = null
grow_as = null
directive = null
faction = null
+ poison_type = null
+ active_callbacks = null
+
return ..()
/obj/structure/spider/spiderling/Initialize()
@@ -172,6 +263,11 @@
return TRUE
/obj/structure/spider/spiderling/process()
+ // ALWAYS check first
+ if(being_deleted || QDELETED(src))
+ STOP_PROCESSING(SSobj, src)
+ return
+
if(travelling_in_vent)
if(isturf(loc))
travelling_in_vent = 0
@@ -190,31 +286,10 @@
visible_message("[src] scrambles into the ventilation ducts!", \
span_italic("You hear something scampering through the ventilation ducts."))
- spawn(rand(20,60))
- forceMove(exit_vent)
- var/travel_time = round(get_dist(loc, exit_vent.loc) / 2)
- spawn(travel_time)
-
- if(!exit_vent || exit_vent.welded)
- forceMove(entry_vent)
- entry_vent = null
- return
-
- if(prob(50))
- audible_message(span_italic("You hear something scampering through the ventilation ducts."))
- sleep(travel_time)
-
- if(!exit_vent || exit_vent.welded)
- forceMove(entry_vent)
- entry_vent = null
- return
- forceMove(exit_vent.loc)
- entry_vent = null
- var/area/new_area = get_area(loc)
- if(new_area)
- new_area.Entered(src)
- //=================
-
+ // Store weakref for callbacks to check
+ var/ourref = WEAKREF(src)
+ // Start the vent travel - callbacks will self-check if we're deleted
+ addtimer(CALLBACK(src, PROC_REF(vent_travel_stage1), ourref, exit_vent, entry_vent), rand(20,60))
else if(prob(33))
var/list/nearby = oview(10, src)
if(nearby.len)
@@ -223,31 +298,16 @@
if(prob(40))
src.visible_message(span_notice("\The [src] skitters[pick(" away"," around","")]."))
else if(prob(10))
- //ventcrawl!
for(var/obj/machinery/atmospherics/components/unary/vent_pump/v in view(7,src))
if(!v.welded)
entry_vent = v
walk_to(src, entry_vent, 1)
break
+
if(isturf(loc))
amount_grown += rand(0,2)
if(amount_grown >= 100)
- if(!grow_as)
- if(prob(3))
- grow_as = pick(/mob/living/simple_animal/hostile/poison/giant_spider/tarantula, /mob/living/simple_animal/hostile/poison/giant_spider/hunter/viper, /mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife)
- else
- grow_as = pick(/mob/living/simple_animal/hostile/poison/giant_spider, /mob/living/simple_animal/hostile/poison/giant_spider/hunter, /mob/living/simple_animal/hostile/poison/giant_spider/nurse)
- var/mob/living/simple_animal/hostile/poison/giant_spider/S = new grow_as(src.loc)
- S.poison_per_bite = poison_per_bite
- S.poison_type = poison_type
- S.faction = faction.Copy()
- S.directive = directive
- if(player_spiders)
- S.playable_spider = TRUE
- notify_ghosts("Spider [S.name] can be controlled", null, enter_link="(Click to play)", source=S, action=NOTIFY_ATTACK, ignore_key = POLL_IGNORE_SPIDER, ignore_dnr_observers = TRUE)
- qdel(src)
-
-
+ grow_into_spider()
/obj/structure/spider/cocoon
name = "cocoon"
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 5a6e217b5af..f12c94bed07 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1177,8 +1177,14 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/proc/updateEmbedding()
if(!LAZYLEN(embedding))
- return
+ // Clean up if no embedding data
+ RemoveElement(/datum/element/embed)
+ return FALSE
+ // Remove existing element before adding new one
+ // This prevents duplicate signal registration
+ RemoveElement(/datum/element/embed)
+
AddElement(/datum/element/embed,\
embed_chance = (!isnull(embedding["embed_chance"]) ? embedding["embed_chance"] : EMBED_CHANCE),\
fall_chance = (!isnull(embedding["fall_chance"]) ? embedding["fall_chance"] : EMBEDDED_ITEM_FALLOUT),\
diff --git a/code/game/objects/items/melee/f13powerfist.dm b/code/game/objects/items/melee/f13powerfist.dm
index 6fdd613a336..ddbe585e9cf 100644
--- a/code/game/objects/items/melee/f13powerfist.dm
+++ b/code/game/objects/items/melee/f13powerfist.dm
@@ -316,9 +316,16 @@
remove_sword()
/obj/item/shishkebabpack/proc/remove_sword()
+ // Check if sword exists before trying to access it
+ if(!sword || QDELETED(sword))
+ return
+
+ // Check if sword is in a mob's inventory
if(ismob(sword.loc))
var/mob/M = sword.loc
M.temporarilyRemoveItemFromInventory(sword, TRUE)
+
+ // Move sword back to backpack
sword.forceMove(src)
/obj/item/shishkebabpack/Destroy()
@@ -346,8 +353,10 @@
return ..()
/obj/item/shishkebabpack/dropped(mob/user)
- ..()
- remove_sword()
+ . = ..()
+ // Only try to remove sword if we're not being deleted
+ if(!QDELETED(src))
+ remove_sword()
// Shishkebab sword Keywords: Damage 55 (fire), Tool welder
/obj/item/weapon/melee/shishkebab //This should never exist without the backpack.
diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm
index fcdb646677d..9cd070800fe 100644
--- a/code/game/objects/items/robot/robot_parts.dm
+++ b/code/game/objects/items/robot/robot_parts.dm
@@ -26,6 +26,18 @@
..()
update_icon()
+// IMPORTANT: Also add this cleanup to robot_suit to prevent circular references
+/obj/item/robot_suit/Destroy()
+ // Clear all bodypart references
+ l_arm = null
+ r_arm = null
+ l_leg = null
+ r_leg = null
+ chest = null
+ head = null
+ forced_ai = null
+ return ..()
+
/obj/item/robot_suit/prebuilt/New()
l_arm = new(src)
r_arm = new(src)
diff --git a/code/modules/admin/verbs/borgpanel.dm b/code/modules/admin/verbs/borgpanel.dm
index 89a6f4ae06b..90b8a541309 100644
--- a/code/modules/admin/verbs/borgpanel.dm
+++ b/code/modules/admin/verbs/borgpanel.dm
@@ -32,6 +32,12 @@
CRASH("Borg panel attempted to open to a mob without a client")
borg = to_borg
+// CRITICAL: Fix borgpanel to properly clean up references
+/datum/borgpanel/Destroy()
+ borg = null
+ user = null
+ return ..()
+
/datum/borgpanel/ui_state(mob/user)
return GLOB.admin_state
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index fa42a66abf0..192a0418d0e 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -357,7 +357,9 @@ GLOBAL_LIST_INIT(warning_ckeys, list())
// Initialize tgui panel
tgui_panel.initialize()
- src << browse(file('html/statbrowser.html'), "window=statbrowser")
+
+ // Initialize statbrowser properly
+ load_statbrowser()
if(alert_mob_dupe_login && !holder)
diff --git a/code/modules/clothing/head/f13factionhead.dm b/code/modules/clothing/head/f13factionhead.dm
index 7a8f8ad2a3f..0ced1448c5a 100644
--- a/code/modules/clothing/head/f13factionhead.dm
+++ b/code/modules/clothing/head/f13factionhead.dm
@@ -743,6 +743,7 @@ obj/item/clothing/head/helmet/f13/enclave/usmcriot
dynamic_hair_suffix = ""
dynamic_fhair_suffix = ""
flash_protect = 1
+ unique_reskin = list("Classic" = "ranger_old", "Classic 2.0" = "new_ranger_classic")
glass_colour_type = /datum/client_colour/glass_colour/red
lighting_alpha = LIGHTING_PLANE_ALPHA_NV_TRAIT
darkness_view = 24
diff --git a/code/modules/clothing/suits/bigiron_suits_factions.dm b/code/modules/clothing/suits/bigiron_suits_factions.dm
index bae49aa63f0..7aafcf490cb 100644
--- a/code/modules/clothing/suits/bigiron_suits_factions.dm
+++ b/code/modules/clothing/suits/bigiron_suits_factions.dm
@@ -475,6 +475,7 @@ Suits. 0-10 in its primary value, slowdown 0, various utility
desc = "The NCR veteran ranger combat armor, or black armor consists of a pre-war L.A.P.D. riot suit under a duster with rodeo jeans. Considered one of the most prestigious suits of armor to earn and wear while in service of the NCR Rangers."
icon_state = "ranger"
item_state = "ranger"
+ unique_reskin = list("Classic" = "ranger_old")
clothing_flags = CUSHIONED_ARMOR
armor = ARMOR_VALUE_HEAVY
armor_tier_desc = ARMOR_CLOTHING_HEAVY
diff --git a/code/modules/mob/dead/new_player/preferences_setup.dm b/code/modules/mob/dead/new_player/preferences_setup.dm
index 599c6303dd9..8ef5846c008 100644
--- a/code/modules/mob/dead/new_player/preferences_setup.dm
+++ b/code/modules/mob/dead/new_player/preferences_setup.dm
@@ -43,14 +43,28 @@
// Set up the dummy for its photoshoot
var/mob/living/carbon/human/dummy/mannequin = generate_or_wait_for_human_dummy(DUMMY_HUMAN_SLOT_PREFERENCES)
+
+ // ALWAYS CLEAR EVERYTHING FIRST - regardless of tab
+ mannequin.cut_overlays()
+
+ // Strip all equipment from previous character/preview
+ for(var/obj/item/I in mannequin.get_all_gear())
+ mannequin.dropItemToGround(I, TRUE)
+ qdel(I)
+ mannequin.delete_equipment()
+
// Apply the Dummy's preview background first so we properly layer everything else on top of it.
mannequin.add_overlay(mutable_appearance('fallout/icons/ui/backgrounds.dmi', bgstate, layer = SPACE_LAYER))
- copy_to(mannequin, initial_spawn = TRUE)
-
+
if(current_tab == LOADOUT_TAB)
- //give it its loadout if not on the appearance tab
+ // For loadout tab: apply appearance WITHOUT job equipment, then add loadout
+ copy_to(mannequin, initial_spawn = FALSE)
+
+ // Equip ONLY loadout items
SSjob.equip_loadout(parent.mob, mannequin, FALSE, bypass_prereqs = TRUE, can_drop = FALSE)
else
+ // For other tabs: apply full character with job equipment
+ copy_to(mannequin, initial_spawn = TRUE)
if(previewJob && equip_job)
mannequin.job = previewJob.title
previewJob.equip(mannequin, TRUE, preference_source = parent)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 09793d0fdaa..bb06bf85bb1 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -425,28 +425,39 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
/mob/dead/observer/verb/reenter_corpse()
set category = "Ghost"
set name = "Re-enter Corpse"
+
+ // Check for client first
if(!client)
return
- if(!ckey)
- to_chat(src, span_warning("You have no ckey to transfer."))
+
+ // Check for mind
+ if(!mind)
+ to_chat(src, span_warning("You have no mind!"))
return
- if(!mind || QDELETED(mind.current))
+
+ // Check if mind.current exists and isn't being deleted
+ if(!mind.current || QDELETED(mind.current))
to_chat(src, span_warning("You have no body."))
return
+
+ // Check if we can re-enter
if(!can_reenter_corpse)
to_chat(src, span_warning("You cannot re-enter your body."))
return
- if(mind.current && mind.current.key && mind.current.key[1] != "@") //makes sure we don't accidentally kick any clients
- to_chat(usr, span_warning("Another consciousness is in your body...It is resisting you."))
- return
- if(!mind.current)
- to_chat(src, span_warning("Your body is gone."))
+
+ // Check if another player is in the body
+ if(mind.current.key && mind.current.key[1] != "@")
+ to_chat(src, span_warning("Another consciousness is in your body... It is resisting you."))
return
+
+ // Now safe to transfer
client.change_view(CONFIG_GET(string/default_view))
transfer_ckey(mind.current, FALSE)
- SStgui.on_transfer(src, mind.current) // Transfer NanoUIs.
+ SStgui.on_transfer(src, mind.current)
+
if(mind.current.client)
mind.current.client.init_verbs()
+
return TRUE
/mob/dead/observer/verb/stay_dead()
diff --git a/code/modules/mob/living/carbon/human/species_types/ghoul.dm b/code/modules/mob/living/carbon/human/species_types/ghoul.dm
index ad919374880..fe72429f3ef 100644
--- a/code/modules/mob/living/carbon/human/species_types/ghoul.dm
+++ b/code/modules/mob/living/carbon/human/species_types/ghoul.dm
@@ -18,7 +18,7 @@
wagging_type = "waggingtail_human"
species_type = "human"
- allowed_limb_ids = list("human","mammal","aquatic","avian")
+ allowed_limb_ids = list("human")
use_skintones = 0
speedmod = 0.3 //slightly slower than humans
sexes = 1
diff --git a/code/modules/mob/living/carbon/human/species_types/supermutant.dm b/code/modules/mob/living/carbon/human/species_types/supermutant.dm
index f68ee69a2ed..9e6c2c4a898 100644
--- a/code/modules/mob/living/carbon/human/species_types/supermutant.dm
+++ b/code/modules/mob/living/carbon/human/species_types/supermutant.dm
@@ -38,7 +38,14 @@
if(rank in GLOB.vault_positions)
return 0
if(rank in GLOB.ncr_positions)
- return 0
+ // Allow Corporal and below ranks
+ if(rank in list(
+ "NCR Rear Echelon",
+ "NCR Conscript",
+ "NCR Trooper",
+ "NCR Corporal"))
+ return 1
+ return 0 // Block higher NCR ranks
if(rank in GLOB.wasteland_positions)
return 0
if(rank in GLOB.outlaw_positions)
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 00f67bc6047..03b00666eea 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -74,9 +74,27 @@
//If there's an MMI in the robot, have it ejected when the mob goes away. --NEO
/mob/living/silicon/robot/Destroy()
-
var/atom/T = drop_location() // Drop location for movable items
+ // CRITICAL: Remove from global lists FIRST to prevent any new references
+ if(shell)
+ GLOB.available_ai_shells -= src
+ GLOB.silicon_mobs -= src
+
+ // Close any borgpanel UIs that might be open - these hold references
+ SStgui.close_uis(src)
+
+ // Disconnect from AI EARLY to break circular references
+ if(connected_ai)
+ set_connected_ai(null)
+
+ // Clear laws datum reference - this can hold references
+ if(laws)
+ if(laws.owner == src)
+ laws.owner = null
+ laws = null
+
+ // Clear client reference early
if(client)
client.mob = null
@@ -98,34 +116,130 @@
ghostize()
stack_trace("Borg MMI lacked a brainmob")
+ // Clear mind reference
if(mind && mind.current == src)
mind.current = null
+ mind = null
- cell = null
+ // IMPORTANT: Unbuckle any buckled mobs BEFORE anything else
+ // Buckled mobs hold references through the riding component
+ if(has_buckled_mobs())
+ unbuckle_all_mobs(force = TRUE)
+
+ // Unregister ALL signals BEFORE clearing other references
+ UnregisterSignal(src)
+
+ // Clean up actions - these can hold references
+ for(var/datum/action/A in actions)
+ A.Remove(src)
+
+ // Clean up components EARLY - riding component especially holds references
+ RemoveElement(/datum/element/empprotection)
+ RemoveElement(/datum/element/cleaning)
+ RemoveComponentByType(/datum/component/riding)
+ RemoveComponentByType(/datum/component/swarming)
+
+ // Clean up effect systems
+ if(spark_system)
+ qdel(spark_system)
+ spark_system = null
+
+ if(ion_trail)
+ qdel(ion_trail)
+ ion_trail = null
+
+ // Clean up wires - this is important as wires hold a reference to the holder
+ if(wires)
+ qdel(wires)
wires = null
+
+ // Clean up radio
+ if(radio)
+ qdel(radio)
radio = null
+
+ // Clean up module - important as it may hold item references
+ if(module)
+ // Move module out first to break the loc reference
+ if(module.loc == src)
+ module.forceMove(T)
+ qdel(module)
module = null
+
+ // Clean up cameras
+ if(builtInCamera)
+ qdel(builtInCamera)
builtInCamera = null
+
+ if(aicamera)
+ qdel(aicamera)
aicamera = null
- spark_system = null
+
+ // Clean up UI elements
+ if(robot_modules_background)
+ qdel(robot_modules_background)
robot_modules_background = null
- equippable_hats = null
-
- if(connected_ai)
- set_connected_ai(null)
+
+ thruster_button = null
+ lamp_button = null
+ hands = null
- if(shell)
- GLOB.available_ai_shells -= src
+ // Clean up HUD - this is critical as HUD can hold references
+ if(hud_used)
+ hud_used = null
- GLOB.silicon_mobs -= src
+ // Clean up upgrades list - this could hold references
+ if(upgrades)
+ for(var/obj/item/borg/upgrade/U in upgrades)
+ if(U.loc == src)
+ U.forceMove(T)
+ upgrades.Cut()
+ upgrades = null
- // Critical GC steps
- UnregisterSignal(src)
- RemoveComponentByType(/datum/component/swarming)
+ // Clear the cell reference (don't qdel, it's been moved or should be handled by robot_suit)
+ cell = null
+
+ // IMPORTANT: Clean up robot_suit reference
+ // This is a major source of circular references
+ if(robot_suit)
+ // The robot_suit still has references to us, so we need to clear them
+ if(robot_suit.loc == src)
+ robot_suit.forceMove(T)
+ robot_suit = null
+
+ // Clear lists - these can hold references
+ if(alarms)
+ for(var/cat in alarms)
+ alarms[cat] = null
+ alarms = null
+
+ equippable_hats = null
+ // Clear other object references
+ hat = null
+ mainframe = null
+ mmi = null
+
+ // Clear faction list reference
+ faction = null
+
+ // Clear held items
+ for(var/obj/item/I in held_items)
+ held_items -= I
+
+ // Clear any timers that might be running
+ deltimer(lamp_cooldown)
+
+ // Clear any status effects or alerts
+ clear_alert("locked")
+ clear_alert("hacked")
+
+ // Set loc to null last
loc = null
+
return ..()
+
/mob/living/silicon/robot/proc/pick_module()
if(module.type != /obj/item/robot_module)
return
@@ -650,31 +764,43 @@
update_icons()
+// Fix the deconstruct proc to properly null the robot_suit reference
/mob/living/silicon/robot/proc/deconstruct()
var/turf/T = get_turf(src)
if (robot_suit)
robot_suit.forceMove(T)
- robot_suit.l_leg.forceMove(T)
- robot_suit.l_leg = null
- robot_suit.r_leg.forceMove(T)
- robot_suit.r_leg = null
- new /obj/item/stack/cable_coil(T, robot_suit.chest.wired)
- robot_suit.chest.forceMove(T)
- robot_suit.chest.wired = 0
- robot_suit.chest = null
- robot_suit.l_arm.forceMove(T)
- robot_suit.l_arm = null
- robot_suit.r_arm.forceMove(T)
- robot_suit.r_arm = null
- robot_suit.head.forceMove(T)
- robot_suit.head.flash1.forceMove(T)
- robot_suit.head.flash1.burn_out()
- robot_suit.head.flash1 = null
- robot_suit.head.flash2.forceMove(T)
- robot_suit.head.flash2.burn_out()
- robot_suit.head.flash2 = null
- robot_suit.head = null
+ if(robot_suit.chest && robot_suit.chest.wired)
+ new /obj/item/stack/cable_coil(T, robot_suit.chest.wired)
+ robot_suit.chest.wired = 0
+ // Properly break down the suit
+ if(robot_suit.l_leg)
+ robot_suit.l_leg.forceMove(T)
+ robot_suit.l_leg = null
+ if(robot_suit.r_leg)
+ robot_suit.r_leg.forceMove(T)
+ robot_suit.r_leg = null
+ if(robot_suit.chest)
+ robot_suit.chest.forceMove(T)
+ robot_suit.chest = null
+ if(robot_suit.l_arm)
+ robot_suit.l_arm.forceMove(T)
+ robot_suit.l_arm = null
+ if(robot_suit.r_arm)
+ robot_suit.r_arm.forceMove(T)
+ robot_suit.r_arm = null
+ if(robot_suit.head)
+ robot_suit.head.forceMove(T)
+ if(robot_suit.head.flash1)
+ robot_suit.head.flash1.forceMove(T)
+ robot_suit.head.flash1.burn_out()
+ robot_suit.head.flash1 = null
+ if(robot_suit.head.flash2)
+ robot_suit.head.flash2.forceMove(T)
+ robot_suit.head.flash2.burn_out()
+ robot_suit.head.flash2 = null
+ robot_suit.head = null
robot_suit.update_icon()
+ robot_suit = null
else
new /obj/item/robot_suit(T)
new /obj/item/bodypart/l_leg/robot(T)
@@ -688,11 +814,14 @@
for(b=0, b!=2, b++)
var/obj/item/assembly/flash/handheld/F = new /obj/item/assembly/flash/handheld(T)
F.burn_out()
- if (cell) //Sanity check.
+
+ if (cell) //Sanity check - this should have been moved already
cell.forceMove(T)
cell = null
+
qdel(src)
+
/mob/living/silicon/robot/modules
var/set_module = null
@@ -1038,24 +1167,31 @@
return TRUE
+// Fix undeploy to properly null references
/mob/living/silicon/robot/proc/undeploy()
-
if(!deployed || !mind || !mainframe)
return
- mainframe.redeploy_action.Grant(mainframe)
- mainframe.redeploy_action.last_used_shell = src
- mind.transfer_to(mainframe)
+
+ var/mob/living/silicon/ai/old_mainframe = mainframe
+
+ old_mainframe.redeploy_action.Grant(old_mainframe)
+ old_mainframe.redeploy_action.last_used_shell = src
+ mind.transfer_to(old_mainframe)
deployed = FALSE
- mainframe.deployed_shell = null
+ old_mainframe.deployed_shell = null
undeployment_action.Remove(src)
+
if(radio) //Return radio to normal
radio.recalculateChannels()
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name //update the camera name too
+
diag_hud_set_aishell()
- mainframe.diag_hud_set_deployed()
- if(mainframe.laws)
- mainframe.laws.show_laws(mainframe) //Always remind the AI when switching
+ old_mainframe.diag_hud_set_deployed()
+
+ if(old_mainframe.laws)
+ old_mainframe.laws.show_laws(old_mainframe) //Always remind the AI when switching
+
mainframe = null
/mob/living/silicon/robot/attack_ai(mob/user)
diff --git a/code/modules/mob/living/silicon/robot/robot_modules.dm b/code/modules/mob/living/silicon/robot/robot_modules.dm
index 665f3427302..f00be42046f 100644
--- a/code/modules/mob/living/silicon/robot/robot_modules.dm
+++ b/code/modules/mob/living/silicon/robot/robot_modules.dm
@@ -56,6 +56,7 @@
ratvar_modules += I
ratvar_modules -= i
+// Ensure module properly clears robot reference
/obj/item/robot_module/Destroy()
basic_modules.Cut()
emag_modules.Cut()
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
index 91013280b94..36de5a1238f 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -63,7 +63,39 @@
lay_web.Grant(src)
/mob/living/simple_animal/hostile/poison/giant_spider/Destroy()
- QDEL_NULL(lay_web)
+ // Remove from global list FIRST
+ GLOB.spidermobs -= src
+
+ // Clear ranged ability reference without calling remove (which may try to access src)
+ if(ranged_ability)
+ if(ranged_ability.ranged_ability_user == src)
+ ranged_ability.ranged_ability_user = null
+ ranged_ability = null
+
+ // Clear ALL abilities with proper cleanup
+ if(LAZYLEN(abilities))
+ var/list/abilities_copy = abilities.Copy()
+ abilities = null // Clear the list first to break references
+
+ for(var/obj/effect/proc_holder/ability in abilities_copy)
+ // Clear cross-references
+ if(ability.ranged_ability_user == src)
+ ability.ranged_ability_user = null
+ if(ability.action)
+ if(ability.action.owner == src)
+ ability.action.owner = null
+ qdel(ability)
+
+ // Clear the lay_web action
+ if(lay_web)
+ lay_web.Remove(src)
+ if(lay_web.owner == src)
+ lay_web.owner = null
+ QDEL_NULL(lay_web)
+
+ // Clear directive
+ directive = null
+
return ..()
/mob/living/simple_animal/hostile/poison/giant_spider/Topic(href, href_list)
@@ -129,10 +161,28 @@
set_directive = new
set_directive.Grant(src)
+
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Destroy()
- RemoveAbility(wrap)
- QDEL_NULL(lay_eggs)
- QDEL_NULL(set_directive)
+ // Clear cocoon target reference
+ cocoon_target = null
+
+ // CRITICAL FIX: Do NOT qdel the wrap here - let parent handle it
+ // Just clear the reference to break the cycle
+ wrap = null
+
+ // Clear actions with proper cleanup
+ if(lay_eggs)
+ lay_eggs.Remove(src)
+ if(lay_eggs.owner == src)
+ lay_eggs.owner = null
+ QDEL_NULL(lay_eggs)
+
+ if(set_directive)
+ set_directive.Remove(src)
+ if(set_directive.owner == src)
+ set_directive.owner = null
+ QDEL_NULL(set_directive)
+
return ..()
//hunters have the most poison and move the fastest, so they can find prey
@@ -213,7 +263,13 @@
letmetalkpls.Grant(src)
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife/Destroy()
- QDEL_NULL(letmetalkpls)
+ // Clear communication action
+ if(letmetalkpls)
+ letmetalkpls.Remove(src)
+ if(letmetalkpls.owner == src)
+ letmetalkpls.owner = null
+ QDEL_NULL(letmetalkpls)
+
return ..()
/mob/living/simple_animal/hostile/poison/giant_spider/ice //spiders dont usually like tempatures of 140 kelvin who knew
@@ -388,6 +444,7 @@
name = "Wrap"
panel = "Spider"
active = FALSE
+ has_action = TRUE
datum/action/spell_action/action = null
desc = "Wrap something or someone in a cocoon. If it's a living being, you'll also consume them, allowing you to lay eggs."
ranged_mousepointer = 'icons/effects/wrap_target.dmi'
@@ -399,6 +456,38 @@
. = ..()
action = new(src)
+/obj/effect/proc_holder/wrap/Destroy()
+ // CRITICAL: Clear ALL references that might keep this alive
+
+ // Clear from user's ranged ability AND click intercept
+ if(ranged_ability_user)
+ // Remove from their abilities list
+ if(ranged_ability_user.abilities)
+ ranged_ability_user.abilities -= src
+ // If we're their active ranged ability, clear it
+ if(ranged_ability_user.ranged_ability == src)
+ ranged_ability_user.ranged_ability = null
+ // If we're their click intercept, clear it
+ if(ranged_ability_user.click_intercept == src)
+ ranged_ability_user.click_intercept = null
+ ranged_ability_user = null
+
+ // Clear the action completely
+ if(action)
+ // Remove from owner's action list
+ if(action.owner)
+ if(action.owner.actions)
+ action.owner.actions -= action
+ action.owner = null
+ // Delete the action
+ QDEL_NULL(action)
+
+ // Clear any other vars
+ active = FALSE
+ has_action = FALSE
+
+ return ..()
+
/obj/effect/proc_holder/wrap/update_icon()
action.button_icon_state = "wrap_[active]"
action.UpdateButtonIcon()
@@ -439,6 +528,8 @@
/obj/effect/proc_holder/wrap/on_lose(mob/living/carbon/user)
remove_ranged_ability()
+ // Don't set ranged_ability_user to null here - let Destroy() handle it
+ ..()
/datum/action/innate/spider/lay_eggs
name = "Lay Eggs"
@@ -501,10 +592,6 @@
. = ..()
GLOB.spidermobs[src] = TRUE
-/mob/living/simple_animal/hostile/poison/giant_spider/Destroy()
- GLOB.spidermobs -= src
- return ..()
-
/datum/action/innate/spider/comm
name = "Command"
desc = "Send a command to all living spiders."
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 8f6bc8df99f..ae5a3d58a05 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -121,16 +121,51 @@
smoke.attach(src)
/mob/living/simple_animal/hostile/Destroy()
+ // Clear target reference with signal cleanup
+ GiveTarget(null)
targets_from = null
+
+ // Clear lists
+ if(friends)
+ friends.Cut()
friends = null
+
+ if(foes)
+ foes.Cut()
foes = null
- GiveTarget(null)
+
+ if(wanted_objects)
+ wanted_objects = null
+
+ if(emote_taunt)
+ emote_taunt = null
+
+ // Clear smoke system
if(smoke)
QDEL_NULL(smoke)
- // Clean up lonely timer if active
+
+ // Clear active emp flags
+ if(active_emp_flags)
+ active_emp_flags = null
+ if(emp_flags)
+ emp_flags = null
+
+ // Clear timers
if(lonely_timer_id)
deltimer(lonely_timer_id)
lonely_timer_id = null
+
+ if(lose_patience_timer_id)
+ deltimer(lose_patience_timer_id)
+ lose_patience_timer_id = null
+
+ if(search_objects_timer_id)
+ deltimer(search_objects_timer_id)
+ search_objects_timer_id = null
+
+ // Unqueue from idle NPC pool
+ SSidlenpcpool.remove_from_culling(src)
+
return ..()
/mob/living/simple_animal/hostile/BiologicalLife(seconds, times_fired)
@@ -813,18 +848,20 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega
else if (M.loc.type in hostile_machines)
. += M.loc
+// Fix the handle_target_del to ensure it clears properly
/mob/living/simple_animal/hostile/proc/handle_target_del(datum/source)
SIGNAL_HANDLER
UnregisterSignal(target, COMSIG_PARENT_QDELETING)
target = null
LoseTarget()
+// Ensure add_target properly cleans up old references
/mob/living/simple_animal/hostile/proc/add_target(new_target)
if(target)
UnregisterSignal(target, COMSIG_PARENT_QDELETING)
target = new_target
if(target)
- RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(handle_target_del), override = TRUE)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(handle_target_del))
/mob/living/simple_animal/hostile/proc/queue_unbirth()
SSidlenpcpool.add_to_culling(src)
diff --git a/code/modules/mob/living/simple_animal/hostile/regalrat.dm b/code/modules/mob/living/simple_animal/hostile/regalrat.dm
index d666db5e406..f695b0e2bb2 100644
--- a/code/modules/mob/living/simple_animal/hostile/regalrat.dm
+++ b/code/modules/mob/living/simple_animal/hostile/regalrat.dm
@@ -42,6 +42,20 @@
// Disabled ghost role for Rat King
// INVOKE_ASYNC(src, PROC_REF(get_player))
+/mob/living/simple_animal/hostile/regalrat/Destroy()
+ // Remove from cheeserats list
+ SSmobs.cheeserats -= src
+
+ // Clear action references
+ if(coffer)
+ coffer.Remove(src)
+ QDEL_NULL(coffer)
+ if(riot)
+ riot.Remove(src)
+ QDEL_NULL(riot)
+
+ return ..()
+
// Disabled: Rat King ghost role selection
// /mob/living/simple_animal/hostile/regalrat/proc/get_player()
// var/list/mob/dead/observer/candidates = pollGhostCandidates("Do you want to play as the Royal Rat, cheesey be his crown?", ROLE_SENTIENCE, null, FALSE, 100, POLL_IGNORE_SENTIENCE_POTION)
@@ -321,7 +335,15 @@
. = ..()
/mob/living/simple_animal/hostile/rat/Destroy()
+ // Remove from global list FIRST
SSmobs.cheeserats -= src
+
+ // Remove swarming component
+ RemoveComponentByType(/datum/component/swarming)
+
+ // Remove mob holder element
+ RemoveElement(/datum/element/mob_holder)
+
return ..()
/mob/living/simple_animal/hostile/rat/examine(mob/user)
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 5e00cd97f10..3f91aa81793 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -342,10 +342,43 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns)
if (SSnpcpool.state == SS_PAUSED && LAZYLEN(SSnpcpool.currentrun))
SSnpcpool.currentrun -= src
sever_link_to_nest()
- if(make_a_nest)
- QDEL_NULL(make_a_nest)
- if(unmake_a_nest)
- QDEL_NULL(unmake_a_nest)
+
+ // CRITICAL FIX: Properly clean up ALL proc holders and abilities BEFORE calling parent
+ // This prevents circular references that block GC
+
+ // Clean up ALL abilities generically first (catches any we might have missed)
+ if(abilities && LAZYLEN(abilities))
+ var/list/abilities_copy = abilities.Copy()
+ abilities = null // Clear the list to break references
+
+ for(var/obj/effect/proc_holder/ability in abilities_copy)
+ // Clear cross-references
+ if(ability.ranged_ability_user == src)
+ ability.ranged_ability_user = null
+ if(ability.action)
+ if(ability.action.owner == src)
+ ability.action.owner = null
+ // Don't need to remove from actions list since we're clearing it below
+ qdel(ability)
+
+ // Clean up ALL actions
+ if(actions && LAZYLEN(actions))
+ var/list/actions_copy = actions.Copy()
+ actions = null // Clear the list to break references
+
+ for(var/datum/action/act in actions_copy)
+ if(act.owner == src)
+ act.owner = null
+ qdel(act)
+
+ // Now clean up the specific references we keep
+ make_a_nest = null
+ unmake_a_nest = null
+ send_mobs = null
+ call_backup = null
+ ghostme = null
+
+ // Clean up mob spawner lists
LAZYREMOVE(GLOB.mob_spawners[initial(name)], src)
if(!LAZYLEN(GLOB.mob_spawners[initial(name)]))
GLOB.mob_spawners -= initial(name)
@@ -353,7 +386,10 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns)
LAZYREMOVE(GLOB.mob_spawners["Tame [initial(name)]"], src)
if(!LAZYLEN(GLOB.mob_spawners["Tame [initial(name)]"]))
GLOB.mob_spawners -= "Tame [initial(name)]"
+
+ // Clear weakrefs
lazarused_by = null
+ nest = null
var/turf/T = get_turf(src)
if (T && AIStatus == AI_Z_OFF)
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 6a2a2e50419..46756dcee40 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -57,6 +57,7 @@
if(client)
var/delay = client.statbrowser_ready ? 1 : 50
addtimer(CALLBACK(client, TYPE_PROC_REF(/client, init_verbs)), delay)
+ addtimer(CALLBACK(client, TYPE_PROC_REF(/client, load_statbrowser)), delay)
log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP)
SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 102be641d09..0caad361d77 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -583,20 +583,33 @@ mob/visible_message(message, self_message, blind_message, vision_distance = DEFA
/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"]!")
+ // Validate target mob
+ if(!new_mob || !istype(new_mob))
+ stack_trace("transfer_ckey() called without a valid mob target!")
+ return FALSE
+
+ // Check if we have a ckey to transfer
+ if(!ckey && new_mob.ckey)
+ stack_trace("transfer_ckey() called on ckey-less mob with a player mob as target!")
+ return FALSE
+
if(!ckey)
- return
+ return FALSE
+
SEND_SIGNAL(new_mob, COMSIG_MOB_PRE_PLAYER_CHANGE, new_mob, src)
- if (client)
+
+ if(client)
if(client.prefs?.auto_ooc)
- if (client.prefs.chat_toggles & CHAT_OOC && isliving(new_mob))
+ 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))
+ 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()
diff --git a/code/modules/paperwork/paper.dm b/code/modules/paperwork/paper.dm
index 20ec678e459..68a4d27a508 100644
--- a/code/modules/paperwork/paper.dm
+++ b/code/modules/paperwork/paper.dm
@@ -4,7 +4,7 @@
*
* lipstick wiping is in code/game/objects/items/weapons/cosmetics.dm!
*/
-#define MAX_PAPER_LENGTH 5000
+#define MAX_PAPER_LENGTH 9000
#define MAX_PAPER_STAMPS 30 // Too low?
#define MAX_PAPER_STAMPS_OVERLAYS 4
#define MODE_READING 0
@@ -44,6 +44,13 @@
var/info = ""
var/show_written_words = TRUE
+ /// Store arrays of formatting data parallel to text additions
+ var/list/add_text = list()
+ var/list/add_font = list()
+ var/list/add_color = list()
+ var/list/add_sign = list()
+ var/list/add_crayon = list()
+
/// The (text for the) stamps on the paper.
var/list/stamps /// Positioning for the stamp in tgui
var/list/stamped /// Overlay info
@@ -55,6 +62,8 @@
/// This is an associated list
var/list/form_fields = list()
var/field_counter = 1
+ var/list/field_fonts = list()
+ var/list/field_colors = list()
/obj/item/paper/Destroy()
stamps = null
@@ -75,8 +84,18 @@
N.update_icon_state()
N.stamps = stamps
N.stamped = stamped.Copy()
- N.form_fields = form_fields.Copy()
- N.field_counter = field_counter
+ N.field_fonts = field_fonts?.Copy()
+ N.field_colors = field_colors?.Copy()
+ if(add_text)
+ N.add_text = add_text.Copy()
+ if(add_font)
+ N.add_font = add_font.Copy()
+ if(add_color)
+ N.add_color = add_color.Copy()
+ if(add_sign)
+ N.add_sign = add_sign.Copy()
+ if(add_crayon)
+ N.add_crayon = add_crayon.Copy()
copy_overlays(N, TRUE)
return N
@@ -89,8 +108,28 @@
info = text
form_fields = null
field_counter = 0
+ field_fonts = list()
+ field_colors = list()
+ add_text = list()
+ add_font = list()
+ add_color = list()
+ add_sign = list()
+ add_crayon = list()
update_icon_state()
+/obj/item/paper/proc/parsemarkdown(text, mob/user)
+ // Don't re-process if already has HTML tags
+ if(findtext(text, "<"))
+ return text
+
+ // Replace %s or %sign with the user's name
+ var/regex/sign_regex = new(@"%s(?:ign)?(?:\s|$)?", "gi")
+ if(user && user.real_name)
+ text = sign_regex.Replace(text, "[user.real_name]")
+
+ // Add any other markdown processing here
+ return text
+
/obj/item/paper/pickup(user)
if(contact_poison && ishuman(user))
var/mob/living/carbon/human/H = user
@@ -107,8 +146,12 @@
update_icon()
/obj/item/paper/update_icon_state()
- if(info && show_written_words)
+ // Check if we have any text in the formatting arrays
+ if(add_text && add_text.len > 0 && show_written_words)
+ icon_state = "[initial(icon_state)]_words"
+ else if(info && show_written_words) // Fallback for old papers
icon_state = "[initial(icon_state)]_words"
+ return ..()
/obj/item/paper/verb/rename()
set name = "Rename paper"
@@ -137,6 +180,13 @@
info = ""
stamps = null
LAZYCLEARLIST(stamped)
+ field_fonts = list()
+ field_colors = list()
+ add_text = list()
+ add_font = list()
+ add_color = list()
+ add_sign = list()
+ add_crayon = list()
cut_overlays()
update_icon_state()
@@ -237,15 +287,27 @@
/obj/item/paper/ui_static_data(mob/user)
. = list()
.["text"] = info
- .["max_length"] = MAX_PAPER_LENGTH
- .["paper_color"] = !color || color == "white" ? "#FFFFFF" : color // color might not be set
- .["paper_state"] = icon_state /// TODO: show the sheet will bloodied or crinkling?
+ .["max_length"] = MAX_PAPER_LENGTH // Send the actual limit
+ .["paper_color"] = !color || color == "white" ? "#FFFFFF" : color
+ .["paper_state"] = icon_state
.["stamps"] = stamps
-
+ .["field_counter"] = field_counter
+ .["add_text"] = add_text
+ .["add_font"] = add_font
+ .["add_color"] = add_color
+ .["add_sign"] = add_sign
+ .["add_crayon"] = add_crayon
+ .["form_fields"] = form_fields
+ .["field_fonts"] = field_fonts
+ .["field_colors"] = field_colors
/obj/item/paper/ui_data(mob/user)
var/list/data = list()
+ data["edit_usr"] = user.real_name
+ data["field_counter"] = field_counter
+ data["raw_text"] = info
+
var/obj/O = user.get_active_held_item()
if(istype(O, /obj/item/toy/crayon))
var/obj/item/toy/crayon/PEN = O
@@ -278,8 +340,6 @@
data["is_crayon"] = FALSE
data["stamp_icon_state"] = "FAKE"
data["stamp_class"] = "FAKE"
- data["field_counter"] = field_counter
- data["form_fields"] = form_fields
return data
@@ -318,25 +378,66 @@
if("save")
var/in_paper = params["text"]
var/paper_len = length(in_paper)
- field_counter = params["field_counter"] ? text2num(params["field_counter"]) : field_counter
-
- if(paper_len > MAX_PAPER_LENGTH)
- // Side note, the only way we should get here is if
- // the javascript was modified, somehow, outside of
- // byond. but right now we are logging it as
- // the generated html might get beyond this limit
- log_paper("[key_name(ui.user)] writing to paper [name], and overwrote it by [paper_len-MAX_PAPER_LENGTH]")
- if(paper_len == 0)
- to_chat(ui.user, pick("Writing block strikes again!", "You forgot to write anthing!"))
+
+ // Update field counter
+ if(params["field_counter"])
+ field_counter = text2num(params["field_counter"])
+
+ // Get the fields data if provided WITH STYLING
+ if(params["fields"])
+ if(!form_fields)
+ form_fields = list()
+ if(!field_fonts)
+ field_fonts = list()
+ if(!field_colors)
+ field_colors = list()
+
+ var/list/fields_data = params["fields"]
+ var/pen_font = params["pen_font"]
+ var/pen_color = params["pen_color"]
+
+ // Store each field's value AND its styling
+ for(var/field_id in fields_data)
+ var/field_value = fields_data[field_id]
+
+ // Check if this is a signature field and replace %s with actual name
+ if(findtext(field_value, "%s") || findtext(field_value, "%sign"))
+ field_value = ui.user.real_name
+ field_fonts[field_id] = "Times New Roman" // Signatures use Times New Roman
+ else
+ field_fonts[field_id] = pen_font
+
+ form_fields[field_id] = field_value
+ field_colors[field_id] = pen_color
+
+ if(paper_len == 0 && !params["fields"])
+ to_chat(ui.user, pick("Writing block strikes again!", "You forgot to write anything!"))
else
log_paper("[key_name(ui.user)] writing to paper [name]")
- if(info != in_paper)
- to_chat(ui.user, "You have added to your paper masterpiece!");
- info = in_paper
- update_static_data(usr,ui)
-
-
- update_icon()
+
+ // Only append new text if there is any
+ if(paper_len > 0)
+ // Don't modify info anymore, it's not used for display
+ if(!add_text) add_text = list()
+ if(!add_font) add_font = list()
+ if(!add_color) add_color = list()
+ if(!add_sign) add_sign = list()
+ if(!add_crayon) add_crayon = list()
+
+ add_text += in_paper
+ add_font += params["pen_font"]
+ add_color += params["pen_color"]
+ add_sign += ui.user.real_name
+ add_crayon += params["is_crayon"] ? TRUE : FALSE
+
+ to_chat(ui.user, "You have added to your paper masterpiece!")
+
+ // If only fields were filled, still save
+ if(params["fields"])
+ to_chat(ui.user, "You have filled in the form!")
+
+ update_static_data(usr, ui)
+ update_icon()
. = TRUE
/**
diff --git a/code/modules/paperwork/photocopier.dm b/code/modules/paperwork/photocopier.dm
index 19f7e00eaf6..5987aa03a7b 100644
--- a/code/modules/paperwork/photocopier.dm
+++ b/code/modules/paperwork/photocopier.dm
@@ -92,7 +92,8 @@
to_chat(usr, "[src] is currently busy copying something. Please wait until it is finished.")
return FALSE
if(paper_copy)
- if(!length(paper_copy.info))
+ var/has_content = (paper_copy.add_text && length(paper_copy.add_text)) || (paper_copy.info && length(paper_copy.info)) || (paper_copy.form_fields && length(paper_copy.form_fields))
+ if(!has_content)
to_chat(usr, "An error message flashes across [src]'s screen: \"The supplied paper is blank. Aborting.\"")
return FALSE
// Basic paper
@@ -232,22 +233,53 @@
return
var/obj/item/paper/copied_paper = new(loc)
give_pixel_offset(copied_paper)
- if(toner_cartridge.charges > 10) // Lots of toner, make it dark.
- copied_paper.info = ""
- else // No toner? shitty copies for you!
- copied_paper.info = ""
-
- var/copied_info = paper_copy.info
- copied_info = replacetext(copied_info, ""
+
+ // Copy formatting arrays (the new system)
+ if(paper_copy.add_text)
+ copied_paper.add_text = paper_copy.add_text.Copy()
+ if(paper_copy.add_font)
+ copied_paper.add_font = paper_copy.add_font.Copy()
+ if(paper_copy.add_color)
+ copied_paper.add_color = paper_copy.add_color.Copy()
+ if(paper_copy.add_sign)
+ copied_paper.add_sign = paper_copy.add_sign.Copy()
+ if(paper_copy.add_crayon)
+ copied_paper.add_crayon = paper_copy.add_crayon.Copy()
+
+ // Copy form fields
+ if(paper_copy.form_fields)
+ copied_paper.form_fields = paper_copy.form_fields.Copy()
+ if(paper_copy.field_fonts)
+ copied_paper.field_fonts = paper_copy.field_fonts.Copy()
+ if(paper_copy.field_colors)
+ copied_paper.field_colors = paper_copy.field_colors.Copy()
+ copied_paper.field_counter = paper_copy.field_counter
+
+ // Copy legacy info field if it exists
+ if(paper_copy.info)
+ if(toner_cartridge.charges > 10) // Lots of toner, make it dark.
+ copied_paper.info = ""
+ else // No toner? shitty copies for you!
+ copied_paper.info = ""
+
+ var/copied_info = paper_copy.info
+ copied_info = replacetext(copied_info, ""
+
+ // Copy name, color, and stamps
copied_paper.name = paper_copy.name
- copied_paper.update_icon()
- copied_paper.stamps = paper_copy.stamps
+ copied_paper.color = paper_copy.color
+ if(paper_copy.stamps)
+ copied_paper.stamps = paper_copy.stamps.Copy()
if(paper_copy.stamped)
copied_paper.stamped = paper_copy.stamped.Copy()
+
+ // Update the paper's appearance
+ copied_paper.update_icon()
copied_paper.copy_overlays(paper_copy, TRUE)
+
toner_cartridge.charges -= PAPER_TONER_USE
/**
diff --git a/code/modules/surgery/organs/liver.dm b/code/modules/surgery/organs/liver.dm
index 920d2d42709..ecfcb6400ec 100755
--- a/code/modules/surgery/organs/liver.dm
+++ b/code/modules/surgery/organs/liver.dm
@@ -27,7 +27,7 @@
/obj/item/organ/liver/on_life()
. = ..()
- if(!. || !owner)//can't process reagents with a failing liver
+ if(!. || !owner || !owner.reagents)
return
if(filterToxins && !HAS_TRAIT(owner, TRAIT_TOXINLOVER))
diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm
index e5b3602e868..5ffc26aca8e 100644
--- a/code/modules/tgui_panel/external.dm
+++ b/code/modules/tgui_panel/external.dm
@@ -5,6 +5,34 @@
/client/var/datum/tgui_panel/tgui_panel
+/client/proc/load_statbrowser()
+ // Clear the subsystem's cached images for this client
+ if(mob?.listed_turf)
+ mob.listed_turf = null
+
+ // Clear the global image cache in the statpanels subsystem
+ SSstatpanels.cached_images.Cut()
+
+ // Close the old window completely to clear all cached data
+ src << browse(null, "window=statbrowser")
+
+ // Small delay to ensure window closes
+ sleep(3)
+
+ // Reload the statbrowser fresh
+ src << browse(file('html/statbrowser.html'), "window=statbrowser")
+
+ // Wait for HTML to load, then reinitialize everything
+ spawn(15)
+ init_verbs() // Restore all verb tabs
+
+ // Restore admin tabs if they're an admin
+ if(holder)
+ src << output("[url_encode(holder.href_token)]", "statbrowser:add_admin_tabs")
+
+ // Set theme to dark mode
+ src << output("dark", "statbrowser:set_theme")
+
/**
* tgui panel / chat troubleshooting verb
*/
@@ -15,6 +43,9 @@
log_tgui(src, "Started fixing.", context = "verb/fix_tgui_panel")
nuke_chat()
+
+ // Also fix statpanel at the same time
+ load_statbrowser()
// Failed to fix, using tgalert as fallback
action = tgalert(src, "Did that work?", "", "Yes", "No, switch to old ui")
@@ -23,6 +54,13 @@
winset(src, "browseroutput", "is-disabled=1;is-visible=0")
log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel")
+/client/verb/fix_statpanel()
+ set name = "Fix Statpanel"
+ set category = "OOC"
+
+ load_statbrowser()
+ to_chat(src, span_notice("Statpanel reloaded. Icons should load correctly now when you alt+click."))
+
/client/proc/nuke_chat()
// Catch all solution (kick the whole thing in the pants)
winset(src, "output", "on-show=&is-disabled=0&is-visible=1")
diff --git a/fallout/code/modules/client/loadout/head.dm b/fallout/code/modules/client/loadout/head.dm
index 9da5ffdfbfa..aac97107ab4 100644
--- a/fallout/code/modules/client/loadout/head.dm
+++ b/fallout/code/modules/client/loadout/head.dm
@@ -372,7 +372,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/steelpot_goggles
@@ -391,7 +393,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/ranger/rigs
@@ -400,9 +404,7 @@
subcategory = LOADOUT_SUBCATEGORY_HEAD_FACTIONS
cost = 1
restricted_desc = "NCR"
- restricted_roles = list("NCR Captain",
- "NCR Veteran Ranger",
- )
+ restricted_roles = list("NCR Veteran Ranger")
/datum/gear/head/ranger/price
name = "Spider Riot Helmet"
@@ -450,7 +452,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/steelpot_bandolier
@@ -469,7 +473,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/ncr_slouch
@@ -488,7 +494,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/ncr_flapcap
@@ -507,7 +515,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/head/ncr_campaign
diff --git a/fallout/code/modules/client/loadout/suit.dm b/fallout/code/modules/client/loadout/suit.dm
index 9d43c2ea713..9e22f73166d 100644
--- a/fallout/code/modules/client/loadout/suit.dm
+++ b/fallout/code/modules/client/loadout/suit.dm
@@ -221,9 +221,7 @@
subcategory = LOADOUT_SUBCATEGORY_SUIT_ARMOR
cost = 2
restricted_desc = "NCR"
- restricted_roles = list("NCR Captain",
- "NCR Veteran Ranger",
- )
+ restricted_roles = list("NCR Veteran Ranger")
/datum/gear/suit/ncr/ranger/spider
name = "Spider Ranger Gear"
diff --git a/fallout/code/modules/client/loadout/uniform.dm b/fallout/code/modules/client/loadout/uniform.dm
index 3fcd313aa75..dd4ed632a5c 100644
--- a/fallout/code/modules/client/loadout/uniform.dm
+++ b/fallout/code/modules/client/loadout/uniform.dm
@@ -1249,7 +1249,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/uniform/ncr/officer
@@ -1279,7 +1281,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/uniform/ncr/sniper
@@ -1298,7 +1302,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/uniform/ncr/pants
@@ -1317,7 +1323,9 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
/datum/gear/uniform/ncr/shorts
@@ -1336,5 +1344,7 @@
"NCR Combat Medic",
"NCR Military Police",
"NCR Trooper",
- "NCR Off-Duty"
+ "NCR Off-Duty",
+ "NCR Conscript",
+ "NCR Rear Echelon",
)
diff --git a/icons/mob/clothing/head.dmi b/icons/mob/clothing/head.dmi
index d7270dc5a48..abc2389381c 100644
Binary files a/icons/mob/clothing/head.dmi and b/icons/mob/clothing/head.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index 759e4f8f806..86dc1d0838e 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/tgui/packages/tgui/interfaces/PaperSheet.js b/tgui/packages/tgui/interfaces/PaperSheet.js
index 7994a992995..a35d890ad1e 100644
--- a/tgui/packages/tgui/interfaces/PaperSheet.js
+++ b/tgui/packages/tgui/interfaces/PaperSheet.js
@@ -1,11 +1,6 @@
/**
* @file
* @copyright 2020 WarlockD (https://github.com/warlockd)
- * @author Original WarlockD (https://github.com/warlockd)
- * @author Changes stylemistake
- * @author Changes ThePotato97
- * @author Changes Ghommie
- * @author Changes Timberpoes
* @license MIT
*/
@@ -17,12 +12,11 @@ import { Box, Flex, Tabs, TextArea } from '../components';
import { Window } from '../layouts';
import { clamp } from 'common/math';
import { sanitizeText } from '../sanitize';
-const MAX_PAPER_LENGTH = 5000; // Question, should we send this with ui_data?
-// Hacky, yes, works?...yes
+const MAX_PAPER_LENGTH = 9000;
+
const textWidth = (text, font, fontsize) => {
- // default font height is 12 in tgui
- font = fontsize + "x " + font;
+ font = fontsize + "px " + font;
const c = document.createElement('canvas');
const ctx = c.getContext("2d");
ctx.font = font;
@@ -34,28 +28,24 @@ const setFontinText = (text, font, color, bold=false) => {
return "" + text + "";
};
const createIDHeader = index => {
return "paperfield_" + index;
};
-// To make a field you do a [_______] or however long the field is
-// we will then output a TEXT input for it that hopefully covers
-// the exact amount of spaces
+
const field_regex = /\[(_+)\]/g;
-const field_tag_regex = /\[paperfield_\d+)"(.*?)\/>\]/gm;
-const sign_regex = /%s(?:ign)?(?=\\s|$)?/igm;
+const field_tag_regex
+ = /\[paperfield_\d+)"(.*?)\/>\]/gm;
+const sign_regex = /%s(?:ign)?(?:\s|$)?/ig;
-const createInputField = (length, width, font,
- fontsize, color, id) => {
+const createInputField = (length, width, font, fontsize, color, id) => {
return "[ {
- const ret_text = txt.replace(field_regex, (match, p1, offset, string) => {
- const width = textWidth(match, font, fontsize) + "px";
- return createInputField(p1.length,
- width, font, fontsize, color, createIDHeader(counter++));
- });
- return {
- counter,
- text: ret_text,
- };
+ const ret_text = txt.replace(
+ field_regex,
+ (match, p1, offset, string) => {
+ const width = textWidth(match, font, fontsize) + "px";
+ return createInputField(
+ p1.length,
+ width,
+ font,
+ fontsize,
+ color,
+ createIDHeader(counter++)
+ );
+ }
+ );
+ return { counter, text: ret_text };
};
const signDocument = (txt, color, user) => {
@@ -85,8 +81,6 @@ const signDocument = (txt, color, user) => {
};
const run_marked_default = value => {
- // Override function, any links and images should
- // kill any other marked tokens we don't want here
const walkTokens = token => {
switch (token.type) {
case 'url':
@@ -95,8 +89,6 @@ const run_marked_default = value => {
case 'link':
case 'image':
token.type = 'text';
- // Once asset system is up change to some default image
- // or rewrite for icon images
token.href = "";
break;
}
@@ -106,37 +98,20 @@ const run_marked_default = value => {
smartypants: true,
smartLists: true,
walkTokens,
- // Once assets are fixed might need to change this for them
baseUrl: 'thisshouldbreakhttp',
});
};
-/*
-** This gets the field, and finds the dom object and sees if
-** the user has typed something in. If so, it replaces,
-** the dom object, in txt with the value, spaces so it
-** fits the [] format and saves the value into a object
-** There may be ways to optimize this in javascript but
-** doing this in byond is nightmarish.
-**
-** It returns any values that were saved and a corrected
-** html code or null if nothing was updated
-*/
const checkAllFields = (txt, font, color, user_name, bold=false) => {
let matches;
let values = {};
let replace = [];
- // I know its tempting to wrap ALL this in a .replace
- // HOWEVER the user might not of entered anything
- // if thats the case we are rebuilding the entire string
- // for nothing, if nothing is entered, txt is just returned
+
while ((matches = field_tag_regex.exec(txt)) !== null) {
const full_match = matches[0];
const id = matches.groups.id;
if (id) {
const dom = document.getElementById(id);
- // make sure we got data, and kill any html that might
- // be in it
const dom_text = dom && dom.value ? dom.value : "";
if (dom_text.length === 0) {
continue;
@@ -145,15 +120,14 @@ const checkAllFields = (txt, font, color, user_name, bold=false) => {
if (sanitized_text.length === 0) {
continue;
}
- // this is easier than doing a bunch of text manipulations
+
const target = dom.cloneNode(true);
- // in case they sign in a field
+
if (sanitized_text.match(sign_regex)) {
target.style.fontFamily = "Times New Roman";
bold = true;
target.defaultValue = user_name;
- }
- else {
+ } else {
target.style.fontFamily = font;
target.defaultValue = sanitized_text;
}
@@ -164,13 +138,15 @@ const checkAllFields = (txt, font, color, user_name, bold=false) => {
target.disabled = true;
const wrap = document.createElement('div');
wrap.appendChild(target);
- values[id] = sanitized_text; // save the data
- replace.push({ value: "[" + wrap.innerHTML + "]", raw_text: full_match });
+ values[id] = sanitized_text;
+ replace.push({
+ value: "[" + wrap.innerHTML + "]",
+ raw_text: full_match,
+ });
}
}
if (replace.length > 0) {
for (const o of replace) {
-
txt = txt.replace(o.raw_text, o.value);
}
}
@@ -185,11 +161,60 @@ const pauseEvent = e => {
return false;
};
+const createPreview = (
+ value,
+ text,
+ do_fields = false,
+ field_counter,
+ color,
+ font,
+ user_name,
+ is_crayon = false,
+) => {
+ const out = { text: text };
+
+ value = value.trim();
+ if (value.length > 0) {
+ value += value[value.length - 1] === "\n" ? " \n" : "\n \n";
+
+ const sanitized_text = sanitizeText(value, []);
+ const signed_text = signDocument(sanitized_text, color, user_name);
+ const fielded_text = createFields(
+ signed_text,
+ font,
+ 12,
+ color,
+ field_counter
+ );
+ const formatted_text = run_marked_default(fielded_text.text);
+ const fonted_text = setFontinText(
+ formatted_text,
+ font,
+ color,
+ is_crayon
+ );
+
+ out.text += fonted_text;
+ out.field_counter = fielded_text.counter;
+ }
+
+ if (do_fields) {
+ const final_processing = checkAllFields(
+ out.text,
+ font,
+ color,
+ user_name,
+ is_crayon
+ );
+ out.text = final_processing.text;
+ out.form_fields = final_processing.fields;
+ }
+ return out;
+};
+
+// Components
const Stamp = (props, context) => {
- const {
- image,
- opacity,
- } = props;
+ const { image, opacity } = props;
const stamp_transform = {
'left': image.x + 'px',
'top': image.y + 'px',
@@ -199,30 +224,19 @@ const Stamp = (props, context) => {
return (
);
};
-
const setInputReadonly = (text, readonly) => {
return readonly
? text.replace(/ {
- const {
- value = "",
- stamps = [],
- backgroundColor,
- readOnly,
- } = props;
+ const { value = "", stamps = [], backgroundColor, readOnly } = props;
const stamp_list = stamps;
const text_html = {
__html: ''
@@ -234,7 +248,7 @@ const PaperSheetView = (props, context) => {
position="relative"
backgroundColor={backgroundColor}
width="100%"
- height="100%" >
+ height="100%">
{
dangerouslySetInnerHTML={text_html}
p="10px" />
{stamp_list.map((o, i) => (
-
+
))}
);
};
-// again, need the states for dragging and such
class PaperSheetStamper extends Component {
constructor(props, context) {
super(props, context);
- this.state = {
- x: 0,
- y: 0,
- rotate: 0,
- };
- this.style = null;
+ this.state = { x: 0, y: 0, rotate: 0 };
+
this.handleMouseMove = e => {
const pos = this.findStampPosition(e);
if (!pos) { return; }
- // center offset of stamp & rotate
pauseEvent(e);
this.setState({ x: pos[0], y: pos[1], rotate: pos[2] });
};
+
this.handleMouseClick = e => {
if (e.pageY <= 30) { return; }
const { act, data } = useBackend(this.context);
const stamp_obj = {
- x: this.state.x, y: this.state.y, r: this.state.rotate,
+ x: this.state.x,
+ y: this.state.y,
+ r: this.state.rotate,
stamp_class: this.props.stamp_class,
stamp_icon_state: data.stamp_icon_state,
};
@@ -286,30 +303,30 @@ class PaperSheetStamper extends Component {
rotating = true;
}
- if (document.getElementById("stamp"))
- {
+ if (document.getElementById("stamp")) {
const stamp = document.getElementById("stamp");
const stampHeight = stamp.clientHeight;
const stampWidth = stamp.clientWidth;
- const currentHeight = rotating ? this.state.y : e.pageY
- - windowRef.scrollTop - stampHeight;
- const currentWidth = rotating ? this.state.x : e.pageX - (stampWidth / 2);
+ const currentHeight = rotating
+ ? this.state.y
+ : e.pageY - windowRef.scrollTop - stampHeight;
+ const currentWidth = rotating
+ ? this.state.x
+ : e.pageX - (stampWidth / 2);
const widthMin = 0;
const heightMin = 0;
-
- const widthMax = (windowRef.clientWidth) - (
- stampWidth);
- const heightMax = (windowRef.clientHeight - windowRef.scrollTop) - (
- stampHeight);
+ const widthMax = (windowRef.clientWidth) - (stampWidth);
+ const heightMax = (windowRef.clientHeight - windowRef.scrollTop)
+ - (stampHeight);
const radians = Math.atan2(
e.pageX - currentWidth,
e.pageY - currentHeight
);
-
- const rotate = rotating ? (radians * (180 / Math.PI) * -1)
+ const rotate = rotating
+ ? (radians * (180 / Math.PI) * -1)
: this.state.rotate;
const pos = [
@@ -332,11 +349,7 @@ class PaperSheetStamper extends Component {
}
render() {
- const {
- value,
- stamp_class,
- stamps,
- } = this.props;
+ const { value, stamp_class, stamps } = this.props;
const stamp_list = stamps || [];
const current_pos = {
sprite: stamp_class,
@@ -350,212 +363,172 @@ class PaperSheetStamper extends Component {
readOnly
value={value}
stamps={stamp_list} />
-
+
>
);
}
}
-// This creates the html from marked text as well as the form fields
-const createPreview = (
- value,
- text,
- do_fields = false,
- field_counter,
- color,
- font,
- user_name,
- is_crayon = false,
-) => {
- const out = { text: text };
- // check if we are adding to paper, if not
- // we still have to check if someone entered something
- // into the fields
- value = value.trim();
- if (value.length > 0) {
- // First lets make sure it ends in a new line
- value += value[value.length] === "\n" ? " \n" : "\n \n";
- // Second, we sanitize the text of html
- const sanitized_text = sanitizeText(value);
- const signed_text = signDocument(sanitized_text, color, user_name);
- // Third we replace the [__] with fields as markedjs fucks them up
- const fielded_text = createFields(
- signed_text, font, 12, color, field_counter);
- // Fourth, parse the text using markup
- const formatted_text = run_marked_default(fielded_text.text);
- // Fifth, we wrap the created text in the pin color, and font.
- // crayon is bold ( tags), maybe make fountain pin italic?
- const fonted_text = setFontinText(
- formatted_text, font, color, is_crayon);
- out.text += fonted_text;
- out.field_counter = fielded_text.counter;
- }
- if (do_fields) {
- // finally we check all the form fields to see
- // if any data was entered by the user and
- // if it was return the data and modify the text
- const final_processing = checkAllFields(
- out.text, font, color, user_name, is_crayon);
- out.text = final_processing.text;
- out.form_fields = final_processing.fields;
- }
- return out;
-};
-
-// ugh. So have to turn this into a full
-// component too if I want to keep updates
-// low and keep the weird flashing down
class PaperSheetEdit extends Component {
constructor(props, context) {
super(props, context);
+
this.state = {
- previewSelected: "Preview",
- old_text: props.value || "",
+ previewSelected: 'Preview',
+ old_text: props.value || '',
counter: props.counter || 0,
- textarea_text: "",
- combined_text: props.value || "",
+ textarea_text: '',
+ combined_text: props.value || '',
};
}
createPreviewFromData(value, do_fields = false) {
const { data } = useBackend(this.context);
- return createPreview(value,
+ return createPreview(
+ value,
this.state.old_text,
do_fields,
this.state.counter,
data.pen_color,
data.pen_font,
data.edit_usr,
- data.is_crayon,
+ data.is_crayon
);
}
+
onInputHandler(e, value) {
if (value !== this.state.textarea_text) {
- const combined_length = this.state.old_text.length
- + this.state.textarea_text.length;
- if (combined_length > MAX_PAPER_LENGTH) {
- if ((combined_length - MAX_PAPER_LENGTH) >= value.length) {
- // Basically we cannot add any more text to the paper
+ const { data } = useBackend(this.context);
+ const maxLength = data.max_length || MAX_PAPER_LENGTH;
+ const combined_length = this.state.old_text.length + value.length;
+
+ if (combined_length > maxLength) {
+ if (combined_length - maxLength >= value.length) {
value = '';
} else {
- value = value.substr(0, value.length
- - (combined_length - MAX_PAPER_LENGTH));
+ value = value.substr(
+ 0,
+ value.length - (combined_length - maxLength)
+ );
}
- // we check again to save an update
if (value === this.state.textarea_text) {
- // Do nothing
return;
}
}
- this.setState(() => ({
+
+ this.setState(prevState => ({
textarea_text: value,
- combined_text: this.createPreviewFromData(value),
+ combined_text: this.createPreviewFromData(value).text,
}));
}
}
- // the final update send to byond, final upkeep
+
finalUpdate(new_text) {
- const { act } = useBackend(this.context);
+ const { act, data } = useBackend(this.context);
const final_processing = this.createPreviewFromData(new_text, true);
- act('save', final_processing);
- this.setState(() => { return {
- textarea_text: "",
- previewSelected: "save",
+
+ act('save', {
+ text: new_text,
+ field_counter: final_processing.field_counter,
+ fields: final_processing.form_fields || {},
+ pen_font: data.pen_font,
+ pen_color: data.pen_color,
+ is_crayon: data.is_crayon,
+ });
+
+ this.setState(() => ({
+ textarea_text: '',
+ previewSelected: 'save',
combined_text: final_processing.text,
old_text: final_processing.text,
counter: final_processing.field_counter,
- }; });
- // byond should switch us to readonly mode from here
+ }));
}
render() {
- const {
- textColor,
- fontFamily,
- stamps,
- backgroundColor,
- } = this.props;
+ const { textColor, fontFamily, stamps, backgroundColor } = this.props;
+
return (
-
+
this.setState({ previewSelected: "Edit" })}>
+ backgroundColor={
+ this.state.previewSelected === 'Edit'
+ ? 'grey'
+ : 'white'
+ }
+ selected={this.state.previewSelected === 'Edit'}
+ onClick={() => this.setState({
+ previewSelected: 'Edit',
+ })}>
Edit
this.setState(() => {
- const new_state = {
- previewSelected: "Preview",
- textarea_text: this.state.textarea_text,
+ backgroundColor={
+ this.state.previewSelected === 'Preview'
+ ? 'grey'
+ : 'white'
+ }
+ selected={this.state.previewSelected === 'Preview'}
+ onClick={() =>
+ this.setState(prevState => ({
+ previewSelected: 'Preview',
+ textarea_text: prevState.textarea_text,
combined_text: this.createPreviewFromData(
- this.state.textarea_text).text,
- };
- return new_state;
- })}>
+ prevState.textarea_text
+ ).text,
+ }))}>
Preview
{
- if (this.state.previewSelected === "confirm") {
+ if (this.state.previewSelected === 'confirm') {
this.finalUpdate(this.state.textarea_text);
- }
- else if (this.state.previewSelected === "Edit") {
- this.setState(() => {
- const new_state = {
- previewSelected: "confirm",
- textarea_text: this.state.textarea_text,
- combined_text: this.createPreviewFromData(
- this.state.textarea_text).text,
- };
- return new_state;
- });
- }
- else {
- this.setState({ previewSelected: "confirm" });
+ } else if (this.state.previewSelected === 'Edit') {
+ this.setState(prevState => ({
+ previewSelected: 'confirm',
+ textarea_text: prevState.textarea_text,
+ combined_text: this.createPreviewFromData(
+ prevState.textarea_text
+ ).text,
+ }));
+ } else {
+ this.setState({ previewSelected: 'confirm' });
}
}}>
- {this.state.previewSelected === "confirm" ? "Confirm" : "Save"}
+ {this.state.previewSelected === 'confirm'
+ ? 'Confirm'
+ : 'Save'}
-
- {this.state.previewSelected === "Edit" && (
+
+ {(this.state.previewSelected === 'Edit' && (
- ) || (
+ )) || (
{
const {
edit_mode,
text,
- paper_color = "white",
- pen_color = "black",
- pen_font = "Verdana",
+ paper_color = 'white',
+ pen_color = 'black',
+ pen_font = 'Verdana',
stamps,
stamp_class,
sizeX,
@@ -585,16 +558,24 @@ export const PaperSheet = (props, context) => {
add_font,
add_color,
add_sign,
+ add_crayon,
field_counter,
+ form_fields,
+ field_fonts,
+ field_colors,
} = data;
- // some features can add text to a paper sheet outside of this ui
- // we need to parse, sanitize and add any of it to the text value.
- const values = { text: text, field_counter: field_counter };
- if (add_text) {
+
+ // Build the formatted text from the stored formatting arrays
+ const values = { text: "", field_counter: field_counter };
+
+ if (add_text && add_text.length > 0) {
+ // Process each saved text segment with its original formatting
for (let index = 0; index < add_text.length; index++) {
const used_color = add_color[index];
const used_font = add_font[index];
const used_sign = add_sign[index];
+ const is_crayon = add_crayon ? add_crayon[index] : false;
+
const processing = createPreview(
add_text[index],
values.text,
@@ -602,28 +583,112 @@ export const PaperSheet = (props, context) => {
values.field_counter,
used_color,
used_font,
- used_sign
+ used_sign,
+ is_crayon
);
values.text = processing.text;
values.field_counter = processing.field_counter;
}
- }
- else {
+
+ // If we have saved form field data, process and inject it
+ // with proper styling
+ if (form_fields && Object.keys(form_fields).length > 0) {
+ for (const field_id in form_fields) {
+ const field_value = form_fields[field_id];
+ const saved_font = field_fonts?.[field_id] || 'Verdana';
+ const saved_color = field_colors?.[field_id] || '#000000';
+
+ // Find and replace the input field
+ const field_regex = new RegExp(
+ `\\[]*id="${field_id}"[^>]*>\\]`,
+ 'g'
+ );
+
+ values.text = values.text.replace(field_regex, (match) => {
+ // Parse the existing input tag to get its attributes
+ const inputMatch = match.match(/]*)>/);
+ if (!inputMatch) return match;
+
+ let inputAttrs = inputMatch[1];
+
+ // Check if this is a signature field (%s or %sign)
+ const isSignature = field_value.match(sign_regex);
+
+ // Determine the display value and styling
+ let displayValue = field_value;
+ let finalFont = saved_font;
+ let isBold = false;
+
+ if (isSignature) {
+ // For signature fields, the value should be
+ // the signer's name
+ // This would have been stored when the field was filled
+ finalFont = "Times New Roman";
+ isBold = true;
+ }
+
+ // Sanitize and process HTML in the value
+ displayValue = sanitizeText(displayValue, []);
+
+ // Update the value attribute
+ if (inputAttrs.includes('value=')) {
+ inputAttrs = inputAttrs.replace(
+ /value="[^"]*"/,
+ `value="${displayValue}"`
+ );
+ } else {
+ inputAttrs += ` value="${displayValue}"`;
+ }
+
+ // Update the style to include saved font, color,
+ // and bold if needed
+ if (inputAttrs.includes('style=')) {
+ inputAttrs = inputAttrs.replace(
+ /style="([^"]*)"/,
+ (styleMatch, existingStyle) => {
+ // Remove existing color, font-family, and font-weight
+ let cleanStyle = existingStyle
+ .replace(/color:[^;]*;?/g, '')
+ .replace(/font-family:[^;]*;?/g, '')
+ .replace(/font:[^;]*;?/g, '')
+ .replace(/font-weight:[^;]*;?/g, '');
+
+ let newStyle = `${cleanStyle};color:${saved_color};`
+ + `font-family:'${finalFont}';`;
+ if (isBold) {
+ newStyle += 'font-weight:bold;';
+ }
+ return `style="${newStyle}"`;
+ }
+ );
+ }
+
+ // Add disabled attribute
+ if (!inputAttrs.includes('disabled')) {
+ inputAttrs = ' disabled' + inputAttrs;
+ }
+
+ return `[]`;
+ });
+ }
+ }
+
+ } else if (text && text.length > 0) {
values.text = sanitizeText(text);
}
- const stamp_list = !stamps
- ? []
- : stamps;
- const decide_mode = mode => {
+
+ const stamp_list = !stamps ? [] : stamps;
+
+ const decide_mode = (mode) => {
switch (mode) {
- case 0:
+ case 0: // Reading mode
return (
);
- case 1:
+ case 1: // Edit mode
return (
{
textColor={pen_color}
fontFamily={pen_font}
stamps={stamp_list}
- backgroundColor={paper_color} />
+ backgroundColor={paper_color}
+ />
);
- case 2:
+ case 2: // Stamp mode
return (
+ stamp_class={stamp_class}
+ />
);
default:
- return "ERROR ERROR WE CANNOT BE HERE!!";
+ return 'ERROR ERROR WE CANNOT BE HERE!!';
}
};
+
return (
-
-
+
+
{decide_mode(edit_mode)}