Files
tbd-station-14/Content.Server/GameTicking/Rules/SuspicionRuleSystem.cs

414 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar;
using Content.Shared.Doors.Systems;
using Content.Shared.GameTicking;
using Content.Shared.MobState.Components;
using Content.Shared.Roles;
using Content.Shared.Sound;
using Content.Shared.Suspicion;
using Content.Shared.Traitor.Uplink;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Simple GameRule that will do a TTT-like gamemode with traitors.
/// </summary>
public sealed class SuspicionRuleSystem : GameRuleSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
public override string Prototype => "Suspicion";
private static readonly TimeSpan DeadCheckDelay = TimeSpan.FromSeconds(1);
private readonly HashSet<SuspicionRoleComponent> _traitors = new();
public IReadOnlyCollection<SuspicionRoleComponent> Traitors => _traitors;
[DataField("addedSound")] private SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg");
private CancellationTokenSource _checkTimerCancel = new();
private TimeSpan? _endTime;
public TimeSpan? EndTime
{
get => _endTime;
set
{
_endTime = value;
SendUpdateToAll();
}
}
public TimeSpan RoundMaxTime { get; set; } = TimeSpan.FromSeconds(CCVars.SuspicionMaxTimeSeconds.DefaultValue);
public TimeSpan RoundEndDelay { get; set; } = TimeSpan.FromSeconds(10);
private const string TraitorID = "SuspicionTraitor";
private const string InnocentID = "SuspicionInnocent";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersAssigned);
SubscribeLocalEvent<RoundStartAttemptEvent>(OnRoundStartAttempt);
SubscribeLocalEvent<RefreshLateJoinAllowedEvent>(OnLateJoinRefresh);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<SuspicionRoleComponent, RoleAddedEvent>(OnRoleAdded);
SubscribeLocalEvent<SuspicionRoleComponent, RoleRemovedEvent>(OnRoleRemoved);
}
private void OnRoundStartAttempt(RoundStartAttemptEvent ev)
{
if (!Enabled)
return;
var minPlayers = _cfg.GetCVar(CCVars.SuspicionMinPlayers);
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.DispatchServerAnnouncement($"Not enough players readied up for the game! There were {ev.Players.Length} players readied up out of {minPlayers} needed.");
ev.Cancel();
return;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement("No players readied up! Can't start Suspicion.");
ev.Cancel();
return;
}
}
private void OnPlayersAssigned(RulePlayerJobsAssignedEvent ev)
{
if (!Enabled)
return;
var minTraitors = _cfg.GetCVar(CCVars.SuspicionMinTraitors);
var playersPerTraitor = _cfg.GetCVar(CCVars.SuspicionPlayersPerTraitor);
var traitorStartingBalance = _cfg.GetCVar(CCVars.SuspicionStartingBalance);
var list = new List<IPlayerSession>(ev.Players);
var prefList = new List<IPlayerSession>();
foreach (var player in list)
{
if (!ev.Profiles.ContainsKey(player.UserId) || player.AttachedEntity is not {} attached)
{
continue;
}
prefList.Add(player);
attached.EnsureComponent<SuspicionRoleComponent>();
}
var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor,
minTraitors, ev.Players.Length);
var traitors = new List<SuspicionTraitorRole>();
for (var i = 0; i < numTraitors; i++)
{
IPlayerSession traitor;
if(prefList.Count == 0)
{
if (list.Count == 0)
{
Logger.InfoS("preset", "Insufficient ready players to fill up with traitors, stopping the selection.");
break;
}
traitor = _random.PickAndTake(list);
Logger.InfoS("preset", "Insufficient preferred traitors, picking at random.");
}
else
{
traitor = _random.PickAndTake(prefList);
list.Remove(traitor);
Logger.InfoS("preset", "Selected a preferred traitor.");
}
var mind = traitor.Data.ContentData()?.Mind;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(TraitorID);
DebugTools.AssertNotNull(mind?.OwnedEntity);
var traitorRole = new SuspicionTraitorRole(mind!, antagPrototype);
mind!.AddRole(traitorRole);
traitors.Add(traitorRole);
// creadth: we need to create uplink for the antag.
// PDA should be in place already, so we just need to
// initiate uplink account.
var uplinkAccount = new UplinkAccount(traitorStartingBalance, mind.OwnedEntity!);
var accounts = EntityManager.EntitySysManager.GetEntitySystem<UplinkAccountsSystem>();
accounts.AddNewAccount(uplinkAccount);
// try to place uplink
if (!EntityManager.EntitySysManager.GetEntitySystem<UplinkSystem>()
.AddUplink(mind.OwnedEntity!.Value, uplinkAccount))
continue;
}
foreach (var player in list)
{
var mind = player.Data.ContentData()?.Mind;
var antagPrototype = _prototypeManager.Index<AntagPrototype>(InnocentID);
DebugTools.AssertNotNull(mind);
mind!.AddRole(new SuspicionInnocentRole(mind, antagPrototype));
}
foreach (var traitor in traitors)
{
traitor.GreetSuspicion(traitors, _chatManager);
}
}
public override void Started()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
RoundMaxTime = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.SuspicionMaxTimeSeconds));
EndTime = _timing.CurTime + RoundMaxTime;
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-added-announcement"));
var filter = Filter.Empty()
.AddWhere(session => ((IPlayerSession) session).ContentData()?.Mind?.HasRole<SuspicionTraitorRole>() ?? false);
SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default);
_doorSystem.AccessType = SharedDoorSystem.AccessTypes.AllowAllNoExternal;
_checkTimerCancel = new CancellationTokenSource();
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
}
public override void Ended()
{
_doorSystem.AccessType = SharedDoorSystem.AccessTypes.Id;
EndTime = null;
_traitors.Clear();
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
_checkTimerCancel.Cancel();
}
private void CheckWinConditions()
{
if (!Enabled || !_cfg.GetCVar(CCVars.GameLobbyEnableWin))
return;
var traitorsAlive = 0;
var innocentsAlive = 0;
foreach (var playerSession in _playerManager.ServerSessions)
{
if (playerSession.AttachedEntity is not {Valid: true} playerEntity
|| !_entities.TryGetComponent(playerEntity, out MobStateComponent? mobState)
|| !_entities.HasComponent<SuspicionRoleComponent>(playerEntity))
{
continue;
}
if (!mobState.IsAlive())
{
continue;
}
var mind = playerSession.ContentData()?.Mind;
if (mind != null && mind.HasRole<SuspicionTraitorRole>())
traitorsAlive++;
else
innocentsAlive++;
}
if (innocentsAlive + traitorsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-stalemate"));
EndRound(Victory.Stalemate);
}
else if (traitorsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-station-win"));
EndRound(Victory.Innocents);
}
else if (innocentsAlive == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-check-winner-traitor-win"));
EndRound(Victory.Traitors);
}
else if (_timing.CurTime > _endTime)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-suspicion-traitor-time-has-run-out"));
EndRound(Victory.Innocents);
}
}
private enum Victory
{
Stalemate,
Innocents,
Traitors
}
private void EndRound(Victory victory)
{
string text;
switch (victory)
{
case Victory.Innocents:
text = Loc.GetString("rule-suspicion-end-round-innocents-victory");
break;
case Victory.Traitors:
text = Loc.GetString("rule-suspicion-end-round-traitors-victory");
break;
default:
text = Loc.GetString("rule-suspicion-end-round-nobody-victory");
break;
}
GameTicker.EndRound(text);
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds", ("seconds", (int) RoundEndDelay.TotalSeconds)));
_checkTimerCancel.Cancel();
Timer.Spawn(RoundEndDelay, () => GameTicker.RestartRound());
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.InGame)
{
SendUpdateTimerMessage(e.Session);
}
}
private void SendUpdateToAll()
{
foreach (var player in _playerManager.ServerSessions.Where(p => p.Status == SessionStatus.InGame))
{
SendUpdateTimerMessage(player);
}
}
private void SendUpdateTimerMessage(IPlayerSession player)
{
var msg = new SuspicionMessages.SetSuspicionEndTimerMessage
{
EndTime = EndTime
};
EntityManager.EntityNetManager?.SendSystemNetworkMessage(msg, player.ConnectedClient);
}
public void AddTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Add(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.AddAlly(role);
}
role.SetAllies(_traitors);
}
public void RemoveTraitor(SuspicionRoleComponent role)
{
if (!_traitors.Remove(role))
{
return;
}
foreach (var traitor in _traitors)
{
traitor.RemoveAlly(role);
}
role.ClearAllies();
}
private void Reset(RoundRestartCleanupEvent ev)
{
EndTime = null;
_traitors.Clear();
}
private void OnPlayerDetached(EntityUid uid, SuspicionRoleComponent component, PlayerDetachedEvent args)
{
component.SyncRoles();
}
private void OnPlayerAttached(EntityUid uid, SuspicionRoleComponent component, PlayerAttachedEvent args)
{
component.SyncRoles();
}
private void OnRoleAdded(EntityUid uid, SuspicionRoleComponent component, RoleAddedEvent args)
{
if (args.Role is not SuspicionRole role) return;
component.Role = role;
}
private void OnRoleRemoved(EntityUid uid, SuspicionRoleComponent component, RoleRemovedEvent args)
{
if (args.Role is not SuspicionRole) return;
component.Role = null;
}
private void OnLateJoinRefresh(RefreshLateJoinAllowedEvent ev)
{
if (!Enabled)
return;
ev.Disallow();
}
}