using Content.Client.Humanoid; using Content.Client.Message; using Content.Client.Players.PlayTimeTracking; using Content.Client.Sprite; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Preferences; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; 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 Direction = Robust.Shared.Maths.Direction; using Content.Client._CD.Records.UI; // CD - Character Records using Content.Shared._DV.Body.Systems; // DV - Traits 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; /// /// If we're attempting to save. /// public event Action? Save; /// /// The character slot for the current profile. /// public int? CharacterSlot; /// /// The work in progress profile being edited. /// public HumanoidCharacterProfile? Profile; private Direction _previewRotation = Direction.North; private bool _isDirty; // Begin CD - Station Records private float _defaultHeight = 1f; private readonly RecordEditorGui _recordsTab; // End CD - Station Records public bool IsDirty { get => _isDirty; set { if (_isDirty == value) return; _isDirty = value; UpdateSaveButton(); } } private ISawmill _sawmill; private MarkingsViewModel _markingsModel = new(); 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); Markings.SetModel(_markingsModel); 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(); }; Traits.OnTraitsChanged += OnTraitsSelectionChanged; // DeltaV #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); OnSkinColorOnValueChanged(); }; // Begin CD - Character Records #region CDHeight CDHeightSlider.OnValueChanged += _ => { if (Profile is null) return; var prototype = _prototypeManager.Index(Profile.Species); var newHeight = MathF.Round(MathHelper.Lerp(prototype.MinHeight, prototype.MaxHeight, CDHeightSlider.Value), 2); var speciesScale = prototype.BaseScale.Y; CDHeightLabel.Text = UnitConversion.GetMetricAndImperialDisplayFromScale(newHeight * speciesScale); CDPullSpeedReductionLabel.Text = SmallCharacterSystem.GetPullSpeedPenaltyDisplayFromScale(newHeight); SetProfileHeight(newHeight); }; #endregion CDHeight // End CD - Character Records #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 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)); _markingsModel.SetOrganEyeColor(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(); // DeltaV #region Markings TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab")); _markingsModel.MarkingsChanged += (_, _) => OnMarkingChange(); _markingsModel.MarkingsReset += OnMarkingChange; #endregion Markings // Begin CD - Character Records #region CosmaticRecords _recordsTab = new RecordEditorGui(UpdateProfileRecords); TabContainer.AddChild(_recordsTab); TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-cd-records-tab")); #endregion CosmaticRecords // End CD - Character Records 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; } 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; } /// /// Reloads the entire dummy entity for preview. /// /// /// This is expensive so not recommended to run if you have a slider. /// private void ReloadPreview() { if (Profile == null) return; SpriteView.LoadPreview(Profile, JobOverride, ShowClothes.Pressed); // 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(); // Begin CD - Character Records UpdateHeightControls(); _recordsTab.Update(profile); // End CD - Character Records UpdateTraitsSelection(); // DeltaV - Traits RefreshAntags(); RefreshJobs(); RefreshLoadouts(); RefreshSpecies(); // RefreshTraits(); // DeltaV 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) return; SpriteView.ReloadProfilePreview(Profile); // Check and set the dirty flag to enable the save/reset buttons as appropriate. SetDirty(); } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!disposing) return; _loadoutWindow?.Dispose(); _loadoutWindow = null; } protected override void EnteredTree() { base.EnteredTree(); ReloadPreview(); } 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); } } }