NPC refactor (#10122)

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-09-06 00:28:23 +10:00
committed by GitHub
parent 138e328c04
commit 0286b88388
290 changed files with 13842 additions and 5939 deletions

View File

@@ -1,4 +1,4 @@
using Content.Client.AI;
using Content.Client.NPC;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;

View File

@@ -1,4 +1,4 @@
using Content.Client.AI;
using Content.Client.NPC;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;

View File

@@ -80,6 +80,8 @@ namespace Content.Client.Entry
prototypes.RegisterIgnore("objective");
prototypes.RegisterIgnore("holiday");
prototypes.RegisterIgnore("aiFaction");
prototypes.RegisterIgnore("htnCompound");
prototypes.RegisterIgnore("htnPrimitive");
prototypes.RegisterIgnore("gameMap");
prototypes.RegisterIgnore("behaviorSet");
prototypes.RegisterIgnore("lobbyBackground");

View File

@@ -48,15 +48,18 @@ public sealed class DamageStateVisualizerSystem : VisualizerSystem<DamageStateVi
}
// So they don't draw over mobs anymore
if (data == DamageState.Dead && sprite.DrawDepth > (int) DrawDepth.Items)
if (data == DamageState.Dead)
{
if (sprite.DrawDepth > (int) DrawDepth.FloorObjects)
{
component.OriginalDrawDepth = sprite.DrawDepth;
sprite.DrawDepth = (int) DrawDepth.Items;
sprite.DrawDepth = (int) DrawDepth.FloorObjects;
}
}
else if (component.OriginalDrawDepth != null)
{
sprite.DrawDepth = component.OriginalDrawDepth.Value;
component. OriginalDrawDepth = null;
component.OriginalDrawDepth = null;
}
}
}

View File

@@ -1,29 +1,23 @@
using System;
using System.Collections.Generic;
using Content.Client.Stylesheets;
using Content.Shared.AI;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.AI
namespace Content.Client.NPC
{
#if DEBUG
public sealed class ClientAiDebugSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
private AiDebugMode _tooltips = AiDebugMode.None;
public AiDebugMode Tooltips { get; private set; } = AiDebugMode.None;
private readonly Dictionary<EntityUid, PanelContainer> _aiBoxes = new();
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_tooltips == 0)
if (Tooltips == 0)
{
if (_aiBoxes.Count > 0)
{
@@ -76,7 +70,7 @@ namespace Content.Client.AI
private void HandleUtilityAiDebugMessage(SharedAiDebug.UtilityAiDebugMessage message)
{
if ((_tooltips & AiDebugMode.Thonk) != 0)
if ((Tooltips & AiDebugMode.Thonk) != 0)
{
// I guess if it's out of range we don't know about it?
var entity = message.EntityUid;
@@ -93,7 +87,7 @@ namespace Content.Client.AI
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
{
if ((_tooltips & AiDebugMode.Paths) != 0)
if ((Tooltips & AiDebugMode.Paths) != 0)
{
var entity = message.EntityUid;
TryCreatePanel(entity);
@@ -107,7 +101,7 @@ namespace Content.Client.AI
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
{
if ((_tooltips & AiDebugMode.Paths) != 0)
if ((Tooltips & AiDebugMode.Paths) != 0)
{
var entity = message.EntityUid;
TryCreatePanel(entity);
@@ -126,23 +120,23 @@ namespace Content.Client.AI
tooltip.Dispose();
}
_aiBoxes.Clear();
_tooltips = AiDebugMode.None;
Tooltips = AiDebugMode.None;
}
private void EnableTooltip(AiDebugMode tooltip)
public void EnableTooltip(AiDebugMode tooltip)
{
_tooltips |= tooltip;
Tooltips |= tooltip;
}
private void DisableTooltip(AiDebugMode tooltip)
public void DisableTooltip(AiDebugMode tooltip)
{
_tooltips &= ~tooltip;
Tooltips &= ~tooltip;
}
public void ToggleTooltip(AiDebugMode tooltip)
{
if ((_tooltips & tooltip) != 0)
if ((Tooltips & tooltip) != 0)
{
DisableTooltip(tooltip);
}
@@ -200,5 +194,4 @@ namespace Content.Client.AI
Paths = 1 << 1,
Thonk = 1 << 2,
}
#endif
}

View File

@@ -1,29 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.AI;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Client.AI
namespace Content.Client.NPC
{
#if DEBUG
public sealed class ClientPathfindingDebugSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private PathfindingDebugMode _modes = PathfindingDebugMode.None;
public PathfindingDebugMode Modes { get; private set; } = PathfindingDebugMode.None;
private float _routeDuration = 4.0f; // How long before we remove a route from the overlay
private DebugPathfindingOverlay? _overlay;
@@ -47,8 +39,8 @@ namespace Content.Client.AI
private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
{
if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
(_modes & PathfindingDebugMode.Route) != 0)
if ((Modes & PathfindingDebugMode.Nodes) != 0 ||
(Modes & PathfindingDebugMode.Route) != 0)
{
_overlay?.AStarRoutes.Add(message);
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
@@ -61,8 +53,8 @@ namespace Content.Client.AI
private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
{
if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
(_modes & PathfindingDebugMode.Route) != 0)
if ((Modes & PathfindingDebugMode.Nodes) != 0 ||
(Modes & PathfindingDebugMode.Route) != 0)
{
_overlay?.JpsRoutes.Add(message);
Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
@@ -96,7 +88,7 @@ namespace Content.Client.AI
}
var overlayManager = IoCManager.Resolve<IOverlayManager>();
_overlay = new DebugPathfindingOverlay(EntityManager, _eyeManager, _playerManager, _prototypeManager) {Modes = _modes};
_overlay = new DebugPathfindingOverlay(EntityManager, _eyeManager, _playerManager, _prototypeManager) {Modes = Modes};
overlayManager.AddOverlay(_overlay);
return _overlay;
@@ -117,22 +109,22 @@ namespace Content.Client.AI
public void Disable()
{
_modes = PathfindingDebugMode.None;
Modes = PathfindingDebugMode.None;
DisableOverlay();
}
private void EnableMode(PathfindingDebugMode tooltip)
public void EnableMode(PathfindingDebugMode tooltip)
{
_modes |= tooltip;
if (_modes != 0)
Modes |= tooltip;
if (Modes != 0)
{
EnableOverlay();
}
if (_overlay != null)
{
_overlay.Modes = _modes;
_overlay.Modes = Modes;
}
if (tooltip == PathfindingDebugMode.Graph)
@@ -149,27 +141,27 @@ namespace Content.Client.AI
// So need further investigation.
}
private void DisableMode(PathfindingDebugMode mode)
public void DisableMode(PathfindingDebugMode mode)
{
if (mode == PathfindingDebugMode.Regions && (_modes & PathfindingDebugMode.Regions) != 0)
if (mode == PathfindingDebugMode.Regions && (Modes & PathfindingDebugMode.Regions) != 0)
{
RaiseNetworkEvent(new SharedAiDebug.UnsubscribeReachableMessage());
}
_modes &= ~mode;
if (_modes == 0)
Modes &= ~mode;
if (Modes == 0)
{
DisableOverlay();
}
else if (_overlay != null)
{
_overlay.Modes = _modes;
_overlay.Modes = Modes;
}
}
public void ToggleTooltip(PathfindingDebugMode mode)
{
if ((_modes & mode) != 0)
if ((Modes & mode) != 0)
{
DisableMode(mode);
}
@@ -525,5 +517,4 @@ namespace Content.Client.AI
CachedRegions = 1 << 3,
Regions = 1 << 4,
}
#endif
}

View File

@@ -0,0 +1,7 @@
namespace Content.Client.NPC.HTN;
[RegisterComponent]
public sealed class HTNComponent : NPCComponent
{
public string DebugText = string.Empty;
}

View File

@@ -0,0 +1,41 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Enums;
namespace Content.Client.NPC.HTN;
public sealed class HTNOverlay : Overlay
{
private readonly IEntityManager _entManager = default!;
private readonly Font _font = default!;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public HTNOverlay(IEntityManager entManager, IResourceCache resourceCache)
{
_entManager = entManager;
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.ViewportControl == null)
return;
var handle = args.ScreenHandle;
foreach (var (comp, xform) in _entManager.EntityQuery<HTNComponent, TransformComponent>(true))
{
if (string.IsNullOrEmpty(comp.DebugText) || xform.MapID != args.MapId)
continue;
var worldPos = xform.WorldPosition;
if (!args.WorldAABB.Contains(worldPos))
continue;
var screenPos = args.ViewportControl.WorldToScreen(worldPos);
handle.DrawString(_font, screenPos + new Vector2(0, 10f), comp.DebugText);
}
}
}

View File

@@ -0,0 +1,54 @@
using Content.Shared.NPC;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
namespace Content.Client.NPC.HTN;
public sealed class HTNSystem : EntitySystem
{
/*
* Mainly handles clientside debugging for HTN NPCs.
*/
public bool EnableOverlay
{
get => _enableOverlay;
set
{
var overlayManager = IoCManager.Resolve<IOverlayManager>();
_enableOverlay = value;
if (_enableOverlay)
{
overlayManager.AddOverlay(new HTNOverlay(EntityManager, IoCManager.Resolve<IResourceCache>()));
RaiseNetworkEvent(new RequestHTNMessage()
{
Enabled = true,
});
}
else
{
overlayManager.RemoveOverlay<HTNOverlay>();
RaiseNetworkEvent(new RequestHTNMessage()
{
Enabled = false,
});
}
}
}
private bool _enableOverlay;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<HTNMessage>(OnHTNMessage);
}
private void OnHTNMessage(HTNMessage ev)
{
if (!TryComp<HTNComponent>(ev.Uid, out var htn))
return;
htn.DebugText = ev.Text;
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.NPC;
namespace Content.Client.NPC;
public abstract class NPCComponent : SharedNPCComponent
{
}

View File

@@ -0,0 +1,22 @@
using Content.Client.Eui;
namespace Content.Client.NPC;
public sealed class NPCEui : BaseEui
{
private NPCWindow? _window = new();
public override void Opened()
{
base.Opened();
_window = new NPCWindow();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window?.Close();
_window = null;
}
}

View File

@@ -0,0 +1,24 @@
<userInterface:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.HUD.UI"
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
Title="NPC debug"
MinSize="200 200">
<BoxContainer Name="Options" Orientation="Vertical">
<ui:StripeBack>
<Label Text="NPC" HorizontalAlignment="Center"/>
</ui:StripeBack>
<BoxContainer Name="NPCBox" Orientation="Vertical">
<CheckBox Name="NPCPath" Text="Path"/>
<CheckBox Name="NPCThonk" Text="Thonk"/>
</BoxContainer>
<ui:StripeBack>
<Label Text="Pathfinder" HorizontalAlignment="Center"/>
</ui:StripeBack>
<BoxContainer Name="PathfinderBox" Orientation="Vertical">
<CheckBox Name="PathNodes" Text="Nodes"/>
<CheckBox Name="PathRoutes" Text="Routes"/>
<CheckBox Name="PathRegions" Text="Regions"/>
</BoxContainer>
</BoxContainer>
</userInterface:FancyWindow>

View File

@@ -0,0 +1,33 @@
using Content.Client.UserInterface;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Console;
namespace Content.Client.NPC;
[GenerateTypedNameReferences]
public sealed partial class NPCWindow : FancyWindow
{
public NPCWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var sysManager = IoCManager.Resolve<IEntitySystemManager>();
var debugSys = sysManager.GetEntitySystem<ClientAiDebugSystem>();
var path = sysManager.GetEntitySystem<ClientPathfindingDebugSystem>();
NPCPath.Pressed = (debugSys.Tooltips & AiDebugMode.Paths) != 0x0;
NPCThonk.Pressed = (debugSys.Tooltips & AiDebugMode.Thonk) != 0x0;
NPCPath.OnToggled += args => debugSys.ToggleTooltip(AiDebugMode.Paths);
NPCThonk.OnToggled += args => debugSys.ToggleTooltip(AiDebugMode.Thonk);
PathNodes.Pressed = (path.Modes & PathfindingDebugMode.Nodes) != 0x0;
PathRegions.Pressed = (path.Modes & PathfindingDebugMode.Regions) != 0x0;
PathRoutes.Pressed = (path.Modes & PathfindingDebugMode.Route) != 0x0;
PathNodes.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Nodes);
PathRegions.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Regions);
PathRoutes.OnToggled += args => path.ToggleTooltip(PathfindingDebugMode.Route);
}
}

View File

@@ -0,0 +1,16 @@
using Content.Client.NPC.HTN;
using Robust.Shared.Console;
namespace Content.Client.NPC;
public sealed class ShowHTNCommand : IConsoleCommand
{
public string Command => "showhtn";
public string Description => "Shows the current status for HTN NPCs";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var npcs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<HTNSystem>();
npcs.EnableOverlay ^= true;
}
}

View File

@@ -1,5 +1,6 @@
using Content.Client.Administration.Managers;
using Content.Client.Decals.UI;
using Content.Client.Eui;
using Content.Client.HUD;
using Content.Client.Markers;
using Content.Client.SubFloor;

View File

@@ -235,8 +235,19 @@ public sealed partial class GunSystem : SharedGunSystem
protected override void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null)
{
if (!Timing.IsFirstTimePredicted || !TryComp<TransformComponent>(uid, out var xform)) return;
var ent = Spawn(message.Prototype, xform.Coordinates);
if (!Timing.IsFirstTimePredicted)
return;
EntityCoordinates coordinates;
if (message.MatchRotation)
coordinates = new EntityCoordinates(uid, Vector2.Zero);
else if (TryComp<TransformComponent>(uid, out var xform))
coordinates = xform.Coordinates;
else
return;
var ent = Spawn(message.Prototype, coordinates);
var effectXform = Transform(ent);
effectXform.LocalRotation -= MathF.PI / 2;

View File

@@ -1,62 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.AiLogic;
using NUnit.Framework;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
namespace Content.IntegrationTests.Tests.AI
{
[TestFixture]
[TestOf(typeof(BehaviorSetPrototype))]
public sealed class BehaviorSetsTest
{
[Test]
public async Task TestBehaviorSets()
{
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
var server = pairTracker.Pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var reflectionManager = server.ResolveDependency<IReflectionManager>();
Dictionary<string, List<string>> behaviorSets = new();
// Test that all BehaviorSet actions exist.
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<BehaviorSetPrototype>())
{
behaviorSets[proto.ID] = proto.Actions.ToList();
foreach (var action in proto.Actions)
{
if (!reflectionManager.TryLooseGetType(action, out var actionType) ||
!typeof(IAiUtility).IsAssignableFrom(actionType))
{
Assert.Fail($"Action {action} is not valid within BehaviorSet {proto.ID}");
}
}
}
});
// Test that all BehaviorSets on NPCs exist.
await server.WaitAssertion(() =>
{
foreach (var entity in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!entity.TryGetComponent<UtilityNPCComponent>("UtilityAI", out var npcNode)) continue;
foreach (var entry in npcNode.BehaviorSets)
{
Assert.That(behaviorSets.ContainsKey(entry), $"BehaviorSet {entry} in entity {entity.ID} not found");
}
}
});
await pairTracker.CleanReturnAsync();
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.NPC.HTN;
using NUnit.Framework;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.NPC;
[TestFixture]
public sealed class NPCTest
{
[Test]
public async Task CompoundRecursion()
{
var pool = await PoolManager.GetServerClient(new PoolSettings() { NoClient = true });
var server = pool.Pair.Server;
await server.WaitIdleAsync();
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
var counts = new Dictionary<string, int>();
foreach (var compound in protoManager.EnumeratePrototypes<HTNCompoundTask>())
{
Count(compound, counts);
counts.Clear();
}
});
await pool.CleanReturnAsync();
}
private static void Count(HTNCompoundTask compound, Dictionary<string, int> counts)
{
foreach (var branch in compound.Branches)
{
foreach (var task in branch.Tasks)
{
if (task is HTNCompoundTask compoundTask)
{
var count = counts.GetOrNew(compound.ID);
count++;
Assert.That(count, Is.LessThan(50));
counts[compound.ID] = count;
Count(compoundTask, counts);
}
}
}
}
}

View File

@@ -1,58 +0,0 @@
using Content.Server.Administration;
using Content.Server.AI.Components;
using Content.Server.AI.EntitySystems;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.AiLogic;
using Content.Shared.Administration;
using Content.Shared.Movement.Components;
using Robust.Shared.Console;
namespace Content.Server.AI.Commands
{
[AdminCommand(AdminFlags.Fun)]
public sealed class AddAiCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entities = default!;
public string Command => "addai";
public string Description => "Add an ai component with a given processor to an entity.";
public string Help => "Usage: addai <entityId> <behaviorSet1> <behaviorSet2>..."
+ "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this."
+ "\n behaviorSet: Name of a behaviorset to add to the component on initialize.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if(args.Length < 1)
{
shell.WriteLine("Wrong number of args.");
return;
}
var entId = new EntityUid(int.Parse(args[0]));
if (!_entities.EntityExists(entId))
{
shell.WriteLine($"Unable to find entity with uid {entId}");
return;
}
if (_entities.HasComponent<NPCComponent>(entId))
{
shell.WriteLine("Entity already has an AI component.");
return;
}
var comp = _entities.AddComponent<UtilityNPCComponent>(entId);
var npcSystem = IoCManager.Resolve<IEntityManager>().EntitySysManager.GetEntitySystem<NPCSystem>();
for (var i = 1; i < args.Length; i++)
{
var bSet = args[i];
npcSystem.AddBehaviorSet(comp, bSet, false);
}
npcSystem.RebuildActions(comp);
shell.WriteLine("AI component added.");
}
}
}

View File

@@ -1,11 +0,0 @@
using Content.Server.AI.EntitySystems;
namespace Content.Server.AI.Components
{
[RegisterComponent]
public sealed class AiFactionTagComponent : Component
{
[DataField("factions")]
public Faction Factions { get; private set; } = Faction.None;
}
}

View File

@@ -1,17 +0,0 @@
using Content.Server.AI.EntitySystems;
namespace Content.Server.AI.Components
{
[Access(typeof(NPCSystem))]
public abstract class NPCComponent : Component
{
// TODO: Soon. I didn't realise how much effort it was to deprecate the old one.
/// <summary>
/// Contains all of the world data for a particular NPC in terms of how it sees the world.
/// </summary>
//[ViewVariables, DataField("blackboardA")]
//public Dictionary<string, object> BlackboardA = new();
public float VisionRadius => 7f;
}
}

View File

@@ -1,38 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.Tracking;
using Content.Shared.Damage;
using Content.Shared.MobState.Components;
using Content.Server.Silicons.Bots;
namespace Content.Server.AI.Utility.Considerations.Bot
{
public sealed class CanInjectCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !entMan.TryGetComponent(target, out DamageableComponent? damageableComponent))
return 0;
if (entMan.TryGetComponent(target, out RecentlyInjectedComponent? recently))
return 0f;
if (!entMan.TryGetComponent(target, out MobStateComponent? mobState) || mobState.IsDead())
return 0f;
if (damageableComponent.TotalDamage == 0)
return 0f;
if (damageableComponent.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
return 1f;
if (damageableComponent.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
return 1f;
return 0f;
}
}
}

View File

@@ -1,20 +0,0 @@
using Content.Server.Fluids.Components;
namespace Content.Server.AI.EntitySystems
{
public sealed class GoToPuddleSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
public EntityUid GetNearbyPuddle(EntityUid cleanbot, float range = 10)
{
foreach (var entity in _lookup.GetEntitiesInRange(cleanbot, range))
{
if (HasComp<PuddleComponent>(entity))
return entity;
}
return default;
}
}
}

View File

@@ -1,74 +0,0 @@
using Content.Server.Chemistry.Components.SolutionManager;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.AI.Tracking;
using Content.Server.Popups;
using Content.Server.Chat.Systems;
using Content.Server.Silicons.Bots;
using Content.Shared.MobState.Components;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Robust.Shared.Player;
using Robust.Shared.Audio;
namespace Content.Server.AI.EntitySystems
{
public sealed class InjectNearbySystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
public EntityUid GetNearbyInjectable(EntityUid medibot, float range = 4)
{
foreach (var entity in _lookup.GetEntitiesInRange(medibot, range))
{
if (HasComp<InjectableSolutionComponent>(entity) && HasComp<MobStateComponent>(entity))
return entity;
}
return default;
}
public bool Inject(EntityUid medibot, EntityUid target)
{
if (!TryComp<MedibotComponent>(medibot, out var botComp))
return false;
if (!TryComp<DamageableComponent>(target, out var damage))
return false;
if (!_solutionSystem.TryGetInjectableSolution(target, out var injectable))
return false;
if (!_interactionSystem.InRangeUnobstructed(medibot, target))
return true; // return true lets the bot reattempt the action on the same target
if (damage.TotalDamage == 0)
return false;
if (damage.TotalDamage <= MedibotComponent.StandardMedDamageThreshold)
{
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
EnsureComp<RecentlyInjectedComponent>(target);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
return true;
}
if (damage.TotalDamage >= MedibotComponent.EmergencyMedDamageThreshold)
{
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
EnsureComp<RecentlyInjectedComponent>(target);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(medibot, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
return true;
}
return false;
}
}
}

View File

@@ -1,42 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.AI.Components;
namespace Content.Server.AI.EntitySystems;
public sealed partial class NPCSystem
{
/*
/// <summary>
/// Tries to get the blackboard data for a particular key. Returns default if not found
/// </summary>
public T? GetValueOrDefault<T>(NPCComponent component, string key)
{
if (component.BlackboardA.TryGetValue(key, out var value))
{
return (T) value;
}
return default;
}
/// <summary>
/// Tries to get the blackboard data for a particular key.
/// </summary>
public bool TryGetValue<T>(NPCComponent component, string key, [NotNullWhen(true)] out T? value)
{
if (component.BlackboardA.TryGetValue(key, out var data))
{
value = (T) data;
return true;
}
value = default;
return false;
}
/*
* Constants to make development easier
*/
public const string VisionRadius = "VisionRadius";
}

View File

@@ -1,284 +0,0 @@
using System.Runtime.ExceptionServices;
using System.Threading;
using Content.Server.AI.Components;
using Content.Server.AI.LoadBalancer;
using Content.Server.AI.Operators;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility;
using Content.Server.CPUJob.JobQueues;
using Content.Server.CPUJob.JobQueues.Queues;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
namespace Content.Server.AI.EntitySystems;
public sealed partial class NPCSystem
{
/*
* Handles Utility AI, implemented via IAUS
*/
private readonly NpcActionComparer _comparer = new();
private Dictionary<string, List<Type>> _behaviorSets = new();
private readonly AiActionJobQueue _aiRequestQueue = new();
private void InitializeUtility()
{
SubscribeLocalEvent<UtilityNPCComponent, ComponentStartup>(OnUtilityStartup);
foreach (var bSet in _prototypeManager.EnumeratePrototypes<BehaviorSetPrototype>())
{
var actions = new List<Type>();
foreach (var act in bSet.Actions)
{
if (!_reflectionManager.TryLooseGetType(act, out var parsedType) ||
!typeof(IAiUtility).IsAssignableFrom(parsedType))
{
_sawmill.Error($"Unable to parse AI action for {act}");
}
else
{
actions.Add(parsedType);
}
}
_behaviorSets[bSet.ID] = actions;
}
}
private void OnUtilityStartup(EntityUid uid, UtilityNPCComponent component, ComponentStartup args)
{
if (component.BehaviorSets.Count > 0)
{
RebuildActions(component);
}
component._planCooldownRemaining = component.PlanCooldown;
component._blackboard = new Blackboard(component.Owner);
}
public AiActionRequestJob RequestAction(UtilityNPCComponent component, AiActionRequest request, CancellationTokenSource cancellationToken)
{
var job = new AiActionRequestJob(0.002, request, cancellationToken.Token);
// AI should already know if it shouldn't request again
_aiRequestQueue.EnqueueJob(job);
return job;
}
private void UpdateUtility(float frameTime)
{
foreach (var (_, comp) in EntityQuery<ActiveNPCComponent, UtilityNPCComponent>())
{
if (_count >= _maxUpdates) break;
Update(comp, frameTime);
_count++;
}
_aiRequestQueue.Process();
}
private void ReceivedAction(UtilityNPCComponent component)
{
if (component._actionRequest == null)
{
return;
}
switch (component._actionRequest.Exception)
{
case null:
break;
default:
_sawmill.Fatal(component._actionRequest.Exception.ToString());
ExceptionDispatchInfo.Capture(component._actionRequest.Exception).Throw();
// The code never actually reaches here, because the above throws.
// This is to tell the compiler that the flow never leaves here.
throw component._actionRequest.Exception;
}
var action = component._actionRequest.Result;
component._actionRequest = null;
// Actions with lower scores should be implicitly dumped by GetAction
// If we're not allowed to replace the action with an action of the same type then dump.
if (action == null || !action.CanOverride && component.CurrentAction?.GetType() == action.GetType())
{
return;
}
var currentOp = component.CurrentAction?.ActionOperators.Peek();
if (currentOp != null && currentOp.HasStartup)
{
currentOp.Shutdown(Outcome.Failed);
}
component.CurrentAction = action;
action.SetupOperators(component._blackboard);
}
private void Update(UtilityNPCComponent component, float frameTime)
{
// If we asked for a new action we don't want to dump the existing one.
if (component._actionRequest != null)
{
if (component._actionRequest.Status != JobStatus.Finished)
{
return;
}
ReceivedAction(component);
// Do something next tick
return;
}
component._planCooldownRemaining -= frameTime;
// Might find a better action while we're doing one already
if (component._planCooldownRemaining <= 0.0f)
{
component._planCooldownRemaining = component.PlanCooldown;
component._actionCancellation = new CancellationTokenSource();
component._actionRequest = RequestAction(component, new AiActionRequest(component.Owner, component._blackboard, component.AvailableActions), component._actionCancellation);
return;
}
// When we spawn in we won't get an action for a bit
if (component.CurrentAction == null)
{
return;
}
var outcome = component.CurrentAction.Execute(frameTime);
switch (outcome)
{
case Outcome.Success:
if (component.CurrentAction.ActionOperators.Count == 0)
{
component.CurrentAction.Shutdown();
component.CurrentAction = null;
// Nothing to compare new action to
component._blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
}
break;
case Outcome.Continuing:
break;
case Outcome.Failed:
component.CurrentAction.Shutdown();
component.CurrentAction = null;
component._blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Adds the BehaviorSet to the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if you want to manually rebuild it after bulk updates.</param>
public void AddBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
{
if (!_behaviorSets.ContainsKey(behaviorSet))
{
_sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!");
return;
}
if (!npc.BehaviorSets.Add(behaviorSet))
{
_sawmill.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!");
return;
}
if (rebuild)
RebuildActions(npc);
if (npc.BehaviorSets.Count == 1 && !IsAwake(npc))
WakeNPC(npc);
}
/// <summary>
/// Removes the BehaviorSet from the NPC.
/// </summary>
/// <param name="npc"></param>
/// <param name="behaviorSet"></param>
/// <param name="rebuild">Set to false if yo uwant to manually rebuild it after bulk updates.</param>
public void RemoveBehaviorSet(UtilityNPCComponent npc, string behaviorSet, bool rebuild = true)
{
if (!_behaviorSets.TryGetValue(behaviorSet, out var actions))
{
Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but no such BehaviorSet found!");
return;
}
if (!npc.BehaviorSets.Remove(behaviorSet))
{
Logger.Error($"Tried to remove BehaviorSet {behaviorSet} from {npc} but it doesn't have that BehaviorSet!");
return;
}
if (rebuild)
RebuildActions(npc);
if (npc.BehaviorSets.Count == 0)
SleepNPC(npc);
}
/// <summary>
/// Clear our actions and re-instantiate them from our BehaviorSets.
/// Will ensure each action is unique.
/// </summary>
/// <param name="npc"></param>
public void RebuildActions(UtilityNPCComponent npc)
{
npc.AvailableActions.Clear();
foreach (var bSet in npc.BehaviorSets)
{
foreach (var action in GetActions(bSet))
{
if (npc.AvailableActions.Contains(action)) continue;
// Setup
action.Owner = npc.Owner;
// Ad to actions.
npc.AvailableActions.Add(action);
}
}
SortActions(npc);
}
private IEnumerable<IAiUtility> GetActions(string behaviorSet)
{
foreach (var action in _behaviorSets[behaviorSet])
{
yield return (IAiUtility) _typeFactory.CreateInstance(action);
}
}
/// <summary>
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
/// </summary>
private void SortActions(UtilityNPCComponent npc)
{
npc.AvailableActions.Sort(_comparer);
}
private sealed class NpcActionComparer : Comparer<IAiUtility>
{
public override int Compare(IAiUtility? x, IAiUtility? y)
{
if (x == null || y == null) return 0;
return y.Bonus.CompareTo(x.Bonus);
}
}
}

View File

@@ -1,29 +0,0 @@
using Content.Server.AI.LoadBalancer;
using Content.Shared.AI;
using JetBrains.Annotations;
namespace Content.Server.AI.EntitySystems
{
#if DEBUG
[UsedImplicitly]
public sealed class ServerAiDebugSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
AiActionRequestJob.FoundAction += NotifyActionJob;
}
public override void Shutdown()
{
base.Shutdown();
AiActionRequestJob.FoundAction -= NotifyActionJob;
}
private void NotifyActionJob(SharedAiDebug.UtilityAiDebugMessage message)
{
RaiseNetworkEvent(message);
}
}
#endif
}

View File

@@ -1,19 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.WorldState;
namespace Content.Server.AI.LoadBalancer
{
public sealed class AiActionRequest
{
public EntityUid EntityUid { get; }
public Blackboard? Context { get; }
public IEnumerable<IAiUtility>? Actions { get; }
public AiActionRequest(EntityUid uid, Blackboard context, IEnumerable<IAiUtility> actions)
{
EntityUid = uid;
Context = context;
Actions = actions;
}
}
}

View File

@@ -1,136 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.AI.Components;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Utility;
using Content.Server.CPUJob.JobQueues;
using Content.Shared.AI;
using Robust.Shared.Utility;
namespace Content.Server.AI.LoadBalancer
{
public sealed class AiActionRequestJob : Job<UtilityAction>
{
#if DEBUG
public static event Action<SharedAiDebug.UtilityAiDebugMessage>? FoundAction;
#endif
private readonly AiActionRequest _request;
public AiActionRequestJob(
double maxTime,
AiActionRequest request,
CancellationToken cancellationToken = default) : base(maxTime, cancellationToken)
{
_request = request;
}
protected override async Task<UtilityAction?> Process()
{
if (_request.Context == null)
{
return null;
}
var entity = _request.Context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().HasComponent<NPCComponent>(entity))
{
return null;
}
if (_request.Actions == null || _request.Context == null)
{
return null;
}
var consideredTaskCount = 0;
// Actions are pre-sorted
var actions = new Stack<IAiUtility>(_request.Actions);
// So essentially we go through and once we have a valid score that score becomes the cutoff;
// once the bonus of new tasks is below the cutoff we can stop evaluating.
// Use last action as the basis for the cutoff
var cutoff = _request.Context.GetState<LastUtilityScoreState>().GetValue();
UtilityAction? foundAction = null;
// To see what I was trying to do watch these 2 videos about Infinite Axis Utility System (IAUS):
// Architecture Tricks: Managing Behaviors in Time, Space, and Depth
// Building a Better Centaur
// We'll want to cap the considered entities at some point, e.g. if 500 guns are in a stack cap it at 256 or whatever
while (actions.Count > 0)
{
if (consideredTaskCount > 0 && consideredTaskCount % 5 == 0)
{
await SuspendIfOutOfTime();
// If this happens then that means something changed when we resumed so ABORT
if (actions.Count == 0 || _request.Context == null)
{
return null;
}
}
var action = actions.Pop();
switch (action)
{
case ExpandableUtilityAction expandableUtilityAction:
if (!expandableUtilityAction.IsValid(_request.Context))
{
break;
}
foreach (var expanded in expandableUtilityAction.GetActions(_request.Context))
{
actions.Push(expanded);
}
break;
case UtilityAction utilityAction:
consideredTaskCount++;
var bonus = utilityAction.Bonus;
if (bonus < cutoff)
{
// We know none of the other actions can beat this as they're pre-sorted
actions.Clear();
break;
}
var score = utilityAction.GetScore(_request.Context, cutoff);
if (score > cutoff)
{
foundAction = utilityAction;
cutoff = score;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
_request.Context.GetState<LastUtilityScoreState>().SetValue(cutoff);
#if DEBUG
if (foundAction != null)
{
var selfState = _request.Context.GetState<SelfState>().GetValue();
DebugTools.AssertNotNull(selfState);
FoundAction?.Invoke(new SharedAiDebug.UtilityAiDebugMessage(
selfState!,
DebugTime,
cutoff,
foundAction.GetType().Name,
consideredTaskCount));
}
#endif
_request.Context.ResetPlanning();
return foundAction;
}
}
}

View File

@@ -1,52 +0,0 @@
namespace Content.Server.AI.Operators
{
public abstract class AiOperator
{
public bool HasStartup { get; private set; }
public bool HasShutdown { get; private set; }
/// <summary>
/// Called once when the NPC starts this action
/// </summary>
/// <returns>true if it hasn't started up previously</returns>
public virtual bool Startup()
{
// If we've already startup then no point continuing
// This signals to the override that it's already startup
// Should probably throw but it made some code elsewhere marginally easier
if (HasStartup)
return false;
HasStartup = true;
return true;
}
/// <summary>
/// Called once when the NPC is done with this action if the outcome is successful or fails.
/// </summary>
public virtual bool Shutdown(Outcome outcome)
{
if (HasShutdown)
return false;
HasShutdown = true;
return true;
}
/// <summary>
/// Called every tick for the AI
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public abstract Outcome Execute(float frameTime);
}
public enum Outcome
{
Success,
Continuing,
Failed,
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.AI.EntitySystems;
namespace Content.Server.AI.Operators.Bots
{
public sealed class InjectOperator : AiOperator
{
private EntityUid _medibot;
private EntityUid _target;
public InjectOperator(EntityUid medibot, EntityUid target)
{
_medibot = medibot;
_target = target;
}
public override Outcome Execute(float frameTime)
{
var injectSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InjectNearbySystem>();
if (injectSystem.Inject(_medibot, _target))
return Outcome.Success;
return Outcome.Failed;
}
}
}

View File

@@ -1,88 +0,0 @@
using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Interaction;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Operators.Combat.Melee
{
public sealed class SwingMeleeWeaponOperator : AiOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly float _burstTime;
private float _elapsedTime;
private readonly EntityUid _owner;
private readonly EntityUid _target;
public SwingMeleeWeaponOperator(EntityUid owner, EntityUid target, float burstTime = 1.0f)
{
IoCManager.InjectDependencies(this);
_owner = owner;
_target = target;
_burstTime = burstTime;
}
public override bool Startup()
{
if (!base.Startup())
{
return true;
}
if (!_entMan.TryGetComponent(_owner, out CombatModeComponent? combatModeComponent))
{
return false;
}
if (!combatModeComponent.IsInCombatMode)
{
combatModeComponent.IsInCombatMode = true;
}
return true;
}
public override bool Shutdown(Outcome outcome)
{
if (!base.Shutdown(outcome))
return false;
if (_entMan.TryGetComponent(_owner, out CombatModeComponent? combatModeComponent))
{
combatModeComponent.IsInCombatMode = false;
}
return true;
}
public override Outcome Execute(float frameTime)
{
if (_burstTime <= _elapsedTime)
{
return Outcome.Success;
}
if (!_entMan.TryGetComponent(_owner, out HandsComponent? hands) || hands.ActiveHandEntity == null)
{
return Outcome.Failed;
}
var meleeWeapon = hands.ActiveHandEntity;
_entMan.TryGetComponent(meleeWeapon, out MeleeWeaponComponent? meleeWeaponComponent);
if ((_entMan.GetComponent<TransformComponent>(_target).Coordinates.Position - _entMan.GetComponent<TransformComponent>(_owner).Coordinates.Position).Length >
meleeWeaponComponent?.Range)
{
return Outcome.Failed;
}
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.AiUseInteraction(_owner, _entMan.GetComponent<TransformComponent>(_target).Coordinates, _target);
_elapsedTime += frameTime;
return Outcome.Continuing;
}
}
}

View File

@@ -1,94 +0,0 @@
using Content.Server.CombatMode;
using Content.Server.Interaction;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Operators.Combat.Melee
{
public sealed class UnarmedCombatOperator : AiOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly float _burstTime;
private float _elapsedTime;
private readonly EntityUid _owner;
private readonly EntityUid _target;
private MeleeWeaponComponent? _unarmedCombat;
public UnarmedCombatOperator(EntityUid owner, EntityUid target, float burstTime = 1.0f)
{
IoCManager.InjectDependencies(this);
_owner = owner;
_target = target;
_burstTime = burstTime;
}
public override bool Startup()
{
if (!base.Startup())
{
return true;
}
if (!_entMan.TryGetComponent(_owner, out CombatModeComponent? combatModeComponent))
{
return false;
}
if (!combatModeComponent.IsInCombatMode)
{
combatModeComponent.IsInCombatMode = true;
}
if (_entMan.TryGetComponent(_owner, out MeleeWeaponComponent? unarmedCombatComponent))
{
_unarmedCombat = unarmedCombatComponent;
}
else
{
return false;
}
return true;
}
public override bool Shutdown(Outcome outcome)
{
if (!base.Shutdown(outcome))
return false;
if (_entMan.TryGetComponent(_owner, out CombatModeComponent? combatModeComponent))
{
combatModeComponent.IsInCombatMode = false;
}
return true;
}
public override Outcome Execute(float frameTime)
{
if (_unarmedCombat == null ||
!_entMan.GetComponent<TransformComponent>(_target).Coordinates.TryDistance(_entMan, _entMan.GetComponent<TransformComponent>(_owner).Coordinates, out var distance) || distance >
_unarmedCombat.Range)
{
return Outcome.Failed;
}
if (_burstTime <= _elapsedTime)
{
return Outcome.Success;
}
if (_unarmedCombat?.Deleted ?? true)
{
return Outcome.Failed;
}
var interactionSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.AiUseInteraction(_owner, _entMan.GetComponent<TransformComponent>(_target).Coordinates, _target);
_elapsedTime += frameTime;
return Outcome.Continuing;
}
}
}

View File

@@ -1,24 +0,0 @@
namespace Content.Server.AI.Operators.Generic
{
public sealed class WaitOperator : AiOperator
{
private readonly float _waitTime;
private float _accumulatedTime = 0.0f;
public WaitOperator(float waitTime)
{
_waitTime = waitTime;
}
public override Outcome Execute(float frameTime)
{
if (_accumulatedTime < _waitTime)
{
_accumulatedTime += frameTime;
return Outcome.Continuing;
}
return Outcome.Success;
}
}
}

View File

@@ -1,76 +0,0 @@
using Content.Server.AI.Utility;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Interaction;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// Close the last EntityStorage we opened
/// This will also update the State for it (which a regular InteractWith won't do)
/// </summary>
public sealed class CloseLastStorageOperator : AiOperator
{
private readonly EntityUid _owner;
private EntityUid _target;
public CloseLastStorageOperator(EntityUid owner)
{
_owner = owner;
}
public override bool Startup()
{
if (!base.Startup())
{
return true;
}
var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
if (blackboard == null)
{
return false;
}
_target = blackboard.GetState<LastOpenedStorageState>().GetValue();
return _target != default;
}
public override bool Shutdown(Outcome outcome)
{
if (!base.Shutdown(outcome))
return false;
var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
blackboard?.GetState<LastOpenedStorageState>().SetValue(default);
return true;
}
public override Outcome Execute(float frameTime)
{
if (_target == default || !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(_owner, _target, popup: true))
{
return Outcome.Failed;
}
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(_target, out EntityStorageComponent? storageComponent) ||
storageComponent.IsWeldedShut)
{
return Outcome.Failed;
}
if (entMan.EntitySysManager.TryGetEntitySystem<EntityStorageSystem>(out var entStorage) && storageComponent.Open)
{
entStorage.ToggleOpen(_owner, _target, storageComponent);
}
return Outcome.Success;
}
}
}

View File

@@ -1,25 +0,0 @@
using Content.Shared.Hands.EntitySystems;
namespace Content.Server.AI.Operators.Inventory
{
public sealed class DropEntityOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _entity;
public DropEntityOperator(EntityUid owner, EntityUid entity)
{
_owner = owner;
_entity = entity;
}
/// <summary>
/// Requires EquipEntityOperator to put it in the active hand first
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public override Outcome Execute(float frameTime)
{
return IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedHandsSystem>().TryDrop(_owner, _entity) ? Outcome.Success : Outcome.Failed;
}
}
}

View File

@@ -1,33 +0,0 @@
using Content.Server.Hands.Components;
using Content.Shared.Hands.EntitySystems;
namespace Content.Server.AI.Operators.Inventory
{
public sealed class DropHandItemsOperator : AiOperator
{
private readonly EntityUid _owner;
public DropHandItemsOperator(EntityUid owner)
{
_owner = owner;
}
public override Outcome Execute(float frameTime)
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(_owner, out HandsComponent? handsComponent))
{
return Outcome.Failed;
}
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedHandsSystem>();
foreach (var hand in handsComponent.Hands.Values)
{
if (!hand.IsEmpty)
sys.TryDrop(_owner, hand, handsComp: handsComponent);
}
return Outcome.Success;
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Shared.Hands.EntitySystems;
namespace Content.Server.AI.Operators.Inventory
{
public sealed class EquipEntityOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _entity;
public EquipEntityOperator(EntityUid owner, EntityUid entity)
{
_owner = owner;
_entity = entity;
}
public override Outcome Execute(float frameTime)
{
var sys = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SharedHandsSystem>();
// TODO: If in clothing then click on it
if (sys.TrySelect(_owner, _entity))
return Outcome.Success;
// TODO: Get free hand count; if no hands free then fail right here
// TODO: Go through inventory
return Outcome.Failed;
}
}
}

View File

@@ -1,52 +0,0 @@
using Content.Server.CombatMode;
using Content.Server.Interaction;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// A Generic interacter; if you need to check stuff then make your own
/// </summary>
public sealed class InteractWithEntityOperator : AiOperator
{
[Dependency] private readonly IEntityManager _entMan = default!;
private readonly EntityUid _owner;
private readonly EntityUid _useTarget;
public InteractWithEntityOperator(EntityUid owner, EntityUid useTarget)
{
IoCManager.InjectDependencies(this);
_owner = owner;
_useTarget = useTarget;
}
public override Outcome Execute(float frameTime)
{
var targetTransform = _entMan.GetComponent<TransformComponent>(_useTarget);
if (targetTransform.GridUid != _entMan.GetComponent<TransformComponent>(_owner).GridUid)
{
return Outcome.Failed;
}
var interactionSystem = EntitySystem.Get<InteractionSystem>();
if (!interactionSystem.InRangeUnobstructed(_owner, _useTarget, popup: true))
{
return Outcome.Failed;
}
if (_entMan.TryGetComponent(_owner, out CombatModeComponent? combatModeComponent))
{
combatModeComponent.IsInCombatMode = false;
}
// Click on da thing
interactionSystem.AiUseInteraction(_owner, targetTransform.Coordinates, _useTarget);
return Outcome.Success;
}
}
}

View File

@@ -1,53 +0,0 @@
using Content.Server.AI.Utility;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Interaction;
using Robust.Shared.Containers;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// If the target is in EntityStorage will open its parent container
/// </summary>
public sealed class OpenStorageOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _target;
public OpenStorageOperator(EntityUid owner, EntityUid target)
{
_owner = owner;
_target = target;
}
public override Outcome Execute(float frameTime)
{
if (!_target.TryGetContainer(out var container))
{
return Outcome.Success;
}
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(_owner, container.Owner, popup: true))
{
return Outcome.Failed;
}
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(container.Owner, out EntityStorageComponent? storageComponent) ||
storageComponent.IsWeldedShut)
{
return Outcome.Failed;
}
if (!storageComponent.Open)
{
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<EntityStorageSystem>().ToggleOpen(_owner, _target, storageComponent);
}
var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
blackboard?.GetState<LastOpenedStorageState>().SetValue(container.Owner);
return Outcome.Success;
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Server.Interaction;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Item;
using Robust.Shared.Containers;
namespace Content.Server.AI.Operators.Inventory
{
public sealed class PickupEntityOperator : AiOperator
{
// Input variables
private readonly EntityUid _owner;
private readonly EntityUid _target;
public PickupEntityOperator(EntityUid owner, EntityUid target)
{
_owner = owner;
_target = target;
}
public override Outcome Execute(float frameTime)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
var interactionSystem = sysMan.GetEntitySystem<InteractionSystem>();
var handsSys = sysMan.GetEntitySystem<SharedHandsSystem>();
if (entMan.Deleted(_target)
|| !entMan.HasComponent<ItemComponent>(_target)
|| _target.IsInContainer()
|| !interactionSystem.InRangeUnobstructed(_owner, _target, popup: true))
{
return Outcome.Failed;
}
// select empty hand
if (!handsSys.TrySelectEmptyHand(_owner))
return Outcome.Failed;
interactionSystem.InteractHand(_owner, _target);
return Outcome.Success;
}
}
}

View File

@@ -1,37 +0,0 @@
using Content.Server.Hands.Components;
using Content.Shared.Hands.EntitySystems;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// Will find the item in storage, put it in an active hand, then use it
/// </summary>
public sealed class UseItemInInventoryOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _target;
public UseItemInInventoryOperator(EntityUid owner, EntityUid target)
{
_owner = owner;
_target = target;
}
public override Outcome Execute(float frameTime)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
var sys = sysMan.GetEntitySystem<SharedHandsSystem>();
// TODO: Also have this check storage a la backpack etc.
if (!entMan.TryGetComponent(_owner, out HandsComponent? handsComponent)
|| !sys.TrySelect(_owner, _target, handsComponent)
|| !sys.TryUseItemInHand(_owner, false, handsComponent))
{
return Outcome.Failed;
}
return Outcome.Success;
}
}
}

View File

@@ -1,73 +0,0 @@
using Content.Server.AI.Steering;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.AI.Operators.Movement
{
public sealed class MoveToEntityOperator : AiOperator
{
// TODO: This and steering need to support InRangeUnobstructed now
private readonly EntityUid _owner;
private readonly EntityUid _target;
// For now we'll just get as close as we can because we're not doing LOS checks to be able to pick up at the max interaction range
public float ArrivalDistance { get; }
public float PathfindingProximity { get; }
private readonly bool _requiresInRangeUnobstructed;
public MoveToEntityOperator(
EntityUid owner,
EntityUid target,
float arrivalDistance = 1.0f,
float pathfindingProximity = 1.5f,
bool requiresInRangeUnobstructed = false)
{
_owner = owner;
_target = target;
ArrivalDistance = arrivalDistance;
PathfindingProximity = pathfindingProximity;
_requiresInRangeUnobstructed = requiresInRangeUnobstructed;
}
public override bool Startup()
{
if (!base.Startup())
{
return true;
}
var steering = EntitySystem.Get<NPCSteeringSystem>();
var comp = steering.Register(_owner, new EntityCoordinates(_target, Vector2.Zero));
comp.Range = ArrivalDistance;
return true;
}
public override bool Shutdown(Outcome outcome)
{
if (!base.Shutdown(outcome))
return false;
var steering = EntitySystem.Get<NPCSteeringSystem>();
steering.Unregister(_owner);
return true;
}
public override Outcome Execute(float frameTime)
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NPCSteeringComponent>(_owner, out var steering))
return Outcome.Failed;
switch (steering.Status)
{
case SteeringStatus.NoPath:
return Outcome.Failed;
case SteeringStatus.InRange:
return Outcome.Success;
case SteeringStatus.Moving:
return Outcome.Continuing;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -1,61 +0,0 @@
using Content.Server.AI.Steering;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Server.AI.Operators.Movement
{
public sealed class MoveToGridOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityCoordinates _target;
public float DesiredRange { get; set; }
public MoveToGridOperator(EntityUid owner, EntityCoordinates target, float desiredRange = 1.5f)
{
_owner = owner;
_target = target;
DesiredRange = desiredRange;
}
public override bool Startup()
{
if (!base.Startup())
{
return true;
}
var steering = EntitySystem.Get<NPCSteeringSystem>();
var comp = steering.Register(_owner, _target);
comp.Range = DesiredRange;
return true;
}
public override bool Shutdown(Outcome outcome)
{
if (!base.Shutdown(outcome))
return false;
var steering = EntitySystem.Get<NPCSteeringSystem>();
steering.Unregister(_owner);
return true;
}
public override Outcome Execute(float frameTime)
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NPCSteeringComponent>(_owner, out var steering))
return Outcome.Failed;
switch (steering.Status)
{
case SteeringStatus.NoPath:
return Outcome.Failed;
case SteeringStatus.InRange:
return Outcome.Success;
case SteeringStatus.Moving:
return Outcome.Continuing;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -1,59 +0,0 @@
using Content.Server.Hands.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Hands.EntitySystems;
using Robust.Shared.Random;
namespace Content.Server.AI.Operators.Nutrition
{
public sealed class UseDrinkInInventoryOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _target;
private float _interactionCooldown;
public UseDrinkInInventoryOperator(EntityUid owner, EntityUid target)
{
_owner = owner;
_target = target;
}
public override Outcome Execute(float frameTime)
{
if (_interactionCooldown >= 0)
{
_interactionCooldown -= frameTime;
return Outcome.Continuing;
}
var entities = IoCManager.Resolve<IEntityManager>();
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
var handsSys = sysMan.GetEntitySystem<SharedHandsSystem>();
// TODO: Also have this check storage a la backpack etc.
if (entities.Deleted(_target) ||
!entities.TryGetComponent(_owner, out HandsComponent? handsComponent))
{
return Outcome.Failed;
}
if (!handsSys.TrySelect<DrinkComponent>(_owner, out var drinkComponent, handsComponent))
return Outcome.Failed;
if (!handsSys.TryUseItemInHand(_owner, false, handsComponent))
return Outcome.Failed;
_interactionCooldown = IoCManager.Resolve<IRobustRandom>().NextFloat() + 0.5f;
if (drinkComponent.Deleted || EntitySystem.Get<DrinkSystem>().IsEmpty(drinkComponent.Owner, drinkComponent)
|| entities.TryGetComponent(_owner, out ThirstComponent? thirstComponent) &&
thirstComponent.CurrentThirst >= thirstComponent.ThirstThresholds[ThirstThreshold.Okay])
{
return Outcome.Success;
}
/// uuhhh do afters for drinks might mess this up?
return Outcome.Continuing;
}
}
}

View File

@@ -1,57 +0,0 @@
using Content.Server.Hands.Components;
using Content.Server.Nutrition.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Nutrition.Components;
namespace Content.Server.AI.Operators.Nutrition
{
public sealed class UseFoodInInventoryOperator : AiOperator
{
private readonly EntityUid _owner;
private readonly EntityUid _target;
private float _interactionCooldown;
public UseFoodInInventoryOperator(EntityUid owner, EntityUid target)
{
_owner = owner;
_target = target;
}
public override Outcome Execute(float frameTime)
{
if (_interactionCooldown >= 0)
{
_interactionCooldown -= frameTime;
return Outcome.Continuing;
}
var entities = IoCManager.Resolve<IEntityManager>();
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
var handsSys = sysMan.GetEntitySystem<SharedHandsSystem>();
// TODO: Also have this check storage a la backpack etc.
if (entities.Deleted(_target) ||
!entities.TryGetComponent(_owner, out HandsComponent? handsComponent))
{
return Outcome.Failed;
}
if (!handsSys.TrySelect<FoodComponent>(_owner, out var foodComponent, handsComponent))
return Outcome.Failed;
if (!handsSys.TryUseItemInHand(_owner, false, handsComponent))
return Outcome.Failed;
if ((!entities.EntityExists(_target) ? EntityLifeStage.Deleted : entities.GetComponent<MetaDataComponent>(_target).EntityLifeStage) >= EntityLifeStage.Deleted ||
foodComponent.UsesRemaining == 0 ||
entities.TryGetComponent(_owner, out HungerComponent? hungerComponent) &&
hungerComponent.CurrentHunger >= hungerComponent.HungerThresholds[HungerThreshold.Okay])
{
return Outcome.Success;
}
/// do afters for food might mess this up?
return Outcome.Continuing;
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Operators.Movement;
namespace Content.Server.AI.Operators.Sequences
{
public sealed class GoPickupEntitySequence : SequenceOperator
{
public GoPickupEntitySequence(EntityUid owner, EntityUid target)
{
Sequence = new Queue<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(owner, target, requiresInRangeUnobstructed: true),
new OpenStorageOperator(owner, target),
new PickupEntityOperator(owner, target),
});
}
}
}

View File

@@ -1,41 +0,0 @@
namespace Content.Server.AI.Operators.Sequences
{
/// <summary>
/// Sequential chain of operators
/// Saves having to duplicate stuff like MoveTo and PickUp everywhere
/// </summary>
public abstract class SequenceOperator : AiOperator
{
public Queue<AiOperator> Sequence { get; protected set; } = new();
public override Outcome Execute(float frameTime)
{
if (Sequence.Count == 0)
{
return Outcome.Success;
}
var op = Sequence.Peek();
op.Startup();
var outcome = op.Execute(frameTime);
switch (outcome)
{
case Outcome.Success:
op.Shutdown(outcome);
// Not over until all operators are done
Sequence.Dequeue();
return Outcome.Continuing;
case Outcome.Continuing:
return Outcome.Continuing;
case Outcome.Failed:
op.Shutdown(outcome);
Sequence.Clear();
return Outcome.Failed;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.Chat.Systems;
namespace Content.Server.AI.Operators.Speech
{
public sealed class SpeakOperator : AiOperator
{
private EntityUid _speaker;
private string _speechString;
public SpeakOperator(EntityUid speaker, string speechString)
{
_speaker = speaker;
_speechString = speechString;
}
public override Outcome Execute(float frameTime)
{
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
chatSystem.TrySendInGameICMessage(_speaker, _speechString, InGameICChatType.Speak, false);
return Outcome.Success;
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Content.Server.AI.Pathfinding;
[RegisterComponent]
[Access(typeof(PathfindingSystem))]
public sealed class GridPathfindingComponent : Component, IPathfindingGraph
{
public Dictionary<Vector2i, PathfindingChunk> Graph = new();
}

View File

@@ -1,6 +0,0 @@
namespace Content.Server.AI.Pathfinding;
public interface IPathfindingGraph
{
}

View File

@@ -1,30 +0,0 @@
using Robust.Shared.Map;
namespace Content.Server.AI.Steering
{
public interface IAiSteeringRequest
{
SteeringStatus Status { get; set; }
MapCoordinates TargetMap { get; }
EntityCoordinates TargetGrid { get; }
/// <summary>
/// How close we have to get before we've arrived
/// </summary>
float ArrivalDistance { get; }
/// <summary>
/// How close the pathfinder needs to get. Typically you want this set lower than ArrivalDistance
/// </summary>
float PathfindingProximity { get; }
/// <summary>
/// If we need LOS on the entity first before interaction
/// </summary>
bool RequiresInRangeUnobstructed { get; }
/// <summary>
/// To avoid spamming InRangeUnobstructed we'll apply a cd to it.
/// </summary>
public float TimeUntilInteractionCheck { get; set; }
}
}

View File

@@ -1,25 +0,0 @@
namespace Content.Server.AI.Tracking
{
public sealed class RecentlyInjectedSystem : EntitySystem
{
Queue<EntityUid> RemQueue = new();
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var toRemove in RemQueue)
{
RemComp<RecentlyInjectedComponent>(toRemove);
}
RemQueue.Clear();
foreach (var entity in EntityQuery<RecentlyInjectedComponent>())
{
entity.Accumulator += frameTime;
if (entity.Accumulator < entity.RemoveTime.TotalSeconds)
continue;
entity.Accumulator = 0;
RemQueue.Enqueue(entity.Owner);
}
}
}
}

View File

@@ -1,50 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Generic;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.ActionBlocker;
using Content.Server.AI.WorldState.States.Movement;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Bots
{
public sealed class GoToPuddleAndWait : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
MoveToEntityOperator moveOperator = new MoveToEntityOperator(Owner, Target, 0, 0);
float waitTime = 3f;
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
moveOperator,
new WaitOperator(waitTime),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(Target);
// Can just set ourselves as entity given unarmed just inherits from meleeweapon
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanMoveCon>()
.BoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,57 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Generic;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Operators.Bots;
using Content.Server.AI.Operators.Speech;
using Content.Server.AI.WorldState;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.ActionBlocker;
using Content.Server.AI.WorldState.States.Movement;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.Utility.Considerations.Bot;
namespace Content.Server.AI.Utility.Actions.Bots
{
public sealed class InjectNearby : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
MoveToEntityOperator moveOperator = new MoveToEntityOperator(Owner, Target);
float waitTime = 3f;
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
moveOperator,
new SpeakOperator(Owner, Loc.GetString("medibot-start-inject")),
new WaitOperator(waitTime),
new InjectOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanMoveCon>()
.BoolCurve(context),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
considerationsManager.Get<CanInjectCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,40 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
{
public sealed class EquipGloves : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
{
public sealed class PickUpGloves : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Head
{
public sealed class EquipHead : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Head
{
public sealed class PickUpHead : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,40 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
{
public sealed class EquipOuterClothing : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
{
public sealed class PickUpOuterClothing : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,40 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
{
public sealed class EquipShoes : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
{
public sealed class PickUpShoes : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,46 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Combat;
namespace Content.Server.AI.Utility.Actions.Combat.Melee
{
public sealed class EquipMelee : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<WeaponEntityState>().SetValue(Target);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanPutTargetInInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<MeleeWeaponSpeedCon>()
.QuadraticCurve(context, 1.0f, 0.5f, 0.0f, 0.0f),
considerationsManager.Get<MeleeWeaponDamageCon>()
.QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f),
};
}
}
}

View File

@@ -1,75 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Combat.Melee;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat;
using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Combat;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.AI.WorldState.States.Movement;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Actions.Combat.Melee
{
public sealed class MeleeWeaponAttackEntity : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
MoveToEntityOperator moveOperator;
var equipped = context.GetState<EquippedEntityState>().GetValue();
if (equipped != default && IoCManager.Resolve<IEntityManager>().TryGetComponent(equipped, out MeleeWeaponComponent? meleeWeaponComponent))
{
moveOperator = new MoveToEntityOperator(Owner, Target, meleeWeaponComponent.Range - 0.01f);
}
else
{
// TODO: Abort
moveOperator = new MoveToEntityOperator(Owner, Target);
}
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
moveOperator,
new SwingMeleeWeaponOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(Target);
var equipped = context.GetState<EquippedEntityState>().GetValue();
context.GetState<WeaponEntityState>().SetValue(equipped);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetIsDeadCon>()
.InverseBoolCurve(context),
considerationsManager.Get<TargetIsCritCon>()
.QuadraticCurve(context, -0.8f, 1.0f, 1.0f, 0.0f),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetHealthCon>()
.PresetCurve(context, PresetCurve.TargetHealth),
considerationsManager.Get<MeleeWeaponSpeedCon>()
.QuadraticCurve(context, 1.0f, 0.5f, 0.0f, 0.0f),
considerationsManager.Get<MeleeWeaponDamageCon>()
.QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,45 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Combat;
namespace Content.Server.AI.Utility.Actions.Combat.Melee
{
public sealed class PickUpMeleeWeapon : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<WeaponEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<MeleeWeaponDamageCon>()
.QuadraticCurve(context, 1.0f, 0.25f, 0.0f, 0.0f),
considerationsManager.Get<MeleeWeaponSpeedCon>()
.QuadraticCurve(context, -1.0f, 0.5f, 1.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,71 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Combat.Melee;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Combat;
using Content.Server.AI.WorldState.States.Movement;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Actions.Combat.Melee
{
public sealed class UnarmedAttackEntity : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
MoveToEntityOperator moveOperator;
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out MeleeWeaponComponent? unarmedCombatComponent))
{
moveOperator = new MoveToEntityOperator(Owner, Target, unarmedCombatComponent.Range - 0.01f);
}
// I think it's possible for this to happen given planning is time-sliced?
// TODO: At this point we should abort
else
{
moveOperator = new MoveToEntityOperator(Owner, Target);
}
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
moveOperator,
new UnarmedCombatOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(Target);
// Can just set ourselves as entity given unarmed just inherits from meleeweapon
context.GetState<WeaponEntityState>().SetValue(Owner);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetIsDeadCon>()
.InverseBoolCurve(context),
considerationsManager.Get<TargetIsCritCon>()
.QuadraticCurve(context, -0.8f, 1.0f, 1.0f, 0.0f),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetHealthCon>()
.PresetCurve(context, PresetCurve.TargetHealth),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
// TODO: Consider our Speed and Damage to compare this to using a weapon
// Also need to unequip our weapon if we have one (xenos can't hold one so no issue for now)
};
}
}
}

View File

@@ -1,15 +0,0 @@
namespace Content.Server.AI.Utility.Actions
{
public interface IAiUtility
{
/// <summary>
/// NPC this action is attached to.
/// </summary>
EntityUid Owner { get; set; }
/// <summary>
/// Highest possible score for this action.
/// </summary>
float Bonus { get; }
}
}

View File

@@ -1,65 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.Utility.Considerations.State;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Inventory;
namespace Content.Server.AI.Utility.Actions.Idle
{
/// <summary>
/// If we just picked up a bunch of stuff and have time then close it
/// </summary>
public sealed class CloseLastEntityStorage : UtilityAction
{
public override float Bonus => IdleBonus + 0.01f;
public override void SetupOperators(Blackboard context)
{
var lastStorage = context.GetState<LastOpenedStorageState>().GetValue();
if (!lastStorage.IsValid())
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new CloseLastStorageOperator(Owner),
});
return;
}
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(Owner, lastStorage),
new CloseLastStorageOperator(Owner),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
var lastStorage = context.GetState<LastOpenedStorageState>();
context.GetState<TargetEntityState>().SetValue(lastStorage.GetValue());
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<StoredStateEntityIsNullCon>().Set(typeof(LastOpenedStorageState), context)
.InverseBoolCurve(context),
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,90 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Generic;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Pathfinding;
using Content.Server.AI.Pathfinding.Accessible;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.ActionBlocker;
using Content.Server.AI.WorldState;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.AI.Utility.Actions.Idle
{
/// <summary>
/// Will move to a random spot close by
/// </summary>
public sealed class WanderAndWait : UtilityAction
{
public override bool CanOverride => false;
public override float Bonus => 1.0f;
public override void SetupOperators(Blackboard context)
{
var robustRandom = IoCManager.Resolve<IRobustRandom>();
var randomGrid = FindRandomGrid(robustRandom);
float waitTime;
if (randomGrid != EntityCoordinates.Invalid)
{
waitTime = robustRandom.Next(3, 8);
}
else
{
waitTime = 0.0f;
}
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new MoveToGridOperator(Owner, randomGrid),
new WaitOperator(waitTime),
});
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<CanMoveCon>()
.BoolCurve(context),
};
}
private EntityCoordinates FindRandomGrid(IRobustRandom robustRandom, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
// Very inefficient (should weight each region by its node count) but better than the old system
var reachableSystem = entMan.EntitySysManager.GetEntitySystem<AiReachableSystem>();
var reachableArgs = ReachableArgs.GetArgs(Owner);
var entityRegion = reachableSystem.GetRegion(Owner);
var reachableRegions = reachableSystem.GetReachableRegions(reachableArgs, entityRegion);
// TODO: When SetupOperators can fail this should be null and fail the setup.
if (reachableRegions.Count == 0)
{
return default;
}
var reachableNodes = new List<PathfindingNode>();
foreach (var region in reachableRegions)
{
foreach (var node in region.Nodes)
{
reachableNodes.Add(node);
}
}
var targetNode = robustRandom.Pick(reachableNodes);
if (!entMan.TryGetComponent(entMan.GetComponent<TransformComponent>(Owner).GridUid, out IMapGridComponent? grid))
return default;
var targetGrid = grid.Grid.GridTileToLocal(targetNode.TileRef.GridIndices);
return targetGrid;
}
}
}

View File

@@ -1,42 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.Utility.Considerations.Nutrition.Drink;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
{
public sealed class PickUpDrink : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<DrinkValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,44 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Operators.Nutrition;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Nutrition.Drink;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
{
public sealed class UseDrinkInInventory : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseDrinkInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetInOurInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<DrinkValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators.Sequences;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Containers;
using Content.Server.AI.Utility.Considerations.Movement;
using Content.Server.AI.Utility.Considerations.Nutrition.Food;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Nutrition.Food
{
public sealed class PickUpFood : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetDistanceCon>()
.PresetCurve(context, PresetCurve.Distance),
considerationsManager.Get<FoodValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
considerationsManager.Get<TargetAccessibleCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,44 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Operators.Nutrition;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Inventory;
using Content.Server.AI.Utility.Considerations.Nutrition.Food;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Actions.Nutrition.Food
{
public sealed class UseFoodInInventory : UtilityAction
{
public EntityUid Target { get; set; } = default!;
public override void SetupOperators(Blackboard context)
{
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, Target),
new UseFoodInInventoryOperator(Owner, Target),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(Target);
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<TargetInOurInventoryCon>()
.BoolCurve(context),
considerationsManager.Get<FoodValueCon>()
.QuadraticCurve(context, 1.0f, 0.4f, 0.0f, 0.0f),
};
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Movement;
using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState;
namespace Content.Server.AI.Utility.Actions.Test
{
/// <summary>
/// Used for pathfinding debugging
/// </summary>
public sealed class MoveRightAndLeftTen : UtilityAction
{
public override bool CanOverride => false;
public override void SetupOperators(Blackboard context)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var currentPosition = entMan.GetComponent<TransformComponent>(Owner).Coordinates;
var nextPosition = entMan.GetComponent<TransformComponent>(Owner).Coordinates.Offset(new Vector2(10.0f, 0.0f));
var originalPosOp = new MoveToGridOperator(Owner, currentPosition, 0.25f);
var newPosOp = new MoveToGridOperator(Owner, nextPosition, 0.25f);
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
newPosOp,
originalPosOp
});
}
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
return new[]
{
considerationsManager.Get<DummyCon>()
.BoolCurve(context),
};
}
}
}

View File

@@ -1,140 +0,0 @@
using Content.Server.AI.Operators;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility;
using Robust.Shared.Utility;
namespace Content.Server.AI.Utility.Actions
{
/// <summary>
/// The same DSE can be used across multiple actions.
/// </summary>
public abstract class UtilityAction : IAiUtility
{
/// <summary>
/// If we're trying to find a new action can we replace a currently running one with one of the same type.
/// e.g. If you're already wandering you don't want to replace it with a different wander.
/// </summary>
public virtual bool CanOverride => false;
/// <summary>
/// This is used to sort actions; if there's a top-tier action available we won't bother checking the lower tiers.
/// Threshold doesn't necessarily mean we'll do an action at a higher threshold;
/// if it's really un-optimal (i.e. low score) then we'll also check lower tiers
/// </summary>
public virtual float Bonus { get; set; } = IdleBonus;
// For GW2 they had the bonuses close together but IMO it feels better when they're more like discrete tiers.
// These are just baselines to make mass-updates easier; actions can do whatever
// e.g. if you want shooting a gun to be considered before picking up a gun you could + 1.0f it or w/e
public const float IdleBonus = 1.0f;
public const float NormalBonus = 5.0f;
public const float NeedsBonus = 10.0f;
public const float CombatPrepBonus = 20.0f;
public const float CombatBonus = 30.0f;
public const float DangerBonus = 50.0f;
public EntityUid Owner { get; set; }
/// <summary>
/// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible.
/// Ideally you put anything that's easy to assess and can cause an early-out first just so the rest aren't evaluated.
/// </summary>
/// Uses Func<float> as you don't want to eval the later considerations unless necessary, but we also need the total count
/// so can't use IEnumerable
protected abstract IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context);
/// <summary>
/// To keep the operators simple we can chain them together here, e.g. move to can be chained with other operators.
/// </summary>
public Queue<AiOperator> ActionOperators { get; protected set; }
/// <summary>
/// Sometimes we may need to set the target for an action or the likes.
/// This is mainly useful for expandable states so each one can have a separate target.
/// </summary>
/// <param name="context"></param>
protected virtual void UpdateBlackboard(Blackboard context) {}
// Needs to be able to be instantiated without args via typefactory.
public UtilityAction()
{
Owner = default!;
ActionOperators = default!;
}
public virtual void Shutdown() {}
/// <summary>
/// If this action is chosen then setup the operators to run. This also allows for operators to be reset.
/// </summary>
public abstract void SetupOperators(Blackboard context);
// Call the task's operator with Execute and get the outcome
public Outcome Execute(float frameTime)
{
if (!ActionOperators.TryPeek(out var op))
{
return Outcome.Success;
}
op.Startup();
var outcome = op.Execute(frameTime);
switch (outcome)
{
case Outcome.Success:
op.Shutdown(outcome);
ActionOperators.Dequeue();
break;
case Outcome.Continuing:
break;
case Outcome.Failed:
op.Shutdown(outcome);
ActionOperators.Clear();
break;
default:
throw new ArgumentOutOfRangeException();
}
return outcome;
}
/// <summary>
/// AKA the Decision Score Evaluator (DSE)
/// This is where the magic happens
/// </summary>
/// <param name="context"></param>
/// <param name="min"></param>
/// <returns></returns>
public float GetScore(Blackboard context, float min)
{
UpdateBlackboard(context);
var considerations = GetConsiderations(context);
DebugTools.Assert(considerations.Count > 0);
// Overall structure is based on Building a better centaur
// Ideally we should early-out each action as cheaply as possible if it's not valid, thus
// the finalScore can only go down over time.
var finalScore = 1.0f;
var minThreshold = min / Bonus;
context.GetState<ConsiderationState>().SetValue(considerations.Count);
foreach (var consideration in considerations)
{
var score = consideration.Invoke();
finalScore *= score;
DebugTools.Assert(!float.IsNaN(score));
// The score can only ever go down from each consideration so if we're below minimum no point continuing.
if (0.0f >= finalScore || finalScore < minThreshold) {
return 0.0f;
}
}
DebugTools.Assert(finalScore <= 1.0f);
return finalScore * Bonus;
}
}
}

View File

@@ -1,46 +0,0 @@
using Content.Server.AI.Components;
using Content.Server.AI.LoadBalancer;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.WorldState;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using System.Threading;
using Content.Server.AI.EntitySystems;
namespace Content.Server.AI.Utility.AiLogic
{
[RegisterComponent, Access(typeof(NPCSystem))]
[ComponentReference(typeof(NPCComponent))]
public sealed class UtilityNPCComponent : NPCComponent
{
public Blackboard Blackboard => _blackboard;
public Blackboard _blackboard = default!;
/// <summary>
/// The sum of all BehaviorSets gives us what actions the AI can take
/// </summary>
[DataField("behaviorSets", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<BehaviorSetPrototype>))]
public HashSet<string> BehaviorSets { get; } = new();
public List<IAiUtility> AvailableActions { get; set; } = new();
/// <summary>
/// The currently running action; most importantly are the operators.
/// </summary>
public UtilityAction? CurrentAction { get; set; }
/// <summary>
/// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
/// or if there's no players nearby increase it.
/// </summary>
public float PlanCooldown { get; } = 0.5f;
public float _planCooldownRemaining;
/// <summary>
/// If we've requested a plan then wait patiently for the action
/// </summary>
public AiActionRequestJob? _actionRequest;
public CancellationTokenSource? _actionCancellation;
}
}

View File

@@ -1,21 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Server.AI.Utility
{
[Prototype("behaviorSet")]
public sealed class BehaviorSetPrototype : IPrototype
{
/// <summary>
/// Name of the BehaviorSet.
/// </summary>
[ViewVariables]
[IdDataFieldAttribute]
public string ID { get; } = default!;
/// <summary>
/// Actions that this BehaviorSet grants to the entity.
/// </summary>
[DataField("actions")]
public IReadOnlyList<string> Actions { get; private set; } = new List<string>();
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.ActionBlocker;
namespace Content.Server.AI.Utility.Considerations.ActionBlocker
{
public sealed class CanMoveCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var self = context.GetState<SelfState>().GetValue();
if (!EntitySystem.Get<ActionBlockerSystem>().CanMove(self))
{
return 0.0f;
}
return 1.0f;
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Clothing;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.Clothing.Components;
using Content.Shared.Inventory;
namespace Content.Server.AI.Utility.Considerations.Clothing
{
public sealed class ClothingInInventoryCon : Consideration
{
public ClothingInInventoryCon Slot(SlotFlags slotFlags, Blackboard context)
{
// Ideally we'd just use a variable but then if we were iterating through multiple AI at once it'd be
// Stuffed so we need to store it on the AI's context.
context.GetState<ClothingSlotFlagConState>().SetValue(slotFlags);
return this;
}
protected override float GetScore(Blackboard context)
{
var slots = context.GetState<ClothingSlotConState>().GetValue();
if (slots == null) return 0.0f;
foreach (var entity in context.GetState<EnumerableInventoryState>().GetValue())
{
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(entity, out ClothingComponent? clothingComponent) ||
!EntitySystem.Get<InventorySystem>().TryGetSlot(entity, slots, out var slotDef))
{
continue;
}
if ((clothingComponent.Slots & slotDef.SlotFlags) != 0)
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Clothing;
namespace Content.Server.AI.Utility.Considerations.Clothing
{
public sealed class ClothingInSlotCon : Consideration
{
public ClothingInSlotCon Slot(string slot, Blackboard context)
{
context.GetState<ClothingSlotConState>().SetValue(slot);
return this;
}
protected override float GetScore(Blackboard context)
{
var slot = context.GetState<ClothingSlotConState>().GetValue();
var inventory = context.GetState<EquippedClothingState>().GetValue();
return slot != null && inventory.ContainsKey(slot) ? 1.0f : 0.0f;
}
}
}

View File

@@ -1,16 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Considerations.Combat.Melee
{
public sealed class CanUnarmedCombatCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var entity = context.GetState<SelfState>().GetValue();
return entityManager.HasComponent<MeleeWeaponComponent>(entity) ? 1.0f : 0.0f;
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Considerations.Combat.Melee
{
public sealed class HasMeleeWeaponCon : Consideration
{
protected override float GetScore(Blackboard context)
{
foreach (var item in context.GetState<EnumerableInventoryState>().GetValue())
{
if (IoCManager.Resolve<IEntityManager>().HasComponent<MeleeWeaponComponent>(item))
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Combat;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Considerations.Combat.Melee
{
public sealed class MeleeWeaponDamageCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<WeaponEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(target, out MeleeWeaponComponent? meleeWeaponComponent))
{
return 0.0f;
}
// Just went with max health
return (meleeWeaponComponent.Damage.Total / 300.0f).Float();
}
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Considerations.Combat.Melee
{
public sealed class MeleeWeaponEquippedCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var equipped = context.GetState<EquippedEntityState>().GetValue();
if (equipped == null)
{
return 0.0f;
}
return IoCManager.Resolve<IEntityManager>().HasComponent<MeleeWeaponComponent>(equipped) ? 1.0f : 0.0f;
}
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Combat;
using Content.Server.Weapon.Melee.Components;
namespace Content.Server.AI.Utility.Considerations.Combat.Melee
{
public sealed class MeleeWeaponSpeedCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<WeaponEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(target, out MeleeWeaponComponent? meleeWeaponComponent))
{
return 0.0f;
}
return meleeWeaponComponent.ArcCooldownTime / 10.0f;
}
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
public sealed class TargetHealthCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(target, out DamageableComponent? damageableComponent))
{
return 0.0f;
}
return (float) damageableComponent.TotalDamage / 300.0f;
}
}
}

View File

@@ -1,26 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.MobState.Components;
namespace Content.Server.AI.Utility.Considerations.Combat
{
public sealed class TargetIsCritCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(target, out MobStateComponent? mobState))
{
return 0.0f;
}
if (mobState.IsCritical())
{
return 1.0f;
}
return 0.0f;
}
}
}

View File

@@ -1,26 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.MobState.Components;
namespace Content.Server.AI.Utility.Considerations.Combat
{
public sealed class TargetIsDeadCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(target, out MobStateComponent? mobState))
{
return 0.0f;
}
if (mobState.IsDead())
{
return 1.0f;
}
return 0.0f;
}
}
}

View File

@@ -1,140 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility;
using JetBrains.Annotations;
namespace Content.Server.AI.Utility.Considerations
{
public abstract class Consideration
{
protected abstract float GetScore(Blackboard context);
private float GetAdjustedScore(Blackboard context)
{
var score = GetScore(context);
/*
* Now using the geometric mean
* for n scores you take the n-th root of the scores multiplied
* e.g. a, b, c scores you take Math.Pow(a * b * c, 1/3)
* To get the ACTUAL geometric mean at any one stage you'd need to divide by the running consideration count
* however, the downside to this is it will fluctuate up and down over time.
* For our purposes if we go below the minimum threshold we want to cut it off, thus we take a
* "running geometric mean" which can only ever go down (and by the final value will equal the actual geometric mean).
*/
// Previously we used a makeupvalue method although the geometric mean is less punishing for more considerations
var considerationsCount = context.GetState<ConsiderationState>().GetValue();
var adjustedScore = MathF.Pow(score, 1 / (float) considerationsCount);
return MathHelper.Clamp(adjustedScore, 0.0f, 1.0f);
}
[Pure]
private static float BoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
return x > 0.0f ? 1.0f : 0.0f;
}
public Func<float> BoolCurve(Blackboard context)
{
float Result()
{
var adjustedScore = GetAdjustedScore(context);
return BoolCurve(adjustedScore);
}
return Result;
}
[Pure]
private static float InverseBoolCurve(float x)
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
return x == 0.0f ? 1.0f : 0.0f;
}
public Func<float> InverseBoolCurve(Blackboard context)
{
float Result()
{
var adjustedScore = GetAdjustedScore(context);
return InverseBoolCurve(adjustedScore);
}
return Result;
}
[Pure]
private static float LogisticCurve(float x, float slope, float exponent, float yOffset, float xOffset)
{
return MathHelper.Clamp(
exponent * (1 / (1 + (float) Math.Pow(Math.Log(1000) * slope, -1 * x + xOffset))) + yOffset, 0.0f, 1.0f);
}
public Func<float> LogisticCurve(Blackboard context, float slope, float exponent, float yOffset, float xOffset)
{
float Result()
{
var adjustedScore = GetAdjustedScore(context);
return LogisticCurve(adjustedScore, slope, exponent, yOffset, xOffset);
}
return Result;
}
[Pure]
private static float QuadraticCurve(float x, float slope, float exponent, float yOffset, float xOffset)
{
return MathHelper.Clamp(slope * (float) Math.Pow(x - xOffset, exponent) + yOffset, 0.0f, 1.0f);
}
public Func<float> QuadraticCurve(Blackboard context, float slope, float exponent, float yOffset, float xOffset)
{
float Result()
{
var adjustedScore = GetAdjustedScore(context);
return QuadraticCurve(adjustedScore, slope, exponent, yOffset, xOffset);
}
return Result;
}
/// <summary>
/// For any curves that are re-used across actions so you only need to update it once.
/// </summary>
/// <param name="context"></param>
/// <param name="preset"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public Func<float> PresetCurve(Blackboard context, PresetCurve preset)
{
float Result()
{
var adjustedScore = GetAdjustedScore(context);
switch (preset)
{
case Considerations.PresetCurve.Distance:
return QuadraticCurve(adjustedScore, -1.0f, 1.0f, 1.0f, 0.02f);
case Considerations.PresetCurve.Nutrition:
return QuadraticCurve(adjustedScore, 2.0f, 1.0f, -1.0f, -0.2f);
case Considerations.PresetCurve.TargetHealth:
return QuadraticCurve(adjustedScore, 1.0f, 0.4f, 0.0f, -0.02f);
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
}
return Result;
}
}
/// <summary>
/// Preset response curves for considerations
/// </summary>
public enum PresetCurve
{
Distance,
Nutrition,
TargetHealth,
}
}

View File

@@ -1,26 +0,0 @@
using Robust.Shared.Reflection;
namespace Content.Server.AI.Utility.Considerations
{
public sealed class ConsiderationsManager
{
private readonly Dictionary<Type, Consideration> _considerations = new();
public void Initialize()
{
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
var typeFactory = IoCManager.Resolve<IDynamicTypeFactory>();
foreach (var conType in reflectionManager.GetAllChildren(typeof(Consideration)))
{
var con = (Consideration) typeFactory.CreateInstance(conType);
_considerations.Add(conType, con);
}
}
public T Get<T>() where T : Consideration
{
return (T) _considerations[typeof(T)];
}
}
}

View File

@@ -1,47 +0,0 @@
using Content.Server.AI.Pathfinding.Accessible;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.Storage.Components;
using Content.Shared.Interaction;
using Robust.Shared.Containers;
namespace Content.Server.AI.Utility.Considerations.Containers
{
/// <summary>
/// Returns 1.0f if the item is freely accessible (e.g. in storage we can open, on ground, etc.)
/// This can be expensive so consider using this last for the considerations
/// </summary>
public sealed class TargetAccessibleCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var target = context.GetState<TargetEntityState>().GetValue();
if (!entMan.EntityExists(target))
{
return 0.0f;
}
if (target!.Value.TryGetContainer(out var container))
{
if (entMan.TryGetComponent(container.Owner, out EntityStorageComponent? storageComponent))
{
if (storageComponent.IsWeldedShut && !storageComponent.Open)
{
return 0.0f;
}
}
else
{
// If we're in a container (e.g. held or whatever) then we probably can't get it. Only exception
// Is a locker / crate
return 0.0f;
}
}
var owner = context.GetState<SelfState>().GetValue();
return EntitySystem.Get<AiReachableSystem>().CanAccess(owner, target.Value, SharedInteractionSystem.InteractionRange) ? 1.0f : 0.0f;
}
}
}

View File

@@ -1,9 +0,0 @@
using Content.Server.AI.WorldState;
namespace Content.Server.AI.Utility.Considerations
{
public sealed class DummyCon : Consideration
{
protected override float GetScore(Blackboard context) => 1.0f;
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.Hands.Components;
namespace Content.Server.AI.Utility.Considerations.Hands
{
public sealed class FreeHandCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var owner = context.GetState<SelfState>().GetValue();
if (!owner.IsValid() || !IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out SharedHandsComponent? handsComponent))
{
return 0.0f;
}
return (float) handsComponent.CountFreeHands() / handsComponent.Count;
}
}
}

View File

@@ -1,33 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Hands;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Shared.Item;
namespace Content.Server.AI.Utility.Considerations.Inventory
{
public sealed class CanPutTargetInInventoryCon : Consideration
{
protected override float GetScore(Blackboard context)
{
// First check if target in inventory already
// If not then check if we have a free hand
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<ItemComponent>(target))
{
return 0.0f;
}
foreach (var item in context.GetState<EnumerableInventoryState>().GetValue())
{
if (item == target)
{
return 1.0f;
}
}
return context.GetState<AnyFreeHandState>().GetValue() ? 1.0f : 0.0f;
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.AI.WorldState.States.Inventory;
using Content.Shared.Item;
namespace Content.Server.AI.Utility.Considerations.Inventory
{
public sealed class TargetInOurInventoryCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().HasComponent<ItemComponent>(target))
{
return 0.0f;
}
foreach (var item in context.GetState<EnumerableInventoryState>().GetValue())
{
if (item == target)
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
namespace Content.Server.AI.Utility.Considerations.Movement
{
public sealed class TargetDistanceCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var self = context.GetState<SelfState>().GetValue();
var entities = IoCManager.Resolve<IEntityManager>();
if (context.GetState<TargetEntityState>().GetValue() is not {Valid: true} target || entities.Deleted(target) ||
entities.GetComponent<TransformComponent>(target).GridUid != entities.GetComponent<TransformComponent>(self).GridUid)
{
return 0.0f;
}
// Anything further than 100 tiles gets clamped
return (entities.GetComponent<TransformComponent>(target).Coordinates.Position - entities.GetComponent<TransformComponent>(self).Coordinates.Position).Length / 100;
}
}
}

View File

@@ -1,32 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Nutrition.Components;
namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink
{
public sealed class DrinkValueCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null
|| IoCManager.Resolve<IEntityManager>().Deleted(target)
|| !EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(target.Value, DrinkComponent.DefaultSolutionName, out var drink))
{
return 0.0f;
}
var nutritionValue = 0;
foreach (var reagent in drink.Contents)
{
// TODO
nutritionValue += (reagent.Quantity * 30).Int();
}
return nutritionValue / 1000.0f;
}
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.Nutrition.Components;
namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink
{
public sealed class ThirstCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var owner = context.GetState<SelfState>().GetValue();
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(owner, out ThirstComponent? thirst))
{
return 0.0f;
}
return 1 - (thirst.CurrentThirst / thirst.ThirstThresholds[ThirstThreshold.OverHydrated]);
}
}
}

View File

@@ -1,31 +0,0 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Nutrition.Components;
namespace Content.Server.AI.Utility.Considerations.Nutrition.Food
{
public sealed class FoodValueCon : Consideration
{
protected override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent<FoodComponent?>(target.Value, out var foodComp)
|| !EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(target.Value, foodComp.SolutionName, out var food))
{
return 0.0f;
}
var nutritionValue = 0;
foreach (var reagent in food.Contents)
{
// TODO
nutritionValue += (reagent.Quantity * 30).Int();
}
return nutritionValue / 1000.0f;
}
}
}

Some files were not shown because too many files have changed in this diff Show More