Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs
Eero 90962b2328 Refactor Item collections for thread safety and performance
Replaces static Item.ItemList and related collections with thread-safe data structures using ConcurrentDictionary and ImmutableHashSet. Adds thread-safe helpers for marking items for deconstruction and managing item lists. Updates all usages of Item.ItemList and DeconstructItems to use new APIs, improving performance and safety in multi-threaded contexts. Also refactors MeleeWeapon and Projectile impact queues to use ConcurrentQueue, and updates related logic throughout the codebase.
2025-12-28 03:57:04 +08:00

285 lines
11 KiB
C#

using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AbandonedOutpostMission : Mission
{
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
private readonly Identifier itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
protected const int HostagesKilledState = 5;
private readonly LocalizedString hostagesKilledMessage;
private const float EndDelay = 5.0f;
private float endTimer;
private readonly bool allowOrderingRescuees;
public override bool AllowRespawning => false;
public override bool AllowUndocking
{
get
{
if (GameMain.GameSession.GameMode is CampaignMode) { return true; }
return state > 0;
}
}
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State == 0)
{
return Targets.Select(t => (Prefab.SonarLabel, t.WorldPosition));
}
else
{
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag).Fallback(msgTag);
itemConfig = prefab.ConfigElement.GetChildElement("Items");
itemTag = prefab.ConfigElement.GetAttributeIdentifier("targetitem", Identifier.Empty);
}
protected override void StartMissionSpecific(Level level)
{
failed = false;
endTimer = 0.0f;
requireKill.Clear();
requireRescue.Clear();
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
InitItems(submarine);
if (!IsClient)
{
InitCharacters(submarine);
}
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
}
private void InitItems(Submarine submarine)
{
if (!itemTag.IsEmpty)
{
var itemsToDestroy = Item.ItemList.Where(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)).ToList();
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".",
contentPackage: Prefab.ContentPackage);
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
Identifier itemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is not ItemPrefab itemPrefab)
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found",
contentPackage: Prefab.ContentPackage);
continue;
}
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
spawnPoint ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 201), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
}
private int previousKillTargetsRemaining = -1;
private void TrackKillTargetCount()
{
if (requireKill.Count == 0) { return; }
if (previousKillTargetsRemaining == -1)
{
previousKillTargetsRemaining = requireKill.Count();
}
int killTargetsRemaining = requireKill.Count(c => !c.Removed && !c.IsDead && !(c.LockHands && c.Submarine == Submarine.MainSub));
// at least one of the targets have been eliminated
if (killTargetsRemaining < previousKillTargetsRemaining)
{
#if CLIENT
SteamTimelineManager.OnOutpostTargetEliminated(this);
#endif
}
previousKillTargetsRemaining = killTargetsRemaining;
}
protected override void UpdateMissionSpecific(float deltaTime)
{
TrackKillTargetCount();
if (State != HostagesKilledState)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
{
State = HostagesKilledState;
return;
}
}
else
{
endTimer += deltaTime;
if (endTimer > EndDelay)
{
#if SERVER
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
#endif
}
}
switch (state)
{
case 0:
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead || (c.LockHands && c.Submarine == Submarine.MainSub)) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
break;
#if SERVER
case 1:
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
protected override bool DetermineCompleted()
{
return State > 0 && State != HostagesKilledState;
}
protected override void EndMissionSpecific(bool completed)
{
failed = !completed && requireRescue.Any(r => r.Removed || r.IsDead);
}
protected override void InitCharacter(Character character, XElement element)
{
base.InitCharacter(character, element);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(character);
}
}
protected override Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
Character spawnedCharacter = base.LoadHuman(humanPrefab, element, submarine);
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
if (requiresRescue)
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager?.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController)
{
var order = OrderPrefab.Prefabs["fightintruders"]
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
// Overrides the team change set in the base method.
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
if (teamId != spawnedCharacter.TeamID)
{
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
}
return spawnedCharacter;
}
}
}