Files
wwdpublic/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
SX-7 ff3595de9e Tajara (#1647)
# Description

~~On user end Felinids are removed and replaced with Tajara, mostly
inspired by the ParadiseStation sprites.~~

~~Most things from Felinids are ported over, either being untouched (in
which case they're left in the nyanotrasen directory), or somewhat
modified and copied to tajara directories. This is done because both
shadowkin and humans reference these files quite a bit, so we don't want
to break anything forwards/backwards,~~

~~Under the hood, felinid components are untouched, just simply set to
`abstract: true` and `roundstart: false`, once again to prevent any
breakage.~~

There's been like 50 changes all the ways, but basically, felinids are
staying, tajara get a few languages, they also are more "specialized"
than felinids due to big upsides and downsides (funny :)

---

# TODO

- [X] Add tajara
~~- [X] Hide felinids~~
~~- [x] Analyze and find now-orphaned felinid
protorypes/textures/references and remove them (optional)~~
- [x] Fix graphical bugs (if any)
- [x] Ensure the code structure is compliant with repo practices

---

<details><summary><h1>Media</h1></summary>
<p>

In captain attire

![{3169C46E-BABB-466F-BD75-D8A00D8F9105}](https://github.com/user-attachments/assets/74a7c43f-9d5d-440f-9c18-f1eb386ad3d7)
Testing emotes and languages

![{7EE18197-E788-46BF-9DA3-97B0718A2F0B}](https://github.com/user-attachments/assets/74dd9c17-92a2-4760-99f8-1bb893fa969d)
Emotes still working, same with hairballs

![{346EB863-EF92-4D82-8E72-409437372DC3}](https://github.com/user-attachments/assets/91b61aae-d460-4373-a0ba-79cbd64f8ec4)
Few character selector pictures
- The previously shown off tajaran

![{559C39A8-5333-4787-A3B7-2F882EB6B5E2}](https://github.com/user-attachments/assets/38d77c48-caa2-461f-858a-a6b1a235cc0f)
- Markingless male body

![{9D762061-C1F8-40A5-9F14-5809E803820C}](https://github.com/user-attachments/assets/719c18ba-0230-4b17-9166-fdd15cb67526)

![{98AFAEF1-D49A-4951-A18E-5E55ACC05A26}](https://github.com/user-attachments/assets/6f848c0a-9a1e-4aed-aeb7-0bbf7c1dab62)
- Markingless female body

![{8F42252C-DDC4-43FD-8B8F-BFF0FBD971CD}](https://github.com/user-attachments/assets/92ea11bc-2ddc-4569-b166-0380b34ee946)

![{EE2B152D-97AD-4C39-B51E-8D89E4C4B097}](https://github.com/user-attachments/assets/c6f3a8ee-20be-486a-bea8-b862cf2ea12e)
- Traits are updated, as is the case with other texts

![{6F1E0309-7262-48BA-ABF6-C2048E087437}](https://github.com/user-attachments/assets/778ede7d-3069-4b81-b09d-32aadb6c6b76)
~~- And a neon colored specimen to show off some markings~~ No longer
the case, fur hue is clamped to some reasonable colors

![{381F7D38-1ABE-4434-8F29-95908E8F1A4B}](https://github.com/user-attachments/assets/1ef52004-db37-4127-a113-0b5640087cc1)

</p>
</details>

---

# Changelog

🆑
- add: Added Tajara and related content

---------

Signed-off-by: SX-7 <92227810+SX-7@users.noreply.github.com>
Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
(cherry picked from commit b7660b95566c8e138cb458a3d70732c1d3ea4602)
2025-01-29 20:19:56 +03:00

2577 lines
98 KiB
C#

using System.IO;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.UI;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.Loadouts.Prototypes;
using Content.Shared.Clothing.Loadouts.Systems;
using Content.Shared.Customization.Systems;
using Content.Shared.Dataset;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Lobby.UI
{
[GenerateTypedNameReferences]
public sealed partial class HumanoidProfileEditor : BoxContainer
{
private readonly IConfigurationManager _cfgManager;
private readonly IEntityManager _entManager;
private readonly IFileDialogManager _dialogManager;
private readonly IPlayerManager _playerManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IClientPreferencesManager _preferencesManager;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private readonly CharacterRequirementsSystem _characterRequirementsSystem;
private readonly LobbyUIController _controller;
private readonly IRobustRandom _random;
private FlavorText.FlavorText? _flavorText;
private BoxContainer _ccustomspecienamecontainerEdit => CCustomSpecieName;
private LineEdit _customspecienameEdit => CCustomSpecieNameEdit;
private TextEdit? _flavorTextEdit;
/// If we're attempting to save
public event Action? Save;
private bool _exporting;
private bool _isDirty;
/// The character slot for the current profile
public int? CharacterSlot;
/// The work in progress profile being edited
public HumanoidCharacterProfile? Profile;
/// Entity Used for the profile editor preview
public EntityUid PreviewDummy;
/// Temporary override of their selected job, used to preview roles
public JobPrototype? JobOverride;
private List<SpeciesPrototype> _species = new();
private List<(string, RequirementsSelector)> _jobPriorities = new();
private readonly Dictionary<string, BoxContainer> _jobCategories;
private Dictionary<Button, ConfirmationData> _confirmationData = new();
private List<TraitPreferenceSelector> _traitPreferences = new();
private int _traitCount;
private HashSet<LoadoutPreferenceSelector> _loadoutPreferences = new();
private Direction _previewRotation = Direction.North;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _customizePronouns;
private bool _customizeStationAiName;
private bool _customizeBorgName;
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string StationAiNames = "NamesAI";
[ValidatePrototypeId<DatasetPrototype>]
private const string CyborgNames = "names_borg";
public HumanoidProfileEditor(
IClientPreferencesManager preferencesManager,
IConfigurationManager cfgManager,
IEntityManager entManager,
IFileDialogManager dialogManager,
IPlayerManager playerManager,
IPrototypeManager prototypeManager,
JobRequirementsManager requirements,
MarkingManager markings,
IRobustRandom random
)
{
RobustXamlLoader.Load(this);
_cfgManager = cfgManager;
_entManager = entManager;
_dialogManager = dialogManager;
_playerManager = playerManager;
_prototypeManager = prototypeManager;
_markingManager = markings;
_preferencesManager = preferencesManager;
_requirements = requirements;
_random = random;
_characterRequirementsSystem = _entManager.System<CharacterRequirementsSystem>();
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
ImportButton.OnPressed += args => { ImportProfile(); };
ExportButton.OnPressed += args => { ExportProfile(); };
SaveButton.OnPressed += args => { Save?.Invoke(); };
ResetButton.OnPressed += args =>
{
SetProfile(
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
_preferencesManager.Preferences?.SelectedCharacterIndex);
};
#region Left
#region Name
NameEdit.OnTextChanged += args => { SetName(args.Text); };
NameRandomize.OnPressed += _ => RandomizeName();
RandomizeEverything.OnPressed += _ => { RandomizeProfile(); };
WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
#endregion Name
#region Custom Species Name
_customspecienameEdit.OnTextChanged += args => { SetCustomSpecieName(args.Text); };
#endregion Custom Species Name
#region Appearance
Appearance.Orphan();
CTabContainer.AddTab(Appearance, Loc.GetString("humanoid-profile-editor-appearance-tab"));
#region Sex
SexButton.OnItemSelected += args =>
{
SexButton.SelectId(args.Id);
SetSex((Sex) args.Id);
};
#endregion Sex
// WD EDIT START
#region Voice
InitializeVoice();
#endregion
// WD EDIT END
#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);
if (Profile?.DisplayPronouns == null)
UpdateDisplayPronounsControls();
};
#endregion Gender
#region Cosmetic Pronouns
_customizePronouns = _cfgManager.GetCVar(CCVars.AllowCosmeticPronouns);
_cfgManager.OnValueChanged(CCVars.AllowCosmeticPronouns, OnCosmeticPronounsValueChanged);
CosmeticPronounsNameEdit.OnTextChanged += args => { SetDisplayPronouns(args.Text); };
if (CosmeticPronousContainer.Visible != _customizePronouns)
CosmeticPronousContainer.Visible = _customizePronouns;
#endregion Cosmetic Pronouns
#region Custom Names
_customizeStationAiName = _cfgManager.GetCVar(CCVars.AllowCustomStationAiName);
_customizeBorgName = _cfgManager.GetCVar(CCVars.AllowCustomCyborgName);
_cfgManager.OnValueChanged(CCVars.AllowCustomStationAiName, OnChangedStationAiNameCustomizationValue);
_cfgManager.OnValueChanged(CCVars.AllowCustomCyborgName, OnChangedCyborgNameCustomizationValue);
StationAINameEdit.OnTextChanged += args => { SetStationAiName(args.Text); };
CyborgNameEdit.OnTextChanged += args => { SetCyborgName(args.Text); };
if (StationAiNameContainer.Visible != _customizeStationAiName)
StationAiNameContainer.Visible = _customizeStationAiName;
if (CyborgNameContainer.Visible != _customizeBorgName)
CyborgNameContainer.Visible = _customizeBorgName;
#endregion
#region Species
RefreshSpecies();
SpeciesButton.OnItemSelected += args =>
{
SpeciesButton.SelectId(args.Id);
SetSpecies(_species[args.Id].ID);
UpdateHairPickers();
OnSkinColorOnValueChanged();
UpdateCustomSpecieNameEdit();
UpdateHeightWidthSliders();
};
#endregion Species
#region Height and Width
var prototype = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
UpdateHeightWidthSliders();
HeightSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Height);
WidthSlider.OnValueChanged += _ => UpdateDimensions(SliderUpdate.Width);
HeightReset.OnPressed += _ =>
{
HeightSlider.Value = prototype.DefaultHeight;
UpdateDimensions(SliderUpdate.Height);
};
WidthReset.OnPressed += _ =>
{
WidthSlider.Value = prototype.DefaultWidth;
UpdateDimensions(SliderUpdate.Width);
};
#endregion Height
#region Skin
Skin.OnValueChanged += _ => { OnSkinColorOnValueChanged(); };
RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new());
_rgbSkinColorSelector.OnColorChanged += _ => { OnSkinColorOnValueChanged(); };
#endregion
#region Hair
HairStylePicker.OnMarkingSelect += newStyle =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithHairStyleName(newStyle.id));
IsDirty = true;
ReloadProfilePreview();
};
HairStylePicker.OnColorChanged += newColor =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsHair();
IsDirty = true;
ReloadProfilePreview();
};
FacialHairPicker.OnMarkingSelect += newStyle =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
IsDirty = true;
ReloadProfilePreview();
};
FacialHairPicker.OnColorChanged += newColor =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsFacialHair();
IsDirty = true;
ReloadProfilePreview();
};
HairStylePicker.OnSlotRemove += _ =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
);
UpdateHairPickers();
UpdateCMarkingsHair();
IsDirty = true;
ReloadProfilePreview();
};
FacialHairPicker.OnSlotRemove += _ =>
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
);
UpdateHairPickers();
UpdateCMarkingsFacialHair();
IsDirty = true;
ReloadProfilePreview();
};
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();
IsDirty = true;
ReloadProfilePreview();
};
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();
IsDirty = true;
ReloadProfilePreview();
};
#endregion Hair
#region SpawnPriority
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
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;
IsDirty = true;
ReloadProfilePreview();
};
#endregion Eyes
#endregion Appearance
#region Jobs
Jobs.Orphan();
CTabContainer.AddTab(Jobs, 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);
IsDirty = true;
};
_jobCategories = new Dictionary<string, BoxContainer>();
#endregion Jobs
#region Antags
Antags.Orphan();
CTabContainer.AddTab(Antags, Loc.GetString("humanoid-profile-editor-antags-tab"));
#endregion Antags
#region Traits
// Set up the traits tab
TraitsTab.Orphan();
CTabContainer.AddTab(TraitsTab, Loc.GetString("humanoid-profile-editor-traits-tab"));
_traitPreferences = new List<TraitPreferenceSelector>();
// Show/Hide the traits tab if they ever get enabled/disabled
var traitsEnabled = cfgManager.GetCVar(CCVars.GameTraitsEnabled);
CTabContainer.SetTabVisible(3, traitsEnabled);
cfgManager.OnValueChanged(CCVars.GameTraitsEnabled,
enabled => CTabContainer.SetTabVisible(3, enabled));
TraitsShowUnusableButton.OnToggled += args => UpdateTraits(args.Pressed);
TraitsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableTraits();
UpdateTraits(false);
#endregion
#region Loadouts
// Set up the loadouts tab
LoadoutsTab.Orphan();
CTabContainer.AddTab(LoadoutsTab, Loc.GetString("humanoid-profile-editor-loadouts-tab"));
_loadoutPreferences = new();
// Show/Hide the loadouts tab if they ever get enabled/disabled
var loadoutsEnabled = cfgManager.GetCVar(CCVars.GameLoadoutsEnabled);
CTabContainer.SetTabVisible(4, loadoutsEnabled);
ShowLoadouts.Visible = loadoutsEnabled;
cfgManager.OnValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
LoadoutsShowUnusableButton.OnToggled += args => UpdateLoadouts(args.Pressed);
LoadoutsRemoveUnusableButton.OnPressed += _ => TryRemoveUnusableLoadouts();
UpdateLoadouts(false);
#endregion
#region Markings
MarkingsTab.Orphan();
CTabContainer.AddTab(MarkingsTab, 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 += _ => { SetProfile(Profile, CharacterSlot); };
ShowLoadouts.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
UpdateSpeciesGuidebookIcon();
ReloadPreview();
IsDirty = false;
}
/// Refreshes the flavor text editor status
public void RefreshFlavorText()
{
if (_cfgManager.GetCVar(CCVars.FlavorText))
{
if (_flavorText != null)
return;
_flavorText = new();
_flavorText.OnFlavorTextChanged += OnFlavorTextChange;
_flavorTextEdit = _flavorText.CFlavorTextInput;
CTabContainer.AddTab(_flavorText, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
}
else
{
if (_flavorText == null)
return;
CTabContainer.RemoveChild(_flavorText);
_flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
_flavorText.Dispose();
_flavorText = null;
_flavorTextEdit?.Dispose();
_flavorTextEdit = null;
}
}
private void OnCosmeticPronounsValueChanged(bool newValue)
{
_customizePronouns = newValue;
CosmeticPronousContainer.Visible = newValue;
}
private void OnChangedStationAiNameCustomizationValue(bool newValue)
{
_customizeStationAiName = newValue;
StationAiNameContainer.Visible = newValue;
}
private void OnChangedCyborgNameCustomizationValue(bool newValue)
{
_customizeBorgName = newValue;
CyborgNameContainer.Visible = newValue;
}
/// Refreshes the species selector
public void RefreshSpecies()
{
SpeciesButton.Clear();
_species.Clear();
_species.AddRange(_prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart));
var speciesIds = _species.Select(o => o.ID).ToList();
for (var i = 0; i < _species.Count; i++)
{
SpeciesButton.AddItem(Loc.GetString(_species[i].Name), i);
if (Profile?.Species.Equals(_species[i].ID) == true)
SpeciesButton.SelectId(i);
}
// If our species isn't available, reset it to default
if (Profile != null && !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<AntagPrototype>().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),
};
var title = Loc.GetString(antag.Name);
var description = Loc.GetString(antag.Objective);
selector.Setup(items, title, 250, description);
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
if (!_characterRequirementsSystem.CheckRequirementsValid(
antag.Requirements ?? new(),
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
antag,
_entManager,
_prototypeManager,
_cfgManager,
out var reasons))
{
var reason = _characterRequirementsSystem.GetRequirementsText(reasons);
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);
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;
}
/// Reloads the entire dummy entity for preview
/// <remarks>This is expensive so not recommended to run if you have a slider</remarks>
private void ReloadPreview()
{
_entManager.DeleteEntity(PreviewDummy);
PreviewDummy = EntityUid.Invalid;
if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
return;
PreviewDummy = _controller.LoadProfileEntity(Profile, ShowClothes.Pressed, ShowLoadouts.Pressed);
SpriteView.SetEntity(PreviewDummy);
}
/// Reloads the dummy entity's clothes for preview
private void ReloadClothes()
{
if (Profile == null)
return;
_controller.RemoveDummyClothes(PreviewDummy);
var job = _controller.GetPreferredJob(Profile);
if (ShowClothes.Pressed)
_controller.GiveDummyJobClothes(PreviewDummy, job, Profile);
if (ShowLoadouts.Pressed)
_controller.GiveDummyLoadout(PreviewDummy, job, Profile);
}
/// 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();
UpdateSexControls();
UpdateTTSVoicesControls(); // WD EDIT
UpdateGenderControls();
UpdateDisplayPronounsControls();
UpdateStationAiControls();
UpdateCyborgControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
UpdateFlavorTextEdit();
UpdateCustomSpecieNameEdit();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
UpdateHeightWidthSliders();
UpdateWeight();
UpdateCharacterRequired();
RefreshAntags();
RefreshJobs();
RefreshSpecies();
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;
if (_entManager.TryGetComponent<HumanoidAppearanceComponent>(PreviewDummy, out var humanoid))
{
var hiddenLayers = humanoid.HiddenLayers;
var appearanceSystem = _entManager.System<HumanoidAppearanceSystem>();
appearanceSystem.LoadProfile(PreviewDummy, Profile, humanoid);
// Reapply the hidden layers set from clothing
appearanceSystem.SetLayersVisibility(PreviewDummy, hiddenLayers, false, humanoid: humanoid);
}
SetPreviewRotation(_previewRotation);
TraitsTabs.UpdateTabMerging();
LoadoutsTabs.UpdateTabMerging();
}
private void LoadoutsChanged(bool enabled)
{
CTabContainer.SetTabVisible(4, enabled);
ShowLoadouts.Visible = enabled;
}
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = species;
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<string, GuideEntry> { { DefaultSpeciesGuidebook, guideRoot } };
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(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<DepartmentPrototype>();
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
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-{department.ID}");
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<JobPrototype>(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(3f, 3f, 3f, 0f) };
var icon = new TextureRect
{
TextureScale = new(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = _prototypeManager.Index<JobIconPrototype>(job.Icon);
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
if (!_requirements.CheckJobWhitelist(job, out var reason))
selector.LockRequirements(reason);
else if (!_characterRequirementsSystem.CheckRequirementsValid(
job.Requirements ?? new(),
job,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
job,
_entManager,
_prototypeManager,
_cfgManager,
out var reasons))
selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
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);
else if (selectedJobPrio == JobPriority.High &&
(JobPriority) other.Selected == JobPriority.High)
{
// 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();
};
_jobPriorities.Add((job.ID, selector));
jobContainer.AddChild(selector);
category.AddChild(jobContainer);
}
}
UpdateJobPriorities();
}
private void UpdateRoleRequirements()
{
JobList.DisposeAllChildren();
_jobCategories.Clear();
_jobPriorities.Clear();
var firstCategory = true;
var departments = _prototypeManager.EnumeratePrototypes<DepartmentPrototype>().ToArray();
Array.Sort(departments, 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-{department.ID}");
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("#232323")}, // WD EDIT
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), };
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = _prototypeManager.Index(job.Icon);
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
if (!_requirements.CheckJobWhitelist(job, out var reason))
selector.LockRequirements(reason);
else if (!_characterRequirementsSystem.CheckRequirementsValid(
job.Requirements ?? new(),
job,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
job,
_entManager,
_prototypeManager,
_cfgManager,
out var reasons))
selector.LockRequirements(_characterRequirementsSystem.GetRequirementsText(reasons));
else
selector.UnlockRequirements();
category.AddChild(selector);
_jobPriorities.Add((job.ID, selector));
EnsureJobRequirementsValid(); // DeltaV
selector.OnSelected += priority =>
{
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(other.Selected);
else if ((JobPriority) other.Selected == JobPriority.High && (JobPriority) other.Selected == JobPriority.High)
{
// Lower any other high priorities to medium.
other.Select((int) JobPriority.Medium);
Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
}
}
Profile = Profile?.WithJobPriority(job.ID, (JobPriority) priority);
ReloadPreview();
SetDirty();
SetProfile(Profile, CharacterSlot);
};
_jobPriorities.Add((job.ID, selector));
category.AddChild(jobContainer);
}
}
if (Profile is not null)
UpdateJobPriorities();
}
/// DeltaV - Make sure that no invalid job priorities get through
private void EnsureJobRequirementsValid()
{
foreach (var (jobId, selector) in _jobPriorities)
{
var proto = _prototypeManager.Index<JobPrototype>(jobId);
if ((JobPriority) selector.Selected == JobPriority.Never
|| _requirements.CheckJobWhitelist(proto, out _)
|| _characterRequirementsSystem.CheckRequirementsValid(
proto.Requirements ?? new(),
proto,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
proto,
_entManager,
_prototypeManager,
_cfgManager,
out _))
continue;
selector.Select((int) JobPriority.Never);
Profile = Profile?.WithJobPriority(proto.ID, JobPriority.Never);
}
}
private void OnFlavorTextChange(string content)
{
if (Profile is null)
return;
Profile = Profile.WithFlavorText(content);
IsDirty = true;
}
private void OnMarkingChange(MarkingSet markings)
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
SetDirty();
ReloadProfilePreview();
}
private void OnSkinColorOnValueChanged()
{
if (Profile is null)
return;
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
var skinColor = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).DefaultSkinTone;
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:
case HumanoidSkinColor.TintedHuesSkin: // DeltaV - Tone blending
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
var color = skin switch // DeltaV - Tone blending
{
HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, skinColor),
_ => Color.White
};
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;
}
case HumanoidSkinColor.AnimalFur: // Einstein Engines - Tajaran
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
var color = SkinColor.ClosestAnimalFurColor(_rgbSkinColorSelector.Color);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
}
SetDirty();
ReloadProfilePreview();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(PreviewDummy);
PreviewDummy = EntityUid.Invalid;
_cfgManager.UnsubValueChanged(CCVars.GameLoadoutsEnabled, LoadoutsChanged);
}
private void SetAge(int newAge)
{
Profile = Profile?.WithAge(newAge);
ReloadPreview();
IsDirty = true;
}
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);
UpdateTTSVoicesControls(); // WD EDIT
ReloadProfilePreview();
SetDirty();
}
// WD EDIT START
private void SetVoice(string newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
}
// WD EDIT END
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
ReloadPreview();
IsDirty = true;
}
private void SetDisplayPronouns(string? displayPronouns)
{
if (displayPronouns == GetFormattedPronounsFromGender())
displayPronouns = null;
Profile = Profile?.WithDisplayPronouns(displayPronouns);
ReloadPreview();
IsDirty = true;
}
private void SetStationAiName(string? stationAiName)
{
Profile = Profile?.WithStationAiName(stationAiName);
ReloadPreview();
IsDirty = true;
}
private void SetCyborgName(string? cyborgName)
{
Profile = Profile?.WithCyborgName(cyborgName);
ReloadPreview();
IsDirty = true;
}
private string GetFormattedPronounsFromGender()
{
if (Profile == null)
return "they/them";
var genderName = Enum.GetName(typeof(Gender), Profile.Gender) ?? "Epicene";
var label = Loc.GetString($"humanoid-profile-editor-pronouns-{genderName.ToLower()}-text");
return label.Replace(" ", string.Empty).ToLower();
}
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.
UpdateSexControls(); // Update sex for new species
UpdateCharacterRequired();
// Changing species provides inaccurate sliders without these
UpdateHeightWidthSliders();
UpdateWeight();
UpdateSpeciesGuidebookIcon();
IsDirty = true;
ReloadProfilePreview();
ReloadClothes(); // Species may have job-specific gear, reload the clothes
}
private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
IsDirty = true;
}
private void SetCustomSpecieName(string customname)
{
Profile = Profile?.WithCustomSpeciesName(customname);
IsDirty = true;
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
{
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
IsDirty = true;
}
private void SetProfileHeight(float height)
{
Profile = Profile?.WithHeight(height);
IsDirty = true;
ReloadProfilePreview();
}
private void SetProfileWidth(float width)
{
Profile = Profile?.WithWidth(width);
IsDirty = true;
ReloadProfilePreview();
}
private bool IsDirty
{
get => _isDirty;
set
{
if (_isDirty == value)
return;
_isDirty = value;
UpdateSaveButton();
}
}
private void UpdateNameEdit()
{
NameEdit.Text = Profile?.Name ?? "";
}
private void UpdateCustomSpecieNameEdit()
{
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
_customspecienameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
_ccustomspecienamecontainerEdit.Visible = species.CustomName;
}
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<Sex>();
// Add species sex options, default to just none if we are in bizzaro world and have no species
if (_prototypeManager.TryIndex<SpeciesPrototype>(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<SpeciesPrototype>(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;
}
case HumanoidSkinColor.AnimalFur: // Einstein Engines - Tajaran
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
_rgbSkinColorSelector.Color = SkinColor.ClosestAnimalFurColor(Profile.Appearance.SkinColor);
break;
}
}
}
public void UpdateSpeciesGuidebookIcon()
{
SpeciesInfoButton.StyleClasses.Clear();
var species = Profile?.Species;
if (species is null
|| !_prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesProto)
|| !_prototypeManager.HasIndex<GuideEntryPrototype>(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 UpdateDisplayPronounsControls()
{
if (Profile == null)
return;
var label = GetFormattedPronounsFromGender();
CosmeticPronounsNameEdit.PlaceHolder = label;
if (Profile.DisplayPronouns == null)
CosmeticPronounsNameEdit.Text = string.Empty;
else
CosmeticPronounsNameEdit.Text = Profile.DisplayPronouns;
}
private void UpdateStationAiControls()
{
if (Profile == null)
return;
StationAINameEdit.Text = Profile.StationAiName ?? string.Empty;
if (StationAINameEdit.Text != string.Empty)
return;
var stationAiNames = _prototypeManager.Index<LocalizedDatasetPrototype>(StationAiNames);
var randomName = _random.Pick(stationAiNames.Values);
StationAINameEdit.PlaceHolder = Loc.GetString(randomName);
}
private void UpdateCyborgControls()
{
if (Profile == null)
return;
CyborgNameEdit.Text = Profile.CyborgName ?? string.Empty;
if (CyborgNameEdit.Text != string.Empty)
return;
var borgNames = _prototypeManager.Index<DatasetPrototype>(CyborgNames);
var randomName = _random.Pick(borgNames.Values);
CyborgNameEdit.PlaceHolder = Loc.GetString(randomName);
}
private void UpdateSpawnPriorityControls()
{
if (Profile == null)
return;
SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
}
private void UpdateHeightWidthSliders()
{
if (Profile is null)
return;
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
HeightSlider.MinValue = species.MinHeight;
HeightSlider.MaxValue = species.MaxHeight;
HeightSlider.SetValueWithoutEvent(Profile?.Height ?? species.DefaultHeight);
WidthSlider.MinValue = species.MinWidth;
WidthSlider.MaxValue = species.MaxWidth;
WidthSlider.SetValueWithoutEvent(Profile?.Width ?? species.DefaultWidth);
var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
UpdateDimensions(SliderUpdate.Both);
}
private enum SliderUpdate
{
Height,
Width,
Both
}
private void UpdateDimensions(SliderUpdate updateType)
{
if (Profile == null)
return;
var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
var heightValue = Math.Clamp(HeightSlider.Value, species.MinHeight, species.MaxHeight);
var widthValue = Math.Clamp(WidthSlider.Value, species.MinWidth, species.MaxWidth);
var sizeRatio = species.SizeRatio;
var ratio = heightValue / widthValue;
if (updateType == SliderUpdate.Height || updateType == SliderUpdate.Both)
if (ratio < 1 / sizeRatio || ratio > sizeRatio)
widthValue = heightValue / (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
if (updateType == SliderUpdate.Width || updateType == SliderUpdate.Both)
if (ratio < 1 / sizeRatio || ratio > sizeRatio)
heightValue = widthValue * (ratio < 1 / sizeRatio ? (1 / sizeRatio) : sizeRatio);
heightValue = Math.Clamp(heightValue, species.MinHeight, species.MaxHeight);
widthValue = Math.Clamp(widthValue, species.MinWidth, species.MaxWidth);
HeightSlider.Value = heightValue;
WidthSlider.Value = widthValue;
SetProfileHeight(heightValue);
SetProfileWidth(widthValue);
var height = MathF.Round(species.AverageHeight * HeightSlider.Value);
HeightLabel.Text = Loc.GetString("humanoid-profile-editor-height-label", ("height", (int) height));
var width = MathF.Round(species.AverageWidth * WidthSlider.Value);
WidthLabel.Text = Loc.GetString("humanoid-profile-editor-width-label", ("width", (int) width));
UpdateWeight();
}
private void UpdateWeight()
{
if (Profile == null)
return;
var species = _species.Find(x => x.ID == Profile.Species) ?? _species.First();
_prototypeManager.Index(species.Prototype).TryGetComponent<FixturesComponent>(out var fixture);
if (fixture != null)
{
var radius = fixture.Fixtures["fix1"].Shape.Radius;
var density = fixture.Fixtures["fix1"].Density;
var avg = (Profile.Width + Profile.Height) / 2;
var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
}
else // Whelp, the fixture doesn't exist, guesstimate it instead
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
SpriteView.InvalidateMeasure();
}
private void UpdateHairPickers()
{
if (Profile == null)
return;
var hairMarking = Profile.Appearance.HairStyleId switch
{
HairStyles.DefaultHairStyle => new List<Marking>(),
_ => new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) },
};
var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
{
HairStyles.DefaultFacialHairStyle => new List<Marking>(),
_ => new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { 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))
{
hairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
? Profile.Appearance.SkinColor
: Profile.Appearance.HairColor;
}
}
if (hairColor != null)
Markings.HairMarking = new(Profile.Appearance.HairStyleId, new List<Color> { 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))
{
facialHairColor = _markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out _, _prototypeManager)
? Profile.Appearance.SkinColor
: Profile.Appearance.FacialHairColor;
}
}
if (facialHairColor != null)
Markings.FacialHairMarking = new(Profile.Appearance.FacialHairStyleId, new List<Color> { 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 RandomizeProfile()
{
Profile = HumanoidCharacterProfile.Random();
SetProfile(Profile, CharacterSlot);
SetDirty();
}
private void SetPreviewRotation(Direction direction)
{
SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
}
private void RandomizeName()
{
if (Profile == null)
return;
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
SetName(name);
UpdateNameEdit();
}
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<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
var oldProfile = Profile;
SetProfile(profile, CharacterSlot);
IsDirty = !profile.MemberwiseEquals(oldProfile);
}
catch (Exception exc)
{
Logger.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<HumanoidAppearanceSystem>().ToDataNode(Profile);
await using var writer = new StreamWriter(file.Value.fileStream);
dataNode.Write(writer);
}
catch (Exception exc)
{
Logger.Error($"Error when exporting profile: {exc.Message}\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;
}
#region Traits
#region Updates
private void UpdateTraitPreferences()
{
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
_traitCount = 0;
foreach (var preferenceSelector in _traitPreferences)
{
var traitId = preferenceSelector.Trait.ID;
var preference = Profile?.TraitPreferences.Contains(traitId) ?? false;
preferenceSelector.Preference = preference;
if (!preference)
continue;
points += preferenceSelector.Trait.Points;
_traitCount += 1;
}
TraitPointsBar.Value = points;
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
("points", points), ("traits", _traitCount),
("maxTraits", _cfgManager.GetCVar(CCVars.GameTraitsMax)));
// Set the remove unusable button's label to have the correct amount of unusable traits
TraitsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-traits-remove-unusable-button",
("count", _traits
.Where(t => _traitPreferences
.Where(tps => tps.Preference).Select(tps => tps.Trait).Contains(t.Key))
.Count(t => !t.Value)));
AdminUIHelpers.RemoveConfirm(TraitsRemoveUnusableButton, _confirmationData);
IsDirty = true;
ReloadProfilePreview();
}
// Yeah this is mostly just copied from UpdateLoadouts
// This whole file is bad though and a lot of loadout code came from traits originally
//TODO Make this file not hell
private Dictionary<TraitPrototype, bool> _traits = new();
public void UpdateTraits(bool? showUnusable = null, bool reload = false)
{
showUnusable ??= TraitsShowUnusableButton.Pressed;
// Reset trait points so you don't get -14 points or something for no reason
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-points-label", ("points", points), ("max", points));
TraitPointsBar.MaxValue = points;
TraitPointsBar.Value = points;
// Reset the whole UI and delete caches
if (reload)
{
foreach (var tab in TraitsTabs.Tabs)
TraitsTabs.RemoveTab(tab);
_loadoutPreferences.Clear();
}
// Get the highest priority job to use for trait filtering
var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
_traits.Clear();
foreach (var trait in _prototypeManager.EnumeratePrototypes<TraitPrototype>())
{
var usable = _characterRequirementsSystem.CheckRequirementsValid(
trait.Requirements,
highJob,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
trait,
_entManager,
_prototypeManager,
_cfgManager,
out _
);
_traits.Add(trait, usable);
if (_traitPreferences.FindIndex(lps => lps.Trait.ID == trait.ID) is not (not -1 and var i))
continue;
var selector = _traitPreferences[i];
selector.Valid = usable;
selector.ShowUnusable = showUnusable.Value;
}
if (_traits.Count == 0)
{
TraitsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-traits-no-traits") },
Loc.GetString("trait-category-Uncategorized"));
return;
}
var uncategorized = TraitsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
if (uncategorized == null)
{
uncategorized = new BoxContainer
{
Name = "Uncategorized",
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
// I hate ScrollContainers
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
TraitsTabs.AddTab(uncategorized, Loc.GetString("trait-category-Uncategorized"));
}
// Create a Dictionary/tree of categories and subcategories
var cats = CreateTree(_prototypeManager.EnumeratePrototypes<TraitCategoryPrototype>()
.Where(c => c.Root)
.OrderBy(c => Loc.GetString($"trait-category-{c.ID}"))
.ToList());
var categories = new Dictionary<string, object>();
foreach (var (key, value) in cats)
categories.Add(key, value);
// Create the UI elements for the category tree
CreateCategoryUI(categories, TraitsTabs);
// Fill categories with traits
foreach (var (trait, usable) in _traits
.OrderBy(l => -l.Key.Points)
.ThenBy(l => l.Key.ID)
.ThenBy(l => Loc.GetString($"trait-name-{l.Key.ID}")))
{
if (_traitPreferences.Select(lps => lps.Trait.ID).Contains(trait.ID))
{
var first = _traitPreferences.First(lps => lps.Trait.ID == trait.ID);
first.Valid = usable;
first.ShowUnusable = showUnusable.Value;
continue;
}
var selector = new TraitPreferenceSelector(
trait, highJob, Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements);
selector.Valid = usable;
selector.ShowUnusable = showUnusable.Value;
AddSelector(selector);
// Look for an existing category tab
var match = FindCategory(trait.Category, TraitsTabs);
// If there is no category put it in Uncategorized (this shouldn't happen)
(match ?? uncategorized).Children.First().Children.First().AddChild(selector);
}
// Hide any empty tabs
HideEmptyTabs(_prototypeManager.EnumeratePrototypes<TraitCategoryPrototype>().ToList());
UpdateTraitPreferences();
return;
void CreateCategoryUI(Dictionary<string, object> tree, NeoTabContainer parent)
{
foreach (var (key, value) in tree)
{
// If the category's container exists already, ignore it
if (parent.Contents.Any(c => c.Name == key))
continue;
// If the value is a list of TraitPrototypes, create a final tab for them
if (value is List<TraitPrototype>)
{
var category = new BoxContainer
{
Name = key,
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
}
// If the value is a dictionary, create a new tab for it and recursively call this function to fill it
else
{
var category = new NeoTabContainer
{
Name = key,
HorizontalExpand = true,
VerticalExpand = true,
SeparatorMargin = new Thickness(0),
};
parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
CreateCategoryUI((Dictionary<string, object>) value, category);
}
}
}
void AddSelector(TraitPreferenceSelector selector)
{
_traitPreferences.Add(selector);
selector.PreferenceChanged += preference =>
{
// Make sure they have enough trait points
preference = CheckPoints(preference ? selector.Trait.Points : -selector.Trait.Points, preference);
// Make sure they have enough trait slots
preference = preference ? _traitCount < _cfgManager.GetCVar(CCVars.GameTraitsMax) : preference;
// Update Preferences
Profile = Profile?.WithTraitPreference(selector.Trait.ID, preference);
IsDirty = true;
UpdateTraitPreferences();
SetProfile(Profile, CharacterSlot);
};
}
bool CheckPoints(int points, bool preference)
{
var temp = TraitPointsBar.Value + points;
return preference ? !(temp < 0) : temp < 0;
}
}
#endregion
#region Functions
private Dictionary<string, object> CreateTree(List<TraitCategoryPrototype> cats)
{
var tree = new Dictionary<string, object>();
foreach (var category in cats)
{
// If the category is already in the tree, ignore it
if (tree.ContainsKey(category.ID))
continue;
// Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
// If there are no subcategories, add a loadout spot to the dictionary
if (subCategories.Count == 0)
{
tree.Add(category.ID, new List<TraitPrototype>());
continue;
}
// If there are subcategories, we need to add them to the dictionary as well
var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
tree.Add(category.ID, subCategoryTree);
}
return tree;
}
private void HideEmptyTabs(List<TraitCategoryPrototype> cats)
{
foreach (var tab in cats.Select(category => FindCategory(category.ID, TraitsTabs)))
{
// If it's empty, hide it
if (tab != null)
((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
// If it has a parent tab container, hide it if it's empty
if (tab?.Parent?.Parent is NeoTabContainer parent)
{
var parentCats = parent.Contents.Select(c => _prototypeManager.Index<TraitCategoryPrototype>(c.Name!)).ToList();
HideEmptyTabs(parentCats);
}
}
}
private void TryRemoveUnusableTraits()
{
// Confirm the user wants to remove unusable loadouts
if (!AdminUIHelpers.TryConfirm(TraitsRemoveUnusableButton, _confirmationData))
return;
// Remove unusable traits
foreach (var (trait, _) in _traits.Where(l => !l.Value).ToList())
Profile = Profile?.WithTraitPreference(trait.ID, false);
UpdateCharacterRequired();
}
#endregion
#endregion
#region Loadouts
#region Updates
private void UpdateLoadoutPreferences()
{
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
LoadoutPointsBar.Value = points;
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
foreach (var preferenceSelector in _loadoutPreferences)
{
var loadoutId = preferenceSelector.Loadout.ID;
var loadoutPreference = Profile?.LoadoutPreferences.FirstOrDefault(l => l.LoadoutName == loadoutId) ?? preferenceSelector.Preference;
var preference = new LoadoutPreference(
loadoutPreference.LoadoutName,
loadoutPreference.CustomName,
loadoutPreference.CustomDescription,
loadoutPreference.CustomColorTint,
loadoutPreference.CustomHeirloom)
{ Selected = loadoutPreference.Selected };
preferenceSelector.Preference = preference;
if (preference.Selected)
{
points -= preferenceSelector.Loadout.Cost;
LoadoutPointsBar.Value = points;
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", LoadoutPointsBar.MaxValue));
}
}
// Set the remove unusable button's label to have the correct amount of unusable loadouts
LoadoutsRemoveUnusableButton.Text = Loc.GetString("humanoid-profile-editor-loadouts-remove-unusable-button",
("count", _loadouts
.Where(l => _loadoutPreferences
.Where(lps => lps.Preference.Selected).Select(lps => lps.Loadout).Contains(l.Key))
.Count(l => !l.Value
|| !_loadoutPreferences.First(lps => lps.Loadout == l.Key).Wearable)));
AdminUIHelpers.RemoveConfirm(LoadoutsRemoveUnusableButton, _confirmationData);
IsDirty = true;
ReloadProfilePreview();
}
private Dictionary<LoadoutPrototype, bool> _loadouts = new();
private Dictionary<string, EntityUid> _dummyLoadouts = new();
public void UpdateLoadouts(bool? showUnusable = null, bool reload = false)
{
showUnusable ??= LoadoutsShowUnusableButton.Pressed;
// Reset loadout points so you don't get -14 points or something for no reason
var points = _cfgManager.GetCVar(CCVars.GameLoadoutsPoints);
LoadoutPointsLabel.Text = Loc.GetString("humanoid-profile-editor-loadouts-points-label", ("points", points), ("max", points));
LoadoutPointsBar.MaxValue = points;
LoadoutPointsBar.Value = points;
// Reset the whole UI and delete caches
if (reload)
{
foreach (var tab in LoadoutsTabs.Tabs)
LoadoutsTabs.RemoveTab(tab);
foreach (var uid in _dummyLoadouts)
_entManager.QueueDeleteEntity(uid.Value);
_loadoutPreferences.Clear();
}
// Get the highest priority job to use for loadout filtering
var highJob = _controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies());
_loadouts.Clear();
foreach (var loadout in _prototypeManager.EnumeratePrototypes<LoadoutPrototype>())
{
var usable = _characterRequirementsSystem.CheckRequirementsValid(
loadout.Requirements,
highJob ?? new JobPrototype(),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
loadout,
_entManager,
_prototypeManager,
_cfgManager,
out _
);
_loadouts.Add(loadout, usable);
var list = _loadoutPreferences.ToList();
if (list.FindIndex(lps => lps.Loadout.ID == loadout.ID) is not (not -1 and var i))
continue;
var selector = list[i];
UpdateSelector(selector, usable);
}
if (_loadouts.Count == 0)
{
LoadoutsTabs.AddTab(new Label { Text = Loc.GetString("humanoid-profile-editor-loadouts-no-loadouts") },
Loc.GetString("loadout-category-Uncategorized"));
return;
}
var uncategorized = LoadoutsTabs.Contents.FirstOrDefault(c => c.Name == "Uncategorized");
if (uncategorized == null)
{
uncategorized = new BoxContainer
{
Name = "Uncategorized",
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
// I hate ScrollContainers
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
LoadoutsTabs.AddTab(uncategorized, Loc.GetString("loadout-category-Uncategorized"));
}
// Create a Dictionary/tree of categories and subcategories
var cats = CreateTree(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>()
.Where(c => c.Root)
.OrderBy(c => Loc.GetString($"loadout-category-{c.ID}"))
.ToList());
var categories = new Dictionary<string, object>();
foreach (var (key, value) in cats)
categories.Add(key, value);
// Create the UI elements for the category tree
CreateCategoryUI(categories, LoadoutsTabs);
// Fill categories with loadouts
foreach (var (loadout, usable) in _loadouts
.OrderBy(l => l.Key.ID)
.ThenBy(l => Loc.GetString($"loadout-name-{l.Key.ID}"))
.ThenBy(l => l.Key.Cost))
{
if (_loadoutPreferences.Select(lps => lps.Loadout.ID).Contains(loadout.ID))
{
var first = _loadoutPreferences.First(lps => lps.Loadout.ID == loadout.ID);
var prof = Profile?.LoadoutPreferences.FirstOrDefault(lp => lp.LoadoutName == loadout.ID);
first.Preference = new(loadout.ID, prof?.CustomName, prof?.CustomDescription, prof?.CustomColorTint, prof?.CustomHeirloom);
UpdateSelector(first, usable);
continue;
}
var selector = new LoadoutPreferenceSelector(
loadout, highJob ?? new JobPrototype(),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(), ref _dummyLoadouts,
_entManager, _prototypeManager, _cfgManager, _characterRequirementsSystem, _requirements)
{ Preference = new(loadout.ID) };
UpdateSelector(selector, usable);
AddSelector(selector);
// Look for an existing category tab
var match = FindCategory(loadout.Category, LoadoutsTabs);
// If there is no category put it in Uncategorized (this shouldn't happen)
(match ?? uncategorized).Children.First().Children.First().AddChild(selector);
}
// Hide any empty tabs
HideEmptyTabs(_prototypeManager.EnumeratePrototypes<LoadoutCategoryPrototype>().ToList());
UpdateLoadoutPreferences();
return;
void UpdateSelector(LoadoutPreferenceSelector selector, bool usable)
{
selector.Valid = usable;
selector.ShowUnusable = showUnusable.Value;
foreach (var item in selector.Loadout.Items)
{
if (_dummyLoadouts.TryGetValue(selector.Loadout.ID + selector.Loadout.Items.IndexOf(item), out var entity)
&& _entManager.GetComponent<MetaDataComponent>(entity).EntityPrototype!.ID == item)
{
if (!_entManager.HasComponent<ClothingComponent>(entity))
{
selector.Wearable = true;
continue;
}
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
continue;
}
entity = _entManager.SpawnEntity(item, MapCoordinates.Nullspace);
_dummyLoadouts[selector.Loadout.ID + selector.Loadout.Items.IndexOf(item)] = entity;
if (!_entManager.HasComponent<ClothingComponent>(entity))
{
selector.Wearable = true;
continue;
}
selector.Wearable = _characterRequirementsSystem.CanEntityWearItem(PreviewDummy, entity);
}
}
void CreateCategoryUI(Dictionary<string, object> tree, NeoTabContainer parent)
{
foreach (var (key, value) in tree)
{
// If the category's container exists already, ignore it
if (parent.Contents.Any(c => c.Name == key))
continue;
// If the value is a list of LoadoutPrototypes, create a final tab for them
if (value is List<LoadoutPrototype>)
{
var category = new BoxContainer
{
Name = key,
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new ScrollContainer
{
HScrollEnabled = false,
HorizontalExpand = true,
VerticalExpand = true,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
},
},
},
},
};
parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
}
// If the value is a dictionary, create a new tab for it and recursively call this function to fill it
else
{
var category = new NeoTabContainer
{
Name = key,
HorizontalExpand = true,
VerticalExpand = true,
SeparatorMargin = new Thickness(0),
};
parent.AddTab(category, Loc.GetString($"loadout-category-{key}"));
CreateCategoryUI((Dictionary<string, object>) value, category);
}
}
}
void AddSelector(LoadoutPreferenceSelector selector)
{
_loadoutPreferences.Add(selector);
selector.PreferenceChanged += preference =>
{
// Make sure they have enough loadout points
var selected = preference.Selected
? CheckPoints(-selector.Loadout.Cost, preference.Selected)
: CheckPoints(selector.Loadout.Cost, preference.Selected);
// Update Preferences
Profile = Profile?.WithLoadoutPreference(
selector.Loadout.ID,
selected,
preference.CustomName,
preference.CustomDescription,
preference.CustomColorTint,
preference.CustomHeirloom);
IsDirty = true;
UpdateLoadoutPreferences();
SetProfile(Profile, CharacterSlot);
};
}
bool CheckPoints(int points, bool preference)
{
var temp = LoadoutPointsBar.Value + points;
return preference ? !(temp < 0) : temp < 0;
}
}
#endregion
#region Functions
private Dictionary<string, object> CreateTree(List<LoadoutCategoryPrototype> cats)
{
var tree = new Dictionary<string, object>();
foreach (var category in cats)
{
// If the category is already in the tree, ignore it
if (tree.ContainsKey(category.ID))
continue;
// Categories don't have a Parent field, so we need to instead check the SubCategories of every Category
var subCategories = category.SubCategories.Where(subCategory => !tree.ContainsKey(subCategory)).ToList();
// If there are no subcategories, add a loadout spot to the dictionary
if (subCategories.Count == 0)
{
tree.Add(category.ID, new List<LoadoutPrototype>());
continue;
}
// If there are subcategories, we need to add them to the dictionary as well
var subCategoryTree = CreateTree(subCategories.Select(c => _prototypeManager.Index(c)).ToList());
tree.Add(category.ID, subCategoryTree);
}
return tree;
}
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
{
BoxContainer? match = null;
foreach (var child in parent.Contents)
{
if (string.IsNullOrEmpty(child.Name))
continue;
if (child.Name == id)
match = (BoxContainer?) child;
}
if (match != null)
return match;
foreach (var subcategory in parent.Contents.Where(c => c is NeoTabContainer).Cast<NeoTabContainer>())
match ??= FindCategory(id, subcategory);
return match;
}
private void HideEmptyTabs(List<LoadoutCategoryPrototype> cats)
{
foreach (var tab in cats.Select(category => FindCategory(category.ID, LoadoutsTabs)))
{
// If it's empty, hide it
if (tab != null)
((NeoTabContainer) tab.Parent!.Parent!.Parent!.Parent!).SetTabVisible(tab, tab.Children.First().Children.First().Children.Any());
// If it has a parent tab container, hide it if it's empty
if (tab?.Parent?.Parent is NeoTabContainer parent)
{
var parentCats = parent.Contents.Select(c => _prototypeManager.Index<LoadoutCategoryPrototype>(c.Name!)).ToList();
HideEmptyTabs(parentCats);
}
}
}
private void TryRemoveUnusableLoadouts()
{
// Confirm the user wants to remove unusable loadouts
if (!AdminUIHelpers.TryConfirm(LoadoutsRemoveUnusableButton, _confirmationData))
return;
// Remove unusable and unwearable loadouts
foreach (var (loadout, _) in
_loadouts.Where(l =>
!l.Value || !_loadoutPreferences.First(lps => lps.Loadout.ID == l.Key.ID).Wearable).ToList())
Profile = Profile?.WithLoadoutPreference(loadout.ID, false);
UpdateCharacterRequired();
}
#endregion
#endregion
private void UpdateCharacterRequired()
{
UpdateRoleRequirements();
UpdateTraits(TraitsShowUnusableButton.Pressed);
UpdateLoadouts(LoadoutsShowUnusableButton.Pressed);
}
}
}