fish-station/Content.Server/RoundEnd/RoundEndSystem.cs
vinogradgarden c7e64224d8
Добавлен звук коду угрозы изоляции (локдаун), добавлены уникальные звуки оповещений о шаттле эвакуации, (#37)
* add new sound files, update audio paths and other minor changes

- Add new sound files for shuttle announcements and sierra alert level
- Update audio paths to use _Fish directory instead of default locations
- Enable previously commented out audio playback for shuttle events
- Update alert level announcement text to include biological threat warning
- Modify ID card and PDA prototypes for special operations and oversight roles

* add audio file attributions for fish resources

Add license, copyright, and source information for audio files in the Fish resources directory. This ensures proper attribution and compliance with licensing requirements.

* remove security pod spawners and mech prototypes

The security pod spawners and mech prototypes were commented out to prevent them from being returned to games as requested by vinogradragden. This change affects both the migration configuration and the prototype definitions.

* update attributions.yml with formatted sources

* update audio file attributions with consolidated sources

Simplify attribution format by merging sources into copyright field and removing redundant entries
2025-08-02 12:43:48 +03:00

460 lines
17 KiB
C#

using System.Threading;
using Content.Server._Sunrise.TransitHub;
using Content.Server.Administration.Logs;
using Content.Server.AlertLevel;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.GameTicking;
using Content.Server.Screens.Components;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.GameTicking;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.DeviceNetwork.Components;
using Robust.Shared.Player; //Fish-edit
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 class RoundEndSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly EmergencyShuttleSystem _shuttle = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
private bool _autoCalledBefore = false;
private CancellationTokenSource? _cooldownTokenSource = null;
private CancellationTokenSource? _countdownTokenSource = null;
public TimeSpan AutoCallStartTime;
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);
public TimeSpan? LastCountdownStart { get; set; } = null;
public TimeSpan? ExpectedCountdownEnd { get; set; } = null;
public TimeSpan? ExpectedShuttleLength => ExpectedCountdownEnd - LastCountdownStart;
public TimeSpan? ShuttleTimeLeft => ExpectedCountdownEnd - _gameTiming.CurTime;
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;
}
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 _, out _, out var data);
if (data == null)
return null;
var targetGrid = _stationSystem.GetLargestGrid(data);
return targetGrid == null ? null : Transform(targetGrid.Value).MapUid;
}
// Sunrise-Start
public EntityUid? GetTransitHub()
{
AllEntityQuery<StationTransitHubComponent>().MoveNext(out var transitHub);
return transitHub == null ? null : transitHub.MapEntity;
}
// Sunrise-End
public bool CanCallOrRecall()
{
return _cooldownTokenSource == null;
}
public bool IsRoundEndRequested()
{
return _countdownTokenSource != null;
}
public void RequestRoundEnd(EntityUid? requester = null,
bool checkCooldown = true,
string text = "round-end-system-shuttle-called-announcement",
string name = "round-end-system-shuttle-sender-announcement")
{
var duration = DefaultCountdownDuration;
if (requester != null)
{
var stationUid = _stationSystem.GetOwningStation(requester.Value);
if (TryComp<AlertLevelComponent>(stationUid, out var alertLevel))
{
duration = _protoManager
.Index<AlertLevelPrototype>(AlertLevelSystem.DefaultAlertLevelSet)
.Levels[alertLevel.CurrentLevel].ShuttleTime;
}
}
RequestRoundEnd(duration, requester, checkCooldown, text, name);
}
public void RequestRoundEnd(TimeSpan countdownTime,
EntityUid? requester = null,
bool checkCooldown = true,
string text = "round-end-system-shuttle-called-announcement",
string name = "round-end-system-shuttle-sender-announcement")
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
if (checkCooldown && _cooldownTokenSource != null)
return;
if (_countdownTokenSource != null)
return;
_countdownTokenSource = new();
if (requester != null)
{
_adminLogger.Add(LogType.ShuttleCalled,
LogImpact.High,
$"Shuttle called by {ToPrettyString(requester.Value):user}");
}
else
{
_adminLogger.Add(LogType.ShuttleCalled, LogImpact.High, $"Shuttle called");
}
// 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,
colorOverride: Color.Gold); // Sunrise-TTS
_audio.PlayGlobal("/Audio/_Fish/Announcements/shuttlecalled.ogg", Filter.Broadcast(), true); //Fish-edit
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 NetworkPayload
{
[ShuttleTimerMasks.ShuttleMap] = shuttle,
[ShuttleTimerMasks.SourceMap] = GetTransitHub(), // Sunrise-Edit
[ShuttleTimerMasks.DestMap] = GetStation(),
[ShuttleTimerMasks.ShuttleTime] = countdownTime,
[ShuttleTimerMasks.SourceTime] = countdownTime +
TimeSpan.FromSeconds(_shuttle.TransitTime +
_cfg.GetCVar(
CCVars.EmergencyShuttleDockTime)),
[ShuttleTimerMasks.DestTime] = countdownTime,
};
_deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency);
}
}
public void CancelRoundEndCountdown(EntityUid? requester = null, bool checkCooldown = true)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
if (checkCooldown && _cooldownTokenSource != null)
return;
if (_countdownTokenSource == null)
return;
_countdownTokenSource.Cancel();
_countdownTokenSource = null;
if (requester != null)
{
_adminLogger.Add(LogType.ShuttleRecalled,
LogImpact.High,
$"Shuttle recalled by {ToPrettyString(requester.Value):user}");
}
else
{
_adminLogger.Add(LogType.ShuttleRecalled, LogImpact.High, $"Shuttle recalled");
}
_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/_Fish/Announcements/shuttlerecalled.ogg", Filter.Broadcast(), true); //Fish-edit
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 NetworkPayload
{
[ShuttleTimerMasks.ShuttleMap] = shuttle,
[ShuttleTimerMasks.SourceMap] = GetTransitHub(), // Sunrsie-Edit
[ShuttleTimerMasks.DestMap] = GetStation(),
[ShuttleTimerMasks.ShuttleTime] = zero,
[ShuttleTimerMasks.SourceTime] = zero,
[ShuttleTimerMasks.DestTime] = zero,
};
_deviceNetworkSystem.QueuePacket(shuttle.Value, null, 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))));
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,
null,
false,
textCall,
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)
{
_autoCalledBefore =
true; // Move before call RequestRoundEnd to play correct announcement sound type
RequestRoundEnd(null, false, "round-end-system-shuttle-auto-called-announcement");
}
// Always reset auto-call in case of a recall.
SetAutoCallTime();
}
}
// Sunrise-start
public TimeSpan TimeToCallShuttle()
{
var autoCalledBefore = _autoCalledBefore
? _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallExtensionTime)
: _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime);
return AutoCallStartTime + TimeSpan.FromMinutes(autoCalledBefore);
}
public void DelayCursedShuttle(TimeSpan delay)
{
if (_gameTicker.RunLevel != GameRunLevel.InRound)
return;
if (_countdownTokenSource == null)
return;
var countdown = ExpectedCountdownEnd - _gameTiming.CurTime + delay;
ExpectedCountdownEnd = _gameTiming.CurTime + countdown;
_countdownTokenSource.Cancel();
_countdownTokenSource = new();
if (countdown != null)
Timer.Spawn(countdown.Value, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token);
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-curse-delayed-announcement"),
Loc.GetString("Station"),
colorOverride: Color.Gold);
}
public bool ShuttleCalled()
{
return ExpectedCountdownEnd != null;
}
// Sunrise-end
}
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
}
}