Repository: tgstation/tgstation
Pull request #90116: Refactors MODsuit slowdown calculations
Author: login: SmArtKar
Labels: name: Fix
description: Rewrites a bug so it appears in different circumstances
name: Refactor
description: Makes the code harder to read
Created: 2025-03-18T17:09:20Z | Merged: 2025-03-26T16:55:33Z


## About The Pull Request

MODsuits now calculate their slowdown by querying modules by sending a signal on themselves instead of having modules try and keep up with control unit's speed updates (which broke in a fabulous fashion after MODsuits were allowed to deploy by piece)
Also added a separate trait to prevent objects from being speed potion-ed to combat stabilized red crossbreed issues, and removed a duplicate list helper (40 lines down in the same file lol)

Closes #89979
Closes #90036

## Changelog
:cl:
fix: MODsuits should now be affected by stabilized red crossbreeds
fix: MODsuit slowdowns should no longer behave weirdly with ash accretion/magboots/armor booster modules.
refactor: Refactored MODsuit slowdown calculations to be query-based instead of modules directly modifying part speed values.
/:cl:


Linked issue #89979: Magnetic Stabilizer module for MODsuits (paired with anything else that makes you faster) allows you to easily reach breakneck speeds
### Client Version:

515.1647

### Issue Summary:

Ever wanted to become A-Train? Well, paired with stabilized light-pink extract, a speed potion (optional but preferable) for the MODsuit and said bug, you can do just that!

https://github.com/user-attachments/assets/fd6e8995-5387-4c4f-8523-b82be017f960

No need for chems, no heart/brain damage would be accumulated for this whatsoever... (if you don't use them that is.)

### Round ID:

_No response_

### Test Merge Information:

_No response_

### Reproduction Steps:

1. Acquire stabilized light pink extract, speed potion (optional), and a MODsuit with magnetic stabilizer (magboots) module
2 (optional). Apply speed potion onto the MODsuit
3. Deploy the boots and activate the MODsuit, also activate the magboots module
4. Retract the boots without turning the MODsuit off
5. Voila, you can't stop!
Linked issue comments: user: login: SmArtKar
created_at: 2025-03-11T12:31:15Z
body: Also applies to ash accretion module btw, caused by modules adjusting the back piece's slowdown, which gets set to 0 by the potion.
author_association: MEMBER
user: login: thetaro
created_at: 2025-03-11T16:04:37Z
body: keep this as a feature this is fucking peak (a belt slot modsuit can make you go EVEN FASTER)
author_association: NONE

Diff:
diff --git a/code/__DEFINES/dcs/signals/signals_mod.dm b/code/__DEFINES/dcs/signals/signals_mod.dm
index 70c434adae3731..6cc6832beb65f3 100644
--- a/code/__DEFINES/dcs/signals/signals_mod.dm
+++ b/code/__DEFINES/dcs/signals/signals_mod.dm
@@ -48,3 +48,5 @@
 #define COMSIG_MODULE_GENERATE_WORN_OVERLAY "mod_module_generate_worn_overlay"
 /// Called when the MOD control unit fetches its visor icon
 #define COMSIG_MOD_GET_VISOR_OVERLAY "mod_get_visor_overlay"
+/// Called when the MOD control unit updates its parts speed
+#define COMSIG_MOD_UPDATE_SPEED "mod_update_speed"
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 3c92c3c4cdbbd3..0d5c7ef9721a38 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -1469,4 +1469,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
 ///trait given to mobs that are easy to tame through mounting
 #define TRAIT_MOB_EASY_TO_MOUNT "easy_to_mount"
 
+/// Prevents items from being speed potion-ed, but allows their speed to be altered in other ways
+#define TRAIT_NO_SPEED_POTION "no_speed_potion"
+
 // END TRAIT DEFINES
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index 4fbf3b4b9688f2..d1c6e6bb7d6a14 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -946,13 +946,6 @@
 		UNTYPED_LIST_ADD(keys, key)
 	return keys
 
-///Gets the total amount of everything in the associative list.
-/proc/assoc_value_sum(list/input)
-	var/keys = 0
-	for(var/key in input)
-		keys += input[key]
-	return keys
-
 ///compare two lists, returns TRUE if they are the same
 /proc/compare_list(list/l,list/d)
 	if(!islist(l) || !islist(d))
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 84ebeb2d298306..4cda4eb50ab1eb 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -627,6 +627,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
 		"TRAIT_NEEDS_TWO_HANDS" = TRAIT_NEEDS_TWO_HANDS,
 		"TRAIT_NO_BARCODES" = TRAIT_NO_BARCODES,
 		"TRAIT_NO_STORAGE_INSERT" = TRAIT_NO_STORAGE_INSERT,
+		"TRAIT_NO_SPEED_POTION" = TRAIT_NO_SPEED_POTION,
 		"TRAIT_NO_TELEPORT" = TRAIT_NO_TELEPORT,
 		"TRAIT_NO_SIDE_KICK" = TRAIT_NO_SIDE_KICK,
 		"TRAIT_NODROP" = TRAIT_NODROP,
diff --git a/code/controllers/subsystem/bitrunning.dm b/code/controllers/subsystem/bitrunning.dm
index 63c2561f0f4969..5e6a1c6bd345b8 100644
--- a/code/controllers/subsystem/bitrunning.dm
+++ b/code/controllers/subsystem/bitrunning.dm
@@ -31,7 +31,7 @@ SUBSYSTEM_DEF(bitrunning)
 			"difficulty" = domain.difficulty,
 			"id" = domain.key,
 			"is_modular" = domain.is_modular,
-			"has_secondary_objectives" = assoc_value_sum(domain.secondary_loot) ? TRUE : FALSE,
+			"has_secondary_objectives" = counterlist_sum(domain.secondary_loot) ? TRUE : FALSE,
 			"name" = can_view ? domain.name : REDACTED,
 			"reward" = can_view_reward ? domain.reward_points : REDACTED,
 		))
@@ -42,7 +42,7 @@ SUBSYSTEM_DEF(bitrunning)
 	var/datum/lazy_template/virtual_domain/domain = completed_domain
 	var/choice
 
-	if(assoc_value_sum(domain.secondary_loot))
+	if(counterlist_sum(domain.secondary_loot))
 		choice = pick_weight(domain.secondary_loot)
 		domain.secondary_loot[choice] -= 1
 	else
diff --git a/code/modules/bitrunning/server/obj_generation.dm b/code/modules/bitrunning/server/obj_generation.dm
index cd2f39738d6f0e..30f7b2c47e6e04 100644
--- a/code/modules/bitrunning/server/obj_generation.dm
+++ b/code/modules/bitrunning/server/obj_generation.dm
@@ -21,7 +21,7 @@
 	if(!length(possible_turfs)) // Out of turfs to place a curiosity
 		return FALSE
 
-	if(generated_domain.secondary_loot_generated >= assoc_value_sum(generated_domain.secondary_loot)) // Out of curiosities to place
+	if(generated_domain.secondary_loot_generated >= counterlist_sum(generated_domain.secondary_loot)) // Out of curiosities to place
 		return FALSE
 
 	shuffle_inplace(possible_turfs)
diff --git a/code/modules/mafia/controller.dm b/code/modules/mafia/controller.dm
index d4edbb37f411e2..957f7f2bc42a3f 100644
--- a/code/modules/mafia/controller.dm
+++ b/code/modules/mafia/controller.dm
@@ -710,7 +710,7 @@ GLOBAL_LIST_INIT(mafia_role_by_alignment, setup_mafia_role_by_alignment())
 	var/req_players = MAFIA_MAX_PLAYER_COUNT
 	var/list/setup = custom_setup
 	if(length(setup))
-		req_players = assoc_value_sum(setup)
+		req_players = counterlist_sum(setup)
 
 	var/list/filtered_keys_and_pdas = filter_players(req_players)
 
diff --git a/code/modules/mafia/controller_ui.dm b/code/modules/mafia/controller_ui.dm
index f45ea3a80aa8f3..83857f668373a1 100644
--- a/code/modules/mafia/controller_ui.dm
+++ b/code/modules/mafia/controller_ui.dm
@@ -123,7 +123,7 @@
 				var/done = FALSE
 
 				while(!done)
-					to_chat(usr, "You have a total player count of [assoc_value_sum(debug_setup)] in this setup.")
+					to_chat(usr, "You have a total player count of [counterlist_sum(debug_setup)] in this setup.")
 					var/chosen_role_name = tgui_input_list(usr, "Select a role!", "Custom Setup Creation", rolelist_dict)
 					if(!chosen_role_name)
 						return
diff --git a/code/modules/mod/mod_clothes.dm b/code/modules/mod/mod_clothes.dm
index 8c4e5481b14d3d..28428a8fe1398f 100644
--- a/code/modules/mod/mod_clothes.dm
+++ b/code/modules/mod/mod_clothes.dm
@@ -9,11 +9,11 @@
 	body_parts_covered = HEAD
 	heat_protection = HEAD
 	cold_protection = HEAD
-	item_flags = IMMUTABLE_SLOW
 
 // Even without a hat stabilizer, hats can be worn - however, they'll fall off very easily
 /obj/item/clothing/head/mod/Initialize(mapload)
 	. = ..()
+	ADD_TRAIT(src, TRAIT_NO_SPEED_POTION, INNATE_TRAIT)
 	AddComponent(/datum/component/hat_stabilizer, loose_hat = TRUE)
 
 /obj/item/clothing/suit/mod
@@ -33,9 +33,12 @@
 	body_parts_covered = CHEST|GROIN
 	heat_protection = CHEST|GROIN
 	cold_protection = CHEST|GROIN
-	item_flags = IMMUTABLE_SLOW
 	drop_sound = null
 
+/obj/item/clothing/suit/mod/Initialize(mapload)
+	. = ..()
+	ADD_TRAIT(src, TRAIT_NO_SPEED_POTION, INNATE_TRAIT)
+
 /obj/item/clothing/gloves/mod
 	name = "MOD gauntlets"
 	desc = "A pair of gauntlets for a MODsuit."
@@ -47,11 +50,14 @@
 	body_parts_covered = HANDS|ARMS
 	heat_protection = HANDS|ARMS
 	cold_protection = HANDS|ARMS
-	item_flags = IMMUTABLE_SLOW
 	equip_sound = null
 	pickup_sound = null
 	drop_sound = null
 
+/obj/item/clothing/gloves/mod/Initialize(mapload)
+	. = ..()
+	ADD_TRAIT(src, TRAIT_NO_SPEED_POTION, INNATE_TRAIT)
+
 /obj/item/clothing/shoes/mod
 	name = "MOD boots"
 	desc = "A pair of boots for a MODsuit."
@@ -63,6 +69,10 @@
 	body_parts_covered = FEET|LEGS
 	heat_protection = FEET|LEGS
 	cold_protection = FEET|LEGS
-	item_flags = IGNORE_DIGITIGRADE | IMMUTABLE_SLOW
+	item_flags = IGNORE_DIGITIGRADE
 	fastening_type = SHOES_SLIPON
 	equip_sound = null
+
+/obj/item/clothing/shoes/mod/Initialize(mapload)
+	. = ..()
+	ADD_TRAIT(src, TRAIT_NO_SPEED_POTION, INNATE_TRAIT)
diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm
index dbb2d5d077775e..d662df70f9b1ad 100644
--- a/code/modules/mod/mod_control.dm
+++ b/code/modules/mod/mod_control.dm
@@ -684,9 +684,19 @@
 	wearer.update_spacesuit_hud_icon(state_to_use || "0")
 
 /obj/item/mod/control/proc/update_speed()
-	for(var/obj/item/part as anything in get_parts(all = TRUE))
-		part.slowdown = slowdown_deployed / length(mod_parts)
-		var/datum/mod_part/part_datum = get_part_datum(part)
+	var/total_slowdown = 0
+	var/prevent_slowdown = HAS_TRAIT(src, TRAIT_SPEED_POTIONED)
+	if (!prevent_slowdown)
+		total_slowdown += slowdown_deployed
+
+	var/list/module_slowdowns = list()
+	SEND_SIGNAL(src, COMSIG_MOD_UPDATE_SPEED, module_slowdowns, prevent_slowdown)
+	for (var/module_slow in module_slowdowns)
+		total_slowdown += module_slow
+
+	for(var/datum/mod_part/part_datum as anything in get_part_datums(all = TRUE))
+		var/obj/item/part = part_datum.part_item
+		part.slowdown = total_slowdown / length(mod_parts)
 		if (!part_datum.sealed)
 			part.slowdown = max(part.slowdown, 0)
 	wearer?.update_equipment_speed_mods()
@@ -747,15 +757,17 @@
 /obj/item/mod/control/proc/on_potion(atom/movable/source, obj/item/slimepotion/speed/speed_potion, mob/living/user)
 	SIGNAL_HANDLER
 
-	if(slowdown_deployed <= 0)
+	if(HAS_TRAIT(src, TRAIT_SPEED_POTIONED))
 		to_chat(user, span_warning("[src] has already been coated with red, that's as fast as it'll go!"))
 		return SPEED_POTION_STOP
+
 	if(active)
 		to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's active!"))
 		return SPEED_POTION_STOP
+
 	to_chat(user, span_notice("You slather the red gunk over [src], making it faster."))
 	set_mod_color(color_transition_filter(COLOR_RED))
-	slowdown_deployed = 0
+	ADD_TRAIT(src, TRAIT_SPEED_POTIONED, SLIME_POTION_TRAIT)
 	update_speed()
 	qdel(speed_potion)
 	return SPEED_POTION_STOP
diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm
index 9c2ada9899e086..7ac3aeda0cc578 100644
--- a/code/modules/mod/mod_theme.dm
+++ b/code/modules/mod/mod_theme.dm
@@ -119,6 +119,7 @@
 		part_datum.part_item = mod_part
 		mod.mod_parts["[mod_part.slot_flags]"] = part_datum
 		parts += mod_part
+
 	for(var/obj/item/part as anything in parts)
 		part.name = "[name] [part.name]"
 		part.desc = "[part.desc] [desc]"
@@ -130,6 +131,7 @@
 		part.max_heat_protection_temperature = max_heat_protection_temperature
 		part.min_cold_protection_temperature = min_cold_protection_temperature
 		part.siemens_coefficient = siemens_coefficient
+
 	set_skin(mod, skin || default_skin)
 
 /datum/mod_theme/proc/set_skin(obj/item/mod/control/mod, skin)
@@ -1156,7 +1158,7 @@
 	complexity_max = DEFAULT_MAX_COMPLEXITY + 3
 	max_heat_protection_temperature = FIRE_SUIT_MAX_TEMP_PROTECT
 	siemens_coefficient = 0
-	slowdown_deployed = 0.5
+	slowdown_deployed = 0
 	ui_theme = "syndicate"
 	resistance_flags = FIRE_PROOF
 	inbuilt_modules = list(/obj/item/mod/module/armor_booster)
@@ -1263,7 +1265,7 @@
 	max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
 	complexity_max = DEFAULT_MAX_COMPLEXITY + 3
 	siemens_coefficient = 0
-	slowdown_deployed = 0.5
+	slowdown_deployed = 0
 	ui_theme = "syndicate"
 	inbuilt_modules = list(/obj/item/mod/module/armor_booster)
 	allowed_suit_storage = list(
@@ -1696,7 +1698,7 @@
 	max_heat_protection_temperature = FIRE_IMMUNITY_MAX_TEMP_PROTECT
 	complexity_max = DEFAULT_MAX_COMPLEXITY + 3
 	siemens_coefficient = 0
-	slowdown_deployed = 0.5
+	slowdown_deployed = 0
 	ui_theme = "ntos_terminal"
 	inbuilt_modules = list(/obj/item/mod/module/armor_booster)
 	allowed_suit_storage = list(
diff --git a/code/modules/mod/modules/modules_antag.dm b/code/modules/mod/modules/modules_antag.dm
index 2c39d3ebdbd03a..9dce0940b3fd42 100644
--- a/code/modules/mod/modules/modules_antag.dm
+++ b/code/modules/mod/modules/modules_antag.dm
@@ -18,17 +18,15 @@
 	mask_worn_overlay = TRUE
 	/// Whether or not this module removes pressure protection.
 	var/remove_pressure_protection = TRUE
-	/// Speed added to the control unit.
-	var/speed_added = 0.5
-	/// Speed that we actually added.
-	var/actual_speed_added = 0
+	/// Slowdown added to the control unit while this module is disabled
+	var/space_slowdown = 0.5
 	/// Armor values added to the suit parts.
 	var/datum/armor/armor_mod = /datum/armor/mod_module_armor_boost
 	/// List of parts of the suit that are spaceproofed, for giving them back the pressure protection.
 	var/list/spaceproofed = list()
 
 /obj/item/mod/module/armor_booster/no_speedbost
-	speed_added = 0
+	space_slowdown = 0
 
 /datum/armor/mod_module_armor_boost
 	melee = 25
@@ -37,36 +35,37 @@
 	energy = 15
 
 /obj/item/mod/module/armor_booster/on_part_activation()
+	RegisterSignal(mod, COMSIG_MOD_UPDATE_SPEED, PROC_REF(on_update_speed))
 	var/obj/item/clothing/head_cover = mod.get_part_from_slot(ITEM_SLOT_HEAD) || mod.get_part_from_slot(ITEM_SLOT_MASK) || mod.get_part_from_slot(ITEM_SLOT_EYES)
 	if(istype(head_cover))
 		head_cover.flash_protect = FLASH_PROTECTION_WELDER
+	mod.update_speed()
 
 /obj/item/mod/module/armor_booster/on_part_deactivation(deleting = FALSE)
 	if(deleting)
 		return
+	UnregisterSignal(mod, COMSIG_MOD_UPDATE_SPEED)
 	var/obj/item/clothing/head_cover = mod.get_part_from_slot(ITEM_SLOT_HEAD) || mod.get_part_from_slot(ITEM_SLOT_MASK) || mod.get_part_from_slot(ITEM_SLOT_EYES)
 	if(istype(head_cover))
 		head_cover.flash_protect = initial(head_cover.flash_protect)
+	mod.update_speed()
 
 /obj/item/mod/module/armor_booster/on_activation()
 	playsound(src, 'sound/vehicles/mecha/mechmove03.ogg', 25, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
 	balloon_alert(mod.wearer, "armor boosted, EVA lost")
-	actual_speed_added = max(0, min(mod.slowdown_deployed, speed_added))
 	var/datum/mod_part/head_cover = mod.get_part_datum_from_slot(ITEM_SLOT_HEAD) || mod.get_part_datum_from_slot(ITEM_SLOT_MASK) || mod.get_part_datum_from_slot(ITEM_SLOT_EYES)
 	if(head_cover)
 		RegisterSignal(mod, COMSIG_MOD_PART_SEALED, PROC_REF(seal_helmet))
 		seal_helmet(mod, head_cover)
-	var/list/mod_parts = mod.get_parts(all = TRUE)
 	for(var/obj/item/part as anything in mod.get_parts(all = TRUE))
 		part.set_armor(part.get_armor().add_other_armor(armor_mod))
-		part.slowdown -= speed_added / length(mod_parts)
 		if(!remove_pressure_protection || !isclothing(part))
 			continue
 		var/obj/item/clothing/clothing_part = part
 		if(clothing_part.clothing_flags & STOPSPRESSUREDAMAGE)
 			clothing_part.clothing_flags &= ~STOPSPRESSUREDAMAGE
 			spaceproofed[clothing_part] = TRUE
-	mod.wearer.update_equipment_speed_mods()
+	mod.update_speed()
 
 /obj/item/mod/module/armor_booster/on_deactivation(display_message = TRUE, deleting = FALSE)
 	if(!deleting)
@@ -76,18 +75,21 @@
 	if(head_cover)
 		UnregisterSignal(mod, COMSIG_MOD_PART_SEALED)
 		REMOVE_TRAIT(mod.wearer, TRAIT_HEAD_INJURY_BLOCKED, REF(src))
-	var/list/mod_parts = mod.get_parts(all = TRUE)
 	for(var/obj/item/part as anything in mod.get_parts(all = TRUE))
 		part.set_armor(part.get_armor().subtract_other_armor(armor_mod))
-		part.slowdown += speed_added / length(mod_parts)
 		if(!remove_pressure_protection || !isclothing(part))
 			continue
 		var/obj/item/clothing/clothing_part = part
 		if(spaceproofed[clothing_part])
 			clothing_part.clothing_flags |= STOPSPRESSUREDAMAGE
-	mod.wearer.update_equipment_speed_mods()
+	mod.update_speed()
 	spaceproofed = list()
 
+/obj/item/mod/module/armor_booster/proc/on_update_speed(datum/source, list/module_slowdowns, prevent_slowdown)
+	SIGNAL_HANDLER
+	if (!active)
+		module_slowdowns += space_slowdown
+
 /obj/item/mod/module/armor_booster/generate_worn_overlay(mutable_appearance/standing)
 	overlay_state_inactive = "[initial(overlay_state_inactive)]-[mod.skin]"
 	overlay_state_active = "[initial(overlay_state_active)]-[mod.skin]"
diff --git a/code/modules/mod/modules/modules_engineering.dm b/code/modules/mod/modules/modules_engineering.dm
index af22a60d383c5d..9af2ea0bc5ac7c 100644
--- a/code/modules/mod/modules/modules_engineering.dm
+++ b/code/modules/mod/modules/modules_engineering.dm
@@ -61,15 +61,24 @@
 	/// A list of traits to add to the wearer when we're active (see: Magboots)
 	var/list/active_traits = list(TRAIT_NO_SLIP_WATER, TRAIT_NO_SLIP_ICE, TRAIT_NO_SLIP_SLIDE, TRAIT_NEGATES_GRAVITY)
 
+/obj/item/mod/module/magboot/on_install()
+	RegisterSignal(mod, COMSIG_MOD_UPDATE_SPEED, PROC_REF(on_update_speed))
+
+/obj/item/mod/module/magboot/on_uninstall(deleting)
+	UnregisterSignal(mod, COMSIG_MOD_UPDATE_SPEED)
+
 /obj/item/mod/module/magboot/on_activation()
 	mod.wearer.add_traits(active_traits, REF(src))
-	mod.slowdown += slowdown_active
-	mod.wearer.update_equipment_speed_mods()
+	mod.update_speed()
 
 /obj/item/mod/module/magboot/on_deactivation(display_message = TRUE, deleting = FALSE)
 	mod.wearer.remove_traits(active_traits, REF(src))
-	mod.slowdown -= slowdown_active
-	mod.wearer.update_equipment_speed_mods()
+	mod.update_speed()
+
+/obj/item/mod/module/magboot/proc/on_update_speed(datum/source, list/module_slowdowns, prevent_slowdown)
+	SIGNAL_HANDLER
+	if (!prevent_slowdown && active)
+		module_slowdowns += slowdown_active
 
 /obj/item/mod/module/magboot/advanced
 	name = "MOD advanced magnetic stability module"
diff --git a/code/modules/mod/modules/modules_supply.dm b/code/modules/mod/modules/modules_supply.dm
index 97346262e56455..bef88f0eb8ea49 100644
--- a/code/modules/mod/modules/modules_supply.dm
+++ b/code/modules/mod/modules/modules_supply.dm
@@ -369,9 +369,7 @@
 	/// Armor values per tile.
 	var/datum/armor/armor_mod = /datum/armor/mod_ash_accretion
 	/// Speed added when you're fully covered in ash.
-	var/speed_added = 0.5
-	/// Speed that we actually added.
-	var/actual_speed_added = 0
+	var/speed_added = -0.5
 	/// Turfs that let us accrete ash.
 	var/static/list/accretion_turfs
 	/// Turfs that let us keep ash.
@@ -409,24 +407,30 @@
 /obj/item/mod/module/ash_accretion/on_part_activation()
 	mod.wearer.add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE), REF(src))
 	RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+	RegisterSignal(mod, COMSIG_MOD_UPDATE_SPEED, PROC_REF(on_update_speed))
 
 /obj/item/mod/module/ash_accretion/on_part_deactivation(deleting = FALSE)
 	mod.wearer.remove_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE), REF(src))
 	UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED)
+	UnregisterSignal(mod, COMSIG_MOD_UPDATE_SPEED)
 	if(!traveled_tiles)
 		return
 	var/datum/armor/to_remove = get_armor_by_type(armor_mod)
 	for(var/obj/item/part as anything in mod.get_parts(all = TRUE))
 		part.set_armor(part.get_armor().subtract_other_armor(to_remove.generate_new_with_multipliers(list(ARMOR_ALL = traveled_tiles))))
 	if(traveled_tiles == max_traveled_tiles)
-		mod.slowdown += speed_added
-		mod.wearer.update_equipment_speed_mods()
+		mod.update_speed()
 	traveled_tiles = 0
 
 /obj/item/mod/module/ash_accretion/generate_worn_overlay(mutable_appearance/standing)
 	overlay_state_inactive = "[initial(overlay_state_inactive)]-[mod.skin]"
 	return ..()
 
+/obj/item/mod/module/ash_accretion/proc/on_update_speed(datum/source, list/module_slowdowns, prevent_slowdown)
+	SIGNAL_HANDLER
+	if (traveled_tiles == max_traveled_tiles)
+		module_slowdowns += speed_added
+
 /obj/item/mod/module/ash_accretion/proc/on_move(atom/source, atom/oldloc, dir, forced)
 	if(!isturf(mod.wearer.loc)) //dont lose ash from going in a locker
 		return
@@ -443,18 +447,15 @@
 			mod.wearer.color = list(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,3) //make them super light
 			animate(mod.wearer, 1 SECONDS, color = null, flags = ANIMATION_PARALLEL)
 			playsound(src, 'sound/effects/sparks/sparks1.ogg', 100, TRUE)
-			actual_speed_added = max(0, min(mod.slowdown_deployed, speed_added))
-			mod.slowdown -= actual_speed_added
-			mod.wearer.update_equipment_speed_mods()
+			mod.update_speed()
 	else if(is_type_in_typecache(mod.wearer.loc, keep_turfs))
 		return
 	else
 		if(traveled_tiles <= 0)
 			return
-		if(traveled_tiles == max_traveled_tiles)
-			mod.slowdown += actual_speed_added
-			mod.wearer.update_equipment_speed_mods()
 		traveled_tiles--
+		if(traveled_tiles == max_traveled_tiles - 1) // Just lost our speed buff
+			mod.update_speed()
 		for(var/obj/item/part as anything in mod.get_parts(all = TRUE))
 			part.set_armor(part.get_armor().subtract_other_armor(armor_mod))
 		if(traveled_tiles <= 0)
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index 71e8f04b4b57ec..d6bcb8cff2db80 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -912,7 +912,7 @@
 		return ITEM_INTERACT_SUCCESS
 	if(isitem(interacting_with))
 		var/obj/item/apply_to = interacting_with
-		if(apply_to.slowdown <= 0 || (apply_to.item_flags & IMMUTABLE_SLOW))
+		if(apply_to.slowdown <= 0 || (apply_to.item_flags & IMMUTABLE_SLOW) || HAS_TRAIT(apply_to, TRAIT_NO_SPEED_POTION))
 			if(interacting_with.atom_storage)
 				return NONE // lets us put the potion in the bag
 			to_chat(user, span_warning("[apply_to] can't be made any faster!"))


Reviews:
user: login: Ghommie
body: I don't like the prevent_slowdown variable when we have a "speed potioned" trait already, but I understand that it won't be added if the signal proc returns `SPEED_POTION_STOP`. A more elegant solution would be to add a `SPEED_POTION_OVERRIDE` return value that only blocks some of the normal operations, while keeping others like, adding the trait, updating speed and deleting the potion, but it's entirely optional.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-26T10:12:17Z

Review comments:
diff_hunk: @@ -60,6 +60,8 @@
 	var/charge_drain = DEFAULT_CHARGE_DRAIN
 	/// Slowdown of the MOD when all of its pieces are deployed.
 	var/slowdown_deployed = 0.75
+	/// Have we had a speed potion applied to us?
+	var/prevent_slowdown = FALSE
path: code/modules/mod/mod_control.dm
user: login: Ghommie
body: ```suggestion
```
created_at: 2025-03-26T09:59:11Z
author_association: MEMBER
diff_hunk: @@ -684,9 +686,18 @@
 	wearer.update_spacesuit_hud_icon(state_to_use || "0")
 
 /obj/item/mod/control/proc/update_speed()
-	for(var/obj/item/part as anything in get_parts(all = TRUE))
-		part.slowdown = slowdown_deployed / length(mod_parts)
-		var/datum/mod_part/part_datum = get_part_datum(part)
+	var/total_slowdown = 0
+	if (!prevent_slowdown)
path: code/modules/mod/mod_control.dm
user: login: Ghommie
body: ```suggestion
	var/prevent_slowdown = HAS_TRAIT(src, TRAIT_SPEED_POTIONED)
	if (!prevent_slowdown)
```
created_at: 2025-03-26T09:59:32Z
author_association: MEMBER
diff_hunk: @@ -747,15 +758,17 @@
 /obj/item/mod/control/proc/on_potion(atom/movable/source, obj/item/slimepotion/speed/speed_potion, mob/living/user)
 	SIGNAL_HANDLER
 
-	if(slowdown_deployed <= 0)
+	if(prevent_slowdown)
 		to_chat(user, span_warning("[src] has already been coated with red, that's as fast as it'll go!"))
 		return SPEED_POTION_STOP
+
 	if(active)
 		to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's active!"))
 		return SPEED_POTION_STOP
+
 	to_chat(user, span_notice("You slather the red gunk over [src], making it faster."))
 	set_mod_color(color_transition_filter(COLOR_RED))
-	slowdown_deployed = 0
+	prevent_slowdown = TRUE
path: code/modules/mod/mod_control.dm
user: login: Ghommie
body: ```suggestion
	ADD_TRAIT(src, TRAIT_SPEED_POTIONED, SLIME_POTION_TRAIT)
```
created_at: 2025-03-26T10:00:43Z
author_association: MEMBER
diff_hunk: @@ -747,15 +758,17 @@
 /obj/item/mod/control/proc/on_potion(atom/movable/source, obj/item/slimepotion/speed/speed_potion, mob/living/user)
 	SIGNAL_HANDLER
 
-	if(slowdown_deployed <= 0)
+	if(prevent_slowdown)
path: code/modules/mod/mod_control.dm
user: login: Ghommie
body: ```suggestion
	if(HAS_TRAIT(src, TRAIT_SPEED_POTIONED))
```
created_at: 2025-03-26T10:03:45Z
author_association: MEMBER

PR comments:
user: login: RandomDudeFromTheRim
created_at: 2025-03-18T17:10:10Z
body: Rip A-Train crew
author_association: CONTRIBUTOR
user: login: Maximal08
created_at: 2025-03-19T13:41:32Z
body: You asked me to send this, it's done.

https://github.com/user-attachments/assets/fc4b4428-35b7-445c-8daa-9950dcdd3ed4


author_association: CONTRIBUTOR