using System.Linq; using System.Text.RegularExpressions; using Content.Shared.CCVar; using Content.Shared.CharacterAppearance; using Content.Shared.GameTicking; using Content.Shared.Random.Helpers; using Content.Shared.Roles; using Content.Shared.Species; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; namespace Content.Shared.Preferences { /// /// Character profile. Looks immutable, but uses non-immutable semantics internally for serialization/code sanity purposes. /// [Serializable, NetSerializable] public sealed class HumanoidCharacterProfile : ICharacterProfile { public const int MinimumAge = 18; public const int MaximumAge = 120; public const int MaxNameLength = 32; private readonly Dictionary _jobPriorities; private readonly List _antagPreferences; private HumanoidCharacterProfile( string name, string species, int age, Sex sex, Gender gender, HumanoidCharacterAppearance appearance, ClothingPreference clothing, BackpackPreference backpack, Dictionary jobPriorities, PreferenceUnavailableMode preferenceUnavailable, List antagPreferences) { Name = name; Species = species; Age = age; Sex = sex; Gender = gender; Appearance = appearance; Clothing = clothing; Backpack = backpack; _jobPriorities = jobPriorities; PreferenceUnavailable = preferenceUnavailable; _antagPreferences = antagPreferences; } /// Copy constructor but with overridable references (to prevent useless copies) private HumanoidCharacterProfile( HumanoidCharacterProfile other, Dictionary jobPriorities, List antagPreferences) : this(other.Name, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, jobPriorities, other.PreferenceUnavailable, antagPreferences) { } /// Copy constructor private HumanoidCharacterProfile(HumanoidCharacterProfile other) : this(other, new Dictionary(other.JobPriorities), new List(other.AntagPreferences)) { } public HumanoidCharacterProfile( string name, string species, int age, Sex sex, Gender gender, HumanoidCharacterAppearance appearance, ClothingPreference clothing, BackpackPreference backpack, IReadOnlyDictionary jobPriorities, PreferenceUnavailableMode preferenceUnavailable, IReadOnlyList antagPreferences) : this(name, species, age, sex, gender, appearance, clothing, backpack, new Dictionary(jobPriorities), preferenceUnavailable, new List(antagPreferences)) { } public static HumanoidCharacterProfile Default() { return new( "John Doe", SpeciesManager.DefaultSpecies, MinimumAge, Sex.Male, Gender.Male, HumanoidCharacterAppearance.Default(), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.SpawnAsOverflow, new List()); } public static HumanoidCharacterProfile Random() { var prototypeManager = IoCManager.Resolve(); var random = IoCManager.Resolve(); var species = random.Pick(prototypeManager .EnumeratePrototypes().Where(x => x.RoundStart).ToArray()).ID; var sex = random.Prob(0.5f) ? Sex.Male : Sex.Female; var gender = sex == Sex.Male ? Gender.Male : Gender.Female; var name = sex.GetName(species, prototypeManager, random); var age = random.Next(MinimumAge, MaximumAge); return new HumanoidCharacterProfile(name, species, age, sex, gender, HumanoidCharacterAppearance.Random(sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, new Dictionary { {SharedGameTicker.FallbackOverflowJob, JobPriority.High} }, PreferenceUnavailableMode.StayInLobby, new List()); } public string Name { get; private set; } public string Species { get; private set; } public int Age { get; private set; } public Sex Sex { get; private set; } public Gender Gender { get; private set; } public ICharacterAppearance CharacterAppearance => 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 PreferenceUnavailableMode PreferenceUnavailable { get; private set; } public HumanoidCharacterProfile WithName(string name) { return new(this) { Name = name }; } 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); } 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); } public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode) { return new(this) { PreferenceUnavailable = mode }; } public HumanoidCharacterProfile WithAntagPreferences(IEnumerable antagPreferences) { return new(this, _jobPriorities, new List(antagPreferences)); } 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); } 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; return Appearance.MemberwiseEquals(other.Appearance); } public void EnsureValid() { var age = Math.Clamp(Age, MinimumAge, MaximumAge); var sex = Sex switch { Sex.Male => Sex.Male, Sex.Female => Sex.Female, _ => Sex.Male // Invalid enum values. }; 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 = Sex.GetName(Species); } else if (Name.Length > MaxNameLength) { name = Name[..MaxNameLength]; } else { name = Name; } name = name.Trim(); if (IoCManager.Resolve().GetCVar(CCVars.RestrictedNames)) { name = Regex.Replace(name, @"[^A-Z,a-z,0-9, -]", string.Empty); } if (string.IsNullOrEmpty(name)) { name = Sex.GetName(Species); } var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species); 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 prototypeManager = IoCManager.Resolve(); 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(); Name = name; 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); } 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 ); } } }