using System.Collections; using System.Diagnostics.CodeAnalysis; using Content.Server.Interaction; using Content.Shared.Access.Systems; using Content.Shared.ActionBlocker; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Inventory; using JetBrains.Annotations; using Robust.Shared.Utility; namespace Content.Server.NPC; [DataDefinition] public sealed partial class NPCBlackboard : IEnumerable> { /// /// Global defaults for NPCs /// private static readonly Dictionary BlackboardDefaults = new() { {"BufferRange", 10f}, {"FollowCloseRange", 3f}, {"FollowRange", 7f}, {"FleeRange", 12f}, {"IdleRange", 7f}, {"InteractRange", SharedInteractionSystem.InteractionRange}, {"MaximumIdleTime", 7f}, {MedibotInjectRange, 4f}, {MeleeMissChance, 0.3f}, {"MeleeRange", 1f}, {"PreadatorAttackRange", 7f}, {"PredatorWarnRange", 10f}, {"MinimumIdleTime", 2f}, {"MovementRangeClose", 0.2f}, {"MovementRange", 1.5f}, {"RangedRange", 10f}, {"RotateSpeed", float.MaxValue}, {"VisionRadius", 10f}, {"AggroVisionRadius", 10f}, }; /// /// The specific blackboard for this NPC. /// private readonly Dictionary _blackboard = new(); /// /// Should we allow setting values on the blackboard. This is true when we are planning. /// /// The effects get stored separately so they can potentially be re-applied during execution. /// /// public bool ReadOnly = false; public void Clear() { _blackboard.Clear(); } public NPCBlackboard ShallowClone() { var dict = new NPCBlackboard(); foreach (var item in _blackboard) { dict.SetValue(item.Key, item.Value); } return dict; } [Pure] public bool ContainsKey(string key) { return _blackboard.ContainsKey(key); } /// /// Get the blackboard data for a particular key. /// [Pure] public T GetValue(string key) { return (T)_blackboard[key]; } /// /// Tries to get the blackboard data for a particular key. Returns default if not found /// [Pure] public T? GetValueOrDefault(string key, IEntityManager entManager) { if (_blackboard.TryGetValue(key, out var value)) { return (T)value; } if (TryGetEntityDefault(key, out value, entManager)) { return (T)value; } if (BlackboardDefaults.TryGetValue(key, out value)) { return (T)value; } return default; } /// /// Tries to get the blackboard data for a particular key. /// public bool TryGetValue(string key, [NotNullWhen(true)] out T? value, IEntityManager entManager) { if (_blackboard.TryGetValue(key, out var data)) { value = (T)data; return true; } if (TryGetEntityDefault(key, out data, entManager)) { value = (T)data; return true; } if (BlackboardDefaults.TryGetValue(key, out data)) { value = (T)data; return true; } value = default; return false; } public void SetValue(string key, object value) { if (ReadOnly) { AssertReadonly(); return; } _blackboard[key] = value; } private void AssertReadonly() { DebugTools.Assert(false, $"Tried to write to an NPC blackboard that is readonly!"); } private bool TryGetEntityDefault(string key, [NotNullWhen(true)] out object? value, IEntityManager entManager) { value = default; EntityUid owner; switch (key) { case Access: { if (!TryGetValue(Owner, out owner, entManager)) { return false; } var access = entManager.EntitySysManager.GetEntitySystem(); value = access.FindAccessTags(owner); return true; } case ActiveHand: { if (!TryGetValue(Owner, out owner, entManager) || !entManager.TryGetComponent(owner, out var hands) || hands.ActiveHand == null) { return false; } value = hands.ActiveHand; return true; } case ActiveHandFree: { if (!TryGetValue(Owner, out owner, entManager) || !entManager.TryGetComponent(owner, out var hands) || hands.ActiveHand == null) { return false; } value = hands.ActiveHand.IsEmpty; return true; } case CanMove: { if (!TryGetValue(Owner, out owner, entManager)) { return false; } var blocker = entManager.EntitySysManager.GetEntitySystem(); value = blocker.CanMove(owner); return true; } case FreeHands: { if (!TryGetValue(Owner, out owner, entManager) || !entManager.TryGetComponent(owner, out var hands) || hands.ActiveHand == null) { return false; } var handos = new List(); foreach (var (id, hand) in hands.Hands) { if (!hand.IsEmpty) continue; handos.Add(id); } value = handos; return true; } case Inventory: { if (!TryGetValue(Owner, out owner, entManager) || !entManager.TryGetComponent(owner, out var hands) || hands.ActiveHand == null) { return false; } var handos = new List(); foreach (var (id, hand) in hands.Hands) { if (!hand.IsEmpty) continue; handos.Add(id); } value = handos; return true; } case OwnerCoordinates: { if (!TryGetValue(Owner, out owner, entManager)) { return false; } if (entManager.TryGetComponent(owner, out var xform)) { value = xform.Coordinates; return true; } return false; } default: return false; } } public bool Remove(string key) { DebugTools.Assert(!_blackboard.ContainsKey(key) || _blackboard[key] is T); return _blackboard.Remove(key); } public string GetVisionRadiusKey(IEntityManager entMan) { return TryGetValue("Target", out _, entMan) ? AggroVisionRadius : VisionRadius; } // I Ummd and Ahhd about using strings vs enums and decided on tags because // if a fork wants to do their own thing they don't need to touch the enum. /* * Constants to make development easier */ public const string Access = "Access"; public const string ActiveHand = "ActiveHand"; public const string ActiveHandFree = "ActiveHandFree"; public const string CanMove = "CanMove"; public const string FreeHands = "FreeHands"; public const string FollowTarget = "FollowTarget"; public const string Inventory = "Inventory"; public const string MedibotInjectRange = "MedibotInjectRange"; public const string MeleeMissChance = "MeleeMissChance"; public const string Owner = "Owner"; public const string OwnerCoordinates = "OwnerCoordinates"; public const string MovementTarget = "MovementTarget"; /// /// Can the NPC click open entities such as doors. /// public const string NavInteract = "NavInteract"; /// /// Can the NPC pry open doors for steering. /// public const string NavPry = "NavPry"; /// /// Can the NPC smash obstacles for steering. /// public const string NavSmash = "NavSmash"; /// /// Can the NPC climb obstacles for steering. /// public const string NavClimb = "NavClimb"; /// /// Default key storage for a movement pathfind. /// public const string PathfindKey = "MovementPathfind"; public const string RotateSpeed = "RotateSpeed"; public const string UtilityTarget = "UtilityTarget"; private const string VisionRadius = "VisionRadius"; private const string AggroVisionRadius = "AggroVisionRadius"; /// /// A configurable "order" enum that can be given to an NPC from an external source. /// public const string CurrentOrders = "CurrentOrders"; /// /// A configurable target that's ordered by external sources. /// public const string CurrentOrderedTarget = "CurrentOrderedTarget"; public IEnumerator> GetEnumerator() { return _blackboard.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }