Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs
T
Joonas Rikkonen 58c92888b7 2ad9b5d...2f107db
commit 2f107db0475bf4b9a8b6f405b9ce8ec489cbc0ba
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 16:30:24 2019 +0200

    Another nullref fix to the OpenHealthWindow property setter. Closes #1090

commit 1d15d18d2501bedaa1b42ac8e0e7e09e2600960e
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 16:19:05 2019 +0200

    Fixed crashing when an incompatible content package is selected in config.xml or if the content package cannot be found. Happened because the game attempted to use TextManager.Get before text files had been loaded. Partially fixes #1093

commit 9ec1980fe90bcae65333c188b265ba16a6db6c05
Author: ezjamsen <ezjames.fi@gmail.com>
Date:   Thu Feb 7 16:16:53 2019 +0200

    added correct effects for alien blood

commit b6d2c56910b5d58477703abc6785ef80719a89a6
Author: ezjamsen <ezjames.fi@gmail.com>
Date:   Thu Feb 7 15:41:25 2019 +0200

    adjustments to fent, glue and antibiotics

commit 8df16acdaac0a521fe09fb62c6815a46a87f6f25
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 15:30:51 2019 +0200

    More descriptive ping exception error messages (only shown in debug builds now), some extra checks to prevent using the workshop without steam (although the checks that actually matter are implemented at Steam's side now)

commit 959c503c140196287ab60ec2357feb8bd5c85b18
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 14:00:48 2019 +0200

    Don't allow severing the joint between a moloch's shell and the "bladder"

commit 0c0b033016c2e82b8cf1b5a4e3f6faa9cde39f40
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 13:51:58 2019 +0200

    Fixed incorrect item panel positioning in the crew command interface when the sub is docked to something. Closes #1082

commit 68366a4bf3c73b70288f043199d3ab9b2f5f4d06
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 13:23:38 2019 +0200

    Fixed outdated damage modifiers in security and clown gear (used damage types instead of afflictions), not defining any affliction types for a damage modifier makes it affect all types of damage. Closes #1088

commit 5d3ab5084b8bc5357bc4a53993ad8dfcc44d0412
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 13:04:55 2019 +0200

    Fixed client-side nullref exception if the round ends while the health window is open. Closes #1090

commit 8249d789a60dfe4bcdd1705d26d6ca36ed407058
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Thu Feb 7 12:48:34 2019 +0200

    The dedicated server doesn't initialize a Steam client (not required to use any of the Steam server functionality). Fixes players being unable to log in as a client when they're hosting a dedicated server on another machine. TODO: SteamCMD support

commit 55fb0bb1ba4bb8385dabbf6e40d4651bf86294a9
Author: ezjamsen <ezjames.fi@gmail.com>
Date:   Thu Feb 7 12:31:46 2019 +0200

    Added linked hulls

commit 937d50dca50d8c889a7c11b55765c208d2d16b67
Merge: 8109ae1a3 57731761f
Author: EdusFF <pitkanen.eetu@gmail.com>
Date:   Thu Feb 7 12:16:43 2019 +0200

    Merge branch 'dev' of https://github.com/Regalis11/Barotrauma into dev

commit 8109ae1a3377910ff9bf066ccac582d879dfb0a4
Author: EdusFF <pitkanen.eetu@gmail.com>
Date:   Thu Feb 7 12:16:37 2019 +0200

    Fixed: AI not reloading coilgun if an empty box of ammunition is inserted.

commit 57731761f64bb33a4f4e7baa38367cfb7cf1f873
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Thu Feb 7 12:10:59 2019 +0200

    Refactor character head customization logic and store the data in a dedicated class. In the net lobby, generate new head when the user presses the right arrow button. Pressing the left arrow will restore the previous head. When the user presses the right arrow after the left arrow, the first head is restored again. So it's effectively (and technically) undo/redo logic.

commit 37577931e2706ce432cee2d48cd55990bc2bab4a
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Thu Feb 7 12:04:49 2019 +0200

    Add properties for getting the count of the undo and redo stacks.

commit f226beb05a55749ac961e3a33ac594fdf903fc3b
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Wed Feb 6 21:15:56 2019 +0200

    Fixes to hull linking & displaying them as one hull on the minimap:
    - Links between hulls are now saved.
    - Simplified grouping logic (just store a list of linked hulls in hulldata).
    - Fixed water/oxygen calculations (use the average of the values instead of their sum).
    TODO: DRY (there's now duplicate linking logic in Item and Hull), merge the hull sprites on the minimap, maybe automatize the linking?

commit 5dfea1fbb24c5bd91605caf65baaac9aca9cfbbf
Merge: 2ad9b5de4 d06a1557f
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Wed Feb 6 20:18:31 2019 +0200

    Merge branch 'dev' of https://github.com/Regalis11/Barotrauma into dev

commit d06a1557ffb85cd32bbb884fadef60b6d9dd27cf
Author: Alex <olimpickusa6@gmail.com>
Date:   Wed Feb 6 18:26:40 2019 +0200

    Test for connecting hulls task
2019-03-18 21:13:11 +02:00

989 lines
36 KiB
C#

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 };
// TODO: Generating the HeadInfo could be simplified.
partial class CharacterInfo
{
public class HeadInfo
{
private int _headSpriteId;
public int HeadSpriteId
{
get { return _headSpriteId; }
set
{
_headSpriteId = value;
if (_headSpriteId < (int)headSpriteRange.X)
{
_headSpriteId = (int)headSpriteRange.Y;
}
if (_headSpriteId > (int)headSpriteRange.Y)
{
_headSpriteId = (int)headSpriteRange.X;
}
}
}
public Vector2 headSpriteRange;
public Gender gender;
public Race race;
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 BeardElement { get; set; }
public XElement MoustacheElement { get; set; }
public XElement FaceAttachment { get; set; }
public HeadInfo() { }
public HeadInfo(int headId)
{
_headSpriteId = headId;
}
public void ResetAttachmentIndices()
{
HairIndex = -1;
BeardIndex = -1;
MoustacheIndex = -1;
FaceAttachmentIndex = -1;
}
}
private HeadInfo head = new HeadInfo();
public HeadInfo Head
{
get { return head; }
set
{
if (head != value && value != null)
{
head = value;
if (head.race == Race.None)
{
head.race = GetRandomRace();
}
if (head.gender == Gender.None)
{
head.gender = GetRandomGender();
}
CalculateHeadSpriteRange();
Head.HeadSpriteId = value.HeadSpriteId;
HeadSprite = null;
AttachmentSprites = null;
}
}
}
private static Dictionary<string, XDocument> cachedConfigs = new Dictionary<string, XDocument>();
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;
}
}
/// <summary>
/// Note: Can be null.
/// </summary>
public Character Character;
public readonly string File;
public Job Job;
public int Salary;
private Sprite headSprite;
public Sprite HeadSprite
{
get
{
if (headSprite == null)
{
LoadHeadSprite();
}
return headSprite;
}
private set
{
if (headSprite != null)
{
headSprite.Remove();
}
headSprite = value;
}
}
private Sprite portrait;
public Sprite Portrait
{
get
{
if (portrait == null)
{
LoadHeadSprite();
}
return portrait;
}
private set
{
if (portrait != null)
{
portrait.Remove();
}
portrait = value;
}
}
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 set
{
if (portraitBackground != null)
{
portraitBackground.Remove();
}
portraitBackground = value;
}
}
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 XElement SourceElement { get; set; }
public readonly string ragdollFileName = string.Empty;
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 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 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<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;
HeadSprite = null;
AttachmentSprites = null;
ResetHeadAttachments();
}
}
private Gender gender;
public Gender Gender
{
get { return Head.gender; }
set
{
if (Head.gender == value) return;
Head.gender = value;
if (Head.gender == Gender.None)
{
Head.gender = Gender.Male;
}
CalculateHeadSpriteRange();
ResetHeadAttachments();
HeadSprite = null;
AttachmentSprites = null;
}
}
public Race Race
{
get { return Head.race; }
set
{
if (Head.race == value) { return; }
Head.race = value;
if (Head.race == Race.None)
{
Head.race = Race.White;
}
CalculateHeadSpriteRange();
ResetHeadAttachments();
HeadSprite = null;
AttachmentSprites = null;
}
}
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 XElement HairElement { get => Head.HairElement; set => Head.HairElement = value; }
public XElement BeardElement { get => Head.BeardElement; set => Head.BeardElement = value; }
public XElement MoustacheElement { get => Head.MoustacheElement; set => Head.MoustacheElement = value; }
public XElement FaceAttachment { get => Head.FaceAttachment; set => Head.FaceAttachment = value; }
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<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
}
return ragdoll;
}
set { ragdoll = value; }
}
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// 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<string>();
XDocument doc = GetConfig(file);
SourceElement = doc.Root;
if (doc.Root.GetAttributeBool("genders", false))
{
Head.gender = gender == Gender.None ? GetRandomGender() : gender;
}
if (!Enum.TryParse(doc.Root.GetAttributeString("race", "None"), true, out Head.race))
{
Head.race = GetRandomRace();
}
if (Head.race == Race.None)
{
Head.race = GetRandomRace();
}
CalculateHeadSpriteRange();
Head.HeadSpriteId = GetRandomHeadID();
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]", (Head.gender == Gender.Female) ? "female" : "male");
Name = ToolBox.GetRandomLine(firstNamePath);
}
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
if (lastNamePath != "")
{
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.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();
Head.gender = (genderStr == "male") ? Gender.Male : Gender.Female;
Enum.TryParse(element.GetAttributeString("race", "white"), true, out Head.race);
File = element.GetAttributeString("file", "");
SourceElement = GetConfig(File).Root;
Salary = element.GetAttributeInt("salary", 1000);
Head.HeadSpriteId = element.GetAttributeInt("headspriteid", 1);
Head.HairIndex = element.GetAttributeInt("hairindex", -1);
Head.BeardIndex = element.GetAttributeInt("beardindex", -1);
Head.MoustacheIndex = element.GetAttributeInt("moustacheindex", -1);
Head.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]", (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);
//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;
}
}
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;
}
public Gender SetRandomGender() => Gender = GetRandomGender();
public Race SetRandomRace() => Race = GetRandomRace();
public int SetRandomHead() => HeadSpriteId = GetRandomHeadID();
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0;
private List<XElement> hairs;
private List<XElement> beards;
private List<XElement> moustaches;
private List<XElement> faceAttachments;
private IEnumerable<XElement> wearables;
private IEnumerable<XElement> Wearables
{
get
{
if (wearables == null)
{
var attachments = SourceElement.Element("HeadAttachments");
if (attachments != null)
{
wearables = attachments.Elements("Wearable");
}
}
return wearables;
}
}
private IEnumerable<XElement> FilterElementsByGenderAndRace(IEnumerable<XElement> elements)
{
if (elements == null) { return elements; }
return elements.Where(w =>
Enum.TryParse(w.GetAttributeString("gender", "male"), true, out Gender g) && g == Head.gender &&
Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == Head.race);
}
private void CalculateHeadSpriteRange()
{
if (SourceElement == null) { return; }
Head.headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero);
if (Head.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)
{
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());
}
}
}
/// <summary>
/// Loads only the elements according to the indices, not the sprites.
/// </summary>
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(Head.HairIndex, hairs))
{
Head.HairElement = hairs[Head.HairIndex];
}
else
{
Head.HairElement = GetRandomElement(hairs);
Head.HairIndex = hairs.IndexOf(Head.HairElement);
}
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);
}
List<XElement> AddEmpty(IEnumerable<XElement> 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<XElement>() { emptyElement };
list.AddRange(elements);
return list;
}
XElement GetRandomElement(IEnumerable<XElement> 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<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> 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 == Head.HeadSpriteId;
});
}
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);
}
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<XElement> list) => index >= 0 && index < list.Count;
IEnumerable<float> GetWeights(IEnumerable<XElement> 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", Head.gender == Gender.Male ? "male" : "female"),
new XAttribute("race", Head.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<ItemContainer>().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)Head.HairIndex);
msg.Write((byte)Head.BeardIndex);
msg.Write((byte)Head.MoustacheIndex);
msg.Write((byte)Head.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<string, float> skillLevels = new Dictionary<string, float>();
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,
};
ch.Head.race = (Race)race;
ch.Head.HeadSpriteId = headSpriteID;
ch.HairIndex = hairIndex;
ch.BeardIndex = beardIndex;
ch.MoustacheIndex = moustacheIndex;
ch.FaceAttachmentIndex = faceAttachmentIndex;
ch.CalculateHeadSpriteRange();
ch.ReloadHeadAttachments();
System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count);
if (ch.Job != null)
{
foreach (KeyValuePair<string, float> 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()
{
Head.ResetAttachmentIndices();
}
private void ResetLoadedAttachments()
{
hairs = null;
beards = null;
moustaches = null;
faceAttachments = null;
}
public void Remove()
{
Character = null;
HeadSprite = null;
Portrait = null;
PortraitBackground = null;
AttachmentSprites = null;
}
}
}