Splits the singularity into its component parts + ECS singularity + Support for singularities in containers. (#12132)

* InitialCommit (Broken)

* Fixes compile errors

* PR comments. More doc comments. Fixes

* Makes a singularity/event horizon without radiation/physics a valid state to be in

* VV 'fake' setters, fixes the visualizer, fixes the singularity trying to eat itself instead of nearby things.

* Removes unused dependency from Content.Client.GravityWellSystem

* Testing containment and fake VV setters for SingularityGeneratorComponent

* Fixes gravity wells (broken due to LookupFlags.None). Adds recursive Event Horizon consumption

* Fix merge skew

* Fixes for the master merge

* Fix engine commit

* Dirty is obsolete

* Switch over dirty

* Fix requested changes

* ambiant -> ambient

* Moves EventHorionComponent to Shared

* Proper container handling

* Fixes master merge. Fixes post insertion assertions for singularities. Extends proper container handling to gravity wells and the distortion shader.

* Better support for admemes throwing singularities.

* Moves update timing from accumulators to target times

* Update doc comments
This commit is contained in:
TemporalOroboros
2022-12-19 18:47:15 -08:00
committed by GitHub
parent 490aefecef
commit 9a72b05a50
35 changed files with 2561 additions and 683 deletions

View File

@@ -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
{
}
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.Singularity.Components;
using Content.Client.Singularity.EntitySystems;
namespace Content.Client.Singularity.Components;
/// <summary>
/// The client-side version of <see cref="SharedSingularityComponent"/>.
/// Primarily managed by <see cref="SingularitySystem"/>.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(SharedSingularityComponent))]
public sealed class SingularityComponent : SharedSingularityComponent
{}

View File

@@ -1,8 +0,0 @@
using Content.Shared.Singularity;
namespace Content.Client.Singularity
{
public sealed class SingularitySystem : SharedSingularitySystem
{
}
}

View File

@@ -0,0 +1,12 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
namespace Content.Client.Singularity.EntitySystems;
/// <summary>
/// The client-side version of <see cref="SharedEventHorizonSystem"/>.
/// Primarily manages <see cref="EventHorizonComponent"/>s.
/// Exists to make relevant signal handlers (ie: <see cref="SharedEventHorizonSystem.OnPreventCollide"/>) work on the client.
/// </summary>
public sealed class EventHorizonSystem : SharedEventHorizonSystem
{}

View File

@@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Content.Shared.Singularity.Components;
using Content.Shared.Singularity.EntitySystems;
namespace Content.Client.Singularity.EntitySystems;
/// <summary>
/// The client-side version of <see cref="SharedSingularitySystem"/>.
/// Primarily manages <see cref="SingularityComponent"/>s.
/// </summary>
public sealed class SingularitySystem : SharedSingularitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedSingularityComponent, ComponentHandleState>(HandleSingularityState);
}
/// <summary>
/// Handles syncing singularities with their server-side versions.
/// </summary>
/// <param name="uid">The uid of the singularity to sync.</param>
/// <param name="comp">The state of the singularity to sync.</param>
/// <param name="args">The event arguments including the state to sync the singularity with.</param>
private void HandleSingularityState(EntityUid uid, SharedSingularityComponent comp, ref ComponentHandleState args)
{
if (args.Current is not SingularityComponentState state)
return;
SetLevel(uid, state.Level, comp);
}
}

View File

@@ -32,7 +32,7 @@ namespace Content.Client.Singularity.Visualizers
return; return;
} }
if (!component.TryGetData(SingularityVisuals.Level, out int level)) if (!component.TryGetData(SingularityVisuals.Level, out byte level))
{ {
return; return;
} }

View File

@@ -0,0 +1,63 @@
using Content.Server.Physics.Controllers;
namespace Content.Server.Physics.Components;
/// <summary>
/// A component which makes its entity move around at random.
/// </summary>
[RegisterComponent]
public sealed class RandomWalkComponent : Component
{
/// <summary>
/// The minimum speed at which this entity will move.
/// </summary>
[DataField("minSpeed")]
[ViewVariables(VVAccess.ReadWrite)]
public float MinSpeed = 7.5f;
/// <summary>
/// The maximum speed at which this entity will move.
/// </summary>
[DataField("maxSpeed")]
[ViewVariables(VVAccess.ReadWrite)]
public float MaxSpeed = 10f;
/// <summary>
/// The amount of speed carried over when the speed updates.
/// </summary>
[DataField("accumulatorRatio")]
[ViewVariables(VVAccess.ReadWrite)]
public float AccumulatorRatio = 0.0f;
/// <summary>
/// Whether this random walker should take a step immediately when it starts up.
/// </summary>
[DataField("stepOnStartup")]
[ViewVariables(VVAccess.ReadOnly)]
public bool StepOnStartup = false;
#region Update Timing
/// <summary>
/// The minimum amount of time between speed updates.
/// </summary>
[DataField("minStepCooldown")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan MinStepCooldown { get; internal set; } = TimeSpan.FromSeconds(2.0);
/// <summary>
/// The maximum amount of time between speed updates.
/// </summary>
[DataField("maxStepCooldown")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan MaxStepCooldown { get; internal set; } = TimeSpan.FromSeconds(5.0);
/// <summary>
/// The next time this should update its speed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[Access(typeof(RandomWalkController))]
public TimeSpan NextStepTime { get; internal set; } = default!;
#endregion Update Timing
}

View File

@@ -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;
/// <summary>
/// The entity system responsible for managing <see cref="RandomWalkComponent"/>s.
/// Handles updating the direction they move in when their cooldown elapses.
/// </summary>
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<RandomWalkComponent, ComponentStartup>(OnRandomWalkStartup);
}
/// <summary>
/// Updates the cooldowns of all random walkers.
/// If each of them is off cooldown it updates their velocity and resets its cooldown.
/// </summary>
/// <param name="prediction">??? Not documented anywhere I can see ???</param> // TODO: Document this.
/// <param name="frameTime">The amount of time that has elapsed since the last time random walk cooldowns were updated.</param>
public override void UpdateBeforeSolve(bool prediction, float frameTime)
{
base.UpdateBeforeSolve(prediction, frameTime);
foreach(var (randomWalk, physics) in EntityManager.EntityQuery<RandomWalkComponent, PhysicsComponent>())
{
if (EntityManager.HasComponent<ActorComponent>(randomWalk.Owner)
|| EntityManager.HasComponent<ThrownItemComponent>(randomWalk.Owner))
continue;
var curTime = _timing.CurTime;
if (randomWalk.NextStepTime <= curTime)
Update(randomWalk.Owner, randomWalk, physics);
}
}
/// <summary>
/// Updates the direction and speed a random walker is moving at.
/// Also resets the random walker's cooldown.
/// </summary>
/// <param name="randomWalk">The random walker state.</param>
/// <param name="physics">The physics body associated with the random walker.</param>
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));
}
/// <summary>
/// Syncs up a random walker step timing when the component starts up.
/// </summary>
/// <param name="uid">The uid of the random walker to start up.</param>
/// <param name="comp">The state of the random walker to start up.</param>
/// <param name="args">The startup prompt arguments.</param>
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));
}
}

View File

@@ -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<ServerSingularityComponent, PhysicsComponent>())
{
if (EntityManager.HasComponent<ActorComponent>(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?
}
}
}

View File

@@ -0,0 +1,71 @@
using Content.Shared.Singularity.Components;
using Content.Server.Singularity.EntitySystems;
namespace Content.Server.Singularity.Components;
/// <summary>
/// The server-side version of <see cref="SharedGravityWellComponent"/>.
/// Primarily managed by <see cref="GravityWellSystem"/>.
/// </summary>
[RegisterComponent]
public sealed class GravityWellComponent : Component
{
/// <summary>
/// The maximum range at which the gravity well can push/pull entities.
/// </summary>
[DataField("maxRange")]
[ViewVariables(VVAccess.ReadWrite)]
public float MaxRange;
/// <summary>
/// The minimum range at which the gravity well can push/pull entities.
/// This is effectively hardfloored at <see cref="GravityWellSystem.MinGravPulseRange"/>.
/// </summary>
[DataField("minRange")]
[ViewVariables(VVAccess.ReadWrite)]
public float MinRange = 0f;
/// <summary>
/// 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.
/// </summary>
[DataField("baseRadialAcceleration")]
[ViewVariables(VVAccess.ReadWrite)]
public float BaseRadialAcceleration = 0.0f;
/// <summary>
/// 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.
/// </summary>
[DataField("baseTangentialAcceleration")]
[ViewVariables(VVAccess.ReadWrite)]
public float BaseTangentialAcceleration = 0.0f;
#region Update Timing
/// <summary>
/// The amount of time that should elapse between automated updates to this gravity well.
/// </summary>
[DataField("gravPulsePeriod")]
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(GravityWellSystem))]
public TimeSpan TargetPulsePeriod { get; internal set; } = TimeSpan.FromSeconds(0.5);
/// <summary>
/// The next time at which this gravity well should pulse.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(GravityWellSystem))]
public TimeSpan NextPulseTime { get; internal set; } = default!;
/// <summary>
/// The last time this gravity well pulsed.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(GravityWellSystem))]
public TimeSpan LastPulseTime { get; internal set; } = default!;
#endregion Update Timing
}

View File

@@ -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<SharedSingularitySystem>();
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<TransformComponent>(Owner).Coordinates);
}
}
}

View File

@@ -0,0 +1,92 @@
using Content.Shared.Singularity.Components;
using Content.Server.Singularity.EntitySystems;
using Robust.Shared.Audio;
namespace Content.Server.Singularity.Components;
/// <summary>
/// The server-side version of <see cref="SharedSingularityComponent">.
/// Primarily managed by <see cref="SingularitySystem">.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(SharedSingularityComponent))]
public sealed class SingularityComponent : SharedSingularityComponent
{
/// <summary>
/// The amount of energy this singularity contains.
/// If you want to set this go through <see cref="SingularitySystem.SetEnergy"/>
/// </summary>
[DataField("energy")]
[Access(friends:typeof(SingularitySystem))]
public float Energy = 180f;
/// <summary>
/// The rate at which this singularity loses energy over time.
/// </summary>
[DataField("energyLoss")]
[ViewVariables(VVAccess.ReadWrite)]
public float EnergyDrain;
#region Audio
/// <summary>
/// The sound that this singularity produces by existing.
/// </summary>
[DataField("ambientSound")]
[ViewVariables(VVAccess.ReadOnly)]
public SoundSpecifier? AmbientSound = new SoundPathSpecifier(
"/Audio/Effects/singularity_form.ogg",
AudioParams.Default.WithVolume(5).WithLoop(true).WithMaxDistance(20f)
);
/// <summary>
/// The audio stream that plays the sound specified by <see cref="AmbientSound"> on loop.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public IPlayingAudioStream? AmbientSoundStream = null;
/// <summary>
/// The sound that the singularity produces when it forms.
/// </summary>
[DataField("formationSound")]
[ViewVariables(VVAccess.ReadOnly)]
public SoundSpecifier? FormationSound = null;
/// <summary>
/// The sound that the singularity produces when it dissipates.
/// </summary>
[DataField("dissipationSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? DissipationSound = new SoundPathSpecifier(
"/Audio/Effects/singularity_collapse.ogg",
AudioParams.Default
);
#endregion Audio
#region Update Timing
/// <summary>
/// The amount of time that should elapse between automated updates to this singularity.
/// </summary>
[DataField("updatePeriod")]
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SingularitySystem))]
public TimeSpan TargetUpdatePeriod { get; internal set; } = TimeSpan.FromSeconds(1.0);
/// <summary>
/// The next time this singularity should be updated by <see cref="SingularitySystem"/>
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SingularitySystem))]
public TimeSpan NextUpdateTime { get; internal set; } = default!;
/// <summary>
/// The last time this singularity was be updated by <see cref="SingularitySystem"/>
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SingularitySystem))]
public TimeSpan LastUpdateTime { get; internal set; } = default!;
#endregion Update Timing
}

View File

@@ -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] /// <summary>
public sealed class SingularityGeneratorComponent : Component /// The amount of power this generator has accumulated.
{ /// If you want to set this use <see cref="SingularityGeneratorSystem.SetPower"/>
[Dependency] private readonly IEntityManager _entMan = default!; /// </summary>
[DataField("power")]
[Access(friends:typeof(SingularityGeneratorSystem))]
public float Power = 0;
[ViewVariables] private int _power; /// <summary>
/// The power threshold at which this generator will spawn a singularity.
/// If you want to set this use <see cref="SingularityGeneratorSystem.SetThreshold"/>
/// </summary>
[DataField("threshold")]
[Access(friends:typeof(SingularityGeneratorSystem))]
public float Threshold = 16;
public int Power /// <summary>
{ /// The prototype ID used to spawn a singularity.
get => _power; /// </summary>
set [DataField("spawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
{ [ViewVariables(VVAccess.ReadWrite)]
if(_power == value) return; public string? SpawnPrototype = "Singularity";
_power = value;
if (_power > 15)
{
_entMan.SpawnEntity("Singularity", _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
//dont delete ourselves, just wait to get eaten
}
}
}
}
} }

View File

@@ -8,6 +8,6 @@ namespace Content.Server.Singularity.Components
{ {
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("energy")] [DataField("energy")]
public int Energy { get; set; } = 1; public float Energy { get; set; } = 1f;
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Singularity.Components; using Content.Server.Singularity.Components;
using Content.Server.Singularity.Events;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Content.Shared.Tag; using Content.Shared.Tag;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -22,6 +23,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
[Dependency] private readonly TagSystem _tags = default!; [Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!; [Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly AppearanceSystem _visualizer = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -34,6 +36,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent); SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt); SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved); SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, EventHorizonAttemptConsumeEntityEvent>(PreventBreach);
} }
public override void Update(float frameTime) public override void Update(float frameTime)
@@ -356,24 +359,11 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
/// <param name="component"></param> /// <param name="component"></param>
private void ChangePowerVisualizer(int power, ContainmentFieldGeneratorComponent component) private void ChangePowerVisualizer(int power, ContainmentFieldGeneratorComponent component)
{ {
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance)) _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.PowerLight, component.PowerBuffer switch {
return; <=0 => PowerLevelVisuals.NoPower,
>=25 => PowerLevelVisuals.HighPower,
if(component.PowerBuffer == 0) _ => (component.PowerBuffer < component.PowerMinimum) ? PowerLevelVisuals.LowPower : PowerLevelVisuals.MediumPower
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);
}
} }
/// <summary> /// <summary>
@@ -382,36 +372,30 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
/// <param name="component"></param> /// <param name="component"></param>
private void ChangeFieldVisualizer(ContainmentFieldGeneratorComponent component) private void ChangeFieldVisualizer(ContainmentFieldGeneratorComponent component)
{ {
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance)) _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.FieldLight, component.Connections.Count switch {
return; >1 => FieldLevelVisuals.MultipleFields,
1 => FieldLevelVisuals.OneField,
if (component.Connections.Count == 0 && !component.Enabled) _ => component.Enabled ? FieldLevelVisuals.On : FieldLevelVisuals.NoLevel
{ });
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);
}
} }
private void ChangeOnLightVisualizer(ContainmentFieldGeneratorComponent component) private void ChangeOnLightVisualizer(ContainmentFieldGeneratorComponent component)
{ {
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance)) _visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
return;
appearance.SetData(ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
} }
#endregion #endregion
/// <summary>
/// Prevents singularities from breaching containment if the containment field generator is connected.
/// </summary>
/// <param name="uid">The entity the singularity is trying to eat.</param>
/// <param name="comp">The containment field generator the singularity is trying to eat.</param>
/// <param name="args">The event arguments.</param>
private void PreventBreach(EntityUid uid, ContainmentFieldGeneratorComponent comp, EventHorizonAttemptConsumeEntityEvent args)
{
if (args.Cancelled)
return;
if (comp.IsConnected && !args.EventHorizon.CanBreachContainment)
args.Cancel();
}
} }

View File

@@ -1,12 +1,11 @@
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Server.Singularity.Components; using Content.Server.Singularity.Components;
using Content.Server.Singularity.EntitySystems;
using Content.Server.Singularity.Events;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Throwing; using Content.Shared.Throwing;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -22,6 +21,7 @@ public sealed class ContainmentFieldSystem : EntitySystem
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ContainmentFieldComponent, StartCollideEvent>(HandleFieldCollide); SubscribeLocalEvent<ContainmentFieldComponent, StartCollideEvent>(HandleFieldCollide);
SubscribeLocalEvent<ContainmentFieldComponent, EventHorizonAttemptConsumeEntityEvent>(HandleEventHorizon);
} }
private void HandleFieldCollide(EntityUid uid, ContainmentFieldComponent component, ref StartCollideEvent args) 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); _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();
}
} }

View File

@@ -15,6 +15,7 @@ using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -31,6 +32,7 @@ namespace Content.Server.Singularity.EntitySystems
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ProjectileSystem _projectile = default!; [Dependency] private readonly ProjectileSystem _projectile = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -205,7 +207,8 @@ namespace Content.Server.Singularity.EntitySystems
private void Fire(EmitterComponent component) private void Fire(EmitterComponent component)
{ {
var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent<TransformComponent>(component.Owner).Coordinates); var uid = component.Owner;
var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
if (!EntityManager.TryGetComponent<PhysicsComponent?>(projectile, out var physicsComponent)) if (!EntityManager.TryGetComponent<PhysicsComponent?>(projectile, out var physicsComponent))
{ {
@@ -223,9 +226,9 @@ namespace Content.Server.Singularity.EntitySystems
_projectile.SetShooter(projectileComponent, component.Owner); _projectile.SetShooter(projectileComponent, component.Owner);
physicsComponent var worldRotation = Transform(uid).WorldRotation;
.LinearVelocity = EntityManager.GetComponent<TransformComponent>(component.Owner).WorldRotation.ToWorldVec() * 20f; _physics.SetLinearVelocity(physicsComponent, worldRotation.ToWorldVec() * 20f);
EntityManager.GetComponent<TransformComponent>(projectile).WorldRotation = EntityManager.GetComponent<TransformComponent>(component.Owner).WorldRotation; Transform(projectile).WorldRotation = worldRotation;
// TODO: Move to projectile's code. // TODO: Move to projectile's code.
Timer.Spawn(3000, () => EntityManager.DeleteEntity(projectile)); Timer.Spawn(3000, () => EntityManager.DeleteEntity(projectile));

View File

@@ -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;
/// <summary>
/// The entity system primarily responsible for managing <see cref="EventHorizonComponent"/>s.
/// Handles their consumption of entities.
/// </summary>
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
/// <summary>
/// The maximum number of nested containers an event horizon is allowed to eat through in an attempt to get to the map.
/// </summary>
private const int MaxEventHorizonUnnestingIterations = 100;
/// <summary>
/// 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.
/// </summary>
private const int MaxEventHorizonDumpSearchIterations = 100;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MapGridComponent, EventHorizonAttemptConsumeEntityEvent>(PreventConsume);
SubscribeLocalEvent<GhostComponent, EventHorizonAttemptConsumeEntityEvent>(PreventConsume);
SubscribeLocalEvent<StationDataComponent, EventHorizonAttemptConsumeEntityEvent>(PreventConsume);
SubscribeLocalEvent<EventHorizonComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<EventHorizonComponent, EntGotInsertedIntoContainerMessage>(OnEventHorizonContained);
SubscribeLocalEvent<EventHorizonContainedEvent>(OnEventHorizonContained);
SubscribeLocalEvent<EventHorizonComponent, EventHorizonAttemptConsumeEntityEvent>(OnAnotherEventHorizonAttemptConsumeThisEventHorizon);
SubscribeLocalEvent<EventHorizonComponent, EventHorizonConsumedEntityEvent>(OnAnotherEventHorizonConsumedThisEventHorizon);
SubscribeLocalEvent<ContainerManagerComponent, EventHorizonConsumedEntityEvent>(OnContainerConsumed);
var vvHandle = Vvm.GetTypeHandler<EventHorizonComponent>();
vvHandle.AddPath(nameof(EventHorizonComponent.TargetConsumePeriod), (_, comp) => comp.TargetConsumePeriod, SetConsumePeriod);
}
public override void Shutdown()
{
var vvHandle = Vvm.GetTypeHandler<EventHorizonComponent>();
vvHandle.RemovePath(nameof(EventHorizonComponent.TargetConsumePeriod));
base.Shutdown();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="frameTime">The amount of time that has elapsed since the last cooldown update.</param>
public override void Update(float frameTime)
{
if(!_timing.IsFirstTimePredicted)
return;
foreach(var (eventHorizon, xform) in EntityManager.EntityQuery<EventHorizonComponent, TransformComponent>())
{
var curTime = _timing.CurTime;
if (eventHorizon.NextConsumeWaveTime <= curTime)
Update(eventHorizon.Owner, eventHorizon, xform);
}
}
/// <summary>
/// Makes an event horizon consume everything nearby and resets the cooldown it for the next automated wave.
/// </summary>
/// <param name="uid">The uid of the event horizon consuming everything nearby.</param>
/// <param name="eventHorizon">The event horizon we want to consume nearby things.</param>
/// <param name="xform">The transform of the event horizon.</param>
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
/// <summary>
/// Makes an event horizon consume a given entity.
/// </summary>
/// <param name="uid">The entity to consume.</param>
/// <param name="eventHorizon">The event horizon consuming the given entity.</param>
/// <param name="outerContainer">The innermost container of the entity to consume that isn't also being consumed by the event horizon.</param>
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));
}
/// <summary>
/// Makes an event horizon attempt to consume a given entity.
/// </summary>
/// <param name="uid">The entity to attempt to consume.</param>
/// <param name="eventHorizon">The event horizon attempting to consume the given entity.</param>
/// <param name="outerContainer">The innermost container of the entity to consume that isn't also being consumed by the event horizon.</param>
public bool AttemptConsumeEntity(EntityUid uid, EventHorizonComponent eventHorizon, IContainer? outerContainer = null)
{
if(!CanConsumeEntity(uid, eventHorizon))
return false;
ConsumeEntity(uid, eventHorizon, outerContainer);
return true;
}
/// <summary>
/// Checks whether an event horizon can consume a given entity.
/// </summary>
/// <param name="uid">The entity to check for consumability.</param>
/// <param name="eventHorizon">The event horizon checking whether it can consume the entity.</param>
public bool CanConsumeEntity(EntityUid uid, EventHorizonComponent eventHorizon)
{
var ev = new EventHorizonAttemptConsumeEntityEvent(uid, eventHorizon);
RaiseLocalEvent(uid, ev);
return !ev.Cancelled;
}
/// <summary>
/// Attempts to consume all entities within a given distance of an entity;
/// Excludes the center entity.
/// </summary>
/// <param name="uid">The entity uid in the center of the region to consume all entities within.</param>
/// <param name="range">The distance of the center entity within which to consume all entities.</param>
/// <param name="xform">The transform component attached to the center entity.</param>
/// <param name="eventHorizon">The event horizon component attached to the center entity.</param>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the event horizon. The single entity that is immune-by-default.</param>
/// <param name="container">The container within which to consume all entities.</param>
/// <param name="eventHorizon">The state of the event horizon.</param>
/// <param name="outerContainer">The location any immune entities within the container should be dumped to.</param>
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<EntityUid> 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
/// <summary>
/// Makes an event horizon consume a specific tile on a grid.
/// </summary>
/// <param name="tile">The tile to consume.</param>
/// <param name="eventHorizon">The event horizon which is consuming the tile on the grid.</param>
public void ConsumeTile(TileRef tile, EventHorizonComponent eventHorizon)
=> ConsumeTiles(new List<(Vector2i, Tile)>(new []{(tile.GridIndices, Tile.Empty)}), _mapMan.GetGrid(tile.GridUid), eventHorizon);
/// <summary>
/// Makes an event horizon attempt to consume a specific tile on a grid.
/// </summary>
/// <param name="tile">The tile to attempt to consume.</param>
/// <param name="eventHorizon">The event horizon which is attempting to consume the tile on the grid.</param>
public void AttemptConsumeTile(TileRef tile, EventHorizonComponent eventHorizon)
=> AttemptConsumeTiles(new TileRef[1]{tile}, _mapMan.GetGrid(tile.GridUid), eventHorizon);
/// <summary>
/// Makes an event horizon consume a set of tiles on a grid.
/// </summary>
/// <param name="tiles">The tiles to consume.</param>
/// <param name="grid">The grid hosting the tiles to consume.</param>
/// <param name="eventHorizon">The event horizon which is consuming the tiles on the grid.</param>
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);
}
/// <summary>
/// Makes an event horizon attempt to consume a set of tiles on a grid.
/// </summary>
/// <param name="tiles">The tiles to attempt to consume.</param>
/// <param name="grid">The grid hosting the tiles to attempt to consume.</param>
/// <param name="eventHorizon">The event horizon which is attempting to consume the tiles on the grid.</param>
public int AttemptConsumeTiles(IEnumerable<TileRef> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="tile">The tile to check for consumability.</param>
/// <param name="grid">The grid hosting the tile to check.</param>
/// <param name="eventHorizon">The event horizon which is checking to see if it can consume the tile on the grid.</param>
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;
}
/// <summary>
/// Consumes all tiles within a given distance of an entity.
/// Some entities are immune to consumption.
/// </summary>
/// <param name="uid">The entity uid in the center of the region to consume all tiles within.</param>
/// <param name="range">The distance of the center entity within which to consume all tiles.</param>
/// <param name="xform">The transform component attached to the center entity.</param>
/// <param name="eventHorizon">The event horizon component attached to the center entity.</param>
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
/// <summary>
/// Consumes most entities and tiles within a given distance of an entity.
/// Some entities are immune to consumption.
/// </summary>
/// <param name="uid">The entity uid in the center of the region to consume everything within.</param>
/// <param name="range">The distance of the center entity within which to consume everything.</param>
/// <param name="xform">The transform component attached to the center entity.</param>
/// <param name="eventHorizon">The event horizon component attached to the center entity.</param>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the event horizon to set the consume wave period for.</param>
/// <param name="value">The amount of time that this subsystem should wait between scans.</param>
/// <param name="eventHorizon">The state of the event horizon to set the consume wave period for.</param>
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
/// <summary>
/// Prevents a singularity from colliding with anything it is incapable of consuming.
/// </summary>
/// <param name="uid">The event horizon entity that is trying to collide with something.</param>
/// <param name="comp">The event horizon that is trying to collide with something.</param>
/// <param name="args">The event arguments.</param>
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;
}
/// <summary>
/// A generic event handler that prevents singularities from consuming entities with a component of a given type if registered.
/// </summary>
/// <param name="uid">The entity the singularity is trying to eat.</param>
/// <param name="comp">The component the singularity is trying to eat.</param>
/// <param name="args">The event arguments.</param>
public void PreventConsume<TComp>(EntityUid uid, TComp comp, EventHorizonAttemptConsumeEntityEvent args)
{
if(!args.Cancelled)
args.Cancel();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The entity the singularity is trying to eat.</param>
/// <param name="comp">The component the singularity is trying to eat.</param>
/// <param name="args">The event arguments.</param>
public void PreventBreach<TComp>(EntityUid uid, TComp comp, EventHorizonAttemptConsumeEntityEvent args)
{
if (args.Cancelled)
return;
if(!args.EventHorizon.CanBreachContainment)
PreventConsume(uid, comp, args);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The event horizon entity.</param>
/// <param name="comp">The event horizon.</param>
/// <param name="args">The event arguments.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The event horizon entity.</param>
/// <param name="comp">The event horizon.</param>
/// <param name="args">The event arguments.</param>
private void OnAnotherEventHorizonAttemptConsumeThisEventHorizon(EntityUid uid, EventHorizonComponent comp, EventHorizonAttemptConsumeEntityEvent args)
{
if(!args.Cancelled && (args.EventHorizon == comp || comp.BeingConsumedByAnotherEventHorizon))
args.Cancel();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The event horizon entity.</param>
/// <param name="comp">The event horizon.</param>
/// <param name="args">The event arguments.</param>
private void OnAnotherEventHorizonConsumedThisEventHorizon(EntityUid uid, EventHorizonComponent comp, EventHorizonConsumedEntityEvent args)
{
comp.BeingConsumedByAnotherEventHorizon = true;
}
/// <summary>
/// Handles event horizons deciding to escape containers they are inserted into.
/// Delegates the actual escape to <see cref="EventHorizonSystem.OnEventHorizonContained(EventHorizonContainedEvent)"> 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
/// <see cref="EntGotInsertedIntoContainerMessage"> are processed.
/// </summary>
/// <param name="uid">The uid of the event horizon.</param>]
/// <param name="comp">The state of the event horizon.</param>]
/// <param name="args">The arguments of the insertion.</param>]
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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="args">The arguments for this event.</param>]
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the container being consumed.</param>
/// <param name="comp">The state of the container being consumed.</param>
/// <param name="args">The event arguments.</param>
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
}

View File

@@ -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;
/// <summary>
/// The server side version of <see cref="SharedGravityWellSystem"/>.
/// Primarily responsible for managing <see cref="GravityWellComponent"/>s.
/// Handles the gravitational pulses they can emit.
/// </summary>
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
/// <summary>
/// The minimum range at which gravpulses will act.
/// Prevents division by zero problems.
/// </summary>
public const float MinGravPulseRange = 0.00001f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GravityWellComponent, ComponentStartup>(OnGravityWellStartup);
var vvHandle = _vvManager.GetTypeHandler<GravityWellComponent>();
vvHandle.AddPath(nameof(GravityWellComponent.TargetPulsePeriod), (_, comp) => comp.TargetPulsePeriod, SetPulsePeriod);
}
public override void Shutdown()
{
var vvHandle = _vvManager.GetTypeHandler<GravityWellComponent>();
vvHandle.RemovePath(nameof(GravityWellComponent.TargetPulsePeriod));
base.Shutdown();
}
/// <summary>
/// Updates the pulse cooldowns of all gravity wells.
/// If they are off cooldown it makes them emit a gravitational pulse and reset their cooldown.
/// </summary>
/// <param name="frameTime">The time elapsed since the last set of updates.</param>
public override void Update(float frameTime)
{
if(!_timing.IsFirstTimePredicted)
return;
foreach(var (gravWell, xform) in EntityManager.EntityQuery<GravityWellComponent, TransformComponent>())
{
var curTime = _timing.CurTime;
if (gravWell.NextPulseTime <= curTime)
Update(gravWell.Owner, curTime - gravWell.LastPulseTime, gravWell, xform);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the gravity well to make pulse.</param>
/// <param name="gravWell">The state of the gravity well to make pulse.</param>
/// <param name="xform">The transform of the gravity well to make pulse.</param>
private void Update(EntityUid uid, GravityWellComponent? gravWell = null, TransformComponent? xform = null)
{
if (Resolve(uid, ref gravWell))
Update(uid, _timing.CurTime - gravWell.LastPulseTime, gravWell, xform);
}
/// <summary>
/// Makes a gravity well emit a gravitational pulse and puts it on cooldown.
/// </summary>
/// <param name="uid">The uid of the gravity well to make pulse.</param>
/// <param name="gravWell">The state of the gravity well to make pulse.</param>
/// <param name="frameTime">The amount to consider as having passed since the last gravitational pulse by the gravity well. Pulse force scales with this.</param>
/// <param name="xform">The transform of the gravity well to make pulse.</param>
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
/// <summary>
/// Checks whether an entity can be affected by gravity pulses.
/// TODO: Make this an event or such.
/// </summary>
/// <param name="entity">The entity to check.</param>
private bool CanGravPulseAffect(EntityUid entity)
{
return !(
EntityManager.HasComponent<GhostComponent>(entity) ||
EntityManager.HasComponent<MapGridComponent>(entity) ||
EntityManager.HasComponent<MapComponent>(entity) ||
EntityManager.HasComponent<GravityWellComponent>(entity)
);
}
/// <summary>
/// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="uid">The entity at the epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="baseMatrixDeltaV">The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.</param>
/// <param name="xform">(optional) The transform of the entity at the epicenter of the gravitational pulse.</param>
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);
}
/// <summary>
/// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="uid">The entity at the epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="baseRadialDeltaV">The base radial velocity that will be added to entities within range towards the center of the gravitational pulse.</param>
/// <param name="baseTangentialDeltaV">The base tangential velocity that will be added to entities within countrclockwise around the center of the gravitational pulse.</param>
/// <param name="xform">(optional) The transform of the entity at the epicenter of the gravitational pulse.</param>
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);
}
/// <summary>
/// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="entityPos">The epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="baseMatrixDeltaV">The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.</param>
public void GravPulse(EntityCoordinates entityPos, float maxRange, float minRange, in Matrix3 baseMatrixDeltaV)
=> GravPulse(entityPos.ToMap(EntityManager), maxRange, minRange, in baseMatrixDeltaV);
/// <summary>
/// Greates a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="entityPos">The epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="baseRadialDeltaV">The base radial velocity that will be added to entities within range towards the center of the gravitational pulse.</param>
/// <param name="baseTangentialDeltaV">The base tangential velocity that will be added to entities within countrclockwise around the center of the gravitational pulse.</param>
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);
/// <summary>
/// Causes a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="mapPos">The epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse. Exists to prevent div/0 errors.</param>
/// <param name="baseMatrixDeltaV">The base velocity added to any entities within affected by the gravity pulse scaled by the displacement of those entities from the epicenter.</param>
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<PhysicsComponent?>(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);
}
}
/// <summary>
/// Causes a gravitational pulse, shoving around all entities within some distance of an epicenter.
/// </summary>
/// <param name="mapPos">The epicenter of the gravity pulse.</param>
/// <param name="maxRange">The maximum distance at which entities can be affected by the gravity pulse.</param>
/// <param name="minRange">The minimum distance at which entities can be affected by the gravity pulse. Exists to prevent div/0 errors.</param>
/// <param name="baseRadialDeltaV">The base amount of velocity that will be added to entities in range towards the epicenter of the pulse.</param>
/// <param name="baseTangentialDeltaV">The base amount of velocity that will be added to entities in range counterclockwise relative to the epicenter of the pulse.</param>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the gravity well to set the pulse period for.</param>
/// <param name="value">The new pulse period for the gravity well.</param>
/// <param name="gravWell">The state of the gravity well to set the pulse period for.</param>
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
/// <summary>
/// Resets the pulse timings of the gravity well when the components starts up.
/// </summary>
/// <param name="uid">The uid of the gravity well to start up.</param>
/// <param name="comp">The state of the gravity well to start up.</param>
/// <param name="args">The startup prompt arguments.</param>
public void OnGravityWellStartup(EntityUid uid, GravityWellComponent comp, ComponentStartup args)
{
comp.LastPulseTime = _timing.CurTime;
comp.NextPulseTime = comp.LastPulseTime + comp.TargetPulsePeriod;
}
#endregion Event Handlers
}

View File

@@ -1,20 +1,126 @@
using Content.Server.ParticleAccelerator.Components; using Content.Server.ParticleAccelerator.Components;
using Content.Server.Singularity.Components; using Content.Server.Singularity.Components;
using Content.Shared.Singularity.Components; using Content.Shared.Singularity.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
namespace Content.Server.Singularity.EntitySystems; namespace Content.Server.Singularity.EntitySystems;
public sealed class SingularityGeneratorSystem : EntitySystem public sealed class SingularityGeneratorSystem : EntitySystem
{ {
#region Dependencies
[Dependency] private readonly IViewVariablesManager _vvm = default!;
#endregion Dependencies
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ParticleProjectileComponent, StartCollideEvent>(HandleParticleCollide); SubscribeLocalEvent<ParticleProjectileComponent, StartCollideEvent>(HandleParticleCollide);
var vvHandle = _vvm.GetTypeHandler<SingularityGeneratorComponent>();
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<SingularityGeneratorComponent>();
vvHandle.RemovePath(nameof(SingularityGeneratorComponent.Power));
vvHandle.RemovePath(nameof(SingularityGeneratorComponent.Threshold));
base.Shutdown();
}
/// <summary>
/// Handles what happens when a singularity generator passes its power threshold.
/// Default behavior is to reset the singularities power level and spawn a singularity.
/// </summary>
/// <param name="uid">The uid of the singularity generator.</param>
/// <param name="comp">The state of the singularity generator.</param>
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
/// <summary>
/// Setter for <see cref="SingularityGeneratorComponent.Power"/>
/// If the singularity generator passes its threshold it also spawns a singularity.
/// </summary>
/// <param name="comp">The singularity generator component.</param>
/// <param name="value">The new power level for the generator component to have.</param>
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);
}
/// <summary>
/// Setter for <see cref="SingularityGeneratorComponent.Threshold"/>
/// If the singularity generator has passed its new threshold it also spawns a singularity.
/// </summary>
/// <param name="comp">The singularity generator component.</param>
/// <param name="value">The new threshold power level for the generator component to have.</param>
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
/// <summary>
/// VV setter for <see cref="SingularityGeneratorComponent.Power"/>
/// If the singularity generator passes its threshold it also spawns a singularity.
/// </summary>
/// <param name="uid">The entity hosting the singularity generator that is being modified.</param>
/// <param name="value">The value of the new power level the singularity generator should have.</param>
/// <param name="comp">The singularity generator to change the power level of.</param>
public void SetPower(EntityUid uid, float value, SingularityGeneratorComponent? comp)
{
if(!Resolve(uid, ref comp))
return;
SetPower(comp, value);
}
/// <summary>
/// VV setter for <see cref="SingularityGeneratorComponent.Threshold"/>
/// If the singularity generator has passed its new threshold it also spawns a singularity.
/// </summary>
/// <param name="uid">The entity hosting the singularity generator that is being modified.</param>
/// <param name="value">The value of the new threshold power level the singularity generator should have.</param>
/// <param name="comp">The singularity generator to change the threshold power level of.</param>
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
/// <summary>
/// Handles PA Particles colliding with a singularity generator.
/// Adds the power from the particles to the generator.
/// TODO: Desnowflake this.
/// </summary>
/// <param name="uid">The uid of the PA particles have collided with.</param>
/// <param name="component">The state of the PA particles.</param>
/// <param name="args">The state of the beginning of the collision.</param>
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args) private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, ref StartCollideEvent args)
{ {
if (EntityManager.TryGetComponent<SingularityGeneratorComponent?>(args.OtherFixture.Body.Owner, out var singularityGeneratorComponent)) if (EntityManager.TryGetComponent<SingularityGeneratorComponent?>(args.OtherFixture.Body.Owner, out var singularityGeneratorComponent))
@@ -31,4 +137,5 @@ public sealed class SingularityGeneratorSystem : EntitySystem
EntityManager.QueueDeleteEntity(uid); EntityManager.QueueDeleteEntity(uid);
} }
} }
#endregion Event Handlers
} }

View File

@@ -1,265 +1,350 @@
using Content.Server.Ghost.Components; using Robust.Shared.GameStates;
using Content.Server.Singularity.Components; using Robust.Shared.Player;
using Content.Server.Station.Components; using Robust.Shared.Timing;
using Content.Shared.Singularity;
using Content.Shared.Singularity.Components;
using JetBrains.Annotations;
using Robust.Server.GameStates; 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;
/// <summary>
/// The server-side version of <seed cref="SharedSingularitySystem">.
/// Primarily responsible for managing <see cref="SingularityComponent"/>s.
/// Handles their accumulation of energy upon consuming entities (see <see cref="EventHorizonComponent">) and gradual dissipation.
/// Also handles synchronizing server-side components with the singuarities level.
/// </summary>
public sealed class SingularitySystem : SharedSingularitySystem
{ {
[UsedImplicitly] #region Dependencies
public sealed class SingularitySystem : SharedSingularitySystem [Dependency] private readonly IGameTiming _timing = default!;
{ [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly PVSOverrideSystem _pvs = default!; [Dependency] private readonly PVSOverrideSystem _pvs = default!;
#endregion Dependencies
/// <summary> /// <summary>
/// How much energy the singulo gains from destroying a tile. /// The amount of energy singulos accumulate when they eat a tile.
/// </summary> /// </summary>
private const int TileEnergyGain = 1; public const float BaseTileEnergy = 1f;
private const float GravityCooldown = 0.5f; /// <summary>
private float _gravityAccumulator; /// The amount of energy singulos accumulate when they eat an entity.
/// </summary>
private int _updateInterval = 1; public const float BaseEntityEnergy = 1f;
private float _accumulator;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ServerSingularityComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<SingularityDistortionComponent, ComponentStartup>(OnDistortionStartup); SubscribeLocalEvent<SingularityDistortionComponent, ComponentStartup>(OnDistortionStartup);
SubscribeLocalEvent<SingularityComponent, ComponentStartup>(OnSingularityStartup);
SubscribeLocalEvent<SingularityComponent, ComponentShutdown>(OnSingularityShutdown);
SubscribeLocalEvent<SingularityComponent, EventHorizonConsumedEntityEvent>(OnConsumed);
SubscribeLocalEvent<SinguloFoodComponent, EventHorizonConsumedEntityEvent>(OnConsumed);
SubscribeLocalEvent<SingularityComponent, EntityConsumedByEventHorizonEvent>(OnConsumedEntity);
SubscribeLocalEvent<SingularityComponent, TilesConsumedByEventHorizonEvent>(OnConsumedTiles);
SubscribeLocalEvent<SingularityComponent, SingularityLevelChangedEvent>(UpdateEnergyDrain);
SubscribeLocalEvent<SingularityComponent, ComponentGetState>(HandleSingularityState);
// TODO: Figure out where all this coupling should be handled.
SubscribeLocalEvent<RandomWalkComponent, SingularityLevelChangedEvent>(UpdateRandomWalk);
SubscribeLocalEvent<GravityWellComponent, SingularityLevelChangedEvent>(UpdateGravityWell);
var vvHandle = Vvm.GetTypeHandler<SingularityComponent>();
vvHandle.AddPath(nameof(SingularityComponent.Energy), (_, comp) => comp.Energy, SetEnergy);
vvHandle.AddPath(nameof(SingularityComponent.TargetUpdatePeriod), (_, comp) => comp.TargetUpdatePeriod, SetUpdatePeriod);
} }
private void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent component, ComponentStartup args) public override void Shutdown()
{
var vvHandle = Vvm.GetTypeHandler<SingularityComponent>();
vvHandle.RemovePath(nameof(SingularityComponent.Energy));
vvHandle.RemovePath(nameof(SingularityComponent.TargetUpdatePeriod));
base.Shutdown();
}
/// <summary>
/// Handles the gradual dissipation of all singularities.
/// </summary>
/// <param name="frameTime">The amount of time since the last set of updates.</param>
public override void Update(float frameTime)
{
if(!_timing.IsFirstTimePredicted)
return;
foreach(var singularity in EntityManager.EntityQuery<SingularityComponent>())
{
var curTime = _timing.CurTime;
if (singularity.NextUpdateTime <= curTime)
Update(singularity.Owner, curTime - singularity.LastUpdateTime, singularity);
}
}
/// <summary>
/// Handles the gradual energy loss and dissipation of singularity.
/// </summary>
/// <param name="uid">The uid of the singularity to update.</param>
/// <param name="singularity">The state of the singularity to update.</param>
public void Update(EntityUid uid, SingularityComponent? singularity = null)
{
if (Resolve(uid, ref singularity))
Update(uid, _timing.CurTime - singularity.LastUpdateTime, singularity);
}
/// <summary>
/// Handles the gradual energy loss and dissipation of a singularity.
/// </summary>
/// <param name="uid">The uid of the singularity to update.</param>
/// <param name="frameTime">The amount of time that has elapsed since the last update.</param>
/// <param name="singularity">The state of the singularity to update.</param>
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
/// <summary>
/// Setter for <see cref="SingularityComponent.Energy"/>.
/// Also updates the level of the singularity accordingly.
/// </summary>
/// <param name="uid">The uid of the singularity to set the energy of.</param>
/// <param name="value">The amount of energy for the singularity to have.</param>
/// <param name="singularity">The state of the singularity to set the energy of.</param>
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);
}
/// <summary>
/// Adjusts the amount of energy the singularity has accumulated.
/// </summary>
/// <param name="uid">The uid of the singularity to adjust the energy of.</param>
/// <param name="delta">The amount to adjust the energy of the singuarity.</param>
/// <param name="min">The minimum amount of energy for the singularity to be adjusted to.</param>
/// <param name="max">The maximum amount of energy for the singularity to be adjusted to.</param>
/// <param name="hardMin">Whether the amount of energy in the singularity should be forced to within the specified range if it already is below it.</param>
/// <param name="hardMax">Whether the amount of energy in the singularity should be forced to within the specified range if it already is above it.</param>
/// <param name="singularity">The state of the singularity to adjust the energy of.</param>
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);
}
/// <summary>
/// Setter for <see cref="SingularityComponent.TargetUpdatePeriod"/>.
/// If the new target time implies that the singularity should have updated it does so immediately.
/// </summary>
/// <param name="uid">The uid of the singularity to set the update period for.</param>
/// <param name="value">The new update period for the singularity.</param>
/// <param name="singularity">The state of the singularity to set the update period for.</param>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The entity UID of the singularity that is forming.</param>
/// <param name="comp">The component of the singularity that is forming.</param>
/// <param name="args">The event arguments.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The entity UID of the entity that is gaining the shader.</param>
/// <param name="comp">The component of the shader that the entity is gaining.</param>
/// <param name="args">The event arguments.</param>
public void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent comp, 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); _pvs.AddGlobalOverride(uid);
} }
protected override bool PreventCollide(EntityUid uid, SharedSingularityComponent component, ref PreventCollideEvent args) /// <summary>
/// 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.
/// </summary>
/// <param name="uid">The entity UID of the singularity that is dissipating.</param>
/// <param name="comp">The component of the singularity that is dissipating.</param>
/// <param name="args">The event arguments.</param>
public void OnSingularityShutdown(EntityUid uid, SingularityComponent comp, ComponentShutdown args)
{ {
if (base.PreventCollide(uid, component, ref args)) return true; comp.AmbientSoundStream?.Stop();
var otherUid = args.BodyB.Owner; MetaDataComponent? metaData = null;
if (Resolve(uid, ref metaData) && metaData.EntityLifeStage >= EntityLifeStage.Terminating)
if (args.Cancelled) return true; _audio.Play(comp.DissipationSound, Filter.Pvs(comp.Owner), comp.Owner, 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<ServerSingularityComponent>())
{
singularity.Energy -= singularity.EnergyDrain;
}
}
while (_gravityAccumulator > GravityCooldown)
{
_gravityAccumulator -= GravityCooldown;
foreach (var (singularity, xform) in EntityManager.EntityQuery<ServerSingularityComponent, TransformComponent>())
{
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<MapGridComponent>(entity) &&
!EntityManager.HasComponent<GhostComponent>(entity) &&
!EntityManager.HasComponent<StationDataComponent>(entity) && // these SHOULD be in null-space... but just in case. Also, maybe someone moves a singularity there..
(component.Level > 4 ||
!EntityManager.HasComponent<ContainmentFieldComponent>(entity) &&
!(EntityManager.TryGetComponent<ContainmentFieldGeneratorComponent>(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<ServerSingularityComponent?>(entity, out var otherSingulo))
{
// MERGE
if (!otherSingulo.BeingDeletedByAnotherSingularity)
{
component.Energy += otherSingulo.Energy;
}
otherSingulo.BeingDeletedByAnotherSingularity = true;
}
if (EntityManager.TryGetComponent<SinguloFoodComponent?>(entity, out var singuloFood))
component.Energy += singuloFood.Energy;
else
component.Energy++;
EntityManager.QueueDeleteEntity(entity);
} }
/// <summary> /// <summary>
/// Handle deleting entities and increasing energy /// Handles wrapping the state of a singularity for server-client syncing.
/// </summary> /// </summary>
private void DestroyEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos) /// <param name="uid">The uid of the singularity that is being synced.</param>
/// <param name="comp">The state of the singularity that is being synced.</param>
/// <param name="args">The event arguments.</param>
private void HandleSingularityState(EntityUid uid, SingularityComponent comp, ref ComponentGetState args)
{ {
// The reason we don't /just/ use collision is because we'll be deleting stuff that may not necessarily have physics (e.g. carpets). args.State = new SingularityComponentState(comp);
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<GhostComponent>(entity) ||
EntityManager.HasComponent<MapGridComponent>(entity) ||
EntityManager.HasComponent<MapComponent>(entity) ||
EntityManager.HasComponent<ServerSingularityComponent>(entity) ||
_container.IsEntityInContainer(entity));
} }
/// <summary> /// <summary>
/// Pull dynamic bodies in range to the singulo. /// Adds the energy of any entities that are consumed to the singularity that consumed them.
/// </summary> /// </summary>
private void PullEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos, float frameTime) /// <param name="uid">The entity UID of the singularity that is consuming the entity.</param>
/// <param name="comp">The component of the singularity that is consuming the entity.</param>
/// <param name="args">The event arguments.</param>
public void OnConsumedEntity(EntityUid uid, SingularityComponent comp, EntityConsumedByEventHorizonEvent args)
{ {
// TODO: When we split up dynamic and static trees we might be able to make items always on the broadphase AdjustEnergy(uid, BaseEntityEnergy, singularity: comp);
// 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)) /// <summary>
/// Adds the energy of any tiles that are consumed to the singularity that consumed them.
/// </summary>
/// <param name="uid">The entity UID of the singularity that is consuming the tiles.</param>
/// <param name="comp">The component of the singularity that is consuming the tiles.</param>
/// <param name="args">The event arguments.</param>
public void OnConsumedTiles(EntityUid uid, SingularityComponent comp, TilesConsumedByEventHorizonEvent args)
{ {
// I tried having it so level 6 can de-anchor. BAD IDEA, MASSIVE LAG. AdjustEnergy(uid, args.Tiles.Count * BaseTileEnergy, singularity: comp);
if (entity == component.Owner || }
!TryComp<PhysicsComponent?>(entity, out var collidableComponent) ||
collidableComponent.BodyType == BodyType.Static) continue;
if (!CanPull(entity)) continue; /// <summary>
/// Adds the energy of this singularity to singularities consume it.
var vec = worldPos - Transform(entity).WorldPosition; /// </summary>
/// <param name="uid">The entity UID of the singularity that is being consumed.</param>
if (vec.Length < destroyRange - 0.01f) continue; /// <param name="comp">The component of the singularity that is being consumed.</param>
/// <param name="args">The event arguments.</param>
var speed = 1f / vec.Length * component.Level * collidableComponent.Mass * 10f; private void OnConsumed(EntityUid uid, SingularityComponent comp, EventHorizonConsumedEntityEvent args)
{
// Because tile friction is so high we'll just multiply by mass so stuff like closets can even move. // Should be slightly more efficient than checking literally everything we consume for a singularity component and doing the reverse.
collidableComponent.ApplyLinearImpulse(vec.Normalized * speed * frameTime); if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizon.Owner, out var singulo))
{
AdjustEnergy(singulo.Owner, comp.Energy, singularity: singulo);
SetEnergy(uid, 0.0f, comp);
} }
} }
/// <summary> /// <summary>
/// Destroy any grid tiles within the relevant Level range. /// Adds some bonus energy from any singularity food to the singularity that consumes it.
/// </summary> /// </summary>
private void DestroyTiles(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos) /// <param name="uid">The entity UID of the singularity food that is being consumed.</param>
/// <param name="comp">The component of the singularity food that is being consumed.</param>
/// <param name="args">The event arguments.</param>
public void OnConsumed(EntityUid uid, SinguloFoodComponent comp, EventHorizonConsumedEntityEvent args)
{ {
var radius = DestroyTileRange(component); if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizon.Owner, out var singulo))
AdjustEnergy(args.EventHorizon.Owner, comp.Energy, singularity: singulo);
}
var circle = new Circle(worldPos, radius); /// <summary>
var box = new Box2(worldPos - radius, worldPos + radius); /// Updates the rate at which the singularities energy drains at when its level changes.
/// </summary>
foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, box)) /// <param name="uid">The entity UID of the singularity that changed in level.</param>
/// <param name="comp">The component of the singularity that changed in level.</param>
/// <param name="args">The event arguments.</param>
public void UpdateEnergyDrain(EntityUid uid, SingularityComponent comp, SingularityLevelChangedEvent args)
{ {
// Bundle these together so we can use the faster helper to set tiles. comp.EnergyDrain = args.NewValue switch {
var toDestroy = new List<(Vector2i, Tile)>(); 6 => 20,
5 => 15,
4 => 10,
3 => 5,
2 => 2,
1 => 1,
_ => 0
};
}
foreach (var tile in grid.GetTilesIntersecting(circle)) /// <summary>
/// Updates the possible speeds of the singulos random walk when the singularities level changes.
/// </summary>
/// <param name="uid">The entity UID of the singularity.</param>
/// <param name="comp">The random walk component component sharing the entity with the singulo component.</param>
/// <param name="args">The event arguments.</param>
private void UpdateRandomWalk(EntityUid uid, RandomWalkComponent comp, SingularityLevelChangedEvent args)
{ {
if (tile.Tile.IsEmpty) continue; var scale = MathF.Max(args.NewValue, 4);
comp.MinSpeed = 7.5f / scale;
comp.MaxSpeed = 10f / scale;
}
// Avoid ripping up tiles that may be essential to containment /// <summary>
if (component.Level < 5) /// Updates the size and strength of the singularities gravity well when the singularities level changes.
/// </summary>
/// <param name="uid">The entity UID of the singularity.</param>
/// <param name="comp">The gravity well component sharing the entity with the singulo component.</param>
/// <param name="args">The event arguments.</param>
private void UpdateGravityWell(EntityUid uid, GravityWellComponent comp, SingularityLevelChangedEvent args)
{ {
var canDelete = true; var singulos = args.Singularity;
comp.MaxRange = GravPulseRange(singulos);
foreach (var ent in grid.GetAnchoredEntities(tile.GridIndices)) (comp.BaseRadialAcceleration, comp.BaseTangentialAcceleration) = GravPulseAcceleration(singulos);
{
if (EntityManager.HasComponent<ContainmentFieldComponent>(ent) ||
EntityManager.HasComponent<ContainmentFieldGeneratorComponent>(ent))
{
canDelete = false;
break;
}
} }
if (!canDelete) continue; #endregion Event Handlers
}
toDestroy.Add((tile.GridIndices, Tile.Empty));
}
component.Energy += TileEnergyGain * toDestroy.Count;
grid.SetTiles(toDestroy);
}
}
}
} }

View File

@@ -0,0 +1,33 @@
using Content.Shared.Singularity.Components;
using Robust.Shared.Containers;
namespace Content.Server.Singularity.Events;
/// <summary>
/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
/// </summary>
public sealed class EntityConsumedByEventHorizonEvent : EntityEventArgs
{
/// <summary>
/// The entity being consumed by the event horizon.
/// </summary>
public readonly EntityUid Entity;
/// <summary>
/// The event horizon consuming the entity.
/// </summary>
public readonly EventHorizonComponent EventHorizon;
/// <summary>
/// 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.
/// </summary>
public readonly IContainer? Container;
public EntityConsumedByEventHorizonEvent(EntityUid entity, EventHorizonComponent eventHorizon, IContainer? container = null)
{
Entity = entity;
EventHorizon = eventHorizon;
Container = container;
}
}

View File

@@ -0,0 +1,26 @@
using Content.Shared.Singularity.Components;
namespace Content.Server.Singularity.Events;
/// <summary>
/// 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.
/// </summary>
public sealed class EventHorizonAttemptConsumeEntityEvent : CancellableEntityEventArgs
{
/// <summary>
/// The entity that the event horizon is attempting to consume.
/// </summary>
public readonly EntityUid Entity;
/// <summary>
/// The event horizon consuming the target entity.
/// </summary>
public readonly EventHorizonComponent EventHorizon;
public EventHorizonAttemptConsumeEntityEvent(EntityUid entity, EventHorizonComponent eventHorizon)
{
Entity = entity;
EventHorizon = eventHorizon;
}
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Singularity.Components;
using Robust.Shared.Containers;
namespace Content.Server.Singularity.Events;
/// <summary>
/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
/// </summary>
public sealed class EventHorizonConsumedEntityEvent : EntityEventArgs
{
/// <summary>
/// The entity being consumed by the event horizon.
/// </summary>
public readonly EntityUid Entity;
/// <summary>
/// The event horizon consuming the target entity.
/// </summary>
public readonly EventHorizonComponent EventHorizon;
/// <summary>
/// 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.
/// </summary>
public readonly IContainer? Container;
public EventHorizonConsumedEntityEvent(EntityUid entity, EventHorizonComponent eventHorizon, IContainer? container = null)
{
Entity = entity;
EventHorizon = eventHorizon;
Container = container;
}
}

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Containers;
using Content.Shared.Singularity.Components;
namespace Content.Shared.Singularity.EntitySystems;
/// <summary>
/// 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.
/// </summary>
public sealed class EventHorizonContainedEvent : EntityEventArgs {
/// <summary>
/// The uid of the event horizon that has been contained.
/// </summary>
public readonly EntityUid Entity;
/// <summary>
/// The state of the event horizon that has been contained.
/// </summary>
public readonly EventHorizonComponent EventHorizon;
/// <summary>
/// The arguments of the action that resulted in the event horizon being contained.
/// </summary>
public readonly EntGotInsertedIntoContainerMessage Args;
public EventHorizonContainedEvent(EntityUid entity, EventHorizonComponent eventHorizon, EntGotInsertedIntoContainerMessage args) {
Entity = entity;
EventHorizon = eventHorizon;
Args = args;
}
}

View File

@@ -0,0 +1,35 @@
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Content.Shared.Singularity.Components;
namespace Content.Server.Singularity.Events;
/// <summary>
/// Event raised on the event horizon entity whenever an event horizon consumes an entity.
/// </summary>
public sealed class TilesConsumedByEventHorizonEvent : EntityEventArgs
{
/// <summary>
/// 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 <see cref="Tile.Empty"/>.
/// </summary>
public readonly IReadOnlyList<(Vector2i, Tile)> Tiles;
/// <summary>
/// The mapgrid that the event horizon is consuming tiles of.
/// </summary>
public readonly MapGridComponent MapGrid;
/// <summary>
/// The event horizon consuming the tiles.
/// </summary>
public readonly EventHorizonComponent EventHorizon;
public TilesConsumedByEventHorizonEvent(IReadOnlyList<(Vector2i, Tile)> tiles, MapGridComponent mapGrid, EventHorizonComponent eventHorizon)
{
Tiles = tiles;
MapGrid = mapGrid;
EventHorizon = eventHorizon;
}
}

View File

@@ -24,13 +24,14 @@ namespace Content.Server.Singularity
} }
var entityManager = IoCManager.Resolve<IEntityManager>(); var entityManager = IoCManager.Resolve<IEntityManager>();
var entitySystemManager = IoCManager.Resolve<IEntitySystemManager>();
foreach (var comp in entityManager.EntityQuery<EmitterComponent>()) foreach (var comp in entityManager.EntityQuery<EmitterComponent>())
{ {
EntitySystem.Get<EmitterSystem>().SwitchOn(comp); entitySystemManager.GetEntitySystem<EmitterSystem>().SwitchOn(comp);
} }
foreach (var comp in entityManager.EntityQuery<RadiationCollectorComponent>()) foreach (var comp in entityManager.EntityQuery<RadiationCollectorComponent>())
{ {
EntitySystem.Get<RadiationCollectorSystem>().SetCollectorEnabled(comp.Owner, true, null, comp); entitySystemManager.GetEntitySystem<RadiationCollectorSystem>().SetCollectorEnabled(comp.Owner, true, null, comp);
} }
foreach (var comp in entityManager.EntityQuery<ParticleAcceleratorControlBoxComponent>()) foreach (var comp in entityManager.EntityQuery<ParticleAcceleratorControlBoxComponent>())
{ {

View File

@@ -0,0 +1,71 @@
using Robust.Shared.GameStates;
using Content.Shared.Singularity.EntitySystems;
namespace Content.Shared.Singularity.Components;
/// <summary>
/// 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 <see cref="SharedEventHorizonSystem"/> and its server/client versions.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class EventHorizonComponent : Component
{
/// <summary>
/// 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 <see cref="SharedEventHorizonSystem.SetRadius"/>.
/// </summary>
[DataField("radius")]
[Access(friends:typeof(SharedEventHorizonSystem))]
public float Radius;
/// <summary>
/// Whether the event horizon can consume/destroy the devices built to contain it.
/// If you want to set this go through <see cref="SharedEventHorizonSystem.SetCanBreachContainment"/>.
/// </summary>
[DataField("canBreachContainment")]
[Access(friends:typeof(SharedEventHorizonSystem))]
public bool CanBreachContainment = false;
/// <summary>
/// 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 <see cref="SharedEventHorizonSystem.SetHorizonFixtureId"/>.
/// </summary>
[DataField("horizonFixtureId")]
[Access(friends:typeof(SharedEventHorizonSystem))]
public string? HorizonFixtureId = "EventHorizon";
/// <summary>
/// Whether the entity this event horizon is attached to is being consumed by another event horizon.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public bool BeingConsumedByAnotherEventHorizon = false;
#region Update Timing
/// <summary>
/// The amount of time that should elapse between this event horizon consuming everything it overlaps with.
/// </summary>
[DataField("consumePeriod")]
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SharedEventHorizonSystem))]
public TimeSpan TargetConsumePeriod { get; set; } = TimeSpan.FromSeconds(0.5);
/// <summary>
/// The last time at which this consumed everything it overlapped with.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SharedEventHorizonSystem))]
public TimeSpan LastConsumeWaveTime { get; set; } = default!;
/// <summary>
/// The next time at which this consumed everything it overlapped with.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(SharedEventHorizonSystem))]
public TimeSpan NextConsumeWaveTime { get; set; } = default!;
#endregion Update Timing
}

View File

@@ -1,42 +1,32 @@
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Singularity.Components using Content.Shared.Singularity.EntitySystems;
namespace Content.Shared.Singularity.Components;
/// <summary>
/// A component that makes the associated entity accumulate energy when an associated event horizon consumes things.
/// Energy management is server-side.
/// </summary>
[NetworkedComponent]
public abstract class SharedSingularityComponent : Component
{ {
[NetworkedComponent]
public abstract class SharedSingularityComponent : Component
{
/// <summary> /// <summary>
/// The radiation pulse component's radsPerSecond is set to the singularity's level multiplied by this number. /// 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 <see cref="SharedSingularitySystem.SetLevel"/>().
/// </summary>
[DataField("level")]
[Access(friends:typeof(SharedSingularitySystem), Other=AccessPermissions.Read, Self=AccessPermissions.Read)]
public byte Level = 1;
/// <summary>
/// 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 <see cref="SharedSingularitySystem.SetRadsPerLevel"/>().
/// </summary> /// </summary>
[DataField("radsPerLevel")] [DataField("radsPerLevel")]
public float RadsPerLevel = 1; [Access(friends:typeof(SharedSingularitySystem), Other=AccessPermissions.Read, Self=AccessPermissions.Read)]
[ViewVariables(VVAccess.ReadWrite)]
/// <summary> public float RadsPerLevel = 2f;
/// Changed by <see cref="SharedSingularitySystem.ChangeSingularityLevel"/>
/// </summary>
[ViewVariables]
public int Level { get; set; }
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
if (curState is not SingularityComponentState state)
{
return;
}
EntitySystem.Get<SharedSingularitySystem>().ChangeSingularityLevel(this, state.Level);
}
}
[Serializable, NetSerializable]
public sealed class SingularityComponentState : ComponentState
{
public int Level { get; }
public SingularityComponentState(int level)
{
Level = level;
}
}
} }

View File

@@ -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;
/// <summary>
/// The entity system primarily responsible for managing <see cref="EventHorizonComponent"/>s.
/// </summary>
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<EventHorizonComponent, ComponentStartup>(OnEventHorizonStartup);
SubscribeLocalEvent<EventHorizonComponent, PreventCollideEvent>(OnPreventCollide);
var vvHandle = Vvm.GetTypeHandler<EventHorizonComponent>();
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<EventHorizonComponent>();
vvHandle.RemovePath(nameof(EventHorizonComponent.Radius));
vvHandle.RemovePath(nameof(EventHorizonComponent.CanBreachContainment));
vvHandle.RemovePath(nameof(EventHorizonComponent.HorizonFixtureId));
base.Shutdown();
}
#region Getters/Setters
/// <summary>
/// Setter for <see cref="EventHorizonComponent.Radius"/>
/// May also update the fixture associated with the event horizon.
/// </summary>
/// <param name="uid">The uid of the event horizon to change the radius of.</param>
/// <param name="value">The new radius of the event horizon.</param>
/// <param name="updateFixture">Whether to update the associated fixture upon changing the radius of the event horizon.</param>
/// <param name="eventHorizon">The state of the event horizon to change the radius of.</param>
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);
}
/// <summary>
/// Setter for <see cref="EventHorizonComponent.CanBreachContainment"/>
/// May also update the fixture associated with the event horizon.
/// </summary>
/// <param name="uid">The uid of the event horizon to make (in)capable of breaching containment.</param>
/// <param name="value">Whether the event horizon should be able to breach containment.</param>
/// <param name="updateFixture">Whether to update the associated fixture upon changing whether the event horizon can breach containment.</param>
/// <param name="eventHorizon">The state of the event horizon to make (in)capable of breaching containment.</param>
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);
}
/// <summary>
/// Setter for <see cref="EventHorizonComponent.HorizonFixtureId"/>
/// May also update the fixture associated with the event horizon.
/// </summary>
/// <param name="uid">The uid of the event horizon with the fixture ID to change.</param>
/// <param name="value">The new fixture ID to associate the event horizon with.</param>
/// <param name="updateFixture">Whether to update the associated fixture upon changing whether the event horizon can breach containment.</param>
/// <param name="eventHorizon">The state of the event horizon with the fixture ID to change.</param>
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);
}
/// <summary>
/// Updates the state of the fixture associated with the event horizon.
/// </summary>
/// <param name="eventHorizon">The uid of the event horizon associated with the fixture to update.</param>
/// <param name="fixtures">The physics component containing the fixture to update.</param>
/// <param name="eventHorizon">The state of the event horizon associated with the fixture to update.</param>
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
/// <summary>
/// Syncs the state of the fixture associated with the event horizon upon startup.
/// </summary>
/// <param name="uid">The entity that has just gained an event horizon component.</param>
/// <param name="comp">The event horizon component that is starting up.</param>
/// <param name="args">The event arguments.</param>
private void OnEventHorizonStartup(EntityUid uid, EventHorizonComponent comp, ComponentStartup args)
{
UpdateEventHorizonFixture(uid, eventHorizon: comp);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The entity that is trying to collide with another entity.</param>
/// <param name="comp">The event horizon of the former.</param>
/// <param name="args">The event arguments.</param>
private void OnPreventCollide(EntityUid uid, EventHorizonComponent comp, ref PreventCollideEvent args)
{
if(!args.Cancelled)
PreventCollide(uid, comp, ref args);
}
/// <summary>
/// The actual, functional part of SharedEventHorizonSystem.OnPreventCollide.
/// The return value allows for overrides to early return if the base successfully handles collision prevention.
/// </summary>
/// <param name="uid">The entity that is trying to collide with another entity.</param>
/// <param name="comp">The event horizon of the former.</param>
/// <param name="args">The event arguments.</param>
/// <returns>A bool indicating whether the collision prevention has been handled.</return>
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<MapGridComponent>(otherUid) ||
EntityManager.HasComponent<SharedGhostComponent>(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<SharedContainmentFieldComponent>(otherUid) ||
EntityManager.HasComponent<SharedContainmentFieldGeneratorComponent>(otherUid))
{
if (comp.CanBreachContainment)
args.Cancelled = true;
return true;
}
return false;
}
#endregion EventHandlers
}

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Singularity.EntitySystems;
/// <summary>
/// The entity system primarily responsible for managing <see cref="SharedGravityWellComponent"/>s.
/// </summary>
public abstract class SharedGravityWellSystem : EntitySystem
{}

View File

@@ -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;
/// <summary>
/// The entity system primarily responsible for managing <see cref="SharedSingularityComponent"/>s.
/// </summary>
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
/// <summary>
/// The minimum level a singularity can be set to.
/// </summary>
public const byte MinSingularityLevel = 0;
/// <summary>
/// The maximum level a singularity can be set to.
/// </summary>
public const byte MaxSingularityLevel = 6;
/// <summary>
/// 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)
/// </summary>
public const float DistortionContainerScaling = 4f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedSingularityComponent, ComponentStartup>(OnSingularityStartup);
SubscribeLocalEvent<AppearanceComponent, SingularityLevelChangedEvent>(UpdateAppearance);
SubscribeLocalEvent<RadiationSourceComponent, SingularityLevelChangedEvent>(UpdateRadiation);
SubscribeLocalEvent<PhysicsComponent, SingularityLevelChangedEvent>(UpdateBody);
SubscribeLocalEvent<EventHorizonComponent, SingularityLevelChangedEvent>(UpdateEventHorizon);
SubscribeLocalEvent<SingularityDistortionComponent, SingularityLevelChangedEvent>(UpdateDistortion);
SubscribeLocalEvent<SingularityDistortionComponent, EntGotInsertedIntoContainerMessage>(UpdateDistortion);
SubscribeLocalEvent<SingularityDistortionComponent, EntGotRemovedFromContainerMessage>(UpdateDistortion);
var vvHandle = Vvm.GetTypeHandler<SharedSingularityComponent>();
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<SharedSingularityComponent>();
vvHandle.RemovePath(nameof(SharedSingularityComponent.Level));
vvHandle.RemovePath(nameof(SharedSingularityComponent.RadsPerLevel));
base.Shutdown();
}
#region Getters/Setters
/// <summary>
/// Setter for <see cref="SharedSingularityComponent.Level"/>
/// Also sends out an event alerting that the singularities level has changed.
/// </summary>
/// <param name="uid">The uid of the singularity to change the level of.</param>
/// <param name="value">The new level the singularity should have.</param>
/// <param name="singularity">The state of the singularity to change the level of.</param>
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);
}
/// <summary>
/// Setter for <see cref="SharedSingularityComponent.RadsPerLevel"/>
/// Also updates the radiation output of the singularity according to the new values.
/// </summary>
/// <param name="uid">The uid of the singularity to change the radioactivity of.</param>
/// <param name="value">The new radioactivity the singularity should have.</param>
/// <param name="singularity">The state of the singularity to change the radioactivity of.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the singularity which's level has changed.</param>
/// <param name="oldValue">The old level of the singularity. May be equal to <see cref="SharedSingularityComponent.Level"/> if the component is starting.</param>
/// <param name="singularity">The state of the singularity which's level has changed.</param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="uid">The uid of the singularity.</param>
/// <param name="singularity">The state of the singularity.</param>
public void UpdateSingularityLevel(EntityUid uid, SharedSingularityComponent? singularity = null)
{
if (Resolve(uid, ref singularity))
UpdateSingularityLevel(uid, singularity.Level, singularity);
}
/// <summary>
/// Updates the amount of radiation the singularity emits to reflect a change in the level or radioactivity per level of the singularity.
/// </summary>
/// <param name="uid">The uid of the singularity to update the radiation of.</param>
/// <param name="singularity">The state of the singularity to update the radiation of.</param>
/// <param name="rads">The state of the radioactivity of the singularity to update.</param>
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
/// <summary>
/// The scaling factor for the size of a singularities gravity well.
/// </summary>
public const float BaseGravityWellRadius = 2f;
/// <summary>
/// The scaling factor for the base acceleration of a singularities gravity well.
/// </summary>
public const float BaseGravityWellAcceleration = 10f;
/// <summary>
/// The level at and above which a singularity should be capable of breaching containment.
/// </summary>
public const byte SingularityBreachThreshold = 5;
/// <summary>
/// Derives the proper gravity well radius for a singularity from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>The gravity well radius the singularity should have given its state.</returns>
public float GravPulseRange(SharedSingularityComponent singulo)
=> BaseGravityWellRadius * (singulo.Level + 1);
/// <summary>
/// Derives the proper base gravitational acceleration for a singularity from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>The base gravitational acceleration the singularity should have given its state.</returns>
public (float, float) GravPulseAcceleration(SharedSingularityComponent singulo)
=> (BaseGravityWellAcceleration * singulo.Level, 0f);
/// <summary>
/// Derives the proper event horizon radius for a singularity from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>The event horizon radius the singularity should have given its state.</returns>
public float EventHorizonRadius(SharedSingularityComponent singulo)
=> (float) singulo.Level - 0.5f;
/// <summary>
/// Derives whether a singularity should be able to breach containment from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>Whether the singularity should be able to breach containment.</returns>
public bool CanBreachContainment(SharedSingularityComponent singulo)
=> singulo.Level >= SingularityBreachThreshold;
/// <summary>
/// Derives the proper distortion shader falloff for a singularity from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>The distortion shader falloff the singularity should have given its state.</returns>
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
};
}
/// <summary>
/// Derives the proper distortion shader intensity for a singularity from its state.
/// </summary>
/// <param name="singulo">A singularity.</param>
/// <returns>The distortion shader intensity the singularity should have given its state.</returns>
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
/// <summary>
/// A state wrapper used to sync the singularity between the server and client.
/// </summary>
[Serializable, NetSerializable]
protected sealed class SingularityComponentState : ComponentState
{
/// <summary>
/// The level of the singularity to sync.
/// </summary>
public readonly byte Level;
public SingularityComponentState(SharedSingularityComponent singulo)
{
Level = singulo.Level;
}
}
#endregion Serialization
#region EventHandlers
/// <summary>
/// Syncs other components with the state of the singularity via event on startup.
/// </summary>
/// <param name="uid">The entity that is becoming a singularity.</param>
/// <param name="comp">The singularity component that is being added to the entity.</param>
/// <param name="args">The event arguments.</param>
protected virtual void OnSingularityStartup(EntityUid uid, SharedSingularityComponent comp, ComponentStartup args)
{
UpdateSingularityLevel(uid, comp);
}
// TODO: Figure out which systems should have control of which coupling.
/// <summary>
/// Syncs the radius of an event horizon associated with a singularity that just changed levels.
/// </summary>
/// <param name="uid">The entity that the event horizon and singularity are attached to.</param>
/// <param name="comp">The event horizon associated with the singularity.</param>
/// <param name="args">The event arguments.</param>
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);
}
/// <summary>
/// Updates the distortion shader associated with a singularity when the singuarity changes levels.
/// </summary>
/// <param name="uid">The uid of the distortion shader.</param>
/// <param name="comp">The state of the distortion shader.</param>
/// <param name="args">The event arguments.</param>
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;
}
/// <summary>
/// Updates the distortion shader associated with a singularity when the singuarity is inserted into a container.
/// </summary>
/// <param name="uid">The uid of the distortion shader.</param>
/// <param name="comp">The state of the distortion shader.</param>
/// <param name="args">The event arguments.</param>
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;
}
/// <summary>
/// Updates the distortion shader associated with a singularity when the singuarity is removed from a container.
/// </summary>
/// <param name="uid">The uid of the distortion shader.</param>
/// <param name="comp">The state of the distortion shader.</param>
/// <param name="args">The event arguments.</param>
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;
}
/// <summary>
/// Updates the state of the physics body associated with a singularity when the singualrity changes levels.
/// </summary>
/// <param name="uid">The entity that the physics body and singularity are attached to.</param>
/// <param name="comp">The physics body associated with the singularity.</param>
/// <param name="args">The event arguments.</param>
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.
}
/// <summary>
/// Updates the appearance of a singularity when the singularities level changes.
/// </summary>
/// <param name="uid">The entity that the singularity is attached to.</param>
/// <param name="comp">The appearance associated with the singularity.</param>
/// <param name="args">The event arguments.</param>
private void UpdateAppearance(EntityUid uid, AppearanceComponent comp, SingularityLevelChangedEvent args)
{
_visualizer.SetData(uid, SingularityVisuals.Level, args.NewValue, comp);
}
/// <summary>
/// Updates the amount of radiation a singularity emits when the singularities level changes.
/// </summary>
/// <param name="uid">The entity that the singularity is attached to.</param>
/// <param name="comp">The radiation source associated with the singularity.</param>
/// <param name="args">The event arguments.</param>
private void UpdateRadiation(EntityUid uid, RadiationSourceComponent comp, SingularityLevelChangedEvent args)
{
UpdateRadiation(uid, args.Singularity, comp);
}
#endregion EventHandlers
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Singularity.Components;
namespace Content.Shared.Singularity.Events;
/// <summary>
/// An event raised whenever a singularity changes its level.
/// </summary>
public sealed class SingularityLevelChangedEvent : EntityEventArgs
{
/// <summary>
/// The new level of the singularity.
/// </summary>
public readonly byte NewValue;
/// <summary>
/// The previous level of the singularity.
/// </summary>
public readonly byte OldValue;
/// <summary>
/// The singularity that just changed level.
/// </summary>
public readonly SharedSingularityComponent Singularity;
public SingularityLevelChangedEvent(byte newValue, byte oldValue, SharedSingularityComponent singularity)
{
NewValue = newValue;
OldValue = oldValue;
Singularity = singularity;
}
}

View File

@@ -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<SharedSingularityComponent, PreventCollideEvent>(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<MapGridComponent>(otherUid) ||
EntityManager.HasComponent<SharedGhostComponent>(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<SharedContainmentFieldComponent>(otherUid) ||
EntityManager.HasComponent<SharedContainmentFieldGeneratorComponent>(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<PhysicsComponent>(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();
}
}
}

View File

@@ -13,7 +13,7 @@
bodyType: Dynamic bodyType: Dynamic
- type: Fixtures - type: Fixtures
fixtures: fixtures:
- id: DeleteCircle - id: EventHorizon
shape: shape:
!type:PhysShapeCircle !type:PhysShapeCircle
radius: 0.5 radius: 0.5
@@ -24,7 +24,15 @@
layer: layer:
- AllMask - AllMask
- type: Singularity - type: Singularity
energy: 180
level: 1
radsPerLevel: 2 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: SingularityDistortion
- type: RadiationSource - type: RadiationSource
slope: 0.2 # its emit really far away slope: 0.2 # its emit really far away