diff --git a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
index ea2628e5cb..180b849958 100644
--- a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
+++ b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
@@ -2,32 +2,69 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Server.Singularity.EntitySystems;
+using Content.Shared.Physics;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Singularity.Components;
-[RegisterComponent]
+[RegisterComponent, AutoGenerateComponentPause]
+[Access(typeof(SingularityGeneratorSystem))]
public sealed partial class SingularityGeneratorComponent : Component
{
///
/// The amount of power this generator has accumulated.
/// If you want to set this use
///
- [DataField("power")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
+ [DataField]
public float Power = 0;
///
/// The power threshold at which this generator will spawn a singularity.
/// If you want to set this use
///
- [DataField("threshold")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
+ [DataField]
public float Threshold = 16;
+ ///
+ /// Allows the generator to ignore all the failsafe stuff, e.g. when emagged
+ ///
+ [DataField]
+ public bool FailsafeDisabled = false;
+
+ ///
+ /// Maximum distance at which the generator will check for a field at
+ ///
+ [DataField]
+ public float FailsafeDistance = 16;
+
///
/// The prototype ID used to spawn a singularity.
///
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer))]
- [ViewVariables(VVAccess.ReadWrite)]
public string? SpawnPrototype = "Singularity";
+
+ ///
+ /// The masks the raycast should not go through
+ ///
+ [DataField]
+ public int CollisionMask = (int)CollisionGroup.FullTileMask;
+
+ ///
+ /// Message to use when there's no containment field on cardinal directions
+ ///
+ [DataField]
+ public LocId ContainmentFailsafeMessage;
+
+ ///
+ /// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning
+ ///
+ [DataField]
+ public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(30);
+
+ ///
+ /// How long until the generator can issue a failsafe warning again
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan NextFailsafe;
}
diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
index a0c0262794..be0c5e49b5 100644
--- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
@@ -1,7 +1,15 @@
+using System.Diagnostics;
using Content.Server.ParticleAccelerator.Components;
+using Content.Server.Popups;
using Content.Server.Singularity.Components;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Popups;
using Content.Shared.Singularity.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
+using Robust.Shared.Timing;
namespace Content.Server.Singularity.EntitySystems;
@@ -9,6 +17,11 @@ public sealed class SingularityGeneratorSystem : EntitySystem
{
#region Dependencies
[Dependency] private readonly IViewVariablesManager _vvm = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly PhysicsSystem _physics = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
#endregion Dependencies
public override void Initialize()
@@ -16,6 +29,7 @@ public sealed class SingularityGeneratorSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent(HandleParticleCollide);
+ SubscribeLocalEvent(OnEmagged);
var vvHandle = _vvm.GetTypeHandler();
vvHandle.AddPath(nameof(SingularityGeneratorComponent.Power), (_, comp) => comp.Power, SetPower);
@@ -100,11 +114,33 @@ public sealed class SingularityGeneratorSystem : EntitySystem
/// The state of the beginning of the collision.
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{
- if (EntityManager.TryGetComponent(args.OtherEntity, out var singularityGeneratorComponent))
+ if (!EntityManager.TryGetComponent(args.OtherEntity, out var generatorComp))
+ return;
+
+ if (_timing.CurTime < _metadata.GetPauseTime(uid) + generatorComp.NextFailsafe)
{
+ EntityManager.QueueDeleteEntity(uid);
+ return;
+ }
+
+ var contained = true;
+ var transform = Transform(args.OtherEntity);
+ var directions = Enum.GetValues().Length;
+ for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals
+ {
+ if (!CheckContainmentField((Direction)i, new Entity(args.OtherEntity, generatorComp), transform))
+ contained = false;
+ }
+
+ if (!contained)
+ {
+ generatorComp.NextFailsafe = _timing.CurTime + generatorComp.FailsafeCooldown;
+ _popupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe", ("target", args.OtherEntity)), args.OtherEntity, PopupType.LargeCaution);
+ }
+ else
SetPower(
args.OtherEntity,
- singularityGeneratorComponent.Power + component.State switch
+ generatorComp.Power + component.State switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
@@ -113,10 +149,51 @@ public sealed class SingularityGeneratorSystem : EntitySystem
ParticleAcceleratorPowerState.Level3 => 8,
_ => 0
},
- singularityGeneratorComponent
+ generatorComp
);
- EntityManager.QueueDeleteEntity(uid);
- }
+ EntityManager.QueueDeleteEntity(uid);
+ }
+
+ private void OnEmagged(EntityUid uid, SingularityGeneratorComponent component, ref GotEmaggedEvent args)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("comp-generator-failsafe-disabled", ("target", uid)), uid);
+ component.FailsafeDisabled = true;
+ args.Handled = true;
}
#endregion Event Handlers
+
+ ///
+ /// Checks whether there's a containment field in a given direction away from the generator
+ ///
+ /// The transform component of the singularity generator.
+ /// Mostly copied from
+ private bool CheckContainmentField(Direction dir, Entity generator, TransformComponent transform)
+ {
+ var component = generator.Comp;
+
+ var (worldPosition, worldRotation) = _transformSystem.GetWorldPositionRotation(transform);
+ var dirRad = dir.ToAngle() + worldRotation;
+
+ var ray = new CollisionRay(worldPosition, dirRad.ToVec(), component.CollisionMask);
+ var rayCastResults = _physics.IntersectRay(transform.MapID, ray, component.FailsafeDistance, generator, false);
+ var genQuery = GetEntityQuery();
+
+ RayCastResults? closestResult = null;
+
+ foreach (var result in rayCastResults)
+ {
+ if (genQuery.HasComponent(result.HitEntity))
+ closestResult = result;
+
+ break;
+ }
+
+ if (closestResult == null)
+ return false;
+
+ var ent = closestResult.Value.HitEntity;
+
+ // Check that the field can't be moved. The fields' transform parenting is weird, so skip that
+ return TryComp(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static;
+ }
}
diff --git a/Resources/Locale/en-US/singularity/components/generator-component.ftl b/Resources/Locale/en-US/singularity/components/generator-component.ftl
new file mode 100644
index 0000000000..f3a2254c38
--- /dev/null
+++ b/Resources/Locale/en-US/singularity/components/generator-component.ftl
@@ -0,0 +1,2 @@
+comp-generator-failsafe = The {$target} shakes as the containment failsafe triggers!
+comp-generator-failsafe = Something fizzles out inside of {$target}...
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml
index 647eae2772..45a40bf0fa 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/generator.yml
@@ -1,7 +1,7 @@
- type: entity
id: SingularityGenerator
name: gravitational singularity generator
- description: An Odd Device which produces a Gravitational Singularity when set up.
+ description: An Odd Device which produces a Gravitational Singularity when set up. Comes with a temporary shutdown containment failsafe.
placement:
mode: SnapgridCenter
components:
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml
index d45e6c58ea..bdd90f2f16 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Tesla/generator.yml
@@ -2,12 +2,12 @@
id: TeslaGenerator
name: tesla generator
parent: BaseStructureDynamic
- description: An Odd Device which produces a powerful Tesla ball when set up.
+ description: An Odd Device which produces a powerful Tesla ball when set up. Comes with a temporary shutdown containment failsafe.
components:
- type: Sprite
noRot: true
sprite: Structures/Power/Generation/Tesla/generator.rsi
- state: icon
+ state: icon
- type: SingularityGenerator # TODO: rename the generator
spawnId: TeslaEnergyBall
- type: InteractionOutline