* Fix usages of TryIndex()
Most usages of TryIndex() were using it incorrectly. Checking whether prototype IDs specified in prototypes actually existed before using them. This is not appropriate as it's just hiding bugs that should be getting caught by the YAML linter and other tools. (#39115)
This then resulted in TryIndex() getting modified to log errors (94f98073b0), which is incorrect as it causes false-positive errors in proper uses of the API: external data validation. (#39098)
This commit goes through and checks every call site of TryIndex() to see whether they were correct. Most call sites were replaced with the new Resolve(), which is suitable for these "defensive programming" use cases.
Fixes #39115
Breaking change: while doing this I noticed IdCardComponent and related systems were erroneously using ProtoId<AccessLevelPrototype> for job prototypes. This has been corrected.
* fix tests
---------
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
1651 lines
56 KiB
C#
1651 lines
56 KiB
C#
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Humanoid;
|
|
using Content.Client.Lobby.UI.Loadouts;
|
|
using Content.Client.Lobby.UI.Roles;
|
|
using Content.Client.Message;
|
|
using Content.Client.Players.PlayTimeTracking;
|
|
using Content.Client.Sprite;
|
|
using Content.Client.Stylesheets;
|
|
using Content.Client.UserInterface.Systems.Guidebook;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Clothing;
|
|
using Content.Shared.GameTicking;
|
|
using Content.Shared.Guidebook;
|
|
using Content.Shared.Humanoid;
|
|
using Content.Shared.Humanoid.Markings;
|
|
using Content.Shared.Humanoid.Prototypes;
|
|
using Content.Shared.Preferences;
|
|
using Content.Shared.Preferences.Loadouts;
|
|
using Content.Shared.Roles;
|
|
using Content.Shared.Traits;
|
|
using Robust.Client.AutoGenerated;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Client.Utility;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.ContentPack;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Utility;
|
|
using Direction = Robust.Shared.Maths.Direction;
|
|
|
|
namespace Content.Client.Lobby.UI
|
|
{
|
|
[GenerateTypedNameReferences]
|
|
public sealed partial class HumanoidProfileEditor : BoxContainer
|
|
{
|
|
private readonly IClientPreferencesManager _preferencesManager;
|
|
private readonly IConfigurationManager _cfgManager;
|
|
private readonly IEntityManager _entManager;
|
|
private readonly IFileDialogManager _dialogManager;
|
|
private readonly IPlayerManager _playerManager;
|
|
private readonly IPrototypeManager _prototypeManager;
|
|
private readonly IResourceManager _resManager;
|
|
private readonly MarkingManager _markingManager;
|
|
private readonly JobRequirementsManager _requirements;
|
|
private readonly LobbyUIController _controller;
|
|
|
|
private readonly SpriteSystem _sprite;
|
|
|
|
// CCvar.
|
|
private int _maxNameLength;
|
|
private bool _allowFlavorText;
|
|
|
|
private FlavorText.FlavorText? _flavorText;
|
|
private TextEdit? _flavorTextEdit;
|
|
|
|
// One at a time.
|
|
private LoadoutWindow? _loadoutWindow;
|
|
|
|
private bool _exporting;
|
|
private bool _imaging;
|
|
|
|
/// <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<(string, RequirementsSelector)> _jobPriorities = new();
|
|
|
|
private readonly Dictionary<string, BoxContainer> _jobCategories;
|
|
|
|
private Direction _previewRotation = Direction.North;
|
|
|
|
private ColorSelectorSliders _rgbSkinColorSelector;
|
|
|
|
private bool _isDirty;
|
|
|
|
private static readonly ProtoId<GuideEntryPrototype> DefaultSpeciesGuidebook = "Species";
|
|
|
|
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
|
|
|
|
private ISawmill _sawmill;
|
|
|
|
public HumanoidProfileEditor(
|
|
IClientPreferencesManager preferencesManager,
|
|
IConfigurationManager configurationManager,
|
|
IEntityManager entManager,
|
|
IFileDialogManager dialogManager,
|
|
ILogManager logManager,
|
|
IPlayerManager playerManager,
|
|
IPrototypeManager prototypeManager,
|
|
IResourceManager resManager,
|
|
JobRequirementsManager requirements,
|
|
MarkingManager markings)
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
_sawmill = logManager.GetSawmill("profile.editor");
|
|
_cfgManager = configurationManager;
|
|
_entManager = entManager;
|
|
_dialogManager = dialogManager;
|
|
_playerManager = playerManager;
|
|
_prototypeManager = prototypeManager;
|
|
_markingManager = markings;
|
|
_preferencesManager = preferencesManager;
|
|
_resManager = resManager;
|
|
_requirements = requirements;
|
|
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
|
|
_sprite = _entManager.System<SpriteSystem>();
|
|
|
|
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
|
_allowFlavorText = _cfgManager.GetCVar(CCVars.FlavorText);
|
|
|
|
ImportButton.OnPressed += args =>
|
|
{
|
|
ImportProfile();
|
|
};
|
|
|
|
ExportButton.OnPressed += args =>
|
|
{
|
|
ExportProfile();
|
|
};
|
|
|
|
ExportImageButton.OnPressed += args =>
|
|
{
|
|
ExportImage();
|
|
};
|
|
|
|
OpenImagesButton.OnPressed += args =>
|
|
{
|
|
_resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
|
|
};
|
|
|
|
ResetButton.OnPressed += args =>
|
|
{
|
|
SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
|
|
};
|
|
|
|
SaveButton.OnPressed += args =>
|
|
{
|
|
Save?.Invoke();
|
|
};
|
|
|
|
#region Left
|
|
|
|
#region Name
|
|
|
|
NameEdit.OnTextChanged += args => { SetName(args.Text); };
|
|
NameEdit.IsValid = args => args.Length <= _maxNameLength;
|
|
NameRandomize.OnPressed += args => RandomizeName();
|
|
RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
|
|
WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
|
|
|
|
#endregion Name
|
|
|
|
#region Appearance
|
|
|
|
TabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
|
|
|
|
#region Sex
|
|
|
|
SexButton.OnItemSelected += args =>
|
|
{
|
|
SexButton.SelectId(args.Id);
|
|
SetSex((Sex) args.Id);
|
|
};
|
|
|
|
#endregion Sex
|
|
|
|
#region Age
|
|
|
|
AgeEdit.OnTextChanged += args =>
|
|
{
|
|
if (!int.TryParse(args.Text, out var newAge))
|
|
return;
|
|
|
|
SetAge(newAge);
|
|
};
|
|
|
|
#endregion Age
|
|
|
|
#region Gender
|
|
|
|
PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
|
|
PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
|
|
PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
|
|
PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
|
|
|
|
PronounsButton.OnItemSelected += args =>
|
|
{
|
|
PronounsButton.SelectId(args.Id);
|
|
SetGender((Gender) args.Id);
|
|
};
|
|
|
|
#endregion Gender
|
|
|
|
RefreshSpecies();
|
|
|
|
SpeciesButton.OnItemSelected += args =>
|
|
{
|
|
SpeciesButton.SelectId(args.Id);
|
|
SetSpecies(_species[args.Id].ID);
|
|
UpdateHairPickers();
|
|
OnSkinColorOnValueChanged();
|
|
};
|
|
|
|
#region Skin
|
|
|
|
Skin.OnValueChanged += _ =>
|
|
{
|
|
OnSkinColorOnValueChanged();
|
|
};
|
|
|
|
RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
|
|
_rgbSkinColorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
|
|
_rgbSkinColorSelector.OnColorChanged += _ =>
|
|
{
|
|
OnSkinColorOnValueChanged();
|
|
};
|
|
|
|
#endregion
|
|
|
|
#region Hair
|
|
|
|
HairStylePicker.OnMarkingSelect += newStyle =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairStyleName(newStyle.id));
|
|
ReloadPreview();
|
|
};
|
|
|
|
HairStylePicker.OnColorChanged += newColor =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
|
|
UpdateCMarkingsHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
FacialHairPicker.OnMarkingSelect += newStyle =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
|
|
ReloadPreview();
|
|
};
|
|
|
|
FacialHairPicker.OnColorChanged += newColor =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
|
|
UpdateCMarkingsFacialHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
HairStylePicker.OnSlotRemove += _ =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
|
|
);
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
FacialHairPicker.OnSlotRemove += _ =>
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
|
|
);
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsFacialHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
HairStylePicker.OnSlotAdd += delegate()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
|
|
.FirstOrDefault();
|
|
|
|
if (string.IsNullOrEmpty(hair))
|
|
return;
|
|
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithHairStyleName(hair)
|
|
);
|
|
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
FacialHairPicker.OnSlotAdd += delegate()
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
|
|
.FirstOrDefault();
|
|
|
|
if (string.IsNullOrEmpty(hair))
|
|
return;
|
|
|
|
Profile = Profile.WithCharacterAppearance(
|
|
Profile.Appearance.WithFacialHairStyleName(hair)
|
|
);
|
|
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsFacialHair();
|
|
ReloadPreview();
|
|
};
|
|
|
|
#endregion Hair
|
|
|
|
#region SpawnPriority
|
|
|
|
foreach (var value in Enum.GetValues<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;
|
|
ReloadProfilePreview();
|
|
};
|
|
|
|
#endregion Eyes
|
|
|
|
#endregion Appearance
|
|
|
|
#region Jobs
|
|
|
|
TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab"));
|
|
|
|
PreferenceUnavailableButton.AddItem(
|
|
Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"),
|
|
(int) PreferenceUnavailableMode.StayInLobby);
|
|
PreferenceUnavailableButton.AddItem(
|
|
Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button",
|
|
("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))),
|
|
(int) PreferenceUnavailableMode.SpawnAsOverflow);
|
|
|
|
PreferenceUnavailableButton.OnItemSelected += args =>
|
|
{
|
|
PreferenceUnavailableButton.SelectId(args.Id);
|
|
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
|
|
SetDirty();
|
|
};
|
|
|
|
_jobCategories = new Dictionary<string, BoxContainer>();
|
|
|
|
RefreshAntags();
|
|
RefreshJobs();
|
|
|
|
#endregion Jobs
|
|
|
|
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
|
|
|
RefreshTraits();
|
|
|
|
#region Markings
|
|
|
|
TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
|
|
|
|
Markings.OnMarkingAdded += OnMarkingChange;
|
|
Markings.OnMarkingRemoved += OnMarkingChange;
|
|
Markings.OnMarkingColorChange += OnMarkingChange;
|
|
Markings.OnMarkingRankChange += OnMarkingChange;
|
|
|
|
#endregion Markings
|
|
|
|
RefreshFlavorText();
|
|
|
|
#region Dummy
|
|
|
|
SpriteRotateLeft.OnPressed += _ =>
|
|
{
|
|
_previewRotation = _previewRotation.TurnCw();
|
|
SetPreviewRotation(_previewRotation);
|
|
};
|
|
SpriteRotateRight.OnPressed += _ =>
|
|
{
|
|
_previewRotation = _previewRotation.TurnCcw();
|
|
SetPreviewRotation(_previewRotation);
|
|
};
|
|
|
|
#endregion Dummy
|
|
|
|
#endregion Left
|
|
|
|
ShowClothes.OnToggled += args =>
|
|
{
|
|
ReloadPreview();
|
|
};
|
|
|
|
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
|
|
|
|
UpdateSpeciesGuidebookIcon();
|
|
IsDirty = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the flavor text editor status.
|
|
/// </summary>
|
|
public void RefreshFlavorText()
|
|
{
|
|
if (_allowFlavorText)
|
|
{
|
|
if (_flavorText != null)
|
|
return;
|
|
|
|
_flavorText = new FlavorText.FlavorText();
|
|
TabContainer.AddChild(_flavorText);
|
|
TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
|
|
_flavorTextEdit = _flavorText.CFlavorTextInput;
|
|
|
|
_flavorText.OnFlavorTextChanged += OnFlavorTextChange;
|
|
}
|
|
else
|
|
{
|
|
if (_flavorText == null)
|
|
return;
|
|
|
|
TabContainer.RemoveChild(_flavorText);
|
|
_flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
|
|
_flavorText.Dispose();
|
|
_flavorTextEdit?.Dispose();
|
|
_flavorTextEdit = null;
|
|
_flavorText = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes traits selector
|
|
/// </summary>
|
|
public void RefreshTraits()
|
|
{
|
|
TraitsList.DisposeAllChildren();
|
|
|
|
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
|
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
|
|
|
if (traits.Count < 1)
|
|
{
|
|
TraitsList.AddChild(new Label
|
|
{
|
|
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
|
FontColorOverride = Color.Gray,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Setup model
|
|
Dictionary<string, List<string>> traitGroups = new();
|
|
List<string> defaultTraits = new();
|
|
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
|
|
|
foreach (var trait in traits)
|
|
{
|
|
if (trait.Category == null)
|
|
{
|
|
defaultTraits.Add(trait.ID);
|
|
continue;
|
|
}
|
|
|
|
if (!_prototypeManager.HasIndex(trait.Category))
|
|
continue;
|
|
|
|
var group = traitGroups.GetOrNew(trait.Category);
|
|
group.Add(trait.ID);
|
|
}
|
|
|
|
// Create UI view from model
|
|
foreach (var (categoryId, categoryTraits) in traitGroups)
|
|
{
|
|
TraitCategoryPrototype? category = null;
|
|
|
|
if (categoryId != TraitCategoryPrototype.Default)
|
|
{
|
|
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
|
// Label
|
|
TraitsList.AddChild(new Label
|
|
{
|
|
Text = Loc.GetString(category.Name),
|
|
Margin = new Thickness(0, 10, 0, 0),
|
|
StyleClasses = { StyleBase.StyleClassLabelHeading },
|
|
});
|
|
}
|
|
|
|
List<TraitPreferenceSelector?> selectors = new();
|
|
var selectionCount = 0;
|
|
|
|
foreach (var traitProto in categoryTraits)
|
|
{
|
|
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
|
var selector = new TraitPreferenceSelector(trait);
|
|
|
|
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
|
if (selector.Preference)
|
|
selectionCount += trait.Cost;
|
|
|
|
selector.PreferenceChanged += preference =>
|
|
{
|
|
if (preference)
|
|
{
|
|
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
|
}
|
|
else
|
|
{
|
|
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
|
}
|
|
|
|
SetDirty();
|
|
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
|
};
|
|
selectors.Add(selector);
|
|
}
|
|
|
|
// Selection counter
|
|
if (category is { MaxTraitPoints: >= 0 })
|
|
{
|
|
TraitsList.AddChild(new Label
|
|
{
|
|
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
|
FontColorOverride = Color.Gray
|
|
});
|
|
}
|
|
|
|
foreach (var selector in selectors)
|
|
{
|
|
if (selector == null)
|
|
continue;
|
|
|
|
if (category is { MaxTraitPoints: >= 0 } &&
|
|
selector.Cost + selectionCount > category.MaxTraitPoints)
|
|
{
|
|
selector.Checkbox.Label.FontColorOverride = Color.Red;
|
|
}
|
|
|
|
TraitsList.AddChild(selector);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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++)
|
|
{
|
|
var name = Loc.GetString(_species[i].Name);
|
|
SpeciesButton.AddItem(name, i);
|
|
|
|
if (Profile?.Species.Equals(_species[i].ID) == true)
|
|
{
|
|
SpeciesButton.SelectId(i);
|
|
}
|
|
}
|
|
|
|
// If our species isn't available then reset it to default.
|
|
if (Profile != null)
|
|
{
|
|
if (!speciesIds.Contains(Profile.Species))
|
|
{
|
|
SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RefreshAntags()
|
|
{
|
|
AntagList.DisposeAllChildren();
|
|
var items = new[]
|
|
{
|
|
("humanoid-profile-editor-antag-preference-yes-button", 0),
|
|
("humanoid-profile-editor-antag-preference-no-button", 1)
|
|
};
|
|
|
|
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
|
|
{
|
|
if (!antag.SetPreference)
|
|
continue;
|
|
|
|
var antagContainer = new BoxContainer()
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
};
|
|
|
|
var selector = new RequirementsSelector()
|
|
{
|
|
Margin = new Thickness(3f, 3f, 3f, 0f),
|
|
};
|
|
selector.OnOpenGuidebook += OnOpenGuidebook;
|
|
|
|
var title = Loc.GetString(antag.Name);
|
|
var description = Loc.GetString(antag.Objective);
|
|
selector.Setup(items, title, 250, description, guides: antag.Guides);
|
|
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
|
|
|
|
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
|
|
if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
|
|
{
|
|
selector.LockRequirements(reason);
|
|
Profile = Profile?.WithAntagPreference(antag.ID, false);
|
|
SetDirty();
|
|
}
|
|
else
|
|
{
|
|
selector.UnlockRequirements();
|
|
}
|
|
|
|
selector.OnSelected += preference =>
|
|
{
|
|
Profile = Profile?.WithAntagPreference(antag.ID, preference == 0);
|
|
SetDirty();
|
|
};
|
|
|
|
antagContainer.AddChild(selector);
|
|
|
|
antagContainer.AddChild(new Button()
|
|
{
|
|
Disabled = true,
|
|
Text = Loc.GetString("loadout-window"),
|
|
HorizontalAlignment = HAlignment.Right,
|
|
Margin = new Thickness(3f, 0f, 0f, 0f),
|
|
});
|
|
|
|
AntagList.AddChild(antagContainer);
|
|
}
|
|
}
|
|
|
|
private void SetDirty()
|
|
{
|
|
// If it equals default then reset the button.
|
|
if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true)
|
|
{
|
|
IsDirty = false;
|
|
return;
|
|
}
|
|
|
|
// TODO: Check if profile matches default.
|
|
IsDirty = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh all loadouts.
|
|
/// </summary>
|
|
public void RefreshLoadouts()
|
|
{
|
|
_loadoutWindow?.Dispose();
|
|
}
|
|
|
|
/// <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, JobOverride, ShowClothes.Pressed);
|
|
SpriteView.SetEntity(PreviewDummy);
|
|
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
|
|
|
|
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
|
|
SetDirty();
|
|
}
|
|
|
|
/// <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();
|
|
UpdateGenderControls();
|
|
UpdateSkinColor();
|
|
UpdateSpawnPriorityControls();
|
|
UpdateAgeEdit();
|
|
UpdateEyePickers();
|
|
UpdateSaveButton();
|
|
UpdateMarkings();
|
|
UpdateHairPickers();
|
|
UpdateCMarkingsHair();
|
|
UpdateCMarkingsFacialHair();
|
|
|
|
RefreshAntags();
|
|
RefreshJobs();
|
|
RefreshLoadouts();
|
|
RefreshSpecies();
|
|
RefreshTraits();
|
|
RefreshFlavorText();
|
|
ReloadPreview();
|
|
|
|
if (Profile != null)
|
|
{
|
|
PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
|
|
}
|
|
}
|
|
|
|
|
|
/// <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;
|
|
|
|
_entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
|
|
|
|
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
|
|
SetDirty();
|
|
}
|
|
|
|
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
|
|
{
|
|
// TODO GUIDEBOOK
|
|
// make the species guide book a field on the species prototype.
|
|
// I.e., do what jobs/antags do.
|
|
|
|
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
|
|
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
|
|
var page = DefaultSpeciesGuidebook;
|
|
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
|
|
page = new ProtoId<GuideEntryPrototype>(species.Id); // Gross. See above todo comment.
|
|
|
|
if (_prototypeManager.Resolve(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();
|
|
var firstCategory = true;
|
|
|
|
// Get all displayed departments
|
|
var departments = new List<DepartmentPrototype>();
|
|
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
|
|
{
|
|
if (department.EditorHidden)
|
|
continue;
|
|
|
|
departments.Add(department);
|
|
}
|
|
|
|
departments.Sort(DepartmentUIComparer.Instance);
|
|
|
|
var items = new[]
|
|
{
|
|
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
|
|
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
|
|
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
|
|
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
|
|
};
|
|
|
|
foreach (var department in departments)
|
|
{
|
|
var departmentName = Loc.GetString(department.Name);
|
|
|
|
if (!_jobCategories.TryGetValue(department.ID, out var category))
|
|
{
|
|
category = new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
Name = department.ID,
|
|
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
|
|
("departmentName", departmentName))
|
|
};
|
|
|
|
if (firstCategory)
|
|
{
|
|
firstCategory = false;
|
|
}
|
|
else
|
|
{
|
|
category.AddChild(new Control
|
|
{
|
|
MinSize = new Vector2(0, 23),
|
|
});
|
|
}
|
|
|
|
category.AddChild(new PanelContainer
|
|
{
|
|
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#464966")},
|
|
Children =
|
|
{
|
|
new Label
|
|
{
|
|
Text = Loc.GetString("humanoid-profile-editor-department-jobs-label",
|
|
("departmentName", departmentName)),
|
|
Margin = new Thickness(5f, 0, 0, 0)
|
|
}
|
|
}
|
|
});
|
|
|
|
_jobCategories[department.ID] = category;
|
|
JobList.AddChild(category);
|
|
}
|
|
|
|
var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
|
|
.Where(job => job.SetPreference)
|
|
.ToArray();
|
|
|
|
Array.Sort(jobs, JobUIComparer.Instance);
|
|
|
|
foreach (var job in jobs)
|
|
{
|
|
var jobContainer = new BoxContainer()
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
};
|
|
|
|
var selector = new RequirementsSelector()
|
|
{
|
|
Margin = new Thickness(3f, 3f, 3f, 0f),
|
|
};
|
|
selector.OnOpenGuidebook += OnOpenGuidebook;
|
|
|
|
var icon = new TextureRect
|
|
{
|
|
TextureScale = new Vector2(2, 2),
|
|
VerticalAlignment = VAlignment.Center
|
|
};
|
|
var jobIcon = _prototypeManager.Index(job.Icon);
|
|
icon.Texture = _sprite.Frame0(jobIcon.Icon);
|
|
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides);
|
|
|
|
if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
|
|
{
|
|
selector.LockRequirements(reason);
|
|
}
|
|
else
|
|
{
|
|
selector.UnlockRequirements();
|
|
}
|
|
|
|
selector.OnSelected += selectedPrio =>
|
|
{
|
|
var selectedJobPrio = (JobPriority) selectedPrio;
|
|
Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio);
|
|
|
|
foreach (var (jobId, other) in _jobPriorities)
|
|
{
|
|
// Sync other selectors with the same job in case of multiple department jobs
|
|
if (jobId == job.ID)
|
|
{
|
|
other.Select(selectedPrio);
|
|
continue;
|
|
}
|
|
|
|
if (selectedJobPrio != JobPriority.High || (JobPriority) other.Selected != JobPriority.High)
|
|
continue;
|
|
|
|
// Lower any other high priorities to medium.
|
|
other.Select((int)JobPriority.Medium);
|
|
Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
|
|
}
|
|
|
|
// TODO: Only reload on high change (either to or from).
|
|
ReloadPreview();
|
|
|
|
UpdateJobPriorities();
|
|
SetDirty();
|
|
};
|
|
|
|
var loadoutWindowBtn = new Button()
|
|
{
|
|
Text = Loc.GetString("loadout-window"),
|
|
HorizontalAlignment = HAlignment.Right,
|
|
VerticalAlignment = VAlignment.Center,
|
|
Margin = new Thickness(3f, 3f, 0f, 0f),
|
|
};
|
|
|
|
var collection = IoCManager.Instance!;
|
|
var protoManager = collection.Resolve<IPrototypeManager>();
|
|
|
|
// If no loadout found then disabled button
|
|
if (!protoManager.TryIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto))
|
|
{
|
|
loadoutWindowBtn.Disabled = true;
|
|
}
|
|
// else
|
|
else
|
|
{
|
|
loadoutWindowBtn.OnPressed += args =>
|
|
{
|
|
RoleLoadout? loadout = null;
|
|
|
|
// Clone so we don't modify the underlying loadout.
|
|
Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
|
|
loadout = loadout?.Clone();
|
|
|
|
if (loadout == null)
|
|
{
|
|
loadout = new RoleLoadout(roleLoadoutProto.ID);
|
|
loadout.SetDefault(Profile, _playerManager.LocalSession, _prototypeManager);
|
|
}
|
|
|
|
OpenLoadout(job, loadout, roleLoadoutProto);
|
|
};
|
|
}
|
|
|
|
_jobPriorities.Add((job.ID, selector));
|
|
jobContainer.AddChild(selector);
|
|
jobContainer.AddChild(loadoutWindowBtn);
|
|
category.AddChild(jobContainer);
|
|
}
|
|
}
|
|
|
|
UpdateJobPriorities();
|
|
}
|
|
|
|
private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto)
|
|
{
|
|
_loadoutWindow?.Dispose();
|
|
_loadoutWindow = null;
|
|
var collection = IoCManager.Instance;
|
|
|
|
if (collection == null || _playerManager.LocalSession == null || Profile == null)
|
|
return;
|
|
|
|
JobOverride = jobProto;
|
|
var session = _playerManager.LocalSession;
|
|
|
|
_loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection)
|
|
{
|
|
Title = Loc.GetString("loadout-window-title-loadout", ("job", $"{jobProto?.LocalizedName}")),
|
|
};
|
|
|
|
// Refresh the buttons etc.
|
|
_loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
|
|
_loadoutWindow.OpenCenteredLeft();
|
|
|
|
_loadoutWindow.OnNameChanged += name =>
|
|
{
|
|
roleLoadout.EntityName = name;
|
|
Profile = Profile.WithLoadout(roleLoadout);
|
|
SetDirty();
|
|
};
|
|
|
|
_loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) =>
|
|
{
|
|
roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager);
|
|
_loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
|
|
Profile = Profile?.WithLoadout(roleLoadout);
|
|
ReloadPreview();
|
|
};
|
|
|
|
_loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) =>
|
|
{
|
|
roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager);
|
|
_loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
|
|
Profile = Profile?.WithLoadout(roleLoadout);
|
|
ReloadPreview();
|
|
};
|
|
|
|
JobOverride = jobProto;
|
|
ReloadPreview();
|
|
|
|
_loadoutWindow.OnClose += () =>
|
|
{
|
|
JobOverride = null;
|
|
ReloadPreview();
|
|
};
|
|
|
|
if (Profile is null)
|
|
return;
|
|
|
|
UpdateJobPriorities();
|
|
}
|
|
|
|
private void OnFlavorTextChange(string content)
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
Profile = Profile.WithFlavorText(content);
|
|
SetDirty();
|
|
}
|
|
|
|
private void OnMarkingChange(MarkingSet markings)
|
|
{
|
|
if (Profile is null)
|
|
return;
|
|
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
private void OnSkinColorOnValueChanged()
|
|
{
|
|
if (Profile is null) return;
|
|
|
|
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
|
|
|
|
switch (skin)
|
|
{
|
|
case HumanoidSkinColor.HumanToned:
|
|
{
|
|
if (!Skin.Visible)
|
|
{
|
|
Skin.Visible = true;
|
|
RgbSkinColorContainer.Visible = false;
|
|
}
|
|
|
|
var color = SkinColor.HumanSkinTone((int) Skin.Value);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.Hues:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.TintedHues:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
|
break;
|
|
}
|
|
case HumanoidSkinColor.VoxFeathers:
|
|
{
|
|
if (!RgbSkinColorContainer.Visible)
|
|
{
|
|
Skin.Visible = false;
|
|
RgbSkinColorContainer.Visible = true;
|
|
}
|
|
|
|
var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
|
|
|
|
Markings.CurrentSkinColor = color;
|
|
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
|
|
break;
|
|
}
|
|
}
|
|
|
|
ReloadProfilePreview();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (!disposing)
|
|
return;
|
|
|
|
_loadoutWindow?.Dispose();
|
|
_loadoutWindow = null;
|
|
}
|
|
|
|
protected override void EnteredTree()
|
|
{
|
|
base.EnteredTree();
|
|
ReloadPreview();
|
|
}
|
|
|
|
protected override void ExitedTree()
|
|
{
|
|
base.ExitedTree();
|
|
_entManager.DeleteEntity(PreviewDummy);
|
|
PreviewDummy = EntityUid.Invalid;
|
|
}
|
|
|
|
private void SetAge(int newAge)
|
|
{
|
|
Profile = Profile?.WithAge(newAge);
|
|
ReloadPreview();
|
|
}
|
|
|
|
private void SetSex(Sex newSex)
|
|
{
|
|
Profile = Profile?.WithSex(newSex);
|
|
// for convenience, default to most common gender when new sex is selected
|
|
switch (newSex)
|
|
{
|
|
case Sex.Male:
|
|
Profile = Profile?.WithGender(Gender.Male);
|
|
break;
|
|
case Sex.Female:
|
|
Profile = Profile?.WithGender(Gender.Female);
|
|
break;
|
|
default:
|
|
Profile = Profile?.WithGender(Gender.Epicene);
|
|
break;
|
|
}
|
|
|
|
UpdateGenderControls();
|
|
Markings.SetSex(newSex);
|
|
ReloadPreview();
|
|
}
|
|
|
|
private void SetGender(Gender newGender)
|
|
{
|
|
Profile = Profile?.WithGender(newGender);
|
|
ReloadPreview();
|
|
}
|
|
|
|
private void SetSpecies(string newSpecies)
|
|
{
|
|
Profile = Profile?.WithSpecies(newSpecies);
|
|
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
|
|
Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
|
|
// In case there's job restrictions for the species
|
|
RefreshJobs();
|
|
// In case there's species restrictions for loadouts
|
|
RefreshLoadouts();
|
|
UpdateSexControls(); // update sex for new species
|
|
UpdateSpeciesGuidebookIcon();
|
|
ReloadPreview();
|
|
}
|
|
|
|
private void SetName(string newName)
|
|
{
|
|
Profile = Profile?.WithName(newName);
|
|
SetDirty();
|
|
|
|
if (!IsDirty)
|
|
return;
|
|
|
|
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
|
|
}
|
|
|
|
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
|
|
{
|
|
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
|
|
SetDirty();
|
|
}
|
|
|
|
public bool IsDirty
|
|
{
|
|
get => _isDirty;
|
|
set
|
|
{
|
|
if (_isDirty == value)
|
|
return;
|
|
|
|
_isDirty = value;
|
|
UpdateSaveButton();
|
|
}
|
|
}
|
|
|
|
private void UpdateNameEdit()
|
|
{
|
|
NameEdit.Text = Profile?.Name ?? "";
|
|
}
|
|
|
|
private void UpdateFlavorTextEdit()
|
|
{
|
|
if (_flavorTextEdit != null)
|
|
{
|
|
_flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
|
|
}
|
|
}
|
|
|
|
private void UpdateAgeEdit()
|
|
{
|
|
AgeEdit.Text = Profile?.Age.ToString() ?? "";
|
|
}
|
|
|
|
/// <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.Resolve<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;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public void UpdateSpeciesGuidebookIcon()
|
|
{
|
|
SpeciesInfoButton.StyleClasses.Clear();
|
|
|
|
var species = Profile?.Species;
|
|
if (species is null)
|
|
return;
|
|
|
|
if (!_prototypeManager.Resolve<SpeciesPrototype>(species, out var speciesProto))
|
|
return;
|
|
|
|
// Don't display the info button if no guide entry is found
|
|
if (!_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 UpdateSpawnPriorityControls()
|
|
{
|
|
if (Profile == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
|
|
}
|
|
|
|
private void UpdateHairPickers()
|
|
{
|
|
if (Profile == null)
|
|
{
|
|
return;
|
|
}
|
|
var hairMarking = Profile.Appearance.HairStyleId == HairStyles.DefaultHairStyle
|
|
? new List<Marking>()
|
|
: new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) };
|
|
|
|
var facialHairMarking = Profile.Appearance.FacialHairStyleId == 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))
|
|
{
|
|
if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
|
|
{
|
|
hairColor = Profile.Appearance.SkinColor;
|
|
}
|
|
else
|
|
{
|
|
hairColor = Profile.Appearance.HairColor;
|
|
}
|
|
}
|
|
}
|
|
if (hairColor != null)
|
|
{
|
|
Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List<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))
|
|
{
|
|
if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
|
|
{
|
|
facialHairColor = Profile.Appearance.SkinColor;
|
|
}
|
|
else
|
|
{
|
|
facialHairColor = Profile.Appearance.FacialHairColor;
|
|
}
|
|
}
|
|
}
|
|
if (facialHairColor != null)
|
|
{
|
|
Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<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 SetPreviewRotation(Direction direction)
|
|
{
|
|
SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
|
|
}
|
|
|
|
private void RandomizeEverything()
|
|
{
|
|
Profile = HumanoidCharacterProfile.Random();
|
|
SetProfile(Profile, CharacterSlot);
|
|
SetDirty();
|
|
}
|
|
|
|
private void RandomizeName()
|
|
{
|
|
if (Profile == null) return;
|
|
var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
|
|
SetName(name);
|
|
UpdateNameEdit();
|
|
}
|
|
|
|
private async void ExportImage()
|
|
{
|
|
if (_imaging)
|
|
return;
|
|
|
|
var dir = SpriteView.OverrideDirection ?? Direction.South;
|
|
|
|
// I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save
|
|
_imaging = true;
|
|
await _entManager.System<ContentSpriteSystem>().Export(PreviewDummy, dir, includeId: false);
|
|
_imaging = false;
|
|
}
|
|
|
|
private async void ImportProfile()
|
|
{
|
|
if (_exporting || CharacterSlot == null || Profile == null)
|
|
return;
|
|
|
|
StartExport();
|
|
await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
|
|
|
|
if (file == null)
|
|
{
|
|
EndExport();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var profile = _entManager.System<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;
|
|
}
|
|
}
|
|
}
|