From a3c8013b45c92fdb8efec7ba827d7b00191e2d55 Mon Sep 17 00:00:00 2001 From: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com> Date: Wed, 11 May 2022 02:49:06 -0400 Subject: [PATCH] Refactors how legs are displayed so they no longer appear above one-another when looking EAST or WEST (#66607) So, for over 5 years, left legs have been displaying over right legs. Never noticed it? Don't blame you. Here's a nice picture provided by #20603 (Bodypart sprites render with incorrect layering), that clearly displays the issue that was happening: It still happened to this day. Notice how the two directions don't look the same? That's because the left leg is always displaying above the right one. Obviously, that's no good, and I was like "oh, that's a rendering issue, so there's nothing I can do about it, it's an issue with BYOND". Until it struck me. "What if we used a mask that would cut out the parts of the right leg, from the left leg, so that it doesn't actually look as if it's above it?" Here I am, after about 25 hours of work, 15 of which of very painful debugging due to BYOND's icon documentation sucking ass. So, how does it work? Basically, we create a mask of a left leg (that'll be explained later down the line), more specifically, a cutout of JUST the WEST dir of the left leg, with every other dir being just white squares. We then cache that mask in a static list on the right leg, so we don't generate it every single time, as that can be expensive. All that happens in update_body_parts(), where I've made it so legs are handled separately, to avoid having to generate limb icons twice in a row, due to it being expensive. In that, when we generate_limb_icon() a right leg, we apply the proper left leg mask if necessary. Now, why masking the right leg, if the issue was the left leg? Because, see, when you actually amputated someone, and gave them a leg again, it would end up being that new leg that would be displayed below the other leg. So I fixed that, by making it so that bodyparts would be sorted correctly, before the end of update_body_parts(). Which means that right legs ended up displaying above left legs, which meant that I had to change everything I had written to work on right legs rather than left legs. I spent so much time looking up BYOND documentation for MapColors() and filters and all icon and image vars and procs, I decided to make a helper proc called generate_icon_alpha_mask(), because honestly it would've saved me at least half a day of pure code debugging if I had it before working on this refactor. I tried to put as much documentation down as I could, because this shit messes with your brain if you spend too long looking at it. icon and image are two truly awful classes to work with, and I don't look forward to messing with them more in the future. Anyway. It's nice, because it requires no other effort from anyone, no matter what the shape of the leg is actually like. It's all handled dynamically, and only one per type of leg, meaning that it's not actually too expensive either, which is very nice. Especially since it's very downstreams-friendly from being done this way. It fixes #20603 (Bodypart sprites render with incorrect layering), an issue that has been around for over half a decade, as well as probably many more issues that I just didn't bother sifting through. Plus, it just looks so much better. --- code/__HELPERS/cmp.dm | 4 + code/__HELPERS/icons.dm | 31 ++++ .../mob/living/carbon/carbon_update_icons.dm | 142 +++++++++++++++++- code/modules/surgery/bodyparts/_bodyparts.dm | 13 +- .../surgery/bodyparts/dismemberment.dm | 3 + code/modules/surgery/bodyparts/parts.dm | 7 + icons/mob/left_leg_mask_base.dmi | Bin 0 -> 256 bytes 7 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 icons/mob/left_leg_mask_base.dmi diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 8545b04d6f3..467f60e8195 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -140,6 +140,10 @@ GLOBAL_VAR_INIT(cmp_field, "name") /proc/cmp_mob_realname_dsc(mob/A,mob/B) return sorttext(A.real_name,B.real_name) +/// Orders bodyparts by their body_part value, ascending. +/proc/cmp_bodypart_by_body_part_asc(obj/item/bodypart/limb_one, obj/item/bodypart/limb_two) + return limb_one.body_part - limb_two.body_part + /// Orders by integrated circuit weight /proc/cmp_port_order_asc(datum/port/compare1, datum/port/compare2) return compare1.order - compare2.order diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 5f9cef0db4a..2d41fc6f1c1 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -897,6 +897,37 @@ world alpha_mask.Blend(image_overlay,ICON_OR)//OR so they are lumped together in a nice overlay. return alpha_mask//And now return the mask. +/** + * Helper proc to generate a cutout alpha mask out of an icon. + * + * Why is it a helper if it's so simple? + * + * Because BYOND's documentation is hot garbage and I don't trust anyone to actually + * figure this out on their own without sinking countless hours into it. Yes, it's that + * simple, now enjoy. + * + * But why not use filters? + * + * Filters do not allow for masks that are not the exact same on every dir. An example of a + * need for that can be found in [/proc/generate_left_leg_mask()]. + * + * Arguments: + * * icon_to_mask - The icon file you want to generate an alpha mask out of. + * * icon_state_to_mask - The specific icon_state you want to generate an alpha mask out of. + * + * Returns an `/icon` that is the alpha mask of the provided icon and icon_state. + */ +/proc/generate_icon_alpha_mask(icon_to_mask, icon_state_to_mask) + var/icon/mask_icon = icon(icon_to_mask, icon_state_to_mask) + // I hate the MapColors documentation, so I'll explain what happens here. + // Basically, what we do here is that we invert the mask by using none of the original + // colors, and then the fourth group of number arguments is actually the alpha values of + // each of the original colors, which we multiply by 255 and subtract a value of 255 to the + // result for the matching pixels, while starting with a base color of white everywhere. + mask_icon.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 255,255,255,-255, 1,1,1,1) + return mask_icon + + /mob/proc/AddCamoOverlay(atom/A)//A is the atom which we are using as the overlay. var/icon/opacity_icon = new(A.icon, A.icon_state)//Don't really care for overlays/underlays. //Now we need to culculate overlays+underlays and add them together to form an image for a mask. diff --git a/code/modules/mob/living/carbon/carbon_update_icons.dm b/code/modules/mob/living/carbon/carbon_update_icons.dm index 17f0ba53f33..39867c4a3a2 100644 --- a/code/modules/mob/living/carbon/carbon_update_icons.dm +++ b/code/modules/mob/living/carbon/carbon_update_icons.dm @@ -248,24 +248,40 @@ update_wound_overlays() var/list/needs_update = list() var/limb_count_update = FALSE + var/obj/item/bodypart/l_leg/left_leg + var/obj/item/bodypart/r_leg/right_leg + var/old_left_leg_key for(var/obj/item/bodypart/limb as anything in bodyparts) limb.update_limb(is_creating = update_limb_data) //Update limb actually doesn't do much, get_limb_icon is the cpu eater. + + if(limb.body_zone == BODY_ZONE_R_LEG) + right_leg = limb + continue // Legs are handled separately + var/old_key = icon_render_keys?[limb.body_zone] //Checks the mob's icon render key list for the bodypart icon_render_keys[limb.body_zone] = (limb.is_husked) ? limb.generate_husk_key().Join() : limb.generate_icon_key().Join() //Generates a key for the current bodypart - if(!(icon_render_keys[limb.body_zone] == old_key)) //If the keys match, that means the limb doesn't need to be redrawn + + if(limb.body_zone == BODY_ZONE_L_LEG) + left_leg = limb + old_left_leg_key = old_key + continue // Legs are handled separately + + if(icon_render_keys[limb.body_zone] != old_key) //If the keys match, that means the limb doesn't need to be redrawn needs_update += limb + // Here we handle legs differently, because legs are a mess due to layering code. So we got to process the left leg first. Thanks BYOND. + var/legs_need_redrawn = update_legs(right_leg, left_leg, old_left_leg_key) + var/list/missing_bodyparts = get_missing_limbs() if(((dna ? dna.species.max_bodypart_count : BODYPARTS_DEFAULT_MAXIMUM) - icon_render_keys.len) != missing_bodyparts.len) //Checks to see if the target gained or lost any limbs. limb_count_update = TRUE for(var/missing_limb in missing_bodyparts) icon_render_keys -= missing_limb //Removes dismembered limbs from the key list - if(!needs_update.len && !limb_count_update) + if(!needs_update.len && !limb_count_update && !legs_need_redrawn) return - remove_overlay(BODYPARTS_LAYER) //GENERATE NEW LIMBS var/list/new_limbs = list() @@ -277,12 +293,51 @@ else new_limbs += limb_icon_cache[icon_render_keys[limb.body_zone]] //Pulls existing sprites from the cache + remove_overlay(BODYPARTS_LAYER) + if(new_limbs.len) overlays_standing[BODYPARTS_LAYER] = new_limbs apply_overlay(BODYPARTS_LAYER) +/** + * Here we update the legs separately from the other bodyparts. Thanks BYOND for so little support for dir layering. + * + * Arguments: + * * right_leg - Right leg that we might need to update. Can be null. + * * left_leg - Left leg that we might need to update. Can be null. + * * old_left_leg_key - The icon_key of the left_leg, passed here to avoid having to re-generate it in this proc. + * + * Returns a boolean, TRUE if the legs need to be redrawn, FALSE if they do not need to be redrawn. + * Necessary so that we can ensure that modifications of legs cause overlay updates. + */ +/mob/living/carbon/proc/update_legs(obj/item/bodypart/r_leg/right_leg, obj/item/bodypart/l_leg/left_leg, old_left_leg_key) + var/list/left_leg_icons // yes it's actually a list, bet you didn't expect that, now did you? + var/legs_need_redrawn = FALSE + if(left_leg) + // We regenerate the look of the left leg if it isn't already cached, we don't if not. + if(icon_render_keys[left_leg.body_zone] != old_left_leg_key) + limb_icon_cache[icon_render_keys[left_leg.body_zone]] = left_leg.get_limb_icon() + legs_need_redrawn = TRUE + + left_leg_icons = limb_icon_cache[icon_render_keys[left_leg.body_zone]] + + if(right_leg) + var/old_right_leg_key = icon_render_keys?[right_leg.body_zone] + right_leg.left_leg_mask_key = left_leg?.generate_mask_key().Join() // We generate a new mask key, to see if it changed. + // We regenerate the left_leg_mask in case that it doesn't exist yet. + if(right_leg.left_leg_mask_key && !right_leg.left_leg_mask_cache[right_leg.left_leg_mask_key] && left_leg_icons) + right_leg.left_leg_mask_cache[right_leg.left_leg_mask_key] = generate_left_leg_mask(left_leg_icons[1], right_leg.left_leg_mask_key) + // We generate a new icon_render_key, which also takes into account the left_leg_mask_key so we cache the masked versions of the limbs too. + icon_render_keys[right_leg.body_zone] = right_leg.is_husked ? right_leg.generate_husk_key().Join("-") : right_leg.generate_icon_key().Join() + + if(icon_render_keys[right_leg.body_zone] != old_right_leg_key) + limb_icon_cache[icon_render_keys[right_leg.body_zone]] = right_leg.get_limb_icon() + legs_need_redrawn = TRUE + + return legs_need_redrawn + ///////////////////////// // Limb Icon Cache 2.0 // @@ -313,6 +368,29 @@ return . +/** + * Generates a cache key for masks (mainly only used for right legs now, but perhaps in the future...). + * + * This is exactly like generate_icon_key(), except that it doesn't add `"-[draw_color]"` + * to the returned list under any circumstance. Why? Because it (generate_icon_key()) is + * a proc that gets called a ton and I don't want this to affect its performance. + * + * Returns a list of strings. + */ +/obj/item/bodypart/proc/generate_mask_key() + RETURN_TYPE(/list) + . = list() + if(is_dimorphic) + . += "[limb_gender]" + . += "[limb_id]" + . += "[body_zone]" + for(var/obj/item/organ/external/external_organ as anything in external_organs) + if(!external_organ.can_draw_on_bodypart(owner)) + continue + . += "[external_organ.generate_icon_cache()]" + + return . + ///Generates a cache key specifically for husks /obj/item/bodypart/proc/generate_husk_key() RETURN_TYPE(/list) @@ -346,3 +424,61 @@ . += "-HAIR_HIDDEN" return . + +/obj/item/bodypart/r_leg/generate_icon_key() + RETURN_TYPE(/list) + . = ..() + if(left_leg_mask_key) // We do this so we can cache the versions with and without a mask, for when there's no left leg. + . += "-[left_leg_mask_key]" + + return . + +/** + * This proc serves as a way to ensure that right legs don't overlap above left legs when their dir is WEST on a mob. + * + * It's using the `left_leg_mask_cache` to avoid generating a new mask when unnecessary, which means that there needs to be one + * for the proc to return anything. + * + * Arguments: + * * right_leg_icon_file - The icon file of the right leg overlay we're trying to apply a mask to. + * * right_leg_icon_state - The icon_state of the right leg overlay we're trying to apply a mask to. + * * image_dir - The direction applied to the icon, only meant for when the leg is dropped, so it remains + * facing SOUTH all the time. + * + * Returns the `/image` of the right leg that was masked, or `null` if the mask didn't exist. + */ +/obj/item/bodypart/r_leg/proc/generate_masked_right_leg(right_leg_icon_file, right_leg_icon_state, image_dir) + RETURN_TYPE(/image) + if(!left_leg_mask_cache[left_leg_mask_key] || !right_leg_icon_file || !right_leg_icon_state) + return + + var/icon/right_leg_icon = icon(right_leg_icon_file, right_leg_icon_state) + right_leg_icon.Blend(left_leg_mask_cache[left_leg_mask_key], ICON_MULTIPLY) + return image(right_leg_icon, right_leg_icon_state, layer = -BODYPARTS_LAYER, dir = image_dir) + + +/** + * The proc that handles generating left leg masks at runtime. + * It basically creates an icon that are all white on all dirs except WEST, where there's a cutout + * of the left leg that needed to be masked. + * + * It does /not/ cache the mask itself, and as such, the caching must be done manually (which it is, look up in update_body_parts()). + * + * Arguments: + * * image/left_leg_image - `image` of the left leg that we need to create a mask out of. + * + * Returns the generated left leg mask as an `/icon`, or `null` if no left_leg_image is provided. + */ +/proc/generate_left_leg_mask(image/left_leg_image) + RETURN_TYPE(/icon) + if(!left_leg_image) + return + var/icon/left_leg_alpha_mask = generate_icon_alpha_mask(left_leg_image.icon, left_leg_image.icon_state) + // Right here, we use the crop_mask_icon to single out the WEST sprite of the mask we just created above. + var/icon/crop_mask_icon = icon(icon = 'icons/mob/left_leg_mask_base.dmi', icon_state = "mask_base") + crop_mask_icon.Blend(left_leg_alpha_mask, ICON_MULTIPLY) + // Then, we add (with ICON_OR) that singled-out WEST mask to a template mask that has the NORTH, + // SOUTH and EAST dirs as full white squares, to finish our WEST-directional mask. + var/icon/new_mask_icon = icon(icon = 'icons/mob/left_leg_mask_base.dmi', icon_state = "mask_rest") + new_mask_icon.Blend(crop_mask_icon, ICON_OR) + return new_mask_icon diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index 459c2a843a4..a1b5ea3b6bd 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -714,7 +714,6 @@ var/image/limb = image(layer = -BODYPARTS_LAYER, dir = image_dir) var/image/aux - . += limb if(animal_origin) if(IS_ORGANIC_LIMB(src)) @@ -731,6 +730,7 @@ var/mutable_appearance/limb_em_block = emissive_blocker(limb.icon, limb.icon_state, alpha = limb.alpha) limb_em_block.dir = image_dir limb.overlays += limb_em_block + . += limb return //HUSK SHIIIIT @@ -738,6 +738,7 @@ limb.icon = icon_husk limb.icon_state = "[husk_type]_husk_[body_zone]" icon_exists(limb.icon, limb.icon_state, scream = TRUE) //Prints a stack trace on the first failure of a given iconstate. + . += limb if(aux_zone) //Hand shit aux = image(limb.icon, "[husk_type]_husk_[aux_zone]", -aux_layer, image_dir) . += aux @@ -756,6 +757,16 @@ icon_exists(limb.icon, limb.icon_state, TRUE) //Prints a stack trace on the first failure of a given iconstate. + if(body_zone == BODY_ZONE_R_LEG) + var/obj/item/bodypart/r_leg/leg = src + var/limb_overlays = limb.overlays + var/image/new_limb = leg.generate_masked_right_leg(limb.icon, limb.icon_state, image_dir) + if(new_limb) + limb = new_limb + limb.overlays = limb_overlays + + . += limb + if(aux_zone) //Hand shit aux = image(limb.icon, "[limb_id]_[aux_zone]", -aux_layer, image_dir) . += aux diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm index 9a1814cacb0..b66c8561d92 100644 --- a/code/modules/surgery/bodyparts/dismemberment.dm +++ b/code/modules/surgery/bodyparts/dismemberment.dm @@ -365,6 +365,9 @@ if(can_be_disabled) update_disabled() + // Bodyparts need to be sorted for leg masking to be done properly. It also will allow for some predictable + // behavior within said bodyparts list. We sort it here, as it's the only place we make changes to bodyparts. + new_limb_owner.bodyparts = sort_list(new_limb_owner.bodyparts, /proc/cmp_bodypart_by_body_part_asc) synchronize_bodytypes(new_limb_owner) new_limb_owner.updatehealth() new_limb_owner.update_body() diff --git a/code/modules/surgery/bodyparts/parts.dm b/code/modules/surgery/bodyparts/parts.dm index 181de8e5c5c..cf355f7cfc7 100644 --- a/code/modules/surgery/bodyparts/parts.dm +++ b/code/modules/surgery/bodyparts/parts.dm @@ -366,6 +366,13 @@ px_y = 12 max_stamina_damage = 50 can_be_disabled = TRUE + /// We store this here to generate our icon key more easily. + var/left_leg_mask_key + /// The associated list of all the left leg mask keys associated to their cached left leg masks. + /// It's static, so it's shared between all the left legs there is. Be careful. + /// Why? Both legs share the same layer for rendering, and since we don't want to do redraws on + /// each dir changes, we're doing it with a mask instead, which we cache for efficiency reasons. + var/static/list/left_leg_mask_cache = list() /obj/item/bodypart/r_leg/set_owner(new_owner) diff --git a/icons/mob/left_leg_mask_base.dmi b/icons/mob/left_leg_mask_base.dmi new file mode 100644 index 0000000000000000000000000000000000000000..f4d95816363a6e4b3acb8fc83d87c0d8872a2298 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeG3?%1&o4*=JaR&H=xE?rg;Q#;sKypv_ZEK() zV@Z%-FoVOh8)-mJX;nx>iA!p6a#3bMNoIZ?14G4};IM+C((hk_3qF2*qUEitb?(gh z;0>V$7mXi0(mC&=d6J>1r+0^iaggz4V=sj{k0u?N6rvEUX};0I+D>z$MQCTYvFhd( z5=$m_8?V(cG#5Q$<`Nt^B`T)Ld+Da8)$_S;2r_hC(~wIMTzL)X02NOc#}JRsCk4a9Im@2VTh(SI!L_k@re+5uIgQu&X%Q~loCII{7Ul0HQ literal 0 HcmV?d00001