Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs
2026-04-30 21:59:54 +08:00

1252 lines
50 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, Color? overrideColor = null);
#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 readonly List<Skill> RequiredSkills = new List<Skill>();
private ItemComponent parent;
public ItemComponent Parent
{
get { return parent; }
set
{
if (parent == value) { return; }
if (InheritParentIsActive)
{
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
}
parent = value;
}
}
[Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")]
public bool InheritParentIsActive { get; set; }
public readonly ContentXElement originalElement;
/// <summary>
/// The default delay for delayed client-side corrections (see <see cref="StartDelayedCorrection"/>.
/// </summary>
protected const float CorrectionDelay = 1.0f;
protected CoroutineHandle delayedCorrectionCoroutine;
/// <summary>
/// If enabled, the contents of the item are not transferred when the player transfers items between subs.
/// Use this if this component uses item containers in a way where removing the item from the container via external means would cause problems.
/// </summary>
public virtual bool DontTransferInventoryBetweenSubs => false;
/// <summary>
/// If enabled, the items inside any of the item containers on this item cannot be sold at an outpost.
/// Use in similar cases as <see cref="DontTransferInventoryBetweenSubs"/>.
/// </summary>
public virtual bool DisallowSellingItemsFromContainer => false;
[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;
[Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)]
public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison
{
get;
set;
}
public List<PropertyConditional> IsActiveConditionals;
public bool Drawable
{
get { return drawable; }
set
{
if (value == drawable) { return; }
if (this is not 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.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
public bool LockGuiFramePosition { get; set; }
[Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
public Point GuiFrameOffset { get; 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;
}
[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 forced). Note that there's also a generic BotPriority for all item prefabs.")]
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; }
/// <summary>
/// Can be used by status effects or conditionals to the speed of the item
/// </summary>
public float Speed => item.Speed;
public readonly record struct ItemUseInfo(Item Item, Character User);
public readonly NamedEvent<ItemUseInfo> OnUsed = new();
public readonly bool InheritStatusEffects;
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>>();
#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,
contentPackage: element.ContentPackage);
}
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,
contentPackage: element.ContentPackage);
}
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.",
contentPackage: element.ContentPackage);
}
else
{
RequiredSkills = component.RequiredSkills;
}
}
string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", "");
if (!string.IsNullOrEmpty(inheritStatusEffectsFrom))
{
InheritStatusEffects = true;
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.",
contentPackage: element.ContentPackage);
}
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 "isactiveconditional":
case "isactive":
IsActiveConditionals ??= new List<PropertyConditional>();
IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement));
break;
case "requireditem":
case "requireditems":
SetRequiredItems(subElement, allowEmpty: true);
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.",
contentPackage: element.ContentPackage);
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;
if (ic.InheritParentIsActive)
{
ic.IsActive = isActive;
OnActiveStateChanged += ic.SetActiveState;
}
item.AddComponent(ic);
break;
}
}
void LoadStatusEffect(ContentXElement subElement)
{
var statusEffect = StatusEffect.Load(subElement, item.Name + ", " + GetType().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.",
contentPackage: element.ContentPackage);
}
}
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, bool setTransform = true) { }
/// <returns>true if the operation was completed</returns>
public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
return false;
}
public virtual bool UpdateWhenInactive => false;
[Serialize(false, IsPropertySaveable.No, "If true, the component will retain its normal functionality when the item reaches 0 condition.")]
public bool UpdateWhenBroken { get; set; }
//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, user: 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);
}
RemoveItem(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);
}
RemoveItem(this.Item);
}
else
{
this.Item.Condition += transferAmount;
}
static void RemoveItem(Item item)
{
if (Screen.Selected is { IsEditor: true })
{
item?.Remove();
}
else
{
Entity.Spawner?.AddItemToRemoveQueue(item);
}
}
}
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()
{
#if CLIENT
HUDOverlay?.Remove();
HUDOverlay = null;
#endif
}
protected string GetTextureDirectory(ContentXElement subElement) => item.Prefab.GetTexturePath(subElement, item.Prefab.ParentPrefab);
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) { }
/// <summary>
/// Returns true if the item is lacking required contained items, or if there's nothing with a non-zero condition inside.
/// </summary>
public bool IsEmpty(Character user) =>
!HasRequiredContainedItems(user, addMessage: false) ||
(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.Count == 0) { return true; }
if (character.Inventory != null && RequiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
{
foreach (RelatedItem relatedItem in relatedItems)
{
foreach (Item otherItem in character.Inventory.AllItems)
{
if (relatedItem.MatchesItem(otherItem))
{
if (otherItem.GetComponent<IdCard>() is IdCard idCard)
{
if (!CheckIdCardAccess(relatedItem, idCard))
{
continue;
}
}
return true;
}
}
}
}
return false;
}
/// <summary>
/// Presumes that matching is already checked.
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine is { IsRespawnShuttle: false })
{
//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"))
{
if (GameMain.GameSession?.GameMode is PvPMode)
{
if (item.Submarine.TeamID != CharacterTeamType.FriendlyNPC && item.Submarine.TeamID != CharacterTeamType.None)
{
// In PvP, always allow access also to FriendlyNPC and None -> restrict access only to the enemy sub.
{
return false;
}
}
}
else
{
return false;
}
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
}
return true;
}
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 (it.GetComponent<IdCard>() is IdCard idCard)
{
if (!CheckIdCardAccess(relatedItem, idCard))
{
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
{
if (itemList.Any(Predicate))
{
hasRequiredItems = !relatedItem.RequireEmpty;
}
else
{
hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
}
if (!hasRequiredItems)
{
shouldBreak = true;
}
}
if (!hasRequiredItems)
{
if (msg == null && !relatedItem.Msg.IsNullOrEmpty())
{
msg = relatedItem.Msg;
}
}
return !shouldBreak;
}
}
/// <param name="attackMultiplier">Multiplier used on afflictions caused by the status effects, except ones that <see cref="AfflictionPrefab.AffectedByAttackMultipliers">have been configured to not be affected by attack multipliers.</see></param>
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float attackMultiplier = 1.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.AttackMultiplier = attackMultiplier;
var c = character;
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
// A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user.
c = user;
}
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
effect.AttackMultiplier = 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)
{
// Use ToArray() snapshot for thread-safe iteration
foreach (ItemComponent ic in item.Components.ToArray())
{
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, bool isItemSwap)
{
if (componentElement != null)
{
foreach (XAttribute attribute in componentElement.Attributes())
{
if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
if (property.OverridePrefabValues ||
!usePrefabValues ||
(isItemSwap && property.GetAttribute<Editable>() is { TransferToSwappedItem: true }))
{
property.TrySetValue(this, attribute.Value);
}
}
ParseMsg();
OverrideRequiredItems(componentElement);
}
#if CLIENT
if (GuiFrame != null)
{
GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset;
if (guiFrameDragHandle != null)
{
guiFrameDragHandle.Enabled = !LockGuiFramePosition;
}
}
#endif
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() { }
/// <summary>
/// Implement in a base class if the instances of the component contain some sort of data that isn't serialized using the normal serializable properties
/// (i.e. some data that changes per-item and isn't loaded from the prefab, but that isn't a property marked with [Serialize] either),
/// but that must be copied when cloning the item.
/// </summary>
public virtual void Clone(ItemComponent original) { }
public virtual void OnScaleChanged() { }
/// <summary>
/// Called when the item has an ItemContainer and the contents inside of it changed.
/// </summary>
public virtual void OnInventoryChanged() { }
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})",
contentPackage: element.ContentPackage);
}
return null;
}
}
catch (Exception e)
{
if (errorMessages)
{
DebugConsole.ThrowError($"Could not find the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
contentPackage: element.ContentPackage);
}
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})",
contentPackage: element.ContentPackage);
return null;
}
}
catch (Exception e)
{
DebugConsole.ThrowError(
$"Could not find the constructor of the component \"{typeName}\" ({item.Prefab.ContentFile.Path})", e,
contentPackage: element.ContentPackage);
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, contentPackage: element.ContentPackage);
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");
//if we have some actual requirements, no need to keep the empty requirement
//as a "placeholder" for the user to add requirements in the sub editor
if (ri.Identifiers.IsEmpty && RequiredItems.Any()) { continue; }
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
}
}