using Barotrauma.Extensions; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { class ScriptedEvent : Event { public sealed record TargetPredicate( TargetPredicate.EntityType Type, Predicate Predicate) { public enum EntityType { Character, Hull, Item, Structure, Submarine } } private readonly Dictionary> targetPredicates = new Dictionary>(); private readonly Dictionary> cachedTargets = new Dictionary>(); /// /// How many targets were there when they were tagged for the first time? Can be used by some EventActions to check how many entities /// there are still left (e.g. how much of the initial cargo still exists) /// private readonly Dictionary initialAmounts = new Dictionary(); private bool newEntitySpawned; private int prevPlayerCount, prevBotCount; private Character prevControlled; public readonly OnRoundEndAction OnRoundEndAction; private readonly Identifier[] requiredDestinationTypes; public readonly bool RequireBeaconStation; public readonly Identifier RequiredDestinationFaction; public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); protected virtual IEnumerable NonActionChildElementNames => Enumerable.Empty(); public override string ToString() { return $"{nameof(ScriptedEvent)} ({prefab.Identifier})"; } public ScriptedEvent(EventPrefab prefab, int seed) : base(prefab, seed) { foreach (var element in prefab.ConfigElement.Elements()) { Identifier elementId = element.Name.ToIdentifier(); if (NonActionChildElementNames.Contains(elementId)) { continue; } if (elementId == nameof(Barotrauma.OnRoundEndAction)) { OnRoundEndAction = EventAction.Instantiate(this, element) as OnRoundEndAction; continue; } if (elementId == "statuseffect") { DebugConsole.ThrowError($"Error in event prefab \"{prefab.Identifier}\". Status effect configured as an action. Please configure status effects as child elements of a StatusEffectAction.", contentPackage: prefab.ContentPackage); continue; } var action = EventAction.Instantiate(this, element); if (action != null) { Actions.Add(action); } } if (!Actions.Any()) { DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing.", contentPackage: prefab.ContentPackage); } requiredDestinationTypes = prefab.ConfigElement.GetAttributeIdentifierArray("requireddestinationtypes", Array.Empty()); 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