using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Lidgren.Network; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; namespace Barotrauma { public enum Gender { None, Male, Female }; public enum Race { None, White, Black, Asian }; partial class CharacterInfo { private static Dictionary cachedConfigs = new Dictionary(); private static ushort idCounter; public string Name; public string DisplayName { get { string disguiseName = "?"; if (Character == null || !Character.HideFace || (GameMain.Server != null && !GameMain.Server.AllowDisguises)) { return Name; } #if CLIENT if (GameMain.Client != null && !GameMain.Client.AllowDisguises) { return Name; } #endif if (Character.Inventory != null) { int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card); if (cardSlotIndex < 0) return disguiseName; var idCard = Character.Inventory.Items[cardSlotIndex]; 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; } } /// /// Note: Can be null. /// public Character Character; public readonly string File; public Job Job; public int Salary; private Vector2 headSpriteRange; private Sprite headSprite; public Sprite HeadSprite { get { if (headSprite == null) { LoadHeadSprite(); } return headSprite; } } private Sprite portrait; public Sprite Portrait { get { if (portrait == null) { LoadHeadSprite(); } return portrait; } } private Sprite clothingSprite; public Sprite ClothingSprite { get { if (clothingSprite == null) { if (Job != null && Job.Prefab.ClothingElement != null) { clothingSprite = new Sprite(Job.Prefab.ClothingElement.Element("sprite")); } } return clothingSprite; } } private Sprite portraitBackground; public Sprite PortraitBackground { get { if (portraitBackground == null) { var portraitBackgroundElement = SourceElement.Element("portraitbackground"); if (portraitBackgroundElement != null) { portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite")); } } return portraitBackground; } } private List attachmentSprites; public List AttachmentsSprites { get { if (attachmentSprites == null) { LoadAttachmentSprites(); } return attachmentSprites; } } public XElement SourceElement { get; set; } public XElement HairElement { get; private set; } public XElement BeardElement { get; private set; } public XElement MoustacheElement { get; private set; } public XElement FaceAttachment { get; private set; } 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 bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; public readonly string ragdollFileName = string.Empty; public bool StartItemsGiven; public CauseOfDeath CauseOfDeath; public byte TeamID; private NPCPersonalityTrait personalityTrait; //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 XElement InventoryData; public List SpriteTags { get; private set; } public NPCPersonalityTrait PersonalityTrait { get { return personalityTrait; } } private int headSpriteId; public int HeadSpriteId { get { return headSpriteId; } set { int oldId = headSpriteId; headSpriteId = value; Vector2 spriteRange = headSpriteRange; if (headSpriteId < (int)spriteRange.X) headSpriteId = (int)(spriteRange.Y); if (headSpriteId > (int)spriteRange.Y) headSpriteId = (int)(spriteRange.X); if (headSpriteId != oldId) { headSprite = null; attachmentSprites = null; ResetHeadAttachments(); } } } private Gender gender; public Gender Gender { get { return gender; } set { if (gender == value) return; gender = value; if (gender == Gender.None) { Gender = Gender.Male; //SetRandomGender(); } CalculateHeadSpriteRange(); ResetHeadAttachments(); headSprite = null; attachmentSprites = null; //SetRandomHead(); //LoadHeadSprite(); } } private Race race; public Race Race { get { return race; } set { if (race == value) { return; } race = value; if (race == Race.None) { race = Race.White; //SetRandomRace(); } CalculateHeadSpriteRange(); ResetHeadAttachments(); headSprite = null; attachmentSprites = null; //SetRandomHead(); //LoadHeadSprite(); } } private RagdollParams ragdoll; public RagdollParams Ragdoll { get { if (ragdoll == null) { string speciesName = SourceElement.GetAttributeString("name", string.Empty); bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false); ragdoll = isHumanoid ? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName) : RagdollParams.GetRagdollParams(speciesName, ragdollFileName) as RagdollParams; } return ragdoll; } set { ragdoll = value; } } // Used for creating the data public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null, string ragdollFileName = null) { ID = idCounter; idCounter++; File = file; SpriteTags = new List(); XDocument doc = GetConfig(file); SourceElement = doc.Root; if (doc.Root.GetAttributeBool("genders", false)) { this.gender = gender == Gender.None ? SetRandomGender() : gender; } Enum.TryParse(doc.Root.GetAttributeString("race", "None"), true, out race); if (race == Race.None) { SetRandomRace(); } CalculateHeadSpriteRange(); SetRandomHeadID(); Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab); if (!string.IsNullOrEmpty(name)) { Name = name; } else { name = ""; if (doc.Root.Element("name") != null) { string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", ""); if (firstNamePath != "") { firstNamePath = firstNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male"); Name = ToolBox.GetRandomLine(firstNamePath); } string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", ""); if (lastNamePath != "") { lastNamePath = lastNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male"); if (Name != "") Name += " "; Name += ToolBox.GetRandomLine(lastNamePath); } } } personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); Salary = CalculateSalary(); if (ragdollFileName != null) { this.ragdollFileName = ragdollFileName; } LoadHeadAttachments(); } // Used for loading the data public CharacterInfo(XElement element) { ID = idCounter; idCounter++; Name = element.GetAttributeString("name", "unnamed"); string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant(); gender = (genderStr == "male") ? Gender.Male : Gender.Female; Enum.TryParse(element.GetAttributeString("race", "white"), true, out race); File = element.GetAttributeString("file", ""); SourceElement = GetConfig(File).Root; Salary = element.GetAttributeInt("salary", 1000); headSpriteId = element.GetAttributeInt("headspriteid", 1); HairIndex = element.GetAttributeInt("hairindex", -1); BeardIndex = element.GetAttributeInt("beardindex", -1); MoustacheIndex = element.GetAttributeInt("moustacheindex", -1); FaceAttachmentIndex = element.GetAttributeInt("faceattachmentindex", -1); StartItemsGiven = element.GetAttributeBool("startitemsgiven", false); string personalityName = element.GetAttributeString("personality", ""); ragdollFileName = element.GetAttributeString("ragdoll", string.Empty); if (!string.IsNullOrEmpty(personalityName)) { personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); } foreach (XElement subElement in element.Elements()) { if (subElement.Name.ToString().ToLowerInvariant() != "job") continue; Job = new Job(subElement); break; } LoadHeadAttachments(); } public void LoadHeadSprite() { foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue; XElement spriteElement = limbElement.Element("sprite"); string spritePath = spriteElement.Attribute("texture").Value; spritePath = spritePath.Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male"); spritePath = spritePath.Replace("[RACE]", race.ToString().ToLowerInvariant()); spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString()); string fileName = Path.GetFileNameWithoutExtension(spritePath); //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.InvariantCultureIgnoreCase)) { 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; } break; } } public Gender SetRandomGender() => gender = (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; public Race SetRandomRace() => race = new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server); public int SetRandomHead() => HeadSpriteId = SetRandomHeadID(); private XDocument GetConfig(string file) { if (!cachedConfigs.TryGetValue(file, out XDocument doc)) { doc = XMLExtensions.TryLoadXml(file); if (doc == null) { return null; } cachedConfigs.Add(file, doc); } return doc; } private int SetRandomHeadID() { if (headSpriteRange != Vector2.Zero) { headSpriteId = Rand.Range((int)headSpriteRange.X, (int)headSpriteRange.Y + 1, Rand.RandSync.Server); } else { headSpriteId = 0; } return headSpriteId; } private List hairs; private List beards; private List moustaches; private List faceAttachments; private IEnumerable wearables; private IEnumerable Wearables { get { if (wearables == null) { var attachments = SourceElement.Element("HeadAttachments"); if (attachments != null) { wearables = attachments.Elements("Wearable"); } } return wearables; } } private IEnumerable FilterElementsByGenderAndRace(IEnumerable elements) { if (elements == null) { return elements; } return elements.Where(w => Enum.TryParse(w.GetAttributeString("gender", "male"), true, out Gender g) && g == gender && Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race); } private void CalculateHeadSpriteRange() { if (SourceElement == null) { return; } headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero); if (headSpriteRange == Vector2.Zero) { // If range is defined, we use it as it is // Else we calculate the range from the wearables. var wearables = FilterElementsByGenderAndRace(Wearables); if (wearables == null) { headSpriteRange = Vector2.Zero; return; } if (wearables.None()) { DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {gender} and the race {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); headSpriteRange = new Vector2(ids.First(), ids.Last()); } } } /// /// Loads only the elements according to the indices, not the sprites. /// public void LoadHeadAttachments() { if (Wearables != null) { if (hairs == null) { hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair); } if (beards == null) { beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Beard), WearableType.Beard); } if (moustaches == null) { moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Moustache), WearableType.Moustache); } if (faceAttachments == null) { faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.FaceAttachment), WearableType.FaceAttachment); } if (IsValidIndex(HairIndex, hairs)) { HairElement = hairs[HairIndex]; } else { HairElement = GetRandomElement(hairs); HairIndex = hairs.IndexOf(HairElement); } if (IsValidIndex(BeardIndex, beards)) { BeardElement = beards[BeardIndex]; } else { BeardElement = GetRandomElement(beards); BeardIndex = beards.IndexOf(BeardElement); } if (IsValidIndex(MoustacheIndex, moustaches)) { MoustacheElement = moustaches[MoustacheIndex]; } else { MoustacheElement = GetRandomElement(moustaches); MoustacheIndex = moustaches.IndexOf(MoustacheElement); } if (IsValidIndex(FaceAttachmentIndex, faceAttachments)) { FaceAttachment = faceAttachments[FaceAttachmentIndex]; } else { FaceAttachment = GetRandomElement(faceAttachments); FaceAttachmentIndex = faceAttachments.IndexOf(FaceAttachment); } List AddEmpty(IEnumerable elements, WearableType type) { // 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()); var list = new List() { emptyElement }; list.AddRange(elements); return list; } XElement GetRandomElement(IEnumerable elements) { var filtered = elements.Where(e => IsWearableAllowed(e)).ToList(); if (filtered.Count == 0) { return null; } var weights = GetWeights(filtered).ToList(); var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server); return element == null || element.Name == "Empty" ? null : element; } IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType) { 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; }); } bool IsWearableAllowed(XElement element) { string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty); return IsAllowed(HairElement, spriteName) && IsAllowed(BeardElement, spriteName) && IsAllowed(MoustacheElement, spriteName) && IsAllowed(FaceAttachment, spriteName); } 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; } bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); } } partial void LoadAttachmentSprites(); private int CalculateSalary() { if (Name == null || Job == null) return 0; int salary = Math.Abs(Name.GetHashCode()) % 100; foreach (Skill skill in Job.Skills) { salary += (int)skill.Level * 50; } return salary; } public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos) { if (Job == null || GameMain.Client != null) return; float prevLevel = Job.GetSkillLevel(skillIdentifier); Job.IncreaseSkillLevel(skillIdentifier, increase); float newLevel = Job.GetSkillLevel(skillIdentifier); OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos); if (GameMain.Server != null && (int)newLevel != (int)prevLevel) { GameMain.Server.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); } } public void SetSkillLevel(string skillIdentifier, float level, Vector2 worldPos) { 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, skill.Level, worldPos); } else { float prevLevel = skill.Level; skill.Level = level; OnSkillChanged(skillIdentifier, prevLevel, skill.Level, worldPos); } } partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos); public virtual XElement Save(XElement parentElement) { XElement charElement = new XElement("Character"); charElement.Add( new XAttribute("name", Name), new XAttribute("file", File), new XAttribute("gender", gender == Gender.Male ? "male" : "female"), new XAttribute("race", race.ToString()), new XAttribute("salary", Salary), new XAttribute("headspriteid", HeadSpriteId), new XAttribute("hairindex", HairIndex), new XAttribute("beardindex", BeardIndex), new XAttribute("moustacheindex", MoustacheIndex), new XAttribute("faceattachmentindex", FaceAttachmentIndex), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); // TODO: animations? if (Character != null) { if (Character.AnimController.CurrentHull != null) { charElement.Add(new XAttribute("hull", Character.AnimController.CurrentHull.ID)); } } Job.Save(charElement); parentElement.Add(charElement); return charElement; } public void SpawnInventoryItems(Inventory inventory, XElement itemData) { SpawnInventoryItemsRecursive(inventory, itemData); } private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element) { foreach (XElement itemElement in element.Elements()) { var newItem = Item.Load(itemElement, inventory.Owner.Submarine); int slotIndex = itemElement.GetAttributeInt("i", 0); if (newItem == null) continue; Entity.Spawner.CreateNetworkEvent(newItem, false); inventory.TryPutItem(newItem, slotIndex, false, false, null); int itemContainerIndex = 0; var itemContainers = newItem.GetComponents().ToList(); foreach (XElement childInvElement in itemElement.Elements()) { if (itemContainerIndex >= itemContainers.Count) break; if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue; SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); itemContainerIndex++; } } } public void ServerWrite(NetBuffer msg) { msg.Write(ID); msg.Write(Name); msg.Write(Gender == Gender.Female); msg.Write((byte)Race); msg.Write((byte)HeadSpriteId); msg.Write((byte)HairIndex); msg.Write((byte)BeardIndex); msg.Write((byte)MoustacheIndex); msg.Write((byte)FaceAttachmentIndex); msg.Write(ragdollFileName); if (Job != null) { msg.Write(Job.Prefab.Identifier); msg.Write((byte)Job.Skills.Count); foreach (Skill skill in Job.Skills) { msg.Write(skill.Identifier); msg.Write(skill.Level); } } else { msg.Write(""); } // TODO: animations } public static CharacterInfo ClientRead(string configPath, NetBuffer inc) { ushort infoID = inc.ReadUInt16(); string newName = inc.ReadString(); bool isFemale = inc.ReadBoolean(); int race = inc.ReadByte(); int headSpriteID = inc.ReadByte(); int hairIndex = inc.ReadByte(); int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); int faceAttachmentIndex = inc.ReadByte(); string ragdollFile = inc.ReadString(); string jobIdentifier = inc.ReadString(); JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) { jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier); int skillCount = inc.ReadByte(); for (int i = 0; i < skillCount; i++) { string skillIdentifier = inc.ReadString(); float skillLevel = inc.ReadSingle(); skillLevels.Add(skillIdentifier, skillLevel); } } // TODO: animations CharacterInfo ch = new CharacterInfo(configPath, newName, isFemale ? Gender.Female : Gender.Male, jobPrefab, ragdollFile) { ID = infoID, race = (Race)race, headSpriteId = headSpriteID, HairIndex = hairIndex, BeardIndex = beardIndex, MoustacheIndex = moustacheIndex, FaceAttachmentIndex = faceAttachmentIndex }; ch.CalculateHeadSpriteRange(); ch.ReloadHeadAttachments(); System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count); if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) { Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key); if (matchingSkill == null) { DebugConsole.ThrowError("Skill \"" + skill.Key + "\" not found in character \"" + newName + "\""); continue; } matchingSkill.Level = skill.Value; } } return ch; } public void ReloadHeadAttachments() { ResetLoadedAttachments(); LoadHeadAttachments(); } public void ResetHeadAttachments() { ResetAttachmentIndices(); ResetLoadedAttachments(); } private void ResetAttachmentIndices() { HairIndex = -1; BeardIndex = -1; MoustacheIndex = -1; FaceAttachmentIndex = -1; } private void ResetLoadedAttachments() { hairs = null; beards = null; moustaches = null; faceAttachments = null; } public void Remove() { Character = null; if (headSprite != null) { headSprite.Remove(); headSprite = null; } if (portrait != null) { portrait.Remove(); portrait = null; } if (portraitBackground != null) { portraitBackground.Remove(); portraitBackground = null; } if (attachmentSprites != null) { attachmentSprites.ForEach(a => a.Sprite.Remove()); attachmentSprites = null; } } } }