using System.IO; using System.Linq; using System.Numerics; using Content.Client.Humanoid; using Content.Client.Lobby.UI.Loadouts; using Content.Client.Lobby.UI.Roles; using Content.Client.Message; using Content.Client.Players.PlayTimeTracking; using Content.Client.Sprite; using Content.Client.Stylesheets; using Content.Client.UserInterface.Systems.Guidebook; using Content.Shared.CCVar; using Content.Shared.Clothing; using Content.Shared.GameTicking; using Content.Shared.Guidebook; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; using Content.Shared.Traits; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Client.Utility; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Direction = Robust.Shared.Maths.Direction; namespace Content.Client.Lobby.UI { [GenerateTypedNameReferences] public sealed partial class HumanoidProfileEditor : BoxContainer { private readonly IClientPreferencesManager _preferencesManager; private readonly IConfigurationManager _cfgManager; private readonly IEntityManager _entManager; private readonly IFileDialogManager _dialogManager; private readonly IPlayerManager _playerManager; private readonly IPrototypeManager _prototypeManager; private readonly IResourceManager _resManager; private readonly MarkingManager _markingManager; private readonly JobRequirementsManager _requirements; private readonly LobbyUIController _controller; private readonly SpriteSystem _sprite; // CCvar. private int _maxNameLength; private bool _allowFlavorText; private FlavorText.FlavorText? _flavorText; private TextEdit? _flavorTextEdit; // One at a time. private LoadoutWindow? _loadoutWindow; private bool _exporting; private bool _imaging; /// /// If we're attempting to save. /// public event Action? Save; /// /// Entity used for the profile editor preview /// public EntityUid PreviewDummy; /// /// Temporary override of their selected job, used to preview roles. /// public JobPrototype? JobOverride; /// /// The character slot for the current profile. /// public int? CharacterSlot; /// /// The work in progress profile being edited. /// public HumanoidCharacterProfile? Profile; private List _species = new(); private List<(string, RequirementsSelector)> _jobPriorities = new(); private readonly Dictionary _jobCategories; private Direction _previewRotation = Direction.North; private ColorSelectorSliders _rgbSkinColorSelector; private bool _isDirty; private static readonly ProtoId DefaultSpeciesGuidebook = "Species"; public event Action>>? OnOpenGuidebook; private ISawmill _sawmill; public HumanoidProfileEditor( IClientPreferencesManager preferencesManager, IConfigurationManager configurationManager, IEntityManager entManager, IFileDialogManager dialogManager, ILogManager logManager, IPlayerManager playerManager, IPrototypeManager prototypeManager, IResourceManager resManager, JobRequirementsManager requirements, MarkingManager markings) { RobustXamlLoader.Load(this); _sawmill = logManager.GetSawmill("profile.editor"); _cfgManager = configurationManager; _entManager = entManager; _dialogManager = dialogManager; _playerManager = playerManager; _prototypeManager = prototypeManager; _markingManager = markings; _preferencesManager = preferencesManager; _resManager = resManager; _requirements = requirements; _controller = UserInterfaceManager.GetUIController(); _sprite = _entManager.System(); _maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength); _allowFlavorText = _cfgManager.GetCVar(CCVars.FlavorText); ImportButton.OnPressed += args => { ImportProfile(); }; ExportButton.OnPressed += args => { ExportProfile(); }; ExportImageButton.OnPressed += args => { ExportImage(); }; OpenImagesButton.OnPressed += args => { _resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports); }; ResetButton.OnPressed += args => { SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); }; SaveButton.OnPressed += args => { Save?.Invoke(); }; #region Left #region Name NameEdit.OnTextChanged += args => { SetName(args.Text); }; NameEdit.IsValid = args => args.Length <= _maxNameLength; NameRandomize.OnPressed += args => RandomizeName(); RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); }; WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]"); #endregion Name #region Appearance TabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab")); #region Sex SexButton.OnItemSelected += args => { SexButton.SelectId(args.Id); SetSex((Sex) args.Id); }; #endregion Sex #region Age AgeEdit.OnTextChanged += args => { if (!int.TryParse(args.Text, out var newAge)) return; SetAge(newAge); }; #endregion Age #region Gender PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male); PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female); PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene); PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter); PronounsButton.OnItemSelected += args => { PronounsButton.SelectId(args.Id); SetGender((Gender) args.Id); }; #endregion Gender RefreshSpecies(); SpeciesButton.OnItemSelected += args => { SpeciesButton.SelectId(args.Id); SetSpecies(_species[args.Id].ID); UpdateHairPickers(); OnSkinColorOnValueChanged(); }; #region Skin Skin.OnValueChanged += _ => { OnSkinColorOnValueChanged(); }; RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders()); _rgbSkinColorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV _rgbSkinColorSelector.OnColorChanged += _ => { OnSkinColorOnValueChanged(); }; #endregion #region Hair HairStylePicker.OnMarkingSelect += newStyle => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(newStyle.id)); ReloadPreview(); }; HairStylePicker.OnColorChanged += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0])); UpdateCMarkingsHair(); ReloadPreview(); }; FacialHairPicker.OnMarkingSelect += newStyle => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(newStyle.id)); ReloadPreview(); }; FacialHairPicker.OnColorChanged += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0])); UpdateCMarkingsFacialHair(); ReloadPreview(); }; HairStylePicker.OnSlotRemove += _ => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle) ); UpdateHairPickers(); UpdateCMarkingsHair(); ReloadPreview(); }; FacialHairPicker.OnSlotRemove += _ => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle) ); UpdateHairPickers(); UpdateCMarkingsFacialHair(); ReloadPreview(); }; HairStylePicker.OnSlotAdd += delegate() { if (Profile is null) return; var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys .FirstOrDefault(); if (string.IsNullOrEmpty(hair)) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithHairStyleName(hair) ); UpdateHairPickers(); UpdateCMarkingsHair(); ReloadPreview(); }; FacialHairPicker.OnSlotAdd += delegate() { if (Profile is null) return; var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys .FirstOrDefault(); if (string.IsNullOrEmpty(hair)) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithFacialHairStyleName(hair) ); UpdateHairPickers(); UpdateCMarkingsFacialHair(); ReloadPreview(); }; #endregion Hair #region SpawnPriority foreach (var value in Enum.GetValues()) { SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value); } SpawnPriorityButton.OnItemSelected += args => { SpawnPriorityButton.SelectId(args.Id); SetSpawnPriority((SpawnPriorityPreference) args.Id); }; #endregion SpawnPriority #region Eyes EyeColorPicker.OnEyeColorPicked += newColor => { if (Profile is null) return; Profile = Profile.WithCharacterAppearance( Profile.Appearance.WithEyeColor(newColor)); Markings.CurrentEyeColor = Profile.Appearance.EyeColor; ReloadProfilePreview(); }; #endregion Eyes #endregion Appearance #region Jobs TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab")); PreferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"), (int) PreferenceUnavailableMode.StayInLobby); PreferenceUnavailableButton.AddItem( Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button", ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))), (int) PreferenceUnavailableMode.SpawnAsOverflow); PreferenceUnavailableButton.OnItemSelected += args => { PreferenceUnavailableButton.SelectId(args.Id); Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id); SetDirty(); }; _jobCategories = new Dictionary(); RefreshAntags(); RefreshJobs(); #endregion Jobs TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab")); RefreshTraits(); #region Markings TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab")); Markings.OnMarkingAdded += OnMarkingChange; Markings.OnMarkingRemoved += OnMarkingChange; Markings.OnMarkingColorChange += OnMarkingChange; Markings.OnMarkingRankChange += OnMarkingChange; #endregion Markings RefreshFlavorText(); #region Dummy SpriteRotateLeft.OnPressed += _ => { _previewRotation = _previewRotation.TurnCw(); SetPreviewRotation(_previewRotation); }; SpriteRotateRight.OnPressed += _ => { _previewRotation = _previewRotation.TurnCcw(); SetPreviewRotation(_previewRotation); }; #endregion Dummy #endregion Left ShowClothes.OnToggled += args => { ReloadPreview(); }; SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed; UpdateSpeciesGuidebookIcon(); IsDirty = false; } /// /// Refreshes the flavor text editor status. /// public void RefreshFlavorText() { if (_allowFlavorText) { if (_flavorText != null) return; _flavorText = new FlavorText.FlavorText(); TabContainer.AddChild(_flavorText); TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab")); _flavorTextEdit = _flavorText.CFlavorTextInput; _flavorText.OnFlavorTextChanged += OnFlavorTextChange; } else { if (_flavorText == null) return; TabContainer.RemoveChild(_flavorText); _flavorText.OnFlavorTextChanged -= OnFlavorTextChange; _flavorText.Dispose(); _flavorTextEdit?.Dispose(); _flavorTextEdit = null; _flavorText = null; } } /// /// Refreshes traits selector /// public void RefreshTraits() { TraitsList.DisposeAllChildren(); var traits = _prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList(); TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab")); if (traits.Count < 1) { TraitsList.AddChild(new Label { Text = Loc.GetString("humanoid-profile-editor-no-traits"), FontColorOverride = Color.Gray, }); return; } // Setup model Dictionary> traitGroups = new(); List defaultTraits = new(); traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits); foreach (var trait in traits) { if (trait.Category == null) { defaultTraits.Add(trait.ID); continue; } if (!_prototypeManager.HasIndex(trait.Category)) continue; var group = traitGroups.GetOrNew(trait.Category); group.Add(trait.ID); } // Create UI view from model foreach (var (categoryId, categoryTraits) in traitGroups) { TraitCategoryPrototype? category = null; if (categoryId != TraitCategoryPrototype.Default) { category = _prototypeManager.Index(categoryId); // Label TraitsList.AddChild(new Label { Text = Loc.GetString(category.Name), Margin = new Thickness(0, 10, 0, 0), StyleClasses = { StyleBase.StyleClassLabelHeading }, }); } List selectors = new(); var selectionCount = 0; foreach (var traitProto in categoryTraits) { var trait = _prototypeManager.Index(traitProto); var selector = new TraitPreferenceSelector(trait); selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true; if (selector.Preference) selectionCount += trait.Cost; selector.PreferenceChanged += preference => { if (preference) { Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager); } else { Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager); } SetDirty(); RefreshTraits(); // If too many traits are selected, they will be reset to the real value. }; selectors.Add(selector); } // Selection counter if (category is { MaxTraitPoints: >= 0 }) { TraitsList.AddChild(new Label { Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)), FontColorOverride = Color.Gray }); } foreach (var selector in selectors) { if (selector == null) continue; if (category is { MaxTraitPoints: >= 0 } && selector.Cost + selectionCount > category.MaxTraitPoints) { selector.Checkbox.Label.FontColorOverride = Color.Red; } TraitsList.AddChild(selector); } } } /// /// Refreshes the species selector. /// public void RefreshSpecies() { SpeciesButton.Clear(); _species.Clear(); _species.AddRange(_prototypeManager.EnumeratePrototypes().Where(o => o.RoundStart)); var speciesIds = _species.Select(o => o.ID).ToList(); for (var i = 0; i < _species.Count; i++) { var name = Loc.GetString(_species[i].Name); SpeciesButton.AddItem(name, i); if (Profile?.Species.Equals(_species[i].ID) == true) { SpeciesButton.SelectId(i); } } // If our species isn't available then reset it to default. if (Profile != null) { if (!speciesIds.Contains(Profile.Species)) { SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies); } } } public void RefreshAntags() { AntagList.DisposeAllChildren(); var items = new[] { ("humanoid-profile-editor-antag-preference-yes-button", 0), ("humanoid-profile-editor-antag-preference-no-button", 1) }; foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name))) { if (!antag.SetPreference) continue; var antagContainer = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, }; var selector = new RequirementsSelector() { Margin = new Thickness(3f, 3f, 3f, 0f), }; selector.OnOpenGuidebook += OnOpenGuidebook; var title = Loc.GetString(antag.Name); var description = Loc.GetString(antag.Objective); selector.Setup(items, title, 250, description, guides: antag.Guides); selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1); var requirements = _entManager.System().GetAntagRequirement(antag); if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) { selector.LockRequirements(reason); Profile = Profile?.WithAntagPreference(antag.ID, false); SetDirty(); } else { selector.UnlockRequirements(); } selector.OnSelected += preference => { Profile = Profile?.WithAntagPreference(antag.ID, preference == 0); SetDirty(); }; antagContainer.AddChild(selector); antagContainer.AddChild(new Button() { Disabled = true, Text = Loc.GetString("loadout-window"), HorizontalAlignment = HAlignment.Right, Margin = new Thickness(3f, 0f, 0f, 0f), }); AntagList.AddChild(antagContainer); } } private void SetDirty() { // If it equals default then reset the button. if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true) { IsDirty = false; return; } // TODO: Check if profile matches default. IsDirty = true; } /// /// Refresh all loadouts. /// public void RefreshLoadouts() { _loadoutWindow?.Dispose(); } /// /// Reloads the entire dummy entity for preview. /// /// /// This is expensive so not recommended to run if you have a slider. /// private void ReloadPreview() { _entManager.DeleteEntity(PreviewDummy); PreviewDummy = EntityUid.Invalid; if (Profile == null || !_prototypeManager.HasIndex(Profile.Species)) return; PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed); SpriteView.SetEntity(PreviewDummy); _entManager.System().SetEntityName(PreviewDummy, Profile.Name); // Check and set the dirty flag to enable the save/reset buttons as appropriate. SetDirty(); } /// /// Resets the profile to the defaults. /// public void ResetToDefault() { SetProfile( (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex); } /// /// Sets the editor to the specified profile with the specified slot. /// public void SetProfile(HumanoidCharacterProfile? profile, int? slot) { Profile = profile?.Clone(); CharacterSlot = slot; IsDirty = false; JobOverride = null; UpdateNameEdit(); UpdateFlavorTextEdit(); UpdateSexControls(); UpdateGenderControls(); UpdateSkinColor(); UpdateSpawnPriorityControls(); UpdateAgeEdit(); UpdateEyePickers(); UpdateSaveButton(); UpdateMarkings(); UpdateHairPickers(); UpdateCMarkingsHair(); UpdateCMarkingsFacialHair(); RefreshAntags(); RefreshJobs(); RefreshLoadouts(); RefreshSpecies(); RefreshTraits(); RefreshFlavorText(); ReloadPreview(); if (Profile != null) { PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable); } } /// /// A slim reload that only updates the entity itself and not any of the job entities, etc. /// private void ReloadProfilePreview() { if (Profile == null || !_entManager.EntityExists(PreviewDummy)) return; _entManager.System().LoadProfile(PreviewDummy, Profile); // Check and set the dirty flag to enable the save/reset buttons as appropriate. SetDirty(); } private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args) { // TODO GUIDEBOOK // make the species guide book a field on the species prototype. // I.e., do what jobs/antags do. var guidebookController = UserInterfaceManager.GetUIController(); var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies; var page = DefaultSpeciesGuidebook; if (_prototypeManager.HasIndex(species)) page = new ProtoId(species.Id); // Gross. See above todo comment. if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot)) { var dict = new Dictionary, GuideEntry>(); dict.Add(DefaultSpeciesGuidebook, guideRoot); //TODO: Don't close the guidebook if its already open, just go to the correct page guidebookController.OpenGuidebook(dict, includeChildren:true, selected: page); } } /// /// Refreshes all job selectors. /// public void RefreshJobs() { JobList.DisposeAllChildren(); _jobCategories.Clear(); _jobPriorities.Clear(); var firstCategory = true; // Get all displayed departments var departments = new List(); foreach (var department in _prototypeManager.EnumeratePrototypes()) { if (department.EditorHidden) continue; departments.Add(department); } departments.Sort(DepartmentUIComparer.Instance); var items = new[] { ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never), ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low), ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium), ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High), }; foreach (var department in departments) { var departmentName = Loc.GetString(department.Name); if (!_jobCategories.TryGetValue(department.ID, out var category)) { category = new BoxContainer { Orientation = LayoutOrientation.Vertical, Name = department.ID, ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip", ("departmentName", departmentName)) }; if (firstCategory) { firstCategory = false; } else { category.AddChild(new Control { MinSize = new Vector2(0, 23), }); } category.AddChild(new PanelContainer { PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")}, Children = { new Label { Text = Loc.GetString("humanoid-profile-editor-department-jobs-label", ("departmentName", departmentName)), Margin = new Thickness(5f, 0, 0, 0) } } }); _jobCategories[department.ID] = category; JobList.AddChild(category); } var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId)) .Where(job => job.SetPreference) .ToArray(); Array.Sort(jobs, JobUIComparer.Instance); foreach (var job in jobs) { var jobContainer = new BoxContainer() { Orientation = LayoutOrientation.Horizontal, }; var selector = new RequirementsSelector() { Margin = new Thickness(3f, 3f, 3f, 0f), }; selector.OnOpenGuidebook += OnOpenGuidebook; var icon = new TextureRect { TextureScale = new Vector2(2, 2), VerticalAlignment = VAlignment.Center }; var jobIcon = _prototypeManager.Index(job.Icon); icon.Texture = _sprite.Frame0(jobIcon.Icon); selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides); if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason)) { selector.LockRequirements(reason); } else { selector.UnlockRequirements(); } selector.OnSelected += selectedPrio => { var selectedJobPrio = (JobPriority) selectedPrio; Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio); foreach (var (jobId, other) in _jobPriorities) { // Sync other selectors with the same job in case of multiple department jobs if (jobId == job.ID) { other.Select(selectedPrio); continue; } if (selectedJobPrio != JobPriority.High || (JobPriority) other.Selected != JobPriority.High) continue; // Lower any other high priorities to medium. other.Select((int)JobPriority.Medium); Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium); } // TODO: Only reload on high change (either to or from). ReloadPreview(); UpdateJobPriorities(); SetDirty(); }; var loadoutWindowBtn = new Button() { Text = Loc.GetString("loadout-window"), HorizontalAlignment = HAlignment.Right, VerticalAlignment = VAlignment.Center, Margin = new Thickness(3f, 3f, 0f, 0f), }; var collection = IoCManager.Instance!; var protoManager = collection.Resolve(); // If no loadout found then disabled button if (!protoManager.TryIndex(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto)) { loadoutWindowBtn.Disabled = true; } // else else { loadoutWindowBtn.OnPressed += args => { RoleLoadout? loadout = null; // Clone so we don't modify the underlying loadout. Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout); loadout = loadout?.Clone(); if (loadout == null) { loadout = new RoleLoadout(roleLoadoutProto.ID); loadout.SetDefault(Profile, _playerManager.LocalSession, _prototypeManager); } OpenLoadout(job, loadout, roleLoadoutProto); }; } _jobPriorities.Add((job.ID, selector)); jobContainer.AddChild(selector); jobContainer.AddChild(loadoutWindowBtn); category.AddChild(jobContainer); } } UpdateJobPriorities(); } private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto) { _loadoutWindow?.Dispose(); _loadoutWindow = null; var collection = IoCManager.Instance; if (collection == null || _playerManager.LocalSession == null || Profile == null) return; JobOverride = jobProto; var session = _playerManager.LocalSession; _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection) { Title = Loc.GetString("loadout-window-title-loadout", ("job", $"{jobProto?.LocalizedName}")), }; // Refresh the buttons etc. _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); _loadoutWindow.OpenCenteredLeft(); _loadoutWindow.OnNameChanged += name => { roleLoadout.EntityName = name; Profile = Profile.WithLoadout(roleLoadout); SetDirty(); }; _loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) => { roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager); _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); Profile = Profile?.WithLoadout(roleLoadout); ReloadPreview(); }; _loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) => { roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager); _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection); Profile = Profile?.WithLoadout(roleLoadout); ReloadPreview(); }; JobOverride = jobProto; ReloadPreview(); _loadoutWindow.OnClose += () => { JobOverride = null; ReloadPreview(); }; if (Profile is null) return; UpdateJobPriorities(); } private void OnFlavorTextChange(string content) { if (Profile is null) return; Profile = Profile.WithFlavorText(content); SetDirty(); } private void OnMarkingChange(MarkingSet markings) { if (Profile is null) return; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList())); ReloadProfilePreview(); } private void OnSkinColorOnValueChanged() { if (Profile is null) return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; switch (skin) { case HumanoidSkinColor.HumanToned: { if (!Skin.Visible) { Skin.Visible = true; RgbSkinColorContainer.Visible = false; } var color = SkinColor.HumanSkinTone((int) Skin.Value); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));// break; } case HumanoidSkinColor.Hues: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } Markings.CurrentSkinColor = _rgbSkinColorSelector.Color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color)); break; } case HumanoidSkinColor.TintedHues: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); break; } case HumanoidSkinColor.VoxFeathers: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color); Markings.CurrentSkinColor = color; Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color)); break; } } ReloadProfilePreview(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; _loadoutWindow?.Dispose(); _loadoutWindow = null; } protected override void EnteredTree() { base.EnteredTree(); ReloadPreview(); } protected override void ExitedTree() { base.ExitedTree(); _entManager.DeleteEntity(PreviewDummy); PreviewDummy = EntityUid.Invalid; } private void SetAge(int newAge) { Profile = Profile?.WithAge(newAge); ReloadPreview(); } private void SetSex(Sex newSex) { Profile = Profile?.WithSex(newSex); // for convenience, default to most common gender when new sex is selected switch (newSex) { case Sex.Male: Profile = Profile?.WithGender(Gender.Male); break; case Sex.Female: Profile = Profile?.WithGender(Gender.Female); break; default: Profile = Profile?.WithGender(Gender.Epicene); break; } UpdateGenderControls(); Markings.SetSex(newSex); ReloadPreview(); } private void SetGender(Gender newGender) { Profile = Profile?.WithGender(newGender); ReloadPreview(); } private void SetSpecies(string newSpecies) { Profile = Profile?.WithSpecies(newSpecies); OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it. Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well. // In case there's job restrictions for the species RefreshJobs(); // In case there's species restrictions for loadouts RefreshLoadouts(); UpdateSexControls(); // update sex for new species UpdateSpeciesGuidebookIcon(); ReloadPreview(); } private void SetName(string newName) { Profile = Profile?.WithName(newName); SetDirty(); if (!IsDirty) return; _entManager.System().SetEntityName(PreviewDummy, newName); } private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority) { Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority); SetDirty(); } public bool IsDirty { get => _isDirty; set { if (_isDirty == value) return; _isDirty = value; UpdateSaveButton(); } } private void UpdateNameEdit() { NameEdit.Text = Profile?.Name ?? ""; } private void UpdateFlavorTextEdit() { if (_flavorTextEdit != null) { _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? ""); } } private void UpdateAgeEdit() { AgeEdit.Text = Profile?.Age.ToString() ?? ""; } /// /// Updates selected job priorities to the profile's. /// private void UpdateJobPriorities() { foreach (var (jobId, prioritySelector) in _jobPriorities) { var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never; prioritySelector.Select((int) priority); } } private void UpdateSexControls() { if (Profile == null) return; SexButton.Clear(); var sexes = new List(); // add species sex options, default to just none if we are in bizzaro world and have no species if (_prototypeManager.Resolve(Profile.Species, out var speciesProto)) { foreach (var sex in speciesProto.Sexes) { sexes.Add(sex); } } else { sexes.Add(Sex.Unsexed); } // add button for each sex foreach (var sex in sexes) { SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex); } if (sexes.Contains(Profile.Sex)) SexButton.SelectId((int) Profile.Sex); else SexButton.SelectId((int) sexes[0]); } private void UpdateSkinColor() { if (Profile == null) return; var skin = _prototypeManager.Index(Profile.Species).SkinColoration; switch (skin) { case HumanoidSkinColor.HumanToned: { if (!Skin.Visible) { Skin.Visible = true; RgbSkinColorContainer.Visible = false; } Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor); break; } case HumanoidSkinColor.Hues: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } // set the RGB values to the direct values otherwise _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; break; } case HumanoidSkinColor.TintedHues: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } // set the RGB values to the direct values otherwise _rgbSkinColorSelector.Color = Profile.Appearance.SkinColor; break; } case HumanoidSkinColor.VoxFeathers: { if (!RgbSkinColorContainer.Visible) { Skin.Visible = false; RgbSkinColorContainer.Visible = true; } _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor); break; } } } public void UpdateSpeciesGuidebookIcon() { SpeciesInfoButton.StyleClasses.Clear(); var species = Profile?.Species; if (species is null) return; if (!_prototypeManager.Resolve(species, out var speciesProto)) return; // Don't display the info button if no guide entry is found if (!_prototypeManager.HasIndex(species)) return; const string style = "SpeciesInfoDefault"; SpeciesInfoButton.StyleClasses.Add(style); } private void UpdateMarkings() { if (Profile == null) { return; } Markings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor ); } private void UpdateGenderControls() { if (Profile == null) { return; } PronounsButton.SelectId((int) Profile.Gender); } private void UpdateSpawnPriorityControls() { if (Profile == null) { return; } SpawnPriorityButton.SelectId((int) Profile.SpawnPriority); } private void UpdateHairPickers() { if (Profile == null) { return; } var hairMarking = Profile.Appearance.HairStyleId == HairStyles.DefaultHairStyle ? new List() : new() { new(Profile.Appearance.HairStyleId, new List() { Profile.Appearance.HairColor }) }; var facialHairMarking = Profile.Appearance.FacialHairStyleId == HairStyles.DefaultFacialHairStyle ? new List() : new() { new(Profile.Appearance.FacialHairStyleId, new List() { Profile.Appearance.FacialHairColor }) }; HairStylePicker.UpdateData( hairMarking, Profile.Species, 1); FacialHairPicker.UpdateData( facialHairMarking, Profile.Species, 1); } private void UpdateCMarkingsHair() { if (Profile == null) { return; } // hair color Color? hairColor = null; if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle && _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto) ) { if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager)) { if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager)) { hairColor = Profile.Appearance.SkinColor; } else { hairColor = Profile.Appearance.HairColor; } } } if (hairColor != null) { Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List() { hairColor.Value }); } else { Markings.HairMarking = null; } } private void UpdateCMarkingsFacialHair() { if (Profile == null) { return; } // facial hair color Color? facialHairColor = null; if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle && _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto)) { if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager)) { if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager)) { facialHairColor = Profile.Appearance.SkinColor; } else { facialHairColor = Profile.Appearance.FacialHairColor; } } } if (facialHairColor != null) { Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List() { facialHairColor.Value }); } else { Markings.FacialHairMarking = null; } } private void UpdateEyePickers() { if (Profile == null) { return; } Markings.CurrentEyeColor = Profile.Appearance.EyeColor; EyeColorPicker.SetData(Profile.Appearance.EyeColor); } private void UpdateSaveButton() { SaveButton.Disabled = Profile is null || !IsDirty; ResetButton.Disabled = Profile is null || !IsDirty; } private void SetPreviewRotation(Direction direction) { SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2); } private void RandomizeEverything() { Profile = HumanoidCharacterProfile.Random(); SetProfile(Profile, CharacterSlot); SetDirty(); } private void RandomizeName() { if (Profile == null) return; var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender); SetName(name); UpdateNameEdit(); } private async void ExportImage() { if (_imaging) return; var dir = SpriteView.OverrideDirection ?? Direction.South; // I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save _imaging = true; await _entManager.System().Export(PreviewDummy, dir, includeId: false); _imaging = false; } private async void ImportProfile() { if (_exporting || CharacterSlot == null || Profile == null) return; StartExport(); await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml"))); if (file == null) { EndExport(); return; } try { var profile = _entManager.System().FromStream(file, _playerManager.LocalSession!); var oldProfile = Profile; SetProfile(profile, CharacterSlot); IsDirty = !profile.MemberwiseEquals(oldProfile); } catch (Exception exc) { _sawmill.Error($"Error when importing profile\n{exc.StackTrace}"); } finally { EndExport(); } } private async void ExportProfile() { if (Profile == null || _exporting) return; StartExport(); var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml"))); if (file == null) { EndExport(); return; } try { var dataNode = _entManager.System().ToDataNode(Profile); await using var writer = new StreamWriter(file.Value.fileStream); dataNode.Write(writer); } catch (Exception exc) { _sawmill.Error($"Error when exporting profile\n{exc.StackTrace}"); } finally { EndExport(); await file.Value.fileStream.DisposeAsync(); } } private void StartExport() { _exporting = true; ImportButton.Disabled = true; ExportButton.Disabled = true; } private void EndExport() { _exporting = false; ImportButton.Disabled = false; ExportButton.Disabled = false; } } }