2233 lines
78 KiB
C#
2233 lines
78 KiB
C#
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<Item> ItemList = new List<Item>();
|
|
public ItemPrefab Prefab => prefab as ItemPrefab;
|
|
|
|
public static bool ShowLinks = true;
|
|
|
|
private HashSet<string> 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<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
|
|
private List<ItemComponent> components;
|
|
private List<IDrawableComponent> 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 Rectangle defaultRect;
|
|
|
|
private Dictionary<string, Connection> connections;
|
|
|
|
private List<Repairable> repairables;
|
|
|
|
//a dictionary containing lists of the status effects in all the components of the item
|
|
private bool[] hasStatusEffectsOfType;
|
|
private Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
|
|
|
|
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
|
|
|
|
private bool? hasInGameEditableProperties;
|
|
bool HasInGameEditableProperties
|
|
{
|
|
get
|
|
{
|
|
if (hasInGameEditableProperties == null)
|
|
{
|
|
hasInGameEditableProperties = false;
|
|
if (SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
|
|
{
|
|
hasInGameEditableProperties = true;
|
|
}
|
|
else
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
if (!component.AllowInGameEditing) { continue; }
|
|
if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().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; }
|
|
}
|
|
|
|
[Editable, Serialize(false, true)]
|
|
public bool HiddenInGame
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
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 : body.Position;
|
|
}
|
|
}
|
|
|
|
public override Vector2 SimPosition
|
|
{
|
|
get
|
|
{
|
|
return (body == null) ? ConvertUnits.ToSimUnits(base.Position) : body.SimPosition;
|
|
}
|
|
}
|
|
|
|
public Rectangle InteractionRect
|
|
{
|
|
get
|
|
{
|
|
return WorldRect;
|
|
}
|
|
}
|
|
|
|
private float scale = 1.0f;
|
|
public override float Scale
|
|
{
|
|
get { return scale; }
|
|
set
|
|
{
|
|
if (scale == value) { return; }
|
|
scale = MathHelper.Clamp(value, 0.1f, 10.0f);
|
|
|
|
float relativeScale = scale / prefab.Scale;
|
|
|
|
if (!ResizeHorizontal || !ResizeVertical)
|
|
{
|
|
int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale);
|
|
int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale);
|
|
Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
|
|
}
|
|
|
|
if (components != null)
|
|
{
|
|
foreach (ItemComponent component in components)
|
|
{
|
|
component.OnScaleChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public float PositionUpdateInterval
|
|
{
|
|
get;
|
|
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)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to check what item this item is contained inside
|
|
/// </summary>
|
|
public string ContainerIdentifier
|
|
{
|
|
get
|
|
{
|
|
return
|
|
Container?.prefab.Identifier ??
|
|
ParentInventory?.Owner?.ToString() ??
|
|
"";
|
|
}
|
|
set { /*do nothing*/ }
|
|
}
|
|
|
|
[Serialize(false, false)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to check if the physics body of the item is active
|
|
/// </summary>
|
|
public bool PhysicsBodyActive
|
|
{
|
|
get
|
|
{
|
|
return body != null && body.Enabled;
|
|
}
|
|
set { /*do nothing*/ }
|
|
}
|
|
|
|
[Serialize(0.0f, false)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to modify the sound range
|
|
/// </summary>
|
|
public float SoundRange
|
|
{
|
|
get { return aiTarget == null ? 0.0f : aiTarget.SoundRange; }
|
|
set { if (aiTarget != null) { aiTarget.SoundRange = Math.Max(0.0f, value); } }
|
|
}
|
|
|
|
[Serialize(0.0f, false)]
|
|
/// <summary>
|
|
/// Can be used by status effects or conditionals to modify the sound range
|
|
/// </summary>
|
|
public float SightRange
|
|
{
|
|
get { return aiTarget == null ? 0.0f : aiTarget.SightRange; }
|
|
set { if (aiTarget != null) { aiTarget.SightRange = Math.Max(0.0f, value); } }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should the item's Use method be called with the "Use" or with the "Shoot" key?
|
|
/// </summary>
|
|
[Serialize(false, false)]
|
|
public bool IsShootable { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, the user has to hold the "aim" key before use is registered. False by default.
|
|
/// </summary>
|
|
[Serialize(false, false)]
|
|
public bool RequireAimToUse
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If true, the user has to hold the "aim" key before secondary use is registered. True by default.
|
|
/// </summary>
|
|
[Serialize(true, false)]
|
|
public bool RequireAimToSecondaryUse
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
public Color Color
|
|
{
|
|
get { return spriteColor; }
|
|
}
|
|
|
|
public bool IsFullCondition => MathUtils.NearlyEqual(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;
|
|
bool wasInFullCondition = IsFullCondition;
|
|
|
|
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)
|
|
{
|
|
if (Math.Abs(lastSentCondition - condition) > 1.0f)
|
|
{
|
|
conditionUpdatePending = true;
|
|
}
|
|
else if (wasInFullCondition != IsFullCondition)
|
|
{
|
|
conditionUpdatePending = true;
|
|
}
|
|
else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= Prefab.Health))
|
|
{
|
|
conditionUpdatePending = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public float Health
|
|
{
|
|
get { return condition; }
|
|
}
|
|
|
|
private bool? indestructible;
|
|
/// <summary>
|
|
/// Per-instance value - if not set, the value of the prefab is used.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A list of items the last signal sent by this item went through
|
|
/// </summary>
|
|
public List<Item> LastSentSignalRecipients
|
|
{
|
|
get;
|
|
private set;
|
|
} = new List<Item>();
|
|
|
|
public string ConfigFile
|
|
{
|
|
get { return Prefab.ConfigFile; }
|
|
}
|
|
|
|
//which type of inventory slots (head, torso, any, etc) the item can be placed in
|
|
public List<InvSlotType> AllowedSlots
|
|
{
|
|
get
|
|
{
|
|
Pickable p = GetComponent<Pickable>();
|
|
return (p == null) ? new List<InvSlotType>() { InvSlotType.Any } : p.AllowedSlots;
|
|
}
|
|
}
|
|
|
|
public List<Connection> Connections
|
|
{
|
|
get
|
|
{
|
|
ConnectionPanel panel = GetComponent<ConnectionPanel>();
|
|
if (panel == null) return null;
|
|
return panel.Connections;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<Item> 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<Repairable> Repairables
|
|
{
|
|
get { return repairables; }
|
|
}
|
|
|
|
public IEnumerable<ItemComponent> 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<ISerializableEntity> allPropertyObjects = new List<ISerializableEntity>();
|
|
public IEnumerable<ISerializableEntity> 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)
|
|
{
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new item
|
|
/// </summary>
|
|
/// <param name="callOnItemLoaded">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).</param>
|
|
public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true)
|
|
: base(itemPrefab, submarine)
|
|
{
|
|
spriteColor = prefab.SpriteColor;
|
|
|
|
components = new List<ItemComponent>();
|
|
drawableComponents = new List<IDrawableComponent>();
|
|
tags = new HashSet<string>();
|
|
repairables = new List<Repairable>();
|
|
|
|
defaultRect = newRect;
|
|
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":
|
|
case "fabricate":
|
|
case "fabricable":
|
|
case "fabricableitem":
|
|
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;
|
|
}
|
|
}
|
|
|
|
hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
if (ic.statusEffectLists == null) continue;
|
|
|
|
if (statusEffectLists == null)
|
|
{
|
|
statusEffectLists = new Dictionary<ActionType, List<StatusEffect>>();
|
|
}
|
|
|
|
//go through all the status effects of the component
|
|
//and add them to the corresponding statuseffect list
|
|
foreach (List<StatusEffect> componentEffectList in ic.statusEffectLists.Values)
|
|
{
|
|
ActionType actionType = componentEffectList.First().type;
|
|
if (!statusEffectLists.TryGetValue(actionType, out List<StatusEffect> statusEffectList))
|
|
{
|
|
statusEffectList = new List<StatusEffect>();
|
|
statusEffectLists.Add(actionType, statusEffectList);
|
|
hasStatusEffectsOfType[(int)actionType] = true;
|
|
}
|
|
|
|
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<ConnectionPanel>();
|
|
if (connectionPanel != null)
|
|
{
|
|
connections = new Dictionary<string, Connection>();
|
|
foreach (Connection c in connectionPanel.Connections)
|
|
{
|
|
if (!connections.ContainsKey(c.Name))
|
|
connections.Add(c.Name, c);
|
|
}
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
body.FarseerBody.OnCollision += OnCollision;
|
|
}
|
|
|
|
var itemContainer = GetComponent<ItemContainer>();
|
|
if (itemContainer != null)
|
|
{
|
|
ownInventory = itemContainer.Inventory;
|
|
}
|
|
|
|
InitProjSpecific();
|
|
|
|
InsertToList();
|
|
ItemList.Add(this);
|
|
|
|
if (callOnItemLoaded)
|
|
{
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.OnItemLoaded();
|
|
}
|
|
}
|
|
|
|
DebugConsole.Log("Created " + Name + " (" + ID + ")");
|
|
}
|
|
|
|
partial void InitProjSpecific();
|
|
|
|
public override MapEntity Clone()
|
|
{
|
|
Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false)
|
|
{
|
|
defaultRect = defaultRect
|
|
};
|
|
foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
|
|
{
|
|
if (!property.Value.Attributes.OfType<Editable>().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<string, SerializableProperty> property in components[i].SerializableProperties)
|
|
{
|
|
if (!property.Value.Attributes.OfType<Editable>().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<T>() where T : ItemComponent
|
|
{
|
|
if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
|
|
{
|
|
return (T)component;
|
|
}
|
|
|
|
return default(T);
|
|
}
|
|
|
|
public IEnumerable<T> GetComponents<T>()
|
|
{
|
|
if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
|
|
|
|
return components.Where(c => c is T).Cast<T>();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public void SetActiveSprite()
|
|
{
|
|
SetActiveSpriteProjSpecific();
|
|
}
|
|
|
|
partial void SetActiveSpriteProjSpecific();
|
|
|
|
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(
|
|
(int)(WorldRect.X + trigger.X * Scale),
|
|
(int)(WorldRect.Y + trigger.Y * Scale),
|
|
(trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale),
|
|
(trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale))
|
|
:
|
|
new Rectangle(
|
|
(int)(Rect.X + trigger.X * Scale),
|
|
(int)(Rect.Y + trigger.Y * Scale),
|
|
(trigger.Width == 0) ? Rect.Width : (int)(trigger.Width * Scale),
|
|
(trigger.Height == 0) ? Rect.Height : (int)(trigger.Height * Scale));
|
|
}
|
|
|
|
/// <summary>
|
|
/// goes through every item and re-checks which hull they are in
|
|
/// </summary>
|
|
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<string> 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 (!hasStatusEffectsOfType[(int)type]) { return; }
|
|
foreach (StatusEffect effect in statusEffectLists[type])
|
|
{
|
|
ApplyStatusEffect(effect, type, deltaTime, character, limb, isNetworkEvent, false);
|
|
}
|
|
}
|
|
|
|
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
|
|
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 * (aiTarget.MaxSightRange / aiTarget.FadeOutTime);
|
|
aiTarget.SoundRange -= deltaTime * (aiTarget.MaxSoundRange / aiTarget.FadeOutTime);
|
|
}
|
|
|
|
bool broken = condition <= 0.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 (broken)
|
|
{
|
|
ic.UpdateBroken(deltaTime, cam);
|
|
}
|
|
else
|
|
{
|
|
ic.Update(deltaTime, cam);
|
|
#if CLIENT
|
|
if (ic.IsActive) ic.PlaySound(ActionType.OnActive, WorldPosition);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
if (!broken)
|
|
{
|
|
ApplyStatusEffects(!waterProof && inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
|
|
}
|
|
|
|
if (body == null || !body.Enabled || !inWater || ParentInventory != null || Removed) { 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);
|
|
}
|
|
else if (Submarine != null && prevSub != null && Submarine != prevSub)
|
|
{
|
|
body.SetTransform(body.SimPosition + prevSub.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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies buoyancy, drag and angular drag caused by water
|
|
/// </summary>
|
|
private void ApplyWaterForces()
|
|
{
|
|
if (body.Mass <= 0.0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
|
|
//apply simple angular drag
|
|
body.ApplyTorque(body.AngularVelocity * volume * -0.05f);
|
|
}
|
|
|
|
private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
|
|
{
|
|
Vector2 normal = contact.Manifold.LocalNormal;
|
|
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
|
|
|
OnCollisionProjSpecific(f1, f2, contact, impact);
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; }
|
|
|
|
if (ImpactTolerance > 0.0f && condition > 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;
|
|
}
|
|
|
|
partial void OnCollisionProjSpecific(Fixture f1, Fixture f2, Contact contact, float impact);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
|
|
/// </summary>
|
|
public List<T> GetConnectedComponents<T>(bool recursive = false) where T : ItemComponent
|
|
{
|
|
List<T> connectedComponents = new List<T>();
|
|
|
|
if (recursive)
|
|
{
|
|
HashSet<Connection> alreadySearched = new HashSet<Connection>();
|
|
GetConnectedComponentsRecursive(alreadySearched, connectedComponents);
|
|
return connectedComponents;
|
|
}
|
|
|
|
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
|
|
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<T>();
|
|
if (component != null) connectedComponents.Add(component);
|
|
}
|
|
}
|
|
|
|
return connectedComponents;
|
|
}
|
|
|
|
private void GetConnectedComponentsRecursive<T>(HashSet<Connection> alreadySearched, List<T> connectedComponents) where T : ItemComponent
|
|
{
|
|
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
|
|
if (connectionPanel == null) { return; }
|
|
|
|
foreach (Connection c in connectionPanel.Connections)
|
|
{
|
|
if (alreadySearched.Contains(c)) { continue; }
|
|
alreadySearched.Add(c);
|
|
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
|
|
/// </summary>
|
|
public List<T> GetConnectedComponentsRecursive<T>(Connection c) where T : ItemComponent
|
|
{
|
|
List<T> connectedComponents = new List<T>();
|
|
HashSet<Connection> alreadySearched = new HashSet<Connection>();
|
|
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents);
|
|
|
|
return connectedComponents;
|
|
}
|
|
|
|
private static readonly Pair<string, string>[] connectionPairs = new Pair<string, string>[]
|
|
{
|
|
new Pair<string, string>("power_in", "power_out"),
|
|
new Pair<string, string>("signal_in1", "signal_out1"),
|
|
new Pair<string, string>("signal_in2", "signal_out2"),
|
|
new Pair<string, string>("signal_in3", "signal_out3"),
|
|
new Pair<string, string>("signal_in4", "signal_out4"),
|
|
new Pair<string, string>("signal_in", "signal_out"),
|
|
new Pair<string, string>("signal_in1", "signal_out"),
|
|
new Pair<string, string>("signal_in2", "signal_out")
|
|
};
|
|
|
|
private void GetConnectedComponentsRecursive<T>(Connection c, HashSet<Connection> alreadySearched, List<T> connectedComponents) where T : ItemComponent
|
|
{
|
|
alreadySearched.Add(c);
|
|
|
|
var recipients = c.Recipients;
|
|
foreach (Connection recipient in recipients)
|
|
{
|
|
if (alreadySearched.Contains(recipient)) { continue; }
|
|
var component = recipient.Item.GetComponent<T>();
|
|
if (component != null)
|
|
{
|
|
connectedComponents.Add(component);
|
|
}
|
|
|
|
//connected to a wifi component -> see which other wifi components it can communicate with
|
|
var wifiComponent = recipient.Item.GetComponent<WifiComponent>();
|
|
if (wifiComponent != null && wifiComponent.CanTransmit())
|
|
{
|
|
foreach (var wifiReceiver in wifiComponent.GetReceiversInRange())
|
|
{
|
|
var receiverConnections = wifiReceiver.Item.Connections;
|
|
if (receiverConnections == null) { continue; }
|
|
foreach (Connection wifiOutput in receiverConnections)
|
|
{
|
|
if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
|
|
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents);
|
|
}
|
|
}
|
|
}
|
|
|
|
recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents);
|
|
}
|
|
|
|
foreach (Pair<string, string> connectionPair in connectionPairs)
|
|
{
|
|
if (connectionPair.First == c.Name)
|
|
{
|
|
var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.Second);
|
|
if (pairedConnection != null)
|
|
{
|
|
if (alreadySearched.Contains(pairedConnection)) { continue; }
|
|
GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents);
|
|
}
|
|
}
|
|
else if (connectionPair.Second == c.Name)
|
|
{
|
|
var pairedConnection = c.Item.Connections.FirstOrDefault(c2 => c2.Name == connectionPair.First);
|
|
if (pairedConnection != null)
|
|
{
|
|
if (alreadySearched.Contains(pairedConnection)) { continue; }
|
|
GetConnectedComponentsRecursive(pairedConnection, 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;
|
|
|
|
stepsTaken++;
|
|
|
|
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<object> 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 (picker.IsKeyDown(InputType.Aim))
|
|
{
|
|
pickHit = false;
|
|
selectHit = false;
|
|
}
|
|
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 != null)
|
|
{
|
|
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.GetWithVariables("InsufficientSkills", new string[2] { "[requiredskill]", "[requiredlevel]" },
|
|
new string[2] { TextManager.Get("SkillName." + requiredSkill.Identifier), ((int)requiredSkill.Level).ToString() }, new bool[2] { true, false }), Color.Red);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (Container != null) Container.RemoveContained(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
|
|
{
|
|
if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
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, 0.5f) < 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 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();
|
|
//send frequent updates after the item has been dropped
|
|
PositionUpdateInterval = 0.0f;
|
|
}
|
|
}
|
|
|
|
if (body != null)
|
|
{
|
|
body.Enabled = true;
|
|
body.ResetDynamics();
|
|
if (dropper != null)
|
|
{
|
|
body.SetTransform(dropper.SimPosition, 0.0f);
|
|
}
|
|
}
|
|
|
|
foreach (ItemComponent ic in components) { ic.Drop(dropper); }
|
|
|
|
if (Container != null)
|
|
{
|
|
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<Pair<object, SerializableProperty>> GetProperties<T>()
|
|
{
|
|
List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();
|
|
|
|
List<SerializableProperty> itemProperties = SerializableProperty.GetProperties<T>(this);
|
|
foreach (var itemProperty in itemProperties)
|
|
{
|
|
allProperties.Add(new Pair<object, SerializableProperty>(this, itemProperty));
|
|
}
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
List<SerializableProperty> componentProperties = SerializableProperty.GetProperties<T>(ic);
|
|
foreach (var componentProperty in componentProperties)
|
|
{
|
|
allProperties.Add(new Pair<object, SerializableProperty>(ic, componentProperty));
|
|
}
|
|
}
|
|
return allProperties;
|
|
}
|
|
|
|
private void WritePropertyChange(NetBuffer msg, object[] extraData, bool inGameEditableOnly)
|
|
{
|
|
var allProperties = inGameEditableOnly ? GetProperties<InGameEditable>() : GetProperties<Editable>();
|
|
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<InGameEditable>() : GetProperties<Editable>();
|
|
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);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Instantiate a new item and load its data from the XML element.
|
|
/// </summary>
|
|
/// <param name="element">The element containing the data of the item</param>
|
|
/// <param name="submarine">The submarine to spawn the item in (can be null)</param>
|
|
/// <param name="createNetworkEvent">Should an EntitySpawner event be created to notify clients about the item being created.</param>
|
|
/// <returns></returns>
|
|
public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent)
|
|
{
|
|
string name = element.Attribute("name").Value;
|
|
string identifier = element.GetAttributeString("identifier", "");
|
|
|
|
ItemPrefab prefab = ItemPrefab.Find(name, identifier);
|
|
|
|
if (prefab == null)
|
|
{
|
|
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<ushort>()
|
|
};
|
|
|
|
#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<Serialize>())
|
|
{
|
|
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<ItemComponent> unloadedComponents = new List<ItemComponent>(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.lastSentCondition = item.condition;
|
|
|
|
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.OriginalName),
|
|
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;
|
|
|
|
int width = ResizeHorizontal ? rect.Width : defaultRect.Width;
|
|
int height = ResizeVertical ? rect.Height : defaultRect.Height;
|
|
element.Add(new XAttribute("rect",
|
|
(int)(rect.X - subPosition.X) + "," +
|
|
(int)(rect.Y - subPosition.Y) + "," +
|
|
width + "," + 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);
|
|
Sprite.ReloadXML();
|
|
SpriteDepth = Sprite.Depth;
|
|
components.ForEach(c => c.Reset());
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
FindHull();
|
|
|
|
foreach (ItemComponent ic in components)
|
|
{
|
|
ic.OnMapLoaded();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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();
|
|
}
|
|
} |