using Content.Server.Administration.Logs;
using Content.Server.Antag;
using Content.Server.EUI;
using Content.Server.Flash;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Popups;
using Content.Server.Revolutionary;
using Content.Server.Revolutionary.Components;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
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.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.Cuffs.Components;
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 AntagSelectionSystem _antag = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = 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 StationSystem _stationSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId RevolutionaryNpcFaction = "Revolutionary";
public readonly ProtoId RevPrototypeId = "Rev";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnCommandMobStateChanged);
SubscribeLocalEvent(OnPostFlash);
SubscribeLocalEvent(OnHeadRevMobStateChanged);
SubscribeLocalEvent(OnGetBriefing);
}
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.CommandCheck = _timing.CurTime + component.TimerWait;
}
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);
}
}
}
protected override void AppendRoundEndText(EntityUid uid,
RevolutionaryRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
var revsLost = CheckRevsLose();
var commandLost = CheckCommandLose();
// This is (revsLost, commandsLost) concatted together
// (moony wrote this comment idk what it means)
var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0);
args.AddLine(Loc.GetString(Outcomes[index]));
var sessionData = _antag.GetAntagIdentifiers(uid);
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
foreach (var (mind, data, name) in sessionData)
{
_role.MindHasRole(mind, out var role);
var count = CompOrNull(role)?.ConvertedCount ?? 0;
args.AddLine(Loc.GetString("rev-headrev-name-user",
("name", name),
("username", data.UserName),
("count", count)));
// TODO: someone suggested listing all alive? revs maybe implement at some point
}
}
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
{
var ent = args.Mind.Comp.OwnedEntity;
var head = HasComp(ent);
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
///
/// Called when a Head Rev uses a flash in melee to convert somebody else.
///
private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev)
{
var alwaysConvertible = HasComp(ev.Target);
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);
var revComp = EnsureComp(ev.Target);
if (ev.User != null)
{
_adminLogManager.Add(LogType.Mind,
LogImpact.Medium,
$"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole(revMindId, out var role))
role.Value.Comp2.ConvertedCount++;
}
}
if (mindId == default || !_role.MindHasRole(mindId))
{
_role.MindAddRole(mindId, "MindRoleRevolutionary");
}
if (mind?.Session != null)
_antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, null);
}
//TODO: Enemies of the revolution
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 IsGroupDetainedOrDead(commandList, true, true, 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
// Cuffing Head Revs is not enough - they must be killed.
if (IsGroupDetainedOrDead(headRevList, false, false, 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 _, 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;
}
///
/// Will take a group of entities and check if these entities are alive, dead or cuffed.
///
/// The list of the entities
/// Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)
/// Bool for if you don't want to count cuffed entities.
/// Bool for if you want to count revolutionaries.
///
private bool IsGroupDetainedOrDead(List list, bool checkOffStation, bool countCuffed, bool countRevolutionaries)
{
var gone = 0;
foreach (var entity in list)
{
if (TryComp(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
{
gone++;
continue;
}
if (TryComp(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
gone++;
continue;
}
if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
gone++;
continue;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
gone++;
continue;
}
if ((HasComp(entity) || HasComp(entity)) && countRevolutionaries)
{
gone++;
continue;
}
}
return gone == list.Count || list.Count == 0;
}
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"
};
}