using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace Barotrauma { class AIObjectiveLoadItem : AIObjective { public override Identifier Identifier { get; set; } = "load item".ToIdentifier(); protected override bool AllowWhileHandcuffed => false; private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; } private Item Container { get; } private ItemContainer ItemContainer { get; } private ImmutableArray TargetContainerTags { get; } private ImmutableHashSet ValidContainableItemIdentifiers { get; } private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); private int itemIndex; private AIObjectiveDecontainItem decontainObjective; private readonly HashSet ignoredItems = new HashSet(); private Item targetItem; private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable"; public AIObjectiveLoadItem(Item container, ImmutableArray targetTags, AIObjectiveLoadItems.ItemCondition targetCondition, Identifier option, Character character, AIObjectiveManager objectiveManager, float priorityModifier) : base(character, objectiveManager, priorityModifier) { Container = container; ItemContainer = container?.GetComponent(); if (ItemContainer?.Inventory == null) { Abandon = true; return; } TargetContainerTags = targetTags; TargetItemCondition = targetCondition; if (!option.IsEmpty) { string optionSpecificDialogueIdentifier = $"{abandonGetItemDialogueIdentifier}.{option}"; if (TextManager.ContainsTag(optionSpecificDialogueIdentifier)) { abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier; } } ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers(); if (ValidContainableItemIdentifiers.None()) { #if DEBUG DebugConsole.LogError($"No valid containable item identifiers found for the Load Item objective targeting {Container}"); #endif Abandon = true; return; } } private enum CheckStatus { Unfinished, Finished } private ImmutableHashSet GetValidContainableItemIdentifiers() { if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers)) { return existingIdentifiers; } // Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results // For example, placing welding fuel tanks inside oxygen tank shelves bool useDefaultContainableItemIdentifiers = true; var potentialContainablePrefabs = MapEntityPrefab.List .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i))) .Cast(); var validContainableItemIdentifiers = new HashSet(); foreach (var component in Container.Components) { if (CheckComponent() == CheckStatus.Finished) { break; } CheckStatus CheckComponent() { if (component.statusEffectLists != null) { foreach (var (_, statusEffects) in component.statusEffectLists) { if (CheckStatusEffects(statusEffects) == CheckStatus.Finished) { return CheckStatus.Finished; } } } if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null) { foreach (var item in itemContainer.ContainableItems) { if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished) { return CheckStatus.Finished; } } } return CheckStatus.Unfinished; CheckStatus CheckStatusEffects(IEnumerable statusEffects) { if (statusEffects == null) { return CheckStatus.Unfinished; } foreach (var statusEffect in statusEffects) { if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; } switch (TargetItemCondition) { case AIObjectiveLoadItems.ItemCondition.Empty: if (!statusEffect.ReducesItemCondition()) { continue; } break; case AIObjectiveLoadItems.ItemCondition.Full: if (!statusEffect.IncreasesItemCondition()) { continue; } break; default: continue; } useDefaultContainableItemIdentifiers = false; if (statusEffect.TargetIdentifiers != null) { foreach (Identifier target in statusEffect.TargetIdentifiers) { foreach (var prefab in potentialContainablePrefabs) { if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; } } } } foreach (var prefab in potentialContainablePrefabs) { if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; } } CheckStatus CheckPrefab(ItemPrefab prefab, Func isValid) { if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; } if (!isValid()) { return CheckStatus.Unfinished; } validContainableItemIdentifiers.Add(prefab.Identifier); if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; } return CheckStatus.Finished; } } return CheckStatus.Unfinished; } } } var identifiers = useDefaultContainableItemIdentifiers ? potentialContainablePrefabs.Select(p => p.Identifier).ToImmutableHashSet() : validContainableItemIdentifiers.ToImmutableHashSet(); AllValidContainableItemIdentifiers.Add(Container.Prefab, identifiers); return identifiers; } protected override float GetPriority() { if (!IsAllowed) { HandleDisallowed(); return Priority; } else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition)) { // Reduce priority to 0 if the this isn't a valid container right now Priority = 0; } else if (targetItem == null) { Priority = 0; } else { float dist = 0.0f; if (character.CurrentHull != targetItem.CurrentHull) { AddDistance(character.WorldPosition, targetItem.WorldPosition); } if (targetItem.CurrentHull != Container.CurrentHull) { AddDistance(targetItem.WorldPosition, Container.WorldPosition); } void AddDistance(Vector2 startPos, Vector2 targetPos) { float yDist = Math.Abs(startPos.Y - targetPos.Y); // If we're on the same level with the target, we'll disregard the vertical distance if (yDist > 100) { dist += yDist * 5; } dist += Math.Abs(character.WorldPosition.X - targetPos.X); } float distanceFactor = GetDistanceFactor(targetItem.WorldPosition, verticalDistanceMultiplier: 5, maxDistance: 5000, factorAtMinDistance: 0.9f, factorAtMaxDistance: 0); bool hasContainable = character.HasItem(targetItem); float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100; float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2); Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1)); if (decontainObjective != null && targetItem.Container != Container) { if (!IsValidContainable(targetItem)) { // Target is not valid anymore, abandon the objective decontainObjective.Abandon = true; } else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition))) { // The container is full and there's no item that should be removed, abandon the objective decontainObjective.Abandon = true; } } if (ItemContainer.Inventory.IsFull()) { // Prioritize containers that still have empty space by lowering the priority of objectives with a full target container Priority /= 4; } } return Priority; } protected override void Act(float deltaTime) { if (targetItem == null) { if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) { if (item == null) { // No possible containables found, abandon the objective Abandon = true; } targetItem = item; } objectiveManager.GetObjective().Wander(deltaTime); float GetPriority(Item item) { try { // Prefer items closer to full condition when target condition is Empty, and vice versa float conditionBasedPriority = TargetItemCondition switch { AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage), AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage), _ => throw new NotImplementedException() }; // Prefer items that have the same identifier as one of the already contained items return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2; } catch (NotImplementedException) { #if DEBUG DebugConsole.LogError($"Unexpected target condition \"{TargetItemCondition}\" in local function GetConditionBasedProperty"); #endif return 0.0f; } } } else { if(decontainObjective == null && !IsValidContainable(targetItem)) { IgnoreTargetItem(); Reset(); return; } TryAddSubObjective(ref decontainObjective, constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier) { AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem), AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier, Equip = true, RemoveExistingWhenNecessary = true, RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), RemoveExistingMax = 1 }, onCompleted: () => { IsCompleted = true; RemoveSubObjective(ref decontainObjective); }, onAbandon: () => { // Try again IgnoreTargetItem(); Reset(); }); } } private bool IsValidContainable(Item item) { if (item == null) { return false; } if (item.Removed) { return false; } if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; } if (ignoredItems.Contains(item)) { return false; } if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; } Item parentItem = item.Container; while (parentItem != null) { if (parentItem.HasTag(Tags.DontTakeItems)) { return false; } parentItem = parentItem.Container; } if (!item.HasAccess(character)) { return false; } if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) { // Ignore items that have had their condition increase recently if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full && item.ConditionIncreasedRecently) { return false; } // Ignore items inside their (condition-restricted) primary containers if (item.ParentInventory is ItemInventory itemInventory && item.IsContainerPreferred(itemInventory.Container, out bool _, out bool isSecondary, requireConditionRestriction: true) && !isSecondary) { return false; } } // Ignore items inside another valid container if (AIObjectiveLoadItems.IsValidTarget(item.Container, character, TargetContainerTags)) { return false; } return true; } protected override bool CheckObjectiveSpecific() => IsCompleted; public override void Reset() { base.Reset(); // Don't reset the target item when resetting the objective because it affects priority calculations decontainObjective = null; itemIndex = 0; } private void IgnoreTargetItem() { if(targetItem == null) { return; } ignoredItems.Add(targetItem); targetItem = null; } } }