using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; using Barotrauma.Abilities; using System.Collections.Immutable; namespace Barotrauma { public enum WearableType { Item, Hair, Beard, Moustache, FaceAttachment, Husk, Herpes } class WearableSprite { public ContentPath UnassignedSpritePath { get; private set; } public string SpritePath { get; private set; } public ContentXElement SourceElement { get; private set; } public WearableType Type { get; private set; } private Sprite _sprite; public Sprite Sprite { get { return _sprite; } private set { if (value == _sprite) { return; } if (_sprite != null) { _sprite.Remove(); } _sprite = value; } } public LimbType Limb { get; private set; } public bool HideLimb { get; private set; } public enum ObscuringMode { None, Hide, AlphaClip } public ObscuringMode ObscureOtherWearables { get; private set; } public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide; public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip; public bool CanBeHiddenByOtherWearables { get; private set; } /// /// Tags or identifiers of items that can hide this one. /// public ImmutableHashSet CanBeHiddenByItem { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } /// /// Does the wearable inherit all the scalings of the wearer? Also the wearable's own scale is used! /// public bool InheritScale { get; private set; } public bool IgnoreRagdollScale { get; private set; } public bool IgnoreLimbScale { get; private set; } public bool IgnoreTextureScale { get; private set; } public bool InheritOrigin { get; private set; } public bool InheritSourceRect { get; private set; } public float Scale { get; private set; } public float Rotation { get; private set; } public LimbType DepthLimb { get; private set; } private Wearable _wearableComponent; public Wearable WearableComponent { get { return _wearableComponent; } set { if (value == _wearableComponent) { return; } if (_wearableComponent != null) { _wearableComponent.Remove(); } _wearableComponent = value; } } public string Sound { get; private set; } public Point? SheetIndex { get; private set; } public LightComponent LightComponent => LightComponents?.FirstOrDefault(); public List LightComponents { get { if (_lightComponents == null) { _lightComponents = new List(); } return _lightComponents; } } private List _lightComponents; public int Variant { get; set; } private Character _picker; /// /// None = Any/Not Defined -> no effect. /// Changing the gender forces re-initialization, because the textures can be different for male and female characters. /// public Character Picker { get { return _picker; } set { if (value == _picker) { return; } _picker = value; IsInitialized = false; UnassignedSpritePath = ParseSpritePath(SourceElement); Init(_picker); } } public WearableSprite(ContentXElement subElement, WearableType type) { Type = type; SourceElement = subElement; UnassignedSpritePath = subElement.GetAttributeContentPath("texture") ?? ContentPath.Empty; Init(); switch (type) { case WearableType.Hair: case WearableType.Beard: case WearableType.Moustache: case WearableType.FaceAttachment: case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; break; } } /// /// Note: this constructor cannot initialize automatically, because the gender is unknown at this point. We only know it when the item is equipped. /// public WearableSprite(ContentXElement subElement, Wearable wearable, int variant = 0) { Type = WearableType.Item; WearableComponent = wearable; Variant = Math.Max(variant, 0); UnassignedSpritePath = ParseSpritePath(subElement); SourceElement = subElement; } private ContentPath ParseSpritePath(ContentXElement element) { if (element.DoesAttributeReferenceFileNameAlone("texture")) { string textureName = element.GetAttributeString("texture", ""); return ContentPath.FromRaw( element.ContentPackage, $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}"); } else { return element.GetAttributeContentPath("texture") ?? ContentPath.Empty; } } public void ParsePath(bool parseSpritePath) { #if SERVER // Server doesn't care about texture paths at all return; #endif SpritePath = UnassignedSpritePath.Value; if (_picker?.Info != null) { SpritePath = _picker.Info.ReplaceVars(SpritePath); } SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString()); if (!File.Exists(SpritePath)) { // If the variant does not exist, parse the path so that it uses first variant. SpritePath = SpritePath.Replace("[VARIANT]", "1"); } if (!File.Exists(SpritePath) && _picker?.Info == null) { // If there's no character info is defined, try to use first tagset from CharacterInfoPrefab var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; SpritePath = charInfoPrefab.ReplaceVars(SpritePath, charInfoPrefab.Heads.First()); } if (parseSpritePath) { Sprite?.ParseTexturePath(file: SpritePath); } } public bool IsInitialized { get; private set; } public void Init(Character picker = null) { if (IsInitialized) { return; } _picker = picker; ParsePath(false); Sprite?.Remove(); Sprite = new Sprite(SourceElement, file: SpritePath); Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); foreach (var mode in Enum.GetValues()) { if (mode == ObscuringMode.None) { continue; } if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false)) { ObscureOtherWearables = mode; } } CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true); CanBeHiddenByItem = SourceElement.GetAttributeIdentifierArray(nameof(CanBeHiddenByItem), Array.Empty()).ToImmutableHashSet(); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); var scale = SourceElement.GetAttribute("inheritscale"); if (scale != null) { InheritScale = scale.GetAttributeBool(Type != WearableType.Item); } else { InheritScale = SourceElement.GetAttributeBool("inherittexturescale", Type != WearableType.Item); } IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false); IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false); IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false); SourceElement.GetAttributeBool("inherittexturescale", false); InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", Type != WearableType.Item); InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", Type != WearableType.Item); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); Scale = SourceElement.GetAttributeFloat("scale", 1.0f); Rotation = MathHelper.ToRadians(SourceElement.GetAttributeFloat("rotation", 0.0f)); var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1)); if (index.X > -1 && index.Y > -1) { SheetIndex = index; } HideWearablesOfType = new List(); var wearableTypes = SourceElement.GetAttributeStringArray("hidewearablesoftype", null); if (wearableTypes != null && wearableTypes.Length > 0) { foreach (var value in wearableTypes) { if (Enum.TryParse(value, ignoreCase: true, out WearableType wearableType)) { HideWearablesOfType.Add(wearableType); } } } IsInitialized = true; } } } namespace Barotrauma.Items.Components { partial class Wearable : Pickable, IServerSerializable { private readonly ContentXElement[] wearableElements; private readonly WearableSprite[] wearableSprites; private readonly LimbType[] limbType; private readonly Limb[] limb; private readonly List damageModifiers; public readonly Dictionary SkillModifiers; public readonly Dictionary WearableStatValues = new Dictionary(); public IEnumerable DamageModifiers { get { return damageModifiers; } } public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn.")] public bool AllowUseWhenWorn { get; set; } public readonly int Variants; private int variant; public int Variant { get { return variant; } set { if (variant == value) { return; } #if SERVER variant = value; if (!item.Submarine?.Loading ?? true) { item.CreateServerEvent(this); } #elif CLIENT Character character = picker; if (character != null) { Unequip(character); } for (int i = 0; i < wearableSprites.Length; i++) { var subElement = wearableElements[i]; wearableSprites[i]?.Sprite?.Remove(); wearableSprites[i] = new WearableSprite(subElement, this, value); } if (character != null) { Equip(character); } variant = value; #endif } } /// /// How deep down does the item protect from pressure? Determined by status effects. /// public readonly float PressureProtection; public Wearable(Item item, ContentXElement element) : base(item, element) { this.item = item; damageModifiers = new List(); SkillModifiers = new Dictionary(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); variant = Rand.Range(1, Variants + 1, Rand.RandSync.ServerAndClient); wearableSprites = new WearableSprite[spriteCount]; wearableElements = new ContentXElement[spriteCount]; limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false); int i = 0; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": if (subElement.GetAttribute("texture") == null) { DebugConsole.ThrowError("Item \"" + item.Name + "\" doesn't have a texture specified!", contentPackage: element.ContentPackage); return; } limbType[i] = (LimbType)Enum.Parse(typeof(LimbType), subElement.GetAttributeString("limb", "Head"), true); wearableSprites[i] = new WearableSprite(subElement, this, variant); wearableElements[i] = subElement; foreach (var lightElement in subElement.Elements()) { if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } wearableSprites[i].LightComponents.Add(new LightComponent(item, lightElement) { Parent = this }); foreach (var light in wearableSprites[i].LightComponents) { item.AddComponent(light); } } i++; break; case "damagemodifier": damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable")); break; case "skillmodifier": Identifier skillIdentifier = subElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty); float skillValue = subElement.GetAttributeFloat("skillvalue", 0f); if (SkillModifiers.ContainsKey(skillIdentifier)) { SkillModifiers[skillIdentifier] += skillValue; } else { SkillModifiers.TryAdd(skillIdentifier, skillValue); } break; case "statvalue": StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name); float statValue = subElement.GetAttributeFloat("value", 0f); if (WearableStatValues.ContainsKey(statType)) { WearableStatValues[statType] += statValue; } else { WearableStatValues.TryAdd(statType, statValue); } break; case "statuseffect": if (subElement.GetAttributeString("Target", string.Empty).ToLowerInvariant().Contains("character")) { PressureProtection = Math.Max(subElement.GetAttributeFloat(nameof(PressureProtection), 0), PressureProtection); } break; } } } public override void Equip(Character character) { foreach (var allowedSlot in allowedSlots) { if (allowedSlot == InvSlotType.Any) { continue; } foreach (Enum value in Enum.GetValues(typeof(InvSlotType))) { var slotType = (InvSlotType)value; if (slotType == InvSlotType.Any || slotType == InvSlotType.None) { continue; } if (allowedSlot.HasFlag(slotType) && !character.Inventory.IsInLimbSlot(item, slotType)) { return; } } } picker = character; for (int i = 0; i < wearableSprites.Length; i++ ) { var wearableSprite = wearableSprites[i]; if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker); } // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. wearableSprite.Picker = picker; Limb equipLimb = character.AnimController.GetLimb(limbType[i]); if (equipLimb == null) { continue; } if (item.body != null) { item.body.Enabled = false; } IsActive = true; if (wearableSprite.LightComponent != null) { foreach (var light in wearableSprite.LightComponents) { light.ParentBody = equipLimb.body; } } limb[i] = equipLimb; if (!equipLimb.WearingItems.Contains(wearableSprite)) { equipLimb.WearingItems.Add(wearableSprite); equipLimb.WearingItems.Sort((wearable, nextWearable) => { float depth = wearable?.Sprite?.Depth ?? 0; float nextDepth = nextWearable?.Sprite?.Depth ?? 0; return nextDepth.CompareTo(depth); }); equipLimb.WearingItems.Sort((wearable, nextWearable) => { var wearableComponent = wearable?.WearableComponent; var nextWearableComponent = nextWearable?.WearableComponent; if (wearableComponent == null && nextWearableComponent == null) { return 0; } if (wearableComponent == null) { return -1; } if (nextWearableComponent == null) { return 1; } return wearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(nextWearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); }); } #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif } character.OnWearablesChanged(); } public override void Drop(Character dropper, bool setTransform = true) { Character previousPicker = picker; Unequip(picker); base.Drop(dropper, setTransform); previousPicker?.OnWearablesChanged(); picker = null; IsActive = false; } public override void Unequip(Character character) { if (character == null || character.Removed) { return; } if (picker == null) { return; } for (int i = 0; i < wearableSprites.Length; i++) { Limb equipLimb = character.AnimController.GetLimb(limbType[i]); if (equipLimb == null) { continue; } if (wearableSprites[i].LightComponent != null) { foreach (var light in wearableSprites[i].LightComponents) { light.ParentBody = null; } } equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]); #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif limb[i] = null; } IsActive = false; } public override void UpdateBroken(float deltaTime, Camera cam) { Update(deltaTime, cam); } public override void Update(float deltaTime, Camera cam) { if (picker == null || picker.Removed) { IsActive = false; return; } //if the item is also being held, let the Holdable component control the position if (item.GetComponent() is not { IsActive: true }) { item.SetTransform(picker.SimPosition, 0.0f); } item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker); #if CLIENT PlaySound(ActionType.OnWearing, picker); #endif } protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); Unequip(picker); foreach (WearableSprite wearableSprite in wearableSprites) { wearableSprite?.Sprite?.Remove(); wearableSprite.Picker = null; } } public override XElement Save(XElement parentElement) { XElement componentElement = base.Save(parentElement); componentElement.Add(new XAttribute("variant", variant)); return componentElement; } private int loadedVariant = -1; public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap) { base.Load(componentElement, usePrefabValues, idRemap, isItemSwap); loadedVariant = componentElement.GetAttributeInt("variant", -1); } public override void OnItemLoaded() { base.OnItemLoaded(); //do this here to prevent creating a network event before the item has been fully initialized if (loadedVariant > 0 && loadedVariant < Variants + 1) { Variant = loadedVariant; } } public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) { msg.WriteByte((byte)Variant); base.ServerEventWrite(msg, c, extraData); } public override void ClientEventRead(IReadMessage msg, float sendingTime) { Variant = (int)msg.ReadByte(); base.ClientEventRead(msg, sendingTime); } } }