using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Lidgren.Network; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; namespace Barotrauma { public enum ActionType { Always, OnPicked, OnUse, OnSecondaryUse, OnWearing, OnContaining, OnContained, OnNotContained, OnActive, OnFailure, OnBroken, OnFire, InWater, NotInWater, OnImpact, OnEating, OnDeath = OnBroken } partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { public static List ItemList = new List(); public ItemPrefab Prefab => prefab as ItemPrefab; public static bool ShowLinks = true; private HashSet tags; private Hull currentHull; public Hull CurrentHull { get { return currentHull; } set { currentHull = value; ParentRuin = currentHull?.ParentRuin; } } public bool Visible = true; public SpriteEffects SpriteEffects = SpriteEffects.None; //components that determine the functionality of the item private Dictionary componentsByType = new Dictionary(); private List components; private List drawableComponents; public PhysicsBody body; public readonly XElement StaticBodyConfig; private float lastSentCondition; private float sendConditionUpdateTimer; private bool conditionUpdatePending; private float condition; private bool inWater; private Inventory parentInventory; private Inventory ownInventory; private Dictionary connections; private List repairables; //a dictionary containing lists of the status effects in all the components of the item private Dictionary> statusEffectLists; public Dictionary SerializableProperties { get; protected set; } private bool? hasInGameEditableProperties; bool HasInGameEditableProperties { get { if (hasInGameEditableProperties == null) { hasInGameEditableProperties = false; if (SerializableProperties.Values.Any(p => p.Attributes.OfType().Any())) { hasInGameEditableProperties = true; } else { foreach (ItemComponent component in components) { if (!component.AllowInGameEditing) { continue; } if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType().Any())) { hasInGameEditableProperties = true; break; } } } } return (bool)hasInGameEditableProperties; } } //the inventory in which the item is contained in public Inventory ParentInventory { get { return parentInventory; } set { parentInventory = value; if (parentInventory != null) Container = parentInventory.Owner as Item; } } private Item container; public Item Container { get { return container; } private set { if (value != container) { container = value; SetActiveSprite(); } } } public override string Name { get { return prefab.Name; } } private string description; public string Description { get { return description ?? prefab.Description; } set { description = value; } } private bool hiddenInGame; [Editable, Serialize(false, true)] public bool HiddenInGame { get { return hiddenInGame; } set { hiddenInGame = value; } } public float ImpactTolerance { get { return Prefab.ImpactTolerance; } } public float InteractDistance { get { return Prefab.InteractDistance; } } public float InteractPriority { get { return Prefab.InteractPriority; } } public override Vector2 Position { get { return (body == null) ? base.Position : ConvertUnits.ToDisplayUnits(SimPosition); } } public override Vector2 SimPosition { get { return (body == null) ? base.SimPosition : body.SimPosition; } } public Rectangle InteractionRect { get { return WorldRect; } } public float PositionUpdateInterval { get; private set; } = float.PositiveInfinity; protected Color spriteColor; [Editable, Serialize("1.0,1.0,1.0,1.0", true)] public Color SpriteColor { get { return spriteColor; } set { spriteColor = value; } } [Serialize("1.0,1.0,1.0,1.0", true), Editable] public Color InventoryIconColor { get; protected set; } [Serialize("1.0,1.0,1.0,1.0", true), Editable(ToolTip = "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")] public Color ContainerColor { get; protected set; } [Serialize("", false)] /// /// Can be used by status effects or conditionals to check what item this item is contained inside /// public string ContainerIdentifier { get { return Container?.prefab.Identifier ?? ""; } set { /*do nothing*/ } } public Color Color { get { return spriteColor; } } public bool IsFullCondition => Condition >= MaxCondition; public float MaxCondition => Prefab.Health; public float ConditionPercentage => MathUtils.Percentage(Condition, MaxCondition); //the default value should be Prefab.Health, but because we can't use it in the attribute, //we'll just use NaN (which does nothing) and set the default value in the constructor/load [Serialize(float.NaN, false), Editable] public float Condition { get { return condition; } set { #if CLIENT if (GameMain.Client != null) return; #endif if (!MathUtils.IsValid(value)) return; if (Indestructible) return; float prev = condition; condition = MathHelper.Clamp(value, 0.0f, Prefab.Health); if (condition == 0.0f && prev > 0.0f) { #if CLIENT foreach (ItemComponent ic in components) { ic.PlaySound(ActionType.OnBroken, WorldPosition); } if (Screen.Selected == GameMain.SubEditorScreen) return; #endif ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } SetActiveSprite(); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(lastSentCondition, condition)) { if (Math.Abs(lastSentCondition - condition) > 1.0f || condition == 0.0f || condition == Prefab.Health) { conditionUpdatePending = true; } } } } public float Health { get { return condition; } } private bool? indestructible; /// /// Per-instance value - if not set, the value of the prefab is used. /// public bool Indestructible { get { return indestructible ?? Prefab.Indestructible; } set { indestructible = value; } } [Editable, Serialize("", true)] public string Tags { get { return string.Join(",", tags); } set { tags.Clear(); // Always add prefab tags prefab.Tags.ForEach(t => tags.Add(t)); if (!string.IsNullOrWhiteSpace(value)) { string[] splitTags = value.Split(','); foreach (string tag in splitTags) { string[] splitTag = tag.Split(':'); splitTag[0] = splitTag[0].ToLowerInvariant(); tags.Add(string.Join(":", splitTag)); } } } } public bool FireProof { get { return Prefab.FireProof; } } public bool WaterProof { get { return Prefab.WaterProof; } } public bool UseInHealthInterface { get { return Prefab.UseInHealthInterface; } } public bool InWater { get { //if the item has an active physics body, inWater is updated in the Update method if (body != null && body.Enabled) return inWater; //if not, we'll just have to check return IsInWater(); } } /// /// A list of items the last signal sent by this item went through /// public List LastSentSignalRecipients { get; private set; } = new List(); public string ConfigFile { get { return Prefab.ConfigFile; } } //which type of inventory slots (head, torso, any, etc) the item can be placed in public List AllowedSlots { get { Pickable p = GetComponent(); return (p == null) ? new List() { InvSlotType.Any } : p.AllowedSlots; } } public List Connections { get { ConnectionPanel panel = GetComponent(); if (panel == null) return null; return panel.Connections; } } public IEnumerable ContainedItems { get { return ownInventory?.Items.Where(i => i != null); } } public Inventory OwnInventory { get { return ownInventory; } } [Serialize(false, true), Editable(ToolTip = "Enable if you want to display the item HUD side by side with another item's HUD, when linked together. " + "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] public bool DisplaySideBySideWhenLinked { get; set; } public IEnumerable Repairables { get { return repairables; } } public IEnumerable Components { get { return components; } } public override bool Linkable { get { return Prefab.Linkable; } } public override string ToString() { #if CLIENT return (GameMain.DebugDraw) ? Name + " (ID: " + ID + ")" : Name; #elif SERVER return Name + " (ID: " + ID + ")"; #endif } private readonly List allPropertyObjects = new List(); public IEnumerable AllPropertyObjects { get { return allPropertyObjects; } } public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine) : this(new Rectangle( (int)(position.X - itemPrefab.sprite.size.X / 2 * itemPrefab.Scale), (int)(position.Y + itemPrefab.sprite.size.Y / 2 * itemPrefab.Scale), (int)(itemPrefab.sprite.size.X * itemPrefab.Scale), (int)(itemPrefab.sprite.size.Y * itemPrefab.Scale)), itemPrefab, submarine) { } /// /// Creates a new item /// /// Should the OnItemLoaded methods of the ItemComponents be called. Use false if the item needs additional initialization before it can be considered fully loaded (e.g. when loading an item from a sub file or cloning an item). public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true) : base(itemPrefab, submarine) { spriteColor = prefab.SpriteColor; linkedTo = new ObservableCollection(); components = new List(); drawableComponents = new List(); tags = new HashSet(); repairables = new List(); rect = newRect; condition = itemPrefab.Health; lastSentCondition = condition; allPropertyObjects.Add(this); XElement element = itemPrefab.ConfigElement; if (element == null) return; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (submarine == null || !submarine.Loading) FindHull(); SetActiveSprite(); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "body": body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale); body.FarseerBody.AngularDamping = 0.2f; body.FarseerBody.LinearDamping = 0.1f; break; case "trigger": case "inventoryicon": case "sprite": case "deconstruct": case "brokensprite": case "decorativesprite": case "price": case "levelcommonness": case "suitabletreatment": case "containedsprite": break; case "staticbody": StaticBodyConfig = subElement; break; case "aitarget": aiTarget = new AITarget(this, subElement); break; default: ItemComponent ic = ItemComponent.Load(subElement, this, itemPrefab.ConfigFile); if (ic == null) break; AddComponent(ic); if (ic is IDrawableComponent && ic.Drawable) drawableComponents.Add(ic as IDrawableComponent); if (ic is Repairable) repairables.Add((Repairable)ic); break; } } foreach (ItemComponent ic in components) { if (ic.statusEffectLists == null) continue; if (statusEffectLists == null) statusEffectLists = new Dictionary>(); //go through all the status effects of the component //and add them to the corresponding statuseffect list foreach (List componentEffectList in ic.statusEffectLists.Values) { ActionType actionType = componentEffectList.First().type; if (!statusEffectLists.TryGetValue(actionType, out List statusEffectList)) { statusEffectList = new List(); statusEffectLists.Add(actionType, statusEffectList); } foreach (StatusEffect effect in componentEffectList) { statusEffectList.Add(effect); } } } if (body != null) { body.Submarine = submarine; } //cache connections into a dictionary for faster lookups var connectionPanel = GetComponent(); if (connectionPanel != null) { connections = new Dictionary(); foreach (Connection c in connectionPanel.Connections) { if (!connections.ContainsKey(c.Name)) connections.Add(c.Name, c); } } //containers need to handle collision events to notify items inside them about the impact var itemContainer = GetComponent(); if (ImpactTolerance > 0.0f || itemContainer != null) { if (body != null) body.FarseerBody.OnCollision += OnCollision; } if (itemContainer != null) { ownInventory = itemContainer.Inventory; } InitProjSpecific(); InsertToList(); ItemList.Add(this); if (callOnItemLoaded) { foreach (ItemComponent ic in components) { ic.OnItemLoaded(); } } } partial void InitProjSpecific(); public override MapEntity Clone() { Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false); foreach (KeyValuePair property in SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this)); } if (components.Count != clone.components.Count) { string errorMsg = "Error while cloning item \"" + Name + "\" - clone does not have the same number of components. "; errorMsg += "Original components: " + string.Join(", ", components.Select(c => c.GetType().ToString())); errorMsg += ", cloned components: " + string.Join(", ", clone.components.Select(c => c.GetType().ToString())); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Item.Clone:" + Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } for (int i = 0; i < components.Count && i < clone.components.Count; i++) { foreach (KeyValuePair property in components[i].SerializableProperties) { if (!property.Value.Attributes.OfType().Any()) continue; clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i])); } //clone requireditem identifiers foreach (var kvp in components[i].requiredItems) { for (int j = 0; j < kvp.Value.Count; j++) { if (!clone.components[i].requiredItems.ContainsKey(kvp.Key) || clone.components[i].requiredItems[kvp.Key].Count <= j) { continue; } clone.components[i].requiredItems[kvp.Key][j].JoinedIdentifiers = kvp.Value[j].JoinedIdentifiers; } } } if (FlippedX) clone.FlipX(false); if (FlippedY) clone.FlipY(false); foreach (ItemComponent component in clone.components) { component.OnItemLoaded(); } if (ContainedItems != null) { foreach (Item containedItem in ContainedItems) { var containedClone = containedItem.Clone(); clone.ownInventory.TryPutItem(containedClone as Item, null); } } return clone; } public void AddComponent(ItemComponent component) { allPropertyObjects.Add(component); components.Add(component); Type type = component.GetType(); if (!componentsByType.ContainsKey(type)) { componentsByType.Add(type, component); Type baseType = type.BaseType; while (baseType != null && baseType != typeof(ItemComponent)) { if (!componentsByType.ContainsKey(baseType)) { componentsByType.Add(baseType, component); } baseType = baseType.BaseType; } } } public void EnableDrawableComponent(IDrawableComponent drawable) { if (!drawableComponents.Contains(drawable)) { drawableComponents.Add(drawable); } } public void DisableDrawableComponent(IDrawableComponent drawable) { drawableComponents.Remove(drawable); } public int GetComponentIndex(ItemComponent component) { return components.IndexOf(component); } public T GetComponent() where T : ItemComponent { if (componentsByType.TryGetValue(typeof(T), out ItemComponent component)) { return (T)component; } return default(T); } public IEnumerable GetComponents() { if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty(); } return components.Where(c => c is T).Cast(); } public void RemoveContained(Item contained) { if (ownInventory != null) { ownInventory.RemoveItem(contained); } contained.Container = null; } public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true) { if (!MathUtils.IsValid(simPosition)) { string errorMsg = "Attempted to move the item " + Name + " to an invalid position (" + simPosition + ")\n" + Environment.StackTrace; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Item.SetPosition:InvalidPosition" + ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } if (body != null) { #if DEBUG try { #endif if (body.Enabled) { body.SetTransform(simPosition, rotation); } else { body.SetTransformIgnoreContacts(simPosition, rotation); } #if DEBUG } catch (Exception e) { DebugConsole.ThrowError("Failed to set item transform", e); } #endif } Vector2 displayPos = ConvertUnits.ToDisplayUnits(simPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); if (findNewHull) FindHull(); } partial void SetActiveSprite(); public override void Move(Vector2 amount) { base.Move(amount); if (ItemList != null && body != null) { body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), body.Rotation); } foreach (ItemComponent ic in components) { ic.Move(amount); } if (body != null && (Submarine==null || !Submarine.Loading)) FindHull(); } public Rectangle TransformTrigger(Rectangle trigger, bool world = false) { return world ? new Rectangle( WorldRect.X + trigger.X, WorldRect.Y + trigger.Y, (trigger.Width == 0) ? Rect.Width : trigger.Width, (trigger.Height == 0) ? Rect.Height : trigger.Height) : new Rectangle( Rect.X + trigger.X, Rect.Y + trigger.Y, (trigger.Width == 0) ? Rect.Width : trigger.Width, (trigger.Height == 0) ? Rect.Height : trigger.Height); } /// /// goes through every item and re-checks which hull they are in /// public static void UpdateHulls() { foreach (Item item in ItemList) item.FindHull(); } public Hull FindHull() { if (parentInventory != null && parentInventory.Owner != null) { if (parentInventory.Owner is Character) { CurrentHull = ((Character)parentInventory.Owner).AnimController.CurrentHull; } else if (parentInventory.Owner is Item) { CurrentHull = ((Item)parentInventory.Owner).CurrentHull; } Submarine = parentInventory.Owner.Submarine; if (body != null) body.Submarine = Submarine; return CurrentHull; } CurrentHull = Hull.FindHull(WorldPosition, CurrentHull); if (body != null && body.Enabled) { Submarine = CurrentHull?.Submarine; body.Submarine = Submarine; } return CurrentHull; } public Item GetRootContainer() { if (Container == null) return null; Item rootContainer = Container; while (rootContainer.Container != null) { rootContainer = rootContainer.Container; } return rootContainer; } public void SetContainedItemPositions() { foreach (ItemComponent component in components) { (component as ItemContainer)?.SetContainedItemPositions(); } } public void AddTag(string tag) { if (tags.Contains(tag)) return; tags.Add(tag); } public bool HasTag(string tag) { if (tag == null) return true; return tags.Contains(tag) || prefab.Tags.Contains(tag); } public bool HasTag(IEnumerable allowedTags) { if (allowedTags == null) return true; foreach (string tag in allowedTags) { if (tags.Contains(tag)) return true; } return false; } public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb limb = null, bool isNetworkEvent = false) { if (statusEffectLists == null) return; if (!statusEffectLists.TryGetValue(type, out List statusEffects)) return; bool broken = condition <= 0.0f; foreach (StatusEffect effect in statusEffects) { if (broken && effect.type != ActionType.OnBroken) continue; ApplyStatusEffect(effect, type, deltaTime, character, limb, isNetworkEvent, false); } } readonly List targets = new List(); public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, bool isNetworkEvent = false, bool checkCondition = true) { if (!isNetworkEvent && checkCondition) { if (condition == 0.0f && effect.type != ActionType.OnBroken) return; } if (effect.type != type) return; bool hasTargets = (effect.TargetIdentifiers == null); targets.Clear(); if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { var containedItems = ContainedItems; if (containedItems != null) { foreach (Item containedItem in containedItems) { if (containedItem == null) continue; if (effect.TargetIdentifiers != null && !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) { continue; } hasTargets = true; targets.Add(containedItem); } } } if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems)) { effect.GetNearbyTargets(WorldPosition, targets); if (targets.Count > 0) { hasTargets = true; } } if (!hasTargets) { return; } if (effect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null) { targets.Add(CurrentHull); } if (effect.HasTargetType(StatusEffect.TargetType.This)) { foreach (var pobject in AllPropertyObjects) { targets.Add(pobject); } } if (effect.HasTargetType(StatusEffect.TargetType.Character)) { if (type == ActionType.OnContained && ParentInventory is CharacterInventory characterInventory) { targets.Add(characterInventory.Owner as ISerializableEntity); } else { targets.Add(character); } } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { targets.Add(limb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { targets.AddRange(character.AnimController.Limbs.ToList()); } if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) targets.Add(Container); effect.Apply(type, deltaTime, this, targets); } public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) { if (Indestructible) return new AttackResult(); float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; return new AttackResult(damageAmount, null); } private bool IsInWater() { if (CurrentHull == null) return true; float surfaceY = CurrentHull.Surface; return CurrentHull.WaterVolume > 0.0f && Position.Y < surfaceY; } public override void Update(float deltaTime, Camera cam) { //aitarget goes silent/invisible if the components don't keep it active if (aiTarget != null) { aiTarget.SightRange -= deltaTime * 1000.0f; aiTarget.SoundRange -= deltaTime * 1000.0f; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { sendConditionUpdateTimer -= deltaTime; if (conditionUpdatePending) { if (sendConditionUpdateTimer <= 0.0f) { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); lastSentCondition = condition; sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval; conditionUpdatePending = false; } } } ApplyStatusEffects(ActionType.Always, deltaTime, null); foreach (ItemComponent ic in components) { if (ic.Parent != null) ic.IsActive = ic.Parent.IsActive; #if CLIENT if (!ic.WasUsed) { ic.StopSounds(ActionType.OnUse); ic.StopSounds(ActionType.OnSecondaryUse); } #endif ic.WasUsed = false; ic.ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime); if (!ic.IsActive) continue; if (condition > 0.0f) { ic.Update(deltaTime, cam); #if CLIENT if (ic.IsActive) ic.PlaySound(ActionType.OnActive, WorldPosition); #endif } else { ic.UpdateBroken(deltaTime, cam); } } if (body != null && body.Enabled) { System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null); if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f) { UpdateTransform(); if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) { Spawner.AddToRemoveQueue(this); return; } } } UpdateNetPosition(deltaTime); inWater = IsInWater(); bool waterProof = WaterProof; if (inWater) { Item container = this.Container; while (!waterProof && container != null) { waterProof = container.WaterProof; container = container.Container; } } ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); if (body == null || !body.Enabled || !inWater || ParentInventory != null) return; ApplyWaterForces(); CurrentHull?.ApplyFlowForces(deltaTime, this); } public void UpdateTransform() { Submarine prevSub = Submarine; FindHull(); if (Submarine == null && prevSub != null) { body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation); } else if (Submarine != null && prevSub == null) { body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation); } Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); if (Math.Abs(body.LinearVelocity.X) > NetConfig.MaxPhysicsBodyVelocity || Math.Abs(body.LinearVelocity.Y) > NetConfig.MaxPhysicsBodyVelocity) { body.LinearVelocity = new Vector2( MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity), MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity)); } } /// /// Applies buoyancy, drag and angular drag caused by water /// private void ApplyWaterForces() { float forceFactor = 1.0f; if (CurrentHull != null) { float floor = CurrentHull.Rect.Y - CurrentHull.Rect.Height; float waterLevel = floor + CurrentHull.WaterVolume / CurrentHull.Rect.Width; //forceFactor is 1.0f if the item is completely submerged, //and goes to 0.0f as the item goes through the surface forceFactor = Math.Min((waterLevel - Position.Y) / rect.Height, 1.0f); if (forceFactor <= 0.0f) return; } float volume = body.Mass / body.Density; var uplift = -GameMain.World.Gravity * forceFactor * volume; Vector2 drag = body.LinearVelocity * volume; body.ApplyForce((uplift - drag) * 10.0f); //apply simple angular drag body.ApplyTorque(body.AngularVelocity * volume * -0.05f); } private bool OnCollision(Fixture f1, Fixture f2, Contact contact) { #if CLIENT if (GameMain.Client != null) return true; #endif Vector2 normal = contact.Manifold.LocalNormal; float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); if (ImpactTolerance > 0.0f && impact > ImpactTolerance) { ApplyStatusEffects(ActionType.OnImpact, 1.0f); #if SERVER GameMain.Server?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact }); #endif } var containedItems = ContainedItems; if (containedItems != null) { foreach (Item contained in containedItems) { if (contained.body == null) continue; contained.OnCollision(f1, f2, contact); } } return true; } public override void FlipX(bool relativeToSub) { base.FlipX(relativeToSub); if (Prefab.CanSpriteFlipX) { SpriteEffects ^= SpriteEffects.FlipHorizontally; } foreach (ItemComponent component in components) { component.FlipX(relativeToSub); } } public override void FlipY(bool relativeToSub) { base.FlipY(relativeToSub); if (Prefab.CanSpriteFlipY) { SpriteEffects ^= SpriteEffects.FlipVertically; } foreach (ItemComponent component in components) { component.FlipY(relativeToSub); } } public List GetConnectedComponents(bool recursive = false) where T : ItemComponent { List connectedComponents = new List(); if (recursive) { List alreadySearched = new List() { this }; GetConnectedComponentsRecursive(alreadySearched, connectedComponents); return connectedComponents; } ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) return connectedComponents; foreach (Connection c in connectionPanel.Connections) { var recipients = c.Recipients; foreach (Connection recipient in recipients) { var component = recipient.Item.GetComponent(); if (component != null) connectedComponents.Add(component); } } return connectedComponents; } private void GetConnectedComponentsRecursive(List alreadySearched, List connectedComponents) where T : ItemComponent { alreadySearched.Add(this); ConnectionPanel connectionPanel = GetComponent(); if (connectionPanel == null) return; foreach (Connection c in connectionPanel.Connections) { var recipients = c.Recipients; foreach (Connection recipient in recipients) { if (alreadySearched.Contains(recipient.Item)) continue; var component = recipient.Item.GetComponent(); if (component != null) { connectedComponents.Add(component); } recipient.Item.GetConnectedComponentsRecursive(alreadySearched, connectedComponents); } } } public List GetConnectedComponentsRecursive(Connection c) where T : ItemComponent { List connectedComponents = new List(); List alreadySearched = new List() { this }; GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); return connectedComponents; } private void GetConnectedComponentsRecursive(Connection c, List alreadySearched, List connectedComponents) where T : ItemComponent { alreadySearched.Add(this); var recipients = c.Recipients; foreach (Connection recipient in recipients) { if (alreadySearched.Contains(recipient.Item)) continue; var component = recipient.Item.GetComponent(); if (component != null) { connectedComponents.Add(component); } recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents); } } public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) { LastSentSignalRecipients.Clear(); if (connections == null) return; public List GetConnectedComponentsRecursive(Connection c) where T : ItemComponent { List connectedComponents = new List(); List alreadySearched = new List() { this }; GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents); if (!connections.TryGetValue(connectionName, out Connection c)) return; if (stepsTaken > 10) { //use a coroutine to prevent infinite loops by creating a one //frame delay if the "signal chain" gets too long CoroutineManager.StartCoroutine(SendSignal(signal, c, sender, power, signalStrength)); } else { c.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength); } } private IEnumerable SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f) { //wait one frame yield return CoroutineStatus.Running; connection.SendSignal(0, signal, this, sender, power, signalStrength); yield return CoroutineStatus.Success; } public float GetDrawDepth() { return SpriteDepth + ((ID % 255) * 0.000001f); } public bool IsInsideTrigger(Vector2 worldPosition) { return IsInsideTrigger(worldPosition, out _); } public bool IsInsideTrigger(Vector2 worldPosition, out Rectangle transformedTrigger) { foreach (Rectangle trigger in Prefab.Triggers) { transformedTrigger = TransformTrigger(trigger, true); if (Submarine.RectContains(transformedTrigger, worldPosition)) return true; } transformedTrigger = Rectangle.Empty; return false; } public bool CanClientAccess(Client c) { return c != null && c.Character != null && c.Character.CanInteractWith(this); } public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false) { bool hasRequiredSkills = true; bool picked = false, selected = false; Skill requiredSkill = null; foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; if (Screen.Selected == GameMain.SubEditorScreen) { pickHit = picker.IsKeyHit(InputType.Select); selectHit = picker.IsKeyHit(InputType.Select); } else { if (forceSelectKey) { if (ic.PickKey == InputType.Select) pickHit = true; if (ic.SelectKey == InputType.Select) selectHit = true; } else if (forceActionKey) { if (ic.PickKey == InputType.Use) pickHit = true; if (ic.SelectKey == InputType.Use) selectHit = true; } else { pickHit = picker.IsKeyHit(ic.PickKey); selectHit = picker.IsKeyHit(ic.SelectKey); #if CLIENT //if the cursor is on a UI component, disable interaction with the left mouse button //to prevent accidentally selecting items when clicking UI elements if (picker == Character.Controlled && GUI.MouseOn != null) { if (GameMain.Config.KeyBind(ic.PickKey).MouseButton == 0) pickHit = false; if (GameMain.Config.KeyBind(ic.SelectKey).MouseButton == 0) selectHit = false; } #endif } } if (!pickHit && !selectHit) continue; if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) hasRequiredSkills = false; bool showUiMsg = false; #if CLIENT showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; #endif if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue; if ((ic.CanBePicked && pickHit && ic.Pick(picker)) || (ic.CanBeSelected && selectHit && ic.Select(picker))) { picked = true; ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT if (picker == Character.Controlled) GUI.ForceMouseOn(null); #endif if (tempRequiredSkill != null) requiredSkill = tempRequiredSkill; if (ic.CanBeSelected) selected = true; } } if (!picked) return false; if (picker.SelectedConstruction == this) { if (picker.IsKeyHit(InputType.Select) || forceSelectKey) picker.SelectedConstruction = null; } else if (selected) { picker.SelectedConstruction = this; } #if CLIENT if (!hasRequiredSkills && Character.Controlled == picker && Screen.Selected != GameMain.SubEditorScreen) { if (requiredSkill != null) { GUI.AddMessage(TextManager.Get("InsufficientSkills") .Replace("[requiredskill]", TextManager.Get("SkillName." + requiredSkill.Identifier)) .Replace("[requiredlevel]", ((int)requiredSkill.Level).ToString()), Color.Red); } } #endif if (Container != null) Container.RemoveContained(this); return true; } public void Use(float deltaTime, Character character = null, Limb targetLimb = null) { if (condition == 0.0f) return; bool remove = false; foreach (ItemComponent ic in components) { bool isControlled = false; #if CLIENT isControlled = character == Character.Controlled; #endif if (!ic.HasRequiredContainedItems(isControlled)) continue; if (ic.Use(deltaTime, character)) { ic.WasUsed = true; #if CLIENT ic.PlaySound(ActionType.OnUse, WorldPosition, character); #endif ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb); if (ic.DeleteOnUse) remove = true; } } if (remove) { Spawner.AddToRemoveQueue(this); } } public void SecondaryUse(float deltaTime, Character character = null) { if (condition == 0.0f) return; bool remove = false; foreach (ItemComponent ic in components) { bool isControlled = false; #if CLIENT isControlled = character == Character.Controlled; #endif if (!ic.HasRequiredContainedItems(isControlled)) continue; if (ic.SecondaryUse(deltaTime, character)) { ic.WasUsed = true; #if CLIENT ic.PlaySound(ActionType.OnSecondaryUse, WorldPosition, character); #endif ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character); if (ic.DeleteOnUse) remove = true; } } if (remove) { Spawner.AddToRemoveQueue(this); } } public void ApplyTreatment(Character user, Character character, Limb targetLimb) { //can't apply treatment to dead characters if (character.IsDead) return; if (!UseInHealthInterface) return; #if CLIENT if (GameMain.Client != null) { GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Treatment, character.ID, targetLimb }); return; } #endif bool remove = false; foreach (ItemComponent ic in components) { if (!ic.HasRequiredContainedItems(user == Character.Controlled)) continue; bool success = Rand.Range(0.0f, 1.0f) < ic.DegreeOfSuccess(user); ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; #if CLIENT ic.PlaySound(actionType, user.WorldPosition, user); #endif ic.WasUsed = true; ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user); if (GameMain.NetworkMember!=null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, ic, character.ID, targetLimb }); } if (ic.DeleteOnUse) remove = true; } if (remove) { Spawner?.AddToRemoveQueue(this); } } public List GetHUDTexts(Character character) { List texts = new List(); foreach (ItemComponent ic in components) { if (string.IsNullOrEmpty(ic.Msg)) continue; if (!ic.CanBePicked && !ic.CanBeSelected) continue; if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue; Color color = Color.Red; if (ic.HasRequiredSkills(character) && ic.HasRequiredItems(character, false)) color = Color.Orange; texts.Add(new ColoredText(ic.Msg, color, false)); } return texts; } public bool Combine(Item item) { bool isCombined = false; foreach (ItemComponent ic in components) { if (ic.Combine(item)) isCombined = true; } return isCombined; } public void Drop(Character dropper, bool createNetworkEvent = true) { if (createNetworkEvent) { if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && GameMain.NetworkMember != null && (GameMain.NetworkMember.IsServer || Character.Controlled == dropper)) { parentInventory.CreateNetworkEvent(); } } foreach (ItemComponent ic in components) { ic.Drop(dropper); } if (Container != null) { if (body != null) { body.Enabled = true; body.LinearVelocity = Vector2.Zero; } SetTransform(Container.SimPosition, 0.0f); Container.RemoveContained(this); Container = null; } if (parentInventory != null) { parentInventory.RemoveItem(this); parentInventory = null; } } public void Equip(Character character) { foreach (ItemComponent ic in components) ic.Equip(character); } public void Unequip(Character character) { character.DeselectItem(this); foreach (ItemComponent ic in components) ic.Unequip(character); } public List> GetProperties() { List> allProperties = new List>(); List itemProperties = SerializableProperty.GetProperties(this); foreach (var itemProperty in itemProperties) { allProperties.Add(new Pair(this, itemProperty)); } foreach (ItemComponent ic in components) { List componentProperties = SerializableProperty.GetProperties(ic); foreach (var componentProperty in componentProperties) { allProperties.Add(new Pair(ic, componentProperty)); } } return allProperties; } private void WritePropertyChange(NetBuffer msg, object[] extraData, bool inGameEditableOnly) { var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); SerializableProperty property = extraData[1] as SerializableProperty; if (property != null) { var propertyOwner = allProperties.Find(p => p.Second == property); if (allProperties.Count > 1) { msg.WriteRangedInteger(0, allProperties.Count - 1, allProperties.FindIndex(p => p.Second == property)); } object value = property.GetValue(propertyOwner.First); if (value is string) { msg.Write((string)value); } else if (value is float) { msg.Write((float)value); } else if (value is int) { msg.Write((int)value); } else if (value is bool) { msg.Write((bool)value); } else if (value is Color color) { msg.Write(color.R); msg.Write(color.G); msg.Write(color.B); msg.Write(color.A); } else if (value is Vector2) { msg.Write(((Vector2)value).X); msg.Write(((Vector2)value).Y); } else if (value is Vector3) { msg.Write(((Vector3)value).X); msg.Write(((Vector3)value).Y); msg.Write(((Vector3)value).Z); } else if (value is Vector4) { msg.Write(((Vector4)value).X); msg.Write(((Vector4)value).Y); msg.Write(((Vector4)value).Z); msg.Write(((Vector4)value).W); } else if (value is Point) { msg.Write(((Point)value).X); msg.Write(((Point)value).Y); } else if (value is Rectangle) { msg.Write(((Rectangle)value).X); msg.Write(((Rectangle)value).Y); msg.Write(((Rectangle)value).Width); msg.Write(((Rectangle)value).Height); } else if (value is Enum) { msg.Write((int)value); } else { throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported"); } } else { throw new ArgumentException("Failed to write propery value - property \"" + (property == null ? "null" : property.Name) + "\" is not serializable."); } } private void ReadPropertyChange(NetBuffer msg, bool inGameEditableOnly, Client sender = null) { var allProperties = inGameEditableOnly ? GetProperties() : GetProperties(); if (allProperties.Count == 0) { return; } int propertyIndex = 0; if (allProperties.Count > 1) { propertyIndex = msg.ReadRangedInteger(0, allProperties.Count - 1); } bool allowEditing = true; object parentObject = allProperties[propertyIndex].First; SerializableProperty property = allProperties[propertyIndex].Second; if (inGameEditableOnly && parentObject is ItemComponent ic) { if (!ic.AllowInGameEditing) allowEditing = false; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !CanClientAccess(sender)) { allowEditing = false; } Type type = property.PropertyType; if (type == typeof(string)) { string val = msg.ReadString(); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(float)) { float val = msg.ReadFloat(); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(int)) { int val = msg.ReadInt32(); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(bool)) { bool val = msg.ReadBoolean(); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Color)) { Color val = new Color(msg.ReadByte(), msg.ReadByte(), msg.ReadByte(), msg.ReadByte()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Vector2)) { Vector2 val = new Vector2(msg.ReadFloat(), msg.ReadFloat()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Vector3)) { Vector3 val = new Vector3(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Vector4)) { Vector4 val = new Vector4(msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat(), msg.ReadFloat()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Point)) { Point val = new Point(msg.ReadInt32(), msg.ReadInt32()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (type == typeof(Rectangle)) { Rectangle val = new Rectangle(msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32(), msg.ReadInt32()); if (allowEditing) property.TrySetValue(parentObject, val); } else if (typeof(Enum).IsAssignableFrom(type)) { int intVal = msg.ReadInt32(); try { if (allowEditing) property.TrySetValue(parentObject, Enum.ToObject(type, intVal)); } catch (Exception e) { #if DEBUG DebugConsole.ThrowError("Failed to convert the int value \"" + intVal + "\" to " + type, e); #endif GameAnalyticsManager.AddErrorEventOnce( "Item.ReadPropertyChange:" + Name + ":" + type, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, "Failed to convert the int value \"" + intVal + "\" to " + type + " (item " + Name + ")"); } } else { return; } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property }); } } partial void UpdateNetPosition(float deltaTime); public static Item Load(XElement element, Submarine submarine) { return Load(element, submarine, createNetworkEvent: false); } /// /// Instantiate a new item and load its data from the XML element. /// /// The element containing the data of the item /// The submarine to spawn the item in (can be null) /// Should an EntitySpawner event be created to notify clients about the item being created. /// public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent) { string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); ItemPrefab prefab; if (string.IsNullOrEmpty(identifier)) { //legacy support: //1. attempt to find a prefab with an empty identifier and a matching name prefab = MapEntityPrefab.Find(name, "", showErrorMessages: false) as ItemPrefab; //2. not found, attempt to find a prefab with a matching name if (prefab == null) prefab = MapEntityPrefab.Find(name) as ItemPrefab; } else { prefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false) as ItemPrefab; //not found, see if we can find a prefab with a matching alias if (prefab == null) { string lowerCaseName = name.ToLowerInvariant(); prefab = MapEntityPrefab.List.Find(me => me.Aliases != null && me.Aliases.Contains(lowerCaseName)) as ItemPrefab; } } if (prefab == null) { DebugConsole.ThrowError("Error loading item - item prefab \"" + name + "\" (identifier \"" + identifier + "\") not found."); return null; } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Width == 0 && rect.Height == 0) { rect.Width = (int)(prefab.Size.X * prefab.Scale); rect.Height = (int)(prefab.Size.Y * prefab.Scale); } Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false) { Submarine = submarine, ID = (ushort)int.Parse(element.Attribute("ID").Value), linkedToID = new List() }; #if SERVER if (createNetworkEvent) { Spawner.CreateNetworkEvent(item, remove: false); } #endif foreach (XAttribute attribute in element.Attributes()) { if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) continue; bool shouldBeLoaded = false; foreach (var propertyAttribute in property.Attributes.OfType()) { if (propertyAttribute.isSaveable) { shouldBeLoaded = true; break; } } if (shouldBeLoaded) { property.TrySetValue(item, attribute.Value); } } string linkedToString = element.GetAttributeString("linked", ""); if (linkedToString != "") { string[] linkedToIds = linkedToString.Split(','); for (int i = 0; i < linkedToIds.Length; i++) { item.linkedToID.Add((ushort)int.Parse(linkedToIds[i])); } } List unloadedComponents = new List(item.components); foreach (XElement subElement in element.Elements()) { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); if (component == null) continue; component.Load(subElement); unloadedComponents.Remove(component); } if (element.GetAttributeBool("flippedx", false)) item.FlipX(false); if (element.GetAttributeBool("flippedy", false)) item.FlipY(false); item.condition = element.GetAttributeFloat("condition", item.Prefab.Health); item.SetActiveSprite(); foreach (ItemComponent component in item.components) { component.OnItemLoaded(); } return item; } public override XElement Save(XElement parentElement) { XElement element = new XElement("Item"); element.Add( new XAttribute("name", prefab.Name), new XAttribute("identifier", prefab.Identifier), new XAttribute("ID", ID)); if (FlippedX) element.Add(new XAttribute("flippedx", true)); if (FlippedY) element.Add(new XAttribute("flippedy", true)); if (condition < Prefab.Health) { element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture))); } Item rootContainer = GetRootContainer() ?? this; System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character); Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition; element.Add(new XAttribute("rect", (int)(rect.X - subPosition.X) + "," + (int)(rect.Y - subPosition.Y) + "," + rect.Width + "," + rect.Height)); if (linkedTo != null && linkedTo.Count > 0) { var saveableLinked = linkedTo.Where(l => l.ShouldBeSaved).ToList(); element.Add(new XAttribute("linked", string.Join(",", saveableLinked.Select(l => l.ID.ToString())))); } SerializableProperty.SerializeProperties(this, element); foreach (ItemComponent ic in components) { ic.Save(element); } parentElement.Add(element); return element; } public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); components.ForEach(c => c.Reset()); } public override void OnMapLoaded() { FindHull(); foreach (ItemComponent ic in components) { ic.OnMapLoaded(); } } /// /// Remove the item so that it doesn't appear to exist in the game world (stop sounds, remove bodies etc) /// but don't reset anything that's required for cloning the item /// public override void ShallowRemove() { base.ShallowRemove(); foreach (ItemComponent ic in components) { ic.ShallowRemove(); } ItemList.Remove(this); if (body != null) { body.Remove(); body = null; } } public override void Remove() { if (Removed) { DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace); return; } DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")"); base.Remove(); foreach (Character character in Character.CharacterList) { if (character.SelectedConstruction == this) character.SelectedConstruction = null; for (int i = 0; i < character.SelectedItems.Length; i++) { if (character.SelectedItems[i] == this) character.SelectedItems[i] = null; } } if (parentInventory != null) { parentInventory.RemoveItem(this); parentInventory = null; } foreach (ItemComponent ic in components) { ic.Remove(); } ItemList.Remove(this); if (body != null) { body.Remove(); body = null; } foreach (Item it in ItemList) { if (it.linkedTo.Contains(this)) { it.linkedTo.Remove(this); } } RemoveProjSpecific(); } partial void RemoveProjSpecific(); } }