From cc87284b455429f76ea2ccd7c8ca293d70963164 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 06:00:25 +0000 Subject: [PATCH 001/357] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-97290.yml | 4 ---- html/changelogs/archive/2026-08.yml | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-97290.yml diff --git a/html/changelogs/AutoChangeLog-pr-97290.yml b/html/changelogs/AutoChangeLog-pr-97290.yml deleted file mode 100644 index 2a2ff3eb6bc4..000000000000 --- a/html/changelogs/AutoChangeLog-pr-97290.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "JohnFulpWillard" -delete-after: True -changes: - - spellcheck: "Cargo bounty list printout now has a slightly better UI." \ No newline at end of file diff --git a/html/changelogs/archive/2026-08.yml b/html/changelogs/archive/2026-08.yml index 57fc82af14b0..ee208ceb5bbd 100644 --- a/html/changelogs/archive/2026-08.yml +++ b/html/changelogs/archive/2026-08.yml @@ -29,3 +29,5 @@ Ghommie: - bugfix: Felinids (and nightmares) can once again use more complex objects, unlike monkeys. + JohnFulpWillard: + - spellcheck: Cargo bounty list printout now has a slightly better UI. From e065f4739aca743274d8149b47b9f19d901939dd Mon Sep 17 00:00:00 2001 From: necromanceranne <40847847+necromanceranne@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:35:04 +1000 Subject: [PATCH 002/357] Goliaths and Tendrils can be punched using your normal punches (#97259) ## About The Pull Request Goliaths and tendrils would take zero damage from standard, unmodified punches. This is no longer the case. This does not affect martial arts or strongarms, which were both working against goliaths just fine. Removes the IMMUNE_TO_FISTS flag as it is now unused entirely (and there are better ways to implement it) ## Why It's Good For The Game It literally doesn't make sense that you can punch bubblegum to death, but you can't punch even one normal goliath to death. Tendrils are seemingly goliath-related, so I assume that is why they have the flag as well. --- code/__DEFINES/basic_mobs.dm | 8 +++----- code/modules/mapping/mapping_helpers.dm | 5 ----- code/modules/mob/living/basic/basic_defense.dm | 2 +- code/modules/mob/living/basic/lavaland/goliath/goliath.dm | 1 - code/modules/mob/living/basic/lavaland/tendril/tendril.dm | 2 +- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/code/__DEFINES/basic_mobs.dm b/code/__DEFINES/basic_mobs.dm index d97418718a16..aa5ab927d093 100644 --- a/code/__DEFINES/basic_mobs.dm +++ b/code/__DEFINES/basic_mobs.dm @@ -14,14 +14,12 @@ #define REMAIN_DENSE_WHILE_DEAD (1<<2) /// Mob can be set on fire #define FLAMMABLE_MOB (1<<3) -/// Mob never takes damage from unarmed attacks -#define IMMUNE_TO_FISTS (1<<4) /// Mob is immune to getting wet -#define IMMUNE_TO_GETTING_WET (1<<5) +#define IMMUNE_TO_GETTING_WET (1<<4) /// Disables the function of attacking random body zones -#define PRECISE_ATTACK_ZONES (1<<6) +#define PRECISE_ATTACK_ZONES (1<<5) /// People would be sad to see this mob die -#define SENDS_DEATH_MOODLETS (1<<7) +#define SENDS_DEATH_MOODLETS (1<<6) /// Temporary trait applied when an attack forecast animation has completed #define TRAIT_BASIC_ATTACK_FORECAST "trait_basic_attack_forecast" diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm index 8a39d3f04743..73797075bcfb 100644 --- a/code/modules/mapping/mapping_helpers.dm +++ b/code/modules/mapping/mapping_helpers.dm @@ -1499,11 +1499,6 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_atoms_ontop) icon_state = "basic_mob_flammable" flag_to_give = FLAMMABLE_MOB -/obj/effect/mapping_helpers/basic_mob_flags/immune_to_fists - name = "Basic mob immune to fists flag helper" - icon_state = "basic_mob_immune_to_fists" - flag_to_give = IMMUNE_TO_FISTS - /obj/effect/mapping_helpers/basic_mob_flags/immune_to_getting_wet name = "Basic mob immune to getting wet flag helper" icon_state = "basic_mob_immune_to_getting_wet" diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm index b297e89479e5..bc2bafa57cca 100644 --- a/code/modules/mob/living/basic/basic_defense.dm +++ b/code/modules/mob/living/basic/basic_defense.dm @@ -22,7 +22,7 @@ to_chat(user, span_warning("You don't want to hurt [src]!")) return TRUE var/obj/item/bodypart/arm/active_arm = user.get_active_hand() - var/damage = (basic_mob_flags & IMMUNE_TO_FISTS) ? 0 : rand(active_arm.unarmed_damage_low, active_arm.unarmed_damage_high) + var/damage = rand(active_arm.unarmed_damage_low, active_arm.unarmed_damage_high) if(check_block(user, damage, "[user]'s punch", UNARMED_ATTACK, 0, BRUTE)) return user.do_attack_animation(src, ATTACK_EFFECT_PUNCH) diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm index 0ea89e1b484d..56ea5ee94e59 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm @@ -10,7 +10,6 @@ base_pixel_x = -12 gender = MALE // Female ones are the bipedal elites speed = 12 - basic_mob_flags = IMMUNE_TO_FISTS maxHealth = 300 health = 300 friendly_verb_continuous = "wails at" diff --git a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm index 6e101413b165..9311539dae08 100644 --- a/code/modules/mob/living/basic/lavaland/tendril/tendril.dm +++ b/code/modules/mob/living/basic/lavaland/tendril/tendril.dm @@ -14,7 +14,7 @@ GLOBAL_LIST_INIT(tendrils, list()) base_pixel_w = -8 status_flags = NONE mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING | MOB_SPECIAL - basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS + basic_mob_flags = DEL_ON_DEATH mob_size = MOB_SIZE_HUGE maxHealth = 800 health = 800 From 7e8c12be25346da84f52a384432889deebb7968a Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:35:28 +0000 Subject: [PATCH 003/357] Automatic changelog for PR #97259 [ci skip] --- html/changelogs/AutoChangeLog-pr-97259.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97259.yml diff --git a/html/changelogs/AutoChangeLog-pr-97259.yml b/html/changelogs/AutoChangeLog-pr-97259.yml new file mode 100644 index 000000000000..51950a269989 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97259.yml @@ -0,0 +1,4 @@ +author: "necromanceranne" +delete-after: True +changes: + - qol: "You can punch a goliath to death. This isn't recommended. But if you are committed enough, you are welcome to do so." \ No newline at end of file From 1d9053ec229315241b352119774db2756c0bf906 Mon Sep 17 00:00:00 2001 From: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:28:03 +0300 Subject: [PATCH 004/357] Adds winter coats into loadout. Yes, department too (#97205) ## About The Pull Request image theyre grey by default but get autoassigned according to your job ## Why It's Good For The Game meow-meow-meow-aghh
instead of doing everyround preparation loop/drip check you now can select winter coat in loadout character preferences. i think its pretty neat. --- code/datums/components/toggle_attached_clothing.dm | 6 ++++++ code/modules/clothing/suits/toggles.dm | 3 +++ code/modules/clothing/suits/wintercoats.dm | 1 + code/modules/jobs/job_types/_job.dm | 1 + .../modules/jobs/job_types/atmospheric_technician.dm | 2 ++ code/modules/jobs/job_types/botanist.dm | 2 ++ code/modules/jobs/job_types/captain.dm | 2 ++ code/modules/jobs/job_types/cargo_technician.dm | 2 ++ code/modules/jobs/job_types/chemist.dm | 2 ++ code/modules/jobs/job_types/chief_engineer.dm | 2 ++ code/modules/jobs/job_types/chief_medical_officer.dm | 2 ++ code/modules/jobs/job_types/coroner.dm | 2 ++ code/modules/jobs/job_types/detective.dm | 2 ++ code/modules/jobs/job_types/geneticist.dm | 2 ++ code/modules/jobs/job_types/head_of_personnel.dm | 2 ++ code/modules/jobs/job_types/head_of_security.dm | 2 ++ code/modules/jobs/job_types/janitor.dm | 2 ++ code/modules/jobs/job_types/medical_doctor.dm | 2 ++ code/modules/jobs/job_types/paramedic.dm | 2 ++ code/modules/jobs/job_types/quartermaster.dm | 2 ++ code/modules/jobs/job_types/research_director.dm | 2 ++ code/modules/jobs/job_types/roboticist.dm | 2 ++ code/modules/jobs/job_types/scientist.dm | 2 ++ code/modules/jobs/job_types/security_officer.dm | 2 ++ code/modules/jobs/job_types/shaft_miner.dm | 2 ++ code/modules/jobs/job_types/station_engineer.dm | 2 ++ code/modules/jobs/job_types/warden.dm | 2 ++ code/modules/loadout/categories/suits.dm | 12 ++++++++++-- 28 files changed, 67 insertions(+), 2 deletions(-) diff --git a/code/datums/components/toggle_attached_clothing.dm b/code/datums/components/toggle_attached_clothing.dm index ffec2e6026d4..0518c5656013 100644 --- a/code/datums/components/toggle_attached_clothing.dm +++ b/code/datums/components/toggle_attached_clothing.dm @@ -31,6 +31,8 @@ var/datum/callback/on_deployed /// Optional callback triggered before we hide our equipment, before as we may delete it afterwards var/datum/callback/on_removed + /// If TRUE, automatically deploys the hood + var/auto_deploy_on_outfit_equip = TRUE /datum/component/toggle_attached_clothing/Initialize( deployable_type, @@ -39,6 +41,7 @@ destroy_on_removal = FALSE, parent_icon_state_suffix = "", down_overlay_state_suffix = "", + auto_deploy_on_outfit_equip = TRUE, datum/callback/pre_creation_check, datum/callback/on_created, datum/callback/on_deployed, @@ -54,6 +57,7 @@ src.destroy_on_removal = destroy_on_removal src.parent_icon_state_suffix = parent_icon_state_suffix src.down_overlay_state_suffix = down_overlay_state_suffix + src.auto_deploy_on_outfit_equip = auto_deploy_on_outfit_equip src.pre_creation_check = pre_creation_check src.on_created = on_created src.on_deployed = on_deployed @@ -152,6 +156,8 @@ /// Display deployed if worn in an outfit /datum/component/toggle_attached_clothing/proc/on_parent_equipped_outfit(obj/item/clothing/source, mob/equipper, visuals_only, slot) SIGNAL_HANDLER + if (!auto_deploy_on_outfit_equip) + return create_deployable() toggle_deployable() diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm index 12ba36c89eb7..7613cb6eda37 100644 --- a/code/modules/clothing/suits/toggles.dm +++ b/code/modules/clothing/suits/toggles.dm @@ -11,6 +11,8 @@ /// Reference to hood object, if it exists var/obj/item/clothing/head/hooded/hood + var/auto_deploy_hood_on_outfit_equip = TRUE + /obj/item/clothing/suit/hooded/Initialize(mapload) . = ..() if (!hoodtype) @@ -27,6 +29,7 @@ on_created = CALLBACK(src, PROC_REF(on_hood_created)),\ on_deployed = CALLBACK(src, PROC_REF(on_hood_up)),\ on_removed = CALLBACK(src, PROC_REF(on_hood_down)),\ + auto_deploy_on_outfit_equip = auto_deploy_hood_on_outfit_equip,\ ) /obj/item/clothing/suit/hooded/Destroy() diff --git a/code/modules/clothing/suits/wintercoats.dm b/code/modules/clothing/suits/wintercoats.dm index 5b4f92e0c473..fb00b5d8db8d 100644 --- a/code/modules/clothing/suits/wintercoats.dm +++ b/code/modules/clothing/suits/wintercoats.dm @@ -16,6 +16,7 @@ var/zipped = FALSE /// Whether alt-clicking this coat zips/unzips it var/can_altclick_zip = TRUE + auto_deploy_hood_on_outfit_equip = FALSE /datum/armor/hooded_wintercoat bio = 10 diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index a2234c8b3958..57a87c22f186 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -347,6 +347,7 @@ var/satchel = /obj/item/storage/backpack/satchel var/duffelbag = /obj/item/storage/backpack/duffelbag var/messenger = /obj/item/storage/backpack/messenger + var/wintercoat = /obj/item/clothing/suit/hooded/wintercoat var/pda_slot = ITEM_SLOT_BELT diff --git a/code/modules/jobs/job_types/atmospheric_technician.dm b/code/modules/jobs/job_types/atmospheric_technician.dm index 0a2472d7b065..a9670e530535 100644 --- a/code/modules/jobs/job_types/atmospheric_technician.dm +++ b/code/modules/jobs/job_types/atmospheric_technician.dm @@ -57,6 +57,8 @@ box = /obj/item/storage/box/survival/engineer pda_slot = ITEM_SLOT_LPOCKET + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/engineering/atmos + /datum/outfit/job/atmos/mod name = "Atmospheric Technician (MODsuit)" diff --git a/code/modules/jobs/job_types/botanist.dm b/code/modules/jobs/job_types/botanist.dm index cff826ec5054..679f07d06fa0 100644 --- a/code/modules/jobs/job_types/botanist.dm +++ b/code/modules/jobs/job_types/botanist.dm @@ -54,3 +54,5 @@ satchel = /obj/item/storage/backpack/satchel/hyd duffelbag = /obj/item/storage/backpack/duffelbag/hydroponics messenger = /obj/item/storage/backpack/messenger/hyd + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/hydro diff --git a/code/modules/jobs/job_types/captain.dm b/code/modules/jobs/job_types/captain.dm index a3ad2d52aa85..f2d737ad31eb 100644 --- a/code/modules/jobs/job_types/captain.dm +++ b/code/modules/jobs/job_types/captain.dm @@ -91,6 +91,8 @@ implants = list(/obj/item/implant/mindshield) skillchips = list(/obj/item/skillchip/disk_verifier) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/captain + var/special_charter /datum/outfit/job/captain/pre_equip(mob/living/carbon/human/H, visuals_only) diff --git a/code/modules/jobs/job_types/cargo_technician.dm b/code/modules/jobs/job_types/cargo_technician.dm index 2cef58a09e84..7856714d157a 100644 --- a/code/modules/jobs/job_types/cargo_technician.dm +++ b/code/modules/jobs/job_types/cargo_technician.dm @@ -49,6 +49,8 @@ ears = /obj/item/radio/headset/headset_cargo l_hand = /obj/item/universal_scanner + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/cargo + /datum/outfit/job/cargo_tech/mod name = "Cargo Technician (MODsuit)" diff --git a/code/modules/jobs/job_types/chemist.dm b/code/modules/jobs/job_types/chemist.dm index f8d65aabb8fa..05955e1a3454 100644 --- a/code/modules/jobs/job_types/chemist.dm +++ b/code/modules/jobs/job_types/chemist.dm @@ -58,3 +58,5 @@ box = /obj/item/storage/box/survival/medical chameleon_extras = /obj/item/gun/syringe + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/medical/chemistry diff --git a/code/modules/jobs/job_types/chief_engineer.dm b/code/modules/jobs/job_types/chief_engineer.dm index 53a28072503e..369d89f5c115 100644 --- a/code/modules/jobs/job_types/chief_engineer.dm +++ b/code/modules/jobs/job_types/chief_engineer.dm @@ -86,6 +86,8 @@ skillchips = list(/obj/item/skillchip/job/engineer) pda_slot = ITEM_SLOT_LPOCKET + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/engineering/ce + /datum/outfit/job/ce/mod name = "Chief Engineer (MODsuit)" diff --git a/code/modules/jobs/job_types/chief_medical_officer.dm b/code/modules/jobs/job_types/chief_medical_officer.dm index 7ffc07fbabb4..7b03fe19121e 100644 --- a/code/modules/jobs/job_types/chief_medical_officer.dm +++ b/code/modules/jobs/job_types/chief_medical_officer.dm @@ -83,6 +83,8 @@ ) skillchips = list(/obj/item/skillchip/entrails_reader) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/medical/cmo + /datum/outfit/job/cmo/mod name = "Chief Medical Officer (MODsuit)" diff --git a/code/modules/jobs/job_types/coroner.dm b/code/modules/jobs/job_types/coroner.dm index 150d029d35cd..c2e15efbd9af 100644 --- a/code/modules/jobs/job_types/coroner.dm +++ b/code/modules/jobs/job_types/coroner.dm @@ -73,3 +73,5 @@ messenger = /obj/item/storage/backpack/messenger/coroner skillchips = list(/obj/item/skillchip/entrails_reader) + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/medical/coroner diff --git a/code/modules/jobs/job_types/detective.dm b/code/modules/jobs/job_types/detective.dm index 68fd432d6707..7c6a5bf8ff79 100644 --- a/code/modules/jobs/job_types/detective.dm +++ b/code/modules/jobs/job_types/detective.dm @@ -78,6 +78,8 @@ skillchips = list(/obj/item/skillchip/job/detectives_taste) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/security + /datum/outfit/job/detective/pre_equip(mob/living/carbon/human/human, visuals_only = FALSE) . = ..() if (human.age < AGE_MINOR) diff --git a/code/modules/jobs/job_types/geneticist.dm b/code/modules/jobs/job_types/geneticist.dm index 68a178bbf55c..95645ebed9ee 100644 --- a/code/modules/jobs/job_types/geneticist.dm +++ b/code/modules/jobs/job_types/geneticist.dm @@ -48,3 +48,5 @@ satchel = /obj/item/storage/backpack/satchel/gen duffelbag = /obj/item/storage/backpack/duffelbag/genetics messenger = /obj/item/storage/backpack/messenger/gen + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/science/genetics diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm index 1d48437ed42f..b4bf3706b9fd 100644 --- a/code/modules/jobs/job_types/head_of_personnel.dm +++ b/code/modules/jobs/job_types/head_of_personnel.dm @@ -110,6 +110,8 @@ /obj/item/stamp/head/hop, ) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/hop + /datum/outfit/job/hop/pre_equip(mob/living/carbon/human/H) ..() if(check_holidays(IAN_HOLIDAY)) diff --git a/code/modules/jobs/job_types/head_of_security.dm b/code/modules/jobs/job_types/head_of_security.dm index 899224b33f34..b4c4a64a16bd 100644 --- a/code/modules/jobs/job_types/head_of_security.dm +++ b/code/modules/jobs/job_types/head_of_security.dm @@ -93,6 +93,8 @@ ) implants = list(/obj/item/implant/mindshield) + wintercoat = /obj/item/clothing/suit/armor/hos/trenchcoat/winter + /datum/outfit/job/hos/mod name = "Head of Security (MODsuit)" diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm index 52735aad579d..d9a52c9d6d9b 100644 --- a/code/modules/jobs/job_types/janitor.dm +++ b/code/modules/jobs/job_types/janitor.dm @@ -42,6 +42,8 @@ skillchips = list(/obj/item/skillchip/job/janitor, /obj/item/skillchip/disposals) backpack_contents = list(/obj/item/access_key) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/janitor + /datum/outfit/job/janitor/pre_equip(mob/living/carbon/human/human_equipper, visuals_only) . = ..() if(check_holidays(GARBAGEDAY)) diff --git a/code/modules/jobs/job_types/medical_doctor.dm b/code/modules/jobs/job_types/medical_doctor.dm index c5da45553292..0c300d2f6315 100644 --- a/code/modules/jobs/job_types/medical_doctor.dm +++ b/code/modules/jobs/job_types/medical_doctor.dm @@ -60,3 +60,5 @@ box = /obj/item/storage/box/survival/medical chameleon_extras = /obj/item/gun/syringe skillchips = list(/obj/item/skillchip/entrails_reader) + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/medical diff --git a/code/modules/jobs/job_types/paramedic.dm b/code/modules/jobs/job_types/paramedic.dm index ad9244333b87..fca347c0cad1 100644 --- a/code/modules/jobs/job_types/paramedic.dm +++ b/code/modules/jobs/job_types/paramedic.dm @@ -68,6 +68,8 @@ chameleon_extras = /obj/item/gun/syringe pda_slot = ITEM_SLOT_LPOCKET + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/medical/paramedic + /datum/outfit/job/doctor/mod name = "Paramedic (MODsuit)" diff --git a/code/modules/jobs/job_types/quartermaster.dm b/code/modules/jobs/job_types/quartermaster.dm index 962f558c602c..b353664100ca 100644 --- a/code/modules/jobs/job_types/quartermaster.dm +++ b/code/modules/jobs/job_types/quartermaster.dm @@ -54,3 +54,5 @@ l_hand = /obj/item/clipboard chameleon_extras = /obj/item/stamp/head/qm + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/cargo/qm diff --git a/code/modules/jobs/job_types/research_director.dm b/code/modules/jobs/job_types/research_director.dm index 7c1e04406687..3da1d9425835 100644 --- a/code/modules/jobs/job_types/research_director.dm +++ b/code/modules/jobs/job_types/research_director.dm @@ -79,6 +79,8 @@ chameleon_extras = /obj/item/stamp/head/rd skillchips = list(/obj/item/skillchip/research_director, /obj/item/skillchip/job/roboticist) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/science/rd + /datum/outfit/job/rd/mod name = "Research Director (MODsuit)" diff --git a/code/modules/jobs/job_types/roboticist.dm b/code/modules/jobs/job_types/roboticist.dm index 9f91c6f22e0a..4306ec080702 100644 --- a/code/modules/jobs/job_types/roboticist.dm +++ b/code/modules/jobs/job_types/roboticist.dm @@ -56,6 +56,8 @@ pda_slot = ITEM_SLOT_LPOCKET skillchips = list(/obj/item/skillchip/job/roboticist) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/science/robotics + /datum/outfit/job/roboticist/mod name = "Roboticist (MODsuit)" suit_store = /obj/item/tank/internals/oxygen diff --git a/code/modules/jobs/job_types/scientist.dm b/code/modules/jobs/job_types/scientist.dm index 923c9bff54b5..d32340c5d258 100644 --- a/code/modules/jobs/job_types/scientist.dm +++ b/code/modules/jobs/job_types/scientist.dm @@ -52,6 +52,8 @@ duffelbag = /obj/item/storage/backpack/duffelbag/science messenger = /obj/item/storage/backpack/messenger/science + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/science + /datum/outfit/job/scientist/pre_equip(mob/living/carbon/human/H) ..() try_giving_horrible_tie() diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index d78fb4f94b07..10b2ca8a9f6a 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -259,6 +259,8 @@ GLOBAL_LIST_EMPTY(security_officer_distribution) //The helmet is necessary because /obj/item/clothing/head/helmet/sec is overwritten in the chameleon list by the standard helmet, which has the same name and icon state implants = list(/obj/item/implant/mindshield) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/security + /datum/outfit/job/security/mod name = "Security Officer (MODsuit)" diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm index d2c7502c5105..8b6d36349e9a 100644 --- a/code/modules/jobs/job_types/shaft_miner.dm +++ b/code/modules/jobs/job_types/shaft_miner.dm @@ -57,6 +57,8 @@ box = /obj/item/storage/box/survival/mining chameleon_extras = /obj/item/gun/energy/recharge/kinetic_accelerator + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/miner + /datum/outfit/job/miner/equipped name = "Shaft Miner (Equipment)" diff --git a/code/modules/jobs/job_types/station_engineer.dm b/code/modules/jobs/job_types/station_engineer.dm index aa71e1bb5508..8e25895c0be3 100644 --- a/code/modules/jobs/job_types/station_engineer.dm +++ b/code/modules/jobs/job_types/station_engineer.dm @@ -65,6 +65,8 @@ pda_slot = ITEM_SLOT_LPOCKET skillchips = list(/obj/item/skillchip/job/engineer) + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/engineering + /datum/outfit/job/engineer/gloved name = "Station Engineer (Gloves)" diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm index eea6d428c101..b83f475a8e41 100644 --- a/code/modules/jobs/job_types/warden.dm +++ b/code/modules/jobs/job_types/warden.dm @@ -81,3 +81,5 @@ box = /obj/item/storage/box/survival/security implants = list(/obj/item/implant/mindshield) + + wintercoat = /obj/item/clothing/suit/hooded/wintercoat/security diff --git a/code/modules/loadout/categories/suits.dm b/code/modules/loadout/categories/suits.dm index c830aff2a12d..cc4c0c63f4ea 100644 --- a/code/modules/loadout/categories/suits.dm +++ b/code/modules/loadout/categories/suits.dm @@ -42,9 +42,17 @@ ) /datum/loadout_item/suit/hoodie_pullover - name = "pullover" + name = "Pullover" item_path = /obj/item/clothing/suit/hooded/wintercoat/pullover /datum/loadout_item/suit/hoodie_zipup - name = "zipup" + name = "Zipup" item_path = /obj/item/clothing/suit/hooded/wintercoat/zipup + +/datum/loadout_item/suit/wintercoat + name = "Winter Coat (Departmental)" + item_path = /obj/item/clothing/suit/hooded/wintercoat + +/datum/loadout_item/suit/wintercoat/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE) + . = ..() + outfit.suit = astype(outfit, /datum/outfit/job)?.wintercoat || item_path From bd6066e3b3ab56c6e85d465aae72e4b0564cad1b Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:28:21 +0000 Subject: [PATCH 005/357] Automatic changelog for PR #97205 [ci skip] --- html/changelogs/AutoChangeLog-pr-97205.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97205.yml diff --git a/html/changelogs/AutoChangeLog-pr-97205.yml b/html/changelogs/AutoChangeLog-pr-97205.yml new file mode 100644 index 000000000000..72bb9826f374 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97205.yml @@ -0,0 +1,5 @@ +author: "Aliceee2ch" +delete-after: True +changes: + - qol: "Winter coats were added into loadout." + - spellcheck: "Capitalizes some of the other suits in loadout menu." \ No newline at end of file From de92ab68459ae8bc6c11c20bcd19439ad8b5a6c7 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:00:27 +0000 Subject: [PATCH 006/357] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-97205.yml | 5 ----- html/changelogs/AutoChangeLog-pr-97259.yml | 4 ---- html/changelogs/archive/2026-08.yml | 6 ++++++ 3 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-97205.yml delete mode 100644 html/changelogs/AutoChangeLog-pr-97259.yml diff --git a/html/changelogs/AutoChangeLog-pr-97205.yml b/html/changelogs/AutoChangeLog-pr-97205.yml deleted file mode 100644 index 72bb9826f374..000000000000 --- a/html/changelogs/AutoChangeLog-pr-97205.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: "Aliceee2ch" -delete-after: True -changes: - - qol: "Winter coats were added into loadout." - - spellcheck: "Capitalizes some of the other suits in loadout menu." \ No newline at end of file diff --git a/html/changelogs/AutoChangeLog-pr-97259.yml b/html/changelogs/AutoChangeLog-pr-97259.yml deleted file mode 100644 index 51950a269989..000000000000 --- a/html/changelogs/AutoChangeLog-pr-97259.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "necromanceranne" -delete-after: True -changes: - - qol: "You can punch a goliath to death. This isn't recommended. But if you are committed enough, you are welcome to do so." \ No newline at end of file diff --git a/html/changelogs/archive/2026-08.yml b/html/changelogs/archive/2026-08.yml index ee208ceb5bbd..6454bbcc08a8 100644 --- a/html/changelogs/archive/2026-08.yml +++ b/html/changelogs/archive/2026-08.yml @@ -26,8 +26,14 @@ - bugfix: the experimental cloner now spits flavor and policy text (if there is any) when spawning 2026-08-02: + Aliceee2ch: + - qol: Winter coats were added into loadout. + - spellcheck: Capitalizes some of the other suits in loadout menu. Ghommie: - bugfix: Felinids (and nightmares) can once again use more complex objects, unlike monkeys. JohnFulpWillard: - spellcheck: Cargo bounty list printout now has a slightly better UI. + necromanceranne: + - qol: You can punch a goliath to death. This isn't recommended. But if you are + committed enough, you are welcome to do so. From c4284fa54dbc2bd170cdb02108d210ca7bb7fedc Mon Sep 17 00:00:00 2001 From: PapaMichael Date: Sun, 2 Aug 2026 16:23:55 -0400 Subject: [PATCH 007/357] Fix AI Holopads leaking unknown languages via runechat (#97297) ## About The Pull Request Closes https://github.com/tgstation/tgstation/issues/97294 ## Why It's Good For The Game Fix a bug --- code/modules/mob/living/silicon/ai/ai.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index dfd30cf0d8f5..1983ed379809 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -859,7 +859,7 @@ GAME_VERB_PROC_DESC(/mob/living/silicon/ai, set_automatic_say_channel, "Set Auto var/rendered = "[start][span_name("[hrefpart][namepart] ([jobpart]) ")][treated_message]" if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker))) - create_chat_message(speaker, message_language, raw_message, spans) + create_chat_message(speaker, message_language, raw_translation, spans) show_message(rendered, 2) /mob/living/silicon/ai/fully_replace_character_name(oldname, newname, log_new_name = FALSE) From bad8c0e1fc721defec694b0048baf99882c9bb00 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:24:15 +0000 Subject: [PATCH 008/357] Automatic changelog for PR #97297 [ci skip] --- html/changelogs/AutoChangeLog-pr-97297.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97297.yml diff --git a/html/changelogs/AutoChangeLog-pr-97297.yml b/html/changelogs/AutoChangeLog-pr-97297.yml new file mode 100644 index 000000000000..9ef1621c77e6 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97297.yml @@ -0,0 +1,4 @@ +author: "PapaMichael" +delete-after: True +changes: + - bugfix: "AI Holopads no longer leak unknown languages in runechat" \ No newline at end of file From d01d538cdb93819b607a4ff4cc9021a31353d2f3 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:00:25 +0000 Subject: [PATCH 009/357] Automatic changelog compile [ci skip] --- html/changelogs/AutoChangeLog-pr-97297.yml | 4 ---- html/changelogs/archive/2026-08.yml | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 html/changelogs/AutoChangeLog-pr-97297.yml diff --git a/html/changelogs/AutoChangeLog-pr-97297.yml b/html/changelogs/AutoChangeLog-pr-97297.yml deleted file mode 100644 index 9ef1621c77e6..000000000000 --- a/html/changelogs/AutoChangeLog-pr-97297.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: "PapaMichael" -delete-after: True -changes: - - bugfix: "AI Holopads no longer leak unknown languages in runechat" \ No newline at end of file diff --git a/html/changelogs/archive/2026-08.yml b/html/changelogs/archive/2026-08.yml index 6454bbcc08a8..93c10f97a775 100644 --- a/html/changelogs/archive/2026-08.yml +++ b/html/changelogs/archive/2026-08.yml @@ -37,3 +37,6 @@ necromanceranne: - qol: You can punch a goliath to death. This isn't recommended. But if you are committed enough, you are welcome to do so. +2026-08-03: + PapaMichael: + - bugfix: AI Holopads no longer leak unknown languages in runechat From 47c19f52b3dc1d48cc0979093a7d4e37d1c372ea Mon Sep 17 00:00:00 2001 From: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:46:58 -0400 Subject: [PATCH 010/357] Multiple generic polish updates for the Mulebot. (#97198) ## About The Pull Request This PR hits a few mostly visual and slightly functional benefits for the mulebot. From the top: Loading an item onto a mulebot causes it to play an animation, mostly lifted from the standard item pick-up/set down animations. Loading and unloading an item onto a mulebot plays a sound effect now. Loading the mulebot plays a small animation to mimick the presence of suspension. The mulebot TGUI has been tweaked so that loaded items are no longer crunched into a tiny corner of the screen, and have their own horizontal slice to be read from. Adjusted the TGUI to the slightly taller, but in turn results in the window not needing to scroll by default, allowing you to see all the controls by default. Re-orders actions to be above settings, so that the behavior when actively using the mulebot is closer together on the screen. image _TGUI Preview._ https://github.com/user-attachments/assets/ce421446-0e0f-4e34-a835-7e513e3554b8 _Older version of the UI, but showcases the animation on load/unload._ ## Why It's Good For The Game I originally set out to make fix the issue with the text under the "load" section being more legible, but I got rabbit-holed and feature crept. Ce' la vie. Overall this adds some additional visual clarity and responsiveness to the mulebot within the gameworld as a whole. More visual feedback, more audio feedback, and a more visually descript TGUI to boot makes mulebots more effective as a whole. --- code/__DEFINES/sound.dm | 1 + code/game/sound/sound_keys/sound_keys.dm | 7 +++ .../basic/bots/mulebot/mulebot_control.dm | 3 +- .../basic/bots/mulebot/mulebot_delivery.dm | 56 ++++++++++++++--- sound/effects/heavy_drop1.ogg | Bin 0 -> 12102 bytes sound/effects/heavy_drop2.ogg | Bin 0 -> 13563 bytes sound/effects/license.txt | 10 ++- tgui/packages/tgui/interfaces/Mule.tsx | 58 +++++++++--------- 8 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 sound/effects/heavy_drop1.ogg create mode 100644 sound/effects/heavy_drop2.ogg diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index 5a89def0f73e..fc98274a8bae 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -297,6 +297,7 @@ GLOBAL_LIST_EMPTY(sfx_datum_by_key) #define SFX_HARD_HAT_DROP "hard_hat_drop" #define SFX_HARD_HAT_EQUIP "hard_hat_equip" #define SFX_ALIEN_SPIT_ACID "alien_split_acid" +#define SFX_HEAVY_DROP "heavy_drop" // Standard is 44.1khz #define MIN_EMOTE_PITCH 40000 diff --git a/code/game/sound/sound_keys/sound_keys.dm b/code/game/sound/sound_keys/sound_keys.dm index 138594315331..00a9ba444c15 100644 --- a/code/game/sound/sound_keys/sound_keys.dm +++ b/code/game/sound/sound_keys/sound_keys.dm @@ -1151,3 +1151,10 @@ 'sound/effects/magic/void_deflect2.ogg', 'sound/effects/magic/void_deflect3.ogg', ) + +/datum/sound_effect/heavy_drop + key = SFX_HEAVY_DROP + file_paths = list( + 'sound/effects/heavy_drop1.ogg', + 'sound/effects/heavy_drop2.ogg', + ) diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm index d7bd93be22c4..8250073a55a1 100644 --- a/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm @@ -18,7 +18,8 @@ data["modeStatus"] = "average" if(BOT_NO_ROUTE) data["modeStatus"] = "bad" - data["load"] = get_load_name() + data["load"] = !!load + data["loadName"] = get_load_name() data["destination"] = ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON] data["homeDestination"] = ai_controller.blackboard[BB_MULEBOT_HOME_BEACON] data["destinationsList"] = GLOB.deliverybeacontags diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm index 5fec23e9bbfc..990d66eb113d 100644 --- a/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm +++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm @@ -47,7 +47,8 @@ if(load || atom_to_load.anchored) return - if(!isturf(atom_to_load.loc)) //To prevent the loading from stuff from someone's inventory or screen icons. + var/prev_loc = atom_to_load.loc + if(!isturf(prev_loc)) //To prevent the loading from stuff from someone's inventory or screen icons. return var/obj/structure/closet/crate/crate = atom_to_load @@ -65,14 +66,18 @@ if(crate) crate.close() //make sure the crate is closed - object_to_load.forceMove(src) + playsound(src, SFX_HEAVY_DROP, 60, TRUE) else if(isliving(atom_to_load)) if(!load_mob(atom_to_load)) //forceMove() is handled in buckling return load = atom_to_load + var/stored_alpha = load.alpha + do_loading_animation(load, prev_loc) + load.alpha = stored_alpha // Restoring previous alpha following the animation's completion. + load.transform = load.transform.Scale(10/7) update_appearance() ///resolves the name to display for the loaded mob. primarily needed for the paranormal subtype since we don't want to show the name of ghosts riding it. @@ -88,9 +93,11 @@ can_buckle = FALSE return TRUE -// called to unload the bot -// argument is optional direction to unload -// if zero or null, unload at bot's location +/** + * called to unload the bot + * argument is optional direction to unload + * if zero or null, unload at bot's location + */ /mob/living/basic/bot/mulebot/proc/unload(dirn) if(QDELETED(load)) if(load) //if our thing was qdel'd, there's likely a leftover reference. just clear it and remove the overlay. we'll let the bot keep moving around to prevent it abruptly stopping somewhere. @@ -101,9 +108,7 @@ update_bot_mode(new_mode = BOT_IDLE) var/atom/movable/cached_load = load //cache the load since unbuckling mobs clears the var. - unbuckle_all_mobs() - if(load) //don't have to do any of this for mobs. cached_load.forceMove(loc) cached_load.pixel_y = initial(cached_load.pixel_y) @@ -113,5 +118,40 @@ if(dirn) //move the thing to the delivery point. cached_load.Move(get_step(loc,dirn), dirn) - + playsound(src, SFX_HEAVY_DROP, 50, TRUE) update_appearance() + +/mob/living/basic/bot/mulebot/proc/do_loading_animation(atom/movable/animated_load, turf/old_loc) + if(!animated_load || !old_loc) + return + animate(src, time = 0.1 SECONDS, pixel_y = -2) + + var/image/pickup_animation = image(icon = animated_load) + SET_PLANE(pickup_animation, GAME_PLANE, old_loc) + pickup_animation.transform.Scale(0.75) + pickup_animation.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA + + var/direction = get_dir(old_loc, src) + var/to_x = base_pixel_x + base_pixel_w + var/to_y = base_pixel_y + base_pixel_z + + if(direction & NORTH) + to_y += 32 + else if(direction & SOUTH) + to_y -= 32 + if(direction & EAST) + to_x += 32 + else if(direction & WEST) + to_x -= 32 + if(!direction) + to_y += 10 + pickup_animation.pixel_x += 6 * (prob(50) ? 1 : -1) //6 to the right or left, helps break up the straight upward move + + var/atom/movable/flick_visual/pickup = old_loc.flick_overlay_view(pickup_animation, 0.4 SECONDS) + var/matrix/animation_matrix = new(pickup.transform) + animation_matrix.Turn(pick(-30, 30)) + animation_matrix.Scale(1.1) + + animate(pickup, alpha = 175, pixel_x = to_x, pixel_y = to_y, time = 0.3 SECONDS, transform = animation_matrix, easing = CUBIC_EASING) + animate(animated_load, alpha = 0, transform = matrix().Scale(0.7), time = 0.1 SECONDS) + animate(src, time = 0.1 SECONDS, pixel_y = 0, delay = 0.1 SECONDS, easing = QUAD_EASING) diff --git a/sound/effects/heavy_drop1.ogg b/sound/effects/heavy_drop1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1e2471e625021f0b1737a88f1fc4e28b6b257876 GIT binary patch literal 12102 zcmeHtcT|(hx9B7x5CUR=1Og(4Y61ZX1_Wye1nDM$#1KG3Q<@$D;b%8szz~X*h=}Nc zP=p8`KtSvP10q=HU^`;RcI@p~cwfMC?s>m=*S&A8yWW3q)_k+3?3w+|E;D;(&#Lw7 z0|6ZT3|WMk45dFM6=n)c{Us?PHd$2#_o$y~0f{rf?XXpiV`FM;(k~k~ zL~q#xV1#;rOI_VWR*y> zy6}-Xl{VV~{w`7*S)4)PTV30ot3MpZ$Q=`XHQ^rzv2A5=nwC@aV z&j=aM2>X-~j?erVKkvNzNfoE3PJn@O3)kWYB$>If?7piEA)h(79lh&z5?a0J%b4X5LY}T zl=hdtyXvY_`l!4PduXU5E9lVtQ*|YWra72!&FBHdOH%K_uJVKZrODU4wM(mKa$EHd zX*ARgLUl$@%5=Jo!XdcW!BUO78$76QaDVCJ+EZ94mnA(kzx7nm%y>V=qHayuwGP*N zP>-yehvH9ZhC#V6QDuKz)l}8}yZF!`zJ-tDyAP)mcIK%^V9jdGicB}}(h>_%M)RvTr%%tE0DuTHEyaH&)1o{h#Z`q8n_l9HD~^3+RZ@TP+T!Pp7cuVI z6iAB6PLLGmwmv?ReB9M5zPfdkZ(A*8`;e+RH@ z04a@VS|_CuIh0Ffdjja1GYtMqa}u$a3QR5+So$_w`VW$?zNM~uM;*{-u&DllxUd0S z%8)^HpmW%eQ}hrgdL%XaMq6~q$(_M%KNV(1ZSIbx{6lk8Dq`-M_rR-A?LRc9oS1jt zCC|dU&@#BtdTa6etkSxJN1oQn(Ep}6F_NPtlA~hD6LB6ft2j2RRDMd9(0ly(>HjwV ztvM@`ETIT#4kO9(ADW|}n7BjQ)J*n&H#5g&0}w$sGYtR9000#E+P>542#aku!gd;A z+i`-H{nr`;wT@8zho}%?_XA)Az{9GV;cYs45&5oJZZRltrAb%l;-_Vmp*ef=Ej+zn zngw3LsI`{#duL#W@AKp4CI@BbHbi;3*|l}E%^uZiK+7HqR2TuBOE43Ego!T#(mYl0 z9s>SY*j0ViE&>I`0F|b{jHb_+DgMt=2oaT~2F0JlK!^r{U<1^jxF9Twg$x=M+DZQB z;XlE}a)Pb;2QC`bpBCi*Kgaa{NBI8~_-`oyi8xFL`V8n5X0~vl3k>XiKr1A8zorT! zgI_P*V(h0s!82GoyHsQ7FYSOf*tgUzrb}nnXy#LAAJMqJC-!d(5fHXQ4M!kgFIB^n zj=$s@`jOXu`ZbyZqB!`{AyI56HC+Cm|C|s2Axr}ZfqpOT?|-WF(gVPyFe(_bFT4t) zVL%uid_)6)!sxB0|M@Wg_qG3q5GEu7uvf-2*ksZN&x;JE6_T|C!N4D}#h8YLses|C zznep~GSDDWqpSc12_`mS`v$43hc3m{v9g{-vhR>bK{R9pWtmf|wMw;y}^r<6MvDqXf5YBahJ{~F~vtA9ng{-|uX~Y%RCzf;{ z%Hy(RJ+o>&WZu+aBULtzw2%VX$pJo>OQ5>>)lm>!SrL0NRHp+O9GfAKDk+;jS(Xgh zABQ0j7@*BiTapmqpcgbvZX8fdsMvL6K16Qs!kVo|=0kRCTw&ErP3h9>KdHa<$WIz4 zg+bg<5z4Ah#iKg891WF*AVOuR>|MG1d1sr7{$nZOw^YP`ep&(1Jtf+xUA29awE$SP z2>_eM@Dx#ztVscFba2&vDWafAkjuzGoq@}db#|KBxaV1*q zFXl@c=wb@0x@wr8Kry)Ps!>rTcYq8GKhylRITw1PNYZ4zD*hLjSYly zr6Py!Px#1s$`gK(Ugu}iKsyLH9+Cx%sNA}4J|sAr(K)$xBhut+NI+ZW0%%rvxA{gK zgYZgCLsQ{2V@^S5NJ#X$RT8F>D|Qt=84@#sNOcmcLQOZgLFOgIGo#4A8Y)DE>|Usc zssTc0RXse@gQ&8Eed_*hsFq6E(|u9XCNgvsRavTfknBp?P`?Za$BgX^5d;n4m~QZa zj;vU^=1haM5Q3}z*#IGe#{by>A%cdP=>cU_Cis38tSU2|)~Rw(aVDKk@5%P9X?PExP8cZ0j9 z@#FHUac>Q?R-SJ(!L8TZ=WsfTq+XYiV^dvHR7~$HVQWX&Qa0yMR=Pe4qF=&8OLjT{ z2wg{g6qc5jrVU(ZUiol<*3zDPkwK%`n^h{xH~G2DKkN*EoA?SQOWZ}ER2UO&Y7oLM zWabXg(?9NYrd?={2<`6*)1ag1m~@^A4W{YhaxZiK@2^F3%E|+Sp3xvY0GNUaX!*JU z%*|y7A56af3^a9(%t#bhH{I#xCEzmvUTmRQ9P`*^@zUiqx`(Hik6!>2THavOJ0T6g zP^g&$r>cxX{k-v*H{Bm(5mdJV&??zDt9l`Q88<8_kV*3lWb(NFfm7@$f7*%_4EI(3 zLB7jqG$zyE-)*_4Pk_7I3RkwDzgrNKoA9O99uGX~|{n^C0V@8c^ zEJCp3fc+g)y+yg}u;PBNg#&c0y`^_|9{)w+P+y&rbNYVCOZ+X|@Ruz}pTvY+Vf*G| z2nth_0A@u|tA(MmB~7ABZ@%a+=`ZK&1@jZ$gw&agHZGn#{>*TkK@*k7H@&_*khoTYT==bGp(w4QKWdPsDom zIkcrV#64fN9?`xee_r;V&xb%GTxvkW9^)g3dYepIW+4$(40YE4FAcuvYlT?t^z8m( zB1u?W%e%LB%4${jj-224A@9HZIbM^x%C##6^Gn~2cWtTL^XksEB_RJKZ#nn%E_iLYtAb(*_Dgy%T<-V&^!{d!7}Y_r zC}K)Wo8)m$5@_v~-G%~jkQdevf%WD~H7^7e;cV*^;p_3|I32S< zVU1cvVCqd)@rb(U;wEf;cvihGhY`r|pvYEcJL0m<<<7_G8FX}RoEzD-51r*sXT;~+ zIOepZWjSd!KJ+8=kIE0;PZbl6MM3EWCpS9vf~{VDHh^cr9mYRbJ0m_Y$Wl+N=kz5p zUf6fQSMIoc`tDlHVG|diWT(^?Ief>z+E^^F;C8-`Jo|gYvP2(@`NS3wdhxQ)0lvk! zdM81w*;qbDSz+lTYg$|$wZckK_2zkJ7h6`nZXnka+Qk;L7JQ+;ZjZ6**GiF@zKxyu zV!mJfU-Jl7`+9*?%`&*xFoNW^m(KB^#ar)I3H{-pn#uhPZxi$nr zMx#lr`dp_F^tkw@i| z4%--vAwCw_nAJL%i-=m>r#;5QjE_y3rjtZ#-xZDi;hvYv08B)I|ok|$L@)(rpWzL!*HcJDPqUKa3C2GQhpC%N+6SYWeNtqFJn_+Sr?WMj}S6n;PRPYTU$i5qM>d z9Bn)&%2yDarWWS$?W!|jMj>zbbP)^IY$T-!VILZ zN7aTp>|Sn>dMvrP$pK~238IX$F^v|HQO3b);Q}K9H--2xHN-&2PjgF{wc}V|u{Q%7 z62#Dw3rbj4hC8=qf6xbjWtUB}CA0||dA;J(IX09D!G4&Q>>-g=m66ZlQpFrcqFL{Y zvct1BJv!UezP*3xXz9auA#g@+g6_5D|H6< z&sn_5ix3@>`BN4gEQvK(g|KAWn4#Pp56{Z%YFTt;IK3;P)zY~nwdP#cwU%!8xJVbl zFZj01Sc|&ttS(aco4NW0E_;57kpEk$br`#yFjS zzuOMZ34*b34c#(Yj7z_diw>~GiX7h-XexV~Jvk@~gGe(NFtTYrDGj@(feB*Rs@ulV zFzRM+z4AJRwIZ^%pqKs8s<$nolO{0g`}ozEQ%98Nz=ciO?|8tWkSKZYRH{ge4z_Y9 zY16$8in}E791mOdG$%GVd(OoM&Ar@wQ$#lq#9-ow$z-rckKaTwylZ)O*+T2+Id>Nw z4q5h=I%xIWHcU|dOO&FVsg0rdtXQP|qDMVG4AUb#m9&b8F#wz3^+`qr`GsYhO2&_y z(|k6&+}3DxDAPtXR)P7Mvwk~v)zbDH4t6VzE;S2}y6;uadrBBE3>#~5h&s5T_}9#6 zSWCLAM&UjB!{*-vUBW2WO{WmBdM|$gFxI>sRvNDMS#6AO?WmKWwGV|QxUXdSJ=)g$ z`!#jJTvCL^qhTkQD2}9V($@KC?>Z7Zy+upsE9enuYSOaRc*%6>`od-|q2EJ|ho0J{ zW49>-ZMM=zhBNaN_ezvoAbuh>b0Fl2NjE#ew;0ABw;DJrvC~M;FJiv7{9w#A zLD2)~9q!Ly6L3FUHJUpxJ5M!Tl<)h{Shv>Z7hBs<(dLJrt#y}$r#_7jukFq-Ra-bg zMAjE?g~NAjYH29ksuu94md4lFjiKk%?0}tBzI!=6$Qy;eD0~!;v^8e%-*csMynlR6 zk;FVLX5?~7`9&Yp2)ERw<^cwmgzwV{`OVuThx;9%YVIia&1wA;hVlD4@!%ZD@^=Bu zG^WURi#eI)-jEd$qa;d3wp6nRD%@MRR5)5LspWI9Fh&Qi(h?Njwsgk|m6v=TsSj_Q zRfUSK?}*g88Fx~`44sqwnzizlRzF|g88ASDC6A6kknLEludgT?Gs91yQ2dO>xDrJS ztC2AhD0d#eIO(>3@gC;J;DUDzb4#4^zpb(Z!`t+cBD3%9M~{79hZcCP`2BL~bJ8Ba zLyZcc=rC40oF3F@KPx586HBct=f7-~8MAzK<1vYZ7SZJ0kMZfFRw!LoZ*QY=%v!yS zOF{+t7;b+5>vU$(=CYSCqMMJk8cma?2T#{N;0_N>0;waNO09~5cyCwr0g2g>mvfd< z!c2IGsO7PM5O{Rif@B|ez+dlGHCre|lGSKjp`qG&pj)04 z;kgbQwll!Ec^+L%U$%oz0TmTY%oBYyYEr@?jyqhs>2r|1xC@R3P7!{K?37LU(I-uO zDFW|$+i@qCXEbn#b(;)Y>ts6y8mMd&neBgtg z+?ZE10LWBNmJ^>5J&-0*m&+vb+VuSrl-fr%GdVH)8o#MV6OFSdHhc(}DPm#(2sLMv zRxfnaO1~P-E+41fcnE*E>110C+qyVAR3A`+(>ZnaKCR|9H*dx?P$X`@`5yb1ogieP zQZERXSU0xMF2eCt@pH+PVSk6Y1#Gi*tbI`ytmehYoETQC4}PojmeqvYTnTD+nH%C-8PqIyIuX|Uvtz4jgJ|xP{S>R zMr$UJU4az>L-KYrAdm4|MsFH4#5CaDjuB)r9de#_>pIe;p}0FK7_*4GZa{+OPR*+T zn=`X>me#&lv8!Xl;)@LKT#+iIjDMl+HAUG*>=W=V^u znYwU!QbQRg`24b8VL1}bdwU3v&M=vAL4K%tNQ+n1IO!XhTIOX6*q~3#*TtEoDFZn$ zY9X7GmK%u~8%Cn~tiGq5dW5`X`~2A|*{)MBKV=s(!Ji$%O3oUvdCjuS8wUOhj;0q3 zZT-MsS9bQS3DV*TB1qOVi&iHrZ;y77`ch^5KwJVt!$uEs?Jj9y$&DEWmWejTxv~Lb zuQ^DkkKVj{*Bt4Fxd$D!LnBh)sMxO;FBfNxs9^>_?k#$Wuv9;U2fkQ`3V~+vBXmc$ zET$_Ww<(=_{+2{N8yCU}qTc7YI@C(Ifk!#BGHnb~H;gTIG6oOn4R-i|#gVVDWh2^#=xJ z<2IL(sM7l9upM!fu{!?w_5)L=x!eX@9RramUEr$G(%r2&VoaiU@>zD zS-6GailYe;_)%?#R&=_-21)suNu5?Fb57I1_z0VZ$27c5LUQKUGwN~R7(1>>e0?9N z-^hra)lo9FR6K&I=Rh!Dx`{MU9+LpgOdi0x(k!>Jl}Zn5 z>E_Df&e#QU24dn&@)|QN?HmglS&d8G2tc$;GtxWiyLsocmWs9AJmaAY4&loN&IA8? zBDksJfS!}52RnCMfHGpeq;Ngcc=cPTF1(>|P~+r#k=!L(?vF{$8mEe_3^-(Y3{Gj> zP>UYHgXtRxwU;sfaRZ_9p6$5j$CyL+TIJ_AWj;lin)`XE;-F;LzqYSW*I#0YF^1-A%~tl}=yZ=JVf?Tc;j5c$Z7q_2_hGQwuJtXSvoFBB-eTYy;YSho)* z9kDvsUajGoapJ?yazof?)6-uX=R0pl6K#%A+>~2!ActM8miBh#`*n+a9-*VUe{>CA z{Rq3+i4>w6tE<{gMiGbGh%(q*E#Dy_KRyU{m|eiNGnZLVF*n%jNA7!Q4yNfX827j* z6R<+d47`Cf!m25MA#a@6Q;j{=m~oprU|975*5=KI)gBb0-r$*?EDoo#mdHCQH|Ih- zOnUhsPJ~9w0Tm3`<)4>7qv++ z{%QaCKB8wmA9ZC{_3f6noCu}78thsDggMP^#=UWc<80R|y^#Fgz~%dq`|%44?2Dj# zH(gS6m4{xdXAo7dfr^n8vdx41N>R0_Di4u=NO^Jb!G$Aw7lX8)2Vof_aWsZowm6Q} zkCveCll}S9x>aj!-pQ2=f2T-XhuDtDpLgNPou&!r1NSOh8{5nzSz@^WVH0rcfefw= zbHkbE_Q8Y^U2Y6#6em)PfHz=AtqH#N$TD{uP&8nqt>{^v^>_>>TizrPmfdLLM^h1* zFfu)(d6Fqmvm^ku-%eJtPh6zi)Z=Is^c*q{*UD@~rc16@lN0UWeZ_M!n(v8-QtOex z13u8yoNH?dH@IXq;QzZ~kD?v+W80nUTdlv}cSMdvJUF-JQ}Pk-3+>u&jJUI_hqH*7 zs3@Z|pSjl_KUdec;EhI0)R810e;kYyjDrs7btc%vPsBx4A5>L*2n+dwm46Rw>9QcqqLt$|yRE^d=36!#n^sRT6IUnYu8L=(@=V zZ^kTYjp$3t)M<_qra1tua9VFp=2*jVc)t1qM81K?@g73oEJ0cUA|9mm+W^7?X|xdn zPwQ);p#`J@+;Nx->Jb&_NVlI*Z@;33-vkE&j}yWsEKUv3gs|oEo!7sfjlTA6c#fCF z?b?saUcKur`%b%Tv&RiD$^zQ5i-2>nC1P$G2FE_r_XN}IK-j|%3yvVLL2h`{9Fh)_ zhN#p_62OS!M~xgHoNUa(9pyHyv9)aiOzORNlRoMMAkerc2sT5Th@KLpok9YZs)LRq z=+JQLn$1F#t3HTu>|K_ka8z8jarVrJP&wt_he9s^%3T^wX62Sd9_7N~V2b24M0uS( zFpc|u;!gL;-Yu8IEo^u4cmJ~Bjc@eXht#a}`>TQQn4zKov?Q8Ha+(9gaU6DD*-e{j zdc`bgNB|@@APvtQVv3b5sE8&66PLnZ)T1TYc4Km0PK6%rY^7BPvGic4ue)~pQurn{ zzqc7go3^Hnq=?vZuI`*!x|9mHzLrLoBK5#iD=93^Zpo&HU{t8rD(z9>x8lu(X5Rs*MWkONd?DzF)YTtaJ+hF?jxvQ1SG`)V2k> zW1=s0=N7pYLQ?UyPNQVa3#9jO6&Y^eYJf#)>OuZT$dyZl)681{IL=My9~+e@V{$tt zNV@wm0#^|-TA?55ujQxRT4OG@+t7KMlv;zs`qoF(hOb+Lb=L(-E=)T~KsL4&GFV(f z9nOxkA*~$&qc>Busi0OyQRjqizZVUJduQEPXTX8xt@HZ?dTH}>^A=-|#0CSrL06Kn zOkL40gkk&O{zWf`1mn) zYKcu++5#9TjI7@e;KgG5wmSz1D#1_%z6A_j<3R(%vXab^H91>`BaEH9g3OBQNv5=A z*xR~hUpgkSNHxG}iXy0yLX`v*9C&43nYDh?P}PPsL(htvfJ^MYia%MlzUy>{K!QGH ze#c&Qm9LNut()W3kfm~QS{UAdFW>roYAvGyp*!?y)$iBl>lgT~nVMS7dQMQj|9bN7 zwfRFUUt0lTB~r8`L~xeiUY6rwJ>xHc#$r-TDxfNwk+2xVxbIsk`=^3BG=jXXWgUuT}k2uJsr zGIC9=0APY6!QXf5%wuSRHy`gkn0m8e_Q4zVKUS>#m64IXW&5}LZ(bB2?x#Xa0i^^E z8rN>pRZ)C85Q+_91jz9Lun(`i-+ZUXk4r3x47(%FSLyA^2vv(ufdBh=$)MXW+!`Xn0})87Ua579yZO}FoPc2ji~W(u$ty2NrwY1 z)5$+35%JGuP^01|hSu#L-B4+ifgIScIx5(;3(d_>jn7KKLBSRUDsBlNuY z_ubc4>{xyI2zRv$u^B4qtZjvFm)TxQfL}QPK<9n?=fl%JBo$7NY;AzS>P{q)%%pne zTFG9q=f-8@=x62|m#$K^^A@>I6Nh#^{3eDE$}M_*QS7WeclfZp$r@v=>@weT(bB&SwZ(r=5{u*#|7e&tyH2 z|C{8!(bT6;@&R?_uh$=z75!<(3N&2{2R*mhg9bg2T+ReB8jROAoIKE@MgWf_5`7>| z4<{bLF_;{WMw^)7TT+|q-dF0UwIS*QVXL1rAL`b=#-tFSXaI1xK8uEo954{X(B1|> zO9Q&iDs%tNU=|GYAbza-w)^y=4{)pV#n;adnQjnVIrL`g>F>i@EuZBVULD@1nd8!3 zy6u^r9)KUw`=n)XhA{I-0K_3H&2Q$`N&v{L;rZ1MUUxg?2ZSzN!i`~OngDvu2x2PK z!Gtj+fI1)fm@7;3tutmn18_)O&e5(;&iADA5njfCjvzc94JoLXX^uIymQEjjSLPsrB+@(1QfOM}K<~e4Zejwb^KDYs~^rVbUf9cnvY@XV6+8f`!v+ U8wbZFb-DPw-&~Q8>)_yj0YMmV0RaVaQybe;gkm*VK)=-}|VgENH|EQv{rj^DD0!rihx zKJ}|DnV2VnU6Nt z?~qqfme)^;E?^G)olu-(OaMRuDNT2O%>&evqPuz0V)*5RNhK3mcSU%KjLa;Uw%cwadILS3n9ipB%nu?<<6&X0kAxdU|)E%B`2{d1L!(cg$3*Vkde!~m(NBr){5&Y|SlAfT7(5%R?OtYE zUy%xT^>JDhAcdz1))W(bTL{A~=2u3o*1clz7P^c&aYOil0yv~auHP=XzCG>w_I+U$ z*%4><#%nXEy>k$?$yYwoF)W_(Xxc1 zTViQjQsY}6n{YDzNJm^_$L8Ty#&E0tJPIscly)>g5noyVf5h0Za>;)mS#d*jz=C-> zm|{MdV#N)%8cb#A%yW1Mz%pe8Gx}4V*KBnj+`0;mYqoJNmIYjS6R_ZKA!x-80E=nf zpJILy=0>pnc&f|#tsd962406*q2!tsZ z5{w|Lx<{!TseFCK-LUe$qJeN^ygQG7WNBwZ#gTalCS5hX3(L~>(&2u^;gQO%SAF#> z>ptdo86VMUZWx1MrVo!A4wy#4c=2PEIt@33aNDqv$_L6$0-Vb$JF>LBlmD^5PioPy z@#xiV_d9TlyqP13ow|{5?zfnuKV8w)!v0--Sg_oprbz?GGAR4@W5fh%J+;(g+X2H; z1#`%@)}nB~w_ihMP_XIJx@{TrGp7KMg`d~rzms`Y{;0*ZC35;D+NsNq!whZG)O&Ts zQ*|%iOP>jAF~bGc;==X^$G4tz_erd4p9;6DD+~6st@~(Euuv&6MV=Vbc_YcGNWElx z0=9~O72FG}YetJl|0P~~Y0fjYhU2KCeTb8XfG=3TeRJ5Mw40~1Bd61DU*C88df}Ig za{gVh{;?bY8%@@{O_qrZn1e?%*Rpj#3ixl!Ng)gtn_n!p@^7^Y9AjL0<-G2-^XNh@ z&p9x}By!XwZJa0xag7{zk&Fu@liMXXIwTRN_l0$QGMJBMbNhPQzgUjeL@eF+-}Na$ z{fFf!X#3~f_Al`*u?j1(*;T$Nud?Cr@y89-xPP;pIQfYR`3af)p=>`buRK1lQq@_V zbm`>NuKzavV>xS5t>6e-4mZ{6Uo1z%H1~q7sg)7<`r{luMq!3-<1YGF0RW&W()XWV zM|i>Zlff>N!S({a$A4`xaP6dX;J7o)*nvhIf+xdH7Fg@m!#F|3Zu99LVAa{?94>{~rE72mV_Qz$y+I!XJ@cMYTgn-5`*A zmsLXXec>#P4tud`Cplo@tdO{BL8Z?4-_`+du& zlguJOT4aQFbY{-hL>-!Qj5WZc!E7>Nc+8o1&#k;JzIrHy;XlqPmcUL>o~1LKH9G!q zSH14F*YLb)^6Gd5yuH9|M{C*c8t2C_42m1_0~9Nux#L!Te^)h44*O54ODT-u*Bl-p zFhme}=&rE*VO}+@vR>7-3?_7Kev5*gySU1Kz|MgLcol|MPF$+XBtGA&F6?=A=}k|6T>L5VaTZr62^f#iQIn<`YQCtWA) zh5_tA-8d)-!^~<+tqcHWH4va^uq!SqTd`(=XI=mTxz!TLU|lGP@?+NNM;o(oRZ#(K zA8Jf+Fx|Ee$EI5!4EFS?s||Mav#%=;MpF-#_%%>!gM)3+K)Tos{)E`yuIt&4&2j+O?mfMJHP!?6uU zsVxg;KRUV<_CJonC=g(M8i@K?0{?jfE%=a{j z=^5?z&Cmm2T`~Z4byQkbX?2SR-slk8{W6P*wS|2y47C=|_OZ<_`rp+blEQ7I_)jMk z*Mg`|xfI;Lo?8Cn{PO=YHyxhX+#U)@SF@3sj3kZh6QwiUoN3AOh&$sU& z9Z90XQcHwk(N2EtFbtkbBEzo}hPCvQ5(~MsBN5u##AX-U`&<26Cc~qf+HGnIO0=-1 z_9@GR>0(}>n1+7plLl}zlMJ*>dMN3=i6fEU1azus`XTjVcjhYhTCPs zG(P&yFhg)Kjrj^c_{d6N>wc^#D}nLqK2^Yk;Ql{Vz=Ys#KDK}}S{MAF7FU~@PaCv3 zxcD*sSWyQPf`K1FT0(H~Q-AY>%7W=1i}3P`)UJL@==ObUoldSotRXlCu;y(RKB3`t zTH2<<8w*k|`T90{d6OR~YNvg5sO>_33B~i0uivo@tSzP?yMSI-QCiL(t_ao_+cCEl zFxR->=d%Y%@RFSY0Mf{DA(p^OPuB--ET19-!0G8P?&Y#r%cwOPMRI`K(qpawc*-v0 z^JM)LW)+!6uSZJz(K&m;(8#pU*)!5*$PFX?kveQF8=oOGXF`0k?@?|`n6Db<$g?rAh%y%M|wz$aKLlf^x7Td`_2i_P)& z@e5cR1TSyU{7%RM5ElFKz^N@`u^(?dSoHb!Kzrk%-4@?adTVS~elT0P!kz8!zm~`I z|G@gN{KEzgd$s4P)oTORt>%WU7ku#fu;hcohvfmfE_(b2nRf%AG+fqf#&7AM){mB% z1>~q;PTxY@*Mwi&-n~Aut=xJJm%4A2#erv|PRAB63aaT@3Q}G8EPW5@ve~19>%GwL zM)Z`)$5-a`ah6gVxxYh2lglU$G6W0XtqM)-t-N#n^QxL7 zLS61_-_)kIV2s9&BLY6Qb*J1)o6Dd*$uci5FNme&*Opza{^5d3<=nfjr(w7nULT)K z#NQM;tpZ@$Cn3K0+>9u*7y;2$@)N+{7ViV%c$mLd7PX=BQJ|v_A>AbK%Z-Y=*#h0H z+GGnc)*FZJeHwO>gSd(W*mNRfE)D(S{VL3{XTgH$I|<-sPKT8a*lwPK(Ok2~+Smf{wgTbu(89|^91Edn}4Y4!|hEIAI^UN zRr*82<+cU-v>vDh%qF>kii_s*Gn>--eMP0=@9WI5 zF>U;-LyF6l=PwJ|i3w!NXo$h4*G|c>o35GsR!5MU3Y5?;`<8zd63W`7>xrX51s=rvFEUKO zDQj3E!km4ws!8C}hsXJ&$J z^3Y)!`oct}H>%K^L*RODi{nfzj-- z&BnHg(9KP5DE=xiSZJG(KO4N{_E*1zVfA__=;_Az3JmdPb-S$k38zgZNScgew_L(8 zs$FIbaZxKzZ~sP8=?&D&3clz{{NV9yne8L#@nsw0clbsxs(9REx458niwQB0Ztrf2 zx1%riAE1>8Yo_SlPycdVo3D~9yB6D|;5(+t5x7fPxB_K6qVmAg_pfh%n|9ysAYudC zKQ8K&&vzR_mQ`6J)`fK}U;P}3wJ|mhMaJn-C^nv)F#k1^QB5IR)}_@*gymdX-JJUQ>HZQNi{0 z+@WULv(@ndh$6k8ekvWX0>oPp$~@K)5gU(;E@a9l>7vCdp)kBs9DW-A(hpV0W0JX} zGEw&L?WKNIGLo+eDv)|^yQ_g^9H->^iAr)i1m@64r*9G_FhIG;`EuLO3Yy(P%S+@2 zW!s5wsgW}`@+JJe{ZIN2ZSOa#Zi|VHjTIk)5}SB>q1--YdyCP{%9-g3Wz?pSQ%(kE z`IBmwXXk};Eg@!%mbOX(+Ag$^-&|F#!bypVZRNTx>H;X6f)q6k5vA;Glgn-|D!g_h zLQSI)9MLY_?yy=TRJln6JN|}cYi`<5;QL*^Wfk#eD>G;o4!xY;Q_tOOP_QrF046u!=n4@`KZ%jy(hH3yytx8B#Y+N)f3JX1Q)UL?iBqWGc z%q$oCZRmXd6Hz*}vZ*l(P19$4M(;A@@M|ga!%M)2({mh%;m)=dYl*D z-?xb#j?HX7-0aYWnQG_{-!!YU6C4aTex&$@Ql|F~1LA)DBjLy&3U_KM-v`KM%IM1G zUMHsoEzbK`BwaB3^1$!~eU`Vy;>l}i?=N58V}H15@|uwMT`0lXZ$pMG;rqv2G~tEA zqDNNyuIrH4*E9UtWs)kP@N~C##B=pAYS^`|ylKhEy`#FZnBI;v=RI}1LI{~Ml`;$A zI}{ss$M7qDU#v^`0b#__A3-%#$k7GG zJRNEZnbAKc1J zG=7}PnmlfO#Utf*eB#<7Z0-aO`Qll1TDQp_ly1tj{e?0y7ZH}IR;D)qVWY_?Iaw#v z9OaV?vW(+Fa==5HfrVF|GpJ}~$G=En)ww!3YUoiFSE3x#bfJfNNi}}X!WY#6c50Tb9MLOkT+aLQtQzt1^UysGt0Orf$FZ*ie zQn7b~#3swK6|6$t%k1K1P1Gri_VWu0WJKrR-#bgs(Ia(7%=UbZ!#=q3#=7Ziid3q0 z$Uz37Xj`n-W^shC!iTRsEDadJh!5zlurx1^d4}9;_;bx67m6hmusYsIs1|+oTGz$T z@}4~E1G1>p$-r2;Lk@l!|5Ne;9X3*?7&~E7eY>^qLtb0lq8uW3DuS%<%DkE!Aago|;r0&M9ulBE+W=u=XH@i@= z<6^;0uuaU>cMRJSzhmdOLMW3mV}ELo4k9U|2dUs9zBqUiOX{imvg<6*By&q&c$>J7 z%&k!f+hsEE*N(E*foi#zc{=}0!ZB;_QBUqR1TeSQK>^3sszOXGc385WFq9!bRyJ|u zK1W`(q41M7Rt+L9L6C^c5k-0hIEWcs!9w)sbde)@lylVT^JM6*j@Da;K=2#r5oH)+gRvRDO))eP=uiUktS5ISj@g$5X$Fy8F@}V znXn`sc&}{RV%;Q`&BY6mX6*_Ja17{_;$$7y8hcW-x~63 z&fm*69*j8KD^lqH-z^+D|2F-8KOo*s2r zR0=7t`d{3GHjput)m@kZNs|d=VAN-X3TiCh!08jVOjCrTI7*EJcHzhbqtO9Nz+D-& zL^i4nWX(#lULyiNbCVo}a#?{yS(QMtDGx>%lP+2>%)#BtqJaocEJe+j+7eb3TF@qD z8>!fD3!PgR3C>gwTdmRrrK6n@iWVsnBs-<p@5FfY+SW!J%fO(E<|#I=QhwKRh(QI7#m`wx#GYTed-jcgxmQ=NaVx{?fyK zz!BF?$@wkbSoFLpcCx#4hV7RWvt$?0#LT${r56?yur{o?-Lv?`o_B;VKfu?Y*ramT zn}3GyFX&s|J8}BLdI^7w>i5}*a(F+Nl(exm=U(1TY|XD`2TPxiS7A~1l5&l<`z z;!Dz1B&aW_8R{0H?1a%s&=la?|VbdtK?k9OiySVIbS90LD*Jdxdu3NOI`fi zjkZfPt)RJD?xIqP<56l>xnv;ye& z%vsb?wP~1W^Id`~&{4nHK%23Kf2ZP&XMjVrTWp#_VA8$((Fp14T6&Y>hSBTDxaM-WeYiqN*HTUhs zUuFFGY$R|rWT$&uZ|k9R9dG$R;^zvHQlL+!ltH=-VgV-F1P?ey{$@UiUTMNXd;9W^zA#njn{&cUA`XBci+JACiqJQvM z^kMM_(g)oS`X3Na$0x@pZr+}onYq$=#^T~pze4qArPGMu>pl;JyR-HMlu0N4ly}L3 zeX`pQxOOxdTyp+A$_8D5(gjk?k}2AgLScp01wZ+R0RTh$^1GG~`O!0HM4qS09Q*|LS^?})0*)^*j#${JDs1j|=5kTI z0+LZ;ztEG<=ScBXPaG?lT@YP-Q4KL#2o8L;`UWeo*SHu`{(dyN`YvrYyGAyIO-mdQo7PYguAg&1EXtE*Za-0>QfFQjHIWAI08FVV}^4@~& z!Lqp0YDZ&)t4z_L1Ffq!u2QsY?iL=XHXYk!js;HO*l4j~1lg^jwa5fBBt*MW?Q6Y9 zhhvHYc<;hPprl-8BlN6LU%|DfK;X@a9#{B*jx^n*M>hz*+8#ilSO^TbAltinP`!EN zL7V8A<*W9^y^>7V)7*q-jp6HxvjMtudD4QBu1%rX!v{=l0=Xs9SePrKLSB1lfyzZZ zaWx16cmhXK(U)_BGwMZX%t>P!_)qkV;T^+Twv^_P5IMrn@$I83Ta`FZW?R^vxWj-{ z1@s)$S%$kQSTlW+jBP|GU=+a8)05l6#8MASQ@UGKx$u9QpeZtt&MiZqW5uDULt!AR z8yh3wQq0xMz_u`41(`0V6PHX0ZvnCia2TrK80t5MGvu|7Z!5-({Npr;7!N?wxK|MX z2E@R3`6SSF>&K(lAR&A?C|$dKDF7J#%X)xCqk)W#y}%;w{h8)UV`&KSA?k!#G_vr^ z)7%imwo6Yg*BZ+T{+yVK2q8)@>r+t5cK7pv?g*-+rlv}jWs8M*2&CcN;hsUf#0k}8 z@E{_?4Wm)>o^4(-SYgsM9akZaQpJZ~tB6nvwkdG(&^~F}bQ7RRGvYAadgg9wmc;_; z(MpYDd=$@iE{KwzZJM01IE-J%1y41?e%scV0s|#@7lT-__$>N=)ce|FxaQT_JC!2F2xb7kRQF**9E?H;VCyq`P!|;BP=Mnslx>+QObTcUav)n) zwixV2g9$De7%@Dj@wwnPEn?6BIa_<_yWkGJ)e#8}_lMoGA9erwCE39PG;Y@;G$F>> zHV(%3<7oIvhdBP~(Oqwjo?>D;yN%9c3VZ_CIi=g5J)gbV+&->h0j!$%MNx;M+NZttbd5jv>V2`VE~ z)?o;5>G*U-X7}S2jwvS+1f^w_6fF;g(ao8YQlqp2w)1q{;ug;sl|Zn$sUCNg(->7j zil;Y~PrZrINn>qDhn+`QG%G3C0d(y`7@J4-O2G-(nSDqB?5^*&dW zYwT2&{grj`G>q~}eRjH>+m1kxoDVy}Hte9rKt^7U@lC+s5?T*Yy_5xN5fvDK?-&Zg znBMe09loIGG8$OBlySJ$TWyMvnik)?I^|0#&dMt+h^|QH8!R;#^77SNC zQd?0JxC_lvc0y`H5)hd!SS5v|=wt@#JR1BZd2btXu(RvWxwDd(caDGgMYn4Uof50T za|ut%^NOhXZOsPsQ*la#%Gh}RKZ&`%h)E3e4za5ja0x95@++i)+fD1U4)}jZ#1fb4ki&?9I73zqK@`)R`?327kwxYAmdJwBaJ<53AiHa;A(T2M)9v=FL1WJtc{|6B!bM#)l#9 zb>$4TPOEl-TLY6k?iz-rM@TY_XzR9A8sqcJJjS&ut~D1gUoSI2i2Qw*U%hYXtqbg>_}&a7?o!dk-9SU?#fNNz z@F!}w5102UQO>MDH}siW%=-akSLH;9gqe)@Nc0>}1l9wbT+z@lGK>9u z+JLGUC~`~oW>1(=apY=D*=ST?jR1|xH?T2yP5JSg&t?4?I0l1UJoUqg*9ID0e!4F= zh=gh}QYyr?)qbK)lnrzI<&rU45!S$pgT!zGLDH{3>uAsLR#ctifaN%F?vty5wmhi z^??{;z)HgGbJjfm+yM3Ki98eszejP&SApsTn{YUQJ^6F25<^CoOo%^!oP9z~?;$KTFOQ$e&YJ$39v(`EG6o zbbj~CNuMJ{Divy89W~C+nlbg$;fx$ELnsam80Y9W5=}R&>#|jK@v?BiM0o{$%7}nR z5#TL{0C>1(8qpDu!!bm~>BojarVA02VL%#YR>yH59_urCiVn*MYq+X!PTxrKWYu5{ zXWW1S@bcCM1cm@|qu~Gi0NY#S2%q2O_Z$p8#QTC^2oy&wVtAJH867nE5uDGvy$>rM zIY8WBxZwR3WUq1$3jyj*SOB%JJ}}2zknCOeR8l*a`E}z&-OlLYcNW)&eXRFlhqsfT z7=M4pGHP@0u`T;sis4^`@W~bc{<9{_X*01hK0Zi4Ai9rj?-F63MBanecglCvz&q`ND4}3p(?xywH zKR4+*P2>-~W!~zH0MQj4Z`{Dy#J zMse0VKR))z*hFg7M;dcZ*LOrc_*@kTq^e29bNC>;JUAhm3^a1#LvlO4V}G&MYrJpv zqUaz^+A3{&xNDgRh?uNH=F_NqP;ZnJ2@JtB0bG#0|M8IB7a8lK)~%;^{PEf{$@KfT zzg#+$x@$DN7xpIrFfh+Lg0znU zJNN5|ATSt3W-;65An>dPcB|g+TYKQj#2efFixZ}puMWiw6nuZ}am(S#)rVImO}V~0 z6l>2~T`~7QoEc88 zudvLS9>ZtS9C}W{{w&CDVwqqWO7wkVhZTgn5x)wqb)vtP#cGA|*Llr zT5mJ`=IUer`r1W%&W@B6e^2vW5_r?~R;Os`3omQ!fZ@j)8By|Ms%5;$oAyM~0xy%aD{p47WZ+%>?JQ(5 z+r0#Swm*M?fKukq4{82kgw`>L_ZNriw3KB;`B5QwW_#X#N2cx#AJ|@ntN$5x0Dr#m z{T9NvQa!kD3HO9KlMpqS)H8oc#hnm_iO% zutq!F3&B{6)RhT=*DiOo&z*7->GBBLuHZn!VR54Nn3gS2ZBAVsjYzAOGgt8Qt4HZJ z31->s&A^14?=4WHmrP|*;R^~vE`=Z#$Jn|G@)V1OtxSe1+c*n6c12sl3vD4ipOOHt z2vY0NL$I2|E+>k02bAlPq!@TiztXoE>CF8~YY1*5fhmy@*}Y9Jtjj!WL3J+zXl$oASm8Gg58`3^r; zioT_e`Ce~#qd~2=uMN^ooNb`Q-}ef8))m24i=qK%+mjKJg9zwjN31|98!7b|3hKKf zo5ebHI*zb*mx|ymXOyaK3s3RqrZ4%x?2+SNJEoS?r3QHa6Joc6$=XV7P#=s%Sf$3T zL8>YgUT7@@C9)P9uaWAr@R%&ROi&0>a9a2U$@?A4Kl1}R(~f`bDO+GIMdG!ub^Z { const { act, data } = useBackend(); const { load, + loadName, autoReturn, autoPickup, reportDelivery, @@ -85,28 +88,6 @@ const MuleControls = (props) => { -
- act('autored')}> - Auto-Return - -
- act('autopick')}> - Auto-Pickup - -
- act('report')}> - Report-Delivery - -
- {!!allowPossession && ( - act('toggle_personality')} - > - Download Personality - - )} -
@@ -136,6 +117,28 @@ const MuleControls = (props) => {
+
+ act('autored')}> + Auto-Return + +
+ act('autopick')}> + Auto-Pickup + +
+ act('report')}> + Report-Delivery + +
+ {!!allowPossession && ( + act('toggle_personality')} + > + Download Personality + + )} +
); }; @@ -146,6 +149,7 @@ export const Mule = (props) => { powerStatus, cellPercent, load, + loadName, mode, modeStatus, locked, @@ -155,7 +159,7 @@ export const Mule = (props) => { const mulebotLocked = locked && !siliconUser; return ( - +
{ value={cellPercent ? cellPercent / 100 : 0} color={cellPercent ? 'good' : 'bad'} /> - + {mode} - - - - - {load || 'None'} + {load ? capitalize(loadName ?? '') : 'None'} From 2a84c1de48861a83d4a6fe6790f893b35939d9d1 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:47:23 +0000 Subject: [PATCH 011/357] Automatic changelog for PR #97198 [ci skip] --- html/changelogs/AutoChangeLog-pr-97198.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97198.yml diff --git a/html/changelogs/AutoChangeLog-pr-97198.yml b/html/changelogs/AutoChangeLog-pr-97198.yml new file mode 100644 index 000000000000..d9a95c808fba --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97198.yml @@ -0,0 +1,6 @@ +author: "ArcaneMusic" +delete-after: True +changes: + - qol: "Tweaked the TGUi on the mulebot in order to read the name of the loaded item more easily." + - qol: "The Mulebot now has more visual feedback when items are being loaded onto them." + - sound: "Mulebots now have sound effects when loading and unloading." \ No newline at end of file From ecc7ebbf6d002ca9774d44cc9333ecfb4b15c9a0 Mon Sep 17 00:00:00 2001 From: Roxy <75404941+TealSeer@users.noreply.github.com> Date: Sun, 2 Aug 2026 23:00:08 -0400 Subject: [PATCH 012/357] Remove Compile BT Trees vscode task (#97302) ## About The Pull Request As the title says ## Why It's Good For The Game Juke handles running this if it's necessary, having it as a vscode task that Build All depends on is redundant ## Changelog N/A --- .vscode/tasks.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d56cb0e14d1d..4d83de733540 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,16 +1,6 @@ { "version": "2.0.0", "tasks": [ - { - "type": "shell", - "command": "tools/bootstrap/python tools/build_bt.py", - "windows": { - "command": ".\\tools\\bootstrap\\python.bat tools/build_bt.py" - }, - "group": "build", - "presentation": { "reveal": "silent", "panel": "shared" }, - "label": "Compile BT Trees" - }, { "type": "process", "command": "tools/build/build.sh", @@ -27,7 +17,7 @@ "kind": "build", "isDefault": true }, - "dependsOn": ["dm: reparse", "Compile BT Trees"], + "dependsOn": "dm: reparse", "label": "Build All" }, { From dae9da928b9056a13ab8d288216b775bbb27643a Mon Sep 17 00:00:00 2001 From: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:01:01 -0500 Subject: [PATCH 013/357] The maplint only runs on maps that have actually had changes made (#97284) ## About The Pull Request Ok, so like I suddenly remembered #95758 and thought to myself, "Why are we running the maplint on all maps even if only one was changed?", so I came here to rectify that. The maplint now only runs on maps which have had changes applied to them. If either of the tooling scripts are edited in any which way, all maps are ran, like you would expect. --- .github/workflows/run_linters.yml | 19 +++++++++---- tools/maplint/source/__main__.py | 3 +++ tools/mapmerge2/dmm.py | 1 - tools/mapmerge2/dmm_test.py | 44 +++++++++++++++++++------------ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/.github/workflows/run_linters.yml b/.github/workflows/run_linters.yml index c7f2e60a1f1d..0493f3fb1dc7 100644 --- a/.github/workflows/run_linters.yml +++ b/.github/workflows/run_linters.yml @@ -86,16 +86,25 @@ jobs: id: map-filter uses: dorny/paths-filter@v4 with: + list-files: shell filters: | - run_maps: - - '**.dmm' + tool_edits: - 'tools/maplint/**' - 'tools/mapmerge2/**' + map_edits: + - '**.dmm' + - name: Run Map Checks - if: steps.linter-setup.conclusion == 'success' && !cancelled() && steps.map-filter.outputs.run_maps == 'true' + if: steps.linter-setup.conclusion == 'success' && !cancelled() && (steps.map-filter.outputs.tool_edits == 'true' || steps.map-filter.outputs.map_edits == 'true') run: | - tools/bootstrap/python -m mapmerge2.dmm_test - tools/bootstrap/python -m tools.maplint.source + if [ "${{ steps.map-filter.outputs.tool_edits }}" = "true" ]; then + MAPS=$(find . -type f -name "*.dmm") + else + MAPS="${{ steps.map-filter.outputs.map_edits_files }}" + fi + + tools/bootstrap/python -m mapmerge2.dmm_test $MAPS + tools/bootstrap/python -m tools.maplint.source $MAPS - name: Check Cutter if: steps.linter-setup.conclusion == 'success' && !cancelled() run: tools/bootstrap/python -m tools.icon_cutter.check diff --git a/tools/maplint/source/__main__.py b/tools/maplint/source/__main__.py index dcb575e99bd5..b53cc822425d 100644 --- a/tools/maplint/source/__main__.py +++ b/tools/maplint/source/__main__.py @@ -1,5 +1,6 @@ import argparse import glob +import os import pathlib import traceback import yaml @@ -76,6 +77,8 @@ def main(args): any_failed = True for map_filename in (args.maps or glob.glob("_maps/**/*.dmm", recursive = True)): + if not os.path.isfile(map_filename) or not map_filename.endswith('.dmm'): + continue print(map_filename, end = " ") success = True diff --git a/tools/mapmerge2/dmm.py b/tools/mapmerge2/dmm.py index bc12a39b3556..a8e87b0dfcb1 100644 --- a/tools/mapmerge2/dmm.py +++ b/tools/mapmerge2/dmm.py @@ -193,7 +193,6 @@ def parse_map_atom(atom): vars = {} in_string = False - in_name = False escaping = False current_name = '' current = '' diff --git a/tools/mapmerge2/dmm_test.py b/tools/mapmerge2/dmm_test.py index 02f5383f2abb..e36ef82a7b6e 100644 --- a/tools/mapmerge2/dmm_test.py +++ b/tools/mapmerge2/dmm_test.py @@ -2,38 +2,48 @@ import sys from .dmm import * - -def _self_test(): - # test: can we load every DMM in the tree - count = 0 +def _test_file(fullpath): + try: + DMM.from_file(fullpath) + except Exception: + print('Failed on:', fullpath) + raise + +def _get_all_dmm_files(): for dirpath, dirnames, filenames in os.walk('.'): if '.git' in dirnames: dirnames.remove('.git') for filename in filenames: if filename.endswith('.dmm'): - fullpath = os.path.join(dirpath, filename) - try: - DMM.from_file(fullpath) - except Exception: - print('Failed on:', fullpath) - raise - count += 1 + yield os.path.join(dirpath, filename) - print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmm files") +def _run_test(file_paths=None): + count = 0 + + if not file_paths: + file_paths = _get_all_dmm_files() + for fullpath in file_paths: + if not os.path.isfile(fullpath): + continue + + _test_file(fullpath) + count += 1 + + print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmm files") def _usage(): print(f"Usage:") print(f" tools{os.sep}bootstrap{os.sep}python -m {__spec__.name}") exit(1) - def _main(): - if len(sys.argv) == 1: - return _self_test() - - return _usage() + args = sys.argv[1:] + if '-help' in args: + _usage() + return + _run_test(args) if __name__ == '__main__': _main() From 823e173cd7d3e7cb43fafc62f7725a9ddf7aae4d Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Aug 2026 22:04:36 -0500 Subject: [PATCH 014/357] Add spawn text overlay for players at roundstart or latejoin (#97289) ## About The Pull Request This ports the player spawn info maptext code from [CM](https://github.com/cmss13-devs/cmss13/blob/b035a0614fdba7526abd31258426ea35f4fe8a34/code/modules/maptext_alerts/text_blurbs.dm#L17) and [Nebula](https://github.com/NebulaSS13/Nebula/blob/064044947c69a7231c881fb7566adfd17125e100/code/controllers/subsystems/jobs.dm#L584-L619) and applies it to TG when a player spawns. (excludes observers) The formatting is as follows: ``` [mob_name] - [job_title] [station_name] [area_name] [time_date] ``` ## Why It's Good For The Game It looks cool and polished. dreamseeker_SbxXyIUYN0 ## Changelog :cl: add: Add spawn text overlay when a player spawns. (excludes observers) /:cl: --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com> --- code/controllers/subsystem/ticker.dm | 2 + code/modules/client/spawn_text_overlay.dm | 46 +++++++++++++++++++++++ code/modules/jobs/job_types/_job.dm | 1 + tgstation.dme | 1 + 4 files changed, 50 insertions(+) create mode 100644 code/modules/client/spawn_text_overlay.dm diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index e1b07fb6b775..c02e491ea354 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -587,6 +587,8 @@ SUBSYSTEM_DEF(ticker) var/atom/movable/screen/splash/fade_out = new(null, null, living.client, TRUE) fade_out.fade(TRUE) living.client.init_verbs() + living.client.show_spawn_text_overlay() + livings += living if(livings.len) addtimer(CALLBACK(src, PROC_REF(release_characters), livings), 3 SECONDS, TIMER_CLIENT_TIME) diff --git a/code/modules/client/spawn_text_overlay.dm b/code/modules/client/spawn_text_overlay.dm new file mode 100644 index 000000000000..cbc18664284d --- /dev/null +++ b/code/modules/client/spawn_text_overlay.dm @@ -0,0 +1,46 @@ +/// Displays a typewriter-style spawn text overlay that includes station/area/job name and time +/client/proc/show_spawn_text_overlay(duration = 5 SECONDS) + set waitfor = FALSE + + var/mob_name = mob.name + var/job_title = mob.mind?.assigned_role.title || "Unknown" + + var/station_name = station_name() + var/area_name = get_area_name(mob, format_text = TRUE) || "Unknown Location" + var/time_date = server_timestamp(format = "YYYY-MM-DD hh:mm:ss", ic_time = TRUE) + + var/text = {" + [mob_name] - [job_title] + [station_name] + [area_name] + [time_date] + "} + text = uppertext(text) + + var/atom/movable/screen/spawn_text = new() + spawn_text.maptext_height = 64 + spawn_text.maptext_width = 512 + spawn_text.layer = FLY_LAYER + spawn_text.plane = HUD_PLANE + spawn_text.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA + spawn_text.screen_loc = "LEFT+1,BOTTOM+2" + + screen += spawn_text + animate(spawn_text, alpha = 255, time = 1 SECONDS) + + for(var/i in 1 to length_char(text) + 1) + if(QDELETED(spawn_text) || QDELETED(src)) + return + spawn_text.maptext = MAPTEXT_PIXELLARI(copytext_char(text, 1, i)) + sleep(1) + + addtimer(CALLBACK(src, PROC_REF(fade_spawn_text_overlay), src, spawn_text), duration) + +/client/proc/fade_spawn_text_overlay(client/player_client, atom/movable/screen/spawn_text) + if(QDELETED(spawn_text)) + return + animate(spawn_text, alpha = 0, time = 0.5 SECONDS) + sleep(5) + if(player_client && !QDELETED(spawn_text)) + player_client.screen -= spawn_text + qdel(spawn_text) diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 57a87c22f186..b6e583c8ba76 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -649,6 +649,7 @@ /datum/job/proc/after_latejoin_spawn(mob/living/spawning) SHOULD_CALL_PARENT(TRUE) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_JOB_AFTER_LATEJOIN_SPAWN, src, spawning) + spawning.client.show_spawn_text_overlay() /// Called when a mob that has this job is admin respawned /datum/job/proc/on_respawn(mob/new_character) diff --git a/tgstation.dme b/tgstation.dme index 86230499a000..5ce5f7970053 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -4210,6 +4210,7 @@ #include "code\modules\client\preferences.dm" #include "code\modules\client\preferences_menu.dm" #include "code\modules\client\preferences_savefile.dm" +#include "code\modules\client\spawn_text_overlay.dm" #include "code\modules\client\preferences\_preference.dm" #include "code\modules\client\preferences\accessibility.dm" #include "code\modules\client\preferences\addict.dm" From 00ef5a00e3ef4e53200a584ef45699e22df223e0 Mon Sep 17 00:00:00 2001 From: mlk <99420088+MelokG000@users.noreply.github.com> Date: Mon, 3 Aug 2026 06:04:52 +0300 Subject: [PATCH 015/357] To far away -> Too far away (#97301) ## About The Pull Request spellcheck in hilbert hotel analyzer ## Why It's Good For The Game spellcheck ## Changelog :cl: spellcheck: "too" far away /:cl: --- code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm index b96df88a3917..e56f09ffb078 100644 --- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm +++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm @@ -580,7 +580,7 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999)) if(!istype(interacting_with, /obj/item/hilbertshotel)) return ..() if(!interacting_with.IsReachableBy(user)) - to_chat(user, span_warning("It's to far away to scan!")) + to_chat(user, span_warning("It's too far away to scan!")) return ITEM_INTERACT_BLOCKING var/obj/item/hilbertshotel/sphere = interacting_with if(length(sphere.activeRooms)) From 4c669564e294ddf139077cb9c16c8e0b0a317ec0 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 03:04:56 +0000 Subject: [PATCH 016/357] Automatic changelog for PR #97289 [ci skip] --- html/changelogs/AutoChangeLog-pr-97289.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97289.yml diff --git a/html/changelogs/AutoChangeLog-pr-97289.yml b/html/changelogs/AutoChangeLog-pr-97289.yml new file mode 100644 index 000000000000..841f3dba0596 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97289.yml @@ -0,0 +1,4 @@ +author: "timothymtorres" +delete-after: True +changes: + - rscadd: "Add spawn text overlay when a player spawns. (excludes observers)" \ No newline at end of file From 2063a4cbd54853a94444eb76206a2e226ac16f32 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:05:10 -0500 Subject: [PATCH 017/357] Ensure law module updates get put in silicon log as well (#97280) ## About The Pull Request Lost this by accident ## Changelog :cl: Melbert admin: Law text is in silicon log again /:cl: --- code/_globalvars/logging.dm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm index 5f38afdd86e2..006371a44670 100644 --- a/code/_globalvars/logging.dm +++ b/code/_globalvars/logging.dm @@ -80,7 +80,11 @@ GLOBAL_PROTECT(lawchanges) /// Adds something to the law change log /proc/log_law_change(mob/living/changer, log_message) + if(isnull(changer)) + log_message = capitalize(log_message) + GLOB.lawchanges += "[round_timestamp()] : [changer ? key_name(changer) : "(No mob)"] [log_message]" + log_silicon("Law change: [changer ? key_name(changer) : "(No mob)"] [log_message]") #undef DECLARE_LOG #undef DECLARE_LOG_NAMED From f6e688e7497ea00615c20879f81cfadd330ff354 Mon Sep 17 00:00:00 2001 From: "tgstation-ci[bot]" <179393467+tgstation-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 03:05:34 +0000 Subject: [PATCH 018/357] Automatic changelog for PR #97280 [ci skip] --- html/changelogs/AutoChangeLog-pr-97280.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-97280.yml diff --git a/html/changelogs/AutoChangeLog-pr-97280.yml b/html/changelogs/AutoChangeLog-pr-97280.yml new file mode 100644 index 000000000000..9647d0bc5044 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-97280.yml @@ -0,0 +1,4 @@ +author: "Melbert" +delete-after: True +changes: + - admin: "Law text is in silicon log again" \ No newline at end of file From 7708d82efab10e3f80b15dd32143b86f2bbf37f8 Mon Sep 17 00:00:00 2001 From: PapaMichael Date: Sun, 2 Aug 2026 23:13:13 -0400 Subject: [PATCH 019/357] Adds Character and Job Specific chat highlights (#97288) ## About The Pull Request Adds the ability to have a text highlight only apply when a specific character and/or job is played. Video of UI: https://files.catbox.moe/4i6dh4.mp4 ## Why It's Good For The Game 99% of the time I open this menu I'm just toggling enabled depending on the character and job I'm playing ## Changelog :cl: PapaMichael add: You can now make text highlights only apply for specific characters or specific jobs /:cl: --- code/datums/mind/_mind.dm | 1 + code/modules/mob/login.dm | 3 + code/modules/tgui_panel/tgui_panel.dm | 14 +++ tgui/packages/tgui-panel/chat/renderer.tsx | 44 ++++++- .../tgui-panel/events/handlers/player.ts | 27 +++++ tgui/packages/tgui-panel/events/listeners.ts | 2 + tgui/packages/tgui-panel/game/atoms.ts | 9 ++ .../tgui-panel/settings/TextHighlight.tsx | 107 ++++++++++++++++-- tgui/packages/tgui-panel/settings/atoms.ts | 2 + .../packages/tgui-panel/settings/migration.ts | 20 ++-- tgui/packages/tgui-panel/settings/types.ts | 4 + 11 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 tgui/packages/tgui-panel/events/handlers/player.ts diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm index 807b1ab15550..d757579cf49f 100644 --- a/code/datums/mind/_mind.dm +++ b/code/datums/mind/_mind.dm @@ -509,6 +509,7 @@ assigned_role = new_role if(!isnull(current)) SEND_SIGNAL(current, COMSIG_MOB_MIND_SET_ROLE, new_role) + current.client?.tgui_panel?.send_player_info() ///Sets your holy role, giving/taking away traits related to if you're gaining/losing it. /datum/mind/proc/set_holy_role(new_holy_role) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 6c32c7a0e790..9e231b00fe89 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -125,6 +125,9 @@ //Check if they should have a stat panel, after they deadmined. client.set_stat_panel() + //Update the chat panel's job/character info for conditional highlights. + client.tgui_panel?.send_player_info() + log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP) log_mob_tag("TAG: [tag] NEW OWNER: [key_name(src)]") SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client) diff --git a/code/modules/tgui_panel/tgui_panel.dm b/code/modules/tgui_panel/tgui_panel.dm index 8b1a366c0a05..c7df6b20fe79 100644 --- a/code/modules/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui_panel/tgui_panel.dm @@ -86,6 +86,7 @@ ), ), )) + send_player_info() return TRUE if(type == "audio/setAdminMusicVolume") @@ -115,6 +116,19 @@ /datum/tgui_panel/proc/send_roundrestart() window.send_message("roundrestart") +/** + * public + * + * Sends the client's current job, character and saved character names, + * used for conditional chat highlights. + */ +/datum/tgui_panel/proc/send_player_info() + window.send_message("player/set", list( + "job" = client.mob?.mind?.assigned_role?.title, + "character" = client.prefs?.read_preference(/datum/preference/name/real_name), + "characters" = client.prefs?.create_character_profiles(), + )) + /** * private * diff --git a/tgui/packages/tgui-panel/chat/renderer.tsx b/tgui/packages/tgui-panel/chat/renderer.tsx index 8c28e1942dc5..26207ed1cbd2 100644 --- a/tgui/packages/tgui-panel/chat/renderer.tsx +++ b/tgui/packages/tgui-panel/chat/renderer.tsx @@ -117,6 +117,8 @@ class ChatRenderer { scrollTracking: boolean; lastScrollHeight: number; highlightParsers: Array | null; + currentJob: string | null; + currentCharacter: string | null; handleScroll: (type: any) => void; constructor() { @@ -130,6 +132,8 @@ class ChatRenderer { // Scroll handler this.scrollNode = null; + this.currentJob = null; + this.currentCharacter = null; this.scrollTracking = true; this.lastScrollHeight = 0; this.handleScroll = (evt) => { @@ -193,6 +197,35 @@ class ChatRenderer { } } + setJob(title) { + this.currentJob = + typeof title === 'string' && title ? title.trim().toLowerCase() : null; + } + + setCharacter(name) { + this.currentCharacter = + typeof name === 'string' && name ? name.trim().toLowerCase() : null; + } + + matchesFilters(parser) { + if ( + parser.jobs?.length && + !(this.currentJob && parser.jobs.includes(this.currentJob)) + ) { + return false; + } + if ( + parser.characters?.length && + !( + this.currentCharacter && + parser.characters.includes(this.currentCharacter) + ) + ) { + return false; + } + return true; + } + setHighlight(highlightSettings, highlightSettingById) { this.highlightParsers = null; if (!highlightSettings) { @@ -206,6 +239,13 @@ class ChatRenderer { const matchWord = setting.matchWord; const matchCase = setting.matchCase; const enabled = setting.enabled; + const jobs = String(setting.jobFilter || '') + .split(',') + .map((str) => str.trim().toLowerCase()) + .filter(Boolean); + const characters = ( + Array.isArray(setting.characterFilter) ? setting.characterFilter : [] + ).map((str) => String(str).trim().toLowerCase()); const allowedRegex = /^[a-zа-яё0-9_\-$/^[\s\]\\]+$/gi; const regexEscapeCharacters = /[!#$%^&*)(+=.<>{}[\]:;'"|~`_\-\\/]/g; const lines = String(text) @@ -281,6 +321,8 @@ class ChatRenderer { highlightRegex, highlightColor, highlightWholeMessage, + jobs, + characters, }); }); } @@ -448,7 +490,7 @@ class ChatRenderer { // Highlight text if (!message.avoidHighlighting && this.highlightParsers) { this.highlightParsers - .filter((parser) => parser.enabled) + .filter((parser) => parser.enabled && this.matchesFilters(parser)) .forEach((parser) => { const highlighted = highlightNode( node, diff --git a/tgui/packages/tgui-panel/events/handlers/player.ts b/tgui/packages/tgui-panel/events/handlers/player.ts new file mode 100644 index 000000000000..e85eb1c0d79e --- /dev/null +++ b/tgui/packages/tgui-panel/events/handlers/player.ts @@ -0,0 +1,27 @@ +import { chatRenderer } from '../../chat/renderer'; +import { + characterProfilesAtom, + currentCharacterAtom, + currentJobAtom, +} from '../../game/atoms'; +import { store } from '../store'; + +type PlayerSetPayload = { + job?: string | null; + character?: string | null; + characters?: (string | null)[] | null; +}; + +export function playerSet(payload: PlayerSetPayload) { + const job = payload?.job || null; + const character = payload?.character || null; + // Drop empty save slots and duplicates + const characters = [ + ...new Set((payload?.characters || []).filter(Boolean) as string[]), + ]; + store.set(currentJobAtom, job); + store.set(currentCharacterAtom, character); + store.set(characterProfilesAtom, characters); + chatRenderer.setJob(job); + chatRenderer.setCharacter(character); +} diff --git a/tgui/packages/tgui-panel/events/listeners.ts b/tgui/packages/tgui-panel/events/listeners.ts index 06e6a4f554ad..de4ffd220941 100644 --- a/tgui/packages/tgui-panel/events/listeners.ts +++ b/tgui/packages/tgui-panel/events/listeners.ts @@ -9,6 +9,7 @@ import { testTelemetryCommand, } from '../telemetry/handlers'; import { handleLoadAssets } from './handlers/assets'; +import { playerSet } from './handlers/player'; import { roundrestart } from './handlers/roundrestart'; const listeners = { @@ -17,6 +18,7 @@ const listeners = { 'audio/playMusic': playMusic, 'audio/stopMusic': stopMusic, 'chat/message': chatMessage, + 'player/set': playerSet, 'ping/reply': pingReply, 'ping/soft': pingSoft, roundrestart, diff --git a/tgui/packages/tgui-panel/game/atoms.ts b/tgui/packages/tgui-panel/game/atoms.ts index 148227074377..c3e13430cd83 100644 --- a/tgui/packages/tgui-panel/game/atoms.ts +++ b/tgui/packages/tgui-panel/game/atoms.ts @@ -4,6 +4,15 @@ import { CONNECTION_LOST_AFTER } from './constants'; export const roundRestartedAtAtom = atom(null); +/** Current job title of the player, as reported by the server */ +export const currentJobAtom = atom(null); + +/** Name of the character the player is currently playing */ +export const currentCharacterAtom = atom(null); + +/** Names of the player's saved characters */ +export const characterProfilesAtom = atom([]); + /** * Ticking clock atom. Only runs while something is subscribed to it. * (And only after we’ve had at least one ping, see connectionLostAtAtom.) diff --git a/tgui/packages/tgui-panel/settings/TextHighlight.tsx b/tgui/packages/tgui-panel/settings/TextHighlight.tsx index 2b352f5dae18..7a61a717b469 100644 --- a/tgui/packages/tgui-panel/settings/TextHighlight.tsx +++ b/tgui/packages/tgui-panel/settings/TextHighlight.tsx @@ -1,9 +1,11 @@ +import { useAtomValue } from 'jotai'; import { useMemo } from 'react'; import { Box, Button, ColorBox, Divider, + Floating, Icon, Input, Section, @@ -11,6 +13,7 @@ import { TextArea, } from 'tgui-core/components'; import { chatRenderer } from '../chat/renderer'; +import { characterProfilesAtom, currentCharacterAtom } from '../game/atoms'; import { WARN_AFTER_HIGHLIGHT_AMT } from './constants'; import { useHighlights } from './use-highlights'; @@ -92,7 +95,32 @@ function TextHighlightSetting(props) { highlightWholeMessage, matchWord, matchCase, + jobFilter, + characterFilter, } = highlightSettingById[id]; + const currentCharacter = useAtomValue(currentCharacterAtom); + const characterProfiles = useAtomValue(characterProfilesAtom); + const jobCount = jobFilter.split(',').filter((str) => str.trim()).length; + + // Known characters plus any selected names no longer in the save slots, + // so stale selections stay visible and can be unchecked + const selectableCharacters = useMemo(() => { + const known = new Set([...characterProfiles, ...characterFilter]); + if (currentCharacter) { + known.add(currentCharacter); + } + return [...known]; + }, [characterProfiles, characterFilter, currentCharacter]); + + function toggleCharacter(name: string): void { + const draft = characterFilter.includes(name) + ? characterFilter.filter((entry) => entry !== name) + : [...characterFilter, name]; + updateHighlight({ + id, + characterFilter: draft, + }); + } const highlightRegex = useMemo( () => extractRegex(highlightText), @@ -125,13 +153,6 @@ function TextHighlightSetting(props) { > Enabled - + + + + {selectableCharacters.map((name) => ( +
+ toggleCharacter(name)} + > + {name} + +
+ ))} + {selectableCharacters.length === 0 && ( + + No known characters yet. +
+ Join the game once to fill this list. +
+ )} + + } + > + + + +
+ + + updateHighlight({ + id, + jobFilter: value, + }) + } + /> + + } + > + + + + +
+ + + +