896 lines
40 KiB
C#
896 lines
40 KiB
C#
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
|
||
{
|
||
public enum OrderCategory
|
||
{
|
||
Emergency,
|
||
Movement,
|
||
Power,
|
||
Maintenance,
|
||
Operate
|
||
}
|
||
|
||
class OrderCategoryIcon : Prefab
|
||
{
|
||
public readonly static PrefabCollection<OrderCategoryIcon> OrderCategoryIcons = new PrefabCollection<OrderCategoryIcon>();
|
||
|
||
public OrderCategoryIcon(ContentXElement element, OrdersFile file) : base(file, element.GetAttributeIdentifier("category", ""))
|
||
{
|
||
Category = Enum.Parse<OrderCategory>(Identifier.Value, true);
|
||
var spriteElement = element.GetChildElement("sprite");
|
||
Sprite = new Sprite(spriteElement, lazyLoad: true);
|
||
Color = element.GetAttributeColor("color", Color.White);
|
||
}
|
||
|
||
public readonly OrderCategory Category;
|
||
public readonly Sprite Sprite;
|
||
public readonly Color Color;
|
||
|
||
public override void Dispose() { Sprite?.Remove(); }
|
||
}
|
||
|
||
class OrderPrefab : PrefabWithUintIdentifier
|
||
{
|
||
public readonly static PrefabCollection<OrderPrefab> Prefabs = new PrefabCollection<OrderPrefab>();
|
||
|
||
public readonly static Identifier DismissalIdentifier = "dismissed".ToIdentifier();
|
||
public static OrderPrefab Dismissal => Prefabs[DismissalIdentifier];
|
||
|
||
public readonly OrderCategory? Category;
|
||
public readonly Identifier CategoryIdentifier;
|
||
|
||
public readonly LocalizedString Name;
|
||
/// <summary>
|
||
/// Name that can be used with the contextual version of the order
|
||
/// </summary>
|
||
public readonly LocalizedString ContextualName;
|
||
|
||
public readonly Sprite SymbolSprite;
|
||
|
||
public readonly Type ItemComponentType;
|
||
public readonly bool CanTypeBeSubclass;
|
||
public readonly ImmutableArray<Identifier> TargetItems;
|
||
public readonly ImmutableArray<Identifier> RequireItems;
|
||
private readonly ImmutableDictionary<Identifier, ImmutableArray<Identifier>> OptionTargetItems;
|
||
public bool HasOptionSpecificTargetItems => OptionTargetItems != null && OptionTargetItems.Any();
|
||
|
||
private readonly Color? color;
|
||
public Color Color
|
||
{
|
||
get
|
||
{
|
||
if (color.HasValue)
|
||
{
|
||
return color.Value;
|
||
}
|
||
else if (OrderCategoryIcon.OrderCategoryIcons.ContainsKey(CategoryIdentifier))
|
||
{
|
||
return OrderCategoryIcon.OrderCategoryIcons[Category.ToIdentifier()].Color;
|
||
}
|
||
else
|
||
{
|
||
return Color.White;
|
||
}
|
||
}
|
||
}
|
||
|
||
//if true, the order is issued to all available characters
|
||
public readonly bool TargetAllCharacters;
|
||
public bool IsReport => TargetAllCharacters && !MustSetTarget;
|
||
|
||
public bool IsVisibleAsReportButton =>
|
||
IsReport && !Hidden && SymbolSprite != null &&
|
||
(!TraitorModeOnly || GameMain.GameSession is { TraitorsEnabled: true });
|
||
|
||
public bool TraitorModeOnly;
|
||
|
||
public bool IsDismissal => Identifier == DismissalIdentifier;
|
||
|
||
public readonly float FadeOutTime;
|
||
|
||
public readonly bool UseController;
|
||
|
||
public readonly ImmutableArray<Identifier> ControllerTags;
|
||
|
||
/// <summary>
|
||
/// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs.
|
||
/// </summary>
|
||
public readonly ImmutableArray<Identifier> AppropriateJobs;
|
||
public readonly ImmutableArray<Identifier> Options;
|
||
public readonly ImmutableArray<Identifier> HiddenOptions;
|
||
public readonly ImmutableArray<Identifier> AllOptions;
|
||
public readonly ListDictionary<Identifier, LocalizedString> OptionNames;
|
||
|
||
public readonly ImmutableDictionary<Identifier, Sprite> OptionSprites;
|
||
|
||
public readonly bool MustSetTarget;
|
||
/// <summary>
|
||
/// Can the order be turned into a non-entity-targeting one if it was originally created with a target entity.
|
||
/// Note: if MustSetTarget is true, CanBeGeneralized will always be false.
|
||
/// </summary>
|
||
public readonly bool CanBeGeneralized;
|
||
public readonly Identifier AppropriateSkill;
|
||
public readonly bool Hidden;
|
||
public readonly bool IgnoreAtOutpost;
|
||
|
||
public bool HasOptions => Options.Length > 1;
|
||
public readonly bool MustManuallyAssign;
|
||
|
||
/// <summary>
|
||
/// If enabled and this is an Operate order, it will remove Operate orders of the same item from other characters.
|
||
/// If this is a Movement order, removes other Movement orders from the character who receives the order.
|
||
/// </summary>
|
||
public readonly bool AutoDismiss;
|
||
|
||
/// <summary>
|
||
/// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs.
|
||
/// </summary>
|
||
public readonly ImmutableArray<Identifier> PreferredJobs;
|
||
|
||
public enum OrderTargetType
|
||
{
|
||
Entity,
|
||
Position,
|
||
WallSection
|
||
}
|
||
public OrderTargetType TargetType { get; }
|
||
public int? WallSectionIndex { get; }
|
||
public bool IsIgnoreOrder => Identifier == Tags.IgnoreThis || Identifier == Tags.UnignoreThis;
|
||
|
||
public bool IsDeconstructOrder => Identifier == Tags.DeconstructThis || Identifier == Tags.DontDeconstructThis;
|
||
|
||
/// <summary>
|
||
/// Should the order icon be drawn when the order target is inside a container
|
||
/// </summary>
|
||
public bool DrawIconWhenContained { get; }
|
||
|
||
/// <summary>
|
||
/// Affects how high on the order list the order will be placed (i.e. the manual priority order when it's given) when it's first given.
|
||
/// Manually rearranging orders will override this priority.
|
||
/// </summary>
|
||
public int AssignmentPriority { get; }
|
||
|
||
public bool ColoredWhenControllingGiver { get; }
|
||
public bool DisplayGiverInTooltip { get; }
|
||
|
||
public OrderPrefab(ContentXElement orderElement, OrdersFile file) : base(file, orderElement.GetAttributeIdentifier("identifier", ""))
|
||
{
|
||
Name = TextManager.Get($"OrderName.{Identifier}");
|
||
ContextualName = TextManager.Get($"OrderNameContextual.{Identifier}").Fallback(Name);
|
||
|
||
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
|
||
if (!string.IsNullOrWhiteSpace(targetItemType))
|
||
{
|
||
try
|
||
{
|
||
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
|
||
}
|
||
|
||
catch (Exception e)
|
||
{
|
||
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
|
||
}
|
||
}
|
||
CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false);
|
||
color = orderElement.GetAttributeColor("color");
|
||
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
|
||
UseController = orderElement.GetAttributeBool("usecontroller", false);
|
||
ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty<Identifier>()).ToImmutableArray();
|
||
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
|
||
AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty<Identifier>()).ToImmutableArray();
|
||
TraitorModeOnly = orderElement.GetAttributeBool("TraitorModeOnly", false);
|
||
PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty<Identifier>()).ToImmutableArray();
|
||
Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty<Identifier>()).ToImmutableArray();
|
||
HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty<Identifier>()).ToImmutableArray();
|
||
AllOptions = Options.Concat(HiddenOptions).ToImmutableArray();
|
||
|
||
var optionTargetItems = new Dictionary<Identifier, ImmutableArray<Identifier>>();
|
||
if (orderElement.GetAttributeString("targetitems", "") is string targetItems && targetItems.Contains(';'))
|
||
{
|
||
string[] splitTargetItems = targetItems.Split(';');
|
||
#if DEBUG
|
||
if (splitTargetItems.Length != AllOptions.Length)
|
||
{
|
||
DebugConsole.ThrowError($"Order \"{Identifier}\" has option-specific target items, but the option count doesn't match the target item count");
|
||
}
|
||
#endif
|
||
var allTargetItems = new List<Identifier>();
|
||
for (int i = 0; i < AllOptions.Length; i++)
|
||
{
|
||
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].ToIdentifiers().ToArray() : Array.Empty<Identifier>();
|
||
allTargetItems.AddRange(optionTargetItemsSplit);
|
||
optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray());
|
||
}
|
||
TargetItems = allTargetItems.ToImmutableArray();
|
||
}
|
||
else
|
||
{
|
||
TargetItems = orderElement.GetAttributeIdentifierArray("targetitems", Array.Empty<Identifier>(), trim: true).ToImmutableArray();
|
||
}
|
||
RequireItems = orderElement.GetAttributeIdentifierArray("requireitems", Array.Empty<Identifier>(), trim: true).ToImmutableArray();
|
||
OptionTargetItems = optionTargetItems.ToImmutableDictionary();
|
||
|
||
var category = orderElement.GetAttributeString("category", null);
|
||
this.Category = !string.IsNullOrWhiteSpace(category) ? Enum.Parse<OrderCategory>(category, true) : (OrderCategory?)null;
|
||
this.CategoryIdentifier = (this.Category?.ToString() ?? string.Empty).ToIdentifier();
|
||
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
|
||
CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true);
|
||
AppropriateSkill = orderElement.GetAttributeIdentifier("appropriateskill", Identifier.Empty);
|
||
Hidden = orderElement.GetAttributeBool("hidden", false);
|
||
IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false);
|
||
|
||
OptionNames =
|
||
new ListDictionary<Identifier, LocalizedString>(
|
||
TextManager.Get("OrderOptions." + Identifier).Split(',', ','), Options.Length, i => Options[i]);
|
||
|
||
var spriteElement = orderElement.GetChildElement("sprite");
|
||
if (spriteElement != null)
|
||
{
|
||
SymbolSprite = new Sprite(spriteElement, lazyLoad: true);
|
||
}
|
||
|
||
var optionSprites = new Dictionary<Identifier, Sprite>();
|
||
if (Options != null && Options.Length > 0)
|
||
{
|
||
var optionSpriteElements = orderElement.GetChildElement("optionsprites")?.GetChildElements("sprite");
|
||
if (optionSpriteElements != null && optionSpriteElements.Any())
|
||
{
|
||
for (int i = 0; i < Options.Length; i++)
|
||
{
|
||
if (i >= optionSpriteElements.Count()) { break; };
|
||
var sprite = new Sprite(optionSpriteElements.ElementAt(i), lazyLoad: true);
|
||
optionSprites.Add(Options[i], sprite);
|
||
}
|
||
}
|
||
}
|
||
OptionSprites = optionSprites.ToImmutableDictionary();
|
||
|
||
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
|
||
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
|
||
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement);
|
||
AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100);
|
||
ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false);
|
||
DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false);
|
||
}
|
||
|
||
private bool HasSpecifiedJob(Character character, IReadOnlyList<Identifier> jobs)
|
||
{
|
||
if (jobs == null || jobs.Count == 0) { return false; }
|
||
Identifier jobIdentifier = character?.Info?.Job?.Prefab?.Identifier ?? Identifier.Empty;
|
||
if (jobIdentifier.IsEmpty) { return false; }
|
||
for (int i = 0; i < jobs.Count; i++)
|
||
{
|
||
if (jobIdentifier == jobs[i]) { return true; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
public bool HasAppropriateJob(Character character) => HasSpecifiedJob(character, AppropriateJobs);
|
||
|
||
public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs);
|
||
|
||
public string GetChatMessage(string targetCharacterName, string targetRoomName, Entity targetEntity, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
|
||
{
|
||
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
|
||
{
|
||
// Use special dialogue when we're rearranging character orders
|
||
if (!givingOrderToSelf)
|
||
{
|
||
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty).Value;
|
||
}
|
||
else
|
||
{
|
||
// Say nothing when rearranging the orders of the character you're controlling
|
||
return string.Empty;
|
||
}
|
||
}
|
||
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}";
|
||
if (!orderOption.IsEmpty)
|
||
{
|
||
if (Identifier != "dismissed")
|
||
{
|
||
messageTag += $".{orderOption}";
|
||
}
|
||
else
|
||
{
|
||
string[] splitOption = orderOption.Value.Split('.');
|
||
if (splitOption.Length > 0)
|
||
{
|
||
messageTag += $".{splitOption[0]}";
|
||
}
|
||
}
|
||
}
|
||
|
||
LocalizedString targetEntityName = string.Empty;
|
||
switch (targetEntity)
|
||
{
|
||
case Item item:
|
||
targetEntityName = item.Name;
|
||
break;
|
||
case Hull hull:
|
||
targetEntityName = hull.DisplayName;
|
||
break;
|
||
case Structure structure:
|
||
targetEntityName = structure.Name;
|
||
break;
|
||
case Character character:
|
||
targetEntityName = character.DisplayName;
|
||
break;
|
||
}
|
||
|
||
return TextManager.GetWithVariables(messageTag,
|
||
("[name]", targetCharacterName ?? string.Empty, FormatCapitals.No),
|
||
("[target]", targetEntityName, FormatCapitals.No),
|
||
("[roomname]", targetRoomName ?? string.Empty, FormatCapitals.Yes)).Fallback("").Value;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Get the target item component based on the target item type
|
||
/// </summary>
|
||
public ItemComponent GetTargetItemComponent(Item item)
|
||
{
|
||
if (item?.Components == null || ItemComponentType == null) { return null; }
|
||
foreach (ItemComponent component in item.Components)
|
||
{
|
||
if (component?.GetType() is Type componentType)
|
||
{
|
||
// Items used via a controller (i.e. turrets) are not selectable but should still be valid targets.
|
||
if (!UseController && !component.CanBeSelected) { continue; }
|
||
if (componentType == ItemComponentType) { return component; }
|
||
if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; }
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent)
|
||
{
|
||
firstMatchingComponent = GetTargetItemComponent(item);
|
||
return firstMatchingComponent != null;
|
||
}
|
||
|
||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null, Identifier orderOption = default)
|
||
{
|
||
List<Item> matchingItems = new List<Item>();
|
||
if (submarine == null) { return matchingItems; }
|
||
if (ItemComponentType != null || TargetItems.Any() || RequireItems.Any())
|
||
{
|
||
foreach (var item in Item.ItemList)
|
||
{
|
||
if (RequireItems.Any() && !TargetItemsMatchItem(RequireItems, item)) { continue; }
|
||
if (TargetItems.Any() && !TargetItemsMatchItem(item, orderOption)) { continue; }
|
||
if (RequireItems.None() && TargetItems.None() && !TryGetTargetItemComponent(item, out _)) { continue; }
|
||
if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||
if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; }
|
||
if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; }
|
||
if (item.NonInteractable) { continue; }
|
||
if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; }
|
||
Controller controller = null;
|
||
if (UseController && !item.TryFindController(out controller, tags: ControllerTags)) { continue; }
|
||
if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; }
|
||
matchingItems.Add(item);
|
||
}
|
||
}
|
||
return matchingItems;
|
||
}
|
||
|
||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null, Identifier orderOption = default)
|
||
{
|
||
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ?
|
||
Submarine.MainSubs[1] :
|
||
Submarine.MainSub;
|
||
return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor, orderOption: orderOption);
|
||
}
|
||
|
||
public LocalizedString GetOptionName(string id)
|
||
{
|
||
return GetOptionName(id.ToIdentifier());
|
||
}
|
||
|
||
public LocalizedString GetOptionName(Identifier id)
|
||
{
|
||
if (OptionNames.ContainsKey(id)) { return OptionNames[id]; }
|
||
return string.Empty;
|
||
}
|
||
|
||
public LocalizedString GetOptionName(int index)
|
||
{
|
||
if (index < 0 || index >= Options.Length) { return null; }
|
||
return OptionNames[Options[index]];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to create the order option for the Dismiss order to know which order it targets
|
||
/// </summary>
|
||
/// <param name="order">The order to target with the dismiss order</param>
|
||
public static Identifier GetDismissOrderOption(Order order)
|
||
{
|
||
Identifier option = order.Identifier;
|
||
if (order.Option != Identifier.Empty)
|
||
{
|
||
option = $"{option}.{order.Option}".ToIdentifier();
|
||
}
|
||
return option;
|
||
}
|
||
|
||
public ImmutableArray<Identifier> GetTargetItems(Identifier option = default)
|
||
{
|
||
if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray<Identifier> optionTargetItems))
|
||
{
|
||
return TargetItems;
|
||
}
|
||
else
|
||
{
|
||
return optionTargetItems;
|
||
}
|
||
}
|
||
|
||
public bool TargetItemsMatchItem(Item item, Identifier option = default)
|
||
{
|
||
if (item == null) { return false; }
|
||
|
||
if (Identifier == Tags.DeconstructThis && item.AllowDeconstruct)
|
||
{
|
||
if (item.AllowDeconstruct && !Item.DeconstructItems.Contains(item) &&
|
||
//only allow deconstructing if there are no deconstruction recipes (= deconstructing yields nothing), or deconstruction recipes that
|
||
(item.Prefab.DeconstructItems.None() ||
|
||
item.Prefab.DeconstructItems.Any(deconstructItem =>
|
||
//1. don't require any additional items (bots can't handle that)
|
||
deconstructItem.RequiredOtherItem.None() &&
|
||
//2. don't require a research station (bots don't know how to use those)
|
||
(deconstructItem.RequiredDeconstructor.Length == 0 || deconstructItem.RequiredDeconstructor.Any(d => d != Tags.GeneticResearchStation)))))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
else if (Identifier == Tags.DontDeconstructThis)
|
||
{
|
||
if (Item.DeconstructItems.Contains(item)) { return true; }
|
||
}
|
||
|
||
ImmutableArray<Identifier> targetItems = GetTargetItems(option);
|
||
return TargetItemsMatchItem(targetItems, item);
|
||
}
|
||
|
||
public static bool TargetItemsMatchItem(ImmutableArray<Identifier> targetItems, Item item)
|
||
{
|
||
return item != null && targetItems != null && targetItems.Length > 0 && (targetItems.Contains(item.Prefab.Identifier) || item.HasTag(targetItems));
|
||
}
|
||
|
||
public override void Dispose() { }
|
||
|
||
/// <summary>
|
||
/// Create an Order instance with a null target
|
||
/// </summary>
|
||
public Order CreateInstance(OrderTargetType targetType, Character orderGiver = null, bool isAutonomous = false)
|
||
{
|
||
try
|
||
{
|
||
return targetType switch
|
||
{
|
||
OrderTargetType.Entity => new Order(this, targetEntity: null, targetItem: null, orderGiver, isAutonomous),
|
||
OrderTargetType.Position => new Order(this, target: null, orderGiver),
|
||
OrderTargetType.WallSection => new Order(this, wall: null, sectionIndex: null, orderGiver),
|
||
_ => throw new NotImplementedException()
|
||
};
|
||
}
|
||
catch (NotImplementedException e)
|
||
{
|
||
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}",
|
||
contentPackage: ContentPackage);
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
class Order
|
||
{
|
||
public readonly OrderPrefab Prefab;
|
||
public readonly Identifier Option;
|
||
public readonly int ManualPriority;
|
||
public readonly OrderType Type;
|
||
public readonly AIObjective Objective;
|
||
public bool IsCurrentOrder => Type == OrderType.Current;
|
||
public bool IsDismissal => Prefab.IsDismissal;
|
||
|
||
public enum OrderType
|
||
{
|
||
Current,
|
||
Previous
|
||
}
|
||
|
||
public readonly Entity TargetEntity;
|
||
public readonly ItemComponent TargetItemComponent;
|
||
public readonly Controller ConnectedController;
|
||
|
||
public readonly Character OrderGiver;
|
||
|
||
public readonly OrderTarget TargetPosition;
|
||
|
||
private ISpatialEntity targetSpatialEntity;
|
||
|
||
/// <summary>
|
||
/// Note this property doesn't return the follow target of the Follow objective, as expected!
|
||
/// </summary>
|
||
public ISpatialEntity TargetSpatialEntity
|
||
{
|
||
get
|
||
{
|
||
if (targetSpatialEntity == null)
|
||
{
|
||
if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue)
|
||
{
|
||
targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value];
|
||
}
|
||
else
|
||
{
|
||
targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity;
|
||
}
|
||
}
|
||
return targetSpatialEntity;
|
||
}
|
||
}
|
||
|
||
public Hull TargetHull => TargetEntity as Hull;
|
||
|
||
public enum OrderTargetType
|
||
{
|
||
Entity,
|
||
Position,
|
||
WallSection
|
||
}
|
||
public readonly OrderTargetType TargetType;
|
||
public readonly int? WallSectionIndex;
|
||
|
||
public LocalizedString Name => Prefab.Name;
|
||
public LocalizedString ContextualName => Prefab.ContextualName;
|
||
public Identifier Identifier => Prefab.Identifier;
|
||
public Type ItemComponentType => Prefab.ItemComponentType;
|
||
public bool CanTypeBeSubclass => Prefab.CanTypeBeSubclass;
|
||
public ref readonly ImmutableArray<Identifier> ControllerTags => ref Prefab.ControllerTags;
|
||
public ref readonly ImmutableArray<Identifier> TargetItems => ref Prefab.TargetItems;
|
||
public ref readonly ImmutableArray<Identifier> RequireItems => ref Prefab.RequireItems;
|
||
public ref readonly ImmutableArray<Identifier> Options => ref Prefab.Options;
|
||
public ref readonly ImmutableArray<Identifier> HiddenOptions => ref Prefab.HiddenOptions;
|
||
public ref readonly ImmutableArray<Identifier> AllOptions => ref Prefab.AllOptions;
|
||
public Sprite SymbolSprite => Prefab.SymbolSprite;
|
||
public Color Color => Prefab.Color;
|
||
public bool TargetAllCharacters => Prefab.TargetAllCharacters;
|
||
public ref readonly ImmutableArray<Identifier> AppropriateJobs => ref Prefab.AppropriateJobs;
|
||
public float FadeOutTime => Prefab.FadeOutTime;
|
||
public bool MustSetTarget => Prefab.MustSetTarget;
|
||
public Identifier AppropriateSkill => Prefab.AppropriateSkill;
|
||
public OrderCategory? Category => Prefab.Category;
|
||
public bool MustManuallyAssign => Prefab.MustManuallyAssign;
|
||
public bool IsIgnoreOrder => Prefab.IsIgnoreOrder;
|
||
public bool IsDeconstructOrder => Prefab.IsDeconstructOrder;
|
||
public bool DrawIconWhenContained => Prefab.DrawIconWhenContained;
|
||
public bool Hidden => Prefab.Hidden;
|
||
public bool IgnoreAtOutpost => Prefab.IgnoreAtOutpost;
|
||
public bool IsReport => Prefab.IsReport;
|
||
public bool AutoDismiss => Prefab.AutoDismiss;
|
||
public int AssignmentPriority => Prefab.AssignmentPriority;
|
||
|
||
public bool ColoredWhenControllingGiver => Prefab.ColoredWhenControllingGiver;
|
||
public bool DisplayGiverInTooltip => Prefab.DisplayGiverInTooltip;
|
||
|
||
public readonly bool UseController;
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Entity
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
|
||
: this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Entity
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
|
||
: this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Position
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null)
|
||
: this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Position
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null)
|
||
: this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.WallSection
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null)
|
||
: this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.WallSection
|
||
/// </summary>
|
||
public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null)
|
||
: this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { }
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Entity
|
||
/// </summary>
|
||
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
|
||
{
|
||
Prefab = prefab;
|
||
Option = option;
|
||
ManualPriority = manualPriority;
|
||
Type = orderType;
|
||
Objective = aiObjective;
|
||
|
||
UseController = Prefab.UseController;
|
||
|
||
OrderGiver = orderGiver;
|
||
TargetEntity = targetEntity;
|
||
if (targetItem != null)
|
||
{
|
||
if (UseController)
|
||
{
|
||
ConnectedController = targetItem.Item?.FindController(tags: ControllerTags);
|
||
if (ConnectedController == null)
|
||
{
|
||
DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any.");
|
||
UseController = false;
|
||
}
|
||
}
|
||
TargetEntity = targetItem.Item;
|
||
TargetItemComponent = targetItem;
|
||
}
|
||
|
||
TargetType = OrderTargetType.Entity;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.Position
|
||
/// </summary>
|
||
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null)
|
||
: this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver)
|
||
{
|
||
TargetPosition = target;
|
||
TargetType = OrderTargetType.Position;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Constructor for orders with the target type OrderTargetType.WallSection
|
||
/// </summary>
|
||
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null)
|
||
: this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver)
|
||
{
|
||
WallSectionIndex = sectionIndex;
|
||
TargetType = OrderTargetType.WallSection;
|
||
}
|
||
|
||
private Order(
|
||
Order other,
|
||
OrderPrefab prefab = null,
|
||
Identifier option = default,
|
||
int? manualPriority = null,
|
||
OrderType? type = null,
|
||
AIObjective objective = null,
|
||
Entity targetEntity = null,
|
||
ItemComponent targetItemComponent = null,
|
||
Controller connectedController = null,
|
||
Character orderGiver = null,
|
||
OrderTarget targetPosition = null,
|
||
OrderTargetType? targetType = null,
|
||
int? wallSectionIndex = null,
|
||
bool? useController = null)
|
||
{
|
||
Prefab = prefab ?? other.Prefab;
|
||
Option = option.IfEmpty(other.Option);
|
||
ManualPriority = manualPriority ?? other.ManualPriority;
|
||
Type = type ?? other.Type;
|
||
Objective = objective ?? other.Objective;
|
||
|
||
TargetEntity = targetEntity ?? other.TargetEntity;
|
||
TargetItemComponent = targetItemComponent ?? other.TargetItemComponent;
|
||
ConnectedController = connectedController ?? other.ConnectedController;
|
||
|
||
OrderGiver = orderGiver ?? other.OrderGiver;
|
||
|
||
TargetPosition = targetPosition ?? other.TargetPosition;
|
||
|
||
TargetType = targetType ?? other.TargetType;
|
||
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
|
||
|
||
UseController = useController ?? other.UseController;
|
||
|
||
#if DEBUG
|
||
if (UseController && ConnectedController == null)
|
||
{
|
||
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
public Order WithOption(Identifier option)
|
||
{
|
||
return new Order(this, option: option);
|
||
}
|
||
|
||
public Order WithManualPriority(int newPriority)
|
||
{
|
||
return new Order(this, manualPriority: newPriority);
|
||
}
|
||
|
||
public Order WithOrderGiver(Character orderGiver)
|
||
{
|
||
return new Order(this, orderGiver: orderGiver);
|
||
}
|
||
|
||
public Order WithObjective(AIObjective objective)
|
||
{
|
||
return new Order(this, objective: objective);
|
||
}
|
||
|
||
public Order WithTargetEntity(Entity entity)
|
||
{
|
||
return new Order(this, targetEntity: entity, targetType: OrderTargetType.Entity);
|
||
}
|
||
|
||
public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity)
|
||
{
|
||
if (spatialEntity is WallSection wallSection)
|
||
{
|
||
Structure wall = wallSection.Wall;
|
||
int sectionIndex = wall.Sections.IndexOf(wallSection);
|
||
return WithWallSection(wall, sectionIndex);
|
||
}
|
||
else if (spatialEntity is Entity entity)
|
||
{
|
||
return WithTargetEntity(entity);
|
||
}
|
||
else if (spatialEntity is OrderTarget orderTarget)
|
||
{
|
||
return WithTargetPosition(orderTarget);
|
||
}
|
||
|
||
throw new InvalidOperationException($"Unexpected input type: {spatialEntity.GetType().Name}");
|
||
}
|
||
|
||
public Order WithItemComponent(Item item, ItemComponent component = null)
|
||
{
|
||
Controller controller = null;
|
||
if (UseController)
|
||
{
|
||
controller = item?.FindController(tags: ControllerTags);
|
||
}
|
||
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
|
||
}
|
||
|
||
public Order WithWallSection(Structure wall, int? sectionIndex)
|
||
{
|
||
return new Order(this, targetEntity: wall, wallSectionIndex: sectionIndex, targetType: OrderTargetType.WallSection);
|
||
}
|
||
|
||
public Order WithType(OrderType type)
|
||
{
|
||
return new Order(this, type: type);
|
||
}
|
||
|
||
public Order WithTargetPosition(OrderTarget targetPosition)
|
||
{
|
||
return new Order(this, targetPosition: targetPosition, targetType: OrderTargetType.Position);
|
||
}
|
||
|
||
public Order Clone()
|
||
{
|
||
return new Order(this);
|
||
}
|
||
|
||
public Order GetDismissal()
|
||
{
|
||
if (IsDismissal) { throw new InvalidOperationException("Attempted to dismiss a dismissal order"); }
|
||
return new Order(this, prefab: OrderPrefab.Prefabs["dismissed"], option: GetDismissOrderOption(this));
|
||
}
|
||
|
||
public bool HasAppropriateJob(Character character)
|
||
=> Prefab.HasAppropriateJob(character);
|
||
|
||
public bool HasPreferredJob(Character character)
|
||
=> Prefab.HasPreferredJob(character);
|
||
|
||
public string GetChatMessage(
|
||
string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption = default, bool isNewOrder = true)
|
||
=> Prefab.GetChatMessage(targetCharacterName, targetRoomName, TargetEntity, givingOrderToSelf, orderOption, isNewOrder);
|
||
|
||
/// <summary>
|
||
/// Get the target item component based on the target item type
|
||
/// </summary>
|
||
public ItemComponent GetTargetItemComponent(Item item)
|
||
{
|
||
return Prefab.GetTargetItemComponent(item);
|
||
}
|
||
|
||
public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent)
|
||
{
|
||
return Prefab.TryGetTargetItemComponent(item, out firstMatchingComponent);
|
||
}
|
||
|
||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null)
|
||
{
|
||
return Prefab.GetMatchingItems(submarine, mustBelongToPlayerSub, requiredTeam, interactableFor);
|
||
}
|
||
|
||
|
||
/// <param name="interactableFor">Only returns items which are interactable for this character</param>
|
||
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null)
|
||
{
|
||
return Prefab.GetMatchingItems(mustBelongToPlayerSub, interactableFor);
|
||
}
|
||
|
||
public LocalizedString GetOptionName(string id)
|
||
{
|
||
return Prefab.GetOptionName(id);
|
||
}
|
||
|
||
public LocalizedString GetOptionName(Identifier id)
|
||
{
|
||
return Prefab.GetOptionName(id);
|
||
}
|
||
|
||
public LocalizedString GetOptionName(int index)
|
||
{
|
||
return Prefab.GetOptionName(index);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Used to create the order option for the Dismiss order to know which order it targets
|
||
/// </summary>
|
||
/// <param name="orderInfo">The order to target with the dismiss order</param>
|
||
public static Identifier GetDismissOrderOption(Order order)
|
||
{
|
||
return OrderPrefab.GetDismissOrderOption(order);
|
||
}
|
||
|
||
public bool MatchesOrder(Identifier orderIdentifier, Identifier orderOption) =>
|
||
orderIdentifier == Identifier && orderOption == Option;
|
||
|
||
/*public bool MatchesOrder(Order order, Identifier option) =>
|
||
order != null && MatchesOrder(order.Identifier, option);*/
|
||
|
||
public bool MatchesOrder(Order order) =>
|
||
order != null && MatchesOrder(order.Identifier, order.Option);
|
||
|
||
public bool MatchesDismissedOrder(Identifier dismissOrderOption)
|
||
{
|
||
Identifier[] dismissedOrder = dismissOrderOption.Value.Split('.').Select(s => s.ToIdentifier()).ToArray();
|
||
if (dismissedOrder != null && dismissedOrder.Length > 0)
|
||
{
|
||
Identifier dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : Identifier.Empty;
|
||
if (dismissedOrderIdentifier == Identifier.Empty || dismissedOrderIdentifier != Identifier) { return false; }
|
||
Identifier dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : Identifier.Empty;
|
||
if (dismissedOrderOption == Identifier.Empty && Option == Identifier.Empty) { return true; }
|
||
return dismissedOrderOption == Option;
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public ImmutableArray<Identifier> GetTargetItems(Identifier option = default)
|
||
=> Prefab.GetTargetItems(option);
|
||
|
||
public override string ToString()
|
||
{
|
||
return $"Order ({Name})";
|
||
}
|
||
}
|
||
}
|