CatStation/Content.Server/RoundEnd/RoundEndSystem.cs
Rouden cc2c14f8e4
Device Network refactor part 1: API improvements (#44346)
* Refactor DeviceNetwork

* Singleton

* NetworkPayloads refactor

* Reorganize DeviceNetworkSystem

* Surveillance cameras

* Fix code

* Fix build

* Fix test fail by rewriting entire singleton logic

* Properly predict examine

* Update DeviceNetworkSystem.API.cs

* Update EntityTest.cs

* Update DeviceNetworkSystem.cs

* Alright I'll do this expensive check fine

* Fix disposals debug assert

* Implement device network static events support

* Use frozen dictionaries for performance

* Update SharedDeviceNetworkSystem.Payloads.cs

* Parallel payload handling support

* Fix some spagetti-code

* Optimize NetworkPayload cancellation

* Parallel payloads are now cancellable

* Fix screens

* Fix robotics console

* Allow new methods for shared

* Mail fix

* Turret fix

* Fix network system method usage + signal and borg fix

* finally fix atmos devices OH MY GOOOD

* Remove stupid parallelism from fax

* Fix surveiollance cameras

* Working on a single thing for 2 days straight award

* Fix the test?

* Update units.yml

* Add IDevice interface because why not

* Add more device network suffixes

* Fix camera router failing test

* yaml linter peak

* Improve docs for DeviceNetworkHandlers

* fix build

* Update CartridgeLoaderSystem.Relay.cs

* Remove parallel system and EntitySystem post init

* Add more docs and also events

* Remove unnecessary caching

* Update DevicePayloadComponentSystem.cs

* Back to entity events

* Implement DeviceData

* Remove serializable from payloads

* Update DeviceData.cs

* Add a prototype for a singleton

* Fix tests

* Fix air alarms

* Update DeviceNetworkSystem.cs

* Fix cameras

* Make camera router event-less

* Fix atmos sync payloads, add more docs

* Remove singleton + instant packets

* oops remove public access

* Fix tests?

* Why the hell overload effect makes puddle sparks

* Apparently it was redundant

* Update gates.yml

* Finish documentation

* Review

* Mark relay properties as obsolete

* Struct payloads support (#blazinglyfastmemorysafe)

* Update DeviceNetworkTest.cs

* Make device structs stuff better

* Move address prefix to DeviceNetworkComponent

* Locale fixes

* Fix tests

* Doesn't know boolean logic award

* Update AtmosAlarmableSystem.cs

* Fix address predicted examine

* Partial fix to air alarmables

* Fix deployable turrets UI

* Fix firelocks, maybe

* Apparently Activatable UI was enough lol

* Fix surveillance cameras

* Rename `QueuePacket` into `SendPacket`

* Simplify DeviceData as much as possible

* Update DeviceData.cs

* Update FaxMachineComponent.cs

* Remove DeviceData because its stupid

* Update ScreenSystem.cs

* Fix build (camera map broken still)

* Fix map

* fix usings

* Review changes

* Build fix
2026-08-31 19:42:56 +00:00

415 lines
16 KiB
C#

using System.Threading;
using Content.Server.Administration.Logs;
using Content.Shared.AlertLevel;
using Content.Shared.CCVar;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.GameTicking;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.RoundEnd;
using Content.Shared.Station.Components;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.RoundEnd
{
/// <summary>
/// Handles ending rounds normally and also via requesting it (e.g. via comms console)
/// If you request a round end then an escape shuttle will be used.
/// </summary>
public sealed partial class RoundEndSystem : EntitySystem
{
[Dependency] private IAdminLogManager _adminLogger = default!;
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IChatManager _chatManager = default!;
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private ChatSystem _chatSystem = default!;
[Dependency] private GameTicker _gameTicker = default!;
[Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private EmergencyShuttleSystem _shuttle = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private StationSystem _stationSystem = default!;
public TimeSpan DefaultCooldownDuration { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Countdown to use where there is no station alert countdown to be found.
/// </summary>
public TimeSpan DefaultCountdownDuration { get; set; } = TimeSpan.FromMinutes(10);
private CancellationTokenSource? _countdownTokenSource = null;
private CancellationTokenSource? _cooldownTokenSource = null;
public TimeSpan? LastCountdownStart { get; set; } = null;
public TimeSpan? ExpectedCountdownEnd { get; set; } = null;
public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart;
public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime;
/// <summary>
/// If the shuttle can't be recalled. if set to true, the station wont be able to recall
/// </summary>
public bool CantRecall = false;
public TimeSpan AutoCallStartTime;
private bool _autoCalledBefore = false;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(_ => Reset());
SetAutoCallTime();
}
private void SetAutoCallTime()
{
AutoCallStartTime = _gameTiming.CurTime;
}
private void Reset()
{
if (_countdownTokenSource != null)
{
_countdownTokenSource.Cancel();
_countdownTokenSource = null;
}
if (_cooldownTokenSource != null)
{
_cooldownTokenSource.Cancel();
_cooldownTokenSource = null;
}
CantRecall = false;
LastCountdownStart = null;
ExpectedCountdownEnd = null;
SetAutoCallTime();
_autoCalledBefore = false;
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
}
/// <summary>
/// Attempts to get the MapUid of the station using <see cref="StationSystem.GetLargestGrid"/>
/// </summary>
public EntityUid? GetStation()
{
AllEntityQuery<StationEmergencyShuttleComponent, StationDataComponent>().MoveNext(out var uid, out _, out var data);
if (data == null)
return null;
var targetGrid = _stationSystem.GetLargestGrid((uid, data));
return targetGrid == null ? null : Transform(targetGrid.Value).MapUid;
}
/// <summary>
/// Attempts to get centcomm's MapUid
/// </summary>
public EntityUid? GetCentcomm()
{
AllEntityQuery<StationCentcommComponent>().MoveNext(out var centcomm);
return centcomm == null ? null : centcomm.MapEntity;
}
public bool CanCallOrRecall()
{
return _cooldownTokenSource == null && !CantRecall;
}
public bool IsRoundEndRequested()
{
return _countdownTokenSource != null;
}
/// <summary>
/// Starts the process of ending the round by calling evac
/// </summary>
/// <param name="requester">who called evac</param>
/// <param name="machine">machine used to call evac</param>
/// <param name="checkCooldown"></param>
/// <param name="text">text in the announcement of shuttle calling</param>
/// <param name="name">name in the announcement of shuttle calling</param>
/// <param name="cantRecall">if the station shouldn't be able to recall the shuttle</param>
public void RequestRoundEnd(EntityUid? requester = null, EntityUid? machine = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement", bool cantRecall = false)
{
var duration = DefaultCountdownDuration;
if (requester != null)
{
var stationUid = _stationSystem.GetOwningStation(requester.Value);
if (TryComp<AlertLevelComponent>(stationUid, out var alertLevelComp))
{
duration = ProtoMan
.Index(alertLevelComp.CurrentAlertLevel)
.ShuttleTime;
}
}
RequestRoundEnd(duration, requester, machine, checkCooldown, text, name, cantRecall);
}
/// <summary>
/// Starts the process of ending the round by calling evac
/// </summary>
/// <param name="countdownTime">time for evac to arrive</param>
/// <param name="requester">who called evac</param>
/// <param name="machine">machine used to call evac</param>
/// <param name="checkCooldown"></param>
/// <param name="text">text in the announcement of shuttle calling</param>
/// <param name="name">name in the announcement of shuttle calling</param>
/// <param name="cantRecall">if the station shouldn't be able to recall the shuttle</param>
public void RequestRoundEnd(TimeSpan countdownTime, EntityUid? requester = null, EntityUid? machine = null, bool checkCooldown = true, string text = "round-end-system-shuttle-called-announcement", string name = "round-end-system-shuttle-sender-announcement", bool cantRecall = false)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
if (checkCooldown && _cooldownTokenSource != null)
return;
if (_countdownTokenSource != null)
return;
_countdownTokenSource = new();
CantRecall = cantRecall;
var what = machine != null ? $" with {ToPrettyString(machine.Value):entity} " : "";
if (requester != null)
_adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called by {ToPrettyString(requester.Value):player}{what}");
else
_adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called{what}");
// I originally had these set up here but somehow time gets passed as 0 to Loc so IDEK.
int time;
string units;
if (countdownTime.TotalSeconds < 60)
{
time = countdownTime.Seconds;
units = "eta-units-seconds";
}
else
{
time = countdownTime.Minutes;
units = "eta-units-minutes";
}
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString(text,
("time", time),
("units", Loc.GetString(units))),
Loc.GetString(name),
false,
null,
Color.Gold);
_audio.PlayGlobal("/Audio/Announcements/shuttlecalled.ogg", Filter.Broadcast(), true);
LastCountdownStart = _gameTiming.CurTime;
ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime;
// TODO full game saves
Timer.Spawn(countdownTime, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token);
ActivateCooldown();
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
var shuttle = _shuttle.GetShuttle();
if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
{
var payload = new ScreenShuttlePayload
{
Shuttle = shuttle,
SourceMap = GetCentcomm(),
DestinationMap = GetStation(),
ShuttleTime = countdownTime,
SourceTime = countdownTime + TimeSpan.FromSeconds(_shuttle.TransitTime + _cfg.GetCVar(CCVars.EmergencyShuttleDockTime)),
DestinationTime = countdownTime,
};
_deviceNetworkSystem.SendPacket(shuttle.Value, null, ref payload, net.TransmitFrequency);
}
}
public void CancelRoundEndCountdown(EntityUid? requester = null, EntityUid? machine = null, bool forceRecall = false)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
if (!forceRecall && (CantRecall || _cooldownTokenSource != null))
return;
if (_countdownTokenSource == null)
return;
_countdownTokenSource.Cancel();
_countdownTokenSource = null;
var what = machine != null ? $" with {ToPrettyString(machine.Value):entity} " : "";
if (requester != null)
_adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled by {ToPrettyString(requester.Value):player}{what}");
else
_adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled{what}");
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"),
Loc.GetString("round-end-system-shuttle-sender-announcement"), false, colorOverride: Color.Gold);
_audio.PlayGlobal("/Audio/Announcements/shuttlerecalled.ogg", Filter.Broadcast(), true);
LastCountdownStart = null;
ExpectedCountdownEnd = null;
ActivateCooldown();
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
// remove active clientside evac shuttle timers by zeroing the target time
var zero = TimeSpan.Zero;
var shuttle = _shuttle.GetShuttle();
if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
{
var payload = new ScreenShuttlePayload
{
Shuttle = shuttle,
SourceMap = GetCentcomm(),
DestinationMap = GetStation(),
ShuttleTime = zero,
SourceTime = zero,
DestinationTime = zero,
};
_deviceNetworkSystem.SendPacket(shuttle.Value, null, ref payload, net.TransmitFrequency);
}
}
public void EndRound(TimeSpan? countdownTime = null)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound) return;
LastCountdownStart = null;
ExpectedCountdownEnd = null;
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
_gameTicker.EndRound();
_countdownTokenSource?.Cancel();
_countdownTokenSource = new();
countdownTime ??= TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.RoundRestartTime));
int time;
string unitsLocString;
if (countdownTime.Value.TotalSeconds < 60)
{
time = countdownTime.Value.Seconds;
unitsLocString = "eta-units-seconds";
}
else
{
time = countdownTime.Value.Minutes;
unitsLocString = "eta-units-minutes";
}
_chatManager.DispatchServerAnnouncement(
Loc.GetString(
"round-end-system-round-restart-eta-announcement",
("time", time),
("units", Loc.GetString(unitsLocString, ("amount", time)))));
Timer.Spawn(countdownTime.Value, AfterEndRoundRestart, _countdownTokenSource.Token);
}
/// <summary>
/// Starts a behavior to end the round
/// </summary>
/// <param name="behavior">The way in which the round will end</param>
/// <param name="time"></param>
/// <param name="sender"></param>
/// <param name="textCall"></param>
/// <param name="textAnnounce"></param>
public void DoRoundEndBehavior(RoundEndBehavior behavior,
TimeSpan time,
string sender = "comms-console-announcement-title-centcom",
string textCall = "round-end-system-shuttle-called-announcement",
string textAnnounce = "round-end-system-shuttle-already-called-announcement")
{
switch (behavior)
{
case RoundEndBehavior.InstantEnd:
EndRound();
break;
case RoundEndBehavior.ShuttleCall:
// Check is shuttle called or not. We should only dispatch announcement if it's already called
if (IsRoundEndRequested())
{
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString(textAnnounce),
Loc.GetString(sender),
colorOverride: Color.Gold);
}
else
{
RequestRoundEnd(time, checkCooldown: false, text: textCall,
name: Loc.GetString(sender));
}
break;
}
}
private void AfterEndRoundRestart()
{
if (_gameTicker.RunLevel != GameRunLevel.PostRound) return;
Reset();
_gameTicker.RestartRound();
}
private void ActivateCooldown()
{
_cooldownTokenSource?.Cancel();
_cooldownTokenSource = new();
// TODO full game saves
Timer.Spawn(DefaultCooldownDuration, () =>
{
_cooldownTokenSource.Cancel();
_cooldownTokenSource = null;
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
}, _cooldownTokenSource.Token);
}
public override void Update(float frameTime)
{
// Check if we should auto-call.
int mins = _autoCalledBefore ? _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallExtensionTime)
: _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime);
if (mins != 0 && _gameTiming.CurTime - AutoCallStartTime > TimeSpan.FromMinutes(mins))
{
if (!_shuttle.EmergencyShuttleArrived && ExpectedCountdownEnd is null)
{
RequestRoundEnd(checkCooldown: false, text: "round-end-system-shuttle-auto-called-announcement");
_autoCalledBefore = true;
}
// Always reset auto-call in case of a recall.
SetAutoCallTime();
}
}
}
public sealed class RoundEndSystemChangedEvent : EntityEventArgs
{
public static RoundEndSystemChangedEvent Default { get; } = new();
}
public enum RoundEndBehavior : byte
{
/// <summary>
/// Instantly end round
/// </summary>
InstantEnd,
/// <summary>
/// Call shuttle with custom announcement
/// </summary>
ShuttleCall,
/// <summary>
/// Do nothing
/// </summary>
Nothing
}
}