#nullable enable using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma { sealed partial class TraitorManager { const int MaxPreviousEventHistory = 10; const int StartDelayMin = 60; const int StartDelayMax = 200; private float startTimer; private bool started = false; private TraitorResults? results = null; public class PreviousTraitorEvent { public TraitorEventPrefab TraitorEvent { get; } public TraitorEvent.State State { get; } public Client Traitor => GameMain.Server.ConnectedClients.Find(c => traitorAccountId.IsSome() && traitorAccountId == c.AccountId) ?? GameMain.Server.ConnectedClients.Find(c => traitorAddress == c.Connection.Endpoint.Address); private readonly Address traitorAddress; private readonly Option traitorAccountId; public PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Client traitor) { TraitorEvent = traitorEvent; State = state; traitorAddress = traitor.Connection.Endpoint.Address; traitorAccountId = traitor.AccountId; } private PreviousTraitorEvent(TraitorEventPrefab traitorEvent, TraitorEvent.State state, Option accountId, Address address) { TraitorEvent = traitorEvent; State = state; traitorAddress = address; traitorAccountId = accountId; } public void Save(XElement parentElement) { parentElement.Add( new XElement(nameof(PreviousTraitorEvent), new XAttribute("id", TraitorEvent.Identifier), new XAttribute("state", State), new XAttribute("accountid", traitorAccountId), new XAttribute("address", traitorAddress))); } public static PreviousTraitorEvent? Load(XElement subElement) { var traitorEventId = subElement.GetAttributeIdentifier("id", Identifier.Empty); var state = subElement.GetAttributeEnum("state", Barotrauma.TraitorEvent.State.Failed); var accountId = Networking.AccountId.Parse( subElement.GetAttributeString("accountid", null) ?? subElement.GetAttributeString("steamid", "")); var address = Address.Parse( subElement.GetAttributeString("address", null) ?? subElement.GetAttributeString("endpoint", "")) .Fallback(new UnknownAddress()); if (EventPrefab.Prefabs.TryGet(traitorEventId, out EventPrefab? prefab) && prefab is TraitorEventPrefab traitorEventPrefab) { return new PreviousTraitorEvent(traitorEventPrefab, state, accountId, address); } else { DebugConsole.ThrowError($"Error when loading {nameof(TraitorManager)}: could not find a traitor event prefab with the identifier \"{traitorEventId}\"."); return null; } } } public readonly record struct ActiveTraitorEvent( Client Traitor, TraitorEvent TraitorEvent); private readonly List previousTraitorEvents = new List(); private readonly List activeEvents = new List(); public IEnumerable ActiveEvents => activeEvents; private readonly GameServer server; private EventManager? eventManager; private Level? level; public bool Enabled; public bool IsTraitor(Character character) { return activeEvents.Any(e => e.Traitor.Character == character); } public TraitorManager(GameServer server) { this.server = server; } public void Initialize(EventManager eventManager, Level level) { this.eventManager = eventManager; this.level = level; startTimer = Rand.Range(StartDelayMin, StartDelayMax); started = false; results = null; } private bool TryCreateTraitorEvents(EventManager eventManager, Level level) { var eventPrefabs = EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Cast(); if (!eventPrefabs.Any()) { DebugConsole.AddWarning("No traitor event available in any of the enabled content packages."); return false; } if (server.ConnectedClients.Count(IsClientViableTraitor) < server.ServerSettings.TraitorsMinPlayerCount) { DebugConsole.AddWarning("Not enough clients to create a traitor event. Active traitor events: " + activeEvents.Count); #if DEBUG DebugConsole.AddWarning("Starting a traitor event anyway because this is a debug build."); #else return false; #endif } int maxDangerLevel = server.ServerSettings.TraitorDangerLevel; int playerCount = server.ConnectedClients.Count(c => c.Character != null && !c.Character.Removed); var campaign = GameMain.GameSession?.Campaign; var suitablePrefabs = eventPrefabs .Where(e => EventManager.IsLevelSuitable(e, level)) .Where(e => e.ReputationRequirementsMet(campaign)) .Where(e => e.MissionRequirementsMet(GameMain.GameSession)) .Where(e => e.LevelRequirementsMet(level)) .Where(e => e.DangerLevel <= maxDangerLevel) .Where(e => playerCount >= e.MinPlayerCount) .ToList(); if (!suitablePrefabs.Any()) { //this is normal, there e.g. might be no missions for an abandoned outpost or end level DebugConsole.Log("No suitable traitor missions available for the level."); return false; } foreach (var previousEvent in previousTraitorEvents.Reverse().DistinctBy(e => e.Traitor)) { if (previousEvent.State == TraitorEvent.State.Completed && previousEvent.TraitorEvent.IsChainable && IsClientViableTraitor(previousEvent.Traitor)) { GameServer.Log($"{NetworkMember.ClientLogName(previousEvent.Traitor)} successfully completed a traitor event ({previousEvent.TraitorEvent.Identifier}) on a previous round. Attempting to give choose them a new, more dangerous event...", ServerLog.MessageType.Traitors); var suitablePrefab = //try finding an event that's continuation from the previous one (= requires the previous one to be completed 1st) suitablePrefabs.GetRandomUnsynced(p => p.RequiredCompletedTags.Any(t => previousEvent.TraitorEvent.Identifier == t || previousEvent.TraitorEvent.Tags.Contains(t))); suitablePrefab ??= //otherwise try finding some higher-difficult event for the same faction suitablePrefabs.GetRandomUnsynced(p => p.RequiredCompletedTags.None() && p.DangerLevel > previousEvent.TraitorEvent.DangerLevel && p.Faction == previousEvent.TraitorEvent.Faction); if (suitablePrefab != null) { CreateTraitorEvent(eventManager, suitablePrefab, previousEvent.Traitor); return true; } else { GameServer.Log($"Could not find a suitable, more difficult traitor event for {NetworkMember.ClientLogName(previousEvent.Traitor)}.", ServerLog.MessageType.Traitors); } } } TraitorEventPrefab selectedPrefab; if (GameMain.GameSession?.Campaign == null) { selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); } else { //events that are suitable as initial traitor events (not requiring some other event to be completed first) var suitableInitialPrefabs = suitablePrefabs.FindAll(e => e.RequiredCompletedTags.None() && IsSuitableDangerLevel(e)); bool IsSuitableDangerLevel(TraitorEventPrefab prefab) { if (prefab.DangerLevel == TraitorEventPrefab.MinDangerLevel) { return true; } //events that require another event to be completed are handled earlier in the method if (prefab.RequirePreviousDangerLevelCompleted) { return false; } return prefab.RequiredPreviousDangerLevel < TraitorEventPrefab.MinDangerLevel || previousTraitorEvents.Any(e2 => e2.TraitorEvent.DangerLevel >= prefab.RequiredPreviousDangerLevel); } selectedPrefab = suitableInitialPrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); if (selectedPrefab == null) { GameServer.Log($"Could not find a suitable danger level {TraitorEventPrefab.MinDangerLevel} traitor event. Choosing a random event instead.", ServerLog.MessageType.Traitors); selectedPrefab = suitablePrefabs.GetRandomByWeight(GetTraitorEventPrefabCommonness, Rand.RandSync.Unsynced); } } if (selectedPrefab != null) { var selectedTraitor = SelectRandomTraitor(); if (selectedTraitor == null) { DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{selectedPrefab.Identifier}\".", contentPackage: selectedPrefab.ContentPackage); return false; } CreateTraitorEvent(eventManager, selectedPrefab, selectedTraitor); return true; } return false; } private Client? SelectRandomTraitor() { if (GameSettings.CurrentConfig.VerboseLogging) { GameServer.Log( $"Choosing a random traitor... Available traitors:" + string.Concat(server.ConnectedClients.Where(IsClientViableTraitor).Select(c => $"\n - {c.Name} ({(int)(GetTraitorProbability(c) * 100)}%)")), ServerLog.MessageType.Traitors); } return server.ConnectedClients.Where(IsClientViableTraitor).GetRandomByWeight(GetTraitorProbability, Rand.RandSync.Unsynced); } private IEnumerable SelectSecondaryTraitors(TraitorEvent traitorEvent, Client mainTraitor) { if (traitorEvent.Prefab.SecondaryTraitorPercentage <= 0.0f && traitorEvent.Prefab.SecondaryTraitorAmount <= 0) { return Enumerable.Empty(); } var viableTraitors = server.ConnectedClients.Where(c => c != mainTraitor && IsClientViableTraitor(c)).ToList(); int amountToChoose = (int)Math.Ceiling(viableTraitors.Count * (traitorEvent.Prefab.SecondaryTraitorPercentage / 100.0f)); amountToChoose = Math.Max(amountToChoose, traitorEvent.Prefab.SecondaryTraitorAmount); if (amountToChoose > viableTraitors.Count) { DebugConsole.ThrowError( $"Error in traitor event {traitorEvent.Prefab.Identifier}. Not enough players to choose {amountToChoose} secondary traitors. " + $"Make sure the {nameof(traitorEvent.Prefab.MinPlayerCount)} of the event is high enough to support to desired amount of secondary traitors.", contentPackage: traitorEvent.Prefab.ContentPackage); amountToChoose = viableTraitors.Count; } List traitors = new List(); for (int i = 0; i < amountToChoose; i++) { var traitor = viableTraitors.GetRandomUnsynced(); viableTraitors.Remove(traitor); traitors.Add(traitor); } return traitors; } private bool IsClientViableTraitor(Client client) { return client != null && server.ConnectedClients.Contains(client) && client.Character != null && !client.Character.IsIncapacitated && !client.Character.Removed && activeEvents.None(e => e.Traitor == client); } private float GetTraitorEventPrefabCommonness(TraitorEventPrefab prefab) { int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.TraitorEvent == prefab); if (roundsSinceLastSelected.HasValue) { float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); //exponentially decreasing commonness the closer the last time the event was selected return prefab.Commonness * normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; } else { return prefab.Commonness; } } private float GetTraitorProbability(Client client) { int? roundsSinceLastSelected = GetRoundsSinceLastSelected(e => e.Traitor == client); if (roundsSinceLastSelected.HasValue) { float normalizedRoundsSinceLastSelected = MathUtils.InverseLerp(0, MaxPreviousEventHistory, roundsSinceLastSelected.Value); //exponentially decreasing commonness the closer the last time the event was selected return normalizedRoundsSinceLastSelected * normalizedRoundsSinceLastSelected; } else { return 1.0f; } } private int? GetRoundsSinceLastSelected(Func condition) { //most recent events are at the end of the list, start from there for (int i = previousTraitorEvents.Count - 1; i >= 0; i--) { if (condition(previousTraitorEvents[i])) { return previousTraitorEvents.Count - i; } } return null; } private void CreateTraitorEvent(EventManager eventManager, TraitorEventPrefab selectedPrefab, Client traitor) { if (selectedPrefab.TryCreateInstance(eventManager.RandomSeed, out var newEvent)) { var secondaryTraitors = SelectSecondaryTraitors(newEvent, traitor); string logMessage = $"{NetworkMember.ClientLogName(traitor)} was selected as a traitor. Selected event: {selectedPrefab.Name}"; if (secondaryTraitors.Any()) { logMessage += $", secondary traitors: {string.Join(", ", secondaryTraitors.Select(c => NetworkMember.ClientLogName(c)))}"; } GameServer.Log(logMessage, ServerLog.MessageType.Traitors); newEvent.OnStateChanged += () => SendCurrentState(newEvent); activeEvents.Add(new ActiveTraitorEvent(traitor, newEvent)); newEvent.SetTraitor(traitor); newEvent.SetSecondaryTraitors(secondaryTraitors); eventManager.ActivateEvent(newEvent); SendCurrentState(newEvent); } else { DebugConsole.ThrowError($"Failed to create an instance of the traitor event prefab \"{selectedPrefab.Identifier}\"!", contentPackage: selectedPrefab.ContentPackage); } } public void ForceTraitorEvent(TraitorEventPrefab traitorEventPrefab) { if (eventManager == null) { throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); } var traitor = SelectRandomTraitor(); if (traitor == null) { DebugConsole.ThrowError($"Could not find a suitable traitor for the event \"{traitorEventPrefab.Identifier}\".", contentPackage: traitorEventPrefab.ContentPackage); return; } CreateTraitorEvent(eventManager, traitorEventPrefab, traitor); } public void SkipStartDelay() { startTimer = 0.0f; if (activeEvents.Any()) { started = true; } } public void Update(float deltaTime) { if (!Enabled) { return; } if (!started) { if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection }) { if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2) { //try starting ASAP if the submarine is already half-way through the level //(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below) startTimer = Math.Min(startTimer, 10.0f); } } startTimer -= deltaTime; if (startTimer >= 0.0f) { return; } if (eventManager == null) { throw new InvalidOperationException("EventManager was null. TraitorManager may not have been initialized properly."); } if (level == null) { throw new InvalidOperationException("Level was null. TraitorManager may not have been initialized properly."); } if (TryCreateTraitorEvents(eventManager, level)) { started = true; } else { //restart timer, we might be able to start a mission later if more clients join startTimer = Rand.Range(StartDelayMin, StartDelayMax); } } } public void EndRound() { Client? votedAsTraitor = GetClientAccusedAsTraitor(); foreach (var activeEvent in activeEvents) { if (results != null) { DebugConsole.AddWarning("Multiple traitor events active during the round, only displaying the results for the last one."); } results = new TraitorResults(votedAsTraitor, activeEvent.TraitorEvent); if (results.Value.MoneyPenalty > 0) { GameMain.GameSession?.Campaign?.Bank?.TryDeduct(results.Value.MoneyPenalty); } if (activeEvent.TraitorEvent.CurrentState != TraitorEvent.State.Completed) { activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; } GameServer.Log( NetworkMember.ClientLogName(activeEvent.Traitor) + (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed ? " completed their traitor objective successfully." : " failed to complete their traitor objective."), ServerLog.MessageType.Traitors); //consider the event failed if the traitor was identifier correctly, //so the traitor doesn't get rewards or get assigned a follow-up event if (results.Value.VotedCorrectTraitor) { GameServer.Log( NetworkMember.ClientLogName(activeEvent.Traitor) + " was correctly identified as the traitor, and will not receive any rewards.", ServerLog.MessageType.Traitors); activeEvent.TraitorEvent.CurrentState = TraitorEvent.State.Failed; } previousTraitorEvents.Add(new PreviousTraitorEvent( activeEvent.TraitorEvent.Prefab, activeEvent.TraitorEvent.CurrentState, activeEvent.Traitor)); if (activeEvent.TraitorEvent.CurrentState == TraitorEvent.State.Completed) { AchievementManager.OnTraitorWin(activeEvent.TraitorEvent.Traitor?.Character); foreach (var secondaryTraitor in activeEvent.TraitorEvent.SecondaryTraitors) { AchievementManager.OnTraitorWin(secondaryTraitor?.Character); } } } if (previousTraitorEvents.Count > MaxPreviousEventHistory) { previousTraitorEvents.RemoveRange(0, previousTraitorEvents.Count - MaxPreviousEventHistory); } activeEvents.Clear(); } public Client? GetClientAccusedAsTraitor() { Client? votedAsTraitor = Voting.HighestVoted(VoteType.Traitor, server.ConnectedClients.Where(c => c.Character is { IsDead: false }), out int voteCount); if (voteCount < server.ConnectedClients.Count * server.ServerSettings.MinPercentageOfPlayersForTraitorAccusation / 100.0f) { //at least x% of the players must've voted for the same player votedAsTraitor = null; } return votedAsTraitor; } public TraitorResults? GetEndResults() { return results; } public XElement Save() { var element = new XElement(nameof(TraitorManager), new XAttribute("version", GameMain.Version.ToString())); foreach (var previousEvent in previousTraitorEvents) { previousEvent.Save(element); } return element; } public void Load(XElement traitorManagerElement) { previousTraitorEvents.Clear(); foreach (XElement subElement in traitorManagerElement.Elements()) { if (subElement.Name.ToIdentifier() == nameof(PreviousTraitorEvent)) { var previousTraitorEvent = PreviousTraitorEvent.Load(subElement); if (previousTraitorEvent != null) { previousTraitorEvents.Add(previousTraitorEvent); } } } } public void SendCurrentState(TraitorEvent ev) { if (ev?.Traitor == null) { return; } var msg = new WriteOnlyMessage(); msg.WriteByte((byte)ServerPacketHeader.TRAITOR_MESSAGE); msg.WriteByte((byte)ev.CurrentState); msg.WriteIdentifier(ev.Prefab.Identifier); server.SendTraitorMessage(msg, ev.Traitor); } } }