using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Microsoft.Xna.Framework; namespace Barotrauma { #if CLIENT class EventSprite : Prefab { public readonly static PrefabCollection Prefabs = new PrefabCollection(); public readonly Sprite Sprite; public EventSprite(ContentXElement element, RandomEventsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty)) { Sprite = new Sprite(element); } public override void Dispose() { Sprite?.Remove(); } } #endif class EventSet : Prefab { internal class EventDebugStats { public readonly EventSet RootSet; public readonly Dictionary MonsterCounts = new Dictionary(); public float MonsterStrength; public EventDebugStats(EventSet rootSet) { RootSet = rootSet; } } public readonly static PrefabCollection Prefabs = new PrefabCollection(); #if CLIENT public static Sprite GetEventSprite(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) { return null; } if (EventSprite.Prefabs.TryGet(identifier.ToIdentifier(), out EventSprite sprite)) { return sprite.Sprite; } #if DEBUG || UNSTABLE DebugConsole.ThrowError($"Could not find the event sprite \"{identifier}\""); #else DebugConsole.AddWarning($"Could not find the event sprite \"{identifier}\""); #endif return null; } #endif public static List GetAllEventPrefabs() { List eventPrefabs = EventPrefab.Prefabs.ToList(); foreach (var eventSet in Prefabs) { AddSetEventPrefabsToList(eventPrefabs, eventSet); } return eventPrefabs; } public static void AddSetEventPrefabsToList(List list, EventSet set) { list.AddRange(set.EventPrefabs.SelectMany(ep => ep.EventPrefabs)); foreach (var childSet in set.ChildSets) { AddSetEventPrefabsToList(list, childSet); } } public static EventPrefab GetEventPrefab(Identifier identifier) { return GetAllEventPrefabs().Find(prefab => prefab.Identifier == identifier); } public readonly bool IsCampaignSet; //0-100 public readonly float MinLevelDifficulty, MaxLevelDifficulty; public readonly Identifier BiomeIdentifier; public readonly LevelData.LevelType LevelType; public readonly ImmutableArray LocationTypeIdentifiers; public readonly bool ChooseRandom; public readonly int EventCount = 1; public readonly float MinDistanceTraveled; public readonly float MinMissionTime; //the events in this set are delayed if the current EventManager intensity is not between these values public readonly float MinIntensity, MaxIntensity; public readonly bool AllowAtStart; public readonly bool IgnoreCoolDown; public readonly bool PerRuin, PerCave, PerWreck; public readonly bool DisableInHuntingGrounds; public readonly bool OncePerOutpost; public readonly bool DelayWhenCrewAway; public readonly bool TriggerEventCooldown; public readonly bool Additive; public readonly float DefaultCommonness; public readonly ImmutableDictionary OverrideCommonness; public readonly struct SubEventPrefab { public SubEventPrefab(Either prefabOrIdentifiers, float? commonness, float? probability) { PrefabOrIdentifier = prefabOrIdentifiers; SelfCommonness = commonness; SelfProbability = probability; } public readonly Either PrefabOrIdentifier; public IEnumerable EventPrefabs { get { if (PrefabOrIdentifier.TryGet(out EventPrefab p)) { yield return p; } else { foreach (var id in (Identifier[])PrefabOrIdentifier) { yield return EventPrefab.Prefabs[id]; } } } } public readonly float? SelfCommonness; public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f; public readonly float? SelfProbability; public float Probability => SelfProbability ?? EventPrefabs.MaxOrNull(p => p.Probability) ?? 0.0f; public void Deconstruct(out IEnumerable eventPrefabs, out float commonness, out float probability) { eventPrefabs = EventPrefabs; commonness = Commonness; probability = Probability; } } public readonly ImmutableArray EventPrefabs; public readonly ImmutableArray ChildSets; private static Identifier DetermineIdentifier(EventSet parent, XElement element, RandomEventsFile file) { Identifier retVal = element.GetAttributeIdentifier("identifier", Identifier.Empty); if (retVal.IsEmpty) { if (parent is null) { if (file.ContentPackage is CorePackage) { throw new Exception($"Error in {file.Path}: All root EventSets in a core package must have identifiers"); } else { DebugConsole.AddWarning($"{file.Path}: All root EventSets should have an identifier"); } } XElement currElement = element; string siblingIndices = ""; while (currElement.Parent != null) { int siblingIndex = currElement.ElementsBeforeSelf().Count(); siblingIndices = $"-{siblingIndex}{siblingIndices}"; if (parent != null) { break; } currElement = currElement.Parent; } retVal = ((parent != null ? parent.Identifier.Value : $"{file.ContentPackage.Name}-{file.Path}") + siblingIndices) .ToIdentifier(); } return retVal; } public EventSet(ContentXElement element, RandomEventsFile file, EventSet parentSet = null) : base(file, DetermineIdentifier(parentSet, element, file)) { var eventPrefabs = new List(); var childSets = new List(); var overrideCommonness = new Dictionary(); BiomeIdentifier = element.GetAttributeIdentifier("biome", Barotrauma.Identifier.Empty); MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0); MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty); Additive = element.GetAttributeBool("additive", false); string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection"); if (!Enum.TryParse(levelTypeStr, true, out LevelType)) { DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type."); } Identifier[] locationTypeStr = element.GetAttributeIdentifierArray("locationtype", null); if (locationTypeStr != null) { LocationTypeIdentifiers = locationTypeStr.ToImmutableArray(); //if (LocationType.List.Any()) { CheckLocationTypeErrors(); } //TODO: perform validation elsewhere } MinIntensity = element.GetAttributeFloat("minintensity", 0.0f); MaxIntensity = Math.Max(element.GetAttributeFloat("maxintensity", 100.0f), MinIntensity); ChooseRandom = element.GetAttributeBool("chooserandom", false); EventCount = element.GetAttributeInt("eventcount", 1); MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); AllowAtStart = element.GetAttributeBool("allowatstart", false); PerRuin = element.GetAttributeBool("perruin", false); PerCave = element.GetAttributeBool("percave", false); PerWreck = element.GetAttributeBool("perwreck", false); DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); DefaultCommonness = 1.0f; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "commonness": DefaultCommonness = subElement.GetAttributeFloat("commonness", 0.0f); foreach (XElement overrideElement in subElement.Elements()) { if (overrideElement.NameAsIdentifier() == "override") { Identifier levelType = overrideElement.GetAttributeIdentifier("leveltype", ""); if (!overrideCommonness.ContainsKey(levelType)) { overrideCommonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f)); } } } break; case "eventset": childSets.Add(new EventSet(subElement, file, this)); break; default: //an element with just an identifier = reference to an event prefab if (!subElement.HasElements && subElement.Attributes().First().Name.ToString().Equals("identifier", StringComparison.OrdinalIgnoreCase)) { Identifier[] identifiers = subElement.GetAttributeIdentifierArray("identifier", Array.Empty()); float commonness = subElement.GetAttributeFloat("commonness", -1f); float probability = subElement.GetAttributeFloat("probability", -1f); eventPrefabs.Add(new SubEventPrefab( identifiers, commonness >= 0f ? commonness : (float?)null, probability >= 0f ? probability : (float?)null)); } else { var prefab = new EventPrefab(subElement, file, $"{Identifier}-{subElement.ElementsBeforeSelf().Count()}".ToIdentifier()); eventPrefabs.Add(new SubEventPrefab(prefab, prefab.Commonness, prefab.Probability)); } break; } } EventPrefabs = eventPrefabs.ToImmutableArray(); ChildSets = childSets.ToImmutableArray(); OverrideCommonness = overrideCommonness.ToImmutableDictionary(); } public void CheckLocationTypeErrors() { if (LocationTypeIdentifiers == null) { return; } foreach (Identifier locationTypeId in LocationTypeIdentifiers) { if (!LocationType.Prefabs.ContainsKey(locationTypeId)) { DebugConsole.ThrowError($"Error in event set \"{Identifier}\". Location type \"{locationTypeId}\" not found."); } } } public float GetCommonness(Level level) { Identifier key = level.GenerationParams?.Identifier ?? Identifier.Empty; return OverrideCommonness.ContainsKey(key) ? OverrideCommonness[key] : DefaultCommonness; } public static List GetDebugStatistics(int simulatedRoundCount = 100, Func filter = null, bool fullLog = false) { List debugLines = new List(); foreach (var eventSet in Prefabs) { List stats = new List(); for (int i = 0; i < simulatedRoundCount; i++) { var newStats = new EventDebugStats(eventSet); CheckEventSet(newStats, eventSet, filter); stats.Add(newStats); } debugLines.Add($"Event stats ({eventSet.Identifier}): "); LogEventStats(stats, debugLines, fullLog); } return debugLines; static void CheckEventSet(EventDebugStats stats, EventSet thisSet, Func filter = null) { if (thisSet.ChooseRandom) { var unusedEvents = thisSet.EventPrefabs.ToList(); if (unusedEvents.Any()) { for (int i = 0; i < thisSet.EventCount; i++) { var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced); if (eventPrefab.EventPrefabs.Any(p => p != null)) { AddEvents(stats, eventPrefab.EventPrefabs, filter); unusedEvents.Remove(eventPrefab); } } } List values = thisSet.ChildSets .SelectMany(s => s.DefaultCommonness.ToEnumerable().Concat(s.OverrideCommonness.Values)) .ToList(); EventSet childSet = ToolBox.SelectWeightedRandom(thisSet.ChildSets, values, Rand.RandSync.Unsynced); if (childSet != null) { CheckEventSet(stats, childSet, filter); } } else { foreach (var eventPrefab in thisSet.EventPrefabs) { AddEvents(stats, eventPrefab.EventPrefabs, filter); } foreach (var childSet in thisSet.ChildSets) { CheckEventSet(stats, childSet, filter); } } } static void AddEvents(EventDebugStats stats, IEnumerable eventPrefabs, Func filter = null) => eventPrefabs.ForEach(p => AddEvent(stats, p, filter)); static void AddEvent(EventDebugStats stats, EventPrefab eventPrefab, Func filter = null) { if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent)) { if (filter != null && !filter(monsterEvent)) { return; } float spawnProbability = monsterEvent.Prefab.Probability; if (Rand.Value() > spawnProbability) { return; } int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1); if (count <= 0) { return; } Identifier character = monsterEvent.SpeciesName; if (stats.MonsterCounts.TryGetValue(character, out int currentCount)) { if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; } } else { stats.MonsterCounts[character] = 0; } stats.MonsterCounts[character] += count; var aiElement = CharacterPrefab.FindBySpeciesName(character)?.ConfigElement?.GetChildElement("ai"); if (aiElement != null) { stats.MonsterStrength += aiElement.GetAttributeFloat("combatstrength", 0) * count; } } } static void LogEventStats(List stats, List debugLines, bool fullLog) { if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0)) { debugLines.Add(" No monster spawns"); debugLines.Add($" "); } else { var allMonsters = new Dictionary(); foreach (var stat in stats) { foreach (var monster in stat.MonsterCounts) { if (!allMonsters.TryAdd(monster.Key, monster.Value)) { allMonsters[monster.Key] += monster.Value; } } } allMonsters = allMonsters.OrderBy(m => m.Key).ToDictionary(m => m.Key, m => m.Value); stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum())); debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))} (Min: {stats.First().MonsterCounts.Values.Sum()}, Max: {stats.Last().MonsterCounts.Values.Sum()})"); debugLines.Add($" {LogMonsterCounts(allMonsters, divider: stats.Count)}"); if (fullLog) { debugLines.Add($" All samples:"); stats.ForEach(s => debugLines.Add($" {LogMonsterCounts(s.MonsterCounts)}")); } stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength)); debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))} (Min: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}, Max: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)})"); debugLines.Add($" "); } } static string LogMonsterCounts(Dictionary stats, float divider = 0) { if (divider > 0) { return string.Join("\n ", stats.Select(mc => mc.Key + " x " + (mc.Value / divider).FormatSingleDecimal())); } else { return string.Join(", ", stats.Select(mc => mc.Key + " x " + mc.Value)); } } } public override void Dispose() { foreach (var childSet in ChildSets) { childSet.Dispose(); } } } }