2023-05-28 05:25:54 -05:00
using Content.Server.Administration.Logs ;
2022-12-19 18:47:15 -08:00
using Content.Server.Singularity.Events ;
2021-07-21 22:26:18 +10:00
using Content.Shared.Singularity.Components ;
using Content.Shared.Tag ;
2022-06-16 14:46:21 +01:00
using Robust.Server.GameObjects ;
using Robust.Shared.Physics ;
2022-06-23 00:27:49 -04:00
using Content.Server.Popups ;
using Content.Shared.Construction.Components ;
2023-05-28 05:25:54 -05:00
using Content.Shared.Database ;
2022-08-05 00:22:37 -04:00
using Content.Shared.Examine ;
2022-06-23 00:27:49 -04:00
using Content.Shared.Interaction ;
2022-08-05 00:22:37 -04:00
using Content.Shared.Popups ;
2022-09-14 17:26:26 +10:00
using Robust.Shared.Physics.Components ;
using Robust.Shared.Physics.Events ;
2021-05-13 02:05:46 +02:00
2022-08-05 00:22:37 -04:00
namespace Content.Server.Singularity.EntitySystems ;
public sealed class ContainmentFieldGeneratorSystem : EntitySystem
2021-05-13 02:05:46 +02:00
{
2023-05-28 05:25:54 -05:00
[Dependency] private readonly IAdminLogManager _adminLogger = default ! ;
2022-08-05 00:22:37 -04:00
[Dependency] private readonly TagSystem _tags = default ! ;
[Dependency] private readonly PopupSystem _popupSystem = default ! ;
[Dependency] private readonly PhysicsSystem _physics = default ! ;
2022-12-19 18:47:15 -08:00
[Dependency] private readonly AppearanceSystem _visualizer = default ! ;
2022-08-05 00:22:37 -04:00
public override void Initialize ( )
2021-05-13 02:05:46 +02:00
{
2022-08-05 00:22:37 -04:00
base . Initialize ( ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , StartCollideEvent > ( HandleGeneratorCollide ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , ExaminedEvent > ( OnExamine ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , InteractHandEvent > ( OnInteract ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , AnchorStateChangedEvent > ( OnAnchorChanged ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , ReAnchorEvent > ( OnReanchorEvent ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , UnanchorAttemptEvent > ( OnUnanchorAttempt ) ;
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , ComponentRemove > ( OnComponentRemoved ) ;
2022-12-19 18:47:15 -08:00
SubscribeLocalEvent < ContainmentFieldGeneratorComponent , EventHorizonAttemptConsumeEntityEvent > ( PreventBreach ) ;
2022-08-05 00:22:37 -04:00
}
2022-02-08 14:08:11 +11:00
2022-08-05 00:22:37 -04:00
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
2022-06-23 00:27:49 -04:00
2023-05-28 05:25:54 -05:00
var query = EntityQueryEnumerator < ContainmentFieldGeneratorComponent > ( ) ;
while ( query . MoveNext ( out var uid , out var generator ) )
2022-06-23 00:27:49 -04:00
{
2022-08-05 00:22:37 -04:00
if ( generator . PowerBuffer < = 0 ) //don't drain power if there's no power, or if it's somehow less than 0.
2022-08-14 22:37:58 -04:00
continue ;
2022-06-23 00:27:49 -04:00
2022-08-05 00:22:37 -04:00
generator . Accumulator + = frameTime ;
if ( generator . Accumulator > = generator . Threshold )
2022-06-23 00:27:49 -04:00
{
2023-05-28 05:25:54 -05:00
LosePower ( uid , generator . PowerLoss , generator ) ;
2022-08-05 00:22:37 -04:00
generator . Accumulator - = generator . Threshold ;
2022-06-23 00:27:49 -04:00
}
2022-08-05 00:22:37 -04:00
}
}
#region Events
2022-06-23 00:27:49 -04:00
2022-08-05 00:22:37 -04:00
/// <summary>
/// A generator receives power from a source colliding with it.
/// </summary>
2022-09-14 17:26:26 +10:00
private void HandleGeneratorCollide ( EntityUid uid , ContainmentFieldGeneratorComponent component , ref StartCollideEvent args )
2022-08-05 00:22:37 -04:00
{
2023-05-09 19:21:26 +12:00
if ( _tags . HasTag ( args . OtherEntity , component . IDTag ) )
2022-08-05 00:22:37 -04:00
{
ReceivePower ( component . PowerReceived , component ) ;
component . Accumulator = 0f ;
2022-06-23 00:27:49 -04:00
}
2022-08-05 00:22:37 -04:00
}
private void OnExamine ( EntityUid uid , ContainmentFieldGeneratorComponent component , ExaminedEvent args )
{
if ( component . Enabled )
args . PushMarkup ( Loc . GetString ( "comp-containment-on" ) ) ;
else
args . PushMarkup ( Loc . GetString ( "comp-containment-off" ) ) ;
}
private void OnInteract ( EntityUid uid , ContainmentFieldGeneratorComponent component , InteractHandEvent args )
{
if ( args . Handled )
return ;
2022-06-23 00:27:49 -04:00
2022-08-05 00:22:37 -04:00
if ( TryComp ( component . Owner , out TransformComponent ? transformComp ) & & transformComp . Anchored )
2022-06-23 00:27:49 -04:00
{
2022-08-05 00:22:37 -04:00
if ( ! component . Enabled )
TurnOn ( component ) ;
else if ( component . Enabled & & component . IsConnected )
2022-06-23 00:27:49 -04:00
{
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-toggle-warning" ) , args . User , args . User , PopupType . LargeCaution ) ;
2022-08-05 00:22:37 -04:00
return ;
2022-06-23 00:27:49 -04:00
}
2022-08-05 00:22:37 -04:00
else
TurnOff ( component ) ;
2022-06-23 00:27:49 -04:00
}
2022-08-05 00:22:37 -04:00
args . Handled = true ;
}
2022-06-23 00:27:49 -04:00
2022-08-05 00:22:37 -04:00
private void OnAnchorChanged ( EntityUid uid , ContainmentFieldGeneratorComponent component , ref AnchorStateChangedEvent args )
{
if ( ! args . Anchored )
2023-05-28 05:25:54 -05:00
RemoveConnections ( uid , component ) ;
2022-08-05 00:22:37 -04:00
}
2022-06-23 00:27:49 -04:00
2022-08-05 00:22:37 -04:00
private void OnReanchorEvent ( EntityUid uid , ContainmentFieldGeneratorComponent component , ref ReAnchorEvent args )
{
2023-05-28 05:25:54 -05:00
GridCheck ( uid , component ) ;
2022-08-05 00:22:37 -04:00
}
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
private void OnUnanchorAttempt ( EntityUid uid , ContainmentFieldGeneratorComponent component ,
UnanchorAttemptEvent args )
{
if ( component . Enabled )
2022-06-16 14:46:21 +01:00
{
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-anchor-warning" ) , args . User , args . User , PopupType . LargeCaution ) ;
2022-08-05 00:22:37 -04:00
args . Cancel ( ) ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
}
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
private void TurnOn ( ContainmentFieldGeneratorComponent component )
{
component . Enabled = true ;
ChangeFieldVisualizer ( component ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-turned-on" ) , component . Owner ) ;
2022-08-05 00:22:37 -04:00
}
private void TurnOff ( ContainmentFieldGeneratorComponent component )
{
component . Enabled = false ;
ChangeFieldVisualizer ( component ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-turned-off" ) , component . Owner ) ;
2022-08-05 00:22:37 -04:00
}
private void OnComponentRemoved ( EntityUid uid , ContainmentFieldGeneratorComponent component , ComponentRemove args )
{
2023-05-28 05:25:54 -05:00
RemoveConnections ( uid , component ) ;
2022-08-05 00:22:37 -04:00
}
/// <summary>
/// Deletes the fields and removes the respective connections for the generators.
/// </summary>
2023-05-28 05:25:54 -05:00
private void RemoveConnections ( EntityUid uid , ContainmentFieldGeneratorComponent component )
2022-08-05 00:22:37 -04:00
{
foreach ( var ( direction , value ) in component . Connections )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
foreach ( var field in value . Item2 )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
QueueDel ( field ) ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
value . Item1 . Connections . Remove ( direction . GetOpposite ( ) ) ;
2021-07-21 22:26:18 +10:00
2022-08-05 00:22:37 -04:00
if ( value . Item1 . Connections . Count = = 0 ) //Change isconnected only if there's no more connections
2021-07-21 22:26:18 +10:00
{
2022-08-05 00:22:37 -04:00
value . Item1 . IsConnected = false ;
ChangeOnLightVisualizer ( value . Item1 ) ;
2021-07-21 22:26:18 +10:00
}
2022-08-05 00:22:37 -04:00
ChangeFieldVisualizer ( value . Item1 ) ;
2021-07-21 22:26:18 +10:00
}
2022-08-05 00:22:37 -04:00
component . Connections . Clear ( ) ;
component . IsConnected = false ;
ChangeOnLightVisualizer ( component ) ;
ChangeFieldVisualizer ( component ) ;
2023-05-28 05:25:54 -05:00
_adminLogger . Add ( LogType . FieldGeneration , LogImpact . Medium , $"{ToPrettyString(uid)} lost field connections" ) ; // Ideally LogImpact would depend on if there is a singulo nearby
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-disconnected" ) , component . Owner , PopupType . LargeCaution ) ;
2022-08-05 00:22:37 -04:00
}
#endregion
#region Connections
2021-07-21 22:26:18 +10:00
2022-08-05 00:22:37 -04:00
/// <summary>
/// Stores power in the generator. If it hits the threshold, it tries to establish a connection.
/// </summary>
/// <param name="power">The power that this generator received from the collision in <see cref="HandleGeneratorCollide"/></param>
public void ReceivePower ( int power , ContainmentFieldGeneratorComponent component )
{
component . PowerBuffer + = power ;
var genXForm = Transform ( component . Owner ) ;
if ( component . PowerBuffer > = component . PowerMinimum )
2021-07-21 22:26:18 +10:00
{
2022-08-05 00:22:37 -04:00
var directions = Enum . GetValues < Direction > ( ) . Length ;
for ( int i = 0 ; i < directions - 1 ; i + = 2 )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
var dir = ( Direction ) i ;
if ( component . Connections . ContainsKey ( dir ) )
continue ; // This direction already has an active connection
TryGenerateFieldConnection ( dir , component , genXForm ) ;
2021-07-21 22:26:18 +10:00
}
}
2022-08-05 00:22:37 -04:00
ChangePowerVisualizer ( power , component ) ;
}
2023-05-28 05:25:54 -05:00
public void LosePower ( EntityUid uid , int power , ContainmentFieldGeneratorComponent component )
2022-08-05 00:22:37 -04:00
{
component . PowerBuffer - = power ;
if ( component . PowerBuffer < component . PowerMinimum & & component . Connections . Count ! = 0 )
2021-07-21 22:26:18 +10:00
{
2023-05-28 05:25:54 -05:00
RemoveConnections ( uid , component ) ;
2021-05-13 02:05:46 +02:00
}
2022-08-05 00:22:37 -04:00
ChangePowerVisualizer ( power , component ) ;
}
/// <summary>
/// This will attempt to establish a connection of fields between two generators.
/// If all the checks pass and fields spawn, it will store this connection on each respective generator.
/// </summary>
/// <param name="dir">The field generator establishes a connection in this direction.</param>
/// <param name="component">The field generator component</param>
/// <param name="gen1XForm">The transform component for the first generator</param>
/// <returns></returns>
private bool TryGenerateFieldConnection ( Direction dir , ContainmentFieldGeneratorComponent component , TransformComponent gen1XForm )
{
if ( ! component . Enabled )
return false ;
if ( ! gen1XForm . Anchored )
return false ;
var genWorldPosRot = gen1XForm . GetWorldPositionRotation ( ) ;
var dirRad = dir . ToAngle ( ) + genWorldPosRot . WorldRotation ; //needs to be like this for the raycast to work properly
var ray = new CollisionRay ( genWorldPosRot . WorldPosition , dirRad . ToVec ( ) , component . CollisionMask ) ;
var rayCastResults = _physics . IntersectRay ( gen1XForm . MapID , ray , component . MaxLength , component . Owner , false ) ;
var genQuery = GetEntityQuery < ContainmentFieldGeneratorComponent > ( ) ;
RayCastResults ? closestResult = null ;
foreach ( var result in rayCastResults )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
if ( genQuery . HasComponent ( result . HitEntity ) )
closestResult = result ;
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
break ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
if ( closestResult = = null )
return false ;
var ent = closestResult . Value . HitEntity ;
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
if ( ! TryComp < ContainmentFieldGeneratorComponent ? > ( ent , out var otherFieldGeneratorComponent ) | |
otherFieldGeneratorComponent = = component | |
! TryComp < PhysicsComponent > ( ent , out var collidableComponent ) | |
collidableComponent . BodyType ! = BodyType . Static | |
gen1XForm . ParentUid ! = Transform ( otherFieldGeneratorComponent . Owner ) . ParentUid )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
return false ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
var fields = GenerateFieldConnection ( component , otherFieldGeneratorComponent ) ;
component . Connections [ dir ] = ( otherFieldGeneratorComponent , fields ) ;
otherFieldGeneratorComponent . Connections [ dir . GetOpposite ( ) ] = ( component , fields ) ;
ChangeFieldVisualizer ( otherFieldGeneratorComponent ) ;
if ( ! component . IsConnected )
2021-05-13 02:05:46 +02:00
{
2022-08-05 00:22:37 -04:00
component . IsConnected = true ;
ChangeOnLightVisualizer ( component ) ;
}
if ( ! otherFieldGeneratorComponent . IsConnected )
{
otherFieldGeneratorComponent . IsConnected = true ;
ChangeOnLightVisualizer ( otherFieldGeneratorComponent ) ;
2021-05-13 02:05:46 +02:00
}
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
ChangeFieldVisualizer ( component ) ;
UpdateConnectionLights ( component ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( Loc . GetString ( "comp-containment-connected" ) , component . Owner ) ;
2022-08-05 00:22:37 -04:00
return true ;
}
/// <summary>
/// Spawns fields between two generators if the <see cref="TryGenerateFieldConnection"/> finds two generators to connect.
/// </summary>
/// <param name="firstGenComp">The source field generator</param>
/// <param name="secondGenComp">The second generator that the source is connected to</param>
/// <returns></returns>
private List < EntityUid > GenerateFieldConnection ( ContainmentFieldGeneratorComponent firstGenComp , ContainmentFieldGeneratorComponent secondGenComp )
{
var fieldList = new List < EntityUid > ( ) ;
var gen1Coords = Transform ( firstGenComp . Owner ) . Coordinates ;
var gen2Coords = Transform ( secondGenComp . Owner ) . Coordinates ;
var delta = ( gen2Coords - gen1Coords ) . Position ;
var dirVec = delta . Normalized ;
var stopDist = delta . Length ;
var currentOffset = dirVec ;
while ( currentOffset . Length < stopDist )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
var currentCoords = gen1Coords . Offset ( currentOffset ) ;
var newField = Spawn ( firstGenComp . CreatedField , currentCoords ) ;
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
var fieldXForm = Transform ( newField ) ;
fieldXForm . AttachParent ( firstGenComp . Owner ) ;
if ( dirVec . GetDir ( ) = = Direction . East | | dirVec . GetDir ( ) = = Direction . West )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
var angle = fieldXForm . LocalPosition . ToAngle ( ) ;
var rotateBy90 = angle . Degrees + 90 ;
var rotatedAngle = Angle . FromDegrees ( rotateBy90 ) ;
fieldXForm . LocalRotation = rotatedAngle ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
fieldList . Add ( newField ) ;
currentOffset + = dirVec ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
return fieldList ;
}
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
/// <summary>
/// Creates a light component for the spawned fields.
/// </summary>
public void UpdateConnectionLights ( ContainmentFieldGeneratorComponent component )
{
if ( EntityManager . TryGetComponent < PointLightComponent > ( component . Owner , out var pointLightComponent ) )
2022-06-16 14:46:21 +01:00
{
2023-01-19 03:56:45 +01:00
pointLightComponent . Enabled = component . Connections . Count > 0 ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
}
/// <summary>
/// Checks to see if this or the other gens connected to a new grid. If they did, remove connection.
/// </summary>
2023-05-28 05:25:54 -05:00
public void GridCheck ( EntityUid uid , ContainmentFieldGeneratorComponent component )
2022-08-05 00:22:37 -04:00
{
var xFormQuery = GetEntityQuery < TransformComponent > ( ) ;
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
foreach ( var ( _ , generators ) in component . Connections )
2022-06-16 14:46:21 +01:00
{
2022-08-05 00:22:37 -04:00
var gen1ParentGrid = xFormQuery . GetComponent ( component . Owner ) . ParentUid ;
var gent2ParentGrid = xFormQuery . GetComponent ( generators . Item1 . Owner ) . ParentUid ;
if ( gen1ParentGrid ! = gent2ParentGrid )
2023-05-28 05:25:54 -05:00
RemoveConnections ( uid , component ) ;
2022-06-16 14:46:21 +01:00
}
2022-08-05 00:22:37 -04:00
}
#endregion
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
#region VisualizerHelpers
/// <summary>
/// Check if a fields power falls between certain ranges to update the field gen visual for power.
/// </summary>
/// <param name="power"></param>
/// <param name="component"></param>
private void ChangePowerVisualizer ( int power , ContainmentFieldGeneratorComponent component )
{
2022-12-19 18:47:15 -08:00
_visualizer . SetData ( component . Owner , ContainmentFieldGeneratorVisuals . PowerLight , component . PowerBuffer switch {
< = 0 = > PowerLevelVisuals . NoPower ,
> = 25 = > PowerLevelVisuals . HighPower ,
_ = > ( component . PowerBuffer < component . PowerMinimum ) ? PowerLevelVisuals . LowPower : PowerLevelVisuals . MediumPower
} ) ;
2022-08-05 00:22:37 -04:00
}
2022-06-16 14:46:21 +01:00
2022-08-05 00:22:37 -04:00
/// <summary>
/// Check if a field has any or no connections and if it's enabled to toggle the field level light
/// </summary>
/// <param name="component"></param>
private void ChangeFieldVisualizer ( ContainmentFieldGeneratorComponent component )
{
2022-12-19 18:47:15 -08:00
_visualizer . SetData ( component . Owner , ContainmentFieldGeneratorVisuals . FieldLight , component . Connections . Count switch {
> 1 = > FieldLevelVisuals . MultipleFields ,
1 = > FieldLevelVisuals . OneField ,
_ = > component . Enabled ? FieldLevelVisuals . On : FieldLevelVisuals . NoLevel
} ) ;
2022-08-05 00:22:37 -04:00
}
private void ChangeOnLightVisualizer ( ContainmentFieldGeneratorComponent component )
{
2022-12-19 18:47:15 -08:00
_visualizer . SetData ( component . Owner , ContainmentFieldGeneratorVisuals . OnLight , component . IsConnected ) ;
2021-05-13 02:05:46 +02:00
}
2022-08-05 00:22:37 -04:00
#endregion
2022-12-19 18:47:15 -08:00
/// <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 ( ) ;
}
2021-05-13 02:05:46 +02:00
}