diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index aeb4cc163f..a9b08aac8b 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -31,7 +31,7 @@ public sealed class IdCardSystem : SharedIdCardSystem private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args) { if (!component.CanMicrowave || !TryComp(args.Microwave, out var micro) || micro.Broken) - return; + return; if (TryComp(uid, out var access)) { @@ -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().ToArray()); + var ids = _prototypeManager.EnumeratePrototypes().Where(x => x.CanAddToIdCard).ToArray(); + + if (ids.Length == 0) + return; + + var random = _random.Pick(ids); access.Tags.Add(random.ID); Dirty(uid, access); diff --git a/Content.Server/NPC/Components/NPCRangedCombatComponent.cs b/Content.Server/NPC/Components/NPCRangedCombatComponent.cs index 2e4fcf5298..21cdfba801 100644 --- a/Content.Server/NPC/Components/NPCRangedCombatComponent.cs +++ b/Content.Server/NPC/Components/NPCRangedCombatComponent.cs @@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component [ViewVariables(VVAccess.ReadWrite)] public bool TargetInLOS = false; + /// + /// If true, only opaque objects will block line of sight. + /// + [ViewVariables(VVAccess.ReadWrite)] + // ReSharper disable once InconsistentNaming + public bool UseOpaqueForLOSChecks = false; + /// /// Delay after target is in LOS before we start shooting. /// diff --git a/Content.Server/NPC/HTN/HTNComponent.cs b/Content.Server/NPC/HTN/HTNComponent.cs index f482e0808c..788a347638 100644 --- a/Content.Server/NPC/HTN/HTNComponent.cs +++ b/Content.Server/NPC/HTN/HTNComponent.cs @@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent /// The base task to use for planning /// [ViewVariables(VVAccess.ReadWrite), - DataField("rootTask", required: true)] + DataField("rootTask", required: true)] public HTNCompoundTask RootTask = default!; /// @@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent /// Is this NPC currently planning? /// [ViewVariables] public bool Planning => PlanningJob != null; + + /// + /// Determines whether plans should be made / updated for this entity + /// + [DataField] + public bool Enabled = true; } diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs index cfa670e144..ce4b248a0d 100644 --- a/Content.Server/NPC/HTN/HTNSystem.cs +++ b/Content.Server/NPC/HTN/HTNSystem.cs @@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem component.PlanningJob = null; } + /// + /// Enable / disable the hierarchical task network of an entity + /// + /// The entity and its + /// Set 'true' to enable, or 'false' to disable, the HTN + /// Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled) + // ReSharper disable once InconsistentNaming + [PublicAPI] + public void SetHTNEnabled(Entity 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); + } + /// /// Forces the NPC to replan. /// @@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem _planQueue.Process(); var query = EntityQueryEnumerator(); - while(query.MoveNext(out var uid, out _, out var comp)) + while (query.MoveNext(out var uid, out _, out var comp)) { // If we're over our max count or it's not MapInit then ignore the NPC. if (count >= maxUpdates) break; + if (!comp.Enabled) + continue; + if (comp.PlanningJob != null) { if (comp.PlanningJob.Exception != null) diff --git a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs index 0b233292a4..bb27ae8868 100644 --- a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs +++ b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs @@ -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(RangeKey, _entManager); + var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable); - return _interaction.InRangeUnobstructed(owner, target, range); + return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup); } } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs index 53c5ed1952..65620e73aa 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs @@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown [DataField("requireLOS")] public bool RequireLOS = false; + /// + /// If true, only opaque objects will block line of sight. + /// + [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? 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(blackboard.GetValue(NPCBlackboard.Owner)); ranged.Target = blackboard.GetValue(TargetKey); + ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks; if (blackboard.TryGetValue(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager)) { diff --git a/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs b/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs new file mode 100644 index 0000000000..600eb20517 --- /dev/null +++ b/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs @@ -0,0 +1,12 @@ +namespace Content.Server.NPC.Queries.Considerations; + +/// +/// Returns 0f if the NPC has a and the +/// target entity is exempt from being targeted, otherwise it returns 1f. +/// See +/// for further details on turret target validation. +/// +public sealed partial class TurretTargetingCon : UtilityConsideration +{ + +} diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs index d7196ea73c..f4e312fbd4 100644 --- a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs +++ b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs @@ -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) diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index 60bc5cdfd8..b5d3ac3cbd 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -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 _puddleQuery; private EntityQuery _xformQuery; @@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem return 1f; return 0f; } + case TurretTargetingCon: + { + if (!TryComp(owner, out var turretTargetSettings) || + _turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid)) + return 1f; + + return 0f; + } default: throw new NotImplementedException(); } diff --git a/Content.Shared/Access/AccessLevelPrototype.cs b/Content.Shared/Access/AccessLevelPrototype.cs index e3a3b426b0..f44ad64228 100644 --- a/Content.Shared/Access/AccessLevelPrototype.cs +++ b/Content.Shared/Access/AccessLevelPrototype.cs @@ -15,9 +15,15 @@ namespace Content.Shared.Access /// /// The player-visible name of the access level, in the ID card console and such. /// - [DataField("name")] + [DataField] public string? Name { get; set; } + /// + /// Denotes whether this access level is intended to be assignable to a crew ID card. + /// + [DataField] + public bool CanAddToIdCard = true; + public string GetAccessLevelName() { if (Name is { } name) diff --git a/Content.Shared/Turrets/TurretTargetSettingsComponent.cs b/Content.Shared/Turrets/TurretTargetSettingsComponent.cs new file mode 100644 index 0000000000..d019b0922a --- /dev/null +++ b/Content.Shared/Turrets/TurretTargetSettingsComponent.cs @@ -0,0 +1,19 @@ +using Content.Shared.Access; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Turrets; + +/// +/// Attached to entities to provide them with turret target selection data. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(TurretTargetSettingsSystem))] +public sealed partial class TurretTargetSettingsComponent : Component +{ + /// + /// Crew with one or more access levels from this list are exempt from being targeted by turrets. + /// + [DataField, AutoNetworkedField] + public HashSet> ExemptAccessLevels = new(); +} diff --git a/Content.Shared/Turrets/TurretTargetSettingsSystem.cs b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs new file mode 100644 index 0000000000..56f60e0e69 --- /dev/null +++ b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs @@ -0,0 +1,126 @@ +using Content.Shared.Access; +using Content.Shared.Access.Systems; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Turrets; + +/// +/// This system is used for validating potential targets for NPCs with a (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 list. +/// +public sealed partial class TurretTargetSettingsSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessReader = default!; + + private ProtoId _accessLevelBorg = "Borg"; + private ProtoId _accessLevelBasicSilicon = "BasicSilicon"; + + /// + /// Adds or removes access levels from a list. + /// + /// The entity and its + /// The proto ID for the access level + /// Set 'true' to add the exemption, or 'false' to remove it + [PublicAPI] + public void SetAccessLevelExemption(Entity ent, ProtoId exemption, bool enabled) + { + if (enabled) + ent.Comp.ExemptAccessLevels.Add(exemption); + else + ent.Comp.ExemptAccessLevels.Remove(exemption); + } + + /// + /// Adds or removes a collection of access levels from a list. + /// + /// The entity and its + /// The collection of access level proto IDs to add or remove + /// Set 'true' to add the collection as exemptions, or 'false' to remove them + [PublicAPI] + public void SetAccessLevelExemptions(Entity ent, ICollection> exemptions, bool enabled) + { + foreach (var exemption in exemptions) + SetAccessLevelExemption(ent, exemption, enabled); + } + + /// + /// Sets a list to contain only a supplied collection of access levels. + /// + /// The entity and its + /// The supplied collection of access level proto IDs + [PublicAPI] + public void SyncAccessLevelExemptions(Entity ent, ICollection> exemptions) + { + ent.Comp.ExemptAccessLevels.Clear(); + SetAccessLevelExemptions(ent, exemptions, true); + } + + /// + /// Sets a list to match that of another. + /// + /// The entity this is having its exemption list updated + /// The entity that is being used as a template for the target + [PublicAPI] + public void SyncAccessLevelExemptions(Entity target, Entity source) + { + SyncAccessLevelExemptions(target, source.Comp.ExemptAccessLevels); + } + + /// + /// Returns whether a list contains a specific access level. + /// + /// The entity and its + /// The access level proto ID being checked + [PublicAPI] + public bool HasAccessLevelExemption(Entity ent, ProtoId exemption) + { + if (ent.Comp.ExemptAccessLevels.Count == 0) + return false; + + return ent.Comp.ExemptAccessLevels.Contains(exemption); + } + + /// + /// Returns whether a list contains one or more access levels from another collection. + /// + /// The entity and its + /// + [PublicAPI] + public bool HasAnyAccessLevelExemption(Entity ent, ICollection> exemptions) + { + if (ent.Comp.ExemptAccessLevels.Count == 0) + return false; + + foreach (var exemption in exemptions) + { + if (HasAccessLevelExemption(ent, exemption)) + return true; + } + + return false; + } + + /// + /// Returns whether an entity is a valid target for a turret. + /// + /// + /// Returns false if the target possesses one or more access tags that are present on the entity's list. + /// + /// The entity and its + /// The target entity + [PublicAPI] + public bool EntityIsTargetForTurret(Entity 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); + } +} diff --git a/Resources/Locale/en-US/prototypes/access/accesses.ftl b/Resources/Locale/en-US/prototypes/access/accesses.ftl index 1f867447ef..3d72fc59a2 100644 --- a/Resources/Locale/en-US/prototypes/access/accesses.ftl +++ b/Resources/Locale/en-US/prototypes/access/accesses.ftl @@ -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 \ No newline at end of file diff --git a/Resources/Prototypes/Access/command.yml b/Resources/Prototypes/Access/command.yml index b74b4bb3b1..92e1355f79 100644 --- a/Resources/Prototypes/Access/command.yml +++ b/Resources/Prototypes/Access/command.yml @@ -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 diff --git a/Resources/Prototypes/Access/silicon.yml b/Resources/Prototypes/Access/silicon.yml new file mode 100644 index 0000000000..a8f91982f3 --- /dev/null +++ b/Resources/Prototypes/Access/silicon.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 79f3d2d83b..6fd3b3fbb9 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -290,6 +290,8 @@ enabled: false groups: - AllAccess + tags: + - Borg - type: AccessReader access: [["Command"], ["Research"]] - type: ShowJobIcons diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 6a6e4285eb..9d193a52e6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -31,6 +31,9 @@ - type: NpcFactionMember factions: - SimpleNeutral + - type: Access + tags: + - BasicSilicon - type: IntrinsicRadioReceiver - type: ActiveRadio channels: diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 3eabbd8700..0933cd5573 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -30,6 +30,7 @@ - type: Access groups: - AllAccess + - Silicon tags: - NuclearOperative - SyndicateAgent diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 8dc147249d..c287c9f008 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -430,6 +430,7 @@ - type: Access groups: - AllAccess + - Silicon - type: Eye drawFov: false - type: Examiner diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml index 126b8fc943..45f86e11b0 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml @@ -827,6 +827,7 @@ - type: Access groups: - AllAccess + - Silicon tags: - CentralCommand - NuclearOperative diff --git a/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml b/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml index 257ec06c91..6dd624af7a 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/access_configurator.yml @@ -90,6 +90,8 @@ - Armory - Atmospherics - Bar + - BasicSilicon + - Borg - Brig - Detective - Captain @@ -117,6 +119,7 @@ - Salvage - Security - Service + - StationAi - Theatre - CentralCommand - NuclearOperative diff --git a/Resources/Prototypes/NPCs/root.yml b/Resources/Prototypes/NPCs/root.yml index c6d2a8ee26..d32e140b29 100644 --- a/Resources/Prototypes/NPCs/root.yml +++ b/Resources/Prototypes/NPCs/root.yml @@ -32,6 +32,39 @@ - tasks: - !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 diff --git a/Resources/Prototypes/NPCs/utility_queries.yml b/Resources/Prototypes/NPCs/utility_queries.yml index e10a0ed30c..03764e2b1f 100644 --- a/Resources/Prototypes/NPCs/utility_queries.yml +++ b/Resources/Prototypes/NPCs/utility_queries.yml @@ -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