Data-driven NPC behaviors (#3271)

* Data-driven NPC behaviors

* Nuked AiLogicProcessor
* BehaviorSets are now all stored in yaml (might try making actions also yaml someday)
* Added a test to validate all BehaviorSets
* Might also try pooling actions in the future to reduce allocs but that'll be way down the line (cough physics).

* Forgot to re-add sorting nothing suss

* Remove last references

* Proper vector2i serialization for tile atmos (#3266)

* update map files

* update submodule

Co-authored-by: cyclowns <cyclowns@protonmail.ch>

* Remove weird "S" jumpsuit from existence (#3267)

* Change character names to use datasets prototypes (#3259)

* Remove old name lists in .txts

* Fix tests

* LATEST MASTER TECHNOLOGY

* Converts AdminMenu to partially use XAML (#3231)

* Cleans up Hydroponics content. (#3025)

* Adds to IgnoredComponents.cs

* Jackboots

* Half Done

* Moved to diff PR

* Everything functional

* Fixed Sprays

* Nice

* Fixed

* Update submodule

* Fix tests

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>

* Stacked sprite visualizer (#3096)

* Add Stack Visualizer

* Add cigarette pack resources

Adds transparent layers for visualizing cigarettes

* Add Bag Open/Close Visualizer

So storage opened in inventory can have different icons when opened
or closed.

* Create a component that only enumerates single item

Used for creating stuff like matchbox, or cigarettes. As a bonus.
It will only update stack visualizer for that particullar item.

* Refactoring stuff

* Fix other usage of stack in Resources

* Add docs

* Apply suggestions from code review

Apply metalgearsloth suggestions

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Applied suggestions from metalgearsloth

* Changed SingleItemStorageComponent to StorageCounterComponent

Difference. New component doesn't spawn items, merely counts them.

* Refactored StackVisualizer

* Fix breakage with master

* Update Resources/Prototypes/Entities/Objects/Consumable/fancy.yml

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Update with MGS suggestions

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* ApcNet updating fix (#3078)

* GridPowerComponent

* ApcNet Powered update bugfix

* PowerTest fix

* Add GridPower to Saltern

* test fix

* Update canceling cleanup

* code cleanup

* nullable & code cleanup for test

* undo power test nullable

* Replaces GridPowerSystem with ApcNetSystem

* build fix

* Update Content.Server/GameObjects/EntitySystems/ApcNetSystem.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Change all XAML to use spacestation14.io namespace (#3277)

* fix pizzaboxes (#3291)

Co-authored-by: cyclowns <cyclowns@protonmail.ch>

* Spikes fix reopened (#3203)

* DoAfter, dead and stun check, DragDropOn

* Not ignored anymore

* Copied comment deleted

* Herbert's an ass

* Woops

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>

* Make component states dependant on the player getting them (#3280)

* Make component states dependant on the player getting them

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>

* Updated submodule to v0.3.7.

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: Acruid <shatter66@gmail.com>

* Hoe fix (#3296)

* Initial (#3297)

* Sort reagent dispenser entries (#3272)

* Sort reagent dispenser entries

Saves manually doing it.

* zumzum's suggestion

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>

* Made firelocks damageable & destructible (#3303)

* Move job priority enum parity test ot unit tests (#3300)

* Spill hand contents when dropping them in a fall (#3304)

* Spill hand contents when dropping them due to falling down

* Better approach

* cleanup

* grammar

* stupid

* PauseManager moved to Shared (#3288)

* Namespace changes for moving IPauseManager to shared.

* Namespace changes for moving ITimerManager from Timers to Timing.

* Rebase Fixes.

* Update engine submodule to v0.3.8

* Improves kick, teleport and ban menus (#3312)

* Fix the admin panel not showing the account name (#3322)

* Fix name serialization for secret stashes (#3301)

* Fix name serialization for secret stashes

* Fix old usages of secret part name

* Separate ghost warp message into two (#3310)

* Separate ghost warp message into two

* Remove redundant arguments

* Address reviews

* Move properties up

* Add health overlay and a command to toggle it (#3278)

* Add health overlay bar and a command to toggle it

* Remove empty line

* Content PR for YAML hot reloading (#3319)

* Content PR for YAML hot reloading

* Add CanAdminReloadPrototypes (host permission)

* IndexedPrototype fixes

* Update RobustToolbox

* Update RobustToolbox

* Add an unconspicuous, meaningless and in no way motivated by any external force XML doc to buckle component

* Update RobustToolbox

* Update submodule to v0.3.12.

* Removed unused using statements that prevented compiling.
Removed references to IIndexedPrototype that does not exist anymore in the engine.

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: mirrorcult <notzombiedude@gmail.com>
Co-authored-by: cyclowns <cyclowns@protonmail.ch>
Co-authored-by: Visne <39844191+Visne@users.noreply.github.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Leo <lzimann@users.noreply.github.com>
Co-authored-by: Swept <sweptwastaken@protonmail.com>
Co-authored-by: Ygg01 <y.laughing.man.y@gmail.com>
Co-authored-by: collinlunn <60152240+collinlunn@users.noreply.github.com>
Co-authored-by: komunre <49118681+komunre@users.noreply.github.com>
Co-authored-by: Acruid <shatter66@gmail.com>
Co-authored-by: Peptide90 <78795277+Peptide90@users.noreply.github.com>
Co-authored-by: Clyybber <darkmine956@gmail.com>
This commit is contained in:
metalgearsloth
2021-02-20 17:37:17 +11:00
committed by GitHub
parent 6f72f8f553
commit 4437fc7a1b
74 changed files with 548 additions and 707 deletions

View File

@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.Utility;
using NUnit.Framework;
using Robust.Server.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
namespace Content.IntegrationTests.Tests.AI
{
[TestFixture]
[TestOf(typeof(AiControllerTest))]
public class AiControllerTest : ContentIntegrationTest
{
[Test]
public async Task AiProcessorNamesValidTest()
{
var server = StartServerDummyTicker();
server.Assert(() =>
{
var entityManager = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
var mapManager = IoCManager.Resolve<IMapManager>();
mapManager.CreateMap(new MapId(1));
var grid = mapManager.CreateGrid(new MapId(1));
var processorNames = new List<string>();
// Verify they all have the required attribute
foreach (var processor in reflectionManager.GetAllChildren(typeof(AiLogicProcessor)))
{
var attrib = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute));
Assert.That(attrib != null, $"No AiLogicProcessorAttribute found on {processor.Name}");
processorNames.Add(attrib.SerializeName);
}
foreach (var entity in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
var comps = entity.Components;
if (!comps.ContainsKey("AiController")) continue;
var aiEntity = entityManager.SpawnEntity(entity.ID, grid.ToCoordinates());
var aiController = aiEntity.GetComponent<AiControllerComponent>();
Assert.That(processorNames.Contains(aiController.LogicName), $"Could not find valid processor named {aiController.LogicName} on entity {entity.ID}");
}
});
await server.WaitIdleAsync();
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Actions;
using NUnit.Framework;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using YamlDotNet.RepresentationModel;
namespace Content.IntegrationTests.Tests.AI
{
[TestFixture]
[TestOf(typeof(BehaviorSetPrototype))]
public class BehaviorSetsTest : ContentIntegrationTest
{
[Test]
public async Task TestBehaviorSets()
{
var options = new ServerIntegrationOptions();
var server = StartServerDummyTicker(options);
await server.WaitIdleAsync();
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.Components.TryGetValue("UtilityAI", out var npcNode)) continue;
var sets = npcNode["behaviorSets"];
foreach (var entry in (YamlSequenceNode) sets)
{
Assert.That(behaviorSets.ContainsKey(entry.ToString()), $"BehaviorSet {entry} in entity {entity.ID} not found");
}
}
});
}
}
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.AI namespace Content.IntegrationTests.Tests
{ {
[TestFixture] [TestFixture]
public sealed class SpriteTest : ContentIntegrationTest public sealed class SpriteTest : ContentIntegrationTest
@@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests.AI
var (client, server) = await StartConnectedServerClientPair(); var (client, server) = await StartConnectedServerClientPair();
await client.WaitIdleAsync(); await client.WaitIdleAsync();
await server.WaitIdleAsync(); await server.WaitIdleAsync();
var resc = client.ResolveDependency<IResourceCache>(); var resc = client.ResolveDependency<IResourceCache>();
var entityManager = client.ResolveDependency<IEntityManager>(); var entityManager = client.ResolveDependency<IEntityManager>();
var mapManager = client.ResolveDependency<IMapManager>(); var mapManager = client.ResolveDependency<IMapManager>();
@@ -58,4 +58,4 @@ namespace Content.IntegrationTests.Tests.AI
await client.WaitIdleAsync(); await client.WaitIdleAsync();
} }
} }
} }

View File

@@ -8,7 +8,7 @@ namespace Content.Server.AI.Operators
public bool HasShutdown { get; private set; } public bool HasShutdown { get; private set; }
/// <summary> /// <summary>
/// Called once when the AiLogicProcessor starts this action /// Called once when the NPC starts this action
/// </summary> /// </summary>
/// <returns>true if it hasn't started up previously</returns> /// <returns>true if it hasn't started up previously</returns>
public virtual bool Startup() public virtual bool Startup()
@@ -24,7 +24,7 @@ namespace Content.Server.AI.Operators
} }
/// <summary> /// <summary>
/// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails. /// Called once when the NPC is done with this action if the outcome is successful or fails.
/// </summary> /// </summary>
public virtual bool Shutdown(Outcome outcome) public virtual bool Shutdown(Outcome outcome)
{ {

View File

@@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
{ {
public sealed class EquipGloves : UtilityAction public sealed class EquipGloves : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public EquipGloves(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, _entity), new UseItemInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -14,25 +14,19 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
{ {
public sealed class PickUpGloves : UtilityAction public sealed class PickUpGloves : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpGloves(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)
{ {
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>(); var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();

View File

@@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head
{ {
public sealed class EquipHead : UtilityAction public sealed class EquipHead : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public EquipHead(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, _entity), new UseItemInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Head
{ {
public sealed class PickUpHead : UtilityAction public sealed class PickUpHead : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpHead(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
{ {
public sealed class EquipOuterClothing : UtilityAction public sealed class EquipOuterClothing : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public EquipOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, _entity), new UseItemInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
{ {
public sealed class PickUpOuterClothing : UtilityAction public sealed class PickUpOuterClothing : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -13,27 +13,21 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
{ {
public sealed class EquipShoes : UtilityAction public sealed class EquipShoes : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public EquipShoes(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseItemInInventoryOperator(Owner, _entity), new UseItemInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
{ {
public sealed class PickUpShoes : UtilityAction public sealed class PickUpShoes : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpShoes(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
{ {
public sealed class EquipMelee : UtilityAction public sealed class EquipMelee : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public EquipMelee(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity) new EquipEntityOperator(Owner, Target)
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<WeaponEntityState>().SetValue(_entity); context.GetState<WeaponEntityState>().SetValue(Target);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -21,13 +21,7 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
{ {
public sealed class MeleeWeaponAttackEntity : UtilityAction public sealed class MeleeWeaponAttackEntity : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public MeleeWeaponAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
@@ -35,26 +29,26 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
var equipped = context.GetState<EquippedEntityState>().GetValue(); var equipped = context.GetState<EquippedEntityState>().GetValue();
if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent)) if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
{ {
moveOperator = new MoveToEntityOperator(Owner, _entity, meleeWeaponComponent.Range - 0.01f); moveOperator = new MoveToEntityOperator(Owner, Target, meleeWeaponComponent.Range - 0.01f);
} }
else else
{ {
// TODO: Abort // TODO: Abort
moveOperator = new MoveToEntityOperator(Owner, _entity); moveOperator = new MoveToEntityOperator(Owner, Target);
} }
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
moveOperator, moveOperator,
new SwingMeleeWeaponOperator(Owner, _entity), new SwingMeleeWeaponOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(_entity); context.GetState<MoveTargetState>().SetValue(Target);
var equipped = context.GetState<EquippedEntityState>().GetValue(); var equipped = context.GetState<EquippedEntityState>().GetValue();
context.GetState<WeaponEntityState>().SetValue(equipped); context.GetState<WeaponEntityState>().SetValue(equipped);
} }

View File

@@ -15,24 +15,18 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
{ {
public sealed class PickUpMeleeWeapon : UtilityAction public sealed class PickUpMeleeWeapon : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpMeleeWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<WeaponEntityState>().SetValue(_entity); context.GetState<WeaponEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -19,40 +19,34 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
{ {
public sealed class UnarmedAttackEntity : UtilityAction public sealed class UnarmedAttackEntity : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public UnarmedAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
MoveToEntityOperator moveOperator; MoveToEntityOperator moveOperator;
if (Owner.TryGetComponent(out UnarmedCombatComponent unarmedCombatComponent)) if (Owner.TryGetComponent(out UnarmedCombatComponent unarmedCombatComponent))
{ {
moveOperator = new MoveToEntityOperator(Owner, _entity, unarmedCombatComponent.Range - 0.01f); moveOperator = new MoveToEntityOperator(Owner, Target, unarmedCombatComponent.Range - 0.01f);
} }
// I think it's possible for this to happen given planning is time-sliced? // I think it's possible for this to happen given planning is time-sliced?
// TODO: At this point we should abort // TODO: At this point we should abort
else else
{ {
moveOperator = new MoveToEntityOperator(Owner, _entity); moveOperator = new MoveToEntityOperator(Owner, Target);
} }
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
moveOperator, moveOperator,
new UnarmedCombatOperator(Owner, _entity), new UnarmedCombatOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
context.GetState<MoveTargetState>().SetValue(_entity); context.GetState<MoveTargetState>().SetValue(Target);
// Can just set ourselves as entity given unarmed just inherits from meleeweapon // Can just set ourselves as entity given unarmed just inherits from meleeweapon
context.GetState<WeaponEntityState>().SetValue(Owner); context.GetState<WeaponEntityState>().SetValue(Owner);
} }

View File

@@ -1,7 +1,17 @@
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.Actions namespace Content.Server.AI.Utility.Actions
{ {
public interface IAiUtility public interface IAiUtility
{ {
/// <summary>
/// NPC this action is attached to.
/// </summary>
IEntity Owner { get; set; }
/// <summary>
/// Highest possible score for this action.
/// </summary>
float Bonus { get; } float Bonus { get; }
} }
} }

View File

@@ -21,17 +21,15 @@ namespace Content.Server.AI.Utility.Actions.Idle
public sealed class CloseLastEntityStorage : UtilityAction public sealed class CloseLastEntityStorage : UtilityAction
{ {
public override float Bonus => IdleBonus + 0.01f; public override float Bonus => IdleBonus + 0.01f;
public CloseLastEntityStorage(IEntity owner) : base(owner) {}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
var lastStorage = context.GetState<LastOpenedStorageState>().GetValue(); var lastStorage = context.GetState<LastOpenedStorageState>().GetValue();
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new MoveToEntityOperator(Owner, lastStorage), new MoveToEntityOperator(Owner, lastStorage),
new CloseLastStorageOperator(Owner), new CloseLastStorageOperator(Owner),
}); });
} }

View File

@@ -23,8 +23,6 @@ namespace Content.Server.AI.Utility.Actions.Idle
public override bool CanOverride => false; public override bool CanOverride => false;
public override float Bonus => 1.0f; public override float Bonus => 1.0f;
public WanderAndWait(IEntity owner) : base(owner) {}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
var robustRandom = IoCManager.Resolve<IRobustRandom>(); var robustRandom = IoCManager.Resolve<IRobustRandom>();

View File

@@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
{ {
public sealed class PickUpDrink : UtilityAction public sealed class PickUpDrink : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpDrink(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
{ {
public sealed class UseDrinkInInventory : UtilityAction public sealed class UseDrinkInInventory : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public UseDrinkInInventory(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseDrinkInInventoryOperator(Owner, _entity), new UseDrinkInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -14,23 +14,17 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food
{ {
public sealed class PickUpFood : UtilityAction public sealed class PickUpFood : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public PickUpFood(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence; ActionOperators = new GoPickupEntitySequence(Owner, Target).Sequence;
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -15,27 +15,21 @@ namespace Content.Server.AI.Utility.Actions.Nutrition.Food
{ {
public sealed class UseFoodInInventory : UtilityAction public sealed class UseFoodInInventory : UtilityAction
{ {
private readonly IEntity _entity; public IEntity Target { get; set; }
public UseFoodInInventory(IEntity owner, IEntity entity, float weight) : base(owner)
{
_entity = entity;
Bonus = weight;
}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
ActionOperators = new Queue<AiOperator>(new AiOperator[] ActionOperators = new Queue<AiOperator>(new AiOperator[]
{ {
new EquipEntityOperator(Owner, _entity), new EquipEntityOperator(Owner, Target),
new UseFoodInInventoryOperator(Owner, _entity), new UseFoodInInventoryOperator(Owner, Target),
}); });
} }
protected override void UpdateBlackboard(Blackboard context) protected override void UpdateBlackboard(Blackboard context)
{ {
base.UpdateBlackboard(context); base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity); context.GetState<TargetEntityState>().SetValue(Target);
} }
protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context) protected override IReadOnlyCollection<Func<float>> GetConsiderations(Blackboard context)

View File

@@ -17,8 +17,6 @@ namespace Content.Server.AI.Utility.Actions.Test
{ {
public override bool CanOverride => false; public override bool CanOverride => false;
public MoveRightAndLeftTen(IEntity owner) : base(owner) {}
public override void SetupOperators(Blackboard context) public override void SetupOperators(Blackboard context)
{ {
var currentPosition = Owner.Transform.Coordinates; var currentPosition = Owner.Transform.Coordinates;

View File

@@ -24,7 +24,7 @@ namespace Content.Server.AI.Utility.Actions
/// Threshold doesn't necessarily mean we'll do an action at a higher threshold; /// 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 /// if it's really un-optimal (i.e. low score) then we'll also check lower tiers
/// </summary> /// </summary>
public virtual float Bonus { get; protected set; } = IdleBonus; 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. // 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 // These are just baselines to make mass-updates easier; actions can do whatever
@@ -36,7 +36,7 @@ namespace Content.Server.AI.Utility.Actions
public const float CombatBonus = 30.0f; public const float CombatBonus = 30.0f;
public const float DangerBonus = 50.0f; public const float DangerBonus = 50.0f;
protected IEntity Owner { get; } public IEntity Owner { get; set; }
/// <summary> /// <summary>
/// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible. /// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible.
@@ -58,10 +58,8 @@ namespace Content.Server.AI.Utility.Actions
/// <param name="context"></param> /// <param name="context"></param>
protected virtual void UpdateBlackboard(Blackboard context) {} protected virtual void UpdateBlackboard(Blackboard context) {}
protected UtilityAction(IEntity owner) // Needs to be able to be instantiated without args via typefactory.
{ public UtilityAction() {}
Owner = owner;
}
public virtual void Shutdown() {} public virtual void Shutdown() {}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.Utility.BehaviorSets;
using JetBrains.Annotations;
using Robust.Server.AI;
namespace Content.Server.AI.Utility.AiLogic
{
[AiLogicProcessor("Civilian")]
[UsedImplicitly]
public sealed class Civilian : UtilityAi
{
public override void Setup()
{
base.Setup();
AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false);
AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false);
AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false);
AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false);
SortActions();
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Utility.BehaviorSets;
using JetBrains.Annotations;
using Robust.Server.AI;
namespace Content.Server.AI.Utility.AiLogic
{
[AiLogicProcessor("Mimic")]
[UsedImplicitly]
public sealed class Mimic : UtilityAi
{
public override void Setup()
{
base.Setup();
AddBehaviorSet(new UnarmedAttackPlayersBehaviorSet(SelfEntity), false);
SortActions();
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Utility.BehaviorSets;
using JetBrains.Annotations;
using Robust.Server.AI;
namespace Content.Server.AI.Utility.AiLogic
{
[AiLogicProcessor("PathingDummy")]
[UsedImplicitly]
public sealed class PathingDummy : UtilityAi
{
public override void Setup()
{
base.Setup();
BehaviorSets.Add(typeof(PathingDummyBehaviorSet), new PathingDummyBehaviorSet(SelfEntity));
SortActions();
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.AI.Utility.BehaviorSets;
using JetBrains.Annotations;
using Robust.Server.AI;
namespace Content.Server.AI.Utility.AiLogic
{
[AiLogicProcessor("Spirate")]
[UsedImplicitly]
public sealed class Spirate : UtilityAi
{
public override void Setup()
{
base.Setup();
AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false);
AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false);
AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false);
AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false);
AddBehaviorSet(new SpirateBehaviorSet(SelfEntity), false);
SortActions();
}
}
}

View File

@@ -3,23 +3,28 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using Content.Server.AI.Operators; using Content.Server.AI.Operators;
using Content.Server.AI.Utility.Actions; using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.BehaviorSets;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States.Utility; using Content.Server.AI.WorldState.States.Utility;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.AI; using Content.Server.GameObjects.EntitySystems.AI;
using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer; using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
using Content.Server.GameObjects.EntitySystems.JobQueues; using Content.Server.GameObjects.EntitySystems.JobQueues;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Server.AI; using Content.Shared.GameObjects.Components.Movement;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Utility; using Robust.Shared.Serialization;
namespace Content.Server.AI.Utility.AiLogic namespace Content.Server.AI.Utility.AiLogic
{ {
public abstract class UtilityAi : AiLogicProcessor // TODO: Need to split out the IMover stuff for NPC to a generic one that can be used for hoomans as well.
[RegisterComponent]
[ComponentReference(typeof(AiControllerComponent)), ComponentReference(typeof(IMoverComponent))]
internal sealed class UtilityAi : AiControllerComponent
{ {
public override string Name => "UtilityAI";
// TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT) // TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT)
// Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity) // Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity)
private AiActionSystem _planner; private AiActionSystem _planner;
@@ -27,10 +32,11 @@ namespace Content.Server.AI.Utility.AiLogic
private Blackboard _blackboard; private Blackboard _blackboard;
/// <summary> /// <summary>
/// The sum of all BehaviorSets gives us what actions the AI can take /// The sum of all BehaviorSets gives us what actions the AI can take
/// </summary> /// </summary>
public Dictionary<Type, BehaviorSet> BehaviorSets { get; } = new(); public HashSet<string> BehaviorSets { get; } = new();
private readonly List<IAiUtility> _availableActions = new();
public List<IAiUtility> AvailableActions { get; set; } = new();
/// <summary> /// <summary>
/// The currently running action; most importantly are the operators. /// The currently running action; most importantly are the operators.
@@ -52,86 +58,39 @@ namespace Content.Server.AI.Utility.AiLogic
private CancellationTokenSource _actionCancellation; private CancellationTokenSource _actionCancellation;
/// <summary> /// <summary>
/// If we can't do anything then stop thinking; should probably use ActionBlocker instead /// If we can't do anything then stop thinking; should probably use ActionBlocker instead
/// </summary> /// </summary>
private bool _isDead = false; private bool _isDead;
// These 2 methods will be used eventually if / when we get a director AI public override void ExposeData(ObjectSerializer serializer)
public void AddBehaviorSet<T>(T behaviorSet, bool sort = true) where T : BehaviorSet
{ {
if (BehaviorSets.TryAdd(typeof(T), behaviorSet) && sort) base.ExposeData(serializer);
{ var bSets = serializer.ReadDataField("behaviorSets", new List<string>());
SortActions();
}
if (BehaviorSets.Count == 1 && !EntitySystem.Get<AiSystem>().IsAwake(this)) if (bSets.Count > 0)
{ {
IoCManager.Resolve<IEntityManager>() var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
.EventBus
.RaiseEvent(EventSource.Local, new SleepAiMessage(this, false));
}
}
public void RemoveBehaviorSet(Type behaviorSet) foreach (var bSet in bSets)
{
DebugTools.Assert(behaviorSet.IsAssignableFrom(typeof(BehaviorSet)));
if (BehaviorSets.ContainsKey(behaviorSet))
{
BehaviorSets.Remove(behaviorSet);
SortActions();
}
if (BehaviorSets.Count == 0)
{
IoCManager.Resolve<IEntityManager>()
.EventBus
.RaiseEvent(EventSource.Local, new SleepAiMessage(this, true));
}
}
/// <summary>
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
/// </summary>
protected void SortActions()
{
_availableActions.Clear();
foreach (var set in BehaviorSets.Values)
{
foreach (var action in set.Actions)
{ {
var found = false; behaviorManager.AddBehaviorSet(this, bSet, false);
for (var i = 0; i < _availableActions.Count; i++)
{
if (_availableActions[i].Bonus < action.Bonus)
{
_availableActions.Insert(i, action);
found = true;
break;
}
}
if (!found)
{
_availableActions.Add(action);
}
} }
behaviorManager.RebuildActions(this);
} }
_availableActions.Reverse();
} }
public override void Setup() public override void Initialize()
{ {
base.Setup(); base.Initialize();
_planCooldownRemaining = PlanCooldown; _planCooldownRemaining = PlanCooldown;
_blackboard = new Blackboard(SelfEntity); _blackboard = new Blackboard(Owner);
_planner = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AiActionSystem>(); _planner = EntitySystem.Get<AiActionSystem>();
} }
public override void Shutdown() public override void OnRemove()
{ {
base.OnRemove();
var currentOp = CurrentAction?.ActionOperators.Peek(); var currentOp = CurrentAction?.ActionOperators.Peek();
currentOp?.Shutdown(Outcome.Failed); currentOp?.Shutdown(Outcome.Failed);
CurrentAction?.Shutdown(); CurrentAction?.Shutdown();
@@ -190,6 +149,8 @@ namespace Content.Server.AI.Utility.AiLogic
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime);
// If we asked for a new action we don't want to dump the existing one. // If we asked for a new action we don't want to dump the existing one.
if (_actionRequest != null) if (_actionRequest != null)
{ {
@@ -210,7 +171,7 @@ namespace Content.Server.AI.Utility.AiLogic
{ {
_planCooldownRemaining = PlanCooldown; _planCooldownRemaining = PlanCooldown;
_actionCancellation = new CancellationTokenSource(); _actionCancellation = new CancellationTokenSource();
_actionRequest = _planner.RequestAction(new AiActionRequest(SelfEntity.Uid, _blackboard, _availableActions), _actionCancellation); _actionRequest = _planner.RequestAction(new AiActionRequest(Owner.Uid, _blackboard, AvailableActions), _actionCancellation);
return; return;
} }

View File

@@ -1,19 +0,0 @@
using Content.Server.AI.Utility.BehaviorSets;
using JetBrains.Annotations;
using Robust.Server.AI;
namespace Content.Server.AI.Utility.AiLogic
{
[AiLogicProcessor("Xeno")]
[UsedImplicitly]
public sealed class Xeno : UtilityAi
{
public override void Setup()
{
base.Setup();
AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false);
AddBehaviorSet(new UnarmedAttackPlayersBehaviorSet(SelfEntity), false);
SortActions();
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel;
namespace Content.Server.AI.Utility
{
[Prototype("behaviorSet")]
public class BehaviorSetPrototype : IPrototype
{
/// <summary>
/// Name of the BehaviorSet.
/// </summary>
public string ID { get; private set; }
/// <summary>
/// Actions that this BehaviorSet grants to the entity.
/// </summary>
public IReadOnlyList<string> Actions { get; private set; }
public void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(this, x => x.ID, "id", string.Empty);
serializer.DataField(this, x => x.Actions, "actions", new List<string>());
}
}
}

View File

@@ -1,21 +0,0 @@
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
/// <summary>
/// AKA DecisionMaker in IAUS. Just a group of actions that can be dynamically added or taken away from an AI.
/// </summary>
public abstract class BehaviorSet
{
protected IEntity Owner;
public BehaviorSet(IEntity owner)
{
Owner = owner;
}
public IEnumerable<IAiUtility> Actions { get; protected set; }
}
}

View File

@@ -1,27 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves;
using Content.Server.AI.Utility.ExpandableActions.Clothing.Head;
using Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing;
using Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class ClothingBehaviorSet : BehaviorSet
{
public ClothingBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new EquipAnyHeadExp(),
new EquipAnyOuterClothingExp(),
new EquipAnyGlovesExp(),
new EquipAnyShoesExp(),
new PickUpAnyNearbyHeadExp(),
new PickUpAnyNearbyOuterClothingExp(),
new PickUpAnyNearbyGlovesExp(),
new PickUpAnyNearbyShoesExp(),
};
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions.Nutrition;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class HungerBehaviorSet : BehaviorSet
{
public HungerBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new PickUpNearbyFoodExp(),
new UseFoodInInventoryExp(),
};
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Idle;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public class IdleBehaviorSet : BehaviorSet
{
public IdleBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new CloseLastEntityStorage(Owner),
new WanderAndWait(Owner),
};
}
}
}

View File

@@ -1,17 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Test;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class PathingDummyBehaviorSet : BehaviorSet
{
public PathingDummyBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new MoveRightAndLeftTen(owner),
};
}
}
}

View File

@@ -1,21 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions.Combat.Melee;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class SpirateBehaviorSet : BehaviorSet
{
public SpirateBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
// TODO: Reload Ballistic
// TODO: Ideally long-term we should just store the weapons in backpack
new EquipMeleeExp(),
new PickUpMeleeWeaponExp(),
new MeleeAttackNearbyExp(),
};
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions.Nutrition;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class ThirstBehaviorSet : BehaviorSet
{
public ThirstBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new PickUpNearbyDrinkExp(),
new UseDrinkInInventoryExp(),
};
}
}
}

View File

@@ -1,17 +0,0 @@
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.ExpandableActions.Combat.Melee;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets
{
public sealed class UnarmedAttackPlayersBehaviorSet : BehaviorSet
{
public UnarmedAttackPlayersBehaviorSet(IEntity owner) : base(owner)
{
Actions = new IAiUtility[]
{
new UnarmedAttackNearbyPlayerExp(),
};
}
}
}

View File

@@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0)
{ {
yield return new EquipGloves(owner, entity, Bonus); yield return new EquipGloves {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -38,7 +38,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0)
{ {
yield return new PickUpGloves(owner, entity, Bonus); yield return new PickUpGloves {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0)
{ {
yield return new EquipHead(owner, entity, Bonus); yield return new EquipHead {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -38,7 +38,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0)
{ {
yield return new PickUpHead(owner, entity, Bonus); yield return new PickUpHead() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0)
{ {
yield return new EquipOuterClothing(owner, entity, Bonus); yield return new EquipOuterClothing() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0)
{ {
yield return new PickUpOuterClothing(owner, entity, Bonus); yield return new PickUpOuterClothing() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -40,7 +40,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0)
{ {
yield return new EquipShoes(owner, entity, Bonus); yield return new EquipShoes() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
if (entity.TryGetComponent(out ClothingComponent clothing) && if (entity.TryGetComponent(out ClothingComponent clothing) &&
(clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0) (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0)
{ {
yield return new PickUpShoes(owner, entity, Bonus); yield return new PickUpShoes {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -37,8 +37,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
{ {
continue; continue;
} }
yield return new EquipMelee(owner, entity, Bonus); yield return new EquipMelee() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
foreach (var target in EntitySystem.Get<AiFactionTagSystem>() foreach (var target in EntitySystem.Get<AiFactionTagSystem>()
.GetNearbyHostiles(owner, controller.VisionRadius)) .GetNearbyHostiles(owner, controller.VisionRadius))
{ {
yield return new MeleeWeaponAttackEntity(owner, target, Bonus); yield return new MeleeWeaponAttackEntity() {Owner = owner, Target = target, Bonus = Bonus};
} }
} }
} }

View File

@@ -35,7 +35,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
foreach (var entity in context.GetState<NearbyMeleeWeapons>().GetValue()) foreach (var entity in context.GetState<NearbyMeleeWeapons>().GetValue())
{ {
yield return new PickUpMeleeWeapon(owner, entity, Bonus); yield return new PickUpMeleeWeapon() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -13,7 +13,7 @@ using Robust.Shared.IoC;
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
{ {
public sealed class UnarmedAttackNearbyPlayerExp : ExpandableUtilityAction public sealed class UnarmedAttackNearbyHostilesExp : ExpandableUtilityAction
{ {
public override float Bonus => UtilityAction.CombatBonus; public override float Bonus => UtilityAction.CombatBonus;
@@ -39,7 +39,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
foreach (var target in EntitySystem.Get<AiFactionTagSystem>() foreach (var target in EntitySystem.Get<AiFactionTagSystem>()
.GetNearbyHostiles(owner, controller.VisionRadius)) .GetNearbyHostiles(owner, controller.VisionRadius))
{ {
yield return new UnarmedAttackEntity(owner, target, Bonus); yield return new UnarmedAttackEntity() {Owner = owner, Target = target, Bonus = Bonus};
} }
} }
} }

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.AI.Utility.Actions; using Content.Server.AI.Utility.Actions;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Robust.Shared.GameObjects;
namespace Content.Server.AI.Utility.ExpandableActions namespace Content.Server.AI.Utility.ExpandableActions
{ {
@@ -11,6 +12,8 @@ namespace Content.Server.AI.Utility.ExpandableActions
/// </summary> /// </summary>
public abstract class ExpandableUtilityAction : IAiUtility public abstract class ExpandableUtilityAction : IAiUtility
{ {
public IEntity Owner { get; set; }
public abstract float Bonus { get; } public abstract float Bonus { get; }
/// <summary> /// <summary>

View File

@@ -32,7 +32,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition
foreach (var entity in context.GetState<NearbyDrinkState>().GetValue()) foreach (var entity in context.GetState<NearbyDrinkState>().GetValue())
{ {
yield return new PickUpDrink(owner, entity, Bonus); yield return new PickUpDrink() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -32,7 +32,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition
foreach (var entity in context.GetState<NearbyFoodState>().GetValue()) foreach (var entity in context.GetState<NearbyFoodState>().GetValue())
{ {
yield return new PickUpFood(owner, entity, Bonus); yield return new PickUpFood {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -15,7 +15,7 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition
public sealed class UseDrinkInInventoryExp : ExpandableUtilityAction public sealed class UseDrinkInInventoryExp : ExpandableUtilityAction
{ {
public override float Bonus => UtilityAction.NeedsBonus; public override float Bonus => UtilityAction.NeedsBonus;
protected override IEnumerable<Func<float>> GetCommonConsiderations(Blackboard context) protected override IEnumerable<Func<float>> GetCommonConsiderations(Blackboard context)
{ {
var considerationsManager = IoCManager.Resolve<ConsiderationsManager>(); var considerationsManager = IoCManager.Resolve<ConsiderationsManager>();
@@ -35,8 +35,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition
{ {
continue; continue;
} }
yield return new UseDrinkInInventory(owner, entity, Bonus); yield return new UseDrinkInInventory {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -35,8 +35,8 @@ namespace Content.Server.AI.Utility.ExpandableActions.Nutrition
{ {
continue; continue;
} }
yield return new UseFoodInInventory(owner, entity, Bonus); yield return new UseFoodInInventory() {Owner = owner, Target = entity, Bonus = Bonus};
} }
} }
} }

View File

@@ -0,0 +1,172 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.EntitySystems.AI;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
namespace Content.Server.AI.Utility
{
internal interface INpcBehaviorManager
{
void Initialize();
void AddBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true);
void RemoveBehaviorSet(UtilityAi npc, string behaviorSet, bool rebuild = true);
void RebuildActions(UtilityAi npc);
}
/// <summary>
/// Handles BehaviorSets and adding / removing behaviors to NPCs
/// </summary>
internal sealed class NpcBehaviorManager : INpcBehaviorManager
{
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly NpcActionComparer _comparer = new();
private Dictionary<string, List<Type>> _behaviorSets = new();
public void Initialize()
{
IoCManager.InjectDependencies(this);
var protoManager = IoCManager.Resolve<IPrototypeManager>();
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
foreach (var bSet in protoManager.EnumeratePrototypes<BehaviorSetPrototype>())
{
var actions = new List<Type>();
foreach (var act in bSet.Actions)
{
if (!reflectionManager.TryLooseGetType(act, out var parsedType) ||
!typeof(IAiUtility).IsAssignableFrom(parsedType))
{
Logger.Error($"Unable to parse AI action for {act}");
}
else
{
actions.Add(parsedType);
}
}
_behaviorSets[bSet.ID] = actions;
}
}
/// <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(UtilityAi npc, string behaviorSet, bool rebuild = true)
{
if (!_behaviorSets.ContainsKey(behaviorSet))
{
Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} but no such BehaviorSet found!");
return;
}
if (!npc.BehaviorSets.Add(behaviorSet))
{
Logger.Error($"Tried to add BehaviorSet {behaviorSet} to {npc} which already has the BehaviorSet!");
return;
}
if (rebuild)
RebuildActions(npc);
if (npc.BehaviorSets.Count == 1 && !EntitySystem.Get<AiSystem>().IsAwake(npc))
{
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, false));
}
}
/// <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(UtilityAi 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 && EntitySystem.Get<AiSystem>().IsAwake(npc))
{
_entityManager.EventBus.RaiseEvent(EventSource.Local, new SleepAiMessage(npc, true));
}
}
/// <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(UtilityAi 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(UtilityAi npc)
{
npc.AvailableActions.Sort(_comparer);
}
private 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

@@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility
return null; return null;
} }
if (aiControllerComponent.Processor is UtilityAi utilityAi) if (aiControllerComponent is UtilityAi utilityAi)
{ {
return utilityAi.Blackboard; return utilityAi.Blackboard;
} }
@@ -22,4 +22,4 @@ namespace Content.Server.AI.Utility
return null; return null;
} }
} }
} }

View File

@@ -1,5 +1,7 @@
#nullable enable #nullable enable
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.AI; using Content.Server.GameObjects.EntitySystems.AI;
using Content.Shared.Administration; using Content.Shared.Administration;
@@ -15,41 +17,48 @@ namespace Content.Server.Commands.AI
{ {
public string Command => "addai"; public string Command => "addai";
public string Description => "Add an ai component with a given processor to an entity."; public string Description => "Add an ai component with a given processor to an entity.";
public string Help => "Usage: addai <processorId> <entityId>" public string Help => "Usage: addai <entityId> <behaviorSet1> <behaviorSet2>..."
+ "\n processorId: Class that inherits AiLogicProcessor and has an AiLogicProcessor attribute." + "\n entityID: Uid of entity to add the AiControllerComponent to. Open its VV menu to find this."
+ "\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) public void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
if(args.Length != 2) if(args.Length < 1)
{ {
shell.WriteLine("Wrong number of args."); shell.WriteLine("Wrong number of args.");
return; return;
} }
var processorId = args[0]; var entId = new EntityUid(int.Parse(args[0]));
var entId = new EntityUid(int.Parse(args[1]));
var ent = IoCManager.Resolve<IEntityManager>().GetEntity(entId);
var aiSystem = EntitySystem.Get<AiSystem>();
if (!aiSystem.ProcessorTypeExists(processorId)) if (!IoCManager.Resolve<IEntityManager>().TryGetEntity(entId, out var ent))
{ {
shell.WriteLine("Invalid processor type. Processor must inherit AiLogicProcessor and have an AiLogicProcessor attribute."); shell.WriteLine($"Unable to find entity with uid {entId}");
return; return;
} }
if (ent.HasComponent<AiControllerComponent>()) if (ent.HasComponent<AiControllerComponent>())
{ {
shell.WriteLine("Entity already has an AI component."); shell.WriteLine("Entity already has an AI component.");
return; return;
} }
// TODO: IMover refffaaccctttooorrr
if (ent.HasComponent<IMoverComponent>()) if (ent.HasComponent<IMoverComponent>())
{ {
ent.RemoveComponent<IMoverComponent>(); ent.RemoveComponent<IMoverComponent>();
} }
var comp = ent.AddComponent<AiControllerComponent>(); var comp = ent.AddComponent<UtilityAi>();
comp.LogicName = processorId; var behaviorManager = IoCManager.Resolve<INpcBehaviorManager>();
for (var i = 1; i < args.Length; i++)
{
var bSet = args[i];
behaviorManager.AddBehaviorSet(comp, bSet, false);
}
behaviorManager.RebuildActions(comp);
shell.WriteLine("AI component added."); shell.WriteLine("AI component added.");
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.Database; using Content.Server.Database;
@@ -92,6 +93,7 @@ namespace Content.Server
IoCManager.Resolve<ConsiderationsManager>().Initialize(); IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize(); IoCManager.Resolve<IPDAUplinkManager>().Initialize();
IoCManager.Resolve<IAdminManager>().Initialize(); IoCManager.Resolve<IAdminManager>().Initialize();
IoCManager.Resolve<INpcBehaviorManager>().Initialize();
_euiManager.Initialize(); _euiManager.Initialize();
} }

View File

@@ -17,27 +17,10 @@ namespace Content.Server.GameObjects.Components.Movement
[RegisterComponent, ComponentReference(typeof(IMoverComponent))] [RegisterComponent, ComponentReference(typeof(IMoverComponent))]
public class AiControllerComponent : Component, IMoverComponent public class AiControllerComponent : Component, IMoverComponent
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTicker _gameTicker = default!;
private string? _logicName;
private float _visionRadius; private float _visionRadius;
public override string Name => "AiController"; public override string Name => "AiController";
[ViewVariables(VVAccess.ReadWrite)]
public string? LogicName
{
get => _logicName;
set
{
_logicName = value;
Processor = null!;
}
}
public UtilityAi? Processor { get; set; }
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public string? StartingGearPrototype { get; set; } public string? StartingGearPrototype { get; set; }
@@ -55,8 +38,6 @@ namespace Content.Server.GameObjects.Components.Movement
// This component requires a physics component. // This component requires a physics component.
Owner.EnsureComponent<PhysicsComponent>(); Owner.EnsureComponent<PhysicsComponent>();
EntitySystem.Get<AiSystem>().ProcessorInitialize(this);
} }
protected override void Startup() protected override void Startup()
@@ -65,10 +46,12 @@ namespace Content.Server.GameObjects.Components.Movement
if (StartingGearPrototype != null) if (StartingGearPrototype != null)
{ {
var startingGear = _prototypeManager.Index<StartingGearPrototype>(StartingGearPrototype); var gameTicker = IoCManager.Resolve<IGameTicker>();
_gameTicker.EquipStartingGear(Owner, startingGear, null); var protoManager = IoCManager.Resolve<IPrototypeManager>();
}
var startingGear = protoManager.Index<StartingGearPrototype>(StartingGearPrototype);
gameTicker.EquipStartingGear(Owner, startingGear, null);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -76,7 +59,6 @@ namespace Content.Server.GameObjects.Components.Movement
{ {
base.ExposeData(serializer); base.ExposeData(serializer);
serializer.DataField(ref _logicName, "logic", null);
serializer.DataReadWriteFunction( serializer.DataReadWriteFunction(
"startingGear", "startingGear",
null, null,
@@ -85,12 +67,6 @@ namespace Content.Server.GameObjects.Components.Movement
serializer.DataField(ref _visionRadius, "vision", 8.0f); serializer.DataField(ref _visionRadius, "vision", 8.0f);
} }
protected override void Shutdown()
{
base.Shutdown();
Processor?.Shutdown();
}
/// <summary> /// <summary>
/// Movement speed (m/s) that the entity walks, after modifiers /// Movement speed (m/s) that the entity walks, after modifiers
/// </summary> /// </summary>
@@ -155,5 +131,7 @@ namespace Content.Server.GameObjects.Components.Movement
public void SetVelocityDirection(Direction direction, ushort subTick, bool enabled) { } public void SetVelocityDirection(Direction direction, ushort subTick, bool enabled) { }
public void SetSprinting(ushort subTick, bool walking) { } public void SetSprinting(ushort subTick, bool walking) { }
public virtual void Update(float frameTime) {}
} }
} }

View File

@@ -1,41 +1,41 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.AiLogic; using Content.Server.AI.Utility.AiLogic;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared; using Content.Shared;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.AI;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection; using Robust.Shared.Reflection;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.GameObjects.EntitySystems.AI namespace Content.Server.GameObjects.EntitySystems.AI
{ {
/// <summary>
/// Handles NPCs running every tick.
/// </summary>
[UsedImplicitly] [UsedImplicitly]
internal class AiSystem : EntitySystem internal class AiSystem : EntitySystem
{ {
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IDynamicTypeFactory _typeFactory = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
private readonly Dictionary<string, Type> _processorTypes = new();
/// <summary> /// <summary>
/// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary. /// To avoid iterating over dead AI continuously they can wake and sleep themselves when necessary.
/// </summary> /// </summary>
private readonly HashSet<AiLogicProcessor> _awakeAi = new(); private readonly HashSet<AiControllerComponent> _awakeAi = new();
// To avoid modifying awakeAi while iterating over it. // To avoid modifying awakeAi while iterating over it.
private readonly List<SleepAiMessage> _queuedSleepMessages = new(); private readonly List<SleepAiMessage> _queuedSleepMessages = new();
private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new(); private readonly List<MobStateChangedMessage> _queuedMobStateMessages = new();
public bool IsAwake(AiLogicProcessor processor) => _awakeAi.Contains(processor); public bool IsAwake(AiControllerComponent npc) => _awakeAi.Contains(npc);
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
@@ -43,15 +43,6 @@ namespace Content.Server.GameObjects.EntitySystems.AI
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep); SubscribeLocalEvent<SleepAiMessage>(HandleAiSleep);
SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged); SubscribeLocalEvent<MobStateChangedMessage>(MobStateChanged);
var processors = _reflectionManager.GetAllChildren<UtilityAi>();
foreach (var processor in processors)
{
var att = (AiLogicProcessorAttribute) Attribute.GetCustomAttribute(processor, typeof(AiLogicProcessorAttribute))!;
// Tests should pick this up
DebugTools.AssertNotNull(att);
_processorTypes.Add(att.SerializeName, processor);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -63,13 +54,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI
foreach (var message in _queuedMobStateMessages) foreach (var message in _queuedMobStateMessages)
{ {
// TODO: Need to generecise this but that will be part of a larger cleanup later anyway.
if (message.Entity.Deleted || if (message.Entity.Deleted ||
!message.Entity.TryGetComponent(out AiControllerComponent? controller)) !message.Entity.TryGetComponent(out UtilityAi? controller))
{ {
continue; continue;
} }
controller.Processor?.MobStateChanged(message); controller.MobStateChanged(message);
} }
_queuedMobStateMessages.Clear(); _queuedMobStateMessages.Clear();
@@ -79,14 +71,14 @@ namespace Content.Server.GameObjects.EntitySystems.AI
switch (message.Sleep) switch (message.Sleep)
{ {
case true: case true:
if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Processor)) if (_awakeAi.Count == cvarMaxUpdates && _awakeAi.Contains(message.Component))
{ {
Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}"); Logger.Warning($"Under AI limit again: {_awakeAi.Count - 1} / {cvarMaxUpdates}");
} }
_awakeAi.Remove(message.Processor); _awakeAi.Remove(message.Component);
break; break;
case false: case false:
_awakeAi.Add(message.Processor); _awakeAi.Add(message.Component);
if (_awakeAi.Count > cvarMaxUpdates) if (_awakeAi.Count > cvarMaxUpdates)
{ {
@@ -97,16 +89,17 @@ namespace Content.Server.GameObjects.EntitySystems.AI
} }
_queuedSleepMessages.Clear(); _queuedSleepMessages.Clear();
var toRemove = new List<AiLogicProcessor>(); var toRemove = new List<AiControllerComponent>();
var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates); var maxUpdates = Math.Min(_awakeAi.Count, cvarMaxUpdates);
var count = 0; var count = 0;
foreach (var processor in _awakeAi) foreach (var npc in _awakeAi)
{ {
if (processor.SelfEntity.Deleted || if (npc.Paused) continue;
!processor.SelfEntity.HasComponent<AiControllerComponent>())
if (npc.Deleted)
{ {
toRemove.Add(processor); toRemove.Add(npc);
continue; continue;
} }
@@ -115,7 +108,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI
break; break;
} }
processor.Update(frameTime); npc.Update(frameTime);
count++; count++;
} }
@@ -139,32 +132,5 @@ namespace Content.Server.GameObjects.EntitySystems.AI
_queuedMobStateMessages.Add(message); _queuedMobStateMessages.Add(message);
} }
/// <summary>
/// Will start up the controller's processor if not already done so.
/// Also add them to the awakeAi for updates.
/// </summary>
/// <param name="controller"></param>
public void ProcessorInitialize(AiControllerComponent controller)
{
if (controller.Processor != null || controller.LogicName == null) return;
controller.Processor = CreateProcessor(controller.LogicName);
controller.Processor.SelfEntity = controller.Owner;
controller.Processor.Setup();
_awakeAi.Add(controller.Processor);
}
private UtilityAi CreateProcessor(string name)
{
if (_processorTypes.TryGetValue(name, out var type))
{
return (UtilityAi)_typeFactory.CreateInstance(type);
}
// processor needs to inherit AiLogicProcessor, and needs an AiLogicProcessorAttribute to define the YAML name
throw new ArgumentException($"Processor type {name} could not be found.", nameof(name));
}
public bool ProcessorTypeExists(string name) => _processorTypes.ContainsKey(name);
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; #nullable enable
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -26,7 +27,7 @@ namespace Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Accessible
public static ReachableArgs GetArgs(IEntity entity) public static ReachableArgs GetArgs(IEntity entity)
{ {
var collisionMask = 0; var collisionMask = 0;
if (entity.TryGetComponent(out IPhysicsComponent physics)) if (entity.TryGetComponent(out IPhysicsComponent? physics))
{ {
collisionMask = physics.CollisionMask; collisionMask = physics.CollisionMask;
} }

View File

@@ -1,4 +1,4 @@
using Robust.Server.AI; using Content.Server.GameObjects.Components.Movement;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems.AI namespace Content.Server.GameObjects.EntitySystems.AI
@@ -13,12 +13,12 @@ namespace Content.Server.GameObjects.EntitySystems.AI
/// Sleep or awake. /// Sleep or awake.
/// </summary> /// </summary>
public bool Sleep { get; } public bool Sleep { get; }
public AiLogicProcessor Processor { get; } public AiControllerComponent Component { get; }
public SleepAiMessage(AiLogicProcessor processor, bool sleep) public SleepAiMessage(AiControllerComponent component, bool sleep)
{ {
Processor = processor; Component = component;
Sleep = sleep; Sleep = sleep;
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.AI.Utility;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.Chat; using Content.Server.Chat;
@@ -60,6 +61,7 @@ namespace Content.Server
IoCManager.Register<EuiManager, EuiManager>(); IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IHolidayManager, HolidayManager>(); IoCManager.Register<IHolidayManager, HolidayManager>();
IoCManager.Register<IVoteManager, VoteManager>(); IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<INpcBehaviorManager, NpcBehaviorManager>();
} }
} }
} }

View File

@@ -6,8 +6,10 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Xeno behaviorSets:
- Idle
- UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag
factions: factions:
- SimpleHostile - SimpleHostile

View File

@@ -7,5 +7,6 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: PathingDummy behaviorSets:
- PathingDummy

View File

@@ -7,8 +7,12 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Civilian behaviorSets:
- Clothing
- Hunger
- Thirst
- Idle
startingGear: AssistantGear startingGear: AssistantGear
- type: AiFactionTag - type: AiFactionTag
factions: factions:
@@ -22,5 +26,10 @@
description: Yarr! description: Yarr!
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Spirate behaviorSets:
- Clothing
- Hunger
- Thirst
- Idle
- Spirate

View File

@@ -7,8 +7,9 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Mimic behaviorSets:
- UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag
factions: factions:
- SimpleHostile - SimpleHostile

View File

@@ -5,8 +5,12 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Civilian behaviorSets:
- Clothing
- Hunger
- Thirst
- Idle
- type: AiFactionTag - type: AiFactionTag
factions: factions:
- SimpleNeutral - SimpleNeutral

View File

@@ -7,8 +7,10 @@
drawdepth: Mobs drawdepth: Mobs
suffix: AI suffix: AI
components: components:
- type: AiController - type: UtilityAI
logic: Xeno behaviorSets:
- Idle
- UnarmedAttackHostiles
- type: AiFactionTag - type: AiFactionTag
factions: factions:
- Xeno - Xeno

View File

@@ -0,0 +1,46 @@
- type: behaviorSet
id: Clothing
actions:
- EquipAnyHeadExp
- EquipAnyOuterClothingExp
- EquipAnyGlovesExp
- EquipAnyShoesExp
- PickUpAnyNearbyHeadExp
- PickUpAnyNearbyOuterClothingExp
- PickUpAnyNearbyGlovesExp
- PickUpAnyNearbyShoesExp
- type: behaviorSet
id: Hunger
actions:
- PickUpNearbyFoodExp
- UseFoodInInventoryExp
- type: behaviorSet
id: Idle
actions:
- CloseLastEntityStorage
- WanderAndWait
- type: behaviorSet
id: PathingDummy
actions:
- MoveRightAndLeftTen
- type: behaviorSet
id: Spirate
actions:
- EquipMeleeExp
- PickUpMeleeWeaponExp
- MeleeAttackNearbyExp
- type: behaviorSet
id: Thirst
actions:
- PickUpNearbyDrinkExp
- UseDrinkInInventoryExp
- type: behaviorSet
id: UnarmedAttackHostiles
actions:
- UnarmedAttackNearbyHostilesExp