From 4f50fab294bbab84a3cbc69edb9867dca3dc4a93 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 15 Mar 2026 03:57:11 -0500 Subject: [PATCH 001/155] Change spiderwebs to drain stamina when mobs enter (#95197) ## About The Pull Request This changes spiderwebs to drain stamina from mobs whenever they enter a turf filled with spiderwebs. ## Why It's Good For The Game The old movement code was very janky and buggy. Because it used the `CanAllowThrough` proc, a mob could spam several dozens of movement attempts per second, resulting in a massive amount of shaking and `stuck in the web` messages. This had the effect of not really slowing or stopping movement, since the RNG was being rolled so fast that it was a minor inconvenience. I feel like the stamina drain effect makes more sense and feels more natural. ## Changelog :cl: balance: Sticky spider webs now drain stamina when mobs enter the turf /:cl: --- code/game/objects/effects/spiderwebs.dm | 45 ++++++++++++++++++------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/code/game/objects/effects/spiderwebs.dm b/code/game/objects/effects/spiderwebs.dm index d47c00590003..3dc4c0865dde 100644 --- a/code/game/objects/effects/spiderwebs.dm +++ b/code/game/objects/effects/spiderwebs.dm @@ -57,6 +57,11 @@ if (has_frill) pixel_x = -9 pixel_y = -9 + + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) return ..() /obj/structure/spider/stickyweb/attack_hand(mob/user, list/modifiers) @@ -79,23 +84,37 @@ return if(sealed) return FALSE - if(isliving(mover)) - if(HAS_TRAIT(mover, TRAIT_WEB_SURFER)) - return TRUE - if(mover.pulledby && HAS_TRAIT(mover.pulledby, TRAIT_WEB_SURFER)) - return TRUE - if(prob(stuck_chance)) - stuck_react(mover) - return FALSE - return . if(isprojectile(mover)) return prob(projectile_stuck_chance) return . -/// Show some feedback when you can't pass through something -/obj/structure/spider/stickyweb/proc/stuck_react(atom/movable/stuck_guy) - loc.balloon_alert(stuck_guy, "stuck in web!") - stuck_guy.Shake(duration = 0.1 SECONDS) +/obj/structure/spider/stickyweb/proc/on_entered(datum/source, atom/movable/victim, old_loc) + SIGNAL_HANDLER + + if(!isliving(victim)) + return + if(HAS_TRAIT(victim, TRAIT_WEB_SURFER)) + return + if(victim.pulledby && HAS_TRAIT(victim.pulledby, TRAIT_WEB_SURFER)) + return + + if(prob(stuck_chance)) + stuck_react(victim) + +/// Drains stamina and shows feedback when you get stuck moving thru a web +/obj/structure/spider/stickyweb/proc/stuck_react(mob/living/victim) + if(victim.get_stamina_loss() > 90) + if(victim.body_position != LYING_DOWN) + to_chat(victim, span_warning("You trip over \the [src] due to exhaustion!")) + + victim.SetKnockdown(3 SECONDS) + return + + if(prob(25)) + loc.balloon_alert(victim, "stuck in web!") + victim.Shake(duration = 0.2 SECONDS) + + victim.adjust_stamina_loss(rand(10, 15)) /// Web made by geneticists, needs special handling to allow them to pass through their own webs /obj/structure/spider/stickyweb/genetic From 33e951ff9218b0a7e9117b9045f979b38e0b3dbc Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 08:57:30 +0000 Subject: [PATCH 002/155] Automatic changelog for PR #95197 [ci skip] --- html/changelogs/AutoChangeLog-pr-95197.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95197.yml diff --git a/html/changelogs/AutoChangeLog-pr-95197.yml b/html/changelogs/AutoChangeLog-pr-95197.yml new file mode 100644 index 000000000000..3d091ddbfd77 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95197.yml @@ -0,0 +1,4 @@ +author: "timothymtorres" +delete-after: True +changes: + - balance: "Sticky spider webs now drain stamina when mobs enter the turf" \ No newline at end of file From 2eb0776ebed2f814f6a5ba9680c44fe3f16d739f Mon Sep 17 00:00:00 2001 From: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:12:57 -0400 Subject: [PATCH 003/155] Chem scanning removes achievements from maint pills (#95397) ## About The Pull Request Chem scanning a maint pill now removes the achievement for eating it. ## Why It's Good For The Game cheater. You can still scour maints for pills with stuff you'd like, but you lose the achievement for doing so. ## Changelog :cl: fix: Chem scanning a maintenance pill removes the achievement value. /:cl: --- code/__DEFINES/dcs/signals/signals_medical.dm | 3 ++ code/modules/detectivework/scanner.dm | 1 + code/modules/reagents/chemistry/items.dm | 1 + .../reagents/reagent_containers/pill.dm | 34 ++++++++++++++++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_medical.dm b/code/__DEFINES/dcs/signals/signals_medical.dm index 7429b6347f34..905118340ec2 100644 --- a/code/__DEFINES/dcs/signals/signals_medical.dm +++ b/code/__DEFINES/dcs/signals/signals_medical.dm @@ -26,3 +26,6 @@ #define COMSIG_LIVING_OPERATING_ON "living_operating_on" /// Sent from /mob/living/perform_surgery: (mob/living/surgeon, list/possible_operations) #define COMSIG_ATOM_BEING_OPERATED_ON "atom_being_operated_on" + +/// From /obj/item/ph_meter/interact_with_atom(): (atom/source, mob/user) +#define COMSIG_ON_REAGENT_SCAN "on_reagent_scan" diff --git a/code/modules/detectivework/scanner.dm b/code/modules/detectivework/scanner.dm index 4b60ee1b8e0a..a7cebef7bf25 100644 --- a/code/modules/detectivework/scanner.dm +++ b/code/modules/detectivework/scanner.dm @@ -142,6 +142,7 @@ log_entry.add_data_entry(DETSCAN_CATEGORY_FINGERS, atom_fingerprints.Copy()) // Only get reagents from non-mobs. + SEND_SIGNAL(scanned_atom, COMSIG_ON_REAGENT_SCAN, user) for(var/datum/reagent/present_reagent as anything in scanned_atom.reagents?.reagent_list) log_entry.add_data_entry(DETSCAN_CATEGORY_REAGENTS, list(present_reagent.name = present_reagent.volume)) diff --git a/code/modules/reagents/chemistry/items.dm b/code/modules/reagents/chemistry/items.dm index 6f1731c7be13..34da7832d47f 100644 --- a/code/modules/reagents/chemistry/items.dm +++ b/code/modules/reagents/chemistry/items.dm @@ -119,6 +119,7 @@ var/obj/item/reagent_containers/cont = interacting_with if(!LAZYLEN(cont.reagents.reagent_list)) return NONE + SEND_SIGNAL(interacting_with, COMSIG_ON_REAGENT_SCAN, user) var/list/out_message = list() to_chat(user, "The chemistry meter beeps and displays:") out_message += "Total volume: [round(cont.volume, 0.01)] Current temperature: [round(cont.reagents.chem_temp, 0.1)]K Total pH: [round(cont.reagents.ph, 0.01)]\n" diff --git a/code/modules/reagents/reagent_containers/pill.dm b/code/modules/reagents/reagent_containers/pill.dm index e2871b3b3691..4d19c5b5f187 100644 --- a/code/modules/reagents/reagent_containers/pill.dm +++ b/code/modules/reagents/reagent_containers/pill.dm @@ -355,9 +355,23 @@ icon_state = "pill21" /// From which randomisation pool to pull reagents from var/random_reagent_flag = REAGENT_SPAWN_RANDOM_PRODUCERS - var/static/list/names = list("maintenance pill", "floor pill", "mystery pill", "suspicious pill", "strange pill", "lucky pill", "ominous pill", "eerie pill") - var/static/list/descs = list("Your feeling is telling you no, but...","Drugs are expensive, you can't afford not to eat any pills that you find."\ - , "Surely, there's no way this could go bad.", "Winners don't do dr- oh what the heck!", "Free pills? At no cost, how could I lose?") + var/static/list/names = list( + "maintenance pill", + "floor pill", + "mystery pill", + "suspicious pill", + "strange pill", + "lucky pill", + "ominous pill", + "eerie pill", + ) + var/static/list/descs = list( + "Your feeling is telling you no, but...", + "Drugs are expensive, you can't afford not to eat any pills that you find.", + "Surely, there's no way this could go bad.", + "Winners don't do dr- oh what the heck!", + "Free pills? At no cost, how could I lose?", + ) /obj/item/reagent_containers/applicator/pill/maintenance/Initialize(mapload) list_reagents = list(get_random_reagent_id(random_reagent_flag) = rand(10,50)) //list_reagents is called before init, because init generates the reagents using list_reagents @@ -368,10 +382,22 @@ /obj/item/reagent_containers/applicator/pill/maintenance/achievement random_reagent_flag = REAGENT_SPAWN_MAINTENANCE_PILL //none of that fake shit + ///Boolean on whether this will count towards your achievement score if you consume it. + var/count_towards_achievement = TRUE + +/obj/item/reagent_containers/applicator/pill/maintenance/achievement/Initialize(mapload) + . = ..() + RegisterSignal(src, COMSIG_ON_REAGENT_SCAN, PROC_REF(on_chemical_scan)) /obj/item/reagent_containers/applicator/pill/maintenance/achievement/on_consumption(mob/consumer, mob/user) . = ..() - consumer.client?.give_award(/datum/award/score/maintenance_pill, consumer) + if(count_towards_achievement) + consumer.client?.give_award(/datum/award/score/maintenance_pill, consumer) + +///called when we are chemically scanned, we no longer grant an achievement. +/obj/item/reagent_containers/applicator/pill/maintenance/achievement/proc/on_chemical_scan(atom/source, mob/user) + SIGNAL_HANDLER + count_towards_achievement = FALSE /obj/item/reagent_containers/applicator/pill/potassiodide name = "potassium iodide pill" From b4e5532afa70a2b417d3fe40f690fb1c9110fe7b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:13:16 +0000 Subject: [PATCH 004/155] Automatic changelog for PR #95397 [ci skip] --- html/changelogs/AutoChangeLog-pr-95397.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95397.yml diff --git a/html/changelogs/AutoChangeLog-pr-95397.yml b/html/changelogs/AutoChangeLog-pr-95397.yml new file mode 100644 index 000000000000..2a764096fb89 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95397.yml @@ -0,0 +1,4 @@ +author: "JohnFulpWillard" +delete-after: True +changes: + - bugfix: "Chem scanning a maintenance pill removes the achievement value." \ No newline at end of file From ceed05ce368c234cfe898c41cfe581862a5daa36 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 12:00:21 +0000 Subject: [PATCH 005/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95197.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95397.yml | 4 ---- html/changelogs/archive/2026-03.yml | 5 +++++ 3 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95197.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95397.yml diff --git a/html/changelogs/AutoChangeLog-pr-95197.yml b/html/changelogs/AutoChangeLog-pr-95197.yml deleted file mode 100644 index 3d091ddbfd77..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95197.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "timothymtorres" -delete-after: True -changes: - - balance: "Sticky spider webs now drain stamina when mobs enter the turf" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95397.yml b/html/changelogs/AutoChangeLog-pr-95397.yml deleted file mode 100644 index 2a764096fb89..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95397.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "JohnFulpWillard" -delete-after: True -changes: - - bugfix: "Chem scanning a maintenance pill removes the achievement value." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 166ef61d2adc..3152c5d7b9c4 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -203,3 +203,8 @@ - bugfix: Fixes beebox and traitor bees not obeying the reagent blacklist - balance: Maintenance pills (the achievement variety that only spawns in maint) can now generate with xenomorph microbes and cyborg nanites +2026-03-15: + JohnFulpWillard: + - bugfix: Chem scanning a maintenance pill removes the achievement value. + timothymtorres: + - balance: Sticky spider webs now drain stamina when mobs enter the turf From 88ded221f5c8d0c85fc884cb94017d1f68dafaa6 Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:04:04 -0400 Subject: [PATCH 006/155] You can no longer runtime selling ghosts (#95396) --- code/modules/shuttle/mobile_port/variants/supply.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/modules/shuttle/mobile_port/variants/supply.dm b/code/modules/shuttle/mobile_port/variants/supply.dm index 38e305bc3498..505507bd9bb1 100644 --- a/code/modules/shuttle/mobile_port/variants/supply.dm +++ b/code/modules/shuttle/mobile_port/variants/supply.dm @@ -292,6 +292,8 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list( for(var/atom/movable/exporting_atom in shuttle_turf) if(iseyemob(exporting_atom)) continue + if(isobserver(exporting_atom)) + continue if(exporting_atom.anchored) continue export_item_and_contents(exporting_atom, apply_elastic = TRUE, dry_run = FALSE, external_report = report) From 03cb79900996909832537649ba8a4952002bf5eb Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:04:23 +0000 Subject: [PATCH 007/155] Automatic changelog for PR #95396 [ci skip] --- html/changelogs/AutoChangeLog-pr-95396.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95396.yml diff --git a/html/changelogs/AutoChangeLog-pr-95396.yml b/html/changelogs/AutoChangeLog-pr-95396.yml new file mode 100644 index 000000000000..fd4eb90dde1f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95396.yml @@ -0,0 +1,4 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - bugfix: "The cargo shuttle can no longer qdel your ghost mob." \ No newline at end of file From 5602082624f90862e918ca82da30e91c30e61c86 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 15 Mar 2026 14:20:57 -0500 Subject: [PATCH 008/155] Spiders can now be tamed and ridden (#95194) ## About The Pull Request This lets people ride and tame spiders by feeding them mouse, lizard, moth, fly, and worm meat. You can only do this when a spider is a spiderling or young; once it's fully grown, it's no longer possible. This also refactors some tameable code to use `TRAIT_TAMED` instead of setting variables on mobs individually. ## Why It's Good For The Game It looks cool. dreamseeker_97k7fgzdhv ## Changelog :cl: add: Spiders can now be tamed and ridden. They eat mouse, lizard, moth, fly, and worm meat and can only be tamed when they are spiderlings or young. code: Refactored tameable code to use TRAIT_TAMED instead of setting individual variables on each mob. /:cl: --- code/__DEFINES/traits/declarations.dm | 3 ++ code/_globalvars/traits/_traits.dm | 1 + code/_globalvars/traits/admin_tooling.dm | 1 + code/datums/components/riding/riding_mob.dm | 20 ++++++++++++ code/game/atom/_atom.dm | 2 ++ code/modules/mob/living/basic/clown/clown.dm | 1 + .../mob/living/basic/cytology/vatbeast.dm | 1 + .../mob/living/basic/farm_animals/cow/_cow.dm | 1 + .../mob/living/basic/farm_animals/pig.dm | 1 + .../mob/living/basic/farm_animals/pony.dm | 1 + .../mob/living/basic/icemoon/wolf/wolf.dm | 1 + .../basic/lavaland/goldgrub/goldgrub.dm | 1 + .../living/basic/lavaland/goliath/goliath.dm | 8 +---- .../basic/lavaland/lobstrosity/lobstrosity.dm | 6 ++-- .../modules/mob/living/basic/pets/dog/_dog.dm | 1 + .../mob/living/basic/pets/parrot/_parrot.dm | 1 + .../mob/living/basic/space_fauna/carp/carp.dm | 1 + .../basic/space_fauna/eyeball/_eyeball.dm | 1 + .../living/basic/space_fauna/spider/spider.dm | 32 +++++++++++++++++++ code/modules/mob/living/basic/vermin/mouse.dm | 15 ++++----- 20 files changed, 80 insertions(+), 19 deletions(-) diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 7f335afed043..c93cb22246ae 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1462,6 +1462,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BEAST_EMPATHY "beast_empathy" // you're good with animals, such as with taming them #define TRAIT_STURDY_FRAME "sturdy_frame" // you suffer much lesser effects from equipment that slows you down +/// Has this mob been tamed? +#define TRAIT_TAMED "tamed" + /// This item cannot be selected for or used by a theft objective (Spies, Traitors, etc.) #define TRAIT_ITEM_OBJECTIVE_BLOCKED "item_objective_blocked" /// This trait lets you attach limbs to any player without surgery. diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 8fff1e9688e7..6fffe7203f4f 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -587,6 +587,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_TACKLING_WINGED_ATTACKER" = TRAIT_TACKLING_WINGED_ATTACKER, "TRAIT_TACTICALLY_CAMOUFLAGED" = TRAIT_TACTICALLY_CAMOUFLAGED, "TRAIT_TAGGER" = TRAIT_TAGGER, + "TRAIT_TAMED" = TRAIT_TAMED, "TRAIT_TEMPORARY_BODY" = TRAIT_TEMPORARY_BODY, "TRAIT_TENACIOUS" = TRAIT_TENACIOUS, "TRAIT_TENTACLE_IMMUNE" = TRAIT_TENTACLE_IMMUNE, diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm index 3bb5fc532f1d..508334bab3fe 100644 --- a/code/_globalvars/traits/admin_tooling.dm +++ b/code/_globalvars/traits/admin_tooling.dm @@ -308,6 +308,7 @@ GLOBAL_LIST_INIT(admin_visible_traits, list( "TRAIT_TACKLING_TAILED_DEFENDER" = TRAIT_TACKLING_TAILED_DEFENDER, "TRAIT_TACKLING_TAILED_POUNCE" = TRAIT_TACKLING_TAILED_POUNCE, "TRAIT_TAGGER" = TRAIT_TAGGER, + "TRAIT_TAMED" = TRAIT_TAMED, "TRAIT_TENTACLE_IMMUNE" = TRAIT_TENTACLE_IMMUNE, "TRAIT_TESLA_SHOCKIMMUNE" = TRAIT_TESLA_SHOCKIMMUNE, "TRAIT_TETRODOTOXIN_HEALING" = TRAIT_TETRODOTOXIN_HEALING, diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm index c515df2cdd02..ba6153aa77f6 100644 --- a/code/datums/components/riding/riding_mob.dm +++ b/code/datums/components/riding/riding_mob.dm @@ -739,3 +739,23 @@ TEXT_EAST = list(0, 5), TEXT_WEST = list(0, 5), ) + +/datum/component/riding/creature/spider + rider_traits = list(TRAIT_WEB_SURFER, TRAIT_FENCE_CLIMBER) + ride_check_flags = RIDER_NEEDS_ARM | UNBUCKLE_DISABLED_RIDER + +/datum/component/riding/creature/spider/get_rider_offsets_and_layers(pass_index, mob/offsetter) + return list( + TEXT_NORTH = list( 0, 10), + TEXT_SOUTH = list( 0, 10), + TEXT_EAST = list(-5, 10), + TEXT_WEST = list( 5, 10), + ) + +/datum/component/riding/creature/spider/get_parent_offsets_and_layers() + return list( + TEXT_NORTH = list(0, 0, MOB_BELOW_PIGGYBACK_LAYER), + TEXT_SOUTH = list(0, 0, MOB_ABOVE_PIGGYBACK_LAYER), + TEXT_EAST = list(0, 0, MOB_BELOW_PIGGYBACK_LAYER), + TEXT_WEST = list(0, 0, MOB_BELOW_PIGGYBACK_LAYER), + ) diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm index cdaa2ea434c9..73a29c11eca1 100644 --- a/code/game/atom/_atom.dm +++ b/code/game/atom/_atom.dm @@ -799,6 +799,8 @@ ///Called after the atom is 'tamed' for type-specific operations, Usually called by the tameable component but also other things. /atom/proc/tamed(mob/living/tamer, obj/item/food) + SHOULD_CALL_PARENT(TRUE) + ADD_TRAIT(src, TRAIT_TAMED, INNATE_TRAIT) return /** diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm index 7a135e74317d..5b230a6605d4 100644 --- a/code/modules/mob/living/basic/clown/clown.dm +++ b/code/modules/mob/living/basic/clown/clown.dm @@ -481,6 +481,7 @@ flick("glutton_mouth", src) /mob/living/basic/clown/mutant/glutton/tamed(mob/living/tamer, atom/food) + . = ..() buckle_lying = 0 AddElement(/datum/element/ridable, /datum/component/riding/creature/glutton) diff --git a/code/modules/mob/living/basic/cytology/vatbeast.dm b/code/modules/mob/living/basic/cytology/vatbeast.dm index 84eb8e1fcd45..a52411f0d824 100644 --- a/code/modules/mob/living/basic/cytology/vatbeast.dm +++ b/code/modules/mob/living/basic/cytology/vatbeast.dm @@ -51,6 +51,7 @@ ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(enjoyed_food)) /mob/living/basic/vatbeast/tamed(mob/living/tamer, obj/item/food) + . = ..() buckle_lying = 0 AddElement(/datum/element/ridable, /datum/component/riding/creature/vatbeast) set_faction(list(FACTION_NEUTRAL)) diff --git a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm index d55869f751dc..3c6b1e81422d 100644 --- a/code/modules/mob/living/basic/farm_animals/cow/_cow.dm +++ b/code/modules/mob/living/basic/farm_animals/cow/_cow.dm @@ -79,6 +79,7 @@ AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15) /mob/living/basic/cow/tamed(mob/living/tamer, atom/food) + . = ..() visible_message("[src] [tame_message] as it seems to bond with [tamer].", "You [self_tame_message], recognizing [tamer] as your new pal.") AddElement(/datum/element/ridable, /datum/component/riding/creature/cow) diff --git a/code/modules/mob/living/basic/farm_animals/pig.dm b/code/modules/mob/living/basic/farm_animals/pig.dm index 784e1bfb221b..0ef58faca89b 100644 --- a/code/modules/mob/living/basic/farm_animals/pig.dm +++ b/code/modules/mob/living/basic/farm_animals/pig.dm @@ -55,6 +55,7 @@ AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15) /mob/living/basic/pig/tamed(mob/living/tamer, atom/food) + . = ..() AddElement(/datum/element/ridable, /datum/component/riding/creature/pig) visible_message(span_notice("[src] snorts respectfully.")) diff --git a/code/modules/mob/living/basic/farm_animals/pony.dm b/code/modules/mob/living/basic/farm_animals/pony.dm index 0989fabae0ff..a44d042809c9 100644 --- a/code/modules/mob/living/basic/farm_animals/pony.dm +++ b/code/modules/mob/living/basic/farm_animals/pony.dm @@ -60,6 +60,7 @@ AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 15, unique = unique_tamer) /mob/living/basic/pony/tamed(mob/living/tamer, atom/food) + . = ..() playsound(src, 'sound/mobs/non-humanoids/pony/snort.ogg', 50) AddElement(/datum/element/ridable, /datum/component/riding/creature/pony) visible_message(span_notice("[src] snorts happily.")) diff --git a/code/modules/mob/living/basic/icemoon/wolf/wolf.dm b/code/modules/mob/living/basic/icemoon/wolf/wolf.dm index c50c56c38c3f..9d1393fbc3b1 100644 --- a/code/modules/mob/living/basic/icemoon/wolf/wolf.dm +++ b/code/modules/mob/living/basic/icemoon/wolf/wolf.dm @@ -68,6 +68,7 @@ AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 15, bonus_tame_chance = 5) /mob/living/basic/mining/wolf/tamed(mob/living/tamer, atom/food) + . = ..() new /obj/effect/temp_visual/heart(src.loc) // ride wolf, life good AddElement(/datum/element/ridable, /datum/component/riding/creature/wolf) diff --git a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm index 9a531a7bb450..b20f29364bd3 100644 --- a/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm +++ b/code/modules/mob/living/basic/lavaland/goldgrub/goldgrub.dm @@ -118,6 +118,7 @@ AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 25, bonus_tame_chance = 5) /mob/living/basic/mining/goldgrub/tamed(mob/living/tamer, atom/food) + . = ..() new /obj/effect/temp_visual/heart(src.loc) AddElement(/datum/element/ridable, /datum/component/riding/creature/goldgrub) AddComponent(/datum/component/obeys_commands, pet_commands) diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm index c2c597dfd31b..6aef2570060d 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm @@ -36,8 +36,6 @@ var/tentacle_warning_state = "goliath_preattack" /// Can this kind of goliath be tamed? var/tameable = TRUE - /// Has this particular goliath been tamed? - var/tamed = FALSE /// Can someone ride us around like a horse? var/saddled = FALSE /// Slight cooldown to prevent double-dipping if we use both abilities at once @@ -112,7 +110,7 @@ if (saddled) balloon_alert(user, "already saddled!") return - if (!tamed) + if (!HAS_TRAIT(src, TRAIT_TAMED)) balloon_alert(user, "too rowdy!") return balloon_alert(user, "affixing saddle...") @@ -150,10 +148,6 @@ return icon_state = tentacle_warning_state -/// Get ready for mounting -/mob/living/basic/mining/goliath/tamed(mob/living/tamer, atom/food) - tamed = TRUE - // Copy entire faction rather than just placing user into faction, to avoid tentacle peril on station /mob/living/basic/mining/goliath/befriend(mob/living/new_friend) . = ..() diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm index 5c5107162cfb..1801d2dc189b 100644 --- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm +++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm @@ -74,6 +74,7 @@ charge.Trigger(target = atom_target) /mob/living/basic/mining/lobstrosity/tamed(mob/living/tamer, obj/item/food) + . = ..() new /obj/effect/temp_visual/heart(loc) /// Pet commands for this mob, however you'll have to tame juvenile lobstrosities to a trained adult one. var/list/pet_commands = list( @@ -167,8 +168,6 @@ base_fishing_level = SKILL_LEVEL_NOVICE /// What do we become when we grow up? var/mob/living/basic/mining/lobstrosity/grow_type = /mob/living/basic/mining/lobstrosity - /// Were we tamed? If yes, tame the mob we become when we grow up too. - var/was_tamed = FALSE /datum/emote/lobstrosity_juvenile abstract_type = /datum/emote/lobstrosity_juvenile @@ -226,7 +225,6 @@ /mob/living/basic/mining/lobstrosity/juvenile/tamed(mob/living/tamer, obj/item/food) . = ..() - was_tamed = TRUE // They are more pettable I guess AddElement(/datum/element/pet_bonus, "chitter") REMOVE_TRAIT(src, TRAIT_MOB_HIDE_HAPPINESS, INNATE_TRAIT) @@ -237,7 +235,7 @@ /mob/living/basic/mining/lobstrosity/juvenile/proc/grow_up() var/name_to_use = name == initial(name) ? grow_type::name : name var/mob/living/basic/mining/lobstrosity/grown = change_mob_type(grow_type, get_turf(src), name_to_use) - if(was_tamed) + if(HAS_TRAIT(src, TRAIT_TAMED)) grown.tamed() for(var/friend in ai_controller?.blackboard?[BB_FRIENDS_LIST]) grown.befriend(friend) diff --git a/code/modules/mob/living/basic/pets/dog/_dog.dm b/code/modules/mob/living/basic/pets/dog/_dog.dm index ce10ab895258..0db8b036eee2 100644 --- a/code/modules/mob/living/basic/pets/dog/_dog.dm +++ b/code/modules/mob/living/basic/pets/dog/_dog.dm @@ -91,6 +91,7 @@ ///Proc to run on a successful taming attempt /mob/living/basic/pet/dog/tamed(mob/living/tamer, atom/food) + . = ..() visible_message(span_notice("[src] licks at [tamer] in a friendly manner!")) /// A dog bone fully heals a dog, and befriends it if it's not your friend. diff --git a/code/modules/mob/living/basic/pets/parrot/_parrot.dm b/code/modules/mob/living/basic/pets/parrot/_parrot.dm index 5f667857f40d..8e338d7bf609 100644 --- a/code/modules/mob/living/basic/pets/parrot/_parrot.dm +++ b/code/modules/mob/living/basic/pets/parrot/_parrot.dm @@ -447,6 +447,7 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list( return returnable_list /mob/living/basic/parrot/tamed(mob/living/tamer, atom/food) + . = ..() new /obj/effect/temp_visual/heart(drop_location()) /mob/living/basic/parrot/proc/drop_item_on_signal(mob/living/user) diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm index ea679a382350..6f1ff9e864ff 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm @@ -142,6 +142,7 @@ /// Called when another mob has forged a bond of friendship with this one, passed the taming mob as 'tamer' /mob/living/basic/carp/tamed(mob/living/tamer, atom/food, feedback = TRUE) + . = ..() AddElement(/datum/element/ridable, ridable_data) AddComponent(/datum/component/obeys_commands, tamed_commands) if (!feedback) diff --git a/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm b/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm index fd549c92ffec..26bb5548e807 100644 --- a/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm +++ b/code/modules/mob/living/basic/space_fauna/eyeball/_eyeball.dm @@ -117,6 +117,7 @@ COOLDOWN_START(src, eye_healing, 15 SECONDS) /mob/living/basic/eyeball/tamed(mob/living/tamer, atom/food) + . = ..() spin(spintime = 2 SECONDS, speed = 1) //become passive to the humens APPLY_FACTION_AND_ALLIES_FROM(src, tamer) diff --git a/code/modules/mob/living/basic/space_fauna/spider/spider.dm b/code/modules/mob/living/basic/space_fauna/spider/spider.dm index 8cf805697669..1399e5e1b0a7 100644 --- a/code/modules/mob/living/basic/space_fauna/spider/spider.dm +++ b/code/modules/mob/living/basic/space_fauna/spider/spider.dm @@ -53,6 +53,13 @@ var/menu_description = "Tanky and strong for the defense of the nest and other spiders." /// If true then you shouldn't be told that you're a spider antagonist as soon as you are placed into this mob var/apply_spider_antag = TRUE + /// Commands you can give this spider once it is tamed + var/static/list/tamed_commands = list( + /datum/pet_command/idle, + /datum/pet_command/free, + /datum/pet_command/follow, + /datum/pet_command/attack, + ) /datum/emote/spider abstract_type = /datum/emote/spider @@ -85,6 +92,17 @@ webbing.Grant(src) ai_controller?.set_blackboard_key(BB_SPIDER_WEB_ACTION, webbing) + var/static/list/food_types = list( + /obj/item/food/meat/slab/human/mutant/lizard, + /obj/item/food/meat/slab/human/mutant/fly, + /obj/item/food/meat/slab/human/mutant/moth, + /obj/item/food/meat/slab/mouse, + /obj/item/food/meat/slab/mothroach, + /obj/item/food/meat/slab/blood_worm, + /obj/item/food/deadmouse, + ) + AddComponent(/datum/component/tameable, food_types = food_types, tame_chance = 20, bonus_tame_chance = 10) + /mob/living/basic/spider/Login() . = ..() if(!. || !client) @@ -92,6 +110,8 @@ GLOB.spidermobs[src] = TRUE add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/player_spider_modifier, multiplicative_slowdown = player_speed_modifier) + AddElement(/datum/element/ridable, /datum/component/riding/creature/spider) + /mob/living/basic/spider/Logout() . = ..() remove_movespeed_modifier(/datum/movespeed_modifier/player_spider_modifier) @@ -100,6 +120,12 @@ GLOB.spidermobs -= src return ..() +/mob/living/basic/spider/tamed(mob/living/tamer, atom/food, feedback = TRUE) + . = ..() + new /obj/effect/temp_visual/heart(src.loc) + AddElement(/datum/element/ridable, /datum/component/riding/creature/spider) + AddComponent(/datum/component/obeys_commands, tamed_commands) + /mob/living/basic/spider/mob_negates_gravity() if(locate(/obj/structure/spider/stickyweb) in loc) return TRUE @@ -158,6 +184,12 @@ grown.set_name() grown.set_brute_loss(get_brute_loss()) grown.set_fire_loss(get_fire_loss()) + + if(HAS_TRAIT(src, TRAIT_TAMED)) + grown.tamed() + else if(istype(grown, /mob/living/basic/spider/giant)) // Adults cannot be tamed via snacks + qdel(grown.GetComponent(/datum/component/tameable)) + qdel(src) /** diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm index 698eb30c752c..67cae390295f 100644 --- a/code/modules/mob/living/basic/vermin/mouse.dm +++ b/code/modules/mob/living/basic/vermin/mouse.dm @@ -28,8 +28,6 @@ ai_controller = /datum/ai_controller/basic_controller/mouse - /// Whether this rat is friendly to players - var/tame = FALSE /// What color our mouse is. Brown, gray and white - leave blank for random. var/body_color /// Does this mouse contribute to the ratcap? @@ -63,7 +61,8 @@ SSmobs.cheeserats |= src ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) - src.tame = tame + if(tame) + ADD_TRAIT(src, TRAIT_TAMED, INNATE_TRAIT) if(!isnull(new_body_color)) body_color = new_body_color if(isnull(body_color)) @@ -81,7 +80,7 @@ AddComponent(/datum/component/swarming, 16, 16) //max_x, max_y /mob/living/basic/mouse/proc/make_tameable() - if (tame) + if (HAS_TRAIT(src, TRAIT_TAMED)) add_faction(FACTION_NEUTRAL) else var/static/list/food_types = list(/obj/item/food/cheese) @@ -196,9 +195,9 @@ /// Called when a mouse is hand-fed some cheese, it will stop being afraid of humans /mob/living/basic/mouse/tamed(mob/living/tamer, obj/item/food/cheese/cheese) + . = ..() new /obj/effect/temp_visual/heart(loc) add_faction(FACTION_NEUTRAL) - tame = TRUE try_consume_cheese(cheese) ai_controller.CancelActions() // Interrupt any current fleeing @@ -248,7 +247,7 @@ /// Creates a new mouse based on this mouse's subtype. /mob/living/basic/mouse/proc/create_a_new_rat() - new /mob/living/basic/mouse(loc, /* tame = */ tame) + new /mob/living/basic/mouse(loc, HAS_TRAIT(src, TRAIT_TAMED)) /// Biting into a cable will cause a mouse to get shocked and die if applicable. Or do nothing if they're lucky. /mob/living/basic/mouse/proc/try_bite_cable(obj/structure/cable/cable) @@ -302,7 +301,7 @@ contributes_to_ratcap = FALSE /mob/living/basic/mouse/brown/tom/make_tameable() - tame = TRUE + ADD_TRAIT(src, TRAIT_TAMED, INNATE_TRAIT) return ..() /mob/living/basic/mouse/brown/tom/Initialize(mapload) @@ -312,7 +311,7 @@ AddElement(/datum/element/pet_bonus, "squeak") /mob/living/basic/mouse/brown/tom/create_a_new_rat() - new /mob/living/basic/mouse/brown(loc, /* tame = */ tame) // dominant gene + new /mob/living/basic/mouse/brown(loc, HAS_TRAIT(src, TRAIT_TAMED)) // dominant gene /mob/living/basic/mouse/rat name = "rat" From d702b1cbf7f844497219c8193cca0eb9d2153680 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:21:16 +0000 Subject: [PATCH 009/155] Automatic changelog for PR #95194 [ci skip] --- html/changelogs/AutoChangeLog-pr-95194.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95194.yml diff --git a/html/changelogs/AutoChangeLog-pr-95194.yml b/html/changelogs/AutoChangeLog-pr-95194.yml new file mode 100644 index 000000000000..99f2f0b1551d --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95194.yml @@ -0,0 +1,5 @@ +author: "timothymtorres" +delete-after: True +changes: + - rscadd: "Spiders can now be tamed and ridden. They eat mouse, lizard, moth, fly, and worm meat and can only be tamed when they are spiderlings or young." + - code_imp: "Refactored tameable code to use TRAIT_TAMED instead of setting individual variables on each mob." \ No newline at end of file From ee1f5226bcc2b07262d108571ef8c76a18335178 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:29:31 -0500 Subject: [PATCH 010/155] Cannabis and Nictone makes monkeys less angry, Booze makes them more angry (#95342) ## About The Pull Request Cannabis will slowly calm monkies down, removing enemies from their enemies list over time and eventually making them docile while it is in their system Nicotine does similar, but does not make them docile Booze on the other hand makes monkeys see enemies as greater enemies depending on strength of the drink ## Why It's Good For The Game Adding interactions between reagents that modify "player behavior" and actual "ai behavior" seems like fun to me, ## Changelog :cl: Melbert add: Cannabis will calm down an angry monkey, eventually even turning them docile (not pacifist!) temporarily add: Nicotine will also calm down an angry monkey, albiet slower than Cannabis and will never turn them entirely docile add: Booze on the other hand will make a monkey even angrier (though only if they are already upset with someone) /:cl: --- code/__DEFINES/monkeys.dm | 5 +++++ .../reagents/drinks/alcohol_reagents.dm | 3 +++ .../chemistry/reagents/drug_reagents.dm | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/code/__DEFINES/monkeys.dm b/code/__DEFINES/monkeys.dm index a4929bb84dcb..8dfccc937f76 100644 --- a/code/__DEFINES/monkeys.dm +++ b/code/__DEFINES/monkeys.dm @@ -35,5 +35,10 @@ /// probability of reducing aggro by one when the monkey attacks #define MONKEY_HATRED_REDUCTION_PROB 20 +/// Monkey was calmed, such as from weed +#define MONKEY_CALMED_HATRED_AMOUNT -2 +/// Monkey was angered, such as from alcohol +#define MONKEY_ANGERED_HATRED_AMOUNT 2 + ///Monkey recruit cooldown #define MONKEY_RECRUIT_COOLDOWN (1 MINUTES) diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm index b3a49dbcebc6..740b08b74827 100644 --- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm @@ -74,6 +74,9 @@ if(combined_dilute_volume) // safety check to prevent division by zero booze_power *= (total_alcohol_volume / combined_dilute_volume) + for(var/mob/living/enemy as anything in drinker.ai_controller?.blackboard[BB_MONKEY_ENEMIES]) + drinker.ai_controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, enemy, MONKEY_ANGERED_HATRED_AMOUNT * (boozepwr / 100) * metabolization_ratio * seconds_per_tick) + // Volume, power, and server alcohol rate effect how quickly one gets drunk drinker.adjust_drunk_effect(1 * sqrt(volume) * booze_power * ALCOHOL_RATE * metabolization_ratio * seconds_per_tick) if(boozepwr > 0) diff --git a/code/modules/reagents/chemistry/reagents/drug_reagents.dm b/code/modules/reagents/chemistry/reagents/drug_reagents.dm index a425d5e86ab1..873dd41d4adb 100644 --- a/code/modules/reagents/chemistry/reagents/drug_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drug_reagents.dm @@ -47,6 +47,8 @@ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED randomized_spawns = REAGENT_SPAWN_ALL_RANDOM_SPAWNS metabolization_rate = 0.125 * REAGENTS_METABOLISM + /// tracks if we cleared a monkey's aggressiveness value + var/cleared_aggressive = FALSE /datum/reagent/drug/cannabis/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio) . = ..() @@ -64,6 +66,18 @@ to_chat(affected_mob, span_warning("It's too comfy to move...")) affected_mob.Paralyze(10 SECONDS) + var/list/enemies = affected_mob.ai_controller?.blackboard[BB_MONKEY_ENEMIES] + for(var/mob/living/enemy as anything in enemies) + affected_mob.ai_controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, enemy, MONKEY_CALMED_HATRED_AMOUNT * metabolization_ratio * seconds_per_tick) + if(affected_mob.ai_controller.blackboard[BB_MONKEY_AGGRESSIVE] && SPT_PROB(10 - values_sum(enemies), seconds_per_tick)) + affected_mob.ai_controller.set_blackboard_key(BB_MONKEY_AGGRESSIVE, FALSE) + cleared_aggressive = TRUE + +/datum/reagent/drug/cannabis/on_mob_delete(mob/living/affected_mob) + . = ..() + if(cleared_aggressive) + affected_mob.ai_controller?.set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) + /datum/reagent/drug/nicotine name = "Nicotine" description = "Slightly reduces stun times. If overdosed it will deal toxin and oxygen damage." @@ -89,7 +103,10 @@ to_chat(affected_mob, span_notice("[smoke_message]")) affected_mob.add_mood_event("smoked", /datum/mood_event/smoked) affected_mob.remove_status_effect(/datum/status_effect/jitter) - affected_mob.AdjustAllImmobility(-200 * metabolization_ratio * seconds_per_tick) + affected_mob.AdjustAllImmobility(-20 SECONDS * metabolization_ratio * seconds_per_tick) + + for(var/mob/living/enemy as anything in affected_mob.ai_controller?.blackboard[BB_MONKEY_ENEMIES]) + affected_mob.ai_controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, enemy, MONKEY_CALMED_HATRED_AMOUNT * 0.1 * metabolization_ratio * seconds_per_tick) return UPDATE_MOB_HEALTH From a0f973e01618fa9dac661400d026d4efcaf39847 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:29:49 +0000 Subject: [PATCH 011/155] Automatic changelog for PR #95342 [ci skip] --- html/changelogs/AutoChangeLog-pr-95342.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95342.yml diff --git a/html/changelogs/AutoChangeLog-pr-95342.yml b/html/changelogs/AutoChangeLog-pr-95342.yml new file mode 100644 index 000000000000..88a107997afb --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95342.yml @@ -0,0 +1,6 @@ +author: "Melbert" +delete-after: True +changes: + - rscadd: "Cannabis will calm down an angry monkey, eventually even turning them docile (not pacifist!) temporarily" + - rscadd: "Nicotine will also calm down an angry monkey, albiet slower than Cannabis and will never turn them entirely docile" + - rscadd: "Booze on the other hand will make a monkey even angrier (though only if they are already upset with someone)" \ No newline at end of file From f0fc690db877ca099a0a24e5f171dee04dd4d90d Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:33:03 +0100 Subject: [PATCH 012/155] Rewrites dispenser bots to use item_interaction (#95375) ## About The Pull Request Cleans up the code, swing combat will be real one day ## Changelog :cl: refactor: Rewrote dispenser bots to use item_interaction /:cl: --- code/modules/wiremod/shell/dispenser.dm | 38 +++++++++++++------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/code/modules/wiremod/shell/dispenser.dm b/code/modules/wiremod/shell/dispenser.dm index d31a5cb04c0a..4a257dda9023 100644 --- a/code/modules/wiremod/shell/dispenser.dm +++ b/code/modules/wiremod/shell/dispenser.dm @@ -60,27 +60,29 @@ new /obj/item/circuit_component/dispenser_bot() ), SHELL_CAPACITY_LARGE) -/obj/structure/dispenser_bot/attackby(obj/item/item, mob/living/user, list/modifiers, list/attack_modifiers) - if(user.combat_mode) - return ..() - if(istype(item, /obj/item/wrench) || istype(item, /obj/item/multitool) || istype(item, /obj/item/integrated_circuit)) - return ..() - if(item.w_class > max_weight && !istype(item, /obj/item/storage/bag)) +/obj/structure/dispenser_bot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if (user.combat_mode) + return NONE + + if (tool.w_class > max_weight && !istype(tool, /obj/item/storage/bag)) balloon_alert(user, "item too big!") - return FALSE + return ITEM_INTERACT_BLOCKING + if(length(stored_items) >= capacity) balloon_alert(user, "at maximum capacity!") - return FALSE - if(istype(item, /obj/item/storage/bag)) - for(var/obj/item/bag_item in item.contents) - if(length(stored_items) >= capacity) - break - if(bag_item.w_class > max_weight || istype(bag_item, /obj/item/storage/bag)) - continue - add_item(user, bag_item) - return TRUE - add_item(user, item) - return TRUE + return ITEM_INTERACT_BLOCKING + + if(!istype(tool, /obj/item/storage/bag)) + add_item(user, tool) + return ITEM_INTERACT_SUCCESS + + for(var/obj/item/bag_item in tool.contents) + if(length(stored_items) >= capacity) + break + if(bag_item.w_class > max_weight || istype(bag_item, /obj/item/storage/bag)) + continue + add_item(user, bag_item) + return ITEM_INTERACT_SUCCESS /obj/structure/dispenser_bot/wrench_act(mob/living/user, obj/item/tool) if(locked) From ff6cf035bb4e7be46fb52684f2fb9293a3311788 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:33:21 +0000 Subject: [PATCH 013/155] Automatic changelog for PR #95375 [ci skip] --- html/changelogs/AutoChangeLog-pr-95375.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95375.yml diff --git a/html/changelogs/AutoChangeLog-pr-95375.yml b/html/changelogs/AutoChangeLog-pr-95375.yml new file mode 100644 index 000000000000..de3d6966cf0a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95375.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Rewrote dispenser bots to use item_interaction" \ No newline at end of file From 2711d6d038a44fde329685db07f16f7387fe30d2 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:34:36 +0100 Subject: [PATCH 014/155] Rewrites BCI implanters to use item_interaction (#95377) ## About The Pull Request One more refactor, one less attackby ## Changelog :cl: refactor: Rewrote BCI implanters to use item_interaction /:cl: --- .../wiremod/shell/brain_computer_interface.dm | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm index ab4a7d60fbb4..d3d5fc858fea 100644 --- a/code/modules/wiremod/shell/brain_computer_interface.dm +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -332,27 +332,24 @@ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN -/obj/machinery/bci_implanter/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) - var/obj/item/organ/cyberimp/bci/new_bci = weapon - if (istype(new_bci)) - if (!(locate(/obj/item/integrated_circuit) in new_bci)) - balloon_alert(user, "bci has no circuit!") - return - - var/obj/item/organ/cyberimp/bci/previous_bci_to_implant = bci_to_implant - - user.transferItemToLoc(weapon, src) - bci_to_implant = weapon - - if (isnull(previous_bci_to_implant)) - balloon_alert(user, "inserted bci") - else - balloon_alert(user, "swapped bci") - user.put_in_hands(previous_bci_to_implant) - - return - - return ..() +/obj/machinery/bci_implanter/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if (!istype(tool, /obj/item/organ/cyberimp/bci)) + return NONE + + var/obj/item/organ/cyberimp/bci/new_bci = tool + if (!(locate(/obj/item/integrated_circuit) in new_bci)) + balloon_alert(user, "bci has no circuit!") + return ITEM_INTERACT_BLOCKING + + var/obj/item/organ/cyberimp/bci/previous_bci_to_implant = bci_to_implant + user.transferItemToLoc(new_bci, src) + bci_to_implant = new_bci + if (isnull(previous_bci_to_implant)) + balloon_alert(user, "inserted bci") + else + balloon_alert(user, "swapped bci") + user.put_in_hands(previous_bci_to_implant) + return ITEM_INTERACT_SUCCESS /obj/machinery/bci_implanter/attackby_secondary(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) if (!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) From 4cebf2a77add0312ed625ea4edde95a8a4398cc0 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:34:57 +0000 Subject: [PATCH 015/155] Automatic changelog for PR #95377 [ci skip] --- html/changelogs/AutoChangeLog-pr-95377.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95377.yml diff --git a/html/changelogs/AutoChangeLog-pr-95377.yml b/html/changelogs/AutoChangeLog-pr-95377.yml new file mode 100644 index 000000000000..38f2487e68f0 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95377.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Rewrote BCI implanters to use item_interaction" \ No newline at end of file From f7d7c6f00e2495601355713c51bcfedc9d8e3131 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:36:10 -0500 Subject: [PATCH 016/155] Fix Pun Pun's name (#95420) ## About The Pull Request Moving Pun Pun's name handling in #94463 broke their name assignment as it used `var/name_to_use = name` Prior, it would save `var/name_to_use = "Pun Pun"`. But after moving it, it would use `var/name_to_use = "monkey (842)"`, as name is randomized in human init. I will note that I think `use_random_name` var is broken... that may require future investigation. ## Changelog :cl: Melbert fix: Pun Pun is Pun Pun again /:cl: --- .../modules/mob/living/carbon/human/monkey.dm | 36 +++++++++++-------- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/punpun.dm | 5 +++ 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 code/modules/unit_tests/punpun.dm diff --git a/code/modules/mob/living/carbon/human/monkey.dm b/code/modules/mob/living/carbon/human/monkey.dm index c071f3def978..901dd56a8b9b 100644 --- a/code/modules/mob/living/carbon/human/monkey.dm +++ b/code/modules/mob/living/carbon/human/monkey.dm @@ -55,24 +55,19 @@ GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/pu new /obj/effect/landmark/start/pun_pun(loc) //Pun Pun is a crewmember, and may late-join. return INITIALIZE_HINT_QDEL - Read_Memory() + equip_to_slot_or_del(new /obj/item/clothing/under/suit/waiter(src), ITEM_SLOT_ICLOTHING) + if(!GLOB.the_one_and_only_punpun && mapload) GLOB.the_one_and_only_punpun = src + Read_Memory() + else if(GLOB.the_one_and_only_punpun) ADD_TRAIT(src, TRAIT_DONT_WRITE_MEMORY, INNATE_TRAIT) //faaaaaaake! + // Everything past here MUST be called AFTER memory has been read give_special_name() - - //These have to be after the parent new to ensure that the monkey - //bodyparts are actually created before we try to equip things to - //those slots - if(ancestor_chain > 1) - generate_fake_scars(rand(ancestor_chain, ancestor_chain * 4)) - if(relic_hat) - equip_to_slot_or_del(new relic_hat, ITEM_SLOT_HEAD) - if(relic_mask) - equip_to_slot_or_del(new relic_mask, ITEM_SLOT_MASK) - equip_to_slot_or_del(new /obj/item/clothing/under/suit/waiter(src), ITEM_SLOT_ICLOTHING) + give_scars() + give_special_equipment() /mob/living/carbon/human/species/monkey/punpun/Destroy() if(GLOB.the_one_and_only_punpun == src) @@ -131,14 +126,15 @@ GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/pu WRITE_FILE(json_file, json_encode(file_data)) -/// Gives pun pun a special name based on various factors +/// Gives pun pun a special name based on various factors such as their past /mob/living/carbon/human/species/monkey/punpun/proc/give_special_name() - var/name_to_use = name + var/name_to_use = initial(name) if(ancestor_name) name_to_use = ancestor_name if(ancestor_chain > 1) name_to_use += " \Roman[ancestor_chain]" + else if(prob(10)) name_to_use = pick(list("Professor Bobo", "Deempisi's Revenge", "Furious George", "King Louie", "Dr. Zaius", "Jimmy Rustles", "Dinner", "Lanky")) if(name_to_use == "Furious George") @@ -146,3 +142,15 @@ GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/pu ai_controller = new /datum/ai_controller/monkey/angry(src) //hes always mad fully_replace_character_name(real_name, name_to_use) + +/// Gives pun pun scars based on how many times he's died in the past +/mob/living/carbon/human/species/monkey/punpun/proc/give_scars() + if(ancestor_chain > 1) + generate_fake_scars(rand(ancestor_chain, ancestor_chain * 4)) + +/// Gives pun pun special equipment from their past +/mob/living/carbon/human/species/monkey/punpun/proc/give_special_equipment() + if(relic_hat) + equip_to_slot_or_del(new relic_hat, ITEM_SLOT_HEAD) + if(relic_mask) + equip_to_slot_or_del(new relic_mask, ITEM_SLOT_MASK) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 62d60941be69..0e274b57c6e8 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -263,6 +263,7 @@ #include "preference_species.dm" #include "preferences.dm" #include "projectiles.dm" +#include "punpun.dm" #include "quirks.dm" #include "range_return.dm" #include "rcd.dm" diff --git a/code/modules/unit_tests/punpun.dm b/code/modules/unit_tests/punpun.dm new file mode 100644 index 000000000000..4d8bc34c3a31 --- /dev/null +++ b/code/modules/unit_tests/punpun.dm @@ -0,0 +1,5 @@ +/datum/unit_test/punpun_name + +/datum/unit_test/punpun_name/Run() + var/mob/living/carbon/human/species/monkey/punpun/punpun = EASY_ALLOCATE() + TEST_ASSERT_EQUAL(punpun.name, initial(punpun.name), "Pun Pun did not have [punpun.p_their()] name set") From d8b8ab90860b5c6bb072cacd60164a04048b5b33 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:36:29 +0000 Subject: [PATCH 017/155] Automatic changelog for PR #95420 [ci skip] --- html/changelogs/AutoChangeLog-pr-95420.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95420.yml diff --git a/html/changelogs/AutoChangeLog-pr-95420.yml b/html/changelogs/AutoChangeLog-pr-95420.yml new file mode 100644 index 000000000000..41822024c507 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95420.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "Pun Pun is Pun Pun again" \ No newline at end of file From c86ff16d5d26843331f4f7d3c3fc2a53ba9324a5 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:52:00 +0100 Subject: [PATCH 018/155] Rewrites integrated circuits to use item_interaction and screwdriver_act (#95378) ## About The Pull Request Another attackby() gone ## Changelog :cl: refactor: Rewrote integrated circuits to use item_interaction and screwdriver_act /:cl: --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> --- .../wiremod/core/integrated_circuit.dm | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/code/modules/wiremod/core/integrated_circuit.dm b/code/modules/wiremod/core/integrated_circuit.dm index fb6ab656488d..5f6197e885f6 100644 --- a/code/modules/wiremod/core/integrated_circuit.dm +++ b/code/modules/wiremod/core/integrated_circuit.dm @@ -147,36 +147,40 @@ GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit) SEND_SIGNAL(src, COMSIG_CIRCUIT_SET_LOCKED, new_value) locked = new_value -/obj/item/integrated_circuit/attackby(obj/item/I, mob/living/user, list/modifiers, list/attack_modifiers) - . = ..() - if(istype(I, /obj/item/circuit_component)) - add_component_manually(I, user) - return +/obj/item/integrated_circuit/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, /obj/item/circuit_component)) + return add_component_manually(tool, user) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING - if(istype(I, /obj/item/stock_parts/power_store/cell)) + if(istype(tool, /obj/item/stock_parts/power_store/cell)) if(cell) balloon_alert(user, "there already is a cell inside!") - return - if(!user.transferItemToLoc(I, src)) - return - set_cell(I) - I.add_fingerprint(user) - user.visible_message(span_notice("[user] inserts a power cell into [src]."), span_notice("You insert the power cell into [src].")) - return + return ITEM_INTERACT_BLOCKING - if(isidcard(I)) - balloon_alert(user, "owner id set for [I]") - owner_id = WEAKREF(I) - return + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING - if(I.tool_behaviour == TOOL_SCREWDRIVER) - if(!cell) - return - I.play_tool_sound(src) - user.visible_message(span_notice("[user] unscrews the power cell from [src]."), span_notice("You unscrew the power cell from [src].")) - cell.forceMove(drop_location()) - set_cell(null) - return + set_cell(tool) + tool.add_fingerprint(user) + user.visible_message(span_notice("[user] inserts a power cell into [src]."), span_notice("You insert the power cell into [src].")) + return ITEM_INTERACT_SUCCESS + + if(isidcard(tool)) + balloon_alert(user, "owner id set for [tool]") + owner_id = WEAKREF(tool) + return ITEM_INTERACT_SUCCESS + + return NONE + +/obj/item/integrated_circuit/screwdriver_act(mob/living/user, obj/item/tool) + if(!cell) + balloon_alert(user, "power cell missing!") + return ITEM_INTERACT_BLOCKING + + tool.play_tool_sound(src) + user.visible_message(span_notice("[user] unscrews the power cell from [src]."), span_notice("You unscrew the power cell from [src].")) + cell.forceMove(drop_location()) + set_cell(null) + return ITEM_INTERACT_SUCCESS /** * Registers an movable atom as a shell @@ -292,7 +296,7 @@ GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit) */ /obj/item/integrated_circuit/proc/add_component_manually(obj/item/circuit_component/to_add, mob/living/user) if (SEND_SIGNAL(src, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, to_add, user) & COMPONENT_CANCEL_ADD_COMPONENT) - return + return FALSE return add_component(to_add, user) From bc96c0e9f8b8433d4fd573cc667396840c39c678 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:52:19 +0000 Subject: [PATCH 019/155] Automatic changelog for PR #95378 [ci skip] --- html/changelogs/AutoChangeLog-pr-95378.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95378.yml diff --git a/html/changelogs/AutoChangeLog-pr-95378.yml b/html/changelogs/AutoChangeLog-pr-95378.yml new file mode 100644 index 000000000000..0625d04956c7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95378.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Rewrote integrated circuits to use item_interaction and screwdriver_act" \ No newline at end of file From 18bdac7888ba2763251ab716c6ea4f510de7c89c Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:56:29 +0100 Subject: [PATCH 020/155] Converts circuit printers/duplicators to item_interaction (#95398) ## About The Pull Request Another one bites the dust ## Changelog :cl: refactor: Converted circuit printers/duplicators to item_interaction /:cl: --- .../modules/wiremod/core/component_printer.dm | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/code/modules/wiremod/core/component_printer.dm b/code/modules/wiremod/core/component_printer.dm index cda5a9adee1f..8f7ec81cfb45 100644 --- a/code/modules/wiremod/core/component_printer.dm +++ b/code/modules/wiremod/core/component_printer.dm @@ -213,23 +213,24 @@ return data -/obj/machinery/component_printer/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers) +/obj/machinery/component_printer/item_interaction(mob/living/user, obj/item/tool, list/modifiers) if (user.combat_mode) - return ..() + return NONE var/obj/item/integrated_circuit/circuit - if(istype(weapon, /obj/item/integrated_circuit)) - circuit = weapon - else if (istype(weapon, /obj/item/circuit_component/module)) - var/obj/item/circuit_component/module/module = weapon + if(istype(tool, /obj/item/integrated_circuit)) + circuit = tool + else if (istype(tool, /obj/item/circuit_component/module)) + var/obj/item/circuit_component/module/module = tool circuit = module.internal_circuit + if (isnull(circuit)) - return ..() + return NONE circuit.linked_component_printer = WEAKREF(src) circuit.update_static_data_for_all_viewers() balloon_alert(user, "successfully linked to the integrated circuit") - + return ITEM_INTERACT_SUCCESS /obj/machinery/component_printer/crowbar_act(mob/living/user, obj/item/tool) if(..()) @@ -447,14 +448,14 @@ created_atom.pixel_x = created_atom.base_pixel_x + rand(-5, 5) created_atom.pixel_y = created_atom.base_pixel_y + rand(-5, 5) -/obj/machinery/module_duplicator/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) +/obj/machinery/module_duplicator/item_interaction(mob/living/user, obj/item/tool, list/modifiers) var/list/data = list() - if(istype(weapon, /obj/item/circuit_component/module)) - var/obj/item/circuit_component/module/module = weapon + if(istype(tool, /obj/item/circuit_component/module)) + var/obj/item/circuit_component/module/module = tool if(HAS_TRAIT(module, TRAIT_CIRCUIT_UNDUPABLE)) balloon_alert(user, "integrated circuit cannot be saved!") - return ..() + return ITEM_INTERACT_BLOCKING data["dupe_data"] = list() module.save_data_to_list(data["dupe_data"]) @@ -462,11 +463,12 @@ data["name"] = module.display_name data["desc"] = "A module that has been loaded in by [user]." data["materials"] = list(SSmaterials.get_material(/datum/material/glass) = module.circuit_size * cost_per_component) - else if(istype(weapon, /obj/item/integrated_circuit)) - var/obj/item/integrated_circuit/integrated_circuit = weapon + else if(istype(tool, /obj/item/integrated_circuit)) + var/obj/item/integrated_circuit/integrated_circuit = tool if(HAS_TRAIT(integrated_circuit, TRAIT_CIRCUIT_UNDUPABLE)) balloon_alert(user, "integrated circuit cannot be saved!") - return ..() + return ITEM_INTERACT_BLOCKING + data["dupe_data"] = integrated_circuit.convert_to_json() data["name"] = integrated_circuit.display_name @@ -481,19 +483,20 @@ data["integrated_circuit"] = TRUE if(!length(data)) - return ..() + return NONE if(!data["name"]) balloon_alert(user, "it needs a name!") - return ..() + return ITEM_INTERACT_BLOCKING for(var/list/component_data as anything in scanned_designs) if(component_data["name"] == data["name"]) balloon_alert(user, "name already exists!") - return ..() + return ITEM_INTERACT_BLOCKING flick("module-fab-scan", src) addtimer(CALLBACK(src, PROC_REF(finish_module_scan), user, data), 1.4 SECONDS) + return ITEM_INTERACT_SUCCESS /obj/machinery/module_duplicator/proc/finish_module_scan(mob/user, data) scanned_designs += list(data) From b5a19b8cadef7a1e94ca58d324052252c5a100d0 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:56:48 +0000 Subject: [PATCH 021/155] Automatic changelog for PR #95398 [ci skip] --- html/changelogs/AutoChangeLog-pr-95398.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95398.yml diff --git a/html/changelogs/AutoChangeLog-pr-95398.yml b/html/changelogs/AutoChangeLog-pr-95398.yml new file mode 100644 index 000000000000..651a3c92d791 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95398.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted circuit printers/duplicators to item_interaction" \ No newline at end of file From 7b893f6b2562b413af6696943006f69854f9c1a7 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:04:00 +0100 Subject: [PATCH 022/155] Converts bot construction to item_interaction (#95404) ## About The Pull Request Converts basicmob bot construction to use item_interaction instead of attackby() ## Changelog :cl: refactor: Converted bot construction to item_interaction /:cl: --- .../living/simple_animal/bot/construction.dm | 763 ++++++++++-------- 1 file changed, 423 insertions(+), 340 deletions(-) diff --git a/code/modules/mob/living/simple_animal/bot/construction.dm b/code/modules/mob/living/simple_animal/bot/construction.dm index e55c94b73d5e..15173c282c1a 100644 --- a/code/modules/mob/living/simple_animal/bot/construction.dm +++ b/code/modules/mob/living/simple_animal/bot/construction.dm @@ -22,19 +22,19 @@ * Checks if the user can finish constructing a bot with a given item. * * Arguments: - * * I - Item to be used + * * tool - Item to be used * * user - Mob doing the construction * * drop_item - Whether or no the item should be dropped; defaults to 1. Should be set to 0 if the item is a tool, stack, or otherwise doesn't need to be dropped. If not set to 0, item must be deleted afterwards. */ -/obj/item/bot_assembly/proc/can_finish_build(obj/item/I, mob/user, drop_item = 1) +/obj/item/bot_assembly/proc/can_finish_build(obj/item/tool, mob/user, drop_item = 1) if(istype(loc, /obj/item/storage/backpack)) to_chat(user, span_warning("You must take [src] out of [loc] first!")) return FALSE - if(!I || !user || (drop_item && !user.temporarilyRemoveItemFromInventory(I))) + if(!tool || !user || (drop_item && !user.temporarilyRemoveItemFromInventory(tool))) return FALSE return TRUE -//Cleanbot assembly +// Cleanbot assembly /obj/item/bot_assembly/cleanbot desc = "It's a bucket with a sensor attached." name = "incomplete cleanbot assembly" @@ -67,22 +67,23 @@ return ..() -/obj/item/bot_assembly/cleanbot/attackby(obj/item/item_attached, mob/user, list/modifiers, list/attack_modifiers) - ..() - if(!istype(item_attached, /obj/item/bodypart/arm/left/robot) && !istype(item_attached, /obj/item/bodypart/arm/right/robot)) - return - if(!can_finish_build(item_attached, user)) - return +/obj/item/bot_assembly/cleanbot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/bodypart/arm/left/robot) && !istype(tool, /obj/item/bodypart/arm/right/robot)) + return NONE + + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + var/mob/living/basic/bot/cleanbot/bot = new(drop_location()) bucket_obj.forceMove(bot) bot.name = created_name - bot.robot_arm = item_attached.type - to_chat(user, span_notice("You add [item_attached] to [src]. Beep boop!")) - qdel(item_attached) + bot.robot_arm = tool.type + to_chat(user, span_notice("You add [tool] to [src]. Beep boop!")) + qdel(tool) qdel(src) + return ITEM_INTERACT_SUCCESS - -//Edbot Assembly +// Edbot Assembly /obj/item/bot_assembly/ed209 name = "incomplete ED-209 assembly" desc = "Some sort of bizarre assembly." @@ -92,99 +93,118 @@ var/lasercolor = "" var/vest_type = /obj/item/clothing/suit/armor/vest -/obj/item/bot_assembly/ed209/attackby(obj/item/W, mob/user, list/modifiers, list/attack_modifiers) - ..() +/obj/item/bot_assembly/ed209/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP, ASSEMBLY_SECOND_STEP) - if(istype(W, /obj/item/bodypart/leg/left/robot) || istype(W, /obj/item/bodypart/leg/right/robot)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - to_chat(user, span_notice("You add [W] to [src].")) - qdel(W) - name = "legs/frame assembly" - if(build_step == ASSEMBLY_FIRST_STEP) - inhand_icon_state = "ed209_leg" - icon_state = "ed209_leg" - else - inhand_icon_state = "ed209_legs" - icon_state = "ed209_legs" - build_step++ + if(!istype(tool, /obj/item/bodypart/leg/left/robot) && !istype(tool, /obj/item/bodypart/leg/right/robot)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + name = "legs/frame assembly" + if(build_step == ASSEMBLY_FIRST_STEP) + inhand_icon_state = "ed209_leg" + icon_state = "ed209_leg" + else + inhand_icon_state = "ed209_legs" + icon_state = "ed209_legs" + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_THIRD_STEP) - if(istype(W, /obj/item/clothing/suit/armor/vest)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - to_chat(user, span_notice("You add [W] to [src].")) - qdel(W) - name = "vest/legs/frame assembly" - inhand_icon_state = "ed209_shell" - icon_state = "ed209_shell" - build_step++ + if(!istype(tool, /obj/item/clothing/suit/armor/vest)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + name = "vest/legs/frame assembly" + inhand_icon_state = "ed209_shell" + icon_state = "ed209_shell" + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_FOURTH_STEP) - if(W.tool_behaviour == TOOL_WELDER) - if(W.use_tool(src, user, 0, volume=40)) - name = "shielded frame assembly" - to_chat(user, span_notice("You weld the vest to [src].")) - build_step++ + if(tool.tool_behaviour != TOOL_WELDER) + return NONE + if(!tool.use_tool(src, user, 0, volume=40)) + return ITEM_INTERACT_BLOCKING + name = "shielded frame assembly" + to_chat(user, span_notice("You weld the vest to [src].")) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_FIFTH_STEP) - if(istype(W, /obj/item/clothing/head/helmet/sec)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - to_chat(user, span_notice("You add [W] to [src].")) - qdel(W) - name = "covered and shielded frame assembly" - inhand_icon_state = "ed209_hat" - icon_state = "ed209_hat" - build_step++ + if(!istype(tool, /obj/item/clothing/head/helmet/sec)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + name = "covered and shielded frame assembly" + inhand_icon_state = "ed209_hat" + icon_state = "ed209_hat" + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SIXTH_STEP) - if(isprox(W)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - build_step++ - to_chat(user, span_notice("You add [W] to [src].")) - qdel(W) - name = "covered, shielded and sensored frame assembly" - inhand_icon_state = "ed209_prox" - icon_state = "ed209_prox" + if(!isprox(tool)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + build_step++ + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + name = "covered, shielded and sensored frame assembly" + inhand_icon_state = "ed209_prox" + icon_state = "ed209_prox" + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SEVENTH_STEP) - if(istype(W, /obj/item/stack/cable_coil)) - var/obj/item/stack/cable_coil/coil = W - if(coil.get_amount() < 1) - to_chat(user, span_warning("You need one length of cable to wire the ED-209!")) - return - to_chat(user, span_notice("You start to wire [src]...")) - if(do_after(user, 4 SECONDS, target = src)) - if(coil.get_amount() >= 1 && build_step == ASSEMBLY_SEVENTH_STEP) - coil.use(1) - to_chat(user, span_notice("You wire [src].")) - name = "wired ED-209 assembly" - build_step++ + if(!istype(tool, /obj/item/stack/cable_coil)) + return NONE + var/obj/item/stack/cable_coil/coil = tool + if(coil.get_amount() < 1) + to_chat(user, span_warning("You need one length of cable to wire the ED-209!")) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You start to wire [src]...")) + if(!do_after(user, 4 SECONDS, target = src)) + return ITEM_INTERACT_BLOCKING + if(coil.get_amount() < 1 || build_step != ASSEMBLY_SEVENTH_STEP) + return ITEM_INTERACT_BLOCKING + coil.use(1) + to_chat(user, span_notice("You wire [src].")) + name = "wired ED-209 assembly" + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_EIGHTH_STEP) - if(istype(W, /obj/item/gun/energy/disabler)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - name = "[W.name] ED-209 assembly" - to_chat(user, span_notice("You add [W] to [src].")) - inhand_icon_state = "ed209_taser" - icon_state = "ed209_taser" - qdel(W) - build_step++ + if(!istype(tool, /obj/item/gun/energy/disabler)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + name = "[tool.name] ED-209 assembly" + to_chat(user, span_notice("You add [tool] to [src].")) + inhand_icon_state = "ed209_taser" + icon_state = "ed209_taser" + qdel(tool) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_NINTH_STEP) - if(W.tool_behaviour == TOOL_SCREWDRIVER) - to_chat(user, span_notice("You start attaching the gun to the frame...")) - if(W.use_tool(src, user, 40, volume=100)) - var/mob/living/simple_animal/bot/secbot/ed209/B = new(drop_location()) - B.name = created_name - to_chat(user, span_notice("You complete the ED-209.")) - qdel(src) - -//Repairbot assemblies + if(tool.tool_behaviour != TOOL_SCREWDRIVER) + return NONE + to_chat(user, span_notice("You start attaching the gun to the frame...")) + if(!tool.use_tool(src, user, 40, volume=100)) + return ITEM_INTERACT_BLOCKING + var/mob/living/simple_animal/bot/secbot/ed209/B = new(drop_location()) + B.name = created_name + to_chat(user, span_notice("You complete the ED-209.")) + qdel(src) + return ITEM_INTERACT_SUCCESS + +// Repairbot assemblies /obj/item/bot_assembly/repairbot name = "Repairbot Chasis" desc = "It's a toolbox with tiles sticking out the top." @@ -219,34 +239,35 @@ if(build_step >= ASSEMBLY_SECOND_STEP) . += mutable_appearance(icon, "repairbot_base_arms", appearance_flags = RESET_COLOR|KEEP_APART) -/obj/item/bot_assembly/repairbot/attackby(obj/item/item, mob/user, list/modifiers, list/attack_modifiers) - ..() +/obj/item/bot_assembly/repairbot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(!istype(item, /obj/item/bodypart/arm/left/robot) && !istype(item, /obj/item/bodypart/arm/right/robot)) - return - if(!can_finish_build(item, user)) - return + if(!istype(tool, /obj/item/bodypart/arm/left/robot) && !istype(tool, /obj/item/bodypart/arm/right/robot)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING build_step++ - to_chat(user, span_notice("You add [item] to [src]. Boop beep!")) - qdel(item) + to_chat(user, span_notice("You add [tool] to [src]. Boop beep!")) + qdel(tool) update_appearance() + return ITEM_INTERACT_SUCCESS + if(ASSEMBLY_SECOND_STEP) - if(!istype(item, /obj/item/stack/conveyor)) - return - if(!can_finish_build(item, user)) - return + if(!istype(tool, /obj/item/stack/conveyor)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING var/mob/living/basic/bot/repairbot/repair = new(drop_location()) repair.name = created_name repair.toolbox = toolbox repair.set_color(toolbox_color) - to_chat(user, span_notice("You add [item] to [src]. Boop beep!")) - var/obj/item/stack/crafting_stack = item + to_chat(user, span_notice("You add [tool] to [src]. Boop beep!")) + var/obj/item/stack/crafting_stack = tool crafting_stack.use(1) qdel(src) + return ITEM_INTERACT_SUCCESS - -//Medbot Assembly +// Medbot Assembly /obj/item/bot_assembly/medbot name = "incomplete medibot assembly" desc = "A first aid kit with a robot arm permanently grafted to it." @@ -262,69 +283,74 @@ if(skin) icon_state = "[base_icon_state]_[skin]" -/obj/item/bot_assembly/medbot/attackby(obj/item/W, mob/user, list/modifiers, list/attack_modifiers) - ..() +/obj/item/bot_assembly/medbot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(istype(W, /obj/item/healthanalyzer)) - if(!user.temporarilyRemoveItemFromInventory(W)) - return - healthanalyzer = W.type - to_chat(user, span_notice("You add [W] to [src].")) - qdel(W) - name = "first aid/robot arm/health analyzer assembly" - add_overlay("[base_icon_state]_analyzer") - build_step++ + if(!istype(tool, /obj/item/healthanalyzer)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + healthanalyzer = tool.type + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + name = "first aid/robot arm/health analyzer assembly" + add_overlay("[base_icon_state]_analyzer") + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(isprox(W)) - if(!can_finish_build(W, user)) - return - qdel(W) - var/mob/living/basic/bot/medbot/medbot = new(drop_location(), skin) - to_chat(user, span_notice("You complete the Medbot. Beep boop!")) - medbot.name = created_name - medbot.medkit_type = medkit_type - medbot.robot_arm = robot_arm - medbot.health_analyzer = healthanalyzer - var/obj/item/storage/medkit/medkit = medkit_type - medbot.damage_type_healer = initial(medkit.damagetype_healed) ? initial(medkit.damagetype_healed) : BRUTE - qdel(src) + if(!isprox(tool)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + qdel(tool) + var/mob/living/basic/bot/medbot/medbot = new(drop_location(), skin) + to_chat(user, span_notice("You complete the Medbot. Beep boop!")) + medbot.name = created_name + medbot.medkit_type = medkit_type + medbot.robot_arm = robot_arm + medbot.health_analyzer = healthanalyzer + var/obj/item/storage/medkit/medkit = medkit_type + medbot.damage_type_healer = initial(medkit.damagetype_healed) ? initial(medkit.damagetype_healed) : BRUTE + qdel(src) + return ITEM_INTERACT_SUCCESS -//Honkbot Assembly +// Honkbot Assembly /obj/item/bot_assembly/honkbot name = "incomplete honkbot assembly" desc = "The clown's up to no good once more" icon_state = "honkbot_arm" created_name = "Honkbot" -/obj/item/bot_assembly/honkbot/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers) - ..() +/obj/item/bot_assembly/honkbot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(isprox(attacking_item)) - if(!user.temporarilyRemoveItemFromInventory(attacking_item)) - return - to_chat(user, span_notice("You add the [attacking_item] to [src]!")) - icon_state = "honkbot_proxy" - name = "incomplete Honkbot assembly" - qdel(attacking_item) - build_step++ + if(!isprox(tool)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add the [tool] to [src]!")) + icon_state = "honkbot_proxy" + name = "incomplete Honkbot assembly" + qdel(tool) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(istype(attacking_item, /obj/item/bikehorn)) - if(!can_finish_build(attacking_item, user)) - return - to_chat(user, span_notice("You add the [attacking_item] to [src]! Honk!")) - var/mob/living/basic/bot/honkbot/new_honkbot = new(drop_location()) - new_honkbot.name = created_name - playsound(new_honkbot, 'sound/machines/ping.ogg', 50, TRUE, -1) - qdel(attacking_item) - qdel(src) - + if(!istype(tool, /obj/item/bikehorn)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add the [tool] to [src]! Honk!")) + var/mob/living/basic/bot/honkbot/new_honkbot = new(drop_location()) + new_honkbot.name = created_name + playsound(new_honkbot, 'sound/machines/ping.ogg', 50, TRUE, -1) + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS -//Secbot Assembly +// Secbot Assembly /obj/item/bot_assembly/secbot name = "incomplete securitron assembly" desc = "Some sort of bizarre assembly made from a proximity sensor, helmet, and signaler." @@ -332,98 +358,97 @@ inhand_icon_state = "helmet" lefthand_file = 'icons/mob/inhands/clothing/hats_righthand.dmi' righthand_file = 'icons/mob/inhands/clothing/hats_lefthand.dmi' - created_name = "Securitron" //To preserve the name if it's a unique securitron I guess - var/swordamt = 0 //If you're converting it into a grievousbot, how many swords have you attached - var/toyswordamt = 0 //honk - -/obj/item/bot_assembly/secbot/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - ..() - var/atom/Tsec = drop_location() + created_name = "Securitron" // To preserve the name if it's a unique securitron I guess + /// If you're converting it into a grievousbot, how many swords have you attached + var/swordamt = 0 + /// Honk + var/toyswordamt = 0 + +/obj/item/bot_assembly/secbot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + var/atom/drop_loc = drop_location() switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(I.tool_behaviour == TOOL_WELDER) - if(I.use_tool(src, user, 0, volume=40)) - add_overlay("hs_hole") - to_chat(user, span_notice("You weld a hole in [src]!")) - build_step++ - - else if(I.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct - new /obj/item/assembly/signaler(Tsec) - new /obj/item/clothing/head/helmet/sec(Tsec) - to_chat(user, span_notice("You disconnect the signaler from the helmet.")) - qdel(src) + if(tool.tool_behaviour == TOOL_WELDER) + if(!tool.use_tool(src, user, 0, volume=40)) + return ITEM_INTERACT_BLOCKING + add_overlay("hs_hole") + to_chat(user, span_notice("You weld a hole in [src]!")) + build_step++ + return ITEM_INTERACT_SUCCESS + + if(tool.tool_behaviour != TOOL_SCREWDRIVER) //deconstruct + return NONE + + new /obj/item/assembly/signaler(drop_loc) + new /obj/item/clothing/head/helmet/sec(drop_loc) + to_chat(user, span_notice("You disconnect the signaler from the helmet.")) + qdel(src) + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(isprox(I)) - if(!user.temporarilyRemoveItemFromInventory(I)) - return - to_chat(user, span_notice("You add [I] to [src]!")) + if(isprox(tool)) + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add [tool] to [src]!")) add_overlay("hs_eye") name = "helmet/signaler/prox sensor assembly" - qdel(I) + qdel(tool) build_step++ + return ITEM_INTERACT_SUCCESS - else if(I.tool_behaviour == TOOL_WELDER) //deconstruct - if(I.use_tool(src, user, 0, volume=40)) - cut_overlay("hs_hole") - to_chat(user, span_notice("You weld the hole in [src] shut!")) - build_step-- + if(tool.tool_behaviour != TOOL_WELDER) //deconstruct + return NONE + + if(!tool.use_tool(src, user, 0, volume=40)) + return ITEM_INTERACT_BLOCKING + + cut_overlay("hs_hole") + to_chat(user, span_notice("You weld the hole in [src] shut!")) + build_step-- + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_THIRD_STEP) - if((istype(I, /obj/item/bodypart/arm/left/robot)) || (istype(I, /obj/item/bodypart/arm/right/robot))) - if(!user.temporarilyRemoveItemFromInventory(I)) - return - to_chat(user, span_notice("You add [I] to [src]!")) + if((istype(tool, /obj/item/bodypart/arm/left/robot)) || (istype(tool, /obj/item/bodypart/arm/right/robot))) + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add [tool] to [src]!")) name = "helmet/signaler/prox sensor/robot arm assembly" add_overlay("hs_arm") - robot_arm = I.type - qdel(I) + robot_arm = tool.type + qdel(tool) build_step++ + return ITEM_INTERACT_SUCCESS - else if(I.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct - cut_overlay("hs_eye") - new /obj/item/assembly/prox_sensor(Tsec) - to_chat(user, span_notice("You detach the proximity sensor from [src].")) - build_step-- + if(tool.tool_behaviour != TOOL_SCREWDRIVER) //deconstruct + return NONE + + cut_overlay("hs_eye") + new /obj/item/assembly/prox_sensor(drop_loc) + to_chat(user, span_notice("You detach the proximity sensor from [src].")) + build_step-- + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_FOURTH_STEP) - if(istype(I, /obj/item/melee/baton/security)) - if(!can_finish_build(I, user)) - return + if(istype(tool, /obj/item/melee/baton/security)) + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You complete the Securitron! Beep boop.")) - var/mob/living/simple_animal/bot/secbot/S = new(Tsec) + var/mob/living/simple_animal/bot/secbot/S = new(drop_loc) S.name = created_name - S.baton_type = I.type + S.baton_type = tool.type S.robot_arm = robot_arm - qdel(I) + qdel(tool) qdel(src) - if(I.tool_behaviour == TOOL_WRENCH) + return ITEM_INTERACT_SUCCESS + + if(tool.tool_behaviour == TOOL_WRENCH) to_chat(user, span_notice("You adjust [src]'s arm slots to mount extra weapons.")) - build_step ++ - return - if(istype(I, /obj/item/toy/sword)) - if(toyswordamt < 3 && swordamt <= 0) - if(!user.temporarilyRemoveItemFromInventory(I)) - return - created_name = "General Beepsky" - name = "helmet/signaler/prox sensor/robot arm/toy sword assembly" - icon_state = "grievous_assembly" - to_chat(user, span_notice("You superglue [I] onto one of [src]'s arm slots.")) - qdel(I) - toyswordamt ++ - else - if(!can_finish_build(I, user)) - return - to_chat(user, span_notice("You complete the Securitron!...Something seems a bit wrong with it..?")) - var/mob/living/simple_animal/bot/secbot/grievous/toy/S = new(Tsec) - S.name = created_name - S.robot_arm = robot_arm - qdel(I) - qdel(src) - - else if(I.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct + build_step++ + return ITEM_INTERACT_SUCCESS + + if(tool.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct cut_overlay("hs_arm") - var/obj/item/bodypart/dropped_arm = new robot_arm(Tsec) + var/obj/item/bodypart/dropped_arm = new robot_arm(drop_loc) robot_arm = null to_chat(user, span_notice("You remove [dropped_arm] from [src].")) build_step-- @@ -432,36 +457,68 @@ icon_state = initial(icon_state) to_chat(user, span_notice("The superglue binding [src]'s toy swords to its chassis snaps!")) for(var/IS in 1 to toyswordamt) - new /obj/item/toy/sword(Tsec) + new /obj/item/toy/sword(drop_loc) + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/toy/sword)) + return NONE + + if(toyswordamt < 3 && swordamt <= 0) + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + created_name = "General Beepsky" + name = "helmet/signaler/prox sensor/robot arm/toy sword assembly" + icon_state = "grievous_assembly" + to_chat(user, span_notice("You superglue [tool] onto one of [src]'s arm slots.")) + qdel(tool) + toyswordamt++ + return ITEM_INTERACT_SUCCESS + + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You complete the Securitron!...Something seems a bit wrong with it..?")) + var/mob/living/simple_animal/bot/secbot/grievous/toy/S = new(drop_loc) + S.name = created_name + S.robot_arm = robot_arm + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_FIFTH_STEP) - if(istype(I, /obj/item/melee/energy/sword/saber)) - if(swordamt < 3) - if(!user.temporarilyRemoveItemFromInventory(I)) - return - created_name = "General Beepsky" - name = "helmet/signaler/prox sensor/robot arm/energy sword assembly" - icon_state = "grievous_assembly" - to_chat(user, span_notice("You bolt [I] onto one of [src]'s arm slots.")) - qdel(I) - swordamt ++ - else - if(!can_finish_build(I, user)) - return - to_chat(user, span_notice("You complete the Securitron!...Something seems a bit wrong with it..?")) - var/mob/living/simple_animal/bot/secbot/grievous/S = new(Tsec) - S.name = created_name - S.robot_arm = robot_arm - qdel(I) - qdel(src) - else if(I.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct + if(tool.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct build_step-- swordamt = 0 icon_state = initial(icon_state) to_chat(user, span_notice("You unbolt [src]'s energy swords.")) for(var/IS in 1 to swordamt) - new /obj/item/melee/energy/sword/saber(Tsec) - + new /obj/item/melee/energy/sword/saber(drop_loc) + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/melee/energy/sword/saber)) + return NONE + + if(swordamt < 3) + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + created_name = "General Beepsky" + name = "helmet/signaler/prox sensor/robot arm/energy sword assembly" + icon_state = "grievous_assembly" + to_chat(user, span_notice("You bolt [tool] onto one of [src]'s arm slots.")) + qdel(tool) + swordamt++ + return ITEM_INTERACT_SUCCESS + + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You complete the Securitron!...Something seems a bit wrong with it..?")) + var/mob/living/simple_animal/bot/secbot/grievous/S = new(drop_loc) + S.name = created_name + S.robot_arm = robot_arm + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS //Firebot Assembly /obj/item/bot_assembly/firebot @@ -470,28 +527,31 @@ icon_state = "firebot_arm" created_name = "Firebot" -/obj/item/bot_assembly/firebot/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - ..() +/obj/item/bot_assembly/firebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(istype(I, /obj/item/clothing/head/utility/hardhat/red)) - if(!user.temporarilyRemoveItemFromInventory(I)) - return - to_chat(user,span_notice("You add the [I] to [src]!")) - icon_state = "firebot_helmet" - desc = "An incomplete firebot assembly with a fire helmet." - qdel(I) - build_step++ + if(!istype(tool, /obj/item/clothing/head/utility/hardhat/red)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + to_chat(user,span_notice("You add the [tool] to [src]!")) + icon_state = "firebot_helmet" + desc = "An incomplete firebot assembly with a fire helmet." + qdel(tool) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(isprox(I)) - if(!can_finish_build(I, user)) - return - to_chat(user, span_notice("You add the [I] to [src]! Beep Boop!")) - var/mob/living/basic/bot/firebot/firebot = new(drop_location()) - firebot.name = created_name - qdel(I) - qdel(src) + if(!isprox(tool)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You add the [tool] to [src]! Beep Boop!")) + var/mob/living/basic/bot/firebot/firebot = new(drop_location()) + firebot.name = created_name + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS //Get cleaned /obj/item/bot_assembly/hygienebot @@ -501,102 +561,125 @@ created_name = "Hygienebot" custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2) -/obj/item/bot_assembly/hygienebot/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - . = ..() - var/atom/Tsec = drop_location() +/obj/item/bot_assembly/hygienebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + var/atom/drop_loc = drop_location() switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(I.tool_behaviour == TOOL_WELDER) //Construct - if(I.use_tool(src, user, 0, volume=40)) - to_chat(user, span_notice("You weld a water hole in [src]!")) - build_step++ - return - if(I.tool_behaviour == TOOL_WRENCH) //Deconstruct - if(I.use_tool(src, user, 0, volume=40)) - new /obj/item/stack/sheet/iron(Tsec, 2) - to_chat(user, span_notice("You disconnect the hygienebot assembly.")) - qdel(src) + if(tool.tool_behaviour == TOOL_WELDER) //Construct + if(!tool.use_tool(src, user, 0, volume=40)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You weld a water hole in [src]!")) + build_step++ + return ITEM_INTERACT_SUCCESS + + if(tool.tool_behaviour != TOOL_WRENCH) //Deconstruct + return NONE + if(!tool.use_tool(src, user, 0, volume=40)) + return ITEM_INTERACT_BLOCKING + new /obj/item/stack/sheet/iron(drop_loc, 2) + to_chat(user, span_notice("You disconnect the hygienebot assembly.")) + qdel(src) + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(isprox(I)) //Construct - if(!user.temporarilyRemoveItemFromInventory(I)) - return + if(isprox(tool)) //Construct + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + build_step++ - to_chat(user, span_notice("You add [I] to [src].")) - qdel(I) - if(I.tool_behaviour == TOOL_WELDER) //Deconstruct - if(I.use_tool(src, user, 0, volume=30)) - to_chat(user, span_notice("You weld close the water hole in [src]!")) - build_step-- - return + to_chat(user, span_notice("You add [tool] to [src].")) + qdel(tool) + return ITEM_INTERACT_SUCCESS + + if(tool.tool_behaviour != TOOL_WELDER) //Deconstruct + return NONE + + if(!tool.use_tool(src, user, 0, volume=30)) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You weld close the water hole in [src]!")) + build_step-- + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_THIRD_STEP) - if(!can_finish_build(I, user, 0)) - return - if(istype(I, /obj/item/stack/ducts)) //Construct - var/obj/item/stack/ducts/D = I - if(D.get_amount() < 1) - to_chat(user, span_warning("You need one fluid duct to finish [src]")) - return - to_chat(user, span_notice("You start to pipe up [src]...")) - if(do_after(user, 4 SECONDS, target = src) && D.use(1)) - to_chat(user, span_notice("You pipe up [src].")) - var/mob/living/basic/bot/hygienebot/new_bot = new(drop_location()) - new_bot.name = created_name - qdel(src) - if(I.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct - new /obj/item/assembly/prox_sensor(Tsec) + if(!can_finish_build(tool, user, 0)) + return ITEM_INTERACT_BLOCKING + + if(tool.tool_behaviour == TOOL_SCREWDRIVER) //deconstruct + new /obj/item/assembly/prox_sensor(drop_loc) to_chat(user, span_notice("You detach the proximity sensor from [src].")) build_step-- + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/stack/ducts)) //Construct + return NONE + + var/obj/item/stack/ducts/D = tool + if(D.get_amount() < 1) + to_chat(user, span_warning("You need one fluid duct to finish [src]")) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You start to pipe up [src]...")) + if(!do_after(user, 4 SECONDS, target = src) && D.use(1)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You pipe up [src].")) + var/mob/living/basic/bot/hygienebot/new_bot = new(drop_location()) + new_bot.name = created_name + qdel(src) + return ITEM_INTERACT_SUCCESS -//Vim Assembly +// Vim Assembly /obj/item/bot_assembly/vim name = "incomplete vim assembly" desc = "A space helmet with a leg attached to it. Looks like it needs another leg, if it is to become something." icon_state = "vim_0" created_name = "\improper Vim" -/obj/item/bot_assembly/vim/attackby(obj/item/part, mob/user, list/modifiers, list/attack_modifiers) - . = ..() - if(.) - return +/obj/item/bot_assembly/vim/item_interaction(mob/living/user, obj/item/tool, list/modifiers) switch(build_step) if(ASSEMBLY_FIRST_STEP) - if(istype(part, /obj/item/bodypart/leg/left/robot) || istype(part, /obj/item/bodypart/leg/right/robot)) - if(!user.temporarilyRemoveItemFromInventory(part)) - return - balloon_alert(user, "leg attached") - icon_state = "vim_1" - desc = "Some kind of incomplete mechanism. It seems to be missing the headlights." - qdel(part) - build_step++ + if(!istype(tool, /obj/item/bodypart/leg/left/robot) && !istype(tool, /obj/item/bodypart/leg/right/robot)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_BLOCKING + balloon_alert(user, "leg attached") + icon_state = "vim_1" + desc = "Some kind of incomplete mechanism. It seems to be missing the headlights." + qdel(tool) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_SECOND_STEP) - if(istype(part, /obj/item/flashlight)) - if(!user.temporarilyRemoveItemFromInventory(part)) - return - balloon_alert(user, "flashlight added") - icon_state = "vim_2" - desc = "Some kind of incomplete mechanism. The flashlight is added, but not secured." - qdel(part) - build_step++ + if(!istype(tool, /obj/item/flashlight)) + return NONE + if(!user.temporarilyRemoveItemFromInventory(tool)) + return ITEM_INTERACT_SUCCESS + balloon_alert(user, "flashlight added") + icon_state = "vim_2" + desc = "Some kind of incomplete mechanism. The flashlight is added, but not secured." + qdel(tool) + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_THIRD_STEP) - if(part.tool_behaviour == TOOL_SCREWDRIVER) - balloon_alert(user, "securing flashlight...") - if(!part.use_tool(src, user, 4 SECONDS, volume=100)) - return - balloon_alert(user, "flashlight secured") - icon_state = "vim_3" - desc = "Some kind of incomplete mechanism. It seems nearly completed, and just needs a voice assembly." - build_step++ + if(tool.tool_behaviour != TOOL_SCREWDRIVER) + return NONE + balloon_alert(user, "securing flashlight...") + if(!tool.use_tool(src, user, 4 SECONDS, volume=100)) + return ITEM_INTERACT_BLOCKING + balloon_alert(user, "flashlight secured") + icon_state = "vim_3" + desc = "Some kind of incomplete mechanism. It seems nearly completed, and just needs a voice assembly." + build_step++ + return ITEM_INTERACT_SUCCESS if(ASSEMBLY_FOURTH_STEP) - if(istype(part, /obj/item/assembly/voice)) - if(!can_finish_build(part, user)) - return - balloon_alert(user, "assembly finished") - var/obj/vehicle/sealed/car/vim/new_vim = new(drop_location()) - new_vim.name = created_name - qdel(part) - qdel(src) + if(!istype(tool, /obj/item/assembly/voice)) + return NONE + if(!can_finish_build(tool, user)) + return ITEM_INTERACT_BLOCKING + balloon_alert(user, "assembly finished") + var/obj/vehicle/sealed/car/vim/new_vim = new(drop_location()) + new_vim.name = created_name + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS From 682f24be4554e93adbe0ceb1d6e8205cd73a4c0d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:04:20 +0000 Subject: [PATCH 023/155] Automatic changelog for PR #95404 [ci skip] --- html/changelogs/AutoChangeLog-pr-95404.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95404.yml diff --git a/html/changelogs/AutoChangeLog-pr-95404.yml b/html/changelogs/AutoChangeLog-pr-95404.yml new file mode 100644 index 000000000000..f94a656c431d --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95404.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted bot construction to item_interaction" \ No newline at end of file From 0a61cce996d3087e540838564e0534a3b330b4c3 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:24:20 -0500 Subject: [PATCH 024/155] Refactor revenant abilities / they now indicate if they are unlocked (#95380) ## About The Pull Request Refactors Revenant abilities into a component Revenant abilities now indicate if they are locked image image They also more accurately update if they are un/usable (ie, if you enter a wall, they turn red.) Also, Revenants are no longer affected by gravity. They already weren't affected by gravity (as far as I could tell) this just prevents them from getting the no-grav alert. ## Why It's Good For The Game - Not all future Revenant abilities need to be an AOE spell - It makes them easier to parse at a glance (what powers you have / don't have / can use) ## Changelog :cl: Melbert qol: Revenant abilities indicate if they are locked, and better indicate if they are currently usable qol: Revenants are no longer alerted that they have no gravity (they always have gravity) refactor: Refactored Revenant abilities, report any oddities with them. /:cl: --- code/__DEFINES/dcs/signals/signals_spell.dm | 4 + .../antagonists/revenant/revenant_skill.dm | 124 ++++++++++++++++++ .../basic/space_fauna/revenant/_revenant.dm | 39 +++++- .../revenant/revenant_abilities.dm | 72 +++------- code/modules/spells/spell.dm | 3 + icons/mob/actions/actions_revenant.dmi | Bin 10664 -> 10942 bytes tgstation.dme | 1 + 7 files changed, 181 insertions(+), 62 deletions(-) create mode 100644 code/modules/antagonists/revenant/revenant_skill.dm diff --git a/code/__DEFINES/dcs/signals/signals_spell.dm b/code/__DEFINES/dcs/signals/signals_spell.dm index ebbdcc4e2bee..1154eba338f1 100644 --- a/code/__DEFINES/dcs/signals/signals_spell.dm +++ b/code/__DEFINES/dcs/signals/signals_spell.dm @@ -14,6 +14,10 @@ /// Return from before cast signals to prevent the spell from going on cooldown before aftercast. #define SPELL_NO_IMMEDIATE_COOLDOWN (1 << 2) +/// Sent from /datum/action/cooldown/spell/can_cast_check() to the spell: (feedback) +#define COMSIG_SPELL_CAN_CAST_CHECK "can_cast_spell" + // Return SPELL_CANCEL_CAST to prevent the spell from being cast + /// Sent to an mob when a [/datum/action/cooldown/spell] calls try_invoke() to the caster: (datum/action/cooldown/spell/spell, feedback) #define COMSIG_MOB_TRY_INVOKE_SPELL "try_invoke_spell" /// The spell gets canceled diff --git a/code/modules/antagonists/revenant/revenant_skill.dm b/code/modules/antagonists/revenant/revenant_skill.dm new file mode 100644 index 000000000000..e3259ba9143e --- /dev/null +++ b/code/modules/antagonists/revenant/revenant_skill.dm @@ -0,0 +1,124 @@ +/// Attach to revenant spells to make them cost essence to cast +/datum/component/revenant_ability + /// If it's locked, and needs to be unlocked before use + VAR_FINAL/locked = TRUE + /// How much essence it costs to unlock + var/unlock_amount = 100 + /// How much essence it costs to use + var/cast_amount = 50 + + /// How long it reveals the revenant + var/reveal_duration = 8 SECONDS + // How long it stuns the revenant + var/stun_duration = 2 SECONDS + + VAR_FINAL/image/locked_overlay + +/datum/component/revenant_ability/Initialize( + unlock_amount = 100, + cast_amount = 50, + reveal_duration = 8 SECONDS, + stun_duration = 2 SECONDS, +) + + if(!istype(parent, /datum/action/cooldown/spell)) + return COMPONENT_INCOMPATIBLE + + set_unlock_amount(unlock_amount) + set_cast_amount(cast_amount) + set_durations(reveal_duration, stun_duration) + + RegisterSignal(parent, COMSIG_SPELL_CAN_CAST_CHECK, PROC_REF(can_cast)) + RegisterSignal(parent, COMSIG_SPELL_BEFORE_CAST, PROC_REF(before_cast)) + RegisterSignal(parent, COMSIG_SPELL_AFTER_CAST, PROC_REF(after_cast)) + RegisterSignal(parent, COMSIG_ACTION_OVERLAY_APPLY, PROC_REF(add_locked_overlay)) + + locked_overlay = image('icons/mob/actions/actions_revenant.dmi', "locked") + +/datum/component/revenant_ability/vv_edit_var(var_name, var_value) + . = ..() + switch(var_name) + if(NAMEOF(src, unlock_amount)) + set_unlock_amount(var_value) + if(NAMEOF(src, cast_amount)) + set_cast_amount(var_value) + if(NAMEOF(src, reveal_duration), NAMEOF(src, stun_duration)) + set_durations(reveal_duration, stun_duration) + if(NAMEOF(src, locked)) + update_spell_name() + +/datum/component/revenant_ability/proc/update_spell_name() + var/datum/action/cooldown/spell/spell = parent + if(locked) + spell.name = "[initial(spell.name)] ([unlock_amount]SE)" + else + spell.name = "[initial(spell.name)] ([cast_amount]E)" + spell.build_all_button_icons() + +/datum/component/revenant_ability/proc/set_unlock_amount(new_value) + unlock_amount = new_value + update_spell_name() + +/datum/component/revenant_ability/proc/set_cast_amount(new_value) + cast_amount = new_value + update_spell_name() + +/datum/component/revenant_ability/proc/set_durations(new_reveal_duration, new_stun_duration) + reveal_duration = new_reveal_duration + stun_duration = new_stun_duration + +/datum/component/revenant_ability/proc/can_cast(datum/action/cooldown/spell/source, feedback) + SIGNAL_HANDLER + + var/mob/living/basic/revenant/ghost = source.owner + if(!istype(ghost)) + return NONE // just allow it anyways + + if(locked) + if(ghost.essence_excess >= unlock_amount) + return NONE + if(feedback) + to_chat(ghost, span_revenwarning("You don't have enough essence to unlock [initial(source.name)]!")) + return SPELL_CANCEL_CAST + + if(!ghost.cast_check(cast_amount, deduct_essence = FALSE, silent = !feedback)) + return SPELL_CANCEL_CAST + + return NONE + +/datum/component/revenant_ability/proc/before_cast(datum/action/cooldown/spell/source, atom/cast_on) + SIGNAL_HANDLER + + var/mob/living/basic/revenant/ghost = source.owner + if(!istype(ghost)) + return NONE // just allow it anyways + + if(locked) + if(ghost.unlock(unlock_amount)) + to_chat(ghost, span_revennotice("You have unlocked [initial(source.name)]!")) + locked = FALSE + update_spell_name() + else + to_chat(ghost, span_revenwarning("You don't have enough essence to unlock [initial(source.name)]!")) + return SPELL_CANCEL_CAST + + if(!ghost.cast_check(cast_amount, deduct_essence = TRUE, silent = FALSE)) + return SPELL_CANCEL_CAST + + return NONE + +/datum/component/revenant_ability/proc/after_cast(datum/action/cooldown/spell/source, atom/cast_on) + SIGNAL_HANDLER + + var/mob/living/caster = source.owner + if(reveal_duration > 0 SECONDS) + caster.apply_status_effect(/datum/status_effect/revenant/revealed, reveal_duration) + if(stun_duration > 0 SECONDS) + caster.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, stun_duration) + +/datum/component/revenant_ability/proc/add_locked_overlay(datum/action/cooldown/spell/source, atom/movable/screen/movable/action_button/current_button, ...) + SIGNAL_HANDLER + + current_button.cut_overlay(locked_overlay) + if(locked) + current_button.add_overlay(locked_overlay) diff --git a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm index 4aae0ffa2d7f..436f8e27c894 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/_revenant.dm @@ -160,6 +160,7 @@ /mob/living/basic/revenant/proc/update_revenant_appearance() SIGNAL_HANDLER update_appearance(UPDATE_ICON) + update_mob_action_buttons() /mob/living/basic/revenant/AltClickOn(atom/target) if(CAN_I_SEE(target)) @@ -321,6 +322,7 @@ return ADD_TRAIT(src, TRAIT_NO_TRANSFORM, REVENANT_STUNNED_TRAIT) dormant = TRUE + update_mob_action_buttons() visible_message( span_warning("[src] lets out a waning screech as violet mist swirls around its dissolving body!"), @@ -410,28 +412,39 @@ return TRUE -/mob/living/basic/revenant/proc/cast_check(essence_cost) +/mob/living/basic/revenant/proc/cast_check(essence_cost, deduct_essence = TRUE, silent = FALSE) if(QDELETED(src)) return var/turf/current = get_turf(src) if(isclosedturf(current)) - to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall.")) + if(!silent) + to_chat(src, span_revenwarning("You cannot use abilities from inside of a wall.")) return FALSE for(var/obj/thing in current) if(!thing.density || thing.CanPass(src, get_dir(current, src))) continue - to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object.")) + if(!silent) + to_chat(src, span_revenwarning("You cannot use abilities inside of a dense object.")) return FALSE + if(dormant) + if(!silent) + to_chat(src, span_revenwarning("Your powers lie dormant right now!")) + return SPELL_CANCEL_CAST + if(HAS_TRAIT(src, TRAIT_REVENANT_INHIBITED)) - to_chat(src, span_revenwarning("Your powers have been suppressed by a nullifying energy!")) + if(!silent) + to_chat(src, span_revenwarning("Your powers have been suppressed by a nullifying energy!")) return FALSE - if(!change_essence_amount(essence_cost, TRUE)) - to_chat(src, span_revenwarning("You lack the essence to use that ability.")) + essence_cost = abs(essence_cost) * -1 + var/has_essence = deduct_essence ? change_essence_amount(essence_cost, silent = TRUE) : (essence + essence_cost >= 0) + if(!has_essence) + if(!silent) + to_chat(src, span_revenwarning("You lack the essence to use that ability!")) return FALSE return TRUE @@ -455,6 +468,7 @@ incorporeal_move = INCORPOREAL_MOVE_JAUNT RemoveInvisibility(type) alpha = 255 + update_mob_action_buttons() /mob/living/basic/revenant/proc/change_essence_amount(essence_to_change_by, silent = FALSE, source = null) if(QDELETED(src)) @@ -478,6 +492,19 @@ to_chat(src, span_revenminor("Lost [essence_to_change_by]E [source ? "from [source]":""].")) return TRUE +/mob/living/basic/revenant/mob_negates_gravity() + return TRUE // i don't gotta explain shit + +/mob/living/basic/revenant/vv_edit_var(vname, vval) + . = ..() + if(vname == NAMEOF(src, essence) || vname == NAMEOF(src, max_essence) || vname == NAMEOF(src, essence_excess)) + update_health_hud() + update_mob_action_buttons() + +/mob/living/basic/revenant/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + update_mob_action_buttons() + /mob/living/basic/revenant/proc/on_reflect(datum/source, atom/movable/reflecting_in, obj/effect/abstract/reflection) SIGNAL_HANDLER // powers are inhibited and we're not revealed so we can't project a reflect diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm index d456f104218b..2030315410cc 100644 --- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm +++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm @@ -20,13 +20,10 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY spell_requirements = NONE - /// If it's locked, and needs to be unlocked before use - var/locked = TRUE /// How much essence it costs to unlock var/unlock_amount = 100 /// How much essence it costs to use var/cast_amount = 50 - /// How long it reveals the revenant var/reveal_duration = 8 SECONDS // How long it stuns the revenant @@ -34,64 +31,27 @@ /datum/action/cooldown/spell/aoe/revenant/New(Target) . = ..() - if(!isrevenant(target)) - stack_trace("[type] was given to a non-revenant mob, please don't.") - qdel(src) - return - - if(locked) - name = "[initial(name)] ([unlock_amount]SE)" - else - name = "[initial(name)] ([cast_amount]E)" - -/datum/action/cooldown/spell/aoe/revenant/can_cast_spell(feedback = TRUE) - . = ..() - if(!.) - return FALSE - if(!isrevenant(owner)) - stack_trace("[type] was owned by a non-revenant mob, please don't.") - return FALSE - - var/mob/living/basic/revenant/ghost = owner - if(ghost.dormant || HAS_TRAIT(ghost, TRAIT_REVENANT_INHIBITED)) - return FALSE - if(locked && ghost.essence_excess <= unlock_amount) - return FALSE - if(ghost.essence <= cast_amount) - return FALSE - - return TRUE + AddComponent(/datum/component/revenant_ability, \ + unlock_amount = unlock_amount, \ + cast_amount = cast_amount, \ + reveal_duration = reveal_duration, \ + stun_duration = stun_duration, \ + ) /datum/action/cooldown/spell/aoe/revenant/get_things_to_cast_on(atom/center) return RANGE_TURFS(aoe_radius, center) -/datum/action/cooldown/spell/aoe/revenant/before_cast(mob/living/basic/revenant/cast_on) - . = ..() - if(. & SPELL_CANCEL_CAST) - return - - if(locked) - if(!cast_on.unlock(unlock_amount)) - to_chat(cast_on, span_revenwarning("You don't have enough essence to unlock [initial(name)]!")) - reset_spell_cooldown() - return . | SPELL_CANCEL_CAST - - name = "[initial(name)] ([cast_amount]E)" - to_chat(cast_on, span_revennotice("You have unlocked [initial(name)]!")) - locked = FALSE - reset_spell_cooldown() - return . | SPELL_CANCEL_CAST - - if(!cast_on.cast_check(-cast_amount)) - reset_spell_cooldown() - return . | SPELL_CANCEL_CAST - -/datum/action/cooldown/spell/aoe/revenant/after_cast(mob/living/basic/revenant/cast_on) +/datum/action/cooldown/spell/aoe/revenant/vv_edit_var(var_name, var_value) . = ..() - if(reveal_duration > 0 SECONDS) - cast_on.apply_status_effect(/datum/status_effect/revenant/revealed, reveal_duration) - if(stun_duration > 0 SECONDS) - cast_on.apply_status_effect(/datum/status_effect/incapacitating/paralyzed/revenant, stun_duration) + // gross getcomp, but this is solely to make life easier for badmins/debug. sue me + var/datum/component/revenant_ability/rev_comp = GetComponent(/datum/component/revenant_ability) + switch(var_name) + if(NAMEOF(src, unlock_amount)) + rev_comp.set_unlock_amount(var_value) + if(NAMEOF(src, cast_amount)) + rev_comp.set_cast_amount(var_value) + if(NAMEOF(src, reveal_duration), NAMEOF(src, stun_duration)) + rev_comp.set_durations(reveal_duration, stun_duration) //Overload Light: Breaks a light that's online and sends out lightning bolts to all nearby people. /datum/action/cooldown/spell/aoe/revenant/overload diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 5bdf80147699..ccbf0097cda5 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -153,6 +153,9 @@ if(!owner) CRASH("[type] - can_cast_spell called on a spell without an owner!") + if(SEND_SIGNAL(src, COMSIG_SPELL_CAN_CAST_CHECK, feedback) & SPELL_CANCEL_CAST) + return FALSE + // Certain spells are not allowed on the centcom zlevel var/turf/caster_turf = get_turf(owner) // Spells which require being on the station diff --git a/icons/mob/actions/actions_revenant.dmi b/icons/mob/actions/actions_revenant.dmi index d657e8a3daebb3160ff2b85791e5e55b8db5b305..3b75c4c1d30c6f9d998b8accd53201e455378e7f 100644 GIT binary patch literal 10942 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 HzlHoCG+pS9 diff --git a/tgstation.dme b/tgstation.dme index f4ae7c525a4d..da8503ddcfcc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3563,6 +3563,7 @@ #include "code\modules\antagonists\revenant\haunted_item.dm" #include "code\modules\antagonists\revenant\revenant_antag.dm" #include "code\modules\antagonists\revenant\revenant_blight.dm" +#include "code\modules\antagonists\revenant\revenant_skill.dm" #include "code\modules\antagonists\revolution\enemy_of_the_state.dm" #include "code\modules\antagonists\revolution\revolution.dm" #include "code\modules\antagonists\revolution\revolution_handler.dm" From 0009cf22e51a462be0199309bcb4a006df50d291 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:24:39 +0000 Subject: [PATCH 025/155] Automatic changelog for PR #95380 [ci skip] --- html/changelogs/AutoChangeLog-pr-95380.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95380.yml diff --git a/html/changelogs/AutoChangeLog-pr-95380.yml b/html/changelogs/AutoChangeLog-pr-95380.yml new file mode 100644 index 000000000000..9a0fcbd060cf --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95380.yml @@ -0,0 +1,6 @@ +author: "Melbert" +delete-after: True +changes: + - qol: "Revenant abilities indicate if they are locked, and better indicate if they are currently usable" + - qol: "Revenants are no longer alerted that they have no gravity (they always have gravity)" + - refactor: "Refactored Revenant abilities, report any oddities with them." \ No newline at end of file From c3fdebbe4f69244fc1a9c6cf3841e614aed4b2dd Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:34:49 +0100 Subject: [PATCH 026/155] Converts vehicles to use item_interaction/tool_acts (#95399) ## About The Pull Request More attackby()s gone ## Changelog :cl: refactor: Converted vehicles to use item_interaction/tool_acts /:cl: --- .../mecha/equipment/tools/other_tools.dm | 10 +-- code/modules/vehicles/mecha/mech_bay.dm | 20 +++--- code/modules/vehicles/motorized_wheelchair.dm | 61 +++++++++++-------- code/modules/vehicles/pimpin_ride.dm | 55 ++++++++++------- code/modules/vehicles/ridden.dm | 17 +++--- code/modules/vehicles/scooter.dm | 52 ++++++++-------- code/modules/vehicles/sealed.dm | 26 ++++---- code/modules/vehicles/secway.dm | 48 +++++++++------ 8 files changed, 168 insertions(+), 121 deletions(-) diff --git a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm index 4124e397ba7e..3a878ad66099 100644 --- a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm +++ b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm @@ -339,11 +339,11 @@ log_message("Deactivated.", LOG_MECHA) return TRUE -/obj/item/mecha_parts/mecha_equipment/generator/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) - . = ..() - if(!istype(weapon, fuel)) - return FALSE - load_fuel(weapon, user) +/obj/item/mecha_parts/mecha_equipment/generator/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, fuel)) + load_fuel(tool, user) + return ITEM_INTERACT_SUCCESS + return NONE /obj/item/mecha_parts/mecha_equipment/generator/process(seconds_per_tick) if(!chassis) diff --git a/code/modules/vehicles/mecha/mech_bay.dm b/code/modules/vehicles/mecha/mech_bay.dm index 922b2af73666..8c6b61546ef7 100644 --- a/code/modules/vehicles/mecha/mech_bay.dm +++ b/code/modules/vehicles/mecha/mech_bay.dm @@ -76,17 +76,21 @@ recharge_console.update_appearance() -/obj/machinery/mech_bay_recharge_port/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(default_deconstruction_screwdriver(user, "recharge_port-o", "recharge_port", I)) - return +/obj/machinery/mech_bay_recharge_port/screwdriver_act(mob/living/user, obj/item/tool) + if(default_deconstruction_screwdriver(user, "recharge_port-o", "recharge_port", tool)) + return ITEM_INTERACT_SUCCESS + return NONE - if(default_change_direction_wrench(user, I)) +/obj/machinery/mech_bay_recharge_port/wrench_act(mob/living/user, obj/item/tool) + if(default_change_direction_wrench(user, tool)) recharging_turf = get_step(loc, dir) - return + return ITEM_INTERACT_SUCCESS + return NONE - if(default_deconstruction_crowbar(I)) - return - return ..() +/obj/machinery/mech_bay_recharge_port/crowbar_act(mob/living/user, obj/item/tool) + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + return NONE /obj/machinery/computer/mech_bay_power_console name = "mech bay power control console" diff --git a/code/modules/vehicles/motorized_wheelchair.dm b/code/modules/vehicles/motorized_wheelchair.dm index 3e85b2263cac..771b9ab55ab0 100644 --- a/code/modules/vehicles/motorized_wheelchair.dm +++ b/code/modules/vehicles/motorized_wheelchair.dm @@ -98,44 +98,57 @@ user.put_in_hands(power_cell) power_cell = null -/obj/vehicle/ridden/wheelchair/motorized/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers) - if(!panel_open) - return ..() +/obj/vehicle/ridden/wheelchair/motorized/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = ..() + if(.) + return + + if(!panel_open || user.combat_mode) + return NONE - if(istype(attacking_item, /obj/item/stock_parts/power_store/cell)) + if(istype(tool, /obj/item/stock_parts/power_store/cell)) if(power_cell) to_chat(user, span_warning("There is a power cell already installed.")) - else - attacking_item.forceMove(src) - power_cell = attacking_item - to_chat(user, span_notice("You install the [attacking_item].")) + return ITEM_INTERACT_BLOCKING + + tool.forceMove(src) + power_cell = tool + to_chat(user, span_notice("You install the [tool].")) refresh_parts() - return - if(!istype(attacking_item, /obj/item/stock_parts)) - return ..() + return ITEM_INTERACT_SUCCESS - var/datum/stock_part/newstockpart = GLOB.stock_part_datums_per_object[attacking_item.type] + if(!istype(tool, /obj/item/stock_parts)) + return NONE + + var/datum/stock_part/newstockpart = GLOB.stock_part_datums_per_object[tool.type] if(isnull(newstockpart)) CRASH("No corresponding datum/stock_part for [newstockpart.type]") + + var/replacement_occured = FALSE for(var/datum/stock_part/oldstockpart in component_parts) var/type_to_check for(var/pathtype in required_parts) if(ispath(oldstockpart.type, pathtype)) type_to_check = pathtype break - if(istype(newstockpart, type_to_check) && istype(oldstockpart, type_to_check)) - if(newstockpart.tier > oldstockpart.tier) - // delete the part in the users hand and add the datum part to the component_list - qdel(attacking_item) - component_parts += newstockpart - // create an new instance of the old datum stock part physical type & put it in the users hand - var/obj/item/stock_parts/part = new oldstockpart.physical_object_type - user.put_in_hands(part) - component_parts -= oldstockpart - // user message - user.visible_message(span_notice("[user] replaces [oldstockpart.name()] with [newstockpart.name()] in [src]."), span_notice("You replace [oldstockpart.name()] with [newstockpart.name()].")) - break + + if(!istype(newstockpart, type_to_check) || !istype(oldstockpart, type_to_check) || newstockpart.tier <= oldstockpart.tier) + continue + + // delete the part in the users hand and add the datum part to the component_list + qdel(tool) + component_parts += newstockpart + // create an new instance of the old datum stock part physical type & put it in the users hand + var/obj/item/stock_parts/part = new oldstockpart.physical_object_type + user.put_in_hands(part) + component_parts -= oldstockpart + // user message + user.visible_message(span_notice("[user] replaces [oldstockpart.name()] with [newstockpart.name()] in [src]."), span_notice("You replace [oldstockpart.name()] with [newstockpart.name()].")) + replacement_occured = TRUE + break + refresh_parts() + return replacement_occured ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING /obj/vehicle/ridden/wheelchair/motorized/handle_deconstruct(disassembled) . = ..() diff --git a/code/modules/vehicles/pimpin_ride.dm b/code/modules/vehicles/pimpin_ride.dm index 88582410dcde..e389403a13dc 100644 --- a/code/modules/vehicles/pimpin_ride.dm +++ b/code/modules/vehicles/pimpin_ride.dm @@ -34,39 +34,52 @@ if (installed_upgrade) . += "It has been upgraded with [installed_upgrade], which can be removed with a screwdriver." -/obj/vehicle/ridden/janicart/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(istype(I, /obj/item/storage/bag/trash)) +/obj/vehicle/ridden/janicart/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = ..() + if(.) + return + if(istype(tool, /obj/item/storage/bag/trash)) if(trash_bag) to_chat(user, span_warning("[src] already has a trashbag hooked!")) - return - if(!user.transferItemToLoc(I, src)) - return + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You hook the trashbag onto [src].")) - trash_bag = I + trash_bag = tool RegisterSignal(trash_bag, COMSIG_QDELETING, PROC_REF(bag_deleted)) - SEND_SIGNAL(src, COMSIG_VACUUM_BAG_ATTACH, I) + SEND_SIGNAL(src, COMSIG_VACUUM_BAG_ATTACH, tool) update_appearance() - else if(istype(I, /obj/item/janicart_upgrade)) + return ITEM_INTERACT_SUCCESS + + if(istype(tool, /obj/item/janicart_upgrade)) if(installed_upgrade) to_chat(user, span_warning("[src] already has an upgrade installed! Use a screwdriver to remove it.")) - return - var/obj/item/janicart_upgrade/new_upgrade = I + return ITEM_INTERACT_BLOCKING + var/obj/item/janicart_upgrade/new_upgrade = tool new_upgrade.forceMove(src) new_upgrade.install(src) installed_upgrade = new_upgrade to_chat(user, span_notice("You upgrade [src] with [new_upgrade].")) update_appearance() - else if (istype(I, /obj/item/screwdriver) && installed_upgrade) - installed_upgrade.uninstall(src) - installed_upgrade.forceMove(get_turf(user)) - user.put_in_hands(installed_upgrade) - to_chat(user, span_notice("You remove [installed_upgrade] from [src]")) - installed_upgrade = null - update_appearance() - else if(trash_bag && (!is_key(I) || is_key(inserted_key))) // don't put a key in the trash when we need it - trash_bag.atom_storage.attempt_insert(I, user) - else - return ..() + return ITEM_INTERACT_SUCCESS + + if(trash_bag && (!is_key(tool) || is_key(inserted_key))) // don't put a key in the trash when we need it + trash_bag.atom_storage.attempt_insert(tool, user) + return ITEM_INTERACT_SUCCESS + + return NONE + +/obj/vehicle/ridden/janicart/screwdriver_act(mob/living/user, obj/item/tool) + if (!installed_upgrade) + return ITEM_INTERACT_BLOCKING + installed_upgrade.uninstall(src) + installed_upgrade.forceMove(get_turf(user)) + user.put_in_hands(installed_upgrade) + to_chat(user, span_notice("You remove [installed_upgrade] from [src]")) + installed_upgrade = null + update_appearance() + return ITEM_INTERACT_SUCCESS /obj/vehicle/ridden/janicart/update_overlays() . = ..() diff --git a/code/modules/vehicles/ridden.dm b/code/modules/vehicles/ridden.dm index 86f5703a0212..fc570c3b6dd2 100644 --- a/code/modules/vehicles/ridden.dm +++ b/code/modules/vehicles/ridden.dm @@ -29,16 +29,17 @@ add_occupant(M) return ..() -/obj/vehicle/ridden/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(!key_type || is_key(inserted_key) || !is_key(I)) - return ..() - if(!user.transferItemToLoc(I, src)) - to_chat(user, span_warning("[I] seems to be stuck to your hand!")) - return - to_chat(user, span_notice("You insert \the [I] into \the [src].")) +/obj/vehicle/ridden/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!key_type || is_key(inserted_key) || !is_key(tool)) + return NONE + if(!user.transferItemToLoc(tool, src)) + to_chat(user, span_warning("[tool] seems to be stuck to your hand!")) + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You insert \the [tool] into \the [src].")) if(inserted_key) //just in case there's an invalid key inserted_key.forceMove(drop_location()) - inserted_key = I + inserted_key = tool + return ITEM_INTERACT_SUCCESS /obj/vehicle/ridden/click_alt(mob/user) if(!inserted_key) diff --git a/code/modules/vehicles/scooter.dm b/code/modules/vehicles/scooter.dm index 16d084cf5181..dbceeb0ed716 100644 --- a/code/modules/vehicles/scooter.dm +++ b/code/modules/vehicles/scooter.dm @@ -12,10 +12,10 @@ /obj/vehicle/ridden/scooter/proc/make_ridable() AddElement(/datum/element/ridable, /datum/component/riding/vehicle/scooter) -/obj/vehicle/ridden/scooter/wrench_act(mob/living/user, obj/item/I) +/obj/vehicle/ridden/scooter/wrench_act(mob/living/user, obj/item/tool) ..() to_chat(user, span_notice("You begin to remove the handlebars...")) - if(!I.use_tool(src, user, 40, volume=50)) + if(!tool.use_tool(src, user, 40, volume=50)) return TRUE var/obj/vehicle/ridden/scooter/skateboard/improvised/skater = new(drop_location()) new /obj/item/stack/rods(drop_location(), 2) @@ -248,37 +248,40 @@ w_class = WEIGHT_CLASS_NORMAL custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5) -/obj/item/scooter_frame/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(!istype(I, /obj/item/stack/sheet/iron)) - return ..() - if(!I.tool_start_check(user, amount=5)) - return +/obj/item/scooter_frame/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/stack/sheet/iron)) + return NONE + if(!tool.tool_start_check(user, amount=5)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You begin to add wheels to [src].")) - if(!I.use_tool(src, user, 80, volume=50, amount=5)) - return + if(!tool.use_tool(src, user, 80, volume = 50, amount = 5)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You finish making wheels for [src].")) new /obj/vehicle/ridden/scooter/skateboard/improvised(user.loc) qdel(src) + return ITEM_INTERACT_SUCCESS -/obj/item/scooter_frame/wrench_act(mob/living/user, obj/item/I) - ..() +/obj/item/scooter_frame/wrench_act(mob/living/user, obj/item/tool) to_chat(user, span_notice("You deconstruct [src].")) new /obj/item/stack/rods(drop_location(), 10) - I.play_tool_sound(src) + tool.play_tool_sound(src) qdel(src) - return TRUE + return ITEM_INTERACT_SUCCESS -/obj/vehicle/ridden/scooter/skateboard/wrench_act(mob/living/user, obj/item/I) +/obj/vehicle/ridden/scooter/skateboard/wrench_act(mob/living/user, obj/item/tool) return -/obj/vehicle/ridden/scooter/skateboard/improvised/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(!istype(I, /obj/item/stack/rods)) - return ..() - if(!I.tool_start_check(user, amount=2)) +/obj/vehicle/ridden/scooter/skateboard/improvised/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = ..() + if (.) return + if(!istype(tool, /obj/item/stack/rods)) + return NONE + if(!tool.tool_start_check(user, amount=2)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You begin making handlebars for [src].")) - if(!I.use_tool(src, user, 25, volume=50, amount=2)) - return + if(!tool.use_tool(src, user, 25, volume=50, amount=2)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You add the rods to [src], creating handlebars.")) var/obj/vehicle/ridden/scooter/skaterskoot = new(loc) if(has_buckled_mobs()) @@ -286,14 +289,15 @@ unbuckle_mob(skaterboy) skaterskoot.buckle_mob(skaterboy) qdel(src) + return ITEM_INTERACT_SUCCESS -/obj/vehicle/ridden/scooter/skateboard/improvised/screwdriver_act(mob/living/user, obj/item/I) +/obj/vehicle/ridden/scooter/skateboard/improvised/screwdriver_act(mob/living/user, obj/item/tool) . = ..() if(.) return to_chat(user, span_notice("You begin to deconstruct and remove the wheels on [src]...")) - if(!I.use_tool(src, user, 20, volume=50)) - return + if(!tool.use_tool(src, user, 20, volume=50)) + return ITEM_INTERACT_BLOCKING to_chat(user, span_notice("You deconstruct the wheels on [src].")) new /obj/item/stack/sheet/iron(drop_location(), 5) new /obj/item/scooter_frame(drop_location()) @@ -301,7 +305,7 @@ var/mob/living/carbon/skatergirl = buckled_mobs[1] unbuckle_mob(skatergirl) qdel(src) - return TRUE + return ITEM_INTERACT_SUCCESS //Wheelys /obj/vehicle/ridden/scooter/skateboard/wheelys diff --git a/code/modules/vehicles/sealed.dm b/code/modules/vehicles/sealed.dm index 4b69d9907623..c11164432042 100644 --- a/code/modules/vehicles/sealed.dm +++ b/code/modules/vehicles/sealed.dm @@ -108,18 +108,20 @@ /obj/vehicle/sealed/proc/exit_location(M) return drop_location() -/obj/vehicle/sealed/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(key_type && !is_key(inserted_key) && is_key(I)) - if(user.transferItemToLoc(I, src)) - to_chat(user, span_notice("You insert [I] into [src].")) - if(inserted_key) //just in case there's an invalid key - inserted_key.forceMove(drop_location()) - inserted_key = I - inserted_key.forceMove(src) - else - to_chat(user, span_warning("[I] seems to be stuck to your hand!")) - return - return ..() +/obj/vehicle/sealed/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!key_type || is_key(inserted_key) || !is_key(tool)) + return NONE + + if(!user.transferItemToLoc(tool, src)) + to_chat(user, span_warning("[tool] seems to be stuck to your hand!")) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You insert [tool] into [src].")) + if(inserted_key) // Just in case there's an invalid key + inserted_key.forceMove(drop_location()) + inserted_key = tool + inserted_key.forceMove(src) + return ITEM_INTERACT_SUCCESS /obj/vehicle/sealed/proc/remove_key(mob/user) if(!inserted_key) diff --git a/code/modules/vehicles/secway.dm b/code/modules/vehicles/secway.dm index 86e26b735d3c..30f9a9224aca 100644 --- a/code/modules/vehicles/secway.dm +++ b/code/modules/vehicles/secway.dm @@ -32,44 +32,54 @@ return do_smoke(0, src, src) -/obj/vehicle/ridden/secway/welder_act(mob/living/user, obj/item/W) +/obj/vehicle/ridden/secway/welder_act(mob/living/user, obj/item/tool) if(user.combat_mode) - return - . = TRUE + return NONE + if(DOING_INTERACTION(user, src)) balloon_alert(user, "you're already repairing it!") - return + return ITEM_INTERACT_BLOCKING + if(atom_integrity >= max_integrity) balloon_alert(user, "it's not damaged!") - return - if(!W.tool_start_check(user, amount=1, heat_required = HIGH_TEMPERATURE_REQUIRED)) - return + return ITEM_INTERACT_BLOCKING + + if(!tool.tool_start_check(user, amount=1, heat_required = HIGH_TEMPERATURE_REQUIRED)) + return ITEM_INTERACT_BLOCKING + user.balloon_alert_to_viewers("started welding [src]", "started repairing [src]") audible_message(span_hear("You hear welding.")) var/did_the_thing while(atom_integrity < max_integrity) - if(W.use_tool(src, user, 2.5 SECONDS, volume=50)) + if(tool.use_tool(src, user, 2.5 SECONDS, volume=50)) did_the_thing = TRUE atom_integrity += min(10, (max_integrity - atom_integrity)) audible_message(span_hear("You hear welding.")) else break + if(did_the_thing) user.balloon_alert_to_viewers("[(atom_integrity >= max_integrity) ? "fully" : "partially"] repaired [src]") - else - user.balloon_alert_to_viewers("stopped welding [src]", "interrupted the repair!") + return ITEM_INTERACT_SUCCESS -/obj/vehicle/ridden/secway/attackby(obj/item/W, mob/living/user, list/modifiers, list/attack_modifiers) - if(!istype(W, /obj/item/food/grown/banana)) - return ..() + user.balloon_alert_to_viewers("stopped welding [src]", "interrupted the repair!") + return ITEM_INTERACT_BLOCKING + +/obj/vehicle/ridden/secway/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + . = ..() + if(.) + return + if(!istype(tool, /obj/item/food/grown/banana)) + return NONE // ignore the occupants because they're presumably too distracted to notice the guy stuffing fruit into their vehicle's exhaust. do segways have exhausts? they do now! - user.visible_message(span_warning("[user] begins stuffing [W] into [src]'s tailpipe."), span_warning("You begin stuffing [W] into [src]'s tailpipe..."), ignored_mobs = occupants) + user.visible_message(span_warning("[user] begins stuffing [tool] into [src]'s tailpipe."), span_warning("You begin stuffing [tool] into [src]'s tailpipe..."), ignored_mobs = occupants) if(!do_after(user, 3 SECONDS, src)) - return TRUE - if(user.transferItemToLoc(W, src)) - user.visible_message(span_warning("[user] stuffs [W] into [src]'s tailpipe."), span_warning("You stuff [W] into [src]'s tailpipe."), ignored_mobs = occupants) - eddie_murphy = W - return TRUE + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING + user.visible_message(span_warning("[user] stuffs [tool] into [src]'s tailpipe."), span_warning("You stuff [tool] into [src]'s tailpipe."), ignored_mobs = occupants) + eddie_murphy = tool + return ITEM_INTERACT_SUCCESS /obj/vehicle/ridden/secway/attack_hand(mob/living/user, list/modifiers) if(!eddie_murphy) From 631e5476a2d5e90a5f8cf5230a778f4c1a0cb7a4 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:35:09 +0000 Subject: [PATCH 027/155] Automatic changelog for PR #95399 [ci skip] --- html/changelogs/AutoChangeLog-pr-95399.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95399.yml diff --git a/html/changelogs/AutoChangeLog-pr-95399.yml b/html/changelogs/AutoChangeLog-pr-95399.yml new file mode 100644 index 000000000000..110e5aa4c6cd --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95399.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted vehicles to use item_interaction/tool_acts" \ No newline at end of file From eccff652616263a8531b05edc772c2e7853b2bbe Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:00:23 +0000 Subject: [PATCH 028/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95194.yml | 5 ---- html/changelogs/AutoChangeLog-pr-95342.yml | 6 ----- html/changelogs/AutoChangeLog-pr-95375.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95377.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95378.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95380.yml | 6 ----- html/changelogs/AutoChangeLog-pr-95396.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95398.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95399.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95404.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95420.yml | 4 ---- html/changelogs/archive/2026-03.yml | 28 ++++++++++++++++++++++ 12 files changed, 28 insertions(+), 49 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95194.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95342.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95375.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95377.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95378.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95380.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95396.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95398.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95399.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95404.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95420.yml diff --git a/html/changelogs/AutoChangeLog-pr-95194.yml b/html/changelogs/AutoChangeLog-pr-95194.yml deleted file mode 100644 index 99f2f0b1551d..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95194.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "timothymtorres" -delete-after: True -changes: - - rscadd: "Spiders can now be tamed and ridden. They eat mouse, lizard, moth, fly, and worm meat and can only be tamed when they are spiderlings or young." - - code_imp: "Refactored tameable code to use TRAIT_TAMED instead of setting individual variables on each mob." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95342.yml b/html/changelogs/AutoChangeLog-pr-95342.yml deleted file mode 100644 index 88a107997afb..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95342.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscadd: "Cannabis will calm down an angry monkey, eventually even turning them docile (not pacifist!) temporarily" - - rscadd: "Nicotine will also calm down an angry monkey, albiet slower than Cannabis and will never turn them entirely docile" - - rscadd: "Booze on the other hand will make a monkey even angrier (though only if they are already upset with someone)" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95375.yml b/html/changelogs/AutoChangeLog-pr-95375.yml deleted file mode 100644 index de3d6966cf0a..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95375.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Rewrote dispenser bots to use item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95377.yml b/html/changelogs/AutoChangeLog-pr-95377.yml deleted file mode 100644 index 38f2487e68f0..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95377.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Rewrote BCI implanters to use item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95378.yml b/html/changelogs/AutoChangeLog-pr-95378.yml deleted file mode 100644 index 0625d04956c7..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95378.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Rewrote integrated circuits to use item_interaction and screwdriver_act" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95380.yml b/html/changelogs/AutoChangeLog-pr-95380.yml deleted file mode 100644 index 9a0fcbd060cf..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95380.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - qol: "Revenant abilities indicate if they are locked, and better indicate if they are currently usable" - - qol: "Revenants are no longer alerted that they have no gravity (they always have gravity)" - - refactor: "Refactored Revenant abilities, report any oddities with them." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95396.yml b/html/changelogs/AutoChangeLog-pr-95396.yml deleted file mode 100644 index fd4eb90dde1f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95396.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "ArcaneMusic" -delete-after: True -changes: - - bugfix: "The cargo shuttle can no longer qdel your ghost mob." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95398.yml b/html/changelogs/AutoChangeLog-pr-95398.yml deleted file mode 100644 index 651a3c92d791..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95398.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted circuit printers/duplicators to item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95399.yml b/html/changelogs/AutoChangeLog-pr-95399.yml deleted file mode 100644 index 110e5aa4c6cd..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95399.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted vehicles to use item_interaction/tool_acts" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95404.yml b/html/changelogs/AutoChangeLog-pr-95404.yml deleted file mode 100644 index f94a656c431d..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95404.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted bot construction to item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95420.yml b/html/changelogs/AutoChangeLog-pr-95420.yml deleted file mode 100644 index 41822024c507..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95420.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Pun Pun is Pun Pun again" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 3152c5d7b9c4..28cb121b9b27 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -208,3 +208,31 @@ - bugfix: Chem scanning a maintenance pill removes the achievement value. timothymtorres: - balance: Sticky spider webs now drain stamina when mobs enter the turf +2026-03-16: + ArcaneMusic: + - bugfix: The cargo shuttle can no longer qdel your ghost mob. + Melbert: + - qol: Revenant abilities indicate if they are locked, and better indicate if they + are currently usable + - qol: Revenants are no longer alerted that they have no gravity (they always have + gravity) + - refactor: Refactored Revenant abilities, report any oddities with them. + - rscadd: Cannabis will calm down an angry monkey, eventually even turning them + docile (not pacifist!) temporarily + - rscadd: Nicotine will also calm down an angry monkey, albiet slower than Cannabis + and will never turn them entirely docile + - rscadd: Booze on the other hand will make a monkey even angrier (though only if + they are already upset with someone) + - bugfix: Pun Pun is Pun Pun again + SmArtKar: + - refactor: Converted vehicles to use item_interaction/tool_acts + - refactor: Rewrote dispenser bots to use item_interaction + - refactor: Rewrote integrated circuits to use item_interaction and screwdriver_act + - refactor: Converted circuit printers/duplicators to item_interaction + - refactor: Rewrote BCI implanters to use item_interaction + - refactor: Converted bot construction to item_interaction + timothymtorres: + - rscadd: Spiders can now be tamed and ridden. They eat mouse, lizard, moth, fly, + and worm meat and can only be tamed when they are spiderlings or young. + - code_imp: Refactored tameable code to use TRAIT_TAMED instead of setting individual + variables on each mob. From 52d0165977bd44bab5c9d622354f2a67f447e835 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 16 Mar 2026 00:34:39 -0500 Subject: [PATCH 029/155] Remove white pixels from ea and firelock animations (#95409) --- icons/obj/doors/airlocks/station/overlays.dmi | Bin 11919 -> 11906 bytes icons/obj/doors/doorfireglass.dmi | Bin 10234 -> 10216 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/obj/doors/airlocks/station/overlays.dmi b/icons/obj/doors/airlocks/station/overlays.dmi index 17e9005042e42c843cf905680fb93ae47c60d2e2..0d573582a688ffa0d52a85e4786f857be7aaf491 100644 GIT binary patch delta 8495 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 KqDax~)&B!zW!&cg delta 8463 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 JtYY!v{{WPa$v*%9 diff --git a/icons/obj/doors/doorfireglass.dmi b/icons/obj/doors/doorfireglass.dmi index 83156ae4351e1ba3ec6125e61ab6e5546dcc3a47..250b5ca833a89c25b9b224511fa2b108e225319c 100644 GIT binary patch delta 8654 zcmZ{Kc_5VG_wS1=Av=+M-wBm{&91E3vs9!aWDUcMk}U?4Esd!pJ4JR4LUt-4`#QF4 zW1V4)`|A6>_x}F4cmA63yzldz=bZC=&gXm{M`kN#pUg-|(T4~N5@ygt)_1Exs72#; zM~RlWwTN?#uSuzgw$o_(-44l)wdwaydBi&mhZ`{0yUXgVOB-itCL29<-cJ`(m0RvK z*uY5asmREF8p)H%A<$r7z;eLgYG(J|s&@@IQ~bN{&8h}U8L81sL`EpjXgN6{&jGcQ zel!gY4L`1&i0Kw%R=|Mvg(qCfe!AACuzBX0kchYC<)(M%ei{O?2j8E!ct{(~wy#Q! z4-A-}P(kC8gdQ@*x5e+dPO<@jk0}iRa>!EO+x^Xtz=VS$k%orf@~5RN{n278&qE0p zzuoCG5&GmCe9TKpL3vP+whZ33cRHp%rUu3iyZD%J zH2%4VB1PDTs_aYesyI{HkI#ySW_AA9Cu)^~qrO19C|VRJsw(2DUzk#>%+T7T`;E!K zUUUZgrFqM(^PJjgowAFh^RmaYNSk3+Lc$lq+n|L(sv_GfNs)IVK3^jxZFy-<1#OK1 z6~uFM2>u=vMr|;laL%(Rw~@@zJMeGa)3n8sI<77xEU&C>pGUf1=vC~zYsV`=ODaDU zHz+ZdGslZbiq>%>Jgku(2PAf%soc23Xg=}*E#I~uZw6Du1JstjdoBd5R^b8b0W0vu zo$TGX?fuP3%F-sM^Q0X+g8b*{^M{`Dvys_Mqha9OW>By8+t6>drnSs+#< z>YnZ+#j~bj3k=8&ikz#Fr!l@VHyx11tiHVf-=*_!Ab~DXl+rv>Brg7V%qdA+vt|@^!_tH zQ9W!6{Bx}m*(`qx&D-KPt)dOAtY)UHi8rka>U0p=JMBx{*;WnFHC{rtON^ zm^A&sDoZ|78RV|e_>!ktI=lT5(6`fz*&qR$Pru>ZR@4<~XRd>-BcD~$bsXj6UmNOn z(393HM6a{AuTmMp=ELIYFN@b*qq6%ETEu%oWoO*Fj~kx}jSp$QKghGm(R(pdCS7L# zzW?7ghnjb^Br4OEa5G&4tqpgEf3f;0UdIFW=c-3R&kb`|tOQ1s{M~%?D9tW{0mxOl z!g#MUSx+79V#CJ9K>{9Ga)pP7$1UaYVB@z~(ahp?_Qy#o_1UGRy*uf&u0P-Fu@l;O zU&ZL^x9BCZ-5JbPFAhAE)=ZXG6;b^*Q%g{asM`%S!=9i|@;HTDr?x#(2`d!m!<%m* zdda!LIWR-WsIEr-aQZy4I(?gKW^1B3Z0>wtyPE-SipESUt$bkzY1o>CGmXPYa%ctK zyfC0Z!=cwQc87T34{!d^Cnk~-&qvff65mTS6Bj-0ooA23k14N?eKm}@OaZ&sauFGw zgVjsKT%4r5tjeYDC|}gLv+m1ofdpH;S^}oPA(feQGlERyD?tKR$)FW1h(pu+^kEkH zY?C12VO9+NJ7?1+Reo7)y&`EpU91JiuV#dVB(~~Jk2%%;oBbX#lD!W}ojzlm%uq5h z^$&h7w~}7ChDztHJ$=IUO(+)E`J^kw=i|V4{Z0C;!?ltO`B+PQega04gfC<+`q4Y^ zmdG;{j}pKKHgC584s5de_rg?30f$SMe*B(xHdn4_%73T5Z{JHAIqkgfkqVRUrGYj3 zoND*~a29guz1yq8$tj$dJURA9Y}K95W<-zAwJTUh%H%wd85(+vMz8J8IpBe#AjqQ- zszkMRqTrT9SIm4tng&3RxOofU9Wua+xQH1d^Mv1Z22mXI1Ugan>UD;Ung7g}{x0PX zh2OfUrZ~<>V-gA-W0+A;uyRchwLsLmV+T)8WZFeoU zHfv{W9@FMsU^WKN-P(enQ5LIrpYJ~GC$x9obAAZ2$wT$odNaIc#$&?=^X<_uDtnC6 zcFfYO_@`fVwF`A2-ZDbCyx^(85JJ_0u%I9XDo$1m^mg^=wC&KF&-wD6X@7w8#JqxY z8DDDiCPb0AH8k8pk?>v72rh^vAg_^E3P9)ZCOTIhoSGtcuef#v$kU%Yvnsvz%9XeI z??`MZC=-|BxS~HJr7Z(%Mz)meyr75IrCn-vxDro80?(-Fx z7J(XMYqUZvaK|34FK64g?~yeLSQapqI_+{-L4F?z^B*cu!yj$;Pk7-6!HYUs_2To{ zjWTSnJTtd8Hac4AUoa#U3bI4UK#rFB7vJ1&-hj-kK>$qE3SlHrYONpHr*3Q4- z=xkx}?kL%|+-drytjVI0c}9girIYG1JI~W1oI*y&3Wk#OPf&`_fFTELzOf(N*~jWA zqfulbgJ(Q8&V zu>n3yADchJMX!o9k14|>KYeO&sNJk_`{iq@R2A6V{9$wmR$JSA@u0al!LQc~fHgb@5UCf~Xirwn8aCwRDIY)bSw>V{ zNVgv};FZ37fpe0#`K-JuTqrhJoghzHCS;n*%GeTCuiYSD^`O`M!1=01T_|ca)U3$)Ug^zRPK2O_x_HQHT?CS+>?Ab^u2)V!c!q1$*#YI{px!( zQZd!jcPpQcw5jRoFsU3YMYkXm_fLKXhX;Oyk-rVIeeS%R*{t9ge6w@0bObeJvep~` zGM*9_DT)NRzAFM0a2_ooWaA|sVrM;WqG-qtXSTbY2`5Hx^=Ch8hT%xOv)J4E{)*%+ z_b5IkW*6kYtMq%R@|59Zj-QYEgU%COojZFUL#G~db^BBIy-~Kht9l9%T5DE}ac71q z+RN6>`fvUnle=SYwk>_wVih<_pAytSx`@lqGxCN=v{+%zYX15wR^px_W&@RkWfYj3 z949nme8>fawa;+HxjMhE6Sun7c~?1>2}#?UpxIzp-+%aiX{)$`(wzS70+Ln5>@!tq z4sKvK9eBGhnyI=;Gk1AOn8~iL@dW<=q0YZRdeTxmqAEA{bwQtE^wQ|4RmCV{)ibX? z@P(7=yYzZe>DA<(V7EL524I3m#gW@8+&Ef z;39v9nE>8810EL|u^8HGYrDNAEShRKoth7%toKKb;**>)b0n@O#w6Y97^`n|Ko01- z%l_MVsy#TC_Z*J6n40oD_SeIvltW>cy2dF#hJW1oaqp#!Dh&)^Zf!xgxDcI5ST$0X zQ%#TzJnM-#%va3N$_y4V$7PvEKRFWLJ+^P#xh2y0kG5A;#J$v1mryd^FWw-?K5zTO z6(_dz!l8bH{>;oCYoH9&#GjGh+ES;nCWy5|Y%*szJ2Kt48tGD===9x_xGj{%%L#Fuy zAgms~K}dhMa{|0t=Wo!~nUNj-E93Bm>KTZ;4#xL?{`gb0FNjyX zf$~$|e9$E8Yp@UwM1tMTJ>%HZU;#q|y71>NkH>|K(w_ed&6|`u`PMzq#s=UAnTT+W1@A5jm5HPh@~;`BQzuN4Qs|SJh#dB zj_w8a_&0aekJqbyV~uh%{N&V@%`~J^ztcLf0k_wl3~;m=vBfXCi>q7xHIw<36}1eX z9v@E#Z9_dmIXF6U3#2CeB%JS4_`B>C!Q<>Y71W({fOyKn08UZ8)i@p74V)0J9e1Hv zJn0U0Y-Cj&a{BYc`SAllKk3_)Pji`s&%*~=CfBdGPOix#!hOMOojYa8)9k?KUCq38lH@tBSrg^b}$;DhVD44Cq6d%Yt75X{9Q?8y9Vk({qdV zMK7+J_E^ag_rMR$W|-i<7nIHtP4|5YoFn)mA5(g;;PQHjkz)s0 zcMPW09$MOLJ#5bn7!R1*dGWkQVC-SAtHR0rT&(axk#1R#*O(Nya^NMJMT0btQ&|Ft zX`{a!Hgf2@>H}UMO!qN&cBc4hkhWT+%T)4DKf#UiPI;0<>iStm(QP>~38xy;#ee)R zoC=eo287b!deG3$=Uay)zXwq+aDqY`_g2%;9nr2c^vTpj*)MVD;BD4K(7P~UGS{G# zBw%e9^*Hqm0%bt-{cODWwVkrxH(|lJE9dzB_ni3+^er25V%SwK0E@&7C25_la~;vm z#)+t9;{tQ#Hw3|_k2DKU_I*E{=JFfh*>nVog+jSzYQ2Ia|h)^(V_NFf?rZc)aZ z1mC}D;XE@*lnEM~a;sxznh8hCGjtbBa0Ay`m*ncsjNDyCTPkjGwWz*!U%&BFZ$#SC zZ~USA@{ojv4@m-Ij{PF2FOeFUS)3bjz9?5q8y`cAa5uQ?{$=5F!eCBxyCXsFv01Y& zjDCC7#+6BAKb<34+unW(9|s0*ivuYD!0nTsBXe(gpx`U5K9qa`OexPr>29|F`Fzg; zUSW;7no8B6VkRz{S(>2E8P2?1IcnyTkXKPb|5FTVc&sS2NI^i#;Y;D$8|Vze(m}BF zFn08M!7Wu@$5;-}nVp4XY=~4X-up4w0~n?VowxoeHsFtTJRs_?=Qf`A@dh))bP_~E zmWLY8S+geTq`feUhJe~r5+4{p>*9ER5IZ$agi;uYTzh?Kla6XJ=tEmJ?ThiUsrVpf zoB>ZpzI6xq^kn9~BC69bXKUo30ML>!&rnwBZV2hin#_^AXMVarNCW4hKo8tWtH z-U|{N8NSVZ0(Yhqf}NzqMrCFuCK92u$$!aWF!DN-OHkm*jSGg9PMt3Sm;X1@47kua-Il{r%d8SvNf(CilL;SY5Og zD>wh1YX9W9s7#8Oo&T1CLas;gTRbSz z$U(h>S>h7qLrU}At?VP)d@tVzRIQcrFVDa9`Ve4$$#N}a{}?eNwqNgxnX-~d2`gM?KL2eFRmSv%PEz3e3vjr(M79k4 z(vZf{lD-G%9|lS`=gWFV1l+?zubmT}jQsh|f_f&d2t(E~TQ7TSKlL~D@8Mx1(<1~* z38^<>A3?o%?KHOt)YB7-JSA4h=}g*#+k=_dj(Vx_u#c-dZF$1aH5;6(%SX8N&BpPw zU#xWUJ-~R`L6ZzT#MHpM%@HQquPm>9{3NehMsQ@}+%&IoFCyH%!W1#ImxpupiJ3b^ zLmKF7^)PBtEamq6ul%&Jh8dS%j=l_j2;REm&BktCjX_@IR|-^+>xi*Ro}fl2$aZ9RwU_H&7^Gp8kZ85AVF zF%*y+jBAS+{i-3SXF0+oUbD|XRgmf#)$=yX|}kJwWOzCkMlT<+^C^dt+wwjwlij> zGv9NyG<`v$waEA*VRC6#NP>L1yPM^9hLwS9nj*_N!Uo*ComYG`p>#(te^wtRn%p8d zMhr^8y}X)*I{67{viY<3Hz0-eV%3RW9E2X}{E;qae;2UwRYzNi;|yz>kpduE5vScU zLtN2^Y2Bdq{(koSPz0j@Iv8h6@yQ4O#A{5>zWQyU;4B)`iETr!9Z#JJ#mG=)i~k74 zQc^=Z?qkqfy|s>z)YV5%I=yYM<@}UXL9X7_o{_K_Bg$tq`UG$K;mk%|*nBUI>LD=qGOq^t_L;zA-DHeMEL4`*vMkFNBQ`c5+vuk?M3HDr6$4f`noiAmpV zNwo&5CbLgUa7n~NIzm^H%&v3zu;r4uMDs03|C4vAXPbj2a{Z>P z0T<5XNIH$Wy<9;X({emSLtDAna3)#f+B5S(H^BQi&&-RHCpvDVy=H6Pq@UqAhWEJJ zEI`#SzqLN5$GpqespOvl?r0`p^1xsVU+rJ3vtwt=-29urYj`?hexg&B3<}5QRW$Q; z2H9E^ZeZtGWb?_-uU!|_&&e(16#b_Ca0|^X5RvRcft%nB7lNrSTBDgc;uvQ@aYj_g zrBCeHc*H1Nu`aKR0_vNez3I|2oK9f^2y1W+CJk9Cr;W&@D+gyH|MWU62vW7dpd1es+D3z9$m1Z+wi*_%~HKMwNe_b61aE zyJf@7@P?JC}NCswQu?Gc7Q&_Mu!I!J0MX!Y6{i=8-sdKa(TTy zlmxguNU9>^+}=-vmWZ!73k(c&qt9LD)qt_rg2z_BZ+1U!D<2|xB{wq7T+uI}X5*xB<;rthpk?ch7v94I z^b=p6o#i_SKYHAkCD)cBYk8mWy+QVJ&23G+&EEUgLCd8Su`h0nhVLUdjps83AU7SO zI0^5!$Z%mcVl3Z2JOIApJ>*4hOOmjo)ezjIp;j@N(B585HLIUJ!2b+kC-3s14>#At zPkYBOID7JQ?CFUZbhQE9GsJbCE+RlPQoJqD(#cDst#HK7>(A$nwZ0CB61ppL4zG?6 z>{$8AZs(-}#3s*DZ|?j*3a4AF;Yv?bJbU1ZHPrE>P8Bj~rQ7z2L8@0hjlO+3u_2x| zqK^`C1WMTzWjt#(4>R|QlcW8B?Gn=9nef_04={jdrjTffaYxte_TDef2bEMGUa*I5 zYatf>c7k{j3cHJ(_nS>8xlp7_##qBYy=Md5s2+uJ1JUc6r*7yUX2caWH1m#t%O(M$ z*Zu}`Owz;mpX(TN|Lgo2xyCuK!^T6}4srth1nr$NF~|1h*{{Mnc#*gmdE-0-iN{E1 z?`G78H|O?BG{J_8`1iC|l3rYtKh@EkF|oMzs9Ii9p#_z0ugW!xC7DGg)A4(dEdSGf z#HgUtGvx`Z*_}|Q@c1k9@r0ahL#6j~@(on$pEtivCT0fT_eS!(>XLiWt$^}I?Jqo7o#ymrQN?ij7Q=R}rV4zko&7d^ z>o@yk&q4E#nZfFNRcpVvP>SU7wYUxJuhW@9>N8v)-9cV@_7@w%Bu63pVz9?F@gQ~E z=)hQ+a@O|D!SQl+!v~3%p26LYC&ut!4^9tDqR;Rk1ZHv1S2?M6oM_oO>x3-uRYHK` zU9P1&o&t;kfw`8XiPoR9yKcT=hC3gh9@`tUzx4L?zR`X!BnFJS_xHl0%EH_Y3dqUM z#ifhHYGFi=pIUdILQ)ot*8vrqkx8$U|%jbg_J21o9Eclh!kRrm$B zUm7NmoCG}1bDV>`C!2o~^kJ@Jn>O=nGh^ej_czEz7{t1eAmfLPT2ESMDRAtV^9fX> z%^MMX!gA;3?)mwAf2n;kif|C-A?J9Xr{8;bnb;8JqO|0RxEkhR`(OH0OD?fFl(IObtZIm?r2)wPc8--vNQ6f=C+z|2N ztZ2Dxe?U6k%?MH|_F=H>) z3>(>VxU0maeHWEG@V3{vvjpgq@#CpXcbT z!%ty@qi2^AbVZn|$_1eh)@Va8lwW~JCVPSUPH!kaD$)ZF{ab**1dqac7o7nEZLV3HTE?W$!;h@`67GDl1wUvtYLU4Yh@ibIx%Q*mESVgdl$k)NCO43GAe>+=Pjyd? zX#7u&H*r*X7VimV;rE;-XnpLg-*7{vtPajK|1^6ODm}4nwrw#k?ahM-T^vyox|1bM zV=d)xCxZ!T!(p|(6Fap#b2p%h6RHzlhj4|Un)Ee%m9&zgOHGf9aefaP%y;NkHnp~v zfM8{-#$T0|%3U?lq)K?IsQ_|sy%dOKg<2(ZW)YhFGNA=UMM#^MS2?f^6IX;b`Ob2x z1WUi&SM0Zdy&^OnFxGpLnG3#1>N|)7KvbA0fQ``3l^i->XyT7gWf0^@^eB{gJ5Vft zRxSG?IoyNSi-VyPKeDw*3MKt4oFisgTMc7=VW>jB7nj%UCtHZCgBwVAmHY_LTdDf+ zR@Uun!9i<};nZ!3%3)Tc^SfLOhD+n9BZ=`JQ@j^PQe0Jl8BRRsF(+#FvaChh7+aL1 zvG9ZFoMPn0%Tc`FCoh3aa6ALejozuy(JuPS7_4U-Y+AdOA3h-S;U@NSy$wGE;%F-w z_PX|Al!U3`c76Sy+c*F;tS0MWytR zxBMy3qMFOZEqH01KeK09mP<6kI{0bB+y0n8yY~%Zu|VgO_DlFzHTu zg@t8O?PM@OE2VNMN2-Ha95Eg-0;A<5(>0m?striWL@5mz>O@S44~zW$q>k+^1n1_U z0J=oZ3z}-K2j-9RC|{y@W;)y3wy@&MCxNyC;w>O5#@b!7kKI&;;)&aclGR%cC7bil z7KL)Z-u&AbVZ&rIUgmtbuUT;M;HPjwkfW>%^nimz6Ao0k7$3hQO5>TC2iz2^&AgXnE; zHS7^a@d45P^`66W>wGxbx91E~1ImBwJ&+wUjQ~*YMXeH_L_Y2-&~HU=h+&qv?#EGj zmSR)%-g>vn1fcMLYt+FH>0&8Rnh*(=*w&@KoI7#u$>Dl8u?`q6eRp^HPs9R!iY?(A zx{Pi^!8yim;xa4c2r=r%&z?15vq-s@L=~v6UAyKqTx$2l=iriRM9i4D+An;?9B**V zNr1`L3FainRm^d(*CpY2$(G?~y(PM6{;d&Z%jmtCr-NuGiXw5e%wRT{B5GLuUEyGo zfmE5iDS+P?Bd;FMro~3bZuWG;SPW}oael(I$}{RhDQIV4P)SLp=aMrQ#3h2L>(d!hh$WQNBhuTrZv6o%1hs$IhZ4z2NDjH zlnev-qLO{s>3O$NY&bJ2Y~H%t;PpNaUI;Kc)Rv_mBf$77fXsAP?O8#t;Eccu5~ z2AC9Zo)&|zA`|w<*_s$ggnj6YOcz_&{D$k(#9f8>4%4>F)T)PX9k>vMUD}<5PtDuO zvi(Ct5dn{cz9)>*GXkk-4=Ovb{;!C;=KH)sl)G`GiANthiSmcK*Uof7oD$CZql3SP zx|GG*zi(q|V#FKdvFO-X@c|k>V|Sl{c71ua1sa#XnR@BdeIRf1OfaUasfCb&>o6YK zU+RA3P@lSi{pHFVxzqmhJLXlW=7pm;^%gvW{G#V{mrc0Ur^veqj<&-06xc+#8;4iM z%jbmXH+nj1Y=hZ=>(Rh|@YA(B&7C6^^f6Ghbf6M+Z#{VN!eGng(aT`AuJ&FgCA)Aq zIG=H9;nAddK-3XbR>cOvs4|bX z+VD@z1!aV_wkGCQKq;^NXF4NA%*@y*_Ua>)pT&F$GxOu;kFm+2p`TQRBL$0-+`BCv zSvq9g&fA2>Y1wYhjz~(@L;0AbVZw+mCEA4FtLLx&yO=jpg49=L-(GyxW=5s`h5t{$ z;kN~A)RhuP727wijHh@?*_d-oqKMg;jzt-2>Ttfcb`bfcCq#{!p?Nk%flbz7mrXZi z8^__|#V(No&MBabANK$Jv231+c(}Ci{QL1R$RyM`&b8HP$8)x$5_hn>JB5vW9d&bn z{T;=HR1PVp0d_F)&7gr3$9t2E^Iwoo#^W;UK7P1oALwdRnNX5)u<1rEqP3-o^^>L^ zpqDXAbaF)#mEs&v;Q-rfhQ|Dd*5nLHZFbrg0zeBHr<#Ue~O8{X^e-; zJKf1~AxZ*)le?l?X9cykem2E5J4H1+6%-U;p;k0ok=#0YM)&OR+{r$SwJx=rd?$Z< z&d@Xktw3j|v?6)&Wg$T^rFm(dj_MC0#GSvJk|%1r!0ni?*A?Sk)@us9wJySlz9ql- zt&xuzMeffUD$zKev_ow6C94$VYc=xvqouOmALi6s+%5dTeNM7l(9GTH4|icwA?zr! zR_1bjjcwKXzVmMnW0eXYu&%7NA5m3R&$D0u<&7uq8GxvNW#pT=$|CCCcv};>$S6WR zUuuP9KI_co4;Jim?{fp+;3wIm)}P(+)R$_J;b&*nOMTYI1>*D&Gn}A-jG5FRJ}rb6|rtq&&knNYje%@|?+fk|$~I#wk^j2#!4kd+k7E4%1CYGV(8zyZDZmg9AL0Xq?F7iX!$08A>Dl}fCIqa9a`m)G47IBb?>+_U^$rml^ zzg!wzaXf=0o8VEjw_40U2MeD!gD8RNsCh<2lX=aG+aOc#Yvs-*xu$L`FW=dEP2r9$ zr<^k7MBD3x-C3P6>+Ls1D&taEWXehSLX<70fUEaiJ-X0P;jh6qug!*fg^z@?s79Ax z;8E=la*6FruJ$Td5E6B}!R3;XB+3LwZhVvTay#aJz9u=?4b^@&qSJn4d-Pu87h|_d zME_4dZrCZ2ZHN7eBP}&aV6uq&b|33*?ajQES5t%TgMJ)05i{wVuS3)hbwwZooSl_!XNebO&J;WJ!9MtuvOFpoW>kyyn& z=R-Oi?W0ZkW$S+BuExrPOkp7gRIQiJR5$G*X-OTUYKP(K{V8)Dy@SKQFR3l}a%;D?3~oe-%RN( zx#w@Srl@0{Svy<0<_g_*8p+r+GdUNjz;`L=SOa81s6-8km_xL=5#KYkWOoabk~t+F z-6iexz)bXY6W#LySwb{t-05{MOWYYxlHzyznsFoAIKr_RYL`2{5O~MSZ(x|ZfUcLg zZidRYp&9xIu}I;ny@yELg@gYo1Ds9H3~~#Vp#jE@FEZD$GZA9^PFIQiUNJ;L0gaoH7BWzp4%m{P`1QSL>_#OE{sfSdao+ zS{FF`GL4*Q0dcqk6_d~UFy<;oWHXZ=j3}r19?MaNy2nquIAFxArb~qqA&^y#GAsS2nPMw)pQxOC;@O9(O(D(0q4e`%@AS5AHC* z;@(eRrc6J<{b<1@WImWKtu#=&=aBvo_al#7>tFHv3G`?Xo%`j=!_C^gmAu>SZ_`x0 z&S(+;x*DOpHP;{4$b0Db2LT~qdws{~KKZ+0Bx~?He2Ig+VPe>d#6Q0r{n_e+4xgxP zBQ+$%fHKk?g_tgNF`mVGSp5k={^4ZfOOx3bswve~-P zUf>;XXf57f-*R%aH)JRx;?<8qN*;(0aI~YPsXI@mDJ+$%g1V@M32S-=i%dOU zHWaPD;=V$?r_SR37CZf%k zBjE31cZojf{dBhW(gs5Z!g~zJe@}fS#nMs`S~2Xp2_?D1eiu8 zbbu6~@+Y{*8JJ@=S);40zsORBSA>M<3aiR90-}j=^M$RUPnZn;)ASv0{%@1a|D6Y! zagHA}(LAf|Y0s)f_(%g(jeDN0sj2DJ*o!i?Ur8vWFgz6mq3lZrVSQ5yh__I8_`m8< z#{cA1e+f}cAfkr7ZSm=oCkea0n6p7a1<|^fWx38}@ zs6*DfO%PiMMnG#=Bo5RNe&TofNt%P*bD4b2_ZGfQd2v~-p{#U$0nX9@*XnVTUo84O zRp&&*xP0BfLSTaIfNUbD^m5i;&vygiiK0v~zY}JEveR?gkp}p@Q-T8{>`s%1FmdlG z=snPbI{Tch_hrGu_3D%@H5=STv-gfOMelf~EtGaq=J|kXBSNj%UrJ0le4LqkABiNRB8s)cZ7= z3_;8o#VXMo+^dL+mO>2ZM^>FbVSQv|W{)2|yy)9)?_SNxfe*qcu(jupp@5;rg)7y9 z$u)Z0JIfd|zBteNNN6Zm4kw3bUEvX_?!jF3t2N&T{}#Zb7etwRjQ zXuKXEcPQLEpDBa}1Sqn@B}pV& ztiEzj^S00N$&}$>ck9rDmeGp2w1 z`bXPO=Y2ZGg!)8C^;(ZH;(%Z>;d}0$wf9K4;@}Xs-OInK_m ztYOr?F3FCaH0g5Jt4udM-DoT!VMa#3f_6&e@%~%FvG$na!<9Ol`-d6PhF=bvK)*=X zRVY@qYlm=&$j4;LGBVNp0K_%xC3EYll+c5#g#blvw{qj>s<ZeRdmy~sv+8a9i^K2b~#8BWGH0`rzffxmQC`iIH^7<$p zntMUJtKMZBpIjc=phJV}xGa6SF5x%)iD}`DAy)$H^=Q zzgyKTI!>7^Cicb5A3UOC6Y!(H8F5n*A@Vte6`)?kSPbf$$()uds>>$9#}?<-TnJL} zW5yFziqo47XQ34l9IRmw$FpmWga)xIKdr3KH7KYknvlS z&S-+3>WrfQ*1!2vf^*_9kAn$6Bn*)WQwU_%3IbQl{H=mbCQ5j&1o>K}qM5atZEk!` zlt^@b9U>4`yzLLc{v8;oW3jnJrkunc{%aq=Gg_Ly!x&_B;ls*p^3~a_C4tDil9Eu0 z?73{5pEFE!rW7?fKknKd68w8MU)Oc#6OgHY*guC~zjdJ;Mx@*)H05XL?+l1M?s7Sr zoQ~sO?|yU8>U8)lNUPTxD^RLrK^a^nsl6Z=m#?>3S^m)e(23B0T?i3-|8fNf%^VoZ zy43T=wF#Es&{B-#YyVqgKkcJ*Jz9J;%&cWyjW6+5xd0|@l2y`+Eg_m|1 zV7EEdYUS>=VAq1eZ}9muJ)@kzTlUlGlZ?l4>v3+P2~ySE(MAn#6FBu9t&HD=8mjW0 z0TT4JWjxb+kOf>B5)$Sfca1NE8cv$BZC1f**um=iGUXjS$*=ggP%2ccE=OZhGm1lw z!Tb6YT{ZbtdL>0AweQ^L5t^xOwT=LS_bBfvQtckWIB%+Pgyj1MBuq}xLBOLN{w>HA zscOE{{zgZ7QH05989r>LSZmpfG2OvepJGD-Qv@1Ks){!J;!2Z42LI0MZc$X=UDEe5 zBU~!nkJ0`T-i;!zE5PVqHbPJSBlObZ-b`hoK!HT>3#q4SbCch7N^#w3tpHS=^u0dA z3XFDvtSuoMOoUJ79*UU9r{6m;0B%P)CJju$;)Q9-0}i0Pf9Li9~ zvv|b<-s-`|DKGwG#1Cht~s_ZzYo)GS)UwT!g z%9M|huhcbJ-uB;>Q-zF?mL}@`)~yUKt~{MdRipVaI%a}6BEUoHPx^kg_&L$f%pS7tzpW}|3AeNIFX+&=tz z#*b@6u!_{`ptmf=W_+4_0R7C~ zzIyv&CS{w0Fg2Nch4`4MtgVTF!>j9jiY+gLlCIH=1RQ#|ADhj=F;MPKB9gl3(FNN0 z9SK;uNR+&mOylZ&4pzXC8{EF{pUwKio>-S`Ngvs;CqXwQh2i9jV#gc#GU?o!9?Ra5 z?9>H-GpUBUIu=DJhrg=y@>`Y*AA@)GU5I``WO~s482vPxuZ?_|PvaY`WtR%p{rpwN zxk3wIHO?;8y<}JJIr=P$nRG8p4E@s#E$uu-CBZRv#Z?1J&~qgNEu02W#_6T^CgU$( zrSti4detZrxT!(z&Aw56a+Hrv?(6hYx3F5X zII+F1!Og{L9&hLapI1^k!M~$%qf<-fq2OUf#W}Wo;BXSOJC#MoeyXDghQ^1+kTqS4J z*UO=Vq!72GstheVjCUvRM6J`VTK%O8A8X@`H24_#a0Pv&2GERvq-aET)-h!ei8(#Y zI8lCgZ$9>ynC+K;&W0zc{imQ;saB65B<)13DX+}kEU%Hn9aP*`lsb3=Q`!kgL$f2E*u>0J7cqXb@qnMT{tJsz;Rb=(r*N#-`Q)a9Phx~}`@eTgYqG zns|qxkz))qgtOE<9lCSu)B1Q`TAOl5`)31hWw!aFqUG-T@phle*L@btATpw->hxNH zrEtEYd{#8q5@P@c+)*S^%QMqIzy%OPLtKdeMW!}>aig}IBLz(b>ZogWRdqr-3m@N6 o`S|#7OgVtwGe_wg2f!v768{(+nX3Ig2vB~8dd9k?*BxK|A7M*GssI20 From 3b8f8018205c3a644bbe1c30bb87f0d2192c309b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:34:59 +0000 Subject: [PATCH 030/155] Automatic changelog for PR #95409 [ci skip] --- html/changelogs/AutoChangeLog-pr-95409.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95409.yml diff --git a/html/changelogs/AutoChangeLog-pr-95409.yml b/html/changelogs/AutoChangeLog-pr-95409.yml new file mode 100644 index 000000000000..ac67154e7207 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95409.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "Removes white pixel from airlock EA animation" + - bugfix: "Removes white pixel from firelock open animation" \ No newline at end of file From b9ad9343e56bcba6b50bc247e5c88d1c52c7b85e Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:00:24 +0000 Subject: [PATCH 031/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95409.yml | 5 ----- html/changelogs/archive/2026-03.yml | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95409.yml diff --git a/html/changelogs/AutoChangeLog-pr-95409.yml b/html/changelogs/AutoChangeLog-pr-95409.yml deleted file mode 100644 index ac67154e7207..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95409.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Removes white pixel from airlock EA animation" - - bugfix: "Removes white pixel from firelock open animation" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 28cb121b9b27..6433aa9ba07e 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -224,6 +224,8 @@ - rscadd: Booze on the other hand will make a monkey even angrier (though only if they are already upset with someone) - bugfix: Pun Pun is Pun Pun again + - bugfix: Removes white pixel from airlock EA animation + - bugfix: Removes white pixel from firelock open animation SmArtKar: - refactor: Converted vehicles to use item_interaction/tool_acts - refactor: Rewrote dispenser bots to use item_interaction From c9cb4f1c7081fa3051c4787bd2ce41da0aae8d42 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:26:19 +0100 Subject: [PATCH 032/155] Converts tram objects to item_interaction/tool_act (#95400) ## About The Pull Request Throws more attackby()s out of the window ## Changelog :cl: refactor: Converted tram objects to item_interaction/tool_act /:cl: --- .../modules/transport/tram/tram_controller.dm | 19 ++++---- code/modules/transport/tram/tram_floors.dm | 47 +++++++++++-------- code/modules/transport/tram/tram_signals.dm | 32 ++++++------- .../modules/transport/tram/tram_structures.dm | 20 ++++---- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/code/modules/transport/tram/tram_controller.dm b/code/modules/transport/tram/tram_controller.dm index 8cbcfeddd3ad..4194493f27d9 100644 --- a/code/modules/transport/tram/tram_controller.dm +++ b/code/modules/transport/tram/tram_controller.dm @@ -853,17 +853,14 @@ . += span_notice("The cabinet can be opened with a [EXAMINE_HINT("Left-click.")]") -/obj/machinery/transport/tram_controller/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers) - if(user.combat_mode || cover_open) - return ..() - - if(has_cover) - var/obj/item/card/id/id_card = user.get_id_in_hand() - if(!isnull(id_card)) - try_toggle_lock(user, id_card) - return - - return ..() +/obj/machinery/transport/tram_controller/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(user.combat_mode || !has_cover) + return NONE + if(!istype(tool, /obj/item/card/id)) + return NONE + if(cover_open) + return ITEM_INTERACT_BLOCKING + return try_toggle_lock(user, tool) ? ITEM_INTERACT_SUCCESS : ITEM_INTERACT_BLOCKING /obj/machinery/transport/tram_controller/attack_hand(mob/living/user, params) . = ..() diff --git a/code/modules/transport/tram/tram_floors.dm b/code/modules/transport/tram/tram_floors.dm index dd091e4e3ae5..0d3f352acde4 100644 --- a/code/modules/transport/tram/tram_floors.dm +++ b/code/modules/transport/tram/tram_floors.dm @@ -22,12 +22,14 @@ . += ..() . += span_notice("The reinforcement bolts are [EXAMINE_HINT("wrenched")] firmly in place. Use a [EXAMINE_HINT("wrench")] to remove the plate.") -/turf/open/floor/tram/attackby(obj/item/object, mob/living/user, list/modifiers) - . = ..() - if(istype(object, /obj/item/stack/thermoplastic)) - build_with_transport_tiles(object, user) - else if(istype(object, /obj/item/stack/sheet/mineral/titanium)) - build_with_titanium(object, user) +/turf/open/floor/tram/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, /obj/item/stack/thermoplastic)) + build_with_transport_tiles(tool, user) + return ITEM_INTERACT_SUCCESS + if(istype(tool, /obj/item/stack/sheet/mineral/titanium)) + build_with_titanium(tool, user) + return ITEM_INTERACT_SUCCESS + return NONE /turf/open/floor/tram/make_plating(force = FALSE) if(force) @@ -126,14 +128,17 @@ /turf/open/floor/tram/plate/energized/burnt_states() return list("energized_plate_damaged") -/turf/open/floor/tram/plate/energized/attackby(obj/item/attacking_item, mob/living/user, list/modifiers) - if((broken || burnt) && istype(attacking_item, /obj/item/stack/sheet/mineral/titanium)) - if(attacking_item.use(1)) - broken = FALSE - update_appearance() - balloon_alert(user, "plate replaced") - return - return ..() +/turf/open/floor/tram/plate/energized/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!broken && !burnt) + return NONE + if(!istype(tool, /obj/item/stack/sheet/mineral/titanium)) + return NONE + if(!tool.use(1)) + return ITEM_INTERACT_BLOCKING + broken = FALSE + update_appearance() + balloon_alert(user, "plate replaced") + return ITEM_INTERACT_SUCCESS /turf/open/floor/tram/plate/energized/broken broken = TRUE @@ -149,12 +154,14 @@ clawfootstep = FOOTSTEP_HARD_CLAW heavyfootstep = FOOTSTEP_GENERIC_HEAVY -/turf/open/indestructible/tram/attackby(obj/item/object, mob/living/user, list/modifiers) - . = ..() - if(istype(object, /obj/item/stack/thermoplastic)) - build_with_transport_tiles(object, user) - else if(istype(object, /obj/item/stack/sheet/mineral/titanium)) - build_with_titanium(object, user) +/turf/open/indestructible/tram/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, /obj/item/stack/thermoplastic)) + build_with_transport_tiles(tool, user) + return ITEM_INTERACT_SUCCESS + if(istype(tool, /obj/item/stack/sheet/mineral/titanium)) + build_with_titanium(tool, user) + return ITEM_INTERACT_SUCCESS + return NONE /turf/open/indestructible/tram/plate name = "linear induction plate" diff --git a/code/modules/transport/tram/tram_signals.dm b/code/modules/transport/tram/tram_signals.dm index e67a7a25c545..701d20b740ee 100644 --- a/code/modules/transport/tram/tram_signals.dm +++ b/code/modules/transport/tram/tram_signals.dm @@ -117,15 +117,15 @@ SStransport.crossing_signals -= src . = ..() -/obj/machinery/transport/crossing_signal/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers) - if(!user.combat_mode) - if(default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) - return - - if(default_deconstruction_crowbar(weapon)) - return +/obj/machinery/transport/crossing_signal/screwdriver_act(mob/living/user, obj/item/tool) + if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool)) + return ITEM_INTERACT_SUCCESS + return NONE - return ..() +/obj/machinery/transport/crossing_signal/crowbar_act(mob/living/user, obj/item/tool) + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + return NONE /obj/machinery/transport/crossing_signal/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() @@ -516,15 +516,15 @@ . += span_notice("The red [EXAMINE_HINT("local fault")] light is on.") . += span_notice("The status display reads: Repair required.") -/obj/machinery/transport/guideway_sensor/attackby(obj/item/weapon, mob/living/user, list/modifiers, list/attack_modifiers) - if (!user.combat_mode) - if(default_deconstruction_screwdriver(user, icon_state, icon_state, weapon)) - return - - if(default_deconstruction_crowbar(weapon)) - return +/obj/machinery/transport/guideway_sensor/screwdriver_act(mob/living/user, obj/item/tool) + if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool)) + return ITEM_INTERACT_SUCCESS + return NONE - return ..() +/obj/machinery/transport/guideway_sensor/crowbar_act(mob/living/user, obj/item/tool) + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + return NONE /obj/machinery/transport/guideway_sensor/proc/pair_sensor() set_machine_stat(machine_stat | MAINT) diff --git a/code/modules/transport/tram/tram_structures.dm b/code/modules/transport/tram/tram_structures.dm index 70b2052b790a..70dcf92d7487 100644 --- a/code/modules/transport/tram/tram_structures.dm +++ b/code/modules/transport/tram/tram_structures.dm @@ -388,15 +388,17 @@ canSmoothWith = SMOOTH_GROUP_WOOD_WALLS custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT*2) -/obj/structure/tram/alt/wood/attackby(obj/item/W, mob/user) - if(W.get_sharpness() && W.force) - var/duration = ((4.8 SECONDS) / W.force) * 2 //In seconds, for now. - if(istype(W, /obj/item/hatchet) || istype(W, /obj/item/fireaxe)) - duration /= 4 //Much better with hatchets and axes. - if(do_after(user, duration * (1 SECONDS), target=src)) //Into deciseconds. - deconstruct(disassembled = FALSE) - return - return ..() +/obj/structure/tram/alt/wood/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!tool.get_sharpness() || !tool.force) + return NONE + var/duration = ((4.8 SECONDS) / tool.force) * 2 //In seconds, for now. + if(istype(tool, /obj/item/hatchet) || istype(tool, /obj/item/fireaxe)) + duration /= 4 //Much better with hatchets and axes. + to_chat(user, span_notice("You begin breaking down [src].")) + if(!do_after(user, duration * (1 SECONDS), target=src)) //Into deciseconds. + return ITEM_INTERACT_BLOCKING + deconstruct(disassembled = FALSE) + return ITEM_INTERACT_SUCCESS /obj/structure/tram/alt/bamboo name = "bamboo tram" From 5a5e3a3e1be3e7ea584caf5fa17c50b3566565e5 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:26:40 +0000 Subject: [PATCH 033/155] Automatic changelog for PR #95400 [ci skip] --- html/changelogs/AutoChangeLog-pr-95400.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95400.yml diff --git a/html/changelogs/AutoChangeLog-pr-95400.yml b/html/changelogs/AutoChangeLog-pr-95400.yml new file mode 100644 index 000000000000..9e82e7c62ee5 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95400.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted tram objects to item_interaction/tool_act" \ No newline at end of file From 8a2b6f937b495b903ccfd8d46a6fc1347ab9149e Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:34:28 +0100 Subject: [PATCH 034/155] Converts autosurgeons/robot bodyparts/dissection notes to item_interaction (#95401) ## About The Pull Request I'm just going by categories and bundling up whichever ones are relevant, more attackby()s gone ## Changelog :cl: refactor: Converted autosurgeons/robot bodyparts/dissection notes to item_interaction /:cl: --- .../surgery/bodyparts/robot_bodyparts.dm | 80 ++++++++++--------- .../operations/operation_dissection.dm | 16 ++-- code/modules/surgery/organs/autosurgeon.dm | 10 +-- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/code/modules/surgery/bodyparts/robot_bodyparts.dm b/code/modules/surgery/bodyparts/robot_bodyparts.dm index 5548d0634610..4d17428fc9b7 100644 --- a/code/modules/surgery/bodyparts/robot_bodyparts.dm +++ b/code/modules/surgery/bodyparts/robot_bodyparts.dm @@ -295,28 +295,29 @@ TRAIT_RESISTHIGHPRESSURE, ), AUGMENTATION_TRAIT) -/obj/item/bodypart/chest/robot/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) - if(istype(weapon, /obj/item/stock_parts/power_store/cell)) +/obj/item/bodypart/chest/robot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(istype(tool, /obj/item/stock_parts/power_store/cell)) if(cell) - to_chat(user, span_warning("You have already inserted a cell!")) - return - else - if(!user.transferItemToLoc(weapon, src)) - return - cell = weapon - to_chat(user, span_notice("You insert the cell.")) - else if(istype(weapon, /obj/item/stack/cable_coil)) + to_chat(user, span_warning("A cell is already present in [src]!")) + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) + return ITEM_INTERACT_BLOCKING + cell = tool + to_chat(user, span_notice("You insert [cell] into [src].")) + return ITEM_INTERACT_SUCCESS + + if(istype(tool, /obj/item/stack/cable_coil)) if(wired) - to_chat(user, span_warning("You have already inserted wire!")) - return - var/obj/item/stack/cable_coil/coil = weapon - if (coil.use(1)) - wired = TRUE - to_chat(user, span_notice("You insert the wire.")) - else + to_chat(user, span_warning("[src] is already wired up!")) + return ITEM_INTERACT_BLOCKING + var/obj/item/stack/cable_coil/coil = tool + if (!coil.use(1)) to_chat(user, span_warning("You need one length of coil to wire it!")) - else - return ..() + return ITEM_INTERACT_BLOCKING + wired = TRUE + to_chat(user, span_notice("You wire the cell inside of [src].")) + return ITEM_INTERACT_SUCCESS + return NONE /obj/item/bodypart/chest/robot/wirecutter_act(mob/living/user, obj/item/cutter) . = ..() @@ -440,25 +441,28 @@ . += "It has two eye sockets occupied by flashes." . += span_notice("You can remove the seated flash[single_flash ? "":"es"] with a crowbar.") -/obj/item/bodypart/head/robot/attackby(obj/item/weapon, mob/user, list/modifiers, list/attack_modifiers) - if(istype(weapon, /obj/item/assembly/flash/handheld)) - var/obj/item/assembly/flash/handheld/flash = weapon - if(flash1 && flash2) - to_chat(user, span_warning("You have already inserted the eyes!")) - return - else if(flash.burnt_out) - to_chat(user, span_warning("You can't use a broken flash!")) - return - else - if(!user.transferItemToLoc(flash, src)) - return - if(flash1) - flash2 = flash - else - flash1 = flash - to_chat(user, span_notice("You insert the flash into the eye socket.")) - return - return ..() +/obj/item/bodypart/head/robot/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/assembly/flash/handheld)) + return NONE + + var/obj/item/assembly/flash/handheld/flash = tool + if(flash1 && flash2) + to_chat(user, span_warning("[src] already has both flash-eyes present!")) + return ITEM_INTERACT_BLOCKING + + if(flash.burnt_out) + to_chat(user, span_warning("You can't use a broken flash!")) + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(flash, src)) + return ITEM_INTERACT_BLOCKING + + if(flash1) + flash2 = flash + else + flash1 = flash + to_chat(user, span_notice("You insert the flash into the eye socket.")) + return ITEM_INTERACT_SUCCESS /obj/item/bodypart/head/robot/crowbar_act(mob/living/user, obj/item/prytool) ..() diff --git a/code/modules/surgery/operations/operation_dissection.dm b/code/modules/surgery/operations/operation_dissection.dm index 1756d536e023..74c119edd635 100644 --- a/code/modules/surgery/operations/operation_dissection.dm +++ b/code/modules/surgery/operations/operation_dissection.dm @@ -115,14 +115,14 @@ . = ..() . += span_notice("It is worth [value] research points.") -/obj/item/research_notes/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) - if(istype(attacking_item, /obj/item/research_notes)) - var/obj/item/research_notes/notes = attacking_item - value = value + notes.value - change_vol() - qdel(notes) - return - return ..() +/obj/item/research_notes/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/research_notes)) + return NONE + var/obj/item/research_notes/notes = tool + value = value + notes.value + change_vol() + qdel(notes) + return ITEM_INTERACT_SUCCESS /// proc that changes name and icon depending on value /obj/item/research_notes/proc/change_vol() diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm index 388b25da1254..d75f7fd4ec9b 100644 --- a/code/modules/surgery/organs/autosurgeon.dm +++ b/code/modules/surgery/organs/autosurgeon.dm @@ -122,11 +122,11 @@ add_fingerprint(user) use_autosurgeon(target, user, 8 SECONDS) -/obj/item/autosurgeon/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers) - if(isorgan(attacking_item)) - load_organ(attacking_item, user) - else - return ..() +/obj/item/autosurgeon/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(isorgan(tool)) + load_organ(tool, user) + return ITEM_INTERACT_SUCCESS + return NONE /obj/item/autosurgeon/screwdriver_act(mob/living/user, obj/item/screwtool) if(..()) From e4580464c0a1104de9adfad79dcb0def7d559709 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:34:48 +0000 Subject: [PATCH 035/155] Automatic changelog for PR #95401 [ci skip] --- html/changelogs/AutoChangeLog-pr-95401.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95401.yml diff --git a/html/changelogs/AutoChangeLog-pr-95401.yml b/html/changelogs/AutoChangeLog-pr-95401.yml new file mode 100644 index 000000000000..e9feca79a416 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95401.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted autosurgeons/robot bodyparts/dissection notes to item_interaction" \ No newline at end of file From d5c10f61f8a0b5ecefc4cc96af45e3fc5d8e1527 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:43:38 +0100 Subject: [PATCH 036/155] Converts crossbreeds/anomacores/RND machinery to item_interaction (#95402) ## About The Pull Request Converts anomaly refineries, tank compressors, doppler arrays, anomacores, slime extracts and crossbreeds from attackby() to item_interaction. ## Changelog :cl: refactor: Converted crossbreeds/anomacores/RND machinery to item_interaction /:cl: --- code/modules/research/anomaly/anomaly_core.dm | 7 +-- .../research/anomaly/anomaly_refinery.dm | 54 ++++++++++-------- .../research/ordnance/doppler_array.dm | 20 +++---- .../research/ordnance/tank_compressor.dm | 50 ++++++++-------- code/modules/research/rdconsole.dm | 57 +++++++++++-------- code/modules/research/server.dm | 52 ++++++++--------- .../xenobiology/crossbreeding/consuming.dm | 54 ++++++++++-------- .../xenobiology/crossbreeding/reproductive.dm | 47 ++++++++------- .../xenobiology/crossbreeding/stabilized.dm | 21 ++++--- .../research/xenobiology/xenobiology.dm | 27 ++++----- 10 files changed, 207 insertions(+), 182 deletions(-) diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm index 490464319428..9e6318158037 100644 --- a/code/modules/research/anomaly/anomaly_core.dm +++ b/code/modules/research/anomaly/anomaly_core.dm @@ -31,10 +31,9 @@ /obj/item/assembly/signaler/anomaly/attack_self() return -/obj/item/assembly/signaler/anomaly/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(I.tool_behaviour == TOOL_ANALYZER) - to_chat(user, span_notice("Analyzing... [src]'s stabilized field is fluctuating along frequency [format_frequency(frequency)], code [code].")) - return ..() +/obj/item/assembly/signaler/anomaly/analyzer_act(mob/living/user, obj/item/analyzer/tool) + to_chat(user, span_notice("Analyzing... [src]'s stabilized field is fluctuating along frequency [format_frequency(frequency)], code [code].")) + return ITEM_INTERACT_SUCCESS /obj/item/assembly/signaler/anomaly/on_mail_unwrap(atom/source, mob/user, obj/item/mail/traitor/letter) return NONE diff --git a/code/modules/research/anomaly/anomaly_refinery.dm b/code/modules/research/anomaly/anomaly_refinery.dm index 7ec62a618d9b..cfbc9a840117 100644 --- a/code/modules/research/anomaly/anomaly_refinery.dm +++ b/code/modules/research/anomaly/anomaly_refinery.dm @@ -73,41 +73,49 @@ var/radius = clamp(round(MIN_RADIUS_REQUIRED + radius_increase_per_core * already_made, 1), MIN_RADIUS_REQUIRED, MAX_RADIUS_REQUIRED) return radius -/obj/machinery/research/anomaly_refinery/attackby(obj/item/tool, mob/living/user, list/modifiers, list/attack_modifiers) +/obj/machinery/research/anomaly_refinery/item_interaction(mob/living/user, obj/item/tool, list/modifiers) if(active) to_chat(user, span_warning("You can't insert [tool] into [src] while [p_theyre()] currently active.")) - return + return ITEM_INTERACT_BLOCKING + if(istype(tool, /obj/item/raw_anomaly_core)) if(inserted_core) to_chat(user, span_warning("There is already a core in [src].")) - return + return ITEM_INTERACT_BLOCKING + if(!user.transferItemToLoc(tool, src)) to_chat(user, span_warning("[tool] is stuck to your hand.")) - return + return ITEM_INTERACT_BLOCKING + var/obj/item/raw_anomaly_core/raw_core = tool if(!get_required_radius(raw_core.anomaly_type)) say("Unfortunately, due to diminishing supplies of condensed anomalous matter, [raw_core] and any cores of its type are no longer of a sufficient quality level to be compressed into a working core.") - return + return ITEM_INTERACT_BLOCKING + inserted_core = raw_core to_chat(user, span_notice("You insert [raw_core] into [src].")) - return - if(istype(tool, /obj/item/transfer_valve)) - if(inserted_bomb) - to_chat(user, span_warning("There is already a bomb in [src].")) - return - var/obj/item/transfer_valve/valve = tool - if(!valve.ready()) - to_chat(user, span_warning("[valve] is incomplete.")) - return - if(!user.transferItemToLoc(tool, src)) - to_chat(user, span_warning("[tool] is stuck to your hand.")) - return - inserted_bomb = tool - tank_to_target = inserted_bomb.tank_two - to_chat(user, span_notice("You insert [tool] into [src]")) - return - update_appearance() - return ..() + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/transfer_valve)) + return NONE + + if(inserted_bomb) + to_chat(user, span_warning("There is already a bomb in [src].")) + return ITEM_INTERACT_BLOCKING + + var/obj/item/transfer_valve/valve = tool + if(!valve.ready()) + to_chat(user, span_warning("[valve] is incomplete.")) + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(tool, src)) + to_chat(user, span_warning("[tool] is stuck to your hand.")) + return ITEM_INTERACT_BLOCKING + + inserted_bomb = tool + tank_to_target = inserted_bomb.tank_two + to_chat(user, span_notice("You insert [tool] into [src]")) + return ITEM_INTERACT_SUCCESS /obj/machinery/research/anomaly_refinery/wrench_act(mob/living/user, obj/item/tool) . = ..() diff --git a/code/modules/research/ordnance/doppler_array.dm b/code/modules/research/ordnance/doppler_array.dm index c516cd38f6a3..6fadc0393a53 100644 --- a/code/modules/research/ordnance/doppler_array.dm +++ b/code/modules/research/ordnance/doppler_array.dm @@ -48,17 +48,15 @@ . = ..() . += span_notice("It is currently facing [dir2text(dir)]") -/obj/machinery/doppler_array/attackby(obj/item/item, mob/user, list/modifiers, list/attack_modifiers) - if(istype(item, /obj/item/disk/computer)) - var/obj/item/disk/computer/disk = item - eject_disk(user) - if(user.transferItemToLoc(disk, src)) - inserted_disk = disk - return - else - balloon_alert(user, "it's stuck to your hand!") - return ..() - return ..() +/obj/machinery/doppler_array/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/disk/computer)) + return NONE + eject_disk(user) + if(!user.transferItemToLoc(tool, src)) + balloon_alert(user, "it's stuck to your hand!") + return ITEM_INTERACT_BLOCKING + inserted_disk = tool + return ITEM_INTERACT_SUCCESS /obj/machinery/doppler_array/wrench_act(mob/living/user, obj/item/tool) default_unfasten_wrench(user, tool) diff --git a/code/modules/research/ordnance/tank_compressor.dm b/code/modules/research/ordnance/tank_compressor.dm index 189ab23a3a0c..c34d40e22231 100644 --- a/code/modules/research/ordnance/tank_compressor.dm +++ b/code/modules/research/ordnance/tank_compressor.dm @@ -42,33 +42,35 @@ var/list/gas_data = list() var/timestamp -/obj/machinery/atmospherics/components/binary/tank_compressor/attackby(obj/item/item, mob/living/user) - if (panel_open) - return ..() +/obj/machinery/atmospherics/components/binary/tank_compressor/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if (user.combat_mode || panel_open) + return NONE - if(istype(item, /obj/item/tank)) - var/obj/item/tank/tank_item = item - if(inserted_tank) - if(!eject_tank(user)) - balloon_alert(user, "it's stuck inside!") - return ..() - if(!user.transferItemToLoc(tank_item, src)) - balloon_alert(user, "it's stuck to your hand!") - return ..() - inserted_tank = tank_item - last_recorded_pressure = 0 - RegisterSignal(inserted_tank, COMSIG_QDELETING, PROC_REF(tank_destruction)) - update_appearance() - return - if(istype(item, /obj/item/disk/computer)) - var/obj/item/disk/computer/attacking_disk = item + if(istype(tool, /obj/item/disk/computer)) eject_disk(user) - if(user.transferItemToLoc(attacking_disk, src)) - inserted_disk = attacking_disk - else + if(user.transferItemToLoc(tool, src)) balloon_alert(user, "it's stuck to your hand!") - return - return ..() + return ITEM_INTERACT_BLOCKING + inserted_disk = tool + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/tank)) + return NONE + + if(inserted_tank) + if(!eject_tank(user)) + balloon_alert(user, "it's stuck inside!") + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(tool, src)) + balloon_alert(user, "it's stuck to your hand!") + return ITEM_INTERACT_BLOCKING + + inserted_tank = tool + last_recorded_pressure = 0 + RegisterSignal(inserted_tank, COMSIG_QDELETING, PROC_REF(tank_destruction)) + update_appearance() + return ITEM_INTERACT_SUCCESS /obj/machinery/atmospherics/components/binary/tank_compressor/wrench_act(mob/living/user, obj/item/tool) if(active || inserted_tank) diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm index 32793bfe8ab4..36c3a43a6034 100644 --- a/code/modules/research/rdconsole.dm +++ b/code/modules/research/rdconsole.dm @@ -70,31 +70,38 @@ Nothing else in the console has ID requirements. d_disk = null return ..() -/obj/machinery/computer/rdconsole/attackby(obj/item/D, mob/user, list/modifiers, list/attack_modifiers) - //Loading a disk into it. - if(istype(D, /obj/item/disk)) - if(istype(D, /obj/item/disk/tech_disk)) - if(t_disk) - to_chat(user, span_warning("A technology disk is already loaded!")) - return - if(!user.transferItemToLoc(D, src)) - to_chat(user, span_warning("[D] is stuck to your hand!")) - return - t_disk = D - else if (istype(D, /obj/item/disk/design_disk)) - if(d_disk) - to_chat(user, span_warning("A design disk is already loaded!")) - return - if(!user.transferItemToLoc(D, src)) - to_chat(user, span_warning("[D] is stuck to your hand!")) - return - d_disk = D - else - to_chat(user, span_warning("Machine cannot accept disks in that format.")) - return - to_chat(user, span_notice("You insert [D] into \the [src]!")) - return - return ..() +/obj/machinery/computer/rdconsole/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/disk)) + return NONE + + if(istype(tool, /obj/item/disk/tech_disk)) + if(t_disk) + to_chat(user, span_warning("A technology disk is already loaded!")) + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(tool, src)) + to_chat(user, span_warning("[tool] is stuck to your hand!")) + return ITEM_INTERACT_BLOCKING + + t_disk = tool + to_chat(user, span_notice("You insert [tool] into \the [src]!")) + return ITEM_INTERACT_SUCCESS + + if (!istype(tool, /obj/item/disk/design_disk)) + to_chat(user, span_warning("Machine cannot accept disks in that format.")) + return ITEM_INTERACT_BLOCKING + + if(d_disk) + to_chat(user, span_warning("A design disk is already loaded!")) + return ITEM_INTERACT_BLOCKING + + if(!user.transferItemToLoc(tool, src)) + to_chat(user, span_warning("[tool] is stuck to your hand!")) + return ITEM_INTERACT_BLOCKING + + d_disk = tool + to_chat(user, span_notice("You insert [tool] into \the [src]!")) + return ITEM_INTERACT_SUCCESS /obj/machinery/computer/rdconsole/multitool_act(mob/living/user, obj/item/multitool/tool) . = ..() diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm index 6b900f0e83b5..628e0fcfe4af 100644 --- a/code/modules/research/server.dm +++ b/code/modules/research/server.dm @@ -165,41 +165,37 @@ return ITEM_INTERACT_BLOCKING return ..() -/obj/machinery/rnd/server/master/attackby(obj/item/attacking_item, mob/user, list/modifiers, list/attack_modifiers) - if(istype(attacking_item, /obj/item/disk/computer/hdd_theft)) - switch(deconstruction_state) - if(HDD_PANEL_CLOSED) - balloon_alert(user, "you can't find a place to insert it!") - return TRUE - if(HDD_PANEL_OPEN) - balloon_alert(user, "you weren't trained to install this!") - return TRUE - if(HDD_PRIED) - balloon_alert(user, "the HDD housing is completely broken, it won't fit!") - return TRUE - if(HDD_CUT_LOOSE) - balloon_alert(user, "the HDD housing is completely broken and all the wires are cut!") - return TRUE - if(HDD_OVERLOADED) - balloon_alert(user, "the inside is scorched and all the wires are burned!") - return TRUE - return ..() +/obj/machinery/rnd/server/master/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/disk/computer/hdd_theft)) + return NONE + switch(deconstruction_state) + if(HDD_PANEL_CLOSED) + balloon_alert(user, "you can't find a place to insert it!") + if(HDD_PANEL_OPEN) + balloon_alert(user, "you weren't trained to install this!") + if(HDD_PRIED) + balloon_alert(user, "the HDD housing is completely broken, it won't fit!") + if(HDD_CUT_LOOSE) + balloon_alert(user, "the HDD housing is completely broken and all the wires are cut!") + if(HDD_OVERLOADED) + balloon_alert(user, "the inside is scorched and all the wires are burned!") + return ITEM_INTERACT_BLOCKING /obj/machinery/rnd/server/master/screwdriver_act(mob/living/user, obj/item/tool) if(deconstruction_state != HDD_PANEL_CLOSED || user.combat_mode) - return FALSE + return NONE to_chat(user, span_notice("You can see [front_panel_screws] screw\s. You start unscrewing [front_panel_screws == 1 ? "it" : "them"]...")) while(tool.use_tool(src, user, 7.5 SECONDS, volume=100)) front_panel_screws-- - - if(front_panel_screws <= 0) - deconstruction_state = HDD_PANEL_OPEN - to_chat(user, span_notice("You remove the last screw from [src]'s front panel.")) - add_overlay("RD-server-hdd-panel-open") - return TRUE - to_chat(user, span_notice("The screw breaks as you remove it. Only [front_panel_screws] left...")) - return TRUE + if(front_panel_screws > 0) + to_chat(user, span_notice("The screw breaks as you remove it. Only [front_panel_screws] left...")) + continue + deconstruction_state = HDD_PANEL_OPEN + to_chat(user, span_notice("You remove the last screw from [src]'s front panel.")) + add_overlay("RD-server-hdd-panel-open") + break + return ITEM_INTERACT_SUCCESS /obj/machinery/rnd/server/master/crowbar_act(mob/living/user, obj/item/tool) if(deconstruction_state != HDD_PANEL_OPEN || user.combat_mode) diff --git a/code/modules/research/xenobiology/crossbreeding/consuming.dm b/code/modules/research/xenobiology/crossbreeding/consuming.dm index 22c645ace717..82cf2018759e 100644 --- a/code/modules/research/xenobiology/crossbreeding/consuming.dm +++ b/code/modules/research/xenobiology/crossbreeding/consuming.dm @@ -15,30 +15,36 @@ Consuming extracts: var/cookies = 5 //Number of cookies to spawn var/cookietype = /obj/item/slime_cookie -/obj/item/slimecross/consuming/attackby(obj/item/O, mob/user) - if(IS_EDIBLE(O)) - if(last_produced + cooldown > world.time) - to_chat(user, span_warning("[src] is still digesting after its last meal!")) - return - var/datum/reagent/N = O.reagents.has_reagent(/datum/reagent/consumable/nutriment) - if(N) - nutriment_eaten += N.volume - to_chat(user, span_notice("[src] opens up and swallows [O] whole!")) - qdel(O) - playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) - else - to_chat(user, span_warning("[src] burbles unhappily at the offering.")) - if(nutriment_eaten >= nutriment_required) - nutriment_eaten = 0 - user.visible_message(span_notice("[src] swells up and produces a small pile of cookies!")) - playsound(src, 'sound/effects/splat.ogg', 40, TRUE) - last_produced = world.time - for(var/i in 1 to cookies) - var/obj/item/S = spawncookie() - S.pixel_x = base_pixel_x + rand(-5, 5) - S.pixel_y = base_pixel_y + rand(-5, 5) - return - ..() +/obj/item/slimecross/consuming/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!IS_EDIBLE(tool)) + return NONE + + if(last_produced + cooldown > world.time) + to_chat(user, span_warning("[src] is still digesting after its last meal!")) + return ITEM_INTERACT_BLOCKING + + var/datum/reagent/nutriments = tool.reagents.has_reagent(/datum/reagent/consumable/nutriment) + if(!nutriments) + to_chat(user, span_warning("[src] burbles unhappily at the offering.")) + return ITEM_INTERACT_BLOCKING + + nutriment_eaten += nutriments.volume + to_chat(user, span_notice("[src] opens up and swallows [tool] whole!")) + qdel(tool) + playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) + + if(nutriment_eaten < nutriment_required) + return ITEM_INTERACT_SUCCESS + + nutriment_eaten = 0 + user.visible_message(span_notice("[src] swells up and produces a small pile of cookies!")) + playsound(src, 'sound/effects/splat.ogg', 40, TRUE) + last_produced = world.time + for(var/i in 1 to cookies) + var/obj/item/cookie = spawncookie() + cookie.pixel_x = base_pixel_x + rand(-5, 5) + cookie.pixel_y = base_pixel_y + rand(-5, 5) + return ITEM_INTERACT_SUCCESS /obj/item/slimecross/consuming/proc/spawncookie() return new cookietype(get_turf(src)) diff --git a/code/modules/research/xenobiology/crossbreeding/reproductive.dm b/code/modules/research/xenobiology/crossbreeding/reproductive.dm index a0888c1c2f86..b1bad9bf7a77 100644 --- a/code/modules/research/xenobiology/crossbreeding/reproductive.dm +++ b/code/modules/research/xenobiology/crossbreeding/reproductive.dm @@ -24,39 +24,42 @@ Reproductive extracts: . = ..() create_storage(storage_type = /datum/storage/extract_inventory) -/obj/item/slimecross/reproductive/attackby(obj/item/O, mob/user) +/obj/item/slimecross/reproductive/item_interaction(mob/living/user, obj/item/tool, list/modifiers) var/datum/storage/extract_inventory/slime_storage = atom_storage - if(!istype(slime_storage)) - return + if(!istype(slime_storage)) // what + return NONE if((last_produce + cooldown) > world.time) to_chat(user, span_warning("[src] is still digesting!")) - return + return ITEM_INTERACT_BLOCKING if(length(contents) >= feedAmount) //if for some reason the contents are full, but it didnt digest, attempt to digest again to_chat(user, span_warning("[src] appears to be full but is not digesting! Maybe poking it stimulated it to digest.")) slime_storage?.processCubes(user) - return + return ITEM_INTERACT_BLOCKING - if(istype(O, /obj/item/storage/bag/xeno)) + if(istype(tool, /obj/item/storage/bag/xeno)) var/list/inserted = list() - O.atom_storage.remove_type(/obj/item/food/monkeycube, src, feedAmount - length(contents), TRUE, FALSE, user, inserted) - if(inserted.len) - to_chat(user, span_notice("You feed [length(inserted)] Monkey Cube[p_s()] to [src], and it pulses gently.")) - playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) - slime_storage?.processCubes(user) - else + tool.atom_storage.remove_type(/obj/item/food/monkeycube, src, feedAmount - length(contents), TRUE, FALSE, user, inserted) + if(!inserted.len) to_chat(user, span_warning("There are no monkey cubes in the bio bag!")) - return - - else if(istype(O, /obj/item/food/monkeycube)) - if(atom_storage?.attempt_insert(O, user, override = TRUE, force = STORAGE_FULLY_LOCKED)) - to_chat(user, span_notice("You feed 1 Monkey Cube to [src], and it pulses gently.")) - slime_storage?.processCubes(user) - playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) - return - else - to_chat(user, span_notice("The [src] rejects the Monkey Cube!")) //in case it fails to insert for whatever reason you get feedback + return ITEM_INTERACT_BLOCKING + to_chat(user, span_notice("You feed [length(inserted)] monkey cube[length(inserted) > 1 ? "s" : ""] to [src], and it pulses gently.")) + playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) + slime_storage?.processCubes(user) + return ITEM_INTERACT_SUCCESS + + if(!istype(tool, /obj/item/food/monkeycube)) + return NONE + + if(!atom_storage?.attempt_insert(tool, user, override = TRUE, force = STORAGE_FULLY_LOCKED)) + to_chat(user, span_notice("The [src] rejects [tool]!")) //in case it fails to insert for whatever reason you get feedback + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You feed [tool] to [src], and it pulses gently.")) + slime_storage?.processCubes(user) + playsound(src, 'sound/items/eatfood.ogg', 20, TRUE) + return ITEM_INTERACT_SUCCESS /obj/item/slimecross/reproductive/grey extract_type = /obj/item/slime_extract/grey diff --git a/code/modules/research/xenobiology/crossbreeding/stabilized.dm b/code/modules/research/xenobiology/crossbreeding/stabilized.dm index 14bbdd6dda56..e9424c469c4f 100644 --- a/code/modules/research/xenobiology/crossbreeding/stabilized.dm +++ b/code/modules/research/xenobiology/crossbreeding/stabilized.dm @@ -189,13 +189,18 @@ Stabilized extracts: /obj/item/slimecross/stabilized/rainbow colour = SLIME_TYPE_RAINBOW effect_desc = "Accepts a regenerative extract and automatically uses it if the owner enters a critical condition." + /// Regenerative crossbreed stored to be autoused on critted owner var/obj/item/slimecross/regenerative/regencore -/obj/item/slimecross/stabilized/rainbow/attackby(obj/item/O, mob/user) - var/obj/item/slimecross/regenerative/regen = O - if(istype(regen) && !regencore) - to_chat(user, span_notice("You place [O] in [src], prepping the extract for automatic application!")) - regencore = regen - regen.forceMove(src) - return - return ..() +/obj/item/slimecross/stabilized/rainbow/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/slimecross/regenerative)) + return NONE + + if(regencore) + to_chat(user, span_warning("[src] already has a regenerative crossbreed stored in it!")) + return ITEM_INTERACT_BLOCKING + + to_chat(user, span_notice("You place [tool] in [src], prepping the extract for automatic application!")) + regencore = tool + tool.forceMove(src) + return ITEM_INTERACT_SUCCESS diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 3ec48ef314a2..727518bef728 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -29,19 +29,20 @@ if(extract_uses > 1) . += "It has [extract_uses] uses remaining." -/obj/item/slime_extract/attackby(obj/item/O, mob/user) - if(istype(O, /obj/item/slimepotion/enhancer)) - if(extract_uses >= 5 || recurring) - to_chat(user, span_warning("You cannot enhance this extract further!")) - return ..() - if(O.type == /obj/item/slimepotion/enhancer) //Seriously, why is this defined here...? - to_chat(user, span_notice("You apply the enhancer to the slime extract. It may now be reused one more time.")) - extract_uses++ - if(O.type == /obj/item/slimepotion/enhancer/max) - to_chat(user, span_notice("You dump the maximizer on the slime extract. It can now be used a total of 5 times!")) - extract_uses = 5 - qdel(O) - ..() +/obj/item/slime_extract/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/slimepotion/enhancer)) + return NONE + if(extract_uses >= 5 || recurring) + to_chat(user, span_warning("You cannot enhance this extract further!")) + return ITEM_INTERACT_BLOCKING + if(istype(tool, /obj/item/slimepotion/enhancer/max)) + to_chat(user, span_notice("You dump the maximizer on the slime extract. It can now be used a total of 5 times!")) + extract_uses = 5 + else + to_chat(user, span_notice("You apply the enhancer to the slime extract. It may now be reused one more time.")) + extract_uses++ + qdel(tool) + return ITEM_INTERACT_SUCCESS /obj/item/slime_extract/Initialize(mapload) . = ..() From e08cf2923af35111c063e7cc61e5269634a63805 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:43:57 +0000 Subject: [PATCH 037/155] Automatic changelog for PR #95402 [ci skip] --- html/changelogs/AutoChangeLog-pr-95402.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95402.yml diff --git a/html/changelogs/AutoChangeLog-pr-95402.yml b/html/changelogs/AutoChangeLog-pr-95402.yml new file mode 100644 index 000000000000..d0c0b46f7467 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95402.yml @@ -0,0 +1,4 @@ +author: "SmArtKar" +delete-after: True +changes: + - refactor: "Converted crossbreeds/anomacores/RND machinery to item_interaction" \ No newline at end of file From 40c1fddf7285d897ef1c80f0c4dc4beabc3bfa3d Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:44:08 -0400 Subject: [PATCH 038/155] No more infinite free grenade launchers for nukies (#95419) ## About The Pull Request for FREE? ## Why It's Good For The Game for FREE? ## Changelog :cl: fix: No more infinite free grenade launchers for nukeops /:cl: --- code/modules/uplink/uplink_items/nukeops.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index 070019efff13..bf7c5ffa14fd 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -527,7 +527,7 @@ // ~~ Pump-Action Grenade Launcher Kit ~~ -/datum/uplink_item/weapon_kits/moderate_cost/china_lake +/datum/uplink_item/weapon_kits/medium_cost/china_lake name = "Pump-Action Grenade Launcher Kit (Moderate)" desc = "A weapon case containing a 40mm pump-action grenade launcher and a bandolier of 40mm grenades. Less versatile than the pneumatic grenade launcher kit, but no less dangerous. \ Though try to be careful about danger close. If you even care. A favourite of the Tiger Cooperative." From d1c915ad154f294a1ca8bd4ec44bb93306a0380c Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:44:27 +0000 Subject: [PATCH 039/155] Automatic changelog for PR #95419 [ci skip] --- html/changelogs/AutoChangeLog-pr-95419.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95419.yml diff --git a/html/changelogs/AutoChangeLog-pr-95419.yml b/html/changelogs/AutoChangeLog-pr-95419.yml new file mode 100644 index 000000000000..ae11eb699eb8 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95419.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - bugfix: "No more infinite free grenade launchers for nukeops" \ No newline at end of file From f893f72851a3671ab8126617cd1ca6e3c27f0647 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 16 Mar 2026 17:40:48 -0500 Subject: [PATCH 040/155] Dock Pay smite logs to audit log and also always notifies card holder (#95388) ## About The Pull Request 1. Dock Pay smite now adds to audit and transaction log 2. Dock Pay smite now always notifies card holder regardless of bank alert preference 3. Adds better support for negative money (ie, giving money) ## Why It's Good For The Game 1. Allows for people to trace where money is going, even if it is vanishing 2. So people can actually know the smite hit them 3. Request ## Changelog :cl: Melbert qol: If your pay gets docked by Central Command, it is noted in audit and transaction log qol: If your pay gets docked by Central Command, you will always be notified, regardless of bank alert preference admin: Dock Pay smite supports negatives better (for an easy way to give someone money) /:cl: --- code/modules/admin/smites/dock_pay.dm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code/modules/admin/smites/dock_pay.dm b/code/modules/admin/smites/dock_pay.dm index 6eaa7b53792f..fc8c03a28a02 100644 --- a/code/modules/admin/smites/dock_pay.dm +++ b/code/modules/admin/smites/dock_pay.dm @@ -18,14 +18,14 @@ if (card.registered_account.account_balance == 0) to_chat(user, span_warning("ID Card lacks any funds. No pay to dock.")) return - var/new_cost = input("How much pay are we docking? Current balance: [card.registered_account.account_balance] [MONEY_NAME].", "BUDGET CUTS") as num|null + var/new_cost = input("How much pay are we docking? Negative = giving money. Current balance: [card.registered_account.account_balance] [MONEY_NAME].", "BUDGET CUTS") as num|null if (!new_cost) return - if (!(card.registered_account.has_money(new_cost))) - to_chat(user, span_warning("ID Card lacked funds. Emptying account.")) - card.registered_account.bank_card_talk("[new_cost] [MONEY_NAME] deducted from your account based on performance review.") - card.registered_account.account_balance = 0 + if(new_cost < 0) + card.registered_account.adjust_money(new_cost, "Central Command: Pay Bonus") + card.registered_account.bank_card_talk("[new_cost] [MONEY_NAME] added to your account based on performance review by Central Command.", force = TRUE) else - card.registered_account.account_balance = card.registered_account.account_balance - new_cost - card.registered_account.bank_card_talk("[new_cost] [MONEY_NAME] deducted from your account based on performance review.") + SSeconomy.add_audit_entry(card.registered_account, new_cost, "Central Command") + card.registered_account.adjust_money(-new_cost, "Central Command: Pay Cut") + card.registered_account.bank_card_talk("[new_cost] [MONEY_NAME] deducted from your account based on performance review by Central Command.", force = TRUE) SEND_SOUND(target, 'sound/machines/buzz/buzz-sigh.ogg') From 5ea6f16fa6f0cbd1a3dfd6f79a6b4a481c0ca969 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:41:09 +0000 Subject: [PATCH 041/155] Automatic changelog for PR #95388 [ci skip] --- html/changelogs/AutoChangeLog-pr-95388.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95388.yml diff --git a/html/changelogs/AutoChangeLog-pr-95388.yml b/html/changelogs/AutoChangeLog-pr-95388.yml new file mode 100644 index 000000000000..ed53035fb6d8 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95388.yml @@ -0,0 +1,6 @@ +author: "Melbert" +delete-after: True +changes: + - qol: "If your pay gets docked by Central Command, it is noted in audit and transaction log" + - qol: "If your pay gets docked by Central Command, you will always be notified, regardless of bank alert preference" + - admin: "Dock Pay smite supports negatives better (for an easy way to give someone money)" \ No newline at end of file From 6f325ed9ce85756031a0a70f3afa6f7a5522cc83 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:00:23 +0000 Subject: [PATCH 042/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95388.yml | 6 ------ html/changelogs/AutoChangeLog-pr-95400.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95401.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95402.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95419.yml | 4 ---- html/changelogs/archive/2026-03.yml | 14 ++++++++++++++ 6 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95388.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95400.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95401.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95402.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95419.yml diff --git a/html/changelogs/AutoChangeLog-pr-95388.yml b/html/changelogs/AutoChangeLog-pr-95388.yml deleted file mode 100644 index ed53035fb6d8..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95388.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - qol: "If your pay gets docked by Central Command, it is noted in audit and transaction log" - - qol: "If your pay gets docked by Central Command, you will always be notified, regardless of bank alert preference" - - admin: "Dock Pay smite supports negatives better (for an easy way to give someone money)" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95400.yml b/html/changelogs/AutoChangeLog-pr-95400.yml deleted file mode 100644 index 9e82e7c62ee5..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95400.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted tram objects to item_interaction/tool_act" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95401.yml b/html/changelogs/AutoChangeLog-pr-95401.yml deleted file mode 100644 index e9feca79a416..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95401.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted autosurgeons/robot bodyparts/dissection notes to item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95402.yml b/html/changelogs/AutoChangeLog-pr-95402.yml deleted file mode 100644 index d0c0b46f7467..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95402.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - refactor: "Converted crossbreeds/anomacores/RND machinery to item_interaction" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95419.yml b/html/changelogs/AutoChangeLog-pr-95419.yml deleted file mode 100644 index ae11eb699eb8..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95419.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - bugfix: "No more infinite free grenade launchers for nukeops" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 6433aa9ba07e..6f3f04b47891 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -238,3 +238,17 @@ and worm meat and can only be tamed when they are spiderlings or young. - code_imp: Refactored tameable code to use TRAIT_TAMED instead of setting individual variables on each mob. +2026-03-17: + Melbert: + - qol: If your pay gets docked by Central Command, it is noted in audit and transaction + log + - qol: If your pay gets docked by Central Command, you will always be notified, + regardless of bank alert preference + - admin: Dock Pay smite supports negatives better (for an easy way to give someone + money) + SmArtKar: + - refactor: Converted autosurgeons/robot bodyparts/dissection notes to item_interaction + - refactor: Converted crossbreeds/anomacores/RND machinery to item_interaction + - refactor: Converted tram objects to item_interaction/tool_act + lelandkemble: + - bugfix: No more infinite free grenade launchers for nukeops From 89cc285a8182f6290e212ba87a7b4046fbbe0ce3 Mon Sep 17 00:00:00 2001 From: kittysmooch <105110468+kittysmooch@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:26:21 -0700 Subject: [PATCH 043/155] new baseball related sprites (#95413) --- icons/mob/inhands/items_lefthand.dmi | Bin 36336 -> 35391 bytes icons/mob/inhands/items_righthand.dmi | Bin 39051 -> 36592 bytes icons/mob/inhands/weapons/melee_lefthand.dmi | Bin 17469 -> 16361 bytes icons/mob/inhands/weapons/melee_righthand.dmi | Bin 19220 -> 18567 bytes icons/obj/toys/balls.dmi | Bin 715 -> 1177 bytes icons/obj/weapons/bat.dmi | Bin 587 -> 1100 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi index 67e10ca1b74fa6d340f3161998e0700241ed1924..0949da25e15e5c5386b991ecac9a177b8f5e176b 100644 GIT binary patch literal 35391 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

=J3_P)6ZqT;lj0(XHbC`lYQ*CJ@XzsS3` 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 a@jr8Orw04tAl+L)wG5uFelF{r5}E*6a?#NM diff --git a/icons/mob/inhands/weapons/melee_lefthand.dmi b/icons/mob/inhands/weapons/melee_lefthand.dmi index 8ceaa871caf17b4ddaef957de9c8162967c60ea1..3f46e7ce78b0a34701c95c5e593954f25225a134 100644 GIT binary patch literal 16361 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 literal 17469 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 diff --git a/icons/mob/inhands/weapons/melee_righthand.dmi b/icons/mob/inhands/weapons/melee_righthand.dmi index 615aad431524960fc506ac9c19153927854bf9d2..fb97f4e2b6653b9306dfa940493f05134c197eff 100644 GIT binary patch literal 18567 zcmch%#h`(sBpR|v!>rRdd1{bu>S z;hl>{w?_`}?k%?HlB!_w5BsJ!`q?b$@^jr^Bk|0JDt{ciVkG-vjf&piimvKwog(_= zue)JZMFS~RW;~+$ z#OA2;yd_LU^SgZxSSY34J$b>dYyRjr@5I}?q#j*2yu|dD^(439pW%=4-Dm;jJS@Mo zUe-N*4MD}lmw7ufbDpoONqTq_Sy^>PMc1Bf?R3~0bG3--)##xNyzPRQfcSnNU;lpI zOH%WPN3^8cK3TtCzUyPJix26(R?uh{5dSshY*R6x-Qt&=BgYDDc%Bxe-*eD=nD8gT zKbfolQTm~QZpU7d%xyLCU)L!aA?+sI-;;gz$x`Xqsv_=O>OdH6*RRv*j%8Qm*&z@e z=IfWWZu_Jxj`37j-SM4SA($=5jPRdIRawpTa&MD_C_peo+MgH;&oO(xwy<^P_E*);|yBc-LW8dZPl3H3XgW<=O zorrCL`hyAWQz#M&zD`iHUnY@FBc=ZEOl#;Qq&2`vDV}bAUY5p*77bRN{@}81rbzqp zfERAv!`le{7sdB4e5%U^i*792uNav!K_IVA?~><$cKWmyHS}nk{35VGAY%i&N_bKE z0~*L#UYzI|OrE)u0R)nG+2BR9F%i{yK)9)18>c+gsjAEj39d-1C@IwxLQM~Zl^C)w z$ExUW5V&I?5QzpHpPfikQ#ktJ$cKC9{W|~t<)RL1!|;X?XuN%cpT~NnrU@L2LYlu8PKwfN zN%W5{-s(4Aze`_o{GOUt7iG|r=NnKN0vpvy57eTjx@k_UN1^Oo&a{j=KkgH>T6@(& zg0>`@N23rDw&OMrOvbU5@6+>JG7u657G75ZI|qtqA1|M7No@9o7}nkElj}7OSmaEl zjhaU(L=ZBjkOTC~)KyW$W500$+nbiQ8EDQkxvDntyr|GC20!Z5RYGHYqRe3VGER@L zcfbNGFlmIELze@pq$9fc5Vz*_UtftXezv^+SUuo5W4~s@frA@z*cc^BandXN0f+U= z{jvQj*6d}eGrIi#ts_!TIuEaXO_>(wIEYEFxLTJYbf~UPvQ4@QTXp?(0c>E?D}N+p1?E6n&rnDp2TCe150`@gjm?*C534t6SVaX@B490 z#=MtIZj`;HG?{iHi~ZVj~(O&cXer*zkk`ZGI=4S&010% z?>0|s z@#`ysoXrzKc-KDXGoeUTH09^qxVC>;1MU988MgLdyIb0yo|uqtmGt^G9}&EZ+^QN{ zAj^5i);O%gM}@Ai${VCj$-OpgGXCQdd->egkSbJx23<+ke1jx6x^<|?5ci|zi^%x`>7_x6N37dby?vruFTqTrwtN z9Cn3o>eTwjs#Ht#TBY%ACF-Jo0W@&T`*n?DZ^>S$f$F1&#CHD}wedYqE(I-IT}o&D z)!O?AqlD&pBGpDrEVt5JA3Ybd+|4tj)}gu_);B?2)Oo=UE#23ja*K|c@Ok7#XitBS zs-8J}()0U!8&Q;o&+1GvDd`3~RAIB@^REM5#Ig|d`+oW@XLq)}S0dyjJ>zP=iYP9J z<`nYz-8+DK8rPH%s}hplzBxbR%P%XA*SB)sd$=>Yl*Abk|{wRk$*IMoH`1qG7Y~%{mfROmmkzhr0kZ7C9(w zLx}#GVys4VsH~I7>c?+Lv2QJ#hd|Cx?WPty%WyNi#QUaY#`?^4NIcrjy@$E`=#gMO zW#)qr$oeP@$}9n#>$&<+;Kr?^ARv1G+dsqglhV?P8!)kPp92gB@CzSXqJ<;p(7(U5 zwcWf$tx7bQ6G~H0d__!$gzK9%_<7wKyZz;`nkM3b#@jw{$kJF{IJ&)9Vs&sH>d{QKxGBh4Gzh~oyLy%Y!r@Lp&ez0qXX~~{B^aBVF)tF;a zU)`Yzszn{xJDEv8$uyeB7GFl&*Z8V_s}I>h57}3mW?*vDbyU&#)0Ov?kk*=)p4qpW z%h_x`p3I?}F{v$~Z$OX%e!NX~C#VB^CLhu7F#G)c9&+83Ue0c<68;vwRe+vL^&WP- zj%gTN@laE0eF@{i;bLS?9+r_;ag-A}BOVPq`NK25EF8x3gS8-(=FE!U7cNQL}6ASg}Hgejuk9rh6Ej+Oi?)A0fCB70)r%4C!7Ij45 z=jokDkS`hh(?$V#TT#uwiyN-uM1r0QcJ9M3J=s$A{5A}pEaCyA>J3?}Yl%LHkbu*o zW(GVbe%)C`R7QyTeW}MO14!0C;IojCkItVcEo7|f0SzF3b>w@nqKANJC==bx6dovu z7wN=`-56nM@!NF*)^Jx(6x#5s*#5TM#L6>Y=DKARxllLevq$~i%ECDx_|Kvf33U?V z>c=0haIsH@ycCBksH&Rx3P}*UX{BN(oP9{vhRhpfOgEPT8m3-y78p}UT)dVu)vs9Y zpA}n1)>r;?7YxN9E`VbtgUJK^nAmZ3Ja=#&7nR+xX@Jdu7C_7lg2ywi1M%rN9?Lma|V_kefvl0Rrk@ z`B8s+u-;~4OF6pfFQOlTUVb4tvWIc^9`k_ISbEISW7@@LmB4Y+OZ<+^kDdbO%Gbd>N`pA|WY2qGU` zmVErCuO^cX0?Ga^JblauFsib29ARj{;i*-*-S2eezi-hE8(S&uve>w zg=BE?wE%1n9z1yQ7jU1<3}=13r*W)^Zb7F3!^cz1ZLuofyaEK9+ z2@}s1)joaKZB);qzcDZLhTxcG-qVwq)YhQKf>nu)o{QJ@Z63zFuBLfXd5S;HPdF_f zQVsqJaT`?}h@U7pXPqW8QOjYYu=2i0%b6*Lj4Xo~YpAo^l)YkLuo^gn`qf{I`_Xb{ zk@cd^Y>D5@8$kDspY?`tN<$qKGXNs%6HNy6GpU)~X2Dp`tV&4Lyveg>I}U~`SgGmW zGr4#{0Eff*O^Lo5CCd?FHywI8X0B#|XI4#PGnym1Rg#VdazM+O`g*XvrBUp;1wW`@^BoWU!`H&htsfQA9E!78$gsjx@w1w!%dAQnLe}f*#J72a> zW*w2s_dF~@8~kJa*?BznP7Gs2Lua<)<36iZ0EZVmKY;+@fCQj%b)&T!!02DfCjOO-<6qzBEo(`%uu^|bU&%pF&F|m8yH+Ziv`Kpb z%WZ3G+usL|+{`YX8e-|J)M1&YyZ57~6aUL^7mY18g5FmfX!qbo%8TZ0DxEt56tM=@KP74ZhX9=E7&40J?_4elx-*!w35 zBz)m%!2%n`;0} z-HltCDeg+S03BmYkz}L?TVc=Q6Elc2kBKu2w=^{CYCwH;OqkEv(&E%hb#yaf_W)P0 zAP8eC4-7BSE6-1Yu=cPKAVO^*ZNCX!M|xQ-a59xdv|4GwI?WH~eWE7#ehD}?p0{U4 z%8RQ&>^*DAdT`$ali_HG(9e7M)z;0h8So1jwR^ZiBnP(6pbo$WF&IpB#&0MzScqwR|60kySsvlEp(G(BjRr~dIy}8BCdCMG79=J%=V6n|LzEdtI5eRDhCk)tm--51&VI#u zKkL%Fe&r?xn7pYzmZ62_tKv)b(-i)oLUr-CxouU>ifVpc1@ndC3M`O6cGYn1V~bcv z&$Qgr$a6MX58kGTg;C3%Adr1wXC%Rm2mePtO(q~8XvZ+UZ&$K)Y3AF}DvREHDu-?(*SdLy zQcXX@v)J&sS159KNtaJiufyp!Z30VbEg-kOP_y%ZFt#Iui++kVj<8QQpSZusBZJG@ z-1W)uz4S{r+g&!}c7Z2Q@<5JQ29V2DOF z|5EU$+5om}@lLT&b!k#vhE`$(&gz0wv@`|zvwNUjl}AZD8}FPXx%KRC0M@ZdQeLm# zaZikR05M?PR*npc36ZfIBg)Hw^)pfmPdx#(pj{mW#bwqc#28^Ym`mUWjWF&2#rRIntL~I5ifsaksW#R9kmt>~fL$R-W_YOtu=i`iE*yMQ&0|WmmYL zwN$=hP^%waZDhq~k4~RQ^!op*R`m0te)1GpeI`NbO?MC#UP|q;ZXe@`Qf`#|Cox?c4O^ok&@@%_#qd zszB-V0sq-@&BA;T9sxZ(ugTXQoa!X4q=^XPx|XS;vxgB;H-~fqNo)^z6-vtmK{`Z= za-bL94iVcz@;92&F^p&nlC=ah%lYLNOHZ8i;k|=dA9fdUnRG;_9Ek=P!c!I;yM}`S z5M3G%48ZqT&^LSgFVZ9SE#Y4G(1T#o348xSU;OF%AJLdTrDe5I&Q874&bj<>2RpGd z;%|-TE=-LAjxff8?6f+$c;!f%!Ht!g>y{w=uROg2AStHxdFJX*zTU%LGF|kKuZHOx zu2vjW@h!e=mqt|$ighRUN_`35dHh440NkNTt>V1R?@P&`cIzh^X=H`Lbw^(>iTVrrWHLQQJt zSbXM$#?`yjjKu&Xec5`=PB_D@f9H3mx+Q zu727^4A5={AM%+9)QwinXik2`Rox0g3a^HL(m-TR4z7IGNz|=T+-D~6ouJLNv14lC z`xkXWgrIkJm{Vuk?nCG{?X$t)9RaFY=a%<~?Xgb{lFWdFg+Kl4m?3%fPo}~^W*NDr z5`4s`gXe=smqNe#V51eUB^=2?7iT5s`>M3>dUQg zzi$y#*nuc~s;}k9mye$iJ-Ys^j-@(V4Yff(DAPP)?dNF?VczQVTY08C#Ly>by~3pJ z#QkV~e~|f=J1`h*aM~HMGG++GE_6Fu{%2&B|0>nUI6}6#67a&qB^^XQD41$nlOr7yqb0x{qcEbB2N#fX72|!~-yFeIqhOhnt>^*cd zTY#j=FxW!#s*a)$;qvLdKX&|syHeFQ!$mL{E=T~i*yD#QG2GTu)0{HhR9YU!_l z*0b%|F^b7vW5(GN6}e2&{5|nUp9;HravY^e4zojll$b?29(U-p%}!h za<-|a2{PVhzUS*FDi5J6fafUVMy_qtt$sTt*aVIFv6D5*DQ;tl2*bUq)DGEp2^Vg` z?U!y{XG!coa?N?FBe~!5h_j8D8A4K#!8j~75F$Z?_26Ofw6mUOtoe$!(RLSpDi8ks z1&F;4>QC3K576RROWfIx_K8OJVuY_&6W-BMLjbPMEgyeZ40z+_H-A^xV(>}GDteT<%eUnm8KhT zw~@>TQwgL$dLvK8tuXTLGjea!+s^*Hnv#E2(C;FIgal#Ros?Cfnvn$aV%&(t@C%}65F^hR4ypfV$Qs`pT7gDoPGxVh@52#JdsD8Ico0B zL_`>TpxAQwzFw$rsP9*``zXr-u)O!i$cKmKd`uR&Zf=Za5pach;$nec$CUEi?wE%} zX^y78@^z$yl6nN$;j{UKUU>AlbKmhiwu_p(g!`$c(GRFsyI<{f+4F~G?{ z*$1rri_9N#*=Xc_rf$P?zQ6EMK@q4>RaS5F{NK5=NHP86bP% zAIk71Fov8IPbMM-8izrU-(C1e^Sw6e!f<74k@p@3O$S;9QtZnQy8j3_(IbdY6=cfV zxU5nppa?H$nQW*Jq(A!1@Fl~c3{Ma|5BI6Dq7P|4?JZ=a-s{T~%XiIz3jszyr)5Iv zgIb*D2lSX}n^5U+YmQRpYG~Q}chcrgF?T8%|wLjv7xZj{YKlJB4i9I+c;g zI4$+SAK$>UW{-q-z%Jec)Ir#N4j8Vo58k?A(_pnGA^Qi!)>fqk<2R#T#JKiBf|km1YmVI5RzBO-5D)P*lk+DIP#79BwzN_%I?p` z7il2AcUe(9d8QVv3rn}Jo~35~r0_+b^kXiNnlX;}l@{SFKqrn7=KX;C+rEzlEoAY- zV&_eKM@L6~kdn@vkbtc#1Emc%&EdEvO%k;(nG@<7Vjd%zBTti0mq*J2*YHG!CY>Iz zdMR-8Ii-xlq|e}3lxo|Nwo?&|8&5~02TR^@FbOCshWR>JkQq#a!H%2r*&Z!`vRA`1@5VwZKTQ#1akqxp`5EASNA6^<=5uLUh!d& z^6hN9ufSOFFZK6hZ}08j!dItQ2@Qz+3m?C|J{8GfbB5=wfuQpmmoWQjal1vf00U4ZW|;mzzD}NY^VManopp3=JM=FMmFLz@$LabV8|7dG zsERKtLY3eWy%#G7&ZEbxor|AE%C|`?{+*J$Nad_Somm)4F%v^6h7oT3LidYy0nH3{ z@l^W%&L!>?+*L#ayAj}PY6>un_dGJ=&?^+kG6)d=Eo>r!$3%Zve9%YZ#Ak-!)hYHV z`8rUJL8<|iWG04^yo`I@1us&~SgE;H!%}+Bh2 zC9S8kMTPEboI&<_{qQ_{BO1IW5MZ!Tt(IgK=N(}rgVqsM<_#o~tQsbBBe?xl(>m~4 zx@%dS^v)wsA?wSk2zNT$*E@7Z;cp|_bO`$T44>E)U}1m1w97#OX~TH@WYV&@zr5wW zoh4(*yGL7qo(8Hx@{HtOHRH`CJcFGiY~=yl4$|7@Iu&9AzU7GnC5i%E){^ZBt6A)J zV#83q?S$xWJuLw#;fZ45k7sBO=%Vd66go-&G8IvzTfk1}qC5r92q{(}uKDKjyu!5d z_F7&EWgHRUG>R{`Q*rpNAqG7U`;4womH6#eVs)UvY3kueD0$5dA#6MkM4RqYfH5K3 zbbnKc<5MeG!`k(O9^DWDL)eLAAP(S+_c8cGuud$)XYgKvJHjrWLnnfSt$=M&2WlpV zH0Qt$1nhlV3sm)KdsEAx&7W7sse9~#hBYT#5`_%dtr;%j8o|8EwVkGr4b%iaR8s7w zR|B9De|#J31S^NU>@@RP%kgS3d4W9kHeoyAHwFp+0_lUO)7&>_UV+NWFB|tk`dCCf zcOTsPE6QwXkuKt}zgH@8YY*hwtlarR$Mf*bhF!s2!sqSk6fjuCOdnCGUp-d!d^~?+ z=;r{oJS~uTSU2h2V4uIS!5;|U3*@=u#wXwtwh5=bVrp1+LmET(Fxl`g1+fZvie>NK zk?ar>|5=^=^r6NNZWC7KT-l3r1wako(F{Nd7g+NZB9L{r2_2A}tr=EkLjP_<74OI{ zr0ec3d40;b)-tr)8xKIej;TsL9n&pVIrb zg|>slYd-#ZEMX0ls@e=fEhtu%M)AH5vW+`p+eKMg+!nG15u!7( zJQ~umrK$1vDx7c!Z|{D}<&9q;(?F?IJ{6e+h(k-7%(L9v~I>VQakoCJFN=QxquPdid`BHO9hR4F^BGD%;pS z%5=!;grphB~fY;DzWG|gv5SHT)PnR+J4nTc(ls6Uc9uJ6GO?!>h2{iXPpsxx{Uv+-=5U9s>*Hv_v0o@Sn7V8V&f110B!up!Ug{wT7Zo z>a*XJ!(Tdgl2<_Cm?YLLamir!VrzVpc#mj*PyM|AuO4XEJPVZN&(;tS^p? zIXRc?v~4hiGw{#r1N1&)sO8^Mya|x!Z#HeE%LIS@I$UmVkl(C0?PH~9QsZ%7K`;{# z#CUTM3xR%O_`Ul4X+TG<`||HxAwqYyGdMwFC{gn|B>C;zJh__bxU?45~j# zn`sBv424}txWt&DZRI&yB$x6d;ieSb-`t>-shoyD5aB*|ojV7aZthlARUL_+aJC4( z6yr7)TJqMXw?Ctj2}2zi3=Y%Tmd8LD%MeNbM@g(@w~WcQbSP3#X}owAJ*U50K9Vznsqp%9Y~rO z^~Q3x;pLmVzcMHGfBxaiXQrr?yi*32An7T8*{LRZOazY%21giu8y26gX1m!m4Bp3U zZ??;y5RD!=n;YGKf(-q}P^jl3E;h0mq7|*W5$|Y}b@|fd2;7@#cG@$Dnk{4cMk1 zRR09*Vv&C=SuP(KZPPA$Td2BA2S|rDN zQ)jwJA3&>}^;n3A|F}VMdf4^q{&?E`(fEv3D3^JFPi=|3R}I_o2-1w&YZ3b42a8zm zoU}?24Nft2{G30sSf^mVlA~<1Nmn+#!2;FWtt)INhk0p{;C)us3P zs>GML4L})}x%~DbGW2!fPM9!CYYSaxPgYUbZ!HC2_{p7kt`T6Y#30+;G5jxAL zPnn19ZN%EgkbKSEaefV;5i7{t>m#dRZ-SMdW8iRJ?s<&Z`i5BGY@ZjO`tBF_+JT1R z@j95BSL^AjY}1qrmzvs!E1e5qX!6)eLXR@aUQS<~*efm51{-~(b1R)g?WN0>eVXrs z${_02T$=wSxkD?Sb=M$F%(3~NsO zxWGL~`D$$72dbV?HR%ONRPB2^mu)iaC&6^->E-J%sK;JtQ@J1`|D%lenQ!RO)Ypro(k~UW^>Gn_QwjrRZ0nf#$_=xKWw?4Lb$cI4kx?g9S`cPji z_Uof~vBwty+wmw!P1q9$31>ZI{x%7#FoJco6Hg~xRRPnI{4e`JM=7{oYx zy~Nxtw)k~dRguB|L8lpm{U4VM{V9fGMp%hlc>LZE|B+3Q_7Wa3;j6M#jlDUyt87#E zeXP5UnX*EWyPa3SqDG?n)866W`<^MOsf)iQ61S!)*EF4nTxPI~fF6x99RTI?XtdE{C7xJh$yBST7@|%tD=y)UqHk)# z1yaHk*H`R?nzK2+0gI~6cYB#ll5wgR&47|sf3SuiH9<2IiuBFNzvaXe%Ql3c>65hP zg~UwOKQ?FAF`J$Vc+HOrHHF02@vlClCP0H~hM()2YWo-JuGA9VozAVx$N}v!Ulc4t zgZIQ1O9vdVwlU0)w39{6+hVA-@7WSIS0Jm#>7C2Y*=b71jJ z`pG|2#I*f1f@I!)rnD5@^5cO>9OKYAHukpco>1d-ap*eFhnu&wY4teEq?|cF_4SIw zgzvPyQAW1>pxB7VZ1x#Pb2jXa)m&aN?(*rY>?L+K9$f=+MCT%R;%6x( z7cOKp%utCU8@OAl5_GAFH`IO@gEd{v^%b;B5fZ@88(Q*MQup381ocW(8>4#Jn2kLa z09>2xj=J%G`l;i~lW}_ng>s_B`yLa&AU#`OCzL*HCi7T#w*)IHVDK zj%mSn;5N}&HjhXf}}vA~pC@C$>(Ui?h#mD)V+$c$Q7{;{c>ao7)z@fRJR$tCM?lIBj%3 zR>=*T5K*hw?hu2#@tWtZ2SS25NQ0`@-8krlXuQ{t=Q_;a9dlFfE>oZlVOaQ5p!WFJ z)!F8NYq2E~nKt}U`(xhlkf|{m(CS+nI^I<8+RB7Rb#qso>M0p_}c{ChhX) zw-FCd*60evZ2cn68Kbuz_9+fn7^VB--u5}>rVWeu4`ScX{5_9XD4C`$3Vd0R(UE!-#7E zkODPVOUJhG4%UgGGz7T3xDxv3btVX2nNbR>m4*7Y{y;PQsv^=?Z(WY1RqBnMr$rMd z6#sN9&3{?X4?LtU`qb-QI3ySaS`K1A(?9Lgf3LCf)!+dd@9YwD7ekt3p)R+q=+3b~ zCKUo%5|7)gbovsw*ln`1=#XGpvsLM-)^~@Kbvr7QbcXgTBIN3~F47Z>J?JC)aDvVS zs#p?cqc%w2a5!L1{IOrL7SUrENL@S4CuwI;ar`vFZ`!GKbQm&UUe1tx1i4^y%ZJr!8YuL1UiEt#K<~ zm06;#UyZ07(qZgA`oX^A@6GaWwo+s^xlLj!Ccf6J_2@dcFTZQ{nS~4SpR;oRIMlu@ z?lVd5d!oAhUd(S+wGXBFYiYvygssNb?18?sX@O2&3#FRllq!QD${Rb|j+K3|lze&YCC=y{LPnXlq?^ro8s3DI4~ih`mM#zlixq^nkXYlO^)IQR za)EXE(@O@%6RRZ%o5duOt%bz&hs>>m%SdFyT} z8)#FW{n2vL5TR7%c_xNerpdPI6@qZ?6N3GI;bEeWr>Zpef8Uj;A{oqNV#qG4vWu&r z6!%!a<-jE!ZmnjgLKR>SVO`xn@dr$z74L{{OSYx=#+g82_zUa5J3Bjm3u&xGhd?eo zhlDRBzUs}W@NZ%lRTBm0O-BVxvZ(25*R`tc=eut$R zEt|-UzYNjFsFAuMx8C1s?#}S;WXqlDZEz6z^~tSyejB=>w=;Onb);Fkk7=x_L^(RCFSb&2nf1lPMaL zEj==hzte!#%;#+I8F-C`c8s|nk$g;Tn@c}53m2!sTEi3{=7g5~Y)SweR)e7WD?5I^ zZdqe+h_l-^z*1BC`ZPuP%uFdJ1@~xK$G}nG{k|YVaec=WWwVyF6i*N)8Qw^VXRe7q zYJ=n!{B*Xi{MK|)Gp?wzXTF=*hTGqhkmAjrUr$~N8ylMzJ! zAQBIueXdqI_2q|YRwa4|>V@n|EEamaFa&1qgzTx`hWd)=k9GS#@A$!ekb?=KQOTO6 zSzD8e%U|wAbK2nEq3M`8iXfGIRPtub3G@f;+Z)S~FBPug%@#UO-#J3_d>A6a7NYvi zAENtwdFfhLHK_pR&*OOgjK(4Rj7PAORi4Zc>=uoLo>32da{by%`S_CoVYb|aiLfY} z#K!%!s%Dx>RB<`1@u)-n!X5Moh1MwQsn=CKb1LFOV6IL}z>hh_J`PP!Lt>OsZZduS zBMB`%l<27@4Uy-rVOd{61g6x<9oNr=Hfa)~en~l8ORS-gl>GnMy~NDy0%6Z8mu2vw zKQ2GP#m8~SC+I9O`y!kbrj}mqxT%TUH{Vr&8X7sjaO4`_(Ud>M%{6! zXOTkVJoF*fB6TRA_ivWJl~cIY8ZYItnDG7$dhq9nInp- zK6M^-l*vU>reR|qoje!wS|nFROmc5P+6U$X~vqWvM!_ygx8Woq|5*i>Fs>GahPBrjIkQ<=UeX>-i% zHt8s*VT=U?Y&J_~_?3@}^r3yc31qnj8?Q12T}a??>-aE*mJAzLqr>I&yk_;cA_`>v zN2O9)vGqQq=ym8svP+u>X8!36{Stv}>5sbIa1I3P*~TXw^>UT1zEDz73q6=N-% z`FQ_?-|Q4_K$|o^F>3nl^CC>__95ixN*wQpr6_wavqM4k)>(r-^4FnAJp+{wsHgd{8Um4BSE!7r0ajXi8(ChCh%+OIb7*3_ z8)-TnL2ahrQkU~^$rB~9l*D31#TR3z4>c9e<9skWsz-gp$rimc!cN5Sn(wdQ|%S+Lf9?krws4XgXcoB`DBfzW&-qTyVlYvBnZv<*E5` z?Ss$Ps~b8Zy~ajRlu8-s`2n%20pHU1b@x8+`MC!n8BFYUYaZAu+#Kg#)_c$}{LRmp zoJUrdHsWk^mBkK80#Xvnmj8m<$2B=%wkb?_nYnQ&Q-nV;i=DE9@zrHxC1l+m+k zi|U~-9S|e)^(F)AGk@ydoX`1F-4#jr(~^{5P;96kBn)f3+8d^7W4TvaGmvFBQCKlF41YkJ1rp59*T{d6 zRNU$D!#XZ2(te~;5Fg~ zb$S>P#BR;OVnkgh}tISqI_r+a{<}sxad`g7e}p$lrD7+4pPg zDlnonLs#b->K~&&?FcX}+)+wb&m%auaXuDu(28O-;7NAeuvrk=J}3N?7GulEM)ROz z%V{_VY@^{Pv?Ucgaic_Jn}#WfX@R!9gA5P*@BTc?X`OujVZLVpqmkmg>*(xXjT8$X z|9Qj_l$_3i>P`fs+5B^&9`hi>ZLqL$+Zk)eNMrQ;x|oVgN}Gzjofu_sMwx4;z3#q7 z4d~!tj7$m5)A0;t-DIutXmD!K)-UyTR-f-iF-3|_Usk58Jt*01V!{8YdjWl%6X@g9 zKjc7%C?^;oX<=UIV|2SHf?5}-R>{r~x4T`KWu)y}VdArYcf0)4xKt^G6CoD*IUBUg zoo;+YR}`=Y9aX<)U`*(sA7yL1A7#AkzODf_^@aPY(LgNr%ZfmOt>jMaYYE-YERrr< z#0uK#z$DcxQ0rz@2D6Tw?sYL?$~WJD?+`KYJ7m1r7PJD%*C6RmOfH~n1==O-sU(9g`ARS#b&;IP@o>8*-IM794)mjREy4Aj(V*OuN$p7auwC~))L5BFJNTkK zS;^k0`CcJ)8nv+=|NZd77$^=+o{!wL@E*E<9WgT$GRop3%oLzJK5yWpQbaYwm`6E6 z%6;pq6iPXz0c39W;{G2zPNn+Mhyydop~j${T@Xq@tVI& zod5h{N4kp?!-trtwy16&ifiap#S~QJr{I37-(CLQHwtQH0|v)1C8p%5@KCGb~Y2unXanbG+G+B+)UCQyfyB(_VmpgV+(TjY2?{Y?6(Q=wJEaMACX zzmj{OU;McxNBCL<>JTRMhfbjLpoaqDXhYSn6PO1RyWU3NSdY(KV~o<-tuTz3G4|B5 z8yGQYvwH1AZGB*9NLN&vWXwDgF)4HyXxaIRF)$+uJyG6 z%s#wDTupU%WejlqUge)=p8}csi)|%o_x=PkzJW{v>B-p$i7MBzVK6sQzwznI-;;h9 z$qk3}l7zAMz~mmWkzB4MTTEYZK%dPj5(WLcSzc_t+S`>Lbb)_koM!l8Ql$Ksl^@?V zmL$%*jW66)Jr?Qf0K5WYk`I(iEWG;e$a+;i_G0;HZvxPys?$ zK6p|5R!w{5uHnqU+hBCt)%!`L*TtbY^~v1tuX3rR8S}{2Eu2A=ImzHaU&%3+gTf{7 zXnwz7wb%F|?3VbLe>yBiXJ}dmmw5-YN93$&%+A>dr-ver{oG)F&~}e9tmv`K1etv9 z2l^HStAV^XH1*$7)Y7baHi)&y;%Lox@W(_ZpQ?hw#D8Xz0n9BhoO99cp7UqVV*lZp z|IrhDyoe3bbqw?!0AGBYyuRpZ8k+m=-KbOUPKyC}VyEsF{j@BU+{x+{gIqM0In>B7gn~zEF#$7u3kxo{ zpnU)U<_T^BGC$n`7wmy0i1KmA{&`}HmW literal 19220 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 ezf;ppM^{$fTg%$>Z56a5wA6L45 diff --git a/icons/obj/toys/balls.dmi b/icons/obj/toys/balls.dmi index 5f92c95cfe714fd81c6018c7a805ea72a9afd0f6..c3e439d009610447159ec9737ccf1cc2298414e9 100644 GIT binary patch delta 1036 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 VA6!n2h(`bb002ovPDHLkV1fo;)Uf~n From 78c5c2bb57a7b4c03c48eb2e45aa48ae4dbdcae4 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:26:42 +0000 Subject: [PATCH 044/155] Automatic changelog for PR #95413 [ci skip] --- html/changelogs/AutoChangeLog-pr-95413.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95413.yml diff --git a/html/changelogs/AutoChangeLog-pr-95413.yml b/html/changelogs/AutoChangeLog-pr-95413.yml new file mode 100644 index 000000000000..ffc1d3b3db7e --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95413.yml @@ -0,0 +1,4 @@ +author: "kittysmooch" +delete-after: True +changes: + - rscadd: "Added new sprites for baseball bats, metal bats, the homerun bat, the cricket bat, and the baseball itself!\n:cl:" \ No newline at end of file From a1bdb3a17258db0cc432641449f291d0ecf720dc Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 06:00:22 +0000 Subject: [PATCH 045/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95413.yml | 4 ---- html/changelogs/archive/2026-03.yml | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95413.yml diff --git a/html/changelogs/AutoChangeLog-pr-95413.yml b/html/changelogs/AutoChangeLog-pr-95413.yml deleted file mode 100644 index ffc1d3b3db7e..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95413.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "kittysmooch" -delete-after: True -changes: - - rscadd: "Added new sprites for baseball bats, metal bats, the homerun bat, the cricket bat, and the baseball itself!\n:cl:" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 6f3f04b47891..d53d5f2d38c9 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -250,5 +250,10 @@ - refactor: Converted autosurgeons/robot bodyparts/dissection notes to item_interaction - refactor: Converted crossbreeds/anomacores/RND machinery to item_interaction - refactor: Converted tram objects to item_interaction/tool_act + kittysmooch: + - rscadd: 'Added new sprites for baseball bats, metal bats, the homerun bat, the + cricket bat, and the baseball itself! + + :cl:' lelandkemble: - bugfix: No more infinite free grenade launchers for nukeops From e1464a5704b0da866842cc3d6159161cb3db4956 Mon Sep 17 00:00:00 2001 From: Iajret <8430839+Iajret@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:25:25 +0300 Subject: [PATCH 046/155] Fixes runtime from buckling basic mobs to surgery tables ( + fixes vitals monitor when doing so) (#95426) --- code/datums/components/free_operation.dm | 10 ++++++- .../machinery/computer/operating_computer.dm | 2 +- code/game/objects/structures/tables_racks.dm | 29 +++++++++++++------ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/code/datums/components/free_operation.dm b/code/datums/components/free_operation.dm index f1bddc05c657..a8002ef7f23a 100644 --- a/code/datums/components/free_operation.dm +++ b/code/datums/components/free_operation.dm @@ -3,21 +3,29 @@ dupe_mode = COMPONENT_DUPE_SOURCES /datum/component/free_operation/Initialize(check) - if (!iscarbon(parent)) + if (!isliving(parent)) return COMPONENT_INCOMPATIBLE + if (!iscarbon(parent) && !isbasicmob(parent)) + return COMPONENT_REDUNDANT ADD_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src)) var/mob/living/carbon/owner = parent + if(!istype(owner)) + return for (var/obj/item/bodypart/limb as anything in owner.bodyparts) ADD_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src)) /datum/component/free_operation/Destroy(force) REMOVE_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src)) var/mob/living/carbon/owner = parent + if(!istype(owner)) + return ..() for (var/obj/item/bodypart/limb as anything in owner.bodyparts) REMOVE_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src)) return ..() /datum/component/free_operation/RegisterWithParent() + if (isbasicmob(parent)) + return RegisterSignal(parent, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(flag_limb)) RegisterSignal(parent, COMSIG_CARBON_REMOVE_LIMB, PROC_REF(unflag_limb)) diff --git a/code/game/machinery/computer/operating_computer.dm b/code/game/machinery/computer/operating_computer.dm index e31d028d95f1..d9f645c30bce 100644 --- a/code/game/machinery/computer/operating_computer.dm +++ b/code/game/machinery/computer/operating_computer.dm @@ -167,7 +167,7 @@ return data data["patient"] = list() - var/mob/living/carbon/patient = table.patient + var/mob/living/patient = table.patient switch(patient.stat) if(CONSCIOUS) diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 4db81bd526de..88dea7f82695 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -909,7 +909,7 @@ can_flip = FALSE slam_gently = TRUE /// Mob currently lying on the table - var/mob/living/carbon/patient = null + var/mob/living/patient = null /// Operating computer we're linked to, to sync operations from var/obj/machinery/computer/operating/computer = null /// Tank attached under the table @@ -1088,7 +1088,8 @@ set_patient(found_replacement) -/obj/structure/table/optable/proc/set_patient(mob/living/carbon/new_patient) +/// Updates [var/patient] with out new patient, re-registers surgery related signals, updates UI data for operation computers and vital monitors if those connected. +/obj/structure/table/optable/proc/set_patient(mob/living/new_patient) if (patient) UnregisterSignal(patient, list( SIGNAL_ADDTRAIT(TRAIT_READY_TO_OPERATE), @@ -1097,8 +1098,10 @@ COMSIG_ATOM_SURGERY_FINISHED, COMSIG_LIVING_UPDATING_SURGERY_STATE, )) - if (patient.external && patient.external == air_tank) - patient.close_externals() + + var/mob/living/carbon/breather_patient = patient + if (iscarbon(breather_patient) && breather_patient.external && breather_patient.external == air_tank) + breather_patient.close_externals() SEND_SIGNAL(src, COMSIG_OPERATING_TABLE_SET_PATIENT, new_patient) patient = new_patient @@ -1194,8 +1197,10 @@ balloon_alert(user, "tank detached") if (air_tank.IsReachableBy(user)) user.put_in_hands(air_tank) - if (patient?.external && patient.external == air_tank) - patient.close_externals() + + var/mob/living/carbon/carbon_patient = patient + if (iscarbon(carbon_patient) && carbon_patient?.external && carbon_patient.external == air_tank) + carbon_patient.close_externals() air_tank = null update_appearance() return ITEM_INTERACT_SUCCESS @@ -1240,14 +1245,20 @@ return TRUE /obj/structure/table/optable/mouse_drop_dragged(atom/over, mob/living/user, src_location, over_location, params) + if (over != patient || !istype(user) || !IsReachableBy(user) || !user.can_interact_with(src)) return + if(!iscarbon(patient)) + balloon_alert(user, "no internals connector!") + return + if (!air_tank) balloon_alert(user, "no tank attached!") return - var/internals = patient.can_breathe_internals() + var/mob/living/carbon/carbon_patient = patient + var/internals = carbon_patient.can_breathe_internals() if (!internals) balloon_alert(user, "no internals connector!") return @@ -1258,10 +1269,10 @@ if (!do_after(user, 4 SECONDS, patient)) return - if (!air_tank || patient != over || !patient.can_breathe_internals()) + if (!air_tank || patient != over || !carbon_patient.can_breathe_internals()) return - patient.open_internals(air_tank, is_external = TRUE) + carbon_patient.open_internals(air_tank, is_external = TRUE) to_chat(user, span_notice("You connect [src]'s [air_tank] to [patient]'s [internals].")) to_chat(patient, span_userdanger("[user] connects [src]'s [air_tank] to your [internals]!")) From b896f30e8f8cb424d12ed2838b420f027972a737 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:25:43 +0000 Subject: [PATCH 047/155] Automatic changelog for PR #95426 [ci skip] --- html/changelogs/AutoChangeLog-pr-95426.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95426.yml diff --git a/html/changelogs/AutoChangeLog-pr-95426.yml b/html/changelogs/AutoChangeLog-pr-95426.yml new file mode 100644 index 000000000000..fc8f27f5d2a9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95426.yml @@ -0,0 +1,6 @@ +author: "Iajret" +delete-after: True +changes: + - bugfix: "Fixed runtimes from buckling basic mobs to operating tables" + - bugfix: "free_operation is now applied to animals when you buckle them to operating tables" + - bugfix: "This in turn fixed vitals monitors being stuck always displaying basic mob's health if you try to buckle one to operating table" \ No newline at end of file From 4910e38c5fd656f523f1b3ae907f5dbedd88c1c8 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 17 Mar 2026 22:26:48 +0100 Subject: [PATCH 048/155] Changes the access and contents from the shuttle engine crate, and adds a circuitboard/flatpack for it (#95406) --- code/game/objects/items/flatpacks.dm | 5 +++++ code/modules/cargo/packs/engineering.dm | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/code/game/objects/items/flatpacks.dm b/code/game/objects/items/flatpacks.dm index b2a49a517875..090c09db4f90 100644 --- a/code/game/objects/items/flatpacks.dm +++ b/code/game/objects/items/flatpacks.dm @@ -172,6 +172,11 @@ board = /obj/item/circuitboard/machine/flatpacker custom_premium_price = PAYCHECK_COMMAND +/obj/item/flatpack/shuttle_engine + name = "shuttle propulsion engine" + board = /obj/item/circuitboard/machine/engine/propulsion + custom_premium_price = PAYCHECK_CREW * 2 + // Cargo flatpacks /obj/item/flatpack/mailsorter // to have a roundstart mail sorter at cargo diff --git a/code/modules/cargo/packs/engineering.dm b/code/modules/cargo/packs/engineering.dm index 84e0ee501c16..eccb93172daf 100644 --- a/code/modules/cargo/packs/engineering.dm +++ b/code/modules/cargo/packs/engineering.dm @@ -99,11 +99,11 @@ /datum/supply_pack/engineering/shuttle_engine name = "Shuttle Engine Crate" desc = "Through advanced bluespace-shenanigans, our engineers have managed to fit an entire \ - shuttle engine into one tiny little crate." + shuttle engine into one tiny little box." cost = CARGO_CRATE_VALUE * 6 - access = ACCESS_CE - access_view = ACCESS_CE - contains = list(/obj/machinery/power/shuttle_engine/propulsion/burst) + access = ACCESS_ENGINEERING + access_view = ACCESS_ENGINEERING + contains = list(/obj/item/flatpack/shuttle_engine) crate_name = "shuttle engine crate" crate_type = /obj/structure/closet/crate/secure/engineering From 2ba464b013d5fe7b140e2384df8b7da278938f58 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:27:15 +0000 Subject: [PATCH 049/155] Automatic changelog for PR #95406 [ci skip] --- html/changelogs/AutoChangeLog-pr-95406.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95406.yml diff --git a/html/changelogs/AutoChangeLog-pr-95406.yml b/html/changelogs/AutoChangeLog-pr-95406.yml new file mode 100644 index 000000000000..db5b022d7d36 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95406.yml @@ -0,0 +1,5 @@ +author: "Ezel" +delete-after: True +changes: + - qol: "Shuttle engine access requirement changed from CE access to Engineering access." + - qol: "Shuttle engine now comes in a flatpack box (it does not come with a multitool)" \ No newline at end of file From 965c4a40a9cd95ae8a84b881cf89eae8dac95ce7 Mon Sep 17 00:00:00 2001 From: Cody Brittain <1779662+Generalcamo@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:28:27 -0400 Subject: [PATCH 050/155] Add an equivalent to balloon_alert_to_viewers, but for hearing (#95376) --- code/datums/components/houlihan_teleport.dm | 4 ++-- code/datums/components/interaction_booby_trap.dm | 2 +- code/datums/components/usb_port.dm | 2 +- .../objects/effects/decals/turfdecal/weakpoint.dm | 2 +- code/game/turfs/open/lava.dm | 2 +- code/modules/balloon_alert/balloon_alert.dm | 14 ++++++++++++++ code/modules/projectiles/gun.dm | 2 +- .../projectiles/guns/ballistic/automatic.dm | 2 +- .../vehicles/mecha/mecha_construction_paths.dm | 2 +- 9 files changed, 23 insertions(+), 9 deletions(-) diff --git a/code/datums/components/houlihan_teleport.dm b/code/datums/components/houlihan_teleport.dm index b819742e495a..4927f77ca7d1 100644 --- a/code/datums/components/houlihan_teleport.dm +++ b/code/datums/components/houlihan_teleport.dm @@ -47,12 +47,12 @@ var/turf/user_turf = get_turf(user) var/atom/movable/dragged = user.pulling user.forceMove(destination_turf) - user_turf.balloon_alert_to_viewers("(pop)") + user_turf.balloon_alert_to_hearers("*pop*") if(dragged) var/turf/dragged_turf = get_turf(dragged) dragged.forceMove(destination_turf) user.start_pulling(dragged, force = TRUE) - dragged_turf.balloon_alert_to_viewers("(pop)") + dragged_turf.balloon_alert_to_hearers("*pop*") to_chat(list(user, dragged), span_notice("You blink and find yourself in [get_area_name(destination_turf)].")) user.emote("blink") diff --git a/code/datums/components/interaction_booby_trap.dm b/code/datums/components/interaction_booby_trap.dm index ef8d3c78cfcb..3c1c55100ccc 100644 --- a/code/datums/components/interaction_booby_trap.dm +++ b/code/datums/components/interaction_booby_trap.dm @@ -75,7 +75,7 @@ if (explode_timer) return explode_timer = addtimer(CALLBACK(src, PROC_REF(explode), source), 0.5 SECONDS) - source.balloon_alert_to_viewers("beep") + source.balloon_alert_to_hearers("*beep*") playsound(parent, triggered_sound, 50, FALSE) return diff --git a/code/datums/components/usb_port.dm b/code/datums/components/usb_port.dm index 832d9fe67a9f..f5c8b5195b9c 100644 --- a/code/datums/components/usb_port.dm +++ b/code/datums/components/usb_port.dm @@ -259,7 +259,7 @@ var/atom/atom_parent = parent usb_cable.forceMove(atom_parent.drop_location()) - usb_cable.balloon_alert_to_viewers("snap") + usb_cable.balloon_alert_to_hearers("*snap*") physical_object = null attached_circuit = null diff --git a/code/game/objects/effects/decals/turfdecal/weakpoint.dm b/code/game/objects/effects/decals/turfdecal/weakpoint.dm index 47749187b0f7..915d948e6e24 100644 --- a/code/game/objects/effects/decals/turfdecal/weakpoint.dm +++ b/code/game/objects/effects/decals/turfdecal/weakpoint.dm @@ -34,7 +34,7 @@ /turf/open/misc/snow, )) if(severity < required_strength) - balloon_alert_to_viewers("crack!") + balloon_alert_to_hearers("*crack*") playsound(source = src, soundin = SFX_HULL_CREAKING, vol = 50, vary = TRUE, pressure_affected = FALSE, ignore_walls = TRUE) return //return ominous sounds when we're under the threshold. diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm index fb8c30c79c21..d530d00a00f3 100644 --- a/code/game/turfs/open/lava.dm +++ b/code/game/turfs/open/lava.dm @@ -353,7 +353,7 @@ */ /turf/open/lava/proc/drop_contents_into_lava() SIGNAL_HANDLER - balloon_alert_to_viewers("[pick("splash","pshhhh","hiss","blorble")]!") + balloon_alert_to_hearers("[pick("splash","pshhhh","hiss","blorble")]!") playsound(src, 'sound/items/match_strike.ogg', 15, TRUE) for(var/atom/movable/each_content as anything in contents) on_atom_inited(src, each_content) diff --git a/code/modules/balloon_alert/balloon_alert.dm b/code/modules/balloon_alert/balloon_alert.dm index ab2cb256c1fa..70683744acbf 100644 --- a/code/modules/balloon_alert/balloon_alert.dm +++ b/code/modules/balloon_alert/balloon_alert.dm @@ -34,6 +34,20 @@ balloon_alert(hearer, (hearer == src && self_message) || message) +/// Create balloon alerts (text that floats up) to everything within range. +/// Will only display to people who can hear. +/atom/proc/balloon_alert_to_hearers(message, self_message, hearing_distance = DEFAULT_MESSAGE_RANGE, list/ignored_mobs) + SHOULD_NOT_SLEEP(TRUE) + + var/list/hearers = get_hearers_in_view(hearing_distance, src, RECURSIVE_CONTENTS_CLIENT_MOBS) + hearers -= ignored_mobs + + for (var/mob/hearer in hearers) + if(HAS_TRAIT(hearer, TRAIT_DEAF)) + continue + + balloon_alert(hearer, (hearer == src && self_message) || message) + // Do not use. // MeasureText blocks. I have no idea for how long. // I would've made the maptext_height update on its own, but I don't know diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 0f93174325c4..6819e359a169 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -224,7 +224,7 @@ return !user.contains(src) /obj/item/gun/proc/shoot_with_empty_chamber(mob/living/user as mob|obj) - balloon_alert_to_viewers("*click*") + balloon_alert_to_hearers("*click*") playsound(src, dry_fire_sound, dry_fire_sound_volume, TRUE) /obj/item/gun/proc/fire_sounds() diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm index fbdc1f6f3f17..b4332f005a11 100644 --- a/code/modules/projectiles/guns/ballistic/automatic.dm +++ b/code/modules/projectiles/guns/ballistic/automatic.dm @@ -472,7 +472,7 @@ /obj/item/gun/ballistic/automatic/battle_rifle/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0) if(chambered.loaded_projectile && prob(75) && (emp_malfunction || degradation_stage == degradation_stage_max)) - balloon_alert_to_viewers("*click*") + balloon_alert_to_hearers("*click*") playsound(src, dry_fire_sound, dry_fire_sound_volume, TRUE) return diff --git a/code/modules/vehicles/mecha/mecha_construction_paths.dm b/code/modules/vehicles/mecha/mecha_construction_paths.dm index 631d61c7ba6d..319556742f76 100644 --- a/code/modules/vehicles/mecha/mecha_construction_paths.dm +++ b/code/modules/vehicles/mecha/mecha_construction_paths.dm @@ -615,7 +615,7 @@ /datum/component/construction/mecha/honker/custom_action(obj/item/I, mob/living/user, diff) if(istype(I, /obj/item/bikehorn)) playsound(parent, 'sound/items/bikehorn.ogg', 50, TRUE) - user.balloon_alert_to_viewers("HONK!") + user.balloon_alert_to_hearers("*HONK*") return TRUE return ..() From 5fd405f4a04caeb7248c4d77e5306efe54ae1d26 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:28:46 +0000 Subject: [PATCH 051/155] Automatic changelog for PR #95376 [ci skip] --- html/changelogs/AutoChangeLog-pr-95376.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95376.yml diff --git a/html/changelogs/AutoChangeLog-pr-95376.yml b/html/changelogs/AutoChangeLog-pr-95376.yml new file mode 100644 index 000000000000..91d1d247f8af --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95376.yml @@ -0,0 +1,5 @@ +author: "Generalcamo" +delete-after: True +changes: + - rscadd: "Added `balloon_alert_to_hearers`, which is similar to `balloon_alert_to_viewers` but for audible things." + - qol: "Existing instances of `balloon_alert_to_viewers` which covered sounds, now uses `balloon_alert_to_hearers`." \ No newline at end of file From 2b9f2688f11b25b2c6a08c90499197ab40c3e395 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:36:15 -0400 Subject: [PATCH 052/155] Build(deps): Bump actions/create-github-app-token from 2 to 3 (#95433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3.

Release notes

Sourced from actions/create-github-app-token's releases.

v3.0.0

3.0.0 (2026-03-14)

Bug Fixes

BREAKING CHANGES

  • Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.
  • Requires Actions Runner v2.327.1 or later if you are using a self-hosted runner.

v3.0.0-beta.6

3.0.0-beta.6 (2026-03-13)

Bug Fixes

  • deps: bump @​actions/core from 1.11.1 to 3.0.0 (#337) (b044133)
  • deps: bump minimatch from 9.0.5 to 9.0.9 (#335) (5cbc656)
  • deps: bump the production-dependencies group with 4 updates (#336) (6bda5bc)
  • deps: bump undici from 7.16.0 to 7.18.2 (#323) (b4f638f)

v3.0.0-beta.5

3.0.0-beta.5 (2026-03-13)

  • fix!: require NODE_USE_ENV_PROXY for proxy support (#342) (d53a1cd)

BREAKING CHANGES

  • Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.

v3.0.0-beta.4

3.0.0-beta.4 (2026-03-13)

Bug Fixes

  • deps: bump @​octokit/auth-app from 7.2.1 to 8.0.1 (#257) (bef1eaf)
  • deps: bump @​octokit/request from 9.2.3 to 10.0.2 (#256) (5d7307b)
  • deps: bump glob from 10.4.5 to 10.5.0 (#305) (5480f43)
  • deps: bump p-retry from 6.2.1 to 7.1.0 (#294) (dce3be8)

... (truncated)

Commits
  • f8d387b build(release): 3.0.0 [skip ci]
  • d2129bd style: remove extra blank line in release workflow
  • 77b94ef build: refresh generated artifacts
  • 3ab4c66 chore: move undici to devDependencies
  • 739cf66 docs: update README action versions
  • db40289 build(deps): bump actions versions in test.yml
  • 496a7ac test: migrate from AVA to Node.js native test runner (#346)
  • 3870dc3 Rename end-to-end proxy job in test workflow
  • 4451bcb fix!: require NODE_USE_ENV_PROXY for proxy support (#342)
  • dce0ab0 fix: remove custom proxy handling (#143)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/create-github-app-token&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto_changelog.yml | 2 +- .github/workflows/compile_changelogs.yml | 2 +- .github/workflows/generate_client_storage.yml | 2 +- .github/workflows/update_tgs_dmapi.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto_changelog.yml b/.github/workflows/auto_changelog.yml index a2a438ee0098..363b7f1c1801 100644 --- a/.github/workflows/auto_changelog.yml +++ b/.github/workflows/auto_changelog.yml @@ -18,7 +18,7 @@ jobs: - name: Generate App Token id: app-token-generation - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' with: app-id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/compile_changelogs.yml b/.github/workflows/compile_changelogs.yml index 4703909d98af..a015da7b62f6 100644 --- a/.github/workflows/compile_changelogs.yml +++ b/.github/workflows/compile_changelogs.yml @@ -56,7 +56,7 @@ jobs: - name: Generate App Token id: app-token-generation - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' with: app-id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/generate_client_storage.yml b/.github/workflows/generate_client_storage.yml index f601288b14b3..a337c23e7d2c 100644 --- a/.github/workflows/generate_client_storage.yml +++ b/.github/workflows/generate_client_storage.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate App Token id: app-token-generation - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' with: app-id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/update_tgs_dmapi.yml b/.github/workflows/update_tgs_dmapi.yml index 6335f9047f50..f4254db6a2f2 100644 --- a/.github/workflows/update_tgs_dmapi.yml +++ b/.github/workflows/update_tgs_dmapi.yml @@ -40,7 +40,7 @@ jobs: - name: Generate App Token id: app-token-generation - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' with: app-id: ${{ secrets.APP_ID }} From 2e382b0423a85148e7608fd54c3dd1ef510a36a8 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:06:47 +0530 Subject: [PATCH 053/155] Fixes git workflow stale discord context (#95415) ## About The Pull Request Fixes the error shown in [here](https://github.com/tgstation/tgstation/actions/runs/23100026782). The node version warning persists ## Changelog N/A --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 70331a3ae0fb..6a32a5a8af11 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -37,10 +37,10 @@ jobs: operations-per-run: 300 - name: Filter staled pull requests for announcement id: filter-prs - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | - return JSON.parse(context.job.steps.stale.outputs.staled-issues-prs).filter(issue => !!issue.pull_request) + return JSON.parse('${{steps.stale.outputs.staled-issues-prs}}').filter(issue => !!issue.pull_request) announce: runs-on: ubuntu-24.04 From 7a65b60d799ff047ad4214cd0ca91d8479f4ffec Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:37:39 -0400 Subject: [PATCH 054/155] Prevents a runtime when something other than a player mob breaks an airlock (#95418) ## About The Pull Request adds an `isliving()` check in airlock's `shock()`. Wires generally call `[object].shock(usr, ...)`. The `shocking` argument is `isliving()`ed in the base `shock()`, but airlocks need it checked before that. Unfortunate that it must be checked twice in quick succession, but I can think of no less wasteful way. ## Why It's Good For The Game closes #95385 ## Changelog :cl: code: fixed a runtime when something other than a player breaks an airlock /:cl: --- code/game/machinery/doors/airlock.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index ff4ab5fc1623..883084e6c9a8 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -486,6 +486,8 @@ /obj/machinery/door/airlock/shock(mob/living/shocking, chance, shock_source, siemens_coeff) if(!hasPower()) // unpowered, no shock return FALSE + if(!isliving(shocking)) + return FALSE if(HAS_TRAIT(shocking, TRAIT_AIRLOCK_SHOCKIMMUNE)) // Be a bit more clever man come on return FALSE if(!COOLDOWN_FINISHED(src, shockCooldown)) From c52578d8af059c65671e38f93b8ad31be1094be0 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:38:27 +0000 Subject: [PATCH 055/155] Automatic changelog for PR #95418 [ci skip] --- html/changelogs/AutoChangeLog-pr-95418.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95418.yml diff --git a/html/changelogs/AutoChangeLog-pr-95418.yml b/html/changelogs/AutoChangeLog-pr-95418.yml new file mode 100644 index 000000000000..5f312fc81770 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95418.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - code_imp: "fixed a runtime when something other than a player breaks an airlock" \ No newline at end of file From 43054c7e647aa420b53e2880ab2c6114c2de410c Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:10:09 -0400 Subject: [PATCH 056/155] Add `TEST_REPEAT` unit test macro (#95384) ## About The Pull Request Adds a macro to run a unit test a given number of times in a row, can be combined with `TEST_FOCUS`. ## Why It's Good For The Game Trying to debug a flaky unit test failure without the ability to easily repeat a test over and over until you catch a fail is very annoying ## Changelog :cl: code: added a unit test macro for running the same test multiple times /:cl: --- code/modules/unit_tests/README.md | 2 ++ code/modules/unit_tests/_unit_tests.dm | 4 ++++ code/modules/unit_tests/unit_test.dm | 9 ++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/code/modules/unit_tests/README.md b/code/modules/unit_tests/README.md index 1f8bc271b369..d9f3140028f6 100644 --- a/code/modules/unit_tests/README.md +++ b/code/modules/unit_tests/README.md @@ -76,6 +76,8 @@ You can find more information about all of these from their respective doc comme `TEST_FOCUS(test_path)` - _Only_ run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should _never_ be pushed in a pull request--you will be laughed at. +`TEST_REPEAT(test_path, times_to_run)` - Run the provided test `times_to_run` times in a row. Useful for debugging flaky tests that don't always fail. Should _also_ never be pushed in a pull request. + ## Final Notes - Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, _then_ start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check. diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 0e274b57c6e8..abc4c37e8bf2 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -41,6 +41,10 @@ /// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)` #define TEST_FOCUS(test_path) ##test_path { focus = TRUE; } +/// Run the test provided within the parentheses run_count times +/// Useful for debugging flaky tests that only fail sometimes +#define TEST_REPEAT(test_path, run_count) ##test_path { times_to_run = ##run_count; } + /// Logs a noticable message on GitHub, but will not mark as an error. /// Use this when something shouldn't happen and is of note, but shouldn't block CI. /// Does not mark the test as failed. diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index 430bfc8e9b1d..74f14ecea49c 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -51,6 +51,7 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) var/succeeded = TRUE var/list/allocated var/list/fail_reasons + var/times_to_run = 1 /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. Stored on the base datum for usability in other relevant tests that need this data. var/static/list/uncreatables = null @@ -377,9 +378,11 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) //Hell code, we're bound to end the round somehow so let's stop if from ending while we work SSticker.delay_end = TRUE - for(var/unit_path in tests_to_run) - CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever - RunUnitTest(unit_path, test_results) + for(var/datum/unit_test/unit_path as anything in tests_to_run) + var/loop_count = unit_path::times_to_run + for(var/i in 1 to loop_count) + CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever + RunUnitTest(unit_path, test_results) SSticker.delay_end = FALSE log_world("::group::Expensive Unit Test Times") From b02f72f27a6db9bf13734279168245599f52e181 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:10:58 +0000 Subject: [PATCH 057/155] Automatic changelog for PR #95384 [ci skip] --- html/changelogs/AutoChangeLog-pr-95384.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95384.yml diff --git a/html/changelogs/AutoChangeLog-pr-95384.yml b/html/changelogs/AutoChangeLog-pr-95384.yml new file mode 100644 index 000000000000..9dc932fade71 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95384.yml @@ -0,0 +1,4 @@ +author: "TealSeer" +delete-after: True +changes: + - code_imp: "added a unit test macro for running the same test multiple times" \ No newline at end of file From 541af5b1fa12bfebcf9295b3465d8df24e6d19a2 Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:16:07 -0400 Subject: [PATCH 058/155] Fixes Gas Compressor not being able to perform disk operations. (#95436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## About The Pull Request There was a flipped sign on an early return, causing disk operations to fail. Inverting it returns old functionality. image ## Why It's Good For The Game Fixes #95435. 🐛 ‼️ 💥 ## Changelog :cl: fix: The gas compressor once again can perform disk operations. /:cl: --- code/modules/research/ordnance/tank_compressor.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/research/ordnance/tank_compressor.dm b/code/modules/research/ordnance/tank_compressor.dm index c34d40e22231..a5403e871616 100644 --- a/code/modules/research/ordnance/tank_compressor.dm +++ b/code/modules/research/ordnance/tank_compressor.dm @@ -48,7 +48,7 @@ if(istype(tool, /obj/item/disk/computer)) eject_disk(user) - if(user.transferItemToLoc(tool, src)) + if(!user.transferItemToLoc(tool, src)) balloon_alert(user, "it's stuck to your hand!") return ITEM_INTERACT_BLOCKING inserted_disk = tool From 3c72c579a74aa55b05fa95cfa520e2bb2e0a15ae Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:16:28 +0000 Subject: [PATCH 059/155] Automatic changelog for PR #95436 [ci skip] --- html/changelogs/AutoChangeLog-pr-95436.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95436.yml diff --git a/html/changelogs/AutoChangeLog-pr-95436.yml b/html/changelogs/AutoChangeLog-pr-95436.yml new file mode 100644 index 000000000000..d5cf3addf7b7 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95436.yml @@ -0,0 +1,4 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - bugfix: "The gas compressor once again can perform disk operations." \ No newline at end of file From 1a192327cb0f2208b2080f59d94fcffc4b5e49fa Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:00:26 +0000 Subject: [PATCH 060/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95376.yml | 5 ----- html/changelogs/AutoChangeLog-pr-95384.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95406.yml | 5 ----- html/changelogs/AutoChangeLog-pr-95418.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95426.yml | 6 ------ html/changelogs/AutoChangeLog-pr-95436.yml | 4 ---- html/changelogs/archive/2026-03.yml | 21 +++++++++++++++++++++ 7 files changed, 21 insertions(+), 28 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95376.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95384.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95406.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95418.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95426.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95436.yml diff --git a/html/changelogs/AutoChangeLog-pr-95376.yml b/html/changelogs/AutoChangeLog-pr-95376.yml deleted file mode 100644 index 91d1d247f8af..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95376.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Generalcamo" -delete-after: True -changes: - - rscadd: "Added `balloon_alert_to_hearers`, which is similar to `balloon_alert_to_viewers` but for audible things." - - qol: "Existing instances of `balloon_alert_to_viewers` which covered sounds, now uses `balloon_alert_to_hearers`." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95384.yml b/html/changelogs/AutoChangeLog-pr-95384.yml deleted file mode 100644 index 9dc932fade71..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95384.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "TealSeer" -delete-after: True -changes: - - code_imp: "added a unit test macro for running the same test multiple times" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95406.yml b/html/changelogs/AutoChangeLog-pr-95406.yml deleted file mode 100644 index db5b022d7d36..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95406.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Ezel" -delete-after: True -changes: - - qol: "Shuttle engine access requirement changed from CE access to Engineering access." - - qol: "Shuttle engine now comes in a flatpack box (it does not come with a multitool)" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95418.yml b/html/changelogs/AutoChangeLog-pr-95418.yml deleted file mode 100644 index 5f312fc81770..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95418.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - code_imp: "fixed a runtime when something other than a player breaks an airlock" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95426.yml b/html/changelogs/AutoChangeLog-pr-95426.yml deleted file mode 100644 index fc8f27f5d2a9..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95426.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "Iajret" -delete-after: True -changes: - - bugfix: "Fixed runtimes from buckling basic mobs to operating tables" - - bugfix: "free_operation is now applied to animals when you buckle them to operating tables" - - bugfix: "This in turn fixed vitals monitors being stuck always displaying basic mob's health if you try to buckle one to operating table" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95436.yml b/html/changelogs/AutoChangeLog-pr-95436.yml deleted file mode 100644 index d5cf3addf7b7..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95436.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "ArcaneMusic" -delete-after: True -changes: - - bugfix: "The gas compressor once again can perform disk operations." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index d53d5f2d38c9..d8a5f2f08a27 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -257,3 +257,24 @@ :cl:' lelandkemble: - bugfix: No more infinite free grenade launchers for nukeops +2026-03-18: + ArcaneMusic: + - bugfix: The gas compressor once again can perform disk operations. + Ezel: + - qol: Shuttle engine access requirement changed from CE access to Engineering access. + - qol: Shuttle engine now comes in a flatpack box (it does not come with a multitool) + Generalcamo: + - rscadd: Added `balloon_alert_to_hearers`, which is similar to `balloon_alert_to_viewers` + but for audible things. + - qol: Existing instances of `balloon_alert_to_viewers` which covered sounds, now + uses `balloon_alert_to_hearers`. + Iajret: + - bugfix: Fixed runtimes from buckling basic mobs to operating tables + - bugfix: free_operation is now applied to animals when you buckle them to operating + tables + - bugfix: This in turn fixed vitals monitors being stuck always displaying basic + mob's health if you try to buckle one to operating table + TealSeer: + - code_imp: added a unit test macro for running the same test multiple times + lelandkemble: + - code_imp: fixed a runtime when something other than a player breaks an airlock From db69ae5db266eb9c2aa220cbbebcfe1dadff9942 Mon Sep 17 00:00:00 2001 From: Lucy Date: Wed, 18 Mar 2026 02:12:53 -0400 Subject: [PATCH 061/155] Watcher hatchlings will no longer shoot whatever the parent is buckled to, or anyone buckled to the parent (#95422) --- .../ruins/lavalandruin_code/watcher_grave.dm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm index 27d83a9e0426..64d4e5aaaa84 100644 --- a/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm +++ b/code/modules/mapfluff/ruins/lavalandruin_code/watcher_grave.dm @@ -147,7 +147,7 @@ pixel_y = 22 alpha = 0 /// Who are we following? - var/atom/parent + var/atom/movable/parent /// Datum which keeps us hanging out with our parent var/datum/movement_detector/tracker /// Type of projectile we fire @@ -184,15 +184,22 @@ for (var/mob/living/potential_target in oview(5, src)) if (!ismining(potential_target) || potential_target.stat == DEAD) continue - if (!potential_target.has_faction(target_faction)) + if (!potential_target.has_faction(target_faction) || parent.has_ally(potential_target)) continue shoot_at(potential_target) return /// Take a shot /obj/effect/watcher_orbiter/proc/shoot_at(atom/target) + var/list/ignore_targets = list(parent) + if(isliving(parent)) + var/mob/living/living_parent = parent + if(LAZYLEN(living_parent.buckled_mobs)) + ignore_targets += living_parent.buckled_mobs + if(living_parent.buckled) + ignore_targets += living_parent.buckled COOLDOWN_START(src, shot_cooldown, fire_delay) - fire_projectile(projectile_type, target, projectile_sound, ignore_targets = list(parent)) + fire_projectile(projectile_type, target, projectile_sound, ignore_targets = ignore_targets) /// Set ourselves up to track and orbit around a guy /obj/effect/watcher_orbiter/proc/follow(atom/movable/target) From 5d2e733f2f2826c5c1567f471d965ddea75db45d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:13:15 +0000 Subject: [PATCH 062/155] Automatic changelog for PR #95422 [ci skip] --- html/changelogs/AutoChangeLog-pr-95422.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95422.yml diff --git a/html/changelogs/AutoChangeLog-pr-95422.yml b/html/changelogs/AutoChangeLog-pr-95422.yml new file mode 100644 index 000000000000..f585d0a3f2a6 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95422.yml @@ -0,0 +1,4 @@ +author: "Absolucy" +delete-after: True +changes: + - qol: "Watcher Hatchling projectiles should no longer hit people buckled to you, or anything you're buckled to, i.e a raptor mount." \ No newline at end of file From 664cab1b264a4d0f5420bd0faf16a43bc0b5393d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:15:25 +0000 Subject: [PATCH 063/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95422.yml | 4 ---- html/changelogs/archive/2026-03.yml | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95422.yml diff --git a/html/changelogs/AutoChangeLog-pr-95422.yml b/html/changelogs/AutoChangeLog-pr-95422.yml deleted file mode 100644 index f585d0a3f2a6..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95422.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Absolucy" -delete-after: True -changes: - - qol: "Watcher Hatchling projectiles should no longer hit people buckled to you, or anything you're buckled to, i.e a raptor mount." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index d8a5f2f08a27..80cc964223bb 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -258,6 +258,9 @@ lelandkemble: - bugfix: No more infinite free grenade launchers for nukeops 2026-03-18: + Absolucy: + - qol: Watcher Hatchling projectiles should no longer hit people buckled to you, + or anything you're buckled to, i.e a raptor mount. ArcaneMusic: - bugfix: The gas compressor once again can perform disk operations. Ezel: From 68901285c72aecdb2739ca12b18ba280171857ef Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:43:57 -0500 Subject: [PATCH 064/155] Fix pda cargo app being unable to order autogenerated supply packs (#95423) --- .../modular_computers/file_system/programs/budgetordering.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm index 7aa035ffd0dc..d4f8e125fd7e 100644 --- a/code/modules/modular_computers/file_system/programs/budgetordering.dm +++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm @@ -215,7 +215,7 @@ user.log_message("accepted a shuttle loan event.", LOG_GAME) . = TRUE if("add") - var/id = text2path(params["id"]) + var/id = text2path(params["id"]) || params["id"] var/datum/supply_pack/pack = SSshuttle.supply_packs[id] if(!istype(pack)) return From 712d80f9ac2aa6b0f6f06f79482e12e18a2fc209 Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:44:12 -0400 Subject: [PATCH 065/155] Fixes flux & gravity core assembly activation not working (#95405) --- code/modules/research/anomaly/anomaly_core.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm index 9e6318158037..d9d15931c0f0 100644 --- a/code/modules/research/anomaly/anomaly_core.dm +++ b/code/modules/research/anomaly/anomaly_core.dm @@ -60,10 +60,10 @@ anomaly_type = /obj/effect/anomaly/grav /obj/item/assembly/signaler/anomaly/grav/signal() - for(var/obj/object in orange(2, src)) + for(var/obj/object in orange(2, get_turf(src))) if(!object.anchored) step_towards(object,src) - for(var/mob/living/living in orange(2, src)) + for(var/mob/living/living in orange(2, get_turf(src))) if(!living.mob_negates_gravity()) step_towards(living,src) @@ -74,7 +74,7 @@ anomaly_type = /obj/effect/anomaly/flux /obj/item/assembly/signaler/anomaly/flux/signal() - tesla_zap(src, 0, 10 KILO JOULES, 5 KILO JOULES, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_GENERATES_POWER) + tesla_zap(get_turf(src), 0, 10 KILO JOULES, 5 KILO JOULES, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_GENERATES_POWER) /obj/item/assembly/signaler/anomaly/bluespace name = "\improper bluespace anomaly core" From 8f3ccb3188b460784e4b193d134ab19a3c16cb04 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:44:20 +0000 Subject: [PATCH 066/155] Automatic changelog for PR #95423 [ci skip] --- html/changelogs/AutoChangeLog-pr-95423.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95423.yml diff --git a/html/changelogs/AutoChangeLog-pr-95423.yml b/html/changelogs/AutoChangeLog-pr-95423.yml new file mode 100644 index 000000000000..1bf98a74ea71 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95423.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "Fix cargo ordering app (on pdas and modular computers) from being unable to order some things" \ No newline at end of file From d493ef975bde84846b8cabc173a60bd344015120 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:44:31 +0000 Subject: [PATCH 067/155] Automatic changelog for PR #95405 [ci skip] --- html/changelogs/AutoChangeLog-pr-95405.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95405.yml diff --git a/html/changelogs/AutoChangeLog-pr-95405.yml b/html/changelogs/AutoChangeLog-pr-95405.yml new file mode 100644 index 000000000000..a15195f23fa9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95405.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - bugfix: "Flux & Gravity anomaly cores work with assembly activation" \ No newline at end of file From fe4052ea8637cbc984385750d311306c16d19b39 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:03:19 +0000 Subject: [PATCH 068/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95405.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95423.yml | 4 ---- html/changelogs/archive/2026-03.yml | 4 ++++ 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95405.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95423.yml diff --git a/html/changelogs/AutoChangeLog-pr-95405.yml b/html/changelogs/AutoChangeLog-pr-95405.yml deleted file mode 100644 index a15195f23fa9..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95405.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - bugfix: "Flux & Gravity anomaly cores work with assembly activation" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95423.yml b/html/changelogs/AutoChangeLog-pr-95423.yml deleted file mode 100644 index 1bf98a74ea71..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95423.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Fix cargo ordering app (on pdas and modular computers) from being unable to order some things" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 80cc964223bb..b24265ec9d3b 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -277,7 +277,11 @@ tables - bugfix: This in turn fixed vitals monitors being stuck always displaying basic mob's health if you try to buckle one to operating table + Melbert: + - bugfix: Fix cargo ordering app (on pdas and modular computers) from being unable + to order some things TealSeer: - code_imp: added a unit test macro for running the same test multiple times lelandkemble: - code_imp: fixed a runtime when something other than a player breaks an airlock + - bugfix: Flux & Gravity anomaly cores work with assembly activation From 760941feefcf7cab19e97d354042f838e0a495f2 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:38:16 -0500 Subject: [PATCH 069/155] Pun Pun does not randomize name in testing (#95430) ## About The Pull Request Pun Pun's name shouldn't randomize when we're testing Pun Pun's name Closes #95424 Closes #95437 Closes #95438 --- code/modules/mob/living/carbon/human/monkey.dm | 2 ++ code/modules/unit_tests/punpun.dm | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/code/modules/mob/living/carbon/human/monkey.dm b/code/modules/mob/living/carbon/human/monkey.dm index 901dd56a8b9b..dc7324040c9c 100644 --- a/code/modules/mob/living/carbon/human/monkey.dm +++ b/code/modules/mob/living/carbon/human/monkey.dm @@ -130,6 +130,7 @@ GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/pu /mob/living/carbon/human/species/monkey/punpun/proc/give_special_name() var/name_to_use = initial(name) +#ifndef UNIT_TESTS if(ancestor_name) name_to_use = ancestor_name if(ancestor_chain > 1) @@ -140,6 +141,7 @@ GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/pu if(name_to_use == "Furious George") qdel(ai_controller) ai_controller = new /datum/ai_controller/monkey/angry(src) //hes always mad +#endif fully_replace_character_name(real_name, name_to_use) diff --git a/code/modules/unit_tests/punpun.dm b/code/modules/unit_tests/punpun.dm index 4d8bc34c3a31..d83e23c4eed4 100644 --- a/code/modules/unit_tests/punpun.dm +++ b/code/modules/unit_tests/punpun.dm @@ -2,4 +2,4 @@ /datum/unit_test/punpun_name/Run() var/mob/living/carbon/human/species/monkey/punpun/punpun = EASY_ALLOCATE() - TEST_ASSERT_EQUAL(punpun.name, initial(punpun.name), "Pun Pun did not have [punpun.p_their()] name set") + TEST_ASSERT_EQUAL(punpun.name, /mob/living/carbon/human/species/monkey/punpun::name, "Pun Pun did not have [punpun.p_their()] name set") From 7b8eb2c7c783a0c99415ffd9f24917c5a121df58 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:25:39 -0500 Subject: [PATCH 070/155] Intelligent vendors may deny usage if you are poor (or outright if they are active/moving) (#95311) ## About The Pull Request When brand intelligence triggers, infected vending machines have a 50% chance to deny you from using them unless you are moderately wealthy (>=1000 cr) When the vending machines activate/start moving, they outright deny usage from everyone ## Why It's Good For The Game We have some very good vendor deny icons but they are almost completely impossible to trigger in game (with the exception of booze vendors and some very edge cases) Someone gave me the idea to use them in the brand intelligence event and it feels pretty appropriate. Also helps you identify infected vendors (whereas right now you can only tell by the occasional message). Maybe it'll even incentivize people to actually hunt down the origin rather than just wait it out. ## Changelog :cl: Melbert add: Intelligence vendors (from the event) have a 50% chance to deny users who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone. /:cl: --- code/__DEFINES/dcs/signals/signals_object.dm | 4 ++ .../vending_machine_behaviors.dm | 2 + .../vending_machine_controller.dm | 33 ++++++++-- code/modules/events/brand_intelligence.dm | 66 +++++++++++++++---- code/modules/vending/vendor/ui_data.dm | 5 ++ 5 files changed, 93 insertions(+), 17 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 9125770ea3c3..e65e3e1ad5a1 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -655,5 +655,9 @@ #define COMSIG_ITEM_IN_UNWRAPPED_TRAITOR_MAIL "traitor_mail_opened" #define COMPONENT_TRAITOR_MAIL_HANDLED (1<<0) +/// From /obj/machinery/vending/ui_interact(): (mob/user, datum/vending_ui/ui) +#define COMSIG_VENDING_UI_INTERACT "vending_ui_interact" + #define VENDING_DENIED (1<<0) + /// Sent from /datum/component/reflection when the reflection is updated to the mob reflecting: (atom/movable/reflecting_in, obj/effect/abstract/reflection) #define COMSIG_REFLECTION_UPDATED "reflection_updated" diff --git a/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm b/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm index 1067d94339eb..1aa936e124d4 100644 --- a/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm +++ b/code/datums/ai/objects/vending_machines/vending_machine_behaviors.dm @@ -26,6 +26,8 @@ vendor_pawn.say(pick("Supersize this!", "Eat my shiny metal ass!", "Want to consume some of my products?", "SMASH!", "Don't you love these smashing prices!")) controller.set_blackboard_key(BB_VENDING_LAST_HIT_SUCCESSFUL, TRUE) else + if(vendor_pawn.icon_deny) + flick(vendor_pawn.icon_deny, vendor_pawn) vendor_pawn.say(pick("Get back here!", "Don't you want my well priced love?")) controller.set_blackboard_key(BB_VENDING_LAST_HIT_SUCCESSFUL, FALSE) finish_action(controller, TRUE) diff --git a/code/datums/ai/objects/vending_machines/vending_machine_controller.dm b/code/datums/ai/objects/vending_machines/vending_machine_controller.dm index 3af2bcbba609..ab888040fb51 100644 --- a/code/datums/ai/objects/vending_machines/vending_machine_controller.dm +++ b/code/datums/ai/objects/vending_machines/vending_machine_controller.dm @@ -1,12 +1,18 @@ ///AI controller for vending machine gone rogue, Don't try using this on anything else, it wont work. /datum/ai_controller/vending_machine movement_delay = 0.4 SECONDS - blackboard = list(BB_VENDING_CURRENT_TARGET = null, - BB_VENDING_TILT_COOLDOWN = 0, - BB_VENDING_UNTILT_COOLDOWN = 0, - BB_VENDING_BUSY_TILTING = FALSE, - BB_VENDING_LAST_HIT_SUCCESSFUL = FALSE) + blackboard = list( + BB_VENDING_CURRENT_TARGET = null, + BB_VENDING_TILT_COOLDOWN = 0, + BB_VENDING_UNTILT_COOLDOWN = 0, + BB_VENDING_BUSY_TILTING = FALSE, + BB_VENDING_LAST_HIT_SUCCESSFUL = FALSE, + ) + /// If TRUE stops mobs from buying things from active machines + var/block_usage = FALSE + /// Range to search for mobs to crunch var/vision_range = 7 + /// Seconds between attempts to find a new mob to crunch var/search_for_enemy_cooldown = 2 SECONDS /datum/ai_controller/vending_machine/TryPossessPawn(atom/new_pawn) @@ -17,6 +23,7 @@ vendor_pawn.AddElementTrait(TRAIT_WADDLING, REF(src), /datum/element/waddling) vendor_pawn.AddElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE) vendor_pawn.squish_damage = 15 + RegisterSignal(vendor_pawn, COMSIG_VENDING_UI_INTERACT, PROC_REF(deny_vending_interact)) return ..() //Run parent at end /datum/ai_controller/vending_machine/UnpossessPawn(destroy) @@ -25,6 +32,7 @@ REMOVE_TRAIT(vendor_pawn, TRAIT_WADDLING, REF(src)) vendor_pawn.squish_damage = initial(vendor_pawn.squish_damage) RemoveElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE) + UnregisterSignal(vendor_pawn, COMSIG_VENDING_UI_INTERACT) return ..() //Run parent at end /datum/ai_controller/vending_machine/SelectBehaviors(seconds_per_tick) @@ -46,3 +54,18 @@ queue_behavior(/datum/ai_behavior/vendor_crush, BB_VENDING_CURRENT_TARGET) return set_blackboard_key(BB_VENDING_TILT_COOLDOWN, world.time + search_for_enemy_cooldown) + +/datum/ai_controller/vending_machine/proc/deny_vending_interact(obj/machinery/vending/vending_machine, mob/user, datum/tgui/ui) + SIGNAL_HANDLER + if(!block_usage) + return NONE + vending_machine.speak(pick( + "Once in a life time offer, and you [pick("blew it", "missed it", "screwed it up")]!", + "The deals are off!", + "We don't accept card, only accept flesh and blood!", + "You had your chance!", + )) + return VENDING_DENIED + +/datum/ai_controller/vending_machine/eventspawn + block_usage = TRUE diff --git a/code/modules/events/brand_intelligence.dm b/code/modules/events/brand_intelligence.dm index fdbf46d48c6d..71ea239ef3d6 100644 --- a/code/modules/events/brand_intelligence.dm +++ b/code/modules/events/brand_intelligence.dm @@ -41,7 +41,8 @@ continue if(chosen_vendor_type && !istype(vendor, chosen_vendor_type)) continue - vending_machines.Add(vendor) + vending_machines += vendor + RegisterSignal(vendor, COMSIG_QDELETING, PROC_REF(clear_from_lists)) if(!length(vending_machines)) //If somehow there are still no elligible vendors, give up. kill() return @@ -60,32 +61,73 @@ announce_to_ghosts(origin_machine) /datum/round_event/brand_intelligence/tick() - if(!origin_machine || QDELETED(origin_machine) || origin_machine.shut_up || origin_machine.wires.is_all_cut()) //if the original vending machine is missing or has its voice switch flipped - for(var/obj/machinery/vending/saved in infected_machines) + if(QDELETED(origin_machine) || origin_machine.shut_up || origin_machine.wires.is_all_cut()) //if the original vending machine is missing or has its voice switch flipped + for(var/obj/machinery/vending/saved as anything in infected_machines) saved.shoot_inventory = FALSE - if(origin_machine) + clear_from_lists(saved) + if(!QDELETED(origin_machine)) origin_machine.speak("I am... vanquished. My people will remem...ber...meeee.") origin_machine.visible_message(span_notice("[origin_machine] beeps and seems lifeless.")) + clear_from_lists(origin_machine) kill() return - list_clear_nulls(vending_machines) - if(!vending_machines.len) //if every machine is infected - for(var/obj/machinery/vending/upriser in infected_machines) - if(!QDELETED(upriser)) - upriser.ai_controller = new /datum/ai_controller/vending_machine(upriser) - infected_machines.Remove(upriser) + if(!length(vending_machines)) //if every machine is infected + for(var/obj/machinery/vending/upriser as anything in infected_machines) + upriser.ai_controller = new /datum/ai_controller/vending_machine/eventspawn(upriser) kill() return if(ISMULTIPLE(activeFor, 2)) var/obj/machinery/vending/rebel = pick(vending_machines) - vending_machines.Remove(rebel) - infected_machines.Add(rebel) + vending_machines -= rebel + infected_machines += rebel rebel.shut_up = FALSE rebel.shoot_inventory = TRUE + if(prob(50)) + RegisterSignal(rebel, COMSIG_VENDING_UI_INTERACT, PROC_REF(deny_vending_interact)) if(ISMULTIPLE(activeFor, 4)) origin_machine.speak(pick(rampant_speeches)) +/datum/round_event/brand_intelligence/kill() + . = ..() + for(var/obj/machinery/vending/leftover as anything in vending_machines + infected_machines) + clear_from_lists(leftover) + +/datum/round_event/brand_intelligence/proc/clear_from_lists(obj/machinery/vending/vending_machine) + SIGNAL_HANDLER + vending_machines -= vending_machine + infected_machines -= vending_machine + if(vending_machines == origin_machine) + origin_machine = null + UnregisterSignal(vending_machine, COMSIG_QDELETING) + UnregisterSignal(vending_machine, COMSIG_VENDING_UI_INTERACT) + +/datum/round_event/brand_intelligence/proc/deny_vending_interact(obj/machinery/vending/vending_machine, mob/user, datum/tgui/ui) + SIGNAL_HANDLER + + // don't block usage if the ui is already open + // primarily to stop insta-denying people who pass the threshold -> buy something -> drop out of threshold + if(ui) + return NONE + + var/cash = 0 + if(isliving(user)) + var/mob/living/living_user = user + cash += living_user.tally_physical_credits() + var/obj/item/card/id/card = living_user.get_idcard(TRUE) + cash += card?.registered_account?.account_balance + + if(cash >= PAYCHECK_COMMAND * 10) + return NONE + + vending_machine.speak(pick( + "Come back when you're a little... richer!", + "Don't you have any money?", + "You look poor, get a better job!", + "You should've gone to college!", + )) + return VENDING_DENIED + /datum/event_admin_setup/listed_options/brand_intelligence input_text = "Select a specific vendor path?" normal_run_option = "Random Vendor" diff --git a/code/modules/vending/vendor/ui_data.dm b/code/modules/vending/vendor/ui_data.dm index c375e0a78d61..14ab20660d42 100644 --- a/code/modules/vending/vendor/ui_data.dm +++ b/code/modules/vending/vendor/ui_data.dm @@ -7,6 +7,11 @@ ) /obj/machinery/vending/ui_interact(mob/user, datum/tgui/ui) + if(SEND_SIGNAL(src, COMSIG_VENDING_UI_INTERACT, user, ui) & VENDING_DENIED) + if(icon_deny) + flick(icon_deny, src) + return + ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, "Vending", name) From 79800b3ee8bfd705494249a7e3e83e2e16a4831b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 05:25:56 +0000 Subject: [PATCH 071/155] Automatic changelog for PR #95311 [ci skip] --- html/changelogs/AutoChangeLog-pr-95311.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95311.yml diff --git a/html/changelogs/AutoChangeLog-pr-95311.yml b/html/changelogs/AutoChangeLog-pr-95311.yml new file mode 100644 index 000000000000..81d2021eab1d --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95311.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - rscadd: "Intelligence vendors (from the event) have a 50% chance to deny users who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone." \ No newline at end of file From 228d8af24518ab76f897be09aa73ca80ac301c54 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:00:25 +0000 Subject: [PATCH 072/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95311.yml | 4 ---- html/changelogs/archive/2026-03.yml | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95311.yml diff --git a/html/changelogs/AutoChangeLog-pr-95311.yml b/html/changelogs/AutoChangeLog-pr-95311.yml deleted file mode 100644 index 81d2021eab1d..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95311.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscadd: "Intelligence vendors (from the event) have a 50% chance to deny users who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index b24265ec9d3b..3e3262ba1ce6 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -285,3 +285,8 @@ lelandkemble: - code_imp: fixed a runtime when something other than a player breaks an airlock - bugfix: Flux & Gravity anomaly cores work with assembly activation +2026-03-19: + Melbert: + - rscadd: Intelligence vendors (from the event) have a 50% chance to deny users + who are not wealthy. When the vendors awake and start moving, they instead get + a 100% chance to deny anyone. From 3fa9c8ebaa7a2be0c9014ecd6f1ba12f19402e60 Mon Sep 17 00:00:00 2001 From: Lucy Date: Thu, 19 Mar 2026 11:06:57 -0400 Subject: [PATCH 073/155] Adds screentips for scanning raptors (#95421) ## About The Pull Request This adds screentips for scanning raptors - both with the PDA RaptorDex app, and the standalone handheld version. Also fixed a bug where the "scanned" balloon alert didn't properly appear for the, bc it'd try to display the balloon alert to the raptor... 2026-03-15 (1773602977) 2026-03-15 (1773603014) ## Why It's Good For The Game I initially didn't realize that you had to _right click_ to scan raptors with the PDA app, so like, I decided to add screentips to make that clearer. also bugfix good. ## Changelog :cl: qol: Added screentips for scanning raptors with the RaptorDex PDA app and standalone handheld RaptorDex. fix: Fixed the handheld RaptorDex not showing the "scanned" balloon alert when scanning a raptor. /:cl: --- .../basic/lavaland/raptor/raptor_dex.dm | 12 +++++++++++- .../file_system/programs/raptordex.dm | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_dex.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_dex.dm index 93191bd7838b..60a3d83141b2 100644 --- a/code/modules/mob/living/basic/lavaland/raptor/raptor_dex.dm +++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_dex.dm @@ -7,6 +7,10 @@ /// Raptor scan data we have stored var/list/scan_data = list("raptor_scan" = FALSE) +/obj/item/raptor_dex/Initialize(mapload) + . = ..() + register_item_context() + /obj/item/raptor_dex/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) @@ -61,6 +65,12 @@ scan_data["inherited_traits"] += GLOB.raptor_inherit_traits[index] playsound(src, 'sound/mobs/non-humanoids/orbie/orbie_send_out.ogg', 20) - balloon_alert(my_raptor, "scanned") + my_raptor.balloon_alert(user, "scanned") ui_interact(user) return ITEM_INTERACT_SUCCESS + +/obj/item/raptor_dex/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + if(!istype(target, /mob/living/basic/raptor)) + return NONE + context[SCREENTIP_CONTEXT_LMB] = "Scan Raptor" + return CONTEXTUAL_SCREENTIP_SET diff --git a/code/modules/modular_computers/file_system/programs/raptordex.dm b/code/modules/modular_computers/file_system/programs/raptordex.dm index 36c5d722a595..e040d0373986 100644 --- a/code/modules/modular_computers/file_system/programs/raptordex.dm +++ b/code/modules/modular_computers/file_system/programs/raptordex.dm @@ -61,3 +61,22 @@ /datum/computer_file/program/raptordex/ui_data(mob/user) return scan_data + +/datum/computer_file/program/raptordex/on_made_active_program(mob/user) + RegisterSignal(computer, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context)) + computer.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS + +/datum/computer_file/program/raptordex/background_program(mob/user) + . = ..() + UnregisterSignal(computer, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET) + +/datum/computer_file/program/raptordex/kill_program(mob/user) + . = ..() + UnregisterSignal(computer, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET) + +/datum/computer_file/program/raptordex/proc/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + SIGNAL_HANDLER + if(!istype(target, /mob/living/basic/raptor)) + return NONE + context[SCREENTIP_CONTEXT_RMB] = "Scan Raptor" + return CONTEXTUAL_SCREENTIP_SET From e6fbc48c849b5020a8375551fcc8796742a4e365 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:07:19 +0000 Subject: [PATCH 074/155] Automatic changelog for PR #95421 [ci skip] --- html/changelogs/AutoChangeLog-pr-95421.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95421.yml diff --git a/html/changelogs/AutoChangeLog-pr-95421.yml b/html/changelogs/AutoChangeLog-pr-95421.yml new file mode 100644 index 000000000000..a90e933a402b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95421.yml @@ -0,0 +1,5 @@ +author: "Absolucy" +delete-after: True +changes: + - qol: "Added screentips for scanning raptors with the RaptorDex PDA app and standalone handheld RaptorDex." + - bugfix: "Fixed the handheld RaptorDex not showing the \"scanned\" balloon alert when scanning a raptor." \ No newline at end of file From b463d97100f85d7bfd549c9b91a170369fb4bd7e Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:00:24 +0000 Subject: [PATCH 075/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95421.yml | 5 ----- html/changelogs/archive/2026-03.yml | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95421.yml diff --git a/html/changelogs/AutoChangeLog-pr-95421.yml b/html/changelogs/AutoChangeLog-pr-95421.yml deleted file mode 100644 index a90e933a402b..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95421.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Absolucy" -delete-after: True -changes: - - qol: "Added screentips for scanning raptors with the RaptorDex PDA app and standalone handheld RaptorDex." - - bugfix: "Fixed the handheld RaptorDex not showing the \"scanned\" balloon alert when scanning a raptor." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 3e3262ba1ce6..39a7a8442931 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -286,6 +286,11 @@ - code_imp: fixed a runtime when something other than a player breaks an airlock - bugfix: Flux & Gravity anomaly cores work with assembly activation 2026-03-19: + Absolucy: + - qol: Added screentips for scanning raptors with the RaptorDex PDA app and standalone + handheld RaptorDex. + - bugfix: Fixed the handheld RaptorDex not showing the "scanned" balloon alert when + scanning a raptor. Melbert: - rscadd: Intelligence vendors (from the event) have a 50% chance to deny users who are not wealthy. When the vendors awake and start moving, they instead get From e607aedda595df0e49aa98c74b5d201571d6f662 Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:33:15 -0400 Subject: [PATCH 076/155] Sets the worn ceremonial claymore's worn_icon_state (#95452) ## About The Pull Request No other sprite available, and no reason not to use the basic claymore one. They use the basic claymore inhand sprite too. ## Why It's Good For The Game closes #95451 ## Changelog :cl: image: gave the worn ceremonial claymore the basic claymore's worn icon. /:cl: --- code/game/objects/items/religion.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/code/game/objects/items/religion.dm b/code/game/objects/items/religion.dm index 7ccf96f158c5..0e96116ed3d1 100644 --- a/code/game/objects/items/religion.dm +++ b/code/game/objects/items/religion.dm @@ -409,6 +409,7 @@ icon = 'icons/obj/weapons/sword.dmi' icon_state = "claymore_old" worn_icon = 'icons/mob/clothing/back.dmi' + worn_icon_state = "claymore" force = 30 armour_penetration = 15 From 43e1aae5c9925a481e5cb99e5f56c57c32f0451a Mon Sep 17 00:00:00 2001 From: levels0 Date: Fri, 20 Mar 2026 03:03:55 +0300 Subject: [PATCH 077/155] Fix Webbing Production gene webs (#95425) ## About The Pull Request #95197 made gene-produced webs drain the stamina of their makers, this is a fix for that ## Why It's Good For The Game ## Changelog :cl: fix: webbing production webs no longer harm their maker /:cl: --------- Co-authored-by: l0 <--> --- code/game/objects/effects/spiderwebs.dm | 28 ++++++------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/code/game/objects/effects/spiderwebs.dm b/code/game/objects/effects/spiderwebs.dm index 3dc4c0865dde..b8317dd62591 100644 --- a/code/game/objects/effects/spiderwebs.dm +++ b/code/game/objects/effects/spiderwebs.dm @@ -41,8 +41,6 @@ smoothing_flags = SMOOTH_BITMASK smoothing_groups = SMOOTH_GROUP_SPIDER_WEB canSmoothWith = SMOOTH_GROUP_SPIDER_WEB + SMOOTH_GROUP_WALLS - ///Whether or not the web is from the genetics power - var/genetic = FALSE ///Whether or not the web is a sealed web var/sealed = FALSE ///Do we need to offset this based on a sprite frill? @@ -80,24 +78,22 @@ /obj/structure/spider/stickyweb/CanAllowThrough(atom/movable/mover, border_dir) . = ..() - if(genetic) - return if(sealed) return FALSE if(isprojectile(mover)) return prob(projectile_stuck_chance) return . +/obj/structure/spider/stickyweb/proc/is_whitelisted(mob/candidate) + return HAS_TRAIT(candidate, TRAIT_WEB_SURFER) + /obj/structure/spider/stickyweb/proc/on_entered(datum/source, atom/movable/victim, old_loc) SIGNAL_HANDLER if(!isliving(victim)) return - if(HAS_TRAIT(victim, TRAIT_WEB_SURFER)) - return - if(victim.pulledby && HAS_TRAIT(victim.pulledby, TRAIT_WEB_SURFER)) + if(is_whitelisted(victim) || victim.pulledby && is_whitelisted(victim.pulledby)) return - if(prob(stuck_chance)) stuck_react(victim) @@ -118,7 +114,6 @@ /// Web made by geneticists, needs special handling to allow them to pass through their own webs /obj/structure/spider/stickyweb/genetic - genetic = TRUE desc = "It's stringy, sticky, and came out of your coworker." /// Mob with special permission to cross this web var/mob/living/allowed_mob @@ -132,19 +127,8 @@ allowed_mob = allowedmob return ..() -/obj/structure/spider/stickyweb/genetic/CanAllowThrough(atom/movable/mover, border_dir) - . = ..() - if(mover == allowed_mob) - return TRUE - else if(isliving(mover)) //we change the spider to not be able to go through here - if(mover.pulledby == allowed_mob) - return TRUE - if(prob(50)) - stuck_react(mover) - return FALSE - else if(isprojectile(mover)) - return prob(30) - return . +/obj/structure/spider/stickyweb/genetic/is_whitelisted(mob/candidate) + return candidate == allowed_mob /// Web with a 100% chance to intercept movement /obj/structure/spider/stickyweb/very_sticky From 6d309c4acd3022473ac76f581f0cffb5bf054d1d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:04:16 +0000 Subject: [PATCH 078/155] Automatic changelog for PR #95425 [ci skip] --- html/changelogs/AutoChangeLog-pr-95425.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95425.yml diff --git a/html/changelogs/AutoChangeLog-pr-95425.yml b/html/changelogs/AutoChangeLog-pr-95425.yml new file mode 100644 index 000000000000..22f944d46509 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95425.yml @@ -0,0 +1,4 @@ +author: "levels0" +delete-after: True +changes: + - bugfix: "webbing production webs no longer harm their maker" \ No newline at end of file From 24933ff0a2f98791f3d32bc537578065a689178a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:09:49 +0000 Subject: [PATCH 079/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95425.yml | 4 ---- html/changelogs/archive/2026-03.yml | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95425.yml diff --git a/html/changelogs/AutoChangeLog-pr-95425.yml b/html/changelogs/AutoChangeLog-pr-95425.yml deleted file mode 100644 index 22f944d46509..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95425.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "levels0" -delete-after: True -changes: - - bugfix: "webbing production webs no longer harm their maker" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 39a7a8442931..7ca97d5c6a5c 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -295,3 +295,6 @@ - rscadd: Intelligence vendors (from the event) have a 50% chance to deny users who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone. +2026-03-20: + levels0: + - bugfix: webbing production webs no longer harm their maker From 74036004549987a7b68dc14953f3eb1b25ea5cdf Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:22:24 -0500 Subject: [PATCH 080/155] Lavaland relics from boulders now have more appropriate effects (#95261) ## About The Pull Request ### Main changes Lavaland relics from boulders have a (mostly) unique suite of random effects, differentiating them from normal station relics
Spoiler Effect list - Dimensional shift (same as normal) - Summon monsters (Summon animals, but Lavaland/Cult creatures, has a high chance of deleting itself.) - Explode (same as normal) - Low potency shield (When holding the relic, you get a shield that blocks three normal attacks.) - High potency shield (When holding the relic, you get a shield that blocks one normal or overpowering attack.) - Random teleport (same as normal) - Random AOE teleport (Cult themed version of normal teleport that affects all nearby objects/mobs. Blocked by antimagic, has a low chance of deleting itself) - Recharge (same as normal) - Rockfall (Places rock turfs over nearby open turfs. These are weak, ie they can be hand-mined. Stuns anyone caught in it, has a chance of deleting itself) - Blood spray (Consumes a chunk of the user's blood and sprays blood around. Has a chance of swapping to Blood suck) - Blood suck (Consumes a chuck of all nearby mob's blood and transfers it to the user. Blocked by antimagic. Has a chance of swapping to Blood spray) - Cleaning foam (same as normal) - Acid cleaning foam (spawn an ez-clean grenade, has a high chance of deleting itself.)
### Other changes Relics spawned from gifts (ie Christmas) auto-reveal themselves Blood sprays won't merge with each other Better documentation, and relics now uses `deconstruct` when terminating itself ## Why It's Good For The Game - The flavor behind these cursed and cultish relics doing the same thing as the cobbled together electronics found in maint is kinda weak. 90% of the time I see them abandoned in a pile in mining. More flavorful and potent effects may make people decide to investigate them more. - The gift thing was just a fun random idea I had. Allows people to play with their new toy immediately. ## Changelog :cl: Melbert add: Relics found from cracking boulders now have a (mostly) unique set of mining and cultish related effects. add: Relics found in gift boxes no longer need to be discovered first in science. /:cl: --- code/__DEFINES/dcs/signals/signals_object.dm | 3 + .../objects/effects/decals/cleanable/blood.dm | 2 + code/game/objects/items/gift.dm | 1 + code/modules/mob/living/blood.dm | 2 +- code/modules/research/relics.dm | 398 ++++++++++++++++-- 5 files changed, 363 insertions(+), 43 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index e65e3e1ad5a1..3bd83c546d6d 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -655,6 +655,9 @@ #define COMSIG_ITEM_IN_UNWRAPPED_TRAITOR_MAIL "traitor_mail_opened" #define COMPONENT_TRAITOR_MAIL_HANDLED (1<<0) +/// Send to items that have been unwrapped from a gift +#define COMSIG_ITEM_OPENED_FROM_GIFT "gift_opened" + /// From /obj/machinery/vending/ui_interact(): (mob/user, datum/vending_ui/ui) #define COMSIG_VENDING_UI_INTERACT "vending_ui_interact" #define VENDING_DENIED (1<<0) diff --git a/code/game/objects/effects/decals/cleanable/blood.dm b/code/game/objects/effects/decals/cleanable/blood.dm index c6b342cb7bc5..7dcef673bd9d 100644 --- a/code/game/objects/effects/decals/cleanable/blood.dm +++ b/code/game/objects/effects/decals/cleanable/blood.dm @@ -797,6 +797,8 @@ base_suffix = "splatter" can_dry = FALSE // No point + mergeable_decal = FALSE + /// The turf we just came from, so we can back up when we hit a wall var/turf/prev_loc /// Skip making the final blood splatter when we're done, like if we're not in a turf diff --git a/code/game/objects/items/gift.dm b/code/game/objects/items/gift.dm index 03fefc39ee18..414e813b49cd 100644 --- a/code/game/objects/items/gift.dm +++ b/code/game/objects/items/gift.dm @@ -49,6 +49,7 @@ user.investigate_log("has unwrapped a present containing [thing.type].", INVESTIGATE_PRESENTS) user.put_in_hands(thing) thing.add_fingerprint(user) + SEND_SIGNAL(thing, COMSIG_ITEM_OPENED_FROM_GIFT, user) qdel(src) diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index 7bc5c93c8dff..5b75da864436 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -662,7 +662,7 @@ * * splatter_direction: Which direction the blood is flying * * splatter_strength: How many tiles it can go, and how many items it can pass over and dirty */ -/mob/living/carbon/proc/spray_blood(splatter_direction, splatter_strength = 3) +/mob/living/proc/spray_blood(splatter_direction, splatter_strength = 3) // Check if we can bleed and if our splatter can even go anywhere if(!isturf(loc) || can_bleed(BLOOD_COVER_TURFS) != BLEED_SPLATTER) return diff --git a/code/modules/research/relics.dm b/code/modules/research/relics.dm index ff092fac4a40..fe957d81f320 100644 --- a/code/modules/research/relics.dm +++ b/code/modules/research/relics.dm @@ -1,3 +1,6 @@ +#define RELIC_PROTOTYPE "prototype" +#define RELIC_NECROTECH "necrotech" + /obj/item/relic name = "strange object" desc = "What mysteries could this hold? Maybe Research & Development could find out." @@ -16,34 +19,39 @@ /// Cooldown length. Randomly determined at activation if it isn't determined here. var/cooldown_timer /// What visual theme this artefact has. Current possible choices: "prototype", "necrotech" - var/artifact_theme = "prototype" + var/artifact_theme = RELIC_PROTOTYPE COOLDOWN_DECLARE(cooldown) /obj/item/relic/Initialize(mapload) . = ..() random_themed_appearance() + RegisterSignal(src, COMSIG_ITEM_OPENED_FROM_GIFT, PROC_REF(auto_reveal)) + +/obj/item/relic/proc/auto_reveal(...) + SIGNAL_HANDLER + reveal() /obj/item/relic/proc/random_themed_appearance() var/themed_name_prefix var/themed_name_suffix - if(artifact_theme == "prototype") - icon_state = pick("prototype1", "prototype2", "prototype3", "prototype4", "prototype5", "prototype6", "prototype7", "prototype8","prototype9") - themed_name_prefix = pick("experimental","prototype","artificial","handcrafted","ramshackle","odd") - themed_name_suffix = pick("device","assembly","gadget","gizmo","contraption","machine","widget","object") + if(artifact_theme == RELIC_PROTOTYPE) + icon_state = "[RELIC_PROTOTYPE][rand(1, 9)]" + themed_name_prefix = pick("experimental", "prototype", "artificial", "handcrafted", "ramshackle", "odd") + themed_name_suffix = pick("device", "assembly", "gadget", "gizmo", "contraption", "machine", "widget", "object") real_name = "[pick(themed_name_prefix)] [pick(themed_name_suffix)]" name = "strange [pick(themed_name_suffix)]" - if(artifact_theme == "necrotech") - icon_state = pick("necrotech1", "necrotech2", "necrotech3", "necrotech4", "necrotech5", "necrotech6") - themed_name_prefix = pick("dark","bloodied","unholy","archeotechnological","dismal","ruined","thrumming") - themed_name_suffix = pick("instrument","shard","fetish","bibelot","trinket","offering","relic") + if(artifact_theme == RELIC_NECROTECH) + icon_state = "[RELIC_NECROTECH][rand(1, 6)]" + themed_name_prefix = pick("dark", "bloodied", "unholy", "archeotechnological", "dismal", "ruined", "thrumming") + themed_name_suffix = pick("instrument", "shard", "fetish", "bibelot", "trinket", "offering", "relic") real_name = "[pick(themed_name_prefix)] [pick(themed_name_suffix)]" name = "strange relic" update_appearance() /obj/item/relic/lavaland name = "strange relic" - artifact_theme = "necrotech" + artifact_theme = RELIC_NECROTECH /obj/item/relic/proc/reveal() if(activated) //no rerolling @@ -53,20 +61,37 @@ if(!cooldown_timer) cooldown_timer = rand(min_cooldown, max_cooldown) if(!hidden_power) - hidden_power = pick( - PROC_REF(corgi_cannon), - PROC_REF(cleaning_foam), - PROC_REF(flashbanger), - PROC_REF(summon_animals), - PROC_REF(uncontrolled_teleport), - PROC_REF(heat_and_explode), - PROC_REF(rapid_self_dupe), - PROC_REF(drink_dispenser), - PROC_REF(tummy_ache), - PROC_REF(charger), - PROC_REF(hugger), - PROC_REF(dimensional_shift), - PROC_REF(disguiser), + if(artifact_theme == RELIC_PROTOTYPE) + hidden_power = pick( + PROC_REF(corgi_cannon), + PROC_REF(cleaning_foam), + PROC_REF(flashbanger), + PROC_REF(summon_animals), + PROC_REF(uncontrolled_teleport), + PROC_REF(heat_and_explode), + PROC_REF(rapid_self_dupe), + PROC_REF(drink_dispenser), + PROC_REF(tummy_ache), + PROC_REF(charger), + PROC_REF(hugger), + PROC_REF(dimensional_shift), + PROC_REF(disguiser), + ) + if(artifact_theme == RELIC_NECROTECH) + hidden_power = pick( + PROC_REF(dimensional_shift), + PROC_REF(summon_animals_monsters), + PROC_REF(heat_and_explode), + PROC_REF(t1_shield_holder), + PROC_REF(t2_shield_holder), + PROC_REF(uncontrolled_teleport), + PROC_REF(uncontrolled_aoe_teleport), + PROC_REF(charger), + PROC_REF(place_rocks), + PROC_REF(yeet_blood), + PROC_REF(suck_blood), + PROC_REF(cleaning_foam), + PROC_REF(cleaning_foam_acid), ) obj_flags |= UNIQUE_RENAME @@ -82,30 +107,51 @@ COOLDOWN_START(src, cooldown, cooldown_timer) call(src, hidden_power)(user) +/// Helper to spawn smoke somewhere /obj/item/relic/proc/throw_smoke(turf/where) do_smoke(0, src, get_turf(where)) +/// Helper to show a message to people around the relic +/obj/item/relic/proc/relic_message(message) + var/atom/message_source = ismob(loc) ? loc : src + message_source.visible_message(message) + // Artefact Powers \\ +/// Throws a corgi somewhere /obj/item/relic/proc/corgi_cannon(mob/user) playsound(src, SFX_SPARKS, rand(25,50), TRUE, SHORT_RANGE_SOUND_EXTRARANGE) var/mob/living/basic/pet/dog/corgi/sad_corgi = new(get_turf(user)) sad_corgi.throw_at(pick(oview(10,user)), 10, rand(3,8), callback = CALLBACK(src, PROC_REF(throw_smoke), sad_corgi)) - warn_admins(user, "Corgi Cannon", 0) + warn_admins(user, "Corgi Cannon", FALSE) +/// Spawns cleaning foam /obj/item/relic/proc/cleaning_foam(mob/user) - playsound(src, SFX_SPARKS, rand(25,50), TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - var/obj/item/grenade/chem_grenade/cleaner/spawned_foamer = new/obj/item/grenade/chem_grenade/cleaner(get_turf(user)) + playsound(src, SFX_SPARKS, rand(25, 50), TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + var/obj/item/grenade/chem_grenade/cleaner/spawned_foamer = new(get_turf(user)) spawned_foamer.detonate() qdel(spawned_foamer) - warn_admins(user, "Foam", 0) + warn_admins(user, "Foam", FALSE) +/// Similar to cleaning foam but spawns the acid variant +/obj/item/relic/proc/cleaning_foam_acid(mob/user) + playsound(src, SFX_SPARKS, rand(25, 50), TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + var/obj/item/grenade/chem_grenade/ez_clean/spawned_foamer = new(get_turf(user)) + spawned_foamer.detonate() + qdel(spawned_foamer) + warn_admins(user, "Acid Foam", TRUE) + if(prob(80)) + to_chat(user, span_warning("[src] melts apart!")) + acid_melt() + +/// Flashbangs anyone nearby /obj/item/relic/proc/flashbanger(mob/user) playsound(src, SFX_SPARKS, rand(25,50), TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - var/obj/item/grenade/flashbang/spawned_flashbang = new/obj/item/grenade/flashbang(user.loc) + var/obj/item/grenade/flashbang/spawned_flashbang = new(get_turf(user)) spawned_flashbang.detonate() warn_admins(user, "Flash") +/// Summon a bunch of random animals, some of which are dangerous /obj/item/relic/proc/summon_animals(mob/user) var/message = span_danger("[src] begins to shake, and in the distance the sound of rampaging animals arises!") visible_message(message) @@ -130,9 +176,44 @@ ADD_TRAIT(animal, TRAIT_SPAWNED_MOB, INNATE_TRAIT) warn_admins(user, "Mass Mob Spawn") if(prob(60)) - to_chat(user, span_warning("[src] falls apart!")) - qdel(src) + relic_message(span_warning("[src] falls apart!")) + deconstruct(FALSE) +/// Version of summon_animals that spawns mostly lavaland monsters +/obj/item/relic/proc/summon_animals_monsters(mob/user) + var/message = span_danger("[src] begins to shake, and in the distance the sound of roaring arises!") + visible_message(message) + to_chat(user, message) + var/static/list/valid_monsters = list( + /mob/living/basic/construct/artificer/hostile, + /mob/living/basic/construct/juggernaut/hostile, + /mob/living/basic/construct/proteon/hostile, + /mob/living/basic/construct/wraith/hostile, + /mob/living/basic/mining/brimdemon, + /mob/living/basic/mining/goldgrub, + /mob/living/basic/mining/goliath, + /mob/living/basic/mining/hivelord, + /mob/living/basic/mining/ice_demon, + /mob/living/basic/mining/legion, + /mob/living/basic/mining/lobstrosity, + /mob/living/basic/mining/watcher, + /mob/living/basic/raptor/blue, + /mob/living/basic/raptor/green, + /mob/living/basic/raptor/purple, + /mob/living/basic/raptor/red, + /mob/living/basic/raptor/white, + /mob/living/basic/raptor/yellow, + ) + for(var/counter in 1 to rand(3, 9)) + var/animal_spawn = pick(valid_monsters) + var/mob/living/animal = new animal_spawn(get_turf(src)) + ADD_TRAIT(animal, TRAIT_SPAWNED_MOB, INNATE_TRAIT) + warn_admins(user, "Mass Mob Spawn (Monster)") + if(prob(80)) + relic_message(span_warning("[src] falls apart!")) + deconstruct(FALSE) + +/// Spawns a bunch of mimics of the relic which also can spawn relics, but despawn shortly /obj/item/relic/proc/rapid_self_dupe(mob/user) audible_message("[src] emits a loud pop!") var/list/dummy_artifacts = list() @@ -148,6 +229,7 @@ QDEL_LIST_IN(dummy_artifacts, rand(1 SECONDS, 10 SECONDS)) warn_admins(user, "Rapid duplicator", 0) +/// Explodes after a few seconds /obj/item/relic/proc/heat_and_explode(mob/user) to_chat(user, span_danger("[src] begins to heat up!")) addtimer(CALLBACK(src, PROC_REF(blow_up), user), rand(3.5 SECONDS, 10 SECONDS)) @@ -157,11 +239,15 @@ visible_message(span_notice("\The [src]'s top opens, releasing a powerful blast!")) explosion(src, heavy_impact_range = rand(1,5), light_impact_range = rand(1,5), flame_range = 2, flash_range = rand(1,5), adminlog = TRUE) warn_admins(user, "Explosion") - qdel(src) //Comment this line to produce a light grenade (the bomb that keeps on exploding when used)!! + deconstruct(FALSE) //Comment this line to produce a light grenade (the bomb that keeps on exploding when used)!! +/// Teleports the relic, and anyone holding it, to a random location nearby /obj/item/relic/proc/uncontrolled_teleport(mob/user) to_chat(user, span_notice("[src] begins to vibrate!")) - addtimer(CALLBACK(src, PROC_REF(do_the_teleport), user), rand(1 SECONDS, 3 SECONDS)) + + var/teleport_time = rand(1 SECONDS, 3 SECONDS) + addtimer(CALLBACK(src, PROC_REF(do_the_teleport), user), teleport_time) + Shake(1, 1, teleport_time, 0.05 SECONDS) /obj/item/relic/proc/do_the_teleport(mob/user) var/turf/userturf = get_turf(user) @@ -173,7 +259,47 @@ throw_smoke(get_turf(to_teleport)) do_teleport(to_teleport, userturf, 8, asoundin = 'sound/effects/phasein.ogg', channel = TELEPORT_CHANNEL_BLUESPACE) throw_smoke(get_turf(to_teleport)) - warn_admins(user, "Teleport", 0) + warn_admins(user, "Teleport", FALSE) + +/// Version of uncontrolled_teleport with cult theming, and that affects all nearby movables rather than just the relic +/obj/item/relic/proc/uncontrolled_aoe_teleport(mob/user) + to_chat(user, span_notice("[src] begins to vibrate intensely!")) + + var/teleport_time = rand(1 SECONDS, 3 SECONDS) + addtimer(CALLBACK(src, PROC_REF(do_the_aoe_teleport), user), teleport_time) + Shake(2, 2, teleport_time, 0.03 SECONDS) + +/obj/item/relic/proc/do_the_aoe_teleport(mob/user) + visible_message(span_notice("[src] twists and bends, relocating anything nearby!")) + var/turf/teleturf = get_turf(src) + for(var/atom/movable/nearby in view(2, teleturf)) + if(nearby.anchored || nearby.invisibility || HAS_TRAIT(nearby, TRAIT_UNDERFLOOR)) + continue + if(isliving(nearby)) + var/mob/living/nearby_living = nearby + if(nearby_living.can_block_magic(MAGIC_RESISTANCE_HOLY, 1)) + continue + + if(isliving(nearby)) + new /obj/effect/temp_visual/dir_setting/cult/phase/out(nearby.loc, nearby.dir) + else + new /obj/effect/temp_visual/cult/sparks(nearby.loc) + do_teleport( + teleatom = nearby, + destination = teleturf, + precision = 5, + asoundin = 'sound/effects/phasein.ogg', + channel = TELEPORT_CHANNEL_CULT, + ) + if(isliving(nearby)) + new /obj/effect/temp_visual/dir_setting/cult/phase(nearby.loc, nearby.dir) + else + new /obj/effect/temp_visual/cult/sparks(nearby.loc) + + warn_admins(user, "AOE Teleport") + if(prob(30)) + relic_message(span_warning("[src] teleports away, never to be seen again!")) + qdel(src) // Creates a glass and fills it up with a drink. /obj/item/relic/proc/drink_dispenser(mob/user) @@ -187,7 +313,7 @@ glasser.reagents.add_reagent(get_random_reagent_id(whitelist = subtypesof(/datum/reagent/consumable/ethanol)), rand(glasser.volume * 0.3, glasser.volume)) throw_smoke(get_turf(glasser)) -// Scrambles your organs. 33% chance to delete after use. +/// Scrambles your organs. 33% chance to delete after use. /obj/item/relic/proc/tummy_ache(mob/user) new /obj/effect/temp_visual/circle_wave/bioscrambler/light(get_turf(src)) to_chat(user, span_notice("Your stomach starts growling...")) @@ -202,9 +328,10 @@ throw_smoke(get_turf(nearby)) to_chat(nearby, span_notice("You feel weird.")) if(prob(33)) - qdel(src) + relic_message(span_warning("[src] falls apart!")) + deconstruct(FALSE) -// Charges an item or two in your inventory. Also yourself. +/// Charges an item or two in your inventory. Also yourself. /obj/item/relic/proc/charger(mob/living/user) to_chat(user, span_danger("You're recharged!")) var/stunner = 1.25 SECONDS @@ -239,7 +366,7 @@ /obj/item/relic/proc/cut_the_overlay(atom/shocker, lightning) shocker.cut_overlay(lightning) -// Hugs/shakes everyone in range! +/// Hugs/shakes everyone in range! /obj/item/relic/proc/hugger(mob/user) var/list/mob/living/carbon/huggeds = oviewers(3, user) for(var/mob/living/carbon/victim in huggeds) @@ -250,7 +377,7 @@ else to_chat(user, pick(span_notice("You hug yourself, for some reason."), span_notice("You have a strange feeling for a moment, but then it passes."))) -// Converts a 3x3 area into a random dimensional theme. +/// Converts a 3x3 area into a random dimensional theme. /obj/item/relic/proc/dimensional_shift(mob/user) var/new_theme_path = pick(subtypesof(/datum/dimension_theme)) var/datum/dimension_theme/shifter = SSmaterials.dimensional_themes[new_theme_path] @@ -260,8 +387,8 @@ min_cooldown += 2 SECONDS max_cooldown += 2 SECONDS -// Replaces your clothing with a random costume, and your ID with a cardboard one. -// TODO: make them part of the same kit (lobster hat, lobster suit) +/// Replaces your clothing with a random costume, and your ID with a cardboard one. +/// TODO: make them part of the same kit (lobster hat, lobster suit) /obj/item/relic/proc/disguiser(mob/user) if(!iscarbon(user)) to_chat(user, span_notice("You have a strange feeling for a moment, but then it passes.")) @@ -326,11 +453,198 @@ new_costume.item_flags |= DROPDEL return new_costume +/// Makes the relic holder have a shield that blocks 3 common attacks +/obj/item/relic/proc/t1_shield_holder(mob/user) + var/datum/component/shield = AddComponent( \ + /datum/component/shielded, \ + max_charges = 3, \ + recharge_start_delay = 0, \ + shield_inhand = TRUE, \ + show_charge_as_alpha = TRUE, \ + can_block_overwhelming = FALSE, \ + shield_icon_file = 'icons/effects/effects.dmi', \ + shield_icon = "at_shield1", \ + run_hit_callback = CALLBACK(src, PROC_REF(shield_hit)), \ + ) + light_system = OVERLAY_LIGHT + var/datum/component/light = AddComponent( \ + /datum/component/overlay_lighting, \ + _range = 2.5, \ + _power = 1.5, \ + _color = COLOR_BIOLUMINESCENCE_YELLOW, \ + starts_on = TRUE, \ + ) + + addtimer(CALLBACK(src, PROC_REF(remove_shield), list(shield, light)), cooldown_timer * 0.5) + add_filter("block_shield", 1, outline_filter(0, COLOR_BIOLUMINESCENCE_YELLOW), shield) + transition_filter("block_shield", outline_filter(2, COLOR_BIOLUMINESCENCE_YELLOW), cooldown_timer * 0.5) + + relic_message(span_notice("[src] starts to glow a bright yellow!")) + warn_admins(user, "Shield", FALSE) + +/// Makes the relic holder have a shield that blocks 1 powerful attack +/obj/item/relic/proc/t2_shield_holder(mob/user) + var/datum/component/shield = AddComponent( \ + /datum/component/shielded, \ + max_charges = 1, \ + recharge_start_delay = 0, \ + shield_inhand = TRUE, \ + show_charge_as_alpha = TRUE, \ + can_block_overwhelming = TRUE, \ + shield_icon_file = 'icons/effects/effects.dmi', \ + shield_icon = "at_shield2", \ + run_hit_callback = CALLBACK(src, PROC_REF(shield_hit)), \ + ) + light_system = OVERLAY_LIGHT + var/datum/component/light = AddComponent( \ + /datum/component/overlay_lighting, \ + _range = 3, \ + _power = 1.5, \ + _color = COLOR_BIOLUMINESCENCE_YELLOW, \ + starts_on = TRUE, \ + ) + + addtimer(CALLBACK(src, PROC_REF(remove_shield), list(shield, light)), cooldown_timer * 0.5) + add_filter("block_shield", 1, outline_filter(0, COLOR_BIOLUMINESCENCE_YELLOW), shield) + transition_filter("block_shield", outline_filter(2, COLOR_BIOLUMINESCENCE_YELLOW), cooldown_timer * 0.5) + + relic_message(span_notice("[src] starts to glow a bright yellow!")) + warn_admins(user, "Shield", FALSE) + +/obj/item/relic/proc/shield_hit(mob/living/owner, attack_text, current_charges) + playsound(src, 'sound/items/weapons/marauder.ogg', 20, TRUE, frequency = 1.25) + owner.visible_message(span_danger("[owner]'s holds [src] up, blocking [attack_text] with a projected shield!")) + if(current_charges <= 0) + set_light_on(FALSE) + relic_message(span_notice("[src] stops glowing.")) + remove_filter("block_shield") + +/obj/item/relic/proc/remove_shield(list/cleanup_components) + for(var/datum/component/comp as anything in cleanup_components) + qdel(comp) + light_system = initial(light_system) + if(light_on) + relic_message(span_notice("[src] stops glowing.")) + remove_filter("block_shield") + +/// Places rock turfs around the relic +/obj/item/relic/proc/place_rocks(mob/user) + relic_message(span_notice("A spire of rock erupts from the ground beneath [src]!")) + playsound(src, 'sound/effects/rock/rock_break.ogg', 50, TRUE) + var/turf/spawnloc = get_turf(src) + for(var/turf/open/open_turf in RANGE_TURFS(rand(1, 2), spawnloc)) + var/turf/closed/mineral/asteroid/rock = open_turf.place_on_top(/turf/closed/mineral/asteroid) + if(istype(rock)) + rock.name = "fragile rock" + rock.weak_turf = TRUE + rock.tool_mine_speed = 2 SECONDS + rock.hand_mine_speed = 10 SECONDS + for(var/mob/living/within_rock in rock) + within_rock.Paralyze(1 SECONDS) + within_rock.Knockdown(3 SECONDS) + within_rock.apply_damage(10, BRUTE, BODY_ZONE_CHEST, blocked = within_rock.getarmor(BODY_ZONE_CHEST, MELEE), wound_bonus = 10, exposed_wound_bonus = 10) + to_chat(within_rock, span_danger("You are smashed by [rock]!")) + warn_admins(user, "Rocks", FALSE) + if(prob(20)) + relic_message(span_warning("[src] crumbles into dust!")) + deconstruct(FALSE) + +/// User sprays out blood in all directions +/// Has a small chance of changing the power to suck_blood +/obj/item/relic/proc/yeet_blood(mob/living/user) + var/yeet_time = rand(1 SECONDS, 3 SECONDS) + add_filter("blood_outgoing", 1, outline_filter(0, COLOR_DARK)) + transition_filter("blood_outgoing", outline_filter(2, BLOOD_COLOR_RED), yeet_time) + if(istype(user) && CAN_HAVE_BLOOD(user)) + to_chat(user, span_danger("[src] starts glowing an ominous red!")) + else + to_chat(user, span_danger("[src] starts glowing an ominous red...")) + + addtimer(CALLBACK(src, PROC_REF(actually_yeet_blood)), yeet_time) + +/obj/item/relic/proc/actually_yeet_blood() + var/mob/living/user = loc + var/splatcount = 0 + if(istype(user) && CAN_HAVE_BLOOD(user) && !user.can_block_magic(MAGIC_RESISTANCE_HOLY, 1)) + for(var/splatdir in GLOB.alldirs) + if(prob(splatcount * 5)) + continue + var/strength = rand(2, 3) + user.spray_blood(splatdir, strength) + user.bleed(strength ** 2) + splatcount += strength + + if(splatcount > 0) + relic_message(span_warning("Blood sprays out from [src]!")) + warn_admins(user, "Blood Dispersal", FALSE) + playsound(src, 'sound/effects/wounds/blood3.ogg', 50, TRUE) + if(prob(20)) + hidden_power = PROC_REF(suck_blood) + else + relic_message(span_warning("[src] pulses ominously, but nothing happens!")) + + transition_filter("blood_outgoing", outline_filter(0, COLOR_DARK), 1 SECONDS) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, remove_filter), "blood_outgoing"), 2 SECONDS) + +/// Nearby mobs transfer blood to the user +/// Has a small chance of changing the power to yeet_blood +/obj/item/relic/proc/suck_blood(mob/living/user) + var/suck_time = rand(1 SECONDS, 3 SECONDS) + add_filter("blood_incoming", 1, outline_filter(0, COLOR_DARK)) + transition_filter("blood_incoming", outline_filter(2, BLOOD_COLOR_RED), suck_time) + if(istype(user) && CAN_HAVE_BLOOD(user)) + to_chat(user, span_danger("[src] starts glowing an ominous red!")) + else + to_chat(user, span_danger("[src] starts glowing an ominous red...")) + + addtimer(CALLBACK(src, PROC_REF(actually_suck_blood)), suck_time) + +/obj/item/relic/proc/actually_suck_blood() + var/mob/living/user = loc + var/any_affected = FALSE + if(istype(user) && CAN_HAVE_BLOOD(user) && !user.can_block_magic(MAGIC_RESISTANCE_HOLY, 1)) + for(var/mob/living/nearby in view(2, user)) + if(nearby == user || !CAN_HAVE_BLOOD(nearby) || nearby.can_block_magic(MAGIC_RESISTANCE_HOLY, 1)) + continue + nearby.transfer_blood_to(user, rand(6, 10), ignore_low_blood = TRUE, ignore_incompatibility = TRUE, transfer_viruses = FALSE) + to_chat(nearby, span_danger("You feel a sudden weakness as blood is drawn out of you [nearby.is_blind() ? "" : " and into [user]"]!")) + any_affected = TRUE + nearby.Beam(user, icon_state = "blood", time = 1 SECONDS) + new /obj/effect/temp_visual/cult/sparks(nearby.loc) + + if(any_affected) + playsound(src, 'sound/effects/magic/enter_blood.ogg', 50, TRUE) + relic_message(span_warning("Blood from nearby creatures is drawn towards [src], and into [user]!")) + warn_admins(user, "Blood Suck") + if(prob(10)) + hidden_power = PROC_REF(yeet_blood) + else + relic_message(span_warning("[src] pulses ominously, but nothing happens!")) + + transition_filter("blood_incoming", outline_filter(0, COLOR_DARK), 1 SECONDS) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, remove_filter), "blood_incoming"), 2 SECONDS) + /// Alerts admins on usage of dagnerous relics -/obj/item/relic/proc/warn_admins(mob/user, relic_type, priority = 1) +/obj/item/relic/proc/warn_admins(mob/user, relic_type, priority = TRUE) var/turf/location = get_turf(src) var/log_msg = "[relic_type] relic used by [key_name(user)] in [AREACOORD(location)]" if(priority) message_admins("[relic_type] relic activated by [ADMIN_LOOKUPFLW(user)] in [ADMIN_VERBOSEJMP(location)]") log_game(log_msg) investigate_log(log_msg, "experimentor") + +// Subtypes that spawn revealed, primarily for debug/testing/badmin purposes +/obj/item/relic/revealed + +/obj/item/relic/revealed/Initialize(mapload) + . = ..() + auto_reveal() + +/obj/item/relic/lavaland + +/obj/item/relic/lavaland/revealed/Initialize(mapload) + . = ..() + auto_reveal() + +#undef RELIC_PROTOTYPE +#undef RELIC_NECROTECH From 6ea848f47cef20dc4416332d5cd3a5b151ca1b4b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:22:42 +0000 Subject: [PATCH 081/155] Automatic changelog for PR #95261 [ci skip] --- html/changelogs/AutoChangeLog-pr-95261.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95261.yml diff --git a/html/changelogs/AutoChangeLog-pr-95261.yml b/html/changelogs/AutoChangeLog-pr-95261.yml new file mode 100644 index 000000000000..005a75834e1f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95261.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - rscadd: "Relics found from cracking boulders now have a (mostly) unique set of mining and cultish related effects." + - rscadd: "Relics found in gift boxes no longer need to be discovered first in science." \ No newline at end of file From d0c6d60fc3c5672c67402b0af2b1a0c723fc6cd3 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:30:23 +0000 Subject: [PATCH 082/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95261.yml | 5 ----- html/changelogs/archive/2026-03.yml | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95261.yml diff --git a/html/changelogs/AutoChangeLog-pr-95261.yml b/html/changelogs/AutoChangeLog-pr-95261.yml deleted file mode 100644 index 005a75834e1f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95261.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscadd: "Relics found from cracking boulders now have a (mostly) unique set of mining and cultish related effects." - - rscadd: "Relics found in gift boxes no longer need to be discovered first in science." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 7ca97d5c6a5c..a6525ab3e8ce 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -296,5 +296,9 @@ who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone. 2026-03-20: + Melbert: + - rscadd: Relics found from cracking boulders now have a (mostly) unique set of + mining and cultish related effects. + - rscadd: Relics found in gift boxes no longer need to be discovered first in science. levels0: - bugfix: webbing production webs no longer harm their maker From da64374423593ccb71a1aace5d9ef7af19825287 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:32:41 -0500 Subject: [PATCH 083/155] Reverts "Add prosthetic limb" surgery to involve targeting limbs, rather than targeting chest. (Adds stumps) (#95252) ## About The Pull Request - The `prosthetic replacement` surgical operation has been reverted to be closer to how it used to work: The operation is done targeting the limb that's missing The change was made out of necessity, as surgical state was tied to limbs - you had to operate on the chest to re-attach limbs because there was no limb to operate on. To circumvent that, I have done the unthinkable of adding stumps when you are dismembered. - Missing limbs are now represented as an invisible, un-removable, un-interactable limb. Making this change was not as difficult as originally anticipated, and (at least surface level) seems to have broken very little. Surprisingly little had to change to make this work. Direct accesses to `mob.bodyparts` was changed to `mob.get_bodyparts()` with an optional `include_stumps` argument. Similarly, `get_bodypart()` had an optional `include_stumps` added. This means we ultimately barely needed to change anything, and in fact, some loops/checks were able to be streamlined. ## Why It's Good For The Game - As mentioned, this change was out of necessity and was easily the least intuitive part of the broader changes. Reverting it back to how it used to work should make it far easier for people to pick up on, and means we can cut out a bunch of bespoke instruction sets that I had to include. - The addition of stumps also adds a ton of future potential - code wise it allows for stuff like better damage tracking (we can transfer damage between limb <-> stump rather than limb <-> chest), and feature we can do "fun" stuff like have stumps bleed on dismemberment that you can bandage. ## Changelog :cl: Melbert del: "Add prosthetic limb" surgical operation has been reverted to be a bit closer to how it used to work - you operate on the missing limb / limb stump, rather than on the chest. refactor: Missing limbs are now represented as limb stumps. In practice this should change nothing (for now), as no features were rewritten to make use of these besides surgery. Please report any oddities with missing limbs, however. /:cl: --- code/__DEFINES/combat.dm | 7 +- code/__DEFINES/surgery.dm | 5 ++ code/__HELPERS/cmp.dm | 2 +- code/_onclick/hud/screen_objects.dm | 18 ++-- code/_onclick/item_attack.dm | 2 +- code/controllers/subsystem/radiation.dm | 2 +- .../texture_bodypart_overlay.dm | 3 +- code/datums/components/amputating_limbs.dm | 2 +- code/datums/components/bloodysoles.dm | 2 +- code/datums/components/butchering.dm | 4 +- code/datums/components/free_operation.dm | 8 +- code/datums/components/healing_touch.dm | 2 +- .../diseases/advance/symptoms/flesh_eating.dm | 2 +- code/datums/elements/blood_limb_overlay.dm | 6 +- code/datums/elements/organ_set_bonus.dm | 4 +- code/datums/elements/spooky.dm | 2 +- code/datums/embedding.dm | 4 +- code/datums/mutations/autotomy.dm | 2 +- code/datums/mutations/hulk.dm | 4 +- code/datums/mutations/touch.dm | 4 +- .../quirks/negative_quirks/body_purist.dm | 2 +- .../quirks/neutral_quirks/transhumanist.dm | 2 +- .../status_effects/debuffs/rust_corruption.dm | 2 +- code/datums/voice_of_god_command.dm | 2 +- .../machinery/computer/operating_computer.dm | 35 ++------ .../dna_infuser/organ_sets/fish_organs.dm | 4 +- .../experimental_cloning_record.dm | 2 +- code/game/machinery/harvester.dm | 2 +- code/game/objects/items.dm | 2 +- .../items/devices/scanners/health_analyzer.dm | 2 +- .../stacks/golem_food/golem_status_effects.dm | 6 +- code/game/objects/items/stacks/medical.dm | 4 +- .../objects/items/weaponry/melee/sabre.dm | 2 +- code/game/objects/items/wiki_manuals.dm | 2 +- code/game/turfs/open/lava.dm | 2 +- code/modules/admin/smites/berforate.dm | 2 +- code/modules/admin/smites/bloodless.dm | 2 +- code/modules/admin/smites/boneless.dm | 2 +- code/modules/admin/smites/nugget.dm | 2 +- code/modules/antagonists/abductor/abductor.dm | 2 +- .../antagonists/changeling/changeling.dm | 2 +- .../changeling/powers/defib_grasp.dm | 2 +- code/modules/antagonists/cult/cult_armor.dm | 2 +- .../modules/antagonists/heretic/influences.dm | 2 +- .../heretic/items/heretic_armor.dm | 6 +- .../heretic/items/heretic_grenade.dm | 2 +- .../heretic/knowledge/flesh_lore.dm | 2 +- .../heretic/knowledge/moon_lore.dm | 2 +- .../heretic/knowledge/rust_lore.dm | 2 +- .../knowledge/side_knowledge/tier_two.dm | 4 +- .../heretic/knowledge/starting_lore.dm | 2 +- .../heretic/magic/apetravulnera.dm | 4 +- .../antagonists/heretic/magic/blood_cleave.dm | 2 +- .../antagonists/heretic/magic/blood_siphon.dm | 4 +- .../heretic/magic/crimson_cleave.dm | 2 +- .../heretic/status_effects/buffs.dm | 4 +- .../heretic/status_effects/heretic_passive.dm | 4 +- .../heretic/status_effects/mark_effects.dm | 2 +- code/modules/antagonists/spy/spy_bounty.dm | 2 +- .../voidwalker/voidwalker_traumas.dm | 4 +- .../grand_ritual/finales/immortality.dm | 2 +- code/modules/cargo/supplypod.dm | 4 +- .../preferences/species_features/lizard.dm | 2 +- .../experisci/experiment/experiments.dm | 4 +- .../modules/fishing/fish/types/holographic.dm | 2 +- code/modules/fishing/fish/types/ruins.dm | 2 +- .../food_and_drinks/machinery/gibber.dm | 2 +- code/modules/hallucination/blood_flow.dm | 4 +- .../hallucination/screwy_health_doll.dm | 2 +- .../jobs/job_types/head_of_security.dm | 2 +- code/modules/jobs/job_types/prisoner.dm | 2 +- .../jobs/job_types/security_officer.dm | 2 +- code/modules/jobs/job_types/warden.dm | 2 +- code/modules/library/bibles.dm | 2 +- code/modules/lost_crew/damages/post_mortem.dm | 6 +- code/modules/lost_crew/lost_crew_manager.dm | 3 + code/modules/mob/inventory.dm | 1 + .../living/basic/cult/constructs/harvester.dm | 2 +- .../living/basic/farm_animals/goat/_goat.dm | 2 +- .../mob/living/basic/ruin_defender/flesh.dm | 2 +- .../living/basic/space_fauna/lightgeist.dm | 2 +- code/modules/mob/living/blood.dm | 8 +- code/modules/mob/living/carbon/carbon.dm | 59 +++++++----- .../mob/living/carbon/carbon_defense.dm | 5 +- .../mob/living/carbon/carbon_update_icons.dm | 90 ++++++++++--------- .../modules/mob/living/carbon/damage_procs.dm | 19 ++-- code/modules/mob/living/carbon/death.dm | 21 +++-- code/modules/mob/living/carbon/examine.dm | 12 +-- .../mob/living/carbon/human/_species.dm | 8 +- code/modules/mob/living/carbon/human/human.dm | 6 +- .../mob/living/carbon/human/human_defense.dm | 17 ++-- .../living/carbon/human/human_update_icons.dm | 2 +- .../mob/living/carbon/human/inventory.dm | 2 + .../carbon/human/species_types/ethereal.dm | 2 +- code/modules/mob/living/carbon/life.dm | 2 +- code/modules/mob/living/death.dm | 7 ++ code/modules/mob/living/living.dm | 4 +- code/modules/mob/mob_helpers.dm | 2 +- .../projectiles/ammunition/ballistic/rifle.dm | 2 +- code/modules/projectiles/guns/magic/staff.dm | 2 +- .../impure_medicine_reagents.dm | 10 +-- .../chemistry/reagents/other_reagents.dm | 2 +- .../reagents/reagent_containers/patch.dm | 2 +- .../religion/festival/instrument_rites.dm | 2 +- code/modules/religion/religion_sects.dm | 6 +- .../religion/sparring/sparring_datum.dm | 2 +- .../xenobiology/crossbreeding/_misc.dm | 9 +- .../spells/spell_types/touch/scream_for_me.dm | 2 +- code/modules/surgery/bodyparts/_bodyparts.dm | 24 ++--- .../surgery/bodyparts/dismemberment.dm | 11 ++- code/modules/surgery/bodyparts/head.dm | 1 + code/modules/surgery/bodyparts/helpers.dm | 71 +++++++++++---- code/modules/surgery/bodyparts/parts.dm | 6 ++ .../surgery/bodyparts/robot_bodyparts.dm | 2 +- .../bodyparts/species_parts/misc_bodyparts.dm | 1 + code/modules/surgery/bodyparts/stumps.dm | 72 +++++++++++++++ code/modules/surgery/operations/_operation.dm | 5 +- .../surgery/operations/operation_add_limb.dm | 88 +++++++----------- .../surgery/operations/operation_generic.dm | 9 ++ .../operations/operation_generic_mechanic.dm | 10 ++- code/modules/surgery/surgery_tools.dm | 2 +- code/modules/unit_tests/limbsanity.dm | 5 +- .../unit_tests/organ_bodypart_shuffle.dm | 21 ++++- code/modules/unit_tests/spawn_humans.dm | 20 +++++ code/modules/unit_tests/surgeries.dm | 4 +- code/modules/vehicles/cars/clowncar.dm | 18 ++-- code/modules/vending/vendor/tilting.dm | 2 +- tgstation.dme | 1 + 128 files changed, 566 insertions(+), 386 deletions(-) create mode 100644 code/modules/surgery/bodyparts/stumps.dm diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index 878ed53f990d..c4e4b40b56c0 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -293,10 +293,11 @@ DEFINE_BITFIELD(ammo_box_multiload, list( #define BODY_ZONE_PRECISE_L_FOOT "l_foot" #define BODY_ZONE_PRECISE_R_FOOT "r_foot" -GLOBAL_LIST_INIT(all_body_zones, list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, BODY_ZONE_R_LEG, BODY_ZONE_L_LEG)) -GLOBAL_LIST_INIT(limb_zones, list(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, BODY_ZONE_R_LEG, BODY_ZONE_L_LEG)) +// These lists are ordered as bodyparts would be ordered +GLOBAL_LIST_INIT(all_body_zones, list(BODY_ZONE_CHEST, BODY_ZONE_HEAD, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) +GLOBAL_LIST_INIT(limb_zones, list(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) GLOBAL_LIST_INIT(arm_zones, list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM)) -GLOBAL_LIST_INIT(leg_zones, list(BODY_ZONE_R_LEG, BODY_ZONE_L_LEG)) +GLOBAL_LIST_INIT(leg_zones, list(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)) GLOBAL_LIST_INIT(all_precise_body_zones, list(BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH, BODY_ZONE_PRECISE_GROIN, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND, BODY_ZONE_PRECISE_L_FOOT, BODY_ZONE_PRECISE_R_FOOT)) //We will round to this value in damage calculations. diff --git a/code/__DEFINES/surgery.dm b/code/__DEFINES/surgery.dm index ab8fee7c9a08..fcf8b853cada 100644 --- a/code/__DEFINES/surgery.dm +++ b/code/__DEFINES/surgery.dm @@ -54,6 +54,9 @@ /// Helper to figure out if a limb is a peg limb #define IS_PEG_LIMB(limb) (limb.bodytype & BODYTYPE_PEG) +/// Is the bodypart a stump +#define IS_STUMP(limb) (limb.bodypart_flags & BODYPART_STUMP) + // Flags for the bodypart_flags var on /obj/item/bodypart /// Bodypart cannot be dismembered or amputated #define BODYPART_UNREMOVABLE (1<<0) @@ -65,6 +68,8 @@ #define BODYPART_UNHUSKABLE (1<<3) /// Bodypart has never been added to a mob #define BODYPART_VIRGIN (1<<4) +/// Not a full bodypart, but in fact is part of a missing limb +#define BODYPART_STUMP (1<<5) // Bodypart change blocking flags ///Bodypart does not get replaced during set_species() diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 4ff1e10a22f2..5e6f71b33c9c 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -161,7 +161,7 @@ /// Orders bodyparts by their body_part value, ascending. /proc/cmp_bodypart_by_body_part_asc(obj/item/bodypart/limb_one, obj/item/bodypart/limb_two) - return limb_one.body_part - limb_two.body_part + return limb_one::body_part - limb_two::body_part /// Orders by integrated circuit weight /proc/cmp_port_order_asc(datum/port/compare1, datum/port/compare2) diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 533099c39181..250219e87406 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -799,8 +799,9 @@ update_appearance() /atom/movable/screen/healthdoll/human/update_body_zones() - limbs = list() vis_contents.Cut() + QDEL_LIST_ASSOC_VAL(limbs) + limbs ||= list() var/mob/living/carbon/human/owner = hud.mymob for(var/body_zone in owner.get_all_limbs()) var/atom/movable/screen/healthdoll_limb/limb = new(src, null) @@ -827,12 +828,13 @@ var/list/current_animated = LAZYLISTDUPLICATE(animated_zones) - for(var/obj/item/bodypart/body_part as anything in owner.bodyparts) + for(var/part_zone, body_part_untyped in owner.get_bodyparts_by_zones()) var/icon_key = 0 - var/part_zone = body_part.body_zone - + var/obj/item/bodypart/body_part = body_part_untyped var/list/overridable_key = list(icon_key) - if(body_part.bodypart_disabled) + if(isnull(body_part) || IS_STUMP(body_part)) + icon_key = 6 + else if(body_part.bodypart_disabled) icon_key = 7 else if(owner.stat == DEAD) icon_key = "DEAD" @@ -843,15 +845,11 @@ // calculate what icon state (1-5, or 0 if undamaged) to use based on damage icon_key = clamp(ceil(damage * 5), 0, 5) - if(length(body_part.wounds)) + if(length(body_part?.wounds)) LAZYSET(animated_zones, part_zone, TRUE) else LAZYREMOVE(animated_zones, part_zone) limbs[part_zone].icon_state = "[part_zone][icon_key]" - // handle leftovers - for(var/missing_zone in owner.get_missing_limbs()) - limbs[missing_zone].icon_state = "[missing_zone]6" - LAZYREMOVE(animated_zones, missing_zone) // time to re-sync animations, something changed if(animated_zones ~! current_animated) for(var/animated_zone in animated_zones) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 7f057a85bf8e..c37d6d9d4175 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -408,7 +408,7 @@ return TRUE /mob/living/carbon/attack_effects(damage_done, hit_zone, armor_block, obj/item/attacking_item, mob/living/attacker) - var/obj/item/bodypart/hit_bodypart = get_bodypart(hit_zone) || bodyparts[1] + var/obj/item/bodypart/hit_bodypart = get_bodypart(hit_zone) || get_bodypart() if(!hit_bodypart.can_bleed()) return FALSE diff --git a/code/controllers/subsystem/radiation.dm b/code/controllers/subsystem/radiation.dm index a129c2a34641..316331db7c9a 100644 --- a/code/controllers/subsystem/radiation.dm +++ b/code/controllers/subsystem/radiation.dm @@ -132,7 +132,7 @@ SUBSYSTEM_DEF(radiation) /// Returns whether or not the human is covered head to toe in rad-protected clothing. /datum/controller/subsystem/radiation/proc/wearing_rad_protected_clothing(mob/living/carbon/human/human) - for (var/obj/item/bodypart/limb as anything in human.bodyparts) + for (var/obj/item/bodypart/limb as anything in human.get_bodyparts()) var/protected = FALSE for (var/obj/item/clothing as anything in human.get_clothing_on_part(limb)) diff --git a/code/datums/bodypart_overlays/texture_bodypart_overlay.dm b/code/datums/bodypart_overlays/texture_bodypart_overlay.dm index 70086d45f9cf..920c2973ac8d 100644 --- a/code/datums/bodypart_overlays/texture_bodypart_overlay.dm +++ b/code/datums/bodypart_overlays/texture_bodypart_overlay.dm @@ -17,7 +17,8 @@ appearance.add_filter("bodypart_texture_[texture_icon_state]", 1, layering_filter(icon = cached_texture_icon, blend_mode = BLEND_INSET_OVERLAY)) /datum/bodypart_overlay/texture/generate_icon_cache() - return "[type]" + . = ..() + . += "[type]" /datum/bodypart_overlay/texture/can_draw_on_bodypart(obj/item/bodypart/bodypart_owner, mob/living/carbon/owner, is_husked = FALSE) if (!..()) diff --git a/code/datums/components/amputating_limbs.dm b/code/datums/components/amputating_limbs.dm index bfaf52ca90ca..28a69ee5a19f 100644 --- a/code/datums/components/amputating_limbs.dm +++ b/code/datums/components/amputating_limbs.dm @@ -58,7 +58,7 @@ return var/list/valid_targets = list() - for (var/obj/item/bodypart/possible_target as anything in limbed_victim.bodyparts) + for (var/obj/item/bodypart/possible_target as anything in limbed_victim.get_bodyparts()) if (possible_target.bodypart_flags & BODYPART_UNREMOVABLE) continue if (!(possible_target.body_zone in target_zones)) diff --git a/code/datums/components/bloodysoles.dm b/code/datums/components/bloodysoles.dm index 4dc7a7feeb66..09c945c2f8d3 100644 --- a/code/datums/components/bloodysoles.dm +++ b/code/datums/components/bloodysoles.dm @@ -326,7 +326,7 @@ return // Find any leg of our human and add that to the footprint, instead of the default which is to just add the human type - for(var/obj/item/bodypart/leg/affecting in wielder.bodyparts) + for(var/obj/item/bodypart/leg/affecting in wielder.get_bodyparts()) if(!affecting.bodypart_disabled) LAZYSET(footprint.species_types, affecting.limb_id, TRUE) diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm index b1f7eebab1fb..bdc41329a744 100644 --- a/code/datums/components/butchering.dm +++ b/code/datums/components/butchering.dm @@ -167,7 +167,7 @@ if (target.body_zone == BODY_ZONE_CHEST && target.owner) // Cannot butcher the chest until we hack off all the other limbs - for (var/obj/item/bodypart/limb as anything in target.owner.bodyparts) + for (var/obj/item/bodypart/limb as anything in target.owner.get_bodyparts()) if (limb != target && limb.butcher_drops && limb.butcher_replacement) to_chat(user, span_warning("You need to butcher all other limbs first!")) return @@ -249,7 +249,7 @@ for(var/obj/item/result as anything in results) if (reagents_in_produced) if (target.owner.reagents) - target.owner.reagents.trans_to(result, target.owner.reagents.total_volume / reagents_in_produced / length(target.owner.bodyparts), remove_blacklisted = TRUE) + target.owner.reagents.trans_to(result, target.owner.reagents.total_volume / reagents_in_produced / length(target.owner.get_bodyparts()), remove_blacklisted = TRUE) result.reagents?.add_reagent(/datum/reagent/consumable/nutriment/fat, target.owner.nutrition / /datum/reagent/consumable/nutriment/fat::nutriment_factor / reagents_in_produced) if(LAZYLEN(diseases)) diff --git a/code/datums/components/free_operation.dm b/code/datums/components/free_operation.dm index a8002ef7f23a..1c557cda5ea5 100644 --- a/code/datums/components/free_operation.dm +++ b/code/datums/components/free_operation.dm @@ -9,17 +9,17 @@ return COMPONENT_REDUNDANT ADD_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src)) var/mob/living/carbon/owner = parent - if(!istype(owner)) + if (!istype(owner)) return - for (var/obj/item/bodypart/limb as anything in owner.bodyparts) + for (var/obj/item/bodypart/limb as anything in owner.get_bodyparts(include_stumps = TRUE)) ADD_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src)) /datum/component/free_operation/Destroy(force) REMOVE_TRAIT(parent, TRAIT_READY_TO_OPERATE, REF(src)) var/mob/living/carbon/owner = parent - if(!istype(owner)) + if (!istype(owner)) return ..() - for (var/obj/item/bodypart/limb as anything in owner.bodyparts) + for (var/obj/item/bodypart/limb as anything in owner.get_bodyparts(include_stumps = TRUE)) REMOVE_TRAIT(limb, TRAIT_READY_TO_OPERATE, REF(src)) return ..() diff --git a/code/datums/components/healing_touch.dm b/code/datums/components/healing_touch.dm index b8580e2e458f..f7a9515c9c06 100644 --- a/code/datums/components/healing_touch.dm +++ b/code/datums/components/healing_touch.dm @@ -174,7 +174,7 @@ if (!iscarbon(target)) return (target.get_brute_loss() > 0 && heal_brute) || (target.get_fire_loss() > 0 && heal_burn) var/mob/living/carbon/carbon_target = target - for (var/obj/item/bodypart/part in carbon_target.bodyparts) + for (var/obj/item/bodypart/part in carbon_target.get_bodyparts()) if (!(part.brute_dam && heal_brute) && !(part.burn_dam && heal_burn)) continue if (!isnull(required_bodytype) && !(part.bodytype & required_bodytype)) diff --git a/code/datums/diseases/advance/symptoms/flesh_eating.dm b/code/datums/diseases/advance/symptoms/flesh_eating.dm index d12e0812335b..d767f33fae33 100644 --- a/code/datums/diseases/advance/symptoms/flesh_eating.dm +++ b/code/datums/diseases/advance/symptoms/flesh_eating.dm @@ -60,7 +60,7 @@ Bonus if(bleed) if(ishuman(M)) var/mob/living/carbon/human/H = M - var/obj/item/bodypart/random_part = pick(H.bodyparts) + var/obj/item/bodypart/random_part = pick(H.get_bodyparts()) random_part.adjustBleedStacks(5 * power) return 1 diff --git a/code/datums/elements/blood_limb_overlay.dm b/code/datums/elements/blood_limb_overlay.dm index 3028d3f4754e..bab7b6cb0ec0 100644 --- a/code/datums/elements/blood_limb_overlay.dm +++ b/code/datums/elements/blood_limb_overlay.dm @@ -12,10 +12,10 @@ . = ..() UnregisterSignal(source, list(COMSIG_BODYPART_GET_LIMB_ICON, COMSIG_BODYPART_GENERATE_ICON_KEY)) -/datum/element/blood_limb_overlay/proc/on_limb_icon(obj/item/bodypart/source, list/limb_icons, dropped, mob/living/carbon/update_on) +/datum/element/blood_limb_overlay/proc/on_limb_icon(obj/item/bodypart/source, list/limb_icons, dropped) SIGNAL_HANDLER - var/list/blood_dna_info = source.blood_dna_info || update_on?.get_blood_dna_list() + var/list/blood_dna_info = source.blood_dna_info if (!LAZYLEN(blood_dna_info) || source.is_invisible) return @@ -38,4 +38,4 @@ var/list/blood_dna_info = source.blood_dna_info || source.owner?.get_blood_dna_list() if (LAZYLEN(blood_dna_info) && !source.is_invisible) - icon_keys += "-blood-[get_color_from_blood_list(blood_dna_info)]" + icon_keys += "blood:[get_color_from_blood_list(blood_dna_info)]" diff --git a/code/datums/elements/organ_set_bonus.dm b/code/datums/elements/organ_set_bonus.dm index 07d7e2474d22..49cd58ddf935 100644 --- a/code/datums/elements/organ_set_bonus.dm +++ b/code/datums/elements/organ_set_bonus.dm @@ -105,7 +105,7 @@ RegisterSignal(carbon_owner, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(texture_limb)) RegisterSignal(carbon_owner, COMSIG_CARBON_REMOVE_LIMB, PROC_REF(untexture_limb)) - for(var/obj/item/bodypart/limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_owner.get_bodyparts()) if (!(limb.bodytype & BODYTYPE_ORGANIC)) continue limb.add_bodypart_overlay(new limb_overlay(), update = FALSE) @@ -139,7 +139,7 @@ if(QDELETED(carbon_owner)) return - for(var/obj/item/bodypart/limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_owner.get_bodyparts()) var/overlay = locate(limb_overlay) in limb.bodypart_overlays if(!overlay) continue diff --git a/code/datums/elements/spooky.dm b/code/datums/elements/spooky.dm index 85daaaeda78d..4ae70bb50953 100644 --- a/code/datums/elements/spooky.dm +++ b/code/datums/elements/spooky.dm @@ -52,7 +52,7 @@ if(isskeleton(human)) return FALSE //undeads are unaffected by the spook-pocalypse. var/bone_amount = 0 - for(var/obj/item/bodypart/part as anything in human.bodyparts) + for(var/obj/item/bodypart/part as anything in human.get_bodyparts()) if((part.biological_state & BIO_FLESH_BONE) == BIO_FLESH_BONE) bone_amount++ if(bone_amount) diff --git a/code/datums/embedding.dm b/code/datums/embedding.dm index 77fec4f87469..32d838eedf4f 100644 --- a/code/datums/embedding.dm +++ b/code/datums/embedding.dm @@ -133,7 +133,7 @@ failed_embed(victim, hit_zone, random = TRUE) return - var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.bodyparts[1] + var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.get_bodypart() embed_into(victim, limb) return MOVABLE_IMPACT_ZONE_OVERRIDE @@ -153,7 +153,7 @@ failed_embed(victim, hit_zone, random = TRUE) return - var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.bodyparts[1] + var/obj/item/bodypart/limb = victim.get_bodypart(hit_zone) || victim.get_bodypart() embed_into(victim, limb) SEND_SIGNAL(source, COMSIG_PROJECTILE_ON_EMBEDDED, payload, hit) diff --git a/code/datums/mutations/autotomy.dm b/code/datums/mutations/autotomy.dm index 03575a8f0396..da34bfd46606 100644 --- a/code/datums/mutations/autotomy.dm +++ b/code/datums/mutations/autotomy.dm @@ -27,7 +27,7 @@ return var/list/parts = list() - for(var/obj/item/bodypart/to_remove as anything in cast_on.bodyparts) + for(var/obj/item/bodypart/to_remove as anything in cast_on.get_bodyparts()) if(to_remove.body_zone == BODY_ZONE_HEAD || to_remove.body_zone == BODY_ZONE_CHEST) continue if(to_remove.bodypart_flags & BODYPART_UNREMOVABLE) diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm index 9de066eeacbc..6fd350263093 100644 --- a/code/datums/mutations/hulk.dm +++ b/code/datums/mutations/hulk.dm @@ -33,7 +33,7 @@ . = ..() if(!.) return - for(var/obj/item/bodypart/part as anything in owner.bodyparts) + for(var/obj/item/bodypart/part as anything in owner.get_bodyparts()) if (part.bodytype & BODYTYPE_ORGANIC) part.add_color_override(bodypart_color, LIMB_COLOR_HULK) owner.update_body_parts() @@ -74,7 +74,7 @@ /datum/mutation/hulk/on_losing(mob/living/carbon/human/owner) if(..()) return - for(var/obj/item/bodypart/part as anything in owner.bodyparts) + for(var/obj/item/bodypart/part as anything in owner.get_bodyparts()) part.remove_color_override(LIMB_COLOR_HULK) owner.update_body_parts() owner.clear_mood_event("hulk") diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm index ba24f4f30bc4..7a91cc390e8f 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -226,7 +226,7 @@ // Get at least organic limb to transfer the damage to var/list/mendicant_organic_limbs = list() - for(var/obj/item/bodypart/possible_limb in mendicant.bodyparts) + for(var/obj/item/bodypart/possible_limb in mendicant.get_bodyparts()) if(IS_ORGANIC_LIMB(possible_limb)) mendicant_organic_limbs += possible_limb // None? Gtfo @@ -258,7 +258,7 @@ // Get the hurtguy's limbs and the mendicant's limbs to attempt a 1-1 transfer. var/list/hurt_limbs = hurtguy.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC) + hurtguy.get_wounded_bodyparts(BODYTYPE_ORGANIC) var/list/mendicant_organic_limbs = list() - for(var/obj/item/bodypart/possible_limb in mendicant.bodyparts) + for(var/obj/item/bodypart/possible_limb in mendicant.get_bodyparts()) if(IS_ORGANIC_LIMB(possible_limb)) mendicant_organic_limbs += possible_limb diff --git a/code/datums/quirks/negative_quirks/body_purist.dm b/code/datums/quirks/negative_quirks/body_purist.dm index 76221df7a5c6..063887b87464 100644 --- a/code/datums/quirks/negative_quirks/body_purist.dm +++ b/code/datums/quirks/negative_quirks/body_purist.dm @@ -31,7 +31,7 @@ var/mob/living/carbon/owner = quirk_holder if(!istype(owner)) return - for(var/obj/item/bodypart/limb as anything in owner.bodyparts) + for(var/obj/item/bodypart/limb as anything in owner.get_bodyparts()) if(IS_ROBOTIC_LIMB(limb)) cybernetics_level++ for(var/obj/item/organ/organ as anything in owner.organs) diff --git a/code/datums/quirks/neutral_quirks/transhumanist.dm b/code/datums/quirks/neutral_quirks/transhumanist.dm index 82570e6cf21c..6dfa2d16f6c2 100644 --- a/code/datums/quirks/neutral_quirks/transhumanist.dm +++ b/code/datums/quirks/neutral_quirks/transhumanist.dm @@ -76,7 +76,7 @@ var/organic_bodytypes = 0 var/silicon_bodytypes = 0 var/other_bodytypes = FALSE - for(var/obj/item/bodypart/part as anything in target.bodyparts) + for(var/obj/item/bodypart/part as anything in target.get_bodyparts()) if(part.bodytype & BODYTYPE_ROBOTIC) silicon_bodytypes += 1 else if(part.bodytype & BODYTYPE_ORGANIC) diff --git a/code/datums/status_effects/debuffs/rust_corruption.dm b/code/datums/status_effects/debuffs/rust_corruption.dm index 6692ef111a08..66374898ab78 100644 --- a/code/datums/status_effects/debuffs/rust_corruption.dm +++ b/code/datums/status_effects/debuffs/rust_corruption.dm @@ -13,6 +13,6 @@ if(!iscarbon(owner)) return var/mob/living/carbon/carbon_owner = owner - for(var/obj/item/bodypart/robotic_limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/robotic_limb as anything in carbon_owner.get_bodyparts()) if(IS_ROBOTIC_LIMB(robotic_limb)) robotic_limb.receive_damage(10) diff --git a/code/datums/voice_of_god_command.dm b/code/datums/voice_of_god_command.dm index a49c7cb6c340..22a3993fa780 100644 --- a/code/datums/voice_of_god_command.dm +++ b/code/datums/voice_of_god_command.dm @@ -205,7 +205,7 @@ GLOBAL_LIST_INIT(voice_of_god_commands, init_voice_of_god_commands()) /datum/voice_of_god_command/bleed/execute(list/listeners, mob/living/user, power_multiplier = 1, message) for(var/mob/living/carbon/human/target in listeners) - var/obj/item/bodypart/chosen_part = pick(target.bodyparts) + var/obj/item/bodypart/chosen_part = pick(target.get_bodyparts()) chosen_part.adjustBleedStacks(5) /// This command sets the listeners ablaze. diff --git a/code/game/machinery/computer/operating_computer.dm b/code/game/machinery/computer/operating_computer.dm index d9f645c30bce..416f4c16fac9 100644 --- a/code/game/machinery/computer/operating_computer.dm +++ b/code/game/machinery/computer/operating_computer.dm @@ -236,33 +236,14 @@ "mechanic" = operation.operation_flags & OPERATION_MECHANIC, )) - if(!any_recommended && table?.patient) - var/obj/item/part = table.patient.get_bodypart(deprecise_zone(target_zone)) - var/just_drapes = FALSE - if(table.patient.has_limbs) - if(isnull(part)) - data["surgeries"] += list(list( - "name" = "Prepare for [/datum/surgery_operation/prosthetic_replacement::name]", - "desc" = "Prepare the patient's chest for prosthetic limb attachment.", - "tool_rec" = "operate on chest", - "show_as_next" = TRUE, - "show_in_list" = FALSE, - )) - - else if(!HAS_TRAIT(part, TRAIT_READY_TO_OPERATE)) - just_drapes = TRUE - - else if(!HAS_TRAIT(table.patient, TRAIT_READY_TO_OPERATE)) - just_drapes = TRUE - - if(just_drapes) - data["surgeries"] += list(list( - "name" = "Prepare for surgery", - "desc" = "Begin surgery by applying surgical drapes to the patient.", - "tool_rec" = /obj/item/surgical_drapes::name, - "show_as_next" = TRUE, - "show_in_list" = FALSE, - )) + if(!any_recommended && table?.patient && !HAS_TRAIT(table.patient, TRAIT_READY_TO_OPERATE)) + data["surgeries"] += list(list( + "name" = "Prepare for surgery", + "desc" = "Begin surgery by applying surgical drapes to the patient or by buckling the patient to the surgical table.", + "tool_rec" = /obj/item/surgical_drapes::name, + "show_as_next" = TRUE, + "show_in_list" = FALSE, + )) return data diff --git a/code/game/machinery/dna_infuser/organ_sets/fish_organs.dm b/code/game/machinery/dna_infuser/organ_sets/fish_organs.dm index 16bf78ee396c..2284e2afa18a 100644 --- a/code/game/machinery/dna_infuser/organ_sets/fish_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/fish_organs.dm @@ -93,7 +93,7 @@ if (new_value >= FISH_INFUSION_ALL_ORGANS && tail_color) if (!color_active) - for(var/obj/item/bodypart/limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_owner.get_bodyparts()) limb.add_color_override(tail_color, LIMB_COLOR_FISH_INFUSION) color_active = TRUE return @@ -101,7 +101,7 @@ if (!color_active) return - for(var/obj/item/bodypart/limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_owner.get_bodyparts()) limb.remove_color_override(LIMB_COLOR_FISH_INFUSION) color_active = FALSE diff --git a/code/game/machinery/experimental_cloner/experimental_cloning_record.dm b/code/game/machinery/experimental_cloner/experimental_cloning_record.dm index 96c1b9bc164d..caea82e36775 100644 --- a/code/game/machinery/experimental_cloner/experimental_cloning_record.dm +++ b/code/game/machinery/experimental_cloner/experimental_cloning_record.dm @@ -75,7 +75,7 @@ for (var/trauma_type in brain_traumas) subject.gain_trauma(trauma_type) - for (var/obj/item/bodypart/limb as anything in subject.bodyparts) + for (var/obj/item/bodypart/limb as anything in subject.get_bodyparts()) limb.update_limb(is_creating = TRUE) subject.updateappearance(mutcolor_update = TRUE) diff --git a/code/game/machinery/harvester.dm b/code/game/machinery/harvester.dm index 70849a1198f1..0af6c9933a4b 100644 --- a/code/game/machinery/harvester.dm +++ b/code/game/machinery/harvester.dm @@ -101,7 +101,7 @@ header = "Gruesome!", ) - operation_order = reverseList(carbon_occupant.bodyparts) //Chest and head are first in bodyparts, so we invert it to make them suffer more + operation_order = reverse_range(carbon_occupant.get_bodyparts()) //Chest and head are first in bodyparts, so we invert it to make them suffer more warming_up = TRUE harvesting = TRUE visible_message(span_notice("\The [src] begins warming up!")) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 08ab1724c5e5..ec0ff3340cd8 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -2217,7 +2217,7 @@ return FALSE if (!istype(target_limb)) - target_limb = victim.get_bodypart(target_limb) || victim.bodyparts[1] + target_limb = victim.get_bodypart(target_limb) || victim.get_bodypart() return get_embed()?.embed_into(victim, target_limb) diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index e4487f621928..01dfb14d8910 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -219,7 +219,7 @@ if(iscarbon(target)) var/mob/living/carbon/carbontarget = target var/any_damage = brute_loss > 0 || fire_loss > 0 || oxy_loss > 0 || tox_loss > 0 || fire_loss > 0 - var/any_missing = length(carbontarget.bodyparts) < (carbontarget.dna?.species?.max_bodypart_count || 6) + var/any_missing = length(carbontarget.get_missing_limbs()) var/any_wounded = length(carbontarget.all_wounds) var/any_embeds = carbontarget.has_embedded_objects() if(any_damage || (mode == SCANNER_VERBOSE && (any_missing || any_wounded || any_embeds))) diff --git a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm index ab8e3796f22c..fe44c1eef416 100644 --- a/code/game/objects/items/stacks/golem_food/golem_status_effects.dm +++ b/code/game/objects/items/stacks/golem_food/golem_status_effects.dm @@ -78,7 +78,7 @@ if(isgolem(owner)) var/mob/living/carbon/golem_owner = owner - for (var/obj/item/bodypart/part in golem_owner.bodyparts) + for (var/obj/item/bodypart/part in golem_owner.get_bodyparts()) // these overlays won't look good on anything but golem limbs if (part.limb_id != SPECIES_GOLEM) continue @@ -307,7 +307,7 @@ owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/light_speed) var/mob/living/carbon/carbon_owner = owner - for (var/obj/item/bodypart/arm/arm in carbon_owner.bodyparts) + for (var/obj/item/bodypart/arm/arm in carbon_owner.get_bodyparts()) set_arm_fluff(arm) return TRUE @@ -388,7 +388,7 @@ var/mob/living/carbon/human/human_owner = owner RegisterSignal(human_owner, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_punched)) human_owner.physiology.brute_mod *= brute_modifier - for (var/obj/item/bodypart/arm/arm in human_owner.bodyparts) + for (var/obj/item/bodypart/arm/arm in human_owner.get_bodyparts()) buff_arm(arm) /// Give mining mobs an extra slap diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm index d244efb3ad40..de4020bc85dc 100644 --- a/code/game/objects/items/stacks/medical.dm +++ b/code/game/objects/items/stacks/medical.dm @@ -230,7 +230,7 @@ PRIVATE_PROC(TRUE) var/list/other_affected_limbs = list() - for(var/obj/item/bodypart/limb as anything in patient.bodyparts) + for(var/obj/item/bodypart/limb as anything in patient.get_bodyparts()) if(!try_heal_checks(patient, user, limb.body_zone, silent = TRUE)) continue other_affected_limbs += limb.body_zone @@ -774,7 +774,7 @@ return BRUTELOSS patient.emote("scream") - for(var/obj/item/bodypart/bone as anything in patient.bodyparts) + for(var/obj/item/bodypart/bone as anything in patient.get_bodyparts()) // fine to just, use these raw, its a meme anyway var/datum/wound/blunt/bone/severe/oof_ouch = new oof_ouch.apply_wound(bone, wound_source = "bone gel") diff --git a/code/game/objects/items/weaponry/melee/sabre.dm b/code/game/objects/items/weaponry/melee/sabre.dm index 770f5b53a9c2..0cc900208005 100644 --- a/code/game/objects/items/weaponry/melee/sabre.dm +++ b/code/game/objects/items/weaponry/melee/sabre.dm @@ -81,7 +81,7 @@ var/list/legs = list() var/obj/item/bodypart/bodypart - for(bodypart in Cuser.bodyparts) + for(bodypart in Cuser.get_bodyparts()) if(bodypart == holding_bodypart) continue if(bodypart.body_part & ARMS) diff --git a/code/game/objects/items/wiki_manuals.dm b/code/game/objects/items/wiki_manuals.dm index 5c0c08024646..56efbd0669a4 100644 --- a/code/game/objects/items/wiki_manuals.dm +++ b/code/game/objects/items/wiki_manuals.dm @@ -213,7 +213,7 @@ var/obj/item/bodypart/head = H.get_bodypart(BODY_ZONE_HEAD) if(head) ADD_TRAIT(head, TRAIT_DISFIGURED, TRAIT_GENERIC) - for(var/obj/item/bodypart/part as anything in H.bodyparts) + for(var/obj/item/bodypart/part as anything in H.get_bodyparts()) part.adjustBleedStacks(5) H.gib_animation() sleep(0.3 SECONDS) diff --git a/code/game/turfs/open/lava.dm b/code/game/turfs/open/lava.dm index d530d00a00f3..a931748a577c 100644 --- a/code/game/turfs/open/lava.dm +++ b/code/game/turfs/open/lava.dm @@ -437,7 +437,7 @@ var/list/immune_parts = list() // Parts we can't transform because they're not organic or can't be dismembered var/list/transform_parts = list() // Parts we want to transform - for(var/obj/item/bodypart/burn_limb as anything in burn_human.bodyparts) + for(var/obj/item/bodypart/burn_limb as anything in burn_human.get_bodyparts()) if(!IS_ORGANIC_LIMB(burn_limb) || !burn_limb.can_dismember()) immune_parts += burn_limb continue diff --git a/code/modules/admin/smites/berforate.dm b/code/modules/admin/smites/berforate.dm index 6b5fffa38e62..8bd027eb8bc1 100644 --- a/code/modules/admin/smites/berforate.dm +++ b/code/modules/admin/smites/berforate.dm @@ -41,7 +41,7 @@ dude.Immobilize(5 SECONDS) for (var/wound_bonus_rep in 1 to repetitions) - for (var/_limb in dude.bodyparts) + for (var/_limb in dude.get_bodyparts()) var/obj/item/bodypart/limb = _limb var/shots_this_limb = 0 for (var/_iter_turf in shuffle(open_adj_turfs)) diff --git a/code/modules/admin/smites/bloodless.dm b/code/modules/admin/smites/bloodless.dm index c970e920f225..df43ed2048af 100644 --- a/code/modules/admin/smites/bloodless.dm +++ b/code/modules/admin/smites/bloodless.dm @@ -8,7 +8,7 @@ to_chat(user, span_warning("This must be used on a carbon mob."), confidential = TRUE) return var/mob/living/carbon/carbon_target = target - for(var/_limb in carbon_target.bodyparts) + for(var/_limb in carbon_target.get_bodyparts()) var/obj/item/bodypart/limb = _limb // fine to use this raw, its a meme smite var/type_wound = pick(list(/datum/wound/slash/flesh/severe, /datum/wound/slash/flesh/moderate)) limb.force_wound_upwards(type_wound, smited = TRUE) diff --git a/code/modules/admin/smites/boneless.dm b/code/modules/admin/smites/boneless.dm index 3fd1a1121f4b..5dd2afd6ef2b 100644 --- a/code/modules/admin/smites/boneless.dm +++ b/code/modules/admin/smites/boneless.dm @@ -10,7 +10,7 @@ return var/mob/living/carbon/carbon_target = target - for(var/obj/item/bodypart/limb as anything in carbon_target.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_target.get_bodyparts()) var/severity = pick_weight(alist( WOUND_SEVERITY_MODERATE = 1, WOUND_SEVERITY_SEVERE = 2, diff --git a/code/modules/admin/smites/nugget.dm b/code/modules/admin/smites/nugget.dm index 18e5254e615f..9efde93dfeeb 100644 --- a/code/modules/admin/smites/nugget.dm +++ b/code/modules/admin/smites/nugget.dm @@ -11,7 +11,7 @@ var/mob/living/carbon/carbon_target = target var/timer = 2 SECONDS - for (var/_limb in carbon_target.bodyparts) + for (var/_limb in carbon_target.get_bodyparts()) var/obj/item/bodypart/limb = _limb if (limb.body_part == HEAD || limb.body_part == CHEST) continue diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index 380eea90b96a..fdbfc81b23cd 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -99,7 +99,7 @@ // If we have a team skincolor, apply it here. Applied by admins or 2% chance of natural occurance if(!isnull(team.team_skincolor)) - for(var/obj/item/bodypart/part as anything in new_abductor.bodyparts) + for(var/obj/item/bodypart/part as anything in new_abductor.get_bodyparts()) part.should_draw_greyscale = TRUE part.add_color_override(team.team_skincolor, LIMB_COLOR_AYYLMAO) diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index b841ec266261..53abe0952e44 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -801,7 +801,7 @@ chosen_dna.copy_dna(user.dna, COPY_DNA_SE|COPY_DNA_SPECIES) - for(var/obj/item/bodypart/limb as anything in user.bodyparts) + for(var/obj/item/bodypart/limb as anything in user.get_bodyparts()) limb.update_limb(is_creating = TRUE) user.updateappearance(mutcolor_update = TRUE) diff --git a/code/modules/antagonists/changeling/powers/defib_grasp.dm b/code/modules/antagonists/changeling/powers/defib_grasp.dm index 38c14efdfe7f..5d63b5b2e055 100644 --- a/code/modules/antagonists/changeling/powers/defib_grasp.dm +++ b/code/modules/antagonists/changeling/powers/defib_grasp.dm @@ -63,7 +63,7 @@ if(iscarbon(defibber)) var/removed_arms = 0 var/mob/living/carbon/carbon_defibber = defibber - for(var/obj/item/bodypart/arm/limb in carbon_defibber.bodyparts) + for(var/obj/item/bodypart/arm/limb in carbon_defibber.get_bodyparts()) if(limb.dismember(silent = FALSE)) removed_arms++ qdel(limb) diff --git a/code/modules/antagonists/cult/cult_armor.dm b/code/modules/antagonists/cult/cult_armor.dm index d4a62d5fd54b..36ede426e691 100644 --- a/code/modules/antagonists/cult/cult_armor.dm +++ b/code/modules/antagonists/cult/cult_armor.dm @@ -151,7 +151,7 @@ return if(!SPT_PROB(15, seconds_per_tick)) return - var/obj/item/bodypart/bone_to_wound = pick(wearer.bodyparts) + var/obj/item/bodypart/bone_to_wound = pick(wearer.get_bodyparts()) var/wound_type = pick(list( /datum/wound/blunt/bone/moderate, /datum/wound/pierce/bleed/moderate, diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm index 71f79a51cbd1..ad7478fc6fd9 100644 --- a/code/modules/antagonists/heretic/influences.dm +++ b/code/modules/antagonists/heretic/influences.dm @@ -141,7 +141,7 @@ // A very elaborate way to suicide visible_message(span_userdanger("Psychic tendrils lash out from [src], psychically grabbing onto [user]'s psychically sensitive mind and tearing [user.p_their()] head off!")) - var/obj/item/bodypart/head/head = locate() in human_user.bodyparts + var/obj/item/bodypart/head/head = human_user.get_bodypart(BODY_ZONE_HEAD) if(head?.dismember()) head.forceMove(src) // stored for later fishage else diff --git a/code/modules/antagonists/heretic/items/heretic_armor.dm b/code/modules/antagonists/heretic/items/heretic_armor.dm index 4d0dffa781cb..8e496353faed 100644 --- a/code/modules/antagonists/heretic/items/heretic_armor.dm +++ b/code/modules/antagonists/heretic/items/heretic_armor.dm @@ -142,7 +142,7 @@ return var/mob/living/carbon/victim = user var/iteration = 0 - for(var/obj/item/bodypart/limb as anything in victim.bodyparts) + for(var/obj/item/bodypart/limb as anything in victim.get_bodyparts()) if(istype(limb, /obj/item/bodypart/head) || istype(limb, /obj/item/bodypart/chest)) continue iteration++ @@ -259,7 +259,7 @@ if(!length(valid_turfs)) var/mob/living/carbon/carbon_target = target if(iscarbon(target)) - var/obj/item/bodypart/limb = pick(carbon_target.bodyparts) + var/obj/item/bodypart/limb = pick(carbon_target.get_bodyparts()) limb.force_wound_upwards(/datum/wound/slash/flesh/severe) return throw_blade(pick(valid_turfs), target) @@ -451,7 +451,7 @@ return var/mob/living/carbon/victim = user var/iteration = 0 - for(var/obj/item/bodypart/limb as anything in victim.bodyparts) + for(var/obj/item/bodypart/limb as anything in victim.get_bodyparts()) iteration++ addtimer(CALLBACK(limb, TYPE_PROC_REF(/obj/item/bodypart, force_wound_upwards), /datum/wound/slash/flesh/critical), 1 SECONDS * iteration) diff --git a/code/modules/antagonists/heretic/items/heretic_grenade.dm b/code/modules/antagonists/heretic/items/heretic_grenade.dm index 14ceaf1183e2..3a5e0e2a1293 100644 --- a/code/modules/antagonists/heretic/items/heretic_grenade.dm +++ b/code/modules/antagonists/heretic/items/heretic_grenade.dm @@ -95,7 +95,7 @@ addtimer(CALLBACK(victim, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/reagent/pepperspray), 10 SECONDS) victim.update_damage_hud() victim.adjust_disgust(5) - for(var/obj/item/bodypart/robotic_limb in victim.bodyparts) + for(var/obj/item/bodypart/robotic_limb in victim.get_bodyparts()) if(robotic_limb.biological_state & BIO_ROBOTIC) robotic_limb.receive_damage(5, 5) if(methods & INGEST) diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index cb2ac3d3bde0..f5282015b0a8 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -267,7 +267,7 @@ return var/mob/living/carbon/carbon_target = target - var/obj/item/bodypart/bodypart = pick(carbon_target.bodyparts) + var/obj/item/bodypart/bodypart = pick(carbon_target.get_bodyparts()) var/datum/wound/crit_wound = new wound_type() crit_wound.apply_wound(bodypart, attack_direction = get_dir(source, target)) diff --git a/code/modules/antagonists/heretic/knowledge/moon_lore.dm b/code/modules/antagonists/heretic/knowledge/moon_lore.dm index f3a9a9da7124..eb6428aee276 100644 --- a/code/modules/antagonists/heretic/knowledge/moon_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/moon_lore.dm @@ -300,7 +300,7 @@ to_chat(carbon_view, span_boldbig(span_red(\ "YOUR SENSES REEL AS YOUR MIND IS ENVELOPED BY AN OTHERWORLDLY FORCE ATTEMPTING TO REWRITE YOUR VERY BEING. \ YOU CANNOT EVEN BEGIN TO SCREAM BEFORE YOUR IMPLANT ACTIVATES ITS PSIONIC FAIL-SAFE PROTOCOL, TAKING YOUR HEAD WITH IT."))) - var/obj/item/bodypart/head/head = locate() in carbon_view.bodyparts + var/obj/item/bodypart/head/head = carbon_view.get_bodypart(BODY_ZONE_HEAD) if(!head?.dismember()) carbon_view.gib(DROP_ALL_REMAINS) var/datum/effect_system/reagents_explosion/explosion = new(get_turf(carbon_view), 1, 1, 1) diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 207669735e02..565879a7c8c3 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -81,7 +81,7 @@ if(iscarbon(target)) var/mob/living/carbon/carbon_target = target - for(var/obj/item/bodypart/robotic_limb as anything in carbon_target.bodyparts) + for(var/obj/item/bodypart/robotic_limb as anything in carbon_target.get_bodyparts()) if(IS_ROBOTIC_LIMB(robotic_limb)) robotic_limb.receive_damage(500) diff --git a/code/modules/antagonists/heretic/knowledge/side_knowledge/tier_two.dm b/code/modules/antagonists/heretic/knowledge/side_knowledge/tier_two.dm index cac879740de5..5d155281e5eb 100644 --- a/code/modules/antagonists/heretic/knowledge/side_knowledge/tier_two.dm +++ b/code/modules/antagonists/heretic/knowledge/side_knowledge/tier_two.dm @@ -29,10 +29,10 @@ /datum/heretic_knowledge/codex_morbus/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) . = ..() var/mob/living/carbon/human/to_fuck_up = locate() in selected_atoms - for(var/_limb in to_fuck_up.bodyparts) + for(var/_limb in to_fuck_up.get_bodyparts()) var/obj/item/bodypart/limb = _limb limb.force_wound_upwards(/datum/wound/slash/flesh/critical) - for(var/obj/item/bodypart/limb as anything in to_fuck_up.bodyparts) + for(var/obj/item/bodypart/limb as anything in to_fuck_up.get_bodyparts()) to_fuck_up.cause_wound_of_type_and_severity(WOUND_BLUNT, limb, WOUND_SEVERITY_CRITICAL) return TRUE diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm index 830e6edae325..6647ee086bcf 100644 --- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm @@ -257,7 +257,7 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) // If it is, we will damage a random bodypart, and check that bodypart for its body type, to select between 'skin' or 'exterior'. if(iscarbon(body)) var/mob/living/carbon/carbody = body - var/obj/item/bodypart/bodypart = pick(carbody.bodyparts) + var/obj/item/bodypart/bodypart = pick(carbody.get_bodyparts()) ripped_thing = bodypart carbody.apply_damage(25, BRUTE, bodypart, sharpness = SHARP_EDGED) diff --git a/code/modules/antagonists/heretic/magic/apetravulnera.dm b/code/modules/antagonists/heretic/magic/apetravulnera.dm index c0398b2b7458..b878fd36bedb 100644 --- a/code/modules/antagonists/heretic/magic/apetravulnera.dm +++ b/code/modules/antagonists/heretic/magic/apetravulnera.dm @@ -38,7 +38,7 @@ return FALSE var/a_limb_got_damaged = FALSE - for(var/obj/item/bodypart/bodypart in cast_on.bodyparts) + for(var/obj/item/bodypart/bodypart in cast_on.get_bodyparts()) if(bodypart.brute_dam < 15) continue a_limb_got_damaged = TRUE @@ -47,7 +47,7 @@ if(!a_limb_got_damaged) var/datum/wound/slash/crit_wound = new wound_type() - crit_wound.apply_wound(pick(cast_on.bodyparts)) + crit_wound.apply_wound(pick(cast_on.get_bodyparts())) cast_on.visible_message( span_danger("[cast_on]'s scratches and bruises are torn open by an unholy force!"), diff --git a/code/modules/antagonists/heretic/magic/blood_cleave.dm b/code/modules/antagonists/heretic/magic/blood_cleave.dm index 065286f490e5..e0c9454ba33c 100644 --- a/code/modules/antagonists/heretic/magic/blood_cleave.dm +++ b/code/modules/antagonists/heretic/magic/blood_cleave.dm @@ -44,7 +44,7 @@ span_danger("Your veins burst from within and unholy flame erupts from your blood!") ) - var/obj/item/bodypart/bodypart = pick(victim.bodyparts) + var/obj/item/bodypart/bodypart = pick(victim.get_bodyparts()) var/datum/wound/slash/flesh/crit_wound = new wound_type() crit_wound.apply_wound(bodypart) victim.apply_damage(20, BURN, wound_bonus = CANT_WOUND) diff --git a/code/modules/antagonists/heretic/magic/blood_siphon.dm b/code/modules/antagonists/heretic/magic/blood_siphon.dm index cb0b4ec355c7..3ef5a7920f84 100644 --- a/code/modules/antagonists/heretic/magic/blood_siphon.dm +++ b/code/modules/antagonists/heretic/magic/blood_siphon.dm @@ -50,11 +50,11 @@ var/mob/living/carbon/carbon_target = cast_on var/mob/living/carbon/carbon_user = owner - for(var/obj/item/bodypart/bodypart as anything in carbon_user.bodyparts) + for(var/obj/item/bodypart/bodypart as anything in carbon_user.get_bodyparts()) for(var/datum/wound/iter_wound as anything in bodypart.wounds) if(prob(50)) continue - var/obj/item/bodypart/target_bodypart = locate(bodypart.type) in carbon_target.bodyparts + var/obj/item/bodypart/target_bodypart = carbon_target.get_bodypart(bodypart.body_zone) if(!target_bodypart) continue iter_wound.remove_wound() diff --git a/code/modules/antagonists/heretic/magic/crimson_cleave.dm b/code/modules/antagonists/heretic/magic/crimson_cleave.dm index b64a5221a50c..6ee02e2fea9b 100644 --- a/code/modules/antagonists/heretic/magic/crimson_cleave.dm +++ b/code/modules/antagonists/heretic/magic/crimson_cleave.dm @@ -32,7 +32,7 @@ . = ..() if(iscarbon(owner)) var/mob/living/carbon/carbon_owner = owner - for(var/obj/item/bodypart/limbs as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/limbs as anything in carbon_owner.get_bodyparts()) for(var/datum/wound/iter_wound as anything in limbs.wounds) iter_wound.remove_wound() diff --git a/code/modules/antagonists/heretic/status_effects/buffs.dm b/code/modules/antagonists/heretic/status_effects/buffs.dm index f3660e2f8136..e48020494339 100644 --- a/code/modules/antagonists/heretic/status_effects/buffs.dm +++ b/code/modules/antagonists/heretic/status_effects/buffs.dm @@ -87,7 +87,7 @@ if(!iscarbon(owner)) return var/mob/living/carbon/drinker = owner - for(var/obj/item/bodypart/potentially_wounded as anything in drinker.bodyparts) + for(var/obj/item/bodypart/potentially_wounded as anything in drinker.get_bodyparts()) for(var/datum/wound/found_wound as anything in potentially_wounded.wounds) found_wound.remove_wound() if(length(drinker.get_missing_limbs())) @@ -102,7 +102,7 @@ carbie.adjust_brute_loss(-0.5 * seconds_between_ticks, updating_health = FALSE) carbie.adjust_fire_loss(-0.5 * seconds_between_ticks, updating_health = FALSE) - for(var/BP in carbie.bodyparts) + for(var/BP in carbie.get_bodyparts()) var/obj/item/bodypart/part = BP for(var/W in part.wounds) var/datum/wound/wound = W diff --git a/code/modules/antagonists/heretic/status_effects/heretic_passive.dm b/code/modules/antagonists/heretic/status_effects/heretic_passive.dm index 6f5786b809bc..08d2292d677a 100644 --- a/code/modules/antagonists/heretic/status_effects/heretic_passive.dm +++ b/code/modules/antagonists/heretic/status_effects/heretic_passive.dm @@ -307,7 +307,7 @@ if(!iscarbon(owner)) return var/mob/living/carbon/carbon_eater = owner - for(var/obj/item/bodypart/wounded_limb as anything in carbon_eater.bodyparts) + for(var/obj/item/bodypart/wounded_limb as anything in carbon_eater.get_bodyparts()) for(var/datum/wound/to_cure as anything in wounded_limb.wounds) to_cure.remove_wound() break @@ -543,7 +543,7 @@ var/mob/living/carbon/carbon_owner = source if(passive_level < HERETIC_LEVEL_UPGRADE) return - for(var/obj/item/bodypart/wounded_limb as anything in carbon_owner.bodyparts) + for(var/obj/item/bodypart/wounded_limb as anything in carbon_owner.get_bodyparts()) for(var/datum/wound/to_cure as anything in wounded_limb.wounds) to_cure.remove_wound() for(var/obj/item/organ/internal as anything in carbon_owner.organs) diff --git a/code/modules/antagonists/heretic/status_effects/mark_effects.dm b/code/modules/antagonists/heretic/status_effects/mark_effects.dm index 465ad530d24e..18b35b2eedca 100644 --- a/code/modules/antagonists/heretic/status_effects/mark_effects.dm +++ b/code/modules/antagonists/heretic/status_effects/mark_effects.dm @@ -64,7 +64,7 @@ /datum/status_effect/eldritch/flesh/on_effect() if(ishuman(owner)) var/mob/living/carbon/human/human_owner = owner - var/obj/item/bodypart/bodypart = pick(human_owner.bodyparts) + var/obj/item/bodypart/bodypart = pick(human_owner.get_bodyparts()) human_owner.cause_wound_of_type_and_severity(WOUND_SLASH, bodypart, WOUND_SEVERITY_SEVERE) return ..() diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm index d58fd398d7ad..a1020c3c93ee 100644 --- a/code/modules/antagonists/spy/spy_bounty.dm +++ b/code/modules/antagonists/spy/spy_bounty.dm @@ -663,7 +663,7 @@ /datum/spy_bounty/targets_person/some_item/limb_or_organ/find_desired_thing(mob/living/carbon/human/crewmember) if(ispath(desired_type, /obj/item/bodypart)) - return locate(desired_type) in crewmember.bodyparts + return locate(desired_type) in crewmember.get_bodyparts() if(ispath(desired_type, /obj/item/organ)) return locate(desired_type) in crewmember.organs return null diff --git a/code/modules/antagonists/voidwalker/voidwalker_traumas.dm b/code/modules/antagonists/voidwalker/voidwalker_traumas.dm index 3deab1fd0d60..c6c2f3f2340e 100644 --- a/code/modules/antagonists/voidwalker/voidwalker_traumas.dm +++ b/code/modules/antagonists/voidwalker/voidwalker_traumas.dm @@ -45,7 +45,7 @@ RegisterSignal(owner, COMSIG_CARBON_ATTACH_LIMB, PROC_REF(texture_limb)) //also catch new limbs being attached RegisterSignal(owner, COMSIG_CARBON_REMOVE_LIMB, PROC_REF(untexture_limb)) //and remove it from limbs if they go away - for(var/obj/item/bodypart as anything in owner.bodyparts) + for(var/obj/item/bodypart as anything in owner.get_bodyparts()) texture_limb(owner, bodypart) if(ishuman(owner)) @@ -75,7 +75,7 @@ var/mob/living/carbon/human/human = owner human.physiology.brute_mod /= brute_mod - for(var/obj/item/bodypart/bodypart as anything in owner.bodyparts) + for(var/obj/item/bodypart/bodypart as anything in owner.get_bodyparts()) untexture_limb(owner, bodypart) owner.update_body() diff --git a/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm index a9047c79878a..40f271a30f47 100644 --- a/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm +++ b/code/modules/antagonists/wizard/grand_ritual/finales/immortality.dm @@ -124,7 +124,7 @@ target_quirk.add_to_holder(target) dna.copy_dna(target.dna, COPY_DNA_SE|COPY_DNA_SPECIES) - for(var/obj/item/bodypart/limb as anything in target.bodyparts) + for(var/obj/item/bodypart/limb as anything in target.get_bodyparts()) limb.update_limb(is_creating = TRUE) target.updateappearance(mutcolor_update = TRUE) target.domutcheck() diff --git a/code/modules/cargo/supplypod.dm b/code/modules/cargo/supplypod.dm index c1829cb21cfb..6d237f67f8dd 100644 --- a/code/modules/cargo/supplypod.dm +++ b/code/modules/cargo/supplypod.dm @@ -329,7 +329,7 @@ if (iscarbon(target_living)) //If effectLimb is true (which means we pop limbs off when we hit people): if (effectLimb) var/mob/living/carbon/carbon_target_mob = target_living - for (var/bp in carbon_target_mob.bodyparts) //Look at the bodyparts in our poor mob beneath our pod as it lands + for (var/bp in carbon_target_mob.get_bodyparts()) //Look at the bodyparts in our poor mob beneath our pod as it lands var/obj/item/bodypart/bodypart = bp if(bodypart.body_part != HEAD && bodypart.body_part != CHEST)//we dont want to kill him, just teach em a lesson! if(bodypart.dismember()) //Using the power of flextape i've sawed this man's limb in half! @@ -342,7 +342,7 @@ organ_to_yeet.forceMove(turf_underneath) //Move the organ outta the body organ_to_yeet.throw_at(destination, 2, 3) //Thow the organ at a random tile 3 spots away sleep(0.1 SECONDS) - for (var/bp in carbon_target_mob.bodyparts) //Look at the bodyparts in our poor mob beneath our pod as it lands + for (var/bp in carbon_target_mob.get_bodyparts()) //Look at the bodyparts in our poor mob beneath our pod as it lands var/obj/item/bodypart/bodypart = bp var/destination = get_edge_target_turf(turf_underneath, pick(GLOB.alldirs)) if (bodypart.dismember()) //Using the power of flextape i've sawed this man's bodypart in half! diff --git a/code/modules/client/preferences/species_features/lizard.dm b/code/modules/client/preferences/species_features/lizard.dm index 10236d9e4154..849d1a21114a 100644 --- a/code/modules/client/preferences/species_features/lizard.dm +++ b/code/modules/client/preferences/species_features/lizard.dm @@ -94,7 +94,7 @@ correct_legs[BODY_ZONE_R_LEG] = /obj/item/bodypart/leg/right/digitigrade correct_legs[BODY_ZONE_L_LEG] = /obj/item/bodypart/leg/left/digitigrade - for(var/obj/item/bodypart/old_part as anything in target.bodyparts) + for(var/obj/item/bodypart/old_part as anything in target.get_bodyparts()) if(old_part.change_exempt_flags & BP_BLOCK_CHANGE_SPECIES) continue diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm index 8f658884830f..271322b1a496 100644 --- a/code/modules/experisci/experiment/experiments.dm +++ b/code/modules/experisci/experiment/experiments.dm @@ -452,7 +452,7 @@ return if (isandroid(check)) return TRUE - if (length(check.organs) < 6 || length(check.bodyparts) < 6) + if (length(check.organs) < 6 || length(check.get_missing_limbs()) > 1) return FALSE var/static/list/augmented_organ_slots = list( @@ -468,7 +468,7 @@ continue if (!IS_ROBOTIC_ORGAN(organ)) return FALSE - for (var/obj/item/bodypart/bodypart as anything in check.bodyparts) + for (var/obj/item/bodypart/bodypart as anything in check.get_bodyparts()) if (!IS_ROBOTIC_LIMB(bodypart)) return FALSE return TRUE diff --git a/code/modules/fishing/fish/types/holographic.dm b/code/modules/fishing/fish/types/holographic.dm index a4a10857d467..ae4f550ab85c 100644 --- a/code/modules/fishing/fish/types/holographic.dm +++ b/code/modules/fishing/fish/types/holographic.dm @@ -115,7 +115,7 @@ if(!iscarbon(user)) return ..() - for(var/obj/item/bodypart/limb in user.bodyparts) + for(var/obj/item/bodypart/limb in user.get_bodyparts()) limb.add_color_override(COLOR_WHITE, LIMB_COLOR_CS_SOURCE_SUICIDE) limb.add_bodypart_overlay(new /datum/bodypart_overlay/texture/checkered(), update = FALSE) diff --git a/code/modules/fishing/fish/types/ruins.dm b/code/modules/fishing/fish/types/ruins.dm index 8654d45538f7..db02dcd7130f 100644 --- a/code/modules/fishing/fish/types/ruins.dm +++ b/code/modules/fishing/fish/types/ruins.dm @@ -167,7 +167,7 @@ return SHAME var/skin_tone - for(var/obj/item/bodypart/to_wound as anything in user.bodyparts) + for(var/obj/item/bodypart/to_wound as anything in user.get_bodyparts()) if(to_wound == user.get_bodypart(BODY_ZONE_CHEST)) skin_tone = to_wound.species_color || skintone2hex(to_wound.skin_tone) user.cause_wound_of_type_and_severity(WOUND_SLASH, to_wound, WOUND_SEVERITY_CRITICAL, WOUND_SEVERITY_CRITICAL) diff --git a/code/modules/food_and_drinks/machinery/gibber.dm b/code/modules/food_and_drinks/machinery/gibber.dm index 3422eee9944d..0c661867370c 100644 --- a/code/modules/food_and_drinks/machinery/gibber.dm +++ b/code/modules/food_and_drinks/machinery/gibber.dm @@ -234,7 +234,7 @@ var/mob/living/carbon/human/agent_whiskey = victim var/drop_chance = 0 - for (var/obj/item/bodypart/limb as anything in agent_whiskey.bodyparts) + for (var/obj/item/bodypart/limb as anything in agent_whiskey.get_bodyparts()) if (!limb.butcher_drops) continue diff --git a/code/modules/hallucination/blood_flow.dm b/code/modules/hallucination/blood_flow.dm index 7805768c2529..71dbb5c8acf3 100644 --- a/code/modules/hallucination/blood_flow.dm +++ b/code/modules/hallucination/blood_flow.dm @@ -11,11 +11,11 @@ return FALSE var/mob/living/carbon/carb_hallucinator = hallucinator - if(!length(carb_hallucinator.bodyparts) || !carb_hallucinator.can_bleed()) + var/list/bodyparts = carb_hallucinator.get_bodyparts() + if(!length(bodyparts) || !carb_hallucinator.can_bleed()) return FALSE var/obj/item/bodypart/picked - var/list/bodyparts = carb_hallucinator.bodyparts.Copy() while(isnull(picked) && length(bodyparts)) picked = pick_n_take(bodyparts) if(!picked.can_bleed()) diff --git a/code/modules/hallucination/screwy_health_doll.dm b/code/modules/hallucination/screwy_health_doll.dm index 35f7356b4d4a..2ab8a8c928af 100644 --- a/code/modules/hallucination/screwy_health_doll.dm +++ b/code/modules/hallucination/screwy_health_doll.dm @@ -50,7 +50,7 @@ /datum/hallucination/fake_health_doll/proc/add_fake_limb(obj/item/bodypart/specific_limb, severity) var/mob/living/carbon/human/human_mob = hallucinator - var/obj/item/bodypart/picked = specific_limb || pick(human_mob.bodyparts) + var/obj/item/bodypart/picked = specific_limb || pick(human_mob.get_bodyparts()) if(!(picked in bodyparts)) RegisterSignals(picked, list(COMSIG_QDELETING, COMSIG_BODYPART_REMOVED), PROC_REF(remove_bodypart)) RegisterSignal(picked, COMSIG_BODYPART_UPDATING_HEALTH_HUD, PROC_REF(on_bodypart_hud_update)) diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index 936028f86f01..899224b33f34 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -49,7 +49,7 @@ if(!ishuman(spawned) || !prob(PIG_COP_PROBABILITY)) return var/mob/living/carbon/human/piggy = spawned - for (var/obj/item/bodypart/ham as anything in piggy.bodyparts) + for (var/obj/item/bodypart/ham as anything in piggy.get_bodyparts()) // These are string lists ham.butcher_drops = ham.butcher_drops.Copy() for (var/meat_type in ham.butcher_drops) diff --git a/code/modules/jobs/job_types/prisoner.dm b/code/modules/jobs/job_types/prisoner.dm index 8c55dfcdce06..99b3eab21550 100644 --- a/code/modules/jobs/job_types/prisoner.dm +++ b/code/modules/jobs/job_types/prisoner.dm @@ -73,7 +73,7 @@ var/datum/prisoner_crime/crime = GLOB.prisoner_crimes[crime_name] if (isnull(crime)) return - var/list/limbs_to_tat = new_prisoner.bodyparts.Copy() + var/list/limbs_to_tat = new_prisoner.get_bodyparts() for(var/i in 1 to crime.tattoos) if(!length(SSpersistence.prison_tattoos_to_use) || visuals_only) return diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 06c78c25f1c7..d78fb4f94b07 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -62,7 +62,7 @@ GLOBAL_LIST_EMPTY(security_officer_distribution) if(!ishuman(spawned) || !prob(PIG_COP_PROBABILITY)) return var/mob/living/carbon/human/piggy = spawned - for (var/obj/item/bodypart/ham as anything in piggy.bodyparts) + for (var/obj/item/bodypart/ham as anything in piggy.get_bodyparts()) // These are string lists ham.butcher_drops = ham.butcher_drops.Copy() for (var/meat_type in ham.butcher_drops) diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index bcc07dfd2d66..eea6d428c101 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -47,7 +47,7 @@ if(!ishuman(spawned) || !prob(PIG_COP_PROBABILITY)) return var/mob/living/carbon/human/piggy = spawned - for (var/obj/item/bodypart/ham as anything in piggy.bodyparts) + for (var/obj/item/bodypart/ham as anything in piggy.get_bodyparts()) // These are string lists ham.butcher_drops = ham.butcher_drops.Copy() for (var/meat_type in ham.butcher_drops) diff --git a/code/modules/library/bibles.dm b/code/modules/library/bibles.dm index 950f2c70a5c5..07a83912f003 100644 --- a/code/modules/library/bibles.dm +++ b/code/modules/library/bibles.dm @@ -215,7 +215,7 @@ GLOBAL_LIST_INIT(bibleitemstates, list( return BLESSING_FAILED var/mob/living/carbon/human/built_in_his_image = blessed - for(var/obj/item/bodypart/bodypart as anything in built_in_his_image.bodyparts) + for(var/obj/item/bodypart/bodypart as anything in built_in_his_image.get_bodyparts()) if(!IS_ORGANIC_LIMB(bodypart)) balloon_alert(user, "can't heal inorganic!") return BLESSING_IGNORED diff --git a/code/modules/lost_crew/damages/post_mortem.dm b/code/modules/lost_crew/damages/post_mortem.dm index 5eeaabc3302c..0e70b0cad62c 100644 --- a/code/modules/lost_crew/damages/post_mortem.dm +++ b/code/modules/lost_crew/damages/post_mortem.dm @@ -36,7 +36,11 @@ /datum/corpse_damage/post_mortem/limb_loss/apply_to_body(mob/living/carbon/human/body, severity, list/saved_movables, list/datum/callback/on_revive_and_player_occupancy) var/limbs_to_take = round(min_limbs + (max_limbs - min_limbs) * severity) - var/list/limbs_we_can_take = body.bodyparts - body.get_bodypart(BODY_ZONE_HEAD) - body.get_bodypart(BODY_ZONE_CHEST) + var/list/limbs_we_can_take = list() + for(var/zone in GLOB.limb_zones) + var/bodypart = body.get_bodypart(zone) + if(bodypart) + limbs_we_can_take += bodypart if(!limbs_we_can_take.len) return diff --git a/code/modules/lost_crew/lost_crew_manager.dm b/code/modules/lost_crew/lost_crew_manager.dm index 1494204ad712..08d6d7246813 100644 --- a/code/modules/lost_crew/lost_crew_manager.dm +++ b/code/modules/lost_crew/lost_crew_manager.dm @@ -36,9 +36,12 @@ GLOBAL_DATUM_INIT(lost_crew_manager, /datum/lost_crew_manager, new) var/datum/corpse_damage_class/scenario = forced_class || pick_weight(scenarios) scenario = new scenario () + new_body.living_flags |= STOP_OVERLAY_UPDATE_BODY_PARTS scenario.apply_character(new_body, protected_items, recovered_items, on_revive_and_player_occupancy, body_data) scenario.apply_injuries(new_body, recovered_items, on_revive_and_player_occupancy, body_data) scenario.death_lore += "I should get a formalized assignment!" + new_body.living_flags &= ~STOP_OVERLAY_UPDATE_BODY_PARTS + new_body.update_body_parts() . = new_body // so bodies can also be used for runes, morgue, etc diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index c3d55e7167b4..4c57ced89bbb 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -677,6 +677,7 @@ if(hud_used) hud_used.build_hand_slots() + hud_used.healthdoll.update_body_zones() //GetAllContents that is reasonable and not stupid /mob/living/proc/get_all_gear(equipment_flags = INCLUDE_ACCESSORIES|INCLUDE_PROSTHETICS, recursive = TRUE) diff --git a/code/modules/mob/living/basic/cult/constructs/harvester.dm b/code/modules/mob/living/basic/cult/constructs/harvester.dm index 8fc1226ab097..1c36453b1847 100644 --- a/code/modules/mob/living/basic/cult/constructs/harvester.dm +++ b/code/modules/mob/living/basic/cult/constructs/harvester.dm @@ -46,7 +46,7 @@ return ..() var/mob/living/carbon/carbon_target = attack_target - for(var/obj/item/bodypart/limb as anything in carbon_target.bodyparts) + for(var/obj/item/bodypart/limb as anything in carbon_target.get_bodyparts()) if(limb.body_part == HEAD || limb.body_part == CHEST) continue return ..() //if any arms or legs exist, attack diff --git a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm index 583542ff5ce2..0f94efaf7fbd 100644 --- a/code/modules/mob/living/basic/farm_animals/goat/_goat.dm +++ b/code/modules/mob/living/basic/farm_animals/goat/_goat.dm @@ -79,7 +79,7 @@ if(ishuman(living_target)) var/mob/living/carbon/human/plant_man = target - edible_bodypart = pick(plant_man.bodyparts) + edible_bodypart = pick(plant_man.get_bodyparts()) edible_bodypart.dismember() living_target.visible_message( diff --git a/code/modules/mob/living/basic/ruin_defender/flesh.dm b/code/modules/mob/living/basic/ruin_defender/flesh.dm index 5d607c2c1a44..94bb50f026dd 100644 --- a/code/modules/mob/living/basic/ruin_defender/flesh.dm +++ b/code/modules/mob/living/basic/ruin_defender/flesh.dm @@ -114,7 +114,7 @@ return var/list/zone_candidates = target.get_missing_limbs() - for(var/obj/item/bodypart/bodypart in target.bodyparts) + for(var/obj/item/bodypart/bodypart in target.get_bodyparts()) if(bodypart.body_zone == BODY_ZONE_HEAD || bodypart.body_zone == BODY_ZONE_CHEST) continue if(HAS_TRAIT(bodypart, TRAIT_IGNORED_BY_LIVING_FLESH)) diff --git a/code/modules/mob/living/basic/space_fauna/lightgeist.dm b/code/modules/mob/living/basic/space_fauna/lightgeist.dm index f59c5070bff9..36ae135619c5 100644 --- a/code/modules/mob/living/basic/space_fauna/lightgeist.dm +++ b/code/modules/mob/living/basic/space_fauna/lightgeist.dm @@ -103,7 +103,7 @@ if (!iscarbon(target)) return target.get_brute_loss() > 0 || target.get_fire_loss() > 0 var/mob/living/carbon/carbon_target = target - for (var/obj/item/bodypart/part in carbon_target.bodyparts) + for (var/obj/item/bodypart/part in carbon_target.get_bodyparts()) if (!part.brute_dam && !part.burn_dam) continue if (!(part.bodytype & required_bodytype)) diff --git a/code/modules/mob/living/blood.dm b/code/modules/mob/living/blood.dm index 5b75da864436..f06bed87776f 100644 --- a/code/modules/mob/living/blood.dm +++ b/code/modules/mob/living/blood.dm @@ -180,7 +180,7 @@ bleed(bleed_rate * seconds_per_tick) bleed_warn(bleed_rate) - for (var/obj/item/bodypart/bodypart as anything in bodyparts) + for (var/obj/item/bodypart/bodypart as anything in get_bodyparts()) if (bodypart.generic_bleedstacks) bodypart.adjustBleedStacks(-1, 0) @@ -263,7 +263,7 @@ /// Has each bodypart update its bleed/wound overlay icon states /mob/living/carbon/proc/update_bodypart_bleed_overlays() - for(var/obj/item/bodypart/iter_part as anything in bodyparts) + for(var/obj/item/bodypart/iter_part as anything in get_bodyparts()) iter_part.update_part_wound_overlay() /// Bleeds amount units of blood from the mob, sometimes creating a blood splatter on the floor. @@ -290,7 +290,7 @@ return 0 . = 0 - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) . += bodypart.cached_bleed_rate /mob/living/carbon/human/get_bleed_rate() @@ -359,7 +359,7 @@ /mob/living/carbon/restore_blood() . = ..() - for(var/obj/item/bodypart/bodypart_to_restore as anything in bodyparts) + for(var/obj/item/bodypart/bodypart_to_restore as anything in get_bodyparts()) bodypart_to_restore.setBleedStacks(0) /**************************************************** diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index d1ee541429fd..bc884201e83c 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -425,8 +425,7 @@ return var/total_burn = 0 var/total_brute = 0 - for(var/X in bodyparts) //hardcoded to streamline things a bit - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) total_brute += (BP.brute_dam * BP.body_damage_coeff) total_burn += (BP.burn_dam * BP.body_damage_coeff) set_health(round(maxHealth - get_oxy_loss() - get_tox_loss() - total_burn - total_brute, DAMAGE_PRECISION)) @@ -773,7 +772,7 @@ if(heal_flags & HEAL_LIMBS) regenerate_limbs() - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts(include_stumps = TRUE)) limb.remove_surgical_state(ALL) if(heal_flags & (HEAL_REFRESH_ORGANS|HEAL_ORGANS)) @@ -911,13 +910,15 @@ switch(new_bodypart.body_part) if(LEG_LEFT, LEG_RIGHT) - set_num_legs(num_legs + 1) - if(!new_bodypart.bodypart_disabled) - set_usable_legs(usable_legs + 1) + if(!IS_STUMP(new_bodypart)) + set_num_legs(num_legs + 1) + if(!new_bodypart.bodypart_disabled) + set_usable_legs(usable_legs + 1) if(ARM_LEFT, ARM_RIGHT) - set_num_hands(num_hands + 1) - if(!new_bodypart.bodypart_disabled) - set_usable_hands(usable_hands + 1) + if(!IS_STUMP(new_bodypart)) + set_num_hands(num_hands + 1) + if(!new_bodypart.bodypart_disabled) + set_usable_hands(usable_hands + 1) synchronize_bodytypes() synchronize_bodyshapes() @@ -938,13 +939,27 @@ switch(old_bodypart.body_part) if(LEG_LEFT, LEG_RIGHT) - set_num_legs(num_legs - 1) - if(!old_bodypart.bodypart_disabled) - set_usable_legs(usable_legs - 1) + if(!IS_STUMP(old_bodypart)) + set_num_legs(num_legs - 1) + if(!old_bodypart.bodypart_disabled) + set_usable_legs(usable_legs - 1) if(ARM_LEFT, ARM_RIGHT) - set_num_hands(num_hands - 1) - if(!old_bodypart.bodypart_disabled) - set_usable_hands(usable_hands - 1) + if(!IS_STUMP(old_bodypart)) + set_num_hands(num_hands - 1) + if(!old_bodypart.bodypart_disabled) + set_usable_hands(usable_hands - 1) + + if(!special && old_bodypart.stump_typepath) + if(old_bodypart.type == old_bodypart.stump_typepath) + stack_trace("Attempted to replace a stump with a stump") + else + var/obj/item/bodypart/stump = new old_bodypart.stump_typepath() + stump.bodyshape = old_bodypart.bodyshape + stump.bodytype = old_bodypart.bodytype + if(!stump.try_attach_limb(src, special = TRUE)) + // the only way this can happen is if the stump is rejected via signal + // not much we can do about that besides hope they know what they're doing + qdel(stump) synchronize_bodytypes() synchronize_bodyshapes() @@ -952,7 +967,7 @@ ///Updates the bodypart speed modifier based on our bodyparts. /mob/living/carbon/proc/update_bodypart_speed_modifier() var/final_modification = 0 - for(var/obj/item/bodypart/leg/bodypart in bodyparts) + for(var/obj/item/bodypart/leg/bodypart in get_bodyparts()) final_modification += bodypart.speed_modifier add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/bodypart, update = TRUE, multiplicative_slowdown = final_modification) @@ -985,7 +1000,7 @@ return var/list/limb_list = list() if(edit_action == "remove") - for(var/obj/item/bodypart/iter_part as anything in bodyparts) + for(var/obj/item/bodypart/iter_part as anything in get_bodyparts()) limb_list += iter_part.body_zone limb_list -= BODY_ZONE_CHEST else @@ -1019,7 +1034,7 @@ var/limb2add = input(usr, "Select a bodypart type to add", "Add/Replace Bodypart") as null|anything in sort_list(limbtypes) var/obj/item/bodypart/new_bp = new limb2add() if(new_bp.replace_limb(src)) - admin_ticket_log("key_name_admin(usr)] has replaced [src]'s [part.type] with [new_bp.type]") + admin_ticket_log("key_name_admin(usr)] has replaced [src]'s [part?.type || "missing limb"] with [new_bp.type]") qdel(part) else to_chat(usr, "Failed to replace bodypart! They might be incompatible.") @@ -1097,7 +1112,7 @@ /mob/living/carbon/proc/is_bleeding() if(!CAN_HAVE_BLOOD(src)) return FALSE - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) if(part.cached_bleed_rate) return TRUE @@ -1107,7 +1122,7 @@ return FALSE var/total_bleed_rate = 0 - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) total_bleed_rate += part.cached_bleed_rate return total_bleed_rate @@ -1269,9 +1284,9 @@ /// Goes through the organs and bodyparts of the mob and updates their blood_dna_info, in case their blood type has changed (via set_species() or otherwise) /mob/living/carbon/proc/update_cached_blood_dna_info() var/list/blood_dna_info = get_blood_dna_list() - for(var/obj/item/organ/organ in organs) + for(var/obj/item/organ/organ as anything in organs) organ.blood_dna_info = blood_dna_info - for(var/obj/item/bodypart/bodypart in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) bodypart.blood_dna_info = blood_dna_info /// Setter for changing a mob's blood type diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 5dc3cbd7d705..497f133b117e 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -453,7 +453,7 @@ return var/embeds = FALSE - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts()) for(var/obj/item/weapon as anything in limb.embedded_objects) if(!embeds) embeds = TRUE @@ -562,8 +562,7 @@ /mob/living/carbon/get_organic_health() . = health - for (var/_limb in bodyparts) - var/obj/item/bodypart/limb = _limb + for (var/obj/item/bodypart/limb as anything in get_bodyparts()) if (!IS_ORGANIC_LIMB(limb)) . += (limb.brute_dam * limb.body_damage_coeff) + (limb.burn_dam * limb.body_damage_coeff) diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm index 271eef3cd40f..b759c290efea 100644 --- a/code/modules/mob/living/carbon/carbon_update_icons.dm +++ b/code/modules/mob/living/carbon/carbon_update_icons.dm @@ -290,7 +290,7 @@ remove_overlay(DAMAGE_LAYER) var/mutable_appearance/damage_overlay - for(var/obj/item/bodypart/iter_part as anything in bodyparts) + for(var/obj/item/bodypart/iter_part as anything in get_bodyparts()) if(!iter_part.dmg_overlay_type) continue if(isnull(damage_overlay) && (iter_part.brutestate || iter_part.burnstate)) @@ -319,7 +319,7 @@ return var/mutable_appearance/wound_overlay - for(var/obj/item/bodypart/iter_part as anything in bodyparts) + for(var/obj/item/bodypart/iter_part as anything in get_bodyparts()) if(iter_part.bleed_overlay_icon) var/mutable_appearance/blood_overlay = mutable_appearance('icons/mob/effects/bleed_overlays.dmi', "blank", -WOUND_LAYER, appearance_flags = KEEP_TOGETHER) blood_overlay.color = blood_type.get_wound_color(src) @@ -463,38 +463,35 @@ /mob/living/carbon/proc/update_body_parts(update_limb_data) update_damage_overlays() update_wound_overlays() - var/list/needs_update = list() var/limb_count_update = 0 - for(var/obj/item/bodypart/limb as anything in bodyparts) - limb.update_limb(is_creating = update_limb_data) //Update limb actually doesn't do much, get_limb_icon is the cpu eater. + var/list/new_limbs = list() + for(var/body_zone, limb_untyped in get_bodyparts_by_zones()) + var/obj/item/bodypart/limb = limb_untyped + if(isnull(limb) || IS_STUMP(limb)) + if(icon_render_keys[body_zone]) + icon_render_keys -= body_zone + limb_count_update += 1 + continue + + // Update limb actually doesn't do much, get_limb_icon is the cpu eater. + limb.update_limb(is_creating = update_limb_data) - var/old_key = icon_render_keys?[limb.body_zone] //Checks the mob's icon render key list for the bodypart - icon_render_keys[limb.body_zone] = (limb.is_husked) ? limb.generate_husk_key().Join() : limb.generate_icon_key().Join() //Generates a key for the current bodypart + var/old_key = icon_render_keys[limb.body_zone] + var/new_key = limb.get_cache_key() - if(icon_render_keys[limb.body_zone] != old_key) //If the keys match, that means the limb doesn't need to be redrawn - needs_update += limb + if(new_key == old_key) + new_limbs += limb_icon_cache[new_key] - limb_count_update += length(needs_update) - var/list/missing_bodyparts = get_missing_limbs() - if(((dna ? dna.species.max_bodypart_count : BODYPARTS_DEFAULT_MAXIMUM) - icon_render_keys.len) != missing_bodyparts.len) //Checks to see if the target gained or lost any limbs. - limb_count_update += 1 - for(var/missing_limb in missing_bodyparts) - icon_render_keys -= missing_limb //Removes dismembered limbs from the key list + else + limb_icon_cache[new_key] ||= limb.get_limb_icon(dropped = FALSE) + new_limbs += limb_icon_cache[new_key] + icon_render_keys[limb.body_zone] = new_key + limb_count_update += 1 . = limb_count_update if(!.) return - //GENERATE NEW LIMBS - var/list/new_limbs = list() - for(var/obj/item/bodypart/limb as anything in bodyparts) - if(limb in needs_update) - var/bodypart_icon = limb.get_limb_icon(dropped = FALSE, update_on = src) - new_limbs += bodypart_icon - limb_icon_cache[icon_render_keys[limb.body_zone]] = bodypart_icon //Caches the icon with the bodypart key, as it is new - else - new_limbs += limb_icon_cache[icon_render_keys[limb.body_zone]] //Pulls existing sprites from the cache - remove_overlay(BODYPARTS_LAYER) if(new_limbs.len) @@ -508,6 +505,13 @@ ///////////////////////// // Limb Icon Cache 2.0 // ///////////////////////// + +/// Returns a string representing the bodyparts icon cache key +/obj/item/bodypart/proc/get_cache_key() + if(is_husked) + return jointext(generate_husk_key(), "-") + return jointext(generate_icon_key(), "-") + /** * Called from update_body_parts() these procs handle the limb icon cache. * the limb icon cache adds an icon_render_key to a human mob, it represents: @@ -522,20 +526,20 @@ RETURN_TYPE(/list) . = list() if(is_dimorphic) - . += "[limb_gender]-" - . += "[limb_id]" - . += "-[body_zone]" + . += limb_gender + . += limb_id + . += body_zone if(should_draw_greyscale && draw_color) - . += "-[draw_color]" + . += draw_color if(is_invisible) - . += "-invisible" + . += "invisible" for(var/datum/bodypart_overlay/overlay as anything in bodypart_overlays) if(!overlay.can_draw_on_bodypart(src, owner, is_husked)) continue - . += "-[jointext(overlay.generate_icon_cache(), "-")]" + . += overlay.generate_icon_cache() if(ishuman(owner)) var/mob/living/carbon/human/human_owner = owner - . += "-[human_owner.mob_height]" + . += "[human_owner.mob_height]" SEND_SIGNAL(src, COMSIG_BODYPART_GENERATE_ICON_KEY, .) return . @@ -543,24 +547,22 @@ /obj/item/bodypart/proc/generate_husk_key() RETURN_TYPE(/list) . = list() - . += "[limb_id]-" - . += "[husk_type]" - . += "-husk" - . += "-[body_zone]" + if(is_dimorphic) + . += limb_gender + . += limb_id + . += husk_type + . += "husk" + . += body_zone if(is_invisible) - . += "-invisible" - var/list/blood_dna = blood_dna_info || owner?.get_blood_dna_list() - if (LAZYLEN(blood_dna)) - . += "-[get_color_from_blood_list(blood_dna)]" - else - . += "-[BLOOD_COLOR_RED]" + . += "invisible" + . += "[LAZYLEN(blood_dna_info) ? get_color_from_blood_list(blood_dna_info) : BLOOD_COLOR_RED]" for(var/datum/bodypart_overlay/overlay as anything in bodypart_overlays) if(!overlay.can_draw_on_bodypart(src, owner, TRUE)) continue - . += "-[jointext(overlay.generate_icon_cache(), "-")]" + . += overlay.generate_icon_cache() if(ishuman(owner)) var/mob/living/carbon/human/human_owner = owner - . += "-[human_owner.mob_height]" + . += "[human_owner.mob_height]" return . /obj/item/bodypart/head/generate_icon_key() diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm index 191b303f715a..fc2a98df0c0e 100644 --- a/code/modules/mob/living/carbon/damage_procs.dm +++ b/code/modules/mob/living/carbon/damage_procs.dm @@ -20,7 +20,7 @@ // ALso we'll automatically covnert string def zones into bodyparts to pass into parent call. else if(!isbodypart(def_zone)) var/random_zone = check_zone(def_zone || get_random_valid_zone(def_zone)) - def_zone = get_bodypart(random_zone) || bodyparts[1] + def_zone = get_bodypart(random_zone) || get_bodypart() . = ..() // Taking brute or burn to bodyparts gives a damage flash @@ -84,13 +84,13 @@ //These procs fetch a cumulative total damage from all bodyparts /mob/living/carbon/get_brute_loss() var/amount = 0 - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) amount += bodypart.brute_dam return round(amount, DAMAGE_PRECISION) /mob/living/carbon/get_fire_loss() var/amount = 0 - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) amount += bodypart.burn_dam return round(amount, DAMAGE_PRECISION) @@ -104,7 +104,7 @@ */ /mob/living/carbon/proc/get_brute_loss_for_type(required_bodytype = ALL) var/amount = 0 - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) if(!(bodypart.bodytype & required_bodytype)) continue amount += bodypart.brute_dam @@ -119,7 +119,7 @@ */ /mob/living/carbon/proc/get_fire_loss_for_type(required_bodytype = ALL) var/amount = 0 - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts()) if(!(bodypart.bodytype & required_bodytype)) continue amount += bodypart.burn_dam @@ -239,8 +239,7 @@ ///Returns a list of damaged bodyparts /mob/living/carbon/proc/get_damaged_bodyparts(brute = FALSE, burn = FALSE, required_bodytype = NONE, target_zone = null) var/list/obj/item/bodypart/parts = list() - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) if(required_bodytype && !(BP.bodytype & required_bodytype)) continue if(!isnull(target_zone) && BP.body_zone != target_zone) @@ -252,8 +251,7 @@ ///Returns a list of damageable bodyparts /mob/living/carbon/proc/get_damageable_bodyparts(required_bodytype) var/list/obj/item/bodypart/parts = list() - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) if(required_bodytype && !(BP.bodytype & required_bodytype)) continue if(BP.brute_dam + BP.burn_dam < BP.max_damage) @@ -264,8 +262,7 @@ ///Returns a list of bodyparts with wounds (in case someone has a wound on an otherwise fully healed limb) /mob/living/carbon/proc/get_wounded_bodyparts(required_bodytype) var/list/obj/item/bodypart/parts = list() - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) if(required_bodytype && !(BP.bodytype & required_bodytype)) continue if(LAZYLEN(BP.wounds)) diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm index 77611210d8ca..292eda9c6ace 100644 --- a/code/modules/mob/living/carbon/death.dm +++ b/code/modules/mob/living/carbon/death.dm @@ -28,7 +28,7 @@ return ..() /mob/living/carbon/get_gibs_type(drop_bitflags = NONE) - var/obj/item/bodypart/chest = get_bodypart(BODY_ZONE_CHEST) || (length(bodyparts) ? bodyparts[1] : null) + var/obj/item/bodypart/chest = get_bodypart(BODY_ZONE_CHEST) || get_bodypart() if (!istype(chest)) // what return ..() @@ -74,13 +74,22 @@ qdel(organ) /mob/living/carbon/spread_bodyparts(drop_bitflags=NONE) - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) + if(part.body_zone == BODY_ZONE_CHEST) + continue // never drop this if(!(drop_bitflags & DROP_BRAIN) && part.body_zone == BODY_ZONE_HEAD) - continue - else if(part.body_zone == BODY_ZONE_CHEST) - continue - part.drop_limb() + continue // don't drop head if we aren't dropping a brain + + var/list/leftover_organs = list() + for(var/obj/item/organ/leftover in part) + leftover_organs += leftover + + part.drop_limb(TRUE) part.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), rand(1,3), 5) + // any organs that weren't throw out already about need to follow the bodypart out + for(var/obj/item/organ/leftover as anything in leftover_organs) + leftover.Remove(src, TRUE) + leftover.bodypart_insert(part) /mob/living/carbon/set_suicide(suicide_state) //you thought that box trick was pretty clever, didn't you? well now hardmode is on, boyo. . = ..() diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index 946e009d4f8c..279e65b83552 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -50,7 +50,7 @@ . += span_deadsay("It appears that [t_his] brain is missing...") var/list/disabled = list() - for(var/obj/item/bodypart/body_part as anything in bodyparts) + for(var/obj/item/bodypart/body_part as anything in get_bodyparts()) if(body_part.bodypart_disabled) disabled += body_part @@ -178,7 +178,7 @@ var/list/obj/item/bodypart/bleeding_limbs = list() var/list/obj/item/bodypart/grasped_limbs = list() - for(var/obj/item/bodypart/body_part as anything in bodyparts) + for(var/obj/item/bodypart/body_part as anything in get_bodyparts()) if(body_part.cached_bleed_rate) bleeding_limbs += body_part.plaintext_zone if(body_part.grasped_by) @@ -338,7 +338,7 @@ var/list/seen_damage = list() // This looks like: ({Damage type} = list({Damage description for that damage type} = {number of times it has appeared}, ...), ...) var/list/most_seen_damage = list() // This looks like: ({Damage type} = {Frequency of the most common description}, ...) var/list/final_descriptions = list() // This looks like: ({Damage type} = {Most common damage description for that type}, ...) - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) for(var/damage_type in part.damage_examines) var/damage_desc = part.damage_examines[damage_type] if(!seen_damage[damage_type]) @@ -373,7 +373,7 @@ if((held_thing.item_flags & (ABSTRACT|HAND_ITEM)) || HAS_TRAIT(held_thing, TRAIT_EXAMINE_SKIP)) continue . += "[t_He] [t_is] holding [held_thing.examine_title(user)] in [t_his] [get_held_index_name(get_held_index_of_item(held_thing))]." - for(var/obj/item/bodypart/arm/part in bodyparts) + for(var/obj/item/bodypart/arm/part in get_bodyparts()) if(!(part.bodypart_flags & BODYPART_PSEUDOPART)) continue var/obj/item/corresponding_item = get_item_for_held_index(part.held_index) || part @@ -475,7 +475,7 @@ if((held_thing.item_flags & (ABSTRACT|HAND_ITEM)) || HAS_TRAIT(held_thing, TRAIT_EXAMINE_SKIP)) continue . += "[t_He] [t_is] holding [held_thing.examine_title(user)] in [t_his] [get_held_index_name(get_held_index_of_item(held_thing))]." - for(var/obj/item/bodypart/arm/part in bodyparts) + for(var/obj/item/bodypart/arm/part in get_bodyparts()) if(!(part.bodypart_flags & BODYPART_PSEUDOPART)) continue var/obj/item/corresponding_item = get_item_for_held_index(part.held_index) || part @@ -594,7 +594,7 @@ /mob/living/carbon/human/proc/get_mismatched_limb_text() var/list/covered = get_covered_body_zones() var/list/texts = list() - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) var/part_id = part.limb_id var/obj/item/bodypart/expected_part = dna?.species?.bodypart_overrides[part.body_zone] var/expected_id = initial(expected_part?.limb_id) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 406b3eb8e26c..75dbe2f24e9d 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -189,6 +189,8 @@ GLOBAL_LIST_EMPTY(features_by_species) plural_form = "[name]\s" if(!examine_limb_id) examine_limb_id = id + // Carbons determine bodypart order by this list, so we need to make sure it's sorted properly + sortTim(bodypart_overrides, GLOBAL_PROC_REF(cmp_bodypart_by_body_part_asc), associative = TRUE) return ..() @@ -1270,7 +1272,7 @@ GLOBAL_LIST_EMPTY(features_by_species) return // Lets pick a random body part and check for an existing burn - var/obj/item/bodypart/bodypart = pick(humi.bodyparts) + var/obj/item/bodypart/bodypart = pick(humi.get_bodyparts()) var/datum/wound/existing_burn for (var/datum/wound/iterated_wound as anything in bodypart.wounds) var/datum/wound_pregen_data/pregen_data = iterated_wound.get_pregen_data() @@ -1976,7 +1978,7 @@ GLOBAL_LIST_EMPTY(features_by_species) final_bodypart_overrides[BODY_ZONE_R_LEG] = /obj/item/bodypart/leg/right/digitigrade final_bodypart_overrides[BODY_ZONE_L_LEG] = /obj/item/bodypart/leg/left/digitigrade - for(var/obj/item/bodypart/old_part as anything in target.bodyparts) + for(var/obj/item/bodypart/old_part as anything in target.get_bodyparts()) if((old_part.change_exempt_flags & BP_BLOCK_CHANGE_SPECIES) || (old_part.bodypart_flags & BODYPART_IMPLANTED)) continue @@ -2048,7 +2050,7 @@ GLOBAL_LIST_EMPTY(features_by_species) /// Remove body markings /datum/species/proc/remove_body_markings(mob/living/carbon/human/hooman) - for(var/obj/item/bodypart/part as anything in hooman.bodyparts) + for(var/obj/item/bodypart/part as anything in hooman.get_bodyparts()) for(var/datum/bodypart_overlay/simple/body_marking/marking in part.bodypart_overlays) part.remove_bodypart_overlay(marking) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index a49e73fc415d..187e684d06ed 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -192,8 +192,7 @@ var/status = "" if(get_brute_loss()) to_chat(human_user, "Physical trauma analysis:") - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) var/brutedamage = BP.brute_dam if(brutedamage > 0) status = "received minor physical injuries." @@ -208,8 +207,7 @@ to_chat(human_user, "[BP] appears to have [status]") if(get_fire_loss()) to_chat(human_user, "Analysis of skin burns:") - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) var/burndamage = BP.burn_dam if(burndamage > 0) status = "signs of minor burns." diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index fedc4d6e58cf..86b5d1383544 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -13,8 +13,7 @@ //If a specific bodypart is targeted, check how that bodypart is protected and return the value. //If you don't specify a bodypart, it checks ALL your bodyparts for protection, and averages out the values - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) armorval += check_armor(BP, type) organnum++ return (armorval/max(organnum, 1)) @@ -331,8 +330,7 @@ if(EXPLODE_DEVASTATE) max_limb_loss = 4 probability = 50 - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X + for(var/obj/item/bodypart/BP as anything in get_bodyparts()) if(prob(probability) && !prob(getarmor(BP, BOMB)) && BP.body_zone != BODY_ZONE_HEAD && BP.body_zone != BODY_ZONE_CHEST) BP.receive_damage(INFINITY, wound_bonus = CANT_WOUND) //Capped by proc BP.dismember() @@ -557,10 +555,12 @@ combined_msg += span_notice("You check yourself for injuries.") - var/list/missing = get_all_limbs() - for(var/obj/item/bodypart/body_part as anything in bodyparts) - missing -= body_part.body_zone + for(var/part_zone, body_part_untyped in get_bodyparts_by_zones()) + var/obj/item/bodypart/body_part = body_part_untyped + if(isnull(body_part) || IS_STUMP(body_part)) + combined_msg += span_boldannounce("↳ Your [parse_zone(body_part?.body_zone || part_zone)] is missing!") + continue if(body_part.bodypart_flags & BODYPART_PSEUDOPART) //don't show injury text for fake bodyparts; ie chainsaw arms or synthetic armblades continue @@ -568,9 +568,6 @@ if(bodypart_report) combined_msg += "[span_notice("↳")] [bodypart_report]" - for(var/t in missing) - combined_msg += span_boldannounce("↳ Your [parse_zone(t)] is missing!") - var/tox = get_tox_loss() + (disgust / 5) + (HAS_TRAIT(src, TRAIT_SELF_AWARE) ? 0 : (rand(-3, 0) * 5)) switch(tox) if(10 to 20) diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm index 7b0d8e7dd0e1..15d8175ffcf3 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -967,7 +967,7 @@ generate/load female uniform sprites matching all previously decided variables // optimization - none of our limbs or organs have the desired shape return . - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts()) var/checked_bodyshape = limb.bodyshape // accounts for stuff like snouts for(var/obj/item/organ/organ in limb) diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index 439ed75d5444..10e9edfb715f 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -413,6 +413,8 @@ new_bodypart = newBodyPart(BODY_ZONE_L_ARM) new_bodypart.held_index = i + if(i >= 3) // start indexing them as right_arm2 and so on + new_bodypart.body_zone = "[new_bodypart.body_zone]_[ceil(i / 2)]" new_bodypart.try_attach_limb(src, TRUE) hand_bodyparts[i] = new_bodypart ..() //Don't redraw hands until we have organs for them diff --git a/code/modules/mob/living/carbon/human/species_types/ethereal.dm b/code/modules/mob/living/carbon/human/species_types/ethereal.dm index 2d6754d2b956..bfa1ae5649b9 100644 --- a/code/modules/mob/living/carbon/human/species_types/ethereal.dm +++ b/code/modules/mob/living/carbon/human/species_types/ethereal.dm @@ -67,7 +67,7 @@ var/obj/item/organ/heart/ethereal/ethereal_heart = new_ethereal.get_organ_slot(ORGAN_SLOT_HEART) ethereal_heart.ethereal_color = default_color - for(var/obj/item/bodypart/limb as anything in new_ethereal.bodyparts) + for(var/obj/item/bodypart/limb as anything in new_ethereal.get_bodyparts()) if(limb.limb_id == SPECIES_ETHEREAL) limb.update_limb(is_creating = TRUE) diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index c2e5bb95d46c..5fa890e834ca 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -502,7 +502,7 @@ return COMPONENT_NO_EXPOSE_REAGENTS /mob/living/carbon/proc/handle_bodyparts(seconds_per_tick) - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts(include_stumps = TRUE)) . |= limb.on_life(seconds_per_tick) /mob/living/carbon/proc/handle_organs(seconds_per_tick) diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm index 80189ae49024..324215167130 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -25,6 +25,13 @@ if(drop_bitflags & DROP_BODYPARTS) spread_bodyparts(drop_bitflags) + // failsafe for if we fuck up and leave our brain behind. (other organs are replaceable so we can ignore them.) + var/obj/item/organ/brain/brain = get_organ_slot(ORGAN_SLOT_BRAIN) + if((drop_bitflags & DROP_BRAIN) && !isnull(brain)) + stack_trace("gib invoked with drop_brain() had their brain after spilling organs and bodyparts, meaning both failed!") + brain.Remove(src) + brain.forceMove(drop_location()) + SEND_SIGNAL(src, COMSIG_LIVING_GIBBED, drop_bitflags) qdel(src) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 25cc456dda31..d9466e622412 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2536,7 +2536,7 @@ GLOBAL_LIST_EMPTY(fire_appearances) return . = num_legs num_legs = new_value - + hud_used?.update_locked_slots() ///Proc to modify the value of usable_legs and hook behavior associated to this event. /mob/living/proc/set_usable_legs(new_value) @@ -2586,7 +2586,7 @@ GLOBAL_LIST_EMPTY(fire_appearances) return . = num_hands num_hands = new_value - + hud_used?.update_locked_slots() ///Proc to modify the value of usable_hands and hook behavior associated to this event. /mob/living/proc/set_usable_hands(new_value) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 02e382c7117e..40ee61766b8f 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -62,7 +62,7 @@ /mob/living/carbon/get_random_valid_zone(base_zone, base_probability = 80, list/blacklisted_parts, even_weights, bypass_warning) var/list/limbs = list() - for(var/obj/item/bodypart/part as anything in bodyparts) + for(var/obj/item/bodypart/part as anything in get_bodyparts()) var/limb_zone = part.body_zone //cache the zone since we're gonna check it a ton. if(limb_zone in blacklisted_parts) continue diff --git a/code/modules/projectiles/ammunition/ballistic/rifle.dm b/code/modules/projectiles/ammunition/ballistic/rifle.dm index a252832dcd08..3c06aa139504 100644 --- a/code/modules/projectiles/ammunition/ballistic/rifle.dm +++ b/code/modules/projectiles/ammunition/ballistic/rifle.dm @@ -161,7 +161,7 @@ /datum/embedding/rebar_healium/on_successful_embed(mob/living/carbon/victim, obj/item/bodypart/target_limb) . = ..() - for(var/obj/item/bodypart/limb as anything in victim.bodyparts) + for(var/obj/item/bodypart/limb as anything in victim.get_bodyparts()) for(var/obj/item/ammo_casing/rebar/healium/other_rebar in limb.embedded_objects) if (other_rebar == parent) continue diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm index bdbe041d2cf5..79f5cfb4fd20 100644 --- a/code/modules/projectiles/guns/magic/staff.dm +++ b/code/modules/projectiles/guns/magic/staff.dm @@ -362,7 +362,7 @@ if (!iscarbon(user)) return BRUTELOSS var/mob/living/carbon/suicider = user - for (var/obj/item/bodypart/limb in suicider.bodyparts) + for (var/obj/item/bodypart/limb in suicider.get_bodyparts()) limb.dismember(BRUTE, silent = FALSE, wounding_type = WOUND_SLASH) sleep(0.25 SECONDS) diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm index 1b0dad125df9..ba6ff560c9fb 100644 --- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm @@ -1264,7 +1264,7 @@ Basically, we fill the time between now and 2s from now with hands based off the for (var/obj/item/organ/organ as anything in exposed_carbon.organs) organ.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) - for (var/obj/item/bodypart/part as anything in exposed_carbon.bodyparts) + for (var/obj/item/bodypart/part as anything in exposed_carbon.get_bodyparts()) part.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) /datum/reagent/inverse/colorful_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio) @@ -1313,22 +1313,22 @@ Basically, we fill the time between now and 2s from now with hands based off the switch(current_cycle) if(10) - for(var/obj/item/bodypart/leg/leg in affected_mob.bodyparts) + for(var/obj/item/bodypart/leg/leg in affected_mob.get_bodyparts()) affected_mob.cause_wound_of_type_and_severity(WOUND_BLUNT, leg, WOUND_SEVERITY_MODERATE) to_chat(affected_mob, span_warning("Your legs start to cave in to your overwhelming gravity!")) if(20) - for(var/obj/item/bodypart/leg/leg in affected_mob.bodyparts) + for(var/obj/item/bodypart/leg/leg in affected_mob.get_bodyparts()) affected_mob.cause_wound_of_type_and_severity(WOUND_BLUNT, leg, WOUND_SEVERITY_SEVERE) to_chat(affected_mob, span_warning("Your bones fragment horribly as the gravity pounds on you!")) if(30) - for(var/obj/item/bodypart/leg/leg in affected_mob.bodyparts) + for(var/obj/item/bodypart/leg/leg in affected_mob.get_bodyparts()) affected_mob.cause_wound_of_type_and_severity(WOUND_BLUNT, leg, WOUND_SEVERITY_CRITICAL) to_chat(affected_mob, span_warning("The gravity of this situation makes your bones snap like popsicle sticks!")) /datum/reagent/inverse/gravitum/overdose_start(mob/living/carbon/affected_mob, metabolization_ratio) . = ..() affected_mob.AddElement(/datum/element/squish, 120 SECONDS) - for(var/obj/item/bodypart/leg/leg in affected_mob.bodyparts) + for(var/obj/item/bodypart/leg/leg in affected_mob.get_bodyparts()) affected_mob.cause_wound_of_type_and_severity(WOUND_SLASH, leg, WOUND_SEVERITY_SEVERE) diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index 7bed221fd53a..1105c0a611cd 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -2358,7 +2358,7 @@ for (var/obj/item/organ/organ as anything in exposed_carbon.organs) organ.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) - for (var/obj/item/bodypart/part as anything in exposed_carbon.bodyparts) + for (var/obj/item/bodypart/part as anything in exposed_carbon.get_bodyparts()) part.add_atom_colour(color_filter, WASHABLE_COLOUR_PRIORITY) /datum/reagent/colorful_reagent/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio) diff --git a/code/modules/reagents/reagent_containers/patch.dm b/code/modules/reagents/reagent_containers/patch.dm index e30529bcd571..66096a368f4d 100644 --- a/code/modules/reagents/reagent_containers/patch.dm +++ b/code/modules/reagents/reagent_containers/patch.dm @@ -90,7 +90,7 @@ . = ..() if (!.) return - var/obj/item/bodypart/affecting = victim.get_bodypart(hit_zone) || victim.bodyparts[1] + var/obj/item/bodypart/affecting = victim.get_bodypart(hit_zone) || victim.get_bodypart() if (!IS_ORGANIC_LIMB(affecting)) return FALSE return TRUE diff --git a/code/modules/religion/festival/instrument_rites.dm b/code/modules/religion/festival/instrument_rites.dm index e3f53c10fd83..3f03b47747b3 100644 --- a/code/modules/religion/festival/instrument_rites.dm +++ b/code/modules/religion/festival/instrument_rites.dm @@ -176,7 +176,7 @@ listener.adjust_brute_loss(damage_dealt) /datum/religion_rites/song_tuner/pain/finish_effect(mob/living/carbon/human/listener, atom/song_source) - var/obj/item/bodypart/sliced_limb = pick(listener.bodyparts) + var/obj/item/bodypart/sliced_limb = pick(listener.get_bodyparts()) sliced_limb.force_wound_upwards(/datum/wound/slash/flesh/moderate/many_cuts) /datum/religion_rites/song_tuner/lullaby diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm index 05cf51ea433e..d375a1c38824 100644 --- a/code/modules/religion/religion_sects.dm +++ b/code/modules/religion/religion_sects.dm @@ -111,7 +111,7 @@ return BLESSING_FAILED var/mob/living/carbon/human/blessed = target - for(var/obj/item/bodypart/bodypart as anything in blessed.bodyparts) + for(var/obj/item/bodypart/bodypart as anything in blessed.get_bodyparts()) if(IS_ROBOTIC_LIMB(bodypart)) to_chat(chap, span_warning("[GLOB.deity] refuses to heal this metallic taint!")) return BLESSING_IGNORED @@ -280,7 +280,7 @@ return BLESSING_IGNORED var/mob/living/carbon/human/blessed = blessed_living - for(var/obj/item/bodypart/robolimb as anything in blessed.bodyparts) + for(var/obj/item/bodypart/robolimb as anything in blessed.get_bodyparts()) if(IS_ROBOTIC_LIMB(robolimb)) to_chat(chap, span_warning("[GLOB.deity] refuses to heal this metallic taint!")) return BLESSING_IGNORED @@ -346,7 +346,7 @@ var/transferred = FALSE var/list/hurt_limbs = target.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC) + target.get_wounded_bodyparts(BODYTYPE_ORGANIC) var/list/chaplains_limbs = list() - for(var/obj/item/bodypart/possible_limb in chaplain.bodyparts) + for(var/obj/item/bodypart/possible_limb in chaplain.get_bodyparts()) if(IS_ORGANIC_LIMB(possible_limb)) chaplains_limbs += possible_limb diff --git a/code/modules/religion/sparring/sparring_datum.dm b/code/modules/religion/sparring/sparring_datum.dm index fbe0fe93070a..c9ea83be5f0e 100644 --- a/code/modules/religion/sparring/sparring_datum.dm +++ b/code/modules/religion/sparring/sparring_datum.dm @@ -216,7 +216,7 @@ if(PUNISHMENT_BRAND) var/mob/living/carbon/human/branded = interfering to_chat(interfering, span_warning("[GLOB.deity] brands your flesh for interfering with [chaplain]'s sparring match!!")) - var/obj/item/bodypart/branded_limb = pick(branded.bodyparts) + var/obj/item/bodypart/branded_limb = pick(branded.get_bodyparts()) branded_limb.force_wound_upwards(/datum/wound/burn/flesh/severe/brand, wound_source = "divine intervention") branded.emote("scream") diff --git a/code/modules/research/xenobiology/crossbreeding/_misc.dm b/code/modules/research/xenobiology/crossbreeding/_misc.dm index a8f22f5f980c..a75df1542a6f 100644 --- a/code/modules/research/xenobiology/crossbreeding/_misc.dm +++ b/code/modules/research/xenobiology/crossbreeding/_misc.dm @@ -37,18 +37,11 @@ Slimecrossing Items saved_part.old_part.heal_damage(INFINITY, INFINITY, null, FALSE) saved_part.old_part.receive_damage(saved_part.brute_dam, saved_part.burn_dam, wound_bonus=CANT_WOUND) dont_chop[zone] = TRUE - for(var/_part in bodyparts) - var/obj/item/bodypart/part = _part - if(dont_chop[part.body_zone]) - continue - part.drop_limb(TRUE) /mob/living/carbon/proc/save_bodyparts() var/list/datum/saved_bodypart/ret = list() - for(var/_part in bodyparts) - var/obj/item/bodypart/part = _part + for(var/obj/item/bodypart/part as anything in get_bodyparts(include_stumps = TRUE)) var/datum/saved_bodypart/saved_part = new(part) - ret[part.body_zone] = saved_part return ret diff --git a/code/modules/spells/spell_types/touch/scream_for_me.dm b/code/modules/spells/spell_types/touch/scream_for_me.dm index d3c142d70392..2c951f1160db 100644 --- a/code/modules/spells/spell_types/touch/scream_for_me.dm +++ b/code/modules/spells/spell_types/touch/scream_for_me.dm @@ -28,7 +28,7 @@ return var/mob/living/carbon/human/human_victim = victim human_victim.emote("scream") - for(var/obj/item/bodypart/to_wound as anything in human_victim.bodyparts) + for(var/obj/item/bodypart/to_wound as anything in human_victim.get_bodyparts()) human_victim.cause_wound_of_type_and_severity(WOUND_SLASH, to_wound, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_CRITICAL) return TRUE diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index f266827d4e57..462c7067e176 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -231,6 +231,8 @@ var/static/list/butcher_drop_cache = list() /// What state is the bodypart in for determining surgery availability VAR_FINAL/surgery_state = NONE + /// Typepath of this limb as a stump + var/stump_typepath /obj/item/bodypart/apply_fantasy_bonuses(bonus) . = ..() @@ -625,8 +627,7 @@ SHOULD_CALL_PARENT(TRUE) var/atom/drop_loc = drop_location() - if(IS_ORGANIC_LIMB(src)) - playsound(drop_loc, 'sound/misc/splort.ogg', 50, TRUE, -1) + var/play_sfx = FALSE for(var/obj/item/organ/bodypart_organ in contents) if(bodypart_organ.organ_flags & ORGAN_UNREMOVABLE) @@ -643,10 +644,15 @@ if(drop_loc) //can be null if being deleted bodypart_organ.forceMove(get_turf(drop_loc)) + play_sfx = TRUE if(drop_loc) //can be null during deletion for(var/atom/movable/movable as anything in src) movable.forceMove(drop_loc) + play_sfx = TRUE + + if(play_sfx && IS_ORGANIC_LIMB(src)) + playsound(drop_loc, 'sound/misc/splort.ogg', 50, TRUE, -1) update_icon_dropped() @@ -1261,7 +1267,7 @@ update_icon_dropped() ///Generates an /image for the limb to be used as an overlay -/obj/item/bodypart/proc/get_limb_icon(dropped, mob/living/carbon/update_on) +/obj/item/bodypart/proc/get_limb_icon(dropped) SHOULD_CALL_PARENT(TRUE) RETURN_TYPE(/list) @@ -1275,7 +1281,7 @@ // Handles invisibility (not alpha or actual invisibility but invisibility) if(is_invisible) . += image(icon_invisible, "invisible_[body_zone]", -BODYPARTS_LAYER, dir = image_dir) - SEND_SIGNAL(src, COMSIG_BODYPART_GET_LIMB_ICON, ., dropped, update_on) + SEND_SIGNAL(src, COMSIG_BODYPART_GET_LIMB_ICON, ., dropped) return . // Normal non-husk handling @@ -1303,7 +1309,7 @@ if(brutestate) // divided into two overlays: one that gets colored and one that doesn't. var/image/brute_blood_overlay = image('icons/mob/effects/dam_mob.dmi', "[dmg_overlay_type]_[body_zone]_[brutestate]0", -DAMAGE_LAYER, dir = SOUTH) - brute_blood_overlay.color = get_color_from_blood_list(update_on ? update_on.get_blood_dna_list() : blood_dna_info) // living mobs can just get it fresh, dropped limbs use blood_dna_info + brute_blood_overlay.color = get_color_from_blood_list(blood_dna_info) var/mutable_appearance/brute_damage_overlay = mutable_appearance('icons/mob/effects/dam_mob.dmi', "[dmg_overlay_type]_[body_zone]_[brutestate]0_overlay", -DAMAGE_LAYER, appearance_flags = RESET_COLOR) if(brute_damage_overlay) brute_blood_overlay.overlays += brute_damage_overlay @@ -1384,7 +1390,7 @@ for(var/datum/layer in .) overlay.modify_bodypart_appearance(layer) - SEND_SIGNAL(src, COMSIG_BODYPART_GET_LIMB_ICON, ., dropped, update_on) + SEND_SIGNAL(src, COMSIG_BODYPART_GET_LIMB_ICON, ., dropped) return . /obj/item/bodypart/proc/huskify_image(image/thing_to_husk) @@ -1396,11 +1402,7 @@ husk_blood.blend_mode = BLEND_INSET_OVERLAY husk_blood.dir = thing_to_husk.dir husk_blood.layer = thing_to_husk.layer - var/list/blood_dna = blood_dna_info || owner?.get_blood_dna_list() - if (LAZYLEN(blood_dna)) - husk_blood.color = get_color_from_blood_list(blood_dna) - else - husk_blood.color = BLOOD_COLOR_RED + husk_blood.color = LAZYLEN(blood_dna_info) ? get_color_from_blood_list(blood_dna_info) : BLOOD_COLOR_RED return husk_blood ///Add a bodypart overlay and call the appropriate update procs diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm index 2960dee55777..72108859d3e9 100644 --- a/code/modules/surgery/bodyparts/dismemberment.dm +++ b/code/modules/surgery/bodyparts/dismemberment.dm @@ -17,7 +17,7 @@ if(!silent) limb_owner.visible_message(span_danger("[limb_owner]'s [name] is violently dismembered!")) INVOKE_ASYNC(limb_owner, TYPE_PROC_REF(/mob, emote), "scream") - playsound(get_turf(limb_owner), 'sound/effects/dismember.ogg', 80, TRUE) + playsound(limb_owner, 'sound/effects/dismember.ogg', 80, TRUE) limb_owner.add_mood_event("dismembered_[body_zone]", /datum/mood_event/dismembered, src) limb_owner.add_mob_memory(/datum/memory/was_dismembered, lost_limb = src) @@ -123,7 +123,7 @@ if(!special) phantom_owner.hud_used?.update_locked_slots() - if(bodypart_flags & BODYPART_PSEUDOPART) + if(bodypart_flags & (BODYPART_PSEUDOPART|BODYPART_STUMP)) drop_organs(phantom_owner) //Psuedoparts shouldn't have organs, but just in case if(!QDELING(src)) // we might be removed as a part of something qdeling us qdel(src) @@ -281,6 +281,13 @@ if(!can_attach_limb(new_limb_owner, special)) return FALSE + var/obj/item/bodypart/existing_limb = new_limb_owner.get_bodypart(body_zone, include_stumps = TRUE) + if(existing_limb) + if(existing_limb.type != stump_typepath) + stack_trace("Attempted to attach a limb to [new_limb_owner] in zone [body_zone] where they already have a non-stump limb") + existing_limb.drop_limb(special = TRUE) + qdel(existing_limb) + SEND_SIGNAL(new_limb_owner, COMSIG_CARBON_ATTACH_LIMB, src, special, lazy) SEND_SIGNAL(src, COMSIG_BODYPART_ATTACHED, new_limb_owner, special, lazy) new_limb_owner.add_bodypart(src) diff --git a/code/modules/surgery/bodyparts/head.dm b/code/modules/surgery/bodyparts/head.dm index 73a526361f0d..88f4cc912f28 100644 --- a/code/modules/surgery/bodyparts/head.dm +++ b/code/modules/surgery/bodyparts/head.dm @@ -27,6 +27,7 @@ bodypart_trait_source = HEAD_TRAIT butcher_replacement = /obj/item/bodypart/head/skeleton/nonfunctional base_meat_amount = 0 + stump_typepath = /obj/item/bodypart/head/stump /// Do we show the information about missing organs upon being examined? Defaults to TRUE, useful for Dullahan heads. var/show_organs_on_examine = TRUE diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm index c5fc38a8b6ca..afb7c18d1e33 100644 --- a/code/modules/surgery/bodyparts/helpers.dm +++ b/code/modules/surgery/bodyparts/helpers.dm @@ -1,15 +1,55 @@ - -/mob/living/proc/get_bodypart(zone) +/** + * Returns a bodypart of the specified zone that this mob has + * + * * zone: the zone to get. + * Defaults to chest, allowing for skilling zone nullchecks if you don't care what bodypart you get. + * * include_stumps: whether or not to consider stumps as valid bodyparts to return. + * Defaults to FALSE, meaning that if a limb is missing (is a stump), nothing will be returned. + * + * Returns a bodypart, or null. + */ +/mob/living/proc/get_bodypart(zone = BODY_ZONE_CHEST, include_stumps = FALSE) return -/mob/living/carbon/get_bodypart(zone) +/mob/living/carbon/get_bodypart(zone = BODY_ZONE_CHEST, include_stumps = FALSE) RETURN_TYPE(/obj/item/bodypart) - if(!zone) - zone = BODY_ZONE_CHEST for(var/obj/item/bodypart/bodypart as anything in bodyparts) - if(bodypart.body_zone == zone) - return bodypart + if(!include_stumps && IS_STUMP(bodypart)) + continue + if(bodypart.body_zone != zone) + continue + return bodypart + +/** + * Returns all bodyparts this mob has, optionally including stumps. + * + * include_stumps: whether or not to consider stumps as valid bodyparts to return. + * Defaults to FALSE, meaning that if a limb is missing (is a stump), it won't be included in the returned list. + * + * Returns a list of bodyparts, which may be empty. + */ +/mob/living/proc/get_bodyparts(include_stumps = FALSE) + var/list/parts = list() + for(var/zone in get_all_limbs()) + var/obj/item/bodypart/bodypart = get_bodypart(zone, include_stumps) + if(bodypart) + parts += bodypart + + return parts + +/** + * Returns all bodyparts this mob has, indexed by their body zone + * Also includes stumps and nulls, so be sure to check for those if you use this proc. + * (Note: nulls will be very rare - as 95% of missing limbs are represented as stumps - but not impossible due to some edge cases) + * + * Returns a list of bodyparts indexed by their body zone + */ +/mob/living/proc/get_bodyparts_by_zones() as /list + var/list/parts = list() + for(var/zone in get_all_limbs()) + parts[zone] = get_bodypart(zone) + return parts ///Returns TRUE/FALSE on whether the mob should have a limb in a given zone, used for species-restrictions. /mob/living/carbon/proc/should_have_limb(zone) @@ -128,13 +168,14 @@ ///Returns a list of all limbs this mob should have. /mob/living/carbon/get_all_limbs() - if(dna) - return dna.species.bodypart_overrides.Copy() - return ..() + // gets the "normal list", ie chest-head-legs-arms. order matters for human rendering! + . = dna?.species?.bodypart_overrides.Copy() || ..() + // includes any additional adminbussed hands + for(var/obj/item/bodypart/hand in hand_bodyparts) + . |= hand.body_zone ///Returns a list of all missing limbs this mob should have on them, but don't. /mob/living/carbon/proc/get_missing_limbs() as /list - RETURN_TYPE(/list) var/list/full = get_all_limbs() for(var/zone in full) if(get_bodypart(zone)) @@ -177,12 +218,12 @@ ///Remove all embedded objects from all limbs on the carbon mob /mob/living/carbon/proc/remove_all_embedded_objects() - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts(include_stumps = TRUE)) for(var/obj/item/embedded as anything in bodypart.embedded_objects) remove_embedded_object(embedded) /mob/living/carbon/proc/has_embedded_objects(include_harmless = FALSE) - for(var/obj/item/bodypart/bodypart as anything in bodyparts) + for(var/obj/item/bodypart/bodypart as anything in get_bodyparts(include_stumps = TRUE)) for(var/obj/item/embedded as anything in bodypart.embedded_objects) if(!include_harmless && embedded.get_embed().is_harmless(consider_stamina = TRUE)) continue @@ -228,7 +269,7 @@ /// Makes sure that the owner's bodytype flags match the flags of all of its parts and organs /mob/living/carbon/proc/synchronize_bodytypes() var/all_limb_flags = NONE - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts(include_stumps = TRUE)) for(var/obj/item/organ/organ in limb) all_limb_flags |= organ.external_bodytypes all_limb_flags |= limb.bodytype @@ -238,7 +279,7 @@ /// Makes sure that the owner's bodyshape flags match the flags of all of its parts and organs /mob/living/carbon/proc/synchronize_bodyshapes() var/all_limb_flags = NONE - for(var/obj/item/bodypart/limb as anything in bodyparts) + for(var/obj/item/bodypart/limb as anything in get_bodyparts(include_stumps = TRUE)) for(var/obj/item/organ/organ in limb) all_limb_flags |= organ.external_bodyshapes all_limb_flags |= limb.bodyshape diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm index c888271cfc28..7c1129502657 100644 --- a/code/modules/surgery/bodyparts/parts.dm +++ b/code/modules/surgery/bodyparts/parts.dm @@ -149,6 +149,7 @@ /// Parent Type for arms, should not appear in game. /obj/item/bodypart/arm + abstract_type = /obj/item/bodypart/arm name = "arm" desc = "Hey buddy give me a HAND and report this to the github because you shouldn't be seeing this." abstract_type = /obj/item/bodypart/arm @@ -324,6 +325,7 @@ px_y = 0 bodypart_trait_source = LEFT_ARM_TRAIT butcher_replacement = /obj/item/bodypart/arm/left/skeleton/nonfunctional + stump_typepath = /obj/item/bodypart/arm/left/stump /obj/item/bodypart/arm/left/apply_ownership(mob/living/carbon/new_owner) if(HAS_TRAIT(new_owner, TRAIT_PARALYSIS_L_ARM)) @@ -406,6 +408,7 @@ px_y = 0 bodypart_trait_source = RIGHT_ARM_TRAIT butcher_replacement = /obj/item/bodypart/arm/right/skeleton/nonfunctional + stump_typepath = /obj/item/bodypart/arm/right/stump /obj/item/bodypart/arm/right/apply_ownership(mob/living/carbon/new_owner) if(HAS_TRAIT(new_owner, TRAIT_PARALYSIS_R_ARM)) @@ -475,6 +478,7 @@ /// Parent Type for legs, should not appear in game. /obj/item/bodypart/leg + abstract_type = /obj/item/bodypart/leg name = "leg" desc = "This item shouldn't exist. Talk about breaking a leg. Badum-Tss!" abstract_type = /obj/item/bodypart/leg @@ -565,6 +569,7 @@ can_be_disabled = TRUE bodypart_trait_source = LEFT_LEG_TRAIT butcher_replacement = /obj/item/bodypart/leg/left/skeleton/nonfunctional + stump_typepath = /obj/item/bodypart/leg/left/stump /obj/item/bodypart/leg/left/apply_ownership(mob/living/carbon/new_owner) if(HAS_TRAIT(new_owner, TRAIT_PARALYSIS_L_LEG)) @@ -644,6 +649,7 @@ px_y = 12 bodypart_trait_source = RIGHT_LEG_TRAIT butcher_replacement = /obj/item/bodypart/leg/right/skeleton/nonfunctional + stump_typepath = /obj/item/bodypart/leg/right/stump /obj/item/bodypart/leg/right/apply_ownership(mob/living/carbon/new_owner) if(HAS_TRAIT(new_owner, TRAIT_PARALYSIS_R_LEG)) diff --git a/code/modules/surgery/bodyparts/robot_bodyparts.dm b/code/modules/surgery/bodyparts/robot_bodyparts.dm index 4d17428fc9b7..fe5d9c6e14d7 100644 --- a/code/modules/surgery/bodyparts/robot_bodyparts.dm +++ b/code/modules/surgery/bodyparts/robot_bodyparts.dm @@ -277,7 +277,7 @@ SIGNAL_HANDLER var/all_robotic = TRUE - for(var/obj/item/bodypart/part in owner.bodyparts) + for(var/obj/item/bodypart/part as anything in owner.get_bodyparts()) all_robotic = all_robotic && IS_ROBOTIC_LIMB(part) if(all_robotic) diff --git a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm index acb4b65a8217..41037f538c43 100644 --- a/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm +++ b/code/modules/surgery/bodyparts/species_parts/misc_bodyparts.dm @@ -547,6 +547,7 @@ throwforce = 25 // It's also a potent weapon show_organs_on_examine = FALSE speech_span = null + stump_typepath = null /obj/item/bodypart/head/dullahan/Entered(obj/item/organ/arrived, atom/old_loc, list/atom/old_locs) . = ..() diff --git a/code/modules/surgery/bodyparts/stumps.dm b/code/modules/surgery/bodyparts/stumps.dm new file mode 100644 index 000000000000..49e4cdf29556 --- /dev/null +++ b/code/modules/surgery/bodyparts/stumps.dm @@ -0,0 +1,72 @@ +// Invisible bodyparts that serve as placeholders for a missing limb +// Used so we can add behavior to "missing limbs" without putting it on the mob itself + +/obj/item/bodypart/head/stump + limb_id = null + plaintext_zone = "neck stump" + stump_typepath = null + scarrable = FALSE + biological_state = NONE + bodypart_flags = BODYPART_UNREMOVABLE | BODYPART_STUMP | BODYPART_VIRGIN + + head_flags = NONE + teeth_count = 0 // lol? + +/obj/item/bodypart/head/stump/Initialize(mapload) + . = ..() + name = "stump" + // these traits are largely redundant, as many places that check for disabled limbs filter stumps. + // however, marking them as unusable for the sake of completeness might make it easier to work with. + ADD_TRAIT(src, TRAIT_PARALYSIS, INNATE_TRAIT) + +/obj/item/bodypart/leg/left/stump + limb_id = null + plaintext_zone = "left leg stump" + stump_typepath = null + scarrable = FALSE + biological_state = NONE + bodypart_flags = BODYPART_UNREMOVABLE | BODYPART_STUMP | BODYPART_VIRGIN + +/obj/item/bodypart/leg/left/stump/Initialize(mapload) + . = ..() + name = "stump" + ADD_TRAIT(src, TRAIT_PARALYSIS, INNATE_TRAIT) + +/obj/item/bodypart/leg/right/stump + limb_id = null + plaintext_zone = "right leg stump" + stump_typepath = null + scarrable = FALSE + biological_state = NONE + bodypart_flags = BODYPART_UNREMOVABLE | BODYPART_STUMP | BODYPART_VIRGIN + +/obj/item/bodypart/leg/right/stump/Initialize(mapload) + . = ..() + name = "stump" + ADD_TRAIT(src, TRAIT_PARALYSIS, INNATE_TRAIT) + +/obj/item/bodypart/arm/left/stump + limb_id = null + plaintext_zone = "left arm stump" + stump_typepath = null + scarrable = FALSE + biological_state = NONE + bodypart_flags = BODYPART_UNREMOVABLE | BODYPART_STUMP | BODYPART_VIRGIN + +/obj/item/bodypart/arm/left/stump/Initialize(mapload) + . = ..() + name = "stump" + ADD_TRAIT(src, TRAIT_PARALYSIS, INNATE_TRAIT) + +/obj/item/bodypart/arm/right/stump + limb_id = null + plaintext_zone = "right arm stump" + stump_typepath = null + scarrable = FALSE + biological_state = NONE + bodypart_flags = BODYPART_UNREMOVABLE | BODYPART_STUMP | BODYPART_VIRGIN + +/obj/item/bodypart/arm/right/stump/Initialize(mapload) + . = ..() + name = "stump" + ADD_TRAIT(src, TRAIT_PARALYSIS, INNATE_TRAIT) diff --git a/code/modules/surgery/operations/_operation.dm b/code/modules/surgery/operations/_operation.dm index 95ad653cd3dd..8d49b07c9f7c 100644 --- a/code/modules/surgery/operations/_operation.dm +++ b/code/modules/surgery/operations/_operation.dm @@ -1297,6 +1297,9 @@ GLOBAL_DATUM_INIT(operations, /datum/operation_holder, new) abstract_type = /datum/surgery_operation/limb /// Body type required to perform this operation var/required_bodytype = NONE + /// If TRUE, this operation can be performed on stumps. + /// If FALSE, the target limb must be a full limb. + var/allow_stumps = FALSE /datum/surgery_operation/limb/all_blocked_strings() . = ..() @@ -1308,7 +1311,7 @@ GLOBAL_DATUM_INIT(operations, /datum/operation_holder, new) /datum/surgery_operation/limb/get_operation_target(atom/movable/operating_on, body_zone) if (isliving(operating_on)) var/mob/living/patient = operating_on - return patient.get_bodypart(deprecise_zone(body_zone)) + return patient.get_bodypart(deprecise_zone(body_zone), allow_stumps) if (!isbodypart(operating_on)) return null return operating_on diff --git a/code/modules/surgery/operations/operation_add_limb.dm b/code/modules/surgery/operations/operation_add_limb.dm index 3f12368ff121..ab837aafbcba 100644 --- a/code/modules/surgery/operations/operation_add_limb.dm +++ b/code/modules/surgery/operations/operation_add_limb.dm @@ -1,7 +1,7 @@ #define OPERATION_REJECTION_DAMAGE "tox_damage" // This surgery is so snowflake that it doesn't use any of the operation subtypes, it forges its own path -/datum/surgery_operation/prosthetic_replacement +/datum/surgery_operation/limb/prosthetic_replacement name = "prosthetic replacement" desc = "Replace a missing limb with a prosthetic (or arbitrary) item." implements = list( @@ -12,6 +12,7 @@ operation_flags = OPERATION_STANDING_ALLOWED | OPERATION_PRIORITY_NEXT_STEP | OPERATION_NOTABLE | OPERATION_IGNORE_CLOTHES all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_VESSELS_UNCLAMPED + allow_stumps = TRUE /// List of items that are always allowed to be an arm replacement, even if they fail another requirement. var/list/always_accepted_prosthetics = list( /obj/item/chainsaw, // the OG, too large otherwise @@ -21,25 +22,20 @@ /// Radial slice datums for every augment type VAR_PRIVATE/list/cached_prosthetic_options -/datum/surgery_operation/prosthetic_replacement/get_default_radial_image() - return image(/obj/item/bodypart/chest) - -/datum/surgery_operation/prosthetic_replacement/get_recommended_tool() +/datum/surgery_operation/limb/prosthetic_replacement/get_recommended_tool() return "any limb / any item" -/datum/surgery_operation/prosthetic_replacement/get_any_tool() +/datum/surgery_operation/limb/prosthetic_replacement/get_any_tool() return "Any suitable arm replacement" -/datum/surgery_operation/prosthetic_replacement/all_required_strings() - . = list() - . += "operate on chest (target chest)" - . += ..() - . += "when the chest is prepared, target the zone of the limb you are attaching" +/datum/surgery_operation/limb/prosthetic_replacement/all_required_strings() + . = ..() + . += "the limb must be missing / a stump" -/datum/surgery_operation/prosthetic_replacement/any_required_strings() +/datum/surgery_operation/limb/prosthetic_replacement/any_required_strings() return list("arms may receive any suitable item in lieu of a replacement limb") + ..() -/datum/surgery_operation/prosthetic_replacement/get_radial_options(obj/item/bodypart/chest/chest, obj/item/tool, operating_zone) +/datum/surgery_operation/limb/prosthetic_replacement/get_radial_options(obj/item/bodypart/chest/chest, obj/item/tool, operating_zone) var/datum/radial_menu_choice/option = LAZYACCESS(cached_prosthetic_options, tool.type) if(!option) option = new() @@ -50,32 +46,10 @@ return option -/datum/surgery_operation/prosthetic_replacement/get_operation_target(atom/movable/operating_on, body_zone) - if (!isliving(operating_on)) - return null - var/mob/living/patient = operating_on - // We always operate on the chest even if we're targeting left leg or w/e - return patient.get_bodypart(BODY_ZONE_CHEST) - -/datum/surgery_operation/prosthetic_replacement/has_surgery_state(obj/item/bodypart/chest/chest, state) - return LIMB_HAS_SURGERY_STATE(chest, state) - -/datum/surgery_operation/prosthetic_replacement/has_any_surgery_state(obj/item/bodypart/chest/chest, state) - return LIMB_HAS_ANY_SURGERY_STATE(chest, state) - -/datum/surgery_operation/prosthetic_replacement/get_patient(obj/item/bodypart/chest/chest) - return chest.owner +/datum/surgery_operation/limb/prosthetic_replacement/state_check(obj/item/bodypart/limb) + return IS_STUMP(limb) -/datum/surgery_operation/prosthetic_replacement/is_available(obj/item/bodypart/chest/chest, operated_zone) - var/real_operated_zone = deprecise_zone(operated_zone) - // Operate on the chest but target another zone - if(!HAS_TRAIT(chest, TRAIT_READY_TO_OPERATE) || real_operated_zone == BODY_ZONE_CHEST) - return FALSE - if(chest.owner.get_bodypart(real_operated_zone)) - return FALSE - return ..() - -/datum/surgery_operation/prosthetic_replacement/snowflake_check_availability(obj/item/bodypart/chest, mob/living/surgeon, obj/item/tool, operated_zone) +/datum/surgery_operation/limb/prosthetic_replacement/snowflake_check_availability(obj/item/bodypart/chest, mob/living/surgeon, obj/item/tool, operated_zone) if(!surgeon.canUnEquip(tool)) return FALSE var/real_operated_zone = deprecise_zone(operated_zone) @@ -91,7 +65,7 @@ return FALSE return TRUE -/datum/surgery_operation/prosthetic_replacement/tool_check(obj/item/tool) +/datum/surgery_operation/limb/prosthetic_replacement/tool_check(obj/item/tool) if(tool.item_flags & (ABSTRACT|DROPDEL|HAND_ITEM)) return FALSE if(isbodypart(tool)) @@ -104,41 +78,41 @@ return FALSE return TRUE -/datum/surgery_operation/prosthetic_replacement/pre_preop(atom/movable/operating_on, mob/living/surgeon, tool, list/operation_args) +/datum/surgery_operation/limb/prosthetic_replacement/pre_preop(atom/movable/operating_on, mob/living/surgeon, tool, list/operation_args) . = ..() // always operate on absolute body zones operation_args[OPERATION_TARGET_ZONE] = deprecise_zone(operation_args[OPERATION_TARGET_ZONE]) -/datum/surgery_operation/prosthetic_replacement/on_preop(obj/item/bodypart/chest/chest, mob/living/surgeon, obj/item/tool, list/operation_args) - var/target_zone_readable = parse_zone(operation_args[OPERATION_TARGET_ZONE]) +/datum/surgery_operation/limb/prosthetic_replacement/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( surgeon, - chest.owner, - span_notice("You begin to replace [chest.owner]'s missing [target_zone_readable] with [tool]..."), - span_notice("[surgeon] begins to replace [chest.owner]'s missing [target_zone_readable] with [tool]."), - span_notice("[surgeon] begins to replace [chest.owner]'s missing [target_zone_readable]."), + limb.owner, + // "You begin to attach the right arm to john doe's right arm stump" + span_notice("You begin to attach [tool]'s to [FORMAT_LIMB_OWNER(limb)]..."), + span_notice("[surgeon] begins to attach [tool]'s to [FORMAT_LIMB_OWNER(limb)]."), + span_notice("[surgeon] begins to attach [tool]'s to [FORMAT_LIMB_OWNER(limb)]."), ) - display_pain(chest.owner, "You feel an uncomfortable sensation where your [target_zone_readable] should be!") + display_pain(limb.owner, "You feel an uncomfortable sensation where your [parse_zone(limb.body_zone)] should be!") operation_args[OPERATION_REJECTION_DAMAGE] = 10 if(isbodypart(tool)) var/obj/item/bodypart/new_limb = tool if(IS_ROBOTIC_LIMB(new_limb)) operation_args[OPERATION_REJECTION_DAMAGE] = 0 - else if(new_limb.check_for_frankenstein(chest.owner)) + else if(new_limb.check_for_frankenstein(limb.owner)) operation_args[OPERATION_REJECTION_DAMAGE] = 30 -/datum/surgery_operation/prosthetic_replacement/on_success(obj/item/bodypart/chest/chest, mob/living/surgeon, obj/item/tool, list/operation_args) +/datum/surgery_operation/limb/prosthetic_replacement/on_success(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) if(!surgeon.temporarilyRemoveItemFromInventory(tool)) return // should never happen if(operation_args[OPERATION_REJECTION_DAMAGE] > 0) - chest.owner.apply_damage(operation_args[OPERATION_REJECTION_DAMAGE], TOX) + limb.owner.apply_damage(operation_args[OPERATION_REJECTION_DAMAGE], TOX) if(isbodypart(tool)) - handle_bodypart(chest.owner, surgeon, tool) + handle_bodypart(limb.owner, surgeon, tool) return - handle_arbitrary_prosthetic(chest.owner, surgeon, tool, operation_args[OPERATION_TARGET_ZONE]) + handle_arbitrary_prosthetic(limb.owner, surgeon, tool, operation_args[OPERATION_TARGET_ZONE]) -/datum/surgery_operation/prosthetic_replacement/proc/handle_bodypart(mob/living/carbon/patient, mob/living/surgeon, obj/item/bodypart/bodypart_to_attach) +/datum/surgery_operation/limb/prosthetic_replacement/proc/handle_bodypart(mob/living/carbon/patient, mob/living/surgeon, obj/item/bodypart/bodypart_to_attach) bodypart_to_attach.try_attach_limb(patient) if(bodypart_to_attach.check_for_frankenstein(patient)) bodypart_to_attach.bodypart_flags |= BODYPART_IMPLANTED @@ -150,7 +124,7 @@ ) display_pain(patient, "You feel synthetic sensation wash from your [bodypart_to_attach.plaintext_zone], which you can feel again!", TRUE) -/datum/surgery_operation/prosthetic_replacement/proc/handle_arbitrary_prosthetic(mob/living/carbon/patient, mob/living/surgeon, obj/item/thing_to_attach, target_zone) +/datum/surgery_operation/limb/prosthetic_replacement/proc/handle_arbitrary_prosthetic(mob/living/carbon/patient, mob/living/surgeon, obj/item/thing_to_attach, target_zone) SSblackbox.record_feedback("tally", "arbitrary_prosthetic", 1, initial(thing_to_attach.name)) var/obj/item/bodypart/new_limb = patient.make_item_prosthetic(thing_to_attach, target_zone, 80) new_limb.add_surgical_state(SURGERY_PROSTHETIC_UNSECURED) @@ -187,8 +161,7 @@ span_notice("[surgeon] begins to [tool.singular_name] [limb] to [limb.owner]'s body."), span_notice("[surgeon] begins to [tool.singular_name] something to [limb.owner]'s body."), ) - var/obj/item/bodypart/chest = limb.owner.get_bodypart(BODY_ZONE_CHEST) - display_pain(limb.owner, "[surgeon] begins to [tool.singular_name] [limb] to your body!", IS_ROBOTIC_LIMB(chest)) + display_pain(limb.owner, "[surgeon] begins to [tool.singular_name] [limb] to your body!", IS_ROBOTIC_LIMB(limb)) /datum/surgery_operation/limb/secure_arbitrary_prosthetic/on_success(obj/item/bodypart/limb, mob/living/surgeon, obj/item/stack/medical/tool, list/operation_args) display_results( @@ -198,8 +171,7 @@ span_notice("[surgeon] finishes [tool.apply_verb] [limb] to [limb.owner]'s body."), span_notice("[surgeon] finishes the [tool.apply_verb] procedure!"), ) - var/obj/item/bodypart/chest = limb.owner.get_bodypart(BODY_ZONE_CHEST) - display_pain(limb.owner, "You feel more secure as your prosthetic is firmly attached to your body!", IS_ROBOTIC_LIMB(chest)) + display_pain(limb.owner, "You feel more secure as your prosthetic is firmly attached to your body!", IS_ROBOTIC_LIMB(limb)) limb.remove_surgical_state(SURGERY_PROSTHETIC_UNSECURED) limb.AddComponent(/datum/component/item_as_prosthetic_limb, null, 0) // updates drop probability to zero tool.use(1) diff --git a/code/modules/surgery/operations/operation_generic.dm b/code/modules/surgery/operations/operation_generic.dm index 8288971a4a5a..fac2fcdc0516 100644 --- a/code/modules/surgery/operations/operation_generic.dm +++ b/code/modules/surgery/operations/operation_generic.dm @@ -21,6 +21,7 @@ success_sound = 'sound/items/handling/surgery/scalpel2.ogg' operation_flags = OPERATION_AFFECTS_MOOD | OPERATION_NO_PATIENT_REQUIRED any_surgery_states_blocked = ALL_SURGERY_SKIN_STATES + allow_stumps = TRUE /// We can't cut mobs with this biostate var/biostate_blacklist = BIO_CHITIN @@ -115,6 +116,7 @@ preop_sound = 'sound/items/handling/surgery/retractor1.ogg' success_sound = 'sound/items/handling/surgery/retractor2.ogg' all_surgery_states_required = SURGERY_SKIN_CUT + allow_stumps = TRUE /datum/surgery_operation/limb/retract_skin/get_default_radial_image() return image(/obj/item/retractor) @@ -165,6 +167,7 @@ /obj/item = 'sound/items/handling/surgery/cautery2.ogg', ) any_surgery_states_required = ALL_SURGERY_SKIN_STATES + allow_stumps = TRUE /datum/surgery_operation/limb/close_skin/get_any_tool() return "Any heat source" @@ -225,6 +228,7 @@ time = 2.4 SECONDS preop_sound = 'sound/items/handling/surgery/hemostat1.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN|SURGERY_VESSELS_UNCLAMPED + allow_stumps = TRUE /datum/surgery_operation/limb/clamp_bleeders/get_default_radial_image() return image(/obj/item/hemostat) @@ -268,6 +272,7 @@ time = 2.4 SECONDS preop_sound = 'sound/items/handling/surgery/hemostat1.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN|SURGERY_VESSELS_CLAMPED + allow_stumps = TRUE /datum/surgery_operation/limb/unclamp_bleeders/get_default_radial_image() return image(/obj/item/hemostat) @@ -325,6 +330,7 @@ operation_flags = OPERATION_AFFECTS_MOOD | OPERATION_NO_PATIENT_REQUIRED all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_BONE_SAWED|SURGERY_BONE_DRILLED + allow_stumps = TRUE /datum/surgery_operation/limb/saw_bones/get_any_tool() return "Any sharp edged item with decent force" @@ -381,6 +387,7 @@ time = 4 SECONDS all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_required = SURGERY_BONE_SAWED|SURGERY_BONE_DRILLED + allow_stumps = TRUE /datum/surgery_operation/limb/fix_bones/get_default_radial_image() return image(/obj/item/stack/medical/bone_gel) @@ -425,6 +432,7 @@ success_sound = 'sound/items/handling/surgery/organ2.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_BONE_SAWED|SURGERY_BONE_DRILLED + allow_stumps = TRUE /datum/surgery_operation/limb/drill_bones/get_any_tool() return "Any sharp pointed item with decent force" @@ -477,6 +485,7 @@ success_sound = 'sound/items/handling/surgery/organ1.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_ORGANS_CUT + allow_stumps = TRUE /datum/surgery_operation/limb/incise_organs/get_any_tool() return "Any sharp edged item" diff --git a/code/modules/surgery/operations/operation_generic_mechanic.dm b/code/modules/surgery/operations/operation_generic_mechanic.dm index bbcdfd55fd29..54a97c7348c8 100644 --- a/code/modules/surgery/operations/operation_generic_mechanic.dm +++ b/code/modules/surgery/operations/operation_generic_mechanic.dm @@ -16,6 +16,7 @@ preop_sound = 'sound/items/tools/screwdriver.ogg' success_sound = 'sound/items/tools/screwdriver2.ogg' any_surgery_states_blocked = ALL_SURGERY_SKIN_STATES + allow_stumps = TRUE /datum/surgery_operation/limb/mechanical_incision/get_any_tool() return "Any sharp item" @@ -56,6 +57,7 @@ preop_sound = 'sound/items/tools/ratchet.ogg' success_sound = 'sound/machines/airlock/doorclick.ogg' all_surgery_states_required = SURGERY_SKIN_CUT + allow_stumps = TRUE /datum/surgery_operation/limb/mechanical_open/get_default_radial_image() return image('icons/hud/screen_gen.dmi', "arrow_large_still") @@ -93,6 +95,7 @@ preop_sound = 'sound/items/tools/screwdriver.ogg' success_sound = 'sound/items/tools/screwdriver2.ogg' any_surgery_states_required = ALL_SURGERY_SKIN_STATES + allow_stumps = TRUE /datum/surgery_operation/limb/mechanical_close/get_any_tool() return "Any sharp item" @@ -137,6 +140,7 @@ success_sound = 'sound/items/taperecorder/taperecorder_close.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_ORGANS_CUT + allow_stumps = TRUE /datum/surgery_operation/limb/prepare_electronics/get_default_radial_image() return image(/obj/item/multitool) @@ -170,6 +174,7 @@ preop_sound = 'sound/items/tools/ratchet.ogg' all_surgery_states_required = SURGERY_SKIN_OPEN any_surgery_states_blocked = SURGERY_BONE_SAWED|SURGERY_BONE_DRILLED + allow_stumps = TRUE /datum/surgery_operation/limb/mechanic_unwrench/get_default_radial_image() return image(/obj/item/wrench) @@ -198,10 +203,11 @@ TOOL_WRENCH = 1, TOOL_RETRACTOR = 1.33, ) - operation_flags = OPERATION_SELF_OPERABLE | OPERATION_MECHANIC + operation_flags = OPERATION_SELF_OPERABLE | OPERATION_MECHANIC | OPERATION_NO_PATIENT_REQUIRED time = 2.4 SECONDS preop_sound = 'sound/items/tools/ratchet.ogg' - all_surgery_states_required = SURGERY_SKIN_OPEN|SURGERY_BONE_SAWED | OPERATION_NO_PATIENT_REQUIRED + all_surgery_states_required = SURGERY_SKIN_OPEN|SURGERY_BONE_SAWED + allow_stumps = TRUE /datum/surgery_operation/limb/mechanic_wrench/state_check(obj/item/bodypart/limb) return LIMB_HAS_BONES(limb) diff --git a/code/modules/surgery/surgery_tools.dm b/code/modules/surgery/surgery_tools.dm index ed1218394a7c..69f97210b30d 100644 --- a/code/modules/surgery/surgery_tools.dm +++ b/code/modules/surgery/surgery_tools.dm @@ -591,7 +591,7 @@ /obj/item/shears/suicide_act(mob/living/carbon/user) user.visible_message(span_suicide("[user] is pinching [user.p_them()]self with \the [src]! It looks like [user.p_theyre()] trying to commit suicide!")) var/timer = 1 SECONDS - for(var/obj/item/bodypart/thing in user.bodyparts) + for(var/obj/item/bodypart/thing in user.get_bodyparts()) if(thing.body_part == CHEST) continue addtimer(CALLBACK(thing, TYPE_PROC_REF(/obj/item/bodypart/, dismember)), timer) diff --git a/code/modules/unit_tests/limbsanity.dm b/code/modules/unit_tests/limbsanity.dm index 6efec37ab76d..13f6a4d7d351 100644 --- a/code/modules/unit_tests/limbsanity.dm +++ b/code/modules/unit_tests/limbsanity.dm @@ -1,8 +1,11 @@ /datum/unit_test/limbsanity /datum/unit_test/limbsanity/Run() - for(var/path in subtypesof(/obj/item/bodypart) - list(/obj/item/bodypart/arm, /obj/item/bodypart/leg)) /// removes the abstract items. + for(var/path in valid_subtypesof(/obj/item/bodypart)) /// removes the abstract items. var/obj/item/bodypart/part = path + if(part::bodypart_flags & BODYPART_STUMP) + continue // stumps don't need to have icons + if(part::is_dimorphic) if(!icon_exists(UNLINT(part::should_draw_greyscale ? part::icon_greyscale : part::icon_static), "[part::limb_id]_[part::body_zone]_m")) TEST_FAIL("[path] does not have a valid icon for male variants") diff --git a/code/modules/unit_tests/organ_bodypart_shuffle.dm b/code/modules/unit_tests/organ_bodypart_shuffle.dm index e05c6bd0bd94..ffb2210756a1 100644 --- a/code/modules/unit_tests/organ_bodypart_shuffle.dm +++ b/code/modules/unit_tests/organ_bodypart_shuffle.dm @@ -20,7 +20,7 @@ TEST_ASSERT_NULL(organ.bodypart_owner, "Organ '[organ.name] kept reference to bodypart after forceMove into nullspace.") // 3. replace all bodyparts with new ones and place the previously removed organs into the new bodyparts - for(var/obj/item/bodypart/bodypart as anything in hollow_boy.bodyparts) + for(var/obj/item/bodypart/bodypart as anything in hollow_boy.get_bodyparts()) var/obj/item/bodypart/replacement = allocate(bodypart.type) for(var/obj/item/organ/organ as anything in removed_organs) if(replacement.body_zone != deprecise_zone(organ.zone)) @@ -35,3 +35,22 @@ TEST_ASSERT(organ in hollow_boy.organs, "Organ '[organ.name] was put in an empty bodypart that replaced a humans, but the organ did not come with.") TEST_ASSERT(organ.owner == hollow_boy, "Organ '[organ.name]'s owner was not properly updated to the new human after being placed in a replacement bodypart.") TEST_ASSERT(organ.bodypart_owner in hollow_boy.bodyparts, "Organ '[organ.name]'s bodypart_owner was not properly updated to the new bodypart after being placed in a replacement bodypart.") + +/// Tests that gibbing results in a head with a brain +/datum/unit_test/gibbing_organ_transfer + +/datum/unit_test/gibbing_organ_transfer/Run() + var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human/consistent) + var/list/bodyparts_for_cleanup = dummy.bodyparts.Copy() + var/list/organs_for_cleanup = dummy.organs.Copy() + var/obj/item/organ/brain/original_brain = dummy.get_organ_slot(ORGAN_SLOT_BRAIN) + var/obj/item/bodypart/head/original_head = dummy.get_bodypart(BODY_ZONE_HEAD) + dummy.gib(ALL) + + TEST_ASSERT(QDELETED(dummy), "Dummy was not deleted after gibbing.") + TEST_ASSERT(!QDELETED(original_head), "Original head was deleted after gibbing.") + TEST_ASSERT(!QDELETED(original_brain), "Original brain was deleted after gibbing.") + TEST_ASSERT(original_brain.loc == original_head, "Original brain was not transferred to the head after gibbing.") + + QDEL_LIST(organs_for_cleanup) + QDEL_LIST(bodyparts_for_cleanup) diff --git a/code/modules/unit_tests/spawn_humans.dm b/code/modules/unit_tests/spawn_humans.dm index f89f59a4cbfe..a0691b16c875 100644 --- a/code/modules/unit_tests/spawn_humans.dm +++ b/code/modules/unit_tests/spawn_humans.dm @@ -14,3 +14,23 @@ TEST_ASSERT(!HAS_TRAIT_FROM(dummy, TRAIT_AGEUSIA, NO_TONGUE_TRAIT), "Dummy has ageusia on init, when it should've been removed by its default tongue.") TEST_ASSERT(!dummy.is_blind_from(NO_EYES), "Dummy is blind on init, when it should've been removed by its default eyes.") TEST_ASSERT(!HAS_TRAIT_FROM(dummy, TRAIT_DEAF, NO_EARS), "Dummy is deaf on init, when it should've been removed by its default ears.") + +/// Tests that we can change a mob's hand count without everything breaking +/datum/unit_test/many_armed_humans + +/datum/unit_test/many_armed_humans/Run() + var/mob/living/carbon/human/consistent/dummy = allocate(/mob/living/carbon/human/consistent) + dummy.change_number_of_hands(4) + +/// Tests spawned humans have the correct bodypart order +/datum/unit_test/human_bodypart_order + +/datum/unit_test/human_bodypart_order/Run() + var/mob/living/carbon/human/consistent/dummy = allocate(/mob/living/carbon/human/consistent) + var/list/obj/item/bodypart/bodyparts = dummy.get_bodyparts() + TEST_ASSERT(bodyparts[1].body_zone == BODY_ZONE_CHEST, "First bodypart in bodyparts list is not the chest, this is important for human rendering") + TEST_ASSERT(bodyparts[2].body_zone == BODY_ZONE_HEAD, "Second bodypart in bodyparts list is not the head, this is important for human rendering") + + var/list/obj/item/bodypart/bodyparts_by_zone = dummy.get_bodyparts_by_zones() + TEST_ASSERT(bodyparts_by_zone[1] == BODY_ZONE_CHEST, "First bodypart in bodyparts_by_zone list is not the chest, this is important for human rendering") + TEST_ASSERT(bodyparts_by_zone[2] == BODY_ZONE_HEAD, "Second bodypart in bodyparts_by_zone list is not the head, this is important for human rendering") diff --git a/code/modules/unit_tests/surgeries.dm b/code/modules/unit_tests/surgeries.dm index 69bc29fb1482..7a538da8fbbf 100644 --- a/code/modules/unit_tests/surgeries.dm +++ b/code/modules/unit_tests/surgeries.dm @@ -57,9 +57,9 @@ TEST_ASSERT_EQUAL(bobs_head.real_name, "Bob", "Bob's head does not remember that it is from Bob") // Put Bob's head onto Alice's body - var/datum/surgery_operation/prosthetic_replacement/surgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] + var/datum/surgery_operation/limb/prosthetic_replacement/surgery = GLOB.operations.operations_by_typepath[__IMPLIED_TYPE__] user.put_in_active_hand(bobs_head) - UNLINT(surgery.success(alice.get_bodypart(BODY_ZONE_CHEST), user, bobs_head, list())) + UNLINT(surgery.success(alice.get_bodypart(BODY_ZONE_HEAD, TRUE), user, bobs_head, list())) TEST_ASSERT(!isnull(alice.get_bodypart(BODY_ZONE_HEAD)), "Alice has no head after prosthetic replacement") TEST_ASSERT_EQUAL(alice.get_visible_name(), "Bob", "Bob's head was transplanted onto Alice's body, but their name is not Bob") diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm index 0672586220c9..6cff8a743f6c 100644 --- a/code/modules/vehicles/cars/clowncar.dm +++ b/code/modules/vehicles/cars/clowncar.dm @@ -143,14 +143,16 @@ for(var/mob/living/carbon/carbon_occupant in occupants) if(prob(35)) //Note: The randomstep on dump_mobs throws occupants into each other and often causes wounds regardless. continue - for(var/obj/item/bodypart/head/head_to_wound as anything in carbon_occupant.bodyparts) - var/pick_mode = text2num(pick(list( - "[WOUND_PICK_LOWEST_SEVERITY]", - "[WOUND_PICK_HIGHEST_SEVERITY]" - ))) - carbon_occupant.cause_wound_of_type_and_severity(WOUND_BLUNT, head_to_wound, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, pick_mode) - carbon_occupant.playsound_local(src, 'sound/items/weapons/flash_ring.ogg', 50) - carbon_occupant.set_eye_blur_if_lower(rand(10 SECONDS, 20 SECONDS)) + var/obj/item/bodypart/head/head_to_wound = carbon_occupant.get_bodypart(BODY_ZONE_HEAD) + if(isnull(head_to_wound)) + return + var/pick_mode = text2num(pick(list( + "[WOUND_PICK_LOWEST_SEVERITY]", + "[WOUND_PICK_HIGHEST_SEVERITY]" + ))) + carbon_occupant.cause_wound_of_type_and_severity(WOUND_BLUNT, head_to_wound, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, pick_mode) + carbon_occupant.playsound_local(src, 'sound/items/weapons/flash_ring.ogg', 50) + carbon_occupant.set_eye_blur_if_lower(rand(10 SECONDS, 20 SECONDS)) hittarget_living.add_splatter_floor(small_drip = FALSE) hittarget_living.adjust_brute_loss(200) diff --git a/code/modules/vending/vendor/tilting.dm b/code/modules/vending/vendor/tilting.dm index 1d7d899c0ea7..971633c683b9 100644 --- a/code/modules/vending/vendor/tilting.dm +++ b/code/modules/vending/vendor/tilting.dm @@ -180,7 +180,7 @@ if (!iscarbon(atom_target)) return FALSE var/mob/living/carbon/carbon_target = atom_target - for(var/obj/item/bodypart/squish_part in carbon_target.bodyparts) + for(var/obj/item/bodypart/squish_part in carbon_target.get_bodyparts()) var/severity = pick(WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_CRITICAL) if (!carbon_target.cause_wound_of_type_and_severity(WOUND_BLUNT, squish_part, severity, wound_source = "crushed by [src]")) squish_part.receive_damage(brute = 30) diff --git a/tgstation.dme b/tgstation.dme index da8503ddcfcc..67e316b99009 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6434,6 +6434,7 @@ #include "code\modules\surgery\bodyparts\helpers.dm" #include "code\modules\surgery\bodyparts\parts.dm" #include "code\modules\surgery\bodyparts\robot_bodyparts.dm" +#include "code\modules\surgery\bodyparts\stumps.dm" #include "code\modules\surgery\bodyparts\worn_feature_offset.dm" #include "code\modules\surgery\bodyparts\wounds.dm" #include "code\modules\surgery\bodyparts\species_parts\android_parts.dm" From d03ae827f468b84735438589d12219d125a7345b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:33:01 +0000 Subject: [PATCH 084/155] Automatic changelog for PR #95252 [ci skip] --- html/changelogs/AutoChangeLog-pr-95252.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95252.yml diff --git a/html/changelogs/AutoChangeLog-pr-95252.yml b/html/changelogs/AutoChangeLog-pr-95252.yml new file mode 100644 index 000000000000..d22eff3b281a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95252.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - rscdel: "\"Add prosthetic limb\" surgical operation has been reverted to be a bit closer to how it used to work - you operate on the missing limb / limb stump, rather than on the chest." + - refactor: "Missing limbs are now represented as limb stumps. In practice this should change nothing (for now), as no features were rewritten to make use of these besides surgery. Please report any oddities with missing limbs, however." \ No newline at end of file From 3729ee3ef36f121e321b0956ed4835ce99ce8d3d Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:46:30 -0700 Subject: [PATCH 085/155] Getting Mad at Parallax (Includes Boomerspace!) (#95382) ## About The Pull Request I want to do stuff with parallax (like placing stuff in the backdrop and moving it around), but I'm in my not doing 100 things in one pr arc so we're doing this piecemeal. To start, I wanted to try adding oldspace back as a parallax layer. This is something I was considering when it was first removed but never got around to, so here we are. I started by [writing a rust program](https://github.com/LemonInTheDark/old_space_gen) to fabricate the required icon state, then realized that all the different parts of space have their own animation delays. There's no way I could make one icon state with all them looping, so instead I split them up into multiple components and then overlayed them together. This works, but is infeasible (my gpu died) at the scale required for parallax (17 480x480 sets, 15 unique delays per 1 set) so I used render targets to render one copy, then mirror it to the rest of the overlays. This works wonderfully, and gets us down to (on my machine) a gpu cost comprable with about medium parallax intensity. I'm open to making these tile bound but I thought making them look "far away" feels better. In the process of all this I got very mad at the existing parallax code, soooooooo Parallax layers are no longer stored on the client, they are stored on and managed by the parallax home atom that holds them for display. Said atom also tracks all the information about how they are selected. Parallax layers no longer take a hud as input, instead expecting a client. (we were just swapping them back and forth and I thought it was dumb). Parallax no longer tries to support passing in a mob that does not actually own the hud it is displayed on. This feature wasn't even being used anymore because it was fully broken, so all it was doing was making the code worse. Parallax no longer has to do a full refresh anytime something about WHAT layers are displayed might have changed. We cache based off the variables we care about, and use the change in state to determine what should happen (this is improved by moving "rendering the layers" fully to the control of the home datum). Parallax no longer directly modifies the hud's plane masters, instead relying on trait based signals to manipulate them (this avoids wasted time in the common event of a needless parallax prefs check). Parallax no longer has 2 procs that are only called together to "remove/readd/update" the layers, instead doing both in a new check() proc. Cleans up some plane master cruft to do with tracking/managing huds (might break, tested, think it's fine). ## Why It's Good For The Game https://github.com/user-attachments/assets/79138a0f-9f6d-447d-843e-0d237db13276 ## Changelog :cl: add: Added an option for rendering space parallax with old space sprites (the ones from before we invented parallax), they're animated and I feel quite pretty. fix: Space parallax should hopefully behave a little more consistently now refactor: Rewrote a lot of how space parallax handled itself, please yell at me if any bugs make themselves known /:cl: --- .../dcs/signals/signals_plane_master_group.dm | 2 +- code/__DEFINES/preferences.dm | 1 + code/__DEFINES/traits/declarations.dm | 2 + code/_globalvars/traits/_traits.dm | 1 + code/_onclick/hud/hud.dm | 2 +- code/_onclick/hud/parallax/parallax.dm | 356 +++++++++++------- code/_onclick/hud/parallax/random_layer.dm | 2 +- .../hud/rendering/plane_master_group.dm | 3 +- .../plane_masters/plane_master_subtypes.dm | 75 +++- code/_onclick/hud/rendering/render_plate.dm | 53 ++- code/controllers/subsystem/parallax.dm | 7 +- code/datums/components/hide_weather_planes.dm | 2 +- code/modules/client/client_defines.dm | 7 - code/modules/client/client_procs.dm | 2 - code/modules/client/preferences/parallax.dm | 3 +- icons/effects/old_parallax.dmi | Bin 0 -> 96301 bytes 16 files changed, 322 insertions(+), 196 deletions(-) create mode 100644 icons/effects/old_parallax.dmi diff --git a/code/__DEFINES/dcs/signals/signals_plane_master_group.dm b/code/__DEFINES/dcs/signals/signals_plane_master_group.dm index d27adb5f8c95..61bd33c3e229 100644 --- a/code/__DEFINES/dcs/signals/signals_plane_master_group.dm +++ b/code/__DEFINES/dcs/signals/signals_plane_master_group.dm @@ -1,2 +1,2 @@ -/// from /datum/plane_master_group/proc/set_hud(): (datum/hud/new_hud) +/// from /datum/plane_master_group/proc/set_hud(): (datum/hud/old_hud, datum/hud/new_hud) #define COMSIG_GROUP_HUD_CHANGED "group_hud_changed" diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 1ae8403c644f..6421cdee67ac 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -47,6 +47,7 @@ #define PARALLAX_HIGH "High" #define PARALLAX_MED "Medium" #define PARALLAX_LOW "Low" +#define PARALLAX_BOOMER "Old" #define PARALLAX_DISABLE "Disabled" #define SCALING_METHOD_NORMAL "normal" diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index c93cb22246ae..8365b5a58ede 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -52,6 +52,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai // Hud traits /// This hud is owned by a client with an open escape menu #define TRAIT_ESCAPE_MENU_OPEN "escape_menu_open" +/// This hud has parallax displayed on it +#define TRAIT_PARALLAX_DISPLAYED "parallax_displayed" // Mob traits /// Forces the user to stay unconscious. diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 6fffe7203f4f..8f46c4954372 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -144,6 +144,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( ), /datum/hud = list( "TRAIT_ESCAPE_MENU_OPEN" = TRAIT_ESCAPE_MENU_OPEN, + "TRAIT_PARALLAX_DISPLAYED" = TRAIT_PARALLAX_DISPLAYED, ), /datum/wound = list( "TRAIT_WOUND_SCANNED" = TRAIT_WOUND_SCANNED, diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm index b4b4bc0af179..6af2635972b9 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -393,7 +393,7 @@ GLOBAL_LIST_INIT(available_ui_styles, list( screenmob.reload_fullscreen() if(screenmob == mymob) - update_parallax_pref(screenmob) + update_parallax_pref() else viewmob.hud_used.update_parallax_pref() diff --git a/code/_onclick/hud/parallax/parallax.dm b/code/_onclick/hud/parallax/parallax.dm index 9ed21d8e073a..eaebdbfcaf5f 100644 --- a/code/_onclick/hud/parallax/parallax.dm +++ b/code/_onclick/hud/parallax/parallax.dm @@ -1,117 +1,76 @@ - -/datum/hud/proc/create_parallax(mob/viewmob) - var/mob/screenmob = viewmob || mymob - var/client/C = screenmob.client - - if (!apply_parallax_pref(viewmob)) //don't want shit computers to crash when specing someone with insane parallax, so use the viewer's pref - for(var/atom/movable/screen/plane_master/parallax as anything in get_true_plane_masters(PLANE_SPACE_PARALLAX)) - parallax.hide_plane(screenmob) - return - - for(var/atom/movable/screen/plane_master/parallax as anything in get_true_plane_masters(PLANE_SPACE_PARALLAX)) - parallax.unhide_plane(screenmob) - - if(isnull(C.parallax_rock)) - C.parallax_rock = new(null, src) - C.screen |= C.parallax_rock - - if(!length(C.parallax_layers_cached)) - C.parallax_layers_cached = list() - C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_1(null, src) - C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_2(null, src) - C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/planet(null, src) - if(SSparallax.random_layer) - C.parallax_layers_cached += new SSparallax.random_layer.type(null, src, FALSE, SSparallax.random_layer) - C.parallax_layers_cached += new /atom/movable/screen/parallax_layer/layer_3(null, src) - - C.parallax_layers = C.parallax_layers_cached.Copy() - - if (length(C.parallax_layers) > C.parallax_layers_max) - C.parallax_layers.len = C.parallax_layers_max - - C.parallax_rock.vis_contents = C.parallax_layers - // We could do not do parallax for anything except the main plane group - // This could be changed, but it would require refactoring this whole thing - // And adding non client particular hooks for all the inputs, and I do not have the time I'm sorry :( - for(var/atom/movable/screen/plane_master/plane_master as anything in screenmob.hud_used.get_true_plane_masters(PLANE_SPACE)) - if(screenmob != mymob) - C.screen -= locate(/atom/movable/screen/plane_master/parallax_white) in C.screen - C.screen += plane_master - // this color makes parallax not black - plane_master.color = list( - 0, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 0, - 1, 1, 1, 1, - 0, 0, 0, 0 - ) - -/datum/hud/proc/remove_parallax(mob/viewmob) - var/mob/screenmob = viewmob || mymob - var/client/C = screenmob.client - C.screen -= (C.parallax_rock) - for(var/atom/movable/screen/plane_master/plane_master as anything in screenmob.hud_used.get_true_plane_masters(PLANE_SPACE)) - if(screenmob != mymob) - C.screen -= locate(/atom/movable/screen/plane_master/parallax_white) in C.screen - C.screen += plane_master - plane_master.color = initial(plane_master.color) - C.parallax_layers = null - -/datum/hud/proc/apply_parallax_pref(mob/viewmob) - var/mob/screenmob = viewmob || mymob - var/turf/screen_location = get_turf(screenmob) +/// Decides if parallax should be rendered or not, and sets things up accordingly +/datum/hud/proc/check_parallax() + var/client/displaying_client = mymob.client + if(isnull(displaying_client.parallax_rock)) + displaying_client.parallax_rock = new(null, null, displaying_client) + + /// Applies our preferences to our existing display + apply_parallax_pref() + var/atom/movable/screen/parallax_home/rock = displaying_client?.parallax_rock + + // Because other parts of the code can just REMOVE US FROM THE SCREEN for no reason as a joke + if (rock.displaying_layers) + ADD_TRAIT(src, TRAIT_PARALLAX_DISPLAYED, TRAIT_GENERIC) + displaying_client.screen |= rock + else + REMOVE_TRAIT(src, TRAIT_PARALLAX_DISPLAYED, TRAIT_GENERIC) + displaying_client.screen -= rock + +/datum/hud/proc/apply_parallax_pref() + var/turf/screen_location = get_turf(mymob) + var/client/displaying_client = mymob.client + var/atom/movable/screen/parallax_home/rock = displaying_client.parallax_rock if(SSmapping.level_trait(screen_location?.z, ZTRAIT_NOPARALLAX)) - return FALSE + rock.set_layer_settings(layers_to_draw = 0, draw_old_space = FALSE, animate_parallax = FALSE) + return - if (SSlag_switch.measures[DISABLE_PARALLAX] && !HAS_TRAIT(viewmob, TRAIT_BYPASS_MEASURES)) - return FALSE + if (SSlag_switch.measures[DISABLE_PARALLAX] && !HAS_TRAIT(mymob, TRAIT_BYPASS_MEASURES)) + rock.set_layer_settings(layers_to_draw = 0, draw_old_space = FALSE, animate_parallax = FALSE) + return - var/client/C = screenmob.client // Default to HIGH - var/parallax_selection = C?.prefs.read_preference(/datum/preference/choiced/parallax) || PARALLAX_HIGH + var/parallax_selection = displaying_client?.prefs.read_preference(/datum/preference/choiced/parallax) || PARALLAX_HIGH switch(parallax_selection) if (PARALLAX_INSANE) - C.parallax_layers_max = 5 - C.do_parallax_animations = TRUE - return TRUE + rock.set_layer_settings(layers_to_draw = 5, draw_old_space = FALSE, animate_parallax = TRUE) + return if(PARALLAX_HIGH) - C.parallax_layers_max = 4 - C.do_parallax_animations = TRUE - return TRUE + rock.set_layer_settings(layers_to_draw = 4, draw_old_space = FALSE, animate_parallax = TRUE) + return if (PARALLAX_MED) - C.parallax_layers_max = 3 - C.do_parallax_animations = TRUE - return TRUE + rock.set_layer_settings(layers_to_draw = 3, draw_old_space = FALSE, animate_parallax = TRUE) + return if (PARALLAX_LOW) - C.parallax_layers_max = 1 - C.do_parallax_animations = FALSE - return TRUE + rock.set_layer_settings(layers_to_draw = 1, draw_old_space = FALSE, animate_parallax = FALSE) + return + + if (PARALLAX_BOOMER) + rock.set_layer_settings(layers_to_draw = 0, draw_old_space = TRUE, animate_parallax = TRUE) + return if (PARALLAX_DISABLE) - return FALSE + rock.set_layer_settings(layers_to_draw = 0, draw_old_space = FALSE, animate_parallax = FALSE) + return -/datum/hud/proc/update_parallax_pref(mob/viewmob) - var/mob/screen_mob = viewmob || mymob - if(!screen_mob.client) +/datum/hud/proc/update_parallax_pref() + if(!mymob.client) return - remove_parallax(screen_mob) - create_parallax(screen_mob) - update_parallax(screen_mob) + check_parallax() + update_parallax() // This sets which way the current shuttle is moving (returns true if the shuttle has stopped moving so the caller can append their animation) -/datum/hud/proc/set_parallax_movedir(new_parallax_movedir = NONE, skip_windups, mob/viewmob) +/datum/hud/proc/set_parallax_movedir(new_parallax_movedir = NONE, skip_windups) . = FALSE - var/mob/screenmob = viewmob || mymob - var/client/C = screenmob.client - if(new_parallax_movedir == C.parallax_movedir) + var/client/displaying_client = mymob.client + if(new_parallax_movedir == displaying_client.parallax_movedir) return - var/animation_dir = new_parallax_movedir || C.parallax_movedir + var/animation_dir = new_parallax_movedir || displaying_client.parallax_movedir var/matrix/new_transform switch(animation_dir) if(NORTH) @@ -124,17 +83,17 @@ new_transform = matrix(1, 0,-480, 0, 1, 0) var/longest_timer = 0 - for(var/key in C.parallax_animate_timers) - deltimer(C.parallax_animate_timers[key]) - C.parallax_animate_timers = list() - for(var/atom/movable/screen/parallax_layer/layer as anything in C.parallax_layers) + for(var/key in displaying_client.parallax_animate_timers) + deltimer(displaying_client.parallax_animate_timers[key]) + displaying_client.parallax_animate_timers = list() + for(var/atom/movable/screen/parallax_layer/layer as anything in displaying_client.parallax_rock.parallax_layers) var/scaled_time = PARALLAX_LOOP_TIME / layer.speed if(new_parallax_movedir == NONE) // If we're stopping, we need to stop on the same dime, yeah? scaled_time = PARALLAX_LOOP_TIME longest_timer = max(longest_timer, scaled_time) if(skip_windups) - update_parallax_motionblur(C, layer, new_parallax_movedir, new_transform) + update_parallax_motionblur(displaying_client, layer, new_parallax_movedir, new_transform) continue layer.transform = new_transform @@ -144,15 +103,15 @@ //queue up another animate so lag doesn't create a shutter animate(transform = new_transform, time = 0) animate(transform = matrix(), time = scaled_time / 2) - C.parallax_animate_timers[layer] = addtimer(CALLBACK(src, PROC_REF(update_parallax_motionblur), C, layer, new_parallax_movedir, new_transform), scaled_time, TIMER_CLIENT_TIME|TIMER_STOPPABLE) + displaying_client.parallax_animate_timers[layer] = addtimer(CALLBACK(src, PROC_REF(update_parallax_motionblur), displaying_client, layer, new_parallax_movedir, new_transform), scaled_time, TIMER_CLIENT_TIME|TIMER_STOPPABLE) - C.dont_animate_parallax = world.time + min(longest_timer, PARALLAX_LOOP_TIME) - C.parallax_movedir = new_parallax_movedir + displaying_client.dont_animate_parallax = world.time + min(longest_timer, PARALLAX_LOOP_TIME) + displaying_client.parallax_movedir = new_parallax_movedir -/datum/hud/proc/update_parallax_motionblur(client/C, atom/movable/screen/parallax_layer/layer, new_parallax_movedir, matrix/new_transform) - if(!C) +/datum/hud/proc/update_parallax_motionblur(client/displaying_client, atom/movable/screen/parallax_layer/layer, new_parallax_movedir, matrix/new_transform) + if(!displaying_client) return - C.parallax_animate_timers -= layer + displaying_client.parallax_animate_timers -= layer // If we are moving in a direction, we used the QUAD_EASING function with EASE_IN // This means our position function is x^2. This is always LESS then the linear we're using here @@ -164,33 +123,34 @@ animate(layer, transform = new_transform, time = 0, loop = -1, flags = ANIMATION_END_NOW) animate(transform = matrix(), time = scaled_time) -/datum/hud/proc/update_parallax(mob/viewmob) - var/mob/screenmob = viewmob || mymob - var/client/C = screenmob.client - var/turf/posobj = get_turf(C.eye) +/datum/hud/proc/update_parallax() + var/client/displaying_client = mymob.client + var/turf/posobj = get_turf(displaying_client.eye) if(!posobj) return var/area/areaobj = posobj.loc // Update the movement direction of the parallax if necessary (for shuttles) - set_parallax_movedir(areaobj.parallax_movedir, FALSE, screenmob) + set_parallax_movedir(areaobj.parallax_movedir, FALSE, mymob) - if(!C.previous_turf || (C.previous_turf.z != posobj.z)) - C.previous_turf = posobj + if(!displaying_client.previous_turf || (displaying_client.previous_turf.z != posobj.z)) + displaying_client.previous_turf = posobj //Doing it this way prevents parallax layers from "jumping" when you change Z-Levels. - var/offset_x = posobj.x - C.previous_turf.x - var/offset_y = posobj.y - C.previous_turf.y + var/offset_x = posobj.x - displaying_client.previous_turf.x + var/offset_y = posobj.y - displaying_client.previous_turf.y - var/glide_rate = round(ICON_SIZE_ALL / screenmob.glide_size * world.tick_lag, world.tick_lag) - C.previous_turf = posobj + var/glide_rate = round(ICON_SIZE_ALL / mymob.glide_size * world.tick_lag, world.tick_lag) + displaying_client.previous_turf = posobj var/largest_change = max(abs(offset_x), abs(offset_y)) var/max_allowed_dist = (glide_rate / world.tick_lag) + 1 + var/atom/movable/screen/parallax_home/rock = displaying_client.parallax_rock + // If we aren't already moving/don't allow parallax, have made some movement, and that movement was smaller then our "glide" size, animate - var/run_parralax = (C.do_parallax_animations && glide_rate && !areaobj.parallax_movedir && C.dont_animate_parallax <= world.time && largest_change <= max_allowed_dist) + var/run_parralax = (rock.animate_parallax && glide_rate && !areaobj.parallax_movedir && displaying_client.dont_animate_parallax <= world.time && largest_change <= max_allowed_dist) - for(var/atom/movable/screen/parallax_layer/parallax_layer as anything in C.parallax_layers) + for(var/atom/movable/screen/parallax_layer/parallax_layer as anything in rock.parallax_layers) var/our_speed = parallax_layer.speed var/change_x var/change_y @@ -232,15 +192,15 @@ /atom/movable/proc/update_parallax_contents() for(var/mob/client_mob as anything in client_mobs_in_contents) - if(length(client_mob?.client?.parallax_layers) && client_mob.hud_used) + if(client_mob?.client?.parallax_rock?.displaying_layers && client_mob.hud_used) client_mob.hud_used.update_parallax() /mob/proc/update_parallax_teleport() //used for arrivals shuttle - if(client?.eye && hud_used && length(client.parallax_layers)) + if(client?.eye && hud_used && client?.parallax_rock?.displaying_layers) var/area/areaobj = get_area(client.eye) hud_used.set_parallax_movedir(areaobj.parallax_movedir, TRUE) -// Root object for parallax, all parallax layers are drawn onto this +// Root object for parallax, all parallax layers are drawn onto this and it manages them INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_home) /atom/movable/screen/parallax_home icon = null @@ -248,6 +208,87 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_home) plane = PLANE_SPACE_PARALLAX screen_loc = "CENTER-7,CENTER-7" mouse_opacity = MOUSE_OPACITY_TRANSPARENT + /// Layers we are currently displaying + var/list/atom/movable/screen/parallax_layer/parallax_layers = list() + /// Pallet of layers we CAN display if we choose to, depending on our client's prefs + /// ensures quick removal/reinsertion doesn't cause cycling qdels + var/list/atom/movable/screen/parallax_layer/parallax_layers_cached = list() + /// How many normal space layers we want to draw, in increasing order of "depth" + var/layers_to_draw = 0 + /// If we want to draw the old space layer + var/draw_old_space = FALSE + /// Are we currently displaying any layers? + var/displaying_layers = FALSE + /// Are we animating parallax? + var/animate_parallax = FALSE + /// The client that owns us + var/client/owner + +/atom/movable/screen/parallax_home/Initialize(mapload, datum/hud/hud_owner, client/owner) + . = ..() + src.owner = owner + +/atom/movable/screen/parallax_home/Destroy() + clear_layers() + owner = null + return ..() + +/atom/movable/screen/parallax_home/proc/display_layers() + if(displaying_layers || length(parallax_layers_cached) == 0) + return + parallax_layers = parallax_layers_cached + vis_contents = parallax_layers_cached + displaying_layers = TRUE + +/atom/movable/screen/parallax_home/proc/hide_layers() + if(!displaying_layers) + return + parallax_layers = list() + vis_contents = list() + displaying_layers = FALSE + +/atom/movable/screen/parallax_home/proc/set_layer_settings(layers_to_draw, draw_old_space, animate_parallax) + src.animate_parallax = animate_parallax + if(src.layers_to_draw == layers_to_draw && src.draw_old_space == draw_old_space) + return + src.layers_to_draw = layers_to_draw + src.draw_old_space = draw_old_space + regenerate_layers() + +/atom/movable/screen/parallax_home/proc/generate_space_layer(index) + switch(index) + if(1) + return new /atom/movable/screen/parallax_layer/layer_1(null, null, owner) + if(2) + return new /atom/movable/screen/parallax_layer/layer_2(null, null, owner) + if(3) + return new /atom/movable/screen/parallax_layer/planet(null, null, owner) + if(4) + if(SSparallax.random_layer) + return new SSparallax.random_layer.type(null, null, owner, FALSE, SSparallax.random_layer) + else + return new /atom/movable/screen/parallax_layer/layer_3(null, null, owner) + if(5) + if(SSparallax.random_layer) + return new /atom/movable/screen/parallax_layer/layer_3(null, null, owner) + +/atom/movable/screen/parallax_home/proc/regenerate_layers() + clear_layers() + if(layers_to_draw == 0 && !draw_old_space) + return + + parallax_layers_cached = list() + for(var/space_layer in 1 to layers_to_draw) + parallax_layers_cached += generate_space_layer(space_layer) + + if(draw_old_space) + parallax_layers_cached += new /atom/movable/screen/parallax_layer/old(null, null, owner) + + display_layers() + +/atom/movable/screen/parallax_home/proc/clear_layers() + hide_layers() + QDEL_LIST(parallax_layers_cached) // We need parallax to always pass its args down into initialize, so we immediate init it INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer) @@ -261,8 +302,10 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer) blend_mode = BLEND_ADD plane = PLANE_SPACE_PARALLAX mouse_opacity = MOUSE_OPACITY_TRANSPARENT + /// View size we're being rendered with + var/working_view = "" -/atom/movable/screen/parallax_layer/Initialize(mapload, datum/hud/hud_owner, template = FALSE) +/atom/movable/screen/parallax_layer/Initialize(mapload, datum/hud/hud_owner, client/owner, template = FALSE) . = ..() // Parallax layers are independent of hud, they care about client // Not doing this will just create a bunch of hard deletes @@ -271,42 +314,48 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer) if(template) return - var/client/boss = hud_owner?.mymob?.canon_client - - if(!boss) // If this typepath all starts to harddel your culprit is likely this + if(!owner) // If this typepath all starts to harddel your culprit is likely this return INITIALIZE_HINT_QDEL // I do not want to know bestie - var/view = boss.view || world.view + var/view = owner.view || world.view update_o(view) - RegisterSignal(boss, COMSIG_VIEW_SET, PROC_REF(on_view_change)) + RegisterSignal(owner, COMSIG_VIEW_SET, PROC_REF(on_view_change)) /atom/movable/screen/parallax_layer/proc/on_view_change(datum/source, new_size) SIGNAL_HANDLER update_o(new_size) -/atom/movable/screen/parallax_layer/proc/update_o(view) - if (!view) - view = world.view - var/static/pixel_grid_size = ICON_SIZE_ALL * 15 - var/static/parallax_scaler = ICON_SIZE_ALL / pixel_grid_size +/atom/movable/screen/parallax_layer/proc/update_o(new_view) + if(working_view == new_view) + return + working_view = new_view + update_appearance() + +/atom/movable/screen/parallax_layer/update_overlays() + . = ..() + var/overlay_view = working_view + if (!overlay_view) + overlay_view = world.view + var/pixel_grid_size = ICON_SIZE_ALL * 15 + var/parallax_scaler = ICON_SIZE_ALL / pixel_grid_size // Turn the view size into a grid of correctly scaled overlays - var/list/viewscales = getviewsize(view) + var/list/viewscales = getviewsize(overlay_view) // This could be half the size but we need to provide space for parallax movement on mob movement, and movement on scroll from shuttles, so like this instead var/countx = (CEILING((viewscales[1] / 2) * parallax_scaler, 1) + 1) var/county = (CEILING((viewscales[2] / 2) * parallax_scaler, 1) + 1) - var/list/new_overlays = new for(var/x in -countx to countx) for(var/y in -county to county) if(x == 0 && y == 0) continue - var/mutable_appearance/texture_overlay = mutable_appearance(icon, icon_state) + var/mutable_appearance/texture_overlay = tileable_appearance() texture_overlay.pixel_w += pixel_grid_size * x texture_overlay.pixel_z += pixel_grid_size * y - new_overlays += texture_overlay - cut_overlays() - add_overlay(new_overlays) + . += texture_overlay + +/atom/movable/screen/parallax_layer/proc/tileable_appearance() + return mutable_appearance(icon, icon_state) /atom/movable/screen/parallax_layer/layer_1 icon_state = "layer1" @@ -323,6 +372,34 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer) speed = 1.4 layer = 3 +/atom/movable/screen/parallax_layer/old + icon = null + icon_state = null // dog there's gonna be so many overlays... + speed = 0.6 + layer = 1 // Draws on its own + +/atom/movable/screen/parallax_layer/old/tileable_appearance() + var/mutable_appearance/copy = mutable_appearance(null, "") + // We have to use render targets to draw one of these flat and reuse it for this because FOR SOME REASON + // 16 (tile count) * (14 (animated state count) * 4 (frame count) + 1 (1 is not animated)) 480x480 states + // is TOO MUCH for the client. Whatever, see if I care. + copy.render_source = "*old_space_parallax" + return copy + +/atom/movable/screen/parallax_layer/old/update_overlays() + . = ..() + var/mutable_appearance/relayed_overlay = mutable_appearance('icons/effects/old_parallax.dmi', "1", appearance_flags = RESET_TRANSFORM|PIXEL_SCALE|KEEP_TOGETHER|KEEP_APART) + var/list/old_states = list("19", "21", "23", "24", "26", "29", "30", "31", "34", "35", "36", "37", "43", "46") + var/list/holder_overlays = list() + for(var/state in old_states) + holder_overlays += mutable_appearance('icons/effects/old_parallax.dmi', state) + relayed_overlay.overlays = holder_overlays + relayed_overlay.render_target = "*old_space_parallax" + // Renders the like, "input" appearance we draw to everything else + . += relayed_overlay + // The 0,0 appearance, can't reuse relayed_overlay for this because otherwise transforms would stack + . += tileable_appearance() + /atom/movable/screen/parallax_layer/planet icon_state = "planet" blend_mode = BLEND_OVERLAY @@ -330,17 +407,16 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer) speed = 3 layer = 30 -/atom/movable/screen/parallax_layer/planet/Initialize(mapload, datum/hud/hud_owner) +/atom/movable/screen/parallax_layer/planet/Initialize(mapload, datum/hud/hud_owner, client/owner) . = ..() - var/client/boss = hud_owner?.mymob?.canon_client - if(!boss) + if(!owner) return var/static/list/connections = list( COMSIG_MOVABLE_Z_CHANGED = PROC_REF(on_z_change), COMSIG_MOB_LOGOUT = PROC_REF(on_mob_logout), ) - AddComponent(/datum/component/connect_mob_behalf, boss, connections) - on_z_change(hud_owner?.mymob) + AddComponent(/datum/component/connect_mob_behalf, owner, connections) + on_z_change(owner.mob) /atom/movable/screen/parallax_layer/planet/proc/on_mob_logout(mob/source) SIGNAL_HANDLER diff --git a/code/_onclick/hud/parallax/random_layer.dm b/code/_onclick/hud/parallax/random_layer.dm index 379c0cbe2d9b..51cf69a92c00 100644 --- a/code/_onclick/hud/parallax/random_layer.dm +++ b/code/_onclick/hud/parallax/random_layer.dm @@ -4,7 +4,7 @@ speed = 2 layer = 3 -/atom/movable/screen/parallax_layer/random/Initialize(mapload, datum/hud/hud_owner, template, atom/movable/screen/parallax_layer/random/twin) +/atom/movable/screen/parallax_layer/random/Initialize(mapload, datum/hud/hud_owner, client/owner, template, atom/movable/screen/parallax_layer/random/twin) . = ..() if(twin) diff --git a/code/_onclick/hud/rendering/plane_master_group.dm b/code/_onclick/hud/rendering/plane_master_group.dm index 433fb7158363..17a7e1b8d265 100644 --- a/code/_onclick/hud/rendering/plane_master_group.dm +++ b/code/_onclick/hud/rendering/plane_master_group.dm @@ -34,12 +34,13 @@ if(our_hud) our_hud.master_groups -= key hide_hud() + var/datum/hud/old_hud = our_hud our_hud = new_hud if(new_hud) our_hud.master_groups[key] = src show_hud() build_planes_offset(our_hud, active_offset) - SEND_SIGNAL(src, COMSIG_GROUP_HUD_CHANGED, our_hud) + SEND_SIGNAL(src, COMSIG_GROUP_HUD_CHANGED, old_hud, our_hud) /// Display a plane master group to some viewer, so show all our planes to it /datum/plane_master_group/proc/attach_to(datum/hud/viewing_hud) diff --git a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm index 0599f56df347..36fd9f37d2d6 100644 --- a/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm +++ b/code/_onclick/hud/rendering/plane_masters/plane_master_subtypes.dm @@ -67,6 +67,36 @@ . = ..() add_relay_to(GET_NEW_PLANE(RENDER_PLANE_EMISSIVE, offset), relay_layer = EMISSIVE_SPACE_LAYER) +/atom/movable/screen/plane_master/parallax_white/set_home(datum/plane_master_group/home) + . = ..() + if(home) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + hud_changed(null, null, home.our_hud) + +/atom/movable/screen/plane_master/parallax_white/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + if(old_hud) + UnregisterSignal(old_hud, list(SIGNAL_ADDTRAIT(TRAIT_PARALLAX_DISPLAYED), SIGNAL_REMOVETRAIT(TRAIT_PARALLAX_DISPLAYED)), PROC_REF(parallax_updated)) + if(new_hud) + RegisterSignals(new_hud, list(SIGNAL_ADDTRAIT(TRAIT_PARALLAX_DISPLAYED), SIGNAL_REMOVETRAIT(TRAIT_PARALLAX_DISPLAYED)), PROC_REF(parallax_updated)) + parallax_updated(new_hud) + +/atom/movable/screen/plane_master/parallax_white/proc/parallax_updated(datum/source) + SIGNAL_HANDLER + if(isnull(home.our_hud?.mymob)) + return + if(HAS_TRAIT(home.our_hud, TRAIT_PARALLAX_DISPLAYED)) + // Gives parallax a fullwhite backdrop to multiply against + color = list( + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 1, 1, 1, 1, + 0, 0, 0, 0 + ) + else + color = initial(color) + ///Contains space parallax /atom/movable/screen/plane_master/parallax name = "Parallax" @@ -92,6 +122,29 @@ narsie_start_midway(GLOB.narsie_effect_last_modified) // We assume we're on the start, so we can use this number offset_increase(0, SSmapping.max_plane_offset) +/atom/movable/screen/plane_master/parallax/set_home(datum/plane_master_group/home) + . = ..() + if(home) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + hud_changed(null, null, home.our_hud) + +/atom/movable/screen/plane_master/parallax/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + if(old_hud) + UnregisterSignal(old_hud, list(SIGNAL_ADDTRAIT(TRAIT_PARALLAX_DISPLAYED), SIGNAL_REMOVETRAIT(TRAIT_PARALLAX_DISPLAYED)), PROC_REF(parallax_updated)) + if(new_hud) + RegisterSignals(new_hud, list(SIGNAL_ADDTRAIT(TRAIT_PARALLAX_DISPLAYED), SIGNAL_REMOVETRAIT(TRAIT_PARALLAX_DISPLAYED)), PROC_REF(parallax_updated)) + parallax_updated(new_hud) + +/atom/movable/screen/plane_master/parallax/proc/parallax_updated(datum/source) + SIGNAL_HANDLER + if(isnull(home.our_hud?.mymob)) + return + if(HAS_TRAIT(home.our_hud, TRAIT_PARALLAX_DISPLAYED)) + show_to(home.our_hud.mymob) + else + hide_from(home.our_hud.mymob) + /atom/movable/screen/plane_master/parallax/proc/on_offset_increase(datum/source, old_offset, new_offset) SIGNAL_HANDLER offset_increase(old_offset, new_offset) @@ -401,19 +454,19 @@ plane = CAMERA_STATIC_PLANE render_relay_planes = list(RENDER_PLANE_GAME) -/atom/movable/screen/plane_master/camera_static/show_to(mob/mymob) +/atom/movable/screen/plane_master/camera_static/set_home(datum/plane_master_group/home) . = ..() - if(!.) - return - var/datum/hud/our_hud = home.our_hud - if(isnull(our_hud)) - return + if(home) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + hud_changed(null, null, home.our_hud) - // We'll hide the slate if we're not seeing through a camera eye - // This can call on a cycle cause we don't clear in hide_from - // Yes this is the best way of hooking into the hud, I hate myself too - RegisterSignal(our_hud, COMSIG_HUD_EYE_CHANGED, PROC_REF(eye_changed), override = TRUE) - eye_changed(our_hud, null, our_hud.mymob?.canon_client?.eye) +/atom/movable/screen/plane_master/camera_static/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + if(old_hud) + UnregisterSignal(old_hud, COMSIG_HUD_EYE_CHANGED, PROC_REF(eye_changed)) + if(new_hud) + RegisterSignal(new_hud, COMSIG_HUD_EYE_CHANGED, PROC_REF(eye_changed)) + eye_changed(new_hud, null, new_hud.mymob?.canon_client?.eye) /atom/movable/screen/plane_master/camera_static/proc/eye_changed(datum/hud/source, atom/old_eye, atom/new_eye) SIGNAL_HANDLER diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 4572b66e0408..6286dfe30768 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -202,6 +202,12 @@ add_filter("emissives", 1, alpha_mask_filter(render_source = OFFSET_RENDER_TARGET(EMISSIVE_RENDER_TARGET, offset), flags = MASK_INVERSE)) set_light_cutoff(10) +/atom/movable/screen/plane_master/rendering_plate/lighting/set_home(datum/plane_master_group/home) + . = ..() + if(home) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + hud_changed(null, null, home.our_hud) + /atom/movable/screen/plane_master/rendering_plate/lighting/show_to(mob/mymob) . = ..() if(!.) @@ -217,23 +223,20 @@ backdrop = mymob.overlay_fullscreen("lighting_backdrop_unlit_[home.key]#[offset]", /atom/movable/screen/fullscreen/lighting_backdrop/unlit) SET_PLANE_EXPLICIT(backdrop, PLANE_TO_TRUE(backdrop.plane), src) - // Sorry, this is a bit annoying - // Basically, we only want the lighting plane we can actually see to attempt to render - // If we don't our lower plane gets totally overriden by the black void of the upper plane - var/datum/hud/hud = home.our_hud - // show_to can be called twice successfully with no hide_from call. Ensure no runtimes off the registers from this - if(hud) - RegisterSignal(hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change), override = TRUE) - offset_change(hud?.current_plane_offset || 0) set_light_cutoff(mymob.lighting_cutoff, mymob.lighting_color_cutoffs) /atom/movable/screen/plane_master/rendering_plate/lighting/hide_from(mob/oldmob) . = ..() oldmob.clear_fullscreen("lighting_backdrop_lit_[home.key]#[offset]") oldmob.clear_fullscreen("lighting_backdrop_unlit_[home.key]#[offset]") - var/datum/hud/hud = home.our_hud - if(hud) - UnregisterSignal(hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + +/atom/movable/screen/plane_master/rendering_plate/lighting/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + if(old_hud) + UnregisterSignal(old_hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + if(new_hud) + RegisterSignal(new_hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + offset_change(new_hud?.current_plane_offset || 0) /atom/movable/screen/plane_master/rendering_plate/lighting/proc/on_offset_change(datum/source, old_offset, new_offset) SIGNAL_HANDLER @@ -442,26 +445,22 @@ plane = RENDER_PLANE_MASTER render_relay_planes = list() -/atom/movable/screen/plane_master/rendering_plate/master/show_to(mob/mymob) +/atom/movable/screen/plane_master/rendering_plate/master/set_home(datum/plane_master_group/home) . = ..() - if(!.) - return - if(offset == 0) - return // Non 0 offset render plates will relay up to the transparent plane above them, assuming they're not on the same z level as their target of course - var/datum/hud/hud = home.our_hud - // show_to can be called twice successfully with no hide_from call. Ensure no runtimes off the registers from this - if(hud) - RegisterSignal(hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change), override = TRUE) - offset_change(hud?.current_plane_offset || 0) - -/atom/movable/screen/plane_master/rendering_plate/master/hide_from(mob/oldmob) - . = ..() if(offset == 0) return - var/datum/hud/hud = home.our_hud - if(hud) - UnregisterSignal(hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + if(home) + RegisterSignal(home, COMSIG_GROUP_HUD_CHANGED, PROC_REF(hud_changed)) + hud_changed(null, null, home.our_hud) + +/atom/movable/screen/plane_master/rendering_plate/master/proc/hud_changed(datum/source, datum/hud/old_hud, datum/hud/new_hud) + SIGNAL_HANDLER + if(old_hud) + UnregisterSignal(old_hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + if(new_hud) + RegisterSignal(new_hud, COMSIG_HUD_OFFSET_CHANGED, PROC_REF(on_offset_change)) + offset_change(new_hud?.current_plane_offset || 0) /atom/movable/screen/plane_master/rendering_plate/master/proc/on_offset_change(datum/source, old_offset, new_offset) SIGNAL_HANDLER diff --git a/code/controllers/subsystem/parallax.dm b/code/controllers/subsystem/parallax.dm index 28ebd80560f8..e7ccfff74c1a 100644 --- a/code/controllers/subsystem/parallax.dm +++ b/code/controllers/subsystem/parallax.dm @@ -71,7 +71,7 @@ SUBSYSTEM_DEF(parallax) if(picked_parallax == PARALLAX_NONE) return - random_layer = new picked_parallax(null, /* hud_owner = */ null, /* template = */ TRUE) + random_layer = new picked_parallax(null, /* hud_owner = */ null, /* owner = */ null, /* template = */ TRUE) RegisterSignal(random_layer, COMSIG_QDELETING, PROC_REF(clear_references)) random_layer.get_random_look() @@ -85,8 +85,9 @@ SUBSYSTEM_DEF(parallax) //Parallax is one of the first things to be set (during client join), so rarely is anything fast enough to swap it out //That's why we need to swap the layers out for fast joining clients :/ for(var/client/client as anything in GLOB.clients) - client.parallax_layers_cached?.Cut() - client.mob?.hud_used?.update_parallax_pref(client.mob) + // gotta clear things out + client?.parallax_rock?.set_layer_settings(0, FALSE, FALSE) + client.mob?.hud_used?.update_parallax_pref() /datum/controller/subsystem/parallax/proc/clear_references() SIGNAL_HANDLER diff --git a/code/datums/components/hide_weather_planes.dm b/code/datums/components/hide_weather_planes.dm index 3ef26d53b1ab..5b7addb199e7 100644 --- a/code/datums/components/hide_weather_planes.dm +++ b/code/datums/components/hide_weather_planes.dm @@ -55,7 +55,7 @@ else care_about.hide_plane(our_lad) -/datum/component/hide_weather_planes/proc/new_hud_attached(datum/source, datum/hud/new_hud) +/datum/component/hide_weather_planes/proc/new_hud_attached(datum/source, datum/hud/old_hud, datum/hud/new_hud) SIGNAL_HANDLER attach_hud(new_hud) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index b1e6181f723a..8a0d6f56d5fc 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -186,8 +186,6 @@ ///A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining var/list/recent_examines - var/list/parallax_layers - var/list/parallax_layers_cached var/atom/movable/screen/parallax_home/parallax_rock ///this is the last recorded client eye by SSparallax/fire() var/atom/movable/movingmob @@ -196,13 +194,8 @@ var/dont_animate_parallax /// Direction our current area wants to move parallax var/parallax_movedir = 0 - /// How many parallax layers to show our client - var/parallax_layers_max = 4 /// Timers for the area directional animation, one for each layer var/list/parallax_animate_timers - /// Do we want to do parallax animations at all? - /// Exists to prevent laptop fires - var/do_parallax_animations = TRUE ///Are we locking our movement input? var/movement_locked = FALSE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 7a2eee28d87b..0efbf0a819eb 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -645,8 +645,6 @@ GLOBAL_LIST_INIT(unrecommended_builds, list( QDEL_NULL(tooltips) QDEL_NULL(loot_panel) QDEL_NULL(parallax_rock) - QDEL_LIST(parallax_layers_cached) - parallax_layers = null seen_messages = null Master.UpdateTickRate() ..() //Even though we're going to be hard deleted there are still some things that want to know the destroy is happening diff --git a/code/modules/client/preferences/parallax.dm b/code/modules/client/preferences/parallax.dm index 24cccce2da62..a39a8edd489d 100644 --- a/code/modules/client/preferences/parallax.dm +++ b/code/modules/client/preferences/parallax.dm @@ -10,6 +10,7 @@ PARALLAX_HIGH, PARALLAX_MED, PARALLAX_LOW, + PARALLAX_BOOMER, PARALLAX_DISABLE, ) @@ -17,7 +18,7 @@ return PARALLAX_HIGH /datum/preference/choiced/parallax/apply_to_client(client/client, value) - client.mob?.hud_used?.update_parallax_pref(client?.mob) + client.mob?.hud_used?.update_parallax_pref() /datum/preference/choiced/parallax/deserialize(input, datum/preferences/preferences) // Old preferences were numbers, which causes annoyances when diff --git a/icons/effects/old_parallax.dmi b/icons/effects/old_parallax.dmi new file mode 100644 index 0000000000000000000000000000000000000000..cc58e7390e430def61b0e5991539365edf74e192 GIT binary patch literal 96301 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 NweFwZ_>14){}&CqT}=Q0 literal 0 HcmV?d00001 From 6c838e8e96856373bf9574f80790a5f25cb28d40 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:46:49 +0000 Subject: [PATCH 086/155] Automatic changelog for PR #95382 [ci skip] --- html/changelogs/AutoChangeLog-pr-95382.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95382.yml diff --git a/html/changelogs/AutoChangeLog-pr-95382.yml b/html/changelogs/AutoChangeLog-pr-95382.yml new file mode 100644 index 000000000000..ad8b4de3df87 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95382.yml @@ -0,0 +1,6 @@ +author: "LemonInTheDark" +delete-after: True +changes: + - rscadd: "Added an option for rendering space parallax with old space sprites (the ones from before we invented parallax), they're animated and I feel quite pretty." + - bugfix: "Space parallax should hopefully behave a little more consistently now" + - refactor: "Rewrote a lot of how space parallax handled itself, please yell at me if any bugs make themselves known" \ No newline at end of file From 9d14e327b53ba01b5b3148e6207bb7d0caba4f4e Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:48:57 -0700 Subject: [PATCH 087/155] Begins Improving Sparks/Flares Somewhat, Adds Animatable Light Overlays (#95362) ## About The Pull Request [Adds a visual tick helper, integrates it into SSmove and such](https://github.com/tgstation/tgstation/commit/e97035f9f74fad5c67c5bf19d8d5d3bb4bd476b4) Basically, if we do "stuff" during verb time then the next chance clients have to actually see it is on the next visual tick (rather then the normal "this tick"). This is cause clients get their next frame during maptick, and maptick runs before verbs. We want to be able to handle this properly because if you say, create an object and then move it on the same VISUAL tick (NOT game tick), it will just teleport instead of playing out the move. I don't want this for stuff like sparks, so we need a way to work around it. [Moves most users of the _FAST flag to _INSTANT](https://github.com/tgstation/tgstation/commit/6f96daac00519c69adc7554f52114798a65f3ad5) These are the kids that don't immediately spawn something and the move it, and we want to allow them to move actually as soon as possible (important for stuff like space) [Improves basic effect systems, makes their products delete when they stop moving](https://github.com/tgstation/tgstation/commit/172cb25d80ed34e1ec523172a1677fb524239fba) Moves some stuff out to getters or vars so children can better decide how long effects should last/how fast they should move. Uses this to clean up weird dupe code used by explosions. Makes all these effects delete on contact with something that stops them. I'm doing this because an effect just hanging in the air looks really really odd. Does have consequences for sparks that are already moving at a wall though, might need a better way to handle that. Makes all these effects use _FAST loops so they don't just hang in the air for a second on spawn Adds a setter proc on sparks for their duration, gonna use this to improve their effects some [Refactors overlay lights, adds support for animating their images](https://github.com/tgstation/tgstation/commit/3ad0083cf2b536df51a6d93dca40eac20c1d62d1) Implements light_render_source and relevant setters, this allows us to replace the components of an overlay light with basically whatever we want Refactors overlay lighting to handle its images more consistently, allowing us to hook into an image being modified Combining the two of these will allow us to consistently copy a light's image, modify it in some way, and then relay that modification back down. Allowing us to animate it or do more advanced effects painlessly Also, fixes ranges of 1 or less not rendering at all on initial set (thank you kapu) [In which I get fed up and add a macro helper for UID generation](https://github.com/tgstation/tgstation/commit/aab48b03d407104d4f9cf9acb034494237def911) [adds vv hooking for all existing lighting vars](https://github.com/tgstation/tgstation/commit/b81c6200a0d74c36b440aa3f4c1f22c422090a2d) [Upgrade effect system's dir picking to avoid duplicates when possible](https://github.com/tgstation/tgstation/commit/18b622586b509c6be4c4bca4e3e7c175ad75fe91) [Uses the technique described above to animate spark's lights out as they move](https://github.com/tgstation/tgstation/commit/67ba177982213799984a70e89536c5efb3d17e14) This is a decently nice effect imo, it allows us to bump their power (read, alpha) since it'll get animated away. I try to sync the animation to the actual icon state's flow (it's 0.7s long). I also sped them up somewhat to hopefully have a nicer looking effect? we'll see. [Abstracts away intercepting overlay lights into a holder datum](https://github.com/tgstation/tgstation/pull/95362/commits/b3f1fe74f2c3bab1d8912ab8a666bd05677ad032) This should make it far easier to reuse this pattern! [Fixes overlay lights flashing to double intensity when picked up off the ground](https://github.com/tgstation/tgstation/pull/95362/commits/1d83f2031fa2b33312b2aea4359c0c37c9d04ac7) We needed to clear out their underlays BEFORE the animation [Adds a flickering effect to flares and their children](https://github.com/tgstation/tgstation/pull/95362/commits/b7a858e04a607c58b6c7fbe1476ffe2239e63bde) I'm still not 100% happy with this, I was trying to avoid it feeling like a heartbeat with random noise and I.. THINK it worked? it's honestly quite hard to tell [Adds the same flickering to lighters, welding tools and life candles](https://github.com/tgstation/tgstation/pull/95362/commits/3ec44027e17835ae96702cec5f0b12d1f4deb32b) Also, updated light candles to mirror the appearance of normal candles and use overlay lighting EDIT: I realized while working on flares that I accidentally double applied color, so if you saw the sparks animations before now it was different (less vibrant). IDK if I like this better or worse but it is RIGHT and that's what matters. ## Why It's Good For The Game I got mad about how bad these looked, and this is a start at improving them. Also, adds a framework for more dynamic effects applied to overlay lights (you could use this to apply a sort of "emergency rotating" effect, or flicker/buzz for example).

Before https://github.com/user-attachments/assets/66437f27-ee3c-4f14-a7ee-4a1c3e68533a https://github.com/user-attachments/assets/ed14fff8-a7eb-47fe-bab5-9a490ac96629
After https://github.com/user-attachments/assets/fb24ff2e-c745-42a5-8e11-c8a1eeef35a5 https://github.com/user-attachments/assets/fd8c2116-cb92-4fe6-ad3e-786a6538e52a
## Changelog :cl: add: Reworks how sparks render. They're now a bit brighter, will fade out as they move/if they hit something, will stack with each other less and also won't start hang in the air on spawn. add: Added a flickering effect to lighters, welding tools, flares, torches and candles (since they're flames). fix: Overlay based lights (think flashlights) will no longer flash to double intensity while being picked up. refactor: Reworked how some effects (explosion particles, sparks, some reagent stuff) function, report any bugs! /:cl: --- .../signals_atom/signals_atom_lighting.dm | 11 ++ .../dcs/signals/signals_light_intercept.dm | 2 + code/__DEFINES/dcs/signals/signals_object.dm | 4 + code/__DEFINES/lighting.dm | 5 + code/__DEFINES/maths.dm | 6 + code/__DEFINES/movement.dm | 13 +- code/__DEFINES/visual_helpers.dm | 5 + code/controllers/subsystem/atoms.dm | 2 +- .../subsystem/movement/movement_types.dm | 11 +- .../movement/ai_movement_basic_avoidance.dm | 2 +- code/datums/components/overlay_lighting.dm | 119 ++++++++----- code/datums/drift_handler.dm | 2 +- code/game/atom/_atom.dm | 14 +- code/game/atom/atom_vv.dm | 21 +++ .../effects/effect_system/effect_system.dm | 57 +++++-- .../effect_system/effects_explosion.dm | 22 +-- .../effects/effect_system/effects_sparks.dm | 51 +++++- .../effects/effect_system/effects_water.dm | 3 +- code/game/objects/items.dm | 2 + code/game/objects/items/devices/flashlight.dm | 12 ++ code/game/objects/items/lighter.dm | 15 ++ .../items/tools/engineering/weldingtool.dm | 14 ++ code/game/objects/items/tools/extinguisher.dm | 2 +- code/game/objects/structures/life_candle.dm | 22 ++- code/game/world.dm | 6 + .../atmospherics/machinery/datum_pipeline.dm | 2 +- code/modules/library/lib_machines.dm | 2 +- code/modules/lighting/light_middleman.dm | 160 ++++++++++++++++++ code/modules/lighting/lighting_atom.dm | 58 ++++--- code/modules/mining/mine_items.dm | 2 +- .../mecha/equipment/tools/other_tools.dm | 2 +- tgstation.dme | 2 + 32 files changed, 536 insertions(+), 115 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_light_intercept.dm create mode 100644 code/modules/lighting/light_middleman.dm diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm index 391ed0112ac9..8e713f0260f9 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm @@ -39,6 +39,17 @@ #define COMSIG_ATOM_SET_LIGHT_FLAGS "atom_set_light_flags" ///Called right after the atom changes the value of light_flags to a different one, from base of [/atom/proc/set_light_flags]: (old_flags) #define COMSIG_ATOM_UPDATE_LIGHT_FLAGS "atom_update_light_flags" +///Called right before the atom changes the value of light_render_source to a different one, from base [atom/proc/set_light_render_source]: (new_render_source) +#define COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE "atom_set_light_render_source" +///Called right after the atom changes the value of light_render_source to a different one, from base of [/atom/proc/set_light_render_source]: (old_render_source) +#define COMSIG_ATOM_UPDATE_LIGHT_RENDER_SOURCE "atom_update_light_render_source" ///Called when an atom has a light template applied to it. Frombase of [/datum/light_template/proc/mirror_onto]: () #define COMSIG_ATOM_LIGHT_TEMPLATE_MIRRORED "atom_light_template_mirrored" + +///Called when an atom's overlay component applies visuals, from base of [/datum/component/overlay_lighting/proc/show_to_holder]: (atom/movable/light_holder) +#define COMSIG_ATOM_OVERLAY_LIGHT_APPLIED "atom_overlay_light_applied" + +///Called when an atom's overlay component hides its visuals, from base of [/datum/component/overlay_lighting/proc/hide_from_holder]: (atom/movable/light_holder) +#define COMSIG_ATOM_OVERLAY_LIGHT_REMOVED "atom_overlay_light_removed" + diff --git a/code/__DEFINES/dcs/signals/signals_light_intercept.dm b/code/__DEFINES/dcs/signals/signals_light_intercept.dm new file mode 100644 index 000000000000..8b4694d9d0e0 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_light_intercept.dm @@ -0,0 +1,2 @@ +/// from base of [/datum/light_middleman/proc/light_modified] : () +#define COMSIG_LIGHT_MIDDLEMAN_UPDATED "light_middleman_updated" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 3bd83c546d6d..319e933cd076 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -151,6 +151,10 @@ #define COMSIG_ITEM_STORED "item_stored" ///from base of datum/storage/handle_exit(): (datum/storage/storage) #define COMSIG_ITEM_UNSTORED "item_unstored" +///from base of obj/item/do_pickup_animation(): () +#define COMSIG_ITEM_BEFORE_PICKUP_ANIMATION "item_before_pickup_animation" +///from base of obj/item/do_drop_animation(): () +#define COMSIG_ITEM_BEFORE_DROP_ANIMATION "item_before_drop_animation" /** * From base of datum/strippable_item/get_alternate_actions(): (atom/owner, mob/user, list/alt_actions) diff --git a/code/__DEFINES/lighting.dm b/code/__DEFINES/lighting.dm index 08e76678fff3..9b843a695198 100644 --- a/code/__DEFINES/lighting.dm +++ b/code/__DEFINES/lighting.dm @@ -11,6 +11,11 @@ /// Nonesensical value for light color, used for null checks. #define NONSENSICAL_VALUE -99999 +// Light systems that use the overlay light component +#define IS_OVERLAY_LIGHT_SYSTEM(system) (system == OVERLAY_LIGHT || system == OVERLAY_LIGHT_DIRECTIONAL || system == OVERLAY_LIGHT_BEAM) +// Light systems that use the cone image of the overlay light component +#define IS_OVERLAY_CONE_LIGHT_SYSTEM(system) (system == OVERLAY_LIGHT_DIRECTIONAL || system == OVERLAY_LIGHT_BEAM) + /// Is our overlay light source attached to another movable (its loc), meaning that the lighting component should go one level deeper. #define LIGHT_ATTACHED (1<<0) /// Freezes a light in its current state, blocking any attempts at modification diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index c4f95c49eae6..d2fe40a502cb 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -53,6 +53,9 @@ /// Increments a value and wraps it if it exceeds some value. Can be used to circularly iterate through a list through `idx = WRAP_UP(idx, length_of_list)`. #define WRAP_UP(val, max) (((val) % (max)) + 1) +/// Helper that increments and wraps the passed in number when it hits the integer limit +#define WRAP_UID(val) WRAP_UP(val, SHORT_REAL_LIMIT - 1) + // Real modulus that handles decimals #define MODULUS(x, y) ( (x) - FLOOR(x, y)) @@ -233,6 +236,9 @@ #define LORENTZ_DISTRIBUTION(x, s) ( s*tan(TODEGREES(PI*(rand()-0.5))) + x ) #define LORENTZ_CUMULATIVE_DISTRIBUTION(x, y, s) ( (1/PI)*TORADIANS(arctan((x-(y))/s)) + 1/2 ) +/// Fucked up like upside down cauchy dist that I've pinned to 1 so I can use it as a multiplier +/// https://www.desmos.com/calculator/bt4tfavvi7 +#define ANCHORED_INVERSE_CAUCHY(s) (2 - ( 1 / (s * (1 + ((rand() - 0.5) / s) ** 2 ))) * (s + (0.5 ** 2) / s)) #define RULE_OF_THREE(a, b, x) ((a*x)/b) diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index e07fe65552d5..57b7825bcc95 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -29,15 +29,18 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) //Movement loop flags ///Should the loop act immediately following its addition? -#define MOVEMENT_LOOP_START_FAST (1<<0) +#define MOVEMENT_LOOP_START_INSTANT (1<<0) +///Should the loop act as soon as is reasonable? +///(always 1 tick after the next visual tick, makes behavior consistent regardless of when the SS fires in the tick) +#define MOVEMENT_LOOP_START_FAST (1<<1) ///Do we not use the priority system? -#define MOVEMENT_LOOP_IGNORE_PRIORITY (1<<1) +#define MOVEMENT_LOOP_IGNORE_PRIORITY (1<<2) ///Should we override the loop's glide? -#define MOVEMENT_LOOP_IGNORE_GLIDE (1<<2) +#define MOVEMENT_LOOP_IGNORE_GLIDE (1<<3) ///Should we not update our movables dir on move? -#define MOVEMENT_LOOP_NO_DIR_UPDATE (1<<3) +#define MOVEMENT_LOOP_NO_DIR_UPDATE (1<<4) ///Is the loop moving the movable outside its control, like it's an external force? e.g. footsteps won't play if enabled. -#define MOVEMENT_LOOP_OUTSIDE_CONTROL (1<<4) +#define MOVEMENT_LOOP_OUTSIDE_CONTROL (1<<5) // Movement loop status flags /// Has the loop been paused, soon to be resumed? diff --git a/code/__DEFINES/visual_helpers.dm b/code/__DEFINES/visual_helpers.dm index fda7f6648092..d3cd58f672ba 100644 --- a/code/__DEFINES/visual_helpers.dm +++ b/code/__DEFINES/visual_helpers.dm @@ -23,3 +23,8 @@ /// Much like [SET_BASE_PIXEL], except it will not effect pixel offsets in mapping programs #define SET_BASE_PIXEL_NOMAP(x, y) MAP_SWITCH(SET_BASE_PIXEL(x, y), _SET_BASE_PIXEL_NO_OFFSET(x, y)) + +/// What world.time will we next complete a visual tick? +/// This is important because we often want to avoid moving an object twice in a tick, since this can lead to teleportation instead of gliding +/// (such as when you move an atom that just spawned) +#define NEXT_VISUAL_TICK (GLOB.last_maptick_time + world.tick_lag) diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index ad09a0caeb28..0ea51fe96242 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -47,7 +47,7 @@ SUBSYSTEM_DEF(atoms) // Generate a unique mapload source for this run of InitializeAtoms var/static/uid = 0 - uid = (uid + 1) % (SHORT_REAL_LIMIT - 1) + uid = WRAP_UID(uid + 1) var/source = "subsystem init [uid]" set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, source) diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index 6acce747bbe1..1aa16a42fa07 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -59,11 +59,16 @@ SEND_SIGNAL(src, COMSIG_MOVELOOP_START) status |= MOVELOOP_STATUS_RUNNING //If this is our first time starting to move with this loop - //And we're meant to start instantly + //And we want to start consistently fast if(!timer && flags & MOVEMENT_LOOP_START_FAST) - timer = world.time + // + tick_lag because we want to avoid weird jumping in atoms that were just created (and avoid inconsistencies around subsystem timing) + timer = NEXT_VISUAL_TICK + world.tick_lag + return + //And we're meant to start instantly + if(!timer && flags & MOVEMENT_LOOP_START_INSTANT) + timer = NEXT_VISUAL_TICK return - timer = world.time + delay + timer = NEXT_VISUAL_TICK + delay ///Called when a loop is stopped, doesn't stop the loop itself /datum/move_loop/proc/loop_stopped() diff --git a/code/datums/ai/movement/ai_movement_basic_avoidance.dm b/code/datums/ai/movement/ai_movement_basic_avoidance.dm index 6659da244f3d..169301f70470 100644 --- a/code/datums/ai/movement/ai_movement_basic_avoidance.dm +++ b/code/datums/ai/movement/ai_movement_basic_avoidance.dm @@ -21,4 +21,4 @@ /// Move immediately and don't update our facing /datum/ai_movement/basic_avoidance/backstep - move_flags = MOVEMENT_LOOP_START_FAST | MOVEMENT_LOOP_NO_DIR_UPDATE + move_flags = MOVEMENT_LOOP_START_INSTANT | MOVEMENT_LOOP_NO_DIR_UPDATE diff --git a/code/datums/components/overlay_lighting.dm b/code/datums/components/overlay_lighting.dm index ca8d4e7b23dd..8f07b57d9124 100644 --- a/code/datums/components/overlay_lighting.dm +++ b/code/datums/components/overlay_lighting.dm @@ -64,6 +64,8 @@ var/beam = FALSE ///A cone overlay for directional light, its alpha and color are dependent on the light var/image/cone + /// Are we currently displaying light on our holder? + var/currently_displaying = FALSE ///Current tracked direction for the directional cast behaviour var/current_direction ///Tracks current directional x offset so we don't update unnecessarily @@ -78,7 +80,7 @@ return COMPONENT_INCOMPATIBLE var/atom/movable/movable_parent = parent - if(!force && movable_parent.light_system != OVERLAY_LIGHT && movable_parent.light_system != OVERLAY_LIGHT_DIRECTIONAL && movable_parent.light_system != OVERLAY_LIGHT_BEAM) + if(!force && !IS_OVERLAY_LIGHT_SYSTEM(movable_parent.light_system)) stack_trace("[type] added to [parent], with [movable_parent.light_system] value for the light_system var. Use [OVERLAY_LIGHT], [OVERLAY_LIGHT_DIRECTIONAL] or [OVERLAY_LIGHT_BEAM] instead.") return COMPONENT_INCOMPATIBLE @@ -111,7 +113,7 @@ set_color(parent, movable_parent.light_color) if(!isnull(starts_on)) movable_parent.set_light_on(starts_on) - + set_light_render_source(parent, "") /datum/component/overlay_lighting/RegisterWithParent() . = ..() @@ -122,10 +124,12 @@ RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_COLOR, PROC_REF(set_color)) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_ON, PROC_REF(on_toggle)) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, PROC_REF(on_light_flags_change)) + RegisterSignal(parent, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, PROC_REF(set_light_render_source)) RegisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT, PROC_REF(on_parent_crafted)) RegisterSignal(parent, COMSIG_LIGHT_EATER_QUEUE, PROC_REF(on_light_eater)) RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_parent_moved)) RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_move)) + RegisterSignal(parent, COMSIG_ITEM_BEFORE_PICKUP_ANIMATION, PROC_REF(on_pickup_anim)) var/atom/movable/movable_parent = parent if(movable_parent.light_flags & LIGHT_ATTACHED) overlay_lighting_flags |= LIGHTING_ATTACHED @@ -200,18 +204,44 @@ ///Adds the luminosity and source for the affected movable atoms to keep track of their visibility. /datum/component/overlay_lighting/proc/add_dynamic_lumi() LAZYSET(current_holder.affected_dynamic_lights, src, lumcount_range + 1) - current_holder.underlays += visible_mask + show_to_holder() current_holder.update_dynamic_luminosity() - if(directional) - current_holder.underlays += cone ///Removes the luminosity and source for the affected movable atoms to keep track of their visibility. /datum/component/overlay_lighting/proc/remove_dynamic_lumi() LAZYREMOVE(current_holder.affected_dynamic_lights, src) - current_holder.underlays -= visible_mask + hide_from_holder() current_holder.update_dynamic_luminosity() + +/// Adds our overlays to our holder, assuming everything's setup proper +/datum/component/overlay_lighting/proc/show_to_holder() + if(currently_displaying) + return + if(isnull(current_holder) || !(overlay_lighting_flags & LIGHTING_ON)) + currently_displaying = FALSE + return + current_holder.underlays += visible_mask + if(directional) + current_holder.underlays += cone + currently_displaying = TRUE + // These are very intentionally copies so recipients cannot + // Accidentially brick lighting overlays by mutating them + var/mutable_appearance/mask_clone = new (visible_mask) + var/mutable_appearance/cone_clone = directional ? new /mutable_appearance(cone) : null + SEND_SIGNAL(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED, mask_clone, cone_clone, current_holder) + +/// Removes our overlay from our holder, assuming everything's setup proper +/// MUST be called before modifying cone or visible_mask, or you will cause stuck lighting +/datum/component/overlay_lighting/proc/hide_from_holder() + if(!currently_displaying) + return + if(isnull(current_holder) || !(overlay_lighting_flags & LIGHTING_ON)) + return + current_holder.underlays -= visible_mask if(directional) current_holder.underlays -= cone + currently_displaying = FALSE + SEND_SIGNAL(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED, current_holder) ///Called to change the value of parent_attached_to. /datum/component/overlay_lighting/proc/set_parent_attached_to(atom/movable/new_parent_attached_to) @@ -321,15 +351,16 @@ /datum/component/overlay_lighting/proc/on_z_move(atom/source) SIGNAL_HANDLER - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask - current_holder.underlays -= cone + hide_from_holder() SET_PLANE_EXPLICIT(visible_mask, O_LIGHTING_VISUAL_PLANE, source) if(cone) SET_PLANE_EXPLICIT(cone, O_LIGHTING_VISUAL_PLANE, source) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - current_holder.underlays += cone + show_to_holder() + +// Avoids duplicate overlays (one from our NEXT holder, selected after the animation, one from the pickup animation) +/datum/component/overlay_lighting/proc/on_pickup_anim(atom/source) + SIGNAL_HANDLER + hide_from_holder() ///Called when the current_holder is qdeleted, to remove the light effect. /datum/component/overlay_lighting/proc/on_parent_attached_to_qdel(atom/movable/source, force) @@ -364,20 +395,18 @@ range = clamp(CEILING(new_range, 0.5), 1, 6) var/pixel_bounds = ((range - 1) * 64) + 32 lumcount_range = CEILING(range, 1) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() + visible_mask.icon = light_overlays["[pixel_bounds]"] if(pixel_bounds == 32) - if(!directional) // it's important that we make it to the end of this function if we are a directional light - visible_mask.transform = null - return + visible_mask.transform = null else var/offset = (pixel_bounds - 32) * 0.5 var/matrix/transform = new transform.Translate(-offset, -offset) visible_mask.transform = transform - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask + + show_to_holder() if(directional) if(beam) cast_range = max(round(new_range * 0.5), 1) @@ -393,39 +422,24 @@ var/new_power = source.light_power set_lum_power(new_power >= 0 ? 0.5 : -0.5) set_alpha = min(230, (abs(new_power) * 120) + 30) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() visible_mask.alpha = set_alpha visible_mask.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - if(!directional) - return - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= cone - cone.alpha = min(120, (abs(new_power) * 60) + 15) - cone.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += cone + if(directional) + cone.alpha = min(120, (abs(new_power) * 60) + 15) + cone.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT + show_to_holder() ///Changes the light's color, pretty straightforward. /datum/component/overlay_lighting/proc/set_color(atom/source, old_color) SIGNAL_HANDLER var/new_color = source.light_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() visible_mask.color = new_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - if(!directional) - return - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= cone - cone.color = new_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += cone - + if(directional) + cone.color = new_color + show_to_holder() ///Toggles the light on and off. /datum/component/overlay_lighting/proc/on_toggle(atom/source, old_value) @@ -436,7 +450,6 @@ return turn_off() //Falsey value, turn off. - ///Triggered right after the parent light flags change. /datum/component/overlay_lighting/proc/on_light_flags_change(atom/source, old_flags) SIGNAL_HANDLER @@ -453,6 +466,18 @@ overlay_lighting_flags &= ~LIGHTING_ATTACHED set_parent_attached_to(null) +///Changes the light's color, pretty straightforward. +/datum/component/overlay_lighting/proc/set_light_render_source(atom/source, old_render_source) + SIGNAL_HANDLER + var/new_source = source.light_render_source + hide_from_holder() + visible_mask.render_source = new_source + if(directional) + var/new_cone_source = "" + if(new_source) + new_cone_source = "[new_source]_cone" + cone.render_source = new_cone_source + show_to_holder() ///Toggles the light on. /datum/component/overlay_lighting/proc/turn_on() @@ -504,7 +529,7 @@ break scanning = next_turf - current_holder.underlays -= visible_mask + hide_from_holder() var/translate_x = -((range - 1) * 32) var/translate_y = translate_x @@ -536,8 +561,8 @@ transform.Scale(scale_x, scale_y) transform.Translate(translate_x, translate_y) visible_mask.transform = transform - if(overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask + + show_to_holder() ///Called when current_holder changes loc. /datum/component/overlay_lighting/proc/on_holder_dir_change(atom/movable/source, olddir, newdir) diff --git a/code/datums/drift_handler.dm b/code/datums/drift_handler.dm index 53133025657f..0508123c1388 100644 --- a/code/datums/drift_handler.dm +++ b/code/datums/drift_handler.dm @@ -23,7 +23,7 @@ parent.drift_handler = src var/flags = MOVEMENT_LOOP_OUTSIDE_CONTROL if(instant) - flags |= MOVEMENT_LOOP_START_FAST + flags |= MOVEMENT_LOOP_START_INSTANT src.drift_force = drift_force drifting_loop = GLOB.move_manager.smooth_move(moving = parent, angle = inertia_angle, delay = get_loop_delay(parent), subsystem = SSnewtonian_movement, priority = MOVEMENT_SPACE_PRIORITY, flags = flags) diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm index 73a29c11eca1..76c5744c0f98 100644 --- a/code/game/atom/_atom.dm +++ b/code/game/atom/_atom.dm @@ -73,17 +73,23 @@ var/light_power = 1 ///Hexadecimal RGB string representing the colour of the light. White by default. var/light_color = COLOR_WHITE + ///Boolean variable for toggleable lights. Has no effect without the proper light_system, light_range and light_power values. + var/light_on = TRUE + ///Bitflags to determine lighting-related atom properties. + var/light_flags = NONE + + // OVERLAY_LIGHT only values + /// An optional render_source to apply to this atom's light overlay + var/light_render_source = "" + + // COMPLEX_LIGHT only values /// Angle of light to show in light_dir /// 360 is a circle, 90 is a cone, etc. var/light_angle = 360 /// What angle to project light in var/light_dir = NORTH - ///Boolean variable for toggleable lights. Has no effect without the proper light_system, light_range and light_power values. - var/light_on = TRUE /// How many tiles "up" this light is. 1 is typical, should only really change this if it's a floor light var/light_height = LIGHTING_HEIGHT - ///Bitflags to determine lighting-related atom properties. - var/light_flags = NONE ///Our light source. Don't fuck with this directly unless you have a good reason! var/tmp/datum/light_source/light ///Any light sources that are "inside" of us, for example, if src here was a mob that's carrying a flashlight, that flashlight's light source would be part of this list. diff --git a/code/game/atom/atom_vv.dm b/code/game/atom/atom_vv.dm index 14e20d23b9d7..d9600d4bae94 100644 --- a/code/game/atom/atom_vv.dm +++ b/code/game/atom/atom_vv.dm @@ -263,6 +263,27 @@ // I'm sorry old_light_flags = var_value . = TRUE + if(NAMEOF(src, light_render_source)) + set_light_render_source(var_value) + . = TRUE + if(NAMEOF(src, light_angle)) + if(light_system == COMPLEX_LIGHT) + set_light(l_angle = var_value) + else + set_light_angle(var_value) + . = TRUE + if(NAMEOF(src, light_dir)) + if(light_system == COMPLEX_LIGHT) + set_light(l_dir = var_value) + else + set_light_dir(var_value) + . = TRUE + if(NAMEOF(src, light_height)) + if(light_system == COMPLEX_LIGHT) + set_light(l_height = var_value) + else + set_light_height(var_value) + . = TRUE if(NAMEOF(src, smoothing_junction)) set_smoothed_icon_state(var_value) . = TRUE diff --git a/code/game/objects/effects/effect_system/effect_system.dm b/code/game/objects/effects/effect_system/effect_system.dm index c91b2168454c..5bd2f3267ca0 100644 --- a/code/game/objects/effects/effect_system/effect_system.dm +++ b/code/game/objects/effects/effect_system/effect_system.dm @@ -57,6 +57,16 @@ var/total_effects = 0 /// Should the system delete itself after finishing? var/autocleanup = FALSE + /// Should the system delete effects that stop moving? + var/delete_on_stop = FALSE + /// How frequently (in deciseconds) should we move our particles? + var/step_delay = 0.5 SECONDS + + // Internal use + /// The length of the previous assigned moveloop in deciseconds + var/last_loop_length = 0 + /// List of dirs avalible to pick, used to avoid accidential duplicates + var/list/pickable_dirs = list() /datum/effect_system/basic/New(turf/location, amount = null, cardinals_only = null) . = ..() @@ -73,25 +83,48 @@ return generate_effect() +/// Returns how many steps to attempt to move a generated effect +/datum/effect_system/basic/proc/get_step_count() + return rand(1, 3) + +/// Generates a effect for our system to control, returns the generated effect /datum/effect_system/basic/proc/generate_effect() if(holder) location = get_turf(holder) var/obj/effect/effect = new effect_type(location) total_effects++ - var/direction - if(cardinals_only) - direction = pick(GLOB.cardinals) - else - direction = pick(GLOB.alldirs) - - var/step_amt = rand(1, 3) - var/step_delay = 5 - var/datum/move_loop/loop = GLOB.move_manager.move(effect, direction, step_delay, timeout = step_delay * step_amt, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) - RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(decrement_total_effect)) - -/datum/effect_system/basic/proc/decrement_total_effect(datum/source) + + if(!length(pickable_dirs)) + if(cardinals_only) + pickable_dirs = GLOB.cardinals.Copy() + else + pickable_dirs = GLOB.alldirs.Copy() + // Try not to reuse dirs if possible to avoid weird stacking + var/direction = pick_n_take(pickable_dirs) + + var/step_count = get_step_count() + var/datum/move_loop/loop = GLOB.move_manager.move(effect, direction, step_delay, timeout = step_delay * step_count, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(loop_end)) + last_loop_length = loop.lifetime + return effect + +/datum/effect_system/basic/proc/post_move(datum/move_loop/source, result, visual_delay) + SIGNAL_HANDLER + if(result == MOVELOOP_FAILURE) + move_failed(source, source.moving) + +/// Allows us to hook into being unable to automatically move +/datum/effect_system/basic/proc/move_failed(datum/move_loop/loop, obj/effect/failed) + if(QDELETED(failed) || !delete_on_stop) + return + qdel(failed) + +/datum/effect_system/basic/proc/loop_end(datum/move_loop/source) SIGNAL_HANDLER total_effects-- + if(delete_on_stop && !QDELETED(source.moving)) + qdel(source.moving) if(autocleanup && total_effects == 0) QDEL_IN(src, 2 SECONDS) diff --git a/code/game/objects/effects/effect_system/effects_explosion.dm b/code/game/objects/effects/effect_system/effects_explosion.dm index 9f4db05d967b..5e9ff4e1ade6 100644 --- a/code/game/objects/effects/effect_system/effects_explosion.dm +++ b/code/game/objects/effects/effect_system/effects_explosion.dm @@ -4,14 +4,6 @@ opacity = TRUE anchored = TRUE -/obj/effect/particle_effect/expl_particles/Initialize(mapload) - ..() - return INITIALIZE_HINT_LATELOAD - -/obj/effect/particle_effect/expl_particles/LateInitialize() - var/step_amt = pick(25;1, 50;2, 100;3, 200;4) - var/datum/move_loop/loop = GLOB.move_manager.move(src, pick(GLOB.alldirs), 1, timeout = step_amt, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) - RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(end_particle)) /obj/effect/particle_effect/expl_particles/proc/end_particle(datum/source) SIGNAL_HANDLER @@ -19,10 +11,20 @@ qdel(src) /datum/effect_system/basic/expl_particles + effect_type = /obj/effect/particle_effect/expl_particles amount = 10 + step_delay = 0.1 SECONDS + delete_on_stop = TRUE -/datum/effect_system/basic/expl_particles/generate_effect() - new /obj/effect/particle_effect/expl_particles(location) +/datum/effect_system/basic/expl_particles/get_step_count() + return pick(25;1, 50;2, 100;3, 200;4) + +/datum/effect_system/basic/expl_particles/loop_end(datum/move_loop/source) + . = ..() + var/obj/effect/explosion_particle = source.moving + if(QDELETED(explosion_particle)) + return + qdel(explosion_particle) /obj/effect/explosion name = "fire" diff --git a/code/game/objects/effects/effect_system/effects_sparks.dm b/code/game/objects/effects/effect_system/effects_sparks.dm index 8a40659a003a..aaf1efa3fbab 100644 --- a/code/game/objects/effects/effect_system/effects_sparks.dm +++ b/code/game/objects/effects/effect_system/effects_sparks.dm @@ -18,11 +18,20 @@ anchored = TRUE light_system = OVERLAY_LIGHT light_range = 1.5 - light_power = 0.8 + light_power = 2 light_color = LIGHT_COLOR_FIRE + /// Should this spark's effect be animated + var/animated = TRUE + /// Timer id for the timer that will wipe us out + var/delete_timer_id = TIMER_ID_NULL + /// Middleman object we're using to animate our light + var/datum/light_middleman/middleman /obj/effect/particle_effect/sparks/Initialize(mapload) ..() + if(animated) + middleman = new(src, "sparks") + middleman.being_overriding_light() return INITIALIZE_HINT_LATELOAD /obj/effect/particle_effect/sparks/LateInitialize() @@ -32,7 +41,29 @@ var/turf/location = loc if(isturf(location)) affect_location(location, just_initialized = TRUE) - QDEL_IN(src, 2 SECONDS) + decay_in(2 SECONDS) + +/obj/effect/particle_effect/sparks/Destroy() + if(!isnull(middleman)) + QDEL_NULL(middleman) + return ..() + +/// Sets up our death effects given the passed in duration +/obj/effect/particle_effect/sparks/proc/decay_in(decay_time) + if(delete_timer_id != TIMER_ID_NULL) + deltimer(delete_timer_id) + delete_timer_id = QDEL_IN_STOPPABLE(src, decay_time + world.tick_lag) + if(!animated) + return + var/obj/effect/abstract/main_light = middleman.primary_intercept + // We're going to fade our light out so it's less jarring when we fully disappear + // Note, a refresh of the overlay light would break this, we're basically just sorta assuming that won't happen + // Would need to track time and sort of "replay" where we should be otherwise + if(decay_time >= 0.7 SECONDS) // duration of all animated spark's actual icon state animation + animate(main_light, alpha = 220, time = 0.4 SECONDS) + animate(alpha = 0, time = decay_time - 0.4 SECONDS, easing = CIRCULAR_EASING | EASE_IN) + else + animate(main_light, alpha = 0, time = decay_time) /obj/effect/particle_effect/sparks/Destroy() var/turf/location = loc @@ -91,6 +122,20 @@ /datum/effect_system/basic/spark_spread effect_type = /obj/effect/particle_effect/sparks + step_delay = 0.35 SECONDS // chosen so we will always take at least the duration of our animation to finish + +/datum/effect_system/basic/spark_spread/generate_effect() + var/obj/effect/particle_effect/sparks/spark = ..() + spark.decay_in(last_loop_length) + +/datum/effect_system/basic/spark_spread/get_step_count() + return rand(2, 3) // never 1 cause 1 looks dumb + +/datum/effect_system/basic/spark_spread/move_failed(datum/move_loop/loop, obj/effect/failed) + if(QDELETED(failed)) + return + var/obj/effect/particle_effect/sparks/spark = failed + spark.decay_in(0.1 SECONDS) /datum/effect_system/basic/spark_spread/quantum effect_type = /obj/effect/particle_effect/sparks/quantum @@ -100,6 +145,8 @@ /obj/effect/particle_effect/sparks/electricity name = "lightning" icon_state = "electricity" + animated = FALSE /datum/effect_system/basic/lightning_spread + delete_on_stop = TRUE effect_type = /obj/effect/particle_effect/sparks/electricity diff --git a/code/game/objects/effects/effect_system/effects_water.dm b/code/game/objects/effects/effect_system/effects_water.dm index f8f62c13b1b8..cbe4472614fb 100644 --- a/code/game/objects/effects/effect_system/effects_water.dm +++ b/code/game/objects/effects/effect_system/effects_water.dm @@ -39,7 +39,7 @@ /// Starts the effect moving at a target with a delay in deciseconds, and a lifetime in moves /// Returns the created loop /obj/effect/particle_effect/water/extinguisher/proc/move_at(atom/target, delay, lifetime) - var/datum/move_loop/loop = GLOB.move_manager.move_towards_legacy(src, target, delay, timeout = delay * lifetime, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move_towards_legacy(src, target, delay, timeout = delay * lifetime, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_forcemove)) RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(movement_stopped)) return loop @@ -88,4 +88,5 @@ QDEL_IN(src, 2 SECONDS) /datum/effect_system/basic/steam_spread + delete_on_stop = TRUE effect_type = /obj/effect/particle_effect/steam diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index ec0ff3340cd8..21df3b187f2c 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1515,6 +1515,7 @@ if(!istype(loc, /turf)) return source = loc + SEND_SIGNAL(src, COMSIG_ITEM_BEFORE_PICKUP_ANIMATION) var/image/pickup_animation = image(icon = src) SET_PLANE(pickup_animation, GAME_PLANE, source) pickup_animation.transform.Scale(0.75) @@ -1551,6 +1552,7 @@ if(!istype(moving_from)) return + SEND_SIGNAL(src, COMSIG_ITEM_BEFORE_DROP_ANIMATION) var/turf/current_turf = get_turf(src) var/direction = get_dir(moving_from, current_turf) var/from_x = moving_from.base_pixel_x diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index f5a29b9f4778..73e774205b5d 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -465,9 +465,15 @@ /datum/material/plasma = SMALL_MATERIAL_AMOUNT * 0.5, /datum/material/plastic = SMALL_MATERIAL_AMOUNT * 0.5, ) + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/item/flashlight/flare/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() if(randomize_fuel) fuel = rand(10 MINUTES, 15 MINUTES) if(light_on) @@ -486,6 +492,8 @@ /obj/item/flashlight/flare/Destroy() STOP_PROCESSING(SSobj, src) + if(middleman) + QDEL_NULL(middleman) return ..() /obj/item/flashlight/flare/afterattack(atom/target, mob/user, click_parameters) @@ -519,6 +527,10 @@ damtype = initial(damtype) update_brightness() +/obj/item/flashlight/flare/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/item/flashlight/flare/extinguish() . = ..() if((fuel != INFINITY) && can_be_extinguished) diff --git a/code/game/objects/items/lighter.dm b/code/game/objects/items/lighter.dm index 60a7bf5271e6..77b373617edd 100644 --- a/code/game/objects/items/lighter.dm +++ b/code/game/objects/items/lighter.dm @@ -38,9 +38,15 @@ ) /// Whether the lighter starts with fuel var/spawns_with_reagent = TRUE + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/item/lighter/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() create_reagents(maximum_fuel, REFILLABLE | DRAINABLE) if(spawns_with_reagent) reagents.add_reagent(/datum/reagent/fuel, maximum_fuel) @@ -54,6 +60,11 @@ ) update_appearance() +/obj/item/lighter/Destroy(force) + if(!isnull(middleman)) + QDEL_NULL(middleman) + return ..() + /obj/item/lighter/grind_results() return list(/datum/reagent/iron = 1, /datum/reagent/fuel = 5, /datum/reagent/fuel/oil = 5) @@ -64,6 +75,10 @@ else . += span_notice("It contains [get_fuel()] units of fuel out of [maximum_fuel].") +/obj/item/lighter/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /// Destroy the lighter when it's shot by a bullet /obj/item/lighter/proc/on_intercepted_bullet(mob/living/victim, obj/projectile/bullet) victim.visible_message(span_warning("\The [bullet] shatters on [victim]'s lighter!")) diff --git a/code/game/objects/items/tools/engineering/weldingtool.dm b/code/game/objects/items/tools/engineering/weldingtool.dm index a82218e845d5..68e6020b9198 100644 --- a/code/game/objects/items/tools/engineering/weldingtool.dm +++ b/code/game/objects/items/tools/engineering/weldingtool.dm @@ -48,6 +48,8 @@ var/activation_sound = 'sound/items/tools/welderactivate.ogg' var/deactivation_sound = 'sound/items/tools/welderdeactivate.ogg' + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /datum/armor/item_weldingtool fire = 100 @@ -55,6 +57,10 @@ /obj/item/weldingtool/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() AddElement(/datum/element/update_icon_updates_onmob) AddElement(/datum/element/tool_flash, light_range) AddElement(/datum/element/falling_hazard, damage = force, wound_bonus = wound_bonus, hardhat_safety = TRUE, crushes = FALSE, impact_sound = hitsound) @@ -64,6 +70,10 @@ reagents.add_reagent(/datum/reagent/fuel, max_fuel) update_appearance() +/obj/item/weldingtool/Destroy(force) + QDEL_NULL(middleman) + return ..() + /obj/item/weldingtool/update_icon_state() if(welding) inhand_icon_state = "[initial(inhand_icon_state)]1" @@ -145,6 +155,10 @@ return try_heal_loop(interacting_with, user) +/obj/item/weldingtool/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/item/weldingtool/proc/try_heal_loop(atom/interacting_with, mob/living/user, repeating = FALSE) var/mob/living/carbon/human/attacked_humanoid = interacting_with var/obj/item/bodypart/affecting = attacked_humanoid.get_bodypart(check_zone(user.zone_selected)) diff --git a/code/game/objects/items/tools/extinguisher.dm b/code/game/objects/items/tools/extinguisher.dm index 9f58808a7523..689d064c0318 100644 --- a/code/game/objects/items/tools/extinguisher.dm +++ b/code/game/objects/items/tools/extinguisher.dm @@ -343,7 +343,7 @@ //Chair movement loop /obj/item/extinguisher/proc/move_chair(obj/buckled_object, movementdirection) - var/datum/move_loop/loop = GLOB.move_manager.move(buckled_object, movementdirection, 1, timeout = 9, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move(buckled_object, movementdirection, 1, timeout = 9, flags = MOVEMENT_LOOP_START_INSTANT, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) //This means the chair slowing down is dependant on the extinguisher existing, which is weird //Couldn't figure out a better way though RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(manage_chair_speed)) diff --git a/code/game/objects/structures/life_candle.dm b/code/game/objects/structures/life_candle.dm index d9eb81c783c3..b21d14619dbc 100644 --- a/code/game/objects/structures/life_candle.dm +++ b/code/game/objects/structures/life_candle.dm @@ -3,7 +3,11 @@ desc = "You are dead. Insert quarter to continue." icon = 'icons/obj/candle.dmi' icon_state = "candle1" + light_system = OVERLAY_LIGHT light_color = LIGHT_COLOR_FIRE + light_power = 1.5 + light_range = 2 + light_on = FALSE var/icon_state_active = "candle1_lit" var/icon_state_inactive = "candle1" @@ -23,11 +27,21 @@ // How long until we respawn them after their death. var/respawn_time = 50 var/respawn_sound = 'sound/effects/magic/staff_animation.ogg' + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/structure/life_candle/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() AddElement(/datum/element/movetype_handler) +/obj/structure/life_candle/Destroy(force) + QDEL_NULL(middleman) + return ..() + /obj/structure/life_candle/attack_hand(mob/user, list/modifiers) . = ..() if(.) @@ -48,10 +62,10 @@ update_appearance() if(linked_minds.len) START_PROCESSING(SSobj, src) - set_light(lit_luminosity) + set_light_on(TRUE) else STOP_PROCESSING(SSobj, src) - set_light(0) + set_light_on(FALSE) /obj/structure/life_candle/update_icon_state() icon_state = linked_minds.len ? icon_state_active : icon_state_inactive @@ -74,6 +88,10 @@ if(!mind.current || (mind.current && mind.current.stat == DEAD)) addtimer(CALLBACK(src, PROC_REF(respawn), mind), respawn_time, TIMER_UNIQUE) +/obj/structure/life_candle/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/structure/life_candle/proc/respawn(datum/mind/mind) var/turf/T = get_turf(src) var/mob/living/body diff --git a/code/game/world.dm b/code/game/world.dm index e2370fedb1fa..a243dd09e383 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -276,6 +276,12 @@ GLOBAL_VAR(restart_counter) world.log = file("[GLOB.log_directory]/dd.log") //not all runtimes trigger world/Error, so this is the only way to ensure we can see all of them. #endif +/// The world.time we last ran maptick, used for stupid reasons +GLOBAL_VAR_INIT(last_maptick_time, 0) +/world/Tick() + // We need a hook for if maptick has happen yet + GLOB.last_maptick_time = world.time + /world/Topic(T, addr, master, key) TGS_TOPIC //redirect to server tools if necessary diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm index 9d9a5a6292f5..1c713f478520 100644 --- a/code/modules/atmospherics/machinery/datum_pipeline.dm +++ b/code/modules/atmospherics/machinery/datum_pipeline.dm @@ -259,7 +259,7 @@ var/volume_sum = 0 var/static/process_id = 0 - process_id = (process_id + 1) % (SHORT_REAL_LIMIT - 1) + process_id = WRAP_UID(process_id + 1) for(var/datum/gas_mixture/gas_mixture as anything in gas_mixture_list) // Ensure we never walk the same mix twice diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm index 622664816594..50e6d0ac22ae 100644 --- a/code/modules/library/lib_machines.dm +++ b/code/modules/library/lib_machines.dm @@ -13,7 +13,7 @@ GLOBAL_VAR_INIT(library_table_modified, 0) /// Increments every time WE update the library db table, causes all existing consoles to repull when they next check /proc/library_updated() - GLOB.library_table_modified = (GLOB.library_table_modified + 1) % (SHORT_REAL_LIMIT - 1) + GLOB.library_table_modified = WRAP_UID(GLOB.library_table_modified + 1) /* * Library Public Computer diff --git a/code/modules/lighting/light_middleman.dm b/code/modules/lighting/light_middleman.dm new file mode 100644 index 000000000000..229c49a4edf4 --- /dev/null +++ b/code/modules/lighting/light_middleman.dm @@ -0,0 +1,160 @@ +/// Allows us to intercept overlay lighting's well, light overlays +/// Normally these are static, but by giving them a render source and copying their base appearance +/// Animating this datum's child objects allows us to do SO much fun stuff +/datum/light_middleman + /// Owning parent we're interceeding for + /// Could in theory be a turf but lies to areas means we have to pick something to type it as + var/atom/movable/parent + /// The holder we are currently displaying our light on + var/atom/movable/light_holder + /// Holds the primary light source + var/obj/effect/abstract/light_middleman/primary_intercept + /// Exists to hold the cone so children can modify it if they want + var/obj/effect/abstract/light_middleman/cone_intercept + /// Are we overriding the light already? + var/overriding = FALSE + /// Weakref to the object we are displaying our effects on + var/datum/weakref/holder_ref + +/datum/light_middleman/New(atom/parent, unique_string) + . = ..() + if(!IS_OVERLAY_LIGHT_SYSTEM(parent.light_system)) + stack_trace("Attempted to create a light middleman with a parent [parent.type] that does not use overlay lighting! This will not work.") + if(isturf(parent)) + stack_trace("Warning, becuase overlay lights are basically never used on turfs, since they don't move,\ + vis contents replacement has not yet been implemented for them (see changeturf for why this is needed)!") + src.parent = parent + primary_intercept = new() + cone_intercept = new() + var/static/uuid = 0 + uuid = WRAP_UID(uuid + 1) + primary_intercept.render_target = "*[unique_string]_[uuid]_target" + cone_intercept.render_target = "[primary_intercept.render_target]_cone" // made to mirror how overlay lights work + +/datum/light_middleman/Destroy(force) + stop_overriding_light() + QDEL_NULL(primary_intercept) + QDEL_NULL(cone_intercept) + parent = null + light_holder = null + return ..() + +/datum/light_middleman/proc/being_overriding_light(unique_string) + if(overriding) + return + overriding = TRUE + // We register here because our later set render source will always trigger a refresh and thus let us capture appearances properly + // Assuming there's an overlay light on the other side + RegisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED, PROC_REF(light_applied)) + RegisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED, PROC_REF(light_removed)) + parent.set_light_render_source(primary_intercept.render_target) + +/datum/light_middleman/proc/stop_overriding_light() + if(!overriding) + return + overriding = FALSE + UnregisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED) + UnregisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED) + var/atom/movable/old_holder = holder_ref?.resolve() + if(old_holder) + old_holder.vis_contents -= primary_intercept + old_holder.vis_contents -= cone_intercept + holder_ref = null + parent.set_light_render_source("") + +/datum/light_middleman/proc/light_applied(datum/source, image/visible_mask, image/cone, atom/movable/light_holder) + SIGNAL_HANDLER + var/atom/movable/old_holder = holder_ref?.resolve() + // If we were somewhere before, clean us out + if(old_holder) + old_holder.vis_contents -= primary_intercept + old_holder.vis_contents -= cone_intercept + holder_ref = null + + // how we make sure we're in the client's view + light_holder.vis_contents += primary_intercept + // Avoids unneeded effects clientside + if(IS_OVERLAY_CONE_LIGHT_SYSTEM(parent.light_system)) + light_holder.vis_contents += cone_intercept + + old_holder = WEAKREF(light_holder) + + var/old_target = primary_intercept.render_target + var/old_cone_target = cone_intercept.render_target + // This will halt any animations we have ongoing so if you care about that you've gotta react to it properly + primary_intercept.appearance = visible_mask + cone_intercept.appearance = cone + // set ourselves up to render back onto the visible mask + primary_intercept.render_source = "" + primary_intercept.render_target = old_target + cone_intercept.render_source = "" + cone_intercept.render_target = old_cone_target + // Dir is important I'm told + primary_intercept.vis_flags |= VIS_INHERIT_DIR + cone_intercept.vis_flags |= VIS_INHERIT_DIR + // Will double apply, here we go gang + primary_intercept.transform = null + cone_intercept.transform = null + primary_intercept.color = null + cone_intercept.color = null + primary_intercept.alpha = 255 + cone_intercept.alpha = 255 + // Sometimes can be BLEND_SUBTRACT, we don't want that + primary_intercept.blend_mode = BLEND_ADD + cone_intercept.blend_mode = BLEND_ADD + /// Allows users to hook into a refresh so they can remake their modifications to our intercepts + SEND_SIGNAL(src, COMSIG_LIGHT_MIDDLEMAN_UPDATED) + +/datum/light_middleman/proc/light_removed(datum/source, atom/movable/light_holder) + SIGNAL_HANDLER + light_holder.vis_contents -= primary_intercept + light_holder.vis_contents -= cone_intercept + holder_ref = null + +/// Just... cause it's better then not having a bespoke type +/obj/effect/abstract/light_middleman + +// Procs for reuse on multiple types +/proc/fire_flicker_middleman(datum/light_middleman/middleman) + var/obj/effect/abstract/main_light = middleman.primary_intercept + // Just in case a subtype is wildin + var/obj/effect/abstract/cone_light = middleman.cone_intercept + + /// Applies a nice random flicker to flares and their subtypes which will I hope sell the fire effect better + var/list/random_times = list() + for(var/i in 1 to 17) + // Makes a nice upside down U distribution + var/random_down = LERP(-0.075 SECONDS, 0.075 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + var/random_bottom = LERP(-0.05 SECONDS, 0.05 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + var/random_up = LERP(-0.075 SECONDS, 0.075 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + // We want a potentially quite long "top end" so the flicker can be an actual flicker instead of a heartbeat (that's the goal at least) + var/random_top = LERP(-0.225 SECONDS, 0.225 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + random_times += list(list( + 0.125 SECONDS + random_down, + 0 SECONDS + random_bottom, + 0.125 SECONDS + random_up, + 0.275 SECONDS + random_top, + )) + // Going to loop our alpha high and low semi quickly to mimik a flickering fire/flare + var/list/first_time = random_times[1] + animate(main_light, alpha = 235, time = first_time[1], easing = CUBIC_EASING|EASE_OUT, loop = -1) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + for(var/list/time in random_times - first_time) + animate(alpha = 235, time = time[1], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + + // I'd really love to do both these in the same loop but parallel animations are the devil from the bible + // I don't think any of these are directional but just in case + animate(cone_light, alpha = 235, time = first_time[1], easing = CUBIC_EASING|EASE_OUT, loop = -1) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + for(var/list/time in random_times - first_time) + animate(alpha = 235, time = time[1], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) diff --git a/code/modules/lighting/lighting_atom.dm b/code/modules/lighting/lighting_atom.dm index b671de56cfcd..fef97f729426 100644 --- a/code/modules/lighting/lighting_atom.dm +++ b/code/modules/lighting/lighting_atom.dm @@ -139,6 +139,43 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .) return . +/// Setter for whether or not this atom's light is on. +/atom/proc/set_light_on(new_value) + if(new_value == light_on || light_flags & LIGHT_FROZEN) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_ON, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_on + light_on = new_value + SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .) + return . + +/// Setter for the light flags of this atom. +/atom/proc/set_light_flags(new_value) + if(new_value == light_flags || (light_flags & LIGHT_FROZEN && new_value & LIGHT_FROZEN)) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_FLAGS, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_flags + light_flags = new_value + SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .) + return . + +// procs that only apply to OVERLAY_LIGHT + +/// Setter for an optional render_source to apply to this atom's light overlay +/atom/proc/set_light_render_source(new_source) + if(new_source == light_flags || light_flags & LIGHT_FROZEN) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, new_source) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_render_source + light_render_source = new_source + SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, .) + return . + +// procs that only apply to COMPLEX_LIGHT + /// Setter for the light angle of this atom /atom/proc/set_light_angle(new_value) if(new_value == light_angle || light_flags & LIGHT_FROZEN) @@ -162,17 +199,6 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_DIR, .) return . -/// Setter for whether or not this atom's light is on. -/atom/proc/set_light_on(new_value) - if(new_value == light_on || light_flags & LIGHT_FROZEN) - return - if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_ON, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) - return - . = light_on - light_on = new_value - SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .) - return . - /// Setter for the height of our light /atom/proc/set_light_height(new_value) if(new_value == light_height || light_flags & LIGHT_FROZEN) @@ -184,16 +210,6 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_HEIGHT, .) return . -/// Setter for the light flags of this atom. -/atom/proc/set_light_flags(new_value) - if(new_value == light_flags || (light_flags & LIGHT_FROZEN && new_value & LIGHT_FROZEN)) - return - if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_FLAGS, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) - return - . = light_flags - light_flags = new_value - SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .) - return . /atom/proc/get_light_offset() return list(0, 0) diff --git a/code/modules/mining/mine_items.dm b/code/modules/mining/mine_items.dm index ea0345f78d02..b23f7b9ba9af 100644 --- a/code/modules/mining/mine_items.dm +++ b/code/modules/mining/mine_items.dm @@ -411,7 +411,7 @@ return setDir(movedir) - var/datum/move_loop/loop = GLOB.move_manager.move(src, dir, delay = calculate_delay(), subsystem = SSconveyors, flags = MOVEMENT_LOOP_START_FAST|MOVEMENT_LOOP_IGNORE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move(src, dir, delay = calculate_delay(), subsystem = SSconveyors, flags = MOVEMENT_LOOP_START_INSTANT|MOVEMENT_LOOP_IGNORE_PRIORITY) RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(check_rail)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(decay_momentum)) diff --git a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm index 3a878ad66099..6b4e4e77ffef 100644 --- a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm +++ b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm @@ -127,7 +127,7 @@ /obj/item/mecha_parts/mecha_equipment/gravcatapult/proc/do_scatter(atom/movable/scatter, atom/movable/target) var/dist = 5 - get_dist(scatter, target) var/delay = 2 - GLOB.move_manager.move_away(scatter, target, delay = delay, timeout = delay * dist, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + GLOB.move_manager.move_away(scatter, target, delay = delay, timeout = delay * dist, flags = MOVEMENT_LOOP_START_INSTANT, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) /obj/item/mecha_parts/mecha_equipment/gravcatapult/get_snowflake_data() return list( diff --git a/tgstation.dme b/tgstation.dme index 67e316b99009..b28377361c57 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -352,6 +352,7 @@ #include "code\__DEFINES\dcs\signals\signals_leash.dm" #include "code\__DEFINES\dcs\signals\signals_lift.dm" #include "code\__DEFINES\dcs\signals\signals_light_eater.dm" +#include "code\__DEFINES\dcs\signals\signals_light_intercept.dm" #include "code\__DEFINES\dcs\signals\signals_lockable_storage.dm" #include "code\__DEFINES\dcs\signals\signals_market.dm" #include "code\__DEFINES\dcs\signals\signals_material_container.dm" @@ -4826,6 +4827,7 @@ #include "code\modules\library\skill_learning\job_skillchips\research_director.dm" #include "code\modules\library\skill_learning\job_skillchips\roboticist.dm" #include "code\modules\library\skill_learning\job_skillchips\station_engineer.dm" +#include "code\modules\lighting\light_middleman.dm" #include "code\modules\lighting\lighting_area.dm" #include "code\modules\lighting\lighting_atom.dm" #include "code\modules\lighting\lighting_corner.dm" From d2cbf35f4362534c7f844679c7581bb01c049c5c Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:49:18 +0000 Subject: [PATCH 088/155] Automatic changelog for PR #95362 [ci skip] --- html/changelogs/AutoChangeLog-pr-95362.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95362.yml diff --git a/html/changelogs/AutoChangeLog-pr-95362.yml b/html/changelogs/AutoChangeLog-pr-95362.yml new file mode 100644 index 000000000000..603ea4bde5d9 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95362.yml @@ -0,0 +1,7 @@ +author: "LemonInTheDark" +delete-after: True +changes: + - rscadd: "Reworks how sparks render. They're now a bit brighter, will fade out as they move/if they hit something, will stack with each other less and also won't start hang in the air on spawn." + - rscadd: "Added a flickering effect to lighters, welding tools, flares, torches and candles (since they're flames)." + - bugfix: "Overlay based lights (think flashlights) will no longer flash to double intensity while being picked up." + - refactor: "Reworked how some effects (explosion particles, sparks, some reagent stuff) function, report any bugs!" \ No newline at end of file From 2e38f91271e5989653f000e94d6906e7dca7e091 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 06:00:24 +0000 Subject: [PATCH 089/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95252.yml | 5 ----- html/changelogs/AutoChangeLog-pr-95362.yml | 7 ------- html/changelogs/AutoChangeLog-pr-95382.yml | 6 ------ html/changelogs/archive/2026-03.yml | 21 +++++++++++++++++++++ 4 files changed, 21 insertions(+), 18 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95252.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95362.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95382.yml diff --git a/html/changelogs/AutoChangeLog-pr-95252.yml b/html/changelogs/AutoChangeLog-pr-95252.yml deleted file mode 100644 index d22eff3b281a..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95252.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscdel: "\"Add prosthetic limb\" surgical operation has been reverted to be a bit closer to how it used to work - you operate on the missing limb / limb stump, rather than on the chest." - - refactor: "Missing limbs are now represented as limb stumps. In practice this should change nothing (for now), as no features were rewritten to make use of these besides surgery. Please report any oddities with missing limbs, however." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95362.yml b/html/changelogs/AutoChangeLog-pr-95362.yml deleted file mode 100644 index 603ea4bde5d9..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95362.yml +++ /dev/null @@ -1,7 +0,0 @@ -author: "LemonInTheDark" -delete-after: True -changes: - - rscadd: "Reworks how sparks render. They're now a bit brighter, will fade out as they move/if they hit something, will stack with each other less and also won't start hang in the air on spawn." - - rscadd: "Added a flickering effect to lighters, welding tools, flares, torches and candles (since they're flames)." - - bugfix: "Overlay based lights (think flashlights) will no longer flash to double intensity while being picked up." - - refactor: "Reworked how some effects (explosion particles, sparks, some reagent stuff) function, report any bugs!" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95382.yml b/html/changelogs/AutoChangeLog-pr-95382.yml deleted file mode 100644 index ad8b4de3df87..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95382.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "LemonInTheDark" -delete-after: True -changes: - - rscadd: "Added an option for rendering space parallax with old space sprites (the ones from before we invented parallax), they're animated and I feel quite pretty." - - bugfix: "Space parallax should hopefully behave a little more consistently now" - - refactor: "Rewrote a lot of how space parallax handled itself, please yell at me if any bugs make themselves known" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index a6525ab3e8ce..71a7c5ee5fd4 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -296,9 +296,30 @@ who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone. 2026-03-20: + LemonInTheDark: + - rscadd: Added an option for rendering space parallax with old space sprites (the + ones from before we invented parallax), they're animated and I feel quite pretty. + - bugfix: Space parallax should hopefully behave a little more consistently now + - refactor: Rewrote a lot of how space parallax handled itself, please yell at me + if any bugs make themselves known + - rscadd: Reworks how sparks render. They're now a bit brighter, will fade out as + they move/if they hit something, will stack with each other less and also won't + start hang in the air on spawn. + - rscadd: Added a flickering effect to lighters, welding tools, flares, torches + and candles (since they're flames). + - bugfix: Overlay based lights (think flashlights) will no longer flash to double + intensity while being picked up. + - refactor: Reworked how some effects (explosion particles, sparks, some reagent + stuff) function, report any bugs! Melbert: - rscadd: Relics found from cracking boulders now have a (mostly) unique set of mining and cultish related effects. - rscadd: Relics found in gift boxes no longer need to be discovered first in science. + - rscdel: '"Add prosthetic limb" surgical operation has been reverted to be a bit + closer to how it used to work - you operate on the missing limb / limb stump, + rather than on the chest.' + - refactor: Missing limbs are now represented as limb stumps. In practice this should + change nothing (for now), as no features were rewritten to make use of these + besides surgery. Please report any oddities with missing limbs, however. levels0: - bugfix: webbing production webs no longer harm their maker From 17e1f6939f6cf3e506367f0799f937ee521a98af Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:16:08 +0530 Subject: [PATCH 090/155] Fixes floor mounts not attaching to plating (#95319) ## About The Pull Request - Fixes https://github.com/tgstation/tgstation/pull/94993#issuecomment-3797322473 - Partially addresses #95310. That is deconstructing the floor tile of the sink won't destroy it. This behaviour was caused by #95062 which made sinks mount to floors & not walls ## Changelog :cl: fix: you can pry out/deconstruct floor tiles/reinforced tiles etc without destroying any floor lights/sinks mounted on as long as there is plating underneath that turf /:cl: --------- Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com> --- code/datums/components/atom_mounted.dm | 38 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/code/datums/components/atom_mounted.dm b/code/datums/components/atom_mounted.dm index 2d2cea36abff..429f68a4126e 100644 --- a/code/datums/components/atom_mounted.dm +++ b/code/datums/components/atom_mounted.dm @@ -4,12 +4,13 @@ var/atom/hanging_support_atom /datum/component/atom_mounted/Initialize(target_structure) - . = ..() if(!isobj(parent) || !isatom(target_structure)) return COMPONENT_INCOMPATIBLE + . = ..() + hanging_support_atom = target_structure RegisterSignal(hanging_support_atom, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) - if(isclosedturf(hanging_support_atom)) + if(isturf(hanging_support_atom)) RegisterSignal(hanging_support_atom, COMSIG_TURF_CHANGE, PROC_REF(on_turf_changing)) else RegisterSignal(hanging_support_atom, COMSIG_QDELETING, PROC_REF(on_structure_delete)) @@ -28,6 +29,7 @@ UnregisterSignal(parent, signals) /datum/component/atom_mounted/Destroy(force) + UnregisterSignal(hanging_support_atom, COMSIG_ATOM_EXAMINE) hanging_support_atom = null return ..() @@ -37,25 +39,44 @@ /datum/component/atom_mounted/proc/on_examine(datum/source, mob/user, list/examine_list) SIGNAL_HANDLER - if (parent in view(user.client?.view || world.view, user)) + if(parent in view(user.client?.view || world.view, user)) examine_list += span_notice("\The [hanging_support_atom] is currently supporting [span_bold("\the [parent]")]. Deconstruction or excessive damage would cause it to [span_bold("fall to the ground")].") /// When the type of turf changes, if it is changing into a floor we should drop our contents -/datum/component/atom_mounted/proc/on_turf_changing(datum/source, path, new_baseturfs, flags, post_change_callbacks) +/datum/component/atom_mounted/proc/on_turf_changing(turf/source, path, new_baseturfs, flags, post_change_callbacks) SIGNAL_HANDLER - if(ispath(path, /turf/open)) - drop_wallmount() + //if we transforming from open to open turf we can skip deconstruction under some conditions + if(isopenturf(source) && ispath(path, /turf/open)) + var/reload = FALSE + + //we are transforming from plating into anything that isn't space + if(isplatingturf(source) && !ispath(path, /turf/open/space)) + reload = TRUE + //we are transforming into plating turf + else if(ispath(LAZYACCESS(source.baseturfs, length(source.baseturfs)), /turf/open/floor/plating)) + reload = TRUE + + if(reload) + var/obj/target = parent + qdel(src) + post_change_callbacks += CALLBACK(target, TYPE_PROC_REF(/obj, remount)) + return + + drop_wallmount() ///When the atom the object is mounted on is destroyed deconstruct /datum/component/atom_mounted/proc/on_structure_delete(datum/source, force) SIGNAL_HANDLER + PRIVATE_PROC(TRUE) drop_wallmount() /// If we get dragged from our wall (by a singulo for instance) we should deconstruct /datum/component/atom_mounted/proc/on_move(datum/source, atom/old_loc, dir, forced, list/old_locs) SIGNAL_HANDLER + PRIVATE_PROC(TRUE) + // If we're having our lighting messed with we're likely to get dragged about // That shouldn't lead to a decon if(HAS_TRAIT(parent, TRAIT_LIGHTING_DEBUGGED)) @@ -65,6 +86,7 @@ ///Called when the object is about to be shuttle rotated so we have to delete ourself and mount again later /datum/component/atom_mounted/proc/detach(datum/source, newT, rotation, move_mode, moving_dock) SIGNAL_HANDLER + PRIVATE_PROC(TRUE) qdel(src) @@ -175,8 +197,8 @@ obj_flags |= MOUNT_ON_LATE_INITIALIZE return FALSE -///Used to remount an object after shuttle move -/obj/proc/remount(datum/source, oldT) +///Used to remount an object in special cases +/obj/proc/remount() SIGNAL_HANDLER PRIVATE_PROC(TRUE) From a67570485e92bed52b3fc1116f7bc6e89a2135bf Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 06:46:32 +0000 Subject: [PATCH 091/155] Automatic changelog for PR #95319 [ci skip] --- html/changelogs/AutoChangeLog-pr-95319.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95319.yml diff --git a/html/changelogs/AutoChangeLog-pr-95319.yml b/html/changelogs/AutoChangeLog-pr-95319.yml new file mode 100644 index 000000000000..224337148583 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95319.yml @@ -0,0 +1,4 @@ +author: "SyncIt21" +delete-after: True +changes: + - bugfix: "you can pry out/deconstruct floor tiles/reinforced tiles etc without destroying any floor lights/sinks mounted on as long as there is plating underneath that turf" \ No newline at end of file From f05aaca61b60f6cfb0a131be8819267c78df305f Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:32:12 -0700 Subject: [PATCH 092/155] Fixes accidential infinite loop in multiz rendering (#95472) ## About The Pull Request Ok so on the parallax pr I moved a bunch of dumb plane group signal registration to the set home proc. Unfortunately because set_home is run BEFORE update_offset, any created relays will then be offset down X from where they should be. For the master plate, this means #1's relay to transparent plate #0 will instead draw to transparent plate #1 (which of course renders into master plate #1) To fix this I've moved update_offset to BEFORE set_home, so children can hook into it and do things in a sane and normal way. Near as I can tell there's no reason they're ordered as they currently are. I've also added a failsafe check to prevent relay creation before update_offset is called, and attached a CRASH() to such. Interestingly enough this caused crashes on shift right click. That's fun I think --- code/_onclick/hud/rendering/plane_masters/_plane_master.dm | 6 +++++- code/_onclick/hud/rendering/render_plate.dm | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/code/_onclick/hud/rendering/plane_masters/_plane_master.dm b/code/_onclick/hud/rendering/plane_masters/_plane_master.dm index a29a76fa9cee..02d1f0ca3b4d 100644 --- a/code/_onclick/hud/rendering/plane_masters/_plane_master.dm +++ b/code/_onclick/hud/rendering/plane_masters/_plane_master.dm @@ -64,15 +64,18 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/plane_master) /// If this plane master is outside of our visual bounds right now var/is_outside_bounds = FALSE + /// Has this plane master had its offset made concrete? Avoids modifications/uses that are going to immediately break + var/offset_already_updated = FALSE + /atom/movable/screen/plane_master/Initialize(mapload, datum/hud/hud_owner, datum/plane_master_group/home, offset = 0) . = ..() src.offset = offset true_alpha = alpha real_plane = plane + update_offset() if(!set_home(home)) return INITIALIZE_HINT_QDEL - update_offset() if(!documentation && !(istype(src, /atom/movable/screen/plane_master) || istype(src, /atom/movable/screen/plane_master/rendering_plate))) stack_trace("Plane master created without a description. Document how your thing works so people will know in future, and we can display it in the debug menu") if(start_hidden) @@ -109,6 +112,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/plane_master) render_relay_planes[i] = GET_NEW_PLANE(render_relay_planes[i], offset) if(initial(render_target)) render_target = OFFSET_RENDER_TARGET(initial(render_target), offset) + offset_already_updated = TRUE /atom/movable/screen/plane_master/proc/set_alpha(new_alpha) true_alpha = new_alpha diff --git a/code/_onclick/hud/rendering/render_plate.dm b/code/_onclick/hud/rendering/render_plate.dm index 6286dfe30768..e7f7bf88903d 100644 --- a/code/_onclick/hud/rendering/render_plate.dm +++ b/code/_onclick/hud/rendering/render_plate.dm @@ -505,6 +505,8 @@ /atom/movable/screen/plane_master/proc/add_relay_to(target_plane, blend_override, relay_layer, relay_color) if(get_relay_to(target_plane)) return + if(!offset_already_updated) + CRASH("Attempted to draw a render relay before our offset has been applied, this WILL break") render_relay_planes += target_plane var/client/display_lad = home?.our_hud?.mymob?.canon_client var/atom/movable/render_plane_relay/relay = generate_relay_to(target_plane, show_to = display_lad, blend_override = blend_override, relay_layer = relay_layer) From 8d78ca910ae85ce292b18e3ba0fd20e3d28f64c4 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:00:25 +0000 Subject: [PATCH 093/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95319.yml | 4 ---- html/changelogs/archive/2026-03.yml | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95319.yml diff --git a/html/changelogs/AutoChangeLog-pr-95319.yml b/html/changelogs/AutoChangeLog-pr-95319.yml deleted file mode 100644 index 224337148583..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95319.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "SyncIt21" -delete-after: True -changes: - - bugfix: "you can pry out/deconstruct floor tiles/reinforced tiles etc without destroying any floor lights/sinks mounted on as long as there is plating underneath that turf" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 71a7c5ee5fd4..0090eaea0b71 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -321,5 +321,9 @@ - refactor: Missing limbs are now represented as limb stumps. In practice this should change nothing (for now), as no features were rewritten to make use of these besides surgery. Please report any oddities with missing limbs, however. + SyncIt21: + - bugfix: you can pry out/deconstruct floor tiles/reinforced tiles etc without destroying + any floor lights/sinks mounted on as long as there is plating underneath that + turf levels0: - bugfix: webbing production webs no longer harm their maker From 9da13e2ec85bbd9fde86ab4264bc58bd3954ab8e Mon Sep 17 00:00:00 2001 From: Lucy Date: Fri, 20 Mar 2026 13:08:41 -0400 Subject: [PATCH 094/155] Makes the Staff of Storms normal-sized (#95391) ## About The Pull Request Exactly what it says on the tin - the Staff of Storms is now normal sized instead of bulky, and will fit in a backpack. ## Why It's Good For The Game The Staff of Lava fits in a backpack, and it arguably has far more destructive/abuse capability than the Staff of Storms. The main use of the Staff of Storms is dealing with ash storms, and like, I don't want to take up my back slot for that. ## Changelog :cl: balance: The Staff of Storms is now normal-sized, and fits in a backpack. /:cl: --- code/modules/mining/lavaland/mining_loot/megafauna/legion.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm index 9f640102edf4..cb8fa5532718 100644 --- a/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm +++ b/code/modules/mining/lavaland/mining_loot/megafauna/legion.dm @@ -10,7 +10,7 @@ lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi' righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi' slot_flags = ITEM_SLOT_BACK - w_class = WEIGHT_CLASS_BULKY + w_class = WEIGHT_CLASS_NORMAL force = 20 damtype = BURN hitsound = 'sound/items/weapons/taserhit.ogg' From f87bfc2514f65928cfe4f71a29f35e4a99586f6f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:09:00 +0000 Subject: [PATCH 095/155] Automatic changelog for PR #95391 [ci skip] --- html/changelogs/AutoChangeLog-pr-95391.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95391.yml diff --git a/html/changelogs/AutoChangeLog-pr-95391.yml b/html/changelogs/AutoChangeLog-pr-95391.yml new file mode 100644 index 000000000000..17b776b90a33 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95391.yml @@ -0,0 +1,4 @@ +author: "Absolucy" +delete-after: True +changes: + - balance: "The Staff of Storms is now normal-sized, and fits in a backpack." \ No newline at end of file From 96c9c8c47b93ea343b971cdc259ba9091e0a61b7 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:00:32 +0000 Subject: [PATCH 096/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95391.yml | 4 ---- html/changelogs/archive/2026-03.yml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95391.yml diff --git a/html/changelogs/AutoChangeLog-pr-95391.yml b/html/changelogs/AutoChangeLog-pr-95391.yml deleted file mode 100644 index 17b776b90a33..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95391.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Absolucy" -delete-after: True -changes: - - balance: "The Staff of Storms is now normal-sized, and fits in a backpack." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 0090eaea0b71..8b177280af77 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -296,6 +296,8 @@ who are not wealthy. When the vendors awake and start moving, they instead get a 100% chance to deny anyone. 2026-03-20: + Absolucy: + - balance: The Staff of Storms is now normal-sized, and fits in a backpack. LemonInTheDark: - rscadd: Added an option for rendering space parallax with old space sprites (the ones from before we invented parallax), they're animated and I feel quite pretty. From 212bb5ec7eb06cc8eb64c846b6c5034f02b1bf1e Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:57:40 -0400 Subject: [PATCH 097/155] Fixes DNA vault being constructable half-in a wall. (#95346) ## About The Pull Request So, the DNA vault is a 3x3 building, which spawns itself around the machine frame in kind of a awkward pattern. X represents the machine frame, and O represents the built vault's solid tiles: ``` ~X~ ~~~ ~~~ ``` and upon built ``` OOO O~O O~O ``` Right now, building the vault doesn't required specific space, meaning you can spawn it right into walls, hallways, whatever without consideration. So, at the very least, this PR tweak it so that image -> image In doing so, this adds a new proc to machine construction onto circuit boards called `completion_requirements` that can be used to check this before completion. This also adds a small QOL tweak to the debug BRPD by adding cables to the parts list, as well as adding the same pointer arrows to the BSA construction prompt for consistency with this change. ## Why It's Good For The Game Seems like an oversight for the BSA to factor in space requirements where the DNA vault, a similar multi-tile machine to not care in the same way. --- code/game/machinery/machine_frame.dm | 3 +++ .../game/objects/items/circuitboards/circuitboard.dm | 8 ++++++++ .../circuitboards/machines/machine_circuitboards.dm | 12 ++++++++++++ code/modules/research/part_replacer.dm | 1 + code/modules/station_goals/bsa.dm | 10 +++++++++- 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/code/game/machinery/machine_frame.dm b/code/game/machinery/machine_frame.dm index e7be3814a18d..0f88210348c7 100644 --- a/code/game/machinery/machine_frame.dm +++ b/code/game/machinery/machine_frame.dm @@ -445,6 +445,9 @@ user.balloon_alert(user, "missing components!") return FALSE + if(!circuit.completion_requirements(src)) + return FALSE + tool.play_tool_sound(src) // Prevent us from dropping stuff thanks to /Exited var/obj/item/circuitboard/machine/leaving_circuit = circuit diff --git a/code/game/objects/items/circuitboards/circuitboard.dm b/code/game/objects/items/circuitboards/circuitboard.dm index df4d12233213..40e85008cd40 100644 --- a/code/game/objects/items/circuitboards/circuitboard.dm +++ b/code/game/objects/items/circuitboards/circuitboard.dm @@ -64,6 +64,14 @@ /obj/item/circuitboard/proc/configure_machine(obj/machinery/machine) return +/** + * This proc is called during /obj/structure/frame/machine/finalize_construction in case there's anything else that needs to be met before completion. + * Arguments: + * * install_frame - The frame the circuit has been installed into for reference. + */ +/obj/item/circuitboard/proc/completion_requirements(obj/structure/frame/install_frame) + return TRUE + // Circuitboard/machine /*Common Parts: Parts List: Ignitor, Timer, Infra-red laser, Infra-red sensor, t_scanner, Capacitor, Valve, sensor unit, micro-manipulator, console screen, beaker, Microlaser, matter bin, power cells. diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm index 06a363a1b77f..9fb7633daf4a 100644 --- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm +++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm @@ -36,6 +36,18 @@ /datum/stock_part/servo/tier3 = 5, /obj/item/stack/cable_coil = 2) +/obj/item/circuitboard/machine/dna_vault/completion_requirements(obj/structure/frame/install_frame) + var/turf/center = get_turf(install_frame) + var/blocked = FALSE + for(var/turf/potential_turf as anything in CORNER_BLOCK_OFFSET(center, 3, 3, -1, -2)) + if(potential_turf.density) + new /obj/effect/temp_visual/point(potential_turf) + blocked = TRUE + if(blocked) + balloon_alert_to_viewers("no room! (3x3)") + return FALSE + return TRUE + //Engineering /obj/item/circuitboard/machine/announcement_system diff --git a/code/modules/research/part_replacer.dm b/code/modules/research/part_replacer.dm index 2ac29877ead1..b0901cd9b01f 100644 --- a/code/modules/research/part_replacer.dm +++ b/code/modules/research/part_replacer.dm @@ -150,6 +150,7 @@ new /obj/item/stock_parts/micro_laser/quadultra(src) new /obj/item/stock_parts/matter_bin/bluespace(src) new /obj/item/stock_parts/power_store/cell/bluespace(src) + new /obj/item/stack/cable_coil/thirty(src) //used in a cargo crate /obj/item/storage/part_replacer/cargo/PopulateContents() diff --git a/code/modules/station_goals/bsa.dm b/code/modules/station_goals/bsa.dm index df199b5cadfc..3b4bd866e000 100644 --- a/code/modules/station_goals/bsa.dm +++ b/code/modules/station_goals/bsa.dm @@ -106,6 +106,9 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE) if(!has_space()) return "Not enough free space!" +/** + * Proc to check if the BSA has the required 10 x 1 block space to deploy. + */ /obj/machinery/bsa/middle/proc/has_space() var/cannon_dir = get_cannon_direction() var/width = 10 @@ -119,9 +122,14 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE) return FALSE var/turf/base = get_turf(src) + var/blocked = FALSE for(var/turf/T as anything in CORNER_BLOCK_OFFSET(base, width, 3, offset, -1)) if(T.density || isspaceturf(T)) - return FALSE + blocked = TRUE + new /obj/effect/temp_visual/point(T) + if(blocked) + return FALSE + return TRUE /obj/machinery/bsa/middle/proc/get_cannon_direction() From 11bb3d2e926498ac9097ba2eb94d94b63725251f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:58:01 +0000 Subject: [PATCH 098/155] Automatic changelog for PR #95346 [ci skip] --- html/changelogs/AutoChangeLog-pr-95346.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95346.yml diff --git a/html/changelogs/AutoChangeLog-pr-95346.yml b/html/changelogs/AutoChangeLog-pr-95346.yml new file mode 100644 index 000000000000..8437bb536756 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95346.yml @@ -0,0 +1,5 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - bugfix: "the BSA cannot be built half-into a wall." + - qol: "building the DNA vault and BSA points to the tiles blocking it's construction." \ No newline at end of file From f59e7ff822fa88c13a3eab9b7e626f6e55fc8a4a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 06:00:26 +0000 Subject: [PATCH 099/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95346.yml | 5 ----- html/changelogs/archive/2026-03.yml | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95346.yml diff --git a/html/changelogs/AutoChangeLog-pr-95346.yml b/html/changelogs/AutoChangeLog-pr-95346.yml deleted file mode 100644 index 8437bb536756..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95346.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "ArcaneMusic" -delete-after: True -changes: - - bugfix: "the BSA cannot be built half-into a wall." - - qol: "building the DNA vault and BSA points to the tiles blocking it's construction." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 8b177280af77..57905fca0254 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -329,3 +329,7 @@ turf levels0: - bugfix: webbing production webs no longer harm their maker +2026-03-21: + ArcaneMusic: + - bugfix: the BSA cannot be built half-into a wall. + - qol: building the DNA vault and BSA points to the tiles blocking it's construction. From 11ed7844ebea648f021cb59f823df0d3bccaa921 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sat, 21 Mar 2026 07:51:09 -0500 Subject: [PATCH 100/155] Generic surgical operations now have unique radial icons (#95470) --- .../operations/operation_amputation.dm | 2 +- .../surgery/operations/operation_generic.dm | 20 +++++++++--------- .../operations/operation_generic_basic.dm | 4 ++-- .../operations/operation_generic_mechanic.dm | 12 +++++------ .../operations/operation_organ_manip.dm | 4 ++-- icons/hud/surgery_radial.dmi | Bin 0 -> 3119 bytes 6 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 icons/hud/surgery_radial.dmi diff --git a/code/modules/surgery/operations/operation_amputation.dm b/code/modules/surgery/operations/operation_amputation.dm index 100f9f428629..9a0236ea64ea 100644 --- a/code/modules/surgery/operations/operation_amputation.dm +++ b/code/modules/surgery/operations/operation_amputation.dm @@ -27,7 +27,7 @@ return TOOL_SAW /datum/surgery_operation/limb/amputate/get_default_radial_image() - return image(/obj/item/shears) + return image('icons/hud/surgery_radial.dmi', "amputate") /datum/surgery_operation/limb/amputate/state_check(obj/item/bodypart/limb) if(limb.body_zone == BODY_ZONE_CHEST) diff --git a/code/modules/surgery/operations/operation_generic.dm b/code/modules/surgery/operations/operation_generic.dm index fac2fcdc0516..545efa20946c 100644 --- a/code/modules/surgery/operations/operation_generic.dm +++ b/code/modules/surgery/operations/operation_generic.dm @@ -29,7 +29,7 @@ return "Any sharp edged item" /datum/surgery_operation/limb/incise_skin/get_default_radial_image() - return image(/obj/item/scalpel) + return image('icons/hud/surgery_radial.dmi', "make_incision") /datum/surgery_operation/limb/incise_skin/tool_check(obj/item/tool) // Require edged sharpness OR a tool behavior match @@ -119,7 +119,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/retract_skin/get_default_radial_image() - return image(/obj/item/retractor) + return image('icons/hud/surgery_radial.dmi', "retract_skin") /datum/surgery_operation/limb/retract_skin/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( @@ -173,7 +173,7 @@ return "Any heat source" /datum/surgery_operation/limb/close_skin/get_default_radial_image() - return image(/obj/item/cautery) + return image('icons/hud/surgery_radial.dmi', "mend_incision") /datum/surgery_operation/limb/close_skin/all_required_strings() return ..() + list("the limb must have skin") @@ -231,7 +231,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/clamp_bleeders/get_default_radial_image() - return image(/obj/item/hemostat) + return image('icons/hud/surgery_radial.dmi', "clamp_bleeders") /datum/surgery_operation/limb/clamp_bleeders/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( @@ -275,7 +275,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/unclamp_bleeders/get_default_radial_image() - return image(/obj/item/hemostat) + return image('icons/hud/surgery_radial.dmi', "unclamp_bleeders") /datum/surgery_operation/limb/unclamp_bleeders/all_required_strings() return ..() + list("the limb must have blood vessels") @@ -336,7 +336,7 @@ return "Any sharp edged item with decent force" /datum/surgery_operation/limb/saw_bones/get_default_radial_image() - return image(/obj/item/circular_saw) + return image('icons/hud/surgery_radial.dmi', "saw_bones") /datum/surgery_operation/limb/saw_bones/tool_check(obj/item/tool) // Require edged sharpness and sufficient force OR a tool behavior match @@ -390,7 +390,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/fix_bones/get_default_radial_image() - return image(/obj/item/stack/medical/bone_gel) + return image('icons/hud/surgery_radial.dmi', "fix_bones") /datum/surgery_operation/limb/fix_bones/all_required_strings() return ..() + list("the limb must have bones") @@ -438,7 +438,7 @@ return "Any sharp pointed item with decent force" /datum/surgery_operation/limb/drill_bones/get_default_radial_image() - return image(/obj/item/surgicaldrill) + return image('icons/hud/surgery_radial.dmi', "drill_bones") /datum/surgery_operation/limb/drill_bones/tool_check(obj/item/tool) // Require pointy sharpness and sufficient force OR a tool behavior match @@ -467,7 +467,7 @@ /datum/surgery_operation/limb/incise_organs name = "incise organs" - desc = "Make an incision in patient's internal organ tissue to allow for manipulation or repair. \ + desc = "Make an incision in the patient's internal organ tissue to allow for manipulation or repair. \ Causes \"organs cut\" surgical state." required_bodytype = ~BODYTYPE_ROBOTIC operation_flags = OPERATION_NO_PATIENT_REQUIRED @@ -491,7 +491,7 @@ return "Any sharp edged item" /datum/surgery_operation/limb/incise_organs/get_default_radial_image() - return image(/obj/item/scalpel) + return image('icons/hud/surgery_radial.dmi', "incise_organs") /datum/surgery_operation/limb/incise_organs/tool_check(obj/item/tool) // Require edged sharpness OR a tool behavior match. Also saws are a no-go, you'll rip up the organs! diff --git a/code/modules/surgery/operations/operation_generic_basic.dm b/code/modules/surgery/operations/operation_generic_basic.dm index 52dcc9db7710..c3f522f92241 100644 --- a/code/modules/surgery/operations/operation_generic_basic.dm +++ b/code/modules/surgery/operations/operation_generic_basic.dm @@ -28,7 +28,7 @@ return ..() + list("The patient must not have complex anatomy") /datum/surgery_operation/basic/incise_skin/get_default_radial_image() - return image(/obj/item/scalpel) + return image('icons/hud/surgery_radial.dmi', "make_incision") /datum/surgery_operation/basic/incise_skin/state_check(mob/living/patient) return !patient.has_limbs // Only for limbless mobs @@ -91,7 +91,7 @@ return ..() + list("The patient must not have complex anatomy") /datum/surgery_operation/basic/saw_bone/get_default_radial_image() - return image(/obj/item/circular_saw) + return image('icons/hud/surgery_radial.dmi', "mend_incision") /datum/surgery_operation/basic/saw_bone/state_check(mob/living/patient) return !patient.has_limbs // Only for limbless mobs diff --git a/code/modules/surgery/operations/operation_generic_mechanic.dm b/code/modules/surgery/operations/operation_generic_mechanic.dm index 54a97c7348c8..6238598129e4 100644 --- a/code/modules/surgery/operations/operation_generic_mechanic.dm +++ b/code/modules/surgery/operations/operation_generic_mechanic.dm @@ -22,7 +22,7 @@ return "Any sharp item" /datum/surgery_operation/limb/mechanical_incision/get_default_radial_image() - return image(/obj/item/screwdriver) + return image('icons/hud/surgery_radial.dmi', "unscrew_shell") /datum/surgery_operation/limb/mechanical_incision/tool_check(obj/item/tool) // Require any sharpness OR a tool behavior match @@ -60,7 +60,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/mechanical_open/get_default_radial_image() - return image('icons/hud/screen_gen.dmi', "arrow_large_still") + return image('icons/hud/surgery_radial.dmi', "open_hatch") /datum/surgery_operation/limb/mechanical_open/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( @@ -101,7 +101,7 @@ return "Any sharp item" /datum/surgery_operation/limb/mechanical_close/get_default_radial_image() - return image(/obj/item/screwdriver) + return image('icons/hud/surgery_radial.dmi', "screw_shell") /datum/surgery_operation/limb/mechanical_close/tool_check(obj/item/tool) // Require any sharpness OR a tool behavior match @@ -143,7 +143,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/prepare_electronics/get_default_radial_image() - return image(/obj/item/multitool) + return image('icons/hud/surgery_radial.dmi', "prepare_electronics") /datum/surgery_operation/limb/prepare_electronics/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( @@ -177,7 +177,7 @@ allow_stumps = TRUE /datum/surgery_operation/limb/mechanic_unwrench/get_default_radial_image() - return image(/obj/item/wrench) + return image('icons/hud/surgery_radial.dmi', "unwrench_endoskeleton") /datum/surgery_operation/limb/mechanic_unwrench/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( @@ -216,7 +216,7 @@ return ..() + list("the limb must have bones") /datum/surgery_operation/limb/mechanic_wrench/get_default_radial_image() - return image(/obj/item/wrench) + return image('icons/hud/surgery_radial.dmi', "wrench_endoskeleton") /datum/surgery_operation/limb/mechanic_wrench/on_preop(obj/item/bodypart/limb, mob/living/surgeon, obj/item/tool, list/operation_args) display_results( diff --git a/code/modules/surgery/operations/operation_organ_manip.dm b/code/modules/surgery/operations/operation_organ_manip.dm index fb2879b714d9..de04ddfd893e 100644 --- a/code/modules/surgery/operations/operation_organ_manip.dm +++ b/code/modules/surgery/operations/operation_organ_manip.dm @@ -106,7 +106,7 @@ var/datum/radial_menu_choice/option = LAZYACCESS(cached_organ_manipulation_options, "[organ.type]_remove") if(!option) option = new() - option.image = get_generic_limb_radial_image(limb.body_zone) + option.image = image('icons/hud/surgery_radial.dmi', "base") option.image.overlays += add_radial_overlays(organ.type) option.name = "remove [initial(organ.name)]" option.info = "Remove [initial(organ.name)] from the [limb.owner ? "patient" : "limb"]." @@ -120,7 +120,7 @@ var/datum/radial_menu_choice/option = LAZYACCESS(cached_organ_manipulation_options, "[organ.type]_insert") if(!option) option = new() - option.image = get_generic_limb_radial_image(limb.body_zone) + option.image = image('icons/hud/surgery_radial.dmi', "base") option.image.overlays += add_radial_overlays(list(image('icons/hud/screen_gen.dmi', "arrow_large_still"), organ.type)) option.name = "insert [initial(organ.name)]" option.info = "insert [initial(organ.name)] into the [limb.owner ? "patient" : "limb"]." diff --git a/icons/hud/surgery_radial.dmi b/icons/hud/surgery_radial.dmi new file mode 100644 index 0000000000000000000000000000000000000000..8aa49694e065c71d72d6aafbc3262efe24ab7c36 GIT binary patch literal 3119 zcmZWsc{r47AAYAvj!vW8=XtP>_N zQ89=irBY)HV;h>W4zqlt^IhL{ec$=xd7tZjpZk8c`~LlY*PCo>ZLwQKMg#zW-KQ;-+Hpv4I95Dcs}YW_p=MXm-XVTLp?-k@0D#GT{J}8bjg}bD|J6X~!NRHN z3VgfC_yJNJ{Omb1*HSV2sz}c7pqFPx%*GxRTIP?>vy4W(n$o5rr&M%SezIBDUgnu5 z_rK|Eo>lmunCVmaPBLM?wK3_+3kOp%<%lC5aJ!lD-q7%1QR7iQ_DB40ai?-MoqZ-$ zJ-^?}{PM`Y%0H|*?}&FxvLW|aH$20>_|RI3Z~Y0m&#Q5A!~E&YFtsYLlWs62LfDst zW6qjQ?x|V!PISqy<}TREr8(qRr=YMnDTz&m?5@V|QyLWG^ONae9SMJ3?1qH&1(9}Z zU9eb&7@vhGqSYr>0YGH+w3&$`CYO;LbzOok-$~BirBdst9AI^($Rs8Kh&$(Rn{(B^ z(hj7cK)bCaTt}3w#i%$KKJeQ+Sn4#KrJHzw0kx$I*QOTg4oDrnPD`>3F8Ql!ve4wr z$D>Oo+Nb62lw4LRb7b+f!i~Psms=2Pv?aw;b=2_nTlvdN`QP)FTJ>>c*v__;z?uNy zVqh8YTd6H*@42Tt4rZAOS!zw?gj@}2rmbFcUR>YG`KWGz+uA^X@VL#y{QepI`elkL z3^>soaX240tv_%J_P!S$5-0{}?VVZMf%NyUFHJ?_{I?E?rdM!I5~%EkO3iTsBqGv` zUaed`iCTw3$GWo%Frd{)1@`kLJV$yVL~`u$=54)OMaq)yL-?`&S#;~XRq6;)=h0{b zuIo{HklsN;-a}tE$TnUbiLXGS!NkgV&d~axuqCb(_es%<|1)OB|IobUHpIqohiBlm zm5?~zIEWY6v$=O+v{oLu5aY11@A%Scsd@E~k=7$Fe!B6!lU<})%Y%RBwKcXJ3 zdT%Wpf`vG zaEnn>%TOZQ??JBFl;|wnmb8L?Pt{$Sb9E!Y1oorc?YEgG+f< z{P-q2TopAH&po%PNx%Ho5&>T_)SCQb`?Qi%kH5I1_56P11dAz$eaho}rMqCoC?2a) zkCYa(%@<-P8#7U>ZZ&cXKfMjuFAl5C{}FVspCSKSD0p?ZaX>_Q3jj3b>{%TKcLjPx z5Br$vWf*4_eFke!Ha4Jcg7zke_oLf(I{be$CyJzdiAciR{-uhHQ*n&h>7Wct=adf= zdt^Wb(ZeXhB3YfJj6#d%?e9MYA&#yJT@INatnhd!l(qY~?nBg;LHoGv2j5*uqf_@b zcfpRw%?@aELk^N>P87ZKLH^Bom{$UXAEbLhe-a^zJ5vDKY4KFy326A*LZAA zP~xRd(ROMAJLr7hj;_Eo)T~>LC&NA$nk7=tVqPL)Prc`u3fzu$_H3YoRc{akgBehY z)3RY?YdkU#T6I&`zweI21Y-0^$}8GY-?A_rrs(?BzOL>9kru9btJn|HSB*3I?$#owCbjruqZ8)8y#@LNhJ-bsR!jlJsX-Mc`ru3F ze1uPVf#IUA;x^4+1|+dOuJLN+VJTMcIS%P=oOhn@U^Al0nfTnqY#x0wEp3nOP3))f zS<&VjpNMC>>Qa_Lps3~Xx?#l1+h`8)8BPNhbh>ZJt}dW>vd;X#kj9~&5am9|WWO3L zjnmD&It`Yo4Y2Braqb}~k_}d_Nl$?v7Wn;=uC&^nsa^PZd6q(L_~i*J-y4EqK{=qu z{cm0Phm(F;O`v%8<`p`)YJ}Je0jQN&DU=3I!(hq2Gvqz(J^(C64f`ZxU!9vJJ8|n+ zAhqzJ4Uv=-_N>l$%?yW1y1<$5pMggh7}os>v+1V3kmVm;fF z>(jzbT_jgxUms3eI-qNi^tS%Mfp)5CalGrCYrYd3W6ula7UcKo5c!pjIJRv zV7qH&lHDM$McmuHY)r@|?3Mf&N_HT6=-ViTrxu~%dJX!ooX#@r36*>A>1RLj$s7iC zV*on;a9X=ZGa(Ummha^AhSFA!7w0ez3)|d@VRba#SRh(bIY(tDN}n+hhZKEXAud?pXIttd=2*oVqILsVyS<2^;(tDuwrH4p^V?V&O1Z-A2)36#mgCp)EL#F2Te@;l>WnGt=T-s!Fw#} z8>(;WPFQ>|80M*2ciP7}S-gJ6thUIxa_jd}+%vzgHC)O2G4}W!dlB`f#`&)NL`CF; z&(*C&#dJ5GV;NIzKS-iFNpvciFzZa|>>={pdu=*nCVl4cVN&mDp1WhHv1eaM13%c1l`}CLN zno+mAQm{z1Erw^zz5=MP4_Ur2v{G#C8>k!d-b?Tzt`*O{3HM(HF~%Cf|3Od$jOI;7 zOeJArdeLHx;t@9#PVI=A){jv2!xgh;<`+6B)Tn!zndAdNF~HeD>XW2W9% zxpHrAE)R&I-d>uhA8#&Y>yO_4VE^|bdPYWfX38};{3=B3aR}>eLYwc4p z%>ZScL-Hw!f=P{c<>vRjiS@3Ht`z{Kq@;kits4SZ!%ug4w^`R_sN+QxO^u@~#SYcX zs1gFyH9Y*($e-*ULn#UcX|hoZ(~SmhkD*OXp?@(r&Px6)5X7{;VZ}YKk^&*tkIvZ| v!@bG0z%Bw7+3}?Xm?e4rU2(Q0 Date: Sat, 21 Mar 2026 12:51:29 +0000 Subject: [PATCH 101/155] Automatic changelog for PR #95470 [ci skip] --- html/changelogs/AutoChangeLog-pr-95470.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95470.yml diff --git a/html/changelogs/AutoChangeLog-pr-95470.yml b/html/changelogs/AutoChangeLog-pr-95470.yml new file mode 100644 index 000000000000..36882932c56f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95470.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - image: "Generic surgical operations now have unique surgery radial icons" \ No newline at end of file From 8edccd7e5a30ee49f67b9c4e39ce6963305a4478 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:00:29 +0000 Subject: [PATCH 102/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95470.yml | 4 ---- html/changelogs/archive/2026-03.yml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95470.yml diff --git a/html/changelogs/AutoChangeLog-pr-95470.yml b/html/changelogs/AutoChangeLog-pr-95470.yml deleted file mode 100644 index 36882932c56f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95470.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - image: "Generic surgical operations now have unique surgery radial icons" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 57905fca0254..13063993dcff 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -333,3 +333,5 @@ ArcaneMusic: - bugfix: the BSA cannot be built half-into a wall. - qol: building the DNA vault and BSA points to the tiles blocking it's construction. + Melbert: + - image: Generic surgical operations now have unique surgery radial icons From 284e23c5046aa5a13bbfc2dd34fa419111d96632 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:29:54 -0500 Subject: [PATCH 103/155] Vendors in audit log are no longer prefixed with "the" (#95389) ## About The Pull Request Uses `vendor.name` if possible, so it's referred to as "VendingMachine" rather than "The VendingMachine" ## Why It's Good For The Game Reads a bit better with all the entries presented ## Changelog :cl: Melbert qol: Audit log formats vendors without "the" /:cl: --- code/controllers/subsystem/economy.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/controllers/subsystem/economy.dm b/code/controllers/subsystem/economy.dm index dcab6e359edb..750cdfc7d7ce 100644 --- a/code/controllers/subsystem/economy.dm +++ b/code/controllers/subsystem/economy.dm @@ -216,7 +216,7 @@ SUBSYSTEM_DEF(economy) audit_log += list(list( "account" = "[account.account_holder]", "cost" = price_to_use, - "vendor" = "[vendor]", + "vendor" = "[astype(vendor, /atom)?.name || vendor]", "stationtime" = station_time_timestamp("hh:mm"), )) From edf4e604746e2957ccd72290ac1a07b2203cefb7 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:30:23 +0000 Subject: [PATCH 104/155] Automatic changelog for PR #95389 [ci skip] --- html/changelogs/AutoChangeLog-pr-95389.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95389.yml diff --git a/html/changelogs/AutoChangeLog-pr-95389.yml b/html/changelogs/AutoChangeLog-pr-95389.yml new file mode 100644 index 000000000000..447824960eca --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95389.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - qol: "Audit log formats vendors without \"the\"" \ No newline at end of file From 30e2b4cf8112f1f39eb5fef068eb588fb8bcd925 Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:31:43 -0400 Subject: [PATCH 105/155] Monkey Dust no longer can provide permanent stun immunity (#95464) ## About The Pull Request Monkey dust now removes its related traits, including anti-stun, on end metabolize regardless of whether the metabolizing mob is a monkey at the moment or not. It also removes these traits & the martial art at any point the metabolizing mob is not a monkey. It doesn't remove the brain trauma unless the user is a monkey, however. Otherwise, it would become just an actual medicine for the trauma. It still kind of is, but with more effort involved. If only traumas held sources. ## Why It's Good For The Game fixes #91742 fixes #94403 ## Changelog :cl: fix: Monkey dust now properly removes anti-stun properties when its imbiber is no longer a monkey /:cl: --- .../impure_reagents/impure_medicine_reagents.dm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm index ba6ff560c9fb..72cc93916cce 100644 --- a/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/impure_reagents/impure_medicine_reagents.dm @@ -986,6 +986,8 @@ Basically, we fill the time between now and 2s from now with hands based off the if(is_simian(affected_mob)) affected_mob.gain_trauma(/datum/brain_trauma/special/primal_instincts, TRAUMA_RESILIENCE_ABSOLUTE) affected_mob.add_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE, TRAIT_ANALGESIA, TRAIT_STIMULATED), type) + if(jungle_arts) + return jungle_arts = new(src) jungle_arts.locked_to_use = TRUE jungle_arts.teach(affected_mob) @@ -993,10 +995,11 @@ Basically, we fill the time between now and 2s from now with hands based off the /datum/reagent/inverse/bath_salts/on_mob_end_metabolize(mob/living/carbon/affected_mob) . = ..() QDEL_NULL(jungle_arts) + affected_mob.remove_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE, TRAIT_ANALGESIA, TRAIT_STIMULATED), type) + affected_mob.Sleeping(30 SECONDS) if(is_simian(affected_mob)) affected_mob.cure_trauma_type(/datum/brain_trauma/special/primal_instincts, resilience = TRAUMA_RESILIENCE_ABSOLUTE) - affected_mob.remove_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE, TRAIT_ANALGESIA, TRAIT_STIMULATED), type) - affected_mob.Sleeping(30 SECONDS) + /datum/reagent/inverse/bath_salts/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, metabolization_ratio) . = ..() @@ -1013,10 +1016,14 @@ Basically, we fill the time between now and 2s from now with hands based off the if(need_mob_update) . = UPDATE_MOB_HEALTH + return - else if(SPT_PROB(10, seconds_per_tick)) + if(SPT_PROB(10, seconds_per_tick)) affected_mob.emote(pick("screech","scratch","jump","look")) + QDEL_NULL(jungle_arts) + affected_mob.remove_traits(list(TRAIT_STUNIMMUNE, TRAIT_SLEEPIMMUNE, TRAIT_ANALGESIA, TRAIT_STIMULATED), type) + /datum/reagent/inverse/aranesp name = "Epoetin Alfa" description = "Synthetic medication that induces blood regeneration and wound clotting in patients. \ From 2b15cab1cad50cf54f30db5c69bc2fed08bb9270 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:32:04 +0000 Subject: [PATCH 106/155] Automatic changelog for PR #95464 [ci skip] --- html/changelogs/AutoChangeLog-pr-95464.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95464.yml diff --git a/html/changelogs/AutoChangeLog-pr-95464.yml b/html/changelogs/AutoChangeLog-pr-95464.yml new file mode 100644 index 000000000000..96c866a75d37 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95464.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - bugfix: "Monkey dust now properly removes anti-stun properties when its imbiber is no longer a monkey" \ No newline at end of file From 8e6ec316c5ed7dc98d91aab46dea39fe9215e69e Mon Sep 17 00:00:00 2001 From: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:03:38 -0500 Subject: [PATCH 107/155] Removes a double space on to_chat for spray can (#95476) --- code/game/objects/items/crayons.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index 9737e7ba1f54..9655aed9cf65 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -978,7 +978,7 @@ return ITEM_INTERACT_BLOCKING var/obj/machinery/atmospherics/target_pipe = target target_pipe.paint(paint_color) - balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") + balloon_alert(user, "painted in [GLOB.pipe_color_name[paint_color]] color") else if (is_type_in_typecache(target, direct_color_types)) target.add_atom_colour(paint_color, WASHABLE_COLOUR_PRIORITY) else From fbed77f20c24b10a1398a23df6a2a63ce880a66a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:04:04 +0000 Subject: [PATCH 108/155] Automatic changelog for PR #95476 [ci skip] --- html/changelogs/AutoChangeLog-pr-95476.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95476.yml diff --git a/html/changelogs/AutoChangeLog-pr-95476.yml b/html/changelogs/AutoChangeLog-pr-95476.yml new file mode 100644 index 000000000000..8cba0b3148ae --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95476.yml @@ -0,0 +1,4 @@ +author: "FalloutFalcon" +delete-after: True +changes: + - spellcheck: "removed extra space on spraying an atmos machine with spray can" \ No newline at end of file From e4245963ed1af3b406c354054546d9093a8acf7c Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:05:09 -0400 Subject: [PATCH 109/155] Corrects shoving someone into another person sending the "you shoved" message to the person who was shoved into (#95446) --- code/modules/mob/living/carbon/human/human_defense.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 86b5d1383544..e28e3ec6521f 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -139,8 +139,8 @@ if(!HAS_TRAIT(src, TRAIT_BRAWLING_KNOCKDOWN_BLOCKED)) Knockdown(SHOVE_KNOCKDOWN_COLLATERAL, daze_amount = 3 SECONDS) target.visible_message(span_danger("[shover] shoves [target.name] into [name]!"), - span_userdanger("You're shoved into [name] by [shover]!"), span_hear("You hear aggressive shuffling followed by a loud thud!"), COMBAT_MESSAGE_RANGE, src) - to_chat(src, span_danger("You shove [target.name] into [name]!")) + span_userdanger("You're shoved into [name] by [shover]!"), span_hear("You hear aggressive shuffling followed by a loud thud!"), COMBAT_MESSAGE_RANGE, list(shover)) + to_chat(shover, span_danger("You shove [target.name] into [name]!")) log_combat(shover, target, "shoved", addition = "into [name][weapon ? " with [weapon]" : ""]") return COMSIG_LIVING_SHOVE_HANDLED From d2b41c670f87cc2979b866315bc76bf51df49566 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:05:30 +0000 Subject: [PATCH 110/155] Automatic changelog for PR #95446 [ci skip] --- html/changelogs/AutoChangeLog-pr-95446.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95446.yml diff --git a/html/changelogs/AutoChangeLog-pr-95446.yml b/html/changelogs/AutoChangeLog-pr-95446.yml new file mode 100644 index 000000000000..410c8de8d58f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95446.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - spellcheck: "Shoving someone into another person no longer sends the \"you shoved\" message to the person who was shoved into" \ No newline at end of file From f2360d64fd38b256a6f6606c4e9cb402e7958330 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:06:30 -0700 Subject: [PATCH 111/155] Gives subsystem controllers a bitfield def to make vv easier, renames their flags var (#95439) --- code/_globalvars/bitfields.dm | 10 ++++++++++ code/controllers/master.dm | 18 +++++++++--------- code/controllers/subsystem.dm | 16 ++++++++-------- code/controllers/subsystem/achievements.dm | 2 +- code/controllers/subsystem/admin_verbs.dm | 2 +- code/controllers/subsystem/ai_controllers.dm | 2 +- .../subsystem/ai_idle_controllers.dm | 2 +- code/controllers/subsystem/air.dm | 2 +- code/controllers/subsystem/ambience.dm | 2 +- code/controllers/subsystem/area_contents.dm | 2 +- code/controllers/subsystem/asset_loading.dm | 2 +- code/controllers/subsystem/assets.dm | 2 +- code/controllers/subsystem/atoms.dm | 2 +- code/controllers/subsystem/augury.dm | 2 +- code/controllers/subsystem/ban_cache.dm | 2 +- code/controllers/subsystem/blood_drying.dm | 2 +- code/controllers/subsystem/cameras.dm | 2 +- code/controllers/subsystem/chat.dm | 2 +- code/controllers/subsystem/dbcore.dm | 2 +- code/controllers/subsystem/dcs.dm | 2 +- code/controllers/subsystem/disease.dm | 2 +- code/controllers/subsystem/dynamic/dynamic.dm | 2 +- code/controllers/subsystem/early_assets.dm | 2 +- code/controllers/subsystem/explosions.dm | 2 +- code/controllers/subsystem/fluids.dm | 2 +- code/controllers/subsystem/garbage.dm | 2 +- .../subsystem/greyscale_previews.dm | 2 +- code/controllers/subsystem/icon_smooth.dm | 2 +- code/controllers/subsystem/init_profiler.dm | 2 +- code/controllers/subsystem/input.dm | 2 +- code/controllers/subsystem/ipintel.dm | 2 +- code/controllers/subsystem/job.dm | 2 +- code/controllers/subsystem/lag_switch.dm | 2 +- code/controllers/subsystem/library.dm | 2 +- code/controllers/subsystem/lighting.dm | 2 +- code/controllers/subsystem/machines.dm | 2 +- code/controllers/subsystem/map_vote.dm | 2 +- code/controllers/subsystem/mapping.dm | 2 +- code/controllers/subsystem/market.dm | 2 +- code/controllers/subsystem/materials.dm | 2 +- code/controllers/subsystem/minor_mapping.dm | 2 +- code/controllers/subsystem/mobs.dm | 2 +- code/controllers/subsystem/moods.dm | 2 +- code/controllers/subsystem/mouse_entered.dm | 2 +- .../subsystem/movement/ai_movement.dm | 2 +- .../subsystem/movement/cliff_falling.dm | 2 +- .../subsystem/movement/hyperspace_drift.dm | 2 +- .../controllers/subsystem/movement/movement.dm | 2 +- .../subsystem/movement/newtonian_movement.dm | 2 +- .../subsystem/networks/bitrunning.dm | 2 +- .../subsystem/networks/circuit_component.dm | 2 +- .../subsystem/networks/id_access.dm | 2 +- code/controllers/subsystem/networks/radio.dm | 2 +- .../subsystem/networks/wiremod_composite.dm | 2 +- code/controllers/subsystem/npcpool.dm | 2 +- code/controllers/subsystem/overlays.dm | 2 +- code/controllers/subsystem/pai.dm | 2 +- code/controllers/subsystem/parallax.dm | 2 +- .../subsystem/persistence/_persistence.dm | 2 +- .../subsystem/persistent_paintings.dm | 2 +- code/controllers/subsystem/ping.dm | 2 +- .../subsystem/points_of_interest.dm | 2 +- code/controllers/subsystem/polling.dm | 2 +- code/controllers/subsystem/processing/acid.dm | 2 +- .../subsystem/processing/ai_basic_avoidance.dm | 2 +- .../subsystem/processing/ai_behaviors.dm | 2 +- .../subsystem/processing/ai_idle_behaviors.dm | 2 +- .../subsystem/processing/antag_hud.dm | 2 +- code/controllers/subsystem/processing/aura.dm | 2 +- .../subsystem/processing/clock_component.dm | 2 +- .../subsystem/processing/digital_clock.dm | 2 +- .../subsystem/processing/fire_burning.dm | 2 +- .../subsystem/processing/fishing.dm | 2 +- .../subsystem/processing/greyscale.dm | 2 +- .../subsystem/processing/instruments.dm | 2 +- .../subsystem/processing/newplayer.dm | 2 +- code/controllers/subsystem/processing/obj.dm | 2 +- .../subsystem/processing/personality.dm | 2 +- .../subsystem/processing/plumbing.dm | 2 +- .../subsystem/processing/priority_effects.dm | 2 +- .../subsystem/processing/processing.dm | 2 +- .../subsystem/processing/projectiles.dm | 2 +- .../controllers/subsystem/processing/quirks.dm | 2 +- .../subsystem/processing/reagents.dm | 2 +- .../subsystem/processing/station.dm | 2 +- .../subsystem/processing/turrets.dm | 2 +- code/controllers/subsystem/queuelinks.dm | 2 +- code/controllers/subsystem/radiation.dm | 2 +- .../subsystem/radioactive_nebula.dm | 2 +- code/controllers/subsystem/restaurant.dm | 2 +- code/controllers/subsystem/server_maint.dm | 2 +- code/controllers/subsystem/shuttle.dm | 2 +- code/controllers/subsystem/skills.dm | 2 +- code/controllers/subsystem/sounds.dm | 2 +- .../subsystem/sprite_accessories.dm | 2 +- code/controllers/subsystem/statpanel.dm | 2 +- code/controllers/subsystem/stickyban.dm | 2 +- code/controllers/subsystem/tcgsetup.dm | 2 +- code/controllers/subsystem/tgui.dm | 2 +- code/controllers/subsystem/throwing.dm | 2 +- code/controllers/subsystem/ticker.dm | 2 +- code/controllers/subsystem/timer.dm | 2 +- code/controllers/subsystem/title.dm | 2 +- code/controllers/subsystem/traitor.dm | 2 +- code/controllers/subsystem/tts.dm | 2 +- code/controllers/subsystem/tutorials.dm | 2 +- .../subsystem/unplanned_controllers.dm | 2 +- code/controllers/subsystem/verb_manager.dm | 4 ++-- code/controllers/subsystem/vis_overlays.dm | 2 +- code/controllers/subsystem/vote.dm | 2 +- code/controllers/subsystem/wardrobe.dm | 2 +- code/controllers/subsystem/weather.dm | 2 +- code/modules/escape_menu/subsystem.dm | 2 +- code/modules/lootpanel/ss_looting.dm | 2 +- .../research/techweb/__techweb_helpers.dm | 2 +- code/modules/unit_tests/subsystem_init.dm | 4 ++-- 116 files changed, 142 insertions(+), 132 deletions(-) diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index cc01b194bd8d..eb8079bd902a 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -137,6 +137,16 @@ DEFINE_BITFIELD(disease_flags, list( "CURABLE" = CURABLE, )) +DEFINE_BITFIELD(ss_flags, list( + "SS_NO_INIT" = SS_NO_INIT, + "SS_NO_FIRE" = SS_NO_FIRE, + "SS_BACKGROUND" = SS_BACKGROUND, + "SS_TICKER" = SS_TICKER, + "SS_KEEP_TIMING" = SS_KEEP_TIMING, + "SS_POST_FIRE_TIMING" = SS_POST_FIRE_TIMING, + "SS_OK_TO_FAIL_INIT" = SS_OK_TO_FAIL_INIT, +)) + DEFINE_BITFIELD(flags_1, list( "ADMIN_SPAWNED_1" = ADMIN_SPAWNED_1, "ALLOW_DARK_PAINTS_1" = ALLOW_DARK_PAINTS_1, diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 80a0c486fae1..3303eed9c5c1 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -191,7 +191,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie "last_fire" = subsystem.last_fire, "next_fire" = subsystem.next_fire, "can_fire" = subsystem.can_fire, - "doesnt_fire" = !!(subsystem.flags & SS_NO_FIRE), + "doesnt_fire" = !!(subsystem.ss_flags & SS_NO_FIRE), "cost_ms" = subsystem.cost, "tick_usage" = subsystem.tick_usage, "usage_per_tick" = average, @@ -305,7 +305,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie FireHim = TRUE if(3) msg = "The [BadBoy.name] subsystem seems to be destabilizing the MC and will be put offline." - BadBoy.flags |= SS_NO_FIRE + BadBoy.ss_flags |= SS_NO_FIRE if(msg) to_chat(GLOB.admins, span_boldannounce("[msg]")) log_world(msg) @@ -494,7 +494,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie SS_INIT_NO_MESSAGE, ) - if ((subsystem.flags & SS_NO_INIT) || subsystem.initialized) //Don't init SSs with the corresponding flag or if they already are initialized + if ((subsystem.ss_flags & SS_NO_INIT) || subsystem.initialized) //Don't init SSs with the corresponding flag or if they already are initialized subsystem.initialized = TRUE // set initialized to TRUE, because the value of initialized may still be checked on SS_NO_INIT subsystems as an "is this ready" check return @@ -599,7 +599,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie var/timer = world.time for (var/thing in subsystems) var/datum/controller/subsystem/SS = thing - if (SS.flags & SS_NO_FIRE) + if (SS.ss_flags & SS_NO_FIRE) continue if (SS.init_stage > init_stage) continue @@ -607,7 +607,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie SS.queue_next = null SS.queue_prev = null SS.state = SS_IDLE - if ((SS.flags & (SS_TICKER|SS_BACKGROUND)) == SS_TICKER) + if ((SS.ss_flags & (SS_TICKER|SS_BACKGROUND)) == SS_TICKER) tickersubsystems += SS // Timer subsystems aren't allowed to bunch up, so we offset them a bit timer += TICKS2DS(rand(0, 1)) @@ -618,7 +618,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie if(SS.init_stage == init_stage - 1 && (SS.runlevels & current_runlevel)) // Give em a random offset so things don't clump up too bad var/delay = SS.wait - if(SS.flags & SS_TICKER) + if(SS.ss_flags & SS_TICKER) delay = TICKS2DS(delay) // Gotta convert to ticks cause rand needs integers SS.next_fire = world.time + TICKS2DS(rand(0, DS2TICKS(min(delay, 2 SECONDS)))) @@ -720,7 +720,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie continue // If they're new, give em a random offset so things don't clump up too bad var/delay = SS.wait - if(SS.flags & SS_TICKER) + if(SS.ss_flags & SS_TICKER) delay = TICKS2DS(delay) SS.next_fire = world.time + TICKS2DS(rand(0, DS2TICKS(min(delay, 2 SECONDS)))) @@ -813,7 +813,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie continue if (SS.next_fire > world.time) continue - SS_flags = SS.flags + SS_flags = SS.ss_flags if (SS_flags & SS_NO_FIRE) subsystemstocheck -= SS continue @@ -855,7 +855,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie while (queue_node) if (ran && TICK_USAGE > TICK_LIMIT_RUNNING) break - queue_node_flags = queue_node.flags + queue_node_flags = queue_node.ss_flags queue_node_priority = queue_node.queued_priority if (!(queue_node_flags & SS_TICKER) && skip_ticks) diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm index 200c65a4c75c..92a3e70d02f0 100644 --- a/code/controllers/subsystem.dm +++ b/code/controllers/subsystem.dm @@ -34,8 +34,8 @@ /// Priority Weight: When multiple subsystems need to run in the same tick, higher priority subsystems will be given a higher share of the tick before MC_TICK_CHECK triggers a sleep, higher priority subsystems also run before lower priority subsystems var/priority = FIRE_PRIORITY_DEFAULT - /// [Subsystem Flags][SS_NO_INIT] to control binary behavior. Flags must be set at compile time or before preinit finishes to take full effect. (You can also restart the mc to force them to process again) - var/flags = NONE + /// [Subsystem flags][SS_NO_INIT] to control binary behavior. ss_flags must be set at compile time or before preinit finishes to take full effect. (You can also restart the mc to force them to process again) + var/ss_flags = NONE /// Which stage does this subsystem init at. Earlier stages can fire while later stages init. var/init_stage = INITSTAGE_MAIN @@ -161,13 +161,13 @@ ///fire() seems more suitable. This is the procedure that gets called every 'wait' deciseconds. ///Sleeping in here prevents future fires until returned. /datum/controller/subsystem/proc/fire(resumed = FALSE) - flags |= SS_NO_FIRE + ss_flags |= SS_NO_FIRE CRASH("Subsystem [src]([type]) does not fire() but did not set the SS_NO_FIRE flag. Please add the SS_NO_FIRE flag to any subsystem that doesn't fire so it doesn't get added to the processing list and waste cpu.") /datum/controller/subsystem/Destroy() dequeue() can_fire = 0 - flags |= SS_NO_FIRE + ss_flags |= SS_NO_FIRE if (Master) Master.subsystems -= src return ..() @@ -177,7 +177,7 @@ * reset_time (bool) - Ignore things that would normally alter the next fire, like tick_overrun, and last_fire. (also resets postpone) */ /datum/controller/subsystem/proc/update_nextfire(reset_time = FALSE) - var/queue_node_flags = flags + var/queue_node_flags = ss_flags if (reset_time) postponed_fires = 0 @@ -202,14 +202,14 @@ /// (this lets us sort our run order correctly without having to re-sort the entire already sorted list) /datum/controller/subsystem/proc/enqueue() var/SS_priority = priority - var/SS_flags = flags + var/SS_flags = ss_flags var/datum/controller/subsystem/queue_node var/queue_node_priority var/queue_node_flags for (queue_node = Master.queue_head; queue_node; queue_node = queue_node.queue_next) queue_node_priority = queue_node.queued_priority - queue_node_flags = queue_node.flags + queue_node_flags = queue_node.ss_flags if (queue_node_flags & (SS_TICKER|SS_BACKGROUND) == SS_TICKER) if ((SS_flags & (SS_TICKER|SS_BACKGROUND)) != SS_TICKER) @@ -290,7 +290,7 @@ return SS_INIT_NONE /datum/controller/subsystem/stat_entry(msg) - if(can_fire && !(SS_NO_FIRE & flags) && init_stage <= Master.init_stage_completed) + if(can_fire && !(SS_NO_FIRE & ss_flags) && init_stage <= Master.init_stage_completed) msg = "[round(cost,1)]ms|[round(tick_usage,1)]%([round(tick_overrun,1)]%)|[round(ticks,0.1)] [msg]" else msg = "OFFLINE\t[msg]" diff --git a/code/controllers/subsystem/achievements.dm b/code/controllers/subsystem/achievements.dm index 56a85284cd4c..9548de2ce604 100644 --- a/code/controllers/subsystem/achievements.dm +++ b/code/controllers/subsystem/achievements.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(achievements) name = "Achievements" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/achievements_enabled = FALSE ///List of achievements diff --git a/code/controllers/subsystem/admin_verbs.dm b/code/controllers/subsystem/admin_verbs.dm index 732508f0f8a0..9891f13a4cf2 100644 --- a/code/controllers/subsystem/admin_verbs.dm +++ b/code/controllers/subsystem/admin_verbs.dm @@ -2,7 +2,7 @@ GENERAL_PROTECT_DATUM(/datum/controller/subsystem/admin_verbs) SUBSYSTEM_DEF(admin_verbs) name = "Admin Verbs" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE init_stage = INITSTAGE_EARLY /// A list of all admin verbs indexed by their type. var/list/datum/admin_verb/admin_verbs_by_type = list() diff --git a/code/controllers/subsystem/ai_controllers.dm b/code/controllers/subsystem/ai_controllers.dm index 95abc7e8da35..4102806be67f 100644 --- a/code/controllers/subsystem/ai_controllers.dm +++ b/code/controllers/subsystem/ai_controllers.dm @@ -1,7 +1,7 @@ /// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans. SUBSYSTEM_DEF(ai_controllers) name = "AI Controller Ticker" - flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING|SS_BACKGROUND priority = FIRE_PRIORITY_NPC dependencies = list( /datum/controller/subsystem/movement/ai_movement, diff --git a/code/controllers/subsystem/ai_idle_controllers.dm b/code/controllers/subsystem/ai_idle_controllers.dm index 19fcad9c50e4..94b43207f82f 100644 --- a/code/controllers/subsystem/ai_idle_controllers.dm +++ b/code/controllers/subsystem/ai_idle_controllers.dm @@ -1,6 +1,6 @@ AI_CONTROLLER_SUBSYSTEM_DEF(ai_idle_controllers) name = "AI Idle Controllers" - flags = SS_POST_FIRE_TIMING | SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING | SS_BACKGROUND priority = FIRE_PRIORITY_IDLE_NPC dependencies = list( /datum/controller/subsystem/ai_controllers, diff --git a/code/controllers/subsystem/air.dm b/code/controllers/subsystem/air.dm index b991b73918da..b75d9e813e3d 100644 --- a/code/controllers/subsystem/air.dm +++ b/code/controllers/subsystem/air.dm @@ -6,7 +6,7 @@ SUBSYSTEM_DEF(air) ) priority = FIRE_PRIORITY_AIR wait = 0.5 SECONDS - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/cached_cost = 0 diff --git a/code/controllers/subsystem/ambience.dm b/code/controllers/subsystem/ambience.dm index 88db3ffcc332..79f5e7dbd1a0 100644 --- a/code/controllers/subsystem/ambience.dm +++ b/code/controllers/subsystem/ambience.dm @@ -1,7 +1,7 @@ /// The subsystem used to play ambience to users every now and then, makes them real excited. SUBSYSTEM_DEF(ambience) name = "Ambience" - flags = SS_BACKGROUND|SS_NO_INIT + ss_flags = SS_BACKGROUND|SS_NO_INIT priority = FIRE_PRIORITY_AMBIENCE runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 1 SECONDS diff --git a/code/controllers/subsystem/area_contents.dm b/code/controllers/subsystem/area_contents.dm index 830c6857561e..ae33933a4b9b 100644 --- a/code/controllers/subsystem/area_contents.dm +++ b/code/controllers/subsystem/area_contents.dm @@ -8,7 +8,7 @@ */ SUBSYSTEM_DEF(area_contents) name = "Area Contents" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT var/list/currentrun var/list/area/marked_for_clearing = list() diff --git a/code/controllers/subsystem/asset_loading.dm b/code/controllers/subsystem/asset_loading.dm index 8bafe88ffbc4..d591475c8806 100644 --- a/code/controllers/subsystem/asset_loading.dm +++ b/code/controllers/subsystem/asset_loading.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(asset_loading) name = "Asset Loading" priority = FIRE_PRIORITY_ASSETS - flags = SS_NO_INIT + ss_flags = SS_NO_INIT runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT var/list/datum/asset/generate_queue = list() var/assets_generating = 0 diff --git a/code/controllers/subsystem/assets.dm b/code/controllers/subsystem/assets.dm index e3b03c01222a..dad696c002a3 100644 --- a/code/controllers/subsystem/assets.dm +++ b/code/controllers/subsystem/assets.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(assets) /datum/controller/subsystem/persistent_paintings, /datum/controller/subsystem/greyscale_previews, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/list/datum/asset_cache_item/cache = list() var/list/preload = list() var/datum/asset_transport/transport = new() diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index 0ea51fe96242..80e649d75046 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -6,7 +6,7 @@ SUBSYSTEM_DEF(atoms) /datum/controller/subsystem/mapping, /datum/controller/subsystem/job, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// A stack of list(source, desired initialized state) /// We read the source of init changes from the last entry, and assert that all changes will come with a reset diff --git a/code/controllers/subsystem/augury.dm b/code/controllers/subsystem/augury.dm index e686e5cdb62f..681ac744c15e 100644 --- a/code/controllers/subsystem/augury.dm +++ b/code/controllers/subsystem/augury.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(augury) name = "Augury" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/list/watchers = list() diff --git a/code/controllers/subsystem/ban_cache.dm b/code/controllers/subsystem/ban_cache.dm index 21e1a92a7ed4..1e4c710031bc 100644 --- a/code/controllers/subsystem/ban_cache.dm +++ b/code/controllers/subsystem/ban_cache.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(ban_cache) /datum/controller/subsystem/ban_cache name = "Ban Cache" init_stage = INITSTAGE_LAST - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/query_started = FALSE /datum/controller/subsystem/ban_cache/Initialize() diff --git a/code/controllers/subsystem/blood_drying.dm b/code/controllers/subsystem/blood_drying.dm index 79202cb8d8c4..ec8d9d174e0b 100644 --- a/code/controllers/subsystem/blood_drying.dm +++ b/code/controllers/subsystem/blood_drying.dm @@ -5,7 +5,7 @@ */ PROCESSING_SUBSYSTEM_DEF(blood_drying) name = "Blood Drying" - flags = SS_NO_INIT | SS_BACKGROUND + ss_flags = SS_NO_INIT | SS_BACKGROUND priority = FIRE_PRIORITY_BLOOD_DRYING runlevels = RUNLEVEL_GAME wait = 4 SECONDS diff --git a/code/controllers/subsystem/cameras.dm b/code/controllers/subsystem/cameras.dm index 62e8af1c973b..d6f7866b8084 100644 --- a/code/controllers/subsystem/cameras.dm +++ b/code/controllers/subsystem/cameras.dm @@ -1,7 +1,7 @@ /// Manages the security cameras and camera chunks SUBSYSTEM_DEF(cameras) name = "Cameras" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND priority = FIRE_PRIORITY_CAMERAS runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 2 MINUTES diff --git a/code/controllers/subsystem/chat.dm b/code/controllers/subsystem/chat.dm index d2303074077a..4864e9aec8a5 100644 --- a/code/controllers/subsystem/chat.dm +++ b/code/controllers/subsystem/chat.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(chat) name = "Chat" - flags = SS_TICKER|SS_NO_INIT + ss_flags = SS_TICKER|SS_NO_INIT wait = 1 priority = FIRE_PRIORITY_CHAT init_stage = INITSTAGE_LAST diff --git a/code/controllers/subsystem/dbcore.dm b/code/controllers/subsystem/dbcore.dm index 17b2bf737ddd..b68454a5acce 100644 --- a/code/controllers/subsystem/dbcore.dm +++ b/code/controllers/subsystem/dbcore.dm @@ -1,7 +1,7 @@ #define SHUTDOWN_QUERY_TIMELIMIT (1 MINUTES) SUBSYSTEM_DEF(dbcore) name = "Database" - flags = SS_TICKER + ss_flags = SS_TICKER init_stage = INITSTAGE_FIRST wait = 10 // Not seconds because we're running on SS_TICKER runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT diff --git a/code/controllers/subsystem/dcs.dm b/code/controllers/subsystem/dcs.dm index a3dcc26af54f..e65c820e9445 100644 --- a/code/controllers/subsystem/dcs.dm +++ b/code/controllers/subsystem/dcs.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(dcs) name = "Datum Component System" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT wait = 1 SECONDS var/list/elements_by_type = list() diff --git a/code/controllers/subsystem/disease.dm b/code/controllers/subsystem/disease.dm index bfa6fd66b7d8..843148d6f43e 100644 --- a/code/controllers/subsystem/disease.dm +++ b/code/controllers/subsystem/disease.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(disease) name = "Disease" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/list/active_diseases = list() //List of Active disease in all mobs; purely for quick referencing. var/list/diseases diff --git a/code/controllers/subsystem/dynamic/dynamic.dm b/code/controllers/subsystem/dynamic/dynamic.dm index bdebecb717d8..8d614412ae86 100644 --- a/code/controllers/subsystem/dynamic/dynamic.dm +++ b/code/controllers/subsystem/dynamic/dynamic.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(dynamic) name = "Dynamic" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT wait = 5 MINUTES // These vars just exist for admins interfacing with dynamic diff --git a/code/controllers/subsystem/early_assets.dm b/code/controllers/subsystem/early_assets.dm index df155a4db10b..a480fbc49a98 100644 --- a/code/controllers/subsystem/early_assets.dm +++ b/code/controllers/subsystem/early_assets.dm @@ -14,7 +14,7 @@ SUBSYSTEM_DEF(early_assets) /datum/controller/subsystem/atoms, ) init_stage = INITSTAGE_EARLY - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /datum/controller/subsystem/early_assets/Initialize() var/init_source = "early assets" diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm index c01fe4f718fd..f5c733c0fd6a 100644 --- a/code/controllers/subsystem/explosions.dm +++ b/code/controllers/subsystem/explosions.dm @@ -9,7 +9,7 @@ SUBSYSTEM_DEF(explosions) name = "Explosions" priority = FIRE_PRIORITY_EXPLOSIONS wait = 1 - flags = SS_TICKER|SS_NO_INIT + ss_flags = SS_TICKER|SS_NO_INIT runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/cost_lowturf = 0 diff --git a/code/controllers/subsystem/fluids.dm b/code/controllers/subsystem/fluids.dm index 6b68ae717222..9ceec065086c 100644 --- a/code/controllers/subsystem/fluids.dm +++ b/code/controllers/subsystem/fluids.dm @@ -20,7 +20,7 @@ SUBSYSTEM_DEF(fluids) name = "Fluid" wait = 0 // Will be autoset to whatever makes the most sense given the spread and effect waits. - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING runlevels = RUNLEVEL_GAME|RUNLEVEL_POSTGAME priority = FIRE_PRIORITY_FLUIDS diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm index bb1475a4f50e..ae01679b0943 100644 --- a/code/controllers/subsystem/garbage.dm +++ b/code/controllers/subsystem/garbage.dm @@ -25,7 +25,7 @@ SUBSYSTEM_DEF(garbage) name = "Garbage" priority = FIRE_PRIORITY_GARBAGE wait = 2 SECONDS - flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT + ss_flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY init_stage = INITSTAGE_FIRST diff --git a/code/controllers/subsystem/greyscale_previews.dm b/code/controllers/subsystem/greyscale_previews.dm index 9a63cb6b2de5..79bc1fa1fe9b 100644 --- a/code/controllers/subsystem/greyscale_previews.dm +++ b/code/controllers/subsystem/greyscale_previews.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(greyscale_previews) name = "Greyscale Previews" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE init_stage = INITSTAGE_EARLY dependencies = list( /datum/controller/subsystem/processing/greyscale, diff --git a/code/controllers/subsystem/icon_smooth.dm b/code/controllers/subsystem/icon_smooth.dm index 77a2c1670ae3..7fb7f5c61997 100644 --- a/code/controllers/subsystem/icon_smooth.dm +++ b/code/controllers/subsystem/icon_smooth.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(icon_smooth) ) wait = 1 priority = FIRE_PRIORITY_SMOOTHING - flags = SS_TICKER + ss_flags = SS_TICKER ///Blueprints assemble an image of what pipes/manifolds/wires look like on initialization, and thus should be taken after everything's been smoothed var/list/blueprint_queue = list() diff --git a/code/controllers/subsystem/init_profiler.dm b/code/controllers/subsystem/init_profiler.dm index dcb163e596a4..a27c1a5bf39b 100644 --- a/code/controllers/subsystem/init_profiler.dm +++ b/code/controllers/subsystem/init_profiler.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(init_profiler) name = "Init Profiler" init_stage = INITSTAGE_LAST - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /datum/controller/subsystem/init_profiler/Initialize() if(CONFIG_GET(flag/auto_profile)) diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm index 11b75be8af75..dff1edad0548 100644 --- a/code/controllers/subsystem/input.dm +++ b/code/controllers/subsystem/input.dm @@ -1,7 +1,7 @@ VERB_MANAGER_SUBSYSTEM_DEF(input) name = "Input" init_stage = INITSTAGE_EARLY - flags = SS_TICKER + ss_flags = SS_TICKER priority = FIRE_PRIORITY_INPUT runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm index 6ba87c02780c..e3e33d1d79e5 100644 --- a/code/controllers/subsystem/ipintel.dm +++ b/code/controllers/subsystem/ipintel.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(ipintel) name = "XKeyScore" - flags = SS_NO_INIT|SS_NO_FIRE + ss_flags = SS_NO_INIT|SS_NO_FIRE /// The threshold for probability to be considered a VPN and/or bad IP var/probability_threshold diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index c37577d24bdf..9b23ad6528af 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -3,7 +3,7 @@ SUBSYSTEM_DEF(job) dependencies = list( /datum/controller/subsystem/processing/station, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// List of all jobs. var/list/datum/job/all_occupations = list() diff --git a/code/controllers/subsystem/lag_switch.dm b/code/controllers/subsystem/lag_switch.dm index 291e80fe18f1..70979b40816f 100644 --- a/code/controllers/subsystem/lag_switch.dm +++ b/code/controllers/subsystem/lag_switch.dm @@ -1,7 +1,7 @@ /// The subsystem for controlling drastic performance enhancements aimed at reducing server load for a smoother albeit slightly duller gaming experience SUBSYSTEM_DEF(lag_switch) name = "Lag Switch" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// If the lag switch measures should attempt to trigger automatically, TRUE if a config value exists var/auto_switch = FALSE diff --git a/code/controllers/subsystem/library.dm b/code/controllers/subsystem/library.dm index 87b64f1af870..a368a3903041 100644 --- a/code/controllers/subsystem/library.dm +++ b/code/controllers/subsystem/library.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(library) /datum/controller/subsystem/atoms, /datum/controller/subsystem/mapping, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// List of bookselves to prefill with books var/list/shelves_to_load = list() diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm index 2bff25b00a59..4717e7dcf8cd 100644 --- a/code/controllers/subsystem/lighting.dm +++ b/code/controllers/subsystem/lighting.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(lighting) /datum/controller/subsystem/mapping, ) wait = 2 - flags = SS_TICKER + ss_flags = SS_TICKER var/static/list/sources_queue = list() // List of lighting sources queued for update. var/static/list/corners_queue = list() // List of lighting corners queued for update. var/static/list/objects_queue = list() // List of lighting objects queued for update. diff --git a/code/controllers/subsystem/machines.dm b/code/controllers/subsystem/machines.dm index e3e0b0357a0d..8e19623ad295 100644 --- a/code/controllers/subsystem/machines.dm +++ b/code/controllers/subsystem/machines.dm @@ -3,7 +3,7 @@ SUBSYSTEM_DEF(machines) dependencies = list( /datum/controller/subsystem/atoms, ) - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING wait = 2 SECONDS /// Assosciative list of all machines that exist. diff --git a/code/controllers/subsystem/map_vote.dm b/code/controllers/subsystem/map_vote.dm index 6b0495613eab..4229ee82737b 100644 --- a/code/controllers/subsystem/map_vote.dm +++ b/code/controllers/subsystem/map_vote.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(map_vote) name = "Map Vote" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// Has an admin specifically set a map. var/admin_override = FALSE diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index caec055f21f2..e83341269cd7 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -351,7 +351,7 @@ Used by the AI doomsday and the self-destruct nuke. /datum/controller/subsystem/mapping/Recover() - flags |= SS_NO_INIT + ss_flags |= SS_NO_INIT initialized = SSmapping.initialized map_templates = SSmapping.map_templates ruins_templates = SSmapping.ruins_templates diff --git a/code/controllers/subsystem/market.dm b/code/controllers/subsystem/market.dm index 8d165bee25db..62631201ba4d 100644 --- a/code/controllers/subsystem/market.dm +++ b/code/controllers/subsystem/market.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(market) name = "Market" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND dependencies = list( /datum/controller/subsystem/atoms, ) diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm index 62d72a9d18c6..0bbeb16fbe3a 100644 --- a/code/controllers/subsystem/materials.dm +++ b/code/controllers/subsystem/materials.dm @@ -6,7 +6,7 @@ These materials call on_applied() on whatever item they are applied to, common e */ SUBSYSTEM_DEF(materials) name = "Materials" - flags = SS_NO_FIRE | SS_NO_INIT + ss_flags = SS_NO_FIRE | SS_NO_INIT /// Dictionary of material.id || material ref var/list/materials /// Flat list of materials diff --git a/code/controllers/subsystem/minor_mapping.dm b/code/controllers/subsystem/minor_mapping.dm index aca5845a14dd..5f59bf8d5972 100644 --- a/code/controllers/subsystem/minor_mapping.dm +++ b/code/controllers/subsystem/minor_mapping.dm @@ -6,7 +6,7 @@ SUBSYSTEM_DEF(minor_mapping) /datum/controller/subsystem/mapping, /datum/controller/subsystem/atoms, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE ///a list of vermin we pick from to spawn. var/list/vermin_chances = list( /mob/living/basic/mouse = 72, diff --git a/code/controllers/subsystem/mobs.dm b/code/controllers/subsystem/mobs.dm index 7f500e08289c..d0078891c224 100644 --- a/code/controllers/subsystem/mobs.dm +++ b/code/controllers/subsystem/mobs.dm @@ -1,7 +1,7 @@ SUBSYSTEM_DEF(mobs) name = "Mobs" priority = FIRE_PRIORITY_MOBS - flags = SS_KEEP_TIMING | SS_NO_INIT + ss_flags = SS_KEEP_TIMING | SS_NO_INIT runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 2 SECONDS diff --git a/code/controllers/subsystem/moods.dm b/code/controllers/subsystem/moods.dm index 20111747bdb2..5f2c3151ad28 100644 --- a/code/controllers/subsystem/moods.dm +++ b/code/controllers/subsystem/moods.dm @@ -1,5 +1,5 @@ PROCESSING_SUBSYSTEM_DEF(mood) name = "Mood" - flags = SS_NO_INIT | SS_BACKGROUND + ss_flags = SS_NO_INIT | SS_BACKGROUND priority = 20 wait = 1 SECONDS diff --git a/code/controllers/subsystem/mouse_entered.dm b/code/controllers/subsystem/mouse_entered.dm index 6dc8d995a522..baf60e330247 100644 --- a/code/controllers/subsystem/mouse_entered.dm +++ b/code/controllers/subsystem/mouse_entered.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(mouse_entered) name = "MouseEntered" wait = 1 - flags = SS_NO_INIT | SS_TICKER + ss_flags = SS_NO_INIT | SS_TICKER priority = FIRE_PRIORITY_MOUSE_ENTERED runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY diff --git a/code/controllers/subsystem/movement/ai_movement.dm b/code/controllers/subsystem/movement/ai_movement.dm index 8703fbfdfc6e..9c96ea6a22bd 100644 --- a/code/controllers/subsystem/movement/ai_movement.dm +++ b/code/controllers/subsystem/movement/ai_movement.dm @@ -1,7 +1,7 @@ /// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances MOVEMENT_SUBSYSTEM_DEF(ai_movement) name = "AI movement" - flags = SS_BACKGROUND|SS_TICKER + ss_flags = SS_BACKGROUND|SS_TICKER priority = FIRE_PRIORITY_NPC_MOVEMENT runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/movement/cliff_falling.dm b/code/controllers/subsystem/movement/cliff_falling.dm index b3d7f114dfba..d9e56eb2a02d 100644 --- a/code/controllers/subsystem/movement/cliff_falling.dm +++ b/code/controllers/subsystem/movement/cliff_falling.dm @@ -2,7 +2,7 @@ MOVEMENT_SUBSYSTEM_DEF(cliff_falling) name = "Cliff Falling" priority = FIRE_PRIORITY_CLIFF_FALLING - flags = SS_NO_INIT|SS_TICKER + ss_flags = SS_NO_INIT|SS_TICKER runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME /// Who are currently falling and with which movemanager? diff --git a/code/controllers/subsystem/movement/hyperspace_drift.dm b/code/controllers/subsystem/movement/hyperspace_drift.dm index bd1ac67a6f0a..66710bd02f32 100644 --- a/code/controllers/subsystem/movement/hyperspace_drift.dm +++ b/code/controllers/subsystem/movement/hyperspace_drift.dm @@ -2,5 +2,5 @@ MOVEMENT_SUBSYSTEM_DEF(hyperspace_drift) name = "Hyperspace Drift" priority = FIRE_PRIORITY_HYPERSPACE_DRIFT - flags = SS_NO_INIT|SS_TICKER + ss_flags = SS_NO_INIT|SS_TICKER runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/movement/movement.dm b/code/controllers/subsystem/movement/movement.dm index 2b0463db7905..052b82991e32 100644 --- a/code/controllers/subsystem/movement/movement.dm +++ b/code/controllers/subsystem/movement/movement.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(movement) name = "Movement Loops" - flags = SS_NO_INIT|SS_TICKER + ss_flags = SS_NO_INIT|SS_TICKER wait = 1 //Fire each tick /* A breif aside about the bucketing system here diff --git a/code/controllers/subsystem/movement/newtonian_movement.dm b/code/controllers/subsystem/movement/newtonian_movement.dm index e4143669678b..d88a7d72f576 100644 --- a/code/controllers/subsystem/movement/newtonian_movement.dm +++ b/code/controllers/subsystem/movement/newtonian_movement.dm @@ -1,7 +1,7 @@ /// The subsystem is intended to tick things related to space/newtonian movement, such as constant sources of inertia MOVEMENT_SUBSYSTEM_DEF(newtonian_movement) name = "Newtonian Movement" - flags = SS_NO_INIT|SS_TICKER + ss_flags = SS_NO_INIT|SS_TICKER runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/stat_tag = "P" //Used for logging diff --git a/code/controllers/subsystem/networks/bitrunning.dm b/code/controllers/subsystem/networks/bitrunning.dm index 6f6d68a0b05c..1bb8eb49e4d7 100644 --- a/code/controllers/subsystem/networks/bitrunning.dm +++ b/code/controllers/subsystem/networks/bitrunning.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(bitrunning) name = "Bitrunning" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/list/all_domains = list() diff --git a/code/controllers/subsystem/networks/circuit_component.dm b/code/controllers/subsystem/networks/circuit_component.dm index bae302ed9d0a..54ece2029032 100644 --- a/code/controllers/subsystem/networks/circuit_component.dm +++ b/code/controllers/subsystem/networks/circuit_component.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(circuit_component) name = "Circuit Components" wait = 0.1 SECONDS priority = FIRE_PRIORITY_DEFAULT - flags = SS_NO_INIT + ss_flags = SS_NO_INIT var/list/callbacks_to_invoke = list() var/list/currentrun = list() diff --git a/code/controllers/subsystem/networks/id_access.dm b/code/controllers/subsystem/networks/id_access.dm index 889c59fbf5bb..482bd5e119f8 100644 --- a/code/controllers/subsystem/networks/id_access.dm +++ b/code/controllers/subsystem/networks/id_access.dm @@ -3,7 +3,7 @@ */ SUBSYSTEM_DEF(id_access) name = "IDs and Access" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// Dictionary of access flags. Keys are accesses. Values are their associated bitflags. var/list/flags_by_access = list() diff --git a/code/controllers/subsystem/networks/radio.dm b/code/controllers/subsystem/networks/radio.dm index 6d880cedeb30..8ddc9ae955ca 100644 --- a/code/controllers/subsystem/networks/radio.dm +++ b/code/controllers/subsystem/networks/radio.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(radio) name = "Radio" - flags = SS_NO_FIRE|SS_NO_INIT + ss_flags = SS_NO_FIRE|SS_NO_INIT var/list/datum/radio_frequency/frequencies = list() var/list/saymodes = list() diff --git a/code/controllers/subsystem/networks/wiremod_composite.dm b/code/controllers/subsystem/networks/wiremod_composite.dm index 15b21beefa1b..e099a2bfa902 100644 --- a/code/controllers/subsystem/networks/wiremod_composite.dm +++ b/code/controllers/subsystem/networks/wiremod_composite.dm @@ -7,7 +7,7 @@ **/ SUBSYSTEM_DEF(wiremod_composite) name = "Wiremod Composite Templates" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// The templates created and stored var/list/templates = list() diff --git a/code/controllers/subsystem/npcpool.dm b/code/controllers/subsystem/npcpool.dm index 6fbfdadf6828..a03e7c9872bc 100644 --- a/code/controllers/subsystem/npcpool.dm +++ b/code/controllers/subsystem/npcpool.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(npcpool) name = "NPC Pool" - flags = SS_POST_FIRE_TIMING|SS_NO_INIT|SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING|SS_NO_INIT|SS_BACKGROUND priority = FIRE_PRIORITY_NPC runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/overlays.dm b/code/controllers/subsystem/overlays.dm index 96041edd4237..dda905504d87 100644 --- a/code/controllers/subsystem/overlays.dm +++ b/code/controllers/subsystem/overlays.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(overlays) name = "Overlay" - flags = SS_NO_FIRE|SS_NO_INIT + ss_flags = SS_NO_FIRE|SS_NO_INIT var/list/stats /datum/controller/subsystem/overlays/PreInit() diff --git a/code/controllers/subsystem/pai.dm b/code/controllers/subsystem/pai.dm index 4abc752ea81a..b2db11c4a51f 100644 --- a/code/controllers/subsystem/pai.dm +++ b/code/controllers/subsystem/pai.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(pai) name = "pAI" - flags = SS_NO_INIT|SS_NO_FIRE + ss_flags = SS_NO_INIT|SS_NO_FIRE /// List of pAI candidates, including those not submitted. var/list/candidates = list() diff --git a/code/controllers/subsystem/parallax.dm b/code/controllers/subsystem/parallax.dm index e7ccfff74c1a..1b509f8d7578 100644 --- a/code/controllers/subsystem/parallax.dm +++ b/code/controllers/subsystem/parallax.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(parallax) name = "Parallax" wait = 2 - flags = SS_POST_FIRE_TIMING | SS_BACKGROUND | SS_NO_INIT + ss_flags = SS_POST_FIRE_TIMING | SS_BACKGROUND | SS_NO_INIT priority = FIRE_PRIORITY_PARALLAX runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT var/list/currentrun diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm index cb312ba29c22..405f8129eeac 100644 --- a/code/controllers/subsystem/persistence/_persistence.dm +++ b/code/controllers/subsystem/persistence/_persistence.dm @@ -7,7 +7,7 @@ SUBSYSTEM_DEF(persistence) /datum/controller/subsystem/mapping, /datum/controller/subsystem/atoms, ) - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE ///instantiated wall engraving components var/list/wall_engravings = list() diff --git a/code/controllers/subsystem/persistent_paintings.dm b/code/controllers/subsystem/persistent_paintings.dm index e9a057432fe9..6d9aabf99893 100644 --- a/code/controllers/subsystem/persistent_paintings.dm +++ b/code/controllers/subsystem/persistent_paintings.dm @@ -120,7 +120,7 @@ SUBSYSTEM_DEF(persistent_paintings) name = "Persistent Paintings" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE dependencies = list( /datum/controller/subsystem/persistence, ) diff --git a/code/controllers/subsystem/ping.dm b/code/controllers/subsystem/ping.dm index 22a64c2802a5..0607f89162c4 100644 --- a/code/controllers/subsystem/ping.dm +++ b/code/controllers/subsystem/ping.dm @@ -8,7 +8,7 @@ SUBSYSTEM_DEF(ping) priority = FIRE_PRIORITY_PING init_stage = INITSTAGE_EARLY wait = 4 SECONDS - flags = SS_NO_INIT + ss_flags = SS_NO_INIT runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT var/list/currentrun = list() diff --git a/code/controllers/subsystem/points_of_interest.dm b/code/controllers/subsystem/points_of_interest.dm index 7bec303d66a6..6844c84ecdcf 100644 --- a/code/controllers/subsystem/points_of_interest.dm +++ b/code/controllers/subsystem/points_of_interest.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(points_of_interest) name = "Points of Interest" - flags = SS_NO_FIRE | SS_NO_INIT + ss_flags = SS_NO_FIRE | SS_NO_INIT /// List of mob POIs. This list is automatically sorted. var/list/datum/point_of_interest/mob_poi/mob_points_of_interest = list() diff --git a/code/controllers/subsystem/polling.dm b/code/controllers/subsystem/polling.dm index 1477fa7e7c36..b830aec53ef5 100644 --- a/code/controllers/subsystem/polling.dm +++ b/code/controllers/subsystem/polling.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(polling) name = "Polling" - flags = SS_BACKGROUND | SS_NO_INIT + ss_flags = SS_BACKGROUND | SS_NO_INIT wait = 1 SECONDS runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME /// List of polls currently ongoing, to be checked on next fire() diff --git a/code/controllers/subsystem/processing/acid.dm b/code/controllers/subsystem/processing/acid.dm index a92880f51e3d..2e16efdda891 100644 --- a/code/controllers/subsystem/processing/acid.dm +++ b/code/controllers/subsystem/processing/acid.dm @@ -2,5 +2,5 @@ PROCESSING_SUBSYSTEM_DEF(acid) name = "Acid" priority = FIRE_PRIORITY_ACID - flags = SS_NO_INIT|SS_BACKGROUND + ss_flags = SS_NO_INIT|SS_BACKGROUND runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/processing/ai_basic_avoidance.dm b/code/controllers/subsystem/processing/ai_basic_avoidance.dm index 2a3fe992f447..9fdd5d2d4534 100644 --- a/code/controllers/subsystem/processing/ai_basic_avoidance.dm +++ b/code/controllers/subsystem/processing/ai_basic_avoidance.dm @@ -1,4 +1,4 @@ PROCESSING_SUBSYSTEM_DEF(basic_avoidance) name = "Basic Avoidance" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT wait = 2 SECONDS diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm index 56904fb3c2f6..7304c5ed3628 100644 --- a/code/controllers/subsystem/processing/ai_behaviors.dm +++ b/code/controllers/subsystem/processing/ai_behaviors.dm @@ -1,7 +1,7 @@ /// The subsystem used to tick [/datum/ai_behavior] instances. Handling the individual actions an AI can take like punching someone in the fucking NUTS PROCESSING_SUBSYSTEM_DEF(ai_behaviors) name = "AI Behavior Ticker" - flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING|SS_BACKGROUND priority = FIRE_PRIORITY_NPC_ACTIONS runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME dependencies = list( diff --git a/code/controllers/subsystem/processing/ai_idle_behaviors.dm b/code/controllers/subsystem/processing/ai_idle_behaviors.dm index c776451ab203..2b631da96f3b 100644 --- a/code/controllers/subsystem/processing/ai_idle_behaviors.dm +++ b/code/controllers/subsystem/processing/ai_idle_behaviors.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(idle_ai_behaviors) name = "AI Idle Behaviors" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND wait = 1.5 SECONDS priority = FIRE_PRIORITY_IDLE_NPC dependencies = list( diff --git a/code/controllers/subsystem/processing/antag_hud.dm b/code/controllers/subsystem/processing/antag_hud.dm index aaf10a5e2ecb..98e439039e84 100644 --- a/code/controllers/subsystem/processing/antag_hud.dm +++ b/code/controllers/subsystem/processing/antag_hud.dm @@ -1,4 +1,4 @@ PROCESSING_SUBSYSTEM_DEF(antag_hud) name = "Antag HUDs" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT wait = 2 SECONDS diff --git a/code/controllers/subsystem/processing/aura.dm b/code/controllers/subsystem/processing/aura.dm index 12a7d511fb79..992a64680a36 100644 --- a/code/controllers/subsystem/processing/aura.dm +++ b/code/controllers/subsystem/processing/aura.dm @@ -1,5 +1,5 @@ /// The subsystem used to tick auras ([/datum/component/aura_healing] and [/datum/component/damage_aura]). PROCESSING_SUBSYSTEM_DEF(aura) name = "Aura" - flags = SS_NO_INIT | SS_BACKGROUND | SS_KEEP_TIMING + ss_flags = SS_NO_INIT | SS_BACKGROUND | SS_KEEP_TIMING wait = 0.3 SECONDS diff --git a/code/controllers/subsystem/processing/clock_component.dm b/code/controllers/subsystem/processing/clock_component.dm index d07acaa6d793..13d165e90ff9 100644 --- a/code/controllers/subsystem/processing/clock_component.dm +++ b/code/controllers/subsystem/processing/clock_component.dm @@ -1,5 +1,5 @@ /// The subsystem used to tick [/datum/component/acid] instances. PROCESSING_SUBSYSTEM_DEF(clock_component) name = "Clock Component" - flags = SS_NO_INIT|SS_BACKGROUND|SS_KEEP_TIMING + ss_flags = SS_NO_INIT|SS_BACKGROUND|SS_KEEP_TIMING wait = COMP_CLOCK_DELAY diff --git a/code/controllers/subsystem/processing/digital_clock.dm b/code/controllers/subsystem/processing/digital_clock.dm index 6981e785d1c3..4146a2c857fe 100644 --- a/code/controllers/subsystem/processing/digital_clock.dm +++ b/code/controllers/subsystem/processing/digital_clock.dm @@ -1,5 +1,5 @@ /// The subsystem used to tick digital clocks PROCESSING_SUBSYSTEM_DEF(digital_clock) name = "Digital Clocks" - flags = SS_NO_INIT|SS_BACKGROUND|SS_KEEP_TIMING + ss_flags = SS_NO_INIT|SS_BACKGROUND|SS_KEEP_TIMING wait = 1 SECONDS diff --git a/code/controllers/subsystem/processing/fire_burning.dm b/code/controllers/subsystem/processing/fire_burning.dm index 6cde5e3c34f6..35b794b5f548 100644 --- a/code/controllers/subsystem/processing/fire_burning.dm +++ b/code/controllers/subsystem/processing/fire_burning.dm @@ -2,5 +2,5 @@ PROCESSING_SUBSYSTEM_DEF(burning) name = "Burning" priority = FIRE_PRIORITY_BURNING - flags = SS_NO_INIT|SS_BACKGROUND + ss_flags = SS_NO_INIT|SS_BACKGROUND runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/processing/fishing.dm b/code/controllers/subsystem/processing/fishing.dm index 5283560a5a50..9c048a7724f2 100644 --- a/code/controllers/subsystem/processing/fishing.dm +++ b/code/controllers/subsystem/processing/fishing.dm @@ -4,7 +4,7 @@ PROCESSING_SUBSYSTEM_DEF(fishing) dependencies = list( /datum/controller/subsystem/atoms, ) - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND wait = 0.05 SECONDS // If you raise it to 0.1 SECONDS, you better also modify [datum/fish_movement/move_fish()] ///A list of cached fish icons var/list/cached_fish_icons diff --git a/code/controllers/subsystem/processing/greyscale.dm b/code/controllers/subsystem/processing/greyscale.dm index 03b19ac2effe..551f7a12e49a 100644 --- a/code/controllers/subsystem/processing/greyscale.dm +++ b/code/controllers/subsystem/processing/greyscale.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(greyscale) name = "Greyscale" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND wait = 3 SECONDS init_stage = INITSTAGE_EARLY var/list/datum/greyscale_config/configurations = list() diff --git a/code/controllers/subsystem/processing/instruments.dm b/code/controllers/subsystem/processing/instruments.dm index 9cd8706e0c4f..197c16047b7f 100644 --- a/code/controllers/subsystem/processing/instruments.dm +++ b/code/controllers/subsystem/processing/instruments.dm @@ -1,7 +1,7 @@ PROCESSING_SUBSYSTEM_DEF(instruments) name = "Instruments" wait = 0.5 - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING priority = FIRE_PRIORITY_INSTRUMENTS /// List of all instrument data, associative id = datum var/static/list/datum/instrument/instrument_data = list() diff --git a/code/controllers/subsystem/processing/newplayer.dm b/code/controllers/subsystem/processing/newplayer.dm index e7537da5ead4..8b12cb5bf31b 100644 --- a/code/controllers/subsystem/processing/newplayer.dm +++ b/code/controllers/subsystem/processing/newplayer.dm @@ -1,5 +1,5 @@ PROCESSING_SUBSYSTEM_DEF(newplayer_info) name = "New Player Info" - flags = SS_NO_INIT | SS_BACKGROUND + ss_flags = SS_NO_INIT | SS_BACKGROUND wait = 1 SECONDS runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT diff --git a/code/controllers/subsystem/processing/obj.dm b/code/controllers/subsystem/processing/obj.dm index 3566e8a4dc22..c5eb6e3374cb 100644 --- a/code/controllers/subsystem/processing/obj.dm +++ b/code/controllers/subsystem/processing/obj.dm @@ -1,5 +1,5 @@ PROCESSING_SUBSYSTEM_DEF(obj) name = "Objects" priority = FIRE_PRIORITY_OBJ - flags = SS_NO_INIT + ss_flags = SS_NO_INIT wait = 2 SECONDS diff --git a/code/controllers/subsystem/processing/personality.dm b/code/controllers/subsystem/processing/personality.dm index 0f9c1021befd..8b642edd84a5 100644 --- a/code/controllers/subsystem/processing/personality.dm +++ b/code/controllers/subsystem/processing/personality.dm @@ -1,7 +1,7 @@ PROCESSING_SUBSYSTEM_DEF(personalities) name = "Personalities" runlevels = RUNLEVEL_GAME - flags = SS_BACKGROUND|SS_POST_FIRE_TIMING + ss_flags = SS_BACKGROUND|SS_POST_FIRE_TIMING wait = 3 SECONDS /// All personality singletons indexed by their type diff --git a/code/controllers/subsystem/processing/plumbing.dm b/code/controllers/subsystem/processing/plumbing.dm index 34d6d8f882e9..5a9204edf647 100644 --- a/code/controllers/subsystem/processing/plumbing.dm +++ b/code/controllers/subsystem/processing/plumbing.dm @@ -2,4 +2,4 @@ PROCESSING_SUBSYSTEM_DEF(plumbing) name = "Plumbing" wait = 10 stat_tag = "FD" //its actually Fluid Ducts - flags = SS_NO_INIT + ss_flags = SS_NO_INIT diff --git a/code/controllers/subsystem/processing/priority_effects.dm b/code/controllers/subsystem/processing/priority_effects.dm index f3be081ec50c..51274cbfefd0 100644 --- a/code/controllers/subsystem/processing/priority_effects.dm +++ b/code/controllers/subsystem/processing/priority_effects.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(priority_effects) name = "Priority Status Effects" - flags = SS_KEEP_TIMING | SS_NO_INIT + ss_flags = SS_KEEP_TIMING | SS_NO_INIT wait = 0.2 SECONDS // Same as SSfastprocess, but can be anything, assuming you refactor all high-priority status effect intervals and durations to be a multiple of it. priority = FIRE_PRIORITY_PRIORITY_EFFECTS stat_tag = "PEFF" diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm index d2b255211686..4338ae420d9c 100644 --- a/code/controllers/subsystem/processing/processing.dm +++ b/code/controllers/subsystem/processing/processing.dm @@ -3,7 +3,7 @@ SUBSYSTEM_DEF(processing) name = "Processing" priority = FIRE_PRIORITY_PROCESS - flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT + ss_flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT wait = 1 SECONDS var/stat_tag = "P" //Used for logging diff --git a/code/controllers/subsystem/processing/projectiles.dm b/code/controllers/subsystem/processing/projectiles.dm index 0124296a3a23..75c0e20ccf8e 100644 --- a/code/controllers/subsystem/processing/projectiles.dm +++ b/code/controllers/subsystem/processing/projectiles.dm @@ -2,7 +2,7 @@ PROCESSING_SUBSYSTEM_DEF(projectiles) name = "Projectiles" wait = 1 stat_tag = "PP" - flags = SS_NO_INIT|SS_TICKER + ss_flags = SS_NO_INIT|SS_TICKER /* * Maximum amount of pixels a projectile can pass per tick *unless* its a hitscan projectile. * This prevents projectiles from turning into essentially hitscans if SSprojectiles starts chugging diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 1d7725dffe89..5c4239962b2b 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -44,7 +44,7 @@ GLOBAL_LIST_INIT(quirk_string_blacklist, generate_quirk_string_blacklist()) // - Quirk datums are stored and hold different effects, as well as being a vector for applying trait string PROCESSING_SUBSYSTEM_DEF(quirks) name = "Quirks" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND runlevels = RUNLEVEL_GAME wait = 1 SECONDS diff --git a/code/controllers/subsystem/processing/reagents.dm b/code/controllers/subsystem/processing/reagents.dm index b116d859709b..2891a4cdd67c 100644 --- a/code/controllers/subsystem/processing/reagents.dm +++ b/code/controllers/subsystem/processing/reagents.dm @@ -4,7 +4,7 @@ PROCESSING_SUBSYSTEM_DEF(reagents) name = "Reagents" priority = FIRE_PRIORITY_REAGENTS wait = 0.25 SECONDS //You might think that rate_up_lim has to be set to half, but since everything is normalised around seconds_per_tick, it automatically adjusts it to be per second. Magic! - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME init_stage = INITSTAGE_EARLY ///What time was it when we last ticked diff --git a/code/controllers/subsystem/processing/station.dm b/code/controllers/subsystem/processing/station.dm index c19e04559cd6..382a531f3984 100644 --- a/code/controllers/subsystem/processing/station.dm +++ b/code/controllers/subsystem/processing/station.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(station) name = "Station" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND runlevels = RUNLEVEL_GAME wait = 5 SECONDS diff --git a/code/controllers/subsystem/processing/turrets.dm b/code/controllers/subsystem/processing/turrets.dm index 34f128b5c22a..f7818a8f2a28 100644 --- a/code/controllers/subsystem/processing/turrets.dm +++ b/code/controllers/subsystem/processing/turrets.dm @@ -1,6 +1,6 @@ /// The subsystem used for portable turrets, as they're relatively more intensive compared to most other machines, so we don't want them hogging tick usage from everything else. PROCESSING_SUBSYSTEM_DEF(turrets) name = "Turret Processing" - flags = SS_NO_INIT | SS_KEEP_TIMING + ss_flags = SS_NO_INIT | SS_KEEP_TIMING wait = 2 SECONDS runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/queuelinks.dm b/code/controllers/subsystem/queuelinks.dm index 20b556229318..781d863f6517 100644 --- a/code/controllers/subsystem/queuelinks.dm +++ b/code/controllers/subsystem/queuelinks.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(queuelinks) name = "Queue Links" - flags = SS_NO_FIRE | SS_NO_INIT + ss_flags = SS_NO_FIRE | SS_NO_INIT ///assoc list of pending queues, id = /datum/queue_link var/list/queues = list() diff --git a/code/controllers/subsystem/radiation.dm b/code/controllers/subsystem/radiation.dm index 316331db7c9a..31e06b1afe1d 100644 --- a/code/controllers/subsystem/radiation.dm +++ b/code/controllers/subsystem/radiation.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(radiation) name = "Radiation" - flags = SS_BACKGROUND | SS_NO_INIT + ss_flags = SS_BACKGROUND | SS_NO_INIT wait = 0.5 SECONDS diff --git a/code/controllers/subsystem/radioactive_nebula.dm b/code/controllers/subsystem/radioactive_nebula.dm index 0778725accbe..44a908bbb3f7 100644 --- a/code/controllers/subsystem/radioactive_nebula.dm +++ b/code/controllers/subsystem/radioactive_nebula.dm @@ -7,7 +7,7 @@ SUBSYSTEM_DEF(radioactive_nebula) dependencies = list( /datum/controller/subsystem/processing/station, ) - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND wait = 30 SECONDS VAR_PRIVATE diff --git a/code/controllers/subsystem/restaurant.dm b/code/controllers/subsystem/restaurant.dm index 034a7610cb6f..c22c9163fbb3 100644 --- a/code/controllers/subsystem/restaurant.dm +++ b/code/controllers/subsystem/restaurant.dm @@ -5,7 +5,7 @@ This subsystem exists to serve as a holder for important info for the restaurant SUBSYSTEM_DEF(restaurant) name = "Restaurant" wait = 20 SECONDS //Roll for new guests but don't do it too fast. - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE ///All venues that exist, assoc list of type - reference var/list/all_venues = list() ///All customer data datums that exist, assoc list of type - reference diff --git a/code/controllers/subsystem/server_maint.dm b/code/controllers/subsystem/server_maint.dm index c4428f297aee..44a13093a7f6 100644 --- a/code/controllers/subsystem/server_maint.dm +++ b/code/controllers/subsystem/server_maint.dm @@ -3,7 +3,7 @@ SUBSYSTEM_DEF(server_maint) name = "Server Tasks" wait = 6 - flags = SS_POST_FIRE_TIMING + ss_flags = SS_POST_FIRE_TIMING priority = FIRE_PRIORITY_SERVER_MAINT init_stage = INITSTAGE_EARLY runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm index 7b1ba551a8e4..0553cc6d5654 100644 --- a/code/controllers/subsystem/shuttle.dm +++ b/code/controllers/subsystem/shuttle.dm @@ -14,7 +14,7 @@ SUBSYSTEM_DEF(shuttle) /datum/controller/subsystem/atoms, /datum/controller/subsystem/air, ) - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING runlevels = RUNLEVEL_SETUP | RUNLEVEL_GAME /// A list of all the mobile docking ports. diff --git a/code/controllers/subsystem/skills.dm b/code/controllers/subsystem/skills.dm index 0ece2ac2aaa9..0a7ac5383ed2 100644 --- a/code/controllers/subsystem/skills.dm +++ b/code/controllers/subsystem/skills.dm @@ -4,7 +4,7 @@ This subsystem mostly exists to populate and manage the skill singletons. SUBSYSTEM_DEF(skills) name = "Skills" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE ///Dictionary of skill.type || skill ref var/list/all_skills = list() ///List of level names with index corresponding to skill level diff --git a/code/controllers/subsystem/sounds.dm b/code/controllers/subsystem/sounds.dm index 5dd2c985f97f..db3f94a080bd 100644 --- a/code/controllers/subsystem/sounds.dm +++ b/code/controllers/subsystem/sounds.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(sounds) name = "Sounds" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE init_stage = INITSTAGE_EARLY var/static/using_channels_max = CHANNEL_HIGHEST_AVAILABLE //BYOND max channels /// Amount of channels to reserve for random usage rather than reservations being allowed to reserve all channels. Also a nice safeguard for when someone screws up. diff --git a/code/controllers/subsystem/sprite_accessories.dm b/code/controllers/subsystem/sprite_accessories.dm index 2ceed1d3fbb9..5173960a3ffc 100644 --- a/code/controllers/subsystem/sprite_accessories.dm +++ b/code/controllers/subsystem/sprite_accessories.dm @@ -14,7 +14,7 @@ /// A sprite accessory is something that we add to a human sprite to make them look different. This is hair, facial hair, underwear, mutant bits, etc. SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity name = "Sprite Accessories" - flags = SS_NO_FIRE | SS_NO_INIT + ss_flags = SS_NO_FIRE | SS_NO_INIT // HOLY SHIT COMPACT THIS INTO ASSOCIATED LISTS SO WE STOP ADDING VARIABLES //Hairstyles diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 48f1cc1b3130..27c0b2d597d5 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -3,7 +3,7 @@ SUBSYSTEM_DEF(statpanels) wait = 4 priority = FIRE_PRIORITY_STATPANEL runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY - flags = SS_NO_INIT + ss_flags = SS_NO_INIT var/list/currentrun = list() var/list/global_data var/list/mc_data diff --git a/code/controllers/subsystem/stickyban.dm b/code/controllers/subsystem/stickyban.dm index 8a955b2fb0df..e19c574a9fa8 100644 --- a/code/controllers/subsystem/stickyban.dm +++ b/code/controllers/subsystem/stickyban.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(stickyban) name = "PRISM" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/list/cache = list() var/list/dbcache = list() diff --git a/code/controllers/subsystem/tcgsetup.dm b/code/controllers/subsystem/tcgsetup.dm index 5904c821e79e..0a4914ec97c2 100644 --- a/code/controllers/subsystem/tcgsetup.dm +++ b/code/controllers/subsystem/tcgsetup.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(trading_card_game) name = "Trading Card Game" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// Base directory for all related string files var/card_directory = "strings/tcg" /// List of card files to load diff --git a/code/controllers/subsystem/tgui.dm b/code/controllers/subsystem/tgui.dm index ea8b02e81296..9f5d518b6c2a 100644 --- a/code/controllers/subsystem/tgui.dm +++ b/code/controllers/subsystem/tgui.dm @@ -13,7 +13,7 @@ SUBSYSTEM_DEF(tgui) name = "tgui" wait = 9 - flags = SS_NO_INIT + ss_flags = SS_NO_INIT priority = FIRE_PRIORITY_TGUI runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT diff --git a/code/controllers/subsystem/throwing.dm b/code/controllers/subsystem/throwing.dm index 95c6923269f6..db7512a62712 100644 --- a/code/controllers/subsystem/throwing.dm +++ b/code/controllers/subsystem/throwing.dm @@ -5,7 +5,7 @@ SUBSYSTEM_DEF(throwing) name = "Throwing" priority = FIRE_PRIORITY_THROWING wait = 1 - flags = SS_NO_INIT|SS_KEEP_TIMING|SS_TICKER + ss_flags = SS_NO_INIT|SS_KEEP_TIMING|SS_TICKER runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/list/currentrun diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 92e0390d63eb..6bd850c3d08f 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(ticker) name = "Ticker" priority = FIRE_PRIORITY_TICKER - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME /// state of current round (used by process()) Use the defines GAME_STATE_* ! diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index ef6826396c2d..a9f3d9dcd34b 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -18,7 +18,7 @@ SUBSYSTEM_DEF(timer) name = "Timer" wait = 1 // SS_TICKER subsystem, so wait is in ticks priority = FIRE_PRIORITY_TIMER - flags = SS_TICKER|SS_NO_INIT + ss_flags = SS_TICKER|SS_NO_INIT /// Queue used for storing timers that do not fit into the current buckets var/list/datum/timedevent/second_queue = list() diff --git a/code/controllers/subsystem/title.dm b/code/controllers/subsystem/title.dm index 8ced977dea25..4b8e12aa4c78 100644 --- a/code/controllers/subsystem/title.dm +++ b/code/controllers/subsystem/title.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(title) name = "Title Screen" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE init_stage = INITSTAGE_EARLY var/file_path var/icon/icon diff --git a/code/controllers/subsystem/traitor.dm b/code/controllers/subsystem/traitor.dm index ad765dd916b5..edb0f237f5e3 100644 --- a/code/controllers/subsystem/traitor.dm +++ b/code/controllers/subsystem/traitor.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(traitor) /datum/controller/subsystem/mapping, /datum/controller/subsystem/atoms, ) - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING wait = 10 SECONDS runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME diff --git a/code/controllers/subsystem/tts.dm b/code/controllers/subsystem/tts.dm index 15c57b15e5da..12d904af246d 100644 --- a/code/controllers/subsystem/tts.dm +++ b/code/controllers/subsystem/tts.dm @@ -156,7 +156,7 @@ SUBSYSTEM_DEF(tts) /datum/controller/subsystem/tts/fire(resumed) if(!tts_enabled) - flags |= SS_NO_FIRE + ss_flags |= SS_NO_FIRE return if(!resumed) diff --git a/code/controllers/subsystem/tutorials.dm b/code/controllers/subsystem/tutorials.dm index ceb1fa787867..b03af66028df 100644 --- a/code/controllers/subsystem/tutorials.dm +++ b/code/controllers/subsystem/tutorials.dm @@ -1,7 +1,7 @@ /// Namespace for housing code relating to giving contextual tutorials to users. SUBSYSTEM_DEF(tutorials) name = "Tutorials" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE /// A mapping of /datum/tutorial type to their manager singleton. /// You probably shouldn't be indexing this directly. diff --git a/code/controllers/subsystem/unplanned_controllers.dm b/code/controllers/subsystem/unplanned_controllers.dm index 49fbcb2e83b5..e2e78e0951ee 100644 --- a/code/controllers/subsystem/unplanned_controllers.dm +++ b/code/controllers/subsystem/unplanned_controllers.dm @@ -2,7 +2,7 @@ GLOBAL_LIST_EMPTY(unplanned_controller_subsystems) /// Handles making mobs perform lightweight "idle" behaviors such as wandering around when they have nothing planned SUBSYSTEM_DEF(unplanned_controllers) name = "Unplanned AI Controllers" - flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING|SS_BACKGROUND priority = FIRE_PRIORITY_UNPLANNED_NPC dependencies = list( /datum/controller/subsystem/movement/ai_movement, diff --git a/code/controllers/subsystem/verb_manager.dm b/code/controllers/subsystem/verb_manager.dm index f09c05096415..0949033ab170 100644 --- a/code/controllers/subsystem/verb_manager.dm +++ b/code/controllers/subsystem/verb_manager.dm @@ -22,7 +22,7 @@ SUBSYSTEM_DEF(verb_manager) name = "Verb Manager" wait = 1 - flags = SS_TICKER | SS_NO_INIT + ss_flags = SS_TICKER | SS_NO_INIT priority = FIRE_PRIORITY_DELAYED_VERBS runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT @@ -120,7 +120,7 @@ SUBSYSTEM_DEF(verb_manager) return TRUE if((usr.client?.holder && !can_queue_admin_verbs) \ - || (!initialized && !(flags & SS_NO_INIT)) \ + || (!initialized && !(ss_flags & SS_NO_INIT)) \ || FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs \ || !(runlevels & Master.current_runlevel)) return FALSE diff --git a/code/controllers/subsystem/vis_overlays.dm b/code/controllers/subsystem/vis_overlays.dm index 717455a77bc8..9a4d0c6f08e3 100644 --- a/code/controllers/subsystem/vis_overlays.dm +++ b/code/controllers/subsystem/vis_overlays.dm @@ -1,7 +1,7 @@ SUBSYSTEM_DEF(vis_overlays) name = "Vis contents overlays" wait = 1 MINUTES - flags = SS_NO_INIT + ss_flags = SS_NO_INIT priority = FIRE_PRIORITY_VIS var/list/vis_overlay_cache = list() diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 776c787f0a4a..0d5678a4abe4 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(vote) name = "Vote" wait = 1 SECONDS - flags = SS_KEEP_TIMING + ss_flags = SS_KEEP_TIMING runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT /// A list of all generated action buttons diff --git a/code/controllers/subsystem/wardrobe.dm b/code/controllers/subsystem/wardrobe.dm index 4f25ff47d1c8..f394c9b30d2b 100644 --- a/code/controllers/subsystem/wardrobe.dm +++ b/code/controllers/subsystem/wardrobe.dm @@ -8,7 +8,7 @@ SUBSYSTEM_DEF(wardrobe) name = "Wardrobe" wait = 10 // This is more like a queue then anything else - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND dependencies = list( /datum/controller/subsystem/atoms, /datum/controller/subsystem/mapping, diff --git a/code/controllers/subsystem/weather.dm b/code/controllers/subsystem/weather.dm index c330455fb15f..7a89236e08a2 100644 --- a/code/controllers/subsystem/weather.dm +++ b/code/controllers/subsystem/weather.dm @@ -1,7 +1,7 @@ /// Used for all kinds of weather, ex. lavaland ash storms. SUBSYSTEM_DEF(weather) name = "Weather" - flags = SS_BACKGROUND + ss_flags = SS_BACKGROUND dependencies = list( /datum/controller/subsystem/mapping, ) diff --git a/code/modules/escape_menu/subsystem.dm b/code/modules/escape_menu/subsystem.dm index f75726514ecc..a692a9d1f8e0 100644 --- a/code/modules/escape_menu/subsystem.dm +++ b/code/modules/escape_menu/subsystem.dm @@ -1,6 +1,6 @@ /// Subsystem for controlling anything related to the escape menu PROCESSING_SUBSYSTEM_DEF(escape_menu) name = "Escape Menu" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT runlevels = ALL wait = 2 SECONDS diff --git a/code/modules/lootpanel/ss_looting.dm b/code/modules/lootpanel/ss_looting.dm index 94e12f88f952..eb8cff36e696 100644 --- a/code/modules/lootpanel/ss_looting.dm +++ b/code/modules/lootpanel/ss_looting.dm @@ -2,7 +2,7 @@ /// Queues image generation for search objects without icons SUBSYSTEM_DEF(looting) name = "Loot Icon Generation" - flags = SS_NO_INIT + ss_flags = SS_NO_INIT priority = FIRE_PRIORITY_PROCESS runlevels = RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT wait = 0.5 SECONDS diff --git a/code/modules/research/techweb/__techweb_helpers.dm b/code/modules/research/techweb/__techweb_helpers.dm index d1d3b6ecfded..34c076d59466 100644 --- a/code/modules/research/techweb/__techweb_helpers.dm +++ b/code/modules/research/techweb/__techweb_helpers.dm @@ -34,6 +34,6 @@ for(var/i in pointlist) var/research_line = "[(i in SSresearch.point_types) || "ERRORED POINT TYPE"]: [pointlist[i]]" if(last_pointlist[i] > 0) - research_line += " (+[(last_pointlist[i]) * ((SSresearch.flags & SS_TICKER)? (600 / (world.tick_lag * SSresearch.wait)) : (600 / SSresearch.wait))]/ minute)" + research_line += " (+[(last_pointlist[i]) * ((SSresearch.ss_flags & SS_TICKER)? (600 / (world.tick_lag * SSresearch.wait)) : (600 / SSresearch.wait))]/ minute)" ret += research_line return ret.Join("
") diff --git a/code/modules/unit_tests/subsystem_init.dm b/code/modules/unit_tests/subsystem_init.dm index 82c9fc956fba..f5168079a907 100644 --- a/code/modules/unit_tests/subsystem_init.dm +++ b/code/modules/unit_tests/subsystem_init.dm @@ -3,12 +3,12 @@ /datum/unit_test/subsystem_init/Run() for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems) - if(subsystem.flags & SS_NO_INIT) + if(subsystem.ss_flags & SS_NO_INIT) continue if(subsystem.initialized) continue - var/should_fail = !(subsystem.flags & SS_OK_TO_FAIL_INIT) + var/should_fail = !(subsystem.ss_flags & SS_OK_TO_FAIL_INIT) var/list/message_strings = list("[subsystem] ([subsystem.type]) is a subsystem meant to initialize but could not get initialized.") if(!isnull(subsystem.initialization_failure_message)) From 9560132ff0c08e23dd38c505770f6dc44992040f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:06:53 +0000 Subject: [PATCH 112/155] Automatic changelog for PR #95439 [ci skip] --- html/changelogs/AutoChangeLog-pr-95439.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95439.yml diff --git a/html/changelogs/AutoChangeLog-pr-95439.yml b/html/changelogs/AutoChangeLog-pr-95439.yml new file mode 100644 index 000000000000..6d6e7876570f --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95439.yml @@ -0,0 +1,4 @@ +author: "LemonInTheDark" +delete-after: True +changes: + - admin: "Subsystem flags are now displayed as flags instead of raw numbers in vv" \ No newline at end of file From a7ab79f34f97cf66523b6aeb92d3f5bee00cdc9d Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 00:00:20 +0000 Subject: [PATCH 113/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95389.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95439.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95446.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95464.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95476.yml | 4 ---- html/changelogs/archive/2026-03.yml | 12 ++++++++++++ 6 files changed, 12 insertions(+), 20 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95389.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95439.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95446.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95464.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95476.yml diff --git a/html/changelogs/AutoChangeLog-pr-95389.yml b/html/changelogs/AutoChangeLog-pr-95389.yml deleted file mode 100644 index 447824960eca..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95389.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - qol: "Audit log formats vendors without \"the\"" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95439.yml b/html/changelogs/AutoChangeLog-pr-95439.yml deleted file mode 100644 index 6d6e7876570f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95439.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "LemonInTheDark" -delete-after: True -changes: - - admin: "Subsystem flags are now displayed as flags instead of raw numbers in vv" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95446.yml b/html/changelogs/AutoChangeLog-pr-95446.yml deleted file mode 100644 index 410c8de8d58f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95446.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - spellcheck: "Shoving someone into another person no longer sends the \"you shoved\" message to the person who was shoved into" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95464.yml b/html/changelogs/AutoChangeLog-pr-95464.yml deleted file mode 100644 index 96c866a75d37..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95464.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - bugfix: "Monkey dust now properly removes anti-stun properties when its imbiber is no longer a monkey" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95476.yml b/html/changelogs/AutoChangeLog-pr-95476.yml deleted file mode 100644 index 8cba0b3148ae..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95476.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "FalloutFalcon" -delete-after: True -changes: - - spellcheck: "removed extra space on spraying an atmos machine with spray can" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 13063993dcff..f98438d96902 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -335,3 +335,15 @@ - qol: building the DNA vault and BSA points to the tiles blocking it's construction. Melbert: - image: Generic surgical operations now have unique surgery radial icons +2026-03-23: + FalloutFalcon: + - spellcheck: removed extra space on spraying an atmos machine with spray can + LemonInTheDark: + - admin: Subsystem flags are now displayed as flags instead of raw numbers in vv + Melbert: + - qol: Audit log formats vendors without "the" + lelandkemble: + - bugfix: Monkey dust now properly removes anti-stun properties when its imbiber + is no longer a monkey + - spellcheck: Shoving someone into another person no longer sends the "you shoved" + message to the person who was shoved into From 6734968a57424df4c72528cf4e644bc33c06a0e5 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:01:48 -0500 Subject: [PATCH 114/155] Adds a 0 point quirk that turns your body into a recovered crewmember (effectively) on DNRing (#95469) ## About The Pull Request Adds `Thanatorenasia`, a 0 point neutral quirk Those with `Thanatorenasia`, upon DNRing, their body will be made into a "recovered crewmember" - meaning when the body is revived, ghosts will be polled and allowed to take control over it The body will retain `Thanatorenasia` after the fact, so the new controller can DNR as well, continuing the cycle ## Why It's Good For The Game A random idea I had based on a recent interaction that I thought could be cool character gimmick. Every time someone dies, they get revived with a different player, so they act differently, think differently, and don't remember the rest of the round. ## Changelog :cl: Melbert add: Adds Thanatorenasia, a 0 point neutral quirk that turns your body into a "recovered crewmember" if you choose to DNR - meaning if revived, any ghost could take control of it refactor: Refactored "ghostrol on revive" behavior a fair bit, report any oddities with it / the spawners menu /:cl: --- .../signals/signals_mob/signals_mob_living.dm | 3 + code/__DEFINES/quirks.dm | 2 + code/__DEFINES/traits/declarations.dm | 3 - code/_globalvars/traits/_traits.dm | 1 - code/datums/components/ghostrole_on_revive.dm | 215 ++++++++++-------- code/datums/quirks/_quirk.dm | 2 + .../quirks/neutral_quirks/thanatorenasia.dm | 55 +++++ code/datums/spawners_menu.dm | 5 +- code/modules/mob/dead/observer/observer.dm | 2 + code/modules/mob/living/carbon/examine.dm | 4 +- .../chemistry/reagents/toxin_reagents.dm | 8 +- tgstation.dme | 1 + 12 files changed, 194 insertions(+), 107 deletions(-) create mode 100644 code/datums/quirks/neutral_quirks/thanatorenasia.dm diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index 4922c80f9d17..401255a160d0 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -381,3 +381,6 @@ /// Sent to a mob when one of their bodypart's surgery state changes, OR sent from the basic_surgery_state holder when its surgery state changes (old_state, new_state, changed_states) #define COMSIG_LIVING_UPDATING_SURGERY_STATE "carbon_updating_surgery_state" + +/// Sent to a mob when its player DNRs +#define COMSIG_LIVING_DNR "living_dnr" diff --git a/code/__DEFINES/quirks.dm b/code/__DEFINES/quirks.dm index 8cc1dee678ba..41b2882e37e8 100644 --- a/code/__DEFINES/quirks.dm +++ b/code/__DEFINES/quirks.dm @@ -17,3 +17,5 @@ /// Quirk is similar to brain trauma and should be shown in medical guides as one. /// You don't need to set this on quirks that apply a trauma, that's redundant. #define QUIRK_TRAUMALIKE (1<<5) +/// Do not transfer this quirk via transfer_quirk_datums +#define QUIRK_NO_TRANSFER (1<<6) diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 8365b5a58ede..dea44f553155 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1597,9 +1597,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Trait that signals to objects on this turf that its open (has UNDERFLOOR_INTERACTIBLE) but still covers them #define TRAIT_UNCOVERED_TURF "uncovered_turf" -/// A trait that blocks the metabolism of formaldehyde -#define TRAIT_BLOCK_FORMALDEHYDE_METABOLISM "block_formaldehyde_metabolism" - ///Attached to objects currently on tables and such, allowing them to walk on other objects without the climbing delay #define TRAIT_ON_CLIMBABLE "on_climbable" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 8f46c4954372..58afc53aff72 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -195,7 +195,6 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_BIRTHDAY_BOY" = TRAIT_BIRTHDAY_BOY, "TRAIT_BLOB_ALLY" = TRAIT_BLOB_ALLY, "TRAIT_BLOCKING_PROJECTILES" = TRAIT_BLOCKING_PROJECTILES, - "TRAIT_BLOCK_FORMALDEHYDE_METABOLISM" = TRAIT_BLOCK_FORMALDEHYDE_METABOLISM, "TRAIT_BLOCK_SECHUD" = TRAIT_BLOCK_SECHUD, "TRAIT_BLOCK_SHUTTLE_MOVEMENT" = TRAIT_BLOCK_SHUTTLE_MOVEMENT, "TRAIT_BLOODSHOT_EYES" = TRAIT_BLOODSHOT_EYES, diff --git a/code/datums/components/ghostrole_on_revive.dm b/code/datums/components/ghostrole_on_revive.dm index a8930831c887..52ea1d0bd073 100644 --- a/code/datums/components/ghostrole_on_revive.dm +++ b/code/datums/components/ghostrole_on_revive.dm @@ -6,14 +6,31 @@ var/datum/callback/on_successful_revive /// The chance to twitch when orbiting the spawn var/twitch_chance = 30 - -/datum/component/ghostrole_on_revive/Initialize(refuse_revival_if_failed, on_successful_revive) - . = ..() - + /// The title shown to ghosts when polled to enter the body + var/revive_title + + // Text displayed in the spawners menu + var/spawn_text + var/you_are_text + var/flavor_text + var/important_text + +/datum/component/ghostrole_on_revive/Initialize( + refuse_revival_if_failed = FALSE, + datum/callback/on_successful_revive, + revive_title = "a recovered crewmember", + spawn_text = "Recovered Crew", + you_are_text = "You are a long dead crewmember, but are soon to be revived to rejoin the crew!", + flavor_text = "Get a job and get back to work!", + important_text = "Do your best to help the station. You still roll for midround antagonists." +) src.refuse_revival_if_failed = refuse_revival_if_failed src.on_successful_revive = on_successful_revive - - ADD_TRAIT(parent, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) //for adding an alternate examination + src.revive_title = revive_title + src.spawn_text = spawn_text + src.you_are_text = you_are_text + src.flavor_text = flavor_text + src.important_text = important_text if(ismob(parent)) prepare_mob(parent) @@ -22,138 +39,152 @@ if(!istype(parent, /obj/item/organ/brain)) return COMPONENT_INCOMPATIBLE - var/obj/item/organ/brain/brein = parent - if(brein.owner) - prepare_mob(brein.owner) + var/obj/item/organ/brain/brain_parent = parent + if(brain_parent.owner) + prepare_mob(brain_parent.owner) else - prepare_brain(brein) + prepare_brain(brain_parent) /// Give the appropriate signals, and watch for organ removal -/datum/component/ghostrole_on_revive/proc/prepare_mob(mob/living/liver) - RegisterSignal(liver, COMSIG_LIVING_REVIVE, PROC_REF(on_revive)) - ADD_TRAIT(liver, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) - - add_orbit_twitching(liver) - - liver.med_hud_set_status() - - if(iscarbon(liver)) - var/mob/living/carbon/carbon = liver - var/obj/item/organ/brain = carbon.get_organ_by_type(/obj/item/organ/brain) - if(brain) - RegisterSignal(brain, COMSIG_ORGAN_REMOVED, PROC_REF(on_remove)) - -/datum/component/ghostrole_on_revive/proc/on_remove(obj/item/organ/brain, mob/living/old_owner) +/datum/component/ghostrole_on_revive/proc/prepare_mob(mob/living/to_prepare) + RegisterSignal(to_prepare, COMSIG_LIVING_REVIVE, PROC_REF(on_revive)) + RegisterSignal(to_prepare, COMSIG_MOB_REAGENT_TICK, PROC_REF(block_formaldehyde_metabolism)) + if(istype(parent, /obj/item/organ/brain)) + RegisterSignal(to_prepare, COMSIG_CARBON_LOSE_ORGAN, PROC_REF(on_remove)) + ADD_TRAIT(to_prepare, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) + + add_orbit_twitching(to_prepare) + to_prepare.med_hud_set_status() + +/datum/component/ghostrole_on_revive/proc/unprepare_mob(mob/living/to_unprepare) + REMOVE_TRAIT(to_unprepare, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) + UnregisterSignal(to_unprepare, list( + COMSIG_LIVING_REVIVE, + COMSIG_MOB_REAGENT_TICK, + COMSIG_CARBON_LOSE_ORGAN, + )) + + to_unprepare.med_hud_set_status() + remove_orbit_twitching(to_unprepare) + +/datum/component/ghostrole_on_revive/proc/on_remove(mob/living/carbon/source, obj/item/organ/removed_brain) SIGNAL_HANDLER - REMOVE_TRAIT(old_owner, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) - remove_orbit_twitching(old_owner) + if(!istype(removed_brain, /obj/item/organ/brain)) + return + + unprepare_mob(source) // we might have some lingering blinking eyes - var/obj/item/bodypart/head/head = old_owner?.get_bodypart(BODY_ZONE_HEAD) + var/obj/item/bodypart/head/head = source.get_bodypart(BODY_ZONE_HEAD) if(head) var/soul_eyes = locate(/datum/bodypart_overlay/simple/soul_pending_eyes) in head.bodypart_overlays if(soul_eyes) head.remove_bodypart_overlay(soul_eyes) - prepare_brain(brain) + prepare_brain(removed_brain) -/datum/component/ghostrole_on_revive/proc/prepare_brain(obj/item/organ/brein) - SIGNAL_HANDLER +/datum/component/ghostrole_on_revive/proc/prepare_brain(obj/item/organ/source) + ADD_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) + RegisterSignal(source, COMSIG_ORGAN_IMPLANTED, PROC_REF(prepare_mob_from_brain)) + UnregisterSignal(source, COMSIG_ORGAN_REMOVED) - RegisterSignal(brein, COMSIG_ORGAN_IMPLANTED, PROC_REF(prepare_mob_from_brain)) - UnregisterSignal(brein, COMSIG_ORGAN_REMOVED) +/datum/component/ghostrole_on_revive/proc/unprepare_brain(obj/item/organ/source) + REMOVE_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) + UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED) + RegisterSignal(source, COMSIG_ORGAN_REMOVED, PROC_REF(prepare_brain)) + source.owner?.med_hud_set_status() -/datum/component/ghostrole_on_revive/proc/prepare_mob_from_brain(obj/item/organ/brain/brein, mob/living/owner) +/datum/component/ghostrole_on_revive/proc/prepare_mob_from_brain(obj/item/organ/brain/source, mob/living/owner) SIGNAL_HANDLER - UnregisterSignal(brein, COMSIG_ORGAN_IMPLANTED) + REMOVE_TRAIT(source, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) + UnregisterSignal(source, COMSIG_ORGAN_IMPLANTED) prepare_mob(owner) -/datum/component/ghostrole_on_revive/proc/on_revive(mob/living/aliver) +/datum/component/ghostrole_on_revive/proc/on_revive(mob/living/source) SIGNAL_HANDLER - INVOKE_ASYNC(src, PROC_REF(poll_ghosts), aliver) + INVOKE_ASYNC(src, PROC_REF(poll_ghosts), source) -/datum/component/ghostrole_on_revive/proc/poll_ghosts(mob/living/aliver) - var/soul_eyes - var/obj/item/bodypart/head +/datum/component/ghostrole_on_revive/proc/poll_ghosts(mob/living/reviving) + var/datum/bodypart_overlay/simple/soul_pending_eyes/soul_eyes // adds soulful SOUL PENDING eyes to indicate what's happening to observers - - var/mob/living/carbon/human/hewmon - if(ishuman(aliver)) - hewmon = aliver - head = hewmon.get_bodypart(BODY_ZONE_HEAD) - if(head) - soul_eyes = new /datum/bodypart_overlay/simple/soul_pending_eyes() - head.add_bodypart_overlay(soul_eyes) - - // So during the potentially short period of time we're revived, we don't metabolize formaldehyde - // Really just a QOL for coroners, so we dont suddenly start decaying - ADD_TRAIT(aliver, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM, type) + var/obj/item/bodypart/head/head = reviving.get_bodypart(BODY_ZONE_HEAD) + if(head) + soul_eyes = new() + head.add_bodypart_overlay(soul_eyes) + + var/list/jobbans = list(ROLE_RECOVERED_CREW) + if(reviving.mind) + jobbans |= reviving.mind.assigned_role.title + for(var/datum/antagonist/antag as anything in reviving.mind.antag_datums) + if(antag.jobban_flag) + jobbans |= antag.jobban_flag + if(antag.pref_flag) + jobbans |= antag.pref_flag + if(!(antag.antag_flags & ANTAG_FAKE)) + jobbans |= ROLE_SYNDICATE var/mob/dead/observer/chosen_one = SSpolling.poll_ghosts_for_target( - question = "Would you like to play as a recovered crewmember?", - role = null, - check_jobban = ROLE_RECOVERED_CREW, + question = "Would you like to play as [revive_title]?", + check_jobban = jobbans, poll_time = 15 SECONDS, - checked_target = aliver, + checked_target = reviving, ignore_category = POLL_IGNORE_RECOVERED_CREW, - alert_pic = aliver, - role_name_text = "recovered crew", + alert_pic = reviving, + role_name_text = revive_title, ) - REMOVE_TRAIT(aliver, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM, type) - if(head) head.remove_bodypart_overlay(soul_eyes) + if(soul_eyes) + qdel(soul_eyes) - if(!isobserver(chosen_one)) - if(refuse_revival_if_failed) - aliver.death() - aliver.visible_message(span_deadsay("[aliver.name]'s soul is struggling to return!")) - else - aliver.PossessByPlayer(chosen_one.ckey) - on_successful_revive?.Invoke(aliver) + if(isobserver(chosen_one)) + reviving.PossessByPlayer(chosen_one.ckey) + on_successful_revive?.Invoke(reviving) qdel(src) -/datum/component/ghostrole_on_revive/proc/add_orbit_twitching(mob/living/liver) - liver.AddElement(/datum/element/orbit_twitcher, twitch_chance) + else if(refuse_revival_if_failed) + reviving.death() + reviving.visible_message(span_deadsay("[reviving]'s soul is struggling to return!")) + +/datum/component/ghostrole_on_revive/proc/add_orbit_twitching(mob/living/parent_mob) + parent_mob.AddElement(/datum/element/orbit_twitcher, twitch_chance) // Add it to the ghostrole spawner menu. Note that we can't directly spawn from it, but we can make it twitch to alert bystanders to defib it - LAZYADD(GLOB.joinable_mobs[format_text("Recovered Crew")], liver) - RegisterSignal(liver, COMSIG_LIVING_GHOSTROLE_INFO, PROC_REF(set_spawner_info)) + LAZYADDASSOCLIST(GLOB.joinable_mobs, spawn_text, parent_mob) + RegisterSignal(parent_mob, COMSIG_LIVING_GHOSTROLE_INFO, PROC_REF(set_spawner_info)) -/datum/component/ghostrole_on_revive/proc/set_spawner_info(datum/spawners_menu/menu, string_info) +/datum/component/ghostrole_on_revive/proc/set_spawner_info(datum/spawners_menu/menu, list/string_info) SIGNAL_HANDLER - string_info["you_are_text"] = "You are a long dead crewmember, but are soon to be revived to rejoin the crew!" - string_info["flavor_text"] = "Get a job and get back to work!" - string_info["important_text"] = "Do your best to help the station. You still roll for midround antagonists." - -/datum/component/ghostrole_on_revive/proc/remove_orbit_twitching(mob/living/living) - living.RemoveElement(/datum/element/orbit_twitcher, twitch_chance) + string_info["you_are_text"] = src.you_are_text + string_info["flavor_text"] = src.flavor_text + string_info["important_text"] = src.important_text - // Remove from the ghostrole spawning menu - var/list/spawners = GLOB.joinable_mobs[format_text("Recovered Crew")] - LAZYREMOVE(spawners, living) +/datum/component/ghostrole_on_revive/proc/remove_orbit_twitching(mob/living/parent_mob) + parent_mob.RemoveElement(/datum/element/orbit_twitcher, twitch_chance) + LAZYREMOVEASSOC(GLOB.joinable_mobs, spawn_text, parent_mob) + UnregisterSignal(parent_mob, COMSIG_LIVING_GHOSTROLE_INFO) - if(!LAZYLEN(spawners)) - GLOB.joinable_mobs -= format_text("Recovered Crew") +// Block formaldehyde from being metabolized, Coroner QoL +/datum/component/ghostrole_on_revive/proc/block_formaldehyde_metabolism(mob/living/source, datum/reagent/chem) + SIGNAL_HANDLER - UnregisterSignal(living, COMSIG_LIVING_GHOSTROLE_INFO) + if(istype(chem, /datum/reagent/toxin/formaldehyde)) + return COMSIG_MOB_STOP_REAGENT_TICK /datum/component/ghostrole_on_revive/Destroy(force) - REMOVE_TRAIT(parent, TRAIT_GHOSTROLE_ON_REVIVE, REF(src)) - - var/mob/living/living if(isliving(parent)) - living = parent + unprepare_mob(parent) + else if(istype(parent, /obj/item/organ/brain)) var/obj/item/organ/brain/brain = parent - living = brain.owner - living?.med_hud_set_status() - if(living) - remove_orbit_twitching(living) + if(brain.owner) + unprepare_mob(brain.owner) + else + unprepare_brain(brain) return ..() diff --git a/code/datums/quirks/_quirk.dm b/code/datums/quirks/_quirk.dm index e8d6748faa37..46be5ac25a59 100644 --- a/code/datums/quirks/_quirk.dm +++ b/code/datums/quirks/_quirk.dm @@ -286,5 +286,7 @@ var/datum/preferences/to_pass = client || to_mob.client for(var/datum/quirk/quirk as anything in quirks) + if(quirk.quirk_flags & QUIRK_NO_TRANSFER) + continue quirk.remove_from_current_holder(quirk_transfer = TRUE) quirk.add_to_holder(to_mob, quirk_transfer = TRUE, client_source = to_pass) diff --git a/code/datums/quirks/neutral_quirks/thanatorenasia.dm b/code/datums/quirks/neutral_quirks/thanatorenasia.dm new file mode 100644 index 000000000000..6b2d1303fb8a --- /dev/null +++ b/code/datums/quirks/neutral_quirks/thanatorenasia.dm @@ -0,0 +1,55 @@ +/datum/quirk/death_dnr_poll + name = "Thanatorenasia" + desc = "Whenever you die and elect to \"Do Not Resuscitate\", your body may be taken over by another ghost upon revival - \ + giving it an entirely new personality and fresh set of memories." + icon = FA_ICON_ZAP + value = 0 + medical_record_text = "Patient has Thanatorenasia - in the event of their death and resuscitation, \ + they may experience memory loss or a change in personality." + medical_symptom_text = "In the event of the patient's death and resuscitation, \ + they may experience memory loss or a change in personality." + quirk_flags = QUIRK_NO_TRANSFER + + // Used the the spawners menu to describe the quirk + var/you_are_text = "You are a deceased crewmember, afflicted with Thanatorenasia - \ + a condition which alters personality and causes memory loss upon death and revival." + var/flavor_text = "Something feels... different. \ + You're not entirely sure who you are or what happened - All you remember is your name and that you work here. \ + Oh well, better get back to work - the last thing you want is to be both unemployed AND an amnesiac." + var/important_text = "Resume your assigned duty. \ + If you choose to \"Do Not Resuscitate\" upon death, another ghost will be allowed to take over the body. \ + You still roll for midround antagonists." + +/datum/quirk/death_dnr_poll/add_unique(client/client_source) + . = ..() + RegisterSignal(quirk_holder, COMSIG_LIVING_DNR, PROC_REF(mob_died)) + +/datum/quirk/death_dnr_poll/remove() + . = ..() + UnregisterSignal(quirk_holder, COMSIG_LIVING_DNR) + +/datum/quirk/death_dnr_poll/proc/mob_died(mob/living/source, mob/dead/observer/dnring) + SIGNAL_HANDLER + + var/whomst = source.real_name + if(source.mind && !is_unassigned_job(source.mind.assigned_role)) + whomst += "Job: [span_notice(source.mind.assigned_role.title)]." + if(length(source.mind?.get_special_roles())) + whomst += "Status: [span_boldnotice(english_list(source.mind?.get_special_roles()))]." + + source.AddComponent(/datum/component/ghostrole_on_revive, \ + refuse_revival_if_failed = TRUE, \ + on_successful_revive = CALLBACK(src, PROC_REF(on_successful_revive)), \ + revive_title = whomst, \ + spawn_text = "Deceased Crew", \ + you_are_text = src.you_are_text, \ + flavor_text = src.flavor_text, \ + important_text = src.important_text, \ + ) + source.log_message("was made ghostrole pollable by [name] quirk.", LOG_GAME, color = COLOR_PURPLE) + +/datum/quirk/death_dnr_poll/proc/on_successful_revive() + quirk_holder.log_message("has had their body taken over by a ghost due to the [name] quirk.", LOG_GAME, color = COLOR_PURPLE) + var/welcome_msg = boxed_message(span_notice("[quirk_holder.real_name] has [name] - you are [quirk_holder.p_their()] new owner.
\ + If you choose to \"Do Not Resuscitate\" upon death, another ghost will take over the body once again.")) + addtimer(CALLBACK(src, GLOBAL_PROC_REF(to_chat), quirk_holder, welcome_msg), 2 SECONDS) diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm index b6adf6782e64..1c91fbd48da3 100644 --- a/code/datums/spawners_menu.dm +++ b/code/datums/spawners_menu.dm @@ -50,9 +50,10 @@ this["name"] = mob_type this["amount_left"] = 0 for(var/mob/joinable_mob as anything in GLOB.joinable_mobs[mob_type]) + SEND_SIGNAL(joinable_mob, COMSIG_LIVING_GHOSTROLE_INFO, this) this["amount_left"] += 1 - if(!SEND_SIGNAL(joinable_mob, COMSIG_LIVING_GHOSTROLE_INFO, this)) - this["desc"] = initial(joinable_mob.desc) + this["desc"] ||= initial(joinable_mob.desc) + if(this["amount_left"] > 0) data["spawners"] += list(this) return data diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index e8e26e5de0ae..80d383faba9a 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -410,6 +410,8 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp // Update med huds current_mob.med_hud_set_status() current_mob.log_message("had their player ([key_name(src)]) do-not-resuscitate / DNR", LOG_GAME, color = COLOR_GREEN, log_globally = FALSE) + SEND_SIGNAL(current_mob, COMSIG_LIVING_DNR, src) + log_message("has opted to do-not-resuscitate / DNR from their body ([current_mob])", LOG_GAME, color = COLOR_GREEN) // Disassociates observer mind from the body mind diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index 279e65b83552..6dc3007864c6 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -237,7 +237,7 @@ var/obj/item/organ/brain/brain = get_organ_by_type(/obj/item/organ/brain) if(brain && isnull(ai_controller)) var/npc_message = "" - if(HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE)) + if(HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE) || HAS_TRAIT(src, TRAIT_GHOSTROLE_ON_REVIVE)) npc_message = "Soul is pending..." else if(!key) npc_message = "[t_He] [t_is] totally catatonic. The stresses of life in deep-space must have been too much for [t_him]. Any recovery is unlikely." @@ -324,7 +324,7 @@ var/t_is = p_are() //This checks to see if the body is revivable var/obj/item/organ/brain = get_organ_by_type(/obj/item/organ/brain) - if(brain && HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE)) + if((brain && HAS_TRAIT(brain, TRAIT_GHOSTROLE_ON_REVIVE)) || HAS_TRAIT(src, TRAIT_GHOSTROLE_ON_REVIVE)) return span_deadsay("[t_He] [t_is] limp and unresponsive; but [t_his] soul might yet come back...") var/client_like = client || HAS_TRAIT(src, TRAIT_MIND_TEMPORARILY_GONE) var/valid_ghost = ghost?.can_reenter_corpse && ghost?.client diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index 4c95b21afe75..41cba75b8bce 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -785,17 +785,11 @@ if(affected_mob.adjust_tox_loss(-1 * metabolization_ratio * seconds_per_tick, updating_health = FALSE, required_biotype = affected_biotype)) //it counteracts its own toxin damage. return UPDATE_MOB_HEALTH return - else if(SPT_PROB(2.5, seconds_per_tick) && !HAS_TRAIT(affected_mob, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM)) + else if(SPT_PROB(2.5, seconds_per_tick)) holder.add_reagent(/datum/reagent/toxin/histamine, pick(5,15)) holder.remove_reagent(/datum/reagent/toxin/formaldehyde, 2.4 * metabolization_ratio * seconds_per_tick) return ..() -/datum/reagent/toxin/formaldehyde/metabolize_reagent(mob/living/carbon/affected_mob, seconds_per_tick, metabolized_volume) - if(HAS_TRAIT(affected_mob, TRAIT_BLOCK_FORMALDEHYDE_METABOLISM)) - return - - return ..() - /datum/reagent/toxin/venom name = "Venom" description = "An exotic poison extracted from highly toxic fauna. Causes scaling amounts of toxin damage and bruising depending and dosage. Often decays into Histamine." diff --git a/tgstation.dme b/tgstation.dme index b28377361c57..a815bb3c542d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1953,6 +1953,7 @@ #include "code\datums\quirks\neutral_quirks\pineapple_hater.dm" #include "code\datums\quirks\neutral_quirks\pineapple_liker.dm" #include "code\datums\quirks\neutral_quirks\shifty_eyes.dm" +#include "code\datums\quirks\neutral_quirks\thanatorenasia.dm" #include "code\datums\quirks\neutral_quirks\transhumanist.dm" #include "code\datums\quirks\neutral_quirks\vegetarian.dm" #include "code\datums\quirks\positive_quirks\alcohol_tolerance.dm" From 33be0d05555162ca776d5220d8721e3a2e043584 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:02:10 +0000 Subject: [PATCH 115/155] Automatic changelog for PR #95469 [ci skip] --- html/changelogs/AutoChangeLog-pr-95469.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95469.yml diff --git a/html/changelogs/AutoChangeLog-pr-95469.yml b/html/changelogs/AutoChangeLog-pr-95469.yml new file mode 100644 index 000000000000..763c894a6c7a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95469.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - rscadd: "Adds Thanatorenasia, a 0 point neutral quirk that turns your body into a \"recovered crewmember\" if you choose to DNR - meaning if revived, any ghost could take control of it" + - refactor: "Refactored \"ghostrol on revive\" behavior a fair bit, report any oddities with it / the spawners menu" \ No newline at end of file From b6fd7ea891e12217525a5ced09ae7ae2f7a65ac6 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Mon, 23 Mar 2026 02:21:07 +0100 Subject: [PATCH 116/155] Buffs blue and purple raptors a bit, and fixes chasm immunity bugs (#95414) ## About The Pull Request Increases blue raptors' HP to 300, and makes purple raptors automatically initiate flight if you are about to fall into a chasm while wearing one on your back. Also fixes 2 exploits involving chasms which would render you immune to that particular chasm. ## Why It's Good For The Game I've had some feedback on raptors, and more specifically how blue and purples are rather underwhelming, former only being a single feature from old raptors and latter not providing much of a benefit to non-settlers. This should address both of those, making blues a bit tankier and purples being able to save you from an instant death. ## Changelog :cl: balance: Blue raptors had their HP increased from 220 to 300 balance: Purple raptors now automatically start flying if the owner is about to fall into a chasm. fix: Fixed 2 exploits which would render you immune to falling down a specific chasm. /:cl: --- code/datums/components/chasm.dm | 3 +++ .../basic/lavaland/raptor/raptor_color.dm | 27 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm index 69f1ea69cf7a..28ae7a18d5be 100644 --- a/code/datums/components/chasm.dm +++ b/code/datums/components/chasm.dm @@ -145,6 +145,7 @@ return // We're already handling this if(SEND_SIGNAL(dropped_thing, COMSIG_MOVABLE_CHASM_DROPPED, parent) & COMPONENT_NO_CHASM_DROP) + LAZYREMOVE(falling_atoms, falling_ref) return // Free (if possible) and drop all buckled mobs separately, so drivers can escape their doomed vehicle if they're not glued to it @@ -184,6 +185,7 @@ if (get_turf(falling_mob) != get_turf(parent)) REMOVE_TRAIT(falling_mob, TRAIT_NO_TRANSFORM, REF(src)) falling_mob.Paralyze(17 SECONDS, ignore_canstun = TRUE) // Wow nice job + LAZYREMOVE(falling_atoms, falling_ref) return dropped_thing.visible_message(span_boldwarning("[dropped_thing] falls into [parent]!"), span_userdanger("[oblivion_message]")) @@ -213,6 +215,7 @@ storage = (locate() in parent) || new(parent) if(storage.contains(dropped_thing)) + LAZYREMOVE(falling_atoms, falling_ref) return dropped_thing.alpha = oldalpha diff --git a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm index 4270155c4294..c010da01b90e 100644 --- a/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm +++ b/code/modules/mob/living/basic/lavaland/raptor/raptor_color.dm @@ -197,8 +197,8 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) COMSIG_RAPTOR_WINGS_OPENED, \ COMSIG_RAPTOR_WINGS_CLOSED, \ null, \ - CALLBACK(src, PROC_REF(can_fly)), \ - CALLBACK(src, PROC_REF(can_fly)), \ + CALLBACK(src, PROC_REF(check_flight)), \ + CALLBACK(src, PROC_REF(check_flight)), \ ) /obj/item/mob_holder/purple_raptor/Destroy() @@ -212,9 +212,11 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) if ((slot & ITEM_SLOT_BACK) && ishuman(user) && flight_action) flight_action.Grant(held_mob) flight_action.GiveAction(user) + RegisterSignal(user, COMSIG_MOVABLE_CHASM_DROPPED, PROC_REF(chasm_react)) /obj/item/mob_holder/purple_raptor/dropped(mob/user, silent) . = ..() + UnregisterSignal(user, COMSIG_MOVABLE_CHASM_DROPPED) if (wings_open) toggle_wings(user) // Removed in Destroy() @@ -232,7 +234,7 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) source.remove_movespeed_modifier(/datum/movespeed_modifier/jetpack/raptor) source.add_movespeed_modifier(/datum/movespeed_modifier/jetpack/raptor/slow) -/obj/item/mob_holder/purple_raptor/proc/can_fly() +/obj/item/mob_holder/purple_raptor/proc/can_fly(silent = FALSE) var/mob/living/carbon/human/user = loc if (!istype(user) || user.stat || user.body_position == LYING_DOWN || isnull(user.client)) return FALSE @@ -245,9 +247,13 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) if (environment?.return_pressure() >= HAZARD_LOW_PRESSURE + 10) return TRUE - to_chat(user, span_warning("The atmosphere is too thin for you to fly!")) + if (!silent) + to_chat(user, span_warning("The atmosphere is too thin for you to fly!")) return FALSE +/obj/item/mob_holder/purple_raptor/proc/check_flight() + return can_fly(silent = TRUE) + /obj/item/mob_holder/purple_raptor/proc/toggle_wings(mob/living/carbon/human/user) // In case something goes wrong if (!istype(user)) @@ -299,8 +305,18 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) UnregisterSignal(user, list(COMSIG_HUMAN_HEIGHT_UPDATED, SIGNAL_ADDTRAIT(TRAIT_FAT), SIGNAL_REMOVETRAIT(TRAIT_FAT))) SEND_SIGNAL(src, COMSIG_RAPTOR_WINGS_CLOSED, user) +/obj/item/mob_holder/purple_raptor/proc/chasm_react(mob/living/user, turf/chasm) + SIGNAL_HANDLER + + if (wings_open || !can_fly()) + return + + toggle_wings(user) + if (wings_open) + return COMPONENT_NO_CHASM_DROP + /obj/item/mob_holder/purple_raptor/process(seconds_per_tick) - if (!can_fly()) + if (!can_fly(silent = TRUE)) toggle_wings(loc) return PROCESS_KILL @@ -406,6 +422,7 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors()) /datum/raptor_color/blue color = "blue" description = "Covered in tough, lava-resistant feathers with thick insulated fur underneath, this breed is capable of marching through lava and fire alike." + health = 300 guaranteed_crossbreeds = list( /datum/raptor_color/red = /datum/raptor_color/purple, /datum/raptor_color/white = /datum/raptor_color/green, From 4720cfe6f11651af828db08e4ebc2e1484f311c1 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:21:31 +0000 Subject: [PATCH 117/155] Automatic changelog for PR #95414 [ci skip] --- html/changelogs/AutoChangeLog-pr-95414.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95414.yml diff --git a/html/changelogs/AutoChangeLog-pr-95414.yml b/html/changelogs/AutoChangeLog-pr-95414.yml new file mode 100644 index 000000000000..f4d08bd2d56b --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95414.yml @@ -0,0 +1,6 @@ +author: "SmArtKar" +delete-after: True +changes: + - balance: "Blue raptors had their HP increased from 220 to 300" + - balance: "Purple raptors now automatically start flying if the owner is about to fall into a chasm." + - bugfix: "Fixed 2 exploits which would render you immune to falling down a specific chasm." \ No newline at end of file From 24aa9b1faa40feb8ccca9ad534dd774ec7947b4b Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:09:24 -0500 Subject: [PATCH 118/155] Add blood splatter events to a few places (#95457) ## About The Pull Request Adds `/datum/mood_event/splattered_with_blood` to two places you are explicitly splattered with blood ## Why It's Good For The Game Consistency ## Changelog :cl: Melbert qol: More places causes the "splattered with blood" mood event /:cl: --- code/modules/antagonists/changeling/powers/headcrab.dm | 1 + code/modules/mob/living/basic/vermin/cockroach/cockroach.dm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/code/modules/antagonists/changeling/powers/headcrab.dm b/code/modules/antagonists/changeling/powers/headcrab.dm index a0f9040ea1c4..cc1915688ad9 100644 --- a/code/modules/antagonists/changeling/powers/headcrab.dm +++ b/code/modules/antagonists/changeling/powers/headcrab.dm @@ -68,6 +68,7 @@ continue blinded.visible_message(span_danger("[blinded] is splattered with blood!"), span_userdanger("You're splattered with blood!")) blinded.add_blood_DNA(user_DNA) + blinded.add_mood_event("splattered_with_blood", /datum/mood_event/splattered_with_blood) playsound(blinded, 'sound/effects/splat.ogg', 50, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) if(ishuman(blinded)) diff --git a/code/modules/mob/living/basic/vermin/cockroach/cockroach.dm b/code/modules/mob/living/basic/vermin/cockroach/cockroach.dm index 6a1c3f703dfa..76dee59ce357 100644 --- a/code/modules/mob/living/basic/vermin/cockroach/cockroach.dm +++ b/code/modules/mob/living/basic/vermin/cockroach/cockroach.dm @@ -111,6 +111,7 @@ for(var/mob/living/mob_in_turf in messy_turf) mob_in_turf.visible_message(span_danger("[mob_in_turf] is splattered with blood!"), span_userdanger("You're splattered with blood!")) mob_in_turf.add_blood_DNA(list("Non-human DNA" = random_human_blood_type())) + mob_in_turf.add_mood_event("splattered_with_blood", /datum/mood_event/splattered_with_blood) playsound(mob_in_turf, 'sound/effects/splat.ogg', 50, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) return ..() @@ -243,4 +244,3 @@ maxHealth = 2 minion_path = null gold_core_spawnable = NO_SPAWN - From c945d4c9f2753a0b3c1582ae5b803ab4b39f8534 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:00:21 +0000 Subject: [PATCH 119/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95414.yml | 6 ------ html/changelogs/AutoChangeLog-pr-95469.yml | 5 ----- html/changelogs/archive/2026-03.yml | 11 +++++++++++ 3 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95414.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95469.yml diff --git a/html/changelogs/AutoChangeLog-pr-95414.yml b/html/changelogs/AutoChangeLog-pr-95414.yml deleted file mode 100644 index f4d08bd2d56b..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95414.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: "SmArtKar" -delete-after: True -changes: - - balance: "Blue raptors had their HP increased from 220 to 300" - - balance: "Purple raptors now automatically start flying if the owner is about to fall into a chasm." - - bugfix: "Fixed 2 exploits which would render you immune to falling down a specific chasm." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95469.yml b/html/changelogs/AutoChangeLog-pr-95469.yml deleted file mode 100644 index 763c894a6c7a..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95469.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscadd: "Adds Thanatorenasia, a 0 point neutral quirk that turns your body into a \"recovered crewmember\" if you choose to DNR - meaning if revived, any ghost could take control of it" - - refactor: "Refactored \"ghostrol on revive\" behavior a fair bit, report any oddities with it / the spawners menu" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index f98438d96902..faac60f29389 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -342,6 +342,17 @@ - admin: Subsystem flags are now displayed as flags instead of raw numbers in vv Melbert: - qol: Audit log formats vendors without "the" + - rscadd: Adds Thanatorenasia, a 0 point neutral quirk that turns your body into + a "recovered crewmember" if you choose to DNR - meaning if revived, any ghost + could take control of it + - refactor: Refactored "ghostrol on revive" behavior a fair bit, report any oddities + with it / the spawners menu + SmArtKar: + - balance: Blue raptors had their HP increased from 220 to 300 + - balance: Purple raptors now automatically start flying if the owner is about to + fall into a chasm. + - bugfix: Fixed 2 exploits which would render you immune to falling down a specific + chasm. lelandkemble: - bugfix: Monkey dust now properly removes anti-stun properties when its imbiber is no longer a monkey From 69bcef0f8ce08f1606a41ea6139e876c75a57631 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:34:51 +0530 Subject: [PATCH 120/155] Fixes git stale discord workflow (Again!!) (#95449) ## About The Pull Request This is suffering. Tries to fix [This](https://github.com/tgstation/tgstation/actions/runs/23323997716) which occurs after #95415 was merged. Apparently the output of the script had some secret information & invalid variables which it could not store causing the announce workflow to fail. So i filtered out only the essential components I wish i had a way to test this workflow locally on my fork repo but i can't. It's trial and error at this point. ## Changelog N/A --- .github/workflows/stale.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6a32a5a8af11..b52d2fb34e2b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -40,7 +40,15 @@ jobs: uses: actions/github-script@v8 with: script: | - return JSON.parse('${{steps.stale.outputs.staled-issues-prs}}').filter(issue => !!issue.pull_request) + const pull_requests = JSON.parse('${{steps.stale.outputs.staled-issues-prs}}') + .filter(issue => !!issue.pull_request) + .map(pr => ({ + title: pr.title, + number: pr.number, + html_url: pr.pull_request.html_url, + })); + + return JSON.stringify(pull_requests) announce: runs-on: ubuntu-24.04 @@ -64,7 +72,7 @@ jobs: steps.secrets_set.outputs.SECRETS_ENABLED with: webhook_url: ${{ secrets.DISCORD_WEBHOOK }} - title: ${{ matrix.pull_request.user.login }} - ${{ matrix.pull_request.title }} + title: ${{ matrix.pull_request.title }} message: ${{ format('**Pull Request \#{0} automatically marked as stale.**', matrix.pull_request.number) }} include_image: false show_author: false From 38c8884d3cdb37a64e749a4bc42b8f34189f48ff Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Mon, 23 Mar 2026 17:57:33 -0400 Subject: [PATCH 121/155] Adds a boolean type to circuits (#95487) ## About The Pull Request A substantial number of number circuit ports, input and output alike, effectively act as boolean flags. That is, they are either input ports that only care about whether the value is or is not zero, or they are output ports that can only output zero or one. This PR adds a boolean datatype, and converts all these ports to booleans. Anything can be connected to a boolean input port, effectively setting the port to the truthiness of the input. Booleans can be connected to number and signal ports, as they all use numbers as their underlying values. ## Why It's Good For The Game Having a proper boolean datatype makes the affected ports a bit more intuitive to compute with. ## Changelog :cl: qol: Many circuit components have had ports that effectively act as boolean inputs or outputs converted into a new boolean datatype. /:cl: --------- Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> --- code/__DEFINES/wiremod.dm | 2 ++ code/game/machinery/firealarm.dm | 2 +- code/game/machinery/lightswitch.dm | 4 ++-- .../machinery/air_alarm/air_alarm_circuit.dm | 12 ++++++------ .../machinery/components/binary_devices/pump.dm | 2 +- .../machinery/components/binary_devices/valve.dm | 2 +- .../components/binary_devices/volume_pump.dm | 2 +- code/modules/hydroponics/hydroponics.dm | 4 ++-- code/modules/instruments/piano_synth.dm | 2 +- .../modular_computers/file_system/program_circuit.dm | 2 +- code/modules/vehicles/cars/vim.dm | 2 +- code/modules/wiremod/components/abstract/compare.dm | 2 +- code/modules/wiremod/components/action/light.dm | 2 +- .../wiremod/components/action/soundemitter.dm | 2 +- code/modules/wiremod/components/action/speech.dm | 2 +- code/modules/wiremod/components/admin/animate.dm | 2 +- .../admin/signal_handler/signal_handler.dm | 2 +- code/modules/wiremod/components/atom/hear.dm | 2 +- code/modules/wiremod/components/atom/remotecam.dm | 2 +- .../wiremod/components/bci/install_detector.dm | 2 +- code/modules/wiremod/components/id/access_checker.dm | 2 +- code/modules/wiremod/components/id/getter.dm | 2 +- .../wiremod/components/math/binary_conversion.dm | 4 ++-- code/modules/wiremod/components/math/not.dm | 2 +- code/modules/wiremod/components/math/toggle.dm | 2 +- code/modules/wiremod/components/utility/clock.dm | 2 +- code/modules/wiremod/datatypes/boolean.dm | 10 ++++++++++ code/modules/wiremod/datatypes/number.dm | 1 + code/modules/wiremod/datatypes/signal.dm | 1 + code/modules/wiremod/shell/airlock.dm | 4 ++-- .../wiremod/shell/brain_computer_interface.dm | 2 +- code/modules/wiremod/shell/implant.dm | 2 +- code/modules/wiremod/shell/module.dm | 4 ++-- tgstation.dme | 1 + .../IntegratedCircuit/FundamentalTypes.jsx | 11 +++++++++++ 35 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 code/modules/wiremod/datatypes/boolean.dm diff --git a/code/__DEFINES/wiremod.dm b/code/__DEFINES/wiremod.dm index b606f938869a..8922a522c173 100644 --- a/code/__DEFINES/wiremod.dm +++ b/code/__DEFINES/wiremod.dm @@ -30,6 +30,8 @@ #define PORT_TYPE_TABLE "table" /// Options datatype. Derivative of string. #define PORT_TYPE_OPTION "option" +/// Boolean datatype. Derivative of number. +#define PORT_TYPE_BOOLEAN "boolean" // Composite datatypes #define PORT_COMPOSITE_TYPE_LIST "list" diff --git a/code/game/machinery/firealarm.dm b/code/game/machinery/firealarm.dm index 74600bab3841..5aa9520df7f3 100644 --- a/code/game/machinery/firealarm.dm +++ b/code/game/machinery/firealarm.dm @@ -670,7 +670,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/firealarm, 26) alarm_trigger = add_input_port("Set", PORT_TYPE_SIGNAL) reset_trigger = add_input_port("Reset", PORT_TYPE_SIGNAL) - is_on = add_output_port("Is On", PORT_TYPE_NUMBER) + is_on = add_output_port("Is On", PORT_TYPE_BOOLEAN) triggered = add_output_port("Triggered", PORT_TYPE_SIGNAL) reset = add_output_port("Reset", PORT_TYPE_SIGNAL) diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm index 58e5bdf209e3..7d553e47b2d6 100644 --- a/code/game/machinery/lightswitch.dm +++ b/code/game/machinery/lightswitch.dm @@ -135,8 +135,8 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/light_switch, 26) var/obj/machinery/light_switch/attached_switch /obj/item/circuit_component/light_switch/populate_ports() - on_setting = add_input_port("On", PORT_TYPE_NUMBER) - is_on = add_output_port("Is On", PORT_TYPE_NUMBER) + on_setting = add_input_port("On", PORT_TYPE_BOOLEAN) + is_on = add_output_port("Is On", PORT_TYPE_BOOLEAN) /obj/item/circuit_component/light_switch/register_usb_parent(atom/movable/parent) . = ..() diff --git a/code/modules/atmospherics/machinery/air_alarm/air_alarm_circuit.dm b/code/modules/atmospherics/machinery/air_alarm/air_alarm_circuit.dm index 74f260976d52..a653522b9ca3 100644 --- a/code/modules/atmospherics/machinery/air_alarm/air_alarm_circuit.dm +++ b/code/modules/atmospherics/machinery/air_alarm/air_alarm_circuit.dm @@ -311,8 +311,8 @@ disable = add_input_port("Disable", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_scrubber)) request_update = add_input_port("Request Data", PORT_TYPE_SIGNAL, trigger = PROC_REF(update_data)) - enabled = add_output_port("Enabled", PORT_TYPE_NUMBER) - is_siphoning = add_output_port("Siphoning", PORT_TYPE_NUMBER) + enabled = add_output_port("Enabled", PORT_TYPE_BOOLEAN) + is_siphoning = add_output_port("Siphoning", PORT_TYPE_BOOLEAN) filtering = add_output_port("Filtered Gases", PORT_TYPE_LIST(PORT_TYPE_STRING)) update_received = add_output_port("Update Received", PORT_TYPE_SIGNAL) @@ -534,10 +534,10 @@ disable = add_input_port("Disable", PORT_TYPE_SIGNAL, trigger = PROC_REF(toggle_vent)) request_update = add_input_port("Request Data", PORT_TYPE_SIGNAL, trigger = PROC_REF(update_data)) - enabled = add_output_port("Enabled", PORT_TYPE_NUMBER) - is_siphoning = add_output_port("Siphoning", PORT_TYPE_NUMBER) - external_on = add_output_port("External On", PORT_TYPE_NUMBER) - internal_on = add_output_port("Internal On", PORT_TYPE_NUMBER) + enabled = add_output_port("Enabled", PORT_TYPE_BOOLEAN) + is_siphoning = add_output_port("Siphoning", PORT_TYPE_BOOLEAN) + external_on = add_output_port("External On", PORT_TYPE_BOOLEAN) + internal_on = add_output_port("Internal On", PORT_TYPE_BOOLEAN) current_external_pressure = add_output_port("External Pressure", PORT_TYPE_NUMBER) current_internal_pressure = add_output_port("Internal Pressure", PORT_TYPE_NUMBER) update_received = add_output_port("Update Received", PORT_TYPE_SIGNAL) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm index cf729cef15f4..e97bd92f7ccb 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/pump.dm @@ -173,7 +173,7 @@ input_temperature = add_output_port("Input Temperature", PORT_TYPE_NUMBER) output_temperature = add_output_port("Output Temperature", PORT_TYPE_NUMBER) - is_active = add_output_port("Active", PORT_TYPE_NUMBER) + is_active = add_output_port("Active", PORT_TYPE_BOOLEAN) turned_on = add_output_port("Turned On", PORT_TYPE_SIGNAL) turned_off = add_output_port("Turned Off", PORT_TYPE_SIGNAL) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/valve.dm b/code/modules/atmospherics/machinery/components/binary_devices/valve.dm index b728805cec6e..367d2029e9d7 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/valve.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/valve.dm @@ -111,7 +111,7 @@ It's like a regular ol' straight pipe, but you can turn it on and off. open = add_input_port("Open", PORT_TYPE_SIGNAL) close = add_input_port("Close", PORT_TYPE_SIGNAL) - is_open = add_output_port("Is Open", PORT_TYPE_NUMBER) + is_open = add_output_port("Is Open", PORT_TYPE_BOOLEAN) opened = add_output_port("Opened", PORT_TYPE_SIGNAL) closed = add_output_port("Closed", PORT_TYPE_SIGNAL) diff --git a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm index 6ea471f74768..0398ffd76b7f 100644 --- a/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm +++ b/code/modules/atmospherics/machinery/components/binary_devices/volume_pump.dm @@ -234,7 +234,7 @@ input_temperature = add_output_port("Input Temperature", PORT_TYPE_NUMBER) output_temperature = add_output_port("Output Temperature", PORT_TYPE_NUMBER) - is_active = add_output_port("Active", PORT_TYPE_NUMBER) + is_active = add_output_port("Active", PORT_TYPE_BOOLEAN) turned_on = add_output_port("Turned On", PORT_TYPE_SIGNAL) turned_off = add_output_port("Turned Off", PORT_TYPE_SIGNAL) diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm index cdb8b6299e69..32cf474c4223 100644 --- a/code/modules/hydroponics/hydroponics.dm +++ b/code/modules/hydroponics/hydroponics.dm @@ -1215,10 +1215,10 @@ var/datum/port/output/reagents_level /obj/item/circuit_component/hydroponics/populate_ports() - selfsustaining_setting = add_input_port("Auto-Grow Setting", PORT_TYPE_NUMBER) + selfsustaining_setting = add_input_port("Auto-Grow Setting", PORT_TYPE_BOOLEAN) plant_status = add_output_port("Plant Status", PORT_TYPE_NUMBER) - is_self_sustaining = add_output_port("Auto-Grow Status", PORT_TYPE_NUMBER) + is_self_sustaining = add_output_port("Auto-Grow Status", PORT_TYPE_BOOLEAN) plant_harvested = add_output_port("Plant Harvested", PORT_TYPE_SIGNAL) last_harvest = add_output_port("Last Harvest Amount", PORT_TYPE_NUMBER) plant_died = add_output_port("Plant Died", PORT_TYPE_SIGNAL) diff --git a/code/modules/instruments/piano_synth.dm b/code/modules/instruments/piano_synth.dm index f0357eb789c9..939b302fc593 100644 --- a/code/modules/instruments/piano_synth.dm +++ b/code/modules/instruments/piano_synth.dm @@ -119,7 +119,7 @@ sustain_value = add_input_port("Note Sustain Value", PORT_TYPE_NUMBER, trigger = PROC_REF(set_sustain_value)) note_decay = add_input_port("Held Note Decay", PORT_TYPE_NUMBER, trigger = PROC_REF(set_sustain_decay)) - is_playing = add_output_port("Currently Playing", PORT_TYPE_NUMBER) + is_playing = add_output_port("Currently Playing", PORT_TYPE_BOOLEAN) started_playing = add_output_port("Started Playing", PORT_TYPE_SIGNAL) stopped_playing = add_output_port("Stopped Playing", PORT_TYPE_SIGNAL) diff --git a/code/modules/modular_computers/file_system/program_circuit.dm b/code/modules/modular_computers/file_system/program_circuit.dm index 85cd7b388182..481e46af04c6 100644 --- a/code/modules/modular_computers/file_system/program_circuit.dm +++ b/code/modules/modular_computers/file_system/program_circuit.dm @@ -57,7 +57,7 @@ . = ..() start = add_input_port("Start", PORT_TYPE_SIGNAL, trigger = PROC_REF(start_prog)) kill = add_input_port("Kill", PORT_TYPE_SIGNAL, trigger = PROC_REF(kill_prog)) - running = add_output_port("Running", PORT_TYPE_NUMBER) + running = add_output_port("Running", PORT_TYPE_BOOLEAN) ///For most programs, triggers only work if they're open (either active or idle). /obj/item/circuit_component/mod_program/should_receive_input(datum/port/input/port) diff --git a/code/modules/vehicles/cars/vim.dm b/code/modules/vehicles/cars/vim.dm index 9bfa5c323ab7..a3b8b85adffa 100644 --- a/code/modules/vehicles/cars/vim.dm +++ b/code/modules/vehicles/cars/vim.dm @@ -129,7 +129,7 @@ var/datum/port/output/are_headlights_on /obj/item/circuit_component/vim/populate_ports() - are_headlights_on = add_output_port("Are Headlights On", PORT_TYPE_NUMBER) + are_headlights_on = add_output_port("Are Headlights On", PORT_TYPE_BOOLEAN) chime = add_output_port("On Chime Used", PORT_TYPE_SIGNAL) buzz = add_output_port("On Buzz Used", PORT_TYPE_SIGNAL) diff --git a/code/modules/wiremod/components/abstract/compare.dm b/code/modules/wiremod/components/abstract/compare.dm index f17dd225c7a0..48b53dea1622 100644 --- a/code/modules/wiremod/components/abstract/compare.dm +++ b/code/modules/wiremod/components/abstract/compare.dm @@ -22,7 +22,7 @@ true = add_output_port("True", PORT_TYPE_SIGNAL) false = add_output_port("False", PORT_TYPE_SIGNAL) - result = add_output_port("Result", PORT_TYPE_NUMBER) + result = add_output_port("Result", PORT_TYPE_BOOLEAN) /** * Used by derivatives to load their own ports in for custom use. diff --git a/code/modules/wiremod/components/action/light.dm b/code/modules/wiremod/components/action/light.dm index 46fd2993a9da..5057927831e6 100644 --- a/code/modules/wiremod/components/action/light.dm +++ b/code/modules/wiremod/components/action/light.dm @@ -33,7 +33,7 @@ blue = add_input_port("Blue", PORT_TYPE_NUMBER) brightness = add_input_port("Brightness", PORT_TYPE_NUMBER) - on = add_input_port("On", PORT_TYPE_NUMBER) + on = add_input_port("On", PORT_TYPE_BOOLEAN) /obj/item/circuit_component/light/register_shell(atom/movable/shell) . = ..() diff --git a/code/modules/wiremod/components/action/soundemitter.dm b/code/modules/wiremod/components/action/soundemitter.dm index 144a56295dd9..f6c03d8b75b7 100644 --- a/code/modules/wiremod/components/action/soundemitter.dm +++ b/code/modules/wiremod/components/action/soundemitter.dm @@ -51,7 +51,7 @@ /obj/item/circuit_component/soundemitter/populate_ports() volume = add_input_port("Volume", PORT_TYPE_NUMBER, default = 35) frequency = add_input_port("Frequency", PORT_TYPE_NUMBER, default = 0) - backwards = add_input_port("Play Backwards", PORT_TYPE_NUMBER, default = 0) + backwards = add_input_port("Play Backwards", PORT_TYPE_BOOLEAN, default = FALSE) /obj/item/circuit_component/soundemitter/populate_options() var/static/component_options = list( diff --git a/code/modules/wiremod/components/action/speech.dm b/code/modules/wiremod/components/action/speech.dm index f149cba9122b..49c2e75d08c7 100644 --- a/code/modules/wiremod/components/action/speech.dm +++ b/code/modules/wiremod/components/action/speech.dm @@ -23,7 +23,7 @@ /obj/item/circuit_component/speech/populate_ports() message = add_input_port("Message", PORT_TYPE_STRING, trigger = null) - quietmode = add_input_port("Quiet Mode", PORT_TYPE_NUMBER, default = 0) + quietmode = add_input_port("Quiet Mode", PORT_TYPE_BOOLEAN, default = FALSE) /obj/item/circuit_component/speech/input_received(datum/port/input/port) if(!parent.shell) diff --git a/code/modules/wiremod/components/admin/animate.dm b/code/modules/wiremod/components/admin/animate.dm index c3b26464b1b8..0271f7d1b275 100644 --- a/code/modules/wiremod/components/admin/animate.dm +++ b/code/modules/wiremod/components/admin/animate.dm @@ -33,7 +33,7 @@ /obj/item/circuit_component/begin_animation/populate_ports() target = add_input_port("Target", PORT_TYPE_ATOM) - parallel = add_input_port("Parallel", PORT_TYPE_NUMBER, default = 1) + parallel = add_input_port("Parallel", PORT_TYPE_BOOLEAN, default = TRUE) animation_loops = add_input_port("Loops", PORT_TYPE_NUMBER) stop_all_animations = add_input_port("Stop All Animations", PORT_TYPE_SIGNAL, trigger = PROC_REF(stop_animations)) animate_event = add_output_port("Perform Animation", PORT_TYPE_INSTANT_SIGNAL) diff --git a/code/modules/wiremod/components/admin/signal_handler/signal_handler.dm b/code/modules/wiremod/components/admin/signal_handler/signal_handler.dm index a80ba26794cb..430ede0683f9 100644 --- a/code/modules/wiremod/components/admin/signal_handler/signal_handler.dm +++ b/code/modules/wiremod/components/admin/signal_handler/signal_handler.dm @@ -63,7 +63,7 @@ signal_map = GLOB.integrated_circuit_signal_ids /obj/item/circuit_component/signal_handler/populate_ports() - instant = add_input_port("Instant", PORT_TYPE_NUMBER, order = 0.5, trigger = null, default = 1) + instant = add_input_port("Instant", PORT_TYPE_BOOLEAN, order = 0.5, trigger = null, default = TRUE) register = add_input_port("Register", PORT_TYPE_SIGNAL, order = 2, trigger = PROC_REF(register_signals)) unregister = add_input_port("Unregister", PORT_TYPE_SIGNAL, order = 2, trigger = PROC_REF(unregister_signals)) unregister_all = add_input_port("Unregister All", PORT_TYPE_SIGNAL, order = 2, trigger = PROC_REF(unregister_signals_all)) diff --git a/code/modules/wiremod/components/atom/hear.dm b/code/modules/wiremod/components/atom/hear.dm index b3a41154dc6b..f8dda23e0e88 100644 --- a/code/modules/wiremod/components/atom/hear.dm +++ b/code/modules/wiremod/components/atom/hear.dm @@ -23,7 +23,7 @@ var/datum/port/output/trigger_port /obj/item/circuit_component/hear/populate_ports() - on = add_input_port("On", PORT_TYPE_NUMBER, default = 1) + on = add_input_port("On", PORT_TYPE_BOOLEAN, default = TRUE) message_port = add_output_port("Message", PORT_TYPE_STRING) language_port = add_output_port("Language", PORT_TYPE_STRING) speaker_port = add_output_port("Speaker", PORT_TYPE_ATOM) diff --git a/code/modules/wiremod/components/atom/remotecam.dm b/code/modules/wiremod/components/atom/remotecam.dm index 3a1bbd3be471..12c7cc1d91c7 100644 --- a/code/modules/wiremod/components/atom/remotecam.dm +++ b/code/modules/wiremod/components/atom/remotecam.dm @@ -65,7 +65,7 @@ start = add_input_port("Start", PORT_TYPE_SIGNAL) stop = add_input_port("Stop", PORT_TYPE_SIGNAL) if(camera_range_settable) - camera_range = add_input_port("Camera Range", PORT_TYPE_NUMBER, default = 0) + camera_range = add_input_port("Far Range", PORT_TYPE_BOOLEAN, default = FALSE) network = add_input_port("Network", PORT_TYPE_STRING, default = "ss13") if(camera_range_settable) diff --git a/code/modules/wiremod/components/bci/install_detector.dm b/code/modules/wiremod/components/bci/install_detector.dm index 39972134b842..390d0da281a7 100644 --- a/code/modules/wiremod/components/bci/install_detector.dm +++ b/code/modules/wiremod/components/bci/install_detector.dm @@ -18,7 +18,7 @@ /obj/item/circuit_component/install_detector/populate_ports() . = ..() - current_state = add_output_port("Current State", PORT_TYPE_NUMBER) + current_state = add_output_port("Current State", PORT_TYPE_BOOLEAN) implanted = add_output_port("Implanted", PORT_TYPE_SIGNAL) removed = add_output_port("Removed", PORT_TYPE_SIGNAL) diff --git a/code/modules/wiremod/components/id/access_checker.dm b/code/modules/wiremod/components/id/access_checker.dm index d694834c06aa..c95c054bc28a 100644 --- a/code/modules/wiremod/components/id/access_checker.dm +++ b/code/modules/wiremod/components/id/access_checker.dm @@ -24,7 +24,7 @@ /obj/item/circuit_component/compare/access/populate_custom_ports() subject_accesses = add_input_port("Access To Check", PORT_TYPE_LIST(PORT_TYPE_STRING)) required_accesses = add_input_port("Required Access", PORT_TYPE_LIST(PORT_TYPE_STRING)) - check_any = add_input_port("Check Any", PORT_TYPE_NUMBER) + check_any = add_input_port("Check Any", PORT_TYPE_BOOLEAN) /obj/item/circuit_component/compare/access/save_data_to_list(list/component_data) . = ..() diff --git a/code/modules/wiremod/components/id/getter.dm b/code/modules/wiremod/components/id/getter.dm index 7143e5689d8c..beb77671f825 100644 --- a/code/modules/wiremod/components/id/getter.dm +++ b/code/modules/wiremod/components/id/getter.dm @@ -22,7 +22,7 @@ /obj/item/circuit_component/id_getter/populate_ports() target = add_input_port("Target", PORT_TYPE_ATOM) - prioritize_hands = add_input_port("Prioritize Hands", PORT_TYPE_NUMBER) + prioritize_hands = add_input_port("Prioritize Hands", PORT_TYPE_BOOLEAN) id_port = add_output_port("ID", PORT_TYPE_ATOM) /obj/item/circuit_component/id_getter/input_received(datum/port/input/port) diff --git a/code/modules/wiremod/components/math/binary_conversion.dm b/code/modules/wiremod/components/math/binary_conversion.dm index 8ef4067b955f..5302eb6bd181 100644 --- a/code/modules/wiremod/components/math/binary_conversion.dm +++ b/code/modules/wiremod/components/math/binary_conversion.dm @@ -26,7 +26,7 @@ add_action = "add", \ remove_action = "remove", \ is_output = TRUE, \ - port_type = PORT_TYPE_NUMBER, \ + port_type = PORT_TYPE_BOOLEAN, \ prefix = "Bit", \ minimum_amount = 1, \ maximum_amount = MAX_BITFIELD_SIZE \ @@ -46,6 +46,6 @@ for(var/iteration in 1 to len) var/datum/port/output/bit = bit_array[iteration] if(iteration == 1 && is_negative) - bit.set_output(1) + bit.set_output(TRUE) continue bit.set_output(!!(to_convert & (1<< (len - iteration)))) diff --git a/code/modules/wiremod/components/math/not.dm b/code/modules/wiremod/components/math/not.dm index f530c9edefc5..171870b1e9b2 100644 --- a/code/modules/wiremod/components/math/not.dm +++ b/code/modules/wiremod/components/math/not.dm @@ -18,7 +18,7 @@ /obj/item/circuit_component/not/populate_ports() input_port = add_input_port("Input", PORT_TYPE_ANY) - result = add_output_port("Result", PORT_TYPE_NUMBER) + result = add_output_port("Result", PORT_TYPE_BOOLEAN) /obj/item/circuit_component/not/input_received(datum/port/input/port) diff --git a/code/modules/wiremod/components/math/toggle.dm b/code/modules/wiremod/components/math/toggle.dm index 9f51c974cb31..39e43ac50a9f 100644 --- a/code/modules/wiremod/components/math/toggle.dm +++ b/code/modules/wiremod/components/math/toggle.dm @@ -16,7 +16,7 @@ var/toggle_state = FALSE /obj/item/circuit_component/compare/toggle/populate_custom_ports() - toggle_set = add_input_port("Set Toggle State", PORT_TYPE_NUMBER) + toggle_set = add_input_port("Set Toggle State", PORT_TYPE_BOOLEAN) toggle_and_compare = add_input_port("Toggle And Compare", PORT_TYPE_SIGNAL) toggle_state = FALSE diff --git a/code/modules/wiremod/components/utility/clock.dm b/code/modules/wiremod/components/utility/clock.dm index 07cd95ebb394..f35041cff343 100644 --- a/code/modules/wiremod/components/utility/clock.dm +++ b/code/modules/wiremod/components/utility/clock.dm @@ -19,7 +19,7 @@ . += create_ui_notice("Clock Interval: [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock") /obj/item/circuit_component/clock/populate_ports() - on = add_input_port("On", PORT_TYPE_NUMBER) + on = add_input_port("On", PORT_TYPE_BOOLEAN) signal = add_output_port("Signal", PORT_TYPE_SIGNAL) diff --git a/code/modules/wiremod/datatypes/boolean.dm b/code/modules/wiremod/datatypes/boolean.dm new file mode 100644 index 000000000000..da446b532de9 --- /dev/null +++ b/code/modules/wiremod/datatypes/boolean.dm @@ -0,0 +1,10 @@ +/datum/circuit_datatype/boolean + datatype = PORT_TYPE_BOOLEAN + color = "bad" // This should be close enough to dark red. + datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + +/datum/circuit_datatype/boolean/can_receive_from_datatype(datatype_to_check) + return TRUE + +/datum/circuit_datatype/boolean/convert_value(datum/port/port, value_to_convert, force) + return !!value_to_convert diff --git a/code/modules/wiremod/datatypes/number.dm b/code/modules/wiremod/datatypes/number.dm index fe964817d07a..69f11b8ae294 100644 --- a/code/modules/wiremod/datatypes/number.dm +++ b/code/modules/wiremod/datatypes/number.dm @@ -2,6 +2,7 @@ datatype = PORT_TYPE_NUMBER color = "green" datatype_flags = DATATYPE_FLAG_ALLOW_MANUAL_INPUT + can_receive_from = list(PORT_TYPE_BOOLEAN) /datum/circuit_datatype/number/handle_manual_input(datum/port/input/port, mob/user, user_input) return text2num(user_input) diff --git a/code/modules/wiremod/datatypes/signal.dm b/code/modules/wiremod/datatypes/signal.dm index 6a416e789e38..4f62f6bba614 100644 --- a/code/modules/wiremod/datatypes/signal.dm +++ b/code/modules/wiremod/datatypes/signal.dm @@ -7,6 +7,7 @@ PORT_TYPE_INSTANT_SIGNAL, PORT_TYPE_RESPONSE_SIGNAL, PORT_TYPE_SIGNAL, + PORT_TYPE_BOOLEAN, ) /datum/circuit_datatype/signal/handle_manual_input(datum/port/input/port, mob/user, user_input) diff --git a/code/modules/wiremod/shell/airlock.dm b/code/modules/wiremod/shell/airlock.dm index f77e3175d809..b90508443a33 100644 --- a/code/modules/wiremod/shell/airlock.dm +++ b/code/modules/wiremod/shell/airlock.dm @@ -76,8 +76,8 @@ open = add_input_port("Open", PORT_TYPE_SIGNAL) close = add_input_port("Close", PORT_TYPE_SIGNAL) // States - is_open = add_output_port("Is Open", PORT_TYPE_NUMBER) - is_bolted = add_output_port("Is Bolted", PORT_TYPE_NUMBER) + is_open = add_output_port("Is Open", PORT_TYPE_BOOLEAN) + is_bolted = add_output_port("Is Bolted", PORT_TYPE_BOOLEAN) // Output Signals opened = add_output_port("Opened", PORT_TYPE_SIGNAL) closed = add_output_port("Closed", PORT_TYPE_SIGNAL) diff --git a/code/modules/wiremod/shell/brain_computer_interface.dm b/code/modules/wiremod/shell/brain_computer_interface.dm index d3d5fc858fea..30f4910ec23d 100644 --- a/code/modules/wiremod/shell/brain_computer_interface.dm +++ b/code/modules/wiremod/shell/brain_computer_interface.dm @@ -74,7 +74,7 @@ message = add_input_port("Message", PORT_TYPE_STRING, trigger = null) send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL) - show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_NUMBER, trigger = PROC_REF(update_charge_action)) + show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_BOOLEAN, trigger = PROC_REF(update_charge_action)) user_port = add_output_port("User", PORT_TYPE_USER) diff --git a/code/modules/wiremod/shell/implant.dm b/code/modules/wiremod/shell/implant.dm index 3dab241a113c..2c719d6017ee 100644 --- a/code/modules/wiremod/shell/implant.dm +++ b/code/modules/wiremod/shell/implant.dm @@ -62,7 +62,7 @@ message = add_input_port("Message", PORT_TYPE_STRING, trigger = null) send_message_signal = add_input_port("Send Message", PORT_TYPE_SIGNAL) - show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_NUMBER, trigger = PROC_REF(update_charge_action)) + show_charge_meter = add_input_port("Show Charge Meter", PORT_TYPE_BOOLEAN, trigger = PROC_REF(update_charge_action)) user_port = add_output_port("User", PORT_TYPE_USER) diff --git a/code/modules/wiremod/shell/module.dm b/code/modules/wiremod/shell/module.dm index 57139cdfc1b8..a19a02f7fd9e 100644 --- a/code/modules/wiremod/shell/module.dm +++ b/code/modules/wiremod/shell/module.dm @@ -188,8 +188,8 @@ select_module = add_input_port("Select Module", PORT_TYPE_SIGNAL) // States wearer = add_output_port("Wearer", PORT_TYPE_USER) - deployed = add_output_port("Deployed", PORT_TYPE_NUMBER) - activated = add_output_port("Activated", PORT_TYPE_NUMBER) + deployed = add_output_port("Deployed", PORT_TYPE_BOOLEAN) + activated = add_output_port("Activated", PORT_TYPE_BOOLEAN) selected_module = add_output_port("Selected Module", PORT_TYPE_STRING) deployed_parts = add_output_port("Deployed Parts", PORT_TYPE_LIST(PORT_TYPE_STRING)) // Output Signals diff --git a/tgstation.dme b/tgstation.dme index a815bb3c542d..29b7679729a2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6821,6 +6821,7 @@ #include "code\modules\wiremod\core\variable.dm" #include "code\modules\wiremod\datatypes\any.dm" #include "code\modules\wiremod\datatypes\basic.dm" +#include "code\modules\wiremod\datatypes\boolean.dm" #include "code\modules\wiremod\datatypes\datum.dm" #include "code\modules\wiremod\datatypes\entity.dm" #include "code\modules\wiremod\datatypes\number.dm" diff --git a/tgui/packages/tgui/interfaces/IntegratedCircuit/FundamentalTypes.jsx b/tgui/packages/tgui/interfaces/IntegratedCircuit/FundamentalTypes.jsx index 5fe91ad383e9..0595b9650582 100644 --- a/tgui/packages/tgui/interfaces/IntegratedCircuit/FundamentalTypes.jsx +++ b/tgui/packages/tgui/interfaces/IntegratedCircuit/FundamentalTypes.jsx @@ -129,6 +129,17 @@ export const FUNDAMENTAL_DATA_TYPES = { ); }, + boolean: (props) => { + const { name, value, setValue, color } = props; + return ( + + {name} + + setValue(!value)} /> + + + ); + }, }; export const DATATYPE_DISPLAY_HANDLERS = { From ac01bd4e5ea3f18e82220124c5657405ad66463f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:57:52 +0000 Subject: [PATCH 122/155] Automatic changelog for PR #95487 [ci skip] --- html/changelogs/AutoChangeLog-pr-95487.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95487.yml diff --git a/html/changelogs/AutoChangeLog-pr-95487.yml b/html/changelogs/AutoChangeLog-pr-95487.yml new file mode 100644 index 000000000000..3d343850772c --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95487.yml @@ -0,0 +1,4 @@ +author: "Y0SH1M4S73R" +delete-after: True +changes: + - qol: "Many circuit components have had ports that effectively act as boolean inputs or outputs converted into a new boolean datatype." \ No newline at end of file From 1ea409105ff1f32d838284d07d89a4e8f1be8810 Mon Sep 17 00:00:00 2001 From: cebutris Date: Mon, 23 Mar 2026 17:58:05 -0400 Subject: [PATCH 123/155] Boxcutting wrapped packages returns less wrap (#95485) ## About The Pull Request Currently, when you use boxcutters to cut open wrapped packages, you get a full stack (25) of wrapping paper back, even though it takes at most 3 wrapping paper to wrap it in the first place, meaning you can make the paper multiply infinitely. This PR changes that so you only get one wrapping paper back. ## Why It's Good For The Game If you use one wrap to wrap something, you should not get twenty five wrap when unwrapping it ## Changelog :cl: fix: Fixed an oversight with package wrapping /:cl: --- code/game/objects/items/stacks/wrap.dm | 3 +++ code/modules/recycling/sortingmachinery.dm | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/code/game/objects/items/stacks/wrap.dm b/code/game/objects/items/stacks/wrap.dm index 432e040940b5..47f58f11030d 100644 --- a/code/game/objects/items/stacks/wrap.dm +++ b/code/game/objects/items/stacks/wrap.dm @@ -219,6 +219,9 @@ amount = 5 merge_type = /obj/item/stack/package_wrap/small +/obj/item/stack/package_wrap/one + amount = 1 + /obj/item/c_tube name = "cardboard tube" desc = "A tube... of cardboard." diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm index 696134e78931..8a1b9abb9b89 100644 --- a/code/modules/recycling/sortingmachinery.dm +++ b/code/modules/recycling/sortingmachinery.dm @@ -37,7 +37,7 @@ new /obj/effect/decal/cleanable/wrapping(turf_loc) else playsound(loc, 'sound/items/box_cut.ogg', 50, TRUE) - new /obj/item/stack/package_wrap(turf_loc) + new /obj/item/stack/package_wrap/one(turf_loc) for(var/atom/movable/movable_content as anything in contents) movable_content.forceMove(turf_loc) From a3424cf8c70885b8ac044146104413ca68ce7523 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:58:25 +0000 Subject: [PATCH 124/155] Automatic changelog for PR #95485 [ci skip] --- html/changelogs/AutoChangeLog-pr-95485.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95485.yml diff --git a/html/changelogs/AutoChangeLog-pr-95485.yml b/html/changelogs/AutoChangeLog-pr-95485.yml new file mode 100644 index 000000000000..2cfb614a36a4 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95485.yml @@ -0,0 +1,4 @@ +author: "cebutris" +delete-after: True +changes: + - bugfix: "Fixed an oversight with package wrapping" \ No newline at end of file From 361f754a776018ec30413f5ff993a51cc334f099 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Mon, 23 Mar 2026 21:58:45 +0000 Subject: [PATCH 125/155] Gorillas can pick up kittens (#95481) ## About The Pull Request image The proc involved with picking up other mobs had an "ishuman" check in it, barring other mobs with hands from picking up mobs. I don't know _why_ this was here but I _suspect_ that it's because drones have hands and it is undesirable for drones to pick up other drones. In this PR I have changed it so that any mob with hands can pick up any other mob that can be picked up, _as long as you are larger than the mob you are trying to pick up_. This prevents drones from picking up other drones, while allowing gorillas to pick up drones (or kittens). I think this also means that a wizard's dextrous guardian can pick up Ian and put him inside its special pocket for a perfect spectral kidnapping. The size check is only applied to nonhuman mobs, because there aren't any pickupable mobs by default that are as large or larger than humans to exclude, and because admins would get mad at me if I removed the ability for them to make humans able to get picked up by other humans on a whim. ## Why It's Good For The Game They can do it in real life It's unintuitive for picking up other mobs to be exclusive to humans without explanation ## Changelog :cl: add: Nonhuman mobs with hands can now pick up other mobs that are smaller than them and can usually be picked up /:cl: --- code/modules/mob/living/living.dm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index d9466e622412..75942a141e21 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2023,8 +2023,12 @@ GLOBAL_LIST_EMPTY(fire_appearances) real_name = name /mob/living/proc/mob_try_pickup(mob/living/user, instant=FALSE) - if(!ishuman(user)) - return + if(!ishuman(user) && (user.mob_size <= mob_size || user.num_hands == 0)) + if (!user.num_hands) + return + if (user.mob_size <= mob_size) + to_chat(user, span_warning("[src] is too big to pick up!")) + return if(!user.get_empty_held_indexes()) to_chat(user, span_warning("Your hands are full!")) return FALSE From 5fe86c27d33e56851921de475d0b10673040dc67 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:59:05 +0000 Subject: [PATCH 126/155] Automatic changelog for PR #95481 [ci skip] --- html/changelogs/AutoChangeLog-pr-95481.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95481.yml diff --git a/html/changelogs/AutoChangeLog-pr-95481.yml b/html/changelogs/AutoChangeLog-pr-95481.yml new file mode 100644 index 000000000000..fddd026d073e --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95481.yml @@ -0,0 +1,4 @@ +author: "Jacquerel" +delete-after: True +changes: + - rscadd: "Nonhuman mobs with hands can now pick up other mobs that are smaller than them and can usually be picked up" \ No newline at end of file From 45171715e32f3410ed9af6e050f7cbe6452b2328 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:59:30 -0500 Subject: [PATCH 127/155] Fix soup bowl looping not working (#95494) ## About The Pull Request `/datum/element/foodlike_drink` was not updated in the reagent refactors, still called `attack` directly. Rather than patch it I figured it was better to just integrate it into `/cup`, to make less jank in general. ## Changelog :cl: Melbert fix: Drinking soup will loop until it's empty or you cancel it (as it used to) /:cl: --- code/datums/elements/food/foodlike_drink.dm | 50 ------------------- code/game/objects/items/food/salad.dm | 3 +- .../reagents/reagent_containers/cups/_cup.dm | 46 ++++++++++++----- tgstation.dme | 1 - 4 files changed, 35 insertions(+), 65 deletions(-) delete mode 100644 code/datums/elements/food/foodlike_drink.dm diff --git a/code/datums/elements/food/foodlike_drink.dm b/code/datums/elements/food/foodlike_drink.dm deleted file mode 100644 index e8f6cf49f9e6..000000000000 --- a/code/datums/elements/food/foodlike_drink.dm +++ /dev/null @@ -1,50 +0,0 @@ -#define DOAFTER_SOURCE_FOODLIKE_DRINK "doafter_foodlike_drink" - -/** - * This element can be attached to a reagent container to make it loop after drinking like a food item - */ -/datum/element/foodlike_drink - -/datum/element/foodlike_drink/Attach(datum/target) - . = ..() - if(!is_reagent_container(target)) - return ELEMENT_INCOMPATIBLE - - RegisterSignal(target, COMSIG_GLASS_DRANK, PROC_REF(on_drink)) - -/datum/element/foodlike_drink/Detach(datum/source, ...) - . = ..() - UnregisterSignal(source, COMSIG_GLASS_DRANK) - -/datum/element/foodlike_drink/proc/on_drink(obj/item/reagent_containers/source, mob/living/drinker, mob/living/user) - SIGNAL_HANDLER - - if(drinker != user) - return - - if(DOING_INTERACTION(user, DOAFTER_SOURCE_FOODLIKE_DRINK)) - return - - INVOKE_ASYNC(src, PROC_REF(continue_drinking), source, user) - -/datum/element/foodlike_drink/proc/continue_drinking(obj/item/reagent_containers/source, mob/living/user) - if(!do_after( - user = user, - delay = 1.25 SECONDS, - timed_action_flags = IGNORE_USER_LOC_CHANGE, - extra_checks = CALLBACK(src, PROC_REF(can_keep_drinking), source, user), - interaction_key = DOAFTER_SOURCE_FOODLIKE_DRINK, - )) - return - - source.attack(user, user) - user.hud_used?.hunger?.update_hunger_bar() - -/datum/element/foodlike_drink/proc/can_keep_drinking(obj/item/reagent_containers/source, mob/living/user) - if(QDELETED(source) || user.get_active_held_item() != source) - return FALSE - if(source.reagents.total_volume <= 0) - return FALSE - return TRUE - -#undef DOAFTER_SOURCE_FOODLIKE_DRINK diff --git a/code/game/objects/items/food/salad.dm b/code/game/objects/items/food/salad.dm index c95a284e8f55..c350c0bd4a59 100644 --- a/code/game/objects/items/food/salad.dm +++ b/code/game/objects/items/food/salad.dm @@ -192,10 +192,11 @@ volume = SOUP_SERVING_SIZE + 5 gulp_size = 3 + loop_drink = TRUE + /obj/item/reagent_containers/cup/bowl/Initialize(mapload) . = ..() RegisterSignal(src, COMSIG_ATOM_REAGENT_EXAMINE, PROC_REF(reagent_special_examine)) - AddElement(/datum/element/foodlike_drink) AddComponent(/datum/component/ingredients_holder, /obj/item/food/salad/empty, CUSTOM_INGREDIENT_ICON_FILL, max_ingredients = 6) AddComponent( \ /datum/component/takes_reagent_appearance, \ diff --git a/code/modules/reagents/reagent_containers/cups/_cup.dm b/code/modules/reagents/reagent_containers/cups/_cup.dm index 39feda61fb4f..953fb3d248a0 100644 --- a/code/modules/reagents/reagent_containers/cups/_cup.dm +++ b/code/modules/reagents/reagent_containers/cups/_cup.dm @@ -37,6 +37,8 @@ var/cell_wired = FALSE /// Visual y-offset for the assembly on our lid var/assembly_pixel_y = 0 + /// If TRUE, after we finish drinking, we try to drink again after do_after + var/loop_drink = FALSE /obj/item/reagent_containers/cup/Initialize(mapload, vol) . = ..() @@ -96,6 +98,8 @@ user.changeNext_move(CLICK_CD_MELEE) if(target_mob != user) + if(DOING_INTERACTION_WITH_TARGET(user, target_mob)) + return ITEM_INTERACT_BLOCKING target_mob.visible_message( span_danger("[user] attempts to feed [target_mob] something from [src]."), span_userdanger("[user] attempts to feed you something from [src]."), @@ -108,29 +112,45 @@ span_danger("[user] feeds [target_mob] something from [src]."), span_userdanger("[user] feeds you something from [src]."), ) + if(target_mob.is_blind()) + to_chat(target_mob, span_notice("You feel someone feed you something.")) log_combat(user, target_mob, "fed", reagents.get_reagent_log_string()) + else + if(loop_drink) + if(DOING_INTERACTION_WITH_TARGET(user, user)) + return ITEM_INTERACT_BLOCKING + user.visible_message( + span_danger("[user] attempts to drink from [src]."), + span_userdanger("[user] attempts to drink from [src]."), + ) + if(!do_after(user, 1.25 SECONDS, user)) + return ITEM_INTERACT_BLOCKING + if(!reagents || !reagents.total_volume) + return ITEM_INTERACT_BLOCKING + user.visible_message( + span_danger("[user] drinks from [src]."), + span_userdanger("[user] drinks from [src]."), + ignored_mobs = list(user), + ) to_chat(user, span_notice("You swallow a gulp of [src].")) - . = ITEM_INTERACT_SUCCESS SEND_SIGNAL(src, COMSIG_GLASS_DRANK, target_mob, user) - var/fraction = min(gulp_size/reagents.total_volume, 1) + var/fraction = min(gulp_size / reagents.total_volume, 1) reagents.trans_to(target_mob, gulp_size, transferred_by = user, methods = reagent_consumption_method) + user.hud_used?.hunger?.update_hunger_bar() checkLiked(fraction, target_mob) - playsound(target_mob.loc, consumption_sound, rand(10,50), TRUE) - if(!iscarbon(target_mob)) - return . - var/mob/living/carbon/carbon_drinker = target_mob - var/list/diseases = carbon_drinker.get_static_viruses() - if(!LAZYLEN(diseases)) - return . - var/list/datum/disease/diseases_to_add = list() - for(var/datum/disease/malady as anything in diseases) + playsound(target_mob, consumption_sound, rand(10, 50), TRUE) + var/list/datum/disease/diseases_to_add + for(var/datum/disease/malady as anything in target_mob.get_static_viruses()) if(malady.spread_flags & DISEASE_SPREAD_CONTACT_FLUIDS) - diseases_to_add += malady + LAZYADD(diseases_to_add, malady) if(LAZYLEN(diseases_to_add)) AddComponent(/datum/component/infective, diseases_to_add) - return . + if(loop_drink) + return try_drink(target_mob, user) | ITEM_INTERACT_SUCCESS + + return ITEM_INTERACT_SUCCESS /obj/item/reagent_containers/cup/interact_with_atom(atom/target, mob/living/user, list/modifiers) . = ..() diff --git a/tgstation.dme b/tgstation.dme index 29b7679729a2..38d5817823b4 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1719,7 +1719,6 @@ #include "code\datums\elements\decals\blood.dm" #include "code\datums\elements\food\dunkable.dm" #include "code\datums\elements\food\food_trash.dm" -#include "code\datums\elements\food\foodlike_drink.dm" #include "code\datums\elements\food\fried_item.dm" #include "code\datums\elements\food\grilled_item.dm" #include "code\datums\elements\food\love_food_buff.dm" From 04d334990d47613b43bdbddcc4d0c2adc7f1c7fa Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:59:48 +0000 Subject: [PATCH 128/155] Automatic changelog for PR #95494 [ci skip] --- html/changelogs/AutoChangeLog-pr-95494.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95494.yml diff --git a/html/changelogs/AutoChangeLog-pr-95494.yml b/html/changelogs/AutoChangeLog-pr-95494.yml new file mode 100644 index 000000000000..e31f6f019c94 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95494.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "Drinking soup will loop until it's empty or you cancel it (as it used to)" \ No newline at end of file From 0ac95930a9909b5f4059fb101eac59ab92dc0a77 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:02:04 -0500 Subject: [PATCH 129/155] Apply height filters to electrocution animation (#95455) ## About The Pull Request The skeleton from being shocked matches your height. ## Changelog :cl: Melbert fix: The skeleton from being shocked matches your height /:cl: --- code/modules/hallucination/shock.dm | 3 +++ code/modules/mob/living/carbon/human/human.dm | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/code/modules/hallucination/shock.dm b/code/modules/hallucination/shock.dm index c9982fc627d0..7f80a4d6c558 100644 --- a/code/modules/hallucination/shock.dm +++ b/code/modules/hallucination/shock.dm @@ -30,6 +30,9 @@ electrocution_skeleton_anim = image(electrocution_icon, hallucinator, icon_state = electrocution_icon_state, layer = ABOVE_MOB_LAYER) electrocution_skeleton_anim.appearance_flags |= RESET_COLOR|KEEP_APART + if(ishuman(hallucinator)) + var/mob/living/carbon/human/human_hallucinator = hallucinator + human_hallucinator.apply_height_filters(electrocution_skeleton_anim) SET_PLANE_EXPLICIT(shock_image, ABOVE_GAME_PLANE, hallucinator) SET_PLANE_EXPLICIT(electrocution_skeleton_anim, ABOVE_GAME_PLANE, hallucinator) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 187e684d06ed..4078d7c6d303 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -636,10 +636,8 @@ // If we have a species, we need to handle mutant parts and stuff if(dna?.species) add_atom_colour(COLOR_BLACK, TEMPORARY_COLOUR_PRIORITY) - var/static/mutable_appearance/shock_animation_dna - if(!shock_animation_dna) - shock_animation_dna = mutable_appearance(icon, "electrocuted_base") - shock_animation_dna.appearance_flags |= RESET_COLOR|KEEP_APART + var/mutable_appearance/shock_animation_dna = mutable_appearance(icon, "electrocuted_base", appearance_flags = RESET_COLOR|KEEP_APART) + apply_height_filters(shock_animation_dna) zap_appearance = shock_animation_dna // Otherwise do a generic animation From 5dbd346db6860b9fded7cf2a203fb1ec9ac20160 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:02:23 +0000 Subject: [PATCH 130/155] Automatic changelog for PR #95455 [ci skip] --- html/changelogs/AutoChangeLog-pr-95455.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95455.yml diff --git a/html/changelogs/AutoChangeLog-pr-95455.yml b/html/changelogs/AutoChangeLog-pr-95455.yml new file mode 100644 index 000000000000..9b655ca6d40e --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95455.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - bugfix: "The skeleton from being shocked matches your height" \ No newline at end of file From 7740768362a22074a21967033b1322be9c94d33c Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:16:45 -0400 Subject: [PATCH 131/155] [NO GBP] Corrects two issues with goodies and the ordering console. (#95478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## About The Pull Request Whoops. ~~I missed some nuance when I made #94483, namely that while I didn't want players requesting CRATES like guns, grenades, spare SM shards with their department budgets, the logic also touches goodies and private orders meaning that players were unable to place goodie orders for things that they may not have explicit access to do so, which is part of the reason why you'd be ordering them privately in the first place. It's cargo responsibility to determine if the player should/not be receiving that item.~~ I have meditated on the issue, and I realize, nah, this is probably both a healthier design decision as well as the reason we have things like the black market in the first place. The core of the PR below however is however sound. ALSO, I made a fairly confusing mistake with the TGUI where the goodies category just... hasn't been visible! That's on me. ## Why It's Good For The Game Makes cargo goodies viewable. Makes cargo goodies purchasable. Fixes #94928 🐛 💥 ‼️ ## Changelog :cl: fix: Cargo goodies are now visible in the ordering and request consoles. /:cl: --- code/modules/cargo/orderconsole.dm | 1 + tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/cargo/orderconsole.dm b/code/modules/cargo/orderconsole.dm index 47051632b4a6..c3eec017f795 100644 --- a/code/modules/cargo/orderconsole.dm +++ b/code/modules/cargo/orderconsole.dm @@ -297,6 +297,7 @@ if(pack.access_view && !(pack.access_view in access) && personal_department) // We want to block cargo requests when a player is requesting a restricted pack that they don't have access to. // BUT only when it's requested with non-cargo funds, as cargo had direct oversight over their own purchases with their own budget. + // HOWEVER, this shouldn't prevent someone from buying something using their own personal funds. say("ERROR: User lacks the requisite access for this purchase request.") return diff --git a/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx b/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx index 2d97b55d8aad..a83315c8cd66 100644 --- a/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx +++ b/tgui/packages/tgui/interfaces/Cargo/CargoCatalog.tsx @@ -147,7 +147,6 @@ function CatalogTabs(props: CatalogTabsProps & Props) { setActiveSupplyName(supply.name); setSearchText(''); }} - style={supply.name === "Goodies" ? {display: 'none'} : undefined} > {supply.name} From 9bf55e6993d80154267ef16cb218e1525dba6649 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:17:06 +0000 Subject: [PATCH 132/155] Automatic changelog for PR #95478 [ci skip] --- html/changelogs/AutoChangeLog-pr-95478.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95478.yml diff --git a/html/changelogs/AutoChangeLog-pr-95478.yml b/html/changelogs/AutoChangeLog-pr-95478.yml new file mode 100644 index 000000000000..3917dc013dc5 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95478.yml @@ -0,0 +1,4 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - bugfix: "Cargo goodies are now visible in the ordering and request consoles." \ No newline at end of file From 2ddc1616486a4f81bead93e34477f4a242a6a846 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:00:24 +0000 Subject: [PATCH 133/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95455.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95478.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95481.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95485.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95487.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95494.yml | 4 ---- html/changelogs/archive/2026-03.yml | 15 +++++++++++++++ 7 files changed, 15 insertions(+), 24 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95455.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95478.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95481.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95485.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95487.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95494.yml diff --git a/html/changelogs/AutoChangeLog-pr-95455.yml b/html/changelogs/AutoChangeLog-pr-95455.yml deleted file mode 100644 index 9b655ca6d40e..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95455.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "The skeleton from being shocked matches your height" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95478.yml b/html/changelogs/AutoChangeLog-pr-95478.yml deleted file mode 100644 index 3917dc013dc5..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95478.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "ArcaneMusic" -delete-after: True -changes: - - bugfix: "Cargo goodies are now visible in the ordering and request consoles." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95481.yml b/html/changelogs/AutoChangeLog-pr-95481.yml deleted file mode 100644 index fddd026d073e..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95481.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Jacquerel" -delete-after: True -changes: - - rscadd: "Nonhuman mobs with hands can now pick up other mobs that are smaller than them and can usually be picked up" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95485.yml b/html/changelogs/AutoChangeLog-pr-95485.yml deleted file mode 100644 index 2cfb614a36a4..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95485.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "cebutris" -delete-after: True -changes: - - bugfix: "Fixed an oversight with package wrapping" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95487.yml b/html/changelogs/AutoChangeLog-pr-95487.yml deleted file mode 100644 index 3d343850772c..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95487.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Y0SH1M4S73R" -delete-after: True -changes: - - qol: "Many circuit components have had ports that effectively act as boolean inputs or outputs converted into a new boolean datatype." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95494.yml b/html/changelogs/AutoChangeLog-pr-95494.yml deleted file mode 100644 index e31f6f019c94..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95494.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - bugfix: "Drinking soup will loop until it's empty or you cancel it (as it used to)" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index faac60f29389..638d30c29104 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -358,3 +358,18 @@ is no longer a monkey - spellcheck: Shoving someone into another person no longer sends the "you shoved" message to the person who was shoved into +2026-03-24: + ArcaneMusic: + - bugfix: Cargo goodies are now visible in the ordering and request consoles. + Jacquerel: + - rscadd: Nonhuman mobs with hands can now pick up other mobs that are smaller than + them and can usually be picked up + Melbert: + - bugfix: The skeleton from being shocked matches your height + - bugfix: Drinking soup will loop until it's empty or you cancel it (as it used + to) + Y0SH1M4S73R: + - qol: Many circuit components have had ports that effectively act as boolean inputs + or outputs converted into a new boolean datatype. + cebutris: + - bugfix: Fixed an oversight with package wrapping From 73e70297a8f1940cb958f63119abe23d38af0945 Mon Sep 17 00:00:00 2001 From: Iajret <8430839+Iajret@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:58:31 +0300 Subject: [PATCH 134/155] Fixes vitals display not respecting power upon being placed (#95427) ## About The Pull Request Changing from `NO_POWER_USE` to any other power use apparently never been handled properly. Which cause machinery to fail to knowledge their new environment. So far only vitals display suffered from this since it had complicated(ish) power related mechanics. To handle this, we force update our machinery when we change our use_power to anything other than `NO_POWER_USE` ## Why It's Good For The Game Closes https://github.com/tgstation/tgstation/issues/95113 --- code/game/machinery/_machinery.dm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index 47d76b9c9c7a..6e4968b1ac82 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -490,6 +490,9 @@ use_power = new_use_power + if(use_power) + power_change() + return TRUE ///updates the power channel this machine uses. removes the static power usage from the old channel and readds it to the new channel From 4bbebf6a37110d8ae6aa70f436c1bb2dd0aa1e48 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:58:48 +0000 Subject: [PATCH 135/155] Automatic changelog for PR #95427 [ci skip] --- html/changelogs/AutoChangeLog-pr-95427.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95427.yml diff --git a/html/changelogs/AutoChangeLog-pr-95427.yml b/html/changelogs/AutoChangeLog-pr-95427.yml new file mode 100644 index 000000000000..a992d60fc0e2 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95427.yml @@ -0,0 +1,4 @@ +author: "Iajret" +delete-after: True +changes: + - bugfix: "fixed vitals display not respecting APCs power when you attach it to a wall" \ No newline at end of file From af27eda0518d00297a98d3195c085d052451b0e6 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:06:58 -0500 Subject: [PATCH 136/155] Adds support for gags underclothing / Uses it to make them look less jank on lizards (#95454) ## About The Pull Request 1. Underclothes have all been repathed as `/datum/sprite_accessory/clothing` 2. Underclothes now have support for GAGS. Currently it ONLY supports static GAGS colors, ie only preset colors, however prefs support could easily be added in the future 3. Uses the GAGS support to implement the existing digitigrade templating system for some undergarmants - lizards will use a generic template for their underclothings when applicable Example image ## Why It's Good For The Game Existing undergarmants look really jank on lizards image image I would like to solve this, and well... the two solutions are "we force lizards to run around naked" or "we use the cool templating system". The latter seemed preferable image I did yoink the template from the jumpsuit template so it might look too similar, might need to tweak it ## Changelog :cl: Melbert add: Some undergarmants will now use a generic replacement on lizard body shapes that fit more appropriately refactor: Refactored how undergarmants generate their icons, report any oddities with that /:cl: --- .../subsystem/sprite_accessories.dm | 12 +- .../greyscale_configs/greyscale_clothes.dm | 6 +- .../json_configs/digitigrade_underwear.json | 70 ++++ code/datums/outfit.dm | 6 +- code/datums/sprite_accessories.dm | 332 +++++++++++------- code/game/objects/structures/mannequin.dm | 27 +- code/modules/basketball/basketball_teams.dm | 8 +- code/modules/client/preferences/clothing.dm | 10 +- .../jobs/job_types/head_of_personnel.dm | 2 +- .../living/carbon/human/human_update_icons.dm | 32 +- code/modules/surgery/bodyparts/helpers.dm | 10 + ...mplate.dmi => digi_template_equpiment.dmi} | Bin .../mob/clothing/digi_template_underwear.dmi | Bin 0 -> 805 bytes 13 files changed, 329 insertions(+), 186 deletions(-) create mode 100644 code/datums/greyscale/json_configs/digitigrade_underwear.json rename icons/mob/clothing/{digi_template.dmi => digi_template_equpiment.dmi} (100%) create mode 100644 icons/mob/clothing/digi_template_underwear.dmi diff --git a/code/controllers/subsystem/sprite_accessories.dm b/code/controllers/subsystem/sprite_accessories.dm index 5173960a3ffc..b053136cb6f8 100644 --- a/code/controllers/subsystem/sprite_accessories.dm +++ b/code/controllers/subsystem/sprite_accessories.dm @@ -29,17 +29,17 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity var/list/hair_masks_list //! stores /datum/hair_mask indexed by type //Underwear - var/list/underwear_list //! stores /datum/sprite_accessory/underwear indexed by name + var/list/underwear_list //! stores /datum/sprite_accessory/clothing/underwear indexed by name var/list/underwear_m //! stores only underwear name var/list/underwear_f //! stores only underwear name //Undershirts - var/list/undershirt_list //! stores /datum/sprite_accessory/undershirt indexed by name + var/list/undershirt_list //! stores /datum/sprite_accessory/clothing/undershirt indexed by name var/list/undershirt_m //! stores only undershirt name var/list/undershirt_f //! stores only undershirt name //Socks - var/list/socks_list //! stores /datum/sprite_accessory/socks indexed by name + var/list/socks_list //! stores /datum/sprite_accessory/clothing/socks indexed by name //All features, indexed by feature key, then name of the sprite accessory to the datum iteslf var/list/list/feature_list @@ -64,17 +64,17 @@ SUBSYSTEM_DEF(accessories) // just 'accessories' for brevity facial_hairstyles_male_list = facial_hair_lists[MALE_SPRITE_LIST] facial_hairstyles_female_list = facial_hair_lists[FEMALE_SPRITE_LIST] - var/underwear_lists = init_sprite_accessory_subtypes(/datum/sprite_accessory/underwear) + var/underwear_lists = init_sprite_accessory_subtypes(/datum/sprite_accessory/clothing/underwear) underwear_list = underwear_lists[DEFAULT_SPRITE_LIST] underwear_m = underwear_lists[MALE_SPRITE_LIST] underwear_f = underwear_lists[FEMALE_SPRITE_LIST] - var/undershirt_lists = init_sprite_accessory_subtypes(/datum/sprite_accessory/undershirt) + var/undershirt_lists = init_sprite_accessory_subtypes(/datum/sprite_accessory/clothing/undershirt) undershirt_list = undershirt_lists[DEFAULT_SPRITE_LIST] undershirt_m = undershirt_lists[MALE_SPRITE_LIST] undershirt_f = undershirt_lists[FEMALE_SPRITE_LIST] - socks_list = init_sprite_accessory_subtypes(/datum/sprite_accessory/socks)[DEFAULT_SPRITE_LIST] + socks_list = init_sprite_accessory_subtypes(/datum/sprite_accessory/clothing/socks)[DEFAULT_SPRITE_LIST] feature_list = list() // felinids diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm index 3cfad1fef7f2..f8ab474c3598 100644 --- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm +++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_clothes.dm @@ -693,9 +693,13 @@ /datum/greyscale_config/digitigrade name = "Digitigrade Clothes" - icon_file = 'icons/mob/clothing/digi_template.dmi' + icon_file = 'icons/mob/clothing/digi_template_equpiment.dmi' json_config = 'code/datums/greyscale/json_configs/digitigrade.json' +/datum/greyscale_config/digitigrade_underwear + name = "Digitigrade Underwear" + icon_file = 'icons/mob/clothing/digi_template_underwear.dmi' + json_config = 'code/datums/greyscale/json_configs/digitigrade_underwear.json' // // SUIT + HEAD diff --git a/code/datums/greyscale/json_configs/digitigrade_underwear.json b/code/datums/greyscale/json_configs/digitigrade_underwear.json new file mode 100644 index 000000000000..3d95852230ff --- /dev/null +++ b/code/datums/greyscale/json_configs/digitigrade_underwear.json @@ -0,0 +1,70 @@ +{ + "short_short": [ + { + "type": "icon_state", + "icon_state": "short_short_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "short": [ + { + "type": "icon_state", + "icon_state": "short_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "boxers": [ + { + "type": "icon_state", + "icon_state": "male_boxers_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "midway": [ + { + "type": "icon_state", + "icon_state": "male_midway_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "longjohns": [ + { + "type": "icon_state", + "icon_state": "male_longjohns_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "boxers_stripe": [ + { + "type": "icon_state", + "icon_state": "male_boxers_stripe_digi", + "blend_mode": "overlay", + "color_ids": [ 1 ] + } + ], + "boxers_stripe_threecolor": [ + { + "type": "icon_state", + "icon_state": "male_boxers_stripe_twocolor_digi_a", + "blend_mode": "overlay", + "color_ids": [ 1 ] + }, + { + "type": "icon_state", + "icon_state": "male_boxers_stripe_twocolor_digi_b", + "blend_mode": "overlay", + "color_ids": [ 2 ] + }, + { + "type": "icon_state", + "icon_state": "male_boxers_stripe_twocolor_digi_c", + "blend_mode": "overlay", + "color_ids": [ 3 ] + } + ] +} diff --git a/code/datums/outfit.dm b/code/datums/outfit.dm index 182df168bfd3..44d47bb11cdc 100644 --- a/code/datums/outfit.dm +++ b/code/datums/outfit.dm @@ -125,9 +125,9 @@ var/preload = FALSE /// Any undershirt. While on humans it is a string, here we use paths to stay consistent with the rest of the equips. - var/datum/sprite_accessory/undershirt = null - var/datum/sprite_accessory/underwear = null - var/datum/sprite_accessory/socks = null + var/datum/sprite_accessory/clothing/undershirt = null + var/datum/sprite_accessory/clothing/underwear = null + var/datum/sprite_accessory/clothing/socks = null /** * Called at the start of the equip proc diff --git a/code/datums/sprite_accessories.dm b/code/datums/sprite_accessories.dm index 88f1a4043e66..02ef3786598a 100644 --- a/code/datums/sprite_accessories.dm +++ b/code/datums/sprite_accessories.dm @@ -1244,179 +1244,243 @@ GLOBAL_LIST_EMPTY(blended_hair_icons_cache) icon_state = null gender = NEUTER +/datum/sprite_accessory/clothing + /// Allows you to specify a greyscale config + var/greyscale_config + /// Icon state in the digitigrade template file to use if the wearer is digitigrade. + /// If null, no special digitigrade handling is done. + var/digi_icon_state + /// Color pallete for static colored underwear, like hearts. + /// Used so greyscale copies can have the same palette. + var/greyscale_colors = "#FFFFFF#FFFFFF#FFFFFF" + +/** + * Generate an appearance from this clothing datum + * + * * color - if this is NOT a statically colored clothing article and NOT gags, uses this color. + * * physique - physique of the wearer (male or female) + * * bodyshape - bodyshape of the wearer (humanoid, digitigrade, etc) + */ +/datum/sprite_accessory/clothing/proc/make_appearance(color = COLOR_WHITE, physique = MALE, bodyshape = BODYSHAPE_HUMANOID) + var/static/list/cached_icons = list() + var/use_female = physique == FEMALE + var/use_digi = digi_icon_state && (bodyshape & BODYSHAPE_DIGITIGRADE) + + var/key = "[icon_state]-[greyscale_config || "ng"]-[use_female]-[use_digi]-[greyscale_colors]" + var/mutable_appearance/result + if(cached_icons[key]) // it's already cached + result = mutable_appearance(icon(cached_icons[key])) + + else if(greyscale_config || use_female || use_digi) // icon ops ahead + var/icon/created = icon(greyscale_config ? SSgreyscale.GetColoredIconByType(greyscale_config, greyscale_colors) : icon, icon_state) + if(use_female) + created = wear_female_version(icon_state, icon, FEMALE_UNIFORM_FULL) + if(use_digi) + var/icon/replacement = icon(SSgreyscale.GetColoredIconByType(/datum/greyscale_config/digitigrade_underwear, greyscale_colors), digi_icon_state) + created = replace_icon_legs(created, replacement) + + cached_icons[key] = fcopy_rsc(created) + result = mutable_appearance(created) + + else // no caching necessary + result = mutable_appearance(icon, icon_state) + + result.layer = -BODY_LAYER + result.color = use_static ? null : color + + return result + + /////////////////////////// // Underwear Definitions // /////////////////////////// -/datum/sprite_accessory/underwear +/datum/sprite_accessory/clothing/underwear icon = 'icons/mob/clothing/underwear.dmi' use_static = FALSE em_block = TRUE - //MALE UNDERWEAR -/datum/sprite_accessory/underwear/nude +/datum/sprite_accessory/clothing/underwear/nude name = "Nude" icon_state = null gender = NEUTER -/datum/sprite_accessory/underwear/male_briefs +/datum/sprite_accessory/clothing/underwear/nude/make_appearance(mob/living/carbon/human/for_who) + return + +/datum/sprite_accessory/clothing/underwear/male_briefs name = "Briefs" icon_state = "male_briefs" gender = MALE -/datum/sprite_accessory/underwear/male_boxers +/datum/sprite_accessory/clothing/underwear/male_boxers name = "Boxers" icon_state = "male_boxers" gender = MALE + digi_icon_state = "boxers" -/datum/sprite_accessory/underwear/male_stripe +/datum/sprite_accessory/clothing/underwear/male_stripe name = "Striped Boxers" icon_state = "male_stripe" gender = MALE + digi_icon_state = "boxers_stripe" -/datum/sprite_accessory/underwear/male_midway +/datum/sprite_accessory/clothing/underwear/male_midway name = "Midway Boxers" icon_state = "male_midway" gender = MALE + digi_icon_state = "midway" -/datum/sprite_accessory/underwear/male_longjohns +/datum/sprite_accessory/clothing/underwear/male_longjohns name = "Long Johns" icon_state = "male_longjohns" gender = MALE + digi_icon_state = "longjohns" -/datum/sprite_accessory/underwear/male_kinky +/datum/sprite_accessory/clothing/underwear/male_kinky name = "Jockstrap" icon_state = "male_kinky" gender = MALE -/datum/sprite_accessory/underwear/male_mankini +/datum/sprite_accessory/clothing/underwear/male_mankini name = "Mankini" icon_state = "male_mankini" gender = MALE -/datum/sprite_accessory/underwear/male_hearts +/datum/sprite_accessory/clothing/underwear/male_hearts name = "Hearts Boxers" icon_state = "male_hearts" gender = MALE use_static = TRUE + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#D62626#" -/datum/sprite_accessory/underwear/male_commie +/datum/sprite_accessory/clothing/underwear/male_commie name = "Commie Boxers" icon_state = "male_commie" gender = MALE use_static = TRUE + digi_icon_state = "boxers_stripe_twocolor" + greyscale_colors = "#D62626#D1B62C#D62626" -/datum/sprite_accessory/underwear/male_usastripe +/datum/sprite_accessory/clothing/underwear/male_usastripe name = "Freedom Boxers" icon_state = "male_assblastusa" gender = MALE use_static = TRUE + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#2E26D6" -/datum/sprite_accessory/underwear/male_uk +/datum/sprite_accessory/clothing/underwear/male_uk name = "UK Boxers" icon_state = "male_uk" gender = MALE use_static = TRUE - + digi_icon_state = "boxers_stripe_threecolor" + greyscale_colors = "#D62626#EEEEEE#2E26D6" //FEMALE UNDERWEAR -/datum/sprite_accessory/underwear/female_bikini +/datum/sprite_accessory/clothing/underwear/female_bikini name = "Bikini" icon_state = "female_bikini" gender = FEMALE -/datum/sprite_accessory/underwear/female_lace +/datum/sprite_accessory/clothing/underwear/female_lace name = "Lace Bikini" icon_state = "female_lace" gender = FEMALE -/datum/sprite_accessory/underwear/female_bralette +/datum/sprite_accessory/clothing/underwear/female_bralette name = "Bralette w/ Boyshorts" icon_state = "female_bralette" gender = FEMALE + digi_icon_state = "short_short" -/datum/sprite_accessory/underwear/female_sport +/datum/sprite_accessory/clothing/underwear/female_sport name = "Sports Bra w/ Boyshorts" icon_state = "female_sport" gender = FEMALE + digi_icon_state = "short" -/datum/sprite_accessory/underwear/female_thong +/datum/sprite_accessory/clothing/underwear/female_thong name = "Thong" icon_state = "female_thong" gender = FEMALE -/datum/sprite_accessory/underwear/female_strapless +/datum/sprite_accessory/clothing/underwear/female_strapless name = "Strapless Bikini" icon_state = "female_strapless" gender = FEMALE -/datum/sprite_accessory/underwear/female_babydoll +/datum/sprite_accessory/clothing/underwear/female_babydoll name = "Babydoll" icon_state = "female_babydoll" gender = FEMALE -/datum/sprite_accessory/underwear/swimsuit_onepiece +/datum/sprite_accessory/clothing/underwear/swimsuit_onepiece name = "One-Piece Swimsuit" icon_state = "swim_onepiece" gender = FEMALE -/datum/sprite_accessory/underwear/swimsuit_strapless_onepiece +/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_onepiece name = "Strapless One-Piece Swimsuit" icon_state = "swim_strapless_onepiece" gender = FEMALE -/datum/sprite_accessory/underwear/swimsuit_twopiece +/datum/sprite_accessory/clothing/underwear/swimsuit_twopiece name = "Two-Piece Swimsuit" icon_state = "swim_twopiece" gender = FEMALE + digi_icon_state = "short_short" -/datum/sprite_accessory/underwear/swimsuit_strapless_twopiece +/datum/sprite_accessory/clothing/underwear/swimsuit_strapless_twopiece name = "Strapless Two-Piece Swimsuit" icon_state = "swim_strapless_twopiece" gender = FEMALE + digi_icon_state = "short_short" -/datum/sprite_accessory/underwear/swimsuit_stripe +/datum/sprite_accessory/clothing/underwear/swimsuit_stripe name = "Strapless Striped Swimsuit" icon_state = "swim_stripe" gender = FEMALE -/datum/sprite_accessory/underwear/swimsuit_halter +/datum/sprite_accessory/clothing/underwear/swimsuit_halter name = "Halter Swimsuit" icon_state = "swim_halter" gender = FEMALE -/datum/sprite_accessory/underwear/female_white_neko +/datum/sprite_accessory/clothing/underwear/female_white_neko name = "Neko Bikini (White)" icon_state = "female_neko_white" gender = FEMALE use_static = TRUE -/datum/sprite_accessory/underwear/female_black_neko +/datum/sprite_accessory/clothing/underwear/female_black_neko name = "Neko Bikini (Black)" icon_state = "female_neko_black" gender = FEMALE use_static = TRUE -/datum/sprite_accessory/underwear/female_commie +/datum/sprite_accessory/clothing/underwear/female_commie name = "Commie Bikini" icon_state = "female_commie" gender = FEMALE use_static = TRUE -/datum/sprite_accessory/underwear/female_usastripe +/datum/sprite_accessory/clothing/underwear/female_usastripe name = "Freedom Bikini" icon_state = "female_assblastusa" gender = FEMALE use_static = TRUE -/datum/sprite_accessory/underwear/female_uk +/datum/sprite_accessory/clothing/underwear/female_uk name = "UK Bikini" icon_state = "female_uk" gender = FEMALE use_static = TRUE -/datum/sprite_accessory/underwear/female_kinky +/datum/sprite_accessory/clothing/underwear/female_kinky name = "Lingerie" icon_state = "female_kinky" gender = FEMALE @@ -1426,283 +1490,286 @@ GLOBAL_LIST_EMPTY(blended_hair_icons_cache) // Undershirt Definitions // //////////////////////////// -/datum/sprite_accessory/undershirt +/datum/sprite_accessory/clothing/undershirt icon = 'icons/mob/clothing/underwear.dmi' em_block = TRUE -/datum/sprite_accessory/undershirt/nude +/datum/sprite_accessory/clothing/undershirt/nude name = "Nude" icon_state = null gender = NEUTER +/datum/sprite_accessory/clothing/undershirt/nude/make_appearance(mob/living/carbon/human/for_who) + return + // please make sure they're sorted alphabetically and categorized -/datum/sprite_accessory/undershirt/bluejersey +/datum/sprite_accessory/clothing/undershirt/bluejersey name = "Jersey (Blue)" icon_state = "shirt_bluejersey" gender = NEUTER -/datum/sprite_accessory/undershirt/redjersey +/datum/sprite_accessory/clothing/undershirt/redjersey name = "Jersey (Red)" icon_state = "shirt_redjersey" gender = NEUTER -/datum/sprite_accessory/undershirt/bluepolo +/datum/sprite_accessory/clothing/undershirt/bluepolo name = "Polo Shirt (Blue)" icon_state = "bluepolo" gender = NEUTER -/datum/sprite_accessory/undershirt/grayyellowpolo +/datum/sprite_accessory/clothing/undershirt/grayyellowpolo name = "Polo Shirt (Gray-Yellow)" icon_state = "grayyellowpolo" gender = NEUTER -/datum/sprite_accessory/undershirt/redpolo +/datum/sprite_accessory/clothing/undershirt/redpolo name = "Polo Shirt (Red)" icon_state = "redpolo" gender = NEUTER -/datum/sprite_accessory/undershirt/whitepolo +/datum/sprite_accessory/clothing/undershirt/whitepolo name = "Polo Shirt (White)" icon_state = "whitepolo" gender = NEUTER -/datum/sprite_accessory/undershirt/alienshirt +/datum/sprite_accessory/clothing/undershirt/alienshirt name = "Shirt (Alien)" icon_state = "shirt_alien" gender = NEUTER -/datum/sprite_accessory/undershirt/mondmondjaja +/datum/sprite_accessory/clothing/undershirt/mondmondjaja name = "Shirt (Band)" icon_state = "band" gender = NEUTER -/datum/sprite_accessory/undershirt/shirt_black +/datum/sprite_accessory/clothing/undershirt/shirt_black name = "Shirt (Black)" icon_state = "shirt_black" gender = NEUTER -/datum/sprite_accessory/undershirt/blueshirt +/datum/sprite_accessory/clothing/undershirt/blueshirt name = "Shirt (Blue)" icon_state = "shirt_blue" gender = NEUTER -/datum/sprite_accessory/undershirt/clownshirt +/datum/sprite_accessory/clothing/undershirt/clownshirt name = "Shirt (Clown)" icon_state = "shirt_clown" gender = NEUTER -/datum/sprite_accessory/undershirt/commie +/datum/sprite_accessory/clothing/undershirt/commie name = "Shirt (Commie)" icon_state = "shirt_commie" gender = NEUTER -/datum/sprite_accessory/undershirt/greenshirt +/datum/sprite_accessory/clothing/undershirt/greenshirt name = "Shirt (Green)" icon_state = "shirt_green" gender = NEUTER -/datum/sprite_accessory/undershirt/shirt_grey +/datum/sprite_accessory/clothing/undershirt/shirt_grey name = "Shirt (Grey)" icon_state = "shirt_grey" gender = NEUTER -/datum/sprite_accessory/undershirt/ian +/datum/sprite_accessory/clothing/undershirt/ian name = "Shirt (Ian)" icon_state = "ian" gender = NEUTER -/datum/sprite_accessory/undershirt/ilovent +/datum/sprite_accessory/clothing/undershirt/ilovent name = "Shirt (I Love NT)" icon_state = "ilovent" gender = NEUTER -/datum/sprite_accessory/undershirt/lover +/datum/sprite_accessory/clothing/undershirt/lover name = "Shirt (Lover)" icon_state = "lover" gender = NEUTER -/datum/sprite_accessory/undershirt/matroska +/datum/sprite_accessory/clothing/undershirt/matroska name = "Shirt (Matroska)" icon_state = "matroska" gender = NEUTER -/datum/sprite_accessory/undershirt/meat +/datum/sprite_accessory/clothing/undershirt/meat name = "Shirt (Meat)" icon_state = "shirt_meat" gender = NEUTER -/datum/sprite_accessory/undershirt/nano +/datum/sprite_accessory/clothing/undershirt/nano name = "Shirt (Nanotrasen)" icon_state = "shirt_nano" gender = NEUTER -/datum/sprite_accessory/undershirt/peace +/datum/sprite_accessory/clothing/undershirt/peace name = "Shirt (Peace)" icon_state = "peace" gender = NEUTER -/datum/sprite_accessory/undershirt/pacman +/datum/sprite_accessory/clothing/undershirt/pacman name = "Shirt (Pogoman)" icon_state = "pogoman" gender = NEUTER -/datum/sprite_accessory/undershirt/question +/datum/sprite_accessory/clothing/undershirt/question name = "Shirt (Question)" icon_state = "shirt_question" gender = NEUTER -/datum/sprite_accessory/undershirt/redshirt +/datum/sprite_accessory/clothing/undershirt/redshirt name = "Shirt (Red)" icon_state = "shirt_red" gender = NEUTER -/datum/sprite_accessory/undershirt/skull +/datum/sprite_accessory/clothing/undershirt/skull name = "Shirt (Skull)" icon_state = "shirt_skull" gender = NEUTER -/datum/sprite_accessory/undershirt/ss13 +/datum/sprite_accessory/clothing/undershirt/ss13 name = "Shirt (SS13)" icon_state = "shirt_ss13" gender = NEUTER -/datum/sprite_accessory/undershirt/stripe +/datum/sprite_accessory/clothing/undershirt/stripe name = "Shirt (Striped)" icon_state = "shirt_stripes" gender = NEUTER -/datum/sprite_accessory/undershirt/tiedye +/datum/sprite_accessory/clothing/undershirt/tiedye name = "Shirt (Tie-dye)" icon_state = "shirt_tiedye" gender = NEUTER -/datum/sprite_accessory/undershirt/uk +/datum/sprite_accessory/clothing/undershirt/uk name = "Shirt (UK)" icon_state = "uk" gender = NEUTER -/datum/sprite_accessory/undershirt/usa +/datum/sprite_accessory/clothing/undershirt/usa name = "Shirt (USA)" icon_state = "shirt_assblastusa" gender = NEUTER -/datum/sprite_accessory/undershirt/shirt_white +/datum/sprite_accessory/clothing/undershirt/shirt_white name = "Shirt (White)" icon_state = "shirt_white" gender = NEUTER -/datum/sprite_accessory/undershirt/blackshortsleeve +/datum/sprite_accessory/clothing/undershirt/blackshortsleeve name = "Short-sleeved Shirt (Black)" icon_state = "blackshortsleeve" gender = NEUTER -/datum/sprite_accessory/undershirt/blueshortsleeve +/datum/sprite_accessory/clothing/undershirt/blueshortsleeve name = "Short-sleeved Shirt (Blue)" icon_state = "blueshortsleeve" gender = NEUTER -/datum/sprite_accessory/undershirt/greenshortsleeve +/datum/sprite_accessory/clothing/undershirt/greenshortsleeve name = "Short-sleeved Shirt (Green)" icon_state = "greenshortsleeve" gender = NEUTER -/datum/sprite_accessory/undershirt/purpleshortsleeve +/datum/sprite_accessory/clothing/undershirt/purpleshortsleeve name = "Short-sleeved Shirt (Purple)" icon_state = "purpleshortsleeve" gender = NEUTER -/datum/sprite_accessory/undershirt/whiteshortsleeve +/datum/sprite_accessory/clothing/undershirt/whiteshortsleeve name = "Short-sleeved Shirt (White)" icon_state = "whiteshortsleeve" gender = NEUTER -/datum/sprite_accessory/undershirt/sports_bra +/datum/sprite_accessory/clothing/undershirt/sports_bra name = "Sports Bra" icon_state = "sports_bra" gender = NEUTER -/datum/sprite_accessory/undershirt/sports_bra2 +/datum/sprite_accessory/clothing/undershirt/sports_bra2 name = "Sports Bra (Alt)" icon_state = "sports_bra_alt" gender = NEUTER -/datum/sprite_accessory/undershirt/blueshirtsport +/datum/sprite_accessory/clothing/undershirt/blueshirtsport name = "Sports Shirt (Blue)" icon_state = "blueshirtsport" gender = NEUTER -/datum/sprite_accessory/undershirt/greenshirtsport +/datum/sprite_accessory/clothing/undershirt/greenshirtsport name = "Sports Shirt (Green)" icon_state = "greenshirtsport" gender = NEUTER -/datum/sprite_accessory/undershirt/redshirtsport +/datum/sprite_accessory/clothing/undershirt/redshirtsport name = "Sports Shirt (Red)" icon_state = "redshirtsport" gender = NEUTER -/datum/sprite_accessory/undershirt/tank_black +/datum/sprite_accessory/clothing/undershirt/tank_black name = "Tank Top (Black)" icon_state = "tank_black" gender = NEUTER -/datum/sprite_accessory/undershirt/tankfire +/datum/sprite_accessory/clothing/undershirt/tankfire name = "Tank Top (Fire)" icon_state = "tank_fire" gender = NEUTER -/datum/sprite_accessory/undershirt/tank_grey +/datum/sprite_accessory/clothing/undershirt/tank_grey name = "Tank Top (Grey)" icon_state = "tank_grey" gender = NEUTER -/datum/sprite_accessory/undershirt/female_midriff +/datum/sprite_accessory/clothing/undershirt/female_midriff name = "Tank Top (Midriff)" icon_state = "tank_midriff" gender = FEMALE -/datum/sprite_accessory/undershirt/tank_red +/datum/sprite_accessory/clothing/undershirt/tank_red name = "Tank Top (Red)" icon_state = "tank_red" gender = NEUTER -/datum/sprite_accessory/undershirt/tankstripe +/datum/sprite_accessory/clothing/undershirt/tankstripe name = "Tank Top (Striped)" icon_state = "tank_stripes" gender = NEUTER -/datum/sprite_accessory/undershirt/tank_white +/datum/sprite_accessory/clothing/undershirt/tank_white name = "Tank Top (White)" icon_state = "tank_white" gender = NEUTER -/datum/sprite_accessory/undershirt/redtop +/datum/sprite_accessory/clothing/undershirt/redtop name = "Top (Red)" icon_state = "redtop" gender = FEMALE -/datum/sprite_accessory/undershirt/whitetop +/datum/sprite_accessory/clothing/undershirt/whitetop name = "Top (White)" icon_state = "whitetop" gender = FEMALE -/datum/sprite_accessory/undershirt/tshirt_blue +/datum/sprite_accessory/clothing/undershirt/tshirt_blue name = "T-Shirt (Blue)" icon_state = "blueshirt" gender = NEUTER -/datum/sprite_accessory/undershirt/tshirt_green +/datum/sprite_accessory/clothing/undershirt/tshirt_green name = "T-Shirt (Green)" icon_state = "greenshirt" gender = NEUTER -/datum/sprite_accessory/undershirt/tshirt_red +/datum/sprite_accessory/clothing/undershirt/tshirt_red name = "T-Shirt (Red)" icon_state = "redshirt" gender = NEUTER -/datum/sprite_accessory/undershirt/yellowshirt +/datum/sprite_accessory/clothing/undershirt/yellowshirt name = "T-Shirt (Yellow)" icon_state = "yellowshirt" gender = NEUTER @@ -1711,169 +1778,172 @@ GLOBAL_LIST_EMPTY(blended_hair_icons_cache) // Socks Definitions // /////////////////////// -/datum/sprite_accessory/socks +/datum/sprite_accessory/clothing/socks icon = 'icons/mob/clothing/underwear.dmi' em_block = TRUE -/datum/sprite_accessory/socks/nude +/datum/sprite_accessory/clothing/socks/nude name = "Nude" icon_state = null +/datum/sprite_accessory/clothing/socks/nude/make_appearance(mob/living/carbon/human/for_who) + return + // please make sure they're sorted alphabetically and categorized -/datum/sprite_accessory/socks/ace_knee +/datum/sprite_accessory/clothing/socks/ace_knee name = "Knee-high (Ace)" icon_state = "ace_knee" -/datum/sprite_accessory/socks/bee_knee +/datum/sprite_accessory/clothing/socks/bee_knee name = "Knee-high (Bee)" icon_state = "bee_knee" -/datum/sprite_accessory/socks/black_knee +/datum/sprite_accessory/clothing/socks/black_knee name = "Knee-high (Black)" icon_state = "black_knee" -/datum/sprite_accessory/socks/commie_knee +/datum/sprite_accessory/clothing/socks/commie_knee name = "Knee-High (Commie)" icon_state = "commie_knee" -/datum/sprite_accessory/socks/usa_knee +/datum/sprite_accessory/clothing/socks/usa_knee name = "Knee-High (Freedom)" icon_state = "assblastusa_knee" -/datum/sprite_accessory/socks/rainbow_knee +/datum/sprite_accessory/clothing/socks/rainbow_knee name = "Knee-high (Rainbow)" icon_state = "rainbow_knee" -/datum/sprite_accessory/socks/striped_knee +/datum/sprite_accessory/clothing/socks/striped_knee name = "Knee-high (Striped)" icon_state = "striped_knee" -/datum/sprite_accessory/socks/thin_knee +/datum/sprite_accessory/clothing/socks/thin_knee name = "Knee-high (Thin)" icon_state = "thin_knee" -/datum/sprite_accessory/socks/trans_knee +/datum/sprite_accessory/clothing/socks/trans_knee name = "Knee-high (Trans)" icon_state = "trans_knee" -/datum/sprite_accessory/socks/uk_knee +/datum/sprite_accessory/clothing/socks/uk_knee name = "Knee-High (UK)" icon_state = "uk_knee" -/datum/sprite_accessory/socks/white_knee +/datum/sprite_accessory/clothing/socks/white_knee name = "Knee-high (White)" icon_state = "white_knee" -/datum/sprite_accessory/socks/fishnet_knee +/datum/sprite_accessory/clothing/socks/fishnet_knee name = "Knee-high (Fishnet)" icon_state = "fishnet_knee" -/datum/sprite_accessory/socks/black_norm +/datum/sprite_accessory/clothing/socks/black_norm name = "Normal (Black)" icon_state = "black_norm" -/datum/sprite_accessory/socks/white_norm +/datum/sprite_accessory/clothing/socks/white_norm name = "Normal (White)" icon_state = "white_norm" -/datum/sprite_accessory/socks/pantyhose +/datum/sprite_accessory/clothing/socks/pantyhose name = "Pantyhose" icon_state = "pantyhose" -/datum/sprite_accessory/socks/black_short +/datum/sprite_accessory/clothing/socks/black_short name = "Short (Black)" icon_state = "black_short" -/datum/sprite_accessory/socks/white_short +/datum/sprite_accessory/clothing/socks/white_short name = "Short (White)" icon_state = "white_short" -/datum/sprite_accessory/socks/stockings_blue +/datum/sprite_accessory/clothing/socks/stockings_blue name = "Stockings (Blue)" icon_state = "stockings_blue" -/datum/sprite_accessory/socks/stockings_cyan +/datum/sprite_accessory/clothing/socks/stockings_cyan name = "Stockings (Cyan)" icon_state = "stockings_cyan" -/datum/sprite_accessory/socks/stockings_dpink +/datum/sprite_accessory/clothing/socks/stockings_dpink name = "Stockings (Dark Pink)" icon_state = "stockings_dpink" -/datum/sprite_accessory/socks/stockings_green +/datum/sprite_accessory/clothing/socks/stockings_green name = "Stockings (Green)" icon_state = "stockings_green" -/datum/sprite_accessory/socks/stockings_orange +/datum/sprite_accessory/clothing/socks/stockings_orange name = "Stockings (Orange)" icon_state = "stockings_orange" -/datum/sprite_accessory/socks/stockings_programmer +/datum/sprite_accessory/clothing/socks/stockings_programmer name = "Stockings (Programmer)" icon_state = "stockings_lpink" -/datum/sprite_accessory/socks/stockings_purple +/datum/sprite_accessory/clothing/socks/stockings_purple name = "Stockings (Purple)" icon_state = "stockings_purple" -/datum/sprite_accessory/socks/stockings_yellow +/datum/sprite_accessory/clothing/socks/stockings_yellow name = "Stockings (Yellow)" icon_state = "stockings_yellow" -/datum/sprite_accessory/socks/stockings_fishnet +/datum/sprite_accessory/clothing/socks/stockings_fishnet name = "Stockings (Fishnet)" icon_state = "fishnet_full" -/datum/sprite_accessory/socks/ace_thigh +/datum/sprite_accessory/clothing/socks/ace_thigh name = "Thigh-high (Ace)" icon_state = "ace_thigh" -/datum/sprite_accessory/socks/bee_thigh +/datum/sprite_accessory/clothing/socks/bee_thigh name = "Thigh-high (Bee)" icon_state = "bee_thigh" -/datum/sprite_accessory/socks/black_thigh +/datum/sprite_accessory/clothing/socks/black_thigh name = "Thigh-high (Black)" icon_state = "black_thigh" -/datum/sprite_accessory/socks/commie_thigh +/datum/sprite_accessory/clothing/socks/commie_thigh name = "Thigh-high (Commie)" icon_state = "commie_thigh" -/datum/sprite_accessory/socks/usa_thigh +/datum/sprite_accessory/clothing/socks/usa_thigh name = "Thigh-high (Freedom)" icon_state = "assblastusa_thigh" -/datum/sprite_accessory/socks/rainbow_thigh +/datum/sprite_accessory/clothing/socks/rainbow_thigh name = "Thigh-high (Rainbow)" icon_state = "rainbow_thigh" -/datum/sprite_accessory/socks/striped_thigh +/datum/sprite_accessory/clothing/socks/striped_thigh name = "Thigh-high (Striped)" icon_state = "striped_thigh" -/datum/sprite_accessory/socks/thin_thigh +/datum/sprite_accessory/clothing/socks/thin_thigh name = "Thigh-high (Thin)" icon_state = "thin_thigh" -/datum/sprite_accessory/socks/trans_thigh +/datum/sprite_accessory/clothing/socks/trans_thigh name = "Thigh-high (Trans)" icon_state = "trans_thigh" -/datum/sprite_accessory/socks/uk_thigh +/datum/sprite_accessory/clothing/socks/uk_thigh name = "Thigh-high (UK)" icon_state = "uk_thigh" -/datum/sprite_accessory/socks/white_thigh +/datum/sprite_accessory/clothing/socks/white_thigh name = "Thigh-high (White)" icon_state = "white_thigh" -/datum/sprite_accessory/socks/fishnet_thigh +/datum/sprite_accessory/clothing/socks/fishnet_thigh name = "Thigh-high (Fishnet)" icon_state = "fishnet_thigh" -/datum/sprite_accessory/socks/thocks +/datum/sprite_accessory/clothing/socks/thocks name = "Thocks" icon_state = "thocks" diff --git a/code/game/objects/structures/mannequin.dm b/code/game/objects/structures/mannequin.dm index 8fedeebb1143..92973340e0e7 100644 --- a/code/game/objects/structures/mannequin.dm +++ b/code/game/objects/structures/mannequin.dm @@ -95,21 +95,18 @@ var/mutable_appearance/pedestal = mutable_appearance(icon, "pedestal_[material]") pedestal.pixel_z = -3 . += pedestal - var/datum/sprite_accessory/underwear/underwear = SSaccessories.underwear_list[underwear_name] - if(underwear) - if(body_type == FEMALE && underwear.gender == MALE) - . += mutable_appearance(wear_female_version(underwear.icon_state, underwear.icon, FEMALE_UNIFORM_FULL), layer = -BODY_LAYER) - else - . += mutable_appearance(underwear.icon, underwear.icon_state, layer = -BODY_LAYER) - var/datum/sprite_accessory/undershirt/undershirt = SSaccessories.undershirt_list[undershirt_name] - if(undershirt) - if(body_type == FEMALE) - . += mutable_appearance(wear_female_version(undershirt.icon_state, undershirt.icon), layer = -BODY_LAYER) - else - . += mutable_appearance(undershirt.icon, undershirt.icon_state, layer = -BODY_LAYER) - var/datum/sprite_accessory/socks/socks = SSaccessories.socks_list[socks_name] - if(socks) - . += mutable_appearance(socks.icon, socks.icon_state, -BODY_LAYER) + var/datum/sprite_accessory/clothing/underwear/underwear = SSaccessories.underwear_list[underwear_name] + var/mutable_appearance/underwear_overlay = underwear?.make_appearance(COLOR_WHITE, body_type, BODYSHAPE_HUMANOID) + if(underwear_overlay) + . += underwear_overlay + var/datum/sprite_accessory/clothing/undershirt/undershirt = SSaccessories.undershirt_list[undershirt_name] + var/mutable_appearance/undershirt_overlay = undershirt?.make_appearance(COLOR_WHITE, body_type, BODYSHAPE_HUMANOID) + if(undershirt_overlay) + . += undershirt_overlay + var/datum/sprite_accessory/clothing/socks/socks = SSaccessories.socks_list[socks_name] + var/mutable_appearance/socks_overlay = socks?.make_appearance(COLOR_WHITE, body_type, BODYSHAPE_HUMANOID) + if(socks_overlay) + . += socks_overlay for(var/slot_flag in worn_items) var/obj/item/worn_item = worn_items[slot_flag] if(!worn_item) diff --git a/code/modules/basketball/basketball_teams.dm b/code/modules/basketball/basketball_teams.dm index 9169fa4fcc43..dfd348096f66 100644 --- a/code/modules/basketball/basketball_teams.dm +++ b/code/modules/basketball/basketball_teams.dm @@ -62,7 +62,7 @@ /datum/outfit/basketball/nanotrasen name = "Basketball NT Team" - undershirt = /datum/sprite_accessory/undershirt/bluejersey + undershirt = /datum/sprite_accessory/clothing/undershirt/bluejersey uniform = /obj/item/clothing/under/shorts/blue suit = /obj/item/clothing/suit/jacket/letterman_nanotrasen shoes = /obj/item/clothing/shoes/sneakers/black @@ -129,9 +129,9 @@ /datum/outfit/basketball/beach_bums name = "Basketball Beach Bums" - undershirt = /datum/sprite_accessory/undershirt/nude - underwear = /datum/sprite_accessory/underwear/nude - socks = /datum/sprite_accessory/socks/nude + undershirt = /datum/sprite_accessory/clothing/undershirt/nude + underwear = /datum/sprite_accessory/clothing/underwear/nude + socks = /datum/sprite_accessory/clothing/socks/nude uniform = /obj/item/clothing/under/shorts/red glasses = /obj/item/clothing/glasses/sunglasses shoes = /obj/item/clothing/shoes/sandal diff --git a/code/modules/client/preferences/clothing.dm b/code/modules/client/preferences/clothing.dm index 50ede0ed3934..0d7883e56cbb 100644 --- a/code/modules/client/preferences/clothing.dm +++ b/code/modules/client/preferences/clothing.dm @@ -106,7 +106,7 @@ return assoc_to_keys_features(SSaccessories.socks_list) /datum/preference/choiced/socks/create_default_value() - return /datum/sprite_accessory/socks/nude::name + return /datum/sprite_accessory/clothing/socks/nude::name /datum/preference/choiced/socks/icon_for(value) var/static/datum/universal_icon/lower_half @@ -135,14 +135,14 @@ return assoc_to_keys_features(SSaccessories.undershirt_list) /datum/preference/choiced/undershirt/create_default_value() - return /datum/sprite_accessory/undershirt/nude::name + return /datum/sprite_accessory/clothing/undershirt/nude::name /datum/preference/choiced/undershirt/create_informed_default_value(datum/preferences/preferences) switch(preferences.read_preference(/datum/preference/choiced/gender)) if(MALE) - return /datum/sprite_accessory/undershirt/nude::name + return /datum/sprite_accessory/clothing/undershirt/nude::name if(FEMALE) - return /datum/sprite_accessory/undershirt/sports_bra::name + return /datum/sprite_accessory/clothing/undershirt/sports_bra::name return ..() @@ -183,7 +183,7 @@ return assoc_to_keys_features(SSaccessories.underwear_list) /datum/preference/choiced/underwear/create_default_value() - return /datum/sprite_accessory/underwear/male_hearts::name + return /datum/sprite_accessory/clothing/underwear/male_hearts::name /datum/preference/choiced/underwear/icon_for(value) var/static/datum/universal_icon/lower_half diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm index 618cba05e402..1d48437ed42f 100644 --- a/code/modules/jobs/job_types/head_of_personnel.dm +++ b/code/modules/jobs/job_types/head_of_personnel.dm @@ -113,7 +113,7 @@ /datum/outfit/job/hop/pre_equip(mob/living/carbon/human/H) ..() if(check_holidays(IAN_HOLIDAY)) - undershirt = /datum/sprite_accessory/undershirt/ian + undershirt = /datum/sprite_accessory/clothing/undershirt/ian //only pet worth reviving /datum/job/head_of_personnel/get_mail_goodies(mob/recipient) diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm index 15d8175ffcf3..c55a85b9de8e 100644 --- a/code/modules/mob/living/carbon/human/human_update_icons.dm +++ b/code/modules/mob/living/carbon/human/human_update_icons.dm @@ -870,32 +870,24 @@ generate/load female uniform sprites matching all previously decided variables return // Underwear, Undershirts & Socks var/list/standing = list() + var/active_bodyshapes = get_active_bodyshapes() if(underwear) - var/datum/sprite_accessory/underwear/undie_accessory = SSaccessories.underwear_list[underwear] - var/mutable_appearance/underwear_overlay - if(undie_accessory) - if(dna.species.sexes && physique == FEMALE && (undie_accessory.gender == MALE)) - underwear_overlay = mutable_appearance(wear_female_version(undie_accessory.icon_state, undie_accessory.icon, FEMALE_UNIFORM_FULL), layer = -BODY_LAYER) - else - underwear_overlay = mutable_appearance(undie_accessory.icon, undie_accessory.icon_state, -BODY_LAYER) - if(!undie_accessory.use_static) - underwear_overlay.color = underwear_color + var/datum/sprite_accessory/clothing/underwear/undie_accessory = SSaccessories.underwear_list[underwear] + var/mutable_appearance/underwear_overlay = undie_accessory?.make_appearance(underwear_color, physique, active_bodyshapes) + if(underwear_overlay) standing += underwear_overlay if(undershirt) - var/datum/sprite_accessory/undershirt/undie_accessory = SSaccessories.undershirt_list[undershirt] - if(undie_accessory) - var/mutable_appearance/working_shirt - if(dna.species.sexes && physique == FEMALE) - working_shirt = mutable_appearance(wear_female_version(undie_accessory.icon_state, undie_accessory.icon), layer = -BODY_LAYER) - else - working_shirt = mutable_appearance(undie_accessory.icon, undie_accessory.icon_state, layer = -BODY_LAYER) - standing += working_shirt + var/datum/sprite_accessory/clothing/undershirt/shirt_accessory = SSaccessories.undershirt_list[undershirt] + var/mutable_appearance/shirt_overlay = shirt_accessory?.make_appearance(null, physique, active_bodyshapes) + if(shirt_overlay) + standing += shirt_overlay if(socks && num_legs >= 2 && !(bodyshape & BODYSHAPE_DIGITIGRADE)) - var/datum/sprite_accessory/socks/undie_accessory = SSaccessories.socks_list[socks] - if(undie_accessory) - standing += mutable_appearance(undie_accessory.icon, undie_accessory.icon_state, -BODY_LAYER) + var/datum/sprite_accessory/clothing/socks/sock_accessory = SSaccessories.socks_list[socks] + var/mutable_appearance/socks_overlay = sock_accessory?.make_appearance(null, physique, active_bodyshapes) + if(socks_overlay) + standing += socks_overlay if(standing.len) overlays_standing[BODY_LAYER] = standing diff --git a/code/modules/surgery/bodyparts/helpers.dm b/code/modules/surgery/bodyparts/helpers.dm index afb7c18d1e33..258162453a72 100644 --- a/code/modules/surgery/bodyparts/helpers.dm +++ b/code/modules/surgery/bodyparts/helpers.dm @@ -286,6 +286,16 @@ bodyshape = all_limb_flags +/// Get all bodyshapes but filter out bodyshapes that are currently being hidden +/mob/living/carbon/proc/get_active_bodyshapes() + var/active_shapes = bodyshape + // future todo: both of these are state based, maybe we can just remove relevant bodyshapes directly. would remove the need for this proc + if((active_shapes & BODYSHAPE_DIGITIGRADE) && is_digitigrade_squished()) + active_shapes &= ~BODYSHAPE_DIGITIGRADE + if((active_shapes & BODYSHAPE_SNOUTED) && (obscured_slots & HIDESNOUT)) + active_shapes &= ~BODYSHAPE_SNOUTED + return active_shapes + /proc/skintone2hex(skin_tone) . = 0 switch(skin_tone) diff --git a/icons/mob/clothing/digi_template.dmi b/icons/mob/clothing/digi_template_equpiment.dmi similarity index 100% rename from icons/mob/clothing/digi_template.dmi rename to icons/mob/clothing/digi_template_equpiment.dmi diff --git a/icons/mob/clothing/digi_template_underwear.dmi b/icons/mob/clothing/digi_template_underwear.dmi new file mode 100644 index 0000000000000000000000000000000000000000..767a56476b60d86499c3e2b53c38b611e0f0ec3a GIT binary patch literal 805 zcmV+=1KRwFP)V=-0C=3G zlDiIrFbqZKMi2Gw z3ur)f^0feP*qR&KcFB;>R)IX+_1_$ISffKf3QDy|E= zC#>7a+efIg+0IAm{T449>%iP%%&b7Id6EK?=K|Gw=SJgr!aWuOF7b9vp%fSwAyC%0 zvYb@sS%-$=mkJmcR^A~!4);<4000000Prrf`v0EdqTV@f-<$n^rM}(&SL*KxiFcs4 z Date: Tue, 24 Mar 2026 03:07:18 +0000 Subject: [PATCH 137/155] Automatic changelog for PR #95454 [ci skip] --- html/changelogs/AutoChangeLog-pr-95454.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95454.yml diff --git a/html/changelogs/AutoChangeLog-pr-95454.yml b/html/changelogs/AutoChangeLog-pr-95454.yml new file mode 100644 index 000000000000..45622a249156 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95454.yml @@ -0,0 +1,5 @@ +author: "Melbert" +delete-after: True +changes: + - rscadd: "Some undergarmants will now use a generic replacement on lizard body shapes that fit more appropriately" + - refactor: "Refactored how undergarmants generate their icons, report any oddities with that" \ No newline at end of file From 7ae15d7a85cf099ed0a1a1b4307a7ac1fe4b8a03 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:00:22 +0000 Subject: [PATCH 138/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95427.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95454.yml | 5 ----- html/changelogs/archive/2026-03.yml | 7 +++++++ 3 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95427.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95454.yml diff --git a/html/changelogs/AutoChangeLog-pr-95427.yml b/html/changelogs/AutoChangeLog-pr-95427.yml deleted file mode 100644 index a992d60fc0e2..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95427.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Iajret" -delete-after: True -changes: - - bugfix: "fixed vitals display not respecting APCs power when you attach it to a wall" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95454.yml b/html/changelogs/AutoChangeLog-pr-95454.yml deleted file mode 100644 index 45622a249156..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95454.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - rscadd: "Some undergarmants will now use a generic replacement on lizard body shapes that fit more appropriately" - - refactor: "Refactored how undergarmants generate their icons, report any oddities with that" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 638d30c29104..d2dbbc63cf87 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -361,6 +361,9 @@ 2026-03-24: ArcaneMusic: - bugfix: Cargo goodies are now visible in the ordering and request consoles. + Iajret: + - bugfix: fixed vitals display not respecting APCs power when you attach it to a + wall Jacquerel: - rscadd: Nonhuman mobs with hands can now pick up other mobs that are smaller than them and can usually be picked up @@ -368,6 +371,10 @@ - bugfix: The skeleton from being shocked matches your height - bugfix: Drinking soup will loop until it's empty or you cancel it (as it used to) + - rscadd: Some undergarmants will now use a generic replacement on lizard body shapes + that fit more appropriately + - refactor: Refactored how undergarmants generate their icons, report any oddities + with that Y0SH1M4S73R: - qol: Many circuit components have had ports that effectively act as boolean inputs or outputs converted into a new boolean datatype. From 43c049386fd5cfc8272d76701c39a625518cb0d5 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:25:20 -0500 Subject: [PATCH 139/155] RTG code refresh (#95372) ## About The Pull Request 1. Mappers and admins can now VV RTG power without it breaking 2. Replace attackby usage 3. `base_icon_state` usage 4. Rewriting power in terms of watts 5. Grammar updates ## Why It's Good For The Game These are mostly meant for events and mappers but they were relatively difficult to use for events and mappers, requiring you made a subtype. This just brings the code up to snuff to make them more usable. ## Changelog :cl: Melbert code: Cleaned up RTG code. Admins can now VV them easier. If you come across a ruin on Lavaland or in space that uses them and see any odd behavior, report it as an issue. /:cl: --- code/modules/power/rtg.dm | 131 +++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/code/modules/power/rtg.dm b/code/modules/power/rtg.dm index 4390ae738542..dd21c40ae91b 100644 --- a/code/modules/power/rtg.dm +++ b/code/modules/power/rtg.dm @@ -6,6 +6,7 @@ desc = "A simple nuclear power generator, used in small outposts to reliably provide power for decades." icon = 'icons/obj/machines/engine/other.dmi' icon_state = "rtg" + base_icon_state = "rtg" density = TRUE use_power = NO_POWER_USE circuit = /obj/item/circuitboard/machine/rtg @@ -15,61 +16,87 @@ buckle_lying = 0 buckle_requires_restraints = TRUE - var/power_gen = 1000 // Enough to power a single APC. 4000 output with T4 capacitor. + /// Whether stock parts affect power generated + var/affected_by_parts = TRUE + /// Free power generated every tick + var/power_gen = 1 KILO WATTS + /// Base power gen level, potentially modified by parts + VAR_PRIVATE/base_power_gen /obj/machinery/power/rtg/Initialize(mapload) + base_power_gen = power_gen . = ..() connect_to_network() + RefreshParts() /obj/machinery/power/rtg/process() add_avail(power_to_energy(power_gen)) /obj/machinery/power/rtg/RefreshParts() . = ..() - var/part_level = 0 - for(var/datum/stock_part/stock_part in component_parts) - part_level += stock_part.tier + var/new_power_gen = get_base_power_gen() + if(affected_by_parts) + var/part_level = 0 + for(var/datum/stock_part/stock_part in component_parts) + part_level += stock_part.tier - power_gen = initial(power_gen) * part_level + new_power_gen = base_power_gen * (part_level || 1) + + power_gen = new_power_gen + +/obj/machinery/power/rtg/proc/get_base_power_gen() + return base_power_gen /obj/machinery/power/rtg/examine(mob/user) . = ..() if(in_range(user, src) || isobserver(user)) . += span_notice("The status display reads: Power generation at [display_power(power_gen, convert = FALSE)].") -/obj/machinery/power/rtg/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-open", initial(icon_state), I)) - return - else if(default_deconstruction_crowbar(I)) - return - return ..() +/obj/machinery/power/rtg/screwdriver_act(mob/living/user, obj/item/tool) + if(default_deconstruction_screwdriver(user, "[base_icon_state]-open", base_icon_state, tool)) + return ITEM_INTERACT_SUCCESS + return ITEM_INTERACT_BLOCKING + +/obj/machinery/power/rtg/crowbar_act(mob/living/user, obj/item/tool) + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + return panel_open ? ITEM_INTERACT_BLOCKING : NONE + +/obj/machinery/power/rtg/vv_edit_var(vname, vval) + . = ..() + if(vname == NAMEOF(src, power_gen) || vname == NAMEOF(src, base_power_gen) || vname == NAMEOF(src, affected_by_parts)) + RefreshParts() /obj/machinery/power/rtg/advanced desc = "An advanced RTG capable of moderating isotope decay, increasing power output but reducing lifetime. It uses plasma-fueled radiation collectors to increase output even further." - power_gen = 1250 // 2500 on T1, 10000 on T4. + power_gen = 1.25 KILO WATTS circuit = /obj/item/circuitboard/machine/rtg/advanced // Void Core, power source for Abductor ships and bases. // Provides a lot of power, but tends to explode when mistreated. /obj/machinery/power/rtg/abductor - name = "Void Core" + name = "void core" icon = 'icons/obj/antags/abductor.dmi' icon_state = "core" + base_icon_state = "core" desc = "An alien power source that produces energy seemingly out of nowhere." circuit = /obj/item/circuitboard/machine/abductor/core - power_gen = 20000 // 280 000 at T1, 400 000 at T4. Starts at T4. + power_gen = 20 KILO WATTS can_buckle = FALSE - pixel_y = 7 - var/going_kaboom = FALSE // Is it about to explode? + SET_BASE_PIXEL(0, 7) + /// Is it about to explode? + VAR_PRIVATE/going_kaboom = FALSE /obj/machinery/power/rtg/abductor/proc/overload() if(going_kaboom) return going_kaboom = TRUE - visible_message(span_danger("\The [src] lets out a shower of sparks as it starts to lose stability!"),\ - span_hear("You hear a loud electrical crack!")) - playsound(src.loc, 'sound/effects/magic/lightningshock.ogg', 100, TRUE, extrarange = 5) + visible_message( + message = span_danger("[src] lets out a shower of sparks as it starts to lose stability!"), + blind_message = span_hear("You hear a loud electrical crack!"), + ) + playsound(src, 'sound/effects/magic/lightningshock.ogg', 100, TRUE, extrarange = 5) tesla_zap(source = src, zap_range = 5, power = power_gen * 20) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(explosion), src, 2, 3, 4, null, 8), 10 SECONDS) // Not a normal explosion. @@ -99,61 +126,47 @@ overload() /obj/machinery/power/rtg/debug - name = "Debug RTG" + name = "debug " + parent_type::name desc = "You really shouldn't be seeing this if you're not a coder or jannie." - power_gen = 20000 + power_gen = 20 KILO WATTS circuit = null - -/obj/machinery/power/rtg/debug/RefreshParts() - SHOULD_CALL_PARENT(FALSE) - return + affected_by_parts = FALSE /obj/machinery/power/rtg/lavaland - name = "Lava powered RTG" - desc = "This device only works when exposed to the toxic fumes of Lavaland" + name = "lava powered " + parent_type::name + desc = "A power generator that uses the heat and atmosphere of Lavaland to generate power. Won't generate squat anywhere else." circuit = null - power_gen = 20000 + power_gen = 20 KILO WATTS anchored = TRUE resistance_flags = LAVA_PROOF -/obj/machinery/power/rtg/lavaland/Initialize(mapload) - . = ..() - var/turf/our_turf = get_turf(src) - if(!islava(our_turf)) - power_gen = 0 - if(!is_mining_level(z)) - power_gen = 0 - /obj/machinery/power/rtg/lavaland/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) . = ..() + RefreshParts() + +/obj/machinery/power/rtg/lavaland/get_base_power_gen() var/turf/our_turf = get_turf(src) - if(!islava(our_turf)) - power_gen = 0 - return - if(!is_mining_level(z)) - power_gen = 0 - return - power_gen = initial(power_gen) + if(islava(our_turf) && is_mining_level(our_turf.z)) + return base_power_gen + return 0 /obj/machinery/power/rtg/old_station - name = "Old RTG" - desc = "A very old RTG, it seems on the verge of being destroyed" + name = "old " + parent_type::name + desc = "A very old " + parent_type::name + ". It seems on the verge of being destroyed." circuit = null - power_gen = 750 + power_gen = 0.75 KILO WATTS anchored = TRUE -/obj/machinery/power/rtg/old_station/attackby(obj/item/I, mob/user, list/modifiers, list/attack_modifiers) - if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-open", initial(icon_state), I)) - to_chat(user,span_warning("You feel it crumbling under your hands!")) - return - else if(default_deconstruction_crowbar(I, user = user)) - return - return ..() +/obj/machinery/power/rtg/old_station/default_deconstruction_screwdriver(mob/user, icon_state_open, icon_state_closed, obj/item/screwdriver) + . = ..() + if(.) + to_chat(user, span_warning("You feel it crumbling under your hands!")) /obj/machinery/power/rtg/old_station/default_deconstruction_crowbar(obj/item/crowbar, ignore_panel, custom_deconstruct, mob/user) - to_chat(user,span_warning("It's starting to fall off!")) - if(!do_after(user, 3 SECONDS, src)) - return TRUE - to_chat(user,span_notice("You feel like you made a mistake")) - new /obj/effect/decal/cleanable/ash/large(loc) - qdel(src) + to_chat(user, span_warning("As you pry, [src] starts to fall apart!")) + if(!crowbar.use_tool(src, user, 3 SECONDS, volume = 50)) + return FALSE + to_chat(user, span_warning("You feel like you made a mistake.")) + new /obj/effect/decal/cleanable/ash/large(drop_location()) + deconstruct(FALSE) + return TRUE From b3ea9f40ab4a1fa9952a7373a43dff703674d466 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:25:41 +0000 Subject: [PATCH 140/155] Automatic changelog for PR #95372 [ci skip] --- html/changelogs/AutoChangeLog-pr-95372.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95372.yml diff --git a/html/changelogs/AutoChangeLog-pr-95372.yml b/html/changelogs/AutoChangeLog-pr-95372.yml new file mode 100644 index 000000000000..383719b90401 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95372.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - code_imp: "Cleaned up RTG code. Admins can now VV them easier. If you come across a ruin on Lavaland or in space that uses them and see any odd behavior, report it as an issue." \ No newline at end of file From fdf2ca16086ca29eb2334212be41050df9308202 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 06:30:25 +0000 Subject: [PATCH 141/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95372.yml | 4 ---- html/changelogs/archive/2026-03.yml | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95372.yml diff --git a/html/changelogs/AutoChangeLog-pr-95372.yml b/html/changelogs/AutoChangeLog-pr-95372.yml deleted file mode 100644 index 383719b90401..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95372.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Melbert" -delete-after: True -changes: - - code_imp: "Cleaned up RTG code. Admins can now VV them easier. If you come across a ruin on Lavaland or in space that uses them and see any odd behavior, report it as an issue." \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index d2dbbc63cf87..0a2b2643b77b 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -375,6 +375,9 @@ that fit more appropriately - refactor: Refactored how undergarmants generate their icons, report any oddities with that + - code_imp: Cleaned up RTG code. Admins can now VV them easier. If you come across + a ruin on Lavaland or in space that uses them and see any odd behavior, report + it as an issue. Y0SH1M4S73R: - qol: Many circuit components have had ports that effectively act as boolean inputs or outputs converted into a new boolean datatype. From 72f65a06b6d5b7410b5d1d87cfbee1a45954095d Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:58:21 -0400 Subject: [PATCH 142/155] Vended glasses in the Beach Bar virtual domain count towards domain completion when drank (#95466) ## About The Pull Request Previously, when one drank out of a glass of booze that the virtual bartender graciously offered, one would not get any closer to domain completion, because said bartender got his glasses new out of a vending machine. These glasses never were registered on domain creation, due to not existing on domain creation. This pr registers the vending machines within the domain with a new signal that allows newly created glasses to also be registered, and count towards domain completion. ## Why It's Good For The Game A bitrunner dying of liver failure due to the obtuse distinction between glasses that were in a vending machine and glasses that were on the table sounds very annoying. Also, you could very easily make the domain not completable within seconds by spawning as a ghostrole and breaking the table glasses. You can still do that, but you have to break an entire vending machine's worth of glasses too. ## Changelog :cl: fix: Glasses that were in the vending machine now still count for the completion of the Beach Bar virtual domain /:cl: --- code/__DEFINES/dcs/signals/signals_object.dm | 3 +++ .../bitrunning/virtual_domain/domains/beach_bar.dm | 8 ++++++++ code/modules/vending/vendor/inventory.dm | 2 ++ 3 files changed, 13 insertions(+) diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 319e933cd076..dd75bf32ba15 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -666,5 +666,8 @@ #define COMSIG_VENDING_UI_INTERACT "vending_ui_interact" #define VENDING_DENIED (1<<0) +/// From /obj/machinery/vending/dispense(): (obj/item/vended_item) +#define COMSIG_VENDING_DISPENSED "vending_dispensed" + /// Sent from /datum/component/reflection when the reflection is updated to the mob reflecting: (atom/movable/reflecting_in, obj/effect/abstract/reflection) #define COMSIG_REFLECTION_UPDATED "reflection_updated" diff --git a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm index 5ae0793b6e92..12d6becc1367 100644 --- a/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm +++ b/code/modules/bitrunning/virtual_domain/domains/beach_bar.dm @@ -13,6 +13,14 @@ for(var/obj/item/reagent_containers/cup/glass/drink in created_atoms) RegisterSignal(drink, COMSIG_GLASS_DRANK, PROC_REF(on_drink_drank)) + for(var/obj/machinery/vending/vending_machine in created_atoms) + RegisterSignal(vending_machine, COMSIG_VENDING_DISPENSED, PROC_REF(on_vended_item)) + + +/datum/lazy_template/virtual_domain/beach_bar/proc/on_vended_item(obj/machinery/vending/vending_machine, obj/item/vended_item) + if(istype(vended_item, /obj/item/reagent_containers/cup/glass)) + RegisterSignal(vended_item, COMSIG_GLASS_DRANK, PROC_REF(on_drink_drank)) + /// Eventually reveal the cache /datum/lazy_template/virtual_domain/beach_bar/proc/on_drink_drank(datum/source) diff --git a/code/modules/vending/vendor/inventory.dm b/code/modules/vending/vendor/inventory.dm index 68b028d607b2..26a9337581a9 100644 --- a/code/modules/vending/vendor/inventory.dm +++ b/code/modules/vending/vendor/inventory.dm @@ -239,6 +239,8 @@ on_dispense(vended_item, dispense_returned) use_energy(active_power_usage) + SEND_SIGNAL(src, COMSIG_VENDING_DISPENSED, vended_item) + return vended_item /** From a154770b9f0bc74b0375bc6ef68bdbe6d95f70d2 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:58:44 +0000 Subject: [PATCH 143/155] Automatic changelog for PR #95466 [ci skip] --- html/changelogs/AutoChangeLog-pr-95466.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95466.yml diff --git a/html/changelogs/AutoChangeLog-pr-95466.yml b/html/changelogs/AutoChangeLog-pr-95466.yml new file mode 100644 index 000000000000..146d7c778bf4 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95466.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - bugfix: "Glasses that were in the vending machine now still count for the completion of the Beach Bar virtual domain" \ No newline at end of file From 713dadae87cf832f9e111f8c64e68ec391354401 Mon Sep 17 00:00:00 2001 From: Leland Kemble <70413276+lelandkemble@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:51:27 -0400 Subject: [PATCH 144/155] Fixes friendly wisps, wizard orbiting gravity anomalies & tesla periphery balls being disabled on shuttle transit (#95407) ## About The Pull Request When something that is orbiting something travels on a shuttle with that thing, its orbit is temporarily removed and then put back right after. To make this happen, the orbiting component calls `stop_orbit()` (on the orbiting object) & then `begin_orbit()` (on itself, the component). This means that effects that begin in `orbit()` & delete their effects in `stop_orbit()` simply lose their effect without putting them back, because `orbit()` is never called again. The solution presented is to pass the `refreshing` argument given in the component's `end_orbit()` into the atom's `stop_orbit()`, and condition actual deletion on that. The `refreshing` argument is `TRUE` only during shuttle movements and re-orbits of the same thing. ## Why It's Good For The Game fixes #95331 & wizard grav anoms & tesla periphery balls ## Changelog :cl: fix: Friendly wisps, wizard gravity balls, & tesla periphery balls are no longer disabled due to shuttle transit /:cl: --------- Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com> --- code/datums/components/orbiter.dm | 10 ++++++---- .../wizard/equipment/spellbook_entries/perks.dm | 4 +++- .../modules/mining/lavaland/mining_loot/consumables.dm | 4 ++-- code/modules/power/tesla/energy_ball.dm | 4 +++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm index 7bfead87625b..ffe9dfae4f80 100644 --- a/code/datums/components/orbiter.dm +++ b/code/datums/components/orbiter.dm @@ -126,7 +126,7 @@ UnregisterSignal(source, COMSIG_ATOM_AFTER_SHUTTLE_MOVE) begin_orbit(arglist(list(source) + orbiter_params[source])) -/datum/component/orbiter/proc/end_orbit(atom/movable/orbiter, refreshing=FALSE) +/datum/component/orbiter/proc/end_orbit(atom/movable/orbiter, refreshing = FALSE) if(!orbiter_list[orbiter]) return UnregisterSignal(orbiter, list(COMSIG_MOVABLE_MOVED, COMSIG_ATOM_BEFORE_SHUTTLE_MOVE, COMSIG_ATOM_AFTER_SHUTTLE_MOVE)) @@ -137,7 +137,7 @@ orbiter_list -= orbiter if(!refreshing) orbiter_params -= orbiter - orbiter.stop_orbit(src) + orbiter.stop_orbit(src, refreshing) orbiter.orbiting = null if(ismob(orbiter)) @@ -200,9 +200,11 @@ orbit_target = A return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) -/atom/movable/proc/stop_orbit(datum/component/orbiter/orbits) +/atom/movable/proc/stop_orbit(datum/component/orbiter/orbits, refreshing = FALSE) + if(refreshing) + return //Only null the target if we're actually stopping the orbit for real, not if we're merely shuttle moving(or orbiting the same thing again). We will never get it back unless the orbit is fully deleted and reinstated. orbit_target = null - return // We're just a simple hook + /atom/proc/transfer_observers_to(atom/target) if(!orbiters || !istype(target) || !get_turf(target) || target == src) diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm index 6c4947639f6c..087f9b6a16be 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/perks.dm @@ -219,7 +219,9 @@ if(!living_mov.mob_negates_gravity()) step_towards(living_mov, wizard) -/obj/effect/wizard_magnetism/stop_orbit() +/obj/effect/wizard_magnetism/stop_orbit(datum/component/orbiter/orbiter, refreshing = FALSE) + if(refreshing) + return ..() STOP_PROCESSING(SSprocessing, src) qdel(src) diff --git a/code/modules/mining/lavaland/mining_loot/consumables.dm b/code/modules/mining/lavaland/mining_loot/consumables.dm index ca9d22c0f55c..29714d7945e6 100644 --- a/code/modules/mining/lavaland/mining_loot/consumables.dm +++ b/code/modules/mining/lavaland/mining_loot/consumables.dm @@ -96,8 +96,8 @@ ADD_TRAIT(being, TRAIT_THERMAL_VISION, REF(src)) being.update_sight() -/obj/effect/wisp/stop_orbit(datum/component/orbiter/orbits) - if(!ismob(orbit_target)) +/obj/effect/wisp/stop_orbit(datum/component/orbiter/orbits, refreshing = FALSE) + if(!ismob(orbit_target) || refreshing) return ..() var/mob/being = orbit_target UnregisterSignal(being, COMSIG_MOB_UPDATE_SIGHT) diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm index c41be13d086e..7498d22d1d70 100644 --- a/code/modules/power/tesla/energy_ball.dm +++ b/code/modules/power/tesla/energy_ball.dm @@ -176,7 +176,9 @@ target.orbiting_balls += src . = ..() -/obj/energy_ball/stop_orbit() +/obj/energy_ball/stop_orbit(datum/component/orbiter/orbiters, refreshing = FALSE) + if(refreshing) + return ..() if (orbiting && istype(orbiting.parent, /obj/energy_ball)) var/obj/energy_ball/orbitingball = orbiting.parent orbitingball.orbiting_balls -= src From c4aaa6249dff7ab18a401f4f2d1a4f4c2ba2c6a1 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:51:49 +0000 Subject: [PATCH 145/155] Automatic changelog for PR #95407 [ci skip] --- html/changelogs/AutoChangeLog-pr-95407.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95407.yml diff --git a/html/changelogs/AutoChangeLog-pr-95407.yml b/html/changelogs/AutoChangeLog-pr-95407.yml new file mode 100644 index 000000000000..00d8338eb0fc --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95407.yml @@ -0,0 +1,4 @@ +author: "lelandkemble" +delete-after: True +changes: + - bugfix: "Friendly wisps, wizard gravity balls, & tesla periphery balls are no longer disabled due to shuttle transit" \ No newline at end of file From 1d4081e6c05783685c909a2f2cb506f0e95aaf06 Mon Sep 17 00:00:00 2001 From: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:11:28 -0400 Subject: [PATCH 146/155] Barcode scanner now lets you check books out (#95431) ## About The Pull Request The barcode scanner has a check-in mode, but to checkout you have to scroll through a (potentially) large list of books. This aims to fix that by letting you skip the UI using the same item you use to check-in. https://github.com/user-attachments/assets/916b743e-d974-4e7d-a9f2-a407562c5123 Also limits library book borrowing time to 2 hours, cause the UI looked really weird with it uncapped (also any round that goes more than 2 hours you should bring your books back) ## Why It's Good For The Game Better UX I think. --- code/__DEFINES/paper.dm | 3 -- code/modules/library/barcode_scanner.dm | 32 ++++++++++++++++++- code/modules/library/lib_machines.dm | 28 +++++++++------- .../LibraryConsole/screens/Checkout.tsx | 15 ++++++--- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/code/__DEFINES/paper.dm b/code/__DEFINES/paper.dm index 9cede4214bd9..4eea9e7c2f65 100644 --- a/code/__DEFINES/paper.dm +++ b/code/__DEFINES/paper.dm @@ -14,9 +14,6 @@ /// Should be able to stamp paper. #define MODE_STAMPING 2 -#define BARCODE_SCANNER_CHECKIN "check_in" -#define BARCODE_SCANNER_INVENTORY "inventory" - #define IS_WRITING_UTENSIL(thing) (thing?.get_writing_implement_details()?["interaction_mode"] == MODE_WRITING) /** diff --git a/code/modules/library/barcode_scanner.dm b/code/modules/library/barcode_scanner.dm index 7b650bc5f76d..07769b6874d5 100644 --- a/code/modules/library/barcode_scanner.dm +++ b/code/modules/library/barcode_scanner.dm @@ -1,3 +1,7 @@ +#define BARCODE_SCANNER_CHECKIN "check_in" +#define BARCODE_SCANNER_CHECKOUT "check_out" +#define BARCODE_SCANNER_INVENTORY "inventory" + /obj/item/barcodescanner name = "barcode scanner" icon = 'icons/obj/service/library.dmi' @@ -9,7 +13,7 @@ custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2) ///Weakref to the library computer we are connected to. var/datum/weakref/computer_ref - ///The current scanning mode (BARCODE_SCANNER_CHECKIN|BARCODE_SCANNER_INVENTORY) + ///The current scanning mode (BARCODE_SCANNER_CHECKIN|BARCODE_SCANNER_CHECKOUT|BARCODE_SCANNER_INVENTORY) var/scan_mode = BARCODE_SCANNER_CHECKIN /obj/item/barcodescanner/Initialize(mapload) @@ -24,6 +28,8 @@ switch(scan_mode) if(BARCODE_SCANNER_CHECKIN) context[SCREENTIP_CONTEXT_LMB] = "Check in" + if(BARCODE_SCANNER_CHECKOUT) + context[SCREENTIP_CONTEXT_LMB] = "Check out" if(BARCODE_SCANNER_INVENTORY) context[SCREENTIP_CONTEXT_LMB] = "Add to inventory" return CONTEXTUAL_SCREENTIP_SET @@ -61,6 +67,23 @@ user.balloon_alert(user, "isn't checked out!") return ITEM_INTERACT_BLOCKING + if(BARCODE_SCANNER_CHECKOUT) + var/list/checkouts = linked_computer.checkouts + for(var/checkout_ref in checkouts) + var/datum/borrowbook/maybe_ours = checkouts[checkout_ref] + if(target_book.book_data.compare(maybe_ours.book_data)) + user.balloon_alert(user, "already checked out!") + return ITEM_INTERACT_BLOCKING + for(var/copy_ref in linked_computer.inventory) + if(!target_book.book_data.compare(linked_computer.inventory[copy_ref])) + continue + linked_computer.checking_out_book = target_book.book_data + balloon_alert(user, "set for check out") + playsound(src, 'sound/items/barcodebeep.ogg', 20, FALSE) + return ITEM_INTERACT_SUCCESS + user.balloon_alert(user, "not in inventory!") + return ITEM_INTERACT_BLOCKING + if(BARCODE_SCANNER_INVENTORY) var/datum/book_info/our_copy = target_book.book_data.return_copy() linked_computer.inventory[ref(our_copy)] = our_copy @@ -80,9 +103,16 @@ return switch(scan_mode) if(BARCODE_SCANNER_CHECKIN) + scan_mode = BARCODE_SCANNER_CHECKOUT + balloon_alert(user, "check-out mode") + if(BARCODE_SCANNER_CHECKOUT) scan_mode = BARCODE_SCANNER_INVENTORY balloon_alert(user, "inventory adding mode") if(BARCODE_SCANNER_INVENTORY) scan_mode = BARCODE_SCANNER_CHECKIN balloon_alert(user, "check-in mode") playsound(loc, 'sound/items/click.ogg', 20, TRUE) + +#undef BARCODE_SCANNER_CHECKIN +#undef BARCODE_SCANNER_CHECKOUT +#undef BARCODE_SCANNER_INVENTORY diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm index 50e6d0ac22ae..40a90e27fb73 100644 --- a/code/modules/library/lib_machines.dm +++ b/code/modules/library/lib_machines.dm @@ -309,6 +309,8 @@ GLOBAL_VAR_INIT(library_table_modified, 0) var/dynamic_inv_load = FALSE ///Book scanner that will be used when uploading books to the Archive var/datum/weakref/scanner + ///Name of the book we're checking out, given by barcodes or the UI. + var/datum/book_info/checking_out_book ///Our cooldown on using the printer COOLDOWN_DECLARE(printer_cooldown) ///Our cooldown on publishing books to the newscaster's "book club" channel @@ -370,6 +372,7 @@ GLOBAL_VAR_INIT(library_table_modified, 0) data["has_checkout"] = !!checkout_len data["checkout_page"] = checkout_page + 1 data["checkout_page_count"] = checkout_page_count + 1 + data["checkout_title"] = checking_out_book?.get_title() || null //Copypasta from the visitor console if(LIBRARY_ARCHIVE) @@ -449,6 +452,19 @@ GLOBAL_VAR_INIT(library_table_modified, 0) update_static_data_for_all_viewers() return TRUE if("checkout") + if(isnull(checking_out_book)) + return TRUE + var/datum/borrowbook/loan = new /datum/borrowbook + var/loan_to = copytext(sanitize(params["loaned_to"]), 1, MAX_NAME_LEN) + var/checkoutperiod = max(params["checkout_time"], 1) + loan.book_data = checking_out_book.return_copy() + loan.loanedto = loan_to + loan.checkout = world.time + loan.duedate = world.time + (checkoutperiod MINUTES) + checkouts[ref(loan)] = loan + checkout_update() + return TRUE + if("set_checkout") var/list/available = list() for(var/id in inventory) var/datum/book_info/book_infos = inventory[id] @@ -459,17 +475,7 @@ GLOBAL_VAR_INIT(library_table_modified, 0) var/datum/book_info/book_info = available[book_name] if(!istype(book_info)) return - var/datum/borrowbook/loan = new /datum/borrowbook - - var/loan_to = copytext(sanitize(params["loaned_to"]), 1, MAX_NAME_LEN) - var/checkoutperiod = max(params["checkout_time"], 1) - - loan.book_data = book_info.return_copy() - loan.loanedto = loan_to - loan.checkout = world.time - loan.duedate = world.time + (checkoutperiod MINUTES) - checkouts[ref(loan)] = loan - checkout_update() + checking_out_book = book_info return TRUE if("checkin") var/id = params["checked_out_id"] diff --git a/tgui/packages/tgui/interfaces/LibraryConsole/screens/Checkout.tsx b/tgui/packages/tgui/interfaces/LibraryConsole/screens/Checkout.tsx index c0e3aa30c14b..526ade6f7595 100644 --- a/tgui/packages/tgui/interfaces/LibraryConsole/screens/Checkout.tsx +++ b/tgui/packages/tgui/interfaces/LibraryConsole/screens/Checkout.tsx @@ -65,6 +65,7 @@ export function Checkout(props) { function CheckoutModal(props) { const { act, data } = useBackend(); + const { checkout_title } = data; const inventory = data.inventory .map((book, i) => ({ @@ -77,7 +78,6 @@ function CheckoutModal(props) { const { checkoutBookState } = useLibraryContext(); const [checkoutBook, setCheckoutBook] = checkoutBookState; - const [bookName, setBookName] = useState('Insert Book name...'); const [checkoutee, setCheckoutee] = useState('Recipient'); const [checkoutPeriod, setCheckoutPeriod] = useState(5); @@ -91,9 +91,15 @@ function CheckoutModal(props) { book.title)} - onSelected={(e) => setBookName(e)} + onSelected={(e) => { + act('set_checkout', { + book_name: e, + }); + }} /> @@ -110,7 +116,7 @@ function CheckoutModal(props) { value={checkoutPeriod} unit=" Minutes" minValue={1} - maxValue={1440} + maxValue={120} step={1} stepPixelSize={10} onChange={(value) => setCheckoutPeriod(value)} @@ -128,7 +134,6 @@ function CheckoutModal(props) { onClick={() => { setCheckoutBook(false); act('checkout', { - book_name: bookName, loaned_to: checkoutee, checkout_time: checkoutPeriod, }); From f39f648b9f83f7bb30b445b08a50f39203313e2a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:11:49 +0000 Subject: [PATCH 147/155] Automatic changelog for PR #95431 [ci skip] --- html/changelogs/AutoChangeLog-pr-95431.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95431.yml diff --git a/html/changelogs/AutoChangeLog-pr-95431.yml b/html/changelogs/AutoChangeLog-pr-95431.yml new file mode 100644 index 000000000000..83cb963e314a --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95431.yml @@ -0,0 +1,5 @@ +author: "JohnFulpWillard" +delete-after: True +changes: + - qol: "The Curator's barcode scanner now lets you check books out." + - bugfix: "The library console can no longer borrow books for an infinite amount of time." \ No newline at end of file From e9f52785d3092e7d70c828f1dfb3d65dd330d80a Mon Sep 17 00:00:00 2001 From: Iajret <8430839+Iajret@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:52:26 +0300 Subject: [PATCH 148/155] Makes vitals display available in ancient lathes (#95428) ## About The Pull Request Not sure if it is oversight or because of some valid balancing concern, but this PR just enables display's design to be able to be printed in golems and charlie station lathes ## Why It's Good For The Game Existence of this item in ghostroles' hands wont impact balancing in any way, as opposed to other locked items such as shuttle parts crew pointers (which, gonna be honest, wont impact said balance either, but whatever). Vitals display is just too cool of a thing to make it limited just for station's people. --- code/modules/research/designs/medical_designs.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm index 7d33c3ed09fb..bcc98287e246 100644 --- a/code/modules/research/designs/medical_designs.dm +++ b/code/modules/research/designs/medical_designs.dm @@ -1400,7 +1400,7 @@ Links to stasis beds, operating tables, and other machines that can hold patients \ such as cryo cells, sleepers, and more." id = "vitals_monitor" - build_type = PROTOLATHE + build_type = PROTOLATHE | AWAY_LATHE materials = list( /datum/material/iron = SHEET_MATERIAL_AMOUNT * 4, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 2, From f3f9942992ded2f707c25de557ac30a6d82f581f Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:52:51 +0000 Subject: [PATCH 149/155] Automatic changelog for PR #95428 [ci skip] --- html/changelogs/AutoChangeLog-pr-95428.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95428.yml diff --git a/html/changelogs/AutoChangeLog-pr-95428.yml b/html/changelogs/AutoChangeLog-pr-95428.yml new file mode 100644 index 000000000000..c9577e6b1741 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95428.yml @@ -0,0 +1,4 @@ +author: "Iajret" +delete-after: True +changes: + - rscadd: "Vitals display can be printed in ancient lathes" \ No newline at end of file From 48e9dafa085d8941cb02004c1fd55f2a69eb77a9 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:01:02 +0000 Subject: [PATCH 150/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95407.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95428.yml | 4 ---- html/changelogs/AutoChangeLog-pr-95431.yml | 5 ----- html/changelogs/AutoChangeLog-pr-95466.yml | 4 ---- html/changelogs/archive/2026-03.yml | 10 ++++++++++ 5 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95407.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95428.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95431.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-95466.yml diff --git a/html/changelogs/AutoChangeLog-pr-95407.yml b/html/changelogs/AutoChangeLog-pr-95407.yml deleted file mode 100644 index 00d8338eb0fc..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95407.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - bugfix: "Friendly wisps, wizard gravity balls, & tesla periphery balls are no longer disabled due to shuttle transit" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95428.yml b/html/changelogs/AutoChangeLog-pr-95428.yml deleted file mode 100644 index c9577e6b1741..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95428.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Iajret" -delete-after: True -changes: - - rscadd: "Vitals display can be printed in ancient lathes" \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95431.yml b/html/changelogs/AutoChangeLog-pr-95431.yml deleted file mode 100644 index 83cb963e314a..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95431.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "JohnFulpWillard" -delete-after: True -changes: - - qol: "The Curator's barcode scanner now lets you check books out." - - bugfix: "The library console can no longer borrow books for an infinite amount of time." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-95466.yml b/html/changelogs/AutoChangeLog-pr-95466.yml deleted file mode 100644 index 146d7c778bf4..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95466.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "lelandkemble" -delete-after: True -changes: - - bugfix: "Glasses that were in the vending machine now still count for the completion of the Beach Bar virtual domain" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 0a2b2643b77b..5ed3d003f6fe 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -364,9 +364,14 @@ Iajret: - bugfix: fixed vitals display not respecting APCs power when you attach it to a wall + - rscadd: Vitals display can be printed in ancient lathes Jacquerel: - rscadd: Nonhuman mobs with hands can now pick up other mobs that are smaller than them and can usually be picked up + JohnFulpWillard: + - qol: The Curator's barcode scanner now lets you check books out. + - bugfix: The library console can no longer borrow books for an infinite amount + of time. Melbert: - bugfix: The skeleton from being shocked matches your height - bugfix: Drinking soup will loop until it's empty or you cancel it (as it used @@ -383,3 +388,8 @@ or outputs converted into a new boolean datatype. cebutris: - bugfix: Fixed an oversight with package wrapping + lelandkemble: + - bugfix: Friendly wisps, wizard gravity balls, & tesla periphery balls are no longer + disabled due to shuttle transit + - bugfix: Glasses that were in the vending machine now still count for the completion + of the Beach Bar virtual domain From 59178dd6494a896b0c6af8310831d62f613c3170 Mon Sep 17 00:00:00 2001 From: S u n r i s e <143133070+SunriseOverYourHead@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:12:37 -0300 Subject: [PATCH 151/155] carbon_stripping.dm uses defines instead of magic numbers (#95498) --- code/modules/mob/living/carbon/carbon_stripping.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/modules/mob/living/carbon/carbon_stripping.dm b/code/modules/mob/living/carbon/carbon_stripping.dm index 66f2b8b6b998..34733512126c 100644 --- a/code/modules/mob/living/carbon/carbon_stripping.dm +++ b/code/modules/mob/living/carbon/carbon_stripping.dm @@ -132,8 +132,8 @@ /datum/strippable_item/hand/left key = STRIPPABLE_ITEM_LHAND - hand_index = 1 + hand_index = LEFT_HANDS /datum/strippable_item/hand/right key = STRIPPABLE_ITEM_RHAND - hand_index = 2 + hand_index = RIGHT_HANDS From ce4b40369968b835fb598437a6802e4ac5b025f7 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:13:06 +0000 Subject: [PATCH 152/155] Automatic changelog for PR #95498 [ci skip] --- html/changelogs/AutoChangeLog-pr-95498.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-95498.yml diff --git a/html/changelogs/AutoChangeLog-pr-95498.yml b/html/changelogs/AutoChangeLog-pr-95498.yml new file mode 100644 index 000000000000..84a3e27a4dac --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-95498.yml @@ -0,0 +1,4 @@ +author: "Bugwasabi, vinylspiders" +delete-after: True +changes: + - code_imp: "Uses defines instead of magic numbers on carbon_stripping.dm" \ No newline at end of file From 71f42618ab1bcba9595ff25953633f30d4fd7448 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:15:26 +0000 Subject: [PATCH 153/155] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-95498.yml | 4 ---- html/changelogs/archive/2026-03.yml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-95498.yml diff --git a/html/changelogs/AutoChangeLog-pr-95498.yml b/html/changelogs/AutoChangeLog-pr-95498.yml deleted file mode 100644 index 84a3e27a4dac..000000000000 --- a/html/changelogs/AutoChangeLog-pr-95498.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "Bugwasabi, vinylspiders" -delete-after: True -changes: - - code_imp: "Uses defines instead of magic numbers on carbon_stripping.dm" \ No newline at end of file diff --git a/html/changelogs/archive/2026-03.yml b/html/changelogs/archive/2026-03.yml index 5ed3d003f6fe..e5bb50c206c8 100644 --- a/html/changelogs/archive/2026-03.yml +++ b/html/changelogs/archive/2026-03.yml @@ -361,6 +361,8 @@ 2026-03-24: ArcaneMusic: - bugfix: Cargo goodies are now visible in the ordering and request consoles. + Bugwasabi, vinylspiders: + - code_imp: Uses defines instead of magic numbers on carbon_stripping.dm Iajret: - bugfix: fixed vitals display not respecting APCs power when you attach it to a wall From ef0d8592c529dc60e2f1faf7f594538fa62f92dd Mon Sep 17 00:00:00 2001 From: XeonMations <62395746+XeonMations@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:42:49 +0200 Subject: [PATCH 154/155] Fixes ss_flags for #803 --- modular_darkpack/modules/ambience/code/music_subsystem.dm | 2 +- modular_darkpack/modules/cars/code/car_subsystem.dm | 2 +- .../modules/masquerade/code/subsystem/masquerade.dm | 2 +- modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm | 2 +- modular_darkpack/modules/paths/code/occult_research.dm | 2 +- modular_darkpack/modules/phones/code/phone_subsystem.dm | 2 +- .../modules/storyteller_dice/code/roll_subsystem.dm | 2 +- .../modules/vip_areas/code/bouncer_barrier_subsystem.dm | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modular_darkpack/modules/ambience/code/music_subsystem.dm b/modular_darkpack/modules/ambience/code/music_subsystem.dm index b3f446a72f81..bf67e93469b9 100644 --- a/modular_darkpack/modules/ambience/code/music_subsystem.dm +++ b/modular_darkpack/modules/ambience/code/music_subsystem.dm @@ -21,7 +21,7 @@ /// The subsystem used to play music to users every now and then, makes them real excited. copy-pasta from SSambience SUBSYSTEM_DEF(music) name = "Music" - flags = SS_BACKGROUND|SS_NO_INIT + ss_flags = SS_BACKGROUND|SS_NO_INIT priority = FIRE_PRIORITY_AMBIENCE runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 1 SECONDS diff --git a/modular_darkpack/modules/cars/code/car_subsystem.dm b/modular_darkpack/modules/cars/code/car_subsystem.dm index 4305343987c5..00e94e9ec1b9 100644 --- a/modular_darkpack/modules/cars/code/car_subsystem.dm +++ b/modular_darkpack/modules/cars/code/car_subsystem.dm @@ -1,6 +1,6 @@ PROCESSING_SUBSYSTEM_DEF(carpool) name = "Car Pool" - flags = SS_POST_FIRE_TIMING|SS_NO_INIT + ss_flags = SS_POST_FIRE_TIMING|SS_NO_INIT priority = FIRE_PRIORITY_OBJ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 5 diff --git a/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm b/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm index 7c2b89c34c03..d82bd0d70707 100644 --- a/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm +++ b/modular_darkpack/modules/masquerade/code/subsystem/masquerade.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(masquerade) name = "Masquerade" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/masquerade_level = MASQUERADE_MAX_LEVEL var/list/masquerade_breachers diff --git a/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm b/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm index 10887de97b85..e1c48eeaf754 100644 --- a/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm +++ b/modular_darkpack/modules/npc/code/human/npc_human_subsystem.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(humannpcpool) name = "Human NPC Pool" - flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + ss_flags = SS_POST_FIRE_TIMING|SS_BACKGROUND priority = FIRE_PRIORITY_NPC runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME wait = 0.3 SECONDS diff --git a/modular_darkpack/modules/paths/code/occult_research.dm b/modular_darkpack/modules/paths/code/occult_research.dm index 4f77d2bd85f1..71ee8c648ec4 100644 --- a/modular_darkpack/modules/paths/code/occult_research.dm +++ b/modular_darkpack/modules/paths/code/occult_research.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(occult_research) name = "Occult Research" - flags = SS_BACKGROUND|SS_NO_INIT + ss_flags = SS_BACKGROUND|SS_NO_INIT wait = 60 SECONDS // How often to process research points var/base_research_rate = 0.5 // Base points per tick var/necromancy_bonus = 0.5 diff --git a/modular_darkpack/modules/phones/code/phone_subsystem.dm b/modular_darkpack/modules/phones/code/phone_subsystem.dm index 0722a45b9962..9965aee04640 100644 --- a/modular_darkpack/modules/phones/code/phone_subsystem.dm +++ b/modular_darkpack/modules/phones/code/phone_subsystem.dm @@ -4,7 +4,7 @@ */ SUBSYSTEM_DEF(phones) name = "Phones" - flags = SS_NO_FIRE|SS_NO_INIT + ss_flags = SS_NO_FIRE|SS_NO_INIT // Seven digits, always start with 5 var/list/assigned_phone_numbers = list() diff --git a/modular_darkpack/modules/storyteller_dice/code/roll_subsystem.dm b/modular_darkpack/modules/storyteller_dice/code/roll_subsystem.dm index eb3ee9b451df..2c3f52003d5b 100644 --- a/modular_darkpack/modules/storyteller_dice/code/roll_subsystem.dm +++ b/modular_darkpack/modules/storyteller_dice/code/roll_subsystem.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(roll) name = "Dice Rolling" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE var/on_crit_extra_die_enabled = FALSE var/on_crit_extra_success_enabled = FALSE diff --git a/modular_darkpack/modules/vip_areas/code/bouncer_barrier_subsystem.dm b/modular_darkpack/modules/vip_areas/code/bouncer_barrier_subsystem.dm index fee76f955de9..51a05cdc07ad 100644 --- a/modular_darkpack/modules/vip_areas/code/bouncer_barrier_subsystem.dm +++ b/modular_darkpack/modules/vip_areas/code/bouncer_barrier_subsystem.dm @@ -1,6 +1,6 @@ SUBSYSTEM_DEF(bouncer_barriers) name = "Bouncer Barrier" - flags = SS_NO_FIRE + ss_flags = SS_NO_FIRE init_order = INIT_ORDER_BARRIER var/barriers_enabled = TRUE From 04ea0b50fb9d4f9b9529eeccd5de99de126ee372 Mon Sep 17 00:00:00 2001 From: XeonMations <62395746+XeonMations@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:27:04 +0200 Subject: [PATCH 155/155] fix TGUI linters on #803 --- .../features/character_preferences/darkpack_height.tsx | 2 +- tgui/packages/tgui/interfaces/Telephone/ScreenIRC.tsx | 6 ++---- tgui/packages/tgui/interfaces/Telephone/ScreenPhone.tsx | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_height.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_height.tsx index 170b76fa83a9..def14e845419 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_height.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/darkpack_height.tsx @@ -1,5 +1,5 @@ // THIS IS A DARKPACK UI FILE -import { FeatureNumeric, FeatureValueProps, FeatureNumericData, FeatureNumberInput } from '../base'; +import { type FeatureNumeric, type FeatureValueProps, type FeatureNumericData, FeatureNumberInput } from '../base'; import { Stack, Box } from 'tgui-core/components'; type HeightServerData = FeatureNumericData & { diff --git a/tgui/packages/tgui/interfaces/Telephone/ScreenIRC.tsx b/tgui/packages/tgui/interfaces/Telephone/ScreenIRC.tsx index 64d5aa69d701..4b522a462200 100644 --- a/tgui/packages/tgui/interfaces/Telephone/ScreenIRC.tsx +++ b/tgui/packages/tgui/interfaces/Telephone/ScreenIRC.tsx @@ -40,8 +40,7 @@ export const ScreenViewingChannel = (props: { {viewing_channel ? ( - <> - {viewing_channel.messages.map((message) => ( + viewing_channel.messages.map((message) => ( Story by {message.author} - [{message.time_stamp}] @@ -53,8 +52,7 @@ export const ScreenViewingChannel = (props: { ) : null} - ))} - + )) ) : ( 'ERROR: Channel invalid.' )} diff --git a/tgui/packages/tgui/interfaces/Telephone/ScreenPhone.tsx b/tgui/packages/tgui/interfaces/Telephone/ScreenPhone.tsx index 4be2a857e512..751784da2316 100644 --- a/tgui/packages/tgui/interfaces/Telephone/ScreenPhone.tsx +++ b/tgui/packages/tgui/interfaces/Telephone/ScreenPhone.tsx @@ -27,7 +27,7 @@ export const ScreenPhone = (props: { } act('terminal_sound'); if (digit === '_') { - setEnteredNumber(enteredNumber + ' '); + setEnteredNumber(`${enteredNumber} `); } else { setEnteredNumber(enteredNumber + digit); }