diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 42783f163b..82bdb78816 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -194,7 +194,7 @@ namespace Content.Server.RoundEnd ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime; // TODO full game saves - Timer.Spawn(countdownTime, _shuttle.CallEmergencyShuttle, _countdownTokenSource.Token); + Timer.Spawn(countdownTime, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token); ActivateCooldown(); RaiseLocalEvent(RoundEndSystemChangedEvent.Default); diff --git a/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs b/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs index 8febe51f5a..f219602bcb 100644 --- a/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs +++ b/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs @@ -19,6 +19,6 @@ public sealed class DockEmergencyShuttleCommand : IConsoleCommand public void Execute(IConsoleShell shell, string argStr, string[] args) { var system = _sysManager.GetEntitySystem(); - system.CallEmergencyShuttle(); + system.DockEmergencyShuttle(); } } diff --git a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs index 597d74dcc7..aabfaa31dd 100644 --- a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs +++ b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs @@ -17,7 +17,7 @@ public sealed partial class DockingSystem private const int DockRoundingDigits = 2; - public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform, EntityQuery xformQuery) + public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform) { var (shuttlePos, shuttleRot) = _transform.GetWorldPositionRotation(xform); var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform); @@ -288,9 +288,7 @@ public sealed partial class DockingSystem // Prioritise by priority docks, then by maximum connected ports, then by most similar angle. validDockConfigs = validDockConfigs - .OrderByDescending(x => x.Docks.Any(docks => - TryComp(docks.DockBUid, out var priority) && - priority.Tag?.Equals(priorityTag) == true)) + .OrderByDescending(x => IsConfigPriority(x, priorityTag)) .ThenByDescending(x => x.Docks.Count) .ThenBy(x => Math.Abs(Angle.ShortestDistance(x.Angle.Reduced(), targetGridAngle).Theta)).ToList(); @@ -301,6 +299,13 @@ public sealed partial class DockingSystem return location; } + public bool IsConfigPriority(DockingConfig config, string? priorityTag) + { + return config.Docks.Any(docks => + TryComp(docks.DockBUid, out var priority) + && priority.Tag?.Equals(priorityTag) == true); + } + /// /// Checks whether the shuttle can warp to the specified position. /// diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs index 4c13a2cc82..6c4bdc0814 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using System.Threading; using Content.Server.Access.Systems; @@ -255,18 +256,19 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem } /// - /// Attempts to dock the emergency shuttle to the station. + /// Attempts to dock a station's emergency shuttle. /// - public void CallEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null) + /// + public ShuttleDockResult? DockSingleEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null) { if (!Resolve(stationUid, ref stationShuttle)) - return; + return null; if (!TryComp(stationShuttle.EmergencyShuttle, out TransformComponent? xform) || !TryComp(stationShuttle.EmergencyShuttle, out var shuttle)) { Log.Error($"Attempted to call an emergency shuttle for an uninitialized station? Station: {ToPrettyString(stationUid)}. Shuttle: {ToPrettyString(stationShuttle.EmergencyShuttle)}"); - return; + return null; } var targetGrid = _station.GetLargestGrid(Comp(stationUid)); @@ -274,60 +276,126 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem // UHH GOOD LUCK if (targetGrid == null) { - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}"); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-good-luck"), playDefaultSound: false); + _logger.Add( + LogType.EmergencyShuttle, + LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}"); + + return new ShuttleDockResult + { + Station = (stationUid, stationShuttle), + ResultType = ShuttleDockResultType.GoodLuck, + }; + } + + ShuttleDockResultType resultType; + if (_shuttle.TryFTLDock(stationShuttle.EmergencyShuttle.Value, shuttle, targetGrid.Value, out var config, DockTag)) + { + _logger.Add( + LogType.EmergencyShuttle, + LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations"); + + resultType = _dock.IsConfigPriority(config, DockTag) + ? ShuttleDockResultType.PriorityDock + : ShuttleDockResultType.OtherDock; + } + else + { + _logger.Add( + LogType.EmergencyShuttle, + LogImpact.High, + $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}"); + + resultType = ShuttleDockResultType.NoDock; + } + + return new ShuttleDockResult + { + Station = (stationUid, stationShuttle), + DockingConfig = config, + ResultType = resultType, + TargetGrid = targetGrid, + }; + } + + /// + /// Do post-shuttle-dock setup. Announce to the crew and set up shuttle timers. + /// + public void AnnounceShuttleDock(ShuttleDockResult result, bool extended) + { + var shuttle = result.Station.Comp.EmergencyShuttle; + + DebugTools.Assert(shuttle != null); + + if (result.ResultType == ShuttleDockResultType.GoodLuck) + { + _chatSystem.DispatchStationAnnouncement( + result.Station, + Loc.GetString("emergency-shuttle-good-luck"), + playDefaultSound: false); + // TODO: Need filter extensions or something don't blame me. _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true); return; } - var xformQuery = GetEntityQuery(); + DebugTools.Assert(result.TargetGrid != null); - if (_shuttle.TryFTLDock(stationShuttle.EmergencyShuttle.Value, shuttle, targetGrid.Value, DockTag)) + // Send station announcement. + + var targetXform = Transform(result.TargetGrid.Value); + var angle = _dock.GetAngle( + shuttle.Value, + Transform(shuttle.Value), + result.TargetGrid.Value, + targetXform); + + var direction = ContentLocalizationManager.FormatDirection(angle.GetDir()); + var location = FormattedMessage.RemoveMarkupPermissive( + _navMap.GetNearestBeaconString((shuttle.Value, Transform(shuttle.Value)))); + + var extendedText = extended ? Loc.GetString("emergency-shuttle-extended") : ""; + var locKey = result.ResultType == ShuttleDockResultType.NoDock + ? "emergency-shuttle-nearby" + : "emergency-shuttle-docked"; + + _chatSystem.DispatchStationAnnouncement( + result.Station, + Loc.GetString( + locKey, + ("time", $"{_consoleAccumulator:0}"), + ("direction", direction), + ("location", location), + ("extended", extendedText)), + playDefaultSound: false); + + // Trigger shuttle timers on the shuttle. + + var time = TimeSpan.FromSeconds(_consoleAccumulator); + if (TryComp(shuttle, out var netComp)) { - if (TryComp(targetGrid.Value, out TransformComponent? targetXform)) + var payload = new NetworkPayload { - var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery); - var direction = ContentLocalizationManager.FormatDirection(angle.GetDir()); - var location = FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform))); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false); - } - - // shuttle timers - var time = TimeSpan.FromSeconds(_consoleAccumulator); - if (TryComp(stationShuttle.EmergencyShuttle.Value, out var netComp)) - { - var payload = new NetworkPayload - { - [ShuttleTimerMasks.ShuttleMap] = stationShuttle.EmergencyShuttle.Value, - [ShuttleTimerMasks.SourceMap] = targetXform?.MapUid, - [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(), - [ShuttleTimerMasks.ShuttleTime] = time, - [ShuttleTimerMasks.SourceTime] = time, - [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime), - [ShuttleTimerMasks.Docked] = true - }; - _deviceNetworkSystem.QueuePacket(stationShuttle.EmergencyShuttle.Value, null, payload, netComp.TransmitFrequency); - } - - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations"); - // TODO: Need filter extensions or something don't blame me. - _audio.PlayGlobal("/Audio/Announcements/shuttle_dock.ogg", Filter.Broadcast(), true); + [ShuttleTimerMasks.ShuttleMap] = shuttle, + [ShuttleTimerMasks.SourceMap] = targetXform.MapUid, + [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(), + [ShuttleTimerMasks.ShuttleTime] = time, + [ShuttleTimerMasks.SourceTime] = time, + [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime), + [ShuttleTimerMasks.Docked] = true, + }; + _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, netComp.TransmitFrequency); } - else - { - if (TryComp(targetGrid.Value, out var targetXform)) - { - var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery); - var direction = ContentLocalizationManager.FormatDirection(angle.GetDir()); - var location = FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform))); - _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-nearby", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false); - } - _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}"); - // TODO: Need filter extensions or something don't blame me. - _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true); - } + // Play announcement audio. + + var audioFile = result.ResultType == ShuttleDockResultType.NoDock + ? "/Audio/Misc/notice1.ogg" + : "/Audio/Announcements/shuttle_dock.ogg"; + + // TODO: Need filter extensions or something don't blame me. + _audio.PlayGlobal(audioFile, Filter.Broadcast(), true); } private void OnStationInit(EntityUid uid, StationCentcommComponent component, MapInitEvent args) @@ -353,9 +421,12 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem } /// - /// Spawns the emergency shuttle for each station and starts the countdown until controls unlock. + /// Teleports the emergency shuttle to its station and starts the countdown until it launches. /// - public void CallEmergencyShuttle() + /// + /// If the emergency shuttle is disabled, this immediately ends the round. + /// + public void DockEmergencyShuttle() { if (EmergencyShuttleArrived) return; @@ -371,9 +442,34 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem var query = AllEntityQuery(); + var dockResults = new List(); + while (query.MoveNext(out var uid, out var comp)) { - CallEmergencyShuttle(uid, comp); + if (DockSingleEmergencyShuttle(uid, comp) is { } dockResult) + dockResults.Add(dockResult); + } + + // Make the shuttle wait longer if it couldn't dock in the normal spot. + // We have to handle the possibility of there being multiple stations, so since the shuttle timer is global, + // use the WORST value we have. + var worstResult = dockResults.Max(x => x.ResultType); + var multiplier = worstResult switch + { + ShuttleDockResultType.OtherDock => _configManager.GetCVar( + CCVars.EmergencyShuttleDockTimeMultiplierOtherDock), + ShuttleDockResultType.NoDock => _configManager.GetCVar( + CCVars.EmergencyShuttleDockTimeMultiplierNoDock), + // GoodLuck doesn't get a multiplier. + // Quite frankly at that point the round is probably so fucked that you'd rather it be over ASAP. + _ => 1, + }; + + _consoleAccumulator *= multiplier; + + foreach (var shuttleDockResult in dockResults) + { + AnnounceShuttleDock(shuttleDockResult, multiplier > 1); } _commsConsole.UpdateCommsConsoleInterface(); @@ -579,4 +675,66 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem return _transformSystem.GetWorldMatrix(shuttleXform).TransformBox(grid.LocalAABB).Contains(_transformSystem.GetWorldPosition(xform)); } + + /// + /// A result of a shuttle dock operation done by . + /// + /// + public sealed class ShuttleDockResult + { + /// + /// The station for which the emergency shuttle got docked. + /// + public Entity Station; + + /// + /// The target grid of the station that the shuttle tried to dock to. + /// + /// + /// Not present if is . + /// + public EntityUid? TargetGrid; + + /// + /// Enum code describing the dock result. + /// + public ShuttleDockResultType ResultType; + + /// + /// The docking config used to actually dock to the station. + /// + /// + /// Only present if is + /// or . + /// + public DockingConfig? DockingConfig; + } + + /// + /// Emergency shuttle dock result codes used by . + /// + public enum ShuttleDockResultType : byte + { + // This enum is ordered from "best" to "worst". This is used to sort the results. + + /// + /// The shuttle was docked at a priority dock, which is the intended destination. + /// + PriorityDock, + + /// + /// The shuttle docked at another dock on the station then the intended priority dock. + /// + OtherDock, + + /// + /// The shuttle couldn't find any suitable dock on the station at all, it did not dock. + /// + NoDock, + + /// + /// No station grid was found at all, shuttle did not get moved. + /// + GoodLuck, + } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index 8da7aaa641..f30cab253a 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -669,8 +669,28 @@ public sealed partial class ShuttleSystem /// Tries to dock with the target grid, otherwise falls back to proximity. /// This bypasses FTL travel time. /// - public bool TryFTLDock(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, string? priorityTag = null) + public bool TryFTLDock( + EntityUid shuttleUid, + ShuttleComponent component, + EntityUid targetUid, + string? priorityTag = null) { + return TryFTLDock(shuttleUid, component, targetUid, out _, priorityTag); + } + + /// + /// Tries to dock with the target grid, otherwise falls back to proximity. + /// This bypasses FTL travel time. + /// + public bool TryFTLDock( + EntityUid shuttleUid, + ShuttleComponent component, + EntityUid targetUid, + [NotNullWhen(true)] out DockingConfig? config, + string? priorityTag = null) + { + config = null; + if (!_xformQuery.TryGetComponent(shuttleUid, out var shuttleXform) || !_xformQuery.TryGetComponent(targetUid, out var targetXform) || targetXform.MapUid == null || @@ -679,7 +699,7 @@ public sealed partial class ShuttleSystem return false; } - var config = _dockSystem.GetDockingConfig(shuttleUid, targetUid, priorityTag); + config = _dockSystem.GetDockingConfig(shuttleUid, targetUid, priorityTag); if (config != null) { diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 95fb7bd692..9e95231c84 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1556,6 +1556,18 @@ namespace Content.Shared.CCVar public static readonly CVarDef EmergencyShuttleDockTime = CVarDef.Create("shuttle.emergency_dock_time", 180f, CVar.SERVERONLY); + /// + /// If the emergency shuttle can't dock at a priority port, the dock time will be multiplied with this value. + /// + public static readonly CVarDef EmergencyShuttleDockTimeMultiplierOtherDock = + CVarDef.Create("shuttle.emergency_dock_time_multiplier_other_dock", 1.6667f, CVar.SERVERONLY); + + /// + /// If the emergency shuttle can't dock at all, the dock time will be multiplied with this value. + /// + public static readonly CVarDef EmergencyShuttleDockTimeMultiplierNoDock = + CVarDef.Create("shuttle.emergency_dock_time_multiplier_no_dock", 2f, CVar.SERVERONLY); + /// /// How long after the console is authorized for the shuttle to early launch. /// diff --git a/Resources/Locale/en-US/shuttles/emergency.ftl b/Resources/Locale/en-US/shuttles/emergency.ftl index be3f0962fa..ef3582c623 100644 --- a/Resources/Locale/en-US/shuttles/emergency.ftl +++ b/Resources/Locale/en-US/shuttles/emergency.ftl @@ -13,9 +13,10 @@ emergency-shuttle-command-launch-desc = Early launches the emergency shuttle if # Emergency shuttle emergency-shuttle-left = The Emergency Shuttle has left the station. Estimate {$transitTime} seconds until the shuttle arrives at CentComm. emergency-shuttle-launch-time = The emergency shuttle will launch in {$consoleAccumulator} seconds. -emergency-shuttle-docked = The Emergency Shuttle has docked {$direction} of the station, {$location}. It will leave in {$time} seconds. +emergency-shuttle-docked = The Emergency Shuttle has docked {$direction} of the station, {$location}. It will leave in {$time} seconds.{$extended} emergency-shuttle-good-luck = The Emergency Shuttle is unable to find a station. Good luck. -emergency-shuttle-nearby = The Emergency Shuttle is unable to find a valid docking port. It has warped in {$direction} of the station, {$location}. +emergency-shuttle-nearby = The Emergency Shuttle is unable to find a valid docking port. It has warped in {$direction} of the station, {$location}. It will leave in {$time} seconds.{$extended} +emergency-shuttle-extended = {" "}Launch time has been extended due to inconvenient circumstances. # Emergency shuttle console popup / announcement emergency-shuttle-console-no-early-launches = Early launch is disabled