using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Xml.Linq; using Microsoft.Xna.Framework; 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, ClearAlienRuins = 0x1000, All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins } partial class MissionPrefab : PrefabWithUintIdentifier { public static readonly PrefabCollection Prefabs = new PrefabCollection(); public static readonly Dictionary CoOpMissionClasses = new Dictionary() { { 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.ClearAlienRuins, typeof(AlienRuinMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { { MissionType.Combat, typeof(CombatMission) } }; public static readonly HashSet HiddenMissionClasses = new HashSet() { MissionType.GoTo }; private readonly ConstructorInfo constructor; public readonly MissionType Type; public readonly bool MultiplayerOnly, SingleplayerOnly; public readonly Identifier TextIdentifier; private readonly string[] tags; public IEnumerable Tags { get { return tags; } } public readonly LocalizedString Name; public readonly LocalizedString Description; public readonly LocalizedString SuccessMessage; public readonly LocalizedString FailureMessage; public readonly LocalizedString SonarLabel; public readonly Identifier SonarIconIdentifier; public readonly Identifier AchievementIdentifier; public readonly Dictionary ReputationRewards = new Dictionary(); public readonly List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)> DataRewards = new List<(Identifier Identifier, object Value, SetDataAction.OperationType OperationType)>(); public readonly int Commonness; public readonly int? Difficulty; public const int MinDifficulty = 1, MaxDifficulty = 4; public readonly int Reward; public readonly ImmutableArray Headers; public readonly ImmutableArray Messages; public readonly bool AllowRetry; public readonly bool IsSideObjective; public readonly bool RequireWreck, RequireRuin; /// /// The mission can only be received when travelling from a location of the first type to a location of the second type /// public readonly List<(Identifier from, Identifier to)> AllowedConnectionTypes; /// /// The mission can only be received in these location types /// public readonly List AllowedLocationTypes = new List(); /// /// Show entities belonging to these sub categories when the mission starts /// public readonly List UnhideEntitySubCategories = new List(); public class TriggerEvent { [Serialize("", IsPropertySaveable.Yes)] public string EventIdentifier { get; private set; } [Serialize(0, IsPropertySaveable.Yes)] public int State { get; private set; } [Serialize(0.0f, IsPropertySaveable.Yes)] public float Delay { get; private set; } [Serialize(false, IsPropertySaveable.Yes)] public bool CampaignOnly { get; private set; } public TriggerEvent(XElement element) { SerializableProperty.DeserializeProperties(this, element); } } public readonly List TriggerEvents = new List(); public LocationTypeChange LocationTypeChangeOnCompleted; public readonly ContentXElement ConfigElement; public MissionPrefab(ContentXElement element, MissionsFile file) : base(file, element.GetAttributeIdentifier("identifier", "")) { ConfigElement = element; TextIdentifier = element.GetAttributeIdentifier("textidentifier", Identifier); tags = element.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); string nameTag = element.GetAttributeString("name", ""); Name = TextManager.Get($"MissionName.{TextIdentifier}"); if (!string.IsNullOrEmpty(nameTag)) { Name = Name .Fallback(TextManager.Get(nameTag)) .Fallback(nameTag); } string descriptionTag = element.GetAttributeString("description", ""); Description = TextManager.Get($"MissionDescription.{TextIdentifier}"); if (!string.IsNullOrEmpty(descriptionTag)) { Description = Description .Fallback(TextManager.Get(descriptionTag)) .Fallback(descriptionTag); } Reward = element.GetAttributeInt("reward", 1); AllowRetry = element.GetAttributeBool("allowretry", false); IsSideObjective = element.GetAttributeBool("sideobjective", false); RequireWreck = element.GetAttributeBool("requirewreck", false); RequireRuin = element.GetAttributeBool("requireruin", false); Commonness = element.GetAttributeInt("commonness", 1); if (element.GetAttribute("difficulty") != null) { int difficulty = element.GetAttributeInt("difficulty", MinDifficulty); Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty); } string successMessageTag = element.GetAttributeString("successmessage", ""); SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}"); if (!string.IsNullOrEmpty(successMessageTag)) { SuccessMessage = SuccessMessage .Fallback(TextManager.Get(successMessageTag)) .Fallback(successMessageTag); } SuccessMessage = SuccessMessage.Fallback(TextManager.Get("missioncompleted")); string failureMessageTag = element.GetAttributeString("failuremessage", ""); FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}"); if (!string.IsNullOrEmpty(failureMessageTag)) { FailureMessage = FailureMessage .Fallback(TextManager.Get(failureMessageTag)) .Fallback(failureMessageTag); } FailureMessage = FailureMessage.Fallback(TextManager.Get("missionfailed")); string sonarLabelTag = element.GetAttributeString("sonarlabel", ""); SonarLabel = TextManager.Get($"MissionSonarLabel.{sonarLabelTag}") .Fallback(TextManager.Get(sonarLabelTag)) .Fallback(TextManager.Get($"MissionSonarLabel.{TextIdentifier}")); if (!string.IsNullOrEmpty(sonarLabelTag)) { SonarLabel = SonarLabel.Fallback(sonarLabelTag); } SonarIconIdentifier = element.GetAttributeIdentifier("sonaricon", ""); MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false); SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false); AchievementIdentifier = element.GetAttributeIdentifier("achievementidentifier", ""); UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", Array.Empty()).ToList(); var headers = new List(); var messages = new List(); AllowedConnectionTypes = new List<(Identifier from, Identifier to)>(); for (int i = 0; i < 100; i++) { LocalizedString header = TextManager.Get($"MissionHeader{i}.{TextIdentifier}"); LocalizedString message = TextManager.Get($"MissionMessage{i}.{TextIdentifier}"); if (!message.IsNullOrEmpty()) { headers.Add(header); messages.Add(message); } } int messageIndex = 0; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "message": if (messageIndex > headers.Count - 1) { headers.Add(string.Empty); messages.Add(string.Empty); } headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}") .Fallback(TextManager.Get(subElement.GetAttributeString("header", ""))) .Fallback(subElement.GetAttributeString("header", "")); messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}") .Fallback(TextManager.Get(subElement.GetAttributeString("text", ""))) .Fallback(subElement.GetAttributeString("text", "")); messageIndex++; break; case "locationtype": case "connectiontype": if (subElement.GetAttribute("identifier") != null) { AllowedLocationTypes.Add(subElement.GetAttributeIdentifier("identifier", "")); } else { AllowedConnectionTypes.Add(( subElement.GetAttributeIdentifier("from", ""), subElement.GetAttributeIdentifier("to", ""))); } break; case "locationtypechange": LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeIdentifier("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); break; case "reputation": case "reputationreward": Identifier factionIdentifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); float amount = subElement.GetAttributeFloat("amount", 0.0f); if (ReputationRewards.ContainsKey(factionIdentifier)) { DebugConsole.ThrowError($"Error in mission prefab \"{Identifier}\". Multiple reputation changes defined for the identifier \"{factionIdentifier}\"."); continue; } ReputationRewards.Add(factionIdentifier, amount); break; case "metadata": Identifier identifier = subElement.GetAttributeIdentifier("identifier", Identifier.Empty); string stringValue = subElement.GetAttributeString("value", string.Empty); if (!string.IsNullOrWhiteSpace(stringValue) && !identifier.IsEmpty) { object value = SetDataAction.ConvertXMLValue(stringValue); SetDataAction.OperationType operation = SetDataAction.OperationType.Set; string operatingString = subElement.GetAttributeString("operation", string.Empty); if (!string.IsNullOrWhiteSpace(operatingString)) { operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString); } DataRewards.Add((identifier, value, operation)); } break; case "triggerevent": TriggerEvents.Add(new TriggerEvent(subElement)); break; } } Headers = headers.ToImmutableArray(); Messages = messages.ToImmutableArray(); Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty); //backwards compatibility if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue") { missionTypeName = "AbandonedOutpost".ToIdentifier(); } if (!Enum.TryParse(missionTypeName.Value, true, out Type)) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; } if (Type == MissionType.None) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); return; } if (CoOpMissionClasses.ContainsKey(Type)) { 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.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); } if (constructor == null) { DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!"); } InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); public bool IsAllowed(Location from, Location to) { if (from == to) { return AllowedLocationTypes.Any(lt => lt == "any") || AllowedLocationTypes.Any(lt => lt == from.Type.Identifier); } foreach (var (fromType, toType) in AllowedConnectionTypes) { if (fromType == "any" || fromType == from.Type.Identifier || (fromType == "anyoutpost" && from.HasOutpost())) { if (toType == "any" || toType == to.Type.Identifier || (toType == "anyoutpost" && to.HasOutpost())) { return true; } } } if (Type == MissionType.Beacon) { var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; } } else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) { var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; } } return false; } public Mission Instantiate(Location[] locations, Submarine sub) { return constructor?.Invoke(new object[] { this, locations, sub }) as Mission; } partial void DisposeProjectSpecific(); public override void Dispose() { DisposeProjectSpecific(); } } }