Sentry turrets - Part 3: Turret AI (#35058)

* Initial commit

* Updated Access/command.yml

* Fix for Access/AccessLevelPrototype.cs

* Added silicon access levels to admin items

* Included self-recharging battery changes

* Revert "Included self-recharging battery changes"

* Addressed reviewers comments

* Additional reviewer comments
This commit is contained in:
chromiumboy
2025-03-01 11:42:33 -06:00
committed by GitHub
parent e8c812f90f
commit 10c868011e
23 changed files with 332 additions and 10 deletions

View File

@@ -78,7 +78,12 @@ public sealed class IdCardSystem : SharedIdCardSystem
}
// Give them a wonderful new access to compensate for everything
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
if (ids.Length == 0)
return;
var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);

View File

@@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
public bool TargetInLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
// ReSharper disable once InconsistentNaming
public bool UseOpaqueForLOSChecks = false;
/// <summary>
/// Delay after target is in LOS before we start shooting.
/// </summary>

View File

@@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
/// Is this NPC currently planning?
/// </summary>
[ViewVariables] public bool Planning => PlanningJob != null;
/// <summary>
/// Determines whether plans should be made / updated for this entity
/// </summary>
[DataField]
public bool Enabled = true;
}

View File

@@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
component.PlanningJob = null;
}
/// <summary>
/// Enable / disable the hierarchical task network of an entity
/// </summary>
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
/// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
// ReSharper disable once InconsistentNaming
[PublicAPI]
public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
{
if (ent.Comp.Enabled == state)
return;
ent.Comp.Enabled = state;
ent.Comp.PlanAccumulator = planCooldown;
ent.Comp.PlanningToken?.Cancel();
ent.Comp.PlanningToken = null;
if (ent.Comp.Plan != null)
{
var currentOperator = ent.Comp.Plan.CurrentOperator;
ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
ShutdownPlan(ent.Comp);
ent.Comp.Plan = null;
}
if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
RequestPlan(ent.Comp);
}
/// <summary>
/// Forces the NPC to replan.
/// </summary>
@@ -153,6 +186,9 @@ public sealed class HTNSystem : EntitySystem
if (count >= maxUpdates)
break;
if (!comp.Enabled)
continue;
if (comp.PlanningJob != null)
{
if (comp.PlanningJob.Exception != null)

View File

@@ -1,4 +1,5 @@
using Content.Server.Interaction;
using Content.Shared.Physics;
namespace Content.Server.NPC.HTN.Preconditions;
@@ -13,6 +14,9 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
[DataField("rangeKey")]
public string RangeKey = "RangeKey";
[DataField("opaqueKey")]
public bool UseOpaqueForLOSChecksKey = true;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
@@ -27,7 +31,8 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
return false;
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
return _interaction.InRangeUnobstructed(owner, target, range);
return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
}
}

View File

@@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
[DataField("requireLOS")]
public bool RequireLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[DataField("opaqueKey")]
public bool UseOpaqueForLOSChecks = false;
// Like movement we add a component and pass it off to the dedicated system.
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
@@ -56,8 +62,10 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
{

View File

@@ -0,0 +1,12 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
/// target entity is exempt from being targeted, otherwise it returns 1f.
/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
/// for further details on turret target validation.
/// </summary>
public sealed partial class TurretTargetingCon : UtilityConsideration
{
}

View File

@@ -1,6 +1,7 @@
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Map;
@@ -132,8 +133,10 @@ public sealed partial class NPCCombatSystem
if (comp.LOSAccumulator < 0f)
{
comp.LOSAccumulator += UnoccludedCooldown;
// For consistency with NPC steering.
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
}
if (!comp.TargetInLOS)

View File

@@ -20,6 +20,7 @@ using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.Systems;
using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -53,6 +54,7 @@ public sealed class NPCUtilitySystem : EntitySystem
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
[Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
private EntityQuery<PuddleComponent> _puddleQuery;
private EntityQuery<TransformComponent> _xformQuery;
@@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem
return 1f;
return 0f;
}
case TurretTargetingCon:
{
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
_turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
return 1f;
return 0f;
}
default:
throw new NotImplementedException();
}

View File

@@ -15,9 +15,15 @@ namespace Content.Shared.Access
/// <summary>
/// The player-visible name of the access level, in the ID card console and such.
/// </summary>
[DataField("name")]
[DataField]
public string? Name { get; set; }
/// <summary>
/// Denotes whether this access level is intended to be assignable to a crew ID card.
/// </summary>
[DataField]
public bool CanAddToIdCard = true;
public string GetAccessLevelName()
{
if (Name is { } name)

View File

@@ -0,0 +1,19 @@
using Content.Shared.Access;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Turrets;
/// <summary>
/// Attached to entities to provide them with turret target selection data.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(TurretTargetSettingsSystem))]
public sealed partial class TurretTargetSettingsComponent : Component
{
/// <summary>
/// Crew with one or more access levels from this list are exempt from being targeted by turrets.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<ProtoId<AccessLevelPrototype>> ExemptAccessLevels = new();
}

View File

@@ -0,0 +1,126 @@
using Content.Shared.Access;
using Content.Shared.Access.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.Turrets;
/// <summary>
/// This system is used for validating potential targets for NPCs with a <see cref="TurretTargetSettingsComponent"/> (i.e., turrets).
/// A turret will consider an entity a valid target if the entity does not possess any access tags which appear on the
/// turret's <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
/// </summary>
public sealed partial class TurretTargetSettingsSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
private ProtoId<AccessLevelPrototype> _accessLevelBorg = "Borg";
private ProtoId<AccessLevelPrototype> _accessLevelBasicSilicon = "BasicSilicon";
/// <summary>
/// Adds or removes access levels from a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
/// </summary>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="exemption">The proto ID for the access level</param>
/// <param name="enabled">Set 'true' to add the exemption, or 'false' to remove it</param>
[PublicAPI]
public void SetAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ProtoId<AccessLevelPrototype> exemption, bool enabled)
{
if (enabled)
ent.Comp.ExemptAccessLevels.Add(exemption);
else
ent.Comp.ExemptAccessLevels.Remove(exemption);
}
/// <summary>
/// Adds or removes a collection of access levels from a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
/// </summary>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="exemption">The collection of access level proto IDs to add or remove</param>
/// <param name="enabled">Set 'true' to add the collection as exemptions, or 'false' to remove them</param>
[PublicAPI]
public void SetAccessLevelExemptions(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions, bool enabled)
{
foreach (var exemption in exemptions)
SetAccessLevelExemption(ent, exemption, enabled);
}
/// <summary>
/// Sets a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list to contain only a supplied collection of access levels.
/// </summary>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="exemptions">The supplied collection of access level proto IDs</param>
[PublicAPI]
public void SyncAccessLevelExemptions(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions)
{
ent.Comp.ExemptAccessLevels.Clear();
SetAccessLevelExemptions(ent, exemptions, true);
}
/// <summary>
/// Sets a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list to match that of another.
/// </summary>
/// <param name="target">The entity this is having its exemption list updated <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="source">The entity that is being used as a template for the target</param>
[PublicAPI]
public void SyncAccessLevelExemptions(Entity<TurretTargetSettingsComponent> target, Entity<TurretTargetSettingsComponent> source)
{
SyncAccessLevelExemptions(target, source.Comp.ExemptAccessLevels);
}
/// <summary>
/// Returns whether a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list contains a specific access level.
/// </summary>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="exemption">The access level proto ID being checked</param>
[PublicAPI]
public bool HasAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ProtoId<AccessLevelPrototype> exemption)
{
if (ent.Comp.ExemptAccessLevels.Count == 0)
return false;
return ent.Comp.ExemptAccessLevels.Contains(exemption);
}
/// <summary>
/// Returns whether a <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list contains one or more access levels from another collection.
/// </summary>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="exemptions"></param>
[PublicAPI]
public bool HasAnyAccessLevelExemption(Entity<TurretTargetSettingsComponent> ent, ICollection<ProtoId<AccessLevelPrototype>> exemptions)
{
if (ent.Comp.ExemptAccessLevels.Count == 0)
return false;
foreach (var exemption in exemptions)
{
if (HasAccessLevelExemption(ent, exemption))
return true;
}
return false;
}
/// <summary>
/// Returns whether an entity is a valid target for a turret.
/// </summary>
/// <remarks>
/// Returns false if the target possesses one or more access tags that are present on the entity's <see cref="TurretTargetSettingsComponent.ExemptAccessLevels"/> list.
/// </remarks>
/// <param name="ent">The entity and its <see cref="TurretTargetSettingsComponent"/></param>
/// <param name="target">The target entity</param>
[PublicAPI]
public bool EntityIsTargetForTurret(Entity<TurretTargetSettingsComponent> ent, EntityUid target)
{
var accessLevels = _accessReader.FindAccessTags(target);
if (accessLevels.Contains(_accessLevelBorg))
return !HasAccessLevelExemption(ent, _accessLevelBorg);
if (accessLevels.Contains(_accessLevelBasicSilicon))
return !HasAccessLevelExemption(ent, _accessLevelBasicSilicon);
return !HasAnyAccessLevelExemption(ent, accessLevels);
}
}

View File

@@ -45,3 +45,7 @@ id-card-access-level-syndicate-agent = Syndicate Agent
id-card-access-level-central-command = Central Command
id-card-access-level-wizard = Wizard
id-card-access-level-station-ai = Artifical Intelligence
id-card-access-level-borg = Cyborg
id-card-access-level-basic-silicon = Robot

View File

@@ -13,14 +13,20 @@
- type: accessGroup
id: Command
tags:
- Command
- Captain
- HeadOfPersonnel
- Command
- ChiefEngineer
- ChiefMedicalOfficer
- Cryogenics
- HeadOfPersonnel
- HeadOfSecurity
- Quartermaster
- ResearchDirector
- type: accessLevel
id: EmergencyShuttleRepealAll
name: id-card-access-level-emergency-shuttle-repeal
canAddToIdCard: false
- type: accessLevel
id: Cryogenics

View File

@@ -0,0 +1,21 @@
- type: accessLevel
id: Borg
name: id-card-access-level-borg
canAddToIdCard: false
- type: accessLevel
id: BasicSilicon
name: id-card-access-level-basic-silicon
canAddToIdCard: false
- type: accessLevel
id: StationAi
name: id-card-access-level-station-ai
canAddToIdCard: false
- type: accessGroup
id: Silicon
tags:
- StationAi
- Borg
- BasicSilicon

View File

@@ -290,6 +290,8 @@
enabled: false
groups:
- AllAccess
tags:
- Borg
- type: AccessReader
access: [["Command"], ["Research"]]
- type: ShowJobIcons

View File

@@ -31,6 +31,9 @@
- type: NpcFactionMember
factions:
- SimpleNeutral
- type: Access
tags:
- BasicSilicon
- type: IntrinsicRadioReceiver
- type: ActiveRadio
channels:

View File

@@ -30,6 +30,7 @@
- type: Access
groups:
- AllAccess
- Silicon
tags:
- NuclearOperative
- SyndicateAgent

View File

@@ -430,6 +430,7 @@
- type: Access
groups:
- AllAccess
- Silicon
- type: Eye
drawFov: false
- type: Examiner

View File

@@ -827,6 +827,7 @@
- type: Access
groups:
- AllAccess
- Silicon
tags:
- CentralCommand
- NuclearOperative

View File

@@ -90,6 +90,8 @@
- Armory
- Atmospherics
- Bar
- BasicSilicon
- Borg
- Brig
- Detective
- Captain
@@ -117,6 +119,7 @@
- Salvage
- Security
- Service
- StationAi
- Theatre
- CentralCommand
- NuclearOperative

View File

@@ -33,6 +33,39 @@
- !type:HTNCompoundTask
task: IdleSpinCompound
- type: htnCompound
id: EnergyTurretCompound
branches:
- tasks:
- !type:HTNPrimitiveTask
operator: !type:UtilityOperator
proto: NearbyGunTargets
- !type:HTNPrimitiveTask
preconditions:
- !type:KeyExistsPrecondition
key: Target
- !type:TargetInRangePrecondition
targetKey: Target
# TODO: Non-scuffed
rangeKey: RangedRange
- !type:TargetInLOSPrecondition
targetKey: Target
rangeKey: RangedRange
opaqueKey: true
operator: !type:GunOperator
targetKey: Target
opaqueKey: true
services:
- !type:UtilityService
id: RangedService
proto: NearbyGunTargets
key: Target
- tasks:
- !type:HTNCompoundTask
task: IdleSpinCompound
- type: htnCompound
id: SimpleRangedHostileCompound
branches:

View File

@@ -188,6 +188,10 @@
considerations:
- !type:TargetIsAliveCon
curve: !type:BoolCurve
- !type:TargetIsCritCon
curve: !type:InverseBoolCurve
- !type:TurretTargetingCon
curve: !type:BoolCurve
- !type:TargetDistanceCon
curve: !type:PresetCurve
preset: TargetDistance