Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
Eero e167a34f32 Make entity lists thread-safe with copy-on-write wrappers
Replaced static entity lists (e.g., HullList, GapList, MapEntityList, etc.) with thread-safe copy-on-write wrappers to improve concurrency and prevent race conditions. Updated usages and related methods to support the new thread-safe collections, ensuring atomic operations and lock-free reads throughout the codebase.
2025-12-28 21:59:03 +08:00

1323 lines
56 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EventManager
{
public enum NetworkEventType
{
CONVERSATION,
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT,
MISSION,
UNLOCKPATH,
EVENTLOG,
EVENTOBJECTIVE,
}
[NetworkSerialize]
public readonly record struct NetEventLogEntry(Identifier EventPrefabId, Identifier LogEntryId, string Text) : INetSerializableStruct;
[NetworkSerialize]
public readonly record struct NetEventObjective(
EventObjectiveAction.SegmentActionType Type,
Identifier Identifier,
Identifier ObjectiveTag,
Identifier TextTag,
Identifier ParentObjectiveId,
bool CanBeCompleted) : INetSerializableStruct;
const float IntensityUpdateInterval = 5.0f;
const float CalculateDistanceTraveledInterval = 5.0f;
const int MaxEventHistory = 20;
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
//The "intensity" of the current situation (a value between 0.0 - 1.0).
//High when a disaster has struck, low when nothing special is going on.
private float currentIntensity;
//The exact intensity of the current situation, current intensity is lerped towards this value
private float targetIntensity;
//follows targetIntensity a bit faster than currentIntensity to prevent e.g. combat musing staying on very long after the monsters are dead
private float musicIntensity;
//How low the intensity has to be for an event to be triggered.
//Gradually increases with time, so additional problems can still appear eventually even if
//the sub is laying broken on the ocean floor or if the players are trying to abuse the system
//by intentionally keeping the intensity high by causing breaches, damaging themselves or such
private float eventThreshold = 0.2f;
//New events can't be triggered when the cooldown is active.
private float eventCoolDown;
private float intensityUpdateTimer;
private PathFinder pathFinder;
private float totalPathLength;
private float calculateDistanceTraveledTimer;
private float distanceTraveled;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterStrength;
public float CumulativeMonsterStrengthMain;
public float CumulativeMonsterStrengthRuins;
public float CumulativeMonsterStrengthWrecks;
public float CumulativeMonsterStrengthCaves;
private float roundDuration;
private bool isCrewAway;
//how long it takes after the crew returns for the event manager to resume normal operation
const float CrewAwayResetDelay = 60.0f;
private float crewAwayResetTimer;
private float crewAwayDuration;
private readonly List<EventSet> pendingEventSets = new List<EventSet>();
private readonly Dictionary<EventSet, List<Event>> selectedEvents = new Dictionary<EventSet, List<Event>>();
private readonly List<Event> activeEvents = new List<Event>();
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
#if DEBUG && SERVER
private DateTime nextIntensityLogTime;
#endif
private EventManagerSettings settings;
private readonly bool isClient;
public float CurrentIntensity
{
get { return currentIntensity; }
}
public float MusicIntensity
{
get { return musicIntensity; }
}
public IEnumerable<Event> ActiveEvents
{
get { return activeEvents; }
}
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
private readonly struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
public TimeStamp(Event e)
{
Event = e;
Time = Timing.TotalTime;
}
}
private readonly List<TimeStamp> timeStamps = new List<TimeStamp>();
public void AddTimeStamp(Event e) => timeStamps.Add(new TimeStamp(e));
public readonly EventLog EventLog = new EventLog();
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
}
public bool Enabled = true;
private MTRandom random;
public int RandomSeed { get; private set; }
public void StartRound(Level level)
{
this.level = level;
if (isClient) { return; }
timeStamps.Clear();
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
#if SERVER
MissionAction.ResetMissionsUnlockedThisRound();
UnlockPathAction.ResetPathsUnlockedThisRound();
#endif
pathFinder = new PathFinder(WayPoint.WayPointList.ToList(), false);
totalPathLength = 0.0f;
if (level != null)
{
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition));
totalPathLength = steeringPath.TotalLength;
}
SelectSettings();
if (level != null)
{
RandomSeed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
RandomSeed ^= ToolBox.IdentifierToInt(previousEvent);
}
}
random = new MTRandom(RandomSeed);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
//ensure that the sets that have been configured to be always selected get selected if there's any available
EventSet initialEventSet = null;
EventSet additiveSet = null;
var selectAlwaysEventSets = GetAllowedEventSets(EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign).Where(s => s.SelectAlways);
foreach (var eventSet in selectAlwaysEventSets)
{
if (eventSet.GetCommonness(level) <= 0.0f)
{
//you might be wondering why an event set would be configured to SelectAlways, but have a commonness of 0:
//the set might have a non-zero commonness in some other biome or level type, but not this one
continue;
}
if (eventSet.Additive)
{
additiveSet = eventSet;
}
else
{
if (initialEventSet == null)
{
initialEventSet = eventSet;
}
else //initial set already chosen, ignore this one
{
continue;
}
}
AddSet(eventSet);
}
if (initialEventSet == null)
{
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: random);
}
//we happened to choose an additive set as the initial one, choose an additive one too
if (initialEventSet != null && initialEventSet.Additive)
{
additiveSet = initialEventSet;
initialEventSet = SelectRandomEvents(
EventSet.Prefabs.Where(e => !e.Additive).ToList(),
requireCampaignSet: playingCampaign,
random: random);
}
if (initialEventSet != null) { AddSet(initialEventSet); }
if (additiveSet != null) { AddSet(additiveSet); }
void AddSet(EventSet eventSet)
{
if (pendingEventSets.Contains(eventSet)) { return; }
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
}
if (level?.LevelData != null)
{
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
{
var unlockPathEventPrefab = EventPrefab.GetUnlockPathEvent(level.LevelData.Biome.Identifier, level.StartLocation.Faction);
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance(RandomSeed);
activeEvents.Add(newEvent);
}
else
{
//if no event that unlocks the path can be found, unlock it automatically
level.StartLocation.Connections.ForEach(c => c.Locked = false);
}
}
if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null)
{
foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents)
{
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage);
}
else
{
var newEvent = eventPrefab.CreateInstance(RandomSeed);
ActivateEvent(newEvent);
}
}
}
}
RegisterNonRepeatableChildEvents(initialEventSet);
void RegisterNonRepeatableChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
if (eventSet.OncePerLevel)
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep.Identifier);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
{
RegisterNonRepeatableChildEvents(childSet);
}
}
}
while (QueuedEventsForNextRound.TryDequeue(out var id))
{
var eventPrefab = EventSet.GetEventPrefab(id) ?? EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(id)).GetRandomUnsynced();
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
continue;
}
var ev = eventPrefab.CreateInstance(RandomSeed);
if (ev != null)
{
QueuedEvents.Enqueue(ev);
}
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
eventsInitialized = false;
isCrewAway = false;
crewAwayDuration = 0.0f;
crewAwayResetTimer = 0.0f;
intensityUpdateTimer = 0.0f;
CalculateCurrentIntensity(0.0f);
currentIntensity = musicIntensity = targetIntensity;
eventCoolDown = 0.0f;
CumulativeMonsterStrengthMain = 0;
CumulativeMonsterStrengthRuins = 0;
CumulativeMonsterStrengthWrecks = 0;
CumulativeMonsterStrengthCaves = 0;
distanceTraveled = 0;
}
public void ActivateEvent(Event newEvent)
{
activeEvents.Add(newEvent);
newEvent.Init();
}
public void ClearEvents()
{
activeEvents.Clear();
}
private void SelectSettings()
{
if (!EventManagerSettings.Prefabs.Any())
{
throw new InvalidOperationException("Could not select EventManager settings (no settings loaded).");
}
var orderedByDifficulty = EventManagerSettings.OrderedByDifficulty.ToArray();
if (level == null)
{
#if CLIENT
if (GameMain.GameSession.GameMode is TestGameMode)
{
settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient);
if (settings != null)
{
eventThreshold = settings.DefaultEventThreshold;
}
return;
}
#endif
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
float extraDifficulty = 0;
if (GameMain.GameSession.Campaign?.Settings != null)
{
extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty;
}
float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100);
var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s =>
modifiedDifficulty >= s.MinLevelDifficulty &&
modifiedDifficulty <= s.MaxLevelDifficulty).ToArray();
if (suitableSettings.Length == 0)
{
DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")");
settings = orderedByDifficulty.GetRandom(Rand.RandSync.ServerAndClient);
}
else
{
settings = suitableSettings.GetRandom(Rand.RandSync.ServerAndClient);
}
if (settings != null)
{
eventThreshold = settings.DefaultEventThreshold;
}
}
public IEnumerable<ContentFile> GetFilesToPreload()
{
foreach (List<Event> eventList in selectedEvents.Values)
{
foreach (Event ev in eventList)
{
foreach (ContentFile contentFile in ev.GetFilesToPreload())
{
yield return contentFile;
}
}
}
}
public void PreloadContent(IEnumerable<ContentFile> contentFiles)
{
var filesToPreload = contentFiles.ToList();
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.WreckAI == null) { continue; }
if (!sub.WreckAI.Config.DefensiveAgent.IsEmpty)
{
var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent);
if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath))
{
filesToPreload.Add(prefab.ContentFile);
}
}
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) { continue; }
foreach (Items.Components.ItemComponent component in item.Components)
{
if (component.statusEffectLists == null) { continue; }
foreach (var statusEffectList in component.statusEffectLists.Values)
{
foreach (StatusEffect statusEffect in statusEffectList)
{
foreach (var spawnInfo in statusEffect.SpawnCharacters)
{
var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName);
if (prefab != null && !filesToPreload.Contains(prefab.ContentFile))
{
filesToPreload.Add(prefab.ContentFile);
}
}
}
}
}
}
}
foreach (ContentFile file in filesToPreload)
{
file.Preload(preloadedSprites.Add);
}
}
public void TriggerOnEndRoundActions()
{
foreach (var ev in activeEvents)
{
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
}
}
public void EndRound()
{
pendingEventSets.Clear();
selectedEvents.Clear();
activeEvents.Clear();
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
timeStamps.Clear();
pathFinder = null;
}
/// <summary>
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
/// </summary>
public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false)
{
if (level?.LevelData == null) { return; }
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (registerFinishedOnly)
{
foreach (var finishedEvent in finishedEvents)
{
EventSet parentSet = finishedEvent.ParentSet;
if (parentSet == null) { continue; }
if (parentSet.Exhaustible)
{
level.LevelData.ExhaustEventSet(parentSet);
}
if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
{
level.LevelData.FinishedEvents[parentSet] += 1;
}
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values
.SelectMany(v => v)
.Select(e => e.Prefab.Identifier)
.Where(eventId => Register(eventId) && !level.LevelData.EventHistory.Contains(eventId)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(eventId => Register(eventId) && !level.LevelData.NonRepeatableEvents.Contains(eventId)));
if (!registerFinishedOnly)
{
level.LevelData.FinishedEvents.Clear();
}
bool Register(Identifier eventId) => !registerFinishedOnly || finishedEvents.Any(fe => fe.Prefab.Identifier == eventId);
}
public void SkipEventCooldown()
{
eventCoolDown = 0.0f;
}
private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
{
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.Identifier)) { return 0.0f; }
float retVal = baseCommonness;
if (level.LevelData.EventHistory.Contains(eventPrefab.Identifier)) { retVal *= 0.1f; }
return retVal;
}
private void CreateEvents(EventSet eventSet)
{
selectedEvents.Remove(eventSet);
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
{
applyCount = level.Ruins.Count;
foreach (var ruin in level.Ruins)
{
spawnPosFilter.Add(pos => pos.Ruin == ruin);
}
}
else if (eventSet.PerCave)
{
applyCount = level.Caves.Count;
foreach (var cave in level.Caves)
{
spawnPosFilter.Add(pos => pos.Cave == cave);
}
}
else if (eventSet.PerWreck)
{
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add(pos => pos.Submarine == wreck);
}
}
foreach (var subEventPrefab in eventSet.EventPrefabs)
{
foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers())
{
DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".",
contentPackage: eventSet.ContentPackage);
}
}
var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
e => IsFactionSuitable(e.Faction, level) && e.EventPrefabs.Any(ep => IsSuitable(ep, level))).ToArray();
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
if (suitablePrefabSubsets.Any())
{
var unusedEvents = suitablePrefabSubsets.ToList();
int eventCount = eventSet.GetEventCount(level);
for (int j = 0; j < eventCount; j++)
{
if (unusedEvents.All(e => e.EventPrefabs.All(p => CalculateCommonness(p, e.Commonness) <= 0.0f))) { break; }
EventSet.SubEventPrefab subEventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, e => e.EventPrefabs.Max(p => CalculateCommonness(p, e.Commonness)), random);
(IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) = subEventPrefab;
if (eventPrefabs != null && random.NextDouble() <= probability)
{
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance(RandomSeed);
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
unusedEvents.Remove(subEventPrefab);
}
}
}
if (eventSet.ChildSets.Any())
{
int setCount = eventSet.SubSetCount;
if (setCount > 1)
{
var unusedSets = eventSet.ChildSets.ToList();
for (int j = 0; j < setCount; j++)
{
var newEventSet = SelectRandomEvents(unusedSets, random: random);
if (newEventSet == null) { break; }
unusedSets.Remove(newEventSet);
CreateEvents(newEventSet);
}
}
else
{
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: random);
if (newEventSet != null)
{
CreateEvents(newEventSet);
}
}
}
}
else
{
foreach ((IEnumerable<EventPrefab> eventPrefabs, float commonness, float probability) in suitablePrefabSubsets)
{
if (random.NextDouble() > probability) { continue; }
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(e => IsSuitable(e, level)), e => e.Commonness, random);
var newEvent = eventPrefab.CreateInstance(RandomSeed);
if (newEvent == null) { continue; }
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
if (!selectedEvents.ContainsKey(eventSet))
{
selectedEvents.Add(eventSet, new List<Event>());
}
selectedEvents[eventSet].Add(newEvent);
}
var location = GetEventLocation();
foreach (EventSet childEventSet in eventSet.ChildSets)
{
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (!IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet);
}
}
}
}
private IEnumerable<EventSet> GetAllowedEventSets(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null)
{
if (level == null) { return Enumerable.Empty<EventSet>(); }
var allowedEventSets =
eventSets.Where(set => IsValidForLevel(set, level));
if (requireCampaignSet.HasValue)
{
if (requireCampaignSet.Value)
{
if (allowedEventSets.Any(es => es.IsCampaignSet))
{
allowedEventSets =
allowedEventSets.Where(es => es.IsCampaignSet);
}
else
{
DebugConsole.AddWarning("No campaign event sets available. Using a non-campaign-specific set instead.");
}
}
else
{
allowedEventSets =
allowedEventSets.Where(es => !es.IsCampaignSet);
}
}
var location = GetEventLocation();
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
allowedEventSets = allowedEventSets.Where(set => !set.CampaignTutorialOnly ||
(GameMain.IsSingleplayer && GameMain.GameSession?.Campaign?.Settings is { TutorialEnabled: true }));
int? discoveryIndex = GameMain.GameSession?.Map?.GetDiscoveryIndex(location);
int? visitIndex = GameMain.GameSession?.Map?.GetVisitIndex(location);
if (discoveryIndex is not null && discoveryIndex >= 0 && allowedEventSets.Any(set => set.ForceAtDiscoveredNr == discoveryIndex))
{
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr == discoveryIndex);
}
else if (visitIndex is not null && visitIndex >= 0 && allowedEventSets.Any(set => set.ForceAtVisitedNr == visitIndex))
{
allowedEventSets = allowedEventSets.Where(set => set.ForceAtVisitedNr == visitIndex);
}
else
{
// When there are no forced sets, only allow sets that aren't forced at any specific location
allowedEventSets = allowedEventSets.Where(set => set.ForceAtDiscoveredNr < 0 && set.ForceAtVisitedNr < 0);
}
return allowedEventSets;
}
private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
{
var allowedEventSets = GetAllowedEventSets(eventSets, requireCampaignSet);
if (allowedEventSets.Count() == 1)
{
// When there's only a single set available, just select it directly
return allowedEventSets.First();
}
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
float randomNumber = (float)rand.NextDouble();
randomNumber *= totalCommonness;
foreach (EventSet eventSet in allowedEventSets)
{
float commonness = eventSet.GetCommonness(level);
if (randomNumber <= commonness)
{
return eventSet;
}
randomNumber -= commonness;
}
return null;
}
public static bool IsSuitable(EventPrefab e, Level level)
{
return IsLevelSuitable(e, level) && IsFactionSuitable(e.Faction, level);
}
public static bool IsLevelSuitable(EventPrefab e, Level level)
{
return
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
(e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) &&
(e.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(e.RequiredSpawnPointTag))) &&
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
}
private static bool IsFactionSuitable(Identifier factionId, Level level)
{
return factionId.IsEmpty || factionId == level.StartLocation?.Faction?.Prefab.Identifier || factionId == level.StartLocation?.SecondaryFaction?.Prefab.Identifier;
}
private static bool IsValidForLevel(EventSet eventSet, Level level)
{
return
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
eventSet.LevelType.HasFlag(level.LevelData.Type) &&
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
(eventSet.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(eventSet.RequiredSpawnPointTag))) &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
}
private bool IsValidForLocation(EventSet eventSet, Location location)
{
if (location is null) { return true; }
if (!eventSet.Faction.IsEmpty)
{
if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
}
var locationType = location.GetLocationTypeToDisplay();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
if (eventSet.LocationTypeIdentifiers == null) { return false; }
// EventLocationType is used to have the event set consider the location id as something else, for example "city" to get events that go to city locations
bool hasMatchingEventLocationId = !locationType.EventLocationType.IsEmpty &&
eventSet.LocationTypeIdentifiers.Contains(locationType.EventLocationType);
bool hasMatchingLocationId = eventSet.LocationTypeIdentifiers.Contains(locationType.Identifier);
return hasMatchingEventLocationId || hasMatchingLocationId;
}
private Location GetEventLocation()
{
return GameMain.GameSession?.Campaign?.Map?.CurrentLocation ?? level?.StartLocation;
}
private bool CanStartEventSet(EventSet eventSet)
{
if (!eventSet.AllowAtStart)
{
ISpatialEntity refEntity = GetRefEntity();
float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
if (distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f)
{
return false;
}
}
if (eventSet.DelayWhenCrewAway)
{
if ((isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) || crewAwayResetTimer > 0.0f)
{
return false;
}
}
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
roundDuration < eventSet.MinMissionTime)
{
return false;
}
if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
{
return false;
}
return true;
}
private bool eventsInitialized;
public void Update(float deltaTime)
{
if (!Enabled) { return; }
if (GameMain.GameSession.Campaign?.DisableEvents ?? false) { return; }
if (!eventsInitialized)
{
foreach (var eventSet in selectedEvents.Keys)
{
foreach (var ev in selectedEvents[eventSet])
{
ev.Init(eventSet);
}
}
eventsInitialized = true;
}
//clients only calculate the intensity but don't create any events
//(the intensity is used for controlling the background music)
CalculateCurrentIntensity(deltaTime);
#if DEBUG && SERVER
if (DateTime.Now > nextIntensityLogTime)
{
DebugConsole.NewMessage("EventManager intensity: " + (int)Math.Round(currentIntensity * 100) + " %");
nextIntensityLogTime = DateTime.Now + new TimeSpan(0, minutes: 1, seconds: 0);
}
#endif
if (isClient) { return; }
roundDuration += deltaTime;
if (settings == null)
{
DebugConsole.ThrowError("Event settings not set before updating EventManager. Attempting to select...");
SelectSettings();
if (settings == null)
{
DebugConsole.ThrowError("Could not select EventManager settings. Disabling EventManager for the round...");
#if SERVER
GameMain.Server?.SendChatMessage("Could not select EventManager settings. Disabling EventManager for the round...", Networking.ChatMessageType.Error);
#endif
Enabled = false;
return;
}
}
if (IsCrewAway())
{
isCrewAway = true;
crewAwayResetTimer = CrewAwayResetDelay;
crewAwayDuration += deltaTime;
}
else if (crewAwayResetTimer > 0.0f)
{
isCrewAway = false;
crewAwayResetTimer -= deltaTime;
}
else
{
isCrewAway = false;
crewAwayDuration = 0.0f;
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventThreshold = Math.Min(eventThreshold, 1.0f);
eventCoolDown -= deltaTime;
}
calculateDistanceTraveledTimer -= deltaTime;
if (calculateDistanceTraveledTimer <= 0.0f)
{
distanceTraveled = CalculateDistanceTraveled();
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
}
bool recheck = false;
do
{
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
{
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown))
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in selectedEvents[eventSet])
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
};
}
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
} while (recheck);
foreach (Event ev in activeEvents)
{
if (!ev.IsFinished)
{
ev.Update(deltaTime);
}
else if (ev.Prefab != null && !finishedEvents.Any(e => e.Prefab == ev.Prefab))
{
if (level?.LevelData != null && level.LevelData.Type == LevelData.LevelType.Outpost)
{
if (!level.LevelData.EventHistory.Contains(ev.Prefab.Identifier)) { level.LevelData.EventHistory.Add(ev.Prefab.Identifier); }
}
finishedEvents.Add(ev);
}
}
if (QueuedEvents.Count > 0)
{
activeEvents.Add(QueuedEvents.Dequeue());
}
}
public void EntitySpawned(Entity entity)
{
foreach (var ev in activeEvents)
{
if (ev is ScriptedEvent scriptedEvent)
{
scriptedEvent.EntitySpawned(entity);
}
}
}
private void CalculateCurrentIntensity(float deltaTime)
{
intensityUpdateTimer -= deltaTime;
if (intensityUpdateTimer > 0.0f) { return; }
intensityUpdateTimer = IntensityUpdateInterval;
// crew health --------------------------------------------------------
avgCrewHealth = 0.0f;
int characterCount = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.TeamID == CharacterTeamType.FriendlyNPC) { continue; }
if (character.AIController is HumanAIController || character.IsRemotePlayer)
{
avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
characterCount++;
}
}
if (characterCount > 0)
{
avgCrewHealth /= characterCount;
}
else
{
avgCrewHealth = 0.5f;
}
// enemy amount --------------------------------------------------------
enemyDanger = 0.0f;
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; }
if (character.AIController is EnemyAIController enemyAI)
{
if (!enemyAI.AIParams.StayInAbyss)
{
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
monsterStrength += enemyAI.CombatStrength;
}
if (Submarine.MainSub != null &&
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.Player } &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
// Enemy outside targeting the sub or something in it
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
enemyDanger += enemyAI.CombatStrength / 5000.0f;
}
}
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
{
if (character.Submarine != null && Submarine.MainSub != null &&
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
{
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
//so let's just go with a fixed value.
//5 living enemy characters in an enemy sub in sonar range is enough to bump the intensity to max
enemyDanger += 0.2f;
}
}
}
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
// And if they get inside, we add 0.1 per crawler on that.
// So, in practice the danger per enemy that is attacking the sub is half of what it would be when the enemy is not targeting the sub.
// 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside).
// 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
// 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
// 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
enemyDanger += monsterStrength / 5000f;
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
// The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
// Some examples that result in the max intensity even when the creatures would just idle around.
// The values are theoretical, because in practice many of the monsters are targeting the sub, which will double the danger of those monster and effectively halve the max monster count.
// In practice we don't use the max intensity. For example on level 50 we use max intensity 50, which would mean that we'd halve the numbers below.
// There's no hard cap for the monster count, but if the amount of monsters is higher than this, we don't spawn more monsters from the events:
// 50 Crawlers (We shouldn't actually ever spawn that many. 12 is the max per event, but theoretically 25 crawlers would result in max intensity).
// 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity).
// 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity).
// 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity).
// hull status (gaps, flooding, fire) --------------------------------------------------------
float holeCount = 0.0f;
float waterAmount = 0.0f;
float dryHullVolume = 0.0f;
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (GameMain.GameSession?.GameMode is PvPMode)
{
if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; }
}
else
{
if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; }
}
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
if (hull.IsWetRoom) { continue; }
foreach (Gap gap in hull.ConnectedGaps)
{
if (!gap.IsRoomToRoom)
{
holeCount += gap.Open;
}
}
waterAmount += hull.WaterVolume;
dryHullVolume += hull.Volume;
}
if (dryHullVolume > 0)
{
floodingAmount = waterAmount / dryHullVolume;
}
//hull integrity at 0.0 if there are 10 or more wide-open holes
avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f);
//a fire of any size bumps up the fire amount to 20%
//if the total width of the fires is 1000 or more, the fire amount is considered to be at 100%
fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f);
//flooding less than 10% of the sub is ignored
//to prevent ballast tanks from affecting the intensity
if (floodingAmount < 0.1f)
{
floodingAmount = 0.0f;
}
else
{
floodingAmount *= 1.5f;
}
// calculate final intensity --------------------------------------------------------
targetIntensity =
((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f;
targetIntensity += fireAmount * 0.5f;
targetIntensity += enemyDanger;
targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f);
if (targetIntensity > currentIntensity)
{
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 0.0 to 1.0
musicIntensity = Math.Min(musicIntensity + 0.05f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
//20 seconds for intensity to go from 1.0 to 0.0
musicIntensity = Math.Max(musicIntensity - 0.05f * IntensityUpdateInterval, targetIntensity);
}
}
private float CalculateDistanceTraveled()
{
if (level == null || pathFinder == null) { return 0.0f; }
var refEntity = GetRefEntity();
if (refEntity == null) { return 0.0f; }
Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength))
{
//use horizontal position in the level as a fallback if a path can't be found
return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
}
else
{
return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f);
}
}
/// <summary>
/// Get the entity that should be used in determining how far the player has progressed in the level.
/// = The submarine or player character that has progressed the furthest.
/// </summary>
public static ISpatialEntity GetRefEntity(bool acceptRemoteControlledSubs = false)
{
ISpatialEntity refEntity = Submarine.MainSub;
#if CLIENT
if (Character.Controlled != null)
{
if (Character.Controlled.Submarine is { Info.Type: SubmarineType.Player } playerSub)
{
GetRefSubForCharacter(Character.Controlled);
}
else
{
refEntity = Character.Controlled;
}
}
#else
if (refEntity == null) { return null; }
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == null) { continue; }
GetRefSubForCharacter(client.Character);
}
#endif
void GetRefSubForCharacter(Character character)
{
if (character.Submarine is { Info.Type: SubmarineType.Player } playerSub)
{
if (playerSub.WorldPosition.X > refEntity.WorldPosition.X)
{
refEntity = playerSub;
}
}
if (acceptRemoteControlledSubs)
{
if (character.ViewTarget?.Submarine is { Info.Type: SubmarineType.Player } viewedSub)
{
if (viewedSub.WorldPosition.X > refEntity.WorldPosition.X)
{
refEntity = viewedSub;
}
}
if (character.SelectedItem?.GetComponent<Steering>()?.ControlledSub is { } controlledSub)
{
if (controlledSub.WorldPosition.X > refEntity.WorldPosition.X)
{
refEntity = controlledSub;
}
}
}
}
return refEntity;
}
private bool IsCrewAway()
{
#if CLIENT
return Character.Controlled != null && IsCharacterAway(Character.Controlled);
#else
int playerCount = 0;
int awayPlayerCount = 0;
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == null || client.Character.IsDead || client.Character.IsIncapacitated) { continue; }
playerCount++;
if (IsCharacterAway(client.Character)) { awayPlayerCount++; }
}
return playerCount > 0 && awayPlayerCount / (float)playerCount > 0.5f;
#endif
}
private bool IsCharacterAway(Character character)
{
if (character.Submarine != null)
{
switch (character.Submarine.Info.Type)
{
case SubmarineType.Player:
case SubmarineType.Outpost:
case SubmarineType.OutpostModule:
return false;
case SubmarineType.Wreck:
case SubmarineType.BeaconStation:
case SubmarineType.Ruin:
return true;
}
}
const int maxDist = 1000;
if (level != null && !level.Removed)
{
foreach (var ruin in level.Ruins)
{
Rectangle area = ruin.Area;
area.Inflate(maxDist, maxDist);
if (area.Contains(character.WorldPosition)) { return true; }
}
foreach (var cave in level.Caves)
{
Rectangle area = cave.Area;
area.Inflate(maxDist, maxDist);
if (area.Contains(character.WorldPosition)) { return true; }
}
}
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.BeaconStation && sub.Info.Type != SubmarineType.Wreck) { continue; }
Rectangle worldBorders = new Rectangle(
sub.Borders.X + (int)sub.WorldPosition.X - maxDist,
sub.Borders.Y + (int)sub.WorldPosition.Y + maxDist,
sub.Borders.Width + maxDist * 2,
sub.Borders.Height + maxDist * 2);
if (Submarine.RectContains(worldBorders, character.WorldPosition))
{
return true;
}
}
return false;
}
public void Load(XElement element)
{
foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty<Identifier>()))
{
QueuedEventsForNextRound.Enqueue(id);
}
}
public XElement Save()
{
return new XElement("eventmanager",
new XAttribute(nameof(QueuedEventsForNextRound),
string.Join(',', QueuedEventsForNextRound)));
}
}
}