HumanoidProfileEditor.xaml.cs 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642
  1. using System.IO;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Client.Humanoid;
  5. using Content.Client.Lobby.UI.Loadouts;
  6. using Content.Client.Lobby.UI.Roles;
  7. using Content.Client.Message;
  8. using Content.Client.Players.PlayTimeTracking;
  9. using Content.Client.Sprite;
  10. using Content.Client.Stylesheets;
  11. using Content.Client.UserInterface.Systems.Guidebook;
  12. using Content.Shared.CCVar;
  13. using Content.Shared.Clothing;
  14. using Content.Shared.GameTicking;
  15. using Content.Shared.Guidebook;
  16. using Content.Shared.Humanoid;
  17. using Content.Shared.Humanoid.Markings;
  18. using Content.Shared.Humanoid.Prototypes;
  19. using Content.Shared.Preferences;
  20. using Content.Shared.Preferences.Loadouts;
  21. using Content.Shared.Roles;
  22. using Content.Shared.Traits;
  23. using Robust.Client.AutoGenerated;
  24. using Robust.Client.Graphics;
  25. using Robust.Client.Player;
  26. using Robust.Client.UserInterface;
  27. using Robust.Client.UserInterface.Controls;
  28. using Robust.Client.UserInterface.XAML;
  29. using Robust.Client.Utility;
  30. using Robust.Shared.Configuration;
  31. using Robust.Shared.ContentPack;
  32. using Robust.Shared.Enums;
  33. using Robust.Shared.Prototypes;
  34. using Robust.Shared.Utility;
  35. using Direction = Robust.Shared.Maths.Direction;
  36. namespace Content.Client.Lobby.UI
  37. {
  38. [GenerateTypedNameReferences]
  39. public sealed partial class HumanoidProfileEditor : BoxContainer
  40. {
  41. private readonly IClientPreferencesManager _preferencesManager;
  42. private readonly IConfigurationManager _cfgManager;
  43. private readonly IEntityManager _entManager;
  44. private readonly IFileDialogManager _dialogManager;
  45. private readonly IPlayerManager _playerManager;
  46. private readonly IPrototypeManager _prototypeManager;
  47. private readonly IResourceManager _resManager;
  48. private readonly MarkingManager _markingManager;
  49. private readonly JobRequirementsManager _requirements;
  50. private readonly LobbyUIController _controller;
  51. private FlavorText.FlavorText? _flavorText;
  52. private TextEdit? _flavorTextEdit;
  53. // One at a time.
  54. private LoadoutWindow? _loadoutWindow;
  55. private bool _exporting;
  56. private bool _imaging;
  57. /// <summary>
  58. /// If we're attempting to save.
  59. /// </summary>
  60. public event Action? Save;
  61. /// <summary>
  62. /// Entity used for the profile editor preview
  63. /// </summary>
  64. public EntityUid PreviewDummy;
  65. /// <summary>
  66. /// Temporary override of their selected job, used to preview roles.
  67. /// </summary>
  68. public JobPrototype? JobOverride;
  69. /// <summary>
  70. /// The character slot for the current profile.
  71. /// </summary>
  72. public int? CharacterSlot;
  73. /// <summary>
  74. /// The work in progress profile being edited.
  75. /// </summary>
  76. public HumanoidCharacterProfile? Profile;
  77. private List<SpeciesPrototype> _species = new();
  78. private List<(string, RequirementsSelector)> _jobPriorities = new();
  79. private readonly Dictionary<string, BoxContainer> _jobCategories;
  80. private Direction _previewRotation = Direction.North;
  81. private ColorSelectorSliders _rgbSkinColorSelector;
  82. private bool _isDirty;
  83. [ValidatePrototypeId<GuideEntryPrototype>]
  84. private const string DefaultSpeciesGuidebook = "Species";
  85. public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
  86. private ISawmill _sawmill;
  87. public HumanoidProfileEditor(
  88. IClientPreferencesManager preferencesManager,
  89. IConfigurationManager configurationManager,
  90. IEntityManager entManager,
  91. IFileDialogManager dialogManager,
  92. ILogManager logManager,
  93. IPlayerManager playerManager,
  94. IPrototypeManager prototypeManager,
  95. IResourceManager resManager,
  96. JobRequirementsManager requirements,
  97. MarkingManager markings)
  98. {
  99. RobustXamlLoader.Load(this);
  100. _sawmill = logManager.GetSawmill("profile.editor");
  101. _cfgManager = configurationManager;
  102. _entManager = entManager;
  103. _dialogManager = dialogManager;
  104. _playerManager = playerManager;
  105. _prototypeManager = prototypeManager;
  106. _markingManager = markings;
  107. _preferencesManager = preferencesManager;
  108. _resManager = resManager;
  109. _requirements = requirements;
  110. _controller = UserInterfaceManager.GetUIController<LobbyUIController>();
  111. ImportButton.OnPressed += args =>
  112. {
  113. ImportProfile();
  114. };
  115. ExportButton.OnPressed += args =>
  116. {
  117. ExportProfile();
  118. };
  119. ExportImageButton.OnPressed += args =>
  120. {
  121. ExportImage();
  122. };
  123. OpenImagesButton.OnPressed += args =>
  124. {
  125. _resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
  126. };
  127. ResetButton.OnPressed += args =>
  128. {
  129. SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
  130. };
  131. SaveButton.OnPressed += args =>
  132. {
  133. Save?.Invoke();
  134. };
  135. #region Left
  136. #region Name
  137. NameEdit.OnTextChanged += args => { SetName(args.Text); };
  138. NameRandomize.OnPressed += args => RandomizeName();
  139. RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
  140. WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
  141. #endregion Name
  142. #region Appearance
  143. TabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
  144. #region Sex
  145. SexButton.OnItemSelected += args =>
  146. {
  147. SexButton.SelectId(args.Id);
  148. SetSex((Sex) args.Id);
  149. };
  150. #endregion Sex
  151. #region Age
  152. AgeEdit.OnTextChanged += args =>
  153. {
  154. if (!int.TryParse(args.Text, out var newAge))
  155. return;
  156. SetAge(newAge);
  157. };
  158. #endregion Age
  159. #region Gender
  160. PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
  161. PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
  162. PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
  163. PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
  164. PronounsButton.OnItemSelected += args =>
  165. {
  166. PronounsButton.SelectId(args.Id);
  167. SetGender((Gender) args.Id);
  168. };
  169. #endregion Gender
  170. RefreshSpecies();
  171. SpeciesButton.OnItemSelected += args =>
  172. {
  173. SpeciesButton.SelectId(args.Id);
  174. SetSpecies(_species[args.Id].ID);
  175. UpdateHairPickers();
  176. OnSkinColorOnValueChanged();
  177. };
  178. #region Skin
  179. Skin.OnValueChanged += _ =>
  180. {
  181. OnSkinColorOnValueChanged();
  182. };
  183. RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
  184. _rgbSkinColorSelector.OnColorChanged += _ =>
  185. {
  186. OnSkinColorOnValueChanged();
  187. };
  188. #endregion
  189. #region Hair
  190. HairStylePicker.OnMarkingSelect += newStyle =>
  191. {
  192. if (Profile is null)
  193. return;
  194. Profile = Profile.WithCharacterAppearance(
  195. Profile.Appearance.WithHairStyleName(newStyle.id));
  196. ReloadPreview();
  197. };
  198. HairStylePicker.OnColorChanged += newColor =>
  199. {
  200. if (Profile is null)
  201. return;
  202. Profile = Profile.WithCharacterAppearance(
  203. Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
  204. UpdateCMarkingsHair();
  205. ReloadPreview();
  206. };
  207. FacialHairPicker.OnMarkingSelect += newStyle =>
  208. {
  209. if (Profile is null)
  210. return;
  211. Profile = Profile.WithCharacterAppearance(
  212. Profile.Appearance.WithFacialHairStyleName(newStyle.id));
  213. ReloadPreview();
  214. };
  215. FacialHairPicker.OnColorChanged += newColor =>
  216. {
  217. if (Profile is null)
  218. return;
  219. Profile = Profile.WithCharacterAppearance(
  220. Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
  221. UpdateCMarkingsFacialHair();
  222. ReloadPreview();
  223. };
  224. HairStylePicker.OnSlotRemove += _ =>
  225. {
  226. if (Profile is null)
  227. return;
  228. Profile = Profile.WithCharacterAppearance(
  229. Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
  230. );
  231. UpdateHairPickers();
  232. UpdateCMarkingsHair();
  233. ReloadPreview();
  234. };
  235. FacialHairPicker.OnSlotRemove += _ =>
  236. {
  237. if (Profile is null)
  238. return;
  239. Profile = Profile.WithCharacterAppearance(
  240. Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
  241. );
  242. UpdateHairPickers();
  243. UpdateCMarkingsFacialHair();
  244. ReloadPreview();
  245. };
  246. HairStylePicker.OnSlotAdd += delegate()
  247. {
  248. if (Profile is null)
  249. return;
  250. var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
  251. .FirstOrDefault();
  252. if (string.IsNullOrEmpty(hair))
  253. return;
  254. Profile = Profile.WithCharacterAppearance(
  255. Profile.Appearance.WithHairStyleName(hair)
  256. );
  257. UpdateHairPickers();
  258. UpdateCMarkingsHair();
  259. ReloadPreview();
  260. };
  261. FacialHairPicker.OnSlotAdd += delegate()
  262. {
  263. if (Profile is null)
  264. return;
  265. var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
  266. .FirstOrDefault();
  267. if (string.IsNullOrEmpty(hair))
  268. return;
  269. Profile = Profile.WithCharacterAppearance(
  270. Profile.Appearance.WithFacialHairStyleName(hair)
  271. );
  272. UpdateHairPickers();
  273. UpdateCMarkingsFacialHair();
  274. ReloadPreview();
  275. };
  276. #endregion Hair
  277. #region SpawnPriority
  278. foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
  279. {
  280. SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value);
  281. }
  282. SpawnPriorityButton.OnItemSelected += args =>
  283. {
  284. SpawnPriorityButton.SelectId(args.Id);
  285. SetSpawnPriority((SpawnPriorityPreference) args.Id);
  286. };
  287. #endregion SpawnPriority
  288. #region Eyes
  289. EyeColorPicker.OnEyeColorPicked += newColor =>
  290. {
  291. if (Profile is null)
  292. return;
  293. Profile = Profile.WithCharacterAppearance(
  294. Profile.Appearance.WithEyeColor(newColor));
  295. Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
  296. ReloadProfilePreview();
  297. };
  298. #endregion Eyes
  299. #endregion Appearance
  300. #region Jobs
  301. TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab"));
  302. PreferenceUnavailableButton.AddItem(
  303. Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"),
  304. (int) PreferenceUnavailableMode.StayInLobby);
  305. PreferenceUnavailableButton.AddItem(
  306. Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button",
  307. ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))),
  308. (int) PreferenceUnavailableMode.SpawnAsOverflow);
  309. PreferenceUnavailableButton.OnItemSelected += args =>
  310. {
  311. PreferenceUnavailableButton.SelectId(args.Id);
  312. Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
  313. SetDirty();
  314. };
  315. _jobCategories = new Dictionary<string, BoxContainer>();
  316. RefreshAntags();
  317. RefreshJobs();
  318. #endregion Jobs
  319. TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
  320. RefreshTraits();
  321. #region Markings
  322. TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
  323. Markings.OnMarkingAdded += OnMarkingChange;
  324. Markings.OnMarkingRemoved += OnMarkingChange;
  325. Markings.OnMarkingColorChange += OnMarkingChange;
  326. Markings.OnMarkingRankChange += OnMarkingChange;
  327. #endregion Markings
  328. RefreshFlavorText();
  329. #region Dummy
  330. SpriteRotateLeft.OnPressed += _ =>
  331. {
  332. _previewRotation = _previewRotation.TurnCw();
  333. SetPreviewRotation(_previewRotation);
  334. };
  335. SpriteRotateRight.OnPressed += _ =>
  336. {
  337. _previewRotation = _previewRotation.TurnCcw();
  338. SetPreviewRotation(_previewRotation);
  339. };
  340. #endregion Dummy
  341. #endregion Left
  342. ShowClothes.OnToggled += args =>
  343. {
  344. ReloadPreview();
  345. };
  346. SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
  347. UpdateSpeciesGuidebookIcon();
  348. IsDirty = false;
  349. }
  350. /// <summary>
  351. /// Refreshes the flavor text editor status.
  352. /// </summary>
  353. public void RefreshFlavorText()
  354. {
  355. if (_cfgManager.GetCVar(CCVars.FlavorText))
  356. {
  357. if (_flavorText != null)
  358. return;
  359. _flavorText = new FlavorText.FlavorText();
  360. TabContainer.AddChild(_flavorText);
  361. TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
  362. _flavorTextEdit = _flavorText.CFlavorTextInput;
  363. _flavorText.OnFlavorTextChanged += OnFlavorTextChange;
  364. }
  365. else
  366. {
  367. if (_flavorText == null)
  368. return;
  369. TabContainer.RemoveChild(_flavorText);
  370. _flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
  371. _flavorText.Dispose();
  372. _flavorTextEdit?.Dispose();
  373. _flavorTextEdit = null;
  374. _flavorText = null;
  375. }
  376. }
  377. /// <summary>
  378. /// Refreshes traits selector
  379. /// </summary>
  380. public void RefreshTraits()
  381. {
  382. TraitsList.DisposeAllChildren();
  383. var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
  384. TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
  385. if (traits.Count < 1)
  386. {
  387. TraitsList.AddChild(new Label
  388. {
  389. Text = Loc.GetString("humanoid-profile-editor-no-traits"),
  390. FontColorOverride = Color.Gray,
  391. });
  392. return;
  393. }
  394. // Setup model
  395. Dictionary<string, List<string>> traitGroups = new();
  396. List<string> defaultTraits = new();
  397. traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
  398. foreach (var trait in traits)
  399. {
  400. if (trait.Category == null)
  401. {
  402. defaultTraits.Add(trait.ID);
  403. continue;
  404. }
  405. if (!_prototypeManager.HasIndex(trait.Category))
  406. continue;
  407. var group = traitGroups.GetOrNew(trait.Category);
  408. group.Add(trait.ID);
  409. }
  410. // Create UI view from model
  411. foreach (var (categoryId, categoryTraits) in traitGroups)
  412. {
  413. TraitCategoryPrototype? category = null;
  414. if (categoryId != TraitCategoryPrototype.Default)
  415. {
  416. category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
  417. // Label
  418. TraitsList.AddChild(new Label
  419. {
  420. Text = Loc.GetString(category.Name),
  421. Margin = new Thickness(0, 10, 0, 0),
  422. StyleClasses = { StyleBase.StyleClassLabelHeading },
  423. });
  424. }
  425. List<TraitPreferenceSelector?> selectors = new();
  426. var selectionCount = 0;
  427. foreach (var traitProto in categoryTraits)
  428. {
  429. var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
  430. var selector = new TraitPreferenceSelector(trait);
  431. selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
  432. if (selector.Preference)
  433. selectionCount += trait.Cost;
  434. selector.PreferenceChanged += preference =>
  435. {
  436. if (preference)
  437. {
  438. Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
  439. }
  440. else
  441. {
  442. Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
  443. }
  444. SetDirty();
  445. RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
  446. };
  447. selectors.Add(selector);
  448. }
  449. // Selection counter
  450. if (category is { MaxTraitPoints: >= 0 })
  451. {
  452. TraitsList.AddChild(new Label
  453. {
  454. Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
  455. FontColorOverride = Color.Gray
  456. });
  457. }
  458. foreach (var selector in selectors)
  459. {
  460. if (selector == null)
  461. continue;
  462. if (category is { MaxTraitPoints: >= 0 } &&
  463. selector.Cost + selectionCount > category.MaxTraitPoints)
  464. {
  465. selector.Checkbox.Label.FontColorOverride = Color.Red;
  466. }
  467. TraitsList.AddChild(selector);
  468. }
  469. }
  470. }
  471. /// <summary>
  472. /// Refreshes the species selector.
  473. /// </summary>
  474. public void RefreshSpecies()
  475. {
  476. SpeciesButton.Clear();
  477. _species.Clear();
  478. _species.AddRange(_prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart));
  479. var speciesIds = _species.Select(o => o.ID).ToList();
  480. for (var i = 0; i < _species.Count; i++)
  481. {
  482. var name = Loc.GetString(_species[i].Name);
  483. SpeciesButton.AddItem(name, i);
  484. if (Profile?.Species.Equals(_species[i].ID) == true)
  485. {
  486. SpeciesButton.SelectId(i);
  487. }
  488. }
  489. // If our species isn't available then reset it to default.
  490. if (Profile != null)
  491. {
  492. if (!speciesIds.Contains(Profile.Species))
  493. {
  494. SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
  495. }
  496. }
  497. }
  498. public void RefreshAntags()
  499. {
  500. AntagList.DisposeAllChildren();
  501. var items = new[]
  502. {
  503. ("humanoid-profile-editor-antag-preference-yes-button", 0),
  504. ("humanoid-profile-editor-antag-preference-no-button", 1)
  505. };
  506. foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
  507. {
  508. if (!antag.SetPreference)
  509. continue;
  510. var antagContainer = new BoxContainer()
  511. {
  512. Orientation = LayoutOrientation.Horizontal,
  513. };
  514. var selector = new RequirementsSelector()
  515. {
  516. Margin = new Thickness(3f, 3f, 3f, 0f),
  517. };
  518. selector.OnOpenGuidebook += OnOpenGuidebook;
  519. var title = Loc.GetString(antag.Name);
  520. var description = Loc.GetString(antag.Objective);
  521. selector.Setup(items, title, 250, description, guides: antag.Guides);
  522. selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
  523. var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
  524. if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
  525. {
  526. selector.LockRequirements(reason);
  527. Profile = Profile?.WithAntagPreference(antag.ID, false);
  528. SetDirty();
  529. }
  530. else
  531. {
  532. selector.UnlockRequirements();
  533. }
  534. selector.OnSelected += preference =>
  535. {
  536. Profile = Profile?.WithAntagPreference(antag.ID, preference == 0);
  537. SetDirty();
  538. };
  539. antagContainer.AddChild(selector);
  540. antagContainer.AddChild(new Button()
  541. {
  542. Disabled = true,
  543. Text = Loc.GetString("loadout-window"),
  544. HorizontalAlignment = HAlignment.Right,
  545. Margin = new Thickness(3f, 0f, 0f, 0f),
  546. });
  547. AntagList.AddChild(antagContainer);
  548. }
  549. }
  550. private void SetDirty()
  551. {
  552. // If it equals default then reset the button.
  553. if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true)
  554. {
  555. IsDirty = false;
  556. return;
  557. }
  558. // TODO: Check if profile matches default.
  559. IsDirty = true;
  560. }
  561. /// <summary>
  562. /// Refresh all loadouts.
  563. /// </summary>
  564. public void RefreshLoadouts()
  565. {
  566. _loadoutWindow?.Dispose();
  567. }
  568. /// <summary>
  569. /// Reloads the entire dummy entity for preview.
  570. /// </summary>
  571. /// <remarks>
  572. /// This is expensive so not recommended to run if you have a slider.
  573. /// </remarks>
  574. private void ReloadPreview()
  575. {
  576. _entManager.DeleteEntity(PreviewDummy);
  577. PreviewDummy = EntityUid.Invalid;
  578. if (Profile == null || !_prototypeManager.HasIndex(Profile.Species))
  579. return;
  580. PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
  581. SpriteView.SetEntity(PreviewDummy);
  582. _entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
  583. // Check and set the dirty flag to enable the save/reset buttons as appropriate.
  584. SetDirty();
  585. }
  586. /// <summary>
  587. /// Resets the profile to the defaults.
  588. /// </summary>
  589. public void ResetToDefault()
  590. {
  591. SetProfile(
  592. (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
  593. _preferencesManager.Preferences?.SelectedCharacterIndex);
  594. }
  595. /// <summary>
  596. /// Sets the editor to the specified profile with the specified slot.
  597. /// </summary>
  598. public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
  599. {
  600. Profile = profile?.Clone();
  601. CharacterSlot = slot;
  602. IsDirty = false;
  603. JobOverride = null;
  604. UpdateNameEdit();
  605. UpdateFlavorTextEdit();
  606. UpdateSexControls();
  607. UpdateGenderControls();
  608. UpdateSkinColor();
  609. UpdateSpawnPriorityControls();
  610. UpdateAgeEdit();
  611. UpdateEyePickers();
  612. UpdateSaveButton();
  613. UpdateMarkings();
  614. UpdateHairPickers();
  615. UpdateCMarkingsHair();
  616. UpdateCMarkingsFacialHair();
  617. RefreshAntags();
  618. RefreshJobs();
  619. RefreshLoadouts();
  620. RefreshSpecies();
  621. RefreshTraits();
  622. RefreshFlavorText();
  623. ReloadPreview();
  624. if (Profile != null)
  625. {
  626. PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
  627. }
  628. }
  629. /// <summary>
  630. /// A slim reload that only updates the entity itself and not any of the job entities, etc.
  631. /// </summary>
  632. private void ReloadProfilePreview()
  633. {
  634. if (Profile == null || !_entManager.EntityExists(PreviewDummy))
  635. return;
  636. _entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
  637. // Check and set the dirty flag to enable the save/reset buttons as appropriate.
  638. SetDirty();
  639. }
  640. private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
  641. {
  642. // TODO GUIDEBOOK
  643. // make the species guide book a field on the species prototype.
  644. // I.e., do what jobs/antags do.
  645. var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
  646. var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
  647. var page = DefaultSpeciesGuidebook;
  648. if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
  649. page = species;
  650. if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
  651. {
  652. var dict = new Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry>();
  653. dict.Add(DefaultSpeciesGuidebook, guideRoot);
  654. //TODO: Don't close the guidebook if its already open, just go to the correct page
  655. guidebookController.OpenGuidebook(dict, includeChildren:true, selected: page);
  656. }
  657. }
  658. /// <summary>
  659. /// Refreshes all job selectors.
  660. /// </summary>
  661. public void RefreshJobs()
  662. {
  663. JobList.DisposeAllChildren();
  664. _jobCategories.Clear();
  665. _jobPriorities.Clear();
  666. var firstCategory = true;
  667. // Get all displayed departments
  668. var departments = new List<DepartmentPrototype>();
  669. foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
  670. {
  671. if (department.EditorHidden)
  672. continue;
  673. departments.Add(department);
  674. }
  675. departments.Sort(DepartmentUIComparer.Instance);
  676. var items = new[]
  677. {
  678. ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
  679. ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
  680. ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
  681. ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
  682. };
  683. foreach (var department in departments)
  684. {
  685. var departmentName = Loc.GetString(department.Name);
  686. if (!_jobCategories.TryGetValue(department.ID, out var category))
  687. {
  688. category = new BoxContainer
  689. {
  690. Orientation = LayoutOrientation.Vertical,
  691. Name = department.ID,
  692. ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
  693. ("departmentName", departmentName))
  694. };
  695. if (firstCategory)
  696. {
  697. firstCategory = false;
  698. }
  699. else
  700. {
  701. category.AddChild(new Control
  702. {
  703. MinSize = new Vector2(0, 23),
  704. });
  705. }
  706. category.AddChild(new PanelContainer
  707. {
  708. PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")},
  709. Children =
  710. {
  711. new Label
  712. {
  713. Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
  714. ("departmentName", departmentName)),
  715. Margin = new Thickness(5f, 0, 0, 0)
  716. }
  717. }
  718. });
  719. _jobCategories[department.ID] = category;
  720. JobList.AddChild(category);
  721. }
  722. var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
  723. .Where(job => job.SetPreference)
  724. .ToArray();
  725. Array.Sort(jobs, JobUIComparer.Instance);
  726. foreach (var job in jobs)
  727. {
  728. var jobContainer = new BoxContainer()
  729. {
  730. Orientation = LayoutOrientation.Horizontal,
  731. };
  732. var selector = new RequirementsSelector()
  733. {
  734. Margin = new Thickness(3f, 3f, 3f, 0f),
  735. };
  736. selector.OnOpenGuidebook += OnOpenGuidebook;
  737. var icon = new TextureRect
  738. {
  739. TextureScale = new Vector2(2, 2),
  740. VerticalAlignment = VAlignment.Center
  741. };
  742. var jobIcon = _prototypeManager.Index(job.Icon);
  743. icon.Texture = jobIcon.Icon.Frame0();
  744. selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides);
  745. if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
  746. {
  747. selector.LockRequirements(reason);
  748. }
  749. else
  750. {
  751. selector.UnlockRequirements();
  752. }
  753. selector.OnSelected += selectedPrio =>
  754. {
  755. var selectedJobPrio = (JobPriority) selectedPrio;
  756. Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio);
  757. foreach (var (jobId, other) in _jobPriorities)
  758. {
  759. // Sync other selectors with the same job in case of multiple department jobs
  760. if (jobId == job.ID)
  761. {
  762. other.Select(selectedPrio);
  763. continue;
  764. }
  765. if (selectedJobPrio != JobPriority.High || (JobPriority) other.Selected != JobPriority.High)
  766. continue;
  767. // Lower any other high priorities to medium.
  768. other.Select((int)JobPriority.Medium);
  769. Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
  770. }
  771. // TODO: Only reload on high change (either to or from).
  772. ReloadPreview();
  773. UpdateJobPriorities();
  774. SetDirty();
  775. };
  776. var loadoutWindowBtn = new Button()
  777. {
  778. Text = Loc.GetString("loadout-window"),
  779. HorizontalAlignment = HAlignment.Right,
  780. VerticalAlignment = VAlignment.Center,
  781. Margin = new Thickness(3f, 3f, 0f, 0f),
  782. };
  783. var collection = IoCManager.Instance!;
  784. var protoManager = collection.Resolve<IPrototypeManager>();
  785. // If no loadout found then disabled button
  786. if (!protoManager.TryIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto))
  787. {
  788. loadoutWindowBtn.Disabled = true;
  789. }
  790. // else
  791. else
  792. {
  793. loadoutWindowBtn.OnPressed += args =>
  794. {
  795. RoleLoadout? loadout = null;
  796. // Clone so we don't modify the underlying loadout.
  797. Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
  798. loadout = loadout?.Clone();
  799. if (loadout == null)
  800. {
  801. loadout = new RoleLoadout(roleLoadoutProto.ID);
  802. loadout.SetDefault(Profile, _playerManager.LocalSession, _prototypeManager);
  803. }
  804. OpenLoadout(job, loadout, roleLoadoutProto);
  805. };
  806. }
  807. _jobPriorities.Add((job.ID, selector));
  808. jobContainer.AddChild(selector);
  809. jobContainer.AddChild(loadoutWindowBtn);
  810. category.AddChild(jobContainer);
  811. }
  812. }
  813. UpdateJobPriorities();
  814. }
  815. private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto)
  816. {
  817. _loadoutWindow?.Dispose();
  818. _loadoutWindow = null;
  819. var collection = IoCManager.Instance;
  820. if (collection == null || _playerManager.LocalSession == null || Profile == null)
  821. return;
  822. JobOverride = jobProto;
  823. var session = _playerManager.LocalSession;
  824. _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection)
  825. {
  826. Title = jobProto?.ID + "-loadout",
  827. };
  828. // Refresh the buttons etc.
  829. _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
  830. _loadoutWindow.OpenCenteredLeft();
  831. _loadoutWindow.OnNameChanged += name =>
  832. {
  833. roleLoadout.EntityName = name;
  834. Profile = Profile.WithLoadout(roleLoadout);
  835. SetDirty();
  836. };
  837. _loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) =>
  838. {
  839. roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager);
  840. _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
  841. Profile = Profile?.WithLoadout(roleLoadout);
  842. ReloadPreview();
  843. };
  844. _loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) =>
  845. {
  846. roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager);
  847. _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
  848. Profile = Profile?.WithLoadout(roleLoadout);
  849. ReloadPreview();
  850. };
  851. JobOverride = jobProto;
  852. ReloadPreview();
  853. _loadoutWindow.OnClose += () =>
  854. {
  855. JobOverride = null;
  856. ReloadPreview();
  857. };
  858. if (Profile is null)
  859. return;
  860. UpdateJobPriorities();
  861. }
  862. private void OnFlavorTextChange(string content)
  863. {
  864. if (Profile is null)
  865. return;
  866. Profile = Profile.WithFlavorText(content);
  867. SetDirty();
  868. }
  869. private void OnMarkingChange(MarkingSet markings)
  870. {
  871. if (Profile is null)
  872. return;
  873. Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
  874. ReloadProfilePreview();
  875. }
  876. private void OnSkinColorOnValueChanged()
  877. {
  878. if (Profile is null) return;
  879. var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
  880. switch (skin)
  881. {
  882. case HumanoidSkinColor.HumanToned:
  883. {
  884. if (!Skin.Visible)
  885. {
  886. Skin.Visible = true;
  887. RgbSkinColorContainer.Visible = false;
  888. }
  889. var color = SkinColor.HumanSkinTone((int) Skin.Value);
  890. Markings.CurrentSkinColor = color;
  891. Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
  892. break;
  893. }
  894. case HumanoidSkinColor.Hues:
  895. {
  896. if (!RgbSkinColorContainer.Visible)
  897. {
  898. Skin.Visible = false;
  899. RgbSkinColorContainer.Visible = true;
  900. }
  901. Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
  902. Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
  903. break;
  904. }
  905. case HumanoidSkinColor.TintedHues:
  906. {
  907. if (!RgbSkinColorContainer.Visible)
  908. {
  909. Skin.Visible = false;
  910. RgbSkinColorContainer.Visible = true;
  911. }
  912. var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
  913. Markings.CurrentSkinColor = color;
  914. Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
  915. break;
  916. }
  917. case HumanoidSkinColor.VoxFeathers:
  918. {
  919. if (!RgbSkinColorContainer.Visible)
  920. {
  921. Skin.Visible = false;
  922. RgbSkinColorContainer.Visible = true;
  923. }
  924. var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
  925. Markings.CurrentSkinColor = color;
  926. Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
  927. break;
  928. }
  929. }
  930. ReloadProfilePreview();
  931. }
  932. protected override void Dispose(bool disposing)
  933. {
  934. base.Dispose(disposing);
  935. if (!disposing)
  936. return;
  937. _loadoutWindow?.Dispose();
  938. _loadoutWindow = null;
  939. }
  940. protected override void EnteredTree()
  941. {
  942. base.EnteredTree();
  943. ReloadPreview();
  944. }
  945. protected override void ExitedTree()
  946. {
  947. base.ExitedTree();
  948. _entManager.DeleteEntity(PreviewDummy);
  949. PreviewDummy = EntityUid.Invalid;
  950. }
  951. private void SetAge(int newAge)
  952. {
  953. Profile = Profile?.WithAge(newAge);
  954. ReloadPreview();
  955. }
  956. private void SetSex(Sex newSex)
  957. {
  958. Profile = Profile?.WithSex(newSex);
  959. // for convenience, default to most common gender when new sex is selected
  960. switch (newSex)
  961. {
  962. case Sex.Male:
  963. Profile = Profile?.WithGender(Gender.Male);
  964. break;
  965. case Sex.Female:
  966. Profile = Profile?.WithGender(Gender.Female);
  967. break;
  968. default:
  969. Profile = Profile?.WithGender(Gender.Epicene);
  970. break;
  971. }
  972. UpdateGenderControls();
  973. Markings.SetSex(newSex);
  974. ReloadPreview();
  975. }
  976. private void SetGender(Gender newGender)
  977. {
  978. Profile = Profile?.WithGender(newGender);
  979. ReloadPreview();
  980. }
  981. private void SetSpecies(string newSpecies)
  982. {
  983. Profile = Profile?.WithSpecies(newSpecies);
  984. OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
  985. Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
  986. // In case there's job restrictions for the species
  987. RefreshJobs();
  988. // In case there's species restrictions for loadouts
  989. RefreshLoadouts();
  990. UpdateSexControls(); // update sex for new species
  991. UpdateSpeciesGuidebookIcon();
  992. ReloadPreview();
  993. }
  994. private void SetName(string newName)
  995. {
  996. Profile = Profile?.WithName(newName);
  997. SetDirty();
  998. if (!IsDirty)
  999. return;
  1000. _entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
  1001. }
  1002. private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
  1003. {
  1004. Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
  1005. SetDirty();
  1006. }
  1007. public bool IsDirty
  1008. {
  1009. get => _isDirty;
  1010. set
  1011. {
  1012. if (_isDirty == value)
  1013. return;
  1014. _isDirty = value;
  1015. UpdateSaveButton();
  1016. }
  1017. }
  1018. private void UpdateNameEdit()
  1019. {
  1020. NameEdit.Text = Profile?.Name ?? "";
  1021. }
  1022. private void UpdateFlavorTextEdit()
  1023. {
  1024. if (_flavorTextEdit != null)
  1025. {
  1026. _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
  1027. }
  1028. }
  1029. private void UpdateAgeEdit()
  1030. {
  1031. AgeEdit.Text = Profile?.Age.ToString() ?? "";
  1032. }
  1033. /// <summary>
  1034. /// Updates selected job priorities to the profile's.
  1035. /// </summary>
  1036. private void UpdateJobPriorities()
  1037. {
  1038. foreach (var (jobId, prioritySelector) in _jobPriorities)
  1039. {
  1040. var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
  1041. prioritySelector.Select((int) priority);
  1042. }
  1043. }
  1044. private void UpdateSexControls()
  1045. {
  1046. if (Profile == null)
  1047. return;
  1048. SexButton.Clear();
  1049. var sexes = new List<Sex>();
  1050. // add species sex options, default to just none if we are in bizzaro world and have no species
  1051. if (_prototypeManager.TryIndex<SpeciesPrototype>(Profile.Species, out var speciesProto))
  1052. {
  1053. foreach (var sex in speciesProto.Sexes)
  1054. {
  1055. sexes.Add(sex);
  1056. }
  1057. }
  1058. else
  1059. {
  1060. sexes.Add(Sex.Unsexed);
  1061. }
  1062. // add button for each sex
  1063. foreach (var sex in sexes)
  1064. {
  1065. SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex);
  1066. }
  1067. if (sexes.Contains(Profile.Sex))
  1068. SexButton.SelectId((int) Profile.Sex);
  1069. else
  1070. SexButton.SelectId((int) sexes[0]);
  1071. }
  1072. private void UpdateSkinColor()
  1073. {
  1074. if (Profile == null)
  1075. return;
  1076. var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
  1077. switch (skin)
  1078. {
  1079. case HumanoidSkinColor.HumanToned:
  1080. {
  1081. if (!Skin.Visible)
  1082. {
  1083. Skin.Visible = true;
  1084. RgbSkinColorContainer.Visible = false;
  1085. }
  1086. Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
  1087. break;
  1088. }
  1089. case HumanoidSkinColor.Hues:
  1090. {
  1091. if (!RgbSkinColorContainer.Visible)
  1092. {
  1093. Skin.Visible = false;
  1094. RgbSkinColorContainer.Visible = true;
  1095. }
  1096. // set the RGB values to the direct values otherwise
  1097. _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
  1098. break;
  1099. }
  1100. case HumanoidSkinColor.TintedHues:
  1101. {
  1102. if (!RgbSkinColorContainer.Visible)
  1103. {
  1104. Skin.Visible = false;
  1105. RgbSkinColorContainer.Visible = true;
  1106. }
  1107. // set the RGB values to the direct values otherwise
  1108. _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
  1109. break;
  1110. }
  1111. case HumanoidSkinColor.VoxFeathers:
  1112. {
  1113. if (!RgbSkinColorContainer.Visible)
  1114. {
  1115. Skin.Visible = false;
  1116. RgbSkinColorContainer.Visible = true;
  1117. }
  1118. _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
  1119. break;
  1120. }
  1121. }
  1122. }
  1123. public void UpdateSpeciesGuidebookIcon()
  1124. {
  1125. SpeciesInfoButton.StyleClasses.Clear();
  1126. var species = Profile?.Species;
  1127. if (species is null)
  1128. return;
  1129. if (!_prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesProto))
  1130. return;
  1131. // Don't display the info button if no guide entry is found
  1132. if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
  1133. return;
  1134. const string style = "SpeciesInfoDefault";
  1135. SpeciesInfoButton.StyleClasses.Add(style);
  1136. }
  1137. private void UpdateMarkings()
  1138. {
  1139. if (Profile == null)
  1140. {
  1141. return;
  1142. }
  1143. Markings.SetData(Profile.Appearance.Markings, Profile.Species,
  1144. Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor
  1145. );
  1146. }
  1147. private void UpdateGenderControls()
  1148. {
  1149. if (Profile == null)
  1150. {
  1151. return;
  1152. }
  1153. PronounsButton.SelectId((int) Profile.Gender);
  1154. }
  1155. private void UpdateSpawnPriorityControls()
  1156. {
  1157. if (Profile == null)
  1158. {
  1159. return;
  1160. }
  1161. SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
  1162. }
  1163. private void UpdateHairPickers()
  1164. {
  1165. if (Profile == null)
  1166. {
  1167. return;
  1168. }
  1169. var hairMarking = Profile.Appearance.HairStyleId switch
  1170. {
  1171. HairStyles.DefaultHairStyle => new List<Marking>(),
  1172. _ => new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) },
  1173. };
  1174. var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
  1175. {
  1176. HairStyles.DefaultFacialHairStyle => new List<Marking>(),
  1177. _ => new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) },
  1178. };
  1179. HairStylePicker.UpdateData(
  1180. hairMarking,
  1181. Profile.Species,
  1182. 1);
  1183. FacialHairPicker.UpdateData(
  1184. facialHairMarking,
  1185. Profile.Species,
  1186. 1);
  1187. }
  1188. private void UpdateCMarkingsHair()
  1189. {
  1190. if (Profile == null)
  1191. {
  1192. return;
  1193. }
  1194. // hair color
  1195. Color? hairColor = null;
  1196. if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle &&
  1197. _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto)
  1198. )
  1199. {
  1200. if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager))
  1201. {
  1202. if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
  1203. {
  1204. hairColor = Profile.Appearance.SkinColor;
  1205. }
  1206. else
  1207. {
  1208. hairColor = Profile.Appearance.HairColor;
  1209. }
  1210. }
  1211. }
  1212. if (hairColor != null)
  1213. {
  1214. Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List<Color>() { hairColor.Value });
  1215. }
  1216. else
  1217. {
  1218. Markings.HairMarking = null;
  1219. }
  1220. }
  1221. private void UpdateCMarkingsFacialHair()
  1222. {
  1223. if (Profile == null)
  1224. {
  1225. return;
  1226. }
  1227. // facial hair color
  1228. Color? facialHairColor = null;
  1229. if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
  1230. _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
  1231. {
  1232. if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
  1233. {
  1234. if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
  1235. {
  1236. facialHairColor = Profile.Appearance.SkinColor;
  1237. }
  1238. else
  1239. {
  1240. facialHairColor = Profile.Appearance.FacialHairColor;
  1241. }
  1242. }
  1243. }
  1244. if (facialHairColor != null)
  1245. {
  1246. Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<Color>() { facialHairColor.Value });
  1247. }
  1248. else
  1249. {
  1250. Markings.FacialHairMarking = null;
  1251. }
  1252. }
  1253. private void UpdateEyePickers()
  1254. {
  1255. if (Profile == null)
  1256. {
  1257. return;
  1258. }
  1259. Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
  1260. EyeColorPicker.SetData(Profile.Appearance.EyeColor);
  1261. }
  1262. private void UpdateSaveButton()
  1263. {
  1264. SaveButton.Disabled = Profile is null || !IsDirty;
  1265. ResetButton.Disabled = Profile is null || !IsDirty;
  1266. }
  1267. private void SetPreviewRotation(Direction direction)
  1268. {
  1269. SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
  1270. }
  1271. private void RandomizeEverything()
  1272. {
  1273. Profile = HumanoidCharacterProfile.Random();
  1274. SetProfile(Profile, CharacterSlot);
  1275. SetDirty();
  1276. }
  1277. private void RandomizeName()
  1278. {
  1279. if (Profile == null) return;
  1280. var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
  1281. SetName(name);
  1282. UpdateNameEdit();
  1283. }
  1284. private async void ExportImage()
  1285. {
  1286. if (_imaging)
  1287. return;
  1288. var dir = SpriteView.OverrideDirection ?? Direction.South;
  1289. // I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save
  1290. _imaging = true;
  1291. await _entManager.System<ContentSpriteSystem>().Export(PreviewDummy, dir, includeId: false);
  1292. _imaging = false;
  1293. }
  1294. private async void ImportProfile()
  1295. {
  1296. if (_exporting || CharacterSlot == null || Profile == null)
  1297. return;
  1298. StartExport();
  1299. await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
  1300. if (file == null)
  1301. {
  1302. EndExport();
  1303. return;
  1304. }
  1305. try
  1306. {
  1307. var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
  1308. var oldProfile = Profile;
  1309. SetProfile(profile, CharacterSlot);
  1310. IsDirty = !profile.MemberwiseEquals(oldProfile);
  1311. }
  1312. catch (Exception exc)
  1313. {
  1314. _sawmill.Error($"Error when importing profile\n{exc.StackTrace}");
  1315. }
  1316. finally
  1317. {
  1318. EndExport();
  1319. }
  1320. }
  1321. private async void ExportProfile()
  1322. {
  1323. if (Profile == null || _exporting)
  1324. return;
  1325. StartExport();
  1326. var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
  1327. if (file == null)
  1328. {
  1329. EndExport();
  1330. return;
  1331. }
  1332. try
  1333. {
  1334. var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
  1335. await using var writer = new StreamWriter(file.Value.fileStream);
  1336. dataNode.Write(writer);
  1337. }
  1338. catch (Exception exc)
  1339. {
  1340. _sawmill.Error($"Error when exporting profile\n{exc.StackTrace}");
  1341. }
  1342. finally
  1343. {
  1344. EndExport();
  1345. await file.Value.fileStream.DisposeAsync();
  1346. }
  1347. }
  1348. private void StartExport()
  1349. {
  1350. _exporting = true;
  1351. ImportButton.Disabled = true;
  1352. ExportButton.Disabled = true;
  1353. }
  1354. private void EndExport()
  1355. {
  1356. _exporting = false;
  1357. ImportButton.Disabled = false;
  1358. ExportButton.Disabled = false;
  1359. }
  1360. }
  1361. }