2022-06-26 15:20:45 +10:00
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
using Content.Server.Administration.Logs ;
using Content.Server.Administration.Managers ;
using Content.Server.Chat.Systems ;
using Content.Server.Communications ;
using Content.Server.GameTicking.Events ;
using Content.Server.Shuttles.Components ;
using Content.Server.Station.Components ;
using Content.Server.Station.Systems ;
using Content.Shared.CCVar ;
using Content.Shared.Database ;
using Content.Shared.Shuttles.Events ;
using Robust.Server.Maps ;
using Robust.Server.Player ;
using Robust.Shared.Audio ;
using Robust.Shared.Configuration ;
using Robust.Shared.Map ;
using Robust.Shared.Player ;
using Robust.Shared.Random ;
namespace Content.Server.Shuttles.Systems ;
public sealed partial class ShuttleSystem
{
/ *
* Handles the escape shuttle + Centcomm .
* /
[Dependency] private readonly IAdminLogManager _logger = default ! ;
[Dependency] private readonly IAdminManager _admin = default ! ;
[Dependency] private readonly IConfigurationManager _configManager = default ! ;
[Dependency] private readonly IMapLoader _loader = default ! ;
[Dependency] private readonly IRobustRandom _random = default ! ;
[Dependency] private readonly ChatSystem _chatSystem = default ! ;
[Dependency] private readonly CommunicationsConsoleSystem _commsConsole = default ! ;
[Dependency] private readonly DockingSystem _dockSystem = default ! ;
[Dependency] private readonly StationSystem _station = default ! ;
private MapId ? _centcommMap ;
2022-06-27 15:11:39 +02:00
private EntityUid ? _centcomm ;
2022-06-26 15:20:45 +10:00
/// <summary>
/// Used for multiple shuttle spawn offsets.
/// </summary>
private float _shuttleIndex ;
private const float ShuttleSpawnBuffer = 1f ;
2022-06-27 15:19:40 +10:00
private bool _emergencyShuttleEnabled ;
2022-06-26 15:20:45 +10:00
private void InitializeEscape ( )
{
2022-06-27 15:19:40 +10:00
#if ! FULL_RELEASE
_configManager . OverrideDefault ( CCVars . EmergencyShuttleEnabled , false ) ;
#endif
_emergencyShuttleEnabled = _configManager . GetCVar ( CCVars . EmergencyShuttleEnabled ) ;
// Don't immediately invoke as roundstart will just handle it.
_configManager . OnValueChanged ( CCVars . EmergencyShuttleEnabled , SetEmergencyShuttleEnabled ) ;
2022-06-26 15:20:45 +10:00
SubscribeLocalEvent < RoundStartingEvent > ( OnRoundStart ) ;
SubscribeLocalEvent < StationDataComponent , ComponentStartup > ( OnStationStartup ) ;
SubscribeNetworkEvent < EmergencyShuttleRequestPositionMessage > ( OnShuttleRequestPosition ) ;
}
2022-06-27 15:19:40 +10:00
private void SetEmergencyShuttleEnabled ( bool value )
{
if ( _emergencyShuttleEnabled = = value ) return ;
_emergencyShuttleEnabled = value ;
if ( value )
{
SetupEmergencyShuttle ( ) ;
}
else
{
CleanupEmergencyShuttle ( ) ;
}
}
private void ShutdownEscape ( )
{
_configManager . UnsubValueChanged ( CCVars . EmergencyShuttleEnabled , SetEmergencyShuttleEnabled ) ;
}
2022-06-26 15:20:45 +10:00
/// <summary>
/// If the client is requesting debug info on where an emergency shuttle would dock.
/// </summary>
private void OnShuttleRequestPosition ( EmergencyShuttleRequestPositionMessage msg , EntitySessionEventArgs args )
{
if ( ! _admin . IsAdmin ( ( IPlayerSession ) args . SenderSession ) ) return ;
var player = args . SenderSession . AttachedEntity ;
if ( player = = null | |
2022-06-27 15:11:39 +02:00
! TryComp < StationDataComponent > ( _station . GetOwningStation ( player . Value ) , out var stationData ) | |
! TryComp < ShuttleComponent > ( stationData . EmergencyShuttle , out var shuttle ) ) return ;
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
var targetGrid = GetLargestGrid ( stationData ) ;
if ( targetGrid = = null ) return ;
var config = GetDockingConfig ( shuttle , targetGrid . Value ) ;
if ( config = = null ) return ;
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
RaiseNetworkEvent ( new EmergencyShuttlePositionMessage ( )
2022-06-26 15:20:45 +10:00
{
2022-06-27 15:11:39 +02:00
StationUid = targetGrid ,
Position = config . Area ,
} ) ;
2022-06-26 15:20:45 +10:00
}
/// <summary>
/// Checks whether the emergency shuttle can warp to the specified position.
/// </summary>
private bool ValidSpawn ( IMapGridComponent grid , Box2 area )
{
return ! grid . Grid . GetLocalTilesIntersecting ( area ) . Any ( ) ;
}
2022-06-27 15:11:39 +02:00
private DockingConfig ? GetDockingConfig ( ShuttleComponent component , EntityUid targetGrid )
2022-06-26 15:20:45 +10:00
{
2022-06-27 15:11:39 +02:00
var gridDocks = GetDocks ( targetGrid ) ;
2022-06-26 15:20:45 +10:00
if ( gridDocks . Count < = 0 ) return null ;
var xformQuery = GetEntityQuery < TransformComponent > ( ) ;
2022-06-27 15:11:39 +02:00
var targetGridGrid = Comp < IMapGridComponent > ( targetGrid ) ;
var targetGridXform = xformQuery . GetComponent ( targetGrid ) ;
2022-06-27 03:48:49 +10:00
var targetGridAngle = targetGridXform . WorldRotation . Reduced ( ) ;
var targetGridRotation = targetGridAngle . ToVec ( ) ;
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
var shuttleDocks = GetDocks ( component . Owner ) ;
var shuttleAABB = Comp < IMapGridComponent > ( component . Owner ) . Grid . LocalAABB ;
2022-06-26 15:20:45 +10:00
var validDockConfigs = new List < DockingConfig > ( ) ;
2022-06-27 15:11:39 +02:00
SetPilotable ( component , false ) ;
2022-06-26 15:20:45 +10:00
if ( shuttleDocks . Count > 0 )
{
// We'll try all combinations of shuttle docks and see which one is most suitable
foreach ( var shuttleDock in shuttleDocks )
{
var shuttleDockXform = xformQuery . GetComponent ( shuttleDock . Owner ) ;
foreach ( var gridDock in gridDocks )
{
var gridXform = xformQuery . GetComponent ( gridDock . Owner ) ;
if ( ! CanDock (
shuttleDock , shuttleDockXform ,
gridDock , gridXform ,
targetGridRotation ,
shuttleAABB ,
targetGridGrid ,
out var dockedAABB ,
out var matty ,
out var targetAngle ) ) continue ;
2022-06-27 03:48:49 +10:00
// Can't just use the AABB as we want to get bounds as tight as possible.
2022-06-27 15:11:39 +02:00
var spawnPosition = new EntityCoordinates ( targetGrid , matty . Transform ( Vector2 . Zero ) ) ;
2022-06-27 03:48:49 +10:00
spawnPosition = new EntityCoordinates ( targetGridXform . MapUid ! . Value , spawnPosition . ToMapPos ( EntityManager ) ) ;
var dockedBounds = new Box2Rotated ( shuttleAABB . Translated ( spawnPosition . Position ) , targetGridAngle , spawnPosition . Position ) ;
// Check if there's no intersecting grids (AKA oh god it's docking at cargo).
if ( _mapManager . FindGridsIntersecting ( targetGridXform . MapID ,
dockedBounds ) . Any ( o = > o . GridEntityId ! = targetGrid ) )
{
break ;
}
2022-06-26 15:20:45 +10:00
// Alright well the spawn is valid now to check how many we can connect
// Get the matrix for each shuttle dock and test it against the grid docks to see
// if the connected position / direction matches.
var dockedPorts = new List < ( DockingComponent DockA , DockingComponent DockB ) > ( )
{
( shuttleDock , gridDock ) ,
} ;
// TODO: Check shuttle orientation as the tiebreaker.
foreach ( var other in shuttleDocks )
{
if ( other = = shuttleDock ) continue ;
foreach ( var otherGrid in gridDocks )
{
if ( otherGrid = = gridDock ) continue ;
if ( ! CanDock (
other ,
xformQuery . GetComponent ( other . Owner ) ,
otherGrid ,
xformQuery . GetComponent ( otherGrid . Owner ) ,
targetGridRotation ,
shuttleAABB , targetGridGrid ,
out var otherDockedAABB ,
out _ ,
out var otherTargetAngle ) | |
! otherDockedAABB . Equals ( dockedAABB ) | |
! targetAngle . Equals ( otherTargetAngle ) ) continue ;
dockedPorts . Add ( ( other , otherGrid ) ) ;
}
}
var spawnRotation = shuttleDockXform . LocalRotation +
gridXform . LocalRotation +
targetGridXform . LocalRotation ;
validDockConfigs . Add ( new DockingConfig ( )
{
Docks = dockedPorts ,
Area = dockedAABB . Value ,
Coordinates = spawnPosition ,
Angle = spawnRotation ,
} ) ;
}
}
}
if ( validDockConfigs . Count < = 0 ) return null ;
// Prioritise by priority docks, then by maximum connected ports, then by most similar angle.
validDockConfigs = validDockConfigs
. OrderByDescending ( x = > x . Docks . Any ( docks = > HasComp < EmergencyDockComponent > ( docks . DockB . Owner ) ) )
. ThenByDescending ( x = > x . Docks . Count )
. ThenBy ( x = > Math . Abs ( Angle . ShortestDistance ( x . Angle . Reduced ( ) , targetGridAngle ) . Theta ) ) . ToList ( ) ;
var location = validDockConfigs . First ( ) ;
2022-06-27 15:11:39 +02:00
location . TargetGrid = targetGrid ;
2022-06-26 15:20:45 +10:00
// TODO: Ideally do a hyperspace warpin, just have it run on like a 10 second timer.
return location ;
}
/// <summary>
/// Calls the emergency shuttle for the station.
/// </summary>
public void CallEmergencyShuttle ( EntityUid ? stationUid )
{
if ( ! TryComp < StationDataComponent > ( stationUid , out var stationData ) | |
2022-06-27 15:11:39 +02:00
! TryComp < TransformComponent > ( stationData . EmergencyShuttle , out var xform ) | |
! TryComp < ShuttleComponent > ( stationData . EmergencyShuttle , out var shuttle ) ) return ;
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
var targetGrid = GetLargestGrid ( stationData ) ;
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
// UHH GOOD LUCK
if ( targetGrid = = null )
2022-06-26 15:20:45 +10:00
{
2022-06-27 15:11:39 +02:00
_logger . Add ( LogType . EmergencyShuttle , LogImpact . High , $"Emergency shuttle {ToPrettyString(stationUid.Value)} unable to dock with station {ToPrettyString(stationUid.Value)}" ) ;
_chatSystem . DispatchStationAnnouncement ( stationUid . Value , Loc . GetString ( "emergency-shuttle-good-luck" ) , playDefaultSound : false ) ;
// TODO: Need filter extensions or something don't blame me.
SoundSystem . Play ( "/Audio/Misc/notice1.ogg" , Filter . Broadcast ( ) ) ;
return ;
}
2022-06-26 15:20:45 +10:00
2022-06-27 15:11:39 +02:00
if ( TryHyperspaceDock ( shuttle , targetGrid . Value ) )
{
2022-06-26 15:20:45 +10:00
_logger . Add ( LogType . EmergencyShuttle , LogImpact . High , $"Emergency shuttle {ToPrettyString(stationUid.Value)} docked with stations" ) ;
_chatSystem . DispatchStationAnnouncement ( stationUid . Value , Loc . GetString ( "emergency-shuttle-docked" , ( "time" , $"{_consoleAccumulator:0}" ) ) , playDefaultSound : false ) ;
// TODO: Need filter extensions or something don't blame me.
SoundSystem . Play ( "/Audio/Announcements/shuttle_dock.ogg" , Filter . Broadcast ( ) ) ;
}
else
{
_logger . Add ( LogType . EmergencyShuttle , LogImpact . High , $"Emergency shuttle {ToPrettyString(stationUid.Value)} unable to find a valid docking port for {ToPrettyString(stationUid.Value)}" ) ;
_chatSystem . DispatchStationAnnouncement ( stationUid . Value , Loc . GetString ( "emergency-shuttle-nearby" ) , playDefaultSound : false ) ;
// TODO: Need filter extensions or something don't blame me.
SoundSystem . Play ( "/Audio/Misc/notice1.ogg" , Filter . Broadcast ( ) ) ;
}
}
/// <summary>
/// Checks if 2 docks can be connected by moving the shuttle directly onto docks.
/// </summary>
private bool CanDock (
DockingComponent shuttleDock ,
TransformComponent shuttleXform ,
DockingComponent gridDock ,
TransformComponent gridXform ,
Vector2 targetGridRotation ,
Box2 shuttleAABB ,
IMapGridComponent grid ,
[NotNullWhen(true)] out Box2 ? shuttleDockedAABB ,
out Matrix3 matty ,
out Vector2 gridRotation )
{
gridRotation = Vector2 . Zero ;
matty = Matrix3 . Identity ;
shuttleDockedAABB = null ;
if ( shuttleDock . Docked | |
gridDock . Docked | |
! shuttleXform . Anchored | |
! gridXform . Anchored )
{
return false ;
}
// First, get the station dock's position relative to the shuttle, this is where we rotate it around
var stationDockPos = shuttleXform . LocalPosition +
shuttleXform . LocalRotation . RotateVec ( new Vector2 ( 0f , - 1f ) ) ;
var stationDockMatrix = Matrix3 . CreateInverseTransform ( stationDockPos , - shuttleXform . LocalRotation ) ;
var gridXformMatrix = Matrix3 . CreateTransform ( gridXform . LocalPosition , gridXform . LocalRotation ) ;
Matrix3 . Multiply ( in stationDockMatrix , in gridXformMatrix , out matty ) ;
shuttleDockedAABB = matty . TransformBox ( shuttleAABB ) ;
if ( ! ValidSpawn ( grid , shuttleDockedAABB . Value ) ) return false ;
gridRotation = matty . Transform ( targetGridRotation ) ;
return true ;
}
private void OnStationStartup ( EntityUid uid , StationDataComponent component , ComponentStartup args )
{
AddEmergencyShuttle ( component ) ;
}
private void OnRoundStart ( RoundStartingEvent ev )
{
2022-06-27 15:19:40 +10:00
SetupEmergencyShuttle ( ) ;
2022-06-26 15:20:45 +10:00
}
/// <summary>
/// Spawns the emergency shuttle for each station and starts the countdown until controls unlock.
/// </summary>
public void CallEmergencyShuttle ( )
{
if ( EmergencyShuttleArrived ) return ;
2022-06-27 15:19:40 +10:00
if ( ! _emergencyShuttleEnabled )
{
_roundEnd . EndRound ( ) ;
return ;
}
2022-06-26 15:20:45 +10:00
_consoleAccumulator = _configManager . GetCVar ( CCVars . EmergencyShuttleDockTime ) ;
EmergencyShuttleArrived = true ;
if ( _centcommMap ! = null )
_mapManager . SetMapPaused ( _centcommMap . Value , false ) ;
foreach ( var comp in EntityQuery < StationDataComponent > ( true ) )
{
CallEmergencyShuttle ( comp . Owner ) ;
}
_commsConsole . UpdateCommsConsoleInterface ( ) ;
}
/// <summary>
/// Gets the largest member grid from a station.
/// </summary>
private EntityUid ? GetLargestGrid ( StationDataComponent component )
{
EntityUid ? largestGrid = null ;
Box2 largestBounds = new Box2 ( ) ;
foreach ( var gridUid in component . Grids )
{
if ( ! TryComp < IMapGridComponent > ( gridUid , out var grid ) ) continue ;
if ( grid . Grid . LocalAABB . Size . LengthSquared < largestBounds . Size . LengthSquared ) continue ;
largestBounds = grid . Grid . LocalAABB ;
largestGrid = gridUid ;
}
return largestGrid ;
}
private List < DockingComponent > GetDocks ( EntityUid uid )
{
var result = new List < DockingComponent > ( ) ;
foreach ( var ( dock , xform ) in EntityQuery < DockingComponent , TransformComponent > ( true ) )
{
if ( xform . ParentUid ! = uid | | ! dock . Enabled ) continue ;
result . Add ( dock ) ;
}
return result ;
}
2022-06-27 15:19:40 +10:00
private void SetupEmergencyShuttle ( )
2022-06-26 15:20:45 +10:00
{
2022-06-27 15:19:40 +10:00
if ( ! _emergencyShuttleEnabled | | _centcommMap ! = null & & _mapManager . MapExists ( _centcommMap . Value ) ) return ;
2022-06-26 15:20:45 +10:00
_centcommMap = _mapManager . CreateMap ( ) ;
_mapManager . SetMapPaused ( _centcommMap . Value , true ) ;
2022-06-27 15:11:39 +02:00
// Load Centcomm
var centcommPath = _configManager . GetCVar ( CCVars . CentcommMap ) ;
if ( ! string . IsNullOrEmpty ( centcommPath ) )
{
var ( _ , centcomm ) = _loader . LoadBlueprint ( _centcommMap . Value , "/Maps/centcomm.yml" ) ;
_centcomm = centcomm ;
}
else
{
_sawmill . Info ( "No centcomm map found, skipping setup." ) ;
}
2022-06-26 15:20:45 +10:00
foreach ( var comp in EntityQuery < StationDataComponent > ( true ) )
{
AddEmergencyShuttle ( comp ) ;
}
}
private void AddEmergencyShuttle ( StationDataComponent component )
{
2022-06-27 15:19:40 +10:00
if ( ! _emergencyShuttleEnabled | | _centcommMap = = null | | component . EmergencyShuttle ! = null ) return ;
2022-06-26 15:20:45 +10:00
// Load escape shuttle
var ( _ , shuttle ) = _loader . LoadBlueprint ( _centcommMap . Value , component . EmergencyShuttlePath . ToString ( ) , new MapLoadOptions ( )
{
// Should be far enough... right? I'm too lazy to bounds check centcomm rn.
Offset = new Vector2 ( 500f + _shuttleIndex , 0f )
} ) ;
if ( shuttle = = null )
{
_sawmill . Error ( $"Unable to spawn emergency shuttle {component.EmergencyShuttlePath} for {ToPrettyString(component.Owner)}" ) ;
return ;
}
_shuttleIndex + = _mapManager . GetGrid ( shuttle . Value ) . LocalAABB . Width + ShuttleSpawnBuffer ;
component . EmergencyShuttle = shuttle ;
}
private void CleanupEmergencyShuttle ( )
{
2022-06-27 15:19:40 +10:00
// If we get cleaned up mid roundend just end it.
if ( _launchedShuttles )
{
_roundEnd . EndRound ( ) ;
}
2022-06-26 15:20:45 +10:00
_shuttleIndex = 0f ;
if ( _centcommMap = = null | | ! _mapManager . MapExists ( _centcommMap . Value ) )
{
_centcommMap = null ;
return ;
}
_mapManager . DeleteMap ( _centcommMap . Value ) ;
}
/// <summary>
/// Stores the data for a valid docking configuration for the emergency shuttle
/// </summary>
private sealed class DockingConfig
{
/// <summary>
/// The pairs of docks that can connect.
/// </summary>
public List < ( DockingComponent DockA , DockingComponent DockB ) > Docks = new ( ) ;
/// <summary>
/// Area relative to the target grid the emergency shuttle will spawn in on.
/// </summary>
public Box2 Area ;
/// <summary>
/// Target grid for docking.
/// </summary>
public EntityUid TargetGrid ;
public EntityCoordinates Coordinates ;
public Angle Angle ;
}
}