Files
wwdpublic/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Cinkafox b2d255cdd2 [tweak] UI Tweaks (#1001)
* - tweak: update StyleSheetify

* - add: flexbox

* - fix: size of flexbox in launchergui

* - tweak: Profile editor: start.

* - add: categories

* - tweak: help me please with this shi... loadouts

* - fix: container path think

* - tweak: thinks for optimisation

* - add: group selection for loadoutpicker

* - tweak: change position of preview

* - add: reason text

* - fix: Кролькины фиксы

* - fix: кролькины фиксы ч.2

* - fix: кролькины фиксы ч.3

* - кролькины фиксы - финал

* - fix: Ворчливого дедушкины фиксы, удаление старого барахла и пометка wwdp

* - tweak: some ui change for LoadoutCategories and LoadoutEntry

* - ворчливый дед фиксы ч.2

* - fix: очередные кролькины фиксы

* - add: loadout prototype validation

* - fix: description read from edit field
2026-01-04 23:33:01 +02:00

2427 lines
88 KiB
C#

using System.IO;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.UI;
using Content.Client.Humanoid;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Roles;
using Content.Client.Sprite;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._EE.Contractors.Prototypes;
using Content.Shared._White.CCVar;
using Content.Shared._White.Humanoid.Prototypes;
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.Guidebook;
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.ContentPack;
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 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 IRobustRandom _random;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private readonly LobbyUIController _controller;
private readonly CharacterRequirementsSystem _characterRequirementsSystem;
private readonly RoleSystem _roleSystem;
private FlavorText.FlavorText? _flavorText;
private TextEdit? _flavorTextEdit;
private bool _exporting;
private bool _imaging;
/// <summary>
/// If we're attempting to save.
/// </summary>
public event Action? Save;
/// <summary>
/// Entity used for the profile editor preview
/// </summary>
public EntityUid PreviewDummy;
/// <summary>
/// Temporary override of their selected job, used to preview roles.
/// </summary>
public JobPrototype? JobOverride;
/// <summary>
/// The character slot for the current profile.
/// </summary>
public int? CharacterSlot;
/// <summary>
/// The work in progress profile being edited.
/// </summary>
public HumanoidCharacterProfile? Profile;
private List<SpeciesPrototype> _species = new();
private List<BodyTypePrototype> _bodyTypes = new(); // WD EDIT
// EE - Contractor System Changes Start
private List<NationalityPrototype> _nationalies = new();
private List<EmployerPrototype> _employers = new();
private List<LifepathPrototype> _lifepaths = new();
// EE - Contractor System Changes End
private Dictionary<Button, ConfirmationData> _confirmationData = new();
private List<TraitPreferenceSelector> _traitPreferences = new();
private int _traitCount;
private bool _customizePronouns;
private bool _customizeStationAiName;
private bool _customizeBorgName;
private bool _customizeClownName; // WD EDIT
private bool _customizeMimeName; // WD EDIT
private List<(string, RequirementsSelector)> _jobPriorities = new();
private readonly Dictionary<string, BoxContainer> _jobCategories;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _isDirty;
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
private ISawmill _sawmill;
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string StationAiNames = "NamesAI";
[ValidatePrototypeId<DatasetPrototype>]
private const string CyborgNames = "names_borg";
// WD EDIT START
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string ClownNames = "ClownNames";
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string MimeNames = "MimeNames";
private const string Uncategorized = "Uncategorized";
public SpriteView? CharacterSpriteView;
// WD EDIT END
public HumanoidProfileEditor(
IClientPreferencesManager preferencesManager,
IConfigurationManager cfgManager,
IEntityManager entManager,
IFileDialogManager dialogManager,
ILogManager logManager,
IPlayerManager playerManager,
IPrototypeManager prototypeManager,
IResourceManager resManager,
JobRequirementsManager requirements,
MarkingManager markings,
IRobustRandom random
)
{
RobustXamlLoader.Load(this);
_sawmill = logManager.GetSawmill("profile.editor");
_cfgManager = cfgManager;
_entManager = entManager;
_dialogManager = dialogManager;
_playerManager = playerManager;
_prototypeManager = prototypeManager;
_markingManager = markings;
_preferencesManager = preferencesManager;
_resManager = resManager;
_requirements = requirements;
_random = random;
_roleSystem = _entManager.System<RoleSystem>();
_characterRequirementsSystem = _entManager.System<CharacterRequirementsSystem>();
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
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); };
NameRandomize.OnPressed += _ => RandomizeName();
RandomizeEverything.OnPressed += _ => { RandomizeProfile(); };
#endregion Name
#region Custom Species Name
CCustomSpecieNameEdit.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();
InitializeBark();
#endregion
#region BodyType
CBodyTypesButton.OnItemSelected += args =>
{
CBodyTypesButton.SelectId(args.Id);
SetBodyType(_bodyTypes[args.Id].ID);
};
#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);
_customizeClownName = _cfgManager.GetCVar(WhiteCVars.AllowCustomClownName); // WD EDIT
_customizeMimeName = _cfgManager.GetCVar(WhiteCVars.AllowCustomMimeName); // WD EDIT
_cfgManager.OnValueChanged(CCVars.AllowCustomStationAiName, OnChangedStationAiNameCustomizationValue);
_cfgManager.OnValueChanged(CCVars.AllowCustomCyborgName, OnChangedCyborgNameCustomizationValue);
_cfgManager.OnValueChanged(WhiteCVars.AllowCustomClownName, OnChangedClownNameCustomizationValue); // WD EDIT
_cfgManager.OnValueChanged(WhiteCVars.AllowCustomMimeName, OnChangedMimeNameCustomizationValue); // WD EDIT
StationAINameEdit.OnTextChanged += args => { SetStationAiName(args.Text); };
CyborgNameEdit.OnTextChanged += args => { SetCyborgName(args.Text); };
ClownNameEdit.OnTextChanged += args => { SetClownName(args.Text); }; // WD EDIT
MimeNameEdit.OnTextChanged += args => { SetMimeName(args.Text); }; // WD EDIT
if (StationAiNameContainer.Visible != _customizeStationAiName)
StationAiNameContainer.Visible = _customizeStationAiName;
if (CyborgNameContainer.Visible != _customizeBorgName)
CyborgNameContainer.Visible = _customizeBorgName;
// WD EDIT START
if (ClownNameContainer.Visible != _customizeClownName)
ClownNameContainer.Visible = _customizeClownName;
if (MimeNameContainer.Visible != _customizeMimeName)
MimeNameContainer.Visible = _customizeMimeName;
// WD EDIT END
#endregion
#region Species
RefreshSpecies();
SpeciesButton.OnItemSelected += args =>
{
SpeciesButton.SelectId(args.Id);
SetSpecies(_species[args.Id].ID);
UpdateHairPickers();
OnSkinColorOnValueChanged();
UpdateCustomSpecieNameEdit();
UpdateHeightWidthSliders();
};
#endregion Species
#region Contractors
if(_cfgManager.GetCVar(CCVars.ContractorsEnabled))
{
Background.Orphan();
CTabContainer.AddTab(Background, Loc.GetString("humanoid-profile-editor-background-tab"));
RefreshNationalities();
RefreshEmployers();
RefreshLifepaths();
NationalityButton.OnItemSelected += args =>
{
NationalityButton.SelectId(args.Id);
SetNationality(_nationalies[args.Id].ID);
};
EmployerButton.OnItemSelected += args =>
{
EmployerButton.SelectId(args.Id);
SetEmployer(_employers[args.Id].ID);
};
LifepathButton.OnItemSelected += args =>
{
LifepathButton.SelectId(args.Id);
SetLifepath(_lifepaths[args.Id].ID);
};
}
else
{
Background.Visible = false;
}
#endregion Contractors
#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 Markings
Markings.OnMarkingAdded += OnMarkingChange;
Markings.OnMarkingRemoved += OnMarkingChange;
Markings.OnMarkingColorChange += OnMarkingChange;
Markings.OnMarkingRankChange += OnMarkingChange;
#endregion Markings
RefreshFlavorText();
#endregion Left
ShowClothes.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
ShowLoadouts.OnToggled += _ => { SetProfile(Profile, CharacterSlot); };
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
UpdateSpeciesGuidebookIcon();
ReloadPreview();
InitializeCharacterMenu(); // WWDP EDIT
IsDirty = false;
}
/// <summary>
/// Refreshes the flavor text editor status.
/// </summary>
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;
}
// WD EDIT START
private void OnChangedClownNameCustomizationValue(bool newValue)
{
_customizeClownName = newValue;
UpdateClownControls();
}
private void OnChangedMimeNameCustomizationValue(bool newValue)
{
_customizeMimeName = newValue;
UpdateMimeControls();
}
// WD EDIT END
/// <summary>
/// Refreshes the species selector.
/// </summary>
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 then reset it to default.
if (Profile != null && !speciesIds.Contains(Profile.Species))
SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
}
public void RefreshNationalities()
{
NationalityButton.Clear();
_nationalies.Clear();
_nationalies.AddRange(_prototypeManager.EnumeratePrototypes<NationalityPrototype>()
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
o,
_entManager,
_prototypeManager,
_cfgManager, out _)));
var nationalityIds = _nationalies.Select(o => o.ID).ToList();
for (var i = 0; i < _nationalies.Count; i++)
{
NationalityButton.AddItem(Loc.GetString(_nationalies[i].NameKey), i);
if (Profile?.Nationality == _nationalies[i].ID)
NationalityButton.SelectId(i);
}
// If our nationality isn't available, reset it to default
if (Profile != null && !nationalityIds.Contains(Profile.Nationality))
SetNationality(SharedHumanoidAppearanceSystem.DefaultNationality);
if(Profile != null)
UpdateNationalityDescription(Profile.Nationality);
}
public void RefreshEmployers()
{
EmployerButton.Clear();
_employers.Clear();
_employers.AddRange(_prototypeManager.EnumeratePrototypes<EmployerPrototype>()
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
o,
_entManager,
_prototypeManager,
_cfgManager, out _)));
var employerIds = _employers.Select(o => o.ID).ToList();
for (var i = 0; i < _employers.Count; i++)
{
EmployerButton.AddItem(Loc.GetString(_employers[i].NameKey), i);
if (Profile?.Employer == _employers[i].ID)
EmployerButton.SelectId(i);
}
// If our employer isn't available, reset it to default
if (Profile != null && !employerIds.Contains(Profile.Employer))
SetEmployer(SharedHumanoidAppearanceSystem.DefaultEmployer);
if(Profile != null)
UpdateEmployerDescription(Profile.Employer);
}
public void RefreshLifepaths()
{
LifepathButton.Clear();
_lifepaths.Clear();
_lifepaths.AddRange(_prototypeManager.EnumeratePrototypes<LifepathPrototype>()
.Where(o => _characterRequirementsSystem.CheckRequirementsValid(o.Requirements,
_controller.GetPreferredJob(Profile ?? HumanoidCharacterProfile.DefaultWithSpecies()),
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
_requirements.GetRawPlayTimeTrackers(),
_requirements.IsWhitelisted(),
o,
_entManager,
_prototypeManager,
_cfgManager, out _)));
var lifepathIds = _lifepaths.Select(o => o.ID).ToList();
for (var i = 0; i < _lifepaths.Count; i++)
{
LifepathButton.AddItem(Loc.GetString(_lifepaths[i].NameKey), i);
if (Profile?.Lifepath == _lifepaths[i].ID)
LifepathButton.SelectId(i);
}
// If our lifepath isn't available, reset it to default
if (Profile != null && !lifepathIds.Contains(Profile.Lifepath))
SetLifepath(SharedHumanoidAppearanceSystem.DefaultLifepath);
if(Profile != null)
UpdateLifepathDescription(Profile.Lifepath);
}
private void UpdateNationalityDescription(string nationality)
{
var prototype = _prototypeManager.Index<NationalityPrototype>(nationality);
NationalityDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
}
private void UpdateLifepathDescription(string lifepath)
{
var prototype = _prototypeManager.Index<LifepathPrototype>(lifepath);
LifepathDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
}
private void UpdateEmployerDescription(string employer)
{
var prototype = _prototypeManager.Index<EmployerPrototype>(employer);
EmployerDescriptionLabel.SetMessage(Loc.GetString(prototype.DescriptionKey));
}
public void RefreshAntags()
{
AntagList.DisposeAllChildren();
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
// Causes a weird error if I just replace AntagList so whatever, have a child
var alt = new AlternatingBGContainer { Orientation = LayoutOrientation.Vertical, };
AntagList.AddChild(alt);
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var antagContainer = new BoxContainer()
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
var selector = new RequirementsSelector()
{
Margin = new(3f, 3f, 3f, 0f),
HorizontalExpand = true,
};
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);
if (!_characterRequirementsSystem.CheckRequirementsValid(
_roleSystem.GetAntagRequirement(antag) ?? 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);
alt.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;
}
/// <summary>
/// Reloads the entire dummy entity for preview.
/// </summary>
/// <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(Profile.Species))
return;
PreviewDummy = _controller.LoadProfileEntity(Profile, null, ShowClothes.Pressed, ShowLoadouts.Pressed);
CharacterSpriteView?.SetEntity(PreviewDummy); // WWDP EDIT
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
}
/// <summary>
/// Reloads the dummy entity's clothes for preview
/// </summary>
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);
}
/// <summary>
/// Resets the profile to the defaults.
/// </summary>
public void ResetToDefault()
{
SetProfile(
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
_preferencesManager.Preferences?.SelectedCharacterIndex);
}
/// <summary>
/// Sets the editor to the specified profile with the specified slot.
/// </summary>
public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
{
Profile = profile?.Clone();
CharacterSlot = slot;
IsDirty = false;
JobOverride = null;
UpdateNameEdit();
UpdateFlavorTextEdit();
UpdateSexControls();
UpdateTTSVoicesControls(); // WD EDIT
UpdateBarksControl(); // WD EDIT
UpdateBodyTypes(); // WD EDIT
UpdateGenderControls();
UpdateDisplayPronounsControls();
UpdateStationAiControls();
UpdateCyborgControls();
UpdateClownControls(); // WD EDIT
UpdateMimeControls(); // WD EDIT
UpdateSkinColor();
UpdateSpawnPriorityControls();
UpdateFlavorTextEdit();
UpdateCustomSpecieNameEdit();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
UpdateLoadouts(); // WD EDIT
CheckpointLoadouts(); // WD EDIT
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
UpdateHeightWidthSliders();
UpdateWeight();
UpdateCharacterRequired();
RefreshAntags();
RefreshJobs();
RefreshSpecies();
RefreshNationalities();
RefreshEmployers();
RefreshLifepaths();
RefreshFlavorText();
ReloadPreview();
if (Profile != null)
PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
}
/// <summary>
/// A slim reload that only updates the entity itself and not any of the job entities, etc.
/// </summary>
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, false, false);
// Reapply the hidden layers set from clothing
appearanceSystem.SetLayersVisibility(PreviewDummy, hiddenLayers, false, humanoid: humanoid);
}
TraitsTabs.UpdateTabMerging();
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
}
private void LoadoutsChanged(bool enabled)
{
CTabContainer.SetTabVisible(4, enabled);
ShowLoadouts.Visible = enabled;
}
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<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<ProtoId<GuideEntryPrototype>, 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);
}
}
/// <summary>
/// Refreshes all job selectors.
/// </summary>
public void RefreshJobs()
{
JobList.DisposeAllChildren();
_jobCategories.Clear();
_jobPriorities.Clear();
// 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),
};
var firstCategory = true;
foreach (var department in departments)
{
var departmentName = Loc.GetString(department.Name);
if (!_jobCategories.TryGetValue(department.ID, out var category))
{
category = new AlternatingBGContainer
{
Orientation = LayoutOrientation.Vertical,
Name = department.ID,
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
("departmentName", departmentName)),
Margin = new(0, firstCategory ? 0 : 20, 0, 0),
Children =
{
new Label
{
Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
("departmentName", departmentName)),
StyleClasses = { StyleBase.StyleClassLabelHeading, },
Margin = new(5f, 0, 0, 0),
},
},
};
firstCategory = false;
_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, HorizontalExpand = true, };
var selector = new RequirementsSelector { Margin = new(3f, 3f, 3f, 0f), HorizontalExpand = true, };
selector.OnOpenGuidebook += OnOpenGuidebook;
var icon = new TextureRect
{
TextureScale = new(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, job.Guides);
if (!_requirements.CheckJobWhitelist(job, out var reason))
selector.LockRequirements(reason);
else if (!_characterRequirementsSystem.CheckRequirementsValid(
_roleSystem.GetJobRequirement(job) ?? 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);
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();
};
_jobPriorities.Add((job.ID, selector));
jobContainer.AddChild(selector);
category.AddChild(jobContainer);
}
}
UpdateJobPriorities();
}
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()));
ReloadProfilePreview();
}
private void OnSkinColorOnValueChanged()
{
if (Profile is null)
return;
var species = _prototypeManager.Index(Profile.Species);
switch (species.SkinColoration)
{
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:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
var color = species.SkinColoration switch
{
HumanoidSkinColor.TintedHues => SkinColor.TintedHues(_rgbSkinColorSelector.Color),
HumanoidSkinColor.TintedHuesSkin => SkinColor.TintedHuesSkin(_rgbSkinColorSelector.Color, species.DefaultSkinTone),
_ => 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;
}
}
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();
}
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
UpdateBodyTypes(); // WD EDIT
UpdateBarksControl(); // WD EDIT
ReloadProfilePreview();
}
// WD EDIT START
private void SetVoice(string newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
}
private void SetBodyType(string newBodyType)
{
Profile = Profile?.WithBodyType(newBodyType);
ReloadPreview();
IsDirty = true;
}
// WD EDIT END
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
ReloadPreview();
}
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;
}
// WD EDIT START
private void SetClownName(string? clownName)
{
Profile = Profile?.WithClownName(clownName);
ReloadPreview();
IsDirty = true;
}
private void SetMimeName(string? mimeName)
{
Profile = Profile?.WithMimeName(mimeName);
IsDirty = true;
}
// WD EDIT END
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();
UpdateBodyTypes(); // WD EDIT
ReloadProfilePreview();
ReloadClothes(); // Species may have job-specific gear, reload the clothes
}
private void SetNationality(string newNationality)
{
Profile = Profile?.WithNationality(newNationality);
UpdateCharacterRequired();
IsDirty = true;
ReloadProfilePreview();
ReloadClothes(); // Nationalities may have specific gear, reload the clothes
UpdateNationalityDescription(newNationality);
}
private void SetEmployer(string newEmployer)
{
Profile = Profile?.WithEmployer(newEmployer);
UpdateCharacterRequired();
IsDirty = true;
ReloadProfilePreview();
ReloadClothes(); // Employers may have specific gear, reload the clothes
UpdateEmployerDescription(newEmployer);
}
private void SetLifepath(string newLifepath)
{
Profile = Profile?.WithLifepath(newLifepath);
UpdateCharacterRequired();
IsDirty = true;
ReloadProfilePreview();
ReloadClothes(); // Lifepaths may have specific gear, reload the clothes
UpdateLifepathDescription(newLifepath);
}
private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
IsDirty = true;
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
}
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();
}
public 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();
CCustomSpecieNameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
CCustomSpecieName.Visible = species.CustomName;
}
private void UpdateFlavorTextEdit()
{
if (_flavorTextEdit != null)
_flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
}
private void UpdateAgeEdit()
{
AgeEdit.Text = Profile?.Age.ToString() ?? "";
}
// WD EDIT START
private void UpdateBodyTypes()
{
if (Profile is null)
return;
CBodyTypesButton.Clear();
var species = _prototypeManager.Index<SpeciesPrototype>(Profile.Species);
var sex = Profile.Sex;
_bodyTypes = species.BodyTypes.Select(protoId => _prototypeManager.Index<BodyTypePrototype>(protoId))
.Where(proto => !proto.SexRestrictions.Contains(sex.ToString()))
.ToList();
for (var i = 0; i < _bodyTypes.Count; i++)
CBodyTypesButton.AddItem(Loc.GetString(_bodyTypes[i].Name), i);
// If current body type is not valid.
if (!_bodyTypes.Select(proto => proto.ID).Contains(Profile.BodyType))
{
// Then replace it with a first valid body type.
SetBodyType(_bodyTypes.First().ID);
}
CBodyTypesButton.Select(_bodyTypes.FindIndex(x => x.ID == Profile.BodyType));
IsDirty = true;
}
// WD EDIT END
/// <summary>
/// Updates selected job priorities to the profile's.
/// </summary>
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);
}
// WD EDIT START
private void UpdateClownControls()
{
if (Profile == null)
return;
ClownNameEdit.Text = Profile.ClownName ?? string.Empty;
if (ClownNameEdit.Text != string.Empty)
return;
var clownNames = _prototypeManager.Index<LocalizedDatasetPrototype>(ClownNames);
var randomName = _random.Pick(clownNames.Values);
ClownNameEdit.PlaceHolder = Loc.GetString(randomName);
}
private void UpdateMimeControls()
{
if (Profile == null)
return;
MimeNameEdit.Text = Profile.MimeName ?? string.Empty;
if (MimeNameEdit.Text != string.Empty)
return;
var mimeNames = _prototypeManager.Index<LocalizedDatasetPrototype>(MimeNames);
var randomName = _random.Pick(mimeNames.Values);
MimeNameEdit.PlaceHolder = Loc.GetString(randomName);
}
// WD EDIT END
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));
CharacterSpriteView?.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 RandomizeName()
{
if (Profile == null)
return;
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
SetName(name);
UpdateNameEdit();
}
private async void ExportImage()
{
if (_imaging)
return;
// 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<ContentSpriteSystem>().Export(PreviewDummy, 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<HumanoidAppearanceSystem>().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<HumanoidAppearanceSystem>().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;
}
#region Traits
#region Updates
private void UpdateTraitPreferences()
{
var points = _cfgManager.GetCVar(CCVars.GameTraitsDefaultPoints);
var maxTraits = _cfgManager.GetCVar(CCVars.GameTraitsMax);
if (Profile is not null && _prototypeManager.TryIndex<SpeciesPrototype>(Profile.Species, out var speciesPrototype))
points += speciesPrototype.BonusTraitPoints;
_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 += preferenceSelector.Trait.Slots;
}
TraitPointsBar.Value = points;
TraitPointsLabel.Text = Loc.GetString("humanoid-profile-editor-traits-header",
("points", points), ("traits", _traitCount),
("maxTraits", maxTraits));
// 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 = 10; // WD EDIT
TraitPointsBar.Value = points;
// Reset the whole UI and delete caches
if (reload)
{
foreach (var tab in TraitsTabs.TakenIds)
TraitsTabs.RemoveTab(tab);
}
// 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 = trait.Enable && // WD EDIT
_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;
}
if (!TraitsTabs.TryFindTabByAlias(Uncategorized, out var id))
{
var uncategorizedA = 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,
},
},
},
},
};
id = TraitsTabs.AddTab(uncategorizedA, Loc.GetString("trait-category-Uncategorized"));
TraitsTabs.SetTabAlias(id, Uncategorized);
}
var uncategorized = TraitsTabs.GetControl<BoxContainer>(id)!;
// 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.TryFindTabByAlias(key, out var _))
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,
},
},
},
},
};
var catId = parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
parent.SetTabAlias(catId, 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),
};
var catId =parent.AddTab(category, Loc.GetString($"trait-category-{key}"));
parent.SetTabAlias(catId, 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 = CheckSlots(preference ? selector.Trait.Slots : -selector.Trait.Slots, 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;
}
bool CheckSlots(int slots, bool preference)
{
var temp = _traitCount + slots;
var max = _cfgManager.GetCVar(CCVars.GameTraitsMax);
return preference ? !(temp > max) : temp > max;
}
}
#endregion
#region Functions
private BoxContainer? FindCategory(string id, NeoTabContainer parent)
{
BoxContainer? match = null;
if(parent.TryFindTabByAlias(id, out var tabId))
match = parent.GetControl<BoxContainer>(tabId);
if (match != null)
return match;
foreach (var subcategory in parent.GetControls<NeoTabContainer>())
match ??= FindCategory(id, subcategory);
return match;
}
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)
{
// TODO: HIDE LOGIC LATER
}
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
private void UpdateCharacterRequired()
{
RefreshNationalities();
RefreshEmployers();
RefreshLifepaths();
RefreshJobs();
UpdateTraits(TraitsShowUnusableButton.Pressed);
}
}
}
// TODO: Rewrite this shitty code! This shit is repeat and repeat!