4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Globalization;
10 using System.Xml.Linq;
18 readonly record
struct ContainedItem(
Item Item, bool Hide, Vector2?
ItemPos, float Rotation);
20 class SlotRestrictions
26 public SlotRestrictions(
int maxStackSize, List<RelatedItem> containableItems,
bool autoInject)
43 public bool MatchesItem(Identifier identifierOrTag)
47 ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag));
53 private bool alwaysContainedItemsSpawned;
57 private readonly List<ActiveContainedItem> activeContainedItems =
new List<ActiveContainedItem>();
59 private readonly List<ContainedItem> containedItems =
new List<ContainedItem>();
61 private List<ushort>[] itemIds;
68 get {
return capacity; }
71 capacity = Math.Max(value, 0);
81 private int maxStackSize;
82 [
Serialize(64,
IsPropertySaveable.No, description:
"How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")]
85 get {
return maxStackSize; }
86 set { maxStackSize = Math.Max(value, 1); }
89 private bool hideItems;
91 +
" If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")]
94 get {
return hideItems; }
102 [
Serialize(
"0.0,0.0",
IsPropertySaveable.No, description:
"The position where the contained items get drawn at (offset from the upper left corner of the sprite in pixels).")]
105 [
Serialize(
"0.0,0.0",
IsPropertySaveable.No, description:
"The interval at which the contained items are spaced apart from each other (in pixels).")]
132 [
Serialize(
true,
IsPropertySaveable.Yes, description:
"When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
135 [
Serialize(
false,
IsPropertySaveable.No, description:
"If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
154 private readonly HashSet<Identifier> containableRestrictions =
new HashSet<Identifier>();
155 [
Editable,
Serialize(
"",
IsPropertySaveable.Yes, description:
"Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
158 get {
return string.Join(
",", containableRestrictions); }
161 containableRestrictions.Clear();
162 if (!value.IsNullOrEmpty())
164 foreach (var str
in value.Split(
','))
166 if (str.IsNullOrWhiteSpace()) {
continue; }
167 containableRestrictions.Add(str.ToIdentifier());
176 private float itemRotation;
180 get {
return MathHelper.ToDegrees(itemRotation); }
181 set { itemRotation = MathHelper.ToRadians(value); }
191 [
Serialize(
false,
IsPropertySaveable.No, description:
"Should the items configured using SpawnWithId spawn if this item is broken.")]
205 [
Serialize(0.5f,
IsPropertySaveable.No, description:
"The health threshold that the user must reach in order to activate the autoinjection.")]
246 private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
248 readonly List<ISerializableEntity> targets =
new List<ISerializableEntity>();
250 private float prevContainedItemRefreshRotation;
251 private Vector2 prevContainedItemRefreshPosition;
253 private float autoInjectCooldown = 1.0f;
254 const float AutoInjectInterval = 1.0f;
256 private bool subContainersCanAutoInject;
261 isRestrictionsDefined = containableRestrictions.Any();
262 if (slotRestrictions.None(s => s.MatchesItem(
item))) {
return false; }
263 if (!isRestrictionsDefined) {
return true; }
264 return identifiersOrTags.Any(
id => containableRestrictions.Any(r => r ==
id));
269 isRestrictionsDefined = containableRestrictions.Any();
270 if (slotRestrictions.None(s => s.MatchesItem(
item))) {
return false; }
271 if (!isRestrictionsDefined) {
return true; }
275 private ImmutableHashSet<Identifier> containableItemIdentifiers;
285 private string totalConditionValueString =
"", totalConditionPercentageString =
"", totalItemsString =
"";
286 private float prevTotalConditionValue = 0, prevTotalConditionPercentage = 0;
int prevTotalItems = 0;
289 : base(
item, element)
291 int totalCapacity = capacity;
293 foreach (var subElement
in element.Elements())
295 switch (subElement.Name.ToString().ToLowerInvariant())
299 if (containable ==
null)
301 DebugConsole.ThrowError(
"Error in item config \"" +
item.
ConfigFilePath +
"\" - containable with no identifiers.",
309 totalCapacity += subElement.GetAttributeInt(
"capacity", 1);
319 List<SlotRestrictions> newSlotRestrictions =
new List<SlotRestrictions>(totalCapacity);
320 for (
int i = 0; i < capacity; i++)
322 newSlotRestrictions.Add(
new SlotRestrictions(maxStackSize,
ContainableItems, autoInject:
false));
325 int subContainerIndex = capacity;
326 foreach (var subElement
in element.Elements())
328 if (subElement.Name.ToString().ToLowerInvariant() !=
"subcontainer") {
continue; }
330 int subCapacity = subElement.GetAttributeInt(
"capacity", 1);
331 int subMaxStackSize = subElement.GetAttributeInt(
"maxstacksize", maxStackSize);
332 bool autoInject = subElement.GetAttributeBool(
"autoinject",
false);
334 subContainersCanAutoInject |= autoInject;
336 var subContainableItems =
new List<RelatedItem>();
337 foreach (var subSubElement
in subElement.Elements())
339 if (subSubElement.Name.ToString().ToLowerInvariant() !=
"containable") {
continue; }
342 if (containable ==
null)
344 DebugConsole.ThrowError(
"Error in item config \"" +
item.
ConfigFilePath +
"\" - containable with no identifiers.",
348 subContainableItems.Add(containable);
353 for (
int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
355 newSlotRestrictions.Add(
new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject));
357 subContainerIndex += subCapacity;
359 capacity = totalCapacity;
360 slotRestrictions = newSlotRestrictions.ToImmutableArray();
361 System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length);
362 InitProjSpecific(element);
367 int containableIndex = 0;
368 foreach (var subElement
in element.GetChildElements(
"containable"))
371 if (containable ==
null)
373 DebugConsole.ThrowError(
"Error when loading containable restrictions for \"" +
item.
Name +
"\" - containable with no identifiers.",
374 contentPackage: element.ContentPackage);
381 for (
int i = 0; i < capacity; i++)
386 if (element.GetChildElement(
"clearsubcontainerrestrictions") !=
null)
399 if (slotIndex < 0 || slotIndex >= capacity)
403 return slotRestrictions[slotIndex].MaxStackSize;
412 if (index >= 0 && index < slotRestrictions.Length)
416 activeContainedItems.RemoveAll(i => i.Item == containedItem);
419 if (!containableItem.MatchesItem(containedItem)) {
continue; }
421 relatedItem ??= containableItem;
422 foreach (
StatusEffect effect
in containableItem.StatusEffects)
424 activeContainedItems.Add(
new ActiveContainedItem(
427 containableItem.ExcludeBroken,
428 containableItem.ExcludeFullCondition,
429 containableItem.BlameEquipperForDeath));
435 var containedItemInfo =
new ContainedItem(containedItem,
436 Hide: relatedItem?.Hide ??
false,
438 Rotation: relatedItem?.Rotation ?? 0.0f);
439 containedItems.RemoveAll(d => d.Item == containedItem);
444 containedItems.Add(containedItemInfo);
448 int containedIndex = 0;
449 while (containedIndex < containedItems.Count)
458 containedItems.Insert(containedIndex, containedItemInfo);
461 if (
item.GetComponent<Planter>() !=
null)
473 SetContainedActive(
true);
477 containedItem.
FlipX(relativeToSub:
false);
481 containedItem.
FlipY(relativeToSub:
false);
488 public override void Move(Vector2 amount,
bool ignoreContacts =
false)
495 activeContainedItems.RemoveAll(i => i.Item == containedItem);
496 containedItems.RemoveAll(i => i.Item == containedItem);
506 return activeContainedItems.Any(c => c.BlameEquipperForDeath);
512 return slotRestrictions.Any(s => s.MatchesItem(
item));
517 if (index < 0 || index >= capacity) {
return false; }
519 return slotRestrictions[index].MatchesItem(
item);
524 return slotRestrictions.Any(s => s.MatchesItem(itemPrefab));
529 if (index < 0 || index >= capacity) {
return false; }
530 return slotRestrictions[index].MatchesItem(itemPrefab);
535 if (
item ==
null) {
return false; }
546 public override void FlipX(
bool relativeToSub)
548 base.FlipX(relativeToSub);
555 containedItem.
FlipX(relativeToSub);
562 if (!
string.IsNullOrEmpty(
SpawnWithId) && !alwaysContainedItemsSpawned)
564 SpawnAlwaysContainedItems();
565 alwaysContainedItemsSpawned =
true;
570 float totalConditionValue = 0, totalConditionPercentage = 0;
int totalItems = 0;
581 if (!MathUtils.NearlyEqual(totalConditionValue, prevTotalConditionValue))
583 totalConditionValueString = ((int)totalConditionValue).ToString(CultureInfo.InvariantCulture);
584 prevTotalConditionValue = totalConditionValue;
587 if (!MathUtils.NearlyEqual(totalConditionPercentage, prevTotalConditionPercentage))
589 totalConditionPercentageString = ((int)totalConditionPercentage).ToString(CultureInfo.InvariantCulture);
590 prevTotalConditionPercentage = totalConditionPercentage;
593 if (totalItems != prevTotalItems)
595 totalItemsString = totalItems.ToString(CultureInfo.InvariantCulture);
596 prevTotalItems = totalItems;
599 item.
SendSignal(totalConditionValueString,
"contained_conditions");
600 item.
SendSignal(totalConditionPercentageString,
"contained_conditions_percentage");
606 SetContainedItemPositionsIfNeeded();
616 autoInjectCooldown -= deltaTime;
617 if (autoInjectCooldown <= 0.0f &&
618 ownerInventory?.Owner is
Character ownerCharacter &&
620 ownerCharacter.HasEquippedItem(
item))
628 for (
int i = 0; i < slotRestrictions.Length; i++)
642 autoInjectCooldown = AutoInjectInterval;
651 SetContainedItemPositionsIfNeeded();
660 foreach (var activeContainedItem
in activeContainedItems)
662 Item contained = activeContainedItem.
Item;
664 if (activeContainedItem.ExcludeBroken && contained.
Condition <= 0.0f) {
continue; }
665 if (activeContainedItem.ExcludeFullCondition && contained.
IsFullCondition) {
continue; }
680 targets.Add(character);
696 private void SetContainedItemPositionsIfNeeded()
698 if (Vector2.DistanceSquared(prevContainedItemRefreshPosition,
item.
Position) > 10.0f ||
753 return base.Select(character);
781 return picker !=
null;
787 if (!slotRestrictions.Any(s => s.MatchesItem(
item))) {
return false; }
790 if (this.Item.GetComponent<
GeneticMaterial>() !=
null) {
return false; }
806 SetContainedActive(
false);
814 SetContainedActive(
true);
818 SetContainedActive(
false);
822 private void SetContainedActive(
bool active)
831 RelatedItem containableItem = FindContainableItem(containedItem);
832 if (containableItem !=
null && containableItem.
SetActive)
836 ic.IsActive = active;
838 if (containedItem.
body !=
null)
854 private RelatedItem FindContainableItem(
Item item)
857 if (index == -1 ) {
return null; }
858 return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(
item));
866 for (
int i = 0; i < slotRestrictions.Length; i++)
868 if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) {
return i; }
875 switch (connection.
Name)
880 if (signal.
value !=
"0")
888 #warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
893 Vector2 transformedItemIntervalHorizontal =
new Vector2(transformedItemInterval.X, 0.0f);
894 Vector2 transformedItemIntervalVertical =
new Vector2(0.0f, transformedItemInterval.Y);
906 transformedItemPos.X = -transformedItemPos.X;
907 transformedItemPos.X +=
item.
Rect.Width;
908 transformedItemInterval.X = -transformedItemInterval.X;
909 transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
913 transformedItemPos.Y = -transformedItemPos.Y;
914 transformedItemPos.Y -=
item.
Rect.Height;
915 transformedItemInterval.Y = -transformedItemInterval.Y;
916 transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
923 transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
924 transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
925 transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
933 transformedItemPos.X = -transformedItemPos.X;
934 transformedItemInterval.X = -transformedItemInterval.X;
935 transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
937 transformedItemPos = Vector2.Transform(transformedItemPos, transform);
938 transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
939 transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
945 Vector2 currentItemPos = transformedItemPos;
946 foreach (ContainedItem contained
in containedItems)
948 Vector2 itemPos = currentItemPos;
949 if (contained.ItemPos.HasValue)
951 Vector2 pos = contained.ItemPos.Value;
964 itemPos.X = -itemPos.X;
969 itemPos.Y = -itemPos.Y;
981 if (contained.Item.body !=
null)
985 Vector2 simPos = ConvertUnits.ToSimUnits(itemPos);
986 float rotation = itemRotation;
987 if (contained.Rotation != 0)
989 rotation = MathHelper.ToRadians(contained.Rotation);
1000 contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
1001 contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
1002 contained.Item.body.UpdateDrawPosition();
1006 DebugConsole.Log(
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message +
")\n" + e.StackTrace.CleanupStackTrace());
1007 GameAnalyticsManager.AddErrorEventOnce(
"ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Item.Name,
1008 GameAnalyticsManager.ErrorSeverity.Error,
1009 "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message +
")\n" + e.StackTrace.CleanupStackTrace());
1014 contained.Item.Rect =
1016 (
int)(itemPos.X - contained.Item.Rect.Width / 2.0f),
1017 (
int)(itemPos.Y + contained.Item.Rect.Height / 2.0f),
1018 contained.Item.Rect.Width, contained.Item.Rect.Height);
1022 contained.Item.SetContainedItemPositions();
1028 currentItemPos += transformedItemIntervalHorizontal;
1031 currentItemPos = transformedItemPos;
1032 currentItemPos += transformedItemIntervalVertical * (i /
ItemsPerRow);
1037 currentItemPos += transformedItemInterval;
1045 containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
1049 SpawnAlwaysContainedItems();
1055 if (itemIds !=
null)
1057 for (ushort i = 0; i < itemIds.Length; i++)
1066 foreach (ushort
id in itemIds[i])
1087 SpawnAlwaysContainedItems();
1091 private void SpawnAlwaysContainedItems()
1096 foreach (
string id in splitIds)
1101 bool isEditor =
false;
1107 var spawnedItem =
new Item(prefab, Vector2.Zero,
null);
1108 Inventory.
TryPutItem(spawnedItem,
null, spawnedItem.AllowedSlots, createNetworkEvent:
false);
1109 alwaysContainedItemsSpawned =
true;
1127 base.RemoveComponentSpecific();
1129 inventoryTopSprite?.
Remove();
1130 inventoryBackSprite?.
Remove();
1131 inventoryBottomSprite?.
Remove();
1149 base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
1152 string[] itemIdStrings = containedString.Split(
',');
1153 itemIds =
new List<ushort>[itemIdStrings.Length];
1154 for (
int i = 0; i < itemIdStrings.Length; i++)
1156 itemIds[i] ??=
new List<ushort>();
1157 foreach (
string idStr
in itemIdStrings[i].Split(
';'))
1159 if (!
int.TryParse(idStr, out
int id)) {
continue; }
1166 public override XElement
Save(XElement parentElement)
1168 XElement componentElement = base.Save(parentElement);
1173 itemIdStrings[i] =
string.Join(
';', items.Select(it => it.ID.ToString()));
1175 componentElement.Add(
new XAttribute(
"contained",
string.Join(
',', itemIdStrings)));
1177 return componentElement;
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel=CharacterInventory.AccessLevel.Limited)
readonly? ContentPackage ContentPackage
string? GetAttributeString(string key, string? def)
int GetAttributeInt(string key, int def)
static EntitySpawner Spawner
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
void AddItemToSpawnQueue(ItemPrefab itemPrefab, Vector2 worldPosition, float? condition=null, int? quality=null, Action< Item > onSpawned=null)
static GameSession?? GameSession
static SubEditorScreen SubEditorScreen
static NetworkMember NetworkMember
readonly Identifier Identifier
ushort GetOffsetId(XElement element)
virtual bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
void DeleteAllItems()
Deletes all items inside the inventory (and also recursively all items inside the items)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
int FindIndex(Item item)
Find the index of the first slot the item is contained in.
bool AllowSwappingContainedItems
IEnumerable< Item > GetItemsAt(int index)
Get all the item stored in the specified inventory slot. Can return more than one item if the slot co...
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
bool CanBePut(Item item)
Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be p...
List< Connection > Connections
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb limb=null, Entity useTarget=null, bool isNetworkEvent=false, Vector2? worldPosition=null)
Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that s...
Inventory ParentInventory
IReadOnlyList< ISerializableEntity > AllPropertyObjects
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
override Vector2? Position
override void FlipX(bool relativeToSub)
Flip the entity horizontally
void SetContainedItemPositions()
override void FlipY(bool relativeToSub)
Flip the entity vertically
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
bool HasTag(Identifier tag)
ContentPath ConfigFilePath
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
float ConditionPercentage
List< ItemComponent > Components
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
void SendSignal(string signal, string connectionName)
Entity GetRootInventoryOwner()
static readonly PrefabCollection< ItemPrefab > Prefabs
AbilityItemContainer(Item item)
The base class for components holding the different functionalities of the item
override void Update(float deltaTime, Camera cam)
bool CanBeContained(ItemPrefab itemPrefab, int index)
bool BlameEquipperForDeath()
float AutoInjectThreshold
Sprite ContainedStateIndicator
readonly ItemInventory Inventory
void OnItemRemoved(Item containedItem)
bool AccessOnlyWhenBroken
int ContainedItemCount
Can be used by status effects
override void Equip(Character character)
bool ShouldBeContained(Item item, out bool isRestrictionsDefined)
bool SpawnWithIdWhenBroken
override void RemoveComponentSpecific()
readonly NamedEvent< ItemContainer > OnContainedItemsChanged
ItemContainer(Item item, ContentXElement element)
override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg=null)
bool AllowSwappingContainedItems
override void Move(Vector2 amount, bool ignoreContacts=false)
ImmutableHashSet< Identifier > ContainableItemIdentifiers
void OnItemContained(Item containedItem)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
bool CanBeContained(ItemPrefab itemPrefab)
bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
override bool Combine(Item item, Character user)
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
bool CanBeContained(Item item, int index)
bool CanBeContained(Item item)
bool ContainsItemsWithSameIdentifier(Item item)
bool AutoInteractWithContained
int ContainedNonBrokenItemCount
Can be used by status effects
bool QuickUseMovesItemsInside
override void ShallowRemoveComponentSpecific()
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
void SetContainedItemPositions()
override void FlipX(bool relativeToSub)
override XElement Save(XElement parentElement)
override bool Pick(Character picker)
a Character has picked the item
string ContainableRestrictions
List< RelatedItem > AllSubContainableItems
int GetMaxStackSize(int slotIndex)
override bool Select(Character character)
override void ReceiveSignal(Signal signal, Connection connection)
int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
Returns the index of the first slot whose restrictions match the specified tag or identifier
void ReloadContainableRestrictions(ContentXElement element)
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
bool AllowAccessWhenDropped
bool hasSignalConnections
int MainContainerCapacity
The capacity of the main container without taking the sub containers into account....
List< RelatedItem > ContainableItems
readonly bool HasSubContainers
bool Locked
Can be used by status effects to lock the inventory
override void UpdateBroken(float deltaTime, Camera cam)
bool RemoveContainedItemsOnDeconstruct
readonly Identifier Identifier
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
bool HasTargetType(TargetType targetType)
void AddNearbyTargets(Vector2 worldPosition, List< ISerializableEntity > targets)
virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition=null)
static bool IsSubEditor()
ActionType
ActionTypes define when a StatusEffect is executed.