3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics.CodeAnalysis;
10 using System.Xml.Linq;
16 internal readonly record
struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct
19 public JobVariant ToJobVariant()
21 if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) {
return null; }
22 return new JobVariant(jobPrefab, Variant);
25 public static NetJobVariant FromJobVariant(JobVariant jobVariant) =>
new NetJobVariant(jobVariant.Prefab.Identifier, (
byte)jobVariant.Variant);
28 [NetworkSerialize(ArrayMaxSize =
byte.MaxValue)]
29 internal readonly record
struct NetCharacterInfo(string NewName,
30 ImmutableArray<Identifier> Tags,
34 byte FaceAttachmentIndex,
37 Color FacialHairColor,
38 ImmutableArray<NetJobVariant> JobVariants) : INetSerializableStruct;
40 class CharacterInfoPrefab
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;
47 public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
49 if (headsElement ==
null)
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.");
54 Heads = headsElement.Elements().Select(e =>
new CharacterInfo.HeadPreset(
this, e)).ToImmutableArray();
55 if (varsElement !=
null)
57 VarTags = varsElement.Elements()
59 (e.GetAttributeIdentifier(
"var",
""),
60 e.GetAttributeIdentifierArray(
"tags", Array.Empty<Identifier>()).ToImmutableHashSet()))
61 .ToImmutableDictionary();
67 (
"GENDER".ToIdentifier(),
68 new[] {
"female".ToIdentifier(),
"male".ToIdentifier() }.ToImmutableHashSet())
69 }.ToImmutableDictionary();
72 MenuCategoryVar = menuCategoryElement?.GetAttributeIdentifier(
"var", Identifier.Empty) ??
"GENDER".ToIdentifier();
73 Pronouns = pronounsElement?.GetAttributeIdentifier(
"vars", Identifier.Empty) ??
"GENDER".ToIdentifier();
75 public string ReplaceVars(
string str, CharacterInfo.HeadPreset headPreset)
77 return ReplaceVars(str, headPreset.TagSet);
80 public string ReplaceVars(
string str, ImmutableHashSet<Identifier> tagSet)
82 foreach (var key
in VarTags.Keys)
84 str = str.Replace($
"[{key}]", tagSet.FirstOrDefault(t => VarTags[key].Contains(t)).Value, StringComparison.OrdinalIgnoreCase);
103 private int? hairWithHatIndex;
112 DebugConsole.ThrowError(
"Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
114 DebugConsole.AddWarning(
"Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
146 DebugConsole.AddWarning($
"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
155 if (hairWithHatIndex ==
null)
162 DebugConsole.AddWarning($
"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
174 DebugConsole.AddWarning($
"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})");
186 DebugConsole.AddWarning($
"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})");
198 DebugConsole.AddWarning($
"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})");
223 private HeadInfo head;
229 if (head != value && value !=
null)
237 faceAttachments =
null;
242 private readonly Identifier maleIdentifier =
"Male".ToIdentifier();
243 private readonly Identifier femaleIdentifier =
"Female".ToIdentifier();
245 public bool IsMale {
get {
return head?.Preset?.TagSet?.Contains(maleIdentifier) ??
false; } }
246 public bool IsFemale {
get {
return head?.Preset?.TagSet?.Contains(femaleIdentifier) ??
false; } }
251 private readonly CharacterInfoPrefab characterInfoPrefab;
252 public Identifier
MenuCategory =>
TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t));
254 public ImmutableHashSet<Identifier>
TagSet {
get;
private set; }
259 get {
return string.Join(
",",
TagSet); }
263 .Select(s => s.ToIdentifier())
264 .Where(
id => !
id.IsEmpty)
265 .ToImmutableHashSet();
272 public string Name => $
"Head Preset {Tags}";
276 public HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element)
278 characterInfoPrefab = charInfoPrefab;
280 DetermineTagsFromLegacyFormat(element);
283 private void DetermineTagsFromLegacyFormat(XElement element)
285 void addTag(
string tag)
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); }
304 private static ushort idCounter = 1;
305 private const string disguiseName =
"???";
335 return idCard?.GetComponent<
IdCard>()?.OwnerName ?? disguiseName;
354 public HashSet<Identifier>
UnlockedTalents {
get;
private set; } =
new HashSet<Identifier>();
356 public (Identifier
factionId,
float reputation) MinReputationToHire;
363 if (!TalentTree.JobTalentTrees.TryGet(
Job.
Prefab.
Identifier, out TalentTree talentTree)) {
return Enumerable.Empty<Identifier>(); }
373 if (!TalentTree.JobTalentTrees.TryGet(
Job.
Prefab.
Identifier, out TalentTree talentTree)) {
return Enumerable.Empty<Identifier>(); }
379 private int additionalTalentPoints;
382 get {
return additionalTalentPoints; }
386 private Sprite _headSprite;
391 if (_headSprite ==
null)
396 if (_headSprite !=
null)
405 if (_headSprite !=
null)
423 if (portrait ==
null)
431 if (portrait !=
null)
448 IsDisguised = currentlyDisplayedName == disguiseName;
465 GetDisguisedSprites(idCard);
472 disguisedJobIcon =
null;
473 disguisedPortrait =
null;
482 private List<WearableSprite> attachmentSprites;
487 if (attachmentSprites ==
null)
489 LoadAttachmentSprites();
491 return attachmentSprites;
495 if (attachmentSprites !=
null)
497 attachmentSprites.ForEach(s => s.Sprite?.Remove());
499 attachmentSprites = value;
537 return Math.Max(orderPriority, 1);
577 set { ragdoll = value; }
588 public readonly ImmutableArray<(Color Color,
float Commonness)>
HairColors;
590 public readonly ImmutableArray<(Color Color,
float Commonness)>
SkinColors;
592 private void GetName(Rand.RandSync randSync, out
string name)
596 XElement namesXml =
null;
597 if (!namesXmlFile.IsNullOrEmpty())
599 XDocument doc = XMLExtensions.TryLoadXml(namesXmlFile);
604 namesXml =
new XElement(
"names",
new XAttribute(
"format",
"[firstname] [lastname]"));
607 if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
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))));
616 XDocument doc = XMLExtensions.TryLoadXml(
"Content/Characters/Human/names.xml");
620 name = namesXml.GetAttributeString(
"format",
"");
621 Dictionary<Identifier, List<string>> entries =
new Dictionary<Identifier, List<string>>();
622 foreach (var subElement
in namesXml.Elements())
624 Identifier elemName = subElement.NameAsIdentifier();
625 if (!entries.ContainsKey(elemName))
627 entries.Add(elemName,
new List<string>());
629 ImmutableHashSet<Identifier> identifiers = subElement.GetAttributeIdentifierArray(
"tags", Array.Empty<Identifier>()).ToImmutableHashSet();
632 entries[elemName].Add(subElement.GetAttributeString(
"value",
""));
636 foreach (var k
in entries.Keys)
638 name = name.Replace($
"[{k}]", entries[k].GetRandom(randSync), StringComparison.OrdinalIgnoreCase);
642 private static void LoadTagsBackwardsCompatibility(XElement element, HashSet<Identifier> tags)
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()); }
656 private static bool ElementHasSpecifierTags(XElement element)
657 => element.GetAttributeBool(
"specifiertags",
658 element.GetAttributeBool(
"genders",
659 element.GetAttributeBool(
"races",
false)));
672 Identifier speciesName,
674 string originalName =
"",
675 Either<Job, JobPrefab> jobOrJobPrefab =
null,
677 Rand.RandSync randSync = Rand.RandSync.Unsynced,
678 Identifier npcIdentifier =
default)
682 if (jobOrJobPrefab !=
null)
684 jobOrJobPrefab.TryGet(out job);
685 jobOrJobPrefab.TryGet(out jobPrefab);
689 if (idCounter == 0) { idCounter++; }
700 SkinColors =
CharacterConfigElement.GetAttributeTupleArray(
"skincolors",
new (Color,
float)[] { (
new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
702 var headPreset =
Prefab?.Heads.GetRandom(randSync);
703 if (headPreset ==
null)
705 DebugConsole.ThrowError(
"Failed to find a head preset!");
708 SetAttachments(randSync);
711 Job = job ?? ((jobPrefab ==
null) ?
Job.
Random(Rand.RandSync.Unsynced) :
new Job(jobPrefab, randSync, variant));
713 if (!
string.IsNullOrEmpty(name))
721 TryLoadNameAndTitle(npcIdentifier);
722 SetPersonalityTrait();
729 if (loadedLastRewardDistribution >= 0)
735 private void SetPersonalityTrait()
740 GetName(randSync, out
string name);
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)
749 private void SetAttachments(Rand.RandSync randSync)
753 int pickRandomIndex(IReadOnlyList<ContentXElement> list)
756 var weights = GetWeights(elems).ToArray();
757 return list.IndexOf(ToolBox.SelectWeightedRandom(elems, weights, randSync));
766 private void SetColors(Rand.RandSync randSync)
773 private bool IsColorValid(in Color clr)
774 => clr.R != 0 || clr.G != 0 || clr.B != 0;
803 LoadTagsBackwardsCompatibility(infoElement, tags);
816 throw new InvalidOperationException(
"SpeciesName not defined");
818 if (element ==
null) {
return; }
825 tags.ToImmutableHashSet(),
833 SkinColors =
CharacterConfigElement.GetAttributeTupleArray(
"skincolors",
new (Color,
float)[] { (
new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
841 TryLoadNameAndTitle(npcIdentifier);
843 if (
string.IsNullOrEmpty(
Name))
846 if (nameElement !=
null)
848 GetName(Rand.RandSync.ServerAndClient, out
Name);
860 if (personalityName != Identifier.Empty)
869 DebugConsole.ThrowError($
"Error in CharacterInfo \"{OriginalName}\": could not find a personality trait with the identifier \"{personalityName}\".");
882 foreach (var subElement
in infoElement.Elements())
884 bool jobCreated =
false;
886 Identifier elementName = subElement.Name.ToIdentifier();
888 if (elementName ==
"job" && !jobCreated)
890 Job =
new Job(subElement);
895 else if (elementName ==
"savedstatvalues")
897 foreach (XElement savedStat
in subElement.Elements())
899 string statTypeString = savedStat.GetAttributeString(
"stattype",
"").ToLowerInvariant();
900 if (!Enum.TryParse(statTypeString,
true, out
StatTypes statType))
902 DebugConsole.ThrowError(
"Invalid stat type type \"" + statTypeString +
"\" when loading character data in CharacterInfo!");
906 float value = savedStat.GetAttributeFloat(
"statvalue", 0f);
907 if (value == 0f) {
continue; }
909 Identifier statIdentifier = savedStat.GetAttributeIdentifier(
"statidentifier", Identifier.Empty);
910 if (statIdentifier.IsEmpty)
912 DebugConsole.ThrowError(
"Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
916 bool removeOnDeath = savedStat.GetAttributeBool(
"removeondeath",
true);
920 else if (elementName ==
"talents")
922 Version version = subElement.GetAttributeVersion(
"version",
GameMain.
Version);
924 foreach (XElement talentElement
in subElement.Elements())
926 if (talentElement.Name.ToIdentifier() !=
"talent") {
continue; }
928 Identifier talentIdentifier = talentElement.GetAttributeIdentifier(
"identifier", Identifier.Empty);
929 if (talentIdentifier == Identifier.Empty) {
continue; }
933 foreach (TalentMigration migration
in prefab.Migrations)
935 migration.TryApply(version,
this);
947 private void TryLoadNameAndTitle(Identifier npcIdentifier)
949 if (!npcIdentifier.IsEmpty)
951 Title = TextManager.Get(
"npctitle." + npcIdentifier);
952 string nameTag =
"charactername." + npcIdentifier;
953 if (TextManager.ContainsTag(nameTag))
955 Name = TextManager.Get(nameTag).Value;
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;
969 private IEnumerable<ContentXElement> wearables;
974 if (wearables ==
null)
977 if (attachments !=
null)
992 return GetIdentifierHash(
Name);
1004 private int GetIdentifierHash(
string name)
1006 int id = ToolBox.StringToInt(name +
string.Join(
"",
Head.
Preset.
TagSet.OrderBy(s => s)));
1018 public IEnumerable<ContentXElement>
FilterElements(IEnumerable<ContentXElement> elements, ImmutableHashSet<Identifier> tags,
WearableType? targetType =
null)
1020 if (elements is
null) {
return null; }
1021 return elements.Where(w =>
1023 if (!(targetType is
null))
1025 if (Enum.TryParse(w.GetAttributeString(
"type",
""),
true, out
WearableType type) && type != targetType) { return false; }
1027 HashSet<Identifier> t = w.GetAttributeIdentifierArray(
"tags", Array.Empty<Identifier>()).ToHashSet();
1028 LoadTagsBackwardsCompatibility(w, t);
1029 return t.IsSubsetOf(tags);
1033 public void RecreateHead(ImmutableHashSet<Identifier> tags,
int hairIndex,
int beardIndex,
int moustacheIndex,
int faceAttachmentIndex)
1035 HeadPreset headPreset =
Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags));
1036 if (headPreset ==
null)
1038 if (tags.Count == 1)
1040 headPreset =
Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First()));
1042 headPreset ??=
Prefab.Heads.GetRandomUnsynced();
1044 head =
new HeadInfo(
this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
1056 if (characterSettings.
HairIndex == -1 &&
1062 SetAttachments(Rand.RandSync.Unsynced);
1070 characterSettings.
TagSet.ToImmutableHashSet(),
1080 Color ChooseColor(in ImmutableArray<(Color Color,
float Commonness)> availableColors, Color chosenColor)
1082 return availableColors.Any(c => c.Color == chosenColor) ? chosenColor :
SelectRandomColor(availableColors, Rand.RandSync.Unsynced);
1108 RefreshHeadSprites();
1111 partial
void LoadHeadSpriteProjectSpecific(
ContentXElement limbElement);
1113 private bool spriteTagsLoaded;
1116 if (!spriteTagsLoaded)
1122 private void LoadHeadSprite()
1124 LoadHeadElement(loadHeadSprite:
true, loadHeadSpriteTags:
true);
1127 private void LoadSpriteTags()
1129 LoadHeadElement(loadHeadSprite:
false, loadHeadSpriteTags:
true);
1132 private void LoadHeadElement(
bool loadHeadSprite,
bool loadHeadSpriteTags)
1134 if (
Ragdoll?.MainElement ==
null) {
return; }
1137 if (!limbElement.GetAttributeString(
"type",
string.Empty).Equals(
"head", StringComparison.OrdinalIgnoreCase)) {
continue; }
1139 ContentXElement spriteElement = limbElement.GetChildElement(
"sprite");
1140 if (spriteElement ==
null) {
continue; }
1142 string spritePath = spriteElement.GetAttributeContentPath(
"texture")?.Value;
1143 if (
string.IsNullOrEmpty(spritePath)) {
continue; }
1147 string fileName = Path.GetFileNameWithoutExtension(spritePath);
1149 if (
string.IsNullOrEmpty(fileName)) {
continue; }
1152 foreach (
string file
in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
1154 if (!file.EndsWith(
".png", StringComparison.OrdinalIgnoreCase))
1158 string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
1159 fileWithoutTags = fileWithoutTags.Split(
'[',
']').First();
1160 if (fileWithoutTags != fileName) {
continue; }
1164 HeadSprite =
new Sprite(spriteElement,
"", file);
1165 Portrait =
new Sprite(spriteElement,
"", file) { RelativeOrigin = Vector2.Zero };
1168 if (loadHeadSpriteTags)
1171 SpriteTags = file.Split(
'[',
']').Skip(1).Select(
id =>
id.ToIdentifier()).ToList();
1176 spriteTagsLoaded =
true;
1184 LoadHeadSpriteProjectSpecific(limbElement);
1197 float commonness = 0.1f;
1204 if (moustaches ==
null)
1208 if (faceAttachments ==
null)
1215 public static List<ContentXElement>
AddEmpty(IEnumerable<ContentXElement> elements,
WearableType type,
float commonness = 1)
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);
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;
1238 private bool IsAllowed(XElement element,
string spriteName)
1240 if (element !=
null)
1242 var disallowed = element.GetAttributeStringArray(
"disallow", Array.Empty<
string>());
1243 if (disallowed.Any(s => spriteName.Contains(s)))
1251 public static bool IsValidIndex(
int index, List<ContentXElement> list) => index >= 0 && index < list.Count;
1253 private static IEnumerable<float> GetWeights(IEnumerable<ContentXElement> elements) => elements.Select(h => h.GetAttributeFloat(
"commonness", 1f));
1255 partial
void LoadAttachmentSprites();
1259 if (
Name ==
null ||
Job ==
null) {
return 0; }
1274 public void ApplySkillGain(Identifier skillIdentifier,
float baseGain,
bool gainedFromAbility =
false,
float maxGain = 2f)
1279 IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
1285 public void IncreaseSkillLevel(Identifier skillIdentifier,
float increase,
bool gainedFromAbility =
false)
1294 increase = GetSkillSpecificGain(increase, skillIdentifier);
1301 if ((
int)newLevel > (
int)prevLevel)
1306 float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
1307 var abilitySkillGain =
new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier,
Character, gainedFromAbility);
1315 OnSkillChanged(skillIdentifier, prevLevel, newLevel);
1318 private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues =
new Dictionary<Identifier, StatTypes>
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();
1327 private float GetSkillSpecificGain(
float increase, Identifier skillIdentifier)
1329 if (skillGainStatValues.TryGetValue(skillIdentifier, out
StatTypes statType))
1339 if (
Job ==
null) {
return; }
1345 OnSkillChanged(skillIdentifier, 0.0f, level);
1349 float prevLevel = skill.Level;
1350 skill.Level = level;
1351 OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
1355 partial
void OnSkillChanged(Identifier skillIdentifier,
float prevLevel,
float newLevel);
1364 amount = (int)(amount * experienceGainMultiplier.Value);
1365 if (amount < 0) {
return; }
1373 if (newExperience < 0) {
return; }
1380 const int BaseExperienceRequired = 450;
1381 const int AddedExperienceRequiredPerLevel = 500;
1402 return experienceRequired;
1408 return experienceRequired + ExperienceRequiredPerLevel(level);
1417 if (currentLevel >= level) {
return 0; }
1419 for (
int i = 0; i < level; i++)
1421 required += ExperienceRequiredPerLevel(i);
1434 experienceRequired = 0;
1435 while (experienceRequired + ExperienceRequiredPerLevel(level) <=
ExperiencePoints)
1437 experienceRequired += ExperienceRequiredPerLevel(level);
1443 private static int ExperienceRequiredPerLevel(
int level)
1445 return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
1448 partial
void OnExperienceChanged(
int prevAmount,
int newAmount);
1450 partial
void OnPermanentStatChanged(
StatTypes statType);
1454 if (
string.IsNullOrEmpty(newName)) {
return; }
1458 if (!item.HasTag(
"identitycard".ToIdentifier()) && !item.HasTag(
"despawncontainer".ToIdentifier())) {
continue; }
1459 foreach (var tag
in item.Tags.Split(
','))
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>();
1482 public XElement
Save(XElement parentElement)
1484 XElement charElement =
new XElement(
"Character");
1487 new XAttribute(
"name",
Name),
1491 new XAttribute(
"salary",
Salary),
1498 new XAttribute(
"skincolor", XMLExtensions.ColorToString(
Head.
SkinColor)),
1499 new XAttribute(
"haircolor", XMLExtensions.ColorToString(
Head.
HairColor)),
1502 new XAttribute(
"personality",
PersonalityTrait?.Identifier ?? Identifier.Empty),
1503 new XAttribute(
"lastrewarddistribution",
LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
1508 if (HumanPrefabIds !=
default)
1511 new XAttribute(
"npcsetid", HumanPrefabIds.NpcSetIdentifier),
1512 new XAttribute(
"npcid", HumanPrefabIds.NpcIdentifier));
1517 if (!MinReputationToHire.factionId.IsEmpty)
1520 new XAttribute(
"factionId", MinReputationToHire.factionId),
1521 new XAttribute(
"minreputation", MinReputationToHire.reputation));
1534 XElement savedStatElement =
new XElement(
"savedstatvalues");
1537 foreach (var savedStat
in statValuePair.Value)
1539 if (savedStat.StatValue == 0f) {
continue; }
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)
1550 XElement talentElement =
new XElement(
"Talents");
1551 talentElement.Add(
new XAttribute(
"version",
GameMain.
Version.ToString()));
1555 talentElement.Add(
new XElement(
"Talent",
new XAttribute(
"identifier", talentIdentifier)));
1558 charElement.Add(savedStatElement);
1559 charElement.Add(talentElement);
1560 parentElement?.Add(charElement);
1566 if (parentElement ==
null || orders ==
null || orders.None()) {
return; }
1570 int priorityIncrease = 0;
1571 var linkedSubs = GetLinkedSubmarines();
1572 foreach (var orderInfo
in orders)
1574 var order = orderInfo;
1575 if (order ==
null || order.Identifier == Identifier.Empty)
1577 DebugConsole.ThrowError(
"Error saving an order - the order or its identifier is null");
1581 int? linkedSubIndex =
null;
1582 bool targetAvailableInNextLevel =
true;
1583 if (order.TargetSpatialEntity !=
null)
1585 var entitySub = order.TargetSpatialEntity.Submarine;
1586 bool isOutside = entitySub ==
null;
1588 bool isOnConnectedLinkedSub =
false;
1589 if (canBeOnLinkedSub)
1591 for (
int i = 0; i < linkedSubs.Count; i++)
1593 var ls = linkedSubs[i];
1594 if (!ls.LoadSub) {
continue; }
1595 if (ls.Sub != entitySub) {
continue; }
1601 targetAvailableInNextLevel =
1605 if (!targetAvailableInNextLevel)
1607 if (!order.Prefab.CanBeGeneralized)
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.");
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.");
1619 if (orderInfo.ManualPriority < 1)
1621 DebugConsole.ThrowError($
"Error saving an order ({order.Identifier}) - the order priority is less than 1");
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)
1631 orderElement.Add(
new XAttribute(
"option", orderInfo.Option));
1633 if (order.OrderGiver !=
null)
1635 orderElement.Add(
new XAttribute(
"ordergiver", order.OrderGiver.Info?.GetIdentifier()));
1637 if (order.TargetSpatialEntity?.Submarine is
Submarine targetSub)
1641 orderElement.Add(
new XAttribute(
"onmainsub",
true));
1643 else if(linkedSubIndex.HasValue)
1645 orderElement.Add(
new XAttribute(
"linkedsubindex", linkedSubIndex));
1648 switch (order.TargetType)
1651 orderElement.Add(
new XAttribute(
"targetid", (uint)e.ID));
1654 var orderTargetElement =
new XElement(
"ordertarget");
1655 var position = ot.WorldPosition;
1656 if (ot.Hull !=
null)
1658 orderTargetElement.Add(
new XAttribute(
"hullid", (uint)ot.Hull.ID));
1659 position -= ot.Hull.WorldPosition;
1661 orderTargetElement.Add(
new XAttribute(
"position", XMLExtensions.Vector2ToString(position)));
1662 orderElement.Add(orderTargetElement);
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));
1669 parentElement.Add(orderElement);
1678 var currentOrders =
new List<Order>(characterInfo.
CurrentOrders);
1680 currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
1681 SaveOrders(parentElement, currentOrders.ToArray());
1695 if (character ==
null) {
return; }
1697 foreach (var order
in orders)
1699 character.SetOrder(order, isNewOrder:
true, speak:
false, force:
true);
1710 var orders =
new List<Order>();
1711 if (ordersElement ==
null) {
return orders; }
1715 int priorityIncrease = 0;
1716 var linkedSubs = GetLinkedSubmarines();
1717 foreach (var orderElement
in ordersElement.GetChildElements(
"order"))
1720 string orderIdentifier = orderElement.GetAttributeString(
"id",
"");
1723 DebugConsole.ThrowError($
"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
1729 if (orderElement.GetAttribute(
"ordergiver") is XAttribute orderGiverIdAttribute)
1731 int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0);
1734 Entity targetEntity =
null;
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);
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))
1750 DebugConsole.ThrowError($
"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull");
1754 var orderTarget =
new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull);
1755 order =
new Order(orderPrefab, orderTarget, orderGiver: orderGiver);
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))
1763 DebugConsole.ThrowError($
"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure");
1767 order =
new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver);
1770 Identifier orderOption = orderElement.GetAttributeIdentifier(
"option",
"");
1771 int manualPriority = orderElement.GetAttributeInt(
"priority", 0) + priorityIncrease;
1773 orders.Add(orderInfo);
1775 bool GetTargetEntity(ushort targetId, out
Entity targetEntity)
1777 targetEntity =
null;
1780 if (orderElement.GetAttributeBool(
"onmainsub",
false))
1786 int linkedSubIndex = orderElement.GetAttributeInt(
"linkedsubindex", -1);
1787 if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count &&
1790 parentSub = linkedSub.Sub;
1793 if (parentSub !=
null)
1795 targetId = GetOffsetId(parentSub, targetId);
1797 return targetEntity !=
null;
1801 if (!orderPrefab.CanBeGeneralized)
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.");
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.");
1818 private static List<LinkedSubmarine> GetLinkedSubmarines()
1827 private static ushort GetOffsetId(Submarine parentSub, ushort
id)
1829 if (parentSub !=
null)
1831 var idRemap =
new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset);
1832 return idRemap.GetOffsetId(
id);
1839 if (healthData !=
null) { character?.
CharacterHealth.
Load(healthData, afflictionPredicate); }
1847 ResetLoadedAttachments();
1851 private void ResetAttachmentIndices()
1856 private void ResetLoadedAttachments()
1861 faceAttachments =
null;
1877 private void RefreshHeadSprites()
1884 attachmentSprites?.Clear();
1885 LoadAttachmentSprites();
1889 public readonly Dictionary<StatTypes, List<SavedStatValue>>
SavedStatValues =
new Dictionary<StatTypes, List<SavedStatValue>>();
1895 OnPermanentStatChanged(statType);
1903 OnPermanentStatChanged(statType);
1912 if (!savedStatValue.RemoveOnDeath) {
continue; }
1913 if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) {
continue; }
1914 savedStatValue.StatValue = 0.0f;
1924 bool changed =
false;
1927 if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) {
continue; }
1929 if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) {
continue; }
1930 savedStatValue.StatValue = 0.0f;
1933 if (changed) { OnPermanentStatChanged(statType); }
1936 static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
1938 if (statIdentifier == identifier) {
return true; }
1940 if (identifier.IndexOf(
'*') is var index and > -1)
1942 return statIdentifier.StartsWith(identifier[0..index]);
1953 return statValues.Sum(v => v.StatValue);
1964 return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(
static v => v.StatValue);
1998 statValue = Math.Max(statValue, botStatValue);
2004 public void ChangeSavedStatValue(
StatTypes statType,
float value, Identifier statIdentifier,
bool removeOnDeath,
float maxValue =
float.MaxValue,
bool setValue =
false)
2011 bool changed =
false;
2012 if (
SavedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat)
2014 float prevValue = savedStat.StatValue;
2015 savedStat.StatValue = setValue ? value : MathHelper.Min(savedStat.StatValue + value, maxValue);
2016 changed = !MathUtils.NearlyEqual(savedStat.StatValue, prevValue);
2020 SavedStatValues[statType].Add(
new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath));
2023 if (changed) { OnPermanentStatChanged(statType); }
2042 internal sealed
class SavedStatValue
2044 public Identifier StatIdentifier {
get;
set; }
2045 public float StatValue {
get;
set; }
2046 public bool RemoveOnDeath {
get;
set; }
2048 public SavedStatValue(Identifier statIdentifier,
float value,
bool removeOnDeath)
2051 RemoveOnDeath = removeOnDeath;
2052 StatIdentifier = statIdentifier;
2058 public AbilitySkillGain(
float skillAmount, Identifier skillIdentifier, Character character,
bool gainedFromAbility)
2060 Value = skillAmount;
2061 SkillIdentifier = skillIdentifier;
2063 GainedFromAbility = gainedFromAbility;
2066 public float Value {
get;
set; }
2067 public Identifier SkillIdentifier {
get;
set; }
2068 public bool GainedFromAbility {
get; }
2075 Value = experienceGainMultiplier;
AbilityExperienceGainMultiplier(float experienceGainMultiplier)
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)
CharacterHealth CharacterHealth
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
float GetStatValue(StatTypes statType, bool includeSaved=true)
static readonly List< Character > CharacterList
CharacterInventory Inventory
bool HasAbilityFlag(AbilityFlags abilityFlag)
readonly AnimController AnimController
HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex=0, int beardIndex=0, int moustacheIndex=0, int faceAttachmentIndex=0)
ContentXElement??? FaceAttachment
void SetHairWithHatIndex()
ContentXElement??? HairWithHatElement
ContentXElement??? MoustacheElement
readonly CharacterInfo CharacterInfo
ContentXElement??? BeardElement
readonly HeadPreset Preset
ContentXElement??? HairElement
void ResetAttachmentIndices()
Dictionary< Identifier, SerializableProperty > SerializableProperties
ImmutableHashSet< Identifier > TagSet
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....
IReadOnlyList< ContentXElement > FaceAttachments
Option< int > LastRewardDistribution
Keeps track of the last reward distribution that was set on the character's wallet....
void CalculateHeadPosition(Sprite sprite)
void RefreshHead()
Reloads the head sprite and the attachment sprites.
IEnumerable< ContentXElement > FilterElements(IEnumerable< ContentXElement > elements, ImmutableHashSet< Identifier > tags, WearableType? targetType=null)
void SetExperience(int newExperience)
List< Identifier > SpriteTags
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,...
IReadOnlyList< ContentXElement > Beards
readonly bool HasSpecifierTags
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)
IReadOnlyList< ContentXElement > Moustaches
IEnumerable< ContentXElement > Wearables
static void ApplyHealthData(Character character, XElement healthData, Func< AfflictionPrefab, bool > afflictionPredicate=null)
bool IsDisguisedAsAnother
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)
void ClearCurrentOrders()
static int HighestManualOrderPriority
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)
string GetRandomName(Rand.RandSync randSync)
void ReloadHeadAttachments()
Reloads the attachment xml elements according to the indices. Doesn't reload the sprites.
float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
Character Character
Note: Can be null.
void Rename(string newName)
IReadOnlyList< ContentXElement > Hairs
int AdditionalTalentPoints
List< WearableSprite >? AttachmentSprites
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)
CauseOfDeath CauseOfDeath
void ClearSavedStatValues(StatTypes statType)
void ClearSavedStatValues()
float GetProgressTowardsNextLevel()
ContentXElement CharacterConfigElement
int GetIdentifierUsingOriginalName()
Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName,...
const int MaxAdditionalTalentPoints
NPCPersonalityTrait PersonalityTrait
HashSet< Identifier > UnlockedTalents
void SaveOrderData()
Save current orders to OrderData
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.
int GetExperienceRequiredForCurrentLevel()
ushort ID
Unique ID given to character infos in MP. Non-persistent. Used by clients to identify which infos are...
void VerifySpriteTagsLoaded()
void RecreateHead(HeadInfo headInfo)
Identifier NpcSetIdentifier
const int MaxCurrentOrders
int MissionsCompletedSinceDeath
float GetSavedStatValue(StatTypes statType)
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.
XElement Save(XElement parentElement)
int GetManualOrderPriority(Order order)
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,...
string ReplaceVars(string str)
float LastResistanceMultiplierSkillLossDeath
Used to store the last known resistance against skill loss on death when the character dies,...
void ResetSavedStatValue(Identifier statIdentifier)
void LoadHeadAttachments()
int GetTotalTalentPoints()
int CountValidAttachmentsOfType(WearableType wearableType)
float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection< Character > bots)
int GetAvailableTalentPoints()
static List< Order > LoadOrders(XElement ordersElement)
float GetSavedStatValue(StatTypes statType, Identifier statIdentifier)
void GiveExperience(int amount)
void RemoveSavedStatValuesOnDeath()
static void ApplyOrderData(Character character, XElement orderData)
static bool IsValidIndex(int index, List< ContentXElement > list)
List< Order > CurrentOrders
int GetExperienceRequiredToLevelUp()
Item GetItemInLimbSlot(InvSlotType limbSlot)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
static readonly PrefabCollection< CharacterPrefab > Prefabs
static readonly Identifier HumanSpeciesName
static readonly ContentPath Empty
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()
const ushort NullEntityID
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
static GameSession?? GameSession
static readonly Version Version
static NetworkMember NetworkMember
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()
XElement Save(XElement parentElement)
Skill GetSkill(Identifier skillIdentifier)
static Job Random(Rand.RandSync randSync)
float GetSkillLevel(Identifier skillIdentifier)
void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool increasePastMax)
readonly HashSet< Identifier > TagSet
static readonly PrefabCollection< NPCPersonalityTrait > Traits
static NPCPersonalityTrait GetRandom(string seed)
Order WithOption(Identifier option)
Order WithManualPriority(int newPriority)
static readonly PrefabCollection< OrderPrefab > Prefabs
readonly Identifier Identifier
static Dictionary< Identifier, SerializableProperty > DeserializeProperties(object obj, XElement element=null)
readonly float PriceMultiplier
static SkillSettings Current
float SkillIncreaseExponent
float AssistantSkillIncreaseMultiplier
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
static readonly PrefabCollection< TalentPrefab > TalentPrefabs
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.