HumanoidAppearanceSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. using Content.Shared.CCVar;
  2. using Content.Shared.Humanoid;
  3. using Content.Shared.Humanoid.Markings;
  4. using Content.Shared.Humanoid.Prototypes;
  5. using Content.Shared.Inventory;
  6. using Content.Shared.Preferences;
  7. using Robust.Client.GameObjects;
  8. using Robust.Shared.Configuration;
  9. using Robust.Shared.Prototypes;
  10. using Robust.Shared.Utility;
  11. namespace Content.Client.Humanoid;
  12. public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
  13. {
  14. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  15. [Dependency] private readonly MarkingManager _markingManager = default!;
  16. [Dependency] private readonly IConfigurationManager _configurationManager = default!;
  17. public override void Initialize()
  18. {
  19. base.Initialize();
  20. SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
  21. Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
  22. Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
  23. }
  24. private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
  25. {
  26. UpdateSprite(component, Comp<SpriteComponent>(uid));
  27. }
  28. private void OnCvarChanged(bool value)
  29. {
  30. var humanoidQuery = EntityManager.AllEntityQueryEnumerator<HumanoidAppearanceComponent, SpriteComponent>();
  31. while (humanoidQuery.MoveNext(out var _, out var humanoidComp, out var spriteComp))
  32. {
  33. UpdateSprite(humanoidComp, spriteComp);
  34. }
  35. }
  36. private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
  37. {
  38. UpdateLayers(component, sprite);
  39. ApplyMarkingSet(component, sprite);
  40. sprite[sprite.LayerMapReserveBlank(HumanoidVisualLayers.Eyes)].Color = component.EyeColor;
  41. }
  42. private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
  43. => humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
  44. private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
  45. {
  46. var oldLayers = new HashSet<HumanoidVisualLayers>(component.BaseLayers.Keys);
  47. component.BaseLayers.Clear();
  48. // add default species layers
  49. var speciesProto = _prototypeManager.Index(component.Species);
  50. var baseSprites = _prototypeManager.Index<HumanoidSpeciesBaseSpritesPrototype>(speciesProto.SpriteSet);
  51. foreach (var (key, id) in baseSprites.Sprites)
  52. {
  53. oldLayers.Remove(key);
  54. if (!component.CustomBaseLayers.ContainsKey(key))
  55. SetLayerData(component, sprite, key, id, sexMorph: true);
  56. }
  57. // add custom layers
  58. foreach (var (key, info) in component.CustomBaseLayers)
  59. {
  60. oldLayers.Remove(key);
  61. SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color);
  62. }
  63. // hide old layers
  64. // TODO maybe just remove them altogether?
  65. foreach (var key in oldLayers)
  66. {
  67. if (sprite.LayerMapTryGet(key, out var index))
  68. sprite[index].Visible = false;
  69. }
  70. }
  71. private void SetLayerData(
  72. HumanoidAppearanceComponent component,
  73. SpriteComponent sprite,
  74. HumanoidVisualLayers key,
  75. string? protoId,
  76. bool sexMorph = false,
  77. Color? color = null)
  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)
  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 Dictionary<HumanoidVisualLayers, SlotFlags>();
  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. var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
  190. _configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
  191. // The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
  192. var applyUndergarmentTop = censorNudity;
  193. var applyUndergarmentBottom = censorNudity;
  194. foreach (var markingList in humanoid.MarkingSet.Markings.Values)
  195. {
  196. foreach (var marking in markingList)
  197. {
  198. if (_markingManager.TryGetMarking(marking, out var markingPrototype))
  199. {
  200. ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
  201. if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
  202. applyUndergarmentTop = false;
  203. else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
  204. applyUndergarmentBottom = false;
  205. }
  206. }
  207. }
  208. humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
  209. AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
  210. }
  211. private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
  212. {
  213. foreach (var markingList in humanoid.ClientOldMarkings.Markings.Values)
  214. {
  215. foreach (var marking in markingList)
  216. {
  217. RemoveMarking(marking, sprite);
  218. }
  219. }
  220. humanoid.ClientOldMarkings.Clear();
  221. foreach (var markingList in humanoid.MarkingSet.Markings.Values)
  222. {
  223. foreach (var marking in markingList)
  224. {
  225. RemoveMarking(marking, sprite);
  226. }
  227. }
  228. }
  229. private void RemoveMarking(Marking marking, SpriteComponent spriteComp)
  230. {
  231. if (!_markingManager.TryGetMarking(marking, out var prototype))
  232. {
  233. return;
  234. }
  235. foreach (var sprite in prototype.Sprites)
  236. {
  237. if (sprite is not SpriteSpecifier.Rsi rsi)
  238. {
  239. continue;
  240. }
  241. var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
  242. if (!spriteComp.LayerMapTryGet(layerId, out var index))
  243. {
  244. continue;
  245. }
  246. spriteComp.LayerMapRemove(layerId);
  247. spriteComp.RemoveLayer(index);
  248. }
  249. }
  250. private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
  251. {
  252. if (undergarmentTop && humanoid.UndergarmentTop != null)
  253. {
  254. var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
  255. if (_markingManager.TryGetMarking(marking, out var prototype))
  256. {
  257. // Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
  258. humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking>{ marking });
  259. ApplyMarking(prototype, null, true, humanoid, sprite);
  260. }
  261. }
  262. if (undergarmentBottom && humanoid.UndergarmentBottom != null)
  263. {
  264. var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
  265. if (_markingManager.TryGetMarking(marking, out var prototype))
  266. {
  267. humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking>{ marking });
  268. ApplyMarking(prototype, null, true, humanoid, sprite);
  269. }
  270. }
  271. }
  272. private void ApplyMarking(MarkingPrototype markingPrototype,
  273. IReadOnlyList<Color>? colors,
  274. bool visible,
  275. HumanoidAppearanceComponent humanoid,
  276. SpriteComponent sprite)
  277. {
  278. if (!sprite.LayerMapTryGet(markingPrototype.BodyPart, out int targetLayer))
  279. {
  280. return;
  281. }
  282. visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
  283. visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
  284. && setting.AllowsMarkings;
  285. for (var j = 0; j < markingPrototype.Sprites.Count; j++)
  286. {
  287. var markingSprite = markingPrototype.Sprites[j];
  288. if (markingSprite is not SpriteSpecifier.Rsi rsi)
  289. {
  290. continue;
  291. }
  292. var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
  293. if (!sprite.LayerMapTryGet(layerId, out _))
  294. {
  295. var layer = sprite.AddLayer(markingSprite, targetLayer + j + 1);
  296. sprite.LayerMapSet(layerId, layer);
  297. sprite.LayerSetSprite(layerId, rsi);
  298. }
  299. sprite.LayerSetVisible(layerId, visible);
  300. if (!visible || setting == null) // this is kinda implied
  301. {
  302. continue;
  303. }
  304. // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
  305. // and we need to check the index is correct.
  306. // So if that happens just default to white?
  307. if (colors != null && j < colors.Count)
  308. {
  309. sprite.LayerSetColor(layerId, colors[j]);
  310. }
  311. else
  312. {
  313. sprite.LayerSetColor(layerId, Color.White);
  314. }
  315. }
  316. }
  317. public override void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null)
  318. {
  319. if (!Resolve(uid, ref humanoid) || humanoid.SkinColor == skinColor)
  320. return;
  321. base.SetSkinColor(uid, skinColor, false, verify, humanoid);
  322. if (!TryComp(uid, out SpriteComponent? sprite))
  323. return;
  324. foreach (var (layer, spriteInfo) in humanoid.BaseLayers)
  325. {
  326. if (!spriteInfo.MatchSkin)
  327. continue;
  328. var index = sprite.LayerMapReserveBlank(layer);
  329. sprite[index].Color = skinColor.WithAlpha(spriteInfo.LayerAlpha);
  330. }
  331. }
  332. public override void SetLayerVisibility(
  333. Entity<HumanoidAppearanceComponent> ent,
  334. HumanoidVisualLayers layer,
  335. bool visible,
  336. SlotFlags? slot,
  337. ref bool dirty)
  338. {
  339. base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
  340. var sprite = Comp<SpriteComponent>(ent);
  341. if (!sprite.LayerMapTryGet(layer, out var index))
  342. {
  343. if (!visible)
  344. return;
  345. index = sprite.LayerMapReserveBlank(layer);
  346. }
  347. var spriteLayer = sprite[index];
  348. if (spriteLayer.Visible == visible)
  349. return;
  350. spriteLayer.Visible = visible;
  351. // I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
  352. // Just a week away...
  353. foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
  354. {
  355. foreach (var marking in markingList)
  356. {
  357. if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
  358. ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, ent, sprite);
  359. }
  360. }
  361. }
  362. }