diff --git a/Content.Client/Singularity/Components/ClientSingularityComponent.cs b/Content.Client/Singularity/Components/ClientSingularityComponent.cs
deleted file mode 100644
index 992cd32079..0000000000
--- a/Content.Client/Singularity/Components/ClientSingularityComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Content.Shared.Singularity.Components;
-using Robust.Shared.GameObjects;
-
-namespace Content.Client.Singularity.Components
-{
- [RegisterComponent]
- [ComponentReference(typeof(SharedSingularityComponent))]
- public sealed class ClientSingularityComponent : SharedSingularityComponent
- {
- }
-}
diff --git a/Content.Client/Singularity/Components/SingularityComponent.cs b/Content.Client/Singularity/Components/SingularityComponent.cs
new file mode 100644
index 0000000000..4b83d0a184
--- /dev/null
+++ b/Content.Client/Singularity/Components/SingularityComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared.Singularity.Components;
+using Content.Client.Singularity.EntitySystems;
+
+namespace Content.Client.Singularity.Components;
+
+///
+/// The client-side version of .
+/// Primarily managed by .
+///
+[RegisterComponent]
+[ComponentReference(typeof(SharedSingularityComponent))]
+public sealed class SingularityComponent : SharedSingularityComponent
+{}
diff --git a/Content.Client/Singularity/SingularitySystem.cs b/Content.Client/Singularity/SingularitySystem.cs
deleted file mode 100644
index 2790a37e4d..0000000000
--- a/Content.Client/Singularity/SingularitySystem.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.Singularity;
-
-namespace Content.Client.Singularity
-{
- public sealed class SingularitySystem : SharedSingularitySystem
- {
- }
-}
diff --git a/Content.Client/Singularity/Systems/EventHorizonSystem.cs b/Content.Client/Singularity/Systems/EventHorizonSystem.cs
new file mode 100644
index 0000000000..3dd63a0c9c
--- /dev/null
+++ b/Content.Client/Singularity/Systems/EventHorizonSystem.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Singularity.EntitySystems;
+using Content.Shared.Singularity.Components;
+
+namespace Content.Client.Singularity.EntitySystems;
+
+///
+/// The client-side version of .
+/// Primarily manages s.
+/// Exists to make relevant signal handlers (ie: ) work on the client.
+///
+public sealed class EventHorizonSystem : SharedEventHorizonSystem
+{}
diff --git a/Content.Client/Singularity/Systems/SingularitySystem.cs b/Content.Client/Singularity/Systems/SingularitySystem.cs
new file mode 100644
index 0000000000..4aad041653
--- /dev/null
+++ b/Content.Client/Singularity/Systems/SingularitySystem.cs
@@ -0,0 +1,33 @@
+using Robust.Shared.GameStates;
+using Content.Shared.Singularity.Components;
+using Content.Shared.Singularity.EntitySystems;
+
+namespace Content.Client.Singularity.EntitySystems;
+
+///
+/// The client-side version of .
+/// Primarily manages s.
+///
+public sealed class SingularitySystem : SharedSingularitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(HandleSingularityState);
+ }
+
+ ///
+ /// Handles syncing singularities with their server-side versions.
+ ///
+ /// The uid of the singularity to sync.
+ /// The state of the singularity to sync.
+ /// The event arguments including the state to sync the singularity with.
+ private void HandleSingularityState(EntityUid uid, SharedSingularityComponent comp, ref ComponentHandleState args)
+ {
+ if (args.Current is not SingularityComponentState state)
+ return;
+
+ SetLevel(uid, state.Level, comp);
+ }
+}
diff --git a/Content.Client/Singularity/Visualizers/SingularityVisualizer.cs b/Content.Client/Singularity/Visualizers/SingularityVisualizer.cs
index fa5c7cb333..01f95323df 100644
--- a/Content.Client/Singularity/Visualizers/SingularityVisualizer.cs
+++ b/Content.Client/Singularity/Visualizers/SingularityVisualizer.cs
@@ -32,7 +32,7 @@ namespace Content.Client.Singularity.Visualizers
return;
}
- if (!component.TryGetData(SingularityVisuals.Level, out int level))
+ if (!component.TryGetData(SingularityVisuals.Level, out byte level))
{
return;
}
diff --git a/Content.Server/Physics/Components/RandomWalkComponent.cs b/Content.Server/Physics/Components/RandomWalkComponent.cs
new file mode 100644
index 0000000000..63ba22d7cc
--- /dev/null
+++ b/Content.Server/Physics/Components/RandomWalkComponent.cs
@@ -0,0 +1,63 @@
+using Content.Server.Physics.Controllers;
+
+namespace Content.Server.Physics.Components;
+
+///
+/// A component which makes its entity move around at random.
+///
+[RegisterComponent]
+public sealed class RandomWalkComponent : Component
+{
+ ///
+ /// The minimum speed at which this entity will move.
+ ///
+ [DataField("minSpeed")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MinSpeed = 7.5f;
+
+ ///
+ /// The maximum speed at which this entity will move.
+ ///
+ [DataField("maxSpeed")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MaxSpeed = 10f;
+
+ ///
+ /// The amount of speed carried over when the speed updates.
+ ///
+ [DataField("accumulatorRatio")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float AccumulatorRatio = 0.0f;
+
+ ///
+ /// Whether this random walker should take a step immediately when it starts up.
+ ///
+ [DataField("stepOnStartup")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ public bool StepOnStartup = false;
+
+ #region Update Timing
+
+ ///
+ /// The minimum amount of time between speed updates.
+ ///
+ [DataField("minStepCooldown")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan MinStepCooldown { get; internal set; } = TimeSpan.FromSeconds(2.0);
+
+ ///
+ /// The maximum amount of time between speed updates.
+ ///
+ [DataField("maxStepCooldown")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan MaxStepCooldown { get; internal set; } = TimeSpan.FromSeconds(5.0);
+
+ ///
+ /// The next time this should update its speed.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [Access(typeof(RandomWalkController))]
+ public TimeSpan NextStepTime { get; internal set; } = default!;
+
+ #endregion Update Timing
+}
diff --git a/Content.Server/Physics/Controllers/RandomWalkController.cs b/Content.Server/Physics/Controllers/RandomWalkController.cs
new file mode 100644
index 0000000000..13178805a1
--- /dev/null
+++ b/Content.Server/Physics/Controllers/RandomWalkController.cs
@@ -0,0 +1,89 @@
+using Robust.Server.GameObjects;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Controllers;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+using Content.Server.Physics.Components;
+using Content.Shared.Throwing;
+
+namespace Content.Server.Physics.Controllers;
+
+///
+/// The entity system responsible for managing s.
+/// Handles updating the direction they move in when their cooldown elapses.
+///
+internal sealed class RandomWalkController : VirtualController
+{
+ #region Dependencies
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PhysicsSystem _physics = default!;
+ #endregion Dependencies
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRandomWalkStartup);
+ }
+
+ ///
+ /// Updates the cooldowns of all random walkers.
+ /// If each of them is off cooldown it updates their velocity and resets its cooldown.
+ ///
+ /// ??? Not documented anywhere I can see ??? // TODO: Document this.
+ /// The amount of time that has elapsed since the last time random walk cooldowns were updated.
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
+
+ foreach(var (randomWalk, physics) in EntityManager.EntityQuery())
+ {
+ if (EntityManager.HasComponent(randomWalk.Owner)
+ || EntityManager.HasComponent(randomWalk.Owner))
+ continue;
+
+ var curTime = _timing.CurTime;
+ if (randomWalk.NextStepTime <= curTime)
+ Update(randomWalk.Owner, randomWalk, physics);
+ }
+ }
+
+ ///
+ /// Updates the direction and speed a random walker is moving at.
+ /// Also resets the random walker's cooldown.
+ ///
+ /// The random walker state.
+ /// The physics body associated with the random walker.
+ public void Update(EntityUid uid, RandomWalkComponent? randomWalk = null, PhysicsComponent? physics = null)
+ {
+ if(!Resolve(uid, ref randomWalk))
+ return;
+
+ var curTime = _timing.CurTime;
+ randomWalk.NextStepTime = curTime + TimeSpan.FromSeconds(_random.NextDouble(randomWalk.MinStepCooldown.TotalSeconds, randomWalk.MaxStepCooldown.TotalSeconds));
+ if(!Resolve(randomWalk.Owner, ref physics))
+ return;
+
+ var pushAngle = _random.NextAngle();
+ var pushStrength = _random.NextFloat(randomWalk.MinSpeed, randomWalk.MaxSpeed);
+
+ _physics.SetLinearVelocity(physics, physics.LinearVelocity * randomWalk.AccumulatorRatio);
+ _physics.ApplyLinearImpulse(physics, pushAngle.ToVec() * (pushStrength * physics.Mass));
+ }
+
+ ///
+ /// Syncs up a random walker step timing when the component starts up.
+ ///
+ /// The uid of the random walker to start up.
+ /// The state of the random walker to start up.
+ /// The startup prompt arguments.
+ private void OnRandomWalkStartup(EntityUid uid, RandomWalkComponent comp, ComponentStartup args)
+ {
+ if (comp.StepOnStartup)
+ Update(uid, comp);
+ else
+ comp.NextStepTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextDouble(comp.MinStepCooldown.TotalSeconds, comp.MaxStepCooldown.TotalSeconds));
+ }
+}
diff --git a/Content.Server/Physics/Controllers/SingularityController.cs b/Content.Server/Physics/Controllers/SingularityController.cs
deleted file mode 100644
index 3ab436b77d..0000000000
--- a/Content.Server/Physics/Controllers/SingularityController.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Content.Server.Singularity.Components;
-using Robust.Server.GameObjects;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Controllers;
-using Robust.Shared.Random;
-
-namespace Content.Server.Physics.Controllers
-{
- internal sealed class SingularityController : VirtualController
- {
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
-
- // SS13 has 10s but that's quite a while
- private const float MaxMoveCooldown = 5f;
- private const float MinMoveCooldown = 2f;
-
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
- {
- base.UpdateBeforeSolve(prediction, frameTime);
-
- foreach (var (singularity, physics) in EntityManager.EntityQuery())
- {
- if (EntityManager.HasComponent(singularity.Owner) ||
- singularity.BeingDeletedByAnotherSingularity) continue;
-
- singularity.MoveAccumulator -= frameTime;
-
- if (singularity.MoveAccumulator > 0f) continue;
-
- singularity.MoveAccumulator = _robustRandom.NextFloat(MinMoveCooldown, MaxMoveCooldown);
-
- MoveSingulo(singularity, physics);
- }
- }
-
- private void MoveSingulo(ServerSingularityComponent singularity, PhysicsComponent physics)
- {
- // TODO: Need to make this events instead.
- if (singularity.Level <= 1)
- {
- physics.BodyStatus = BodyStatus.OnGround;
- return;
- }
-
- // TODO: Could try gradual changes instead
- var pushAngle = _robustRandom.NextAngle();
- var pushStrength = _robustRandom.NextFloat(0.75f, 1.0f);
-
- physics.LinearVelocity = Vector2.Zero;
- physics.BodyStatus = BodyStatus.InAir;
- physics.ApplyLinearImpulse(pushAngle.ToVec() * (pushStrength + 10f / Math.Min(singularity.Level, 4) * physics.Mass));
- // TODO: Speedcap it probably?
- }
- }
-}
diff --git a/Content.Server/Singularity/Components/GravityWellComponent.cs b/Content.Server/Singularity/Components/GravityWellComponent.cs
new file mode 100644
index 0000000000..53993e3798
--- /dev/null
+++ b/Content.Server/Singularity/Components/GravityWellComponent.cs
@@ -0,0 +1,71 @@
+using Content.Shared.Singularity.Components;
+using Content.Server.Singularity.EntitySystems;
+
+namespace Content.Server.Singularity.Components;
+
+///
+/// The server-side version of .
+/// Primarily managed by .
+///
+[RegisterComponent]
+public sealed class GravityWellComponent : Component
+{
+ ///
+ /// The maximum range at which the gravity well can push/pull entities.
+ ///
+ [DataField("maxRange")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MaxRange;
+
+ ///
+ /// The minimum range at which the gravity well can push/pull entities.
+ /// This is effectively hardfloored at .
+ ///
+ [DataField("minRange")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MinRange = 0f;
+
+ ///
+ /// The acceleration entities will experience towards the gravity well at a distance of 1m.
+ /// Negative values accelerate entities away from the gravity well.
+ /// Actual acceleration scales with the inverse of the distance to the singularity.
+ ///
+ [DataField("baseRadialAcceleration")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BaseRadialAcceleration = 0.0f;
+
+ ///
+ /// The acceleration entities will experience tangent to the gravity well at a distance of 1m.
+ /// Positive tangential acceleration is counter-clockwise.
+ /// Actual acceleration scales with the inverse of the distance to the singularity.
+ ///
+ [DataField("baseTangentialAcceleration")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BaseTangentialAcceleration = 0.0f;
+
+ #region Update Timing
+
+ ///
+ /// The amount of time that should elapse between automated updates to this gravity well.
+ ///
+ [DataField("gravPulsePeriod")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(GravityWellSystem))]
+ public TimeSpan TargetPulsePeriod { get; internal set; } = TimeSpan.FromSeconds(0.5);
+
+ ///
+ /// The next time at which this gravity well should pulse.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(GravityWellSystem))]
+ public TimeSpan NextPulseTime { get; internal set; } = default!;
+
+ ///
+ /// The last time this gravity well pulsed.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(GravityWellSystem))]
+ public TimeSpan LastPulseTime { get; internal set; } = default!;
+
+ #endregion Update Timing
+}
diff --git a/Content.Server/Singularity/Components/ServerSingularityComponent.cs b/Content.Server/Singularity/Components/ServerSingularityComponent.cs
deleted file mode 100644
index 389af2f673..0000000000
--- a/Content.Server/Singularity/Components/ServerSingularityComponent.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-using Content.Shared.Singularity;
-using Content.Shared.Singularity.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-
-namespace Content.Server.Singularity.Components
-{
- [RegisterComponent]
- [ComponentReference(typeof(SharedSingularityComponent))]
- public sealed class ServerSingularityComponent : SharedSingularityComponent
- {
- [Dependency] private readonly IEntityManager _entMan = default!;
-
- private SharedSingularitySystem _singularitySystem = default!;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public int Energy
- {
- get => _energy;
- set
- {
- if (value == _energy) return;
-
- _energy = value;
- if (_energy <= 0)
- {
- _entMan.DeleteEntity(Owner);
- return;
- }
-
- var level = _energy switch
- {
- >= 1500 => 6,
- >= 1000 => 5,
- >= 600 => 4,
- >= 300 => 3,
- >= 200 => 2,
- < 200 => 1
- };
- _singularitySystem.ChangeSingularityLevel(this, level);
- }
- }
- private int _energy = 180;
-
- [ViewVariables]
- public int EnergyDrain =>
- Level switch
- {
- 6 => 20,
- 5 => 15,
- 4 => 10,
- 3 => 5,
- 2 => 2,
- 1 => 1,
- _ => 0
- };
-
- [DataField("moveAccumulator")]
- public float MoveAccumulator;
-
- // This is an interesting little workaround.
- // See, two singularities queuing deletion of each other at the same time will annihilate.
- // This is undesirable behaviour, so this flag allows the imperatively first one processed to take priority.
- [ViewVariables(VVAccess.ReadWrite)]
- public bool BeingDeletedByAnotherSingularity { get; set; }
-
- [DataField("singularityFormingSound")] private SoundSpecifier _singularityFormingSound = new SoundPathSpecifier("/Audio/Effects/singularity_form.ogg");
- [DataField("singularityCollapsingSound")] private SoundSpecifier _singularityCollapsingSound = new SoundPathSpecifier("/Audio/Effects/singularity_collapse.ogg");
-
- public override ComponentState GetComponentState()
- {
- return new SingularityComponentState(Level);
- }
-
- protected override void Initialize()
- {
- base.Initialize();
-
- _singularitySystem = EntitySystem.Get();
-
- var audioParams = AudioParams.Default;
- audioParams.Loop = true;
- audioParams.MaxDistance = 20f;
- audioParams.Volume = 5;
- SoundSystem.Play(_singularityFormingSound.GetSound(), Filter.Pvs(Owner), Owner);
-
- _singularitySystem.ChangeSingularityLevel(this, 1);
- }
-
- protected override void Shutdown()
- {
- base.Shutdown();
- SoundSystem.Play(_singularityCollapsingSound.GetSound(), Filter.Pvs(Owner), _entMan.GetComponent(Owner).Coordinates);
- }
- }
-}
diff --git a/Content.Server/Singularity/Components/SingularityComponent.cs b/Content.Server/Singularity/Components/SingularityComponent.cs
new file mode 100644
index 0000000000..7a67b9ba49
--- /dev/null
+++ b/Content.Server/Singularity/Components/SingularityComponent.cs
@@ -0,0 +1,92 @@
+using Content.Shared.Singularity.Components;
+using Content.Server.Singularity.EntitySystems;
+using Robust.Shared.Audio;
+
+namespace Content.Server.Singularity.Components;
+
+///
+/// The server-side version of .
+/// Primarily managed by .
+///
+[RegisterComponent]
+[ComponentReference(typeof(SharedSingularityComponent))]
+public sealed class SingularityComponent : SharedSingularityComponent
+{
+ ///
+ /// The amount of energy this singularity contains.
+ /// If you want to set this go through
+ ///
+ [DataField("energy")]
+ [Access(friends:typeof(SingularitySystem))]
+ public float Energy = 180f;
+
+ ///
+ /// The rate at which this singularity loses energy over time.
+ ///
+ [DataField("energyLoss")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float EnergyDrain;
+
+ #region Audio
+
+ ///
+ /// The sound that this singularity produces by existing.
+ ///
+ [DataField("ambientSound")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ public SoundSpecifier? AmbientSound = new SoundPathSpecifier(
+ "/Audio/Effects/singularity_form.ogg",
+ AudioParams.Default.WithVolume(5).WithLoop(true).WithMaxDistance(20f)
+ );
+
+ ///
+ /// The audio stream that plays the sound specified by on loop.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public IPlayingAudioStream? AmbientSoundStream = null;
+
+ ///
+ /// The sound that the singularity produces when it forms.
+ ///
+ [DataField("formationSound")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ public SoundSpecifier? FormationSound = null;
+
+ ///
+ /// The sound that the singularity produces when it dissipates.
+ ///
+ [DataField("dissipationSound")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public SoundSpecifier? DissipationSound = new SoundPathSpecifier(
+ "/Audio/Effects/singularity_collapse.ogg",
+ AudioParams.Default
+ );
+
+ #endregion Audio
+
+ #region Update Timing
+
+ ///
+ /// The amount of time that should elapse between automated updates to this singularity.
+ ///
+ [DataField("updatePeriod")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SingularitySystem))]
+ public TimeSpan TargetUpdatePeriod { get; internal set; } = TimeSpan.FromSeconds(1.0);
+
+ ///
+ /// The next time this singularity should be updated by
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SingularitySystem))]
+ public TimeSpan NextUpdateTime { get; internal set; } = default!;
+
+ ///
+ /// The last time this singularity was be updated by
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SingularitySystem))]
+ public TimeSpan LastUpdateTime { get; internal set; } = default!;
+
+ #endregion Update Timing
+}
diff --git a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
index 57fb80eb6d..59a17e834a 100644
--- a/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
+++ b/Content.Server/Singularity/Components/SingularityGeneratorComponent.cs
@@ -1,26 +1,33 @@
-namespace Content.Server.Singularity.Components
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+using Content.Server.Singularity.EntitySystems;
+
+namespace Content.Server.Singularity.Components;
+
+[RegisterComponent]
+public sealed class SingularityGeneratorComponent : Component
{
- [RegisterComponent]
- public sealed class SingularityGeneratorComponent : Component
- {
- [Dependency] private readonly IEntityManager _entMan = default!;
+ ///
+ /// The amount of power this generator has accumulated.
+ /// If you want to set this use
+ ///
+ [DataField("power")]
+ [Access(friends:typeof(SingularityGeneratorSystem))]
+ public float Power = 0;
- [ViewVariables] private int _power;
+ ///
+ /// The power threshold at which this generator will spawn a singularity.
+ /// If you want to set this use
+ ///
+ [DataField("threshold")]
+ [Access(friends:typeof(SingularityGeneratorSystem))]
+ public float Threshold = 16;
- public int Power
- {
- get => _power;
- set
- {
- if(_power == value) return;
-
- _power = value;
- if (_power > 15)
- {
- _entMan.SpawnEntity("Singularity", _entMan.GetComponent(Owner).Coordinates);
- //dont delete ourselves, just wait to get eaten
- }
- }
- }
- }
+ ///
+ /// The prototype ID used to spawn a singularity.
+ ///
+ [DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? SpawnPrototype = "Singularity";
}
diff --git a/Content.Server/Singularity/Components/SinguloFoodComponent.cs b/Content.Server/Singularity/Components/SinguloFoodComponent.cs
index f920985180..0da7363533 100644
--- a/Content.Server/Singularity/Components/SinguloFoodComponent.cs
+++ b/Content.Server/Singularity/Components/SinguloFoodComponent.cs
@@ -8,6 +8,6 @@ namespace Content.Server.Singularity.Components
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("energy")]
- public int Energy { get; set; } = 1;
+ public float Energy { get; set; } = 1f;
}
}
diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs
index 8487c01759..94ac8e9e99 100644
--- a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs
@@ -1,4 +1,5 @@
using Content.Server.Singularity.Components;
+using Content.Server.Singularity.Events;
using Content.Shared.Singularity.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
@@ -22,6 +23,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
+ [Dependency] private readonly AppearanceSystem _visualizer = default!;
public override void Initialize()
{
@@ -34,6 +36,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
SubscribeLocalEvent(OnReanchorEvent);
SubscribeLocalEvent(OnUnanchorAttempt);
SubscribeLocalEvent(OnComponentRemoved);
+ SubscribeLocalEvent(PreventBreach);
}
public override void Update(float frameTime)
@@ -356,24 +359,11 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
///
private void ChangePowerVisualizer(int power, ContainmentFieldGeneratorComponent component)
{
- if (!TryComp(component.Owner, out var appearance))
- return;
-
- if(component.PowerBuffer == 0)
- appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.NoPower);
-
- if (component.PowerBuffer > 0 && component.PowerBuffer < component.PowerMinimum)
- appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.LowPower);
-
- if (component.PowerBuffer >= component.PowerMinimum && component.PowerBuffer < 25)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.MediumPower);
- }
-
- if (component.PowerBuffer == 25)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.HighPower);
- }
+ _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.PowerLight, component.PowerBuffer switch {
+ <=0 => PowerLevelVisuals.NoPower,
+ >=25 => PowerLevelVisuals.HighPower,
+ _ => (component.PowerBuffer < component.PowerMinimum) ? PowerLevelVisuals.LowPower : PowerLevelVisuals.MediumPower
+ });
}
///
@@ -382,36 +372,30 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
///
private void ChangeFieldVisualizer(ContainmentFieldGeneratorComponent component)
{
- if (!TryComp(component.Owner, out var appearance))
- return;
-
- if (component.Connections.Count == 0 && !component.Enabled)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.NoLevel);
- }
-
- if (component.Connections.Count == 0 && component.Enabled)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.On);
- }
-
- if (component.Connections.Count == 1)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.OneField);
- }
-
- if (component.Connections.Count > 1)
- {
- appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.MultipleFields);
- }
+ _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.FieldLight, component.Connections.Count switch {
+ >1 => FieldLevelVisuals.MultipleFields,
+ 1 => FieldLevelVisuals.OneField,
+ _ => component.Enabled ? FieldLevelVisuals.On : FieldLevelVisuals.NoLevel
+ });
}
private void ChangeOnLightVisualizer(ContainmentFieldGeneratorComponent component)
{
- if (!TryComp(component.Owner, out var appearance))
- return;
-
- appearance.SetData(ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
+ _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
}
#endregion
+
+ ///
+ /// Prevents singularities from breaching containment if the containment field generator is connected.
+ ///
+ /// The entity the singularity is trying to eat.
+ /// The containment field generator the singularity is trying to eat.
+ /// The event arguments.
+ private void PreventBreach(EntityUid uid, ContainmentFieldGeneratorComponent comp, EventHorizonAttemptConsumeEntityEvent args)
+ {
+ if (args.Cancelled)
+ return;
+ if (comp.IsConnected && !args.EventHorizon.CanBreachContainment)
+ args.Cancel();
+ }
}
diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs
index a926d98210..13a82691d1 100644
--- a/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldSystem.cs
@@ -1,12 +1,11 @@
using Content.Server.Popups;
using Content.Server.Shuttles.Components;
using Content.Server.Singularity.Components;
+using Content.Server.Singularity.EntitySystems;
+using Content.Server.Singularity.Events;
using Content.Shared.Popups;
-using Content.Shared.Tag;
using Content.Shared.Throwing;
-using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
@@ -22,6 +21,7 @@ public sealed class ContainmentFieldSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent(HandleFieldCollide);
+ SubscribeLocalEvent(HandleEventHorizon);
}
private void HandleFieldCollide(EntityUid uid, ContainmentFieldComponent component, ref StartCollideEvent args)
@@ -42,4 +42,10 @@ public sealed class ContainmentFieldSystem : EntitySystem
_throwing.TryThrow(otherBody, playerDir-fieldDir, strength: component.ThrowForce);
}
}
+
+ private void HandleEventHorizon(EntityUid uid, ContainmentFieldComponent component, EventHorizonAttemptConsumeEntityEvent args)
+ {
+ if(!args.Cancelled && !args.EventHorizon.CanBreachContainment)
+ args.Cancel();
+ }
}
diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
index 9511f7a8f2..b7967898bd 100644
--- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs
@@ -15,6 +15,7 @@ using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -31,6 +32,7 @@ namespace Content.Server.Singularity.EntitySystems
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ProjectileSystem _projectile = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
public override void Initialize()
{
@@ -205,7 +207,8 @@ namespace Content.Server.Singularity.EntitySystems
private void Fire(EmitterComponent component)
{
- var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent(component.Owner).Coordinates);
+ var uid = component.Owner;
+ var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent(uid).Coordinates);
if (!EntityManager.TryGetComponent(projectile, out var physicsComponent))
{
@@ -223,9 +226,9 @@ namespace Content.Server.Singularity.EntitySystems
_projectile.SetShooter(projectileComponent, component.Owner);
- physicsComponent
- .LinearVelocity = EntityManager.GetComponent(component.Owner).WorldRotation.ToWorldVec() * 20f;
- EntityManager.GetComponent(projectile).WorldRotation = EntityManager.GetComponent(component.Owner).WorldRotation;
+ var worldRotation = Transform(uid).WorldRotation;
+ _physics.SetLinearVelocity(physicsComponent, worldRotation.ToWorldVec() * 20f);
+ Transform(projectile).WorldRotation = worldRotation;
// TODO: Move to projectile's code.
Timer.Spawn(3000, () => EntityManager.DeleteEntity(projectile));
diff --git a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs
new file mode 100644
index 0000000000..730a274ae7
--- /dev/null
+++ b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs
@@ -0,0 +1,507 @@
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Events;
+
+using Content.Shared.Singularity.Components;
+using Content.Shared.Singularity.EntitySystems;
+
+using Content.Server.Ghost.Components;
+using Content.Server.Station.Components;
+using Content.Server.Singularity.Components;
+using Content.Server.Singularity.Events;
+
+namespace Content.Server.Singularity.EntitySystems;
+
+///
+/// The entity system primarily responsible for managing s.
+/// Handles their consumption of entities.
+///
+public sealed class EventHorizonSystem : SharedEventHorizonSystem
+{
+#region Dependencies
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IMapManager _mapMan = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+#endregion Dependencies
+
+ ///
+ /// The maximum number of nested containers an event horizon is allowed to eat through in an attempt to get to the map.
+ ///
+ private const int MaxEventHorizonUnnestingIterations = 100;
+
+ ///
+ /// The maximum number of nested containers an immune entity in a container being consumed by an event horizon is allowed to search through before it gives up and just jumps to the map.
+ ///
+ private const int MaxEventHorizonDumpSearchIterations = 100;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(PreventConsume);
+ SubscribeLocalEvent(PreventConsume);
+ SubscribeLocalEvent(PreventConsume);
+ SubscribeLocalEvent(OnStartCollide);
+ SubscribeLocalEvent(OnEventHorizonContained);
+ SubscribeLocalEvent(OnEventHorizonContained);
+ SubscribeLocalEvent(OnAnotherEventHorizonAttemptConsumeThisEventHorizon);
+ SubscribeLocalEvent(OnAnotherEventHorizonConsumedThisEventHorizon);
+ SubscribeLocalEvent(OnContainerConsumed);
+
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.AddPath(nameof(EventHorizonComponent.TargetConsumePeriod), (_, comp) => comp.TargetConsumePeriod, SetConsumePeriod);
+ }
+
+ public override void Shutdown()
+ {
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.RemovePath(nameof(EventHorizonComponent.TargetConsumePeriod));
+
+ base.Shutdown();
+ }
+
+ ///
+ /// Updates the cooldowns of all event horizons.
+ /// If an event horizon are off cooldown this makes it consume everything within range and resets their cooldown.
+ ///
+ /// The amount of time that has elapsed since the last cooldown update.
+ public override void Update(float frameTime)
+ {
+ if(!_timing.IsFirstTimePredicted)
+ return;
+
+ foreach(var (eventHorizon, xform) in EntityManager.EntityQuery())
+ {
+ var curTime = _timing.CurTime;
+ if (eventHorizon.NextConsumeWaveTime <= curTime)
+ Update(eventHorizon.Owner, eventHorizon, xform);
+ }
+ }
+
+ ///
+ /// Makes an event horizon consume everything nearby and resets the cooldown it for the next automated wave.
+ ///
+ /// The uid of the event horizon consuming everything nearby.
+ /// The event horizon we want to consume nearby things.
+ /// The transform of the event horizon.
+ public void Update(EntityUid uid, EventHorizonComponent? eventHorizon = null, TransformComponent? xform = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ eventHorizon.LastConsumeWaveTime = _timing.CurTime;
+ eventHorizon.NextConsumeWaveTime = eventHorizon.LastConsumeWaveTime + eventHorizon.TargetConsumePeriod;
+ if (eventHorizon.BeingConsumedByAnotherEventHorizon)
+ return;
+ if(!Resolve(uid, ref xform))
+ return;
+
+ // Handle singularities some admin smited into a locker.
+ if (_containerSystem.TryGetContainingContainer(uid, out var container, transform: xform)
+ && !AttemptConsumeEntity(container.Owner, eventHorizon))
+ {
+ ConsumeEntitiesInContainer(uid, container, eventHorizon, container);
+ return;
+ }
+
+ if (eventHorizon.Radius > 0.0f)
+ ConsumeEverythingInRange(xform.Owner, eventHorizon.Radius, xform, eventHorizon);
+ }
+
+#region Consume
+
+#region Consume Entities
+
+ ///
+ /// Makes an event horizon consume a given entity.
+ ///
+ /// The entity to consume.
+ /// The event horizon consuming the given entity.
+ /// The innermost container of the entity to consume that isn't also being consumed by the event horizon.
+ public void ConsumeEntity(EntityUid uid, EventHorizonComponent eventHorizon, IContainer? outerContainer = null)
+ {
+ EntityManager.QueueDeleteEntity(uid);
+ RaiseLocalEvent(eventHorizon.Owner, new EntityConsumedByEventHorizonEvent(uid, eventHorizon, outerContainer));
+ RaiseLocalEvent(uid, new EventHorizonConsumedEntityEvent(uid, eventHorizon, outerContainer));
+ }
+
+ ///
+ /// Makes an event horizon attempt to consume a given entity.
+ ///
+ /// The entity to attempt to consume.
+ /// The event horizon attempting to consume the given entity.
+ /// The innermost container of the entity to consume that isn't also being consumed by the event horizon.
+ public bool AttemptConsumeEntity(EntityUid uid, EventHorizonComponent eventHorizon, IContainer? outerContainer = null)
+ {
+ if(!CanConsumeEntity(uid, eventHorizon))
+ return false;
+
+ ConsumeEntity(uid, eventHorizon, outerContainer);
+ return true;
+ }
+
+ ///
+ /// Checks whether an event horizon can consume a given entity.
+ ///
+ /// The entity to check for consumability.
+ /// The event horizon checking whether it can consume the entity.
+ public bool CanConsumeEntity(EntityUid uid, EventHorizonComponent eventHorizon)
+ {
+ var ev = new EventHorizonAttemptConsumeEntityEvent(uid, eventHorizon);
+ RaiseLocalEvent(uid, ev);
+ return !ev.Cancelled;
+ }
+
+ ///
+ /// Attempts to consume all entities within a given distance of an entity;
+ /// Excludes the center entity.
+ ///
+ /// The entity uid in the center of the region to consume all entities within.
+ /// The distance of the center entity within which to consume all entities.
+ /// The transform component attached to the center entity.
+ /// The event horizon component attached to the center entity.
+ public void ConsumeEntitiesInRange(EntityUid uid, float range, TransformComponent? xform = null, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref xform, ref eventHorizon))
+ return;
+
+ foreach(var entity in _lookup.GetEntitiesInRange(xform.MapPosition, range, flags: LookupFlags.Uncontained))
+ {
+ if (entity == uid)
+ continue;
+
+ AttemptConsumeEntity(entity, eventHorizon);
+ }
+ }
+
+ ///
+ /// Attempts to consume all entities within a container.
+ /// Excludes the event horizon itself.
+ /// All immune entities within the container will be dumped to a given container or the map/grid if that is impossible.
+ ///
+ /// The uid of the event horizon. The single entity that is immune-by-default.
+ /// The container within which to consume all entities.
+ /// The state of the event horizon.
+ /// The location any immune entities within the container should be dumped to.
+ public void ConsumeEntitiesInContainer(EntityUid uid, IContainer container, EventHorizonComponent eventHorizon, IContainer? outerContainer = null) {
+ // Removing the immune entities from the container needs to be deferred until after iteration or the iterator raises an error.
+ List immune = new();
+
+ foreach(var entity in container.ContainedEntities)
+ {
+ if (entity == uid || !AttemptConsumeEntity(entity, eventHorizon, outerContainer))
+ immune.Add(entity); // The first check keeps singularities an admin smited into a locker from consuming themselves.
+ // The second check keeps things that have been rendered immune to singularities from being deleted by a singularity eating their container.
+ }
+
+ if (outerContainer == container)
+ return; // The container we are intended to drop immune things to is the same container we are consuming everything in
+ // it's a safe bet that we aren't consuming the container entity so there's no reason to eject anything from this container.
+
+ // We need to get the immune things out of the container because the chances are we are about to eat the container and we don't want them to get deleted despite their immunity.
+ foreach(var entity in immune)
+ {
+ // Attempt to insert immune entities into innermost container at least as outer as outerContainer.
+ var target_container = outerContainer;
+ while(target_container != null)
+ {
+ if (target_container.Insert(entity))
+ break;
+
+ _containerSystem.TryGetContainingContainer(target_container.Owner, out target_container);
+ }
+
+ // If we couldn't or there was no container to insert into just dump them to the map/grid.
+ if (target_container == null)
+ Transform(entity).AttachToGridOrMap();
+ }
+ }
+
+#endregion Consume Entities
+
+#region Consume Tiles
+
+ ///
+ /// Makes an event horizon consume a specific tile on a grid.
+ ///
+ /// The tile to consume.
+ /// The event horizon which is consuming the tile on the grid.
+ public void ConsumeTile(TileRef tile, EventHorizonComponent eventHorizon)
+ => ConsumeTiles(new List<(Vector2i, Tile)>(new []{(tile.GridIndices, Tile.Empty)}), _mapMan.GetGrid(tile.GridUid), eventHorizon);
+
+ ///
+ /// Makes an event horizon attempt to consume a specific tile on a grid.
+ ///
+ /// The tile to attempt to consume.
+ /// The event horizon which is attempting to consume the tile on the grid.
+ public void AttemptConsumeTile(TileRef tile, EventHorizonComponent eventHorizon)
+ => AttemptConsumeTiles(new TileRef[1]{tile}, _mapMan.GetGrid(tile.GridUid), eventHorizon);
+
+ ///
+ /// Makes an event horizon consume a set of tiles on a grid.
+ ///
+ /// The tiles to consume.
+ /// The grid hosting the tiles to consume.
+ /// The event horizon which is consuming the tiles on the grid.
+ public void ConsumeTiles(List<(Vector2i, Tile)> tiles, MapGridComponent grid, EventHorizonComponent eventHorizon)
+ {
+ if (tiles.Count > 0)
+ RaiseLocalEvent(eventHorizon.Owner, new TilesConsumedByEventHorizonEvent(tiles, grid, eventHorizon));
+ grid.SetTiles(tiles);
+ }
+
+ ///
+ /// Makes an event horizon attempt to consume a set of tiles on a grid.
+ ///
+ /// The tiles to attempt to consume.
+ /// The grid hosting the tiles to attempt to consume.
+ /// The event horizon which is attempting to consume the tiles on the grid.
+ public int AttemptConsumeTiles(IEnumerable tiles, MapGridComponent grid, EventHorizonComponent eventHorizon)
+ {
+ var toConsume = new List<(Vector2i, Tile)>();
+ foreach(var tile in tiles) {
+ if (CanConsumeTile(tile, grid, eventHorizon))
+ toConsume.Add((tile.GridIndices, Tile.Empty));
+ }
+
+ var result = toConsume.Count;
+ if (toConsume.Count > 0)
+ ConsumeTiles(toConsume, grid, eventHorizon);
+ return result;
+ }
+
+ ///
+ /// Checks whether an event horizon can consume a given tile.
+ /// This is only possible if it can also consume all entities anchored to the tile.
+ ///
+ /// The tile to check for consumability.
+ /// The grid hosting the tile to check.
+ /// The event horizon which is checking to see if it can consume the tile on the grid.
+ public bool CanConsumeTile(TileRef tile, MapGridComponent grid, EventHorizonComponent eventHorizon)
+ {
+ foreach(var blockingEntity in grid.GetAnchoredEntities(tile.GridIndices))
+ {
+ if(!CanConsumeEntity(blockingEntity, eventHorizon))
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Consumes all tiles within a given distance of an entity.
+ /// Some entities are immune to consumption.
+ ///
+ /// The entity uid in the center of the region to consume all tiles within.
+ /// The distance of the center entity within which to consume all tiles.
+ /// The transform component attached to the center entity.
+ /// The event horizon component attached to the center entity.
+ public void ConsumeTilesInRange(EntityUid uid, float range, TransformComponent? xform, EventHorizonComponent? eventHorizon)
+ {
+ if(!Resolve(uid, ref xform) || !Resolve(uid, ref eventHorizon))
+ return;
+
+ var mapPos = xform.MapPosition;
+ var box = Box2.CenteredAround(mapPos.Position, new Vector2(range, range));
+ var circle = new Circle(mapPos.Position, range);
+ foreach(var grid in _mapMan.FindGridsIntersecting(mapPos.MapId, box))
+ {
+ AttemptConsumeTiles(grid.GetTilesIntersecting(circle), grid, eventHorizon);
+ }
+ }
+
+#endregion Consume Tiles
+
+ ///
+ /// Consumes most entities and tiles within a given distance of an entity.
+ /// Some entities are immune to consumption.
+ ///
+ /// The entity uid in the center of the region to consume everything within.
+ /// The distance of the center entity within which to consume everything.
+ /// The transform component attached to the center entity.
+ /// The event horizon component attached to the center entity.
+ public void ConsumeEverythingInRange(EntityUid uid, float range, TransformComponent? xform = null, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref xform, ref eventHorizon))
+ return;
+
+ ConsumeEntitiesInRange(uid, range, xform, eventHorizon);
+ ConsumeTilesInRange(uid, range, xform, eventHorizon);
+ }
+
+#endregion Consume
+
+ #region Getters/Setters
+
+ ///
+ /// Sets how often an event horizon will scan for overlapping entities to consume.
+ /// The value is specifically how long the subsystem should wait between scans.
+ /// If the new scanning period would have already prompted a scan given the previous scan time one is prompted immediately.
+ ///
+ /// The uid of the event horizon to set the consume wave period for.
+ /// The amount of time that this subsystem should wait between scans.
+ /// The state of the event horizon to set the consume wave period for.
+ public void SetConsumePeriod(EntityUid uid, TimeSpan value, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ if (MathHelper.CloseTo(eventHorizon.TargetConsumePeriod.TotalSeconds, value.TotalSeconds))
+ return;
+
+ eventHorizon.TargetConsumePeriod = value;
+ eventHorizon.NextConsumeWaveTime = eventHorizon.LastConsumeWaveTime + eventHorizon.TargetConsumePeriod;
+
+ var curTime = _timing.CurTime;
+ if (eventHorizon.NextConsumeWaveTime < curTime)
+ Update(uid, eventHorizon);
+ }
+
+ #endregion Getters/Setters
+
+#region Event Handlers
+
+ ///
+ /// Prevents a singularity from colliding with anything it is incapable of consuming.
+ ///
+ /// The event horizon entity that is trying to collide with something.
+ /// The event horizon that is trying to collide with something.
+ /// The event arguments.
+ protected override sealed bool PreventCollide(EntityUid uid, EventHorizonComponent comp, ref PreventCollideEvent args)
+ {
+ if (base.PreventCollide(uid, comp, ref args) || args.Cancelled)
+ return true;
+
+ args.Cancelled = !CanConsumeEntity(args.BodyB.Owner, (EventHorizonComponent)comp);
+ return false;
+ }
+
+ ///
+ /// A generic event handler that prevents singularities from consuming entities with a component of a given type if registered.
+ ///
+ /// The entity the singularity is trying to eat.
+ /// The component the singularity is trying to eat.
+ /// The event arguments.
+ public void PreventConsume(EntityUid uid, TComp comp, EventHorizonAttemptConsumeEntityEvent args)
+ {
+ if(!args.Cancelled)
+ args.Cancel();
+ }
+
+ ///
+ /// A generic event handler that prevents singularities from breaching containment.
+ /// In this case 'breaching containment' means consuming an entity with a component of the given type unless the event horizon is set to breach containment anyway.
+ ///
+ /// The entity the singularity is trying to eat.
+ /// The component the singularity is trying to eat.
+ /// The event arguments.
+ public void PreventBreach(EntityUid uid, TComp comp, EventHorizonAttemptConsumeEntityEvent args)
+ {
+ if (args.Cancelled)
+ return;
+ if(!args.EventHorizon.CanBreachContainment)
+ PreventConsume(uid, comp, args);
+ }
+
+ ///
+ /// Handles event horizons consuming any entities they bump into.
+ /// The event horizon will not consume any entities if it itself has been consumed by an event horizon.
+ ///
+ /// The event horizon entity.
+ /// The event horizon.
+ /// The event arguments.
+ private void OnStartCollide(EntityUid uid, EventHorizonComponent comp, ref StartCollideEvent args)
+ {
+ if (comp.BeingConsumedByAnotherEventHorizon)
+ return;
+ if (args.OurFixture.ID != comp.HorizonFixtureId)
+ return;
+
+ AttemptConsumeEntity(args.OtherFixture.Body.Owner, comp);
+ }
+
+ ///
+ /// Prevents two event horizons from annihilating one another.
+ /// Specifically prevents event horizons from consuming themselves.
+ /// Also ensures that if this event horizon has already been consumed by another event horizon it cannot be consumed again.
+ ///
+ /// The event horizon entity.
+ /// The event horizon.
+ /// The event arguments.
+ private void OnAnotherEventHorizonAttemptConsumeThisEventHorizon(EntityUid uid, EventHorizonComponent comp, EventHorizonAttemptConsumeEntityEvent args)
+ {
+ if(!args.Cancelled && (args.EventHorizon == comp || comp.BeingConsumedByAnotherEventHorizon))
+ args.Cancel();
+ }
+
+ ///
+ /// Prevents two singularities from annihilating one another.
+ /// Specifically ensures if this event horizon is consumed by another event horizon it knows that it has been consumed.
+ ///
+ /// The event horizon entity.
+ /// The event horizon.
+ /// The event arguments.
+ private void OnAnotherEventHorizonConsumedThisEventHorizon(EntityUid uid, EventHorizonComponent comp, EventHorizonConsumedEntityEvent args)
+ {
+ comp.BeingConsumedByAnotherEventHorizon = true;
+ }
+
+ ///
+ /// Handles event horizons deciding to escape containers they are inserted into.
+ /// Delegates the actual escape to on a delay.
+ /// This ensures that the escape is handled after all other handlers for the insertion event and satisfies the assertion that
+ /// the inserted entity SHALL be inside of the specified container after all handles to the entity event
+ /// are processed.
+ ///
+ /// The uid of the event horizon.]
+ /// The state of the event horizon.]
+ /// The arguments of the insertion.]
+ private void OnEventHorizonContained(EntityUid uid, EventHorizonComponent comp, EntGotInsertedIntoContainerMessage args) {
+ // Delegates processing an event until all queued events have been processed.
+ // As of 1:44 AM, Sunday, Dec. 4, 2022 this is the one use for this in the codebase.
+ QueueLocalEvent(new EventHorizonContainedEvent(uid, comp, args));
+ }
+
+ ///
+ /// Handles event horizons attempting to escape containers they have been inserted into.
+ /// If the event horizon has not been consumed by another event horizon this handles making the event horizon consume the containing
+ /// container and drop the the next innermost contaning container.
+ /// This loops until the event horizon has escaped to the map or wound up in an indestructible container.
+ ///
+ /// The arguments for this event.]
+ private void OnEventHorizonContained(EventHorizonContainedEvent args) {
+ var uid = args.Entity;
+ var comp = args.EventHorizon;
+ if (!EntityManager.EntityExists(uid))
+ return;
+ if (comp.BeingConsumedByAnotherEventHorizon)
+ return;
+
+ var containerEntity = args.Args.Container.Owner;
+ if(!(EntityManager.EntityExists(containerEntity) && AttemptConsumeEntity(containerEntity, comp))) {
+ ConsumeEntitiesInContainer(uid, args.Args.Container, comp, args.Args.Container);
+ }
+ }
+
+ ///
+ /// Recursively consumes all entities within a container that is consumed by the singularity.
+ /// If an entity within a consumed container cannot be consumed itself it is removed from the container.
+ ///
+ /// The uid of the container being consumed.
+ /// The state of the container being consumed.
+ /// The event arguments.
+ private void OnContainerConsumed(EntityUid uid, ContainerManagerComponent comp, EventHorizonConsumedEntityEvent args)
+ {
+ var drop_container = args.Container;
+ if (drop_container is null)
+ _containerSystem.TryGetContainingContainer(uid, out drop_container);
+
+ foreach(var container in comp.GetAllContainers())
+ {
+ ConsumeEntitiesInContainer(args.EventHorizon.Owner, container, args.EventHorizon, drop_container);
+ }
+ }
+#endregion Event Handlers
+}
diff --git a/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs
new file mode 100644
index 0000000000..104ae38abd
--- /dev/null
+++ b/Content.Server/Singularity/EntitySystems/GravityWellSystem.cs
@@ -0,0 +1,262 @@
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Timing;
+
+using Content.Shared.Singularity.EntitySystems;
+
+using Content.Server.Ghost.Components;
+using Content.Server.Singularity.Components;
+
+namespace Content.Server.Singularity.EntitySystems;
+
+///
+/// The server side version of .
+/// Primarily responsible for managing s.
+/// Handles the gravitational pulses they can emit.
+///
+public sealed class GravityWellSystem : SharedGravityWellSystem
+{
+#region Dependencies
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IViewVariablesManager _vvManager = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+#endregion Dependencies
+
+ ///
+ /// The minimum range at which gravpulses will act.
+ /// Prevents division by zero problems.
+ ///
+ public const float MinGravPulseRange = 0.00001f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnGravityWellStartup);
+
+ var vvHandle = _vvManager.GetTypeHandler();
+ vvHandle.AddPath(nameof(GravityWellComponent.TargetPulsePeriod), (_, comp) => comp.TargetPulsePeriod, SetPulsePeriod);
+ }
+
+ public override void Shutdown()
+ {
+ var vvHandle = _vvManager.GetTypeHandler();
+ vvHandle.RemovePath(nameof(GravityWellComponent.TargetPulsePeriod));
+ base.Shutdown();
+ }
+
+ ///
+ /// Updates the pulse cooldowns of all gravity wells.
+ /// If they are off cooldown it makes them emit a gravitational pulse and reset their cooldown.
+ ///
+ /// The time elapsed since the last set of updates.
+ public override void Update(float frameTime)
+ {
+ if(!_timing.IsFirstTimePredicted)
+ return;
+
+ foreach(var (gravWell, xform) in EntityManager.EntityQuery())
+ {
+ var curTime = _timing.CurTime;
+ if (gravWell.NextPulseTime <= curTime)
+ Update(gravWell.Owner, curTime - gravWell.LastPulseTime, gravWell, xform);
+ }
+ }
+
+ ///
+ /// Makes a gravity well emit a gravitational pulse and puts it on cooldown.
+ /// The longer since the last gravitational pulse the more force it applies on affected entities.
+ ///
+ /// The uid of the gravity well to make pulse.
+ /// The state of the gravity well to make pulse.
+ /// The transform of the gravity well to make pulse.
+ private void Update(EntityUid uid, GravityWellComponent? gravWell = null, TransformComponent? xform = null)
+ {
+ if (Resolve(uid, ref gravWell))
+ Update(uid, _timing.CurTime - gravWell.LastPulseTime, gravWell, xform);
+ }
+
+ ///
+ /// Makes a gravity well emit a gravitational pulse and puts it on cooldown.
+ ///
+ /// The uid of the gravity well to make pulse.
+ /// The state of the gravity well to make pulse.
+ /// The amount to consider as having passed since the last gravitational pulse by the gravity well. Pulse force scales with this.
+ /// The transform of the gravity well to make pulse.
+ private void Update(EntityUid uid, TimeSpan frameTime, GravityWellComponent? gravWell = null, TransformComponent? xform = null)
+ {
+ if(!Resolve(uid, ref gravWell))
+ return;
+
+ gravWell.LastPulseTime = _timing.CurTime;
+ gravWell.NextPulseTime = gravWell.LastPulseTime + gravWell.TargetPulsePeriod;
+ if (gravWell.MaxRange < 0.0f || !Resolve(uid, ref xform))
+ return;
+
+ var scale = (float)frameTime.TotalSeconds;
+ GravPulse(uid, gravWell.MaxRange, gravWell.MinRange, gravWell.BaseRadialAcceleration * scale, gravWell.BaseTangentialAcceleration * scale, xform);
+ }
+
+ #region GravPulse
+
+ ///
+ /// Checks whether an entity can be affected by gravity pulses.
+ /// TODO: Make this an event or such.
+ ///
+ /// The entity to check.
+ private bool CanGravPulseAffect(EntityUid entity)
+ {
+ return !(
+ EntityManager.HasComponent(entity) ||
+ EntityManager.HasComponent(entity) ||
+ EntityManager.HasComponent(entity) ||
+ EntityManager.HasComponent(entity)
+ );
+ }
+
+ ///
+ /// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The entity at the epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse.
+ /// The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.
+ /// (optional) The transform of the entity at the epicenter of the gravitational pulse.
+ public void GravPulse(EntityUid uid, float maxRange, float minRange, in Matrix3 baseMatrixDeltaV, TransformComponent? xform = null)
+ {
+ if (Resolve(uid, ref xform))
+ GravPulse(xform.Coordinates, maxRange, minRange, in baseMatrixDeltaV);
+ }
+
+ ///
+ /// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The entity at the epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse.
+ /// The base radial velocity that will be added to entities within range towards the center of the gravitational pulse.
+ /// The base tangential velocity that will be added to entities within countrclockwise around the center of the gravitational pulse.
+ /// (optional) The transform of the entity at the epicenter of the gravitational pulse.
+ public void GravPulse(EntityUid uid, float maxRange, float minRange, float baseRadialDeltaV = 0.0f, float baseTangentialDeltaV = 0.0f, TransformComponent? xform = null)
+ {
+ if (Resolve(uid, ref xform))
+ GravPulse(xform.Coordinates, maxRange, minRange, baseRadialDeltaV, baseTangentialDeltaV);
+ }
+
+ ///
+ /// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse.
+ /// The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.
+ public void GravPulse(EntityCoordinates entityPos, float maxRange, float minRange, in Matrix3 baseMatrixDeltaV)
+ => GravPulse(entityPos.ToMap(EntityManager), maxRange, minRange, in baseMatrixDeltaV);
+
+ ///
+ /// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse.
+ /// The base radial velocity that will be added to entities within range towards the center of the gravitational pulse.
+ /// The base tangential velocity that will be added to entities within countrclockwise around the center of the gravitational pulse.
+ public void GravPulse(EntityCoordinates entityPos, float maxRange, float minRange, float baseRadialDeltaV = 0.0f, float baseTangentialDeltaV = 0.0f)
+ => GravPulse(entityPos.ToMap(EntityManager), maxRange, minRange, baseRadialDeltaV, baseTangentialDeltaV);
+
+ ///
+ /// Causes a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse. Exists to prevent div/0 errors.
+ /// The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.
+ public void GravPulse(MapCoordinates mapPos, float maxRange, float minRange, in Matrix3 baseMatrixDeltaV)
+ {
+ if (mapPos == MapCoordinates.Nullspace)
+ return; // No gravpulses in nullspace please.
+
+ var epicenter = mapPos.Position;
+ var minRange2 = MathF.Max(minRange * minRange, MinGravPulseRange); // Cache square value for speed. Also apply a sane minimum value to the minimum value so that div/0s don't happen.
+ foreach(var entity in _lookup.GetEntitiesInRange(mapPos.MapId, epicenter, maxRange, flags: LookupFlags.Dynamic | LookupFlags.Sundries))
+ {
+ if(!TryComp(entity, out var physics)
+ || physics.BodyType == BodyType.Static)
+ continue;
+
+ if(!CanGravPulseAffect(entity))
+ continue;
+
+ var displacement = epicenter - Transform(entity).WorldPosition;
+ var distance2 = displacement.LengthSquared;
+ if (distance2 < minRange2)
+ continue;
+
+ var scaling = (1f / distance2) * physics.Mass; // TODO: Variable falloff gradiants.
+ _physics.ApplyLinearImpulse(physics, (displacement * baseMatrixDeltaV) * scaling);
+ }
+ }
+
+ ///
+ /// Causes a gravitational pulse, shoving around all entities within some distance of an epicenter.
+ ///
+ /// The epicenter of the gravity pulse.
+ /// The maximum distance at which entities can be affected by the gravity pulse.
+ /// The minimum distance at which entities can be affected by the gravity pulse. Exists to prevent div/0 errors.
+ /// The base amount of velocity that will be added to entities in range towards the epicenter of the pulse.
+ /// The base amount of velocity that will be added to entities in range counterclockwise relative to the epicenter of the pulse.
+ public void GravPulse(MapCoordinates mapPos, float maxRange, float minRange = 0.0f, float baseRadialDeltaV = 0.0f, float baseTangentialDeltaV = 0.0f)
+ => GravPulse(mapPos, maxRange, minRange, new Matrix3(
+ baseRadialDeltaV, +baseTangentialDeltaV, 0.0f,
+ -baseTangentialDeltaV, baseRadialDeltaV, 0.0f,
+ 0.0f, 0.0f, 1.0f
+ ));
+
+ #endregion GravPulse
+
+ #region Getters/Setters
+
+ ///
+ /// Sets the pulse period for a gravity well.
+ /// If the new pulse period implies that the gravity well was intended to pulse already it does so immediately.
+ ///
+ /// The uid of the gravity well to set the pulse period for.
+ /// The new pulse period for the gravity well.
+ /// The state of the gravity well to set the pulse period for.
+ public void SetPulsePeriod(EntityUid uid, TimeSpan value, GravityWellComponent? gravWell = null)
+ {
+ if(!Resolve(uid, ref gravWell))
+ return;
+
+ if (MathHelper.CloseTo(gravWell.TargetPulsePeriod.TotalSeconds, value.TotalSeconds))
+ return;
+
+ gravWell.TargetPulsePeriod = value;
+ gravWell.NextPulseTime = gravWell.LastPulseTime + gravWell.TargetPulsePeriod;
+
+ var curTime = _timing.CurTime;
+ if (gravWell.NextPulseTime <= curTime)
+ Update(uid, curTime - gravWell.LastPulseTime, gravWell);
+ }
+
+ #endregion Getters/Setters
+
+ #region Event Handlers
+
+ ///
+ /// Resets the pulse timings of the gravity well when the components starts up.
+ ///
+ /// The uid of the gravity well to start up.
+ /// The state of the gravity well to start up.
+ /// The startup prompt arguments.
+ public void OnGravityWellStartup(EntityUid uid, GravityWellComponent comp, ComponentStartup args)
+ {
+ comp.LastPulseTime = _timing.CurTime;
+ comp.NextPulseTime = comp.LastPulseTime + comp.TargetPulsePeriod;
+ }
+
+ #endregion Event Handlers
+}
diff --git a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
index d416b075d1..dee79427e1 100644
--- a/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
+++ b/Content.Server/Singularity/EntitySystems/SingularityGeneratorSystem.cs
@@ -1,20 +1,126 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Singularity.Components;
using Content.Shared.Singularity.Components;
-using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
namespace Content.Server.Singularity.EntitySystems;
public sealed class SingularityGeneratorSystem : EntitySystem
{
+#region Dependencies
+ [Dependency] private readonly IViewVariablesManager _vvm = default!;
+#endregion Dependencies
+
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(HandleParticleCollide);
+
+ var vvHandle = _vvm.GetTypeHandler();
+ vvHandle.AddPath(nameof(SingularityGeneratorComponent.Power), (_, comp) => comp.Power, SetPower);
+ vvHandle.AddPath(nameof(SingularityGeneratorComponent.Threshold), (_, comp) => comp.Threshold, SetThreshold);
}
+ public override void Shutdown()
+ {
+ var vvHandle = _vvm.GetTypeHandler();
+ vvHandle.RemovePath(nameof(SingularityGeneratorComponent.Power));
+ vvHandle.RemovePath(nameof(SingularityGeneratorComponent.Threshold));
+
+ base.Shutdown();
+ }
+
+
+ ///
+ /// Handles what happens when a singularity generator passes its power threshold.
+ /// Default behavior is to reset the singularities power level and spawn a singularity.
+ ///
+ /// The uid of the singularity generator.
+ /// The state of the singularity generator.
+ private void OnPassThreshold(EntityUid uid, SingularityGeneratorComponent? comp)
+ {
+ if(!Resolve(uid, ref comp))
+ return;
+
+ SetPower(comp, 0);
+ EntityManager.SpawnEntity(comp.SpawnPrototype, Transform(comp.Owner).Coordinates);
+ }
+
+#region Getters/Setters
+ ///
+ /// Setter for
+ /// If the singularity generator passes its threshold it also spawns a singularity.
+ ///
+ /// The singularity generator component.
+ /// The new power level for the generator component to have.
+ public void SetPower(SingularityGeneratorComponent comp, float value)
+ {
+ var oldValue = comp.Power;
+ if (value == oldValue)
+ return;
+
+ comp.Power = value;
+ if (comp.Power >= comp.Threshold && oldValue < comp.Threshold)
+ OnPassThreshold(comp.Owner, comp);
+ }
+
+ ///
+ /// Setter for
+ /// If the singularity generator has passed its new threshold it also spawns a singularity.
+ ///
+ /// The singularity generator component.
+ /// The new threshold power level for the generator component to have.
+ public void SetThreshold(SingularityGeneratorComponent comp, float value)
+ {
+ var oldValue = comp.Threshold;
+ if (value == comp.Threshold)
+ return;
+
+ comp.Power = value;
+ if (comp.Power >= comp.Threshold && comp.Power < oldValue)
+ OnPassThreshold(comp.Owner, comp);
+ }
+#region VV
+ ///
+ /// VV setter for
+ /// If the singularity generator passes its threshold it also spawns a singularity.
+ ///
+ /// The entity hosting the singularity generator that is being modified.
+ /// The value of the new power level the singularity generator should have.
+ /// The singularity generator to change the power level of.
+ public void SetPower(EntityUid uid, float value, SingularityGeneratorComponent? comp)
+ {
+ if(!Resolve(uid, ref comp))
+ return;
+ SetPower(comp, value);
+ }
+
+ ///
+ /// VV setter for
+ /// If the singularity generator has passed its new threshold it also spawns a singularity.
+ ///
+ /// The entity hosting the singularity generator that is being modified.
+ /// The value of the new threshold power level the singularity generator should have.
+ /// The singularity generator to change the threshold power level of.
+ public void SetThreshold(EntityUid uid, float value, SingularityGeneratorComponent? comp)
+ {
+ if(!Resolve(uid, ref comp))
+ return;
+ SetThreshold(comp, value);
+ }
+#endregion VV
+#endregion Getters/Setters
+
+#region Event Handlers
+ ///
+ /// Handles PA Particles colliding with a singularity generator.
+ /// Adds the power from the particles to the generator.
+ /// TODO: Desnowflake this.
+ ///
+ /// The uid of the PA particles have collided with.
+ /// The state of the PA particles.
+ /// The state of the beginning of the collision.
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{
if (EntityManager.TryGetComponent(args.OtherFixture.Body.Owner, out var singularityGeneratorComponent))
@@ -31,4 +137,5 @@ public sealed class SingularityGeneratorSystem : EntitySystem
EntityManager.QueueDeleteEntity(uid);
}
}
+#endregion Event Handlers
}
diff --git a/Content.Server/Singularity/EntitySystems/SingularitySystem.cs b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs
index 9b2e1af8be..878c400e84 100644
--- a/Content.Server/Singularity/EntitySystems/SingularitySystem.cs
+++ b/Content.Server/Singularity/EntitySystems/SingularitySystem.cs
@@ -1,265 +1,350 @@
-using Content.Server.Ghost.Components;
-using Content.Server.Singularity.Components;
-using Content.Server.Station.Components;
-using Content.Shared.Singularity;
-using Content.Shared.Singularity.Components;
-using JetBrains.Annotations;
+using Robust.Shared.GameStates;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
using Robust.Server.GameStates;
-using Robust.Shared.Containers;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Dynamics;
-using Robust.Shared.Physics.Events;
-namespace Content.Server.Singularity.EntitySystems
+using Content.Shared.Singularity.Components;
+using Content.Shared.Singularity.EntitySystems;
+using Content.Shared.Singularity.Events;
+
+using Content.Server.Physics.Components;
+using Content.Server.Singularity.Components;
+using Content.Server.Singularity.Events;
+
+namespace Content.Server.Singularity.EntitySystems;
+
+///
+/// The server-side version of .
+/// Primarily responsible for managing s.
+/// Handles their accumulation of energy upon consuming entities (see ) and gradual dissipation.
+/// Also handles synchronizing server-side components with the singuarities level.
+///
+public sealed class SingularitySystem : SharedSingularitySystem
{
- [UsedImplicitly]
- public sealed class SingularitySystem : SharedSingularitySystem
+#region Dependencies
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly PVSOverrideSystem _pvs = default!;
+#endregion Dependencies
+
+ ///
+ /// The amount of energy singulos accumulate when they eat a tile.
+ ///
+ public const float BaseTileEnergy = 1f;
+
+ ///
+ /// The amount of energy singulos accumulate when they eat an entity.
+ ///
+ public const float BaseEntityEnergy = 1f;
+
+ public override void Initialize()
{
- [Dependency] private readonly EntityLookupSystem _lookup = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly PVSOverrideSystem _pvs = default!;
- ///
- /// How much energy the singulo gains from destroying a tile.
- ///
- private const int TileEnergyGain = 1;
+ base.Initialize();
+ SubscribeLocalEvent(OnDistortionStartup);
+ SubscribeLocalEvent(OnSingularityStartup);
+ SubscribeLocalEvent(OnSingularityShutdown);
+ SubscribeLocalEvent(OnConsumed);
+ SubscribeLocalEvent(OnConsumed);
+ SubscribeLocalEvent(OnConsumedEntity);
+ SubscribeLocalEvent(OnConsumedTiles);
+ SubscribeLocalEvent(UpdateEnergyDrain);
+ SubscribeLocalEvent(HandleSingularityState);
- private const float GravityCooldown = 0.5f;
- private float _gravityAccumulator;
+ // TODO: Figure out where all this coupling should be handled.
+ SubscribeLocalEvent(UpdateRandomWalk);
+ SubscribeLocalEvent(UpdateGravityWell);
- private int _updateInterval = 1;
- private float _accumulator;
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.AddPath(nameof(SingularityComponent.Energy), (_, comp) => comp.Energy, SetEnergy);
+ vvHandle.AddPath(nameof(SingularityComponent.TargetUpdatePeriod), (_, comp) => comp.TargetUpdatePeriod, SetUpdatePeriod);
+ }
- public override void Initialize()
+ public override void Shutdown()
+ {
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.RemovePath(nameof(SingularityComponent.Energy));
+ vvHandle.RemovePath(nameof(SingularityComponent.TargetUpdatePeriod));
+ base.Shutdown();
+ }
+
+ ///
+ /// Handles the gradual dissipation of all singularities.
+ ///
+ /// The amount of time since the last set of updates.
+ public override void Update(float frameTime)
+ {
+ if(!_timing.IsFirstTimePredicted)
+ return;
+
+ foreach(var singularity in EntityManager.EntityQuery())
{
- base.Initialize();
- SubscribeLocalEvent(OnCollide);
- SubscribeLocalEvent(OnDistortionStartup);
- }
-
- private void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent component, ComponentStartup args)
- {
- // to avoid distortion overlay pop-in, entities with distortion ignore PVS. Really this should probably be a
- // PVS range-override, but this is good enough for now.
- _pvs.AddGlobalOverride(uid);
- }
-
- protected override bool PreventCollide(EntityUid uid, SharedSingularityComponent component, ref PreventCollideEvent args)
- {
- if (base.PreventCollide(uid, component, ref args)) return true;
-
- var otherUid = args.BodyB.Owner;
-
- if (args.Cancelled) return true;
-
- // If it's not cancelled then we'll cancel if we can't immediately destroy it on collision
- if (!CanDestroy(component, otherUid))
- args.Cancelled = true;
-
- return true;
- }
-
- private void OnCollide(EntityUid uid, ServerSingularityComponent component, ref StartCollideEvent args)
- {
- if (args.OurFixture.ID != "DeleteCircle") return;
-
- // This handles bouncing off of containment walls.
- // If you want the delete behavior we do it under DeleteEntities for reasons (not everything has physics).
-
- // If we're being deleted by another singularity, this call is probably for that singularity.
- // Even if not, just don't bother.
- if (component.BeingDeletedByAnotherSingularity)
- return;
-
- var otherUid = args.OtherFixture.Body.Owner;
-
- // HandleDestroy will also check CanDestroy for us
- HandleDestroy(component, otherUid);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- _gravityAccumulator += frameTime;
- _accumulator += frameTime;
-
- while (_accumulator > _updateInterval)
- {
- _accumulator -= _updateInterval;
-
- foreach (var singularity in EntityManager.EntityQuery())
- {
- singularity.Energy -= singularity.EnergyDrain;
- }
- }
-
- while (_gravityAccumulator > GravityCooldown)
- {
- _gravityAccumulator -= GravityCooldown;
-
- foreach (var (singularity, xform) in EntityManager.EntityQuery())
- {
- Update(singularity, xform, GravityCooldown);
- }
- }
- }
-
- private void Update(ServerSingularityComponent component, TransformComponent xform, float frameTime)
- {
- if (component.BeingDeletedByAnotherSingularity) return;
-
- var worldPos = xform.WorldPosition;
- DestroyEntities(component, xform, worldPos);
- DestroyTiles(component, xform, worldPos);
- PullEntities(component, xform, worldPos, frameTime);
- }
-
- private float PullRange(ServerSingularityComponent component)
- {
- // Level 6 is normally 15 range but that's yuge.
- return 2 + component.Level * 2;
- }
-
- private float DestroyTileRange(ServerSingularityComponent component)
- {
- return component.Level - 0.5f;
- }
-
- private bool CanDestroy(SharedSingularityComponent component, EntityUid entity)
- {
- return entity != component.Owner &&
- !EntityManager.HasComponent(entity) &&
- !EntityManager.HasComponent(entity) &&
- !EntityManager.HasComponent(entity) && // these SHOULD be in null-space... but just in case. Also, maybe someone moves a singularity there..
- (component.Level > 4 ||
- !EntityManager.HasComponent(entity) &&
- !(EntityManager.TryGetComponent(entity, out var fieldGen) && fieldGen.IsConnected));
- }
-
- private void HandleDestroy(ServerSingularityComponent component, EntityUid entity)
- {
- // TODO: Need singuloimmune tag
- if (!CanDestroy(component, entity)) return;
-
- // Singularity priority management / etc.
- if (EntityManager.TryGetComponent(entity, out var otherSingulo))
- {
- // MERGE
- if (!otherSingulo.BeingDeletedByAnotherSingularity)
- {
- component.Energy += otherSingulo.Energy;
- }
-
- otherSingulo.BeingDeletedByAnotherSingularity = true;
- }
-
- if (EntityManager.TryGetComponent(entity, out var singuloFood))
- component.Energy += singuloFood.Energy;
- else
- component.Energy++;
-
- EntityManager.QueueDeleteEntity(entity);
- }
-
- ///
- /// Handle deleting entities and increasing energy
- ///
- private void DestroyEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos)
- {
- // The reason we don't /just/ use collision is because we'll be deleting stuff that may not necessarily have physics (e.g. carpets).
- var destroyRange = DestroyTileRange(component);
-
- foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, destroyRange))
- {
- HandleDestroy(component, entity);
- }
- }
-
- private bool CanPull(EntityUid entity)
- {
- return !(EntityManager.HasComponent(entity) ||
- EntityManager.HasComponent(entity) ||
- EntityManager.HasComponent(entity) ||
- EntityManager.HasComponent(entity) ||
- _container.IsEntityInContainer(entity));
- }
-
- ///
- /// Pull dynamic bodies in range to the singulo.
- ///
- private void PullEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos, float frameTime)
- {
- // TODO: When we split up dynamic and static trees we might be able to make items always on the broadphase
- // in which case we can just query dynamictree directly for brrt
- var pullRange = PullRange(component);
- var destroyRange = DestroyTileRange(component);
-
- foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, pullRange))
- {
- // I tried having it so level 6 can de-anchor. BAD IDEA, MASSIVE LAG.
- if (entity == component.Owner ||
- !TryComp(entity, out var collidableComponent) ||
- collidableComponent.BodyType == BodyType.Static) continue;
-
- if (!CanPull(entity)) continue;
-
- var vec = worldPos - Transform(entity).WorldPosition;
-
- if (vec.Length < destroyRange - 0.01f) continue;
-
- var speed = 1f / vec.Length * component.Level * collidableComponent.Mass * 10f;
-
- // Because tile friction is so high we'll just multiply by mass so stuff like closets can even move.
- collidableComponent.ApplyLinearImpulse(vec.Normalized * speed * frameTime);
- }
- }
-
- ///
- /// Destroy any grid tiles within the relevant Level range.
- ///
- private void DestroyTiles(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos)
- {
- var radius = DestroyTileRange(component);
-
- var circle = new Circle(worldPos, radius);
- var box = new Box2(worldPos - radius, worldPos + radius);
-
- foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, box))
- {
- // Bundle these together so we can use the faster helper to set tiles.
- var toDestroy = new List<(Vector2i, Tile)>();
-
- foreach (var tile in grid.GetTilesIntersecting(circle))
- {
- if (tile.Tile.IsEmpty) continue;
-
- // Avoid ripping up tiles that may be essential to containment
- if (component.Level < 5)
- {
- var canDelete = true;
-
- foreach (var ent in grid.GetAnchoredEntities(tile.GridIndices))
- {
- if (EntityManager.HasComponent(ent) ||
- EntityManager.HasComponent(ent))
- {
- canDelete = false;
- break;
- }
- }
-
- if (!canDelete) continue;
- }
-
- toDestroy.Add((tile.GridIndices, Tile.Empty));
- }
-
- component.Energy += TileEnergyGain * toDestroy.Count;
- grid.SetTiles(toDestroy);
- }
+ var curTime = _timing.CurTime;
+ if (singularity.NextUpdateTime <= curTime)
+ Update(singularity.Owner, curTime - singularity.LastUpdateTime, singularity);
}
}
+
+ ///
+ /// Handles the gradual energy loss and dissipation of singularity.
+ ///
+ /// The uid of the singularity to update.
+ /// The state of the singularity to update.
+ public void Update(EntityUid uid, SingularityComponent? singularity = null)
+ {
+ if (Resolve(uid, ref singularity))
+ Update(uid, _timing.CurTime - singularity.LastUpdateTime, singularity);
+ }
+
+ ///
+ /// Handles the gradual energy loss and dissipation of a singularity.
+ ///
+ /// The uid of the singularity to update.
+ /// The amount of time that has elapsed since the last update.
+ /// The state of the singularity to update.
+ public void Update(EntityUid uid, TimeSpan frameTime, SingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ singularity.LastUpdateTime = _timing.CurTime;
+ singularity.NextUpdateTime = singularity.LastUpdateTime + singularity.TargetUpdatePeriod;
+ AdjustEnergy(uid, -singularity.EnergyDrain * (float)frameTime.TotalSeconds, singularity: singularity);
+ }
+
+#region Getters/Setters
+
+ ///
+ /// Setter for .
+ /// Also updates the level of the singularity accordingly.
+ ///
+ /// The uid of the singularity to set the energy of.
+ /// The amount of energy for the singularity to have.
+ /// The state of the singularity to set the energy of.
+ public void SetEnergy(EntityUid uid, float value, SingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ var oldValue = singularity.Energy;
+ if (oldValue == value)
+ return;
+
+ singularity.Energy = value;
+ SetLevel(uid, value switch {
+ >= 1500 => 6,
+ >= 1000 => 5,
+ >= 600 => 4,
+ >= 300 => 3,
+ >= 200 => 2,
+ > 0 => 1,
+ _ => 0
+ }, singularity);
+ }
+
+ ///
+ /// Adjusts the amount of energy the singularity has accumulated.
+ ///
+ /// The uid of the singularity to adjust the energy of.
+ /// The amount to adjust the energy of the singuarity.
+ /// The minimum amount of energy for the singularity to be adjusted to.
+ /// The maximum amount of energy for the singularity to be adjusted to.
+ /// Whether the amount of energy in the singularity should be forced to within the specified range if it already is below it.
+ /// Whether the amount of energy in the singularity should be forced to within the specified range if it already is above it.
+ /// The state of the singularity to adjust the energy of.
+ public void AdjustEnergy(EntityUid uid, float delta, float min = float.MinValue, float max = float.MaxValue, bool snapMin = true, bool snapMax = true, SingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ var newValue = singularity.Energy + delta;
+ if((!snapMin && newValue < min)
+ || (!snapMax && newValue > max))
+ return;
+ SetEnergy(uid, MathHelper.Clamp(newValue, min, max), singularity);
+ }
+
+ ///
+ /// Setter for .
+ /// If the new target time implies that the singularity should have updated it does so immediately.
+ ///
+ /// The uid of the singularity to set the update period for.
+ /// The new update period for the singularity.
+ /// The state of the singularity to set the update period for.
+ public void SetUpdatePeriod(EntityUid uid, TimeSpan value, SingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ if (MathHelper.CloseTo(singularity.TargetUpdatePeriod.TotalSeconds, value.TotalSeconds))
+ return;
+
+ singularity.TargetUpdatePeriod = value;
+ singularity.NextUpdateTime = singularity.LastUpdateTime + singularity.TargetUpdatePeriod;
+
+ var curTime = _timing.CurTime;
+ if (singularity.NextUpdateTime <= curTime)
+ Update(uid, curTime - singularity.LastUpdateTime, singularity);
+ }
+
+#endregion Getters/Setters
+
+#region Event Handlers
+
+ ///
+ /// Handles playing the startup sounds when a singulo forms.
+ /// Always sets up the ambient singularity rumble.
+ /// The formation sound only plays if the singularity is being created.
+ ///
+ /// The entity UID of the singularity that is forming.
+ /// The component of the singularity that is forming.
+ /// The event arguments.
+ public void OnSingularityStartup(EntityUid uid, SingularityComponent comp, ComponentStartup args)
+ {
+ comp.LastUpdateTime = _timing.CurTime;
+ comp.NextUpdateTime = comp.LastUpdateTime + comp.TargetUpdatePeriod;
+
+ MetaDataComponent? metaData = null;
+ if (Resolve(uid, ref metaData) && metaData.EntityLifeStage <= EntityLifeStage.Initializing)
+ _audio.Play(comp.FormationSound, Filter.Pvs(comp.Owner), comp.Owner, true);
+
+ comp.AmbientSoundStream = _audio.Play(comp.AmbientSound, Filter.Pvs(comp.Owner), comp.Owner, true);
+ UpdateSingularityLevel(uid, comp);
+ }
+
+ ///
+ /// Makes entities that have the singularity distortion visual warping always get their state shared with the client.
+ /// This prevents some major popin with large distortion ranges.
+ ///
+ /// The entity UID of the entity that is gaining the shader.
+ /// The component of the shader that the entity is gaining.
+ /// The event arguments.
+ public void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent comp, ComponentStartup args)
+ {
+ _pvs.AddGlobalOverride(uid);
+ }
+
+ ///
+ /// Handles playing the shutdown sounds when a singulo dissipates.
+ /// Always stops the ambient singularity rumble.
+ /// The dissipations sound only plays if the singularity is being destroyed.
+ ///
+ /// The entity UID of the singularity that is dissipating.
+ /// The component of the singularity that is dissipating.
+ /// The event arguments.
+ public void OnSingularityShutdown(EntityUid uid, SingularityComponent comp, ComponentShutdown args)
+ {
+ comp.AmbientSoundStream?.Stop();
+
+ MetaDataComponent? metaData = null;
+ if (Resolve(uid, ref metaData) && metaData.EntityLifeStage >= EntityLifeStage.Terminating)
+ _audio.Play(comp.DissipationSound, Filter.Pvs(comp.Owner), comp.Owner, true);
+ }
+
+ ///
+ /// Handles wrapping the state of a singularity for server-client syncing.
+ ///
+ /// The uid of the singularity that is being synced.
+ /// The state of the singularity that is being synced.
+ /// The event arguments.
+ private void HandleSingularityState(EntityUid uid, SingularityComponent comp, ref ComponentGetState args)
+ {
+ args.State = new SingularityComponentState(comp);
+ }
+
+ ///
+ /// Adds the energy of any entities that are consumed to the singularity that consumed them.
+ ///
+ /// The entity UID of the singularity that is consuming the entity.
+ /// The component of the singularity that is consuming the entity.
+ /// The event arguments.
+ public void OnConsumedEntity(EntityUid uid, SingularityComponent comp, EntityConsumedByEventHorizonEvent args)
+ {
+ AdjustEnergy(uid, BaseEntityEnergy, singularity: comp);
+ }
+
+ ///
+ /// Adds the energy of any tiles that are consumed to the singularity that consumed them.
+ ///
+ /// The entity UID of the singularity that is consuming the tiles.
+ /// The component of the singularity that is consuming the tiles.
+ /// The event arguments.
+ public void OnConsumedTiles(EntityUid uid, SingularityComponent comp, TilesConsumedByEventHorizonEvent args)
+ {
+ AdjustEnergy(uid, args.Tiles.Count * BaseTileEnergy, singularity: comp);
+ }
+
+ ///
+ /// Adds the energy of this singularity to singularities consume it.
+ ///
+ /// The entity UID of the singularity that is being consumed.
+ /// The component of the singularity that is being consumed.
+ /// The event arguments.
+ private void OnConsumed(EntityUid uid, SingularityComponent comp, EventHorizonConsumedEntityEvent args)
+ {
+ // Should be slightly more efficient than checking literally everything we consume for a singularity component and doing the reverse.
+ if (EntityManager.TryGetComponent(args.EventHorizon.Owner, out var singulo))
+ {
+ AdjustEnergy(singulo.Owner, comp.Energy, singularity: singulo);
+ SetEnergy(uid, 0.0f, comp);
+ }
+ }
+
+ ///
+ /// Adds some bonus energy from any singularity food to the singularity that consumes it.
+ ///
+ /// The entity UID of the singularity food that is being consumed.
+ /// The component of the singularity food that is being consumed.
+ /// The event arguments.
+ public void OnConsumed(EntityUid uid, SinguloFoodComponent comp, EventHorizonConsumedEntityEvent args)
+ {
+ if (EntityManager.TryGetComponent(args.EventHorizon.Owner, out var singulo))
+ AdjustEnergy(args.EventHorizon.Owner, comp.Energy, singularity: singulo);
+ }
+
+ ///
+ /// Updates the rate at which the singularities energy drains at when its level changes.
+ ///
+ /// The entity UID of the singularity that changed in level.
+ /// The component of the singularity that changed in level.
+ /// The event arguments.
+ public void UpdateEnergyDrain(EntityUid uid, SingularityComponent comp, SingularityLevelChangedEvent args)
+ {
+ comp.EnergyDrain = args.NewValue switch {
+ 6 => 20,
+ 5 => 15,
+ 4 => 10,
+ 3 => 5,
+ 2 => 2,
+ 1 => 1,
+ _ => 0
+ };
+ }
+
+ ///
+ /// Updates the possible speeds of the singulos random walk when the singularities level changes.
+ ///
+ /// The entity UID of the singularity.
+ /// The random walk component component sharing the entity with the singulo component.
+ /// The event arguments.
+ private void UpdateRandomWalk(EntityUid uid, RandomWalkComponent comp, SingularityLevelChangedEvent args)
+ {
+ var scale = MathF.Max(args.NewValue, 4);
+ comp.MinSpeed = 7.5f / scale;
+ comp.MaxSpeed = 10f / scale;
+ }
+
+ ///
+ /// Updates the size and strength of the singularities gravity well when the singularities level changes.
+ ///
+ /// The entity UID of the singularity.
+ /// The gravity well component sharing the entity with the singulo component.
+ /// The event arguments.
+ private void UpdateGravityWell(EntityUid uid, GravityWellComponent comp, SingularityLevelChangedEvent args)
+ {
+ var singulos = args.Singularity;
+ comp.MaxRange = GravPulseRange(singulos);
+ (comp.BaseRadialAcceleration, comp.BaseTangentialAcceleration) = GravPulseAcceleration(singulos);
+ }
+
+#endregion Event Handlers
}
diff --git a/Content.Server/Singularity/Events/EntityConsumedByEventHorizonEvent.cs b/Content.Server/Singularity/Events/EntityConsumedByEventHorizonEvent.cs
new file mode 100644
index 0000000000..116131d8b1
--- /dev/null
+++ b/Content.Server/Singularity/Events/EntityConsumedByEventHorizonEvent.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Singularity.Components;
+using Robust.Shared.Containers;
+
+namespace Content.Server.Singularity.Events;
+
+///
+/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
+///
+public sealed class EntityConsumedByEventHorizonEvent : EntityEventArgs
+{
+ ///
+ /// The entity being consumed by the event horizon.
+ ///
+ public readonly EntityUid Entity;
+
+ ///
+ /// The event horizon consuming the entity.
+ ///
+ public readonly EventHorizonComponent EventHorizon;
+
+ ///
+ /// The innermost container of the entity being consumed by the event horizon that is not also in the process of being consumed by the event horizon.
+ /// Used to correctly dump out the contents containers that are consumed by the event horizon.
+ ///
+ public readonly IContainer? Container;
+
+ public EntityConsumedByEventHorizonEvent(EntityUid entity, EventHorizonComponent eventHorizon, IContainer? container = null)
+ {
+ Entity = entity;
+ EventHorizon = eventHorizon;
+ Container = container;
+ }
+}
diff --git a/Content.Server/Singularity/Events/EventHorizonAttemptConsumeEntityEvent.cs b/Content.Server/Singularity/Events/EventHorizonAttemptConsumeEntityEvent.cs
new file mode 100644
index 0000000000..26119019f9
--- /dev/null
+++ b/Content.Server/Singularity/Events/EventHorizonAttemptConsumeEntityEvent.cs
@@ -0,0 +1,26 @@
+using Content.Shared.Singularity.Components;
+
+namespace Content.Server.Singularity.Events;
+
+///
+/// Event raised on the target entity whenever an event horizon attempts to consume an entity.
+/// Can be cancelled to prevent the target entity from being consumed.
+///
+public sealed class EventHorizonAttemptConsumeEntityEvent : CancellableEntityEventArgs
+{
+ ///
+ /// The entity that the event horizon is attempting to consume.
+ ///
+ public readonly EntityUid Entity;
+
+ ///
+ /// The event horizon consuming the target entity.
+ ///
+ public readonly EventHorizonComponent EventHorizon;
+
+ public EventHorizonAttemptConsumeEntityEvent(EntityUid entity, EventHorizonComponent eventHorizon)
+ {
+ Entity = entity;
+ EventHorizon = eventHorizon;
+ }
+}
diff --git a/Content.Server/Singularity/Events/EventHorizonConsumedEntityEvent.cs b/Content.Server/Singularity/Events/EventHorizonConsumedEntityEvent.cs
new file mode 100644
index 0000000000..eaf1ca2a9a
--- /dev/null
+++ b/Content.Server/Singularity/Events/EventHorizonConsumedEntityEvent.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Singularity.Components;
+using Robust.Shared.Containers;
+
+namespace Content.Server.Singularity.Events;
+
+///
+/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
+///
+public sealed class EventHorizonConsumedEntityEvent : EntityEventArgs
+{
+ ///
+ /// The entity being consumed by the event horizon.
+ ///
+ public readonly EntityUid Entity;
+
+ ///
+ /// The event horizon consuming the target entity.
+ ///
+ public readonly EventHorizonComponent EventHorizon;
+
+ ///
+ /// The innermost container of the entity being consumed by the event horizon that is not also in the process of being consumed by the event horizon.
+ /// Used to correctly dump out the contents containers that are consumed by the event horizon.
+ ///
+ public readonly IContainer? Container;
+
+ public EventHorizonConsumedEntityEvent(EntityUid entity, EventHorizonComponent eventHorizon, IContainer? container = null)
+ {
+ Entity = entity;
+ EventHorizon = eventHorizon;
+ Container = container;
+ }
+}
diff --git a/Content.Server/Singularity/Events/EventHorizonContainedEvent.cs b/Content.Server/Singularity/Events/EventHorizonContainedEvent.cs
new file mode 100644
index 0000000000..3c908efd41
--- /dev/null
+++ b/Content.Server/Singularity/Events/EventHorizonContainedEvent.cs
@@ -0,0 +1,31 @@
+using Robust.Shared.Containers;
+using Content.Shared.Singularity.Components;
+
+namespace Content.Shared.Singularity.EntitySystems;
+
+///
+/// An event queued when an event horizon is contained (put into a container).
+/// Exists to delay the event horizon eating its way out of the container until events relating to the insertion have been processed.
+///
+public sealed class EventHorizonContainedEvent : EntityEventArgs {
+ ///
+ /// The uid of the event horizon that has been contained.
+ ///
+ public readonly EntityUid Entity;
+
+ ///
+ /// The state of the event horizon that has been contained.
+ ///
+ public readonly EventHorizonComponent EventHorizon;
+
+ ///
+ /// The arguments of the action that resulted in the event horizon being contained.
+ ///
+ public readonly EntGotInsertedIntoContainerMessage Args;
+
+ public EventHorizonContainedEvent(EntityUid entity, EventHorizonComponent eventHorizon, EntGotInsertedIntoContainerMessage args) {
+ Entity = entity;
+ EventHorizon = eventHorizon;
+ Args = args;
+ }
+}
diff --git a/Content.Server/Singularity/Events/TilesConsumedByEventHorizonEvent.cs b/Content.Server/Singularity/Events/TilesConsumedByEventHorizonEvent.cs
new file mode 100644
index 0000000000..79261302ba
--- /dev/null
+++ b/Content.Server/Singularity/Events/TilesConsumedByEventHorizonEvent.cs
@@ -0,0 +1,35 @@
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+using Content.Shared.Singularity.Components;
+
+namespace Content.Server.Singularity.Events;
+
+///
+/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
+///
+public sealed class TilesConsumedByEventHorizonEvent : EntityEventArgs
+{
+ ///
+ /// The tiles that the event horizon is consuming.
+ /// Ripped directly from the relevant proc so the second element of each element will be what the tiles are going to be after the grid is updated; usually .
+ ///
+ public readonly IReadOnlyList<(Vector2i, Tile)> Tiles;
+
+ ///
+ /// The mapgrid that the event horizon is consuming tiles of.
+ ///
+ public readonly MapGridComponent MapGrid;
+
+ ///
+ /// The event horizon consuming the tiles.
+ ///
+ public readonly EventHorizonComponent EventHorizon;
+
+ public TilesConsumedByEventHorizonEvent(IReadOnlyList<(Vector2i, Tile)> tiles, MapGridComponent mapGrid, EventHorizonComponent eventHorizon)
+ {
+ Tiles = tiles;
+ MapGrid = mapGrid;
+ EventHorizon = eventHorizon;
+ }
+}
diff --git a/Content.Server/Singularity/StartSingularityEngineCommand.cs b/Content.Server/Singularity/StartSingularityEngineCommand.cs
index 2e053a6c07..60befe58a1 100644
--- a/Content.Server/Singularity/StartSingularityEngineCommand.cs
+++ b/Content.Server/Singularity/StartSingularityEngineCommand.cs
@@ -24,13 +24,14 @@ namespace Content.Server.Singularity
}
var entityManager = IoCManager.Resolve();
+ var entitySystemManager = IoCManager.Resolve();
foreach (var comp in entityManager.EntityQuery())
{
- EntitySystem.Get().SwitchOn(comp);
+ entitySystemManager.GetEntitySystem().SwitchOn(comp);
}
foreach (var comp in entityManager.EntityQuery())
{
- EntitySystem.Get().SetCollectorEnabled(comp.Owner, true, null, comp);
+ entitySystemManager.GetEntitySystem().SetCollectorEnabled(comp.Owner, true, null, comp);
}
foreach (var comp in entityManager.EntityQuery())
{
diff --git a/Content.Shared/Singularity/Components/EventHorizonComponent.cs b/Content.Shared/Singularity/Components/EventHorizonComponent.cs
new file mode 100644
index 0000000000..d4e27a8d79
--- /dev/null
+++ b/Content.Shared/Singularity/Components/EventHorizonComponent.cs
@@ -0,0 +1,71 @@
+using Robust.Shared.GameStates;
+using Content.Shared.Singularity.EntitySystems;
+
+namespace Content.Shared.Singularity.Components;
+
+///
+/// A component that makes the associated entity destroy other within some distance of itself.
+/// Also makes the associated entity destroy other entities upon contact.
+/// Primarily managed by and its server/client versions.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed class EventHorizonComponent : Component
+{
+ ///
+ /// The radius of the event horizon within which it will destroy all entities and tiles.
+ /// If < 0.0 this behavior will not be active.
+ /// If you want to set this go through .
+ ///
+ [DataField("radius")]
+ [Access(friends:typeof(SharedEventHorizonSystem))]
+ public float Radius;
+
+ ///
+ /// Whether the event horizon can consume/destroy the devices built to contain it.
+ /// If you want to set this go through .
+ ///
+ [DataField("canBreachContainment")]
+ [Access(friends:typeof(SharedEventHorizonSystem))]
+ public bool CanBreachContainment = false;
+
+ ///
+ /// The ID of the fixture used to detect if the event horizon has collided with any physics objects.
+ /// Can be set to null, in which case no such fixture is used.
+ /// If you want to set this go through .
+ ///
+ [DataField("horizonFixtureId")]
+ [Access(friends:typeof(SharedEventHorizonSystem))]
+ public string? HorizonFixtureId = "EventHorizon";
+
+ ///
+ /// Whether the entity this event horizon is attached to is being consumed by another event horizon.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public bool BeingConsumedByAnotherEventHorizon = false;
+
+ #region Update Timing
+
+ ///
+ /// The amount of time that should elapse between this event horizon consuming everything it overlaps with.
+ ///
+ [DataField("consumePeriod")]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SharedEventHorizonSystem))]
+ public TimeSpan TargetConsumePeriod { get; set; } = TimeSpan.FromSeconds(0.5);
+
+ ///
+ /// The last time at which this consumed everything it overlapped with.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SharedEventHorizonSystem))]
+ public TimeSpan LastConsumeWaveTime { get; set; } = default!;
+
+ ///
+ /// The next time at which this consumed everything it overlapped with.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(SharedEventHorizonSystem))]
+ public TimeSpan NextConsumeWaveTime { get; set; } = default!;
+
+ #endregion Update Timing
+}
diff --git a/Content.Shared/Singularity/Components/SharedSingularityComponent.cs b/Content.Shared/Singularity/Components/SharedSingularityComponent.cs
index 93e8f96774..84acdd0880 100644
--- a/Content.Shared/Singularity/Components/SharedSingularityComponent.cs
+++ b/Content.Shared/Singularity/Components/SharedSingularityComponent.cs
@@ -1,42 +1,32 @@
using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-namespace Content.Shared.Singularity.Components
+using Content.Shared.Singularity.EntitySystems;
+
+namespace Content.Shared.Singularity.Components;
+
+///
+/// A component that makes the associated entity accumulate energy when an associated event horizon consumes things.
+/// Energy management is server-side.
+///
+[NetworkedComponent]
+public abstract class SharedSingularityComponent : Component
{
- [NetworkedComponent]
- public abstract class SharedSingularityComponent : Component
- {
- ///
- /// The radiation pulse component's radsPerSecond is set to the singularity's level multiplied by this number.
- ///
- [DataField("radsPerLevel")]
- public float RadsPerLevel = 1;
+ ///
+ /// The current level of the singularity.
+ /// Used as a scaling factor for things like visual size, event horizon radius, gravity well radius, radiation output, etc.
+ /// If you want to set this use ().
+ ///
+ [DataField("level")]
+ [Access(friends:typeof(SharedSingularitySystem), Other=AccessPermissions.Read, Self=AccessPermissions.Read)]
+ public byte Level = 1;
- ///
- /// Changed by
- ///
- [ViewVariables]
- public int Level { get; set; }
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- if (curState is not SingularityComponentState state)
- {
- return;
- }
-
- EntitySystem.Get().ChangeSingularityLevel(this, state.Level);
- }
- }
-
- [Serializable, NetSerializable]
- public sealed class SingularityComponentState : ComponentState
- {
- public int Level { get; }
-
- public SingularityComponentState(int level)
- {
- Level = level;
- }
- }
+ ///
+ /// The amount of radiation this singularity emits per its level.
+ /// Has to be on shared in case someone attaches a RadiationPulseComponent to the singularity.
+ /// If you want to set this use ().
+ ///
+ [DataField("radsPerLevel")]
+ [Access(friends:typeof(SharedSingularitySystem), Other=AccessPermissions.Read, Self=AccessPermissions.Read)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float RadsPerLevel = 2f;
}
diff --git a/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs b/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs
new file mode 100644
index 0000000000..74e278a949
--- /dev/null
+++ b/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs
@@ -0,0 +1,206 @@
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+
+using Content.Shared.Ghost;
+using Content.Shared.Singularity.Components;
+
+namespace Content.Shared.Singularity.EntitySystems;
+
+///
+/// The entity system primarily responsible for managing s.
+///
+public abstract class SharedEventHorizonSystem : EntitySystem
+{
+#region Dependencies
+ [Dependency] private readonly FixtureSystem _fixtures = default!;
+ [Dependency] protected readonly IViewVariablesManager Vvm = default!;
+#endregion Dependencies
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // Allows for predicted collisions with singularities.
+ SubscribeLocalEvent(OnEventHorizonStartup);
+ SubscribeLocalEvent(OnPreventCollide);
+
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.AddPath(nameof(EventHorizonComponent.Radius), (_, comp) => comp.Radius, (uid, value, comp) => SetRadius(uid, value, eventHorizon: comp));
+ vvHandle.AddPath(nameof(EventHorizonComponent.CanBreachContainment), (_, comp) => comp.CanBreachContainment, (uid, value, comp) => SetCanBreachContainment(uid, value, eventHorizon: comp));
+ vvHandle.AddPath(nameof(EventHorizonComponent.HorizonFixtureId), (_, comp) => comp.HorizonFixtureId, (uid, value, comp) => SetHorizonFixtureId(uid, value, eventHorizon: comp));
+ }
+
+ public override void Shutdown()
+ {
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.RemovePath(nameof(EventHorizonComponent.Radius));
+ vvHandle.RemovePath(nameof(EventHorizonComponent.CanBreachContainment));
+ vvHandle.RemovePath(nameof(EventHorizonComponent.HorizonFixtureId));
+
+ base.Shutdown();
+ }
+
+#region Getters/Setters
+
+ ///
+ /// Setter for
+ /// May also update the fixture associated with the event horizon.
+ ///
+ /// The uid of the event horizon to change the radius of.
+ /// The new radius of the event horizon.
+ /// Whether to update the associated fixture upon changing the radius of the event horizon.
+ /// The state of the event horizon to change the radius of.
+ public void SetRadius(EntityUid uid, float value, bool updateFixture = true, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ var oldValue = eventHorizon.Radius;
+ if (value == oldValue)
+ return;
+
+ eventHorizon.Radius = value;
+ EntityManager.Dirty(eventHorizon);
+ if (updateFixture)
+ UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
+ }
+
+ ///
+ /// Setter for
+ /// May also update the fixture associated with the event horizon.
+ ///
+ /// The uid of the event horizon to make (in)capable of breaching containment.
+ /// Whether the event horizon should be able to breach containment.
+ /// Whether to update the associated fixture upon changing whether the event horizon can breach containment.
+ /// The state of the event horizon to make (in)capable of breaching containment.
+ public void SetCanBreachContainment(EntityUid uid, bool value, bool updateFixture = true, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ var oldValue = eventHorizon.CanBreachContainment;
+ if (value == oldValue)
+ return;
+
+ eventHorizon.CanBreachContainment = value;
+ EntityManager.Dirty(eventHorizon);
+ if (updateFixture)
+ UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
+ }
+
+ ///
+ /// Setter for
+ /// May also update the fixture associated with the event horizon.
+ ///
+ /// The uid of the event horizon with the fixture ID to change.
+ /// The new fixture ID to associate the event horizon with.
+ /// Whether to update the associated fixture upon changing whether the event horizon can breach containment.
+ /// The state of the event horizon with the fixture ID to change.
+ public void SetHorizonFixtureId(EntityUid uid, string? value, bool updateFixture = true, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ var oldValue = eventHorizon.HorizonFixtureId;
+ if (value == oldValue)
+ return;
+
+ eventHorizon.HorizonFixtureId = value;
+ EntityManager.Dirty(eventHorizon);
+ if (updateFixture)
+ UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
+ }
+
+ ///
+ /// Updates the state of the fixture associated with the event horizon.
+ ///
+ /// The uid of the event horizon associated with the fixture to update.
+ /// The physics component containing the fixture to update.
+ /// The state of the event horizon associated with the fixture to update.
+ public void UpdateEventHorizonFixture(EntityUid uid, PhysicsComponent? fixtures = null, EventHorizonComponent? eventHorizon = null)
+ {
+ if(!Resolve(uid, ref eventHorizon))
+ return;
+
+ var fixtureId = eventHorizon.HorizonFixtureId;
+ if (fixtureId == null || !Resolve(eventHorizon.Owner, ref fixtures, logMissing: false))
+ return;
+
+ var fixture = _fixtures.GetFixtureOrNull(fixtures, fixtureId);
+ if (fixture == null)
+ return;
+
+ var shape = (PhysShapeCircle)fixture.Shape;
+ shape.Radius = eventHorizon.Radius;
+ fixture.Hard = !eventHorizon.CanBreachContainment;
+ EntityManager.Dirty(fixtures);
+ }
+
+#endregion Getters/Setters
+
+
+#region EventHandlers
+
+ ///
+ /// Syncs the state of the fixture associated with the event horizon upon startup.
+ ///
+ /// The entity that has just gained an event horizon component.
+ /// The event horizon component that is starting up.
+ /// The event arguments.
+ private void OnEventHorizonStartup(EntityUid uid, EventHorizonComponent comp, ComponentStartup args)
+ {
+ UpdateEventHorizonFixture(uid, eventHorizon: comp);
+ }
+
+ ///
+ /// Prevents the event horizon from colliding with anything it cannot consume.
+ /// Most notably map grids and ghosts.
+ /// Also makes event horizons phase through containment if it can breach.
+ ///
+ /// The entity that is trying to collide with another entity.
+ /// The event horizon of the former.
+ /// The event arguments.
+ private void OnPreventCollide(EntityUid uid, EventHorizonComponent comp, ref PreventCollideEvent args)
+ {
+ if(!args.Cancelled)
+ PreventCollide(uid, comp, ref args);
+ }
+
+ ///
+ /// The actual, functional part of SharedEventHorizonSystem.OnPreventCollide.
+ /// The return value allows for overrides to early return if the base successfully handles collision prevention.
+ ///
+ /// The entity that is trying to collide with another entity.
+ /// The event horizon of the former.
+ /// The event arguments.
+ /// A bool indicating whether the collision prevention has been handled.
+ protected virtual bool PreventCollide(EntityUid uid, EventHorizonComponent comp, ref PreventCollideEvent args)
+ {
+ var otherUid = args.BodyB.Owner;
+
+ // For prediction reasons always want the client to ignore these.
+ if (EntityManager.HasComponent(otherUid) ||
+ EntityManager.HasComponent(otherUid))
+ {
+ args.Cancelled = true;
+ return true;
+ }
+
+ // If we can, breach containment
+ // otherwise, check if it's containment and just keep the collision
+ if (EntityManager.HasComponent(otherUid) ||
+ EntityManager.HasComponent(otherUid))
+ {
+ if (comp.CanBreachContainment)
+ args.Cancelled = true;
+
+ return true;
+ }
+
+ return false;
+ }
+
+#endregion EventHandlers
+}
diff --git a/Content.Shared/Singularity/EntitySystems/SharedGravityWellSystem.cs b/Content.Shared/Singularity/EntitySystems/SharedGravityWellSystem.cs
new file mode 100644
index 0000000000..a69ed24fbf
--- /dev/null
+++ b/Content.Shared/Singularity/EntitySystems/SharedGravityWellSystem.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Singularity.EntitySystems;
+
+///
+/// The entity system primarily responsible for managing s.
+///
+public abstract class SharedGravityWellSystem : EntitySystem
+{}
diff --git a/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs b/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs
new file mode 100644
index 0000000000..34c0b6ef88
--- /dev/null
+++ b/Content.Shared/Singularity/EntitySystems/SharedSingularitySystem.cs
@@ -0,0 +1,385 @@
+using Robust.Shared.Containers;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Serialization;
+
+using Content.Shared.Radiation.Components;
+using Content.Shared.Singularity.Components;
+using Content.Shared.Singularity.Events;
+
+namespace Content.Shared.Singularity.EntitySystems;
+
+///
+/// The entity system primarily responsible for managing s.
+///
+public abstract class SharedSingularitySystem : EntitySystem
+{
+#region Dependencies
+ [Dependency] private readonly SharedAppearanceSystem _visualizer = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+ [Dependency] private readonly SharedEventHorizonSystem _horizons = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] protected readonly IViewVariablesManager Vvm = default!;
+#endregion Dependencies
+
+ ///
+ /// The minimum level a singularity can be set to.
+ ///
+ public const byte MinSingularityLevel = 0;
+
+ ///
+ /// The maximum level a singularity can be set to.
+ ///
+ public const byte MaxSingularityLevel = 6;
+
+ ///
+ /// The amount to scale a singularities distortion shader by when it's in a container.
+ /// This is the inverse of an exponent, not a linear scaling factor.
+ /// ie. n => intensity = intensity ** (1/n)
+ ///
+ public const float DistortionContainerScaling = 4f;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnSingularityStartup);
+ SubscribeLocalEvent(UpdateAppearance);
+ SubscribeLocalEvent(UpdateRadiation);
+ SubscribeLocalEvent(UpdateBody);
+ SubscribeLocalEvent(UpdateEventHorizon);
+ SubscribeLocalEvent(UpdateDistortion);
+ SubscribeLocalEvent(UpdateDistortion);
+ SubscribeLocalEvent(UpdateDistortion);
+
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.AddPath(nameof(SharedSingularityComponent.Level), (_, comp) => comp.Level, SetLevel);
+ vvHandle.AddPath(nameof(SharedSingularityComponent.RadsPerLevel), (_, comp) => comp.RadsPerLevel, SetRadsPerLevel);
+ }
+
+ public override void Shutdown()
+ {
+ var vvHandle = Vvm.GetTypeHandler();
+ vvHandle.RemovePath(nameof(SharedSingularityComponent.Level));
+ vvHandle.RemovePath(nameof(SharedSingularityComponent.RadsPerLevel));
+
+ base.Shutdown();
+ }
+
+#region Getters/Setters
+
+ ///
+ /// Setter for
+ /// Also sends out an event alerting that the singularities level has changed.
+ ///
+ /// The uid of the singularity to change the level of.
+ /// The new level the singularity should have.
+ /// The state of the singularity to change the level of.
+ public void SetLevel(EntityUid uid, byte value, SharedSingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ value = MathHelper.Clamp(value, MinSingularityLevel, MaxSingularityLevel);
+ var oldValue = singularity.Level;
+ if (oldValue == value)
+ return;
+
+ singularity.Level = value;
+ UpdateSingularityLevel(uid, oldValue, singularity);
+ if(!EntityManager.Deleted(singularity.Owner))
+ EntityManager.Dirty(singularity);
+ }
+
+ ///
+ /// Setter for
+ /// Also updates the radiation output of the singularity according to the new values.
+ ///
+ /// The uid of the singularity to change the radioactivity of.
+ /// The new radioactivity the singularity should have.
+ /// The state of the singularity to change the radioactivity of.
+ public void SetRadsPerLevel(EntityUid uid, float value, SharedSingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ var oldValue = singularity.RadsPerLevel;
+ if (oldValue == value)
+ return;
+
+ singularity.RadsPerLevel = value;
+ UpdateRadiation(uid, singularity);
+ }
+
+ ///
+ /// Alerts the entity hosting the singularity that the level of the singularity has changed.
+ /// Usually follows a SharedSingularitySystem.SetLevel call, but is also used on component startup to sync everything.
+ ///
+ /// The uid of the singularity which's level has changed.
+ /// The old level of the singularity. May be equal to if the component is starting.
+ /// The state of the singularity which's level has changed.
+ public void UpdateSingularityLevel(EntityUid uid, byte oldValue, SharedSingularityComponent? singularity = null)
+ {
+ if(!Resolve(uid, ref singularity))
+ return;
+
+ RaiseLocalEvent(uid, new SingularityLevelChangedEvent(singularity.Level, oldValue, singularity));
+ if (singularity.Level <= 0)
+ EntityManager.DeleteEntity(singularity.Owner);
+ }
+
+ ///
+ /// Alerts the entity hosting the singularity that the level of the singularity has changed without the level actually changing.
+ /// Used to sync components when the singularity component is added to an entity.
+ ///
+ /// The uid of the singularity.
+ /// The state of the singularity.
+ public void UpdateSingularityLevel(EntityUid uid, SharedSingularityComponent? singularity = null)
+ {
+ if (Resolve(uid, ref singularity))
+ UpdateSingularityLevel(uid, singularity.Level, singularity);
+ }
+
+ ///
+ /// Updates the amount of radiation the singularity emits to reflect a change in the level or radioactivity per level of the singularity.
+ ///
+ /// The uid of the singularity to update the radiation of.
+ /// The state of the singularity to update the radiation of.
+ /// The state of the radioactivity of the singularity to update.
+ private void UpdateRadiation(EntityUid uid, SharedSingularityComponent? singularity = null, RadiationSourceComponent? rads = null)
+ {
+ if(!Resolve(uid, ref singularity, ref rads, logMissing: false))
+ return;
+ rads.Intensity = singularity.Level * singularity.RadsPerLevel;
+ }
+
+#endregion Getters/Setters
+
+#region Derivations
+ ///
+ /// The scaling factor for the size of a singularities gravity well.
+ ///
+ public const float BaseGravityWellRadius = 2f;
+
+ ///
+ /// The scaling factor for the base acceleration of a singularities gravity well.
+ ///
+ public const float BaseGravityWellAcceleration = 10f;
+
+ ///
+ /// The level at and above which a singularity should be capable of breaching containment.
+ ///
+ public const byte SingularityBreachThreshold = 5;
+
+ ///
+ /// Derives the proper gravity well radius for a singularity from its state.
+ ///
+ /// A singularity.
+ /// The gravity well radius the singularity should have given its state.
+ public float GravPulseRange(SharedSingularityComponent singulo)
+ => BaseGravityWellRadius * (singulo.Level + 1);
+
+ ///
+ /// Derives the proper base gravitational acceleration for a singularity from its state.
+ ///
+ /// A singularity.
+ /// The base gravitational acceleration the singularity should have given its state.
+ public (float, float) GravPulseAcceleration(SharedSingularityComponent singulo)
+ => (BaseGravityWellAcceleration * singulo.Level, 0f);
+
+ ///
+ /// Derives the proper event horizon radius for a singularity from its state.
+ ///
+ /// A singularity.
+ /// The event horizon radius the singularity should have given its state.
+ public float EventHorizonRadius(SharedSingularityComponent singulo)
+ => (float) singulo.Level - 0.5f;
+
+ ///
+ /// Derives whether a singularity should be able to breach containment from its state.
+ ///
+ /// A singularity.
+ /// Whether the singularity should be able to breach containment.
+ public bool CanBreachContainment(SharedSingularityComponent singulo)
+ => singulo.Level >= SingularityBreachThreshold;
+
+ ///
+ /// Derives the proper distortion shader falloff for a singularity from its state.
+ ///
+ /// A singularity.
+ /// The distortion shader falloff the singularity should have given its state.
+ public float GetFalloff(float level)
+ {
+ return level switch {
+ 0 => 9999f,
+ 1 => MathF.Sqrt(6.4f),
+ 2 => MathF.Sqrt(7.0f),
+ 3 => MathF.Sqrt(8.0f),
+ 4 => MathF.Sqrt(10.0f),
+ 5 => MathF.Sqrt(12.0f),
+ 6 => MathF.Sqrt(12.0f),
+ _ => -1.0f
+ };
+ }
+
+ ///
+ /// Derives the proper distortion shader intensity for a singularity from its state.
+ ///
+ /// A singularity.
+ /// The distortion shader intensity the singularity should have given its state.
+ public float GetIntensity(float level)
+ {
+ return level switch {
+ 0 => 0.0f,
+ 1 => 3645f,
+ 2 => 103680f,
+ 3 => 1113920f,
+ 4 => 16200000f,
+ 5 => 180000000f,
+ 6 => 180000000f,
+ _ => -1.0f
+ };
+ }
+#endregion Derivations
+
+#region Serialization
+ ///
+ /// A state wrapper used to sync the singularity between the server and client.
+ ///
+ [Serializable, NetSerializable]
+ protected sealed class SingularityComponentState : ComponentState
+ {
+ ///
+ /// The level of the singularity to sync.
+ ///
+ public readonly byte Level;
+
+ public SingularityComponentState(SharedSingularityComponent singulo)
+ {
+ Level = singulo.Level;
+ }
+ }
+#endregion Serialization
+
+#region EventHandlers
+ ///
+ /// Syncs other components with the state of the singularity via event on startup.
+ ///
+ /// The entity that is becoming a singularity.
+ /// The singularity component that is being added to the entity.
+ /// The event arguments.
+ protected virtual void OnSingularityStartup(EntityUid uid, SharedSingularityComponent comp, ComponentStartup args)
+ {
+ UpdateSingularityLevel(uid, comp);
+ }
+
+ // TODO: Figure out which systems should have control of which coupling.
+ ///
+ /// Syncs the radius of an event horizon associated with a singularity that just changed levels.
+ ///
+ /// The entity that the event horizon and singularity are attached to.
+ /// The event horizon associated with the singularity.
+ /// The event arguments.
+ private void UpdateEventHorizon(EntityUid uid, EventHorizonComponent comp, SingularityLevelChangedEvent args)
+ {
+ var singulo = args.Singularity;
+ _horizons.SetRadius(uid, EventHorizonRadius(singulo), false, comp);
+ _horizons.SetCanBreachContainment(uid, CanBreachContainment(singulo), false, comp);
+ _horizons.UpdateEventHorizonFixture(uid, eventHorizon: comp);
+ }
+
+ ///
+ /// Updates the distortion shader associated with a singularity when the singuarity changes levels.
+ ///
+ /// The uid of the distortion shader.
+ /// The state of the distortion shader.
+ /// The event arguments.
+ private void UpdateDistortion(EntityUid uid, SingularityDistortionComponent comp, SingularityLevelChangedEvent args)
+ {
+ var newFalloffPower = GetFalloff(args.NewValue);
+ var newIntensity = GetIntensity(args.NewValue);
+ if (_containers.IsEntityInContainer(uid))
+ {
+ var absFalloffPower = MathF.Abs(newFalloffPower);
+ var absIntensity = MathF.Abs(newIntensity);
+
+ var factor = (1f / DistortionContainerScaling) - 1f;
+ newFalloffPower = absFalloffPower > 1f ? newFalloffPower * MathF.Pow(absFalloffPower, factor) : newFalloffPower;
+ newIntensity = absIntensity > 1f ? newIntensity * MathF.Pow(absIntensity, factor) : newIntensity;
+ }
+
+ comp.FalloffPower = newFalloffPower;
+ comp.Intensity = newIntensity;
+ }
+
+ ///
+ /// Updates the distortion shader associated with a singularity when the singuarity is inserted into a container.
+ ///
+ /// The uid of the distortion shader.
+ /// The state of the distortion shader.
+ /// The event arguments.
+ private void UpdateDistortion(EntityUid uid, SingularityDistortionComponent comp, EntGotInsertedIntoContainerMessage args)
+ {
+ var absFalloffPower = MathF.Abs(comp.FalloffPower);
+ var absIntensity = MathF.Abs(comp.Intensity);
+
+ var factor = (1f / DistortionContainerScaling) - 1f;
+ comp.FalloffPower = absFalloffPower > 1 ? comp.FalloffPower * MathF.Pow(absFalloffPower, factor) : comp.FalloffPower;
+ comp.Intensity = absIntensity > 1 ? comp.Intensity * MathF.Pow(absIntensity, factor) : comp.Intensity;
+ }
+
+ ///
+ /// Updates the distortion shader associated with a singularity when the singuarity is removed from a container.
+ ///
+ /// The uid of the distortion shader.
+ /// The state of the distortion shader.
+ /// The event arguments.
+ private void UpdateDistortion(EntityUid uid, SingularityDistortionComponent comp, EntGotRemovedFromContainerMessage args)
+ {
+ var absFalloffPower = MathF.Abs(comp.FalloffPower);
+ var absIntensity = MathF.Abs(comp.Intensity);
+
+ var factor = DistortionContainerScaling - 1;
+ comp.FalloffPower = absFalloffPower > 1 ? comp.FalloffPower * MathF.Pow(absFalloffPower, factor) : comp.FalloffPower;
+ comp.Intensity = absIntensity > 1 ? comp.Intensity * MathF.Pow(absIntensity, factor) : comp.Intensity;
+ }
+
+ ///
+ /// Updates the state of the physics body associated with a singularity when the singualrity changes levels.
+ ///
+ /// The entity that the physics body and singularity are attached to.
+ /// The physics body associated with the singularity.
+ /// The event arguments.
+ private void UpdateBody(EntityUid uid, PhysicsComponent comp, SingularityLevelChangedEvent args)
+ {
+ _physics.SetBodyStatus(comp, (args.NewValue > 1) ? BodyStatus.InAir : BodyStatus.OnGround);
+ if (args.NewValue <= 1 && args.OldValue > 1) // Apparently keeps singularities from getting stuck in the corners of containment fields.
+ _physics.SetLinearVelocity(comp, Vector2.Zero); // No idea how stopping the singularities movement keeps it from getting stuck though.
+ }
+
+ ///
+ /// Updates the appearance of a singularity when the singularities level changes.
+ ///
+ /// The entity that the singularity is attached to.
+ /// The appearance associated with the singularity.
+ /// The event arguments.
+ private void UpdateAppearance(EntityUid uid, AppearanceComponent comp, SingularityLevelChangedEvent args)
+ {
+ _visualizer.SetData(uid, SingularityVisuals.Level, args.NewValue, comp);
+ }
+
+ ///
+ /// Updates the amount of radiation a singularity emits when the singularities level changes.
+ ///
+ /// The entity that the singularity is attached to.
+ /// The radiation source associated with the singularity.
+ /// The event arguments.
+ private void UpdateRadiation(EntityUid uid, RadiationSourceComponent comp, SingularityLevelChangedEvent args)
+ {
+ UpdateRadiation(uid, args.Singularity, comp);
+ }
+
+#endregion EventHandlers
+
+}
diff --git a/Content.Shared/Singularity/Events/SingularityLevelChangedEvent.cs b/Content.Shared/Singularity/Events/SingularityLevelChangedEvent.cs
new file mode 100644
index 0000000000..ba23523f84
--- /dev/null
+++ b/Content.Shared/Singularity/Events/SingularityLevelChangedEvent.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Singularity.Components;
+
+namespace Content.Shared.Singularity.Events;
+
+///
+/// An event raised whenever a singularity changes its level.
+///
+public sealed class SingularityLevelChangedEvent : EntityEventArgs
+{
+ ///
+ /// The new level of the singularity.
+ ///
+ public readonly byte NewValue;
+
+ ///
+ /// The previous level of the singularity.
+ ///
+ public readonly byte OldValue;
+
+ ///
+ /// The singularity that just changed level.
+ ///
+ public readonly SharedSingularityComponent Singularity;
+
+ public SingularityLevelChangedEvent(byte newValue, byte oldValue, SharedSingularityComponent singularity)
+ {
+ NewValue = newValue;
+ OldValue = oldValue;
+ Singularity = singularity;
+ }
+}
diff --git a/Content.Shared/Singularity/SharedSingularitySystem.cs b/Content.Shared/Singularity/SharedSingularitySystem.cs
deleted file mode 100644
index c065a5bc14..0000000000
--- a/Content.Shared/Singularity/SharedSingularitySystem.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using Content.Shared.Ghost;
-using Content.Shared.Radiation;
-using Content.Shared.Radiation.Components;
-using Content.Shared.Singularity.Components;
-using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Collision.Shapes;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Dynamics;
-using Robust.Shared.Physics.Events;
-using Robust.Shared.Physics.Systems;
-
-namespace Content.Shared.Singularity
-{
- public abstract class SharedSingularitySystem : EntitySystem
- {
- [Dependency] private readonly FixtureSystem _fixtures = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SharedPhysicsSystem _physics = default!;
-
- public const string DeleteFixture = "DeleteCircle";
-
- private float GetFalloff(int level)
- {
- return level switch
- {
- 0 => 9999f,
- 1 => MathF.Sqrt(6.4f),
- 2 => MathF.Sqrt(7.0f),
- 3 => MathF.Sqrt(8.0f),
- 4 => MathF.Sqrt(10.0f),
- 5 => MathF.Sqrt(12.0f),
- 6 => MathF.Sqrt(12.0f),
- _ => -1.0f
- };
- }
-
- private float GetIntensity(int level)
- {
- return level switch
- {
- 0 => 0.0f,
- 1 => 3645f,
- 2 => 103680f,
- 3 => 1113920f,
- 4 => 16200000f,
- 5 => 180000000f,
- 6 => 180000000f,
- _ => -1.0f
- };
- }
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnPreventCollide);
- }
-
- protected void OnPreventCollide(EntityUid uid, SharedSingularityComponent component, ref PreventCollideEvent args)
- {
- PreventCollide(uid, component, ref args);
- }
-
- protected virtual bool PreventCollide(EntityUid uid, SharedSingularityComponent component, ref PreventCollideEvent args)
- {
- var otherUid = args.BodyB.Owner;
-
- // For prediction reasons always want the client to ignore these.
- if (EntityManager.HasComponent(otherUid) ||
- EntityManager.HasComponent(otherUid))
- {
- args.Cancelled = true;
- return true;
- }
-
- // If we're above 4 then breach containment
- // otherwise, check if it's containment and just keep the collision
- if (EntityManager.HasComponent(otherUid) ||
- EntityManager.HasComponent(otherUid))
- {
- if (component.Level > 4)
- {
- args.Cancelled = true;
- }
-
- return true;
- }
-
- return false;
- }
-
- public void ChangeSingularityLevel(SharedSingularityComponent singularity, int value)
- {
- if (value == singularity.Level)
- {
- return;
- }
-
- value = Math.Clamp(value, 0, 6);
-
- var physics = EntityManager.GetComponentOrNull(singularity.Owner);
-
- if (singularity.Level > 1 && value <= 1)
- {
- // Prevents it getting stuck (see SingularityController.MoveSingulo)
- if (physics != null)
- {
- _physics.SetLinearVelocity(physics, Vector2.Zero);
- }
- }
-
- singularity.Level = value;
-
- if (EntityManager.TryGetComponent(singularity.Owner, out RadiationSourceComponent? source))
- {
- source.Intensity = singularity.RadsPerLevel * value;
- }
-
- _appearance.SetData(singularity.Owner, SingularityVisuals.Level, value);
-
- if (physics != null)
- {
- var fixture = _fixtures.GetFixtureOrNull(physics, DeleteFixture);
-
- if (fixture != null)
- {
- var circle = (PhysShapeCircle) fixture.Shape;
- circle.Radius = value - 0.5f;
-
- fixture.Hard = value <= 4;
- }
- }
-
- if (EntityManager.TryGetComponent(singularity.Owner, out SingularityDistortionComponent? distortion))
- {
- distortion.FalloffPower = GetFalloff(value);
- distortion.Intensity = GetIntensity(value);
- }
-
- singularity.Dirty();
- }
- }
-}
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/singularity.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/singularity.yml
index 392f414659..c7a2d34d46 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/singularity.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/Singularity/singularity.yml
@@ -13,7 +13,7 @@
bodyType: Dynamic
- type: Fixtures
fixtures:
- - id: DeleteCircle
+ - id: EventHorizon
shape:
!type:PhysShapeCircle
radius: 0.5
@@ -24,7 +24,15 @@
layer:
- AllMask
- type: Singularity
+ energy: 180
+ level: 1
radsPerLevel: 2
+ - type: GravityWell # To make the singularity attract things.
+ - type: EventHorizon # To make the singularity consume things.
+ radius: 0.5
+ canBreachContainment: false
+ horizonFixtureId: EventHorizon
+ - type: RandomWalk # To make the singularity move around.
- type: SingularityDistortion
- type: RadiationSource
slope: 0.2 # its emit really far away