From 5a0a04bde799c78891d299a6ee1d58e103dc6c4a Mon Sep 17 00:00:00 2001 From: Flipp Syder <76629141+vulppine@users.noreply.github.com> Date: Thu, 22 Sep 2022 15:19:00 -0700 Subject: [PATCH] Humanoid appearance refactor (#10882) * initial commit - species prototype modifications - markings points as its own file - shared humanoid component * adds a tool to convert sprite accessories to markings (in go) * removes a fmt call * converts sprite accessory to markings * adds hair and facial hair to marking categories * multiple changes - humanoid visualizer system - markings modifications for visualizer - modifications to shared humanoid component - lays out a base for humanoid system * hidden layers, ports some properties from appearance component, shrinks DefaultMarkings a little * squishes the initialize event calls into one function adds stuff to set species/skin color externally from a server message - currently laid out as if it a dirty call to a networked component, may be subject to change (server-side has not been implemented yet) * makes the sprite pipeline more obvious * apply all markings, hidden layer set replacement * ensures that markings are cleared when the new set is applied * starts refactoring markingsset (unfinished) * more additions to the markingset api * adds constructor logic to markingset * adds a method to filter out markings in a set based on a given species * fixes enumerators in markingset * adds validator into MarkingSet, fixes ForwardMarkingEnumerator * modifications to the humanoid visual system * ensuredefault in markingset * oop * fixes up data keys, populates OnAppearanceChange in visualizer * changes to humanoid component, markings marking equality is now more strict, humanoidcomponent is now implemented for client as a child of sharedhumanoidcomponent * markings are now applied the visualizer by diffing them * base sprites are now applied to humanoids from humanoidvisualizer * passes along base sprite settings to the marking application so that markings know to follow skin color/alpha or not (see: slimes) * custom base layers on humanoids * merges all data keys into one data class for humanoid visualizers * setappearance in sharedhumanoidsystem, removes custombaselayercolors * humanoidcomponent, system (empty) in server * adds some basic public API functions to HumanoidSystem * add marking, remove marking * changes appearance MarkingsSet to a List, adds listener for PlayerSpawnCompleteEvent in HumanoidSystem * ensuredefaultmarkings, oninit for humanoids * markingmanager API changes * removes MarkingsSet * LoadProfile, adjusts randomization in humanoid appearance to account for species * base layer settings in humanoidsystem, eye color from profile * rearranges files to centralize under Humanoid namespace * more reorganization, deletes some stuff gotta break stuff to make other things work, right? goodbye SpriteAccessory... * fixes a good chunk of server-side issues still does not compile, yet * singlemarkingpicker xaml layout * singlemarkingpicker logic * magic mirror window (varying pieces of it, mostly client-oriented) * removes some imports, gives MagicMirror a BUI class (not filled in yet) * populates magic mirror BUI functionality / window callbacks * fixes up some errors in humanoidprofileeditor * changes to SingleMarkingPicker SingleMarkingPicker now accepts a List, species, and total possible markings available in that marking category * fixes up hair pickers on humanoid profile editor * fixes the errors in markingpicker * markingsystem is now gone * fixes a bunch of build errors * so that's why i did it like that * namespace issues, adds robustxamlloader to singlemarkingpicker * another robustxamlloader * human, lizard sprites/points * prototype fixes, deletion of old spriteaccessory * component registration, fixes dwarf skin toning no, 'ReptilianToned' does not exist * removes component registration from abstract humanoid component * visualizer data now cloneable * serialize for visualizer key * zero-count edge case * missing semi-colon moment * setspecies in humanoidsystem * ensures that default markings, if empty, will cause ensuredefault to skip over that given category * tryadd instead of add * whoops * diff and apply should properly apply markings now * always ensure default, fixes double load for player spawning * apply skin color now sets the skin color property in humanoidcomponent * removes sprite from a few species prototypes * sprite changes for specific base layers based on humanoid sex * layer ordering fix, and a missing base layer should now disallow markings on that layer * anymarking base layer, adds the right leg/foot for humans * loading a profile will now clear all markings on that humanoid * adds missing layers for humans * separates species.yml into respective species prototype files * ensures that if layer visibility was changed, all markings have to be reapplied * server-side enforcement of hiding hair (and other head-related markings) when equipping things that hide hair * slime fix, clothingsystem now dictates layer visibility server side * sussy * layer settings should now ensure a marking should match the skin tone * whoops * skincolor static class and functions in UI * skin color validation in humanoidcharacterappearance * markingpicker now shows only the markings for the selected category in used * getter for slot in singlemarkingpicker now ensures slot is 0 if markings exists * FilterSpecies no longer attempts to do removal while iterating * expands for SingleMarkingPicker * humanoid base dummy has blank layers now (and snout/tail/headside/headtop) * fixes an issue with visualizer system if the marking count was different but the markings themselves were (somewhat) the same * whoops * adds edge case handlers for count differences in humanoid markings * preview now loads profile instead of directly setting appearance * moves marking set loading to update controls * clones a marking set in markingpicker by using the deep clone constructor * whoops (deep cloning a marking now copies the marking id) * adds replace function for markingset * points should now update after the markings are remove/added * merging base layer sprites into a humanoid should now clear them before merging * sets dirty range start to count only if the dirty range start was never set above 0 * fixes up some issues with singlemarkingpicker * color selector sliders in single marking picker should now expand * hair from hair pickers should now apply in profile loading (client-side) * category in singlemarkingpicker now sets the private category variable * slot selector should now populate * single marking picker buttons now have text, also shows the category name over all user-clickable elements * removes a comment * removing hair slots now sets it to bald, defaults to zero used slots if current hair is bald on hair/facial hair * random skin color, eye color * populate colors now checks if the marking count is greater than zero in singlemarkingpicker * hair/facial hair pickers now just get the first possible hair from the respective species list * different approach to random skin color * oh, that's why it wasn't working * randomize everything now just updates every single control * selecting a new marking in SingleMarkingPicker should attempt to copy over old colors, populate list now uses cache, * markingmanager now uses OnlyWhitelisted to populate by category and species * filterspecies now uses onlyWhitelist to filter markings based on whitelist or not * oops * ui fix for singlemarkingpicker, ensures that cache is not null if it is null when populatelist is called * order of operations for the horizontal expand for add/remove * hair pickers should now update when you add/remove the hair slot * fixes variable naming error in character appearance * loc string fix in singlemarkingpicker * lizards, vox now have onlyWhitelist, vox restriction for hair/facialhairs * having zero possible hairs should no longer cause an exception in randomization * setting species should now update hair pickers * ignore categories for marking picker * and a clear as well for the category button * places that functionality in its own function instead * adds eye base sprite, vox now also have their own custom eye sprites * loading a profile client-side should do FilterSpecies for markings now * client-side load profile does filter species after adding in the hairs now * magic mirror * callbacks now call the callback instead of adding it on construct * whoops * in removemarking too * adds missing synchronize calls * comments out an updateinterface call in magic mirror * magic mirror window title, minimum sizing * fixes minsize, adds warning for players who try to set their hair for species that have no hair * removes spaces in xaml * namespace changes/organization * whoopsie (merge conflicts) * re-enables identity from humanoid component * damagevisuals now uses the enum given to it instead of the layerstate given on that layer tied to the enum * removes commas from json * changes to visuals system so the change is consistent * chest * reptilian * visualizer system now handles body sprite setting/coloration, similar to how characterappearance did it not a big fan of this * adds a check in applybasesprites * adding/removing parts should now make them invisible on a humanoid * body part removal/adding now enumerates over sublayers instead * synchro now runs in bodycomponent startup * parts instead of slots * humanoidcompnent check * switches from rsi to actualrsi * removes all the body stuff (too slow) * cleans up resolves from humanoid visualizer system * merging sprites now checks if the base sprites have been modified or not (through things like species changes, or custom base sprite changes) * not forgetting that one again * merging now returns an actual dirty value * replaces the sequenceequal with a more accurate solution * permanent layers, layer visibility on add/remove part in body * should send all hidden layers over now * isdirty in visualizer system for base layers * isdirty checks count as well * ok, IsDirty should now set the base layers if the merged sprites are different * equals override in HumanoidSpritePrototypes.cs temporary until record prototypes :heck: * makes fields readonly, equates IDs instead * adds forced markings through marking picker * forced in humanoidsystem api, ignorespecies in markingpicker * marking bui * makes that serializable as well * ignore species/forced toggles now work * adds icon to modifier verb, interface and keys to humanoid bases * needs the actual enum value to open, no? * makes the key the actual key * actions now propagate upwards * ignore species when set now repopulates markingpicker * modifiable base layers in the markings window * oops! * layout changes * info box should now appear * adds ignorespecies for marking picker, collapsible for base layer section of appearance modification window * collapsible layout moment * if base layers have changed, all markings are now dirty (and if a base layer is missing, the marking is still 'applied' but it's now just invisible * small change to marking visibility * small changes to modifier UI * markings now match skin on zombification * zombie stuff * makes the line edit in marking modifier window more obvious * disables vox on round start * horizontal expand on the single label in base layer modifiers * humanoid profiles in prototypes * randomhumanoidappearance won't work if the humanoid has a profile already stored * removes unused code * documentation in humanoidsystem server-side * documentation in shared/client * whoops * converts accessory into marking in locale files (also adds marking loc string into single marking picker) * be gone, shared humanoid appearance system from the last upstream merge * species ignore on randomization (defaults to no ignored species) * more upstream merge parts that bypassed any errors before merge * addresses review (also just adds typeserializers in some places) * submodule moment * upstream merge issues --- .../MagicMirrorBoundUserInterface.cs | 269 ---- .../Systems/HumanoidAppearanceSystem.cs | 165 --- .../Clothing/ClothingVisualsSystem.cs | 24 +- .../Cuffs/Components/CuffableComponent.cs | 2 +- Content.Client/Damage/DamageVisualsSystem.cs | 13 +- Content.Client/Humanoid/EyeColorPicker.cs | 40 + Content.Client/Humanoid/HumanoidComponent.cs | 15 + ...manoidMarkingModifierBoundUserInterface.cs | 62 + .../HumanoidMarkingModifierWindow.xaml | 18 + .../HumanoidMarkingModifierWindow.xaml.cs | 138 ++ Content.Client/Humanoid/HumanoidSystem.cs | 63 + .../Humanoid/HumanoidVisualizerSystem.cs | 447 ++++++ .../{Markings => Humanoid}/MarkingPicker.xaml | 0 Content.Client/Humanoid/MarkingPicker.xaml.cs | 463 +++++++ .../Humanoid/SingleMarkingPicker.xaml | 22 + .../Humanoid/SingleMarkingPicker.xaml.cs | 283 ++++ .../Lobby/UI/LobbyCharacterPreviewPanel.cs | 8 +- .../MagicMirrorBoundUserInterface.cs | 67 + .../MagicMirror/MagicMirrorWindow.xaml | 9 + .../MagicMirror/MagicMirrorWindow.xaml.cs | 52 + Content.Client/Markings/MarkingPicker.xaml.cs | 449 ------ Content.Client/Markings/MarkingsSystem.cs | 173 --- .../Preferences/UI/CharacterSetupGui.xaml.cs | 9 +- .../UI/HumanoidProfileEditor.Random.cs | 13 +- .../Preferences/UI/HumanoidProfileEditor.xaml | 11 +- .../UI/HumanoidProfileEditor.xaml.cs | 242 ++-- .../Body/Components/BodyComponent.cs | 22 + .../Components/MagicMirrorComponent.cs | 90 -- .../RandomHumanoidAppearanceComponent.cs | 8 - .../Systems/HumanoidAppearanceSystem.cs | 31 - .../Systems/MagicMirrorSystem.cs | 42 - .../Systems/RandomHumanoidAppearanceSystem.cs | 33 - Content.Server/Cloning/CloningSystem.cs | 12 +- Content.Server/Clothing/ClothingSystem.cs | 39 + Content.Server/Database/ServerDbBase.cs | 7 +- Content.Server/Dragon/DragonSystem.cs | 4 +- .../GameTicking/Rules/PiratesRuleSystem.cs | 2 +- .../GameTicking/Rules/ZombieRuleSystem.cs | 8 +- .../Humanoid/Components/HumanoidComponent.cs | 38 + .../RandomHumanoidAppearanceComponent.cs | 13 + .../Systems/HumanoidSystem.Modifier.cs | 96 ++ .../Humanoid/Systems/HumanoidSystem.cs | 476 +++++++ .../Systems/RandomHumanoidAppearanceSystem.cs | 35 + .../IdentityManagement/IdentitySystem.cs | 4 +- .../MagicMirror/MagicMirrorComponent.cs | 6 + .../MagicMirror/MagicMirrorSystem.cs | 179 +++ .../BiomassReclaimerSystem.cs | 4 +- .../Polymorph/Systems/PolymorphableSystem.cs | 12 +- .../Managers/ServerPreferencesManager.cs | 5 +- .../EntitySystems/RevenantSystem.Abilities.cs | 4 +- Content.Server/Speech/VocalSystem.cs | 6 +- .../Station/Systems/StationSpawningSystem.cs | 10 +- .../Store/Conditions/BuyerSpeciesCondition.cs | 6 +- .../Zombies/ZombifyOnDeathSystem.cs | 21 +- .../Body/Components/SharedBodyComponent.cs | 7 +- .../Components/HumanoidAppearanceComponent.cs | 77 -- .../Components/SharedMagicMirrorComponent.cs | 78 -- .../CharacterAppearance/HairStyles.cs | 31 - .../HumanoidVisualLayersExtension.cs | 95 -- .../SpriteAccessoryCategories.cs | 15 - .../SpriteAccessoryManager.cs | 64 - .../SpriteAccessoryPrototype.cs | 31 - .../Systems/SharedHumanoidAppearanceSystem.cs | 189 --- Content.Shared/Entry/EntryPoint.cs | 4 +- Content.Shared/Humanoid/HairStyles.cs | 18 + .../HumanoidCharacterAppearance.cs | 142 +- .../HumanoidVisualLayers.cs | 2 +- .../Humanoid/HumanoidVisualLayersExtension.cs | 127 ++ .../Humanoid/HumanoidVisualizerKeys.cs | 34 + .../ICharacterAppearance.cs | 2 +- .../{ => Humanoid}/Markings/Marking.cs | 66 +- .../Humanoid/Markings/MarkingCategories.cs | 47 + .../Humanoid/Markings/MarkingManager.cs | 124 ++ .../Humanoid/Markings/MarkingPoints.cs | 51 + .../Markings/MarkingPrototype.cs | 3 +- .../Humanoid/Markings/MarkingsComponent.cs | 26 + .../Humanoid/Markings/MarkingsSet.cs | 778 +++++++++++ .../Prototypes/HumanoidProfilePrototype.cs | 17 + .../Prototypes/HumanoidSpritePrototypes.cs | 76 + .../Prototypes}/SpeciesPrototype.cs | 41 +- .../{CharacterAppearance => Humanoid}/Sex.cs | 4 +- .../Humanoid/SharedHumanoidComponent.cs | 62 + .../SharedHumanoidMarkingModifierSystem.cs | 55 + .../Humanoid/SharedHumanoidSystem.cs | 45 + Content.Shared/Humanoid/SkinColor.cs | 147 ++ Content.Shared/IoC/SharedContentIoC.cs | 4 +- .../MagicMirror/SharedMagicMirrorSystem.cs | 106 ++ Content.Shared/Markings/MarkingCategories.cs | 18 - Content.Shared/Markings/MarkingManager.cs | 64 - Content.Shared/Markings/MarkingsComponent.cs | 57 - Content.Shared/Markings/MarkingsSet.cs | 291 ---- .../Preferences/HumanoidCharacterProfile.cs | 25 +- .../Preferences/ICharacterProfile.cs | 2 +- Content.Shared/Species/SpeciesManager.cs | 6 - Content.Shared/Zombies/ZombieComponent.cs | 6 + .../Server/Preferences/ServerDbSqliteTests.cs | 5 +- .../Locale/en-US/accessories/accesory.ftl | 2 - .../en-US/accessories/human-facial-hair.ftl | 70 +- .../Locale/en-US/accessories/human-hair.ftl | 348 ++--- .../en-US/accessories/vox-facial-hair.ftl | 10 +- .../Locale/en-US/accessories/vox-hair.ftl | 26 +- .../en-US/preferences/ui/markings-picker.ftl | 7 +- .../Markings/human_facial_hair.yml | 245 ++++ .../Customization/Markings/human_hair.yml | 1218 +++++++++++++++++ .../Markings/vox_facial_hair.yml | 40 + .../Mobs/Customization/Markings/vox_hair.yml | 104 ++ .../Prototypes/Entities/Mobs/NPCs/mimic.yml | 2 +- .../Prototypes/Entities/Mobs/Species/base.yml | 194 +-- .../Entities/Mobs/Species/dwarf.yml | 158 --- .../Entities/Mobs/Species/reptilian.yml | 224 +-- .../Entities/Mobs/Species/skeleton.yml | 155 +-- .../Entities/Mobs/Species/slime.yml | 184 +-- .../Prototypes/Entities/Mobs/Species/vox.yml | 90 +- Resources/Prototypes/HairStyles/common.yml | 19 - .../HairStyles/human_facial_hair.yml | 210 --- .../Prototypes/HairStyles/human_hair.yml | 1044 -------------- .../Prototypes/HairStyles/vox_facial_hair.yml | 34 - Resources/Prototypes/HairStyles/vox_hair.yml | 91 -- Resources/Prototypes/Species/dwarf.yml | 9 + Resources/Prototypes/Species/human.yml | 154 +++ Resources/Prototypes/Species/reptilian.yml | 140 ++ Resources/Prototypes/Species/skeleton.yml | 110 ++ Resources/Prototypes/Species/slime.yml | 131 ++ Resources/Prototypes/Species/vox.yml | 138 ++ Resources/Prototypes/species.yml | 50 - ...so_f_Brute_100.png => Chest_Brute_100.png} | Bin ...orso_f_Brute_20.png => Chest_Brute_20.png} | Bin ...orso_f_Brute_40.png => Chest_Brute_40.png} | Bin ...ead_f_Brute_100.png => Head_Brute_100.png} | Bin ...{head_f_Brute_20.png => Head_Brute_20.png} | Bin ...{head_f_Brute_40.png => Head_Brute_40.png} | Bin ...l_arm_Brute_100.png => LArm_Brute_100.png} | Bin .../{l_arm_Brute_20.png => LArm_Brute_20.png} | Bin .../{l_arm_Brute_40.png => LArm_Brute_40.png} | Bin ...l_leg_Brute_100.png => LLeg_Brute_100.png} | Bin .../{l_leg_Brute_20.png => LLeg_Brute_20.png} | Bin .../{l_leg_Brute_40.png => LLeg_Brute_40.png} | Bin ...r_arm_Brute_100.png => RArm_Brute_100.png} | Bin .../{r_arm_Brute_20.png => RArm_Brute_20.png} | Bin .../{r_arm_Brute_40.png => RArm_Brute_40.png} | Bin ...r_leg_Brute_100.png => RLeg_Brute_100.png} | Bin .../{r_leg_Brute_20.png => RLeg_Brute_20.png} | Bin .../{r_leg_Brute_40.png => RLeg_Brute_40.png} | Bin .../brute_damage.rsi/head_m_Brute_100.png | Bin 604 -> 0 bytes .../brute_damage.rsi/head_m_Brute_20.png | Bin 148 -> 0 bytes .../brute_damage.rsi/head_m_Brute_40.png | Bin 207 -> 0 bytes .../Mobs/Effects/brute_damage.rsi/meta.json | 42 +- .../brute_damage.rsi/torso_m_Brute_100.png | Bin 755 -> 0 bytes .../brute_damage.rsi/torso_m_Brute_20.png | Bin 172 -> 0 bytes .../brute_damage.rsi/torso_m_Brute_40.png | Bin 358 -> 0 bytes ...orso_f_Burn_100.png => Chest_Burn_100.png} | Bin ...{torso_f_Burn_20.png => Chest_Burn_20.png} | Bin ...{torso_f_Burn_40.png => Chest_Burn_40.png} | Bin ...{head_f_Burn_100.png => Head_Burn_100.png} | Bin .../{head_f_Burn_20.png => Head_Burn_20.png} | Bin .../{head_f_Burn_40.png => Head_Burn_40.png} | Bin .../{l_arm_Burn_100.png => LArm_Burn_100.png} | Bin .../{l_arm_Burn_20.png => LArm_Burn_20.png} | Bin .../{l_arm_Burn_40.png => LArm_Burn_40.png} | Bin .../{l_leg_Burn_100.png => LLeg_Burn_100.png} | Bin .../{l_leg_Burn_20.png => LLeg_Burn_20.png} | Bin .../{l_leg_Burn_40.png => LLeg_Burn_40.png} | Bin .../{r_arm_Burn_100.png => RArm_Burn_100.png} | Bin .../{r_arm_Burn_20.png => RArm_Burn_20.png} | Bin .../{r_arm_Burn_40.png => RArm_Burn_40.png} | Bin .../{r_leg_Burn_100.png => RLeg_Burn_100.png} | Bin .../{r_leg_Burn_20.png => RLeg_Burn_20.png} | Bin .../{r_leg_Burn_40.png => RLeg_Burn_40.png} | Bin .../burn_damage.rsi/head_m_Burn_100.png | Bin 299 -> 0 bytes .../burn_damage.rsi/head_m_Burn_20.png | Bin 135 -> 0 bytes .../burn_damage.rsi/head_m_Burn_40.png | Bin 163 -> 0 bytes .../Mobs/Effects/burn_damage.rsi/meta.json | 42 +- .../burn_damage.rsi/torso_m_Burn_100.png | Bin 732 -> 0 bytes .../burn_damage.rsi/torso_m_Burn_20.png | Bin 135 -> 0 bytes .../burn_damage.rsi/torso_m_Burn_40.png | Bin 275 -> 0 bytes Tools/markings/go.mod | 9 + Tools/markings/main.go | 7 + Tools/markings/markings/commands.go | 48 + Tools/markings/markings/conversion.go | 16 + Tools/markings/markings/go.mod | 5 + Tools/markings/markings/go.sum | 3 + Tools/markings/markings/prototypes.go | 76 + Tools/markings/markings/yaml.go | 46 + Tools/markings/markings/yaml_test.go | 63 + 184 files changed, 7667 insertions(+), 5209 deletions(-) delete mode 100644 Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs create mode 100644 Content.Client/Humanoid/EyeColorPicker.cs create mode 100644 Content.Client/Humanoid/HumanoidComponent.cs create mode 100644 Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs create mode 100644 Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml create mode 100644 Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs create mode 100644 Content.Client/Humanoid/HumanoidSystem.cs create mode 100644 Content.Client/Humanoid/HumanoidVisualizerSystem.cs rename Content.Client/{Markings => Humanoid}/MarkingPicker.xaml (100%) create mode 100644 Content.Client/Humanoid/MarkingPicker.xaml.cs create mode 100644 Content.Client/Humanoid/SingleMarkingPicker.xaml create mode 100644 Content.Client/Humanoid/SingleMarkingPicker.xaml.cs create mode 100644 Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs create mode 100644 Content.Client/MagicMirror/MagicMirrorWindow.xaml create mode 100644 Content.Client/MagicMirror/MagicMirrorWindow.xaml.cs delete mode 100644 Content.Client/Markings/MarkingPicker.xaml.cs delete mode 100644 Content.Client/Markings/MarkingsSystem.cs delete mode 100644 Content.Server/CharacterAppearance/Components/MagicMirrorComponent.cs delete mode 100644 Content.Server/CharacterAppearance/Components/RandomHumanoidAppearanceComponent.cs delete mode 100644 Content.Server/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs delete mode 100644 Content.Server/CharacterAppearance/Systems/MagicMirrorSystem.cs delete mode 100644 Content.Server/CharacterAppearance/Systems/RandomHumanoidAppearanceSystem.cs create mode 100644 Content.Server/Clothing/ClothingSystem.cs create mode 100644 Content.Server/Humanoid/Components/HumanoidComponent.cs create mode 100644 Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs create mode 100644 Content.Server/Humanoid/Systems/HumanoidSystem.Modifier.cs create mode 100644 Content.Server/Humanoid/Systems/HumanoidSystem.cs create mode 100644 Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs create mode 100644 Content.Server/MagicMirror/MagicMirrorComponent.cs create mode 100644 Content.Server/MagicMirror/MagicMirrorSystem.cs delete mode 100644 Content.Shared/CharacterAppearance/Components/HumanoidAppearanceComponent.cs delete mode 100644 Content.Shared/CharacterAppearance/Components/SharedMagicMirrorComponent.cs delete mode 100644 Content.Shared/CharacterAppearance/HairStyles.cs delete mode 100644 Content.Shared/CharacterAppearance/HumanoidVisualLayersExtension.cs delete mode 100644 Content.Shared/CharacterAppearance/SpriteAccessoryCategories.cs delete mode 100644 Content.Shared/CharacterAppearance/SpriteAccessoryManager.cs delete mode 100644 Content.Shared/CharacterAppearance/SpriteAccessoryPrototype.cs delete mode 100644 Content.Shared/CharacterAppearance/Systems/SharedHumanoidAppearanceSystem.cs create mode 100644 Content.Shared/Humanoid/HairStyles.cs rename Content.Shared/{CharacterAppearance => Humanoid}/HumanoidCharacterAppearance.cs (53%) rename Content.Shared/{CharacterAppearance => Humanoid}/HumanoidVisualLayers.cs (92%) create mode 100644 Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs create mode 100644 Content.Shared/Humanoid/HumanoidVisualizerKeys.cs rename Content.Shared/{CharacterAppearance => Humanoid}/ICharacterAppearance.cs (72%) rename Content.Shared/{ => Humanoid}/Markings/Marking.cs (65%) create mode 100644 Content.Shared/Humanoid/Markings/MarkingCategories.cs create mode 100644 Content.Shared/Humanoid/Markings/MarkingManager.cs create mode 100644 Content.Shared/Humanoid/Markings/MarkingPoints.cs rename Content.Shared/{ => Humanoid}/Markings/MarkingPrototype.cs (92%) create mode 100644 Content.Shared/Humanoid/Markings/MarkingsComponent.cs create mode 100644 Content.Shared/Humanoid/Markings/MarkingsSet.cs create mode 100644 Content.Shared/Humanoid/Prototypes/HumanoidProfilePrototype.cs create mode 100644 Content.Shared/Humanoid/Prototypes/HumanoidSpritePrototypes.cs rename Content.Shared/{Species => Humanoid/Prototypes}/SpeciesPrototype.cs (59%) rename Content.Shared/{CharacterAppearance => Humanoid}/Sex.cs (97%) create mode 100644 Content.Shared/Humanoid/SharedHumanoidComponent.cs create mode 100644 Content.Shared/Humanoid/SharedHumanoidMarkingModifierSystem.cs create mode 100644 Content.Shared/Humanoid/SharedHumanoidSystem.cs create mode 100644 Content.Shared/Humanoid/SkinColor.cs create mode 100644 Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs delete mode 100644 Content.Shared/Markings/MarkingCategories.cs delete mode 100644 Content.Shared/Markings/MarkingManager.cs delete mode 100644 Content.Shared/Markings/MarkingsComponent.cs delete mode 100644 Content.Shared/Markings/MarkingsSet.cs delete mode 100644 Content.Shared/Species/SpeciesManager.cs delete mode 100644 Resources/Locale/en-US/accessories/accesory.ftl create mode 100644 Resources/Prototypes/Entities/Mobs/Customization/Markings/human_facial_hair.yml create mode 100644 Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml create mode 100644 Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_facial_hair.yml create mode 100644 Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_hair.yml delete mode 100644 Resources/Prototypes/HairStyles/common.yml delete mode 100644 Resources/Prototypes/HairStyles/human_facial_hair.yml delete mode 100644 Resources/Prototypes/HairStyles/human_hair.yml delete mode 100644 Resources/Prototypes/HairStyles/vox_facial_hair.yml delete mode 100644 Resources/Prototypes/HairStyles/vox_hair.yml create mode 100644 Resources/Prototypes/Species/dwarf.yml create mode 100644 Resources/Prototypes/Species/human.yml create mode 100644 Resources/Prototypes/Species/reptilian.yml create mode 100644 Resources/Prototypes/Species/skeleton.yml create mode 100644 Resources/Prototypes/Species/slime.yml create mode 100644 Resources/Prototypes/Species/vox.yml delete mode 100644 Resources/Prototypes/species.yml rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{torso_f_Brute_100.png => Chest_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{torso_f_Brute_20.png => Chest_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{torso_f_Brute_40.png => Chest_Brute_40.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{head_f_Brute_100.png => Head_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{head_f_Brute_20.png => Head_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{head_f_Brute_40.png => Head_Brute_40.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_arm_Brute_100.png => LArm_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_arm_Brute_20.png => LArm_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_arm_Brute_40.png => LArm_Brute_40.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_leg_Brute_100.png => LLeg_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_leg_Brute_20.png => LLeg_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{l_leg_Brute_40.png => LLeg_Brute_40.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_arm_Brute_100.png => RArm_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_arm_Brute_20.png => RArm_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_arm_Brute_40.png => RArm_Brute_40.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_leg_Brute_100.png => RLeg_Brute_100.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_leg_Brute_20.png => RLeg_Brute_20.png} (100%) rename Resources/Textures/Mobs/Effects/brute_damage.rsi/{r_leg_Brute_40.png => RLeg_Brute_40.png} (100%) delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/head_m_Brute_100.png delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/head_m_Brute_20.png delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/head_m_Brute_40.png delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/torso_m_Brute_100.png delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/torso_m_Brute_20.png delete mode 100644 Resources/Textures/Mobs/Effects/brute_damage.rsi/torso_m_Brute_40.png rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{torso_f_Burn_100.png => Chest_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{torso_f_Burn_20.png => Chest_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{torso_f_Burn_40.png => Chest_Burn_40.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{head_f_Burn_100.png => Head_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{head_f_Burn_20.png => Head_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{head_f_Burn_40.png => Head_Burn_40.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_arm_Burn_100.png => LArm_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_arm_Burn_20.png => LArm_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_arm_Burn_40.png => LArm_Burn_40.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_leg_Burn_100.png => LLeg_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_leg_Burn_20.png => LLeg_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{l_leg_Burn_40.png => LLeg_Burn_40.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_arm_Burn_100.png => RArm_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_arm_Burn_20.png => RArm_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_arm_Burn_40.png => RArm_Burn_40.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_leg_Burn_100.png => RLeg_Burn_100.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_leg_Burn_20.png => RLeg_Burn_20.png} (100%) rename Resources/Textures/Mobs/Effects/burn_damage.rsi/{r_leg_Burn_40.png => RLeg_Burn_40.png} (100%) delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/head_m_Burn_100.png delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/head_m_Burn_20.png delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/head_m_Burn_40.png delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/torso_m_Burn_100.png delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/torso_m_Burn_20.png delete mode 100644 Resources/Textures/Mobs/Effects/burn_damage.rsi/torso_m_Burn_40.png create mode 100644 Tools/markings/go.mod create mode 100644 Tools/markings/main.go create mode 100644 Tools/markings/markings/commands.go create mode 100644 Tools/markings/markings/conversion.go create mode 100644 Tools/markings/markings/go.mod create mode 100644 Tools/markings/markings/go.sum create mode 100644 Tools/markings/markings/prototypes.go create mode 100644 Tools/markings/markings/yaml.go create mode 100644 Tools/markings/markings/yaml_test.go diff --git a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs index 924f4093cb..e69de29bb2 100644 --- a/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs +++ b/Content.Client/CharacterAppearance/MagicMirrorBoundUserInterface.cs @@ -1,269 +0,0 @@ -using System; -using System.Linq; -using Content.Client.Stylesheets; -using Content.Shared.CharacterAppearance; -using JetBrains.Annotations; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Client.UserInterface.CustomControls; -using Robust.Client.Utility; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Localization; -using Robust.Shared.Maths; -using static Content.Shared.CharacterAppearance.Components.SharedMagicMirrorComponent; -using static Robust.Client.UserInterface.Controls.BoxContainer; - -namespace Content.Client.CharacterAppearance -{ - [UsedImplicitly] - public sealed class MagicMirrorBoundUserInterface : BoundUserInterface - { - private MagicMirrorWindow? _window; - - public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) - { - } - - protected override void Open() - { - base.Open(); - - _window = new MagicMirrorWindow(this); - _window.OnClose += Close; - _window.Open(); - } - - protected override void ReceiveMessage(BoundUserInterfaceMessage message) - { - switch (message) - { - case MagicMirrorInitialDataMessage initialData: - _window?.SetInitialData(initialData); - break; - } - } - - internal void HairSelected(string name, bool isFacialHair) - { - SendMessage(new HairSelectedMessage(name, isFacialHair)); - } - - internal void HairColorSelected(Color color, bool isFacialHair) - { - SendMessage(new HairColorSelectedMessage((color.RByte, color.GByte, color.BByte), - isFacialHair)); - } - - internal void EyeColorSelected(Color color) - { - SendMessage(new EyeColorSelectedMessage((color.RByte, color.GByte, color.BByte))); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _window?.Dispose(); - } - } - } - - public sealed class HairStylePicker : Control - { - [Dependency] private readonly SpriteAccessoryManager _spriteAccessoryManager = default!; - - public event Action? OnHairColorPicked; - public event Action? OnHairStylePicked; - - private readonly ItemList _items; - - private readonly Control _colorContainer; - private readonly ColorSelectorSliders _colorSelectors; - private Color _lastColor; - private SpriteAccessoryCategories _categories; - - public void SetData(Color color, string styleId, SpriteAccessoryCategories categories, bool canColor) - { - if (_categories != categories) - { - _categories = categories; - Populate(); - } - - _colorContainer.Visible = canColor; - _lastColor = color; - - _colorSelectors.Color = color; - - foreach (var item in _items) - { - var prototype = (SpriteAccessoryPrototype) item.Metadata!; - item.Selected = prototype.ID == styleId; - } - - UpdateStylePickerColor(); - } - - private void UpdateStylePickerColor() - { - foreach (var item in _items) - { - item.IconModulate = _lastColor; - } - } - - public HairStylePicker() - { - IoCManager.InjectDependencies(this); - - var vBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - AddChild(vBox); - - _colorContainer = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - vBox.AddChild(_colorContainer); - _colorContainer.AddChild(_colorSelectors = new ()); - _colorSelectors.OnColorChanged += color => ColorValueChanged(color); - - _items = new ItemList - { - VerticalExpand = true, - MinSize = (300, 250) - }; - vBox.AddChild(_items); - _items.OnItemSelected += ItemSelected; - } - - private void ColorValueChanged(Color newColor) - { - OnHairColorPicked?.Invoke(newColor); - _lastColor = newColor; - UpdateStylePickerColor(); - } - - public void Populate() - { - var styles = _spriteAccessoryManager - .AccessoriesForCategory(_categories) - .ToList(); - styles.Sort(HairStyles.SpriteAccessoryComparer); - - foreach (var style in styles) - { - var item = _items.AddItem(style.Name, style.Sprite.Frame0()); - item.Metadata = style; - } - } - - private void ItemSelected(ItemList.ItemListSelectedEventArgs args) - { - var prototype = (SpriteAccessoryPrototype?) _items[args.ItemIndex].Metadata; - var style = prototype?.ID; - - if (style != null) - { - OnHairStylePicked?.Invoke(style); - } - } - - // ColorSlider - } - - public sealed class EyeColorPicker : Control - { - public event Action? OnEyeColorPicked; - - private readonly ColorSelectorSliders _colorSelectors; - - private Color _lastColor; - - public void SetData(Color color) - { - _lastColor = color; - - _colorSelectors.Color = color; - } - - public EyeColorPicker() - { - var vBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical - }; - AddChild(vBox); - - vBox.AddChild(_colorSelectors = new ColorSelectorSliders()); - - _colorSelectors.OnColorChanged += ColorValueChanged; - } - - private void ColorValueChanged(Color newColor) - { - OnEyeColorPicked?.Invoke(newColor); - - _lastColor = newColor; - } - - // ColorSlider - } - - public sealed class MagicMirrorWindow : DefaultWindow - { - private readonly HairStylePicker _hairStylePicker; - private readonly HairStylePicker _facialHairStylePicker; - private readonly EyeColorPicker _eyeColorPicker; - - public MagicMirrorWindow(MagicMirrorBoundUserInterface owner) - { - SetSize = MinSize = (500, 360); - Title = Loc.GetString("magic-mirror-window-title"); - - _hairStylePicker = new HairStylePicker {HorizontalExpand = true}; - _hairStylePicker.OnHairStylePicked += newStyle => owner.HairSelected(newStyle, false); - _hairStylePicker.OnHairColorPicked += newColor => owner.HairColorSelected(newColor, false); - - _facialHairStylePicker = new HairStylePicker {HorizontalExpand = true}; - _facialHairStylePicker.OnHairStylePicked += newStyle => owner.HairSelected(newStyle, true); - _facialHairStylePicker.OnHairColorPicked += newColor => owner.HairColorSelected(newColor, true); - - _eyeColorPicker = new EyeColorPicker { HorizontalExpand = true }; - _eyeColorPicker.OnEyeColorPicked += newColor => owner.EyeColorSelected(newColor); - - Contents.AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 8, - Children = {_hairStylePicker, _facialHairStylePicker, _eyeColorPicker} - }); - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing) - { - _hairStylePicker.Dispose(); - _facialHairStylePicker.Dispose(); - _eyeColorPicker.Dispose(); - } - } - - public void SetInitialData(MagicMirrorInitialDataMessage initialData) - { - _facialHairStylePicker.SetData(initialData.FacialHairColor, initialData.FacialHairId, initialData.CategoriesFacialHair, initialData.CanColorFacialHair); - _hairStylePicker.SetData(initialData.HairColor, initialData.HairId, initialData.CategoriesHair, initialData.CanColorHair); - _eyeColorPicker.SetData(initialData.EyeColor); - } - } -} diff --git a/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs b/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs deleted file mode 100644 index 328d4f0e62..0000000000 --- a/Content.Client/CharacterAppearance/Systems/HumanoidAppearanceSystem.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Content.Client.Cuffs.Components; -using Content.Shared.Body.Components; -using Content.Shared.CharacterAppearance; -using Content.Shared.CharacterAppearance.Components; -using Content.Shared.CharacterAppearance.Systems; -using Robust.Client.GameObjects; -using Robust.Shared.Prototypes; - -namespace Content.Client.CharacterAppearance.Systems -{ - public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem - { - [Dependency] private readonly SpriteAccessoryManager _accessoryManager = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(UpdateLooks); - SubscribeLocalEvent(BodyPartAdded); - SubscribeLocalEvent(BodyPartRemoved); - } - - public readonly static HumanoidVisualLayers[] BodyPartLayers = { - HumanoidVisualLayers.Chest, - HumanoidVisualLayers.Head, - HumanoidVisualLayers.Snout, - HumanoidVisualLayers.HeadTop, - HumanoidVisualLayers.HeadSide, - HumanoidVisualLayers.Tail, - HumanoidVisualLayers.Eyes, - HumanoidVisualLayers.RArm, - HumanoidVisualLayers.LArm, - HumanoidVisualLayers.RHand, - HumanoidVisualLayers.LHand, - HumanoidVisualLayers.RLeg, - HumanoidVisualLayers.LLeg, - HumanoidVisualLayers.RFoot, - HumanoidVisualLayers.LFoot - }; - - private void UpdateLooks(EntityUid uid, HumanoidAppearanceComponent component, - ChangedHumanoidAppearanceEvent args) - { - var spriteQuery = EntityManager.GetEntityQuery(); - - if (!spriteQuery.TryGetComponent(uid, out var sprite)) - return; - - if (EntityManager.TryGetComponent(uid, out SharedBodyComponent? body)) - { - foreach (var (part, _) in body.Parts) - { - if (spriteQuery.TryGetComponent(part.Owner, out var partSprite)) - { - partSprite.Color = component.Appearance.SkinColor; - } - } - } - - // Like body parts some stuff may not have hair. - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hairLayer)) - { - var hairColor = component.CanColorHair ? component.Appearance.HairColor : Color.White; - hairColor = component.HairMatchesSkin ? component.Appearance.SkinColor : hairColor; - sprite.LayerSetColor(hairLayer, hairColor.WithAlpha(component.HairAlpha)); - - var hairStyle = component.Appearance.HairStyleId; - if (string.IsNullOrWhiteSpace(hairStyle) || - !_accessoryManager.IsValidAccessoryInCategory(hairStyle, component.CategoriesHair)) - { - hairStyle = HairStyles.DefaultHairStyle; - } - - var hairPrototype = _prototypeManager.Index(hairStyle); - sprite.LayerSetSprite(hairLayer, hairPrototype.Sprite); - } - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facialLayer)) - { - var facialHairColor = component.CanColorHair ? component.Appearance.FacialHairColor : Color.White; - facialHairColor = component.HairMatchesSkin ? component.Appearance.SkinColor : facialHairColor; - sprite.LayerSetColor(facialLayer, facialHairColor.WithAlpha(component.HairAlpha)); - - var facialHairStyle = component.Appearance.FacialHairStyleId; - if (string.IsNullOrWhiteSpace(facialHairStyle) || - !_accessoryManager.IsValidAccessoryInCategory(facialHairStyle, component.CategoriesFacialHair)) - { - facialHairStyle = HairStyles.DefaultFacialHairStyle; - } - - var facialHairPrototype = _prototypeManager.Index(facialHairStyle); - sprite.LayerSetSprite(facialLayer, facialHairPrototype.Sprite); - } - - foreach (var layer in BodyPartLayers) - { - // Not every mob may have the furry layers hence we just skip it. - if (!sprite.LayerMapTryGet(layer, out var actualLayer)) continue; - if (!sprite[actualLayer].Visible) continue; - - sprite.LayerSetColor(actualLayer, component.Appearance.SkinColor); - } - - sprite.LayerSetColor(HumanoidVisualLayers.Eyes, component.Appearance.EyeColor); - sprite.LayerSetState(HumanoidVisualLayers.Chest, component.Sex == Sex.Male ? "torso_m" : "torso_f"); - sprite.LayerSetState(HumanoidVisualLayers.Head, component.Sex == Sex.Male ? "head_m" : "head_f"); - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.StencilMask, out _)) - sprite.LayerSetVisible(HumanoidVisualLayers.StencilMask, component.Sex == Sex.Female); - - if (EntityManager.TryGetComponent(uid, out var cuffed)) - { - sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, !cuffed.CanStillInteract); - } - else - { - sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false); - } - } - - // Scaffolding until Body is moved to ECS. - private void BodyPartAdded(HumanoidAppearanceBodyPartAddedEvent args) - { - if (!EntityManager.TryGetComponent(args.Uid, out SpriteComponent? sprite)) - { - return; - } - - if (!EntityManager.HasComponent(args.Args.Part.Owner)) - { - return; - } - - var layers = args.Args.Part.ToHumanoidLayers(); - // TODO BODY Layer color, sprite and state - foreach (var layer in layers) - { - if (!sprite.LayerMapTryGet(layer, out _)) - continue; - - sprite.LayerSetVisible(layer, true); - } - } - - private void BodyPartRemoved(HumanoidAppearanceBodyPartRemovedEvent args) - { - if (!EntityManager.TryGetComponent(args.Uid, out SpriteComponent? sprite)) - { - return; - } - - if (!EntityManager.HasComponent(args.Args.Part.Owner)) - { - return; - } - - var layers = args.Args.Part.ToHumanoidLayers(); - // TODO BODY Layer color, sprite and state - foreach (var layer in layers) - sprite.LayerSetVisible(layer, false); - } - } -} diff --git a/Content.Client/Clothing/ClothingVisualsSystem.cs b/Content.Client/Clothing/ClothingVisualsSystem.cs index 967a96d34d..aecf94eabd 100644 --- a/Content.Client/Clothing/ClothingVisualsSystem.cs +++ b/Content.Client/Clothing/ClothingVisualsSystem.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client.Inventory; -using Content.Shared.CharacterAppearance; using Content.Shared.Clothing; using Content.Shared.Clothing.Components; +using Content.Shared.Humanoid; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; @@ -150,17 +150,6 @@ public sealed class ClothingVisualsSystem : EntitySystem private void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) { - if (component.InSlot == "head" - && _tagSystem.HasTag(uid, "HidesHair") - && TryComp(args.Equipee, out SpriteComponent? sprite)) - { - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facial)) - sprite[facial].Visible = true; - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hair)) - sprite[hair].Visible = true; - } - component.InSlot = null; } @@ -198,17 +187,6 @@ public sealed class ClothingVisualsSystem : EntitySystem { component.InSlot = args.Slot; - if (args.Slot == "head" - && _tagSystem.HasTag(uid, "HidesHair") - && TryComp(args.Equipee, out SpriteComponent? sprite)) - { - if (sprite.LayerMapTryGet(HumanoidVisualLayers.FacialHair, out var facial)) - sprite[facial].Visible = false; - - if (sprite.LayerMapTryGet(HumanoidVisualLayers.Hair, out var hair)) - sprite[hair].Visible = false; - } - RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component); } diff --git a/Content.Client/Cuffs/Components/CuffableComponent.cs b/Content.Client/Cuffs/Components/CuffableComponent.cs index b923491377..a074c044fb 100644 --- a/Content.Client/Cuffs/Components/CuffableComponent.cs +++ b/Content.Client/Cuffs/Components/CuffableComponent.cs @@ -1,6 +1,6 @@ using Content.Shared.ActionBlocker; -using Content.Shared.CharacterAppearance; using Content.Shared.Cuffs.Components; +using Content.Shared.Humanoid; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; diff --git a/Content.Client/Damage/DamageVisualsSystem.cs b/Content.Client/Damage/DamageVisualsSystem.cs index adf245e4a5..9e24a90e2a 100644 --- a/Content.Client/Damage/DamageVisualsSystem.cs +++ b/Content.Client/Damage/DamageVisualsSystem.cs @@ -230,8 +230,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem? OnEyeColorPicked; + + private readonly ColorSelectorSliders _colorSelectors; + + private Color _lastColor; + + public void SetData(Color color) + { + _lastColor = color; + + _colorSelectors.Color = color; + } + + public EyeColorPicker() + { + var vBox = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Vertical + }; + AddChild(vBox); + + vBox.AddChild(_colorSelectors = new ColorSelectorSliders()); + + _colorSelectors.OnColorChanged += ColorValueChanged; + } + + private void ColorValueChanged(Color newColor) + { + OnEyeColorPicked?.Invoke(newColor); + + _lastColor = newColor; + } +} diff --git a/Content.Client/Humanoid/HumanoidComponent.cs b/Content.Client/Humanoid/HumanoidComponent.cs new file mode 100644 index 0000000000..d433013c9d --- /dev/null +++ b/Content.Client/Humanoid/HumanoidComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; + +namespace Content.Client.Humanoid; + +[RegisterComponent] +public sealed class HumanoidComponent : SharedHumanoidComponent +{ + [ViewVariables] public List CurrentMarkings = new(); + + public Dictionary BaseLayers = new(); + + public string LastSpecies = default!; +} diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs new file mode 100644 index 0000000000..75942ba56d --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs @@ -0,0 +1,62 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; + +namespace Content.Client.Humanoid; + +// Marking BUI. +// Do not use this in any non-privileged instance. This just replaces an entire marking set +// with the set sent over. + +public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterface +{ + public HumanoidMarkingModifierBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) + { + } + + private HumanoidMarkingModifierWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = new(); + _window.OnClose += Close; + _window.OnMarkingAdded += SendMarkingSet; + _window.OnMarkingRemoved += SendMarkingSet; + _window.OnMarkingColorChange += SendMarkingSetNoResend; + _window.OnMarkingRankChange += SendMarkingSet; + _window.OnLayerInfoModified += SendBaseLayer; + + _window.OpenCenteredLeft(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (_window == null || state is not HumanoidMarkingModifierState cast) + { + return; + } + + _window.SetState(cast.MarkingSet, cast.Species, cast.SkinColor, cast.CustomBaseLayers); + } + + private void SendMarkingSet(MarkingSet set) + { + SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, true)); + } + + private void SendMarkingSetNoResend(MarkingSet set) + { + SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, false)); + } + + private void SendBaseLayer(HumanoidVisualLayers layer, CustomBaseLayerInfo? info) + { + SendMessage(new HumanoidMarkingModifierBaseLayersSetMessage(layer, info, true)); + } +} + + diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml new file mode 100644 index 0000000000..d32d3ba2cf --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs new file mode 100644 index 0000000000..2f6c396096 --- /dev/null +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs @@ -0,0 +1,138 @@ +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Humanoid; + +// hack for a panel that modifies an entity's markings on demand + +[GenerateTypedNameReferences] +public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow +{ + public Action? OnMarkingAdded; + public Action? OnMarkingRemoved; + public Action? OnMarkingColorChange; + public Action? OnMarkingRankChange; + public Action? OnLayerInfoModified; + + private readonly Dictionary _modifiers = new(); + + public HumanoidMarkingModifierWindow() + { + RobustXamlLoader.Load(this); + + foreach (var layer in Enum.GetValues()) + { + var modifier = new HumanoidBaseLayerModifier(layer); + BaseLayersContainer.AddChild(modifier); + _modifiers.Add(layer, modifier); + + modifier.OnStateChanged += delegate + { + OnLayerInfoModified!( + layer, + modifier.Enabled + ? new CustomBaseLayerInfo(modifier.State, modifier.Color) + : null); + }; + } + + MarkingPickerWidget.OnMarkingAdded += set => OnMarkingAdded!(set); + MarkingPickerWidget.OnMarkingRemoved += set => OnMarkingRemoved!(set); + MarkingPickerWidget.OnMarkingColorChange += set => OnMarkingColorChange!(set); + MarkingPickerWidget.OnMarkingRankChange += set => OnMarkingRankChange!(set); + MarkingForced.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; + MarkingIgnoreSpecies.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; + + MarkingPickerWidget.Forced = MarkingForced.Pressed; + MarkingPickerWidget.IgnoreSpecies = MarkingForced.Pressed; + } + + public void SetState(MarkingSet markings, string species, Color skinColor, Dictionary info) + { + MarkingPickerWidget.SetData(markings, species, skinColor); + + foreach (var (layer, modifier) in _modifiers) + { + if (!info.TryGetValue(layer, out var layerInfo)) + { + modifier.SetState(false, string.Empty, Color.White); + continue; + } + + modifier.SetState(true, layerInfo.ID, layerInfo.Color); + } + } + + private sealed class HumanoidBaseLayerModifier : BoxContainer + { + private CheckBox _enable; + private LineEdit _lineEdit; + private ColorSelectorSliders _colorSliders; + private BoxContainer _infoBox; + + public bool Enabled => _enable.Pressed; + public string State => _lineEdit.Text; + public Color Color => _colorSliders.Color; + + public Action? OnStateChanged; + + public HumanoidBaseLayerModifier(HumanoidVisualLayers layer) + { + HorizontalExpand = true; + Orientation = LayoutOrientation.Vertical; + var labelBox = new BoxContainer + { + MinWidth = 250, + HorizontalExpand = true + }; + AddChild(labelBox); + + labelBox.AddChild(new Label + { + HorizontalExpand = true, + Text = layer.ToString() + }); + _enable = new CheckBox + { + Text = "Enable", + HorizontalAlignment = HAlignment.Right + }; + + labelBox.AddChild(_enable); + _infoBox = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + Visible = false + }; + _enable.OnToggled += args => + { + _infoBox.Visible = args.Pressed; + OnStateChanged!(); + }; + + var lineEditBox = new BoxContainer(); + lineEditBox.AddChild(new Label { Text = "Prototype id: "}); + _lineEdit = new(); + _lineEdit.OnTextEntered += args => OnStateChanged!(); + lineEditBox.AddChild(_lineEdit); + _infoBox.AddChild(lineEditBox); + + _colorSliders = new(); + _colorSliders.OnColorChanged += color => OnStateChanged!(); + _infoBox.AddChild(_colorSliders); + AddChild(_infoBox); + } + + public void SetState(bool enabled, string state, Color color) + { + _enable.Pressed = enabled; + _infoBox.Visible = enabled; + _lineEdit.Text = state; + _colorSliders.Color = color; + } + } +} diff --git a/Content.Client/Humanoid/HumanoidSystem.cs b/Content.Client/Humanoid/HumanoidSystem.cs new file mode 100644 index 0000000000..7051b8711f --- /dev/null +++ b/Content.Client/Humanoid/HumanoidSystem.cs @@ -0,0 +1,63 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; +using Robust.Shared.Prototypes; + +namespace Content.Client.Humanoid; + +public sealed class HumanoidSystem : SharedHumanoidSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MarkingManager _markingManager = default!; + + /// + /// Loads a profile directly into a humanoid. + /// + /// The humanoid entity's UID + /// The profile to load. + /// The humanoid entity's humanoid component. + /// + /// This should not be used if the entity is owned by the server. The server will otherwise + /// override this with the appearance data it sends over. + /// + public void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidComponent? humanoid = null) + { + if (!Resolve(uid, ref humanoid)) + { + return; + } + + humanoid.Species = profile.Species; + var customBaseLayers = new Dictionary + { + [HumanoidVisualLayers.Eyes] = new CustomBaseLayerInfo(string.Empty, profile.Appearance.EyeColor) + }; + + var speciesPrototype = _prototypeManager.Index(profile.Species); + var markings = new MarkingSet(profile.Appearance.Markings, speciesPrototype.MarkingPoints, _markingManager, + _prototypeManager); + markings.EnsureDefault(profile.Appearance.SkinColor, _markingManager); + + // legacy: remove in the future? + markings.RemoveCategory(MarkingCategories.Hair); + markings.RemoveCategory(MarkingCategories.FacialHair); + + var hair = new Marking(profile.Appearance.HairStyleId, new[] { profile.Appearance.HairColor }); + markings.AddBack(MarkingCategories.Hair, hair); + + var facialHair = new Marking(profile.Appearance.FacialHairStyleId, + new[] { profile.Appearance.FacialHairColor }); + markings.AddBack(MarkingCategories.FacialHair, facialHair); + + markings.FilterSpecies(profile.Species, _markingManager, _prototypeManager); + + SetAppearance(uid, + profile.Species, + customBaseLayers, + profile.Appearance.SkinColor, + new(), // doesn't exist yet + markings.GetForwardEnumerator().ToList()); + } +} diff --git a/Content.Client/Humanoid/HumanoidVisualizerSystem.cs b/Content.Client/Humanoid/HumanoidVisualizerSystem.cs new file mode 100644 index 0000000000..f6152d3a06 --- /dev/null +++ b/Content.Client/Humanoid/HumanoidVisualizerSystem.cs @@ -0,0 +1,447 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Humanoid; + +public sealed class HumanoidVisualizerSystem : VisualizerSystem +{ + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private MarkingManager _markingManager = default!; + + protected override void OnAppearanceChange(EntityUid uid, HumanoidComponent component, ref AppearanceChangeEvent args) + { + base.OnAppearanceChange(uid, component, ref args); + + if (args.Sprite == null) + { + return; + } + + if (!args.AppearanceData.TryGetValue(HumanoidVisualizerKey.Key, out var dataRaw) + || dataRaw is not HumanoidVisualizerData data) + { + return; + } + + if (!_prototypeManager.TryIndex(data.Species, out SpeciesPrototype? speciesProto) + || !_prototypeManager.TryIndex(speciesProto.SpriteSet, out HumanoidSpeciesBaseSpritesPrototype? baseSprites)) + { + return; + } + + bool dirty; + if (data.CustomBaseLayerInfo.Count != 0) + { + dirty = MergeCustomBaseSprites(uid, baseSprites.Sprites, data.CustomBaseLayerInfo, component); + } + else + { + dirty = MergeCustomBaseSprites(uid, baseSprites.Sprites, null, component); + } + + if (dirty) + { + ApplyBaseSprites(uid, component, args.Sprite); + ApplySkinColor(uid, data.SkinColor, component, args.Sprite); + } + + if (data.CustomBaseLayerInfo.Count != 0) + { + foreach (var (layer, info) in data.CustomBaseLayerInfo) + { + SetBaseLayerColor(uid, layer, info.Color, args.Sprite); + } + } + + var layerVis = data.LayerVisibility.ToHashSet(); + dirty |= ReplaceHiddenLayers(uid, layerVis, component, args.Sprite); + + DiffAndApplyMarkings(uid, data.Markings, dirty, component, args.Sprite); + } + + private bool ReplaceHiddenLayers(EntityUid uid, HashSet hiddenLayers, + HumanoidComponent humanoid, SpriteComponent sprite) + { + if (hiddenLayers.SetEquals(humanoid.HiddenLayers)) + { + return false; + } + + SetSpriteVisibility(uid, hiddenLayers, false, sprite); + + humanoid.HiddenLayers.ExceptWith(hiddenLayers); + + SetSpriteVisibility(uid, humanoid.HiddenLayers, true, sprite); + + humanoid.HiddenLayers.Clear(); + humanoid.HiddenLayers.UnionWith(hiddenLayers); + + return true; + } + + private void SetSpriteVisibility(EntityUid uid, HashSet layers, bool visibility, SpriteComponent sprite) + { + foreach (var layer in layers) + { + if (!sprite.LayerMapTryGet(layer, out var index)) + { + continue; + } + + sprite[index].Visible = visibility; + } + } + + private void DiffAndApplyMarkings(EntityUid uid, + List newMarkings, + bool layersDirty, + HumanoidComponent humanoid, + SpriteComponent sprite) + { + // skip this entire thing if both sets are empty + if (humanoid.CurrentMarkings.Count == 0 && newMarkings.Count == 0) + { + return; + } + + var dirtyMarkings = new List(); + var dirtyRangeStart = humanoid.CurrentMarkings.Count == 0 ? 0 : -1; + + // edge cases: + // humanoid.CurrentMarkings < newMarkings.Count + // - check if count matches this condition before diffing + // - if count is unequal, set dirty range to start from humanoid.CurrentMarkings.Count + // humanoid.CurrentMarkings > newMarkings.Count, no dirty markings + // - break count upon meeting this condition + // - clear markings from newMarkings.Count to humanoid.CurrentMarkings.Count - newMarkings.Count + + for (var i = 0; i < humanoid.CurrentMarkings.Count; i++) + { + // if we've reached the end of the new set of markings, + // then that means it's time to finish + if (newMarkings.Count == i) + { + break; + } + + // if the marking is different here, set the range start to i and break, we need + // to rebuild all markings starting from i + if (humanoid.CurrentMarkings[i].MarkingId != newMarkings[i].MarkingId) + { + dirtyRangeStart = i; + break; + } + + // otherwise, we add the current marking to dirtyMarkings if it has different + // settings + // however: if the hidden layers are set to dirty, then we need to + // instead just add every single marking, since we don't know ahead of time + // where these markings go + if (humanoid.CurrentMarkings[i] != newMarkings[i] || layersDirty) + { + dirtyMarkings.Add(i); + } + } + + foreach (var i in dirtyMarkings) + { + if (!_markingManager.TryGetMarking(newMarkings[i], out var dirtyMarking)) + { + continue; + } + + ApplyMarking(uid, dirtyMarking, newMarkings[i].MarkingColors, newMarkings[i].Visible, humanoid, sprite); + } + + if (humanoid.CurrentMarkings.Count < newMarkings.Count && dirtyRangeStart < 0) + { + dirtyRangeStart = humanoid.CurrentMarkings.Count; + } + + if (dirtyRangeStart >= 0) + { + var range = newMarkings.GetRange(dirtyRangeStart, newMarkings.Count - dirtyRangeStart); + + if (humanoid.CurrentMarkings.Count > 0) + { + var oldRange = humanoid.CurrentMarkings.GetRange(dirtyRangeStart, humanoid.CurrentMarkings.Count - dirtyRangeStart); + ClearMarkings(uid, oldRange, humanoid, sprite); + } + + ApplyMarkings(uid, range, humanoid, sprite); + } + else if (humanoid.CurrentMarkings.Count != newMarkings.Count) + { + if (newMarkings.Count == 0) + { + ClearAllMarkings(uid, humanoid, sprite); + } + else if (humanoid.CurrentMarkings.Count > newMarkings.Count) + { + var rangeStart = newMarkings.Count; + var rangeCount = humanoid.CurrentMarkings.Count - newMarkings.Count; + var range = humanoid.CurrentMarkings.GetRange(rangeStart, rangeCount); + + ClearMarkings(uid, range, humanoid, sprite); + } + } + + if (dirtyMarkings.Count > 0 || dirtyRangeStart >= 0 || humanoid.CurrentMarkings.Count != newMarkings.Count) + { + humanoid.CurrentMarkings = newMarkings; + } + } + + private void ClearAllMarkings(EntityUid uid, HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + ClearMarkings(uid, humanoid.CurrentMarkings, humanoid, spriteComp); + } + + private void ClearMarkings(EntityUid uid, List markings, HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var marking in markings) + { + RemoveMarking(uid, marking, spriteComp); + } + } + + private void RemoveMarking(EntityUid uid, Marking marking, + SpriteComponent spriteComp) + { + if (!_markingManager.TryGetMarking(marking, out var prototype)) + { + return; + } + + foreach (var sprite in prototype.Sprites) + { + if (sprite is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{marking.MarkingId}-{rsi.RsiState}"; + if (!spriteComp.LayerMapTryGet(layerId, out var index)) + { + continue; + } + + spriteComp.LayerMapRemove(layerId); + spriteComp.RemoveLayer(index); + } + } + + private void ApplyMarkings(EntityUid uid, + List markings, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var marking in new ReverseMarkingEnumerator(markings)) + { + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + { + continue; + } + + ApplyMarking(uid, markingPrototype, marking.MarkingColors, marking.Visible, humanoid, spriteComp); + } + } + + private void ApplyMarking(EntityUid uid, + MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + HumanoidComponent humanoid, + SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(markingPrototype.BodyPart, out int targetLayer)) + { + return; + } + + visible &= !humanoid.HiddenLayers.Contains(markingPrototype.BodyPart); + visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting) + && setting.AllowsMarkings; + + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + if (markingPrototype.Sprites[j] is not SpriteSpecifier.Rsi rsi) + { + continue; + } + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingPrototype.Sprites[j], targetLayer + j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible || setting == null) // this is kinda implied + { + continue; + } + + if (markingPrototype.FollowSkinColor || colors == null || setting.MarkingsMatchSkin) + { + var skinColor = humanoid.SkinColor; + skinColor.A = setting.LayerAlpha; + + sprite.LayerSetColor(layerId, skinColor); + } + else + { + sprite.LayerSetColor(layerId, colors[j]); + } + } + } + + private void ApplySkinColor(EntityUid uid, + Color skinColor, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + humanoid.SkinColor = skinColor; + + foreach (var (layer, spriteInfo) in humanoid.BaseLayers) + { + if (!spriteInfo.MatchSkin) + { + continue; + } + + var color = skinColor; + color.A = spriteInfo.LayerAlpha; + + SetBaseLayerColor(uid, layer, color, spriteComp); + } + } + + private void SetBaseLayerColor(EntityUid uid, HumanoidVisualLayers layer, Color color, + SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(layer, out var index)) + { + return; + } + + sprite[index].Color = color; + } + + private bool MergeCustomBaseSprites(EntityUid uid, Dictionary baseSprites, + Dictionary? customBaseSprites, + HumanoidComponent humanoid) + { + var newBaseLayers = new Dictionary(); + + foreach (var (key, id) in baseSprites) + { + var sexMorph = humanoid.Sex switch + { + Sex.Male when HumanoidVisualLayersExtension.HasSexMorph(key) => $"{id}Male", + Sex.Female when HumanoidVisualLayersExtension.HasSexMorph(key) => $"{id}Female", + _ => id + }; + + if (!_prototypeManager.TryIndex(sexMorph, out HumanoidSpeciesSpriteLayer? baseLayer)) + { + continue; + } + + if (!newBaseLayers.TryAdd(key, baseLayer)) + { + newBaseLayers[key] = baseLayer; + } + } + + if (customBaseSprites == null) + { + return IsDirty(newBaseLayers); + } + + foreach (var (key, info) in customBaseSprites) + { + if (!_prototypeManager.TryIndex(info.ID, out HumanoidSpeciesSpriteLayer? baseLayer)) + { + continue; + } + + if (!newBaseLayers.TryAdd(key, baseLayer)) + { + newBaseLayers[key] = baseLayer; + } + } + + bool IsDirty(Dictionary newBaseLayers) + { + var dirty = false; + if (humanoid.BaseLayers.Count != newBaseLayers.Count) + { + dirty = true; + humanoid.BaseLayers = newBaseLayers; + return dirty; + } + + foreach (var (key, info) in humanoid.BaseLayers) + { + if (!newBaseLayers.TryGetValue(key, out var newInfo)) + { + dirty = true; + break; + } + + if (info.ID != newInfo.ID) + { + dirty = true; + break; + } + } + + if (dirty) + { + humanoid.BaseLayers = newBaseLayers; + } + + return dirty; + } + + return IsDirty(newBaseLayers); + } + + private void ApplyBaseSprites(EntityUid uid, + HumanoidComponent humanoid, + SpriteComponent spriteComp) + { + foreach (var (layer, spriteInfo) in humanoid.BaseLayers) + { + if (spriteInfo.BaseSprite != null && spriteComp.LayerMapTryGet(layer, out var index)) + { + switch (spriteInfo.BaseSprite) + { + case SpriteSpecifier.Rsi rsi: + spriteComp.LayerSetRSI(index, rsi.RsiPath); + spriteComp.LayerSetState(index, rsi.RsiState); + break; + case SpriteSpecifier.Texture texture: + spriteComp.LayerSetTexture(index, texture.TexturePath); + break; + } + } + } + } + + +} diff --git a/Content.Client/Markings/MarkingPicker.xaml b/Content.Client/Humanoid/MarkingPicker.xaml similarity index 100% rename from Content.Client/Markings/MarkingPicker.xaml rename to Content.Client/Humanoid/MarkingPicker.xaml diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs new file mode 100644 index 0000000000..5d9178ba6a --- /dev/null +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -0,0 +1,463 @@ +using System.Linq; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.Utility; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using static Robust.Client.UserInterface.Controls.BoxContainer; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class MarkingPicker : Control +{ + [Dependency] private readonly MarkingManager _markingManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public Action? OnMarkingAdded; + public Action? OnMarkingRemoved; + public Action? OnMarkingColorChange; + public Action? OnMarkingRankChange; + + private List _currentMarkingColors = new(); + + private ItemList.Item? _selectedMarking; + private ItemList.Item? _selectedUnusedMarking; + private MarkingCategories _selectedMarkingCategory = MarkingCategories.Chest; + + private MarkingSet _currentMarkings = new(); + + private List _markingCategories = Enum.GetValues().ToList(); + + private string _currentSpecies = SharedHumanoidSystem.DefaultSpecies; + public Color CurrentSkinColor = Color.White; + + private readonly HashSet _ignoreCategories = new(); + + public string IgnoreCategories + { + get => string.Join(',', _ignoreCategories); + set + { + _ignoreCategories.Clear(); + var split = value.Split(','); + foreach (var category in split) + { + if (!Enum.TryParse(category, out MarkingCategories categoryParse)) + { + continue; + } + + _ignoreCategories.Add(categoryParse); + } + + SetupCategoryButtons(); + } + } + + public bool Forced { get; set; } + + private bool _ignoreSpecies; + + public bool IgnoreSpecies + { + get => _ignoreSpecies; + set + { + _ignoreSpecies = value; + Populate(); + } + } + + public void SetData(List newMarkings, string species, Color skinColor) + { + var pointsProto = _prototypeManager + .Index(species).MarkingPoints; + _currentMarkings = new(newMarkings, pointsProto, _markingManager); + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(species); // should be validated server-side but it can't hurt + } + + _currentSpecies = species; + CurrentSkinColor = skinColor; + + Populate(); + PopulateUsed(); + } + + public void SetData(MarkingSet set, string species, Color skinColor) + { + _currentMarkings = set; + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(species); // should be validated server-side but it can't hurt + } + + _currentSpecies = species; + CurrentSkinColor = skinColor; + + Populate(); + PopulateUsed(); + } + + public void SetSkinColor(Color color) => CurrentSkinColor = color; + + public MarkingPicker() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + SetupCategoryButtons(); + CMarkingCategoryButton.OnItemSelected += OnCategoryChange; + CMarkingsUnused.OnItemSelected += item => + _selectedUnusedMarking = CMarkingsUnused[item.ItemIndex]; + + CMarkingAdd.OnPressed += args => + MarkingAdd(); + + CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected; + + CMarkingRemove.OnPressed += args => + MarkingRemove(); + + CMarkingRankUp.OnPressed += _ => SwapMarkingUp(); + CMarkingRankDown.OnPressed += _ => SwapMarkingDown(); + } + + private void SetupCategoryButtons() + { + CMarkingCategoryButton.Clear(); + for (var i = 0; i < _markingCategories.Count; i++) + { + if (_ignoreCategories.Contains(_markingCategories[i])) + { + continue; + } + + CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{_markingCategories[i].ToString()}"), i); + } + CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory)); + } + + private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}"); + + private List GetMarkingStateNames(MarkingPrototype marking) + { + List result = new(); + foreach (var markingState in marking.Sprites) + { + switch (markingState) + { + case SpriteSpecifier.Rsi rsi: + result.Add(Loc.GetString($"marking-{marking.ID}-{rsi.RsiState}")); + break; + case SpriteSpecifier.Texture texture: + result.Add(Loc.GetString($"marking-{marking.ID}-{texture.TexturePath.Filename}")); + break; + } + } + + return result; + } + + public void Populate() + { + CMarkingsUnused.Clear(); + _selectedUnusedMarking = null; + + var markings = IgnoreSpecies + ? _markingManager.MarkingsByCategory(_selectedMarkingCategory) + : _markingManager.MarkingsByCategoryAndSpecies(_selectedMarkingCategory, _currentSpecies); + + foreach (var marking in markings.Values) + { + if (_currentMarkings.TryGetCategory(_selectedMarkingCategory, out var listing) + && listing.Contains(marking.AsMarking())) + { + continue; + } + + var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0()); + item.Metadata = marking; + } + + CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1; + } + + // Populate the used marking list. Returns a list of markings that weren't + // valid to add to the marking list. + public void PopulateUsed() + { + CMarkingsUsed.Clear(); + CMarkingColors.Visible = false; + _selectedMarking = null; + + if (!IgnoreSpecies) + { + _currentMarkings.FilterSpecies(_currentSpecies, _markingManager); + } + + // walk backwards through the list for visual purposes + foreach (var marking in _currentMarkings.GetReverseEnumerator(_selectedMarkingCategory)) + { + if (!_markingManager.TryGetMarking(marking, out var newMarking)) + { + continue; + } + + var text = Loc.GetString(marking.Forced ? "marking-used-forced" : "marking-used", ("marking-name", $"{GetMarkingName(newMarking)}"), + ("marking-category", Loc.GetString($"markings-category-{newMarking.MarkingCategory}"))); + + var _item = new ItemList.Item(CMarkingsUsed) + { + Text = text, + Icon = newMarking.Sprites[0].Frame0(), + Selectable = true, + Metadata = newMarking, + IconModulate = marking.MarkingColors[0] + }; + + CMarkingsUsed.Add(_item); + } + + // since all the points have been processed, update the points visually + UpdatePoints(); + } + + private void SwapMarkingUp() + { + if (_selectedMarking == null) + { + return; + } + + var i = CMarkingsUsed.IndexOf(_selectedMarking); + if (ShiftMarkingRank(i, -1)) + { + OnMarkingRankChange?.Invoke(_currentMarkings); + } + } + + private void SwapMarkingDown() + { + if (_selectedMarking == null) + { + return; + } + + var i = CMarkingsUsed.IndexOf(_selectedMarking); + if (ShiftMarkingRank(i, 1)) + { + OnMarkingRankChange?.Invoke(_currentMarkings); + } + } + + private bool ShiftMarkingRank(int src, int places) + { + if (src + places >= CMarkingsUsed.Count || src + places < 0) + { + return false; + } + + var visualDest = src + places; // what it would visually look like + var visualTemp = CMarkingsUsed[visualDest]; + CMarkingsUsed[visualDest] = CMarkingsUsed[src]; + CMarkingsUsed[src] = visualTemp; + + switch (places) + { + // i.e., we're going down in rank + case < 0: + _currentMarkings.ShiftRankDownFromEnd(_selectedMarkingCategory, src); + break; + // i.e., we're going up in rank + case > 0: + _currentMarkings.ShiftRankUpFromEnd(_selectedMarkingCategory, src); + break; + // do nothing? + default: + break; + } + + return true; + } + + + + // repopulate in case markings are restricted, + // and also filter out any markings that are now invalid + // attempt to preserve any existing markings as well: + // it would be frustrating to otherwise have all markings + // cleared, imo + public void SetSpecies(string species) + { + _currentSpecies = species; + var markingList = _currentMarkings.GetForwardEnumerator().ToList(); + + var speciesPrototype = _prototypeManager.Index(species); + + _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager); + _currentMarkings.FilterSpecies(species); + + Populate(); + PopulateUsed(); + } + + private void UpdatePoints() + { + var count = _currentMarkings.PointsLeft(_selectedMarkingCategory); + if (count > -1) + { + CMarkingPoints.Text = Loc.GetString("marking-points-remaining", ("points", count)); + } + } + + private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category) + { + CMarkingCategoryButton.SelectId(category.Id); + _selectedMarkingCategory = _markingCategories[category.Id]; + Populate(); + PopulateUsed(); + UpdatePoints(); + } + + // TODO: This should be using ColorSelectorSliders once that's merged, so + private void OnUsedMarkingSelected(ItemList.ItemListSelectedEventArgs item) + { + _selectedMarking = CMarkingsUsed[item.ItemIndex]; + var prototype = (MarkingPrototype) _selectedMarking.Metadata!; + + if (prototype.FollowSkinColor) + { + CMarkingColors.Visible = false; + + return; + } + + var stateNames = GetMarkingStateNames(prototype); + _currentMarkingColors.Clear(); + CMarkingColors.DisposeAllChildren(); + List colorSliders = new(); + for (int i = 0; i < prototype.Sprites.Count; i++) + { + var colorContainer = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + }; + + CMarkingColors.AddChild(colorContainer); + + ColorSelectorSliders colorSelector = new ColorSelectorSliders(); + colorSliders.Add(colorSelector); + + colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" }); + colorContainer.AddChild(colorSelector); + + var listing = _currentMarkings[_selectedMarkingCategory]; + + var currentColor = new Color( + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].RByte, + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].GByte, + listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i].BByte + ); + colorSelector.Color = currentColor; + _currentMarkingColors.Add(currentColor); + var colorIndex = _currentMarkingColors.Count - 1; + + Action colorChanged = _ => + { + _currentMarkingColors[colorIndex] = colorSelector.Color; + + ColorChanged(colorIndex); + }; + colorSelector.OnColorChanged += colorChanged; + } + + CMarkingColors.Visible = true; + } + + private void ColorChanged(int colorIndex) + { + if (_selectedMarking is null) return; + var markingPrototype = (MarkingPrototype) _selectedMarking.Metadata!; + int markingIndex = _currentMarkings.FindIndexOf(_selectedMarkingCategory, markingPrototype.ID); + + if (markingIndex < 0) return; + + _selectedMarking.IconModulate = _currentMarkingColors[colorIndex]; + + var marking = new Marking(_currentMarkings[_selectedMarkingCategory][markingIndex]); + marking.SetColor(colorIndex, _currentMarkingColors[colorIndex]); + _currentMarkings.Replace(_selectedMarkingCategory, markingIndex, marking); + + OnMarkingColorChange?.Invoke(_currentMarkings); + } + + private void MarkingAdd() + { + if (_selectedUnusedMarking is null) return; + + if (_currentMarkings.PointsLeft(_selectedMarkingCategory) == 0 && !Forced) + { + return; + } + + var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!; + + + var markingObject = marking.AsMarking(); + for (var i = 0; i < markingObject.MarkingColors.Count; i++) + { + markingObject.SetColor(i, CurrentSkinColor); + } + + markingObject.Forced = Forced; + + _currentMarkings.AddBack(_selectedMarkingCategory, markingObject); + + UpdatePoints(); + + CMarkingsUnused.Remove(_selectedUnusedMarking); + var item = new ItemList.Item(CMarkingsUsed) + { + Text = Loc.GetString("marking-used", ("marking-name", $"{GetMarkingName(marking)}"), ("marking-category", Loc.GetString($"markings-category-{marking.MarkingCategory}"))), + Icon = marking.Sprites[0].Frame0(), + Selectable = true, + Metadata = marking, + }; + CMarkingsUsed.Insert(0, item); + + _selectedUnusedMarking = null; + OnMarkingAdded?.Invoke(_currentMarkings); + } + + private void MarkingRemove() + { + if (_selectedMarking is null) return; + + var marking = (MarkingPrototype) _selectedMarking.Metadata!; + + _currentMarkings.Remove(_selectedMarkingCategory, marking.ID); + + UpdatePoints(); + + CMarkingsUsed.Remove(_selectedMarking); + + if (marking.MarkingCategory == _selectedMarkingCategory) + { + var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0()); + item.Metadata = marking; + } + _selectedMarking = null; + CMarkingColors.Visible = false; + OnMarkingRemoved?.Invoke(_currentMarkings); + } +} diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml b/Content.Client/Humanoid/SingleMarkingPicker.xaml new file mode 100644 index 0000000000..2dfa661f58 --- /dev/null +++ b/Content.Client/Humanoid/SingleMarkingPicker.xaml @@ -0,0 +1,22 @@ + + +