using System.Linq; 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.Preferences; using Content.Shared.Verbs; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Prototypes; namespace Content.Server.Humanoid; public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem { [Dependency] private readonly MarkingManager _markingManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMarkingsSet); SubscribeLocalEvent(OnBaseLayersSet); SubscribeLocalEvent>(OnVerbsRequest); SubscribeLocalEvent(OnExamined); } private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args) { if (string.IsNullOrEmpty(humanoid.Species)) { return; } if (string.IsNullOrEmpty(humanoid.Initial) || !_prototypeManager.TryIndex(humanoid.Initial, out HumanoidProfilePrototype? startingSet)) { LoadProfile(uid, HumanoidCharacterProfile.DefaultWithSpecies(humanoid.Species), humanoid); return; } // 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, HumanoidAppearanceComponent component, ExaminedEvent args) { var identity = Identity.Entity(component.Owner, EntityManager); var species = GetSpeciesRepresentation(component.Species).ToLower(); var age = GetAgeRepresentation(component.Species, 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, HumanoidAppearanceComponent? humanoid = null) { if (!Resolve(uid, ref humanoid)) { return; } SetSpecies(uid, profile.Species, false, humanoid); SetSex(uid, profile.Sex, false, humanoid); humanoid.EyeColor = profile.Appearance.EyeColor; SetSkinColor(uid, profile.Appearance.SkinColor, false); humanoid.MarkingSet.Clear(); // Add markings that doesn't need coloring. We store them until we add all other markings that doesn't need it. var markingFColored = new Dictionary(); foreach (var marking in profile.Appearance.Markings) { if (_markingManager.TryGetMarking(marking, out var prototype)) { if (!prototype.ForcedColoring) { AddMarking(uid, marking.MarkingId, marking.MarkingColors, false); } else { markingFColored.Add(marking, prototype); } } } // Hair/facial hair - this may eventually be deprecated. // We need to ensure hair before applying it or coloring can try depend on markings that can be invalid var hairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.Hair, _prototypeManager) ? profile.Appearance.SkinColor : profile.Appearance.HairColor; var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, _prototypeManager) ? profile.Appearance.SkinColor : profile.Appearance.FacialHairColor; if (_markingManager.Markings.TryGetValue(profile.Appearance.HairStyleId, out var hairPrototype) && _markingManager.CanBeApplied(profile.Species, hairPrototype, _prototypeManager)) { AddMarking(uid, profile.Appearance.HairStyleId, hairColor, false); } if (_markingManager.Markings.TryGetValue(profile.Appearance.FacialHairStyleId, out var facialHairPrototype) && _markingManager.CanBeApplied(profile.Species, facialHairPrototype, _prototypeManager)) { AddMarking(uid, profile.Appearance.FacialHairStyleId, facialHairColor, false); } humanoid.MarkingSet.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _prototypeManager); // Finally adding marking with forced colors foreach (var (marking, prototype) in markingFColored) { var markingColors = MarkingColoring.GetMarkingLayerColors( prototype, profile.Appearance.SkinColor, profile.Appearance.EyeColor, humanoid.MarkingSet ); AddMarking(uid, marking.MarkingId, markingColors, false); } EnsureDefaultMarkings(uid, humanoid); humanoid.Gender = profile.Gender; if (TryComp(uid, out var grammar)) { grammar.Gender = profile.Gender; } humanoid.Age = profile.Age; Dirty(humanoid); } // 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, HumanoidAppearanceComponent? sourceHumanoid = null, HumanoidAppearanceComponent? targetHumanoid = null) { if (!Resolve(source, ref sourceHumanoid) || !Resolve(target, ref targetHumanoid)) { return; } targetHumanoid.Species = sourceHumanoid.Species; targetHumanoid.SkinColor = sourceHumanoid.SkinColor; targetHumanoid.EyeColor = sourceHumanoid.EyeColor; targetHumanoid.Age = sourceHumanoid.Age; SetSex(target, sourceHumanoid.Sex, false, targetHumanoid); targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers); targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet); targetHumanoid.Gender = sourceHumanoid.Gender; if (TryComp(target, out var grammar)) { grammar.Gender = sourceHumanoid.Gender; } Dirty(targetHumanoid); } /// /// 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, HumanoidAppearanceComponent? 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.MarkingSet.AddBack(prototype.MarkingCategory, markingObject); if (sync) Dirty(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, HumanoidAppearanceComponent? 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.MarkingSet.AddBack(prototype.MarkingCategory, markingObject); if (sync) Dirty(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, HumanoidAppearanceComponent? humanoid = null) { if (!Resolve(uid, ref humanoid) || !_markingManager.Markings.TryGetValue(marking, out var prototype)) { return; } humanoid.MarkingSet.Remove(prototype.MarkingCategory, marking); if (sync) Dirty(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, HumanoidAppearanceComponent? humanoid = null) { if (index < 0 || !Resolve(uid, ref humanoid) || !humanoid.MarkingSet.TryGetCategory(category, out var markings) || index >= markings.Count) { return; } humanoid.MarkingSet.Remove(category, index); Dirty(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, HumanoidAppearanceComponent? humanoid = null) { if (index < 0 || !_markingManager.MarkingsByCategory(category).TryGetValue(markingId, out var markingPrototype) || !Resolve(uid, ref humanoid) || !humanoid.MarkingSet.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.MarkingSet.Replace(category, index, marking); Dirty(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, HumanoidAppearanceComponent? humanoid = null) { if (index < 0 || !Resolve(uid, ref humanoid) || !humanoid.MarkingSet.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]); } Dirty(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(string species, int age) { _prototypeManager.TryIndex(species, out var speciesPrototype); if (speciesPrototype == null) { Logger.Error("Tried to get age representation of species that couldn't be indexed: " + species); return Loc.GetString("identity-age-young"); } if (age < speciesPrototype.YoungAge) { return Loc.GetString("identity-age-young"); } if (age < speciesPrototype.OldAge) { return Loc.GetString("identity-age-middle-aged"); } return Loc.GetString("identity-age-old"); } private void EnsureDefaultMarkings(EntityUid uid, HumanoidAppearanceComponent? humanoid) { if (!Resolve(uid, ref humanoid)) { return; } humanoid.MarkingSet.EnsureDefault(humanoid.SkinColor, humanoid.EyeColor, _markingManager); } }