diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index 84b787a4ec..d60c978ccf 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -4,6 +4,7 @@ using Content.Shared.Audio; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Random; +using Content.Shared.Random.Rules; using Robust.Client.GameObjects; using Robust.Client.Player; using Robust.Client.ResourceManagement; diff --git a/Content.Shared/Audio/AmbientMusicPrototype.cs b/Content.Shared/Audio/AmbientMusicPrototype.cs index 54f70f5728..219c41527d 100644 --- a/Content.Shared/Audio/AmbientMusicPrototype.cs +++ b/Content.Shared/Audio/AmbientMusicPrototype.cs @@ -1,4 +1,5 @@ using Content.Shared.Random; +using Content.Shared.Random.Rules; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; diff --git a/Content.Shared/Random/Rules/AlwaysTrue.cs b/Content.Shared/Random/Rules/AlwaysTrue.cs new file mode 100644 index 0000000000..98b81fae79 --- /dev/null +++ b/Content.Shared/Random/Rules/AlwaysTrue.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Random.Rules; + +/// +/// Always returns true. Used for fallbacks. +/// +public sealed partial class AlwaysTrueRule : RulesRule +{ + public override bool Check(EntityManager entManager, EntityUid uid) + { + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/GridInRange.cs b/Content.Shared/Random/Rules/GridInRange.cs new file mode 100644 index 0000000000..8cbbef1cdc --- /dev/null +++ b/Content.Shared/Random/Rules/GridInRange.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using Robust.Shared.Map; + +namespace Content.Shared.Random.Rules; + +/// +/// Returns true if on a grid or in range of one. +/// +public sealed partial class GridInRangeRule : RulesRule +{ + [DataField] + public float Range = 10f; + + public override bool Check(EntityManager entManager, EntityUid uid) + { + if (!entManager.TryGetComponent(uid, out TransformComponent? xform)) + { + return false; + } + + if (xform.GridUid != null) + { + return !Inverted; + } + + var transform = entManager.System(); + var mapManager = IoCManager.Resolve(); + + var worldPos = transform.GetWorldPosition(xform); + var gridRange = new Vector2(Range, Range); + + foreach (var _ in mapManager.FindGridsIntersecting(xform.MapID, new Box2(worldPos - gridRange, worldPos + gridRange))) + { + return !Inverted; + } + + return false; + } +} diff --git a/Content.Shared/Random/Rules/InSpace.cs b/Content.Shared/Random/Rules/InSpace.cs new file mode 100644 index 0000000000..8163e1f6be --- /dev/null +++ b/Content.Shared/Random/Rules/InSpace.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Random.Rules; + +/// +/// Returns true if the attached entity is in space. +/// +public sealed partial class InSpaceRule : RulesRule +{ + public override bool Check(EntityManager entManager, EntityUid uid) + { + if (!entManager.TryGetComponent(uid, out TransformComponent? xform) || + xform.GridUid != null) + { + return Inverted; + } + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/NearbyAccess.cs b/Content.Shared/Random/Rules/NearbyAccess.cs new file mode 100644 index 0000000000..2a8ad077c6 --- /dev/null +++ b/Content.Shared/Random/Rules/NearbyAccess.cs @@ -0,0 +1,77 @@ +using Content.Shared.Access; +using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Random.Rules; + +/// +/// Checks for an entity nearby with the specified access. +/// +public sealed partial class NearbyAccessRule : RulesRule +{ + // This exists because of door electronics contained inside doors. + /// + /// Does the access entity need to be anchored. + /// + [DataField] + public bool Anchored = true; + + /// + /// Count of entities that need to be nearby. + /// + [DataField] + public int Count = 1; + + [DataField(required: true)] + public List> Access = new(); + + [DataField] + public float Range = 10f; + + public override bool Check(EntityManager entManager, EntityUid uid) + { + var xformQuery = entManager.GetEntityQuery(); + + if (!xformQuery.TryGetComponent(uid, out var xform) || + xform.MapUid == null) + { + return false; + } + + var transform = entManager.System(); + var lookup = entManager.System(); + var reader = entManager.System(); + + var found = false; + var worldPos = transform.GetWorldPosition(xform, xformQuery); + var count = 0; + + // TODO: Update this when we get the callback version + var entities = new HashSet>(); + lookup.GetEntitiesInRange(xform.MapID, worldPos, Range, entities); + foreach (var comp in entities) + { + if (!reader.AreAccessTagsAllowed(Access, comp) || + Anchored && + (!xformQuery.TryGetComponent(comp, out var compXform) || + !compXform.Anchored)) + { + continue; + } + + count++; + + if (count < Count) + continue; + + found = true; + break; + } + + if (!found) + return Inverted; + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/NearbyComponents.cs b/Content.Shared/Random/Rules/NearbyComponents.cs new file mode 100644 index 0000000000..13108e88d6 --- /dev/null +++ b/Content.Shared/Random/Rules/NearbyComponents.cs @@ -0,0 +1,71 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Random.Rules; + +public sealed partial class NearbyComponentsRule : RulesRule +{ + /// + /// Does the entity need to be anchored. + /// + [DataField] + public bool Anchored; + + [DataField] + public int Count; + + [DataField(required: true)] + public ComponentRegistry Components = default!; + + [DataField] + public float Range = 10f; + + public override bool Check(EntityManager entManager, EntityUid uid) + { + var inRange = new HashSet>(); + var xformQuery = entManager.GetEntityQuery(); + + if (!xformQuery.TryGetComponent(uid, out var xform) || + xform.MapUid == null) + { + return false; + } + + var transform = entManager.System(); + var lookup = entManager.System(); + + var found = false; + var worldPos = transform.GetWorldPosition(xform); + var count = 0; + + foreach (var compType in Components.Values) + { + inRange.Clear(); + lookup.GetEntitiesInRange(compType.Component.GetType(), xform.MapID, worldPos, Range, inRange); + foreach (var comp in inRange) + { + if (Anchored && + (!xformQuery.TryGetComponent(comp, out var compXform) || + !compXform.Anchored)) + { + continue; + } + + count++; + + if (count < Count) + continue; + + found = true; + break; + } + + if (found) + break; + } + + if (!found) + return Inverted; + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/NearbyEntities.cs b/Content.Shared/Random/Rules/NearbyEntities.cs new file mode 100644 index 0000000000..0754750bc4 --- /dev/null +++ b/Content.Shared/Random/Rules/NearbyEntities.cs @@ -0,0 +1,58 @@ +using Content.Shared.Whitelist; + +namespace Content.Shared.Random.Rules; + +/// +/// Checks for entities matching the whitelist in range. +/// This is more expensive than so prefer that! +/// +public sealed partial class NearbyEntitiesRule : RulesRule +{ + /// + /// How many of the entity need to be nearby. + /// + [DataField] + public int Count = 1; + + [DataField(required: true)] + public EntityWhitelist Whitelist = new(); + + [DataField] + public float Range = 10f; + + public override bool Check(EntityManager entManager, EntityUid uid) + { + if (!entManager.TryGetComponent(uid, out TransformComponent? xform) || + xform.MapUid == null) + { + return false; + } + + var transform = entManager.System(); + var lookup = entManager.System(); + var whitelistSystem = entManager.System(); + + var found = false; + var worldPos = transform.GetWorldPosition(xform); + var count = 0; + + foreach (var ent in lookup.GetEntitiesInRange(xform.MapID, worldPos, Range)) + { + if (whitelistSystem.IsWhitelistFail(Whitelist, ent)) + continue; + + count++; + + if (count < Count) + continue; + + found = true; + break; + } + + if (!found) + return Inverted; + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/NearbyTilesPercent.cs b/Content.Shared/Random/Rules/NearbyTilesPercent.cs new file mode 100644 index 0000000000..465ac8dc5c --- /dev/null +++ b/Content.Shared/Random/Rules/NearbyTilesPercent.cs @@ -0,0 +1,79 @@ +using Content.Shared.Maps; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Random.Rules; + +public sealed partial class NearbyTilesPercentRule : RulesRule +{ + /// + /// If there are anchored entities on the tile do we ignore the tile. + /// + [DataField] + public bool IgnoreAnchored; + + [DataField(required: true)] + public float Percent; + + [DataField(required: true)] + public List> Tiles = new(); + + [DataField] + public float Range = 10f; + + public override bool Check(EntityManager entManager, EntityUid uid) + { + if (!entManager.TryGetComponent(uid, out TransformComponent? xform) || + !entManager.TryGetComponent(xform.GridUid, out var grid)) + { + return false; + } + + var transform = entManager.System(); + var tileDef = IoCManager.Resolve(); + + var physicsQuery = entManager.GetEntityQuery(); + var tileCount = 0; + var matchingTileCount = 0; + + foreach (var tile in grid.GetTilesIntersecting(new Circle(transform.GetWorldPosition(xform), + Range))) + { + // Only consider collidable anchored (for reasons some subfloor stuff has physics but non-collidable) + if (IgnoreAnchored) + { + var gridEnum = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices); + var found = false; + + while (gridEnum.MoveNext(out var ancUid)) + { + if (!physicsQuery.TryGetComponent(ancUid, out var physics) || + !physics.CanCollide) + { + continue; + } + + found = true; + break; + } + + if (found) + continue; + } + + tileCount++; + + if (!Tiles.Contains(tileDef[tile.Tile.TypeId].ID)) + continue; + + matchingTileCount++; + } + + if (tileCount == 0 || matchingTileCount / (float) tileCount < Percent) + return Inverted; + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/OnMapGrid.cs b/Content.Shared/Random/Rules/OnMapGrid.cs new file mode 100644 index 0000000000..bea841872e --- /dev/null +++ b/Content.Shared/Random/Rules/OnMapGrid.cs @@ -0,0 +1,19 @@ +namespace Content.Shared.Random.Rules; + +/// +/// Returns true if griduid and mapuid match (AKA on 'planet'). +/// +public sealed partial class OnMapGridRule : RulesRule +{ + public override bool Check(EntityManager entManager, EntityUid uid) + { + if (!entManager.TryGetComponent(uid, out TransformComponent? xform) || + xform.GridUid != xform.MapUid || + xform.MapUid == null) + { + return Inverted; + } + + return !Inverted; + } +} diff --git a/Content.Shared/Random/Rules/RulesSystem.cs b/Content.Shared/Random/Rules/RulesSystem.cs new file mode 100644 index 0000000000..1957beab51 --- /dev/null +++ b/Content.Shared/Random/Rules/RulesSystem.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Random.Rules; + +/// +/// Rules-based item selection. Can be used for any sort of conditional selection +/// Every single condition needs to be true for this to be selected. +/// e.g. "choose maintenance audio if 90% of tiles nearby are maintenance tiles" +/// +[Prototype("rules")] +public sealed partial class RulesPrototype : IPrototype +{ + [IdDataField] public string ID { get; } = string.Empty; + + [DataField("rules", required: true)] + public List Rules = new(); +} + +[ImplicitDataDefinitionForInheritors] +public abstract partial class RulesRule +{ + [DataField] + public bool Inverted; + public abstract bool Check(EntityManager entManager, EntityUid uid); +} + +public sealed class RulesSystem : EntitySystem +{ + public bool IsTrue(EntityUid uid, RulesPrototype rules) + { + foreach (var rule in rules.Rules) + { + if (!rule.Check(EntityManager, uid)) + return false; + } + + return true; + } +} diff --git a/Content.Shared/Random/RulesPrototype.cs b/Content.Shared/Random/RulesPrototype.cs deleted file mode 100644 index 20961af8e7..0000000000 --- a/Content.Shared/Random/RulesPrototype.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Content.Shared.Access; -using Content.Shared.Maps; -using Content.Shared.Whitelist; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Shared.Random; - -/// -/// Rules-based item selection. Can be used for any sort of conditional selection -/// Every single condition needs to be true for this to be selected. -/// e.g. "choose maintenance audio if 90% of tiles nearby are maintenance tiles" -/// -[Prototype("rules")] -public sealed partial class RulesPrototype : IPrototype -{ - [IdDataField] public string ID { get; } = string.Empty; - - [DataField("rules", required: true)] - public List Rules = new(); -} - -[ImplicitDataDefinitionForInheritors] -public abstract partial class RulesRule -{ - -} - -/// -/// Returns true if the attached entity is in space. -/// -public sealed partial class InSpaceRule : RulesRule -{ - -} - -/// -/// Checks for entities matching the whitelist in range. -/// This is more expensive than so prefer that! -/// -public sealed partial class NearbyEntitiesRule : RulesRule -{ - /// - /// How many of the entity need to be nearby. - /// - [DataField("count")] - public int Count = 1; - - [DataField("whitelist", required: true)] - public EntityWhitelist Whitelist = new(); - - [DataField("range")] - public float Range = 10f; -} - -public sealed partial class NearbyTilesPercentRule : RulesRule -{ - /// - /// If there are anchored entities on the tile do we ignore the tile. - /// - [DataField("ignoreAnchored")] public bool IgnoreAnchored; - - [DataField("percent", required: true)] - public float Percent; - - [DataField("tiles", required: true, customTypeSerializer:typeof(PrototypeIdListSerializer))] - public List Tiles = new(); - - [DataField("range")] - public float Range = 10f; -} - -/// -/// Always returns true. Used for fallbacks. -/// -public sealed partial class AlwaysTrueRule : RulesRule -{ - -} - -/// -/// Returns true if on a grid or in range of one. -/// -public sealed partial class GridInRangeRule : RulesRule -{ - [DataField("range")] - public float Range = 10f; - - [DataField("inverted")] - public bool Inverted = false; -} - -/// -/// Returns true if griduid and mapuid match (AKA on 'planet'). -/// -public sealed partial class OnMapGridRule : RulesRule -{ - -} - -/// -/// Checks for an entity nearby with the specified access. -/// -public sealed partial class NearbyAccessRule : RulesRule -{ - // This exists because of doorelectronics contained inside doors. - /// - /// Does the access entity need to be anchored. - /// - [DataField("anchored")] - public bool Anchored = true; - - /// - /// Count of entities that need to be nearby. - /// - [DataField("count")] - public int Count = 1; - - [DataField("access", required: true)] - public List> Access = new(); - - [DataField("range")] - public float Range = 10f; -} - -public sealed partial class NearbyComponentsRule : RulesRule -{ - /// - /// Does the entity need to be anchored. - /// - [DataField("anchored")] - public bool Anchored; - - [DataField("count")] public int Count; - - [DataField("components", required: true)] - public ComponentRegistry Components = default!; - - [DataField("range")] - public float Range = 10f; -} diff --git a/Content.Shared/Random/RulesSystem.cs b/Content.Shared/Random/RulesSystem.cs deleted file mode 100644 index 58d67c268e..0000000000 --- a/Content.Shared/Random/RulesSystem.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Numerics; -using Content.Shared.Access.Components; -using Content.Shared.Access.Systems; -using Content.Shared.Whitelist; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Physics.Components; - -namespace Content.Shared.Random; - -public sealed class RulesSystem : EntitySystem -{ - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly ITileDefinitionManager _tileDef = default!; - [Dependency] private readonly AccessReaderSystem _reader = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - public bool IsTrue(EntityUid uid, RulesPrototype rules) - { - var inRange = new HashSet>(); - foreach (var rule in rules.Rules) - { - switch (rule) - { - case AlwaysTrueRule: - break; - case GridInRangeRule griddy: - { - if (!TryComp(uid, out TransformComponent? xform)) - { - return false; - } - - if (xform.GridUid != null) - { - return !griddy.Inverted; - } - - var worldPos = _transform.GetWorldPosition(xform); - var gridRange = new Vector2(griddy.Range, griddy.Range); - - foreach (var _ in _mapManager.FindGridsIntersecting( - xform.MapID, - new Box2(worldPos - gridRange, worldPos + gridRange))) - { - return !griddy.Inverted; - } - - break; - } - case InSpaceRule: - { - if (!TryComp(uid, out TransformComponent? xform) || - xform.GridUid != null) - { - return false; - } - - break; - } - case NearbyAccessRule access: - { - var xformQuery = GetEntityQuery(); - - if (!xformQuery.TryGetComponent(uid, out var xform) || - xform.MapUid == null) - { - return false; - } - - var found = false; - var worldPos = _transform.GetWorldPosition(xform, xformQuery); - var count = 0; - - // TODO: Update this when we get the callback version - var entities = new HashSet>(); - _lookup.GetEntitiesInRange(xform.MapID, worldPos, access.Range, entities); - foreach (var comp in entities) - { - if (!_reader.AreAccessTagsAllowed(access.Access, comp) || - access.Anchored && - (!xformQuery.TryGetComponent(comp, out var compXform) || - !compXform.Anchored)) - { - continue; - } - - count++; - - if (count < access.Count) - continue; - - found = true; - break; - } - - if (!found) - return false; - - break; - } - case NearbyComponentsRule nearbyComps: - { - var xformQuery = GetEntityQuery(); - - if (!xformQuery.TryGetComponent(uid, out var xform) || - xform.MapUid == null) - { - return false; - } - - var found = false; - var worldPos = _transform.GetWorldPosition(xform); - var count = 0; - - foreach (var compType in nearbyComps.Components.Values) - { - inRange.Clear(); - _lookup.GetEntitiesInRange(compType.Component.GetType(), xform.MapID, worldPos, nearbyComps.Range, inRange); - foreach (var comp in inRange) - { - if (nearbyComps.Anchored && - (!xformQuery.TryGetComponent(comp, out var compXform) || - !compXform.Anchored)) - { - continue; - } - - count++; - - if (count < nearbyComps.Count) - continue; - - found = true; - break; - } - - if (found) - break; - } - - if (!found) - return false; - - break; - } - case NearbyEntitiesRule entity: - { - if (!TryComp(uid, out TransformComponent? xform) || - xform.MapUid == null) - { - return false; - } - - var found = false; - var worldPos = _transform.GetWorldPosition(xform); - var count = 0; - - foreach (var ent in _lookup.GetEntitiesInRange(xform.MapID, worldPos, entity.Range)) - { - if (_whitelistSystem.IsWhitelistFail(entity.Whitelist, ent)) - continue; - - count++; - - if (count < entity.Count) - continue; - - found = true; - break; - } - - if (!found) - return false; - - break; - } - case NearbyTilesPercentRule tiles: - { - if (!TryComp(uid, out TransformComponent? xform) || - !TryComp(xform.GridUid, out var grid)) - { - return false; - } - - var physicsQuery = GetEntityQuery(); - var tileCount = 0; - var matchingTileCount = 0; - - foreach (var tile in grid.GetTilesIntersecting(new Circle(_transform.GetWorldPosition(xform), - tiles.Range))) - { - // Only consider collidable anchored (for reasons some subfloor stuff has physics but non-collidable) - if (tiles.IgnoreAnchored) - { - var gridEnum = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices); - var found = false; - - while (gridEnum.MoveNext(out var ancUid)) - { - if (!physicsQuery.TryGetComponent(ancUid, out var physics) || - !physics.CanCollide) - { - continue; - } - - found = true; - break; - } - - if (found) - continue; - } - - tileCount++; - - if (!tiles.Tiles.Contains(_tileDef[tile.Tile.TypeId].ID)) - continue; - - matchingTileCount++; - } - - if (tileCount == 0 || matchingTileCount / (float) tileCount < tiles.Percent) - return false; - - break; - } - case OnMapGridRule: - { - if (!TryComp(uid, out TransformComponent? xform) || - xform.GridUid != xform.MapUid || - xform.MapUid == null) - { - return false; - } - - break; - } - default: - throw new NotImplementedException(); - } - } - - return true; - } -}