1
0

HumanoidAppearanceSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. // SPDX-FileCopyrightText: 2023 DrSmugleaf <DrSmugleaf@users.noreply.github.com>
  2. // SPDX-FileCopyrightText: 2023 Flipp Syder <76629141+vulppine@users.noreply.github.com>
  3. // SPDX-FileCopyrightText: 2023 Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
  4. // SPDX-FileCopyrightText: 2023 Morb <14136326+Morb0@users.noreply.github.com>
  5. // SPDX-FileCopyrightText: 2023 csqrb <56765288+CaptainSqrBeard@users.noreply.github.com>
  6. // SPDX-FileCopyrightText: 2023 metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
  7. // SPDX-FileCopyrightText: 2024 Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
  8. // SPDX-FileCopyrightText: 2024 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
  9. // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
  10. //
  11. // SPDX-License-Identifier: AGPL-3.0-or-later
  12. using Content.Shared.Humanoid;
  13. using Content.Shared.Humanoid.Markings;
  14. using Content.Shared.Humanoid.Prototypes;
  15. using Content.Shared.Preferences;
  16. using Content.Shared.Inventory;
  17. using Robust.Client.GameObjects;
  18. using Robust.Shared.Prototypes;
  19. using Robust.Shared.Utility;
  20. namespace Content.Client.Humanoid;
  21. public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
  22. {
  23. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  24. [Dependency] private readonly MarkingManager _markingManager = default!;
  25. public override void Initialize()
  26. {
  27. base.Initialize();
  28. SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
  29. }
  30. private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
  31. {
  32. UpdateSprite(component, Comp<SpriteComponent>(uid));
  33. }
  34. private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
  35. {
  36. UpdateLayers(component, sprite);
  37. ApplyMarkingSet(component, sprite);
  38. sprite[sprite.LayerMapReserveBlank(HumanoidVisualLayers.Eyes)].Color = component.EyeColor;
  39. }
  40. private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
  41. => humanoid.PermanentlyHidden.Contains(layer);
  42. private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
  43. {
  44. var oldLayers = new HashSet<HumanoidVisualLayers>(component.BaseLayers.Keys);
  45. component.BaseLayers.Clear();
  46. // add default species layers
  47. var speciesProto = _prototypeManager.Index(component.Species);
  48. var baseSprites = _prototypeManager.Index<HumanoidSpeciesBaseSpritesPrototype>(speciesProto.SpriteSet);
  49. foreach (var (key, id) in baseSprites.Sprites)
  50. {
  51. oldLayers.Remove(key);
  52. if (!component.CustomBaseLayers.ContainsKey(key))
  53. SetLayerData(component, sprite, key, id, sexMorph: true);
  54. }
  55. // add custom layers
  56. foreach (var (key, info) in component.CustomBaseLayers)
  57. {
  58. oldLayers.Remove(key);
  59. // Shitmed Change: For whatever reason these weren't actually ignoring the skin color as advertised.
  60. SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color, overrideSkin: true);
  61. }
  62. // hide old layers
  63. // TODO maybe just remove them altogether?
  64. foreach (var key in oldLayers)
  65. {
  66. if (sprite.LayerMapTryGet(key, out var index))
  67. sprite[index].Visible = false;
  68. }
  69. }
  70. private void SetLayerData(
  71. HumanoidAppearanceComponent component,
  72. SpriteComponent sprite,
  73. HumanoidVisualLayers key,
  74. string? protoId,
  75. bool sexMorph = false,
  76. Color? color = null,
  77. bool overrideSkin = false) // Shitmed Change
  78. {
  79. var layerIndex = sprite.LayerMapReserveBlank(key);
  80. var layer = sprite[layerIndex];
  81. layer.Visible = !IsHidden(component, key);
  82. if (color != null)
  83. layer.Color = color.Value;
  84. if (protoId == null)
  85. return;
  86. if (sexMorph)
  87. protoId = HumanoidVisualLayersExtension.GetSexMorph(key, component.Sex, protoId);
  88. var proto = _prototypeManager.Index<HumanoidSpeciesSpriteLayer>(protoId);
  89. component.BaseLayers[key] = proto;
  90. if (proto.MatchSkin && !overrideSkin) // Shitmed Change
  91. layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha);
  92. if (proto.BaseSprite != null)
  93. sprite.LayerSetSprite(layerIndex, proto.BaseSprite);
  94. }
  95. /// <summary>
  96. /// Loads a profile directly into a humanoid.
  97. /// </summary>
  98. /// <param name="uid">The humanoid entity's UID</param>
  99. /// <param name="profile">The profile to load.</param>
  100. /// <param name="humanoid">The humanoid entity's humanoid component.</param>
  101. /// <remarks>
  102. /// This should not be used if the entity is owned by the server. The server will otherwise
  103. /// override this with the appearance data it sends over.
  104. /// </remarks>
  105. public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
  106. {
  107. if (profile == null)
  108. return;
  109. if (!Resolve(uid, ref humanoid))
  110. {
  111. return;
  112. }
  113. var customBaseLayers = new Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo>();
  114. var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(profile.Species);
  115. var markings = new MarkingSet(speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
  116. // Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it.
  117. var markingFColored = new Dictionary<Marking, MarkingPrototype>();
  118. foreach (var marking in profile.Appearance.Markings)
  119. {
  120. if (_markingManager.TryGetMarking(marking, out var prototype))
  121. {
  122. if (!prototype.ForcedColoring)
  123. {
  124. markings.AddBack(prototype.MarkingCategory, marking);
  125. }
  126. else
  127. {
  128. markingFColored.Add(marking, prototype);
  129. }
  130. }
  131. }
  132. // legacy: remove in the future?
  133. //markings.RemoveCategory(MarkingCategories.Hair);
  134. //markings.RemoveCategory(MarkingCategories.FacialHair);
  135. // We need to ensure hair before applying it or coloring can try depend on markings that can be invalid
  136. var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, out var hairAlpha, _prototypeManager)
  137. ? profile.Appearance.SkinColor.WithAlpha(hairAlpha)
  138. : profile.Appearance.HairColor;
  139. var hair = new Marking(profile.Appearance.HairStyleId,
  140. new[] { hairColor });
  141. var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _prototypeManager)
  142. ? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha)
  143. : profile.Appearance.FacialHairColor;
  144. var facialHair = new Marking(profile.Appearance.FacialHairStyleId,
  145. new[] { facialHairColor });
  146. if (_markingManager.CanBeApplied(profile.Species, profile.Sex, hair, _prototypeManager))
  147. {
  148. markings.AddBack(MarkingCategories.Hair, hair);
  149. }
  150. if (_markingManager.CanBeApplied(profile.Species, profile.Sex, facialHair, _prototypeManager))
  151. {
  152. markings.AddBack(MarkingCategories.FacialHair, facialHair);
  153. }
  154. // Finally adding marking with forced colors
  155. foreach (var (marking, prototype) in markingFColored)
  156. {
  157. var markingColors = MarkingColoring.GetMarkingLayerColors(
  158. prototype,
  159. profile.Appearance.SkinColor,
  160. profile.Appearance.EyeColor,
  161. markings
  162. );
  163. markings.AddBack(prototype.MarkingCategory, new Marking(marking.MarkingId, markingColors));
  164. }
  165. markings.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager);
  166. markings.EnsureSexes(profile.Sex, _markingManager);
  167. markings.EnsureDefault(
  168. profile.Appearance.SkinColor,
  169. profile.Appearance.EyeColor,
  170. _markingManager);
  171. DebugTools.Assert(IsClientSide(uid));
  172. humanoid.MarkingSet = markings;
  173. humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
  174. humanoid.HiddenLayers = new HashSet<HumanoidVisualLayers>();
  175. humanoid.CustomBaseLayers = customBaseLayers;
  176. humanoid.Sex = profile.Sex;
  177. humanoid.Gender = profile.Gender;
  178. humanoid.Age = profile.Age;
  179. humanoid.Species = profile.Species;
  180. humanoid.SkinColor = profile.Appearance.SkinColor;
  181. humanoid.EyeColor = profile.Appearance.EyeColor;
  182. UpdateSprite(humanoid, Comp<SpriteComponent>(uid));
  183. }
  184. private void ApplyMarkingSet(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
  185. {
  186. // I am lazy and I CBF resolving the previous mess, so I'm just going to nuke the markings.
  187. // Really, markings should probably be a separate component altogether.
  188. ClearAllMarkings(humanoid, sprite);
  189. foreach (var markingList in humanoid.MarkingSet.Markings.Values)
  190. {
  191. foreach (var marking in markingList)
  192. {
  193. if (_markingManager.TryGetMarking(marking, out var markingPrototype))
  194. ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
  195. }
  196. }
  197. humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
  198. }
  199. private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
  200. {
  201. foreach (var markingList in humanoid.ClientOldMarkings.Markings.Values)
  202. {
  203. foreach (var marking in markingList)
  204. {
  205. RemoveMarking(marking, sprite);
  206. }
  207. }
  208. humanoid.ClientOldMarkings.Clear();
  209. foreach (var markingList in humanoid.MarkingSet.Markings.Values)
  210. {
  211. foreach (var marking in markingList)
  212. {
  213. RemoveMarking(marking, sprite);
  214. }
  215. }
  216. }
  217. private void RemoveMarking(Marking marking, SpriteComponent spriteComp)
  218. {
  219. if (!_markingManager.TryGetMarking(marking, out var prototype))
  220. {
  221. return;
  222. }
  223. foreach (var sprite in prototype.Sprites)
  224. {
  225. if (sprite is not SpriteSpecifier.Rsi rsi)
  226. {
  227. continue;
  228. }
  229. var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
  230. if (!spriteComp.LayerMapTryGet(layerId, out var index))
  231. {
  232. continue;
  233. }
  234. spriteComp.LayerMapRemove(layerId);
  235. spriteComp.RemoveLayer(index);
  236. }
  237. }
  238. private void ApplyMarking(MarkingPrototype markingPrototype,
  239. IReadOnlyList<Color>? colors,
  240. bool visible,
  241. HumanoidAppearanceComponent humanoid,
  242. SpriteComponent sprite)
  243. {
  244. if (!sprite.LayerMapTryGet(markingPrototype.BodyPart, out int targetLayer))
  245. {
  246. return;
  247. }
  248. visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
  249. visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
  250. && setting.AllowsMarkings;
  251. for (var j = 0; j < markingPrototype.Sprites.Count; j++)
  252. {
  253. var markingSprite = markingPrototype.Sprites[j];
  254. if (markingSprite is not SpriteSpecifier.Rsi rsi)
  255. {
  256. continue;
  257. }
  258. var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
  259. if (!sprite.LayerMapTryGet(layerId, out _))
  260. {
  261. var layer = sprite.AddLayer(markingSprite, targetLayer + j + 1);
  262. sprite.LayerMapSet(layerId, layer);
  263. sprite.LayerSetSprite(layerId, rsi);
  264. }
  265. sprite.LayerSetVisible(layerId, visible);
  266. if (!visible || setting == null) // this is kinda implied
  267. {
  268. continue;
  269. }
  270. // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
  271. // and we need to check the index is correct.
  272. // So if that happens just default to white?
  273. if (colors != null && j < colors.Count)
  274. {
  275. sprite.LayerSetColor(layerId, colors[j]);
  276. }
  277. else
  278. {
  279. sprite.LayerSetColor(layerId, Color.White);
  280. }
  281. }
  282. }
  283. public override void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null)
  284. {
  285. if (!Resolve(uid, ref humanoid) || humanoid.SkinColor == skinColor)
  286. return;
  287. base.SetSkinColor(uid, skinColor, false, verify, humanoid);
  288. if (!TryComp(uid, out SpriteComponent? sprite))
  289. return;
  290. foreach (var (layer, spriteInfo) in humanoid.BaseLayers)
  291. {
  292. if (!spriteInfo.MatchSkin)
  293. continue;
  294. var index = sprite.LayerMapReserveBlank(layer);
  295. sprite[index].Color = skinColor.WithAlpha(spriteInfo.LayerAlpha);
  296. }
  297. }
  298. protected override void SetLayerVisibility(
  299. EntityUid uid,
  300. HumanoidAppearanceComponent humanoid,
  301. HumanoidVisualLayers layer,
  302. bool visible,
  303. bool permanent,
  304. ref bool dirty)
  305. {
  306. base.SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
  307. var sprite = Comp<SpriteComponent>(uid);
  308. if (!sprite.LayerMapTryGet(layer, out var index))
  309. {
  310. if (!visible)
  311. return;
  312. else
  313. index = sprite.LayerMapReserveBlank(layer);
  314. }
  315. var spriteLayer = sprite[index];
  316. if (spriteLayer.Visible == visible)
  317. return;
  318. spriteLayer.Visible = visible;
  319. // I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
  320. foreach (var markingList in humanoid.MarkingSet.Markings.Values)
  321. {
  322. foreach (var marking in markingList)
  323. {
  324. if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
  325. ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
  326. }
  327. }
  328. }
  329. }