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,4 +1,5 @@
using Content.Server.Singularity.Components;
using Content.Server.Singularity.Events;
using Content.Shared.Singularity.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
@@ -22,6 +23,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly PhysicsSystem _physics = default!;
[Dependency] private readonly AppearanceSystem _visualizer = default!;
public override void Initialize()
{
@@ -34,6 +36,7 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ReAnchorEvent>(OnReanchorEvent);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, EventHorizonAttemptConsumeEntityEvent>(PreventBreach);
}
public override void Update(float frameTime)
@@ -356,24 +359,11 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
/// <param name="component"></param>
private void ChangePowerVisualizer(int power, ContainmentFieldGeneratorComponent component)
{
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance))
return;
if(component.PowerBuffer == 0)
appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.NoPower);
if (component.PowerBuffer > 0 && component.PowerBuffer < component.PowerMinimum)
appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.LowPower);
if (component.PowerBuffer >= component.PowerMinimum && component.PowerBuffer < 25)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.MediumPower);
}
if (component.PowerBuffer == 25)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.PowerLight, PowerLevelVisuals.HighPower);
}
_visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.PowerLight, component.PowerBuffer switch {
<=0 => PowerLevelVisuals.NoPower,
>=25 => PowerLevelVisuals.HighPower,
_ => (component.PowerBuffer < component.PowerMinimum) ? PowerLevelVisuals.LowPower : PowerLevelVisuals.MediumPower
});
}
/// <summary>
@@ -382,36 +372,30 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
/// <param name="component"></param>
private void ChangeFieldVisualizer(ContainmentFieldGeneratorComponent component)
{
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance))
return;
if (component.Connections.Count == 0 && !component.Enabled)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.NoLevel);
}
if (component.Connections.Count == 0 && component.Enabled)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.On);
}
if (component.Connections.Count == 1)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.OneField);
}
if (component.Connections.Count > 1)
{
appearance.SetData(ContainmentFieldGeneratorVisuals.FieldLight, FieldLevelVisuals.MultipleFields);
}
_visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.FieldLight, component.Connections.Count switch {
>1 => FieldLevelVisuals.MultipleFields,
1 => FieldLevelVisuals.OneField,
_ => component.Enabled ? FieldLevelVisuals.On : FieldLevelVisuals.NoLevel
});
}
private void ChangeOnLightVisualizer(ContainmentFieldGeneratorComponent component)
{
if (!TryComp<AppearanceComponent>(component.Owner, out var appearance))
return;
appearance.SetData(ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
_visualizer.SetData(component.Owner, ContainmentFieldGeneratorVisuals.OnLight, component.IsConnected);
}
#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.Shuttles.Components;
using Content.Server.Singularity.Components;
using Content.Server.Singularity.EntitySystems;
using Content.Server.Singularity.Events;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Content.Shared.Throwing;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
@@ -22,6 +21,7 @@ public sealed class ContainmentFieldSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<ContainmentFieldComponent, StartCollideEvent>(HandleFieldCollide);
SubscribeLocalEvent<ContainmentFieldComponent, EventHorizonAttemptConsumeEntityEvent>(HandleEventHorizon);
}
private void HandleFieldCollide(EntityUid uid, ContainmentFieldComponent component, ref StartCollideEvent args)
@@ -42,4 +42,10 @@ public sealed class ContainmentFieldSystem : EntitySystem
_throwing.TryThrow(otherBody, playerDir-fieldDir, strength: component.ThrowForce);
}
}
private void HandleEventHorizon(EntityUid uid, ContainmentFieldComponent component, EventHorizonAttemptConsumeEntityEvent args)
{
if(!args.Cancelled && !args.EventHorizon.CanBreachContainment)
args.Cancel();
}
}

View File

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

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.Singularity.Components;
using Content.Shared.Singularity.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
namespace Content.Server.Singularity.EntitySystems;
public sealed class SingularityGeneratorSystem : EntitySystem
{
#region Dependencies
[Dependency] private readonly IViewVariablesManager _vvm = default!;
#endregion Dependencies
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<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)
{
if (EntityManager.TryGetComponent<SingularityGeneratorComponent?>(args.OtherFixture.Body.Owner, out var singularityGeneratorComponent))
@@ -31,4 +137,5 @@ public sealed class SingularityGeneratorSystem : EntitySystem
EntityManager.QueueDeleteEntity(uid);
}
}
#endregion Event Handlers
}

View File

@@ -1,265 +1,350 @@
using Content.Server.Ghost.Components;
using Content.Server.Singularity.Components;
using Content.Server.Station.Components;
using Content.Shared.Singularity;
using Content.Shared.Singularity.Components;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Server.GameStates;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
namespace Content.Server.Singularity.EntitySystems
using Content.Shared.Singularity.Components;
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Events;
using Content.Server.Physics.Components;
using Content.Server.Singularity.Components;
using Content.Server.Singularity.Events;
namespace Content.Server.Singularity.EntitySystems;
/// <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]
public sealed class SingularitySystem : SharedSingularitySystem
#region Dependencies
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PVSOverrideSystem _pvs = default!;
#endregion Dependencies
/// <summary>
/// The amount of energy singulos accumulate when they eat a tile.
/// </summary>
public const float BaseTileEnergy = 1f;
/// <summary>
/// The amount of energy singulos accumulate when they eat an entity.
/// </summary>
public const float BaseEntityEnergy = 1f;
public override void Initialize()
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly PVSOverrideSystem _pvs = default!;
/// <summary>
/// How much energy the singulo gains from destroying a tile.
/// </summary>
private const int TileEnergyGain = 1;
base.Initialize();
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);
private const float GravityCooldown = 0.5f;
private float _gravityAccumulator;
// TODO: Figure out where all this coupling should be handled.
SubscribeLocalEvent<RandomWalkComponent, SingularityLevelChangedEvent>(UpdateRandomWalk);
SubscribeLocalEvent<GravityWellComponent, SingularityLevelChangedEvent>(UpdateGravityWell);
private int _updateInterval = 1;
private float _accumulator;
var vvHandle = Vvm.GetTypeHandler<SingularityComponent>();
vvHandle.AddPath(nameof(SingularityComponent.Energy), (_, comp) => comp.Energy, SetEnergy);
vvHandle.AddPath(nameof(SingularityComponent.TargetUpdatePeriod), (_, comp) => comp.TargetUpdatePeriod, SetUpdatePeriod);
}
public override void Initialize()
public override void Shutdown()
{
var vvHandle = Vvm.GetTypeHandler<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>())
{
base.Initialize();
SubscribeLocalEvent<ServerSingularityComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<SingularityDistortionComponent, ComponentStartup>(OnDistortionStartup);
}
private void OnDistortionStartup(EntityUid uid, SingularityDistortionComponent component, ComponentStartup args)
{
// to avoid distortion overlay pop-in, entities with distortion ignore PVS. Really this should probably be a
// PVS range-override, but this is good enough for now.
_pvs.AddGlobalOverride(uid);
}
protected override bool PreventCollide(EntityUid uid, SharedSingularityComponent component, ref PreventCollideEvent args)
{
if (base.PreventCollide(uid, component, ref args)) return true;
var otherUid = args.BodyB.Owner;
if (args.Cancelled) return true;
// If it's not cancelled then we'll cancel if we can't immediately destroy it on collision
if (!CanDestroy(component, otherUid))
args.Cancelled = true;
return true;
}
private void OnCollide(EntityUid uid, ServerSingularityComponent component, ref StartCollideEvent args)
{
if (args.OurFixture.ID != "DeleteCircle") return;
// This handles bouncing off of containment walls.
// If you want the delete behavior we do it under DeleteEntities for reasons (not everything has physics).
// If we're being deleted by another singularity, this call is probably for that singularity.
// Even if not, just don't bother.
if (component.BeingDeletedByAnotherSingularity)
return;
var otherUid = args.OtherFixture.Body.Owner;
// HandleDestroy will also check CanDestroy for us
HandleDestroy(component, otherUid);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_gravityAccumulator += frameTime;
_accumulator += frameTime;
while (_accumulator > _updateInterval)
{
_accumulator -= _updateInterval;
foreach (var singularity in EntityManager.EntityQuery<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>
/// Handle deleting entities and increasing energy
/// </summary>
private void DestroyEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos)
{
// The reason we don't /just/ use collision is because we'll be deleting stuff that may not necessarily have physics (e.g. carpets).
var destroyRange = DestroyTileRange(component);
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, destroyRange))
{
HandleDestroy(component, entity);
}
}
private bool CanPull(EntityUid entity)
{
return !(EntityManager.HasComponent<GhostComponent>(entity) ||
EntityManager.HasComponent<MapGridComponent>(entity) ||
EntityManager.HasComponent<MapComponent>(entity) ||
EntityManager.HasComponent<ServerSingularityComponent>(entity) ||
_container.IsEntityInContainer(entity));
}
/// <summary>
/// Pull dynamic bodies in range to the singulo.
/// </summary>
private void PullEntities(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos, float frameTime)
{
// TODO: When we split up dynamic and static trees we might be able to make items always on the broadphase
// in which case we can just query dynamictree directly for brrt
var pullRange = PullRange(component);
var destroyRange = DestroyTileRange(component);
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, worldPos, pullRange))
{
// I tried having it so level 6 can de-anchor. BAD IDEA, MASSIVE LAG.
if (entity == component.Owner ||
!TryComp<PhysicsComponent?>(entity, out var collidableComponent) ||
collidableComponent.BodyType == BodyType.Static) continue;
if (!CanPull(entity)) continue;
var vec = worldPos - Transform(entity).WorldPosition;
if (vec.Length < destroyRange - 0.01f) continue;
var speed = 1f / vec.Length * component.Level * collidableComponent.Mass * 10f;
// Because tile friction is so high we'll just multiply by mass so stuff like closets can even move.
collidableComponent.ApplyLinearImpulse(vec.Normalized * speed * frameTime);
}
}
/// <summary>
/// Destroy any grid tiles within the relevant Level range.
/// </summary>
private void DestroyTiles(ServerSingularityComponent component, TransformComponent xform, Vector2 worldPos)
{
var radius = DestroyTileRange(component);
var circle = new Circle(worldPos, radius);
var box = new Box2(worldPos - radius, worldPos + radius);
foreach (var grid in _mapManager.FindGridsIntersecting(xform.MapID, box))
{
// Bundle these together so we can use the faster helper to set tiles.
var toDestroy = new List<(Vector2i, Tile)>();
foreach (var tile in grid.GetTilesIntersecting(circle))
{
if (tile.Tile.IsEmpty) continue;
// Avoid ripping up tiles that may be essential to containment
if (component.Level < 5)
{
var canDelete = true;
foreach (var ent in grid.GetAnchoredEntities(tile.GridIndices))
{
if (EntityManager.HasComponent<ContainmentFieldComponent>(ent) ||
EntityManager.HasComponent<ContainmentFieldGeneratorComponent>(ent))
{
canDelete = false;
break;
}
}
if (!canDelete) continue;
}
toDestroy.Add((tile.GridIndices, Tile.Empty));
}
component.Energy += TileEnergyGain * toDestroy.Count;
grid.SetTiles(toDestroy);
}
var curTime = _timing.CurTime;
if (singularity.NextUpdateTime <= curTime)
Update(singularity.Owner, curTime - singularity.LastUpdateTime, singularity);
}
}
/// <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)
{
_pvs.AddGlobalOverride(uid);
}
/// <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)
{
comp.AmbientSoundStream?.Stop();
MetaDataComponent? metaData = null;
if (Resolve(uid, ref metaData) && metaData.EntityLifeStage >= EntityLifeStage.Terminating)
_audio.Play(comp.DissipationSound, Filter.Pvs(comp.Owner), comp.Owner, true);
}
/// <summary>
/// Handles wrapping the state of a singularity for server-client syncing.
/// </summary>
/// <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)
{
args.State = new SingularityComponentState(comp);
}
/// <summary>
/// Adds the energy of any entities that are consumed to the singularity that consumed them.
/// </summary>
/// <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)
{
AdjustEnergy(uid, BaseEntityEnergy, singularity: comp);
}
/// <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)
{
AdjustEnergy(uid, args.Tiles.Count * BaseTileEnergy, singularity: comp);
}
/// <summary>
/// Adds the energy of this singularity to singularities consume it.
/// </summary>
/// <param name="uid">The entity UID of the singularity that is being consumed.</param>
/// <param name="comp">The component of the singularity that is being consumed.</param>
/// <param name="args">The event arguments.</param>
private void OnConsumed(EntityUid uid, SingularityComponent comp, EventHorizonConsumedEntityEvent args)
{
// Should be slightly more efficient than checking literally everything we consume for a singularity component and doing the reverse.
if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizon.Owner, out var singulo))
{
AdjustEnergy(singulo.Owner, comp.Energy, singularity: singulo);
SetEnergy(uid, 0.0f, comp);
}
}
/// <summary>
/// Adds some bonus energy from any singularity food to the singularity that consumes it.
/// </summary>
/// <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)
{
if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizon.Owner, out var singulo))
AdjustEnergy(args.EventHorizon.Owner, comp.Energy, singularity: singulo);
}
/// <summary>
/// Updates the rate at which the singularities energy drains at when its level changes.
/// </summary>
/// <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)
{
comp.EnergyDrain = args.NewValue switch {
6 => 20,
5 => 15,
4 => 10,
3 => 5,
2 => 2,
1 => 1,
_ => 0
};
}
/// <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)
{
var scale = MathF.Max(args.NewValue, 4);
comp.MinSpeed = 7.5f / scale;
comp.MaxSpeed = 10f / scale;
}
/// <summary>
/// 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 singulos = args.Singularity;
comp.MaxRange = GravPulseRange(singulos);
(comp.BaseRadialAcceleration, comp.BaseTangentialAcceleration) = GravPulseAcceleration(singulos);
}
#endregion Event Handlers
}