Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs
2024-06-27 16:46:33 +03:00

2080 lines
88 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct NetJobVariant(Identifier Identifier, byte Variant) : INetSerializableStruct
{
[return: MaybeNull]
public JobVariant ToJobVariant()
{
if (!JobPrefab.Prefabs.TryGet(Identifier, out JobPrefab jobPrefab) || jobPrefab.HiddenJob) { return null; }
return new JobVariant(jobPrefab, Variant);
}
public static NetJobVariant FromJobVariant(JobVariant jobVariant) => new NetJobVariant(jobVariant.Prefab.Identifier, (byte)jobVariant.Variant);
}
[NetworkSerialize(ArrayMaxSize = byte.MaxValue)]
internal readonly record struct NetCharacterInfo(string NewName,
ImmutableArray<Identifier> Tags,
byte HairIndex,
byte BeardIndex,
byte MoustacheIndex,
byte FaceAttachmentIndex,
Color SkinColor,
Color HairColor,
Color FacialHairColor,
ImmutableArray<NetJobVariant> JobVariants) : INetSerializableStruct;
class CharacterInfoPrefab
{
public readonly ImmutableArray<CharacterInfo.HeadPreset> Heads;
public readonly ImmutableDictionary<Identifier, ImmutableHashSet<Identifier>> VarTags;
public readonly Identifier MenuCategoryVar;
public readonly Identifier Pronouns;
public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
{
if (headsElement == null)
{
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.");
}
Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray();
if (varsElement != null)
{
VarTags = varsElement.Elements()
.Select(e =>
(e.GetAttributeIdentifier("var", ""),
e.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet()))
.ToImmutableDictionary();
}
else
{
VarTags = new[]
{
("GENDER".ToIdentifier(),
new[] { "female".ToIdentifier(), "male".ToIdentifier() }.ToImmutableHashSet())
}.ToImmutableDictionary();
}
MenuCategoryVar = menuCategoryElement?.GetAttributeIdentifier("var", Identifier.Empty) ?? "GENDER".ToIdentifier();
Pronouns = pronounsElement?.GetAttributeIdentifier("vars", Identifier.Empty) ?? "GENDER".ToIdentifier();
}
public string ReplaceVars(string str, CharacterInfo.HeadPreset headPreset)
{
return ReplaceVars(str, headPreset.TagSet);
}
public string ReplaceVars(string str, ImmutableHashSet<Identifier> tagSet)
{
foreach (var key in VarTags.Keys)
{
str = str.Replace($"[{key}]", tagSet.FirstOrDefault(t => VarTags[key].Contains(t)).Value, StringComparison.OrdinalIgnoreCase);
}
return str;
}
}
/// <summary>
/// Stores information about the Character that is needed between rounds in the
/// menu etc., whereas Character itself is the object actually spawned in-game.
/// </summary>
partial class CharacterInfo
{
public class HeadInfo
{
public readonly CharacterInfo CharacterInfo;
public readonly HeadPreset Preset;
public int HairIndex { get; set; }
private int? hairWithHatIndex;
public void SetHairWithHatIndex()
{
if (CharacterInfo.Hairs is null)
{
if (HairIndex == -1)
{
#if DEBUG
DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#else
DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#endif
}
hairWithHatIndex = HairIndex;
}
else
{
hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1;
if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
hairWithHatIndex = HairIndex;
}
}
}
public int BeardIndex;
public int MoustacheIndex;
public int FaceAttachmentIndex;
public Color HairColor;
public Color FacialHairColor;
public Color SkinColor;
public Vector2 SheetIndex => Preset.SheetIndex;
public ContentXElement HairElement
{
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (HairIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex);
}
}
public ContentXElement HairWithHatElement
{
get
{
if (hairWithHatIndex == null)
{
SetHairWithHatIndex();
}
if (CharacterInfo.Hairs == null) { return null; }
if (hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value);
}
}
public ContentXElement BeardElement
{
get
{
if (CharacterInfo.Beards == null) { return null; }
if (BeardIndex >= CharacterInfo.Beards.Count)
{
DebugConsole.AddWarning($"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})");
}
return CharacterInfo.Beards.ElementAtOrDefault(BeardIndex);
}
}
public ContentXElement MoustacheElement
{
get
{
if (CharacterInfo.Moustaches == null) { return null; }
if (MoustacheIndex >= CharacterInfo.Moustaches.Count)
{
DebugConsole.AddWarning($"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})");
}
return CharacterInfo.Moustaches.ElementAtOrDefault(MoustacheIndex);
}
}
public ContentXElement FaceAttachment
{
get
{
if (CharacterInfo.FaceAttachments == null) { return null; }
if (FaceAttachmentIndex >= CharacterInfo.FaceAttachments.Count)
{
DebugConsole.AddWarning($"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})");
}
return CharacterInfo.FaceAttachments.ElementAtOrDefault(FaceAttachmentIndex);
}
}
public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0)
{
CharacterInfo = characterInfo;
Preset = headPreset;
HairIndex = hairIndex;
BeardIndex = beardIndex;
MoustacheIndex = moustacheIndex;
FaceAttachmentIndex = faceAttachmentIndex;
}
public void ResetAttachmentIndices()
{
HairIndex = -1;
BeardIndex = -1;
MoustacheIndex = -1;
FaceAttachmentIndex = -1;
}
}
private HeadInfo head;
public HeadInfo Head
{
get { return head; }
set
{
if (head != value && value != null)
{
head = value;
HeadSprite = null;
AttachmentSprites = null;
hairs = null;
beards = null;
moustaches = null;
faceAttachments = null;
}
}
}
private readonly Identifier maleIdentifier = "Male".ToIdentifier();
private readonly Identifier femaleIdentifier = "Female".ToIdentifier();
public bool IsMale { get { return head?.Preset?.TagSet?.Contains(maleIdentifier) ?? false; } }
public bool IsFemale { get { return head?.Preset?.TagSet?.Contains(femaleIdentifier) ?? false; } }
public CharacterInfoPrefab Prefab => CharacterPrefab.Prefabs[SpeciesName].CharacterInfoPrefab;
public class HeadPreset : ISerializableEntity
{
private readonly CharacterInfoPrefab characterInfoPrefab;
public Identifier MenuCategory => TagSet.First(t => characterInfoPrefab.VarTags[characterInfoPrefab.MenuCategoryVar].Contains(t));
public ImmutableHashSet<Identifier> TagSet { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public string Tags
{
get { return string.Join(",", TagSet); }
private set
{
TagSet = value.Split(",")
.Select(s => s.ToIdentifier())
.Where(id => !id.IsEmpty)
.ToImmutableHashSet();
}
}
[Serialize("0,0", IsPropertySaveable.No)]
public Vector2 SheetIndex { get; private set; }
public string Name => $"Head Preset {Tags}";
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public HeadPreset(CharacterInfoPrefab charInfoPrefab, XElement element)
{
characterInfoPrefab = charInfoPrefab;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
DetermineTagsFromLegacyFormat(element);
}
private void DetermineTagsFromLegacyFormat(XElement element)
{
void addTag(string tag)
=> TagSet = TagSet.Add(tag.ToIdentifier());
string headId = element.GetAttributeString("id", "");
string gender = element.GetAttributeString("gender", "");
string race = element.GetAttributeString("race", "");
if (!headId.IsNullOrEmpty()) { addTag($"head{headId}"); }
if (!gender.IsNullOrEmpty()) { addTag(gender); }
if (!race.IsNullOrEmpty()) { addTag(race); }
}
}
public XElement InventoryData;
public XElement HealthData;
public XElement OrderData;
public bool PermanentlyDead;
public bool RenamingEnabled = false;
private static ushort idCounter = 1;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
public string OriginalName { get; private set; }
public string Name;
public LocalizedString Title;
public (Identifier NpcSetIdentifier, Identifier NpcIdentifier) HumanPrefabIds;
public string DisplayName
{
get
{
if (Character == null || !Character.HideFace)
{
IsDisguised = IsDisguisedAsAnother = false;
return Name;
}
else if ((GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises))
{
IsDisguised = IsDisguisedAsAnother = false;
return Name;
}
if (Character.Inventory != null)
{
//Disguise as the ID card name if it's equipped
var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card);
return idCard?.GetComponent<IdCard>()?.OwnerName ?? disguiseName;
}
return disguiseName;
}
}
public Identifier SpeciesName { get; }
/// <summary>
/// Note: Can be null.
/// </summary>
public Character Character;
public Job Job;
public int Salary;
public int ExperiencePoints { get; private set; }
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
public (Identifier factionId, float reputation) MinReputationToHire;
/// <summary>
/// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to cull them from the selection
/// </summary>
public IEnumerable<Identifier> GetUnlockedTalentsInTree()
{
if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t));
}
/// <summary>
/// Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e.g. with an endocrine booster)
/// </summary>
public IEnumerable<Identifier> GetUnlockedTalentsOutsideTree()
{
if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t));
}
public const int MaxAdditionalTalentPoints = 100;
private int additionalTalentPoints;
public int AdditionalTalentPoints
{
get { return additionalTalentPoints; }
set { additionalTalentPoints = MathHelper.Clamp(value, 0, MaxAdditionalTalentPoints); }
}
private Sprite _headSprite;
public Sprite HeadSprite
{
get
{
if (_headSprite == null)
{
LoadHeadSprite();
}
#if CLIENT
if (_headSprite != null)
{
CalculateHeadPosition(_headSprite);
}
#endif
return _headSprite;
}
private set
{
if (_headSprite != null)
{
_headSprite.Remove();
}
_headSprite = value;
}
}
/// <summary>
/// Can be used to disable displaying the job in any info panels
/// </summary>
public bool OmitJobInMenus;
private Sprite portrait;
public Sprite Portrait
{
get
{
if (portrait == null)
{
LoadHeadSprite();
}
return portrait;
}
private set
{
if (portrait != null)
{
portrait.Remove();
}
portrait = value;
}
}
public bool IsDisguised = false;
public bool IsDisguisedAsAnother = false;
public void CheckDisguiseStatus(bool handleBuff, IdCard idCard = null)
{
if (Character == null) { return; }
string currentlyDisplayedName = DisplayName;
IsDisguised = currentlyDisplayedName == disguiseName;
IsDisguisedAsAnother = !IsDisguised && currentlyDisplayedName != Name;
if (IsDisguisedAsAnother)
{
if (handleBuff)
{
if (AfflictionPrefab.Prefabs.TryGet("disguised", out AfflictionPrefab afflictionPrefab))
{
Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(100f));
}
}
idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
if (idCard != null)
{
#if CLIENT
GetDisguisedSprites(idCard);
#endif
return;
}
}
#if CLIENT
disguisedJobIcon = null;
disguisedPortrait = null;
#endif
if (handleBuff)
{
Character.CharacterHealth.ReduceAfflictionOnAllLimbs("disguised".ToIdentifier(), 100f);
}
}
private List<WearableSprite> attachmentSprites;
public List<WearableSprite> AttachmentSprites
{
get
{
if (attachmentSprites == null)
{
LoadAttachmentSprites();
}
return attachmentSprites;
}
private set
{
if (attachmentSprites != null)
{
attachmentSprites.ForEach(s => s.Sprite?.Remove());
}
attachmentSprites = value;
}
}
public ContentXElement CharacterConfigElement { get; set; }
public bool StartItemsGiven;
/// <summary>
/// Newly hired bot that hasn't spawned yet
/// </summary>
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
public CharacterTeamType TeamID;
public NPCPersonalityTrait PersonalityTrait { get; private set; }
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
public int GetManualOrderPriority(Order order)
{
if (order != null && order.AssignmentPriority < 100 && CurrentOrders.Any())
{
int orderPriority = HighestManualOrderPriority;
for (int i = 0; i < CurrentOrders.Count; i++)
{
if (order.AssignmentPriority >= CurrentOrders[i].AssignmentPriority)
{
break;
}
else
{
orderPriority--;
}
}
return Math.Max(orderPriority, 1);
}
else
{
return HighestManualOrderPriority;
}
}
public List<Order> CurrentOrders { get; } = new List<Order>();
/// <summary>
/// Unique ID given to character infos in MP. Non-persistent.
/// Used by clients to identify which infos are the same to prevent duplicate characters in round summary.
/// </summary>
public ushort ID;
public List<Identifier> SpriteTags
{
get;
private set;
}
public readonly bool HasSpecifierTags;
private RagdollParams ragdoll;
public RagdollParams Ragdoll
{
get
{
if (ragdoll == null)
{
Identifier speciesName = SpeciesName;
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName == CharacterPrefab.HumanSpeciesName);
ragdoll = isHumanoid
? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage)
: RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(SpeciesName, CharacterConfigElement, CharacterConfigElement.ContentPackage);
}
return ragdoll;
}
set { ragdoll = value; }
}
public bool IsAttachmentsLoaded => Head.HairIndex > -1 && Head.BeardIndex > -1 && Head.MoustacheIndex > -1 && Head.FaceAttachmentIndex > -1;
public IEnumerable<ContentXElement> GetValidAttachmentElements(IEnumerable<ContentXElement> elements, HeadPreset headPreset, WearableType? wearableType = null)
=> FilterElements(elements, headPreset.TagSet, wearableType);
public int CountValidAttachmentsOfType(WearableType wearableType)
=> GetValidAttachmentElements(Wearables, Head.Preset, wearableType).Count();
public readonly ImmutableArray<(Color Color, float Commonness)> HairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors;
private void GetName(Rand.RandSync randSync, out string name)
{
ContentXElement nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty;
XElement namesXml = null;
if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined
{
XDocument doc = XMLExtensions.TryLoadXml(namesXmlFile);
namesXml = doc.Root;
}
else //the legacy firstnames.txt/lastnames.txt shit is defined
{
namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]"));
string firstNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
string lastNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
{
var firstNames = File.ReadAllLines(firstNamesPath);
var lastNames = File.ReadAllLines(lastNamesPath);
namesXml.Add(firstNames.Select(n => new XElement("firstname", new XAttribute("value", n))));
namesXml.Add(lastNames.Select(n => new XElement("lastname", new XAttribute("value", n))));
}
else //the files don't exist, just fall back to the vanilla names
{
XDocument doc = XMLExtensions.TryLoadXml("Content/Characters/Human/names.xml");
namesXml = doc.Root;
}
}
name = namesXml.GetAttributeString("format", "");
Dictionary<Identifier, List<string>> entries = new Dictionary<Identifier, List<string>>();
foreach (var subElement in namesXml.Elements())
{
Identifier elemName = subElement.NameAsIdentifier();
if (!entries.ContainsKey(elemName))
{
entries.Add(elemName, new List<string>());
}
ImmutableHashSet<Identifier> identifiers = subElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
if (identifiers.IsSubsetOf(Head.Preset.TagSet))
{
entries[elemName].Add(subElement.GetAttributeString("value", ""));
}
}
foreach (var k in entries.Keys)
{
name = name.Replace($"[{k}]", entries[k].GetRandom(randSync), StringComparison.OrdinalIgnoreCase);
}
}
private static void LoadTagsBackwardsCompatibility(XElement element, HashSet<Identifier> tags)
{
//we need this to be able to load save files from
//older versions with the shittier hardcoded character
//info implementation
Identifier gender = element.GetAttributeIdentifier("gender", "");
int headSpriteId = element.GetAttributeInt("headspriteid", -1);
if (!gender.IsEmpty) { tags.Add(gender); }
if (headSpriteId > 0) { tags.Add($"head{headSpriteId}".ToIdentifier()); }
}
// talent-relevant values
public int MissionsCompletedSinceDeath = 0;
private static bool ElementHasSpecifierTags(XElement element)
=> element.GetAttributeBool("specifiertags",
element.GetAttributeBool("genders",
element.GetAttributeBool("races", false)));
/// <summary>
/// Keeps track of the last reward distribution that was set on the character's wallet.
/// Is used to keep salary when the character respawns since CharacterInfo is preserved between deaths.
/// </summary>
/// <remarks>
/// None means the salary has not been set yet, which is not always 0 if default salary is set.
/// </remarks>
public Option<int> LastRewardDistribution = Option.None;
// Used for creating the data
public CharacterInfo(
Identifier speciesName,
string name = "",
string originalName = "",
Either<Job, JobPrefab> jobOrJobPrefab = null,
int variant = 0,
Rand.RandSync randSync = Rand.RandSync.Unsynced,
Identifier npcIdentifier = default)
{
JobPrefab jobPrefab = null;
Job job = null;
if (jobOrJobPrefab != null)
{
jobOrJobPrefab.TryGet(out job);
jobOrJobPrefab.TryGet(out jobPrefab);
}
ID = idCounter;
idCounter++;
if (idCounter == 0) { idCounter++; }
SpeciesName = speciesName;
SpriteTags = new List<Identifier>();
CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement;
if (CharacterConfigElement == null) { return; }
// TODO: support for variants
HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement);
if (HasSpecifierTags)
{
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
var headPreset = Prefab?.Heads.GetRandom(randSync);
if (headPreset == null)
{
DebugConsole.ThrowError("Failed to find a head preset!");
}
Head = new HeadInfo(this, headPreset);
SetAttachments(randSync);
SetColors(randSync);
Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant));
if (!string.IsNullOrEmpty(name))
{
Name = name;
}
else
{
Name = GetRandomName(randSync);
}
TryLoadNameAndTitle(npcIdentifier);
SetPersonalityTrait();
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
}
private void SetPersonalityTrait()
=> PersonalityTrait = NPCPersonalityTrait.GetRandom(Name + string.Concat(Head.Preset.TagSet.OrderBy(tag => tag)));
public string GetRandomName(Rand.RandSync randSync)
{
GetName(randSync, out string name);
return name;
}
public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync)
=> ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync)
.Color;
private void SetAttachments(Rand.RandSync randSync)
{
LoadHeadAttachments();
int pickRandomIndex(IReadOnlyList<ContentXElement> list)
{
var elems = GetValidAttachmentElements(list, Head.Preset).ToArray();
var weights = GetWeights(elems).ToArray();
return list.IndexOf(ToolBox.SelectWeightedRandom(elems, weights, randSync));
}
Head.HairIndex = pickRandomIndex(Hairs);
Head.BeardIndex = pickRandomIndex(Beards);
Head.MoustacheIndex = pickRandomIndex(Moustaches);
Head.FaceAttachmentIndex = pickRandomIndex(FaceAttachments);
}
private void SetColors(Rand.RandSync randSync)
{
Head.HairColor = SelectRandomColor(HairColors, randSync);
Head.FacialHairColor = SelectRandomColor(FacialHairColors, randSync);
Head.SkinColor = SelectRandomColor(SkinColors, randSync);
}
private bool IsColorValid(in Color clr)
=> clr.R != 0 || clr.G != 0 || clr.B != 0;
public void CheckColors()
{
if (!IsColorValid(Head.HairColor))
{
Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced);
}
if (!IsColorValid(Head.FacialHairColor))
{
Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced);
}
if (!IsColorValid(Head.SkinColor))
{
Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced);
}
}
// Used for loading the data
public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default)
{
ID = idCounter;
idCounter++;
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
RenamingEnabled = infoElement.GetAttributeBool("renamingenabled", false);
ContentXElement element;
if (!SpeciesName.IsEmpty)
{
element = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement;
}
else
{
// Backwards support (human only)
// Actually you know what this is backwards!
throw new InvalidOperationException("SpeciesName not defined");
}
if (element == null) { return; }
// TODO: support for variants
CharacterConfigElement = element;
HasSpecifierTags = ElementHasSpecifierTags(CharacterConfigElement);
if (HasSpecifierTags)
{
RecreateHead(
tags.ToImmutableHashSet(),
infoElement.GetAttributeInt("hairindex", -1),
infoElement.GetAttributeInt("beardindex", -1),
infoElement.GetAttributeInt("moustacheindex", -1),
infoElement.GetAttributeInt("faceattachmentindex", -1));
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
//default to transparent color, it's invalid and will be replaced with a random one in CheckColors
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
CheckColors();
TryLoadNameAndTitle(npcIdentifier);
if (string.IsNullOrEmpty(Name))
{
var nameElement = CharacterConfigElement.GetChildElement("names");
if (nameElement != null)
{
GetName(Rand.RandSync.ServerAndClient, out Name);
}
}
}
if (string.IsNullOrEmpty(OriginalName))
{
OriginalName = Name;
}
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
Identifier personalityName = infoElement.GetAttributeIdentifier("personality", "");
if (personalityName != Identifier.Empty)
{
if (NPCPersonalityTrait.Traits.TryGet(personalityName, out var trait) ||
NPCPersonalityTrait.Traits.TryGet(personalityName.Replace(" ".ToIdentifier(), Identifier.Empty), out trait))
{
PersonalityTrait = trait;
}
else
{
DebugConsole.ThrowError($"Error in CharacterInfo \"{OriginalName}\": could not find a personality trait with the identifier \"{personalityName}\".");
}
}
HumanPrefabIds = (
infoElement.GetAttributeIdentifier("npcsetid", Identifier.Empty),
infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
UnlockedTalents = new HashSet<Identifier>();
MinReputationToHire = (infoElement.GetAttributeIdentifier("factionId", Identifier.Empty), infoElement.GetAttributeFloat("minreputation", 0.0f));
foreach (var subElement in infoElement.Elements())
{
bool jobCreated = false;
Identifier elementName = subElement.Name.ToIdentifier();
if (elementName == "job" && !jobCreated)
{
Job = new Job(subElement);
jobCreated = true;
// there used to be a break here, but it had to be removed to make room for statvalues
// using the jobCreated boolean to make sure that only the first job found is created
}
else if (elementName == "savedstatvalues")
{
foreach (XElement savedStat in subElement.Elements())
{
string statTypeString = savedStat.GetAttributeString("stattype", "").ToLowerInvariant();
if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
{
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" when loading character data in CharacterInfo!");
continue;
}
float value = savedStat.GetAttributeFloat("statvalue", 0f);
if (value == 0f) { continue; }
Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
return;
}
bool removeOnDeath = savedStat.GetAttributeBool("removeondeath", true);
ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
}
}
else if (elementName == "talents")
{
Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
foreach (XElement talentElement in subElement.Elements())
{
if (talentElement.Name.ToIdentifier() != "talent") { continue; }
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab))
{
foreach (TalentMigration migration in prefab.Migrations)
{
migration.TryApply(version, this);
}
}
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
private void TryLoadNameAndTitle(Identifier npcIdentifier)
{
if (!npcIdentifier.IsEmpty)
{
Title = TextManager.Get("npctitle." + npcIdentifier);
string nameTag = "charactername." + npcIdentifier;
if (TextManager.ContainsTag(nameTag))
{
Name = TextManager.Get(nameTag).Value;
}
}
}
private List<ContentXElement> hairs;
public IReadOnlyList<ContentXElement> Hairs => hairs;
private List<ContentXElement> beards;
public IReadOnlyList<ContentXElement> Beards => beards;
private List<ContentXElement> moustaches;
public IReadOnlyList<ContentXElement> Moustaches => moustaches;
private List<ContentXElement> faceAttachments;
public IReadOnlyList<ContentXElement> FaceAttachments => faceAttachments;
private IEnumerable<ContentXElement> wearables;
public IEnumerable<ContentXElement> Wearables
{
get
{
if (wearables == null)
{
var attachments = CharacterConfigElement.GetChildElement("HeadAttachments");
if (attachments != null)
{
wearables = attachments.GetChildElements("Wearable");
}
}
return wearables;
}
}
/// <summary>
/// Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifier()
{
return GetIdentifierHash(Name);
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifierUsingOriginalName()
{
return GetIdentifierHash(OriginalName);
}
private int GetIdentifierHash(string name)
{
int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s)));
id ^= Head.HairIndex << 12;
id ^= Head.BeardIndex << 18;
id ^= Head.MoustacheIndex << 24;
id ^= Head.FaceAttachmentIndex << 30;
if (Job != null)
{
id ^= ToolBox.StringToInt(Job.Prefab.Identifier.Value);
}
return id;
}
public IEnumerable<ContentXElement> FilterElements(IEnumerable<ContentXElement> elements, ImmutableHashSet<Identifier> tags, WearableType? targetType = null)
{
if (elements is null) { return null; }
return elements.Where(w =>
{
if (!(targetType is null))
{
if (Enum.TryParse(w.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
}
HashSet<Identifier> t = w.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(w, t);
return t.IsSubsetOf(tags);
});
}
public void RecreateHead(ImmutableHashSet<Identifier> tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
{
HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags));
if (headPreset == null)
{
if (tags.Count == 1)
{
headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First()));
}
headPreset ??= Prefab.Heads.GetRandomUnsynced();
}
head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ReloadHeadAttachments();
}
public string ReplaceVars(string str)
{
return Prefab.ReplaceVars(str, Head.Preset);
}
#if CLIENT
public void RecreateHead(MultiplayerPreferences characterSettings)
{
if (characterSettings.HairIndex == -1 &&
characterSettings.BeardIndex == -1 &&
characterSettings.MoustacheIndex == -1 &&
characterSettings.FaceAttachmentIndex == -1)
{
//randomize if nothing is set
SetAttachments(Rand.RandSync.Unsynced);
characterSettings.HairIndex = Head.HairIndex;
characterSettings.BeardIndex = Head.BeardIndex;
characterSettings.MoustacheIndex = Head.MoustacheIndex;
characterSettings.FaceAttachmentIndex = Head.FaceAttachmentIndex;
}
RecreateHead(
characterSettings.TagSet.ToImmutableHashSet(),
characterSettings.HairIndex,
characterSettings.BeardIndex,
characterSettings.MoustacheIndex,
characterSettings.FaceAttachmentIndex);
Head.SkinColor = ChooseColor(SkinColors, characterSettings.SkinColor);
Head.HairColor = ChooseColor(HairColors, characterSettings.HairColor);
Head.FacialHairColor = ChooseColor(FacialHairColors, characterSettings.FacialHairColor);
Color ChooseColor(in ImmutableArray<(Color Color, float Commonness)> availableColors, Color chosenColor)
{
return availableColors.Any(c => c.Color == chosenColor) ? chosenColor : SelectRandomColor(availableColors, Rand.RandSync.Unsynced);
}
}
#endif
public void RecreateHead(HeadInfo headInfo)
{
RecreateHead(
headInfo.Preset.TagSet,
headInfo.HairIndex,
headInfo.BeardIndex,
headInfo.MoustacheIndex,
headInfo.FaceAttachmentIndex);
Head.SkinColor = headInfo.SkinColor;
Head.HairColor = headInfo.HairColor;
Head.FacialHairColor = headInfo.FacialHairColor;
CheckColors();
}
/// <summary>
/// Reloads the head sprite and the attachment sprites.
/// </summary>
public void RefreshHead()
{
ReloadHeadAttachments();
RefreshHeadSprites();
}
partial void LoadHeadSpriteProjectSpecific(ContentXElement limbElement);
private bool spriteTagsLoaded;
public void VerifySpriteTagsLoaded()
{
if (!spriteTagsLoaded)
{
LoadSpriteTags();
}
}
private void LoadHeadSprite()
{
LoadHeadElement(loadHeadSprite: true, loadHeadSpriteTags: true);
}
private void LoadSpriteTags()
{
LoadHeadElement(loadHeadSprite: false, loadHeadSpriteTags: true);
}
private void LoadHeadElement(bool loadHeadSprite, bool loadHeadSpriteTags)
{
if (Ragdoll?.MainElement == null) { return; }
foreach (var limbElement in Ragdoll.MainElement.Elements())
{
if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
ContentXElement spriteElement = limbElement.GetChildElement("sprite");
if (spriteElement == null) { continue; }
string spritePath = spriteElement.GetAttributeContentPath("texture")?.Value;
if (string.IsNullOrEmpty(spritePath)) { continue; }
spritePath = ReplaceVars(spritePath);
string fileName = Path.GetFileNameWithoutExtension(spritePath);
if (string.IsNullOrEmpty(fileName)) { continue; }
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) { continue; }
if (loadHeadSprite)
{
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
}
if (loadHeadSpriteTags)
{
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).Select(id => id.ToIdentifier()).ToList();
if (SpriteTags.Any())
{
SpriteTags.RemoveAt(SpriteTags.Count - 1);
}
spriteTagsLoaded = true;
}
break;
}
if (loadHeadSprite)
{
LoadHeadSpriteProjectSpecific(limbElement);
}
break;
}
}
public void LoadHeadAttachments()
{
if (Wearables != null)
{
if (hairs == null)
{
float commonness = 0.1f;
hairs = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Hair), WearableType.Hair, commonness);
}
if (beards == null)
{
beards = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Beard), WearableType.Beard);
}
if (moustaches == null)
{
moustaches = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.Moustache), WearableType.Moustache);
}
if (faceAttachments == null)
{
faceAttachments = AddEmpty(FilterElements(wearables, head.Preset.TagSet, WearableType.FaceAttachment), WearableType.FaceAttachment);
}
}
}
public static List<ContentXElement> AddEmpty(IEnumerable<ContentXElement> elements, WearableType type, float commonness = 1)
{
// 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.
var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)).FromPackage(null);
var list = new List<ContentXElement>() { emptyElement };
list.AddRange(elements);
return list;
}
public ContentXElement GetRandomElement(IEnumerable<ContentXElement> elements)
{
var filtered = elements.Where(IsWearableAllowed);
if (filtered.Count() == 0) { return null; }
var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced);
return element == null || element.NameAsIdentifier() == "Empty" ? null : element;
}
private bool IsWearableAllowed(ContentXElement element)
{
string spriteName = element.GetChildElement("sprite").GetAttributeString("name", string.Empty);
return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName);
}
private bool IsAllowed(XElement element, string spriteName)
{
if (element != null)
{
var disallowed = element.GetAttributeStringArray("disallow", Array.Empty<string>());
if (disallowed.Any(s => spriteName.Contains(s)))
{
return false;
}
}
return true;
}
public static bool IsValidIndex(int index, List<ContentXElement> list) => index >= 0 && index < list.Count;
private static IEnumerable<float> GetWeights(IEnumerable<ContentXElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
partial void LoadAttachmentSprites();
public int CalculateSalary()
{
if (Name == null || Job == null) { return 0; }
int salary = 0;
foreach (Skill skill in Job.GetSkills())
{
salary += (int)(skill.Level * skill.PriceMultiplier);
}
return (int)(salary * Job.Prefab.PriceMultiplier);
}
/// <summary>
/// Increases the characters skill at a rate proportional to their current skill.
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
/// </summary>
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
{
float skillLevel = Job.GetSkillLevel(skillIdentifier);
// The formula is too generous on low skill levels, hence the minimum divider.
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
}
/// <summary>
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
/// </summary>
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
if (Job.Prefab.Identifier == "assistant")
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
float newLevel = Job.GetSkillLevel(skillIdentifier);
if ((int)newLevel > (int)prevLevel)
{
float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain);
foreach (Character character in Character.GetFriendlyCrew(Character))
{
character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilitySkillGain);
}
}
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
}
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
{
if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
{
increase *= 1f + Character.GetStatValue(statType);
}
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level)
{
if (Job == null) { return; }
var skill = Job.GetSkill(skillIdentifier);
if (skill == null)
{
Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false);
OnSkillChanged(skillIdentifier, 0.0f, level);
}
else
{
float prevLevel = skill.Level;
skill.Level = level;
OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
}
}
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
public void GiveExperience(int amount)
{
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
experienceGainMultiplier.Value += Character?.GetStatValue(StatTypes.ExperienceGainMultiplier) ?? 0;
amount = (int)(amount * experienceGainMultiplier.Value);
if (amount < 0) { return; }
ExperiencePoints += amount;
OnExperienceChanged(prevAmount, ExperiencePoints);
}
public void SetExperience(int newExperience)
{
if (newExperience < 0) { return; }
int prevAmount = ExperiencePoints;
ExperiencePoints = newExperience;
OnExperienceChanged(prevAmount, ExperiencePoints);
}
const int BaseExperienceRequired = 450;
const int AddedExperienceRequiredPerLevel = 500;
public int GetTotalTalentPoints()
{
return GetCurrentLevel() + AdditionalTalentPoints;
}
public int GetAvailableTalentPoints()
{
// hashset always has at least 1
return Math.Max(GetTotalTalentPoints() - GetUnlockedTalentsInTree().Count(), 0);
}
public float GetProgressTowardsNextLevel()
{
return (ExperiencePoints - GetExperienceRequiredForCurrentLevel()) / (float)(GetExperienceRequiredToLevelUp() - GetExperienceRequiredForCurrentLevel());
}
public int GetExperienceRequiredForCurrentLevel()
{
GetCurrentLevel(out int experienceRequired);
return experienceRequired;
}
public int GetExperienceRequiredToLevelUp()
{
int level = GetCurrentLevel(out int experienceRequired);
return experienceRequired + ExperienceRequiredPerLevel(level);
}
/// <summary>
/// How much more experience does the character need to reach the specified level?
/// </summary>
public int GetExperienceRequiredForLevel(int level)
{
int currentLevel = GetCurrentLevel();
if (currentLevel >= level) { return 0; }
int required = 0;
for (int i = 0; i < level; i++)
{
required += ExperienceRequiredPerLevel(i);
}
return required - ExperiencePoints;
}
public int GetCurrentLevel()
{
return GetCurrentLevel(out _);
}
private int GetCurrentLevel(out int experienceRequired)
{
int level = 0;
experienceRequired = 0;
while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints)
{
experienceRequired += ExperienceRequiredPerLevel(level);
level++;
}
return level;
}
private static int ExperienceRequiredPerLevel(int level)
{
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
}
partial void OnExperienceChanged(int prevAmount, int newAmount);
partial void OnPermanentStatChanged(StatTypes statType);
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
if (!item.HasTag("identitycard".ToIdentifier()) && !item.HasTag("despawncontainer".ToIdentifier())) { continue; }
foreach (var tag in item.Tags.Split(','))
{
var splitTag = tag.Split(":");
if (splitTag.Length < 2) { continue; }
if (splitTag[0] != "name") { continue; }
if (splitTag[1] != Name) { continue; }
item.ReplaceTag(tag, $"name:{newName}");
var idCard = item.GetComponent<IdCard>();
if (idCard != null)
{
idCard.OwnerName = newName;
}
break;
}
}
Name = newName;
}
public void ResetName()
{
Name = OriginalName;
}
public XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
charElement.Add(
new XAttribute("name", Name),
new XAttribute("originalname", OriginalName),
new XAttribute("speciesname", SpeciesName),
new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex),
new XAttribute("moustacheindex", Head.MoustacheIndex),
new XAttribute("faceattachmentindex", Head.FaceAttachmentIndex),
new XAttribute("skincolor", XMLExtensions.ColorToString(Head.SkinColor)),
new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
new XAttribute("permanentlydead", PermanentlyDead),
new XAttribute("renamingenabled", RenamingEnabled)
);
if (HumanPrefabIds != default)
{
charElement.Add(
new XAttribute("npcsetid", HumanPrefabIds.NpcSetIdentifier),
new XAttribute("npcid", HumanPrefabIds.NpcIdentifier));
}
charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath));
if (!MinReputationToHire.factionId.IsEmpty)
{
charElement.Add(
new XAttribute("factionId", MinReputationToHire.factionId),
new XAttribute("minreputation", MinReputationToHire.reputation));
}
if (Character != null)
{
if (Character.AnimController.CurrentHull != null)
{
charElement.Add(new XAttribute("hull", Character.AnimController.CurrentHull.ID));
}
}
Job.Save(charElement);
XElement savedStatElement = new XElement("savedstatvalues");
foreach (var statValuePair in SavedStatValues)
{
foreach (var savedStat in statValuePair.Value)
{
if (savedStat.StatValue == 0f) { continue; }
savedStatElement.Add(new XElement("savedstatvalue",
new XAttribute("stattype", statValuePair.Key.ToString()),
new XAttribute("statidentifier", savedStat.StatIdentifier),
new XAttribute("statvalue", savedStat.StatValue),
new XAttribute("removeondeath", savedStat.RemoveOnDeath)
));
}
}
XElement talentElement = new XElement("Talents");
talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
}
charElement.Add(savedStatElement);
charElement.Add(talentElement);
parentElement?.Add(charElement);
return charElement;
}
public static void SaveOrders(XElement parentElement, params Order[] orders)
{
if (parentElement == null || orders == null || orders.None()) { return; }
// If an order is invalid, we discard the order and increase the priority of the following orders so
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
// 2) the order priorities will remain sequential.
int priorityIncrease = 0;
var linkedSubs = GetLinkedSubmarines();
foreach (var orderInfo in orders)
{
var order = orderInfo;
if (order == null || order.Identifier == Identifier.Empty)
{
DebugConsole.ThrowError("Error saving an order - the order or its identifier is null");
priorityIncrease++;
continue;
}
int? linkedSubIndex = null;
bool targetAvailableInNextLevel = true;
if (order.TargetSpatialEntity != null)
{
var entitySub = order.TargetSpatialEntity.Submarine;
bool isOutside = entitySub == null;
bool canBeOnLinkedSub = !isOutside && Submarine.MainSub != null && entitySub != Submarine.MainSub && linkedSubs.Any();
bool isOnConnectedLinkedSub = false;
if (canBeOnLinkedSub)
{
for (int i = 0; i < linkedSubs.Count; i++)
{
var ls = linkedSubs[i];
if (!ls.LoadSub) { continue; }
if (ls.Sub != entitySub) { continue; }
linkedSubIndex = i;
isOnConnectedLinkedSub = Submarine.MainSub.GetConnectedSubs().Contains(entitySub);
break;
}
}
targetAvailableInNextLevel =
!isOutside &&
GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
(isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
if (!targetAvailableInNextLevel)
{
if (!order.Prefab.CanBeGeneralized)
{
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.");
priorityIncrease++;
continue;
}
else
{
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.");
}
}
}
if (orderInfo.ManualPriority < 1)
{
DebugConsole.ThrowError($"Error saving an order ({order.Identifier}) - the order priority is less than 1");
priorityIncrease++;
continue;
}
var orderElement = new XElement("order",
new XAttribute("id", order.Identifier),
new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease),
new XAttribute("targettype", (int)order.TargetType));
if (orderInfo.Option != Identifier.Empty)
{
orderElement.Add(new XAttribute("option", orderInfo.Option));
}
if (order.OrderGiver != null)
{
orderElement.Add(new XAttribute("ordergiver", order.OrderGiver.Info?.GetIdentifier()));
}
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
{
if (targetSub == Submarine.MainSub)
{
orderElement.Add(new XAttribute("onmainsub", true));
}
else if(linkedSubIndex.HasValue)
{
orderElement.Add(new XAttribute("linkedsubindex", linkedSubIndex));
}
}
switch (order.TargetType)
{
case Order.OrderTargetType.Entity when targetAvailableInNextLevel && order.TargetEntity is Entity e:
orderElement.Add(new XAttribute("targetid", (uint)e.ID));
break;
case Order.OrderTargetType.Position when targetAvailableInNextLevel && order.TargetSpatialEntity is OrderTarget ot:
var orderTargetElement = new XElement("ordertarget");
var position = ot.WorldPosition;
if (ot.Hull != null)
{
orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID));
position -= ot.Hull.WorldPosition;
}
orderTargetElement.Add(new XAttribute("position", XMLExtensions.Vector2ToString(position)));
orderElement.Add(orderTargetElement);
break;
case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue:
orderElement.Add(new XAttribute("structureid", s.ID));
orderElement.Add(new XAttribute("wallsectionindex", order.WallSectionIndex.Value));
break;
}
parentElement.Add(orderElement);
}
}
/// <summary>
/// Save current orders to the parameter element
/// </summary>
public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement)
{
var currentOrders = new List<Order>(characterInfo.CurrentOrders);
// Sort the current orders to make sure the one with the highest priority comes first
currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
SaveOrders(parentElement, currentOrders.ToArray());
}
/// <summary>
/// Save current orders to <see cref="OrderData"/>
/// </summary>
public void SaveOrderData()
{
OrderData = new XElement("orders");
SaveOrderData(this, OrderData);
}
public static void ApplyOrderData(Character character, XElement orderData)
{
if (character == null) { return; }
var orders = LoadOrders(orderData);
foreach (var order in orders)
{
character.SetOrder(order, isNewOrder: true, speak: false, force: true);
}
}
public void ApplyOrderData()
{
ApplyOrderData(Character, OrderData);
}
public static List<Order> LoadOrders(XElement ordersElement)
{
var orders = new List<Order>();
if (ordersElement == null) { return orders; }
// If an order is invalid, we discard the order and increase the priority of the following orders so
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
// 2) the order priorities will remain sequential.
int priorityIncrease = 0;
var linkedSubs = GetLinkedSubmarines();
foreach (var orderElement in ordersElement.GetChildElements("order"))
{
Order order = null;
string orderIdentifier = orderElement.GetAttributeString("id", "");
if (!OrderPrefab.Prefabs.TryGet(orderIdentifier, out OrderPrefab orderPrefab))
{
DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
priorityIncrease++;
continue;
}
var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
Character orderGiver = null;
if (orderElement.GetAttribute("ordergiver") is XAttribute orderGiverIdAttribute)
{
int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0);
orderGiver = Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId);
}
Entity targetEntity = null;
switch (targetType)
{
case Order.OrderTargetType.Entity:
ushort targetId = (ushort)orderElement.GetAttributeUInt("targetid", Entity.NullEntityID);
if (!GetTargetEntity(targetId, out targetEntity)) { continue; }
var targetComponent = orderPrefab.GetTargetItemComponent(targetEntity as Item);
order = new Order(orderPrefab, targetEntity, targetComponent, orderGiver: orderGiver);
break;
case Order.OrderTargetType.Position:
var orderTargetElement = orderElement.GetChildElement("ordertarget");
var position = orderTargetElement.GetAttributeVector2("position", Vector2.Zero);
ushort hullId = (ushort)orderTargetElement.GetAttributeUInt("hullid", 0);
if (!GetTargetEntity(hullId, out targetEntity)) { continue; }
if (!(targetEntity is Hull targetPositionHull))
{
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull");
priorityIncrease++;
continue;
}
var orderTarget = new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull);
order = new Order(orderPrefab, orderTarget, orderGiver: orderGiver);
break;
case Order.OrderTargetType.WallSection:
ushort structureId = (ushort)orderElement.GetAttributeInt("structureid", Entity.NullEntityID);
if (!GetTargetEntity(structureId, out targetEntity)) { continue; }
int wallSectionIndex = orderElement.GetAttributeInt("wallsectionindex", 0);
if (!(targetEntity is Structure targetStructure))
{
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure");
priorityIncrease++;
continue;
}
order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver);
break;
}
Identifier orderOption = orderElement.GetAttributeIdentifier("option", "");
int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease;
var orderInfo = order.WithOption(orderOption).WithManualPriority(manualPriority);
orders.Add(orderInfo);
bool GetTargetEntity(ushort targetId, out Entity targetEntity)
{
targetEntity = null;
if (targetId == Entity.NullEntityID) { return true; }
Submarine parentSub = null;
if (orderElement.GetAttributeBool("onmainsub", false))
{
parentSub = Submarine.MainSub;
}
else
{
int linkedSubIndex = orderElement.GetAttributeInt("linkedsubindex", -1);
if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count &&
linkedSubs[linkedSubIndex] is LinkedSubmarine linkedSub && linkedSub.LoadSub)
{
parentSub = linkedSub.Sub;
}
}
if (parentSub != null)
{
targetId = GetOffsetId(parentSub, targetId);
targetEntity = Entity.FindEntityByID(targetId);
return targetEntity != null;
}
else
{
if (!orderPrefab.CanBeGeneralized)
{
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.");
priorityIncrease++;
return false;
}
else
{
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.");
}
return true;
}
}
}
return orders;
}
private static List<LinkedSubmarine> GetLinkedSubmarines()
{
return Entity.GetEntities()
.OfType<LinkedSubmarine>()
.Where(ls => ls.Submarine == Submarine.MainSub)
.OrderBy(e => e.ID)
.ToList();
}
private static ushort GetOffsetId(Submarine parentSub, ushort id)
{
if (parentSub != null)
{
var idRemap = new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset);
return idRemap.GetOffsetId(id);
}
return id;
}
public static void ApplyHealthData(Character character, XElement healthData, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); }
}
/// <summary>
/// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites.
/// </summary>
public void ReloadHeadAttachments()
{
ResetLoadedAttachments();
LoadHeadAttachments();
}
private void ResetAttachmentIndices()
{
Head.ResetAttachmentIndices();
}
private void ResetLoadedAttachments()
{
hairs = null;
beards = null;
moustaches = null;
faceAttachments = null;
}
public void ClearCurrentOrders()
{
CurrentOrders.Clear();
}
public void Remove()
{
Character = null;
HeadSprite = null;
Portrait = null;
AttachmentSprites = null;
}
private void RefreshHeadSprites()
{
_headSprite = null;
LoadHeadSprite();
#if CLIENT
CalculateHeadPosition(_headSprite);
#endif
attachmentSprites?.Clear();
LoadAttachmentSprites();
}
// This could maybe be a LookUp instead?
public readonly Dictionary<StatTypes, List<SavedStatValue>> SavedStatValues = new Dictionary<StatTypes, List<SavedStatValue>>();
public void ClearSavedStatValues()
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
OnPermanentStatChanged(statType);
}
SavedStatValues.Clear();
}
public void ClearSavedStatValues(StatTypes statType)
{
SavedStatValues.Remove(statType);
OnPermanentStatChanged(statType);
}
public void RemoveSavedStatValuesOnDeath()
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (!savedStatValue.RemoveOnDeath) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
// no need to make a network update, as this is only done after the character has died
}
}
}
public void ResetSavedStatValue(Identifier statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
{
if (statIdentifier == identifier) { return true; }
if (identifier.IndexOf('*') is var index and > -1)
{
return statIdentifier.StartsWith(identifier[0..index]);
}
return false;
}
}
public float GetSavedStatValue(StatTypes statType)
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Sum(v => v.StatValue);
}
else
{
return 0f;
}
}
public float GetSavedStatValue(StatTypes statType, Identifier statIdentifier)
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Where(value => ToolBox.StatIdentifierMatches(value.StatIdentifier, statIdentifier)).Sum(static v => v.StatValue);
}
else
{
return 0f;
}
}
/// <summary>
/// Get the combined stat value of the identifier "all" and the specified identifier.
/// </summary>
/// <remarks>
/// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but
/// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent
/// that increases the fabrication quality of every single item we could use something like:
/// <CharacterAbilityGivePermanentStat stattype="IncreaseFabricationQuality" statidentifier="all" />
/// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code)
/// </remarks>
public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
GetSavedStatValue(statType, statIdentifier);
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot));
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in bots)
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
}
return statValue;
}
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
{
SavedStatValues.Add(statType, new List<SavedStatValue>());
}
bool changed = false;
if (SavedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat)
{
float prevValue = savedStat.StatValue;
savedStat.StatValue = setValue ? value : MathHelper.Min(savedStat.StatValue + value, maxValue);
changed = !MathUtils.NearlyEqual(savedStat.StatValue, prevValue);
}
else
{
SavedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath));
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
/// <summary>
/// Used to store the last known resistance against skill loss on death
/// when the character dies, so it can be correctly applied before
/// reinstantiating the Character object (if respawning).
/// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance
/// </summary>
public float LastResistanceMultiplierSkillLossDeath = 1.0f;
/// <summary>
/// Used to store the last known resistance against skill loss on respawn
/// when the character dies, so it can be correctly applied before
/// reinstantiating the Character object (if respawning).
/// NOTE: The resistances are handled as multipliers here, so 1.0 == 0% resistance
/// </summary>
public float LastResistanceMultiplierSkillLossRespawn = 1.0f;
}
internal sealed class SavedStatValue
{
public Identifier StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
StatIdentifier = statIdentifier;
}
}
internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
{
Value = skillAmount;
SkillIdentifier = skillIdentifier;
Character = character;
GainedFromAbility = gainedFromAbility;
}
public Character Character { get; set; }
public float Value { get; set; }
public Identifier SkillIdentifier { get; set; }
public bool GainedFromAbility { get; }
}
class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue
{
public AbilityExperienceGainMultiplier(float experienceGainMultiplier)
{
Value = experienceGainMultiplier;
}
public float Value { get; set; }
}
}