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
{
/// <summary>
/// The amount of power this generator has accumulated.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
/// </summary>
- [DataField("power")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
+ [DataField]
public float Power = 0;
/// <summary>
/// The power threshold at which this generator will spawn a singularity.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
/// </summary>
- [DataField("threshold")]
- [Access(friends:typeof(SingularityGeneratorSystem))]
+ [DataField]
public float Threshold = 16;
+ /// <summary>
+ /// Allows the generator to ignore all the failsafe stuff, e.g. when emagged
+ /// </summary>
+ [DataField]
+ public bool FailsafeDisabled = false;
+
+ /// <summary>
+ /// Maximum distance at which the generator will check for a field at
+ /// </summary>
+ [DataField]
+ public float FailsafeDistance = 16;
+
/// <summary>
/// The prototype ID used to spawn a singularity.
/// </summary>
[DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
- [ViewVariables(VVAccess.ReadWrite)]
public string? SpawnPrototype = "Singularity";
+
+ /// <summary>
+ /// The masks the raycast should not go through
+ /// </summary>
+ [DataField]
+ public int CollisionMask = (int)CollisionGroup.FullTileMask;
+
+ /// <summary>
+ /// Message to use when there's no containment field on cardinal directions
+ /// </summary>
+ [DataField]
+ public LocId ContainmentFailsafeMessage;
+
+ /// <summary>
+ /// For how long the failsafe will cause the generator to stop working and not issue a failsafe warning
+ /// </summary>
+ [DataField]
+ public TimeSpan FailsafeCooldown = TimeSpan.FromSeconds(30);
+
+ /// <summary>
+ /// How long until the generator can issue a failsafe warning again
+ /// </summary>
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan NextFailsafe;
}
+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;
{
#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()
base.Initialize();
SubscribeLocalEvent<ParticleProjectileComponent, StartCollideEvent>(HandleParticleCollide);
+ SubscribeLocalEvent<SingularityGeneratorComponent, GotEmaggedEvent>(OnEmagged);
var vvHandle = _vvm.GetTypeHandler<SingularityGeneratorComponent>();
vvHandle.AddPath(nameof(SingularityGeneratorComponent.Power), (_, comp) => comp.Power, SetPower);
/// <param name="args">The state of the beginning of the collision.</param>
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{
- if (EntityManager.TryGetComponent<SingularityGeneratorComponent>(args.OtherEntity, out var singularityGeneratorComponent))
+ if (!EntityManager.TryGetComponent<SingularityGeneratorComponent>(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<Direction>().Length;
+ for (var i = 0; i < directions - 1; i += 2) // Skip every other direction, checking only cardinals
+ {
+ if (!CheckContainmentField((Direction)i, new Entity<SingularityGeneratorComponent>(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,
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
+
+ /// <summary>
+ /// Checks whether there's a containment field in a given direction away from the generator
+ /// </summary>
+ /// <param name="transform">The transform component of the singularity generator.</param>
+ /// <remarks>Mostly copied from <see cref="ContainmentFieldGeneratorSystem"/> </remarks>
+ private bool CheckContainmentField(Direction dir, Entity<SingularityGeneratorComponent> 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<ContainmentFieldComponent>();
+
+ 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<PhysicsComponent>(ent, out var collidableComponent) && collidableComponent.BodyType == BodyType.Static;
+ }
}