Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs

1114 lines
43 KiB
C#

using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma;
using MoonSharp.Interpreter;
using Barotrauma.IO;
using Barotrauma.Networking;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Sounds;
#endif
namespace Barotrauma.Items.Components
{
interface IDrawableComponent
{
#if CLIENT
/// <summary>
/// The extents of the sprites or other graphics this component needs to draw. Used to determine which items are visible on the screen.
/// </summary>
Vector2 DrawSize { get; }
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1);
#endif
}
/// <summary>
/// The base class for components holding the different functionalities of the item
/// </summary>
partial class ItemComponent : ISerializableEntity
{
protected Item item;
protected string name;
private bool isActive;
protected bool characterUsable;
protected bool canBePicked;
protected bool canBeSelected;
protected bool canBeCombined;
protected bool removeOnCombined;
public bool WasUsed, WasSecondaryUsed;
public readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
public Dictionary<RelatedItem.RelationType, List<RelatedItem>> requiredItems;
public readonly List<RelatedItem> DisabledRequiredItems = new List<RelatedItem>();
public List<Skill> requiredSkills;
private ItemComponent parent;
public ItemComponent Parent
{
get { return parent; }
set
{
if (parent == value) { return; }
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
parent = value;
}
}
public readonly ContentXElement originalElement;
protected const float CorrectionDelay = 1.0f;
protected CoroutineHandle delayedCorrectionCoroutine;
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How long it takes to pick up the item (in seconds).")]
public float PickingTime
{
get;
set;
}
[Serialize("", IsPropertySaveable.No, description: "What to display on the progress bar when this item is being picked.")]
public string PickingMsg
{
get;
set;
}
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
public Action<bool> OnActiveStateChanged;
public virtual bool IsActive
{
get { return isActive; }
set
{
#if CLIENT
if (!value)
{
IsActiveTimer = 0.0f;
if (isActive)
{
StopSounds(ActionType.OnActive);
}
}
#endif
if (value != IsActive) { OnActiveStateChanged?.Invoke(value); }
isActive = value;
}
}
private bool drawable = true;
public List<PropertyConditional> IsActiveConditionals;
public bool Drawable
{
get { return drawable; }
set
{
if (value == drawable) return;
if (!(this is IDrawableComponent))
{
DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)");
return;
}
drawable = value;
if (drawable)
{
item.EnableDrawableComponent((IDrawableComponent)this);
}
else
{
item.DisableDrawableComponent((IDrawableComponent)this);
}
}
}
[Editable, Serialize(false, IsPropertySaveable.No, description: "Can the item be picked up (or interacted with, if the pick action does something else than picking up the item).")] //Editable for doors to do their magic
public bool CanBePicked
{
get { return canBePicked; }
set { canBePicked = value; }
}
[Serialize(false, IsPropertySaveable.No, description: "Should the interface of the item (if it has one) be drawn when the item is equipped.")]
public bool DrawHudWhenEquipped
{
get;
protected set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")]
public bool CanBeSelected
{
get { return canBeSelected; }
set { canBeSelected = value; }
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item be combined with other items of the same type.")]
public bool CanBeCombined
{
get { return canBeCombined; }
set { canBeCombined = value; }
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be removed if combining it with an other item causes the condition of this item to drop to 0.")]
public bool RemoveOnCombined
{
get { return removeOnCombined; }
set { removeOnCombined = value; }
}
[Serialize(false, IsPropertySaveable.No, description: "Can the \"Use\" action of the item be triggered by characters or just other items/StatusEffects.")]
public bool CharacterUsable
{
get { return characterUsable; }
set { characterUsable = value; }
}
//Remove item if combination results in 0 condition
[Serialize(true, IsPropertySaveable.No, description: "Can the properties of the component be edited in-game (only applicable if the component has in-game editable properties)."), Editable()]
public bool AllowInGameEditing
{
get;
set;
}
public InputType PickKey
{
get;
protected set;
}
public InputType SelectKey
{
get;
protected set;
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it's used.")]
public bool DeleteOnUse
{
get;
set;
}
public Item Item
{
get { return item; }
}
public string Name
{
get { return name; }
}
[Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's highlighted (generally instructs how to interact with the item, e.g. \"[Mouse1] Pick up\").")]
public string Msg
{
get;
set;
}
public LocalizedString DisplayMsg
{
get;
set;
}
/// <summary>
/// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).
/// </summary>
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")]
public float CombatPriority { get; private set; }
/// <summary>
/// Which sound should be played when manual sound selection type is selected? Not [Editable] because we don't want this visible in the editor for every component.
/// </summary>
[Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public int ManuallySelectedSound { get; private set; }
public ItemComponent(Item item, ContentXElement element)
{
this.item = item;
originalElement = element;
name = element.Name.ToString();
SerializableProperties = SerializableProperty.GetProperties(this);
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>();
requiredSkills = new List<Skill>();
#if CLIENT
hasSoundsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
sounds = new Dictionary<ActionType, List<ItemSound>>();
#endif
SelectKey = InputType.Select;
try
{
string selectKeyStr = element.GetAttributeString("selectkey", "Select");
selectKeyStr = ToolBox.ConvertInputType(selectKeyStr);
SelectKey = (InputType)Enum.Parse(typeof(InputType), selectKeyStr, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Invalid select key in " + element + "!", e);
}
PickKey = InputType.Select;
try
{
string pickKeyStr = element.GetAttributeString("pickkey", "Select");
pickKeyStr = ToolBox.ConvertInputType(pickKeyStr);
PickKey = (InputType)Enum.Parse(typeof(InputType), pickKeyStr, true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Invalid pick key in " + element + "!", e);
}
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ParseMsg();
string inheritRequiredSkillsFrom = element.GetAttributeString("inheritrequiredskillsfrom", "");
if (!string.IsNullOrEmpty(inheritRequiredSkillsFrom))
{
var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase));
if (component == null)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found.");
}
else
{
requiredSkills = component.requiredSkills;
}
}
string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", "");
if (!string.IsNullOrEmpty(inheritStatusEffectsFrom))
{
var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase));
if (component == null)
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found.");
}
else if (component.statusEffectLists != null)
{
statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
foreach (KeyValuePair<ActionType, List<StatusEffect>> kvp in component.statusEffectLists)
{
if (!statusEffectLists.TryGetValue(kvp.Key, out List<StatusEffect> effectList))
{
effectList = new List<StatusEffect>();
statusEffectLists.Add(kvp.Key, effectList);
}
effectList.AddRange(kvp.Value);
}
}
}
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "activeconditional":
case "isactive":
IsActiveConditionals = IsActiveConditionals ?? new List<PropertyConditional>();
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
IsActiveConditionals.Add(new PropertyConditional(attribute));
}
}
break;
case "requireditem":
case "requireditems":
SetRequiredItems(subElement);
break;
case "requiredskill":
case "requiredskills":
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - skill requirement in component " + GetType().ToString() + " should use a skill identifier instead of the name of the skill.");
continue;
}
Identifier skillIdentifier = subElement.GetAttributeIdentifier("identifier", "");
requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0)));
break;
case "statuseffect":
statusEffectLists ??= new Dictionary<ActionType, List<StatusEffect>>();
LoadStatusEffect(subElement);
break;
default:
if (LoadElemProjSpecific(subElement)) { break; }
ItemComponent ic = Load(subElement, item, false);
if (ic == null) { break; }
ic.Parent = this;
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
item.AddComponent(ic);
break;
}
}
void LoadStatusEffect(ContentXElement subElement)
{
var statusEffect = StatusEffect.Load(subElement, item.Name);
if (!statusEffectLists.TryGetValue(statusEffect.type, out List<StatusEffect> effectList))
{
effectList = new List<StatusEffect>();
statusEffectLists.Add(statusEffect.type, effectList);
}
effectList.Add(statusEffect);
}
}
private void SetActiveState(bool isActive)
{
IsActive = isActive;
}
public void SetRequiredItems(ContentXElement element, bool allowEmpty = false)
{
bool returnEmpty = false;
#if CLIENT
returnEmpty = Screen.Selected == GameMain.SubEditorScreen;
#endif
RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name);
if (ri != null)
{
if (ri.Identifiers.Count == 0)
{
DisabledRequiredItems.Add(ri);
}
else
{
if (!requiredItems.ContainsKey(ri.Type))
{
requiredItems.Add(ri.Type, new List<RelatedItem>());
}
requiredItems[ri.Type].Add(ri);
}
}
else if (!allowEmpty)
{
DebugConsole.ThrowError("Error in item config \"" + item.ConfigFilePath + "\" - component " + GetType().ToString() + " requires an item with no identifiers.");
}
}
public virtual void Move(Vector2 amount, bool ignoreContacts = false) { }
/// <summary>a Character has picked the item</summary>
public virtual bool Pick(Character picker)
{
return false;
}
public virtual bool Select(Character character)
{
return CanBeSelected;
}
/// <summary>a Character has dropped the item</summary>
public virtual void Drop(Character dropper) { }
/// <returns>true if the operation was completed</returns>
public virtual bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
return false;
}
public virtual bool UpdateWhenInactive => false;
//called when isActive is true and condition > 0.0f
public virtual void Update(float deltaTime, Camera cam)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
//called when isActive is true and condition == 0.0f
public virtual void UpdateBroken(float deltaTime, Camera cam)
{
#if CLIENT
StopSounds(ActionType.OnActive);
#endif
}
//called when the item is equipped and the "use" key is pressed
//returns true if the item was used succesfully (not out of ammo, reloading, etc)
public virtual bool Use(float deltaTime, Character character = null)
{
return characterUsable || character == null;
}
//called when the item is equipped and the "aim" key is pressed or when the item is selected if it doesn't require aiming.
public virtual bool SecondaryUse(float deltaTime, Character character = null)
{
return false;
}
//called when the item is placed in a "limbslot"
public virtual void Equip(Character character) { }
//called then the item is dropped or dragged out of a "limbslot"
public virtual void Unequip(Character character) { }
public virtual void ReceiveSignal(Signal signal, Connection connection)
{
switch (connection.Name)
{
case "activate":
case "use":
case "trigger_in":
if (signal.value != "0")
{
item.Use(1.0f, signal.sender);
}
break;
case "toggle":
if (signal.value != "0")
{
IsActive = !isActive;
}
break;
case "set_active":
case "set_state":
IsActive = signal.value != "0";
break;
}
}
public virtual bool Combine(Item item, Character user)
{
if (canBeCombined && this.item.Prefab == item.Prefab &&
item.Condition > 0.0f && this.item.Condition > 0.0f &&
!item.IsFullCondition && !this.item.IsFullCondition)
{
float transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition);
if (MathUtils.NearlyEqual(transferAmount, 0.0f)) { return false; }
if (removeOnCombined)
{
if (item.Condition - transferAmount <= 0.0f)
{
if (item.ParentInventory != null)
{
if (item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(item))
{
item.Unequip(owner);
}
item.ParentInventory.RemoveItem(item);
}
Entity.Spawner.AddItemToRemoveQueue(item);
}
else
{
item.Condition -= transferAmount;
}
if (this.Item.Condition + transferAmount <= 0.0f)
{
if (this.Item.ParentInventory != null)
{
if (this.Item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(this.Item))
{
this.Item.Unequip(owner);
}
this.Item.ParentInventory.RemoveItem(this.Item);
}
Entity.Spawner.AddItemToRemoveQueue(this.Item);
}
else
{
this.Item.Condition += transferAmount;
}
}
else
{
this.Item.Condition += transferAmount;
item.Condition -= transferAmount;
}
return true;
}
return false;
}
public void Remove()
{
#if CLIENT
if (loopingSoundChannel != null)
{
loopingSoundChannel.Dispose();
loopingSoundChannel = null;
}
//no need to Dispose these - SoundManager will do it when it when it needs a free channel and the sound has stopped playing
//disposing immediately on Remove will for example prevent explosives from playing a sound if the explosion removes the item
/*foreach (SoundChannel channel in playingOneshotSoundChannels)
{
channel.Dispose();
}*/
if (GuiFrame != null)
{
GUI.RemoveFromUpdateList(GuiFrame, true);
GuiFrame.RectTransform.Parent = null;
GuiFrame = null;
}
#endif
if (delayedCorrectionCoroutine != null)
{
CoroutineManager.StopCoroutines(delayedCorrectionCoroutine);
delayedCorrectionCoroutine = null;
}
RemoveComponentSpecific();
}
/// <summary>
/// Remove the component 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 void ShallowRemove()
{
#if CLIENT
if (loopingSoundChannel != null)
{
loopingSoundChannel.Dispose();
loopingSoundChannel = null;
}
#endif
ShallowRemoveComponentSpecific();
}
protected virtual void ShallowRemoveComponentSpecific()
{
RemoveComponentSpecific();
}
protected virtual void RemoveComponentSpecific()
{
}
protected string GetTextureDirectory(ContentXElement subElement)
=> subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty;
public bool HasRequiredSkills(Character character)
{
return HasRequiredSkills(character, out Skill temp);
}
public bool HasRequiredSkills(Character character, out Skill insufficientSkill)
{
foreach (Skill skill in requiredSkills)
{
float characterLevel = character.GetSkillLevel(skill.Identifier);
if (characterLevel < skill.Level * GetSkillMultiplier())
{
insufficientSkill = skill;
return false;
}
}
insufficientSkill = null;
return true;
}
public virtual float GetSkillMultiplier() { return 1; }
/// <summary>
/// Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
/// </summary>
/// <returns>0.5f if all the skills meet the skill requirements exactly, 1.0f if they're way above and 0.0f if way less</returns>
public float DegreeOfSuccess(Character character)
{
return DegreeOfSuccess(character, requiredSkills);
}
/// <summary>
/// Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
/// </summary>
/// <returns>0.5f if all the skills meet the skill requirements exactly, 1.0f if they're way above and 0.0f if way less</returns>
public float DegreeOfSuccess(Character character, List<Skill> requiredSkills)
{
if (requiredSkills.Count == 0) return 1.0f;
if (character == null)
{
string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return 0.0f;
}
float skillSuccessSum = 0.0f;
for (int i = 0; i < requiredSkills.Count; i++)
{
float characterLevel = character.GetSkillLevel(requiredSkills[i].Identifier);
skillSuccessSum += (characterLevel - requiredSkills[i].Level);
}
float average = skillSuccessSum / requiredSkills.Count;
return ((average + 100.0f) / 2.0f) / 100.0f;
}
public virtual void FlipX(bool relativeToSub) { }
public virtual void FlipY(bool relativeToSub) { }
public bool IsLoaded(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
{
if (!requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return true; }
if (item.OwnInventory == null) { return false; }
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Contained])
{
if (!ri.CheckRequirements(user, item))
{
#if CLIENT
msg ??= ri.Msg;
if (addMessage && !msg.IsNullOrEmpty())
{
GUI.AddMessage(msg, Color.Red);
}
#endif
return false;
}
}
return true;
}
/// <summary>
/// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems.
/// </summary>
public virtual bool HasAccess(Character character)
{
if (character.IsBot && item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
{
foreach (Item item in character.Inventory.AllItems)
{
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
{
return true;
}
}
}
return false;
}
public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
if (requiredItems.None()) { return true; }
if (character.Inventory == null) { return false; }
bool hasRequiredItems = false;
bool canContinue = true;
if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped))
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped])
{
canContinue = CheckItems(ri, character.HeldItems);
if (!canContinue) { break; }
}
}
if (canContinue)
{
if (requiredItems.ContainsKey(RelatedItem.RelationType.Picked))
{
foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked])
{
if (!CheckItems(ri, character.Inventory.AllItems)) { break; }
}
}
}
#if CLIENT
if (!hasRequiredItems && addMessage && !msg.IsNullOrEmpty())
{
GUI.AddMessage(msg, Color.Red);
}
#endif
return hasRequiredItems;
bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
{
bool Predicate(Item it)
{
if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
if (item.Submarine != null)
{
var idCard = it.GetComponent<IdCard>();
if (idCard != null)
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
}
}
return true;
};
bool shouldBreak = false;
bool inEditor = false;
#if CLIENT
inEditor = Screen.Selected == GameMain.SubEditorScreen;
#endif
if (relatedItem.IgnoreInEditor && inEditor)
{
hasRequiredItems = true;
}
else if (relatedItem.IsOptional)
{
if (!hasRequiredItems)
{
hasRequiredItems = itemList.Any(Predicate);
}
}
else
{
hasRequiredItems = itemList.Any(Predicate);
if (!hasRequiredItems)
{
shouldBreak = true;
}
}
if (!hasRequiredItems)
{
if (msg == null && !relatedItem.Msg.IsNullOrEmpty())
{
msg = relatedItem.Msg;
}
}
return !shouldBreak;
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f)
{
if (statusEffectLists == null) { return; }
if (!statusEffectLists.TryGetValue(type, out List<StatusEffect> statusEffects)) { return; }
bool broken = item.Condition <= 0.0f;
bool reducesCondition = false;
foreach (StatusEffect effect in statusEffects)
{
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
effect.AfflictionMultiplier = afflictionMultiplier;
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.AfflictionMultiplier = applyOnUserFraction;
item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition);
}
effect.AfflictionMultiplier = 1.0f;
reducesCondition |= effect.ReducesItemCondition();
}
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
if (reducesCondition && user != null && type != ActionType.OnBroken)
{
foreach (ItemComponent ic in item.Components)
{
if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List<StatusEffect> brokenEffects)) { continue; }
foreach (var brokenEffect in brokenEffects)
{
brokenEffect.SetUser(user);
}
}
}
#if CLIENT
HintManager.OnStatusEffectApplied(this, type, character);
#endif
}
public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
if (componentElement != null)
{
foreach (XAttribute attribute in componentElement.Attributes())
{
if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
if (property.OverridePrefabValues || !usePrefabValues)
{
property.TrySetValue(this, attribute.Value);
}
}
ParseMsg();
OverrideRequiredItems(componentElement);
}
if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); }
}
/// <summary>
/// Called when all items have been loaded. Use to initialize connections between items.
/// </summary>
public virtual void OnMapLoaded() { }
/// <summary>
/// Called when all the components of the item have been loaded. Use to initialize connections between components and such.
/// </summary>
public virtual void OnItemLoaded() { }
public virtual void OnScaleChanged() { }
public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true)
{
Type type;
Identifier typeName = element.NameAsIdentifier();
try
{
// Get the type of a specified class.
type = ReflectionUtils.GetDerivedNonAbstract<ItemComponent>().Append(typeof(ItemComponent)).FirstOrDefault(t => t.Name == typeName);
if (type == null)
{
if (errorMessages)
{
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})");
}
return null;
}
}
catch (Exception e)
{
if (errorMessages)
{
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e);
}
return null;
}
ConstructorInfo constructor;
try
{
if (type != typeof(ItemComponent) && !type.IsSubclassOf(typeof(ItemComponent))) { return null; }
constructor = type.GetConstructor(new Type[] { typeof(Item), typeof(ContentXElement) });
if (constructor == null)
{
DebugConsole.ThrowError(
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})");
return null;
}
}
catch (Exception e)
{
DebugConsole.ThrowError(
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e);
return null;
}
ItemComponent ic = null;
try
{
object[] lobject = new object[] { item, element };
object component = constructor.Invoke(lobject);
ic = (ItemComponent)component;
ic.name = element.Name.ToString();
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError($"Error while loading component of the type {type}.", e.InnerException);
GameAnalyticsManager.AddErrorEventOnce(
$"ItemComponent.Load:TargetInvocationException{item.Name}{element.Name}",
GameAnalyticsManager.ErrorSeverity.Error,
$"Error while loading entity of the type {type} ({e.InnerException})\n{Environment.StackTrace.CleanupStackTrace()}");
}
return ic;
}
public virtual XElement Save(XElement parentElement)
{
XElement componentElement = new XElement(name);
foreach (var kvp in requiredItems)
{
foreach (RelatedItem ri in kvp.Value)
{
XElement newElement = new XElement("requireditem");
ri.Save(newElement);
componentElement.Add(newElement);
}
}
foreach (RelatedItem ri in DisabledRequiredItems)
{
XElement newElement = new XElement("requireditem");
ri.Save(newElement);
componentElement.Add(newElement);
}
SerializableProperty.SerializeProperties(this, componentElement);
parentElement.Add(componentElement);
return componentElement;
}
public virtual void Reset()
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, originalElement);
if (this is Pickable) { canBePicked = true; }
ParseMsg();
OverrideRequiredItems(originalElement);
}
private void OverrideRequiredItems(ContentXElement element)
{
var prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
requiredItems.Clear();
bool returnEmptyRequirements = false;
#if CLIENT
returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen;
#endif
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "requireditem":
case "requireditems":
RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name);
if (newRequiredItem == null) continue;
var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ?
prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null;
if (prevRequiredItem != null)
{
newRequiredItem.statusEffects = prevRequiredItem.statusEffects;
newRequiredItem.Msg = prevRequiredItem.Msg;
newRequiredItem.IsOptional = prevRequiredItem.IsOptional;
newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor;
}
if (!requiredItems.ContainsKey(newRequiredItem.Type))
{
requiredItems[newRequiredItem.Type] = new List<RelatedItem>();
}
requiredItems[newRequiredItem.Type].Add(newRequiredItem);
break;
}
}
}
public virtual void ParseMsg()
{
LocalizedString msg = TextManager.Get(Msg);
if (msg.Loaded)
{
msg = TextManager.ParseInputTypes(msg);
DisplayMsg = msg;
}
else
{
DisplayMsg = Msg;
}
}
public interface IEventData { }
public virtual bool ValidateEventData(NetEntityEvent.IData data)
=> true;
protected T ExtractEventData<T>(NetEntityEvent.IData data) where T : IEventData
=> TryExtractEventData(data, out T componentData)
? componentData
: throw new Exception($"Malformed item component state event for {item.Name} " +
$"(item ID {item.ID}, component type {GetType().Name}): " +
$"could not extract ComponentData of type {typeof(T).Name}");
protected bool TryExtractEventData<T>(NetEntityEvent.IData data, out T componentData)
{
componentData = default;
if (data is Item.ComponentStateEventData { ComponentData: T nestedData })
{
componentData = nestedData;
return true;
}
return false;
}
#region AI related
protected const float AIUpdateInterval = 0.2f;
protected float aiUpdateTimer;
protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent
{
AIObjectiveContainItem containObjective = null;
if (character.AIController is HumanAIController aiController)
{
containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers, container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound)
{
ItemCount = itemCount,
Equip = equip,
RemoveEmpty = removeEmpty,
GetItemPriority = i =>
{
if (i.ParentInventory?.Owner is Item)
{
//don't take items from other items of the same type
if (((Item)i.ParentInventory.Owner).GetComponent<T>() != null)
{
return 0.0f;
}
}
// Prefer items with the same identifier as the contained items'
return container.ContainsItemsWithSameIdentifier(i) ? 1.0f : 0.5f;
}
};
containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item);
if (dropItemOnDeselected)
{
currentObjective.Deselected += () =>
{
if (containObjective == null) { return; }
if (containObjective.IsCompleted) { return; }
Item item = containObjective.ItemToContain;
if (item != null && character.CanInteractWith(item, checkLinked: false))
{
item.Drop(character);
}
};
}
currentObjective.AddSubObjective(containObjective);
}
return containObjective;
}
#endregion
}
}