MarkingPicker.xaml.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. using System.Linq;
  2. using Content.Shared.Humanoid;
  3. using Content.Shared.Humanoid.Markings;
  4. using Content.Shared.Humanoid.Prototypes;
  5. using Robust.Client.AutoGenerated;
  6. using Robust.Client.UserInterface;
  7. using Robust.Client.UserInterface.Controls;
  8. using Robust.Client.UserInterface.XAML;
  9. using Robust.Client.Utility;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Utility;
  12. using static Robust.Client.UserInterface.Controls.BoxContainer;
  13. namespace Content.Client.Humanoid;
  14. [GenerateTypedNameReferences]
  15. public sealed partial class MarkingPicker : Control
  16. {
  17. [Dependency] private readonly MarkingManager _markingManager = default!;
  18. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  19. public Action<MarkingSet>? OnMarkingAdded;
  20. public Action<MarkingSet>? OnMarkingRemoved;
  21. public Action<MarkingSet>? OnMarkingColorChange;
  22. public Action<MarkingSet>? OnMarkingRankChange;
  23. private List<Color> _currentMarkingColors = new();
  24. private ItemList.Item? _selectedMarking;
  25. private ItemList.Item? _selectedUnusedMarking;
  26. private MarkingCategories _selectedMarkingCategory = MarkingCategories.Chest;
  27. private MarkingSet _currentMarkings = new();
  28. private List<MarkingCategories> _markingCategories = Enum.GetValues<MarkingCategories>().ToList();
  29. private string _currentSpecies = SharedHumanoidAppearanceSystem.DefaultSpecies;
  30. private Sex _currentSex = Sex.Unsexed;
  31. public Color CurrentSkinColor = Color.White;
  32. public Color CurrentEyeColor = Color.Black;
  33. public Marking? HairMarking;
  34. public Marking? FacialHairMarking;
  35. private readonly HashSet<MarkingCategories> _ignoreCategories = new();
  36. public string IgnoreCategories
  37. {
  38. get => string.Join(',', _ignoreCategories);
  39. set
  40. {
  41. _ignoreCategories.Clear();
  42. var split = value.Split(',');
  43. foreach (var category in split)
  44. {
  45. if (!Enum.TryParse(category, out MarkingCategories categoryParse))
  46. {
  47. continue;
  48. }
  49. _ignoreCategories.Add(categoryParse);
  50. }
  51. SetupCategoryButtons();
  52. }
  53. }
  54. public bool Forced { get; set; }
  55. private bool _ignoreSpecies;
  56. public bool IgnoreSpecies
  57. {
  58. get => _ignoreSpecies;
  59. set
  60. {
  61. _ignoreSpecies = value;
  62. Populate(CMarkingSearch.Text);
  63. }
  64. }
  65. public void SetData(List<Marking> newMarkings, string species, Sex sex, Color skinColor, Color eyeColor)
  66. {
  67. var pointsProto = _prototypeManager
  68. .Index<SpeciesPrototype>(species).MarkingPoints;
  69. _currentMarkings = new(newMarkings, pointsProto, _markingManager);
  70. if (!IgnoreSpecies)
  71. {
  72. _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
  73. }
  74. _currentSpecies = species;
  75. _currentSex = sex;
  76. CurrentSkinColor = skinColor;
  77. CurrentEyeColor = eyeColor;
  78. Populate(CMarkingSearch.Text);
  79. PopulateUsed();
  80. }
  81. public void SetData(MarkingSet set, string species, Sex sex, Color skinColor, Color eyeColor)
  82. {
  83. _currentMarkings = set;
  84. if (!IgnoreSpecies)
  85. {
  86. _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
  87. }
  88. _currentSpecies = species;
  89. _currentSex = sex;
  90. CurrentSkinColor = skinColor;
  91. CurrentEyeColor = eyeColor;
  92. Populate(CMarkingSearch.Text);
  93. PopulateUsed();
  94. }
  95. public void SetSkinColor(Color color) => CurrentSkinColor = color;
  96. public void SetEyeColor(Color color) => CurrentEyeColor = color;
  97. public MarkingPicker()
  98. {
  99. RobustXamlLoader.Load(this);
  100. IoCManager.InjectDependencies(this);
  101. CMarkingCategoryButton.OnItemSelected += OnCategoryChange;
  102. CMarkingsUnused.OnItemSelected += item =>
  103. _selectedUnusedMarking = CMarkingsUnused[item.ItemIndex];
  104. CMarkingAdd.OnPressed += _ =>
  105. MarkingAdd();
  106. CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected;
  107. CMarkingRemove.OnPressed += _ =>
  108. MarkingRemove();
  109. CMarkingRankUp.OnPressed += _ => SwapMarkingUp();
  110. CMarkingRankDown.OnPressed += _ => SwapMarkingDown();
  111. CMarkingSearch.OnTextChanged += args => Populate(args.Text);
  112. }
  113. private void SetupCategoryButtons()
  114. {
  115. CMarkingCategoryButton.Clear();
  116. var validCategories = new List<MarkingCategories>();
  117. for (var i = 0; i < _markingCategories.Count; i++)
  118. {
  119. var category = _markingCategories[i];
  120. var markings = GetMarkings(category);
  121. if (_ignoreCategories.Contains(category) ||
  122. markings.Count == 0)
  123. {
  124. continue;
  125. }
  126. validCategories.Add(category);
  127. CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{category.ToString()}"), i);
  128. }
  129. if (validCategories.Contains(_selectedMarkingCategory))
  130. {
  131. CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory));
  132. }
  133. else if (validCategories.Count > 0)
  134. {
  135. _selectedMarkingCategory = validCategories[0];
  136. }
  137. else
  138. {
  139. _selectedMarkingCategory = MarkingCategories.Chest;
  140. }
  141. }
  142. private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}");
  143. private List<string> GetMarkingStateNames(MarkingPrototype marking)
  144. {
  145. List<string> result = new();
  146. foreach (var markingState in marking.Sprites)
  147. {
  148. switch (markingState)
  149. {
  150. case SpriteSpecifier.Rsi rsi:
  151. result.Add(Loc.GetString($"marking-{marking.ID}-{rsi.RsiState}"));
  152. break;
  153. case SpriteSpecifier.Texture texture:
  154. result.Add(Loc.GetString($"marking-{marking.ID}-{texture.TexturePath.Filename}"));
  155. break;
  156. }
  157. }
  158. return result;
  159. }
  160. private IReadOnlyDictionary<string, MarkingPrototype> GetMarkings(MarkingCategories category)
  161. {
  162. return IgnoreSpecies
  163. ? _markingManager.MarkingsByCategoryAndSex(category, _currentSex)
  164. : _markingManager.MarkingsByCategoryAndSpeciesAndSex(category, _currentSpecies, _currentSex);
  165. }
  166. public void Populate(string filter)
  167. {
  168. SetupCategoryButtons();
  169. CMarkingsUnused.Clear();
  170. _selectedUnusedMarking = null;
  171. var sortedMarkings = GetMarkings(_selectedMarkingCategory).Values.Where(m =>
  172. m.ID.ToLower().Contains(filter.ToLower()) ||
  173. GetMarkingName(m).ToLower().Contains(filter.ToLower())
  174. ).OrderBy(p => Loc.GetString(GetMarkingName(p)));
  175. foreach (var marking in sortedMarkings)
  176. {
  177. if (_currentMarkings.TryGetMarking(_selectedMarkingCategory, marking.ID, out _))
  178. {
  179. continue;
  180. }
  181. var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0());
  182. item.Metadata = marking;
  183. }
  184. CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1;
  185. }
  186. // Populate the used marking list. Returns a list of markings that weren't
  187. // valid to add to the marking list.
  188. public void PopulateUsed()
  189. {
  190. CMarkingsUsed.Clear();
  191. CMarkingColors.Visible = false;
  192. _selectedMarking = null;
  193. if (!IgnoreSpecies)
  194. {
  195. _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
  196. }
  197. // walk backwards through the list for visual purposes
  198. foreach (var marking in _currentMarkings.GetReverseEnumerator(_selectedMarkingCategory))
  199. {
  200. if (!_markingManager.TryGetMarking(marking, out var newMarking))
  201. {
  202. continue;
  203. }
  204. var text = Loc.GetString(marking.Forced ? "marking-used-forced" : "marking-used", ("marking-name", $"{GetMarkingName(newMarking)}"),
  205. ("marking-category", Loc.GetString($"markings-category-{newMarking.MarkingCategory}")));
  206. var _item = new ItemList.Item(CMarkingsUsed)
  207. {
  208. Text = text,
  209. Icon = newMarking.Sprites[0].Frame0(),
  210. Selectable = true,
  211. Metadata = newMarking,
  212. IconModulate = marking.MarkingColors[0]
  213. };
  214. CMarkingsUsed.Add(_item);
  215. }
  216. // since all the points have been processed, update the points visually
  217. UpdatePoints();
  218. }
  219. private void SwapMarkingUp()
  220. {
  221. if (_selectedMarking == null)
  222. {
  223. return;
  224. }
  225. var i = CMarkingsUsed.IndexOf(_selectedMarking);
  226. if (ShiftMarkingRank(i, -1))
  227. {
  228. OnMarkingRankChange?.Invoke(_currentMarkings);
  229. }
  230. }
  231. private void SwapMarkingDown()
  232. {
  233. if (_selectedMarking == null)
  234. {
  235. return;
  236. }
  237. var i = CMarkingsUsed.IndexOf(_selectedMarking);
  238. if (ShiftMarkingRank(i, 1))
  239. {
  240. OnMarkingRankChange?.Invoke(_currentMarkings);
  241. }
  242. }
  243. private bool ShiftMarkingRank(int src, int places)
  244. {
  245. if (src + places >= CMarkingsUsed.Count || src + places < 0)
  246. {
  247. return false;
  248. }
  249. var visualDest = src + places; // what it would visually look like
  250. var visualTemp = CMarkingsUsed[visualDest];
  251. CMarkingsUsed[visualDest] = CMarkingsUsed[src];
  252. CMarkingsUsed[src] = visualTemp;
  253. switch (places)
  254. {
  255. // i.e., we're going down in rank
  256. case < 0:
  257. _currentMarkings.ShiftRankDownFromEnd(_selectedMarkingCategory, src);
  258. break;
  259. // i.e., we're going up in rank
  260. case > 0:
  261. _currentMarkings.ShiftRankUpFromEnd(_selectedMarkingCategory, src);
  262. break;
  263. // do nothing?
  264. // ReSharper disable once RedundantEmptySwitchSection
  265. default:
  266. break;
  267. }
  268. return true;
  269. }
  270. // repopulate in case markings are restricted,
  271. // and also filter out any markings that are now invalid
  272. // attempt to preserve any existing markings as well:
  273. // it would be frustrating to otherwise have all markings
  274. // cleared, imo
  275. public void SetSpecies(string species)
  276. {
  277. _currentSpecies = species;
  278. var markingList = _currentMarkings.GetForwardEnumerator().ToList();
  279. var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(species);
  280. _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
  281. _currentMarkings.EnsureSpecies(species, null, _markingManager);
  282. _currentMarkings.EnsureSexes(_currentSex, _markingManager);
  283. Populate(CMarkingSearch.Text);
  284. PopulateUsed();
  285. }
  286. public void SetSex(Sex sex)
  287. {
  288. _currentSex = sex;
  289. var markingList = _currentMarkings.GetForwardEnumerator().ToList();
  290. var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(_currentSpecies);
  291. _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
  292. _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
  293. _currentMarkings.EnsureSexes(_currentSex, _markingManager);
  294. Populate(CMarkingSearch.Text);
  295. PopulateUsed();
  296. }
  297. private void UpdatePoints()
  298. {
  299. var count = _currentMarkings.PointsLeft(_selectedMarkingCategory);
  300. if (count > -1)
  301. {
  302. CMarkingPoints.Text = Loc.GetString("marking-points-remaining", ("points", count));
  303. }
  304. }
  305. private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category)
  306. {
  307. CMarkingCategoryButton.SelectId(category.Id);
  308. _selectedMarkingCategory = _markingCategories[category.Id];
  309. Populate(CMarkingSearch.Text);
  310. PopulateUsed();
  311. UpdatePoints();
  312. }
  313. // TODO: This should be using ColorSelectorSliders once that's merged, so
  314. private void OnUsedMarkingSelected(ItemList.ItemListSelectedEventArgs item)
  315. {
  316. _selectedMarking = CMarkingsUsed[item.ItemIndex];
  317. var prototype = (MarkingPrototype) _selectedMarking.Metadata!;
  318. if (prototype.ForcedColoring)
  319. {
  320. CMarkingColors.Visible = false;
  321. return;
  322. }
  323. var stateNames = GetMarkingStateNames(prototype);
  324. _currentMarkingColors.Clear();
  325. CMarkingColors.DisposeAllChildren();
  326. List<ColorSelectorSliders> colorSliders = new();
  327. for (int i = 0; i < prototype.Sprites.Count; i++)
  328. {
  329. var colorContainer = new BoxContainer
  330. {
  331. Orientation = LayoutOrientation.Vertical,
  332. };
  333. CMarkingColors.AddChild(colorContainer);
  334. ColorSelectorSliders colorSelector = new ColorSelectorSliders();
  335. colorSliders.Add(colorSelector);
  336. colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" });
  337. colorContainer.AddChild(colorSelector);
  338. var listing = _currentMarkings.Markings[_selectedMarkingCategory];
  339. var color = listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i];
  340. var currentColor = new Color(
  341. color.RByte,
  342. color.GByte,
  343. color.BByte
  344. );
  345. colorSelector.Color = currentColor;
  346. _currentMarkingColors.Add(currentColor);
  347. var colorIndex = _currentMarkingColors.Count - 1;
  348. Action<Color> colorChanged = _ =>
  349. {
  350. _currentMarkingColors[colorIndex] = colorSelector.Color;
  351. ColorChanged(colorIndex);
  352. };
  353. colorSelector.OnColorChanged += colorChanged;
  354. }
  355. CMarkingColors.Visible = true;
  356. }
  357. private void ColorChanged(int colorIndex)
  358. {
  359. if (_selectedMarking is null) return;
  360. var markingPrototype = (MarkingPrototype) _selectedMarking.Metadata!;
  361. int markingIndex = _currentMarkings.FindIndexOf(_selectedMarkingCategory, markingPrototype.ID);
  362. if (markingIndex < 0) return;
  363. _selectedMarking.IconModulate = _currentMarkingColors[colorIndex];
  364. var marking = new Marking(_currentMarkings.Markings[_selectedMarkingCategory][markingIndex]);
  365. marking.SetColor(colorIndex, _currentMarkingColors[colorIndex]);
  366. _currentMarkings.Replace(_selectedMarkingCategory, markingIndex, marking);
  367. OnMarkingColorChange?.Invoke(_currentMarkings);
  368. }
  369. private void MarkingAdd()
  370. {
  371. if (_selectedUnusedMarking is null) return;
  372. if (_currentMarkings.PointsLeft(_selectedMarkingCategory) == 0 && !Forced)
  373. {
  374. return;
  375. }
  376. var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!;
  377. var markingObject = marking.AsMarking();
  378. // We need add hair markings in cloned set manually because _currentMarkings doesn't have it
  379. var markingSet = new MarkingSet(_currentMarkings);
  380. if (HairMarking != null)
  381. {
  382. markingSet.AddBack(MarkingCategories.Hair, HairMarking);
  383. }
  384. if (FacialHairMarking != null)
  385. {
  386. markingSet.AddBack(MarkingCategories.FacialHair, FacialHairMarking);
  387. }
  388. if (!_markingManager.MustMatchSkin(_currentSpecies, marking.BodyPart, out var _, _prototypeManager))
  389. {
  390. // Do default coloring
  391. var colors = MarkingColoring.GetMarkingLayerColors(
  392. marking,
  393. CurrentSkinColor,
  394. CurrentEyeColor,
  395. markingSet
  396. );
  397. for (var i = 0; i < colors.Count; i++)
  398. {
  399. markingObject.SetColor(i, colors[i]);
  400. }
  401. }
  402. else
  403. {
  404. // Color everything in skin color
  405. for (var i = 0; i < marking.Sprites.Count; i++)
  406. {
  407. markingObject.SetColor(i, CurrentSkinColor);
  408. }
  409. }
  410. markingObject.Forced = Forced;
  411. _currentMarkings.AddBack(_selectedMarkingCategory, markingObject);
  412. UpdatePoints();
  413. CMarkingsUnused.Remove(_selectedUnusedMarking);
  414. var item = new ItemList.Item(CMarkingsUsed)
  415. {
  416. Text = Loc.GetString("marking-used", ("marking-name", $"{GetMarkingName(marking)}"), ("marking-category", Loc.GetString($"markings-category-{marking.MarkingCategory}"))),
  417. Icon = marking.Sprites[0].Frame0(),
  418. Selectable = true,
  419. Metadata = marking,
  420. };
  421. CMarkingsUsed.Insert(0, item);
  422. _selectedUnusedMarking = null;
  423. OnMarkingAdded?.Invoke(_currentMarkings);
  424. }
  425. private void MarkingRemove()
  426. {
  427. if (_selectedMarking is null) return;
  428. var marking = (MarkingPrototype) _selectedMarking.Metadata!;
  429. _currentMarkings.Remove(_selectedMarkingCategory, marking.ID);
  430. UpdatePoints();
  431. CMarkingsUsed.Remove(_selectedMarking);
  432. if (marking.MarkingCategory == _selectedMarkingCategory)
  433. {
  434. var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", marking.Sprites[0].Frame0());
  435. item.Metadata = marking;
  436. }
  437. _selectedMarking = null;
  438. CMarkingColors.Visible = false;
  439. OnMarkingRemoved?.Invoke(_currentMarkings);
  440. }
  441. }