Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/CharacterInfo.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics.CodeAnalysis;
8 using Barotrauma.IO;
9 using System.Linq;
10 using System.Xml.Linq;
11 using Barotrauma.Abilities;
12 
13 namespace Barotrauma
14 {
15  [NetworkSerialize]
16  internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct
17  {
18  [return: MaybeNull]
19  public JobVariant ToJobVariant()
20  {
21  if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; }
22  return new JobVariant(jobPrefab, Variant);
23  }
24 
25  public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant);
26  }
27 
28  [NetworkSerialize(ArrayMaxSize = byte.MaxValue)]
29  internal readonly record struct NetCharacterInfo(string NewName,
30  ImmutableArray<Identifier> Tags,
31  byte HairIndex,
32  byte BeardIndex,
33  byte MoustacheIndex,
34  byte FaceAttachmentIndex,
35  Color SkinColor,
36  Color HairColor,
37  Color FacialHairColor,
38  ImmutableArray<NetJobVariant> JobVariants) : INetSerializableStruct;
39 
40  class CharacterInfoPrefab
41  {
42  public readonly ImmutableArray<CharacterInfo.HeadPreset> Heads;
43  public readonly ImmutableDictionary<Identifier, ImmutableHashSet<Identifier>> VarTags;
44  public readonly Identifier MenuCategoryVar;
45  public readonly Identifier Pronouns;
46 
47  public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
48  {
49  if (headsElement == null)
50  {
51  throw new Exception($"No heads configured for the character \"{characterPrefab.Identifier}\". Characters with CharacterInfo must have head sprites. Please add a <Heads> element to the character's config.");
52  }
53 
54  Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray();
55  if (varsElement != null)
56  {
57  VarTags = varsElement.Elements()
58  .Select(e =>
59  (e.GetAttributeIdentifier("var", ""),
60  e.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet()))
61  .ToImmutableDictionary();
62  }
63  else
64  {
65  VarTags = new[]
66  {
67  ("GENDER".ToIdentifier(),
68  new[] { "female".ToIdentifier(), "male".ToIdentifier() }.ToImmutableHashSet())
69  }.ToImmutableDictionary();
70  }
71 
72  MenuCategoryVar = menuCategoryElement?.GetAttributeIdentifier("var", Identifier.Empty) ?? "GENDER".ToIdentifier();
73  Pronouns = pronounsElement?.GetAttributeIdentifier("vars", Identifier.Empty) ?? "GENDER".ToIdentifier();
74  }
75  public string ReplaceVars(string str, CharacterInfo.HeadPreset headPreset)
76  {
77  return ReplaceVars(str, headPreset.TagSet);
78  }
79 
80  public string ReplaceVars(string str, ImmutableHashSet<Identifier> tagSet)
81  {
82  foreach (var key in VarTags.Keys)
83  {
84  str = str.Replace($"[{key}]", tagSet.FirstOrDefault(t => VarTags[key].Contains(t)).Value, StringComparison.OrdinalIgnoreCase);
85  }
86  return str;
87  }
88  }
89 
94  partial class CharacterInfo
95  {
96  public class HeadInfo
97  {
98  public readonly CharacterInfo CharacterInfo;
99  public readonly HeadPreset Preset;
100 
101  public int HairIndex { get; set; }
102 
103  private int? hairWithHatIndex;
104 
105  public void SetHairWithHatIndex()
106  {
107  if (CharacterInfo.Hairs is null)
108  {
109  if (HairIndex == -1)
110  {
111 #if DEBUG
112  DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
113 #else
114  DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
115 #endif
116  }
117  hairWithHatIndex = HairIndex;
118  }
119  else
120  {
121  hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1;
122  if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count)
123  {
124  hairWithHatIndex = HairIndex;
125  }
126  }
127  }
128 
129  public int BeardIndex;
130  public int MoustacheIndex;
132 
133  public Color HairColor;
134  public Color FacialHairColor;
135  public Color SkinColor;
136 
137  public Vector2 SheetIndex => Preset.SheetIndex;
138 
140  {
141  get
142  {
143  if (CharacterInfo.Hairs == null) { return null; }
144  if (HairIndex >= CharacterInfo.Hairs.Count)
145  {
146  DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
147  }
148  return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex);
149  }
150  }
152  {
153  get
154  {
155  if (hairWithHatIndex == null)
156  {
158  }
159  if (CharacterInfo.Hairs == null) { return null; }
160  if (hairWithHatIndex >= CharacterInfo.Hairs.Count)
161  {
162  DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
163  }
164  return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value);
165  }
166  }
168  {
169  get
170  {
171  if (CharacterInfo.Beards == null) { return null; }
172  if (BeardIndex >= CharacterInfo.Beards.Count)
173  {
174  DebugConsole.AddWarning($"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})");
175  }
176  return CharacterInfo.Beards.ElementAtOrDefault(BeardIndex);
177  }
178  }
180  {
181  get
182  {
183  if (CharacterInfo.Moustaches == null) { return null; }
185  {
186  DebugConsole.AddWarning($"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})");
187  }
188  return CharacterInfo.Moustaches.ElementAtOrDefault(MoustacheIndex);
189  }
190  }
192  {
193  get
194  {
195  if (CharacterInfo.FaceAttachments == null) { return null; }
197  {
198  DebugConsole.AddWarning($"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})");
199  }
200  return CharacterInfo.FaceAttachments.ElementAtOrDefault(FaceAttachmentIndex);
201  }
202  }
203 
204  public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0)
205  {
206  CharacterInfo = characterInfo;
207  Preset = headPreset;
208  HairIndex = hairIndex;
209  BeardIndex = beardIndex;
210  MoustacheIndex = moustacheIndex;
211  FaceAttachmentIndex = faceAttachmentIndex;
212  }
213 
215  {
216  HairIndex = -1;
217  BeardIndex = -1;
218  MoustacheIndex = -1;
219  FaceAttachmentIndex = -1;
220  }
221  }
222 
223  private HeadInfo head;
224  public HeadInfo Head
225  {
226  get { return head; }
227  set
228  {
229  if (head != value && value != null)
230  {
231  head = value;
232  HeadSprite = null;
233  AttachmentSprites = null;
234  hairs = null;
235  beards = null;
236  moustaches = null;
237  faceAttachments = null;
238  }
239  }
240  }
241 
242  private readonly Identifier maleIdentifier = "Male".ToIdentifier();
243  private readonly Identifier femaleIdentifier = "Female".ToIdentifier();
244 
245  public bool IsMale { get { return head?.Preset?.TagSet?.Contains(maleIdentifier) ?? false; } }
246  public bool IsFemale { get { return head?.Preset?.TagSet?.Contains(femaleIdentifier) ?? false; } }
247 
248  public CharacterInfoPrefab Prefab => CharacterPrefab.Prefabs[SpeciesName].CharacterInfoPrefab;
250  {
251  private readonly CharacterInfoPrefab characterInfoPrefab;
252  public Identifier MenuCategory => TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t));
253 
254  public ImmutableHashSet<Identifier> TagSet { get; private set; }
255 
256  [Serialize("", IsPropertySaveable.No)]
257  public string Tags
258  {
259  get { return string.Join(",", TagSet); }
260  private set
261  {
262  TagSet = value.Split(",")
263  .Select(s => s.ToIdentifier())
264  .Where(id => !id.IsEmpty)
265  .ToImmutableHashSet();
266  }
267  }
268 
269  [Serialize("0,0", IsPropertySaveable.No)]
270  public Vector2 SheetIndex { get; private set; }
271 
272  public string Name => $"Head Preset {Tags}";
273 
274  public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
275 
276  public HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element)
277  {
278  characterInfoPrefab = charInfoPrefab;
280  DetermineTagsFromLegacyFormat(element);
281  }
282 
283  private void DetermineTagsFromLegacyFormat(XElement element)
284  {
285  void addTag(string tag)
286  => TagSet = TagSet.Add(tag.ToIdentifier());
287 
288  string headId = element.GetAttributeString("id", "");
289  string gender = element.GetAttributeString("gender", "");
290  string race = element.GetAttributeString("race", "");
291  if (!headId.IsNullOrEmpty()) { addTag($"head{headId}"); }
292  if (!gender.IsNullOrEmpty()) { addTag(gender); }
293  if (!race.IsNullOrEmpty()) { addTag(race); }
294  }
295  }
296 
297  public XElement InventoryData;
298  public XElement HealthData;
299  public XElement OrderData;
300 
301  public bool PermanentlyDead;
302  public bool RenamingEnabled = false;
303 
304  private static ushort idCounter = 1;
305  private const string disguiseName = "???";
306 
307  public bool HasNickname => Name != OriginalName;
308  public string OriginalName { get; private set; }
309 
310  public string Name;
311 
313 
314  public (Identifier NpcSetIdentifier, Identifier NpcIdentifier) HumanPrefabIds;
315 
316  public string DisplayName
317  {
318  get
319  {
320  if (Character == null || !Character.HideFace)
321  {
323  return Name;
324  }
325  else if ((GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises))
326  {
328  return Name;
329  }
330 
331  if (Character.Inventory != null)
332  {
333  //Disguise as the ID card name if it's equipped
335  return idCard?.GetComponent<IdCard>()?.OwnerName ?? disguiseName;
336  }
337  return disguiseName;
338  }
339  }
340 
341  public Identifier SpeciesName { get; }
342 
347 
348  public Job Job;
349 
350  public int Salary;
351 
352  public int ExperiencePoints { get; private set; }
353 
354  public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
355 
356  public (Identifier factionId, float reputation) MinReputationToHire;
357 
361  public IEnumerable<Identifier> GetUnlockedTalentsInTree()
362  {
363  if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
364 
365  return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t));
366  }
367 
371  public IEnumerable<Identifier> GetUnlockedTalentsOutsideTree()
372  {
373  if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
374  return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t));
375  }
376 
377  public const int MaxAdditionalTalentPoints = 100;
378 
379  private int additionalTalentPoints;
381  {
382  get { return additionalTalentPoints; }
383  set { additionalTalentPoints = MathHelper.Clamp(value, 0, MaxAdditionalTalentPoints); }
384  }
385 
386  private Sprite _headSprite;
388  {
389  get
390  {
391  if (_headSprite == null)
392  {
393  LoadHeadSprite();
394  }
395 #if CLIENT
396  if (_headSprite != null)
397  {
398  CalculateHeadPosition(_headSprite);
399  }
400 #endif
401  return _headSprite;
402  }
403  private set
404  {
405  if (_headSprite != null)
406  {
407  _headSprite.Remove();
408  }
409  _headSprite = value;
410  }
411  }
412 
416  public bool OmitJobInMenus;
417 
418  private Sprite portrait;
420  {
421  get
422  {
423  if (portrait == null)
424  {
425  LoadHeadSprite();
426  }
427  return portrait;
428  }
429  private set
430  {
431  if (portrait != null)
432  {
433  portrait.Remove();
434  }
435  portrait = value;
436  }
437  }
438 
439  public bool IsDisguised = false;
440  public bool IsDisguisedAsAnother = false;
441 
442  public void CheckDisguiseStatus(bool handleBuff, IdCard idCard = null)
443  {
444  if (Character == null) { return; }
445 
446  string currentlyDisplayedName = DisplayName;
447 
448  IsDisguised = currentlyDisplayedName == disguiseName;
449  IsDisguisedAsAnother = !IsDisguised && currentlyDisplayedName != Name;
450 
452  {
453  if (handleBuff)
454  {
455  if (AfflictionPrefab.Prefabs.TryGet("disguised", out AfflictionPrefab afflictionPrefab))
456  {
457  Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(100f));
458  }
459  }
460 
461  idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
462  if (idCard != null)
463  {
464 #if CLIENT
465  GetDisguisedSprites(idCard);
466 #endif
467  return;
468  }
469  }
470 
471 #if CLIENT
472  disguisedJobIcon = null;
473  disguisedPortrait = null;
474 #endif
475 
476  if (handleBuff)
477  {
478  Character.CharacterHealth.ReduceAfflictionOnAllLimbs("disguised".ToIdentifier(), 100f);
479  }
480  }
481 
482  private List<WearableSprite> attachmentSprites;
483  public List<WearableSprite> AttachmentSprites
484  {
485  get
486  {
487  if (attachmentSprites == null)
488  {
489  LoadAttachmentSprites();
490  }
491  return attachmentSprites;
492  }
493  private set
494  {
495  if (attachmentSprites != null)
496  {
497  attachmentSprites.ForEach(s => s.Sprite?.Remove());
498  }
499  attachmentSprites = value;
500  }
501  }
502 
504 
505  public bool StartItemsGiven;
506 
510  public bool IsNewHire;
511 
513 
515 
516  public NPCPersonalityTrait PersonalityTrait { get; private set; }
517 
518  public const int MaxCurrentOrders = 3;
520 
521  public int GetManualOrderPriority(Order order)
522  {
523  if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any())
524  {
525  int orderPriority = HighestManualOrderPriority;
526  for (int i = 0; i < CurrentOrders.Count; i++)
527  {
528  if (order.AssignmentPriority >= CurrentOrders[i].AssignmentPriority)
529  {
530  break;
531  }
532  else
533  {
534  orderPriority--;
535  }
536  }
537  return Math.Max(orderPriority, 1);
538  }
539  else
540  {
542  }
543  }
544 
545  public List<Order> CurrentOrders { get; } = new List<Order>();
546 
547 
552  public ushort ID;
553 
554  public List<Identifier> SpriteTags
555  {
556  get;
557  private set;
558  }
559 
560  public readonly bool HasSpecifierTags;
561 
562  private RagdollParams ragdoll;
564  {
565  get
566  {
567  if (ragdoll == null)
568  {
569  Identifier speciesName = SpeciesName;
570  bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName);
571  ragdoll = isHumanoid
574  }
575  return ragdoll;
576  }
577  set { ragdoll = value; }
578  }
579 
581 
582  public IEnumerable<ContentXElement> GetValidAttachmentElements(IEnumerable<ContentXElement> elements, HeadPreset headPreset, WearableType? wearableType = null)
583  => FilterElements(elements, headPreset.TagSet, wearableType);
584 
585  public int CountValidAttachmentsOfType(WearableType wearableType)
586  => GetValidAttachmentElements(Wearables, Head.Preset, wearableType).Count();
587 
588  public readonly ImmutableArray<(Color Color, float Commonness)> HairColors;
589  public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors;
590  public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors;
591 
592  private void GetName(Rand.RandSync randSync, out string name)
593  {
595  ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty;
596  XElement namesXml = null;
597  if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined
598  {
599  XDocument doc = XMLExtensions.TryLoadXml(namesXmlFile);
600  namesXml = doc.Root;
601  }
602  else //the legacy firstnames.txt/lastnames.txt shit is defined
603  {
604  namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]"));
605  string firstNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
606  string lastNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
607  if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
608  {
609  var firstNames = File.ReadAllLines(firstNamesPath);
610  var lastNames = File.ReadAllLines(lastNamesPath);
611  namesXml.Add(firstNames.Select(n => new XElement("firstname", new XAttribute("value", n))));
612  namesXml.Add(lastNames.Select(n => new XElement("lastname", new XAttribute("value", n))));
613  }
614  else //the files don't exist, just fall back to the vanilla names
615  {
616  XDocument doc = XMLExtensions.TryLoadXml("Content/Characters/Human/names.xml");
617  namesXml = doc.Root;
618  }
619  }
620  name = namesXml.GetAttributeString("format", "");
621  Dictionary<Identifier, List<string>> entries = new Dictionary<Identifier, List<string>>();
622  foreach (var subElement in namesXml.Elements())
623  {
624  Identifier elemName = subElement.NameAsIdentifier();
625  if (!entries.ContainsKey(elemName))
626  {
627  entries.Add(elemName, new List<string>());
628  }
629  ImmutableHashSet<Identifier> identifiers = subElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
630  if (identifiers.IsSubsetOf(Head.Preset.TagSet))
631  {
632  entries[elemName].Add(subElement.GetAttributeString("value", ""));
633  }
634  }
635 
636  foreach (var k in entries.Keys)
637  {
638  name = name.Replace($"[{k}]", entries[k].GetRandom(randSync), StringComparison.OrdinalIgnoreCase);
639  }
640  }
641 
642  private static void LoadTagsBackwardsCompatibility(XElement element, HashSet<Identifier> tags)
643  {
644  //we need this to be able to load save files from
645  //older versions with the shittier hardcoded character
646  //info implementation
647  Identifier gender = element.GetAttributeIdentifier("gender", "");
648  int headSpriteId = element.GetAttributeInt("headspriteid", -1);
649  if (!gender.IsEmpty) { tags.Add(gender); }
650  if (headSpriteId > 0) { tags.Add($"head{headSpriteId}".ToIdentifier()); }
651  }
652 
653  // talent-relevant values
655 
656  private static bool ElementHasSpecifierTags(XElement element)
657  => element.GetAttributeBool("specifiertags",
658  element.GetAttributeBool("genders",
659  element.GetAttributeBool("races", false)));
660 
668  public Option<int> LastRewardDistribution = Option.None;
669 
670  // Used for creating the data
672  Identifier speciesName,
673  string name = "",
674  string originalName = "",
675  Either<Job, JobPrefab> jobOrJobPrefab = null,
676  int variant = 0,
677  Rand.RandSync randSync = Rand.RandSync.Unsynced,
678  Identifier npcIdentifier = default)
679  {
680  JobPrefab jobPrefab = null;
681  Job job = null;
682  if (jobOrJobPrefab != null)
683  {
684  jobOrJobPrefab.TryGet(out job);
685  jobOrJobPrefab.TryGet(out jobPrefab);
686  }
687  ID = idCounter;
688  idCounter++;
689  if (idCounter == 0) { idCounter++; }
690  SpeciesName = speciesName;
691  SpriteTags = new List<Identifier>();
693  if (CharacterConfigElement == null) { return; }
694  // TODO: support for variants
695  HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement);
696  if (HasSpecifierTags)
697  {
698  HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
699  FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
700  SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
701 
702  var headPreset = Prefab?.Heads.GetRandom(randSync);
703  if (headPreset == null)
704  {
705  DebugConsole.ThrowError("Failed to find a head preset!");
706  }
707  Head = new HeadInfo(this, headPreset);
708  SetAttachments(randSync);
709  SetColors(randSync);
710 
711  Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant));
712 
713  if (!string.IsNullOrEmpty(name))
714  {
715  Name = name;
716  }
717  else
718  {
719  Name = GetRandomName(randSync);
720  }
721  TryLoadNameAndTitle(npcIdentifier);
722  SetPersonalityTrait();
723 
725  }
726  OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
727 
728  int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
729  if (loadedLastRewardDistribution >= 0)
730  {
731  LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
732  }
733  }
734 
735  private void SetPersonalityTrait()
736  => PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet.OrderBy(tag => tag)));
737 
738  public string GetRandomName(Rand.RandSync randSync)
739  {
740  GetName(randSync, out string name);
741 
742  return name;
743  }
744 
745  public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync)
746  => ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync)
747  .Color;
748 
749  private void SetAttachments(Rand.RandSync randSync)
750  {
752 
753  int pickRandomIndex(IReadOnlyList<ContentXElement> list)
754  {
755  var elems = GetValidAttachmentElements(list, Head.Preset).ToArray();
756  var weights = GetWeights(elems).ToArray();
757  return list.IndexOf(ToolBox.SelectWeightedRandom(elems, weights, randSync));
758  }
759 
760  Head.HairIndex = pickRandomIndex(Hairs);
761  Head.BeardIndex = pickRandomIndex(Beards);
762  Head.MoustacheIndex = pickRandomIndex(Moustaches);
763  Head.FaceAttachmentIndex = pickRandomIndex(FaceAttachments);
764  }
765 
766  private void SetColors(Rand.RandSync randSync)
767  {
771  }
772 
773  private bool IsColorValid(in Color clr)
774  => clr.R != 0 || clr.G != 0 || clr.B != 0;
775 
776  public void CheckColors()
777  {
778  if (!IsColorValid(Head.HairColor))
779  {
780  Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced);
781  }
782  if (!IsColorValid(Head.FacialHairColor))
783  {
784  Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced);
785  }
786  if (!IsColorValid(Head.SkinColor))
787  {
788  Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced);
789  }
790  }
791 
792  // Used for loading the data
793  public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default)
794  {
795  ID = idCounter;
796  idCounter++;
797  Name = infoElement.GetAttributeString("name", "");
798  OriginalName = infoElement.GetAttributeString("originalname", null);
799  Salary = infoElement.GetAttributeInt("salary", 1000);
800  ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
801  AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
802  HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
803  LoadTagsBackwardsCompatibility(infoElement, tags);
804  SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
805  PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
806  RenamingEnabled = infoElement.GetAttributeBool("renamingenabled", false);
807  ContentXElement element;
808  if (!SpeciesName.IsEmpty)
809  {
811  }
812  else
813  {
814  // Backwards support (human only)
815  // Actually you know what this is backwards!
816  throw new InvalidOperationException("SpeciesName not defined");
817  }
818  if (element == null) { return; }
819  // TODO: support for variants
820  CharacterConfigElement = element;
821  HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement);
822  if (HasSpecifierTags)
823  {
824  RecreateHead(
825  tags.ToImmutableHashSet(),
826  infoElement.GetAttributeInt("hairindex", -1),
827  infoElement.GetAttributeInt("beardindex", -1),
828  infoElement.GetAttributeInt("moustacheindex", -1),
829  infoElement.GetAttributeInt("faceattachmentindex", -1));
830 
831  HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
832  FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
833  SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
834 
835  //default to transparent color, it's invalid and will be replaced with a random one in CheckColors
836  Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
837  Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
838  Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
839  CheckColors();
840 
841  TryLoadNameAndTitle(npcIdentifier);
842 
843  if (string.IsNullOrEmpty(Name))
844  {
845  var nameElement = CharacterConfigElement.GetChildElement("names");
846  if (nameElement != null)
847  {
848  GetName(Rand.RandSync.ServerAndClient, out Name);
849  }
850  }
851  }
852 
853  if (string.IsNullOrEmpty(OriginalName))
854  {
855  OriginalName = Name;
856  }
857 
858  StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
859  Identifier personalityName = infoElement.GetAttributeIdentifier("personality", "");
860  if (personalityName != Identifier.Empty)
861  {
862  if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) ||
863  NPCPersonalityTrait.Traits.TryGet(personalityName.Replace(" ".ToIdentifier(), Identifier.Empty), out trait))
864  {
865  PersonalityTrait = trait;
866  }
867  else
868  {
869  DebugConsole.ThrowError($"Error in CharacterInfo \"{OriginalName}\": could not find a personality trait with the identifier \"{personalityName}\".");
870  }
871  }
872 
873  HumanPrefabIds = (
874  infoElement.GetAttributeIdentifier("npcsetid", Identifier.Empty),
875  infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
876 
877  MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
878  UnlockedTalents = new HashSet<Identifier>();
879 
880  MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f));
881 
882  foreach (var subElement in infoElement.Elements())
883  {
884  bool jobCreated = false;
885 
886  Identifier elementName = subElement.Name.ToIdentifier();
887 
888  if (elementName == "job" && !jobCreated)
889  {
890  Job = new Job(subElement);
891  jobCreated = true;
892  // there used to be a break here, but it had to be removed to make room for statvalues
893  // using the jobCreated boolean to make sure that only the first job found is created
894  }
895  else if (elementName == "savedstatvalues")
896  {
897  foreach (XElement savedStat in subElement.Elements())
898  {
899  string statTypeString = savedStat.GetAttributeString("stattype", "").ToLowerInvariant();
900  if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
901  {
902  DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" when loading character data in CharacterInfo!");
903  continue;
904  }
905 
906  float value = savedStat.GetAttributeFloat("statvalue", 0f);
907  if (value == 0f) { continue; }
908 
909  Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
910  if (statIdentifier.IsEmpty)
911  {
912  DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
913  return;
914  }
915 
916  bool removeOnDeath = savedStat.GetAttributeBool("removeondeath", true);
917  ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
918  }
919  }
920  else if (elementName == "talents")
921  {
922  Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
923 
924  foreach (XElement talentElement in subElement.Elements())
925  {
926  if (talentElement.Name.ToIdentifier() != "talent") { continue; }
927 
928  Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
929  if (talentIdentifier == Identifier.Empty) { continue; }
930 
931  if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab))
932  {
933  foreach (TalentMigration migration in prefab.Migrations)
934  {
935  migration.TryApply(version, this);
936  }
937  }
938 
939  UnlockedTalents.Add(talentIdentifier);
940  }
941  }
942  }
943 
945  }
946 
947  private void TryLoadNameAndTitle(Identifier npcIdentifier)
948  {
949  if (!npcIdentifier.IsEmpty)
950  {
951  Title = TextManager.Get("npctitle." + npcIdentifier);
952  string nameTag = "charactername." + npcIdentifier;
953  if (TextManager.ContainsTag(nameTag))
954  {
955  Name = TextManager.Get(nameTag).Value;
956  }
957  }
958  }
959 
960  private List<ContentXElement> hairs;
961  public IReadOnlyList<ContentXElement> Hairs => hairs;
962  private List<ContentXElement> beards;
963  public IReadOnlyList<ContentXElement> Beards => beards;
964  private List<ContentXElement> moustaches;
965  public IReadOnlyList<ContentXElement> Moustaches => moustaches;
966  private List<ContentXElement> faceAttachments;
967  public IReadOnlyList<ContentXElement> FaceAttachments => faceAttachments;
968 
969  private IEnumerable<ContentXElement> wearables;
970  public IEnumerable<ContentXElement> Wearables
971  {
972  get
973  {
974  if (wearables == null)
975  {
976  var attachments = CharacterConfigElement.GetChildElement("HeadAttachments");
977  if (attachments != null)
978  {
979  wearables = attachments.GetChildElements("Wearable");
980  }
981  }
982  return wearables;
983  }
984  }
985 
990  public int GetIdentifier()
991  {
992  return GetIdentifierHash(Name);
993  }
994 
1000  {
1001  return GetIdentifierHash(OriginalName);
1002  }
1003 
1004  private int GetIdentifierHash(string name)
1005  {
1006  int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s)));
1007  id ^= Head.HairIndex << 12;
1008  id ^= Head.BeardIndex << 18;
1009  id ^= Head.MoustacheIndex << 24;
1010  id ^= Head.FaceAttachmentIndex << 30;
1011  if (Job != null)
1012  {
1013  id ^= ToolBox.StringToInt(Job.Prefab.Identifier.Value);
1014  }
1015  return id;
1016  }
1017 
1018  public IEnumerable<ContentXElement> FilterElements(IEnumerable<ContentXElement> elements, ImmutableHashSet<Identifier> tags, WearableType? targetType = null)
1019  {
1020  if (elements is null) { return null; }
1021  return elements.Where(w =>
1022  {
1023  if (!(targetType is null))
1024  {
1025  if (Enum.TryParse(w.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
1026  }
1027  HashSet<Identifier> t = w.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
1028  LoadTagsBackwardsCompatibility(w, t);
1029  return t.IsSubsetOf(tags);
1030  });
1031  }
1032 
1033  public void RecreateHead(ImmutableHashSet<Identifier> tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
1034  {
1035  HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags));
1036  if (headPreset == null)
1037  {
1038  if (tags.Count == 1)
1039  {
1040  headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First()));
1041  }
1042  headPreset ??= Prefab.Heads.GetRandomUnsynced();
1043  }
1044  head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
1046  }
1047 
1048  public string ReplaceVars(string str)
1049  {
1050  return Prefab.ReplaceVars(str, Head.Preset);
1051  }
1052 
1053 #if CLIENT
1054  public void RecreateHead(MultiplayerPreferences characterSettings)
1055  {
1056  if (characterSettings.HairIndex == -1 &&
1057  characterSettings.BeardIndex == -1 &&
1058  characterSettings.MoustacheIndex == -1 &&
1059  characterSettings.FaceAttachmentIndex == -1)
1060  {
1061  //randomize if nothing is set
1062  SetAttachments(Rand.RandSync.Unsynced);
1063  characterSettings.HairIndex = Head.HairIndex;
1064  characterSettings.BeardIndex = Head.BeardIndex;
1065  characterSettings.MoustacheIndex = Head.MoustacheIndex;
1066  characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex;
1067  }
1068 
1069  RecreateHead(
1070  characterSettings.TagSet.ToImmutableHashSet(),
1071  characterSettings.HairIndex,
1072  characterSettings.BeardIndex,
1073  characterSettings.MoustacheIndex,
1074  characterSettings.FaceAttachmentIndex);
1075 
1076  Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor);
1077  Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor);
1078  Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor);
1079 
1080  Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor)
1081  {
1082  return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced);
1083  }
1084  }
1085 #endif
1086 
1087  public void RecreateHead(HeadInfo headInfo)
1088  {
1089  RecreateHead(
1090  headInfo.Preset.TagSet,
1091  headInfo.HairIndex,
1092  headInfo.BeardIndex,
1093  headInfo.MoustacheIndex,
1094  headInfo.FaceAttachmentIndex);
1095 
1096  Head.SkinColor = headInfo.SkinColor;
1097  Head.HairColor = headInfo.HairColor;
1099  CheckColors();
1100  }
1101 
1105  public void RefreshHead()
1106  {
1108  RefreshHeadSprites();
1109  }
1110 
1111  partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement);
1112 
1113  private bool spriteTagsLoaded;
1115  {
1116  if (!spriteTagsLoaded)
1117  {
1118  LoadSpriteTags();
1119  }
1120  }
1121 
1122  private void LoadHeadSprite()
1123  {
1124  LoadHeadElement(loadHeadSprite: true, loadHeadSpriteTags: true);
1125  }
1126 
1127  private void LoadSpriteTags()
1128  {
1129  LoadHeadElement(loadHeadSprite: false, loadHeadSpriteTags: true);
1130  }
1131 
1132  private void LoadHeadElement(bool loadHeadSprite, bool loadHeadSpriteTags)
1133  {
1134  if (Ragdoll?.MainElement == null) { return; }
1135  foreach (var limbElement in Ragdoll.MainElement.Elements())
1136  {
1137  if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
1138 
1139  ContentXElement spriteElement = limbElement.GetChildElement("sprite");
1140  if (spriteElement == null) { continue; }
1141 
1142  string spritePath = spriteElement.GetAttributeContentPath("texture")?.Value;
1143  if (string.IsNullOrEmpty(spritePath)) { continue; }
1144 
1145  spritePath = ReplaceVars(spritePath);
1146 
1147  string fileName = Path.GetFileNameWithoutExtension(spritePath);
1148 
1149  if (string.IsNullOrEmpty(fileName)) { continue; }
1150 
1151  //go through the files in the directory to find a matching sprite
1152  foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
1153  {
1154  if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
1155  {
1156  continue;
1157  }
1158  string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
1159  fileWithoutTags = fileWithoutTags.Split('[', ']').First();
1160  if (fileWithoutTags != fileName) { continue; }
1161 
1162  if (loadHeadSprite)
1163  {
1164  HeadSprite = new Sprite(spriteElement, "", file);
1165  Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
1166  }
1167 
1168  if (loadHeadSpriteTags)
1169  {
1170  //extract the tags out of the filename
1171  SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList();
1172  if (SpriteTags.Any())
1173  {
1174  SpriteTags.RemoveAt(SpriteTags.Count - 1);
1175  }
1176  spriteTagsLoaded = true;
1177  }
1178 
1179  break;
1180  }
1181 
1182  if (loadHeadSprite)
1183  {
1184  LoadHeadSpriteProjectSpecific(limbElement);
1185  }
1186 
1187  break;
1188  }
1189  }
1190 
1191  public void LoadHeadAttachments()
1192  {
1193  if (Wearables != null)
1194  {
1195  if (hairs == null)
1196  {
1197  float commonness = 0.1f;
1198  hairs = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Hair), WearableType.Hair, commonness);
1199  }
1200  if (beards == null)
1201  {
1202  beards = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Beard), WearableType.Beard);
1203  }
1204  if (moustaches == null)
1205  {
1206  moustaches = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Moustache), WearableType.Moustache);
1207  }
1208  if (faceAttachments == null)
1209  {
1210  faceAttachments = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.FaceAttachment), WearableType.FaceAttachment);
1211  }
1212  }
1213  }
1214 
1215  public static List<ContentXElement> AddEmpty(IEnumerable<ContentXElement> elements, WearableType type, float commonness = 1)
1216  {
1217  // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
1218  var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)).FromPackage(null);
1219  var list = new List<ContentXElement>() { emptyElement };
1220  list.AddRange(elements);
1221  return list;
1222  }
1223 
1224  public ContentXElement GetRandomElement(IEnumerable<ContentXElement> elements)
1225  {
1226  var filtered = elements.Where(IsWearableAllowed);
1227  if (filtered.Count() == 0) { return null; }
1228  var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced);
1229  return element == null || element.NameAsIdentifier() == "Empty" ? null : element;
1230  }
1231 
1232  private bool IsWearableAllowed(ContentXElement element)
1233  {
1234  string spriteName = element.GetChildElement("sprite").GetAttributeString("name", string.Empty);
1235  return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName);
1236  }
1237 
1238  private bool IsAllowed(XElement element, string spriteName)
1239  {
1240  if (element != null)
1241  {
1242  var disallowed = element.GetAttributeStringArray("disallow", Array.Empty<string>());
1243  if (disallowed.Any(s => spriteName.Contains(s)))
1244  {
1245  return false;
1246  }
1247  }
1248  return true;
1249  }
1250 
1251  public static bool IsValidIndex(int index, List<ContentXElement> list) => index >= 0 && index < list.Count;
1252 
1253  private static IEnumerable<float> GetWeights(IEnumerable<ContentXElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
1254 
1255  partial void LoadAttachmentSprites();
1256 
1257  public int CalculateSalary()
1258  {
1259  if (Name == null || Job == null) { return 0; }
1260 
1261  int salary = 0;
1262  foreach (Skill skill in Job.GetSkills())
1263  {
1264  salary += (int)(skill.Level * skill.PriceMultiplier);
1265  }
1266 
1267  return (int)(salary * Job.Prefab.PriceMultiplier);
1268  }
1269 
1274  public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
1275  {
1276  float skillLevel = Job.GetSkillLevel(skillIdentifier);
1277  // The formula is too generous on low skill levels, hence the minimum divider.
1278  float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
1279  IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
1280  }
1281 
1285  public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
1286  {
1287  if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
1288 
1289  if (Job.Prefab.Identifier == "assistant")
1290  {
1292  }
1293  increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
1294  increase = GetSkillSpecificGain(increase, skillIdentifier);
1295 
1296  float prevLevel = Job.GetSkillLevel(skillIdentifier);
1297  Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
1298 
1299  float newLevel = Job.GetSkillLevel(skillIdentifier);
1300 
1301  if ((int)newLevel > (int)prevLevel)
1302  {
1303  float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
1304  Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
1305  // assume we are getting at least 1 point in skill, since this logic only runs in such cases
1306  float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
1307  var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
1308  Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain);
1309  foreach (Character character in Character.GetFriendlyCrew(Character))
1310  {
1311  character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilitySkillGain);
1312  }
1313  }
1314 
1315  OnSkillChanged(skillIdentifier, prevLevel, newLevel);
1316  }
1317 
1318  private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
1319  {
1320  { new("helm"), StatTypes.HelmSkillGainSpeed },
1321  { new("medical"), StatTypes.WeaponsSkillGainSpeed },
1322  { new("weapons"), StatTypes.MedicalSkillGainSpeed },
1323  { new("electrical"), StatTypes.ElectricalSkillGainSpeed },
1324  { new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
1325  }.ToImmutableDictionary();
1326 
1327  private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
1328  {
1329  if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
1330  {
1331  increase *= 1f + Character.GetStatValue(statType);
1332  }
1333 
1334  return increase;
1335  }
1336 
1337  public void SetSkillLevel(Identifier skillIdentifier, float level)
1338  {
1339  if (Job == null) { return; }
1340 
1341  var skill = Job.GetSkill(skillIdentifier);
1342  if (skill == null)
1343  {
1344  Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false);
1345  OnSkillChanged(skillIdentifier, 0.0f, level);
1346  }
1347  else
1348  {
1349  float prevLevel = skill.Level;
1350  skill.Level = level;
1351  OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
1352  }
1353  }
1354 
1355  partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
1356 
1357  public void GiveExperience(int amount)
1358  {
1359  int prevAmount = ExperiencePoints;
1360 
1361  var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
1362  experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0;
1363 
1364  amount = (int)(amount * experienceGainMultiplier.Value);
1365  if (amount < 0) { return; }
1366 
1367  ExperiencePoints += amount;
1368  OnExperienceChanged(prevAmount, ExperiencePoints);
1369  }
1370 
1371  public void SetExperience(int newExperience)
1372  {
1373  if (newExperience < 0) { return; }
1374 
1375  int prevAmount = ExperiencePoints;
1376  ExperiencePoints = newExperience;
1377  OnExperienceChanged(prevAmount, ExperiencePoints);
1378  }
1379 
1380  const int BaseExperienceRequired = 450;
1381  const int AddedExperienceRequiredPerLevel = 500;
1382 
1384  {
1386  }
1387 
1389  {
1390  // hashset always has at least 1
1391  return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0);
1392  }
1393 
1395  {
1397  }
1398 
1400  {
1401  GetCurrentLevel(out int experienceRequired);
1402  return experienceRequired;
1403  }
1404 
1406  {
1407  int level = GetCurrentLevel(out int experienceRequired);
1408  return experienceRequired + ExperienceRequiredPerLevel(level);
1409  }
1410 
1414  public int GetExperienceRequiredForLevel(int level)
1415  {
1416  int currentLevel = GetCurrentLevel();
1417  if (currentLevel >= level) { return 0; }
1418  int required = 0;
1419  for (int i = 0; i < level; i++)
1420  {
1421  required += ExperienceRequiredPerLevel(i);
1422  }
1423  return required - ExperiencePoints;
1424  }
1425 
1426  public int GetCurrentLevel()
1427  {
1428  return GetCurrentLevel(out _);
1429  }
1430 
1431  private int GetCurrentLevel(out int experienceRequired)
1432  {
1433  int level = 0;
1434  experienceRequired = 0;
1435  while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints)
1436  {
1437  experienceRequired += ExperienceRequiredPerLevel(level);
1438  level++;
1439  }
1440  return level;
1441  }
1442 
1443  private static int ExperienceRequiredPerLevel(int level)
1444  {
1445  return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
1446  }
1447 
1448  partial void OnExperienceChanged(int prevAmount, int newAmount);
1449 
1450  partial void OnPermanentStatChanged(StatTypes statType);
1451 
1452  public void Rename(string newName)
1453  {
1454  if (string.IsNullOrEmpty(newName)) { return; }
1455  // Replace the name tag of any existing id cards or duffel bags
1456  foreach (var item in Item.ItemList)
1457  {
1458  if (!item.HasTag("identitycard".ToIdentifier()) && !item.HasTag("despawncontainer".ToIdentifier())) { continue; }
1459  foreach (var tag in item.Tags.Split(','))
1460  {
1461  var splitTag = tag.Split(":");
1462  if (splitTag.Length < 2) { continue; }
1463  if (splitTag[0] != "name") { continue; }
1464  if (splitTag[1] != Name) { continue; }
1465  item.ReplaceTag(tag, $"name:{newName}");
1466  var idCard = item.GetComponent<IdCard>();
1467  if (idCard != null)
1468  {
1469  idCard.OwnerName = newName;
1470  }
1471  break;
1472  }
1473  }
1474  Name = newName;
1475  }
1476 
1477  public void ResetName()
1478  {
1479  Name = OriginalName;
1480  }
1481 
1482  public XElement Save(XElement parentElement)
1483  {
1484  XElement charElement = new XElement("Character");
1485 
1486  charElement.Add(
1487  new XAttribute("name", Name),
1488  new XAttribute("originalname", OriginalName),
1489  new XAttribute("speciesname", SpeciesName),
1490  new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
1491  new XAttribute("salary", Salary),
1492  new XAttribute("experiencepoints", ExperiencePoints),
1493  new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
1494  new XAttribute("hairindex", Head.HairIndex),
1495  new XAttribute("beardindex", Head.BeardIndex),
1496  new XAttribute("moustacheindex", Head.MoustacheIndex),
1497  new XAttribute("faceattachmentindex", Head.FaceAttachmentIndex),
1498  new XAttribute("skincolor", XMLExtensions.ColorToString(Head.SkinColor)),
1499  new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
1500  new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
1501  new XAttribute("startitemsgiven", StartItemsGiven),
1502  new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
1503  new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
1504  new XAttribute("permanentlydead", PermanentlyDead),
1505  new XAttribute("renamingenabled", RenamingEnabled)
1506  );
1507 
1508  if (HumanPrefabIds != default)
1509  {
1510  charElement.Add(
1511  new XAttribute("npcsetid", HumanPrefabIds.NpcSetIdentifier),
1512  new XAttribute("npcid", HumanPrefabIds.NpcIdentifier));
1513  }
1514 
1515  charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath));
1516 
1517  if (!MinReputationToHire.factionId.IsEmpty)
1518  {
1519  charElement.Add(
1520  new XAttribute("factionId", MinReputationToHire.factionId),
1521  new XAttribute("minreputation", MinReputationToHire.reputation));
1522  }
1523 
1524  if (Character != null)
1525  {
1526  if (Character.AnimController.CurrentHull != null)
1527  {
1528  charElement.Add(new XAttribute("hull", Character.AnimController.CurrentHull.ID));
1529  }
1530  }
1531 
1532  Job.Save(charElement);
1533 
1534  XElement savedStatElement = new XElement("savedstatvalues");
1535  foreach (var statValuePair in SavedStatValues)
1536  {
1537  foreach (var savedStat in statValuePair.Value)
1538  {
1539  if (savedStat.StatValue == 0f) { continue; }
1540 
1541  savedStatElement.Add(new XElement("savedstatvalue",
1542  new XAttribute("stattype", statValuePair.Key.ToString()),
1543  new XAttribute("statidentifier", savedStat.StatIdentifier),
1544  new XAttribute("statvalue", savedStat.StatValue),
1545  new XAttribute("removeondeath", savedStat.RemoveOnDeath)
1546  ));
1547  }
1548  }
1549 
1550  XElement talentElement = new XElement("Talents");
1551  talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
1552 
1553  foreach (Identifier talentIdentifier in UnlockedTalents)
1554  {
1555  talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
1556  }
1557 
1558  charElement.Add(savedStatElement);
1559  charElement.Add(talentElement);
1560  parentElement?.Add(charElement);
1561  return charElement;
1562  }
1563 
1564  public static void SaveOrders(XElement parentElement, params Order[] orders)
1565  {
1566  if (parentElement == null || orders == null || orders.None()) { return; }
1567  // If an order is invalid, we discard the order and increase the priority of the following orders so
1568  // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
1569  // 2) the order priorities will remain sequential.
1570  int priorityIncrease = 0;
1571  var linkedSubs = GetLinkedSubmarines();
1572  foreach (var orderInfo in orders)
1573  {
1574  var order = orderInfo;
1575  if (order == null || order.Identifier == Identifier.Empty)
1576  {
1577  DebugConsole.ThrowError("Error saving an order - the order or its identifier is null");
1578  priorityIncrease++;
1579  continue;
1580  }
1581  int? linkedSubIndex = null;
1582  bool targetAvailableInNextLevel = true;
1583  if (order.TargetSpatialEntity != null)
1584  {
1585  var entitySub = order.TargetSpatialEntity.Submarine;
1586  bool isOutside = entitySub == null;
1587  bool canBeOnLinkedSub = !isOutside && Submarine.MainSub != null && entitySub != Submarine.MainSub && linkedSubs.Any();
1588  bool isOnConnectedLinkedSub = false;
1589  if (canBeOnLinkedSub)
1590  {
1591  for (int i = 0; i < linkedSubs.Count; i++)
1592  {
1593  var ls = linkedSubs[i];
1594  if (!ls.LoadSub) { continue; }
1595  if (ls.Sub != entitySub) { continue; }
1596  linkedSubIndex = i;
1597  isOnConnectedLinkedSub = Submarine.MainSub.GetConnectedSubs().Contains(entitySub);
1598  break;
1599  }
1600  }
1601  targetAvailableInNextLevel =
1602  !isOutside &&
1603  GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
1604  (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
1605  if (!targetAvailableInNextLevel)
1606  {
1607  if (!order.Prefab.CanBeGeneralized)
1608  {
1609  DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved.");
1610  priorityIncrease++;
1611  continue;
1612  }
1613  else
1614  {
1615  DebugConsole.Log($"Saving an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order will be saved as a generalized version.");
1616  }
1617  }
1618  }
1619  if (orderInfo.ManualPriority < 1)
1620  {
1621  DebugConsole.ThrowError($"Error saving an order ({order.Identifier}) - the order priority is less than 1");
1622  priorityIncrease++;
1623  continue;
1624  }
1625  var orderElement = new XElement("order",
1626  new XAttribute("id", order.Identifier),
1627  new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease),
1628  new XAttribute("targettype", (int)order.TargetType));
1629  if (orderInfo.Option != Identifier.Empty)
1630  {
1631  orderElement.Add(new XAttribute("option", orderInfo.Option));
1632  }
1633  if (order.OrderGiver != null)
1634  {
1635  orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier()));
1636  }
1637  if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
1638  {
1639  if (targetSub == Submarine.MainSub)
1640  {
1641  orderElement.Add(new XAttribute("onmainsub", true));
1642  }
1643  else if(linkedSubIndex.HasValue)
1644  {
1645  orderElement.Add(new XAttribute("linkedsubindex", linkedSubIndex));
1646  }
1647  }
1648  switch (order.TargetType)
1649  {
1650  case Order.OrderTargetType.Entity when targetAvailableInNextLevel && order.TargetEntity is Entity e:
1651  orderElement.Add(new XAttribute("targetid", (uint)e.ID));
1652  break;
1653  case Order.OrderTargetType.Position when targetAvailableInNextLevel && order.TargetSpatialEntity is OrderTarget ot:
1654  var orderTargetElement = new XElement("ordertarget");
1655  var position = ot.WorldPosition;
1656  if (ot.Hull != null)
1657  {
1658  orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID));
1659  position -= ot.Hull.WorldPosition;
1660  }
1661  orderTargetElement.Add(new XAttribute("position", XMLExtensions.Vector2ToString(position)));
1662  orderElement.Add(orderTargetElement);
1663  break;
1664  case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue:
1665  orderElement.Add(new XAttribute("structureid", s.ID));
1666  orderElement.Add(new XAttribute("wallsectionindex", order.WallSectionIndex.Value));
1667  break;
1668  }
1669  parentElement.Add(orderElement);
1670  }
1671  }
1672 
1676  public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement)
1677  {
1678  var currentOrders = new List<Order>(characterInfo.CurrentOrders);
1679  // Sort the current orders to make sure the one with the highest priority comes first
1680  currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
1681  SaveOrders(parentElement, currentOrders.ToArray());
1682  }
1683 
1687  public void SaveOrderData()
1688  {
1689  OrderData = new XElement("orders");
1690  SaveOrderData(this, OrderData);
1691  }
1692 
1693  public static void ApplyOrderData(Character character, XElement orderData)
1694  {
1695  if (character == null) { return; }
1696  var orders = LoadOrders(orderData);
1697  foreach (var order in orders)
1698  {
1699  character.SetOrder(order, isNewOrder: true, speak: false, force: true);
1700  }
1701  }
1702 
1703  public void ApplyOrderData()
1704  {
1706  }
1707 
1708  public static List<Order> LoadOrders(XElement ordersElement)
1709  {
1710  var orders = new List<Order>();
1711  if (ordersElement == null) { return orders; }
1712  // If an order is invalid, we discard the order and increase the priority of the following orders so
1713  // 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
1714  // 2) the order priorities will remain sequential.
1715  int priorityIncrease = 0;
1716  var linkedSubs = GetLinkedSubmarines();
1717  foreach (var orderElement in ordersElement.GetChildElements("order"))
1718  {
1719  Order order = null;
1720  string orderIdentifier = orderElement.GetAttributeString("id", "");
1721  if (!OrderPrefab.Prefabs.TryGet(orderIdentifier, out OrderPrefab orderPrefab))
1722  {
1723  DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
1724  priorityIncrease++;
1725  continue;
1726  }
1727  var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
1728  Character orderGiver = null;
1729  if (orderElement.GetAttribute("ordergiver") is XAttribute orderGiverIdAttribute)
1730  {
1731  int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0);
1732  orderGiver = Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId);
1733  }
1734  Entity targetEntity = null;
1735  switch (targetType)
1736  {
1737  case Order.OrderTargetType.Entity:
1738  ushort targetId = (ushort)orderElement.GetAttributeUInt("targetid", Entity.NullEntityID);
1739  if (!GetTargetEntity(targetId, out targetEntity)) { continue; }
1740  var targetComponent = orderPrefab.GetTargetItemComponent(targetEntity as Item);
1741  order = new Order(orderPrefab, targetEntity, targetComponent, orderGiver: orderGiver);
1742  break;
1743  case Order.OrderTargetType.Position:
1744  var orderTargetElement = orderElement.GetChildElement("ordertarget");
1745  var position = orderTargetElement.GetAttributeVector2("position", Vector2.Zero);
1746  ushort hullId = (ushort)orderTargetElement.GetAttributeUInt("hullid", 0);
1747  if (!GetTargetEntity(hullId, out targetEntity)) { continue; }
1748  if (!(targetEntity is Hull targetPositionHull))
1749  {
1750  DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull");
1751  priorityIncrease++;
1752  continue;
1753  }
1754  var orderTarget = new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull);
1755  order = new Order(orderPrefab, orderTarget, orderGiver: orderGiver);
1756  break;
1757  case Order.OrderTargetType.WallSection:
1758  ushort structureId = (ushort)orderElement.GetAttributeInt("structureid", Entity.NullEntityID);
1759  if (!GetTargetEntity(structureId, out targetEntity)) { continue; }
1760  int wallSectionIndex = orderElement.GetAttributeInt("wallsectionindex", 0);
1761  if (!(targetEntity is Structure targetStructure))
1762  {
1763  DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure");
1764  priorityIncrease++;
1765  continue;
1766  }
1767  order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver);
1768  break;
1769  }
1770  Identifier orderOption = orderElement.GetAttributeIdentifier("option", "");
1771  int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease;
1772  var orderInfo = order.WithOption(orderOption).WithManualPriority(manualPriority);
1773  orders.Add(orderInfo);
1774 
1775  bool GetTargetEntity(ushort targetId, out Entity targetEntity)
1776  {
1777  targetEntity = null;
1778  if (targetId == Entity.NullEntityID) { return true; }
1779  Submarine parentSub = null;
1780  if (orderElement.GetAttributeBool("onmainsub", false))
1781  {
1782  parentSub = Submarine.MainSub;
1783  }
1784  else
1785  {
1786  int linkedSubIndex = orderElement.GetAttributeInt("linkedsubindex", -1);
1787  if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count &&
1788  linkedSubs[linkedSubIndex] is LinkedSubmarine linkedSub && linkedSub.LoadSub)
1789  {
1790  parentSub = linkedSub.Sub;
1791  }
1792  }
1793  if (parentSub != null)
1794  {
1795  targetId = GetOffsetId(parentSub, targetId);
1796  targetEntity = Entity.FindEntityByID(targetId);
1797  return targetEntity != null;
1798  }
1799  else
1800  {
1801  if (!orderPrefab.CanBeGeneralized)
1802  {
1803  DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order requires a target so it can't be loaded at all.");
1804  priorityIncrease++;
1805  return false;
1806  }
1807  else
1808  {
1809  DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead.");
1810  }
1811  return true;
1812  }
1813  }
1814  }
1815  return orders;
1816  }
1817 
1818  private static List<LinkedSubmarine> GetLinkedSubmarines()
1819  {
1820  return Entity.GetEntities()
1821  .OfType<LinkedSubmarine>()
1822  .Where(ls => ls.Submarine == Submarine.MainSub)
1823  .OrderBy(e => e.ID)
1824  .ToList();
1825  }
1826 
1827  private static ushort GetOffsetId(Submarine parentSub, ushort id)
1828  {
1829  if (parentSub != null)
1830  {
1831  var idRemap = new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset);
1832  return idRemap.GetOffsetId(id);
1833  }
1834  return id;
1835  }
1836 
1837  public static void ApplyHealthData(Character character, XElement healthData, Func<AfflictionPrefab, bool> afflictionPredicate = null)
1838  {
1839  if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); }
1840  }
1841 
1846  {
1847  ResetLoadedAttachments();
1849  }
1850 
1851  private void ResetAttachmentIndices()
1852  {
1854  }
1855 
1856  private void ResetLoadedAttachments()
1857  {
1858  hairs = null;
1859  beards = null;
1860  moustaches = null;
1861  faceAttachments = null;
1862  }
1863 
1864  public void ClearCurrentOrders()
1865  {
1866  CurrentOrders.Clear();
1867  }
1868 
1869  public void Remove()
1870  {
1871  Character = null;
1872  HeadSprite = null;
1873  Portrait = null;
1874  AttachmentSprites = null;
1875  }
1876 
1877  private void RefreshHeadSprites()
1878  {
1879  _headSprite = null;
1880  LoadHeadSprite();
1881 #if CLIENT
1882  CalculateHeadPosition(_headSprite);
1883 #endif
1884  attachmentSprites?.Clear();
1885  LoadAttachmentSprites();
1886  }
1887 
1888  // This could maybe be a LookUp instead?
1889  public readonly Dictionary<StatTypes, List<SavedStatValue>> SavedStatValues = new Dictionary<StatTypes, List<SavedStatValue>>();
1890 
1891  public void ClearSavedStatValues()
1892  {
1893  foreach (StatTypes statType in SavedStatValues.Keys)
1894  {
1895  OnPermanentStatChanged(statType);
1896  }
1897  SavedStatValues.Clear();
1898  }
1899 
1900  public void ClearSavedStatValues(StatTypes statType)
1901  {
1902  SavedStatValues.Remove(statType);
1903  OnPermanentStatChanged(statType);
1904  }
1905 
1907  {
1908  foreach (StatTypes statType in SavedStatValues.Keys)
1909  {
1910  foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
1911  {
1912  if (!savedStatValue.RemoveOnDeath) { continue; }
1913  if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
1914  savedStatValue.StatValue = 0.0f;
1915  // no need to make a network update, as this is only done after the character has died
1916  }
1917  }
1918  }
1919 
1920  public void ResetSavedStatValue(Identifier statIdentifier)
1921  {
1922  foreach (StatTypes statType in SavedStatValues.Keys)
1923  {
1924  bool changed = false;
1925  foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
1926  {
1927  if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
1928 
1929  if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
1930  savedStatValue.StatValue = 0.0f;
1931  changed = true;
1932  }
1933  if (changed) { OnPermanentStatChanged(statType); }
1934  }
1935 
1936  static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
1937  {
1938  if (statIdentifier == identifier) { return true; }
1939 
1940  if (identifier.IndexOf('*') is var index and > -1)
1941  {
1942  return statIdentifier.StartsWith(identifier[0..index]);
1943  }
1944 
1945  return false;
1946  }
1947  }
1948 
1949  public float GetSavedStatValue(StatTypes statType)
1950  {
1951  if (SavedStatValues.TryGetValue(statType, out var statValues))
1952  {
1953  return statValues.Sum(v => v.StatValue);
1954  }
1955  else
1956  {
1957  return 0f;
1958  }
1959  }
1960  public float GetSavedStatValue(StatTypes statType, Identifier statIdentifier)
1961  {
1962  if (SavedStatValues.TryGetValue(statType, out var statValues))
1963  {
1964  return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue);
1965  }
1966  else
1967  {
1968  return 0f;
1969  }
1970  }
1971 
1982  public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
1983  => GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
1984  GetSavedStatValue(statType, statIdentifier);
1985 
1986  public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
1988 
1989  public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
1990  {
1991  float statValue = GetSavedStatValue(statType, statIdentifier);
1992 
1993  if (GameMain.NetworkMember is null) { return statValue; }
1994 
1995  foreach (Character bot in bots)
1996  {
1997  int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
1998  statValue = Math.Max(statValue, botStatValue);
1999  }
2000 
2001  return statValue;
2002  }
2003 
2004  public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
2005  {
2006  if (!SavedStatValues.ContainsKey(statType))
2007  {
2008  SavedStatValues.Add(statType, new List<SavedStatValue>());
2009  }
2010 
2011  bool changed = false;
2012  if (SavedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat)
2013  {
2014  float prevValue = savedStat.StatValue;
2015  savedStat.StatValue = setValue ? value : MathHelper.Min(savedStat.StatValue + value, maxValue);
2016  changed = !MathUtils.NearlyEqual(savedStat.StatValue, prevValue);
2017  }
2018  else
2019  {
2020  SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath));
2021  changed = true;
2022  }
2023  if (changed) { OnPermanentStatChanged(statType); }
2024  }
2025 
2040  }
2041 
2042  internal sealed class SavedStatValue
2043  {
2044  public Identifier StatIdentifier { get; set; }
2045  public float StatValue { get; set; }
2046  public bool RemoveOnDeath { get; set; }
2047 
2048  public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
2049  {
2050  StatValue = value;
2051  RemoveOnDeath = removeOnDeath;
2052  StatIdentifier = statIdentifier;
2053  }
2054  }
2055 
2056  internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
2057  {
2058  public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
2059  {
2060  Value = skillAmount;
2061  SkillIdentifier = skillIdentifier;
2062  Character = character;
2063  GainedFromAbility = gainedFromAbility;
2064  }
2065  public Character Character { get; set; }
2066  public float Value { get; set; }
2067  public Identifier SkillIdentifier { get; set; }
2068  public bool GainedFromAbility { get; }
2069  }
2070 
2072  {
2073  public AbilityExperienceGainMultiplier(float experienceGainMultiplier)
2074  {
2075  Value = experienceGainMultiplier;
2076  }
2077  public float Value { get; set; }
2078  }
2079 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
static readonly PrefabCollection< AfflictionPrefab > Prefabs
void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking=true, bool ignoreUnkillability=false)
void Load(XElement element, Func< AfflictionPrefab, bool > afflictionPredicate=null)
void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
static IEnumerable< Character > GetFriendlyCrew(Character character)
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
float GetStatValue(StatTypes statType, bool includeSaved=true)
HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex=0, int beardIndex=0, int moustacheIndex=0, int faceAttachmentIndex=0)
Dictionary< Identifier, SerializableProperty > SerializableProperties
HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element)
Stores information about the Character that is needed between rounds in the menu etc....
IEnumerable< Identifier > GetUnlockedTalentsOutsideTree()
Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e....
Option< int > LastRewardDistribution
Keeps track of the last reward distribution that was set on the character's wallet....
void RefreshHead()
Reloads the head sprite and the attachment sprites.
IEnumerable< ContentXElement > FilterElements(IEnumerable< ContentXElement > elements, ImmutableHashSet< Identifier > tags, WearableType? targetType=null)
int GetExperienceRequiredForLevel(int level)
How much more experience does the character need to reach the specified level?
static void SaveOrders(XElement parentElement, params Order[] orders)
int GetIdentifier()
Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name,...
void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility=false, float maxGain=2f)
Increases the characters skill at a rate proportional to their current skill. If you want to increase...
static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync)
static void ApplyHealthData(Character character, XElement healthData, Func< AfflictionPrefab, bool > afflictionPredicate=null)
readonly ImmutableArray<(Color Color, float Commonness)> SkinColors
void RecreateHead(MultiplayerPreferences characterSettings)
CharacterInfo(Identifier speciesName, string name="", string originalName="", Either< Job, JobPrefab > jobOrJobPrefab=null, int variant=0, Rand.RandSync randSync=Rand.RandSync.Unsynced, Identifier npcIdentifier=default)
bool IsNewHire
Newly hired bot that hasn't spawned yet
readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors
CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier=default)
ContentXElement GetRandomElement(IEnumerable< ContentXElement > elements)
static List< ContentXElement > AddEmpty(IEnumerable< ContentXElement > elements, WearableType type, float commonness=1)
void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue=float.MaxValue, bool setValue=false)
void RecreateHead(ImmutableHashSet< Identifier > tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
void ReloadHeadAttachments()
Reloads the attachment xml elements according to the indices. Doesn't reload the sprites.
float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement)
Save current orders to the parameter element
readonly ImmutableArray<(Color Color, float Commonness)> HairColors
void SetSkillLevel(Identifier skillIdentifier, float level)
int GetIdentifierUsingOriginalName()
Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName,...
void CheckDisguiseStatus(bool handleBuff, IdCard idCard=null)
float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
Get the combined stat value of the identifier "all" and the specified identifier.
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
IEnumerable< Identifier > GetUnlockedTalentsInTree()
Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull the...
readonly Dictionary< StatTypes, List< SavedStatValue > > SavedStatValues
void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility=false)
Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
IEnumerable< ContentXElement > GetValidAttachmentElements(IEnumerable< ContentXElement > elements, HeadPreset headPreset, WearableType? wearableType=null)
bool OmitJobInMenus
Can be used to disable displaying the job in any info panels
float LastResistanceMultiplierSkillLossRespawn
Used to store the last known resistance against skill loss on respawn when the character dies,...
float LastResistanceMultiplierSkillLossDeath
Used to store the last known resistance against skill loss on death when the character dies,...
int CountValidAttachmentsOfType(WearableType wearableType)
float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection< Character > bots)
static List< Order > LoadOrders(XElement ordersElement)
float GetSavedStatValue(StatTypes statType, Identifier statIdentifier)
static void ApplyOrderData(Character character, XElement orderData)
static bool IsValidIndex(int index, List< ContentXElement > list)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
static readonly PrefabCollection< CharacterPrefab > Prefabs
static readonly Identifier HumanSpeciesName
static readonly ContentPath Empty
Definition: ContentPath.cs:12
string???????????? Value
Definition: ContentPath.cs:27
string? GetAttributeString(string key, string? def)
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
Color GetAttributeColor(string key, in Color def)
float GetAttributeFloat(string key, float def)
ContentPackage? ContentPackage
IEnumerable< ContentXElement > GetChildElements(string name)
Identifier NameAsIdentifier()
IEnumerable< ContentXElement > Elements()
ContentPath? GetAttributeContentPath(string key)
ContentXElement? GetChildElement(string name)
bool GetAttributeBool(string key, bool def)
int GetAttributeInt(string key, int def)
Identifier GetAttributeIdentifier(string key, string def)
virtual ContentXElement? MainElement
static IReadOnlyCollection< Entity > GetEntities()
Definition: Entity.cs:24
const ushort NullEntityID
Definition: Entity.cs:14
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static GameSession?? GameSession
Definition: GameMain.cs:88
static readonly Version Version
Definition: GameMain.cs:46
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static ImmutableHashSet< Character > GetSessionCrewCharacters(CharacterType type)
Returns a list of crew characters currently in the game with a given filter.
static readonly List< Item > ItemList
IEnumerable< Skill > GetSkills()
Definition: Job.cs:84
XElement Save(XElement parentElement)
Definition: Job.cs:240
Skill GetSkill(Identifier skillIdentifier)
Definition: Job.cs:96
static Job Random(Rand.RandSync randSync)
Definition: Job.cs:77
JobPrefab Prefab
Definition: Job.cs:18
float GetSkillLevel(Identifier skillIdentifier)
Definition: Job.cs:89
void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax)
Definition: Job.cs:117
readonly HashSet< Identifier > TagSet
static readonly PrefabCollection< NPCPersonalityTrait > Traits
static NPCPersonalityTrait GetRandom(string seed)
Order WithOption(Identifier option)
Definition: Order.cs:704
Order WithManualPriority(int newPriority)
Definition: Order.cs:709
int AssignmentPriority
Definition: Order.cs:565
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
readonly Identifier Identifier
Definition: Prefab.cs:34
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
readonly float PriceMultiplier
Definition: Skill.cs:41
float Level
Definition: Skill.cs:19
static SkillSettings Current
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static readonly PrefabCollection< TalentPrefab > TalentPrefabs
Definition: TalentPrefab.cs:35
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
Definition: Enums.cs:615
AbilityEffectType
Definition: Enums.cs:125
CharacterType
Definition: Enums.cs:685
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180