using System.Linq; using Content.Server.Chat.Managers; using Content.Server.Objectives.Interfaces; using Content.Server.Players; using Content.Server.Roles; using Content.Server.Traitor; using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink.Account; using Content.Shared.CCVar; using Content.Shared.Dataset; using Content.Shared.Roles; using Content.Shared.Sound; using Content.Shared.Traitor.Uplink; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.GameTicking.Rules; public sealed class TraitorRuleSystem : GameRuleSystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IObjectivesManager _objectivesManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly GameTicker _gameTicker = default!; public override string Prototype => "Traitor"; private readonly SoundSpecifier _addedSound = new SoundPathSpecifier("/Audio/Misc/tatoralert.ogg"); private readonly List _traitors = new (); private const string TraitorPrototypeID = "Traitor"; public int TotalTraitors => _traitors.Count; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnPlayersSpawned); SubscribeLocalEvent(OnRoundEndText); } public override void Started() {} public override void Ended() { _traitors.Clear(); } private void OnStartAttempt(RoundStartAttemptEvent ev) { if (!Enabled) return; // If the current preset doesn't explicitly contain the traitor game rule, just carry on and remove self. if (_gameTicker.Preset?.Rules.Contains(Prototype) ?? false) { _gameTicker.EndGameRule(_prototypeManager.Index(Prototype)); return; } var minPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); if (!ev.Forced && ev.Players.Length < minPlayers) { _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); ev.Cancel(); return; } if (ev.Players.Length == 0) { _chatManager.DispatchServerAnnouncement(Loc.GetString("traitor-no-one-ready")); ev.Cancel(); return; } } private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) { if (!Enabled) return; var playersPerTraitor = _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); var maxTraitors = _cfg.GetCVar(CCVars.TraitorMaxTraitors); var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty); var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks); var list = new List(ev.Players).Where(x => x.Data.ContentData()?.Mind?.AllRoles.All(role => role is not Job {CanBeAntag: false}) ?? false ).ToList(); var prefList = new List(); foreach (var player in list) { if (!ev.Profiles.ContainsKey(player.UserId)) { continue; } var profile = ev.Profiles[player.UserId]; if (profile.AntagPreferences.Contains(TraitorPrototypeID)) { prefList.Add(player); } } var numTraitors = MathHelper.Clamp(ev.Players.Length / playersPerTraitor, 1, maxTraitors); 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; if (mind == null) { Logger.ErrorS("preset", "Failed getting mind for picked traitor."); continue; } // creadth: we need to create uplink for the antag. // PDA should be in place already, so we just need to // initiate uplink account. DebugTools.AssertNotNull(mind.OwnedEntity); var uplinkAccount = new UplinkAccount(startingBalance, mind.OwnedEntity!); var accounts = EntityManager.EntitySysManager.GetEntitySystem(); accounts.AddNewAccount(uplinkAccount); if (!EntityManager.EntitySysManager.GetEntitySystem() .AddUplink(mind.OwnedEntity!.Value, uplinkAccount)) continue; var antagPrototype = _prototypeManager.Index(TraitorPrototypeID); var traitorRole = new TraitorRole(mind, antagPrototype); mind.AddRole(traitorRole); _traitors.Add(traitorRole); } var adjectives = _prototypeManager.Index("adjectives").Values; var verbs = _prototypeManager.Index("verbs").Values; var codewordPool = adjectives.Concat(verbs).ToList(); var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count); var codewords = new string[finalCodewordCount]; for (var i = 0; i < finalCodewordCount; i++) { codewords[i] = _random.PickAndTake(codewordPool); } foreach (var traitor in _traitors) { traitor.GreetTraitor(codewords); //give traitors their objectives var difficulty = 0f; for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) { var objective = _objectivesManager.GetRandomObjective(traitor.Mind); if (objective == null) continue; if (traitor.Mind.TryAddObjective(objective)) difficulty += objective.Difficulty; } //give traitors their codewords to keep in their character info menu traitor.Mind.Briefing = Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ",codewords))); } SoundSystem.Play(Filter.Empty().AddWhere(s => ((IPlayerSession)s).Data.ContentData()?.Mind?.HasRole() ?? false), _addedSound.GetSound(), AudioParams.Default); } private void OnRoundEndText(RoundEndTextAppendEvent ev) { if (!Enabled) return; var result = Loc.GetString("traitor-round-end-result", ("traitorCount", _traitors.Count)); foreach (var traitor in _traitors) { var name = traitor.Mind.CharacterName; traitor.Mind.TryGetSession(out var session); var username = session?.Name; var objectives = traitor.Mind.AllObjectives.ToArray(); if (objectives.Length == 0) { if (username != null) { if (name == null) result += "\n" + Loc.GetString("traitor-user-was-a-traitor", ("user", username)); else result += "\n" + Loc.GetString("traitor-user-was-a-traitor-named", ("user", username), ("name", name)); } else if (name != null) result += "\n" + Loc.GetString("traitor-was-a-traitor-named", ("name", name)); continue; } if (username != null) { if (name == null) result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives", ("user", username)); else result += "\n" + Loc.GetString("traitor-user-was-a-traitor-with-objectives-named", ("user", username), ("name", name)); } else if (name != null) result += "\n" + Loc.GetString("traitor-was-a-traitor-with-objectives-named", ("name", name)); foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer)) { result += "\n" + Loc.GetString($"preset-traitor-objective-issuer-{objectiveGroup.Key}"); foreach (var objective in objectiveGroup) { foreach (var condition in objective.Conditions) { var progress = condition.Progress; if (progress > 0.99f) { result += "\n- " + Loc.GetString( "traitor-objective-condition-success", ("condition", condition.Title), ("markupColor", "green") ); } else { result += "\n- " + Loc.GetString( "traitor-objective-condition-fail", ("condition", condition.Title), ("progress", (int) (progress * 100)), ("markupColor", "red") ); } } } } } ev.AddLine(result); } }