HumanoidCharacterProfile.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. using System.Linq;
  2. using System.Text.RegularExpressions;
  3. using Content.Shared.CCVar;
  4. using Content.Shared.GameTicking;
  5. using Content.Shared.Humanoid;
  6. using Content.Shared.Humanoid.Prototypes;
  7. using Content.Shared.Preferences.Loadouts;
  8. using Content.Shared.Roles;
  9. using Content.Shared.Traits;
  10. using Robust.Shared.Collections;
  11. using Robust.Shared.Configuration;
  12. using Robust.Shared.Enums;
  13. using Robust.Shared.Player;
  14. using Robust.Shared.Prototypes;
  15. using Robust.Shared.Random;
  16. using Robust.Shared.Serialization;
  17. using Robust.Shared.Utility;
  18. namespace Content.Shared.Preferences
  19. {
  20. /// <summary>
  21. /// Character profile. Looks immutable, but uses non-immutable semantics internally for serialization/code sanity purposes.
  22. /// </summary>
  23. [DataDefinition]
  24. [Serializable, NetSerializable]
  25. public sealed partial class HumanoidCharacterProfile : ICharacterProfile
  26. {
  27. private static readonly Regex RestrictedNameRegex = new(@"[^A-Za-z0-9 '\-]");
  28. private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
  29. public const int MaxNameLength = 32;
  30. public const int MaxLoadoutNameLength = 32;
  31. public const int MaxDescLength = 512;
  32. /// <summary>
  33. /// Job preferences for initial spawn.
  34. /// </summary>
  35. [DataField]
  36. private Dictionary<ProtoId<JobPrototype>, JobPriority> _jobPriorities = new()
  37. {
  38. {
  39. SharedGameTicker.FallbackOverflowJob, JobPriority.High
  40. }
  41. };
  42. /// <summary>
  43. /// Antags we have opted in to.
  44. /// </summary>
  45. [DataField]
  46. private HashSet<ProtoId<AntagPrototype>> _antagPreferences = new();
  47. /// <summary>
  48. /// Enabled traits.
  49. /// </summary>
  50. [DataField]
  51. private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
  52. /// <summary>
  53. /// <see cref="_loadouts"/>
  54. /// </summary>
  55. public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
  56. [DataField]
  57. private Dictionary<string, RoleLoadout> _loadouts = new();
  58. [DataField]
  59. public string Name { get; set; } = "John Doe";
  60. /// <summary>
  61. /// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
  62. /// </summary>
  63. [DataField]
  64. public string FlavorText { get; set; } = string.Empty;
  65. /// <summary>
  66. /// Associated <see cref="SpeciesPrototype"/> for this profile.
  67. /// </summary>
  68. [DataField]
  69. public ProtoId<SpeciesPrototype> Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
  70. [DataField]
  71. public int Age { get; set; } = 18;
  72. [DataField]
  73. public Sex Sex { get; private set; } = Sex.Male;
  74. [DataField]
  75. public Gender Gender { get; private set; } = Gender.Male;
  76. /// <summary>
  77. /// <see cref="Appearance"/>
  78. /// </summary>
  79. public ICharacterAppearance CharacterAppearance => Appearance;
  80. /// <summary>
  81. /// Stores markings, eye colors, etc for the profile.
  82. /// </summary>
  83. [DataField]
  84. public HumanoidCharacterAppearance Appearance { get; set; } = new();
  85. /// <summary>
  86. /// When spawning into a round what's the preferred spot to spawn.
  87. /// </summary>
  88. [DataField]
  89. public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
  90. /// <summary>
  91. /// <see cref="_jobPriorities"/>
  92. /// </summary>
  93. public IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities => _jobPriorities;
  94. /// <summary>
  95. /// <see cref="_antagPreferences"/>
  96. /// </summary>
  97. public IReadOnlySet<ProtoId<AntagPrototype>> AntagPreferences => _antagPreferences;
  98. /// <summary>
  99. /// <see cref="_traitPreferences"/>
  100. /// </summary>
  101. public IReadOnlySet<ProtoId<TraitPrototype>> TraitPreferences => _traitPreferences;
  102. /// <summary>
  103. /// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
  104. /// </summary>
  105. [DataField]
  106. public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
  107. PreferenceUnavailableMode.SpawnAsOverflow;
  108. public HumanoidCharacterProfile(
  109. string name,
  110. string flavortext,
  111. string species,
  112. int age,
  113. Sex sex,
  114. Gender gender,
  115. HumanoidCharacterAppearance appearance,
  116. SpawnPriorityPreference spawnPriority,
  117. Dictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities,
  118. PreferenceUnavailableMode preferenceUnavailable,
  119. HashSet<ProtoId<AntagPrototype>> antagPreferences,
  120. HashSet<ProtoId<TraitPrototype>> traitPreferences,
  121. Dictionary<string, RoleLoadout> loadouts)
  122. {
  123. Name = name;
  124. FlavorText = flavortext;
  125. Species = species;
  126. Age = age;
  127. Sex = sex;
  128. Gender = gender;
  129. Appearance = appearance;
  130. SpawnPriority = spawnPriority;
  131. _jobPriorities = jobPriorities;
  132. PreferenceUnavailable = preferenceUnavailable;
  133. _antagPreferences = antagPreferences;
  134. _traitPreferences = traitPreferences;
  135. _loadouts = loadouts;
  136. var hasHighPrority = false;
  137. foreach (var (key, value) in _jobPriorities)
  138. {
  139. if (value == JobPriority.Never)
  140. _jobPriorities.Remove(key);
  141. else if (value != JobPriority.High)
  142. continue;
  143. if (hasHighPrority)
  144. _jobPriorities[key] = JobPriority.Medium;
  145. hasHighPrority = true;
  146. }
  147. }
  148. /// <summary>Copy constructor</summary>
  149. public HumanoidCharacterProfile(HumanoidCharacterProfile other)
  150. : this(other.Name,
  151. other.FlavorText,
  152. other.Species,
  153. other.Age,
  154. other.Sex,
  155. other.Gender,
  156. other.Appearance.Clone(),
  157. other.SpawnPriority,
  158. new Dictionary<ProtoId<JobPrototype>, JobPriority>(other.JobPriorities),
  159. other.PreferenceUnavailable,
  160. new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
  161. new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
  162. new Dictionary<string, RoleLoadout>(other.Loadouts))
  163. {
  164. }
  165. /// <summary>
  166. /// Get the default humanoid character profile, using internal constant values.
  167. /// Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
  168. /// </summary>
  169. /// <returns></returns>
  170. public HumanoidCharacterProfile()
  171. {
  172. }
  173. /// <summary>
  174. /// Return a default character profile, based on species.
  175. /// </summary>
  176. /// <param name="species">The species to use in this default profile. The default species is <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/>.</param>
  177. /// <returns>Humanoid character profile with default settings.</returns>
  178. public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
  179. {
  180. return new()
  181. {
  182. Species = species,
  183. };
  184. }
  185. // TODO: This should eventually not be a visual change only.
  186. public static HumanoidCharacterProfile Random(HashSet<string>? ignoredSpecies = null)
  187. {
  188. var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
  189. var random = IoCManager.Resolve<IRobustRandom>();
  190. var species = random.Pick(prototypeManager
  191. .EnumeratePrototypes<SpeciesPrototype>()
  192. .Where(x => ignoredSpecies == null ? x.RoundStart : x.RoundStart && !ignoredSpecies.Contains(x.ID))
  193. .ToArray()
  194. ).ID;
  195. return RandomWithSpecies(species);
  196. }
  197. public static HumanoidCharacterProfile RandomWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
  198. {
  199. var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
  200. var random = IoCManager.Resolve<IRobustRandom>();
  201. var sex = Sex.Unsexed;
  202. var age = 18;
  203. if (prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesPrototype))
  204. {
  205. sex = random.Pick(speciesPrototype.Sexes);
  206. 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
  207. }
  208. var gender = Gender.Epicene;
  209. switch (sex)
  210. {
  211. case Sex.Male:
  212. gender = Gender.Male;
  213. break;
  214. case Sex.Female:
  215. gender = Gender.Female;
  216. break;
  217. }
  218. var name = GetName(species, gender);
  219. return new HumanoidCharacterProfile()
  220. {
  221. Name = name,
  222. Sex = sex,
  223. Age = age,
  224. Gender = gender,
  225. Species = species,
  226. Appearance = HumanoidCharacterAppearance.Random(species, sex),
  227. };
  228. }
  229. public HumanoidCharacterProfile WithName(string name)
  230. {
  231. return new(this) { Name = name };
  232. }
  233. public HumanoidCharacterProfile WithFlavorText(string flavorText)
  234. {
  235. return new(this) { FlavorText = flavorText };
  236. }
  237. public HumanoidCharacterProfile WithAge(int age)
  238. {
  239. return new(this) { Age = age };
  240. }
  241. public HumanoidCharacterProfile WithSex(Sex sex)
  242. {
  243. return new(this) { Sex = sex };
  244. }
  245. public HumanoidCharacterProfile WithGender(Gender gender)
  246. {
  247. return new(this) { Gender = gender };
  248. }
  249. public HumanoidCharacterProfile WithSpecies(string species)
  250. {
  251. return new(this) { Species = species };
  252. }
  253. public HumanoidCharacterProfile WithCharacterAppearance(HumanoidCharacterAppearance appearance)
  254. {
  255. return new(this) { Appearance = appearance };
  256. }
  257. public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority)
  258. {
  259. return new(this) { SpawnPriority = spawnPriority };
  260. }
  261. public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<ProtoId<JobPrototype>, JobPriority>> jobPriorities)
  262. {
  263. var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(jobPriorities);
  264. var hasHighPrority = false;
  265. foreach (var (key, value) in dictionary)
  266. {
  267. if (value == JobPriority.Never)
  268. dictionary.Remove(key);
  269. else if (value != JobPriority.High)
  270. continue;
  271. if (hasHighPrority)
  272. dictionary[key] = JobPriority.Medium;
  273. hasHighPrority = true;
  274. }
  275. return new(this)
  276. {
  277. _jobPriorities = dictionary
  278. };
  279. }
  280. public HumanoidCharacterProfile WithJobPriority(ProtoId<JobPrototype> jobId, JobPriority priority)
  281. {
  282. var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(_jobPriorities);
  283. if (priority == JobPriority.Never)
  284. {
  285. dictionary.Remove(jobId);
  286. }
  287. else if (priority == JobPriority.High)
  288. {
  289. // There can only ever be one high priority job.
  290. foreach (var (job, value) in dictionary)
  291. {
  292. if (value == JobPriority.High)
  293. dictionary[job] = JobPriority.Medium;
  294. }
  295. dictionary[jobId] = priority;
  296. }
  297. else
  298. {
  299. dictionary[jobId] = priority;
  300. }
  301. return new(this)
  302. {
  303. _jobPriorities = dictionary,
  304. };
  305. }
  306. public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
  307. {
  308. return new(this) { PreferenceUnavailable = mode };
  309. }
  310. public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<ProtoId<AntagPrototype>> antagPreferences)
  311. {
  312. return new(this)
  313. {
  314. _antagPreferences = new (antagPreferences),
  315. };
  316. }
  317. public HumanoidCharacterProfile WithAntagPreference(ProtoId<AntagPrototype> antagId, bool pref)
  318. {
  319. var list = new HashSet<ProtoId<AntagPrototype>>(_antagPreferences);
  320. if (pref)
  321. {
  322. list.Add(antagId);
  323. }
  324. else
  325. {
  326. list.Remove(antagId);
  327. }
  328. return new(this)
  329. {
  330. _antagPreferences = list,
  331. };
  332. }
  333. public HumanoidCharacterProfile WithTraitPreference(ProtoId<TraitPrototype> traitId, IPrototypeManager protoManager)
  334. {
  335. // null category is assumed to be default.
  336. if (!protoManager.TryIndex(traitId, out var traitProto))
  337. return new(this);
  338. var category = traitProto.Category;
  339. // Category not found so dump it.
  340. TraitCategoryPrototype? traitCategory = null;
  341. if (category != null && !protoManager.TryIndex(category, out traitCategory))
  342. return new(this);
  343. var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences) { traitId };
  344. if (traitCategory == null || traitCategory.MaxTraitPoints < 0)
  345. {
  346. return new(this)
  347. {
  348. _traitPreferences = list,
  349. };
  350. }
  351. var count = 0;
  352. foreach (var trait in list)
  353. {
  354. // If trait not found or another category don't count its points.
  355. if (!protoManager.TryIndex<TraitPrototype>(trait, out var otherProto) ||
  356. otherProto.Category != traitCategory)
  357. {
  358. continue;
  359. }
  360. count += otherProto.Cost;
  361. }
  362. if (count > traitCategory.MaxTraitPoints && traitProto.Cost != 0)
  363. {
  364. return new(this);
  365. }
  366. return new(this)
  367. {
  368. _traitPreferences = list,
  369. };
  370. }
  371. public HumanoidCharacterProfile WithoutTraitPreference(ProtoId<TraitPrototype> traitId, IPrototypeManager protoManager)
  372. {
  373. var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences);
  374. list.Remove(traitId);
  375. return new(this)
  376. {
  377. _traitPreferences = list,
  378. };
  379. }
  380. public string Summary =>
  381. Loc.GetString(
  382. "humanoid-character-profile-summary",
  383. ("name", Name),
  384. ("gender", Gender.ToString().ToLowerInvariant()),
  385. ("age", Age)
  386. );
  387. public bool MemberwiseEquals(ICharacterProfile maybeOther)
  388. {
  389. if (maybeOther is not HumanoidCharacterProfile other) return false;
  390. if (Name != other.Name) return false;
  391. if (Age != other.Age) return false;
  392. if (Sex != other.Sex) return false;
  393. if (Gender != other.Gender) return false;
  394. if (Species != other.Species) return false;
  395. if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
  396. if (SpawnPriority != other.SpawnPriority) return false;
  397. if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
  398. if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
  399. if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
  400. if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
  401. if (FlavorText != other.FlavorText) return false;
  402. return Appearance.MemberwiseEquals(other.Appearance);
  403. }
  404. public void EnsureValid(ICommonSession session, IDependencyCollection collection)
  405. {
  406. var configManager = collection.Resolve<IConfigurationManager>();
  407. var prototypeManager = collection.Resolve<IPrototypeManager>();
  408. if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
  409. {
  410. Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
  411. speciesPrototype = prototypeManager.Index(Species);
  412. }
  413. var sex = Sex switch
  414. {
  415. Sex.Male => Sex.Male,
  416. Sex.Female => Sex.Female,
  417. Sex.Unsexed => Sex.Unsexed,
  418. _ => Sex.Male // Invalid enum values.
  419. };
  420. // ensure the species can be that sex and their age fits the founds
  421. if (!speciesPrototype.Sexes.Contains(sex))
  422. sex = speciesPrototype.Sexes[0];
  423. var age = Math.Clamp(Age, speciesPrototype.MinAge, speciesPrototype.MaxAge);
  424. var gender = Gender switch
  425. {
  426. Gender.Epicene => Gender.Epicene,
  427. Gender.Female => Gender.Female,
  428. Gender.Male => Gender.Male,
  429. Gender.Neuter => Gender.Neuter,
  430. _ => Gender.Epicene // Invalid enum values.
  431. };
  432. string name;
  433. if (string.IsNullOrEmpty(Name))
  434. {
  435. name = GetName(Species, gender);
  436. }
  437. else if (Name.Length > MaxNameLength)
  438. {
  439. name = Name[..MaxNameLength];
  440. }
  441. else
  442. {
  443. name = Name;
  444. }
  445. name = name.Trim();
  446. if (configManager.GetCVar(CCVars.RestrictedNames))
  447. {
  448. name = RestrictedNameRegex.Replace(name, string.Empty);
  449. }
  450. if (configManager.GetCVar(CCVars.ICNameCase))
  451. {
  452. // This regex replaces the first character of the first and last words of the name with their uppercase version
  453. name = ICNameCaseRegex.Replace(name, m => m.Groups["word"].Value.ToUpper());
  454. }
  455. if (string.IsNullOrEmpty(name))
  456. {
  457. name = GetName(Species, gender);
  458. }
  459. string flavortext;
  460. if (FlavorText.Length > MaxDescLength)
  461. {
  462. flavortext = FormattedMessage.RemoveMarkupOrThrow(FlavorText)[..MaxDescLength];
  463. }
  464. else
  465. {
  466. flavortext = FormattedMessage.RemoveMarkupOrThrow(FlavorText);
  467. }
  468. var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex);
  469. var prefsUnavailableMode = PreferenceUnavailable switch
  470. {
  471. PreferenceUnavailableMode.StayInLobby => PreferenceUnavailableMode.StayInLobby,
  472. PreferenceUnavailableMode.SpawnAsOverflow => PreferenceUnavailableMode.SpawnAsOverflow,
  473. _ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
  474. };
  475. var spawnPriority = SpawnPriority switch
  476. {
  477. SpawnPriorityPreference.None => SpawnPriorityPreference.None,
  478. SpawnPriorityPreference.Arrivals => SpawnPriorityPreference.Arrivals,
  479. SpawnPriorityPreference.Cryosleep => SpawnPriorityPreference.Cryosleep,
  480. _ => SpawnPriorityPreference.None // Invalid enum values.
  481. };
  482. var priorities = new Dictionary<ProtoId<JobPrototype>, JobPriority>(JobPriorities
  483. .Where(p => prototypeManager.TryIndex<JobPrototype>(p.Key, out var job) && job.SetPreference && p.Value switch
  484. {
  485. JobPriority.Never => false, // Drop never since that's assumed default.
  486. JobPriority.Low => true,
  487. JobPriority.Medium => true,
  488. JobPriority.High => true,
  489. _ => false
  490. }));
  491. var hasHighPrio = false;
  492. foreach (var (key, value) in priorities)
  493. {
  494. if (value != JobPriority.High)
  495. continue;
  496. if (hasHighPrio)
  497. priorities[key] = JobPriority.Medium;
  498. hasHighPrio = true;
  499. }
  500. var antags = AntagPreferences
  501. .Where(id => prototypeManager.TryIndex(id, out var antag) && antag.SetPreference)
  502. .ToList();
  503. var traits = TraitPreferences
  504. .Where(prototypeManager.HasIndex)
  505. .ToList();
  506. Name = name;
  507. FlavorText = flavortext;
  508. Age = age;
  509. Sex = sex;
  510. Gender = gender;
  511. Appearance = appearance;
  512. SpawnPriority = spawnPriority;
  513. _jobPriorities.Clear();
  514. foreach (var (job, priority) in priorities)
  515. {
  516. _jobPriorities.Add(job, priority);
  517. }
  518. PreferenceUnavailable = prefsUnavailableMode;
  519. _antagPreferences.Clear();
  520. _antagPreferences.UnionWith(antags);
  521. _traitPreferences.Clear();
  522. _traitPreferences.UnionWith(GetValidTraits(traits, prototypeManager));
  523. // Checks prototypes exist for all loadouts and dump / set to default if not.
  524. var toRemove = new ValueList<string>();
  525. foreach (var (roleName, loadouts) in _loadouts)
  526. {
  527. if (!prototypeManager.HasIndex<RoleLoadoutPrototype>(roleName))
  528. {
  529. toRemove.Add(roleName);
  530. continue;
  531. }
  532. loadouts.EnsureValid(this, session, collection);
  533. }
  534. foreach (var value in toRemove)
  535. {
  536. _loadouts.Remove(value);
  537. }
  538. }
  539. /// <summary>
  540. /// Takes in an IEnumerable of traits and returns a List of the valid traits.
  541. /// </summary>
  542. public List<ProtoId<TraitPrototype>> GetValidTraits(IEnumerable<ProtoId<TraitPrototype>> traits, IPrototypeManager protoManager)
  543. {
  544. // Track points count for each group.
  545. var groups = new Dictionary<string, int>();
  546. var result = new List<ProtoId<TraitPrototype>>();
  547. foreach (var trait in traits)
  548. {
  549. if (!protoManager.TryIndex(trait, out var traitProto))
  550. continue;
  551. // Always valid.
  552. if (traitProto.Category == null)
  553. {
  554. result.Add(trait);
  555. continue;
  556. }
  557. // No category so dump it.
  558. if (!protoManager.TryIndex(traitProto.Category, out var category))
  559. continue;
  560. var existing = groups.GetOrNew(category.ID);
  561. existing += traitProto.Cost;
  562. // Too expensive.
  563. if (existing > category.MaxTraitPoints)
  564. continue;
  565. groups[category.ID] = existing;
  566. result.Add(trait);
  567. }
  568. return result;
  569. }
  570. public ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection)
  571. {
  572. var profile = new HumanoidCharacterProfile(this);
  573. profile.EnsureValid(session, collection);
  574. return profile;
  575. }
  576. // sorry this is kind of weird and duplicated,
  577. /// working inside these non entity systems is a bit wack
  578. public static string GetName(string species, Gender gender)
  579. {
  580. var namingSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<NamingSystem>();
  581. return namingSystem.GetName(species, gender);
  582. }
  583. public override bool Equals(object? obj)
  584. {
  585. return ReferenceEquals(this, obj) || obj is HumanoidCharacterProfile other && Equals(other);
  586. }
  587. public override int GetHashCode()
  588. {
  589. var hashCode = new HashCode();
  590. hashCode.Add(_jobPriorities);
  591. hashCode.Add(_antagPreferences);
  592. hashCode.Add(_traitPreferences);
  593. hashCode.Add(_loadouts);
  594. hashCode.Add(Name);
  595. hashCode.Add(FlavorText);
  596. hashCode.Add(Species);
  597. hashCode.Add(Age);
  598. hashCode.Add((int)Sex);
  599. hashCode.Add((int)Gender);
  600. hashCode.Add(Appearance);
  601. hashCode.Add((int)SpawnPriority);
  602. hashCode.Add((int)PreferenceUnavailable);
  603. return hashCode.ToHashCode();
  604. }
  605. public void SetLoadout(RoleLoadout loadout)
  606. {
  607. _loadouts[loadout.Role.Id] = loadout;
  608. }
  609. public HumanoidCharacterProfile WithLoadout(RoleLoadout loadout)
  610. {
  611. // Deep copies so we don't modify the DB profile.
  612. var copied = new Dictionary<string, RoleLoadout>();
  613. foreach (var proto in _loadouts)
  614. {
  615. if (proto.Key == loadout.Role)
  616. continue;
  617. copied[proto.Key] = proto.Value.Clone();
  618. }
  619. copied[loadout.Role] = loadout.Clone();
  620. var profile = Clone();
  621. profile._loadouts = copied;
  622. return profile;
  623. }
  624. public RoleLoadout GetLoadoutOrDefault(string id, ICommonSession? session, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
  625. {
  626. if (!_loadouts.TryGetValue(id, out var loadout))
  627. {
  628. loadout = new RoleLoadout(id);
  629. loadout.SetDefault(this, session, protoManager, force: true);
  630. }
  631. loadout.SetDefault(this, session, protoManager);
  632. return loadout;
  633. }
  634. public HumanoidCharacterProfile Clone()
  635. {
  636. return new HumanoidCharacterProfile(this);
  637. }
  638. }
  639. }