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' && (