using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Antag; using Content.Server.Chat.Managers; using Content.Server.EUI; using Content.Server.Flash; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; using Content.Server.NPC.Components; using Content.Server.NPC.Systems; using Content.Server.Popups; using Content.Server.Revolutionary; using Content.Server.Revolutionary.Components; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mindshield.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Revolutionary.Components; using Content.Shared.Roles; using Content.Shared.Stunnable; using Content.Shared.Zombies; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Timing; namespace Content.Server.GameTicking.Rules; /// /// Where all the main stuff for Revolutionaries happens (Assigning Head Revs, Command on station, and checking for the game to end.) /// public sealed class RevolutionaryRuleSystem : GameRuleSystem { [Dependency] private readonly IAdminLogManager _adminLogManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly EuiManager _euiMan = default!; [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly RoleSystem _role = default!; [Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly AudioSystem _audioSystem = default!; [ValidatePrototypeId] public const string RevolutionaryNpcFaction = "Revolutionary"; [ValidatePrototypeId] public const string RevolutionaryAntagRole = "Rev"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnPlayerJobAssigned); SubscribeLocalEvent(OnCommandMobStateChanged); SubscribeLocalEvent(OnHeadRevMobStateChanged); SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnPostFlash); } protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); component.CommandCheck = _timing.CurTime + component.TimerWait; } /// /// Checks if the round should end and also checks who has a mindshield. /// protected override void ActiveTick(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); if (component.CommandCheck <= _timing.CurTime) { component.CommandCheck = _timing.CurTime + component.TimerWait; if (CheckCommandLose()) { _roundEnd.DoRoundEndBehavior(RoundEndBehavior.ShuttleCall, component.ShuttleCallTime); GameTicker.EndGameRule(uid, gameRule); } } } private void OnRoundEndText(RoundEndTextAppendEvent ev) { var revsLost = CheckRevsLose(); var commandLost = CheckCommandLose(); var query = AllEntityQuery(); while (query.MoveNext(out var headrev)) { // This is (revsLost, commandsLost) concatted together // (moony wrote this comment idk what it means) var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0); ev.AddLine(Loc.GetString(Outcomes[index])); ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count))); foreach (var player in headrev.HeadRevs) { // TODO: when role entities are a thing this has to change var count = CompOrNull(player.Value)?.ConvertedCount ?? 0; _mind.TryGetSession(player.Value, out var session); var username = session?.Name; if (username != null) { ev.AddLine(Loc.GetString("rev-headrev-name-user", ("name", player.Key), ("username", username), ("count", count))); } else { ev.AddLine(Loc.GetString("rev-headrev-name", ("name", player.Key), ("count", count))); } // TODO: someone suggested listing all alive? revs maybe implement at some point } } } private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args) { if (!TryComp(uid, out var mind) || mind.OwnedEntity == null) return; var head = HasComp(mind.OwnedEntity); args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing")); } private void OnStartAttempt(RoundStartAttemptEvent ev) { var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var gameRule)) { _antagSelection.AttemptStartGameRule(ev, uid, comp.MinPlayers, gameRule); } } private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev) { var query = QueryActiveRules(); while (query.MoveNext(out _, out var comp, out _)) { _antagSelection.EligiblePlayers(comp.HeadRevPrototypeId, comp.MaxHeadRevs, comp.PlayersPerHeadRev, comp.HeadRevStartSound, "head-rev-role-greeting", "#5e9cff", out var chosen); if (chosen.Any()) GiveHeadRev(chosen, comp.HeadRevPrototypeId, comp); else { _chatManager.SendAdminAnnouncement(Loc.GetString("rev-no-heads")); } } } private void GiveHeadRev(List chosen, string antagProto, RevolutionaryRuleComponent comp) { foreach (var headRev in chosen) { RemComp(headRev); var inCharacterName = MetaData(headRev).EntityName; if (_mind.TryGetMind(headRev, out var mindId, out var mind)) { if (!_role.MindHasRole(mindId)) { _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = antagProto }); } if (mind.Session != null) { comp.HeadRevs.Add(inCharacterName, mindId); } } _antagSelection.GiveAntagBagGear(headRev, comp.StartingGear); EnsureComp(headRev); EnsureComp(headRev); } } /// /// Called when a Head Rev uses a flash in melee to convert somebody else. /// public void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev) { TryComp(ev.Target, out var alwaysConvertibleComp); var alwaysConvertible = alwaysConvertibleComp != null; if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible) return; if (HasComp(ev.Target) || HasComp(ev.Target) || !HasComp(ev.Target) && !alwaysConvertible || !_mobState.IsAlive(ev.Target) || HasComp(ev.Target)) { return; } _npcFaction.AddFaction(ev.Target, RevolutionaryNpcFaction); EnsureComp(ev.Target); _stun.TryParalyze(ev.Target, comp.StunTime, true); if (ev.User != null) { _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary"); if (_mind.TryGetRole(ev.User.Value, out var headrev)) headrev.ConvertedCount++; } if (mindId == default || !_role.MindHasRole(mindId)) { _role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevolutionaryAntagRole }); } if (mind?.Session != null) { var message = Loc.GetString("rev-role-greeting"); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.ConnectedClient, Color.Red); _audioSystem.PlayGlobal("/Audio/Ambience/Antag/headrev_start.ogg", ev.Target); } } public void OnHeadRevAdmin(EntityUid mindId, MindComponent? mind = null) { if (!Resolve(mindId, ref mind)) return; var revRule = EntityQuery().FirstOrDefault(); if (revRule == null) { GameTicker.StartGameRule("Revolutionary", out var ruleEnt); revRule = Comp(ruleEnt); } if (!HasComp(mind.OwnedEntity)) { if (mind.OwnedEntity != null) { var player = new List { mind.OwnedEntity.Value }; GiveHeadRev(player, RevolutionaryAntagRole, revRule); } if (mind.Session != null) { var message = Loc.GetString("head-rev-role-greeting"); var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); _chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, mind.Session.ConnectedClient, Color.FromHex("#5e9cff")); } } } private void OnCommandMobStateChanged(EntityUid uid, CommandStaffComponent comp, MobStateChangedEvent ev) { if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid) CheckCommandLose(); } /// /// Checks if all of command is dead and if so will remove all sec and command jobs if there were any left. /// private bool CheckCommandLose() { var commandList = new List(); var heads = AllEntityQuery(); while (heads.MoveNext(out var id, out _)) { commandList.Add(id); } return _antagSelection.IsGroupDead(commandList, true); } private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev) { if (ev.NewMobState == MobState.Dead || ev.NewMobState == MobState.Invalid) CheckRevsLose(); } /// /// Checks if all the Head Revs are dead and if so will deconvert all regular revs. /// private bool CheckRevsLose() { var stunTime = TimeSpan.FromSeconds(4); var headRevList = new List(); var headRevs = AllEntityQuery(); while (headRevs.MoveNext(out var uid, out _, out _)) { headRevList.Add(uid); } // If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen if (_antagSelection.IsGroupDead(headRevList, false)) { var rev = AllEntityQuery(); while (rev.MoveNext(out var uid, out _, out var mc)) { if (HasComp(uid)) continue; _npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction); _stun.TryParalyze(uid, stunTime, true); RemCompDeferred(uid); _popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid); _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying."); if (!_mind.TryGetMind(uid, out var mindId, out var mind, mc)) continue; // remove their antag role _role.MindTryRemoveRole(mindId); // make it very obvious to the rev they've been deconverted since // they may not see the popup due to antag and/or new player tunnel vision if (_mind.TryGetSession(mindId, out var session)) _euiMan.OpenEui(new DeconvertedEui(), session); } return true; } return false; } private static readonly string[] Outcomes = { // revs survived and heads survived... how "rev-reverse-stalemate", // revs won and heads died "rev-won", // revs lost and heads survived "rev-lost", // revs lost and heads died "rev-stalemate" }; }