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;
///
/// If we're attempting to save.
///
public event Action? Save;
///
/// Entity used for the profile editor preview
///
public EntityUid PreviewDummy;
///
/// Temporary override of their selected job, used to preview roles.
///
public JobPrototype? JobOverride;
///
/// The character slot for the current profile.
///
public int? CharacterSlot;
///
/// The work in progress profile being edited.
///
public HumanoidCharacterProfile? Profile;
private List _species = new();
private List<(string, RequirementsSelector)> _jobPriorities = new();
private readonly Dictionary _jobCategories;
private Direction _previewRotation = Direction.North;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _isDirty;
private static readonly ProtoId DefaultSpeciesGuidebook = "Species";
public event Action>>? 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();
_sprite = _entManager.System();
_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())
{
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();
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;
}
///
/// Refreshes the flavor text editor status.
///
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;
}
}
///
/// Refreshes traits selector
///
public void RefreshTraits()
{
TraitsList.DisposeAllChildren();
var traits = _prototypeManager.EnumeratePrototypes().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> traitGroups = new();
List 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(categoryId);
// Label
TraitsList.AddChild(new Label
{
Text = Loc.GetString(category.Name),
Margin = new Thickness(0, 10, 0, 0),
StyleClasses = { StyleBase.StyleClassLabelHeading },
});
}
List selectors = new();
var selectionCount = 0;
foreach (var traitProto in categoryTraits)
{
var trait = _prototypeManager.Index(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);
}
}
}
///
/// Refreshes the species selector.
///
public void RefreshSpecies()
{
SpeciesButton.Clear();
_species.Clear();
_species.AddRange(_prototypeManager.EnumeratePrototypes().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().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().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;
}
///
/// Refresh all loadouts.
///
public void RefreshLoadouts()
{
_loadoutWindow?.Dispose();
}
///
/// Reloads the entire dummy entity for preview.
///
///
/// This is expensive so not recommended to run if you have a slider.
///
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().SetEntityName(PreviewDummy, Profile.Name);
// Check and set the dirty flag to enable the save/reset buttons as appropriate.
SetDirty();
}
///
/// Resets the profile to the defaults.
///
public void ResetToDefault()
{
SetProfile(
(HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
_preferencesManager.Preferences?.SelectedCharacterIndex);
}
///
/// Sets the editor to the specified profile with the specified slot.
///
public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
{
Profile = profile?.Clone();
CharacterSlot = slot;
IsDirty = false;
JobOverride = null;
UpdateNameEdit();
UpdateFlavorTextEdit();
UpdateSexControls();
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
RefreshAntags();
RefreshJobs();
RefreshLoadouts();
RefreshSpecies();
RefreshTraits();
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;
_entManager.System().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();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex(species))
page = new ProtoId(species.Id); // Gross. See above todo comment.
if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary, 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();
var firstCategory = true;
// Get all displayed departments
var departments = new List();
foreach (var department in _prototypeManager.EnumeratePrototypes())
{
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();
// If no loadout found then disabled button
if (!protoManager.TryIndex(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(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().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() ?? "";
}
///
/// 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();
// add species sex options, default to just none if we are in bizzaro world and have no species
if (_prototypeManager.TryIndex(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(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.TryIndex(species, out var speciesProto))
return;
// Don't display the info button if no guide entry is found
if (!_prototypeManager.HasIndex(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()
: new() { new(Profile.Appearance.HairStyleId, new List() { Profile.Appearance.HairColor }) };
var facialHairMarking = Profile.Appearance.FacialHairStyleId == HairStyles.DefaultFacialHairStyle
? new List()
: new() { new(Profile.Appearance.FacialHairStyleId, new List() { 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() { 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() { 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().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().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().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;
}
}
}