using System.Linq; using System.Globalization; using System.Text.RegularExpressions; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; using Content.Shared.Random.Helpers; using Content.Shared.Roles; using Content.Shared.Traits; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Content.Shared.Preferences { /// /// Character profile. Looks immutable, but uses non-immutable semantics internally for serialization/code sanity purposes. /// [DataDefinition] [Serializable, NetSerializable] public sealed partial class HumanoidCharacterProfile : ICharacterProfile { public const int MaxNameLength = 32; public const int MaxDescLength = 512; private readonly Dictionary _jobPriorities; private readonly List _antagPreferences; private readonly List _traitPreferences; private HumanoidCharacterProfile( string name, string flavortext, string species, int age, Sex sex, Gender gender, HumanoidCharacterAppearance appearance, ClothingPreference clothing, BackpackPreference backpack, Dictionary jobPriorities, PreferenceUnavailableMode preferenceUnavailable, List antagPreferences, List traitPreferences) { Name = name; FlavorText = flavortext; Species = species; Age = age; Sex = sex; Gender = gender; Appearance = appearance; Clothing = clothing; Backpack = backpack; _jobPriorities = jobPriorities; PreferenceUnavailable = preferenceUnavailable; _antagPreferences = antagPreferences; _traitPreferences = traitPreferences; } /// Copy constructor but with overridable references (to prevent useless copies) private HumanoidCharacterProfile( HumanoidCharacterProfile other, Dictionary jobPriorities, List antagPreferences, List traitPreferences) : this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences) { } /// Copy constructor private HumanoidCharacterProfile(HumanoidCharacterProfile other) : this(other, new Dictionary(other.JobPriorities), new List(other.AntagPreferences), new List(other.TraitPreferences)) { } public HumanoidCharacterProfile( string name, string flavortext, string species, int age, Sex sex, Gender gender, HumanoidCharacterAppearance appearance, ClothingPreference clothing, BackpackPreference backpack, IReadOnlyDictionary jobPriorities, PreferenceUnavailableMode preferenceUnavailable, IReadOnlyList antagPreferences, IReadOnlyList traitPreferences) : this(name, flavortext, species, age, sex, gender, appearance, clothing, backpack, new Dictionary(jobPriorities), preferenceUnavailable, new List(antagPreferences), new List(traitPreferences)) { } /// /// Get the default humanoid character profile, using internal constant values. /// Defaults to for the species. /// /// public HumanoidCharacterProfile() : this( "John Doe", "", SharedHumanoidAppearanceSystem.DefaultSpecies, 18, Sex.Male, Gender.Male, new HumanoidCharacterAppearance(), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.SpawnAsOverflow, new List(), new List()) { } /// /// Return a default character profile, based on species. /// /// The species to use in this default profile. The default species is . /// Humanoid character profile with default settings. public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies) { return new( "John Doe", "", species, 18, Sex.Male, Gender.Male, HumanoidCharacterAppearance.DefaultWithSpecies(species), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.SpawnAsOverflow, new List(), new List()); } // TODO: This should eventually not be a visual change only. public static HumanoidCharacterProfile Random(HashSet? ignoredSpecies = null) { var prototypeManager = IoCManager.Resolve(); var random = IoCManager.Resolve(); var species = random.Pick(prototypeManager .EnumeratePrototypes() .Where(x => ignoredSpecies == null ? x.RoundStart : x.RoundStart && !ignoredSpecies.Contains(x.ID)) .ToArray() ).ID; return RandomWithSpecies(species); } public static HumanoidCharacterProfile RandomWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies) { var prototypeManager = IoCManager.Resolve(); var random = IoCManager.Resolve(); var sex = Sex.Unsexed; var age = 18; if (prototypeManager.TryIndex(species, out var speciesPrototype)) { sex = random.Pick(speciesPrototype.Sexes); age = random.Next(speciesPrototype.MinAge, speciesPrototype.OldAge); // people don't look and keep making 119 year old characters with zero rp, cap it at middle aged } var gender = sex == Sex.Male ? Gender.Male : Gender.Female; var name = GetName(species, gender); return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { {SharedGameTicker.FallbackOverflowJob, JobPriority.High}, }, PreferenceUnavailableMode.StayInLobby, new List(), new List()); } public string Name { get; private set; } public string FlavorText { get; private set; } public string Species { get; private set; } [DataField("age")] public int Age { get; private set; } [DataField("sex")] public Sex Sex { get; private set; } [DataField("gender")] public Gender Gender { get; private set; } public ICharacterAppearance CharacterAppearance => Appearance; [DataField("appearance")] public HumanoidCharacterAppearance Appearance { get; private set; } public ClothingPreference Clothing { get; private set; } public BackpackPreference Backpack { get; private set; } public IReadOnlyDictionary JobPriorities => _jobPriorities; public IReadOnlyList AntagPreferences => _antagPreferences; public IReadOnlyList TraitPreferences => _traitPreferences; public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } public HumanoidCharacterProfile WithName(string name) { return new(this) { Name = name }; } public HumanoidCharacterProfile WithFlavorText(string flavorText) { return new(this) { FlavorText = flavorText }; } public HumanoidCharacterProfile WithAge(int age) { return new(this) { Age = age }; } public HumanoidCharacterProfile WithSex(Sex sex) { return new(this) { Sex = sex }; } public HumanoidCharacterProfile WithGender(Gender gender) { return new(this) { Gender = gender }; } public HumanoidCharacterProfile WithSpecies(string species) { return new(this) { Species = species }; } public HumanoidCharacterProfile WithCharacterAppearance(HumanoidCharacterAppearance appearance) { return new(this) { Appearance = appearance }; } public HumanoidCharacterProfile WithClothingPreference(ClothingPreference clothing) { return new(this) { Clothing = clothing }; } public HumanoidCharacterProfile WithBackpackPreference(BackpackPreference backpack) { return new(this) { Backpack = backpack }; } public HumanoidCharacterProfile WithJobPriorities(IEnumerable> jobPriorities) { return new(this, new Dictionary(jobPriorities), _antagPreferences, _traitPreferences); } public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority) { var dictionary = new Dictionary(_jobPriorities); if (priority == JobPriority.Never) { dictionary.Remove(jobId); } else { dictionary[jobId] = priority; } return new(this, dictionary, _antagPreferences, _traitPreferences); } public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode) { return new(this) { PreferenceUnavailable = mode }; } public HumanoidCharacterProfile WithAntagPreferences(IEnumerable antagPreferences) { return new(this, _jobPriorities, new List(antagPreferences), _traitPreferences); } public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref) { var list = new List(_antagPreferences); if(pref) { if(!list.Contains(antagId)) { list.Add(antagId); } } else { if(list.Contains(antagId)) { list.Remove(antagId); } } return new(this, _jobPriorities, list, _traitPreferences); } public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) { var list = new List(_traitPreferences); // TODO: Maybe just refactor this to HashSet? Same with _antagPreferences if(pref) { if(!list.Contains(traitId)) { list.Add(traitId); } } else { if(list.Contains(traitId)) { list.Remove(traitId); } } return new(this, _jobPriorities, _antagPreferences, list); } public string Summary => Loc.GetString( "humanoid-character-profile-summary", ("name", Name), ("gender", Gender.ToString().ToLowerInvariant()), ("age", Age) ); public bool MemberwiseEquals(ICharacterProfile maybeOther) { if (maybeOther is not HumanoidCharacterProfile other) return false; if (Name != other.Name) return false; if (Age != other.Age) return false; if (Sex != other.Sex) return false; if (Gender != other.Gender) return false; if (PreferenceUnavailable != other.PreferenceUnavailable) return false; if (Clothing != other.Clothing) return false; if (Backpack != other.Backpack) return false; if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false; if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false; if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false; return Appearance.MemberwiseEquals(other.Appearance); } public void EnsureValid() { var prototypeManager = IoCManager.Resolve(); if (!prototypeManager.TryIndex(Species, out var speciesPrototype)) { Species = SharedHumanoidAppearanceSystem.DefaultSpecies; speciesPrototype = prototypeManager.Index(Species); } var sex = Sex switch { Sex.Male => Sex.Male, Sex.Female => Sex.Female, Sex.Unsexed => Sex.Unsexed, _ => Sex.Male // Invalid enum values. }; // ensure the species can be that sex and their age fits the founds var age = Age; if (speciesPrototype != null) { if (!speciesPrototype.Sexes.Contains(sex)) { sex = speciesPrototype.Sexes[0]; } age = Math.Clamp(Age, speciesPrototype.MinAge, speciesPrototype.MaxAge); } var gender = Gender switch { Gender.Epicene => Gender.Epicene, Gender.Female => Gender.Female, Gender.Male => Gender.Male, Gender.Neuter => Gender.Neuter, _ => Gender.Epicene // Invalid enum values. }; string name; if (string.IsNullOrEmpty(Name)) { name = GetName(Species, gender); } else if (Name.Length > MaxNameLength) { name = Name[..MaxNameLength]; } else { name = Name; } name = name.Trim(); var configManager = IoCManager.Resolve(); if (configManager.GetCVar(CCVars.RestrictedNames)) { name = Regex.Replace(name, @"[^A-Z,a-z,0-9, -]", string.Empty); } if (configManager.GetCVar(CCVars.ICNameCase)) { // This regex replaces the first character of the first and last words of the name with their uppercase version name = Regex.Replace(name, @"^(?\w)|\b(?\w)(?=\w*$)", m => m.Groups["word"].Value.ToUpper()); } if (string.IsNullOrEmpty(name)) { name = GetName(Species, gender); } string flavortext; if (FlavorText.Length > MaxDescLength) { flavortext = FormattedMessage.RemoveMarkup(FlavorText)[..MaxDescLength]; } else { flavortext = FormattedMessage.RemoveMarkup(FlavorText); } var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex); var prefsUnavailableMode = PreferenceUnavailable switch { PreferenceUnavailableMode.StayInLobby => PreferenceUnavailableMode.StayInLobby, PreferenceUnavailableMode.SpawnAsOverflow => PreferenceUnavailableMode.SpawnAsOverflow, _ => PreferenceUnavailableMode.StayInLobby // Invalid enum values. }; var clothing = Clothing switch { ClothingPreference.Jumpsuit => ClothingPreference.Jumpsuit, ClothingPreference.Jumpskirt => ClothingPreference.Jumpskirt, _ => ClothingPreference.Jumpsuit // Invalid enum values. }; var backpack = Backpack switch { BackpackPreference.Backpack => BackpackPreference.Backpack, BackpackPreference.Satchel => BackpackPreference.Satchel, BackpackPreference.Duffelbag => BackpackPreference.Duffelbag, _ => BackpackPreference.Backpack // Invalid enum values. }; var priorities = new Dictionary(JobPriorities .Where(p => prototypeManager.HasIndex(p.Key) && p.Value switch { JobPriority.Never => false, // Drop never since that's assumed default. JobPriority.Low => true, JobPriority.Medium => true, JobPriority.High => true, _ => false })); var antags = AntagPreferences .Where(prototypeManager.HasIndex) .ToList(); var traits = TraitPreferences .Where(prototypeManager.HasIndex) .ToList(); Name = name; FlavorText = flavortext; Age = age; Sex = sex; Gender = gender; Appearance = appearance; Clothing = clothing; Backpack = backpack; _jobPriorities.Clear(); foreach (var (job, priority) in priorities) { _jobPriorities.Add(job, priority); } PreferenceUnavailable = prefsUnavailableMode; _antagPreferences.Clear(); _antagPreferences.AddRange(antags); _traitPreferences.Clear(); _traitPreferences.AddRange(traits); } // sorry this is kind of weird and duplicated, /// working inside these non entity systems is a bit wack public static string GetName(string species, Gender gender) { var namingSystem = IoCManager.Resolve().GetEntitySystem(); return namingSystem.GetName(species, gender); } public override bool Equals(object? obj) { return obj is HumanoidCharacterProfile other && MemberwiseEquals(other); } public override int GetHashCode() { return HashCode.Combine( HashCode.Combine( Name, Species, Age, Sex, Gender, Appearance, Clothing, Backpack ), PreferenceUnavailable, _jobPriorities, _antagPreferences, _traitPreferences ); } } }