using System.Linq; using Content.Server.GameTicking; using Content.Shared.Examine; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; using Content.Shared.IdentityManagement; using Content.Shared.Inventory.Events; using Content.Shared.Preferences; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Prototypes; namespace Content.Server.Humanoid; public sealed partial class HumanoidSystem : SharedHumanoidSystem { [Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMarkingsSet); SubscribeLocalEvent(OnBaseLayersSet); SubscribeLocalEvent>(OnVerbsRequest); SubscribeLocalEvent(OnExamined); } private void Synchronize(EntityUid uid, HumanoidComponent? component = null) { if (!Resolve(uid, ref component)) { return; } SetAppearance(uid, component.Species, component.CustomBaseLayers, component.SkinColor, component.AllHiddenLayers.ToList(), component.CurrentMarkings.GetForwardEnumerator().ToList()); } private void OnInit(EntityUid uid, HumanoidComponent humanoid, ComponentInit args) { if (string.IsNullOrEmpty(humanoid.Species)) { return; } SetSpecies(uid, humanoid.Species, false, humanoid); if (!string.IsNullOrEmpty(humanoid.Initial) && _prototypeManager.TryIndex(humanoid.Initial, out HumanoidProfilePrototype? startingSet)) { // Do this first, because profiles currently do not support custom base layers foreach (var (layer, info) in startingSet.CustomBaseLayers) { humanoid.CustomBaseLayers.Add(layer, info); } LoadProfile(uid, startingSet.Profile, humanoid); } } private void OnExamined(EntityUid uid, HumanoidComponent component, ExaminedEvent args) { var identity = Identity.Entity(component.Owner, EntityManager); var species = GetSpeciesRepresentation(component.Species).ToLower(); var age = GetAgeRepresentation(component.Age); args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species))); } /// /// Loads a humanoid character profile directly onto this humanoid mob. /// /// The mob's entity UID. /// The character profile to load. /// Humanoid component of the entity public void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } SetSpecies(uid, profile.Species, false, humanoid); humanoid.Sex = profile.Sex; SetSkinColor(uid, profile.Appearance.SkinColor, false); SetBaseLayerColor(uid, HumanoidVisualLayers.Eyes, profile.Appearance.EyeColor, false); humanoid.CurrentMarkings.Clear(); // Hair/facial hair - this may eventually be deprecated. AddMarking(uid, profile.Appearance.HairStyleId, profile.Appearance.HairColor, false); AddMarking(uid, profile.Appearance.FacialHairStyleId, profile.Appearance.FacialHairColor, false); foreach (var marking in profile.Appearance.Markings) { AddMarking(uid, marking.MarkingId, marking.MarkingColors, false); } EnsureDefaultMarkings(uid, humanoid); humanoid.Gender = profile.Gender; if (TryComp(uid, out var grammar)) { grammar.Gender = profile.Gender; } humanoid.Age = profile.Age; Synchronize(uid); } // this was done enough times that it only made sense to do it here /// /// Clones a humanoid's appearance to a target mob, provided they both have humanoid components. /// /// Source entity to fetch the original appearance from. /// Target entity to apply the source entity's appearance to. /// Source entity's humanoid component. /// Target entity's humanoid component. public void CloneAppearance(EntityUid source, EntityUid target, HumanoidComponent? sourceHumanoid = null, HumanoidComponent? targetHumanoid = null) { if (!Resolve(source, ref sourceHumanoid) || !Resolve(target, ref targetHumanoid)) { return; } targetHumanoid.Species = sourceHumanoid.Species; targetHumanoid.SkinColor = sourceHumanoid.SkinColor; targetHumanoid.Sex = sourceHumanoid.Sex; targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers); targetHumanoid.CurrentMarkings = new(sourceHumanoid.CurrentMarkings); Synchronize(target, targetHumanoid); } /// /// Set a humanoid mob's species. This will change their base sprites, as well as their current /// set of markings to fit against the mob's new species. /// /// The humanoid mob's UID. /// The species to set the mob to. Will return if the species prototype was invalid. /// Whether to immediately synchronize this to the humanoid mob, or not. /// Humanoid component of the entity public void SetSpecies(EntityUid uid, string species, bool sync = true, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_prototypeManager.TryIndex(species, out var prototype)) { return; } humanoid.Species = species; humanoid.CurrentMarkings.FilterSpecies(species, _markingManager); var oldMarkings = humanoid.CurrentMarkings.GetForwardEnumerator().ToList(); humanoid.CurrentMarkings = new(oldMarkings, prototype.MarkingPoints, _markingManager, _prototypeManager); if (sync) { Synchronize(uid, humanoid); } } /// /// Sets the skin color of this humanoid mob. Will only affect base layers that are not custom, /// custom base layers should use instead. /// /// The humanoid mob's UID. /// Skin color to set on the humanoid mob. /// Whether to synchronize this to the humanoid mob, or not. /// Humanoid component of the entity public void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } humanoid.SkinColor = skinColor; if (sync) Synchronize(uid, humanoid); } /// /// Sets the base layer ID of this humanoid mob. A humanoid mob's 'base layer' is /// the skin sprite that is applied to the mob's sprite upon appearance refresh. /// /// The humanoid mob's UID. /// The layer to target on this humanoid mob. /// The ID of the sprite to use. See . /// Whether to synchronize this to the humanoid mob, or not. /// Humanoid component of the entity public void SetBaseLayerId(EntityUid uid, HumanoidVisualLayers layer, string id, bool sync = true, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_prototypeManager.HasIndex(id)) { return; } if (humanoid.CustomBaseLayers.TryGetValue(layer, out var info)) { humanoid.CustomBaseLayers[layer] = new(id, info.Color); } else { var layerInfo = new CustomBaseLayerInfo(id, humanoid.SkinColor); humanoid.CustomBaseLayers.Add(layer, layerInfo); } if (sync) Synchronize(uid, humanoid); } /// /// Sets the color of this humanoid mob's base layer. See for a /// description of how base layers work. /// /// The humanoid mob's UID. /// The layer to target on this humanoid mob. /// The color to set this base layer to. public void SetBaseLayerColor(EntityUid uid, HumanoidVisualLayers layer, Color color, bool sync = true, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } if (humanoid.CustomBaseLayers.TryGetValue(layer, out var info)) { humanoid.CustomBaseLayers[layer] = new(info.ID, color); } else { var layerInfo = new CustomBaseLayerInfo(string.Empty, color); humanoid.CustomBaseLayers.Add(layer, layerInfo); } if (sync) Synchronize(uid, humanoid); } /// /// Toggles a humanoid's sprite layer visibility. /// /// Humanoid mob's UID /// Layer to toggle visibility for /// Humanoid component of the entity public void ToggleHiddenLayer(EntityUid uid, HumanoidVisualLayers layer, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } if (humanoid.HiddenLayers.Contains(layer)) { humanoid.HiddenLayers.Remove(layer); } else { humanoid.HiddenLayers.Add(layer); } Synchronize(uid, humanoid); } /// /// Sets the visibility for multiple layers at once on a humanoid's sprite. /// /// Humanoid mob's UID /// An enumerable of all sprite layers that are going to have their visibility set /// The visibility state of the layers given /// If this is a permanent change, or temporary. Permanent layers are stored in their own hash set. /// Humanoid component of the entity public void SetLayersVisibility(EntityUid uid, IEnumerable layers, bool visible, bool permanent = false, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } foreach (var layer in layers) { if (visible) { if (permanent && humanoid.PermanentlyHidden.Contains(layer)) { humanoid.PermanentlyHidden.Remove(layer); } humanoid.HiddenLayers.Remove(layer); } else { if (permanent) { humanoid.PermanentlyHidden.Add(layer); } humanoid.HiddenLayers.Add(layer); } } Synchronize(uid, humanoid); } /// /// Adds a marking to this humanoid. /// /// Humanoid mob's UID /// Marking ID to use /// Color to apply to all marking layers of this marking /// Whether to immediately sync this marking or not /// If this marking was forced (ignores marking points) /// Humanoid component of the entity public void AddMarking(EntityUid uid, string marking, Color? color = null, bool sync = true, bool forced = false, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_markingManager.Markings.TryGetValue(marking, out var prototype)) { return; } var markingObject = prototype.AsMarking(); markingObject.Forced = forced; if (color != null) { for (var i = 0; i < prototype.Sprites.Count; i++) { markingObject.SetColor(i, color.Value); } } humanoid.CurrentMarkings.AddBack(prototype.MarkingCategory, markingObject); if (sync) Synchronize(uid, humanoid); } /// /// /// /// Humanoid mob's UID /// Marking ID to use /// Colors to apply against this marking's set of sprites. /// Whether to immediately sync this marking or not /// If this marking was forced (ignores marking points) /// Humanoid component of the entity public void AddMarking(EntityUid uid, string marking, IReadOnlyList colors, bool sync = true, bool forced = false, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_markingManager.Markings.TryGetValue(marking, out var prototype)) { return; } var markingObject = new Marking(marking, colors); markingObject.Forced = forced; humanoid.CurrentMarkings.AddBack(prototype.MarkingCategory, markingObject); if (sync) Synchronize(uid, humanoid); } /// /// Removes a marking from a humanoid by ID. /// /// Humanoid mob's UID /// The marking to try and remove. /// Whether to immediately sync this to the humanoid /// Humanoid component of the entity public void RemoveMarking(EntityUid uid, string marking, bool sync = true, HumanoidComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_markingManager.Markings.TryGetValue(marking, out var prototype)) { return; } humanoid.CurrentMarkings.Remove(prototype.MarkingCategory, marking); if (sync) Synchronize(uid, humanoid); } /// /// Removes a marking from a humanoid by category and index. /// /// Humanoid mob's UID /// Category of the marking /// Index of the marking /// Humanoid component of the entity public void RemoveMarking(EntityUid uid, MarkingCategories category, int index, HumanoidComponent? humanoid = null) { if (index < 0 || !Resolve(uid, ref humanoid) || !humanoid.CurrentMarkings.TryGetCategory(category, out var markings) || index >= markings.Count) { return; } humanoid.CurrentMarkings.Remove(category, index); Synchronize(uid, humanoid); } /// /// Sets the marking ID of the humanoid in a category at an index in the category's list. /// /// Humanoid mob's UID /// Category of the marking /// Index of the marking /// The marking ID to use /// Humanoid component of the entity public void SetMarkingId(EntityUid uid, MarkingCategories category, int index, string markingId, HumanoidComponent? humanoid = null) { if (index < 0 || !_markingManager.MarkingsByCategory(category).TryGetValue(markingId, out var markingPrototype) || !Resolve(uid, ref humanoid) || !humanoid.CurrentMarkings.TryGetCategory(category, out var markings) || index >= markings.Count) { return; } var marking = markingPrototype.AsMarking(); for (var i = 0; i < marking.MarkingColors.Count && i < markings[index].MarkingColors.Count; i++) { marking.SetColor(i, markings[index].MarkingColors[i]); } humanoid.CurrentMarkings.Replace(category, index, marking); Synchronize(uid, humanoid); } /// /// Sets the marking colors of the humanoid in a category at an index in the category's list. /// /// Humanoid mob's UID /// Category of the marking /// Index of the marking /// The marking colors to use /// Humanoid component of the entity public void SetMarkingColor(EntityUid uid, MarkingCategories category, int index, List colors, HumanoidComponent? humanoid = null) { if (index < 0 || !Resolve(uid, ref humanoid) || !humanoid.CurrentMarkings.TryGetCategory(category, out var markings) || index >= markings.Count) { return; } for (var i = 0; i < markings[index].MarkingColors.Count && i < colors.Count; i++) { markings[index].SetColor(i, colors[i]); } Synchronize(uid, humanoid); } /// /// Takes ID of the species prototype, returns UI-friendly name of the species. /// public string GetSpeciesRepresentation(string speciesId) { if (_prototypeManager.TryIndex(speciesId, out var species)) { return Loc.GetString(species.Name); } else { return Loc.GetString("humanoid-appearance-component-unknown-species"); } } public string GetAgeRepresentation(int age) { return age switch { <= 30 => Loc.GetString("identity-age-young"), > 30 and <= 60 => Loc.GetString("identity-age-middle-aged"), > 60 => Loc.GetString("identity-age-old") }; } private void EnsureDefaultMarkings(EntityUid uid, HumanoidComponent? humanoid) { if (!Resolve(uid, ref humanoid)) { return; } humanoid.CurrentMarkings.EnsureDefault(humanoid.SkinColor, _markingManager); } }