Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
Eero 7b8275100d Improve thread safety and performance in core systems
Refactors event, entity, and physics management to use thread-safe and lock-free data structures (Immutable collections, ConcurrentQueue, ConcurrentDictionary, Channel) for improved concurrency and performance. Replaces O(n) queue lookups with O(1) set/dictionary checks, ensures atomic updates for shared state, and optimizes queue draining and deferred action processing. Updates related code to use new APIs and patterns, and adds documentation for thread safety and workflow.
2025-12-29 16:47:10 +08:00

1397 lines
60 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
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 volatile ImmutableList<Sprite> _preloadedSprites = ImmutableList<Sprite>.Empty;
//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;
// volatile + ImmutableCollections
private volatile ImmutableList<EventSet> _pendingEventSets = ImmutableList<EventSet>.Empty;
private volatile ImmutableDictionary<EventSet, ImmutableList<Event>> _selectedEvents =
ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private volatile ImmutableList<Event> _activeEvents = ImmutableList<Event>.Empty;
private volatile ImmutableHashSet<Event> _finishedEvents = ImmutableHashSet<Event>.Empty;
private volatile ImmutableHashSet<Identifier> _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
private volatile ImmutableQueue<Action> _deferredActions = ImmutableQueue<Action>.Empty;
#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 ConcurrentQueue<Event> QueuedEvents = new ConcurrentQueue<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 volatile ImmutableList<TimeStamp> _timeStamps = ImmutableList<TimeStamp>.Empty;
public void AddTimeStamp(Event e) => AtomicUpdate(ref _timeStamps, list => list.Add(new TimeStamp(e)));
public readonly EventLog EventLog = new EventLog();
public EventManager()
{
isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
}
public bool Enabled = true;
private static T AtomicUpdate<T>(ref T location, Func<T, T> updateFunc) where T : class
{
T original, updated;
do
{
original = Volatile.Read(ref location);
updated = updateFunc(original);
} while (Interlocked.CompareExchange(ref location, updated, original) != original);
return updated;
}
// activeEvents
private void AddActiveEvent(Event ev) => AtomicUpdate(ref _activeEvents, list => list.Add(ev));
private void ClearActiveEvents() => _activeEvents = ImmutableList<Event>.Empty;
// pendingEventSets
private void AddPendingEventSet(EventSet eventSet) =>
AtomicUpdate(ref _pendingEventSets, list => list.Contains(eventSet) ? list : list.Add(eventSet));
private void RemovePendingEventSetAt(int index) =>
AtomicUpdate(ref _pendingEventSets, list => index < list.Count ? list.RemoveAt(index) : list);
private void ClearPendingEventSets() => _pendingEventSets = ImmutableList<EventSet>.Empty;
// selectedEvents
private void AddSelectedEvent(EventSet eventSet, Event ev) =>
AtomicUpdate(ref _selectedEvents, dict =>
{
var currentList = dict.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
return dict.SetItem(eventSet, currentList.Add(ev));
});
private void RemoveSelectedEventSet(EventSet eventSet) =>
AtomicUpdate(ref _selectedEvents, dict => dict.Remove(eventSet));
private void ClearSelectedEvents() =>
_selectedEvents = ImmutableDictionary<EventSet, ImmutableList<Event>>.Empty;
private ImmutableList<Event> GetSelectedEvents(EventSet eventSet) =>
_selectedEvents.GetValueOrDefault(eventSet, ImmutableList<Event>.Empty);
private bool HasSelectedEvents(EventSet eventSet) => _selectedEvents.ContainsKey(eventSet);
// finishedEvents
private void AddFinishedEvent(Event ev) => AtomicUpdate(ref _finishedEvents, set => set.Add(ev));
private void ClearFinishedEvents() => _finishedEvents = ImmutableHashSet<Event>.Empty;
private bool IsEventFinished(Event ev) => _finishedEvents.Contains(ev);
// nonRepeatableEvents
private void AddNonRepeatableEvent(Identifier id) => AtomicUpdate(ref _nonRepeatableEvents, set => set.Add(id));
private void ClearNonRepeatableEvents() => _nonRepeatableEvents = ImmutableHashSet<Identifier>.Empty;
// preloadedSprites
private void AddPreloadedSprite(Sprite sprite) => AtomicUpdate(ref _preloadedSprites, list => list.Add(sprite));
private void ClearPreloadedSprites()
{
var sprites = Interlocked.Exchange(ref _preloadedSprites, ImmutableList<Sprite>.Empty);
foreach (var s in sprites) { s.Remove(); }
}
// timeStamps
private void ClearTimeStamps() => _timeStamps = ImmutableList<TimeStamp>.Empty;
private void EnqueueDeferredAction(Action action) =>
AtomicUpdate(ref _deferredActions, queue => queue.Enqueue(action));
private void ProcessDeferredActions()
{
var actions = Interlocked.Exchange(ref _deferredActions, ImmutableQueue<Action>.Empty);
foreach (var action in actions) { action(); }
}
private MTRandom random;
public int RandomSeed { get; private set; }
public void StartRound(Level level)
{
this.level = level;
if (isClient) { return; }
ClearTimeStamps();
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
#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; }
AddPendingEventSet(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);
AddActiveEvent(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))
{
AddNonRepeatableEvent(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)
{
AddActiveEvent(newEvent);
newEvent.Init();
}
public void ClearEvents()
{
ClearActiveEvents();
}
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()
{
var snapshot = _selectedEvents;
foreach (ImmutableList<Event> eventList in snapshot.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(AddPreloadedSprite);
}
}
public void TriggerOnEndRoundActions()
{
foreach (var ev in _activeEvents)
{
(ev as ScriptedEvent)?.OnRoundEndAction?.Update(1.0f);
}
}
public void EndRound()
{
ClearPendingEventSets();
ClearSelectedEvents();
ClearActiveEvents();
while (QueuedEvents.TryDequeue(out _)) { } // 清空 ConcurrentQueue
ClearFinishedEvents();
ClearNonRepeatableEvents();
ClearPreloadedSprites();
ClearTimeStamps();
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)
{
RemoveSelectedEventSet(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);
AddSelectedEvent(eventSet, 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]; }
AddSelectedEvent(eventSet, 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)
{
var selectedSnapshot = _selectedEvents;
foreach (var eventSet in selectedSnapshot.Keys)
{
foreach (var ev in selectedSnapshot[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
var pendingSnapshot = _pendingEventSets;
for (int i = pendingSnapshot.Count - 1; i >= 0; i--)
{
var eventSet = pendingSnapshot[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (currentIntensity > eventThreshold && !eventSet.IgnoreIntensity) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
RemovePendingEventSetAt(i);
var selectedEventsList = GetSelectedEvents(eventSet);
if (selectedEventsList.Count > 0)
{
//start events in this set
foreach (Event ev in selectedEventsList)
{
AddActiveEvent(ev);
eventThreshold = settings.DefaultEventThreshold;
if (eventSet.TriggerEventCooldown && selectedEventsList.Any(e => e.Prefab.TriggerEventCooldown))
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
EnqueueDeferredAction(() =>
{
AddPendingEventSet(eventSet);
CreateEvents(eventSet);
foreach (Event newEvent in GetSelectedEvents(eventSet))
{
if (!newEvent.Initialized) { newEvent.Init(eventSet); }
}
});
};
}
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
AddPendingEventSet(childEventSet);
recheck = true;
}
}
} while (recheck);
var activeSnapshot = _activeEvents;
foreach (Event ev in activeSnapshot)
{
if (!ev.IsFinished)
{
ev.Update(deltaTime);
}
else if (ev.Prefab != null && !IsEventFinished(ev))
{
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); }
}
AddFinishedEvent(ev);
}
}
if (QueuedEvents.TryDequeue(out var queuedEvent))
{
AddActiveEvent(queuedEvent);
}
ProcessDeferredActions();
}
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)));
}
}
}