Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs
T
2022-02-10 02:52:08 +09:00

1904 lines
76 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma
{
public enum Gender { None, Male, Female };
public enum Race { None, White, Black, Brown, Asian };
partial class CharacterInfo
{
public class HeadInfo
{
private int _headSpriteId;
public int HeadSpriteId
{
get { return _headSpriteId; }
set
{
_headSpriteId = Math.Max(Math.Clamp(value, (int)headSpriteRange.X, (int)headSpriteRange.Y), 1);
GetSpriteSheetIndex();
}
}
public Vector2? SheetIndex { get; private set; }
public Vector2 headSpriteRange;
public Gender gender;
public Race race;
public Color HairColor;
public Color FacialHairColor;
public Color SkinColor;
public int HairIndex { get; set; } = -1;
public int BeardIndex { get; set; } = -1;
public int MoustacheIndex { get; set; } = -1;
public int FaceAttachmentIndex { get; set; } = -1;
public XElement HairElement { get; set; }
public XElement HairWithHatElement { get; set; }
public XElement BeardElement { get; set; }
public XElement MoustacheElement { get; set; }
public XElement FaceAttachment { get; set; }
public HeadInfo() { }
public HeadInfo(int headId, Gender gender, Race race, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0)
{
_headSpriteId = Math.Max(headId, 1);
this.gender = gender;
this.race = race;
HairIndex = hairIndex;
BeardIndex = beardIndex;
MoustacheIndex = moustacheIndex;
FaceAttachmentIndex = faceAttachmentIndex;
GetSpriteSheetIndex();
}
public void ResetAttachmentIndices()
{
HairIndex = -1;
BeardIndex = -1;
MoustacheIndex = -1;
FaceAttachmentIndex = -1;
}
public void GetSpriteSheetIndex()
{
if (heads != null && heads.Any())
{
var matchingHead = heads.Keys.FirstOrDefault(h => h.ID == HeadSpriteId && IsMatchingGender(h.Gender, gender) && IsMatchingRace(h.Race, race));
if (matchingHead != null)
{
if (heads.TryGetValue(matchingHead, out Vector2 index))
{
SheetIndex = index;
}
}
}
}
}
private HeadInfo head;
public HeadInfo Head
{
get { return head; }
set
{
if (head != value && value != null)
{
head = value;
if (!IsValidRace(head.race))
{
head.race = GetRandomRace(Rand.RandSync.Unsynced);
}
CalculateHeadSpriteRange();
Head.HeadSpriteId = value.HeadSpriteId;
RefreshHeadSprites();
}
}
}
public Dictionary<HeadPreset, Vector2> Heads
{
get
{
if (heads == null)
{
LoadHeadPresets();
}
return heads;
}
}
private static Dictionary<HeadPreset, Vector2> heads;
public class HeadPreset : ISerializableEntity
{
[Serialize(Race.None, false)]
public Race Race { get; private set; }
[Serialize(Gender.None, false)]
public Gender Gender { get; private set; }
[Serialize(0, false)]
public int ID { get; private set; }
[Serialize("0,0", false)]
public Vector2 SheetIndex { get; private set; }
public string Name => $"Head Preset {Race} {Gender} {ID}";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
public HeadPreset(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
public XElement InventoryData;
public XElement HealthData;
public XElement OrderData;
private static ushort idCounter;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
public string OriginalName { get; private set; }
public string Name;
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)
{
var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card);
if (idCard == null) { return disguiseName; }
//Disguise as the ID card name if it's equipped
string[] readTags = idCard.Tags.Split(',');
foreach (string tag in readTags)
{
string[] s = tag.Split(':');
if (s[0] == "name")
{
return s[1];
}
}
}
return disguiseName;
}
}
private string _speciesName;
public string SpeciesName
{
get
{
if (_speciesName == null)
{
_speciesName = CharacterConfigElement.GetAttributeString("speciesname", string.Empty).ToLowerInvariant();
}
return _speciesName;
}
set { _speciesName = value; }
}
/// <summary>
/// Note: Can be null.
/// </summary>
public Character Character;
public Job Job;
public int Salary;
public int ExperiencePoints { get; private set; }
public HashSet<string> UnlockedTalents { get; private set; } = new HashSet<string>();
/// <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<string> GetUnlockedTalentsInTree()
{
if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<string>(); }
return UnlockedTalents.Where(t => talentTree.TalentIsInTree(t));
}
/// <summary>
/// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them
/// </summary>
public IEnumerable<string> GetEndocrineTalents()
{
if (!TalentTree.JobTalentTrees.TryGetValue(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<string>(); }
return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t));
}
public int AdditionalTalentPoints { get; set; }
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;
}
}
public bool OmitJobInPortraitClothing;
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)
{
Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).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.ReduceAffliction(Character.AnimController.GetLimb(LimbType.Head), "disguised", 100f);
}
}
private List<WearableSprite> attachmentSprites;
public List<WearableSprite> AttachmentSprites
{
get
{
if (attachmentSprites == null)
{
LoadAttachmentSprites(OmitJobInPortraitClothing);
}
return attachmentSprites;
}
private set
{
if (attachmentSprites != null)
{
attachmentSprites.ForEach(s => s.Sprite?.Remove());
}
attachmentSprites = value;
}
}
public XElement CharacterConfigElement { get; set; }
public readonly string ragdollFileName = string.Empty;
public bool StartItemsGiven;
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
public CharacterTeamType TeamID;
private NPCPersonalityTrait personalityTrait;
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 (CurrentOrders[i].Order is Order currentOrder && order.AssignmentPriority >= currentOrder.AssignmentPriority)
{
break;
}
else
{
orderPriority--;
}
}
return Math.Max(orderPriority, 1);
}
else
{
return HighestManualOrderPriority;
}
}
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
public List<string> SpriteTags
{
get;
private set;
}
public NPCPersonalityTrait PersonalityTrait
{
get { return personalityTrait; }
}
/// <summary>
/// Setting the value with this property also resets the head attachments. Use Head.headSpriteId if you don't want that.
/// </summary>
public int HeadSpriteId
{
get { return Head.HeadSpriteId; }
set
{
Head.HeadSpriteId = value;
ResetHeadAttachments();
RefreshHeadSprites();
}
}
public readonly bool HasGenders;
public readonly bool HasRaces;
public Gender Gender
{
get { return Head.gender; }
set
{
Gender previousValue = Head.gender;
Head.gender = value;
if (!IsValidGender(Head.gender))
{
Head.gender = GetDefaultGender();
}
if (Head.gender != previousValue)
{
CalculateHeadSpriteRange();
ResetHeadAttachments();
RefreshHeadSprites();
}
}
}
public Race Race
{
get { return Head.race; }
set
{
Race previousValue = Head.race;
Head.race = value;
if (!IsValidRace(Head.race))
{
Head.race = GetDefaultRace();
}
if (Head.race != previousValue)
{
CalculateHeadSpriteRange();
ResetHeadAttachments();
RefreshHeadSprites();
}
}
}
private bool IsValidRace(Race race) => HasRaces ? race != Race.None : race == Race.None;
private bool IsValidGender(Gender gender) => HasGenders ? gender != Gender.None : gender == Gender.None;
private Gender GetDefaultGender() => HasGenders ? Gender.Male : Gender.None;
private Race GetDefaultRace() => HasRaces ? Race.White : Race.None;
public int HairIndex
{
get => Head.HairIndex;
set => Head.HairIndex = value;
}
public int BeardIndex
{
get => Head.BeardIndex;
set => Head.BeardIndex = value;
}
public int MoustacheIndex
{
get => Head.MoustacheIndex;
set => Head.MoustacheIndex = value;
}
public int FaceAttachmentIndex
{
get => Head.FaceAttachmentIndex;
set => Head.FaceAttachmentIndex = value;
}
public readonly ImmutableArray<(Color Color, float Commonness)> HairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors;
public Color HairColor
{
get => Head.HairColor;
set => Head.HairColor = value;
}
public Color FacialHairColor
{
get => Head.FacialHairColor;
set => Head.FacialHairColor = value;
}
public Color SkinColor
{
get => Head.SkinColor;
set => Head.SkinColor = value;
}
public XElement HairElement => Head.HairElement;
public XElement BeardElement => Head.BeardElement;
public XElement MoustacheElement => Head.MoustacheElement;
public XElement FaceAttachment => Head.FaceAttachment;
private RagdollParams ragdoll;
public RagdollParams Ragdoll
{
get
{
if (ragdoll == null)
{
// TODO: support for variants
string speciesName = SpeciesName;
bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase));
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
}
return ragdoll;
}
set { ragdoll = value; }
}
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// talent-relevant values
public int MissionsCompletedSinceDeath = 0;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "")
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
}
ID = idCounter;
idCounter++;
_speciesName = speciesName;
SpriteTags = new List<string>();
XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument;
if (doc == null) { return; }
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
// TODO: support for variants
Head = new HeadInfo();
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
HasRaces = CharacterConfigElement.GetAttributeBool("races", false);
SetGenderAndRace(randSync);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant);
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();
SetColors();
if (!string.IsNullOrEmpty(name))
{
Name = name;
}
else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle)
{
Name = npcTitle;
}
else
{
Name = GetRandomName(randSync);
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
SetPersonalityTrait();
Salary = CalculateSalary();
if (ragdollFileName != null)
{
this.ragdollFileName = ragdollFileName;
}
LoadHeadAttachments();
}
private void SetPersonalityTrait()
{
personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId);
}
public string GetRandomName(Rand.RandSync randSync)
{
string name = "";
if (CharacterConfigElement.Element("name") != null)
{
string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
name = ToolBox.GetRandomLine(firstNamePath, randSync);
}
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
if (name != "") { name += " "; }
name += ToolBox.GetRandomLine(lastNamePath, randSync);
}
}
return name;
}
public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array)
=> ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), Rand.RandSync.Unsynced)
.Color;
private void SetGenderAndRace(Rand.RandSync randSync)
{
Head.gender = GetRandomGender(randSync);
Head.race = GetRandomRace(randSync);
CalculateHeadSpriteRange();
HeadSpriteId = GetRandomHeadID(randSync);
}
private void SetColors()
{
HairColor = SelectRandomColor(HairColors);
FacialHairColor = SelectRandomColor(FacialHairColors);
SkinColor = SelectRandomColor(SkinColors);
}
private void CheckColors()
{
if (HairColor == Color.Black)
{
HairColor = SelectRandomColor(HairColors);
}
if (FacialHairColor == Color.Black)
{
FacialHairColor = SelectRandomColor(FacialHairColors);
}
if (SkinColor == Color.Black)
{
SkinColor = SelectRandomColor(SkinColors);
}
}
// Used for loading the data
public CharacterInfo(XElement infoElement)
{
ID = idCounter;
idCounter++;
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<string>(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
Enum.TryParse(infoElement.GetAttributeString("race", "None"), true, out Race race);
Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender);
_speciesName = infoElement.GetAttributeString("speciesname", null);
XDocument doc = null;
if (_speciesName != null)
{
doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument;
}
else
{
// Backwards support (human only)
string file = infoElement.GetAttributeString("file", "");
doc = XMLExtensions.TryLoadXml(file);
}
if (doc == null) { return; }
// TODO: support for variants
CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root;
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
HasRaces = CharacterConfigElement.GetAttributeBool("hasraces", false);
if (!IsValidGender(gender))
{
gender = GetRandomGender(Rand.RandSync.Unsynced);
}
if (!IsValidRace(race))
{
race = GetRandomRace(Rand.RandSync.Unsynced);
}
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();
RecreateHead(
infoElement.GetAttributeInt("headspriteid", 1),
race,
gender,
infoElement.GetAttributeInt("hairindex", -1),
infoElement.GetAttributeInt("beardindex", -1),
infoElement.GetAttributeInt("moustacheindex", -1),
infoElement.GetAttributeInt("faceattachmentindex", -1));
//backwards compatibility
if (infoElement.Attribute("skincolor") == null && infoElement.Attribute("race") != null)
{
string raceStr = infoElement.GetAttributeString("race", string.Empty);
Race obsoleteRace = Race.None;
Enum.TryParse(raceStr, ignoreCase: true, out obsoleteRace);
switch (obsoleteRace)
{
case Race.White:
case Race.None:
SkinColor = new Color(255, 215, 200, 255);
break;
case Race.Brown:
SkinColor = new Color(158, 95, 72, 255);
break;
case Race.Black:
SkinColor = new Color(153, 75, 42, 255);
break;
case Race.Asian:
SkinColor = new Color(191, 116, 61, 255);
break;
}
}
else
{
SkinColor = infoElement.GetAttributeColor("skincolor", Color.Black);
}
HairColor = infoElement.GetAttributeColor("haircolor", Color.Black);
FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Black);
CheckColors();
if (string.IsNullOrEmpty(Name))
{
if (CharacterConfigElement.Element("name") != null)
{
string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", "");
if (firstNamePath != "")
{
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
if (Name != "") Name += " ";
Name += ToolBox.GetRandomLine(lastNamePath);
}
}
}
if (string.IsNullOrEmpty(OriginalName))
{
OriginalName = Name;
}
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
string personalityName = infoElement.GetAttributeString("personality", "");
ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty);
if (!string.IsNullOrEmpty(personalityName))
{
personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName);
}
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
foreach (XElement subElement in infoElement.Elements())
{
bool jobCreated = false;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !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 (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase))
{
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; }
string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(statIdentifier))
{
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);
}
}
}
LoadHeadAttachments();
}
public Gender GetRandomGender(Rand.RandSync randSync)
{
if (HasGenders)
{
return (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
}
return Gender.None;
}
public Race GetRandomRace(Rand.RandSync randSync)
{
if (HasRaces)
{
return new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync);
}
return Race.None;
}
public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0;
private List<XElement> hairs;
private List<XElement> beards;
private List<XElement> moustaches;
private List<XElement> faceAttachments;
private IEnumerable<XElement> wearables;
public IEnumerable<XElement> Wearables
{
get
{
if (wearables == null)
{
var attachments = CharacterConfigElement.Element("HeadAttachments");
if (attachments != null)
{
wearables = attachments.Elements("Wearable");
}
}
return wearables;
}
}
public int GetIdentifier()
{
return GetIdentifier(Name);
}
public int GetIdentifierUsingOriginalName()
{
return GetIdentifier(OriginalName);
}
private int GetIdentifier(string name)
{
int id = ToolBox.StringToInt(name);
id ^= HeadSpriteId;
id ^= (int)Race << 6;
id ^= HairIndex << 12;
id ^= BeardIndex << 18;
id ^= MoustacheIndex << 24;
id ^= FaceAttachmentIndex << 30;
if (Job != null)
{
id ^= ToolBox.StringToInt(Job.Prefab.Identifier);
}
return id;
}
public IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType, int headSpriteId)
{
if (elements == null) { return elements; }
return elements.Where(e =>
{
if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
int headId = e.GetAttributeInt("headid", -1);
// if the head id is less than 1, the id is not valid and the condition is ignored.
return headId < 1 || headId == headSpriteId;
});
}
public IEnumerable<XElement> FilterElementsByGenderAndRace(IEnumerable<XElement> elements, Gender gender, Race race)
{
if (elements == null) { return elements; }
return elements.Where(w =>
IsMatchingGender(Enum.Parse<Gender>(w.GetAttributeString("gender", "None"), ignoreCase: true), gender) &&
IsMatchingRace(Enum.Parse<Race>(w.GetAttributeString("race", "None"), ignoreCase: true), race));
}
public static bool IsMatchingGender(Gender gender, Gender myGender) => gender == Gender.None || gender == myGender;
public static bool IsMatchingRace(Race race, Race myRace) => race == Race.None || race == myRace;
private void LoadHeadPresets()
{
if (CharacterConfigElement == null) { return; }
heads = new Dictionary<HeadPreset, Vector2>();
var headsElement = CharacterConfigElement.GetChildElement("heads");
if (headsElement != null)
{
foreach (var head in headsElement.GetChildElements("head"))
{
var preset = new HeadPreset(head);
heads.Add(preset, preset.SheetIndex);
}
}
}
private void CalculateHeadSpriteRange()
{
if (CharacterConfigElement == null) { return; }
Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero);
// If the range is defined, we use it as it is
if (Head.headSpriteRange != Vector2.Zero) { return; }
if (heads == null)
{
LoadHeadPresets();
}
// If there are any head presets defined, use them.
if (heads.Any())
{
var ids = heads.Keys.Where(h => IsMatchingRace(Race, h.Race) && IsMatchingGender(Gender, h.Gender)).Select(w => w.ID);
ids = ids.OrderBy(id => id);
if (ids.Any())
{
Head.headSpriteRange = new Vector2(ids.First(), ids.Last());
}
else
{
DebugConsole.ThrowError($"[CharacterInfo] Couldn't find a head definition that matches {Race} and {Gender}!");
}
}
// Else we calculate the range from the wearables.
if (Head.headSpriteRange == Vector2.Zero)
{
var wearableElements = Wearables;
if (wearableElements == null) { return; }
var wearables = FilterElementsByGenderAndRace(wearableElements, head.gender, head.race).ToList();
if (wearables == null)
{
Head.headSpriteRange = Vector2.Zero;
return;
}
if (wearables.None())
{
DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {Head.gender} and the race {Head.race} could be found. Total wearables found: {Wearables.Count()}.");
return;
}
else
{
// Ignore head ids that are less than 1, because they are not supported.
var ids = wearables.Select(w => w.GetAttributeInt("headid", -1)).Where(id => id > 0);
if (ids.None())
{
DebugConsole.ThrowError($"[CharacterInfo] Wearables with matching gender and race were found but none with a valid headid! Total wearables found: {Wearables.Count()}.");
return;
}
ids = ids.OrderBy(id => id);
Head.headSpriteRange = new Vector2(ids.First(), ids.Last());
}
}
}
public void RecreateHead(HeadInfo headInfo)
{
RecreateHead(
headInfo.HeadSpriteId,
headInfo.race,
headInfo.gender,
headInfo.HairIndex,
headInfo.BeardIndex,
headInfo.MoustacheIndex,
headInfo.FaceAttachmentIndex);
SkinColor = headInfo.SkinColor;
HairColor = headInfo.HairColor;
FacialHairColor = headInfo.FacialHairColor;
CheckColors();
}
/// <summary>
/// Recreates the head info and checks that everything is valid.
/// </summary>
public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
{
if (!IsValidGender(gender))
{
gender = GetRandomGender(Rand.RandSync.Unsynced);
}
if (!IsValidRace(race))
{
race = GetRandomRace(Rand.RandSync.Unsynced);
}
if (heads == null)
{
LoadHeadPresets();
}
Color skin = Color.Black;
Color hair = Color.Black;
Color facialHair = Color.Black;
if (head != null)
{
skin = head.SkinColor;
hair = head.HairColor;
facialHair = head.FacialHairColor;
}
head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex)
{
SkinColor = skin,
HairColor = hair,
FacialHairColor = facialHair
};
CalculateHeadSpriteRange();
ReloadHeadAttachments();
RefreshHead();
}
/// <summary>
/// Reloads the head sprite and the attachment sprites.
/// </summary>
public void RefreshHead()
{
ReloadHeadAttachments();
RefreshHeadSprites();
}
partial void LoadHeadSpriteProjectSpecific(XElement limbElement);
private void LoadHeadSprite()
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
XElement spriteElement = limbElement.Element("sprite");
if (spriteElement == null) { continue; }
string spritePath = spriteElement.Attribute("texture").Value;
if (string.IsNullOrEmpty(spritePath)) { continue; }
spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant());
spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString());
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; }
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).ToList();
if (SpriteTags.Any())
{
SpriteTags.RemoveAt(SpriteTags.Count - 1);
}
break;
}
LoadHeadSpriteProjectSpecific(limbElement);
break;
}
}
public void LoadHeadAttachments()
{
if (Wearables != null)
{
if (hairs == null)
{
float commonness = Gender == Gender.Female ? 0.05f : 0.2f;
hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Hair, head.HeadSpriteId), WearableType.Hair, commonness);
}
if (beards == null)
{
beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Beard, head.HeadSpriteId), WearableType.Beard);
}
if (moustaches == null)
{
moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Moustache, head.HeadSpriteId), WearableType.Moustache);
}
if (faceAttachments == null)
{
faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.FaceAttachment, head.HeadSpriteId), WearableType.FaceAttachment);
}
if (IsValidIndex(Head.HairIndex, hairs))
{
Head.HairElement = hairs[Head.HairIndex];
}
else
{
Head.HairElement = GetRandomElement(hairs);
Head.HairIndex = hairs.IndexOf(Head.HairElement);
}
if (Head.HairElement != null)
{
int thisHairIndex = hairs.IndexOf(head.HairElement);
int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex);
if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count)
{
head.HairWithHatElement = hairs[hairWithHatIndex];
}
else
{
head.HairWithHatElement = null;
}
}
if (IsValidIndex(Head.BeardIndex, beards))
{
Head.BeardElement = beards[Head.BeardIndex];
}
else
{
Head.BeardElement = GetRandomElement(beards);
Head.BeardIndex = beards.IndexOf(Head.BeardElement);
}
if (IsValidIndex(Head.MoustacheIndex, moustaches))
{
Head.MoustacheElement = moustaches[Head.MoustacheIndex];
}
else
{
Head.MoustacheElement = GetRandomElement(moustaches);
Head.MoustacheIndex = moustaches.IndexOf(Head.MoustacheElement);
}
if (IsValidIndex(Head.FaceAttachmentIndex, faceAttachments))
{
Head.FaceAttachment = faceAttachments[Head.FaceAttachmentIndex];
}
else
{
Head.FaceAttachment = GetRandomElement(faceAttachments);
Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment);
}
}
}
public static List<XElement> AddEmpty(IEnumerable<XElement> 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));
var list = new List<XElement>() { emptyElement };
list.AddRange(elements);
return list;
}
public XElement GetRandomElement(IEnumerable<XElement> 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.Name == "Empty" ? null : element;
}
private bool IsWearableAllowed(XElement element)
{
string spriteName = element.Element("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", new string[0]);
if (disallowed.Any(s => spriteName.Contains(s)))
{
return false;
}
}
return true;
}
public static bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
private static IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
partial void LoadAttachmentSprites(bool omitJob);
private int CalculateSalary()
{
if (Name == null || Job == null) { return 0; }
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)(skill.Level * skill.PriceMultiplier);
}
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string 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);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
float newLevel = Job.GetSkillLevel(skillIdentifier);
if ((int)newLevel > (int)prevLevel)
{
// 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);
}
public void SetSkillLevel(string skillIdentifier, float level)
{
if (Job == null) { return; }
var skill = Job.Skills.Find(s => s.Identifier == skillIdentifier);
if (skill == null)
{
Job.Skills.Add(new Skill(skillIdentifier, level));
OnSkillChanged(skillIdentifier, 0.0f, level);
}
else
{
float prevLevel = skill.Level;
skill.Level = level;
OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
}
}
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel);
public void GiveExperience(int amount, bool isMissionExperience = false)
{
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
}
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 = -50;
const int AddedExperienceRequiredPerLevel = 500;
public int GetTotalTalentPoints()
{
return GetCurrentLevel() + AdditionalTalentPoints - 1;
}
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);
}
public int GetCurrentLevel()
{
return GetCurrentLevel(out _);
}
private int GetCurrentLevel(out int experienceRequired)
{
int level = 1;
experienceRequired = 0;
while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints)
{
experienceRequired += ExperienceRequiredPerLevel(level);
level++;
}
return level;
}
private 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.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { 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}");
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("gender", Head.gender.ToString()),
new XAttribute("race", Head.race.ToString()),
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("headspriteid", HeadSpriteId),
new XAttribute("hairindex", HairIndex),
new XAttribute("beardindex", BeardIndex),
new XAttribute("moustacheindex", MoustacheIndex),
new XAttribute("faceattachmentindex", FaceAttachmentIndex),
new XAttribute("skincolor", XMLExtensions.ColorToString(SkinColor)),
new XAttribute("haircolor", XMLExtensions.ColorToString(HairColor)),
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("ragdoll", ragdollFileName),
new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name));
// TODO: animations?
charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath));
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)
));
}
}
charElement.Add(savedStatElement);
parentElement.Add(charElement);
return charElement;
}
public static void SaveOrders(XElement parentElement, params OrderInfo[] 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.Order;
if (order == null || string.IsNullOrEmpty(order.Identifier))
{
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?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
if (!targetAvailableInNextLevel)
{
if (!order.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 (!string.IsNullOrEmpty(orderInfo.OrderOption))
{
orderElement.Add(new XAttribute("option", orderInfo.OrderOption));
}
if (order.OrderGiver != null)
{
orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID));
}
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<OrderInfo>(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, order.Order?.OrderGiver, speak: false, force: true);
}
}
public void ApplyOrderData()
{
ApplyOrderData(Character, OrderData);
}
public static List<OrderInfo> LoadOrders(XElement ordersElement)
{
var orders = new List<OrderInfo>();
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", "");
var orderPrefab = Order.GetPrefab(orderIdentifier);
if (orderPrefab == null)
{
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);
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1);
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null;
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;
}
string orderOption = orderElement.GetAttributeString("option", "");
int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease;
var orderInfo = new OrderInfo(order, orderOption, 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);
}
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)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
}
/// <summary>
/// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites.
/// </summary>
private void ReloadHeadAttachments()
{
ResetLoadedAttachments();
LoadHeadAttachments();
}
/// <summary>
/// Loads only the elements according to the indices, not the sprites.
/// </summary>
private void ResetHeadAttachments()
{
ResetAttachmentIndices();
ResetLoadedAttachments();
}
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;
AttachmentSprites = null;
}
// 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(string statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (savedStatValue.StatIdentifier != statIdentifier) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
}
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, string statIdentifier)
{
if (SavedStatValues.TryGetValue(statType, out var statValues))
{
return statValues.Where(s => s.StatIdentifier.Equals(statIdentifier, StringComparison.OrdinalIgnoreCase)).Sum(v => v.StatValue);
}
else
{
return 0f;
}
}
public void ChangeSavedStatValue(StatTypes statType, float value, string 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); }
}
}
public class SavedStatValue
{
public string StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
StatIdentifier = statIdentifier;
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility)
{
Value = skillAmount;
SkillIdentifier = skillIdentifier;
Character = character;
GainedFromAbility = gainedFromAbility;
}
public Character Character { get; set; }
public float Value { get; set; }
public string SkillIdentifier { get; set; }
public bool GainedFromAbility { get; }
}
class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue
{
public AbilityExperienceGainMultiplier(float experienceGainMultiplier)
{
Value = experienceGainMultiplier;
}
public float Value { get; set; }
}
}