Files
wwdpublic/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
ilmenwe 432042e945 Logger Sawmill Cleanup (#2413)
# Description
Cleaned up Logger obsolete compiler warnings in non robust code.
Should probably be changed to a ISawmill reference in classes to avoid
repeated lookups in heavy logging logic.
---

# Changelog

🆑

- tweak: Logger to Logger.GetSawmill("name");

---------

Co-authored-by: ilmenwe <no@mail.com>

(cherry picked from commit 2e8ffd971716d38dc6d5a520bebdf88b743045a3)
2025-05-10 01:00:05 +03:00

2718 lines
103 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.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._EE.Contractors.Prototypes;
using Content.Shared._White;
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.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<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 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 ColorSelectorSliders _rgbSkinColorSelector;
private bool _customizePronouns;
private bool _customizeStationAiName;
private bool _customizeBorgName;
private bool _customizeClownName; // WD EDIT
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
[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";
// WD EDIT END
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(); };
#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
#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
_cfgManager.OnValueChanged(CCVars.AllowCustomStationAiName, OnChangedStationAiNameCustomizationValue);
_cfgManager.OnValueChanged(CCVars.AllowCustomCyborgName, OnChangedCyborgNameCustomizationValue);
_cfgManager.OnValueChanged(WhiteCVars.AllowCustomClownName, OnChangedClownNameCustomizationValue); // WD EDIT
StationAINameEdit.OnTextChanged += args => { SetStationAiName(args.Text); };
CyborgNameEdit.OnTextChanged += args => { SetCyborgName(args.Text); };
ClownNameEdit.OnTextChanged += args => { SetClownName(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;
// 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 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();
#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;
}
// WD EDIT START
private void OnChangedClownNameCustomizationValue(bool newValue)
{
_customizeClownName = newValue;
UpdateClownControls();
}
// WD EDIT END
/// 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 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(
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);
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;
}
/// 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);
SpriteViewS.SetEntity(PreviewDummy);
SpriteViewN.SetEntity(PreviewDummy);
SpriteViewE.SetEntity(PreviewDummy);
SpriteViewW.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
UpdateBodyTypes(); // WD EDIT
UpdateGenderControls();
UpdateDisplayPronounsControls();
UpdateStationAiControls();
UpdateCyborgControls();
UpdateClownControls(); // WD EDIT
UpdateSkinColor();
UpdateSpawnPriorityControls();
UpdateFlavorTextEdit();
UpdateCustomSpecieNameEdit();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
UpdateHeightWidthSliders();
UpdateWeight();
UpdateCharacterRequired();
RefreshAntags();
RefreshJobs();
RefreshSpecies();
RefreshNationalities();
RefreshEmployers();
RefreshLifepaths();
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);
}
TraitsTabs.UpdateTabMerging();
LoadoutsTabs.UpdateTabMerging();
}
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);
}
}
/// Refreshes all job selectors
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-{department.ID}");
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<JobPrototype>(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<JobIconPrototype>(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(
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 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
UpdateBodyTypes(); // WD EDIT
ReloadProfilePreview();
SetDirty();
}
// 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();
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);
IsDirty = true;
}
// WD EDIT START
private void SetClownName(string? clownName)
{
Profile = Profile?.WithClownName(clownName);
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
IsDirty = true;
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;
}
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() ?? "";
}
// 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
/// 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);
}
// 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);
}
// 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));
SpriteViewS.InvalidateMeasure();
SpriteViewN.InvalidateMeasure();
SpriteViewE.InvalidateMeasure();
SpriteViewW.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 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.GetSawmill("humanoidprofile.editor").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.GetSawmill("humanoidprofile.editor").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);
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.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 = 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;
}
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 = 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 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 wasSelected = Profile?.LoadoutPreferences
.FirstOrDefault(it => it.LoadoutName == selector.Loadout.ID)
?.Selected ?? false;
var selected = preference.Selected && (wasSelected || CheckPoints(-selector.Loadout.Cost, true));
// 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()
{
RefreshNationalities();
RefreshEmployers();
RefreshLifepaths();
RefreshJobs();
UpdateTraits(TraitsShowUnusableButton.Pressed);
UpdateLoadouts(LoadoutsShowUnusableButton.Pressed);
}
}
}