v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -45,7 +45,6 @@ namespace Barotrauma
foreach (ContentXElement subElement in conditionalElements)
{
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
Conditionals = conditionalList.ToImmutableArray();
}
@@ -200,6 +200,10 @@ namespace Barotrauma
{
condition = $"{value1.ColorizeObject()} {Operator.ColorizeObject()} {value2.ColorizeObject()}";
}
else if (!Identifier.IsEmpty)
{
condition = $"{Identifier} {Condition}".ColorizeObject();
}
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckDataAction)} -> (Data: {Identifier.ColorizeObject()}, Success: {succeeded.ColorizeObject()}, Expression: {condition})";
}
@@ -66,13 +66,12 @@ namespace Barotrauma
public CheckItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers();
itemTags = ItemTags.Split(",").ToIdentifiers();
itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToArray();
itemTags = ItemTags.ToIdentifiers().ToArray();
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
{
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
conditionals = conditionalList;
@@ -113,13 +113,15 @@ namespace Barotrauma
Text = elem.GetAttributeString("tag", string.Empty);
textElement = elem;
}
}
if (element.GetChildElement("Replace") != null)
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"Replace\".",
contentPackage: element.ContentPackage);
else
{
string thisName = nameof(ConversationAction);
DebugConsole.ThrowError(
$"Error in {thisName} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"{elem.Name}\". If it's an action intended to execute after the {thisName}, " +
$"it should be after the {thisName}, not inside it.",
contentPackage: element.ContentPackage);
}
}
}
@@ -245,7 +247,17 @@ namespace Barotrauma
public int[] GetEndingOptions()
{
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
List<int> endings = Options
.Where(group =>
group.EndConversation ||
//no actions = safe to assume this must end the conversation
!group.Actions.Any() ||
//no follow-up conversation and a goto makes the event jump somewhere else
//we cannot easily determine whether that goto will lead to a follow-up conversation,
//so it's safest to close this conversation to prevent it from getting stuck (the potential follow-up will open a new one)
(group.Actions.None(a => a is ConversationAction) && group.Actions.Any(a => a is GoTo { EndConversation: true })))
.Select(group => Options.IndexOf(group))
.ToList();
if (!ContinueConversation) { endings.Add(-1); }
return endings.ToArray();
}
@@ -11,6 +11,10 @@ namespace Barotrauma
{
public string Text;
public List<EventAction> Actions;
/// <summary>
/// Should this option end the conversation (closing the conversation prompt?). By default, options that don't have any actions inside them, or that only have a GoTo action, end the conversation.
/// But if there are other actions inside the option, the game assumes there may be some kind of a follow-up coming to the conversation, and by default leaves it open.
/// </summary>
public bool EndConversation;
private int currentSubAction = 0;
@@ -1,4 +1,4 @@
namespace Barotrauma
namespace Barotrauma
{
/// <summary>
/// Makes the event jump to a <see cref="Label"/> somewhere else in the event.
@@ -11,6 +11,9 @@ namespace Barotrauma
[Serialize(-1, IsPropertySaveable.Yes, description: "How many times can this GoTo action be repeated? Can be used to make some parts of an event repeat a limited number of times. If negative or zero, there's no limit.")]
public int MaxTimes { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "By default, jumping to another part in the event closes the active conversation prompt. Use this if if you want to keep it open instead.")]
public bool EndConversation { get; set; }
private int counter;
public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -213,7 +213,7 @@ namespace Barotrauma
return false;
}
}
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains("AnyOutpost".ToIdentifier())))
if (!locationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && locationTypes.Contains(Tags.AnyOutpost)))
{
return false;
}
@@ -1,4 +1,4 @@
namespace Barotrauma
namespace Barotrauma
{
/// <summary>
@@ -31,6 +31,11 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
contentPackage: element.ContentPackage);
}
if (Operation == OperationType.Add && State == 0)
{
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.",
contentPackage: element.ContentPackage);
}
}
public override bool IsFinished(ref string goTo)
@@ -55,7 +60,7 @@ namespace Barotrauma
mission.State = State;
break;
case OperationType.Add:
mission.State += 1;
mission.State += State;
break;
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -42,13 +42,13 @@ namespace Barotrauma
{
if (isFinished) { return; }
bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
bool isPlayerTeam = TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2;
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>().ToList();
foreach (Character npc in affectedNpcs)
{
// characters will still remain on friendlyNPC team for rest of the tick
npc.SetOriginalTeam(TeamID);
npc.SetOriginalTeamAndChangeTeam(TeamID);
foreach (Item item in npc.Inventory.AllItems)
{
var idCard = item.GetComponent<Items.Components.IdCard>();
@@ -61,26 +61,43 @@ namespace Barotrauma
}
}
}
if (AddToCrew && isPlayerTeam)
if (GameMain.GameSession.CrewManager is CrewManager crewManager)
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
ChangeItemTeam(Submarine.MainSub, true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (AddToCrew && isPlayerTeam)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
else if (RemoveFromCrew && (npc.TeamID == CharacterTeamType.Team1 || npc.TeamID == CharacterTeamType.Team2))
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.RemoveCharacter(npc, removeInfo: true);
var sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID);
ChangeItemTeam(sub, false);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
if (npc.Info is CharacterInfo info)
{
info.StartItemsGiven = true;
crewManager.AddCharacter(npc);
}
else
{
DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't add to the crew.");
}
ChangeItemTeam(Submarine.MainSub ?? Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID), allowStealing: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
else if (RemoveFromCrew && npc.TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2)
{
if (npc.Info is CharacterInfo info)
{
info.StartItemsGiven = true;
crewManager.RemoveCharacter(npc, removeInfo: true);
}
else
{
DebugConsole.AddWarning($"Attempted to change the team of a character ({npc.Name}) that doesn't have Character Info. Can't remove from the crew.");
}
Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID);
ChangeItemTeam(sub, false);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
}
}
}
void ChangeItemTeam(Submarine sub, bool allowStealing)
@@ -98,7 +115,7 @@ namespace Barotrauma
}
}
WayPoint subWaypoint =
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info.Job?.Prefab) ??
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human && wp.AssignedJob == npc.Info?.Job?.Prefab) ??
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human);
if (subWaypoint != null)
{
@@ -86,7 +86,7 @@ namespace Barotrauma
{
foreach (var objective in humanAiController.ObjectiveManager.Objectives)
{
if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.OperateTarget == target)
if (objective is AIObjectiveOperateItem operateItemObjective && operateItemObjective.Component.Item == target)
{
objective.Abandon = true;
}
@@ -115,7 +115,7 @@ namespace Barotrauma
if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; }
foreach (var operateItemObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveOperateItem>())
{
if (operateItemObjective.OperateTarget == target)
if (operateItemObjective.Component.Item == target)
{
operateItemObjective.Abandon = true;
}
@@ -25,7 +25,7 @@ namespace Barotrauma
{
ItemIdentifiers = element.GetAttributeString("itemidentifier", element.GetAttributeString("identifier", string.Empty));
}
itemIdentifierSplit = ItemIdentifiers.Split(',').ToIdentifiers().ToImmutableHashSet();
itemIdentifierSplit = ItemIdentifiers.ToIdentifiers().ToImmutableHashSet();
}
private bool isFinished = false;
@@ -17,6 +17,8 @@ namespace Barotrauma
MainSub,
Outpost,
MainPath,
Cave,
AbyssCave,
Ruin,
Wreck,
BeaconStation,
@@ -38,6 +40,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the item to spawn.")]
public Identifier ItemIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the item to spawn.")]
public Identifier ItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "The spawned entity will be assigned this tag. The tag can be used to refer to the entity by other actions of the event.")]
public Identifier TargetTag { get; set; }
@@ -67,6 +72,9 @@ namespace Barotrauma
[Serialize(1, IsPropertySaveable.Yes, description: "Number of entities to spawn.")]
public int Amount { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Should the item be spawned even if the target inventory is full (just spawning it at the position of the target)? Only valid if spawning an item in an inventory.")]
public bool SpawnIfInventoryFull { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Random offset to add to the spawn position.")]
public float Offset { get; set; }
@@ -94,6 +102,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "If disabled, the action will choose a spawn position away from players' views if one is available.")]
public bool AllowInPlayerView { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue even if the entity failed to spawn for whatever reason?")]
public bool ContinueIfFailedToSpawn { get; set; }
private bool spawned;
private Entity spawnedEntity;
@@ -115,9 +126,9 @@ namespace Barotrauma
public override bool IsFinished(ref string goTo)
{
if (spawnedEntity != null)
if (spawnedEntity != null || ContinueIfFailedToSpawn)
{
return true;
return spawned;
}
else
{
@@ -176,7 +187,11 @@ namespace Barotrauma
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = TeamID;
//don't set the TeamID directly: we want to leave the character's original team untouched,
//so they can behave offensively (and otherwise act "normally") if we spawn them in a hostile team inside a sub/outpost that doesn't belong to that team
//process the team change immediately in case the character is killed or made unconscious by the event (in which case the team change would not be processed)
newCharacter.SetOriginalTeamAndChangeTeam(TeamID, processImmediately: true);
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPos as WayPoint);
if (LootingIsStealing)
@@ -233,74 +248,82 @@ namespace Barotrauma
}
}
}
else if (!ItemIdentifier.IsEmpty)
else if (!ItemIdentifier.IsEmpty || !ItemTag.IsEmpty)
{
if (MapEntityPrefab.FindByIdentifier(ItemIdentifier) is not ItemPrefab itemPrefab)
ItemPrefab itemPrefab = null;
if (!ItemIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
else
{
Inventory spawnInventory = null;
if (!TargetInventory.IsEmpty)
itemPrefab = MapEntityPrefab.FindByIdentifier(ItemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
var targets = ParentEvent.GetTargets(TargetInventory);
if (targets.Any())
{
var target = targets.First(t => t is Item || t is Character);
if (target is Character character)
{
spawnInventory = character.Inventory;
}
else if (target is Item item)
{
spawnInventory = item.OwnInventory;
}
}
DebugConsole.ThrowError($"Error in SpawnAction (item prefab \"{ItemIdentifier}\" not found)",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
}
else if (!ItemTag.IsEmpty)
{
itemPrefab = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(ItemTag)).GetRandom(Rand.RandSync.Unsynced);
}
if (spawnInventory == null)
Inventory spawnInventory = null;
if (!TargetInventory.IsEmpty)
{
var targets = ParentEvent.GetTargets(TargetInventory);
if (targets.Any())
{
var target = targets.First(t => t is Item || t is Character);
if (target is Character character)
{
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
contentPackage: ParentEvent.Prefab.ContentPackage);
spawnInventory = character.Inventory;
}
else if (target is Item item)
{
spawnInventory = item.OwnInventory;
}
}
if (spawnInventory == null)
{
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
for (int i = 0; i < Amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
}
}
DebugConsole.ThrowError($"Could not spawn \"{ItemIdentifier}\" in target inventory \"{TargetInventory}\" - matching target not found.",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
else
}
if (spawnInventory == null)
{
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
for (int i = 0; i < Amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onSpawned);
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, Rand.Range(0.0f, Offset)), onSpawned: onSpawned);
}
}
void onSpawned(Item newItem)
{
if (newItem != null)
{
if (!TargetTag.IsEmpty)
{
ParentEvent.AddTarget(TargetTag, newItem);
}
if (IgnoreByAI)
{
newItem.AddTag("ignorebyai");
}
}
spawnedEntity = newItem;
}
}
else
{
for (int i = 0; i < Amount; i++)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, spawnInventory, spawnIfInventoryFull: SpawnIfInventoryFull, onSpawned: onSpawned);
}
}
void onSpawned(Item newItem)
{
if (newItem != null)
{
if (!TargetTag.IsEmpty)
{
ParentEvent.AddTarget(TargetTag, newItem);
}
if (IgnoreByAI)
{
newItem.AddTag("ignorebyai");
}
}
spawnedEntity = newItem;
}
}
spawned = true;
@@ -353,8 +376,7 @@ namespace Barotrauma
{
SpawnLocationType.Any => true,
SpawnLocationType.MainSub => submarine == Submarine.MainSub,
SpawnLocationType.NearMainSub => submarine == null,
SpawnLocationType.MainPath => submarine == null,
SpawnLocationType.NearMainSub or SpawnLocationType.MainPath or SpawnLocationType.Cave or SpawnLocationType.AbyssCave => submarine == null,
SpawnLocationType.Outpost => submarine is { Info.IsOutpost: true },
SpawnLocationType.Wreck => submarine is { Info.IsWreck: true },
SpawnLocationType.Ruin => submarine is { Info.IsRuin: true },
@@ -443,10 +465,25 @@ namespace Barotrauma
return potentialSpawnPoints.GetRandomUnsynced();
}
if (spawnLocation == SpawnLocationType.MainPath || spawnLocation == SpawnLocationType.NearMainSub)
switch (spawnLocation)
{
validSpawnPoints = validSpawnPoints.Where(p =>
Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
case SpawnLocationType.MainPath:
case SpawnLocationType.NearMainSub:
validSpawnPoints = validSpawnPoints.Where(p =>
Submarine.Loaded.None(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(p.WorldPosition)));
if (Level.Loaded != null)
{
validSpawnPoints = validSpawnPoints.Where(p =>
p.WorldPosition.Y > Level.Loaded.AbyssStart &&
p.Cave == null && p.Ruin == null);
}
break;
case SpawnLocationType.Cave:
validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y > Level.Loaded.AbyssStart && p.Cave != null);
break;
case SpawnLocationType.AbyssCave:
validSpawnPoints = validSpawnPoints.Where(p => p.WorldPosition.Y < Level.Loaded.AbyssStart && p.Cave != null);
break;
}
//avoid using waypoints if there's any actual spawnpoints available
@@ -11,9 +11,10 @@ namespace Barotrauma
/// </summary>
class TagAction : EventAction
{
public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 }
public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8, Enemy = 16, Ruin = 32 }
public enum CharacterTeam { Any = 0, None = 1, Team1 = 2, Team2 = 4, FriendlyNPC = 8 }
[Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag].")]
[Serialize("", IsPropertySaveable.Yes, description: "What criteria to use to select the entities to target. Valid values are players, player, traitor, nontraitor, nontraitorplayer, bot, crew, humanprefabidentifier:[id], jobidentifier:[id], structureidentifier:[id], structurespecialtag:[tag], itemidentifier:[id], itemtag:[tag], hull, hullname:[name], submarine:[type], eventtag:[tag], speciesname:[id].")]
public string Criteria { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "The tag to apply to the target.")]
@@ -22,6 +23,9 @@ namespace Barotrauma
[Serialize(SubType.Any, IsPropertySaveable.Yes, description: "The type of submarine the target needs to be in.")]
public SubType SubmarineType { get; set; }
[Serialize(CharacterTeam.Any, IsPropertySaveable.Yes, description: "The team the target needs to be on.")]
public CharacterTeam Team { get; set; }
[Serialize("", IsPropertySaveable.Yes, "If set, the target must be in an outpost module that has this tag.")]
public Identifier RequiredModuleTag { get; set; }
@@ -34,6 +38,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "If there are multiple matching targets, should all of them be tagged or one chosen randomly?")]
public bool ChooseRandom { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "If choosing a random target, targets with this tag can optionally be excluded.")]
public Identifier ChooseRandomExcludingTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the event continue if the TagAction can't find any valid targets?")]
public bool ContinueIfNoTargetsFound { get; set; }
@@ -78,6 +85,7 @@ namespace Barotrauma
("hullname", TagHullsByName),
("submarine", TagSubmarinesByType),
("eventtag", TagByEventTag),
("speciesname", TagBySpeciesName)
}.Select(t => (t.k.ToIdentifier(), t.v)).ToImmutableDictionary();
}
@@ -87,9 +95,16 @@ namespace Barotrauma
}
public override void Reset()
{
taggingDone = false;
cantFindTargets = false;
isFinished = false;
}
private void TagBySpeciesName(Identifier speciesName)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.SpeciesName == speciesName && CharacterTeamMatches(c)));
}
private void TagByEventTag(Identifier eventTag)
{
AddTarget(Tag, ParentEvent.GetTargets(eventTag).Where(t => MatchesRequirements(t)));
@@ -100,7 +115,7 @@ namespace Barotrauma
AddTargetPredicate(
Tag,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) && CharacterTeamMatches(c));
}
private void TagTraitors()
@@ -151,7 +166,7 @@ namespace Barotrauma
private void TagHumansByIdentifier(Identifier identifier)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier));
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier && CharacterTeamMatches(c)));
}
private void TagHumansByTag(Identifier tag)
@@ -161,7 +176,7 @@ namespace Barotrauma
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier)));
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier) && CharacterTeamMatches(c)));
}
private void TagStructuresByIdentifier(Identifier identifier)
@@ -233,7 +248,7 @@ namespace Barotrauma
private bool MatchesRequirements(Entity e)
{
return ModuleTagMatches(e) && SubmarineTypeMatches(e.Submarine);
return ModuleTagMatches(e) && SubmarineTypeMatches(e as Submarine ?? e.Submarine);
}
private bool ModuleTagMatches(Entity e)
@@ -267,6 +282,23 @@ namespace Barotrauma
return hull != null && hull.OutpostModuleTags.Contains(RequiredModuleTag);
}
private bool CharacterTeamMatches(Character character)
{
if (Team == CharacterTeam.Any) { return true; }
switch (Team)
{
case CharacterTeam.None:
return character.TeamID == CharacterTeamType.None;
case CharacterTeam.Team1:
return character.TeamID == CharacterTeamType.Team1;
case CharacterTeam.Team2:
return character.TeamID == CharacterTeamType.Team2;
case CharacterTeam.FriendlyNPC:
return character.TeamID == CharacterTeamType.FriendlyNPC;
default:
return false;
}
}
private bool SubmarineTypeMatches(Submarine sub)
{
@@ -280,7 +312,7 @@ namespace Barotrauma
switch (sub.Info.Type)
{
case Barotrauma.SubmarineType.Player:
return submarineType.HasFlag(SubType.Player) && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle;
return submarineType.HasFlag(SubType.Player) && !sub.IsRespawnShuttle;
case Barotrauma.SubmarineType.Outpost:
case Barotrauma.SubmarineType.OutpostModule:
return submarineType.HasFlag(SubType.Outpost);
@@ -288,6 +320,10 @@ namespace Barotrauma
return submarineType.HasFlag(SubType.Wreck);
case Barotrauma.SubmarineType.BeaconStation:
return submarineType.HasFlag(SubType.BeaconStation);
case Barotrauma.SubmarineType.EnemySubmarine:
return submarineType.HasFlag(SubType.Enemy);
case Barotrauma.SubmarineType.Ruin:
return submarineType.HasFlag(SubType.Ruin);
default:
return false;
}
@@ -357,6 +393,11 @@ namespace Barotrauma
private void TagRandom(Identifier tag, IEnumerable<Entity> entities)
{
if (!ChooseRandomExcludingTag.IsEmpty)
{
var excludedTargets = ParentEvent.GetTargets(ChooseRandomExcludingTag);
entities = entities.Except(excludedTargets);
}
if (entities.None())
{
cantFindTargets = true;
@@ -414,7 +455,7 @@ namespace Barotrauma
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()})";
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()}, Team: {Team.ColorizeObject()})";
}
}
}
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -56,7 +56,16 @@ namespace Barotrauma
private float distance;
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public TriggerAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (element.GetAttribute(nameof(TagAction.IgnoreIncapacitatedCharacters)) != null)
{
DebugConsole.AddWarning(
$"Potential error in {nameof(TriggerAction)}, event \"{parentEvent.Prefab.Identifier}\": "+
$"{nameof(TagAction.IgnoreIncapacitatedCharacters)} is a property of {nameof(TagAction)}, did you mean {nameof(DisableIfTargetIncapacitated)}?",
contentPackage: element.ContentPackage);
}
}
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
@@ -157,13 +166,13 @@ namespace Barotrauma
Item item = null;
if (e1 is Character char1)
{
if (char1.IsBot)
{
npc ??= char1;
if (char1.IsPlayer)
{
player = char1;
}
else
{
player = char1;
else
{
npc ??= char1;
}
}
else
@@ -172,13 +181,13 @@ namespace Barotrauma
}
if (e2 is Character char2)
{
if (char2.IsBot)
{
npc ??= char2;
}
else
if (char2.IsPlayer)
{
player = char2;
player = char2;
}
else
{
npc ??= char2;
}
}
else
@@ -190,6 +199,10 @@ namespace Barotrauma
{
if (npc != null)
{
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
{
npcsOrItems.Add(npc);
}
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Talk)
{
//if the NPC has a conversation available, don't assign the trigger until the conversation is done
@@ -197,10 +210,6 @@ namespace Barotrauma
}
else if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
{
if (!npcsOrItems.Any(n => n.TryGet(out Character npc2) && npc2 == npc))
{
npcsOrItems.Add(npc);
}
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated;
#if CLIENT
@@ -339,7 +348,7 @@ namespace Barotrauma
return true;
}
}
else if (c.AIController is EnemyAIController enemyAI && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack))
else if (c.AIController is EnemyAIController { State: AIState.Aggressive or AIState.Attack } enemyAI)
{
if (enemyAI.SelectedAiTarget?.Entity == character || c.CurrentHull == character.CurrentHull)
{
@@ -401,10 +410,18 @@ namespace Barotrauma
{
if (TargetModuleType.IsEmpty)
{
string targetStr = "none";
if (npcsOrItems.Any())
{
targetStr = string.Join(", ",
npcsOrItems.Select(npcOrItem =>
npcOrItem.TryGet(out Character character) ? character.Name : (npcOrItem.TryGet(out Item item) ? item.Name : "none")));
}
return
$"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
(WaitForInteraction ?
$"Selected non-player target: {(npcsOrItems?.ToString() ?? "<null>").ColorizeObject()}, " :
$"Selected non-player target: {targetStr.ColorizeObject()}, " :
$"Distance: {((int)distance).ColorizeObject()}, ") +
$"Radius: {Radius.ColorizeObject()}, " +
$"TargetTags: {Target1Tag.ColorizeObject()}, " +
@@ -1,13 +1,16 @@
namespace Barotrauma
{
/// <summary>
/// Triggers another scripted event.
/// Triggers another event (can also trigger things other than scripted events, for example monster events).
/// </summary>
class TriggerEventAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Identifier of the event to trigger.")]
public Identifier Identifier { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the event to trigger.")]
public Identifier EventTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the event will trigger at the beginning of the next round. Useful for e.g. triggering some scripted event in the outpost after you finish a mission.")]
public bool NextRound { get; set; }
@@ -36,13 +39,8 @@
}
else
{
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
else
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(Identifier, EventTag, ParentEvent.Prefab.ContentPackage);
if (eventPrefab != null)
{
var ev = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
if (ev != null)
@@ -180,11 +180,40 @@ namespace Barotrauma
random = new MTRandom(RandomSeed);
bool playingCampaign = GameMain.GameSession?.GameMode is CampaignMode;
EventSet initialEventSet = SelectRandomEvents(
EventSet.Prefabs.ToList(),
requireCampaignSet: playingCampaign,
random: random);
//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.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;
@@ -193,15 +222,15 @@ namespace Barotrauma
requireCampaignSet: playingCampaign,
random: random);
}
if (initialEventSet != null)
if (initialEventSet != null) { AddSet(initialEventSet); }
if (additiveSet != null) { AddSet(additiveSet); }
void AddSet(EventSet eventSet)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet);
}
if (additiveSet != null)
{
pendingEventSets.Add(additiveSet);
CreateEvents(additiveSet);
if (pendingEventSets.Contains(eventSet)) { return; }
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
}
if (level?.LevelData != null)
@@ -244,7 +273,7 @@ namespace Barotrauma
while (QueuedEventsForNextRound.TryDequeue(out var id))
{
var eventPrefab = EventSet.GetEventPrefab(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}.");
@@ -609,12 +638,11 @@ namespace Barotrauma
}
}
private EventSet SelectRandomEvents(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null, Random random = null)
private IEnumerable<EventSet> GetAllowedEventSets(IReadOnlyList<EventSet> eventSets, bool? requireCampaignSet = null)
{
if (level == null) { return null; }
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
if (level == null) { return Enumerable.Empty<EventSet>(); }
var allowedEventSets =
var allowedEventSets =
eventSets.Where(set => IsValidForLevel(set, level));
if (requireCampaignSet.HasValue)
@@ -659,6 +687,12 @@ namespace Barotrauma
// 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)
{
@@ -666,6 +700,7 @@ namespace Barotrauma
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;
@@ -692,6 +727,7 @@ namespace Barotrauma
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);
}
@@ -706,6 +742,7 @@ namespace Barotrauma
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
level.LevelData.Type == eventSet.LevelType &&
(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);
}
@@ -965,9 +1002,9 @@ namespace Barotrauma
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
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;
@@ -1128,6 +1165,8 @@ namespace Barotrauma
}
}
#else
if (refEntity == null) { return null; }
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == null) { continue; }
@@ -1,4 +1,6 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
@@ -11,6 +13,9 @@ namespace Barotrauma
public readonly ContentXElement ConfigElement;
public readonly Type EventType;
private readonly ImmutableHashSet<Identifier> tags;
public ImmutableHashSet<Identifier> Tags => tags;
/// <summary>
/// The probability for the event to do something if it gets selected. For example, the probability for a MonsterEvent to spawn the monster(s).
/// </summary>
@@ -37,6 +42,11 @@ namespace Barotrauma
/// </summary>
public readonly Identifier RequiredLayer;
/// <summary>
/// If set, this spawn point tag must be present somewhere in the level.
/// </summary>
public readonly Identifier RequiredSpawnPointTag;
/// <summary>
/// If set, the event set can only be chosen in locations that belong to this faction.
/// </summary>
@@ -93,6 +103,7 @@ namespace Barotrauma
Name = TextManager.Get($"eventname.{Identifier}").Fallback(Identifier.ToString());
tags = ConfigElement.GetAttributeIdentifierImmutableHashSet(nameof(tags), ImmutableHashSet<Identifier>.Empty);
BiomeIdentifier = ConfigElement.GetAttributeIdentifier("biome", Identifier.Empty);
Faction = ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
@@ -100,6 +111,7 @@ namespace Barotrauma
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty);
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
@@ -146,5 +158,39 @@ namespace Barotrauma
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == biomeIdentifier) ??
unlockPathEvents.FirstOrDefault(ep => ep.BiomeIdentifier == Identifier.Empty);
}
/// <summary>
/// Finds an event prefab with the specified identifier, or if it isn't defined, a random event prefab with the specified tag.
/// </summary>
/// <param name="source">Which content package is trying to find the event (if any)? Only used for logging error messages.</param>
/// <returns></returns>
public static EventPrefab FindEventPrefab(Identifier identifier, Identifier tag, ContentPackage source)
{
EventPrefab eventPrefab = null;
if (!identifier.IsEmpty)
{
eventPrefab = EventSet.GetEventPrefab(identifier);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Failed to find an event prefab with the identifier {identifier}.",
contentPackage: source);
}
}
else if (!tag.IsEmpty)
{
eventPrefab = EventSet.GetAllEventPrefabs().Where(e => e.Tags.Contains(tag)).GetRandomUnsynced();
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Failed to find an event prefab with the tag {tag}.",
contentPackage: source);
}
}
else
{
DebugConsole.ThrowError($"Failed to find an event prefab: neither an identifier or tag were defined.",
contentPackage: source);
}
return eventPrefab;
}
}
}
@@ -73,7 +73,7 @@ namespace Barotrauma
public static void AddSetEventPrefabsToList(List<EventPrefab> list, EventSet set)
{
list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs));
list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs).Where(ep => !list.Contains(ep)));
foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); }
}
@@ -111,6 +111,11 @@ namespace Barotrauma
/// </summary>
public readonly Identifier RequiredLayer;
/// <summary>
/// If set, this spawn point tag must be present somewhere in the level.
/// </summary>
public readonly Identifier RequiredSpawnPointTag;
/// <summary>
/// If set, the event set can only be chosen in locations of this type.
/// </summary>
@@ -207,7 +212,14 @@ namespace Barotrauma
/// monsters in addition to the vanilla monsters spawned by vanilla sets, without you having to add your custom monsters to every single vanilla set.
/// </summary>
public readonly bool Additive;
/// <summary>
/// This will force the game to always choose this event set if it's suitable for the current level.
/// If the set is additive, it is guaranteed to get chosen regardless of what other sets get selected.
/// If the set is NOT additive, the game will choose the first available non-additive set that is configured to be always selected.
/// </summary>
public readonly bool SelectAlways;
/// <summary>
/// The commonness of the event set (i.e. how likely it is for this specific set to be chosen).
/// </summary>
@@ -349,7 +361,8 @@ namespace Barotrauma
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
Additive = element.GetAttributeBool("additive", false);
Additive = element.GetAttributeBool(nameof(Additive), false);
SelectAlways = element.GetAttributeBool(nameof(SelectAlways), false);
string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection");
if (!Enum.TryParse(levelTypeStr, true, out LevelType))
@@ -392,6 +405,7 @@ namespace Barotrauma
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false);
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
RequiredSpawnPointTag = element.GetAttributeIdentifier(nameof(RequiredSpawnPointTag), Identifier.Empty);
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -27,9 +27,9 @@ namespace Barotrauma
private const float EndDelay = 5.0f;
private float endTimer;
private bool allowOrderingRescuees;
private readonly bool allowOrderingRescuees;
public override bool AllowRespawn => false;
public override bool AllowRespawning => false;
public override bool AllowUndocking
{
@@ -233,7 +233,18 @@ namespace Barotrauma
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
//consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
//that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
if (teamId != originalTeam)
{
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
}
if (element.GetAttribute("color") != null)
{
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
}
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
@@ -265,11 +276,7 @@ namespace Barotrauma
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
InitCharacter(spawnedCharacter, element);
}
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
@@ -280,10 +287,6 @@ namespace Barotrauma
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
if (spawnedCharacter.Inventory != null)
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
@@ -297,9 +300,31 @@ namespace Barotrauma
enemyAi.UnattackableSubmarines.Add(sub);
}
}
InitCharacter(spawnedCharacter, element);
}
private void InitCharacter(Character character, XElement element)
{
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(character);
}
float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
if (playDeadProbability >= 0)
{
character.EvaluatePlayDeadProbability(playDeadProbability);
}
float huskProbability = element.GetAttributeFloat("huskprobability", 0);
if (huskProbability > 0 && Rand.Value() <= huskProbability)
{
character.TurnIntoHusk();
}
else if (element.GetAttributeBool("corpse", false))
{
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (State != HostagesKilledState)
@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -10,12 +11,59 @@ namespace Barotrauma
private readonly LocalizedString[] descriptions;
private static LocalizedString[] teamNames = { "Team A", "Team B" };
public override bool AllowRespawn
private readonly bool allowRespawning;
enum WinCondition
{
get { return false; }
/// <summary>
/// The winner is the team with the last living player(s)
/// </summary>
LastManStanding,
/// <summary>
/// The team who reaches a specific number of kills (determined by WinScore) is the winner
/// </summary>
KillCount,
/// <summary>
/// The team who controls a specific submarine (can be a ruin, outpost or a beacon station too) for some time (determined by WinScore) is the winner
/// </summary>
ControlSubmarine
}
private CharacterTeamType Winner
private readonly WinCondition winCondition;
public override bool AllowRespawning
{
get => allowRespawning;
}
private Submarine targetSubmarine;
private LocalizedString targetSubmarineSonarLabel;
/// <summary>
/// Which type of submarine the team needs to stay in control of to win
/// </summary>
public TagAction.SubType TargetSubmarineType { get; set; }
public readonly int PointsPerKill;
/// <summary>
/// The score required to win the mission.
/// </summary>
public int WinScore => GameMain.NetworkMember?.ServerSettings.WinScorePvP ?? 10;
/// <summary>
/// Is the winner determined by some kind of a scoring mechanism?
/// </summary>
public bool HasWinScore =>
winCondition != WinCondition.LastManStanding || PointsPerKill != 0;
/// <summary>
/// Scores of both teams. What the scoring represents depends on how the mission is configured (kills, time in control of a beacon station?)
/// </summary>
public readonly int[] Scores = new int[2];
public static CharacterTeamType Winner
{
get
{
@@ -46,6 +94,27 @@ namespace Barotrauma
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
allowRespawning = prefab.ConfigElement.GetAttributeBool(nameof(AllowRespawning), false);
winCondition = prefab.ConfigElement.GetAttributeEnum(nameof(WinCondition),
allowRespawning ? WinCondition.KillCount : WinCondition.LastManStanding);
PointsPerKill = prefab.ConfigElement.GetAttributeInt(nameof(PointsPerKill), 0);
TargetSubmarineType = prefab.ConfigElement.GetAttributeEnum(nameof(TargetSubmarineType), TagAction.SubType.Any);
string sonarTag = prefab.ConfigElement.GetAttributeString(nameof(targetSubmarineSonarLabel), string.Empty);
if (!sonarTag.IsNullOrEmpty())
{
targetSubmarineSonarLabel = TextManager.Get(sonarTag);
}
if (allowRespawning && winCondition == WinCondition.LastManStanding)
{
DebugConsole.ThrowError($"Error in mission {prefab.Identifier}: win condition cannot be \"last man standing\" when respawning is enabled.",
contentPackage: prefab.ContentPackage);
}
descriptions = new LocalizedString[]
{
TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")),
@@ -57,15 +126,23 @@ namespace Barotrauma
{
for (int n = 0; n < 2; n++)
{
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName);
descriptions[i] =
descriptions[i]
.Replace($"[location{n + 1}]", locations[n].DisplayName)
.Replace("[winscore]", WinScore.ToString());
}
}
teamNames = new LocalizedString[]
{
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")),
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B"))
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname1", "missionteam1.pvpmission"))),
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname2", "missionteam2.pvpmission"))),
};
if (winCondition == WinCondition.KillCount && PointsPerKill == 0)
{
DebugConsole.AddWarning($"Potential error in mission {Prefab.Identifier}: win condition is kill count, but {nameof(PointsPerKill)} is set to 0.");
}
}
public static LocalizedString GetTeamName(CharacterTeamType teamID)
@@ -82,9 +159,9 @@ namespace Barotrauma
return "Invalid Team";
}
public bool IsInWinningTeam(Character character)
public static bool IsInWinningTeam(Character character)
{
return character != null &&
return character != null &&
Winner != CharacterTeamType.None &&
Winner == character.TeamID;
}
@@ -99,19 +176,32 @@ namespace Barotrauma
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
if (Prefab.LoadSubmarines)
{
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
GameSession.PlaceSubAtInitialPosition(subs[1], level, placeAtStart: false);
subs[1].FlipX();
}
#if SERVER
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
roundEndTimer = RoundEndDuration;
#endif
if (TargetSubmarineType != TagAction.SubType.Any)
{
targetSubmarine = Submarine.Loaded.FirstOrDefault(s => TagAction.SubmarineTypeMatches(s, TargetSubmarineType));
if (targetSubmarine == null)
{
DebugConsole.ThrowError($"Error in mission {Prefab.Identifier}: could not find a submarine of the type {TargetSubmarineType}.",
contentPackage: Prefab.ContentPackage);
}
}
}
protected override bool DetermineCompleted()
@@ -192,7 +192,6 @@ namespace Barotrauma
foreach (ContentXElement element in characterConfig.Elements())
{
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
string colorIdentifier = element.GetAttributeString("color", string.Empty);
for (int k = 0; k < scalingCharacterCount; k++)
{
// for each element defined, we need to initialize that type of character equal to the scaling escorted character count
@@ -111,7 +111,7 @@ namespace Barotrauma
get { return failed; }
}
public virtual bool AllowRespawn
public virtual bool AllowRespawning
{
get { return true; }
}
@@ -211,21 +211,21 @@ namespace Barotrauma
public virtual void SetLevel(LevelData level) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel);
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionTypes, isSinglePlayer, difficultyLevel);
}
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
{
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
if (missionType == MissionType.None)
if (missionTypes.None())
{
return null;
}
else
{
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType)));
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => missionTypes.Contains(m.Type)));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
if (requireCorrectLocationType)
@@ -350,11 +350,12 @@ namespace Barotrauma
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier);
//clients are not allowed to trigger events, they're handled by the server
if (GameMain.NetworkMember is { IsClient: true }) { return; }
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(trigger.EventIdentifier, trigger.EventTag, Prefab.ContentPackage);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").",
contentPackage: Prefab.ContentPackage);
DebugConsole.ThrowError($"Mission {Prefab.Identifier} failed to trigger an event (identifier: {trigger.EventIdentifier}, tag: {trigger.EventTag}).", contentPackage: Prefab.ContentPackage);
return;
}
if (GameMain.GameSession?.EventManager != null)
@@ -1,4 +1,5 @@
using System;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -7,52 +8,42 @@ using System.Xml.Linq;
namespace Barotrauma
{
[Flags]
public enum MissionType
{
None = 0x0,
Salvage = 0x1,
Monster = 0x2,
Cargo = 0x4,
Beacon = 0x8,
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
AbandonedOutpost = 0x80,
Escort = 0x100,
Pirate = 0x200,
GoTo = 0x400,
ScanAlienRuins = 0x800,
EliminateTargets = 0x1000,
End = 0x2000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End
}
partial class MissionPrefab : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<MissionPrefab> Prefabs = new PrefabCollection<MissionPrefab>();
public static readonly Dictionary<MissionType, Type> CoOpMissionClasses = new Dictionary<MissionType, Type>()
/// <summary>
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
/// </summary>
public static readonly Dictionary<Identifier, Type> CoOpMissionClasses = new Dictionary<Identifier, Type>()
{
{ MissionType.Salvage, typeof(SalvageMission) },
{ MissionType.Monster, typeof(MonsterMission) },
{ MissionType.Cargo, typeof(CargoMission) },
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
{ MissionType.End, typeof(EndMission) }
{ "Salvage".ToIdentifier(), typeof(SalvageMission) },
{ "Monster".ToIdentifier(), typeof(MonsterMission) },
{ "Cargo".ToIdentifier(), typeof(CargoMission) },
{ "Beacon".ToIdentifier(), typeof(BeaconMission) },
{ "Nest".ToIdentifier(), typeof(NestMission) },
{ "Mineral".ToIdentifier(), typeof(MineralMission) },
{ "AbandonedOutpost".ToIdentifier(), typeof(AbandonedOutpostMission) },
{ "Escort".ToIdentifier(), typeof(EscortMission) },
{ "Pirate".ToIdentifier(), typeof(PirateMission) },
{ "GoTo".ToIdentifier(), typeof(GoToMission) },
{ "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) },
{ "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) },
{ "End".ToIdentifier(), typeof(EndMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
/// <summary>
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
/// </summary>
public static readonly Dictionary<Identifier, Type> PvPMissionClasses = new Dictionary<Identifier, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
{ "Combat".ToIdentifier(), typeof(CombatMission) }
};
public static readonly HashSet<Identifier> HiddenMissionTypes = new HashSet<Identifier>() { "GoTo".ToIdentifier(), "End".ToIdentifier() };
public class ReputationReward
{
public readonly Identifier FactionIdentifier;
@@ -67,11 +58,11 @@ namespace Barotrauma
}
}
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
private readonly ConstructorInfo constructor;
public readonly MissionType Type;
public readonly Identifier Type;
public readonly Type MissionClass;
public readonly bool MultiplayerOnly, SingleplayerOnly;
@@ -122,7 +113,21 @@ namespace Barotrauma
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck;
public readonly bool SpawnBeaconStationInMiddle;
public readonly bool AllowOutpostNPCs;
public readonly Identifier ForceOutpostGenerationParameters;
public readonly RespawnMode? ForceRespawnMode;
/// <summary>
/// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer.
/// </summary>
public readonly Identifier AllowOutpostSelectionFromTag;
public readonly bool LoadSubmarines = true;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
@@ -157,7 +162,10 @@ namespace Barotrauma
public class TriggerEvent
{
[Serialize("", IsPropertySaveable.Yes)]
public string EventIdentifier { get; private set; }
public Identifier EventIdentifier { get; private set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier EventTag { get; private set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int State { get; private set; }
@@ -214,12 +222,16 @@ namespace Barotrauma
ShowInMenus = element.GetAttributeBool("showinmenus", true);
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
RequireRuin = element.GetAttributeBool("requireruin", false);
RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false);
RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false);
RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false);
SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false);
if (RequireThalamusWreck) { RequireWreck = true; }
LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true);
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt("commonness", 1);
@@ -234,6 +246,15 @@ namespace Barotrauma
MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100));
MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100);
AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true);
ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty);
AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty);
if (element.GetAttribute(nameof(ForceRespawnMode)) != null)
{
ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound);
}
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
@@ -362,47 +383,23 @@ namespace Barotrauma
Messages = messages.ToImmutableArray();
ReputationRewards = reputationRewards.ToImmutableList();
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
//backwards compatibility
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
{
missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
}
else if (missionTypeName == "clearalienruins")
{
missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
}
if (!Enum.TryParse(missionTypeName.Value, true, out Type))
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (Type == MissionType.None)
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
MissionClass = FindMissionClass(element);
Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty);
#if DEBUG
if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty())
if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty())
{
DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set.");
}
#endif
if (CoOpMissionClasses.ContainsKey(Type))
if (!LoadSubmarines && MissionClass != typeof(CombatMission))
{
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else if (PvPMissionClasses.ContainsKey(Type))
{
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.",
contentPackage: element.ContentPackage);
}
constructor = FindMissionConstructor(element, MissionClass);
if (constructor == null)
{
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!",
@@ -411,6 +408,67 @@ namespace Barotrauma
InitProjSpecific(element);
}
private Type FindMissionClass(ContentXElement element)
{
Type type;
Identifier typeName = element.NameAsIdentifier();
type = TryGetClass(typeName.RemoveFromEnd("Mission"));
if (type == null)
{
//backwards compatibility: the actual mission class used to be defined by the "type" attribute,
//Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string,
//but if we failed to find the class based on the name, let's try the type attribute.
Identifier typeNameLegacy = (element.GetAttributeIdentifier("type", Identifier.Empty)).ToIdentifier();
if (typeNameLegacy == "OutpostDestroy" || typeNameLegacy == "OutpostRescue")
{
typeNameLegacy = "AbandonedOutpost".ToIdentifier();
}
else if (typeNameLegacy == "clearalienruins")
{
typeNameLegacy = "EliminateTargets".ToIdentifier();
}
type = TryGetClass(typeNameLegacy) ?? TryGetClass(typeNameLegacy.AppendIfMissing("Mission"));
if (type == null)
{
DebugConsole.ThrowError($"Failed to find the mission type \"{typeNameLegacy}\" for the mission {Identifier}.",
contentPackage: element.ContentPackage);
return null;
}
}
static Type TryGetClass(Identifier typeName)
{
if (CoOpMissionClasses.TryGetValue(typeName, out Type coOpMissionClass))
{
return coOpMissionClass;
}
else if (PvPMissionClasses.TryGetValue(typeName, out Type pvpMissionClass))
{
return pvpMissionClass;
}
return null;
}
return type;
}
private ConstructorInfo FindMissionConstructor(ContentXElement element, Type missionClass)
{
ConstructorInfo constructor;
if (missionClass == null) { return null; }
if (missionClass != typeof(Mission) && !missionClass.IsSubclassOf(typeof(Mission))) { return null; }
constructor = missionClass.GetConstructor(new Type[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
if (constructor == null)
{
DebugConsole.ThrowError(
$"Could not find the constructor of the mission type \"{missionClass}\" for the mission {Identifier}",
contentPackage: element.ContentPackage);
return null;
}
return constructor;
}
partial void InitProjSpecific(ContentXElement element);
@@ -424,7 +482,7 @@ namespace Barotrauma
}
return
AllowedLocationTypes.Any(lt => lt == "any") ||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
AllowedLocationTypes.Any(lt => lt == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost) ||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
}
@@ -432,11 +490,11 @@ namespace Barotrauma
{
if (fromType == "any" ||
fromType == from.Type.Identifier ||
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
(fromType == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost && from.Type.Identifier != "abandoned"))
{
if (toType == "any" ||
toType == to.Type.Identifier ||
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
(toType == Barotrauma.Tags.AnyOutpost && to.HasOutpost() && to.Type.IsAnyOutpost && to.Type.Identifier != "abandoned"))
{
return true;
}
@@ -461,5 +519,28 @@ namespace Barotrauma
{
DisposeProjectSpecific();
}
/// <summary>
/// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission
/// (the mission at the end of the campaign)
/// </summary>
public static IEnumerable<Identifier> GetAllMultiplayerSelectableMissionTypes()
{
List<Identifier> missionTypes = new List<Identifier>();
foreach (var missionPrefab in Prefabs)
{
if (missionPrefab.Commonness <= 0.0f) { continue; }
if (missionPrefab.SingleplayerOnly) { continue; }
if (HiddenMissionTypes.Contains(missionPrefab.Type))
{
continue;
}
if (!missionTypes.Contains(missionPrefab.Type))
{
missionTypes.Add(missionPrefab.Type);
}
}
return missionTypes.OrderBy(t => t.Value);
}
}
}
@@ -25,6 +25,8 @@ namespace Barotrauma
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
// Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
private readonly float pirateSightingUpdateFrequency = 30;
private float pirateSightingUpdateTimer;
@@ -96,14 +98,26 @@ namespace Barotrauma
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
factionIdentifier = prefab.ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
//make sure all referenced character types are defined
foreach (XElement characterElement in characterConfig.Elements())
{
var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty);
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId);
Identifier typeId = characterElement.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
if (typeId.IsEmpty)
{
if (characterElement.GetAttributeIdentifier("identifier", Identifier.Empty).IsEmpty)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character element with neither a typeidentifier or identifier ({characterElement.ToString()}).",
contentPackage: Prefab.ContentPackage);
}
continue;
}
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e =>
e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == typeId);
if (characterTypeElement == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".",
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{typeId}\".",
contentPackage: Prefab.ContentPackage);
}
}
@@ -143,37 +157,47 @@ namespace Barotrauma
levelData = level;
missionDifficulty = level?.Difficulty ?? 0;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
//no specific sub configured, choose a random one
if (submarineTypeConfig == null)
{
submarineInfo = GetRandomDifficultyModifiedSubmarine(missionDifficulty, ShipRandomnessModifier);
alternateReward = (int)submarineInfo.EnemySubmarineInfo.Reward;
}
else
{
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", factionIdentifier);
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (submarinePath.IsNullOrEmpty())
{
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
contentPackage: Prefab.ContentPackage);
return;
}
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!",
contentPackage: Prefab.ContentPackage);
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (submarinePath.IsNullOrEmpty())
{
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
contentPackage: Prefab.ContentPackage);
return;
}
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!",
contentPackage: Prefab.ContentPackage);
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
@@ -185,6 +209,33 @@ namespace Barotrauma
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
}
private SubmarineInfo GetRandomDifficultyModifiedSubmarine(float levelDifficulty, float randomnessModifier)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
// look for the saved submarine that is closest to our difficulty, with some randomness
SubmarineInfo bestSubmarine = null;
float bestValue = float.MaxValue;
var submarineInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsEnemySubmarine);
foreach (SubmarineInfo submarineInfo in submarineInfos)
{
if (!Prefab.Tags.Any(t => submarineInfo.EnemySubmarineInfo.MissionTags.Contains(t))) { continue; }
float applicabilityValue = GetDifficultyModifiedValue(submarineInfo.EnemySubmarineInfo.PreferredDifficulty, levelDifficulty, randomnessModifier, rand);
if (applicabilityValue < bestValue)
{
bestSubmarine = submarineInfo;
bestValue = applicabilityValue;
}
}
if (bestSubmarine == null)
{
DebugConsole.ThrowError("No EnemySubmarine found that matches the mission's tags!");
return SubmarineInfo.SavedSubmarines.First(i => i.IsEnemySubmarine);
}
return bestSubmarine;
}
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
@@ -300,30 +351,54 @@ namespace Barotrauma
bool commanderAssigned = false;
foreach (ContentXElement element in characterConfig.Elements())
{
//there's two ways to define the characters in pirate missions
//1. "the normal way", referring to a human prefab
Identifier humanPrefabId = element.GetAttributeIdentifier("identifier", Identifier.Empty);
//2. the strange way it was initially implemented and the way the vanilla missions work: using a reference to a "character type" in the mission, which refers to a human prefab
Identifier characterTypeId = element.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
int minAmount = element.GetAttributeInt("minamount", 0);
int maxAmount = element.GetAttributeInt("maxamount", 0);
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
var characterId = element.GetAttributeString("typeidentifier", string.Empty);
int amountCreated = minAmount == 0 && maxAmount == 0 ?
//default to 1 character if amount is not defined
1 :
//otherwise choose a value between min and max based on difficulty
GetDifficultyModifiedAmount(minAmount, maxAmount, enemyCreationDifficulty, rand);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
if (characterType == null)
HumanPrefab humanPrefab = null;
bool isCommander = false;
if (!characterTypeId.IsEmpty)
{
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
contentPackage: element.ContentPackage);
return;
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == characterTypeId).FirstOrDefault();
if (characterType == null)
{
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
contentPackage: element.ContentPackage);
return;
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
humanPrefab = GetHumanPrefabFromElement(variantElement);
isCommander = variantElement.GetAttributeBool("iscommander", false);
}
else if (!humanPrefabId.IsEmpty)
{
humanPrefab = GetHumanPrefabFromElement(element);
isCommander = element.GetAttributeBool("iscommander", false);
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
var humanPrefab = GetHumanPrefabFromElement(variantElement);
if (humanPrefab == null) { continue; }
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
if (element.GetAttribute("color") != null)
{
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
}
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.InitShipCommandManager();
@@ -335,6 +410,15 @@ namespace Barotrauma
}
}
foreach (var subElement in element.Elements())
{
if (subElement.NameAsIdentifier() == "statuseffect")
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: Prefab.Name.Value);
newEffect?.Apply(newEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() != null)
@@ -311,7 +311,11 @@ namespace Barotrauma
targets.Add(target);
foreach (ContentXElement subElement in chosenElement.Elements())
{
LoadTarget(subElement, parentTarget: target);
if (subElement.NameAsIdentifier() == "target" ||
subElement.NameAsIdentifier() == "chooserandom")
{
LoadTarget(subElement, parentTarget: target);
}
}
}
}
@@ -72,7 +72,9 @@ namespace Barotrauma
/// Maximum number of the specific type of monster in the entire level. Can be used to prevent the event from spawning more monsters if there's
/// already enough of that type of monster, e.g. spawned by another event or by a mission.
/// </summary>
public readonly int MaxAmountPerLevel = int.MaxValue;
public readonly int MaxAmountPerLevel;
private readonly float? overridePlayDeadProbability;
public IReadOnlyList<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
@@ -137,6 +139,11 @@ namespace Barotrauma
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0);
float playDeadProbability = prefab.ConfigElement.GetAttributeFloat("playdeadprobability", -1f);
if (playDeadProbability >= 0)
{
overridePlayDeadProbability = playDeadProbability;
}
if (GameMain.NetworkMember != null)
{
@@ -175,6 +182,18 @@ namespace Barotrauma
protected override void InitEventSpecific(EventSet parentSet)
{
// apply pvp stun resistance (reduce stun amount via resist multiplier)
if (GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && !networkMember.ServerSettings.PvPSpawnMonsters)
{
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage($"PvP setting: disabling monster event ({SpeciesName})", Color.Yellow);
}
disallowed = true;
return;
}
if (parentSet != null && resetTime == 0)
{
// Use the parent reset time only if there's no reset time defined for the event.
@@ -200,6 +219,10 @@ namespace Barotrauma
disallowed = true;
continue;
}
if (overridePlayDeadProbability.HasValue)
{
createdCharacter.EvaluatePlayDeadProbability(overridePlayDeadProbability);
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
@@ -299,7 +322,7 @@ namespace Barotrauma
{
if (sub.Info.Type != SubmarineType.Player &&
sub.Info.Type != SubmarineType.EnemySubmarine &&
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
!sub.IsRespawnShuttle)
{
continue;
}
@@ -606,7 +629,7 @@ namespace Barotrauma
bool anyInAbyss = false;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
if (submarine.Info.Type != SubmarineType.Player || submarine.IsRespawnShuttle) { continue; }
if (submarine.WorldPosition.Y < 0)
{
anyInAbyss = true;
@@ -37,9 +37,11 @@ namespace Barotrauma
public readonly OnRoundEndAction OnRoundEndAction;
private readonly string[] requiredDestinationTypes;
private readonly Identifier[] requiredDestinationTypes;
public readonly bool RequireBeaconStation;
public readonly Identifier RequiredDestinationFaction;
public int CurrentActionIndex { get; private set; }
public List<EventAction> Actions { get; } = new List<EventAction>();
public Dictionary<Identifier, List<Entity>> Targets { get; } = new Dictionary<Identifier, List<Entity>>();
@@ -78,17 +80,84 @@ namespace Barotrauma
contentPackage: prefab.ContentPackage);
}
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
requiredDestinationTypes = prefab.ConfigElement.GetAttributeIdentifierArray("requireddestinationtypes", Array.Empty<Identifier>());
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
RequiredDestinationFaction = prefab.ConfigElement.GetAttributeIdentifier(nameof(RequiredDestinationFaction), Identifier.Empty);
var allActionsWithIndent = GetAllActions();
var allActions = allActionsWithIndent.Select(a => a.action);
//attempt to check if the event has ConversationActions with options that don't close the prompt and don't lead to any follow-up conversation
foreach (var action in allActions)
{
if (action is ConversationAction conversationAction && conversationAction.Options.Any())
{
int thisActionIndex = allActionsWithIndent.FindIndex(a => a.action == action);
int thisIndentationLevel = allActionsWithIndent[thisActionIndex].indent;
bool isLast = true;
//go through all the actions after this one
foreach (var actionWithIndent in allActionsWithIndent.Skip(thisActionIndex + 1))
{
//if it's an action with the same indentation level, it means it's a ConversationAction coming after this one
if (actionWithIndent.action is ConversationAction && actionWithIndent.indent == thisIndentationLevel)
{
isLast = false;
break;
}
//if the indentation level went back down, we've already searched everything inside this ConversationAction
if (actionWithIndent.indent < thisIndentationLevel) { break; }
}
if (isLast)
{
foreach (var option in conversationAction.Options)
{
if (!conversationAction.GetEndingOptions().Contains(conversationAction.Options.IndexOf(option)) &&
option.Actions.None(a =>
a is ConversationAction || HasConversationSubAction(a) ||
/* if there's a goto action explicitly set to end the conversation, assume it's intentional*/
a is GoTo { EndConversation: false }))
{
DebugConsole.AddWarning($"Potential error in event \"{prefab.Identifier}\": {nameof(ConversationAction)} ({conversationAction.Text}) has an option ({option.Text}) that doesn't end the conversation, but could not find any follow-ups to the conversation.");
}
}
}
}
static bool HasConversationSubAction(EventAction action)
{
foreach (var subAction in action.GetSubActions())
{
if (subAction is ConversationAction) { return true; }
if (HasConversationSubAction(subAction)) { return true; }
}
return false;
}
}
foreach (var label in allActions.OfType<Label>())
{
if (allActions.None(a => a is GoTo gotoAction && label.Name == gotoAction.Name))
{
//this can be safe, because a label with no gotos leading to it does nothing (but it's still a sign that something's misconfigured)
DebugConsole.AddWarning($"Error in event \"{prefab.Identifier}\". Could not find a GoTo matching the Label \"{label.Name}\".",
contentPackage: prefab.ContentPackage);
}
}
var allActions = GetAllActions().Select(a => a.action);
foreach (var gotoAction in allActions.OfType<GoTo>())
{
if (allActions.None(a => a is Label label && label.Name == gotoAction.Name))
int labelCount = allActions.Count(a => a is Label label && label.Name == gotoAction.Name);
if (labelCount == 0)
{
DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Could not find a label matching the GoTo \"{gotoAction.Name}\".",
contentPackage: prefab.ContentPackage);
}
else if (labelCount > 1)
{
DebugConsole.ThrowError($"Error in event \"{prefab.Identifier}\". Multiple labels with the name \"{gotoAction.Name}\".",
contentPackage: prefab.ContentPackage);
}
}
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
@@ -143,7 +212,7 @@ namespace Barotrauma
}
/// <summary>
/// Finds all actions in the ScriptedEvent (recursively going through the subactions as well).
/// Finds all actions in the ScriptedEvent using a depth-first search (recursively going through the subactions as well).
/// Returns a list of tuples where the first value is the indentation level (or "how deep in the hierarchy") the action is.
/// </summary>
public List<(int indent, EventAction action)> GetAllActions()
@@ -433,20 +502,25 @@ namespace Barotrauma
public override bool LevelMeetsRequirements()
{
if (requiredDestinationTypes == null) { return true; }
var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
if (currLocation?.Connections == null) { return true; }
foreach (LocationConnection c in currLocation.Connections)
{
if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier == t))
var otherLocation = c.OtherLocation(currLocation);
if (!RequiredDestinationFaction.IsEmpty && otherLocation.Faction?.Prefab.Identifier != RequiredDestinationFaction) { continue; }
if (requiredDestinationTypes.Contains(Tags.AnyOutpost) && otherLocation.HasOutpost() && otherLocation.Type.IsAnyOutpost) { return true; }
if (requiredDestinationTypes.Any(t => otherLocation.Type.Identifier == t))
{
return true;
}
}
return false;
return RequiredDestinationFaction.IsEmpty && requiredDestinationTypes.None();
}
public override void Finish()
{
base.Finish();