using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma { partial class ScanMission : Mission { private readonly ContentXElement itemConfig; private readonly List startingItems = new List(); private readonly List scanners = new List(); private readonly Dictionary parentInventoryIDs = new Dictionary(); private readonly Dictionary inventorySlotIndices = new Dictionary(); private readonly Dictionary parentItemContainerIndices = new Dictionary(); private readonly int targetsToScan; private readonly Dictionary scanTargets = new Dictionary(); private readonly HashSet newTargetsScanned = new HashSet(); private readonly float minTargetDistance; private Ruin TargetRuin { get; set; } private bool AllTargetsScanned { get { return scanTargets.Any() && scanTargets.All(kvp => kvp.Value); } } public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels { get { if (State > 0 || scanTargets.None()) { return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>(); } else { return scanTargets .Where(kvp => !kvp.Value) .Select(kvp => (Prefab.SonarLabel, kvp.Key.WorldPosition)); } } } public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { itemConfig = prefab.ConfigElement.GetChildElement("Items"); targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); } protected override void StartMissionSpecific(Level level) { Reset(); if (IsClient) { return; } if (itemConfig == null) { DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set", contentPackage: Prefab.ContentPackage); return; } foreach (var element in itemConfig.Elements()) { LoadItem(element, null); } GetScanners(); TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient); if (TargetRuin == null) { DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins", contentPackage: Prefab.ContentPackage); return; } var ruinWaypoints = TargetRuin.Submarine.GetWaypoints(false); ruinWaypoints.RemoveAll(wp => wp.CurrentHull == null); if (ruinWaypoints.Count < targetsToScan) { DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({ruinWaypoints.Count} < {targetsToScan})", contentPackage: Prefab.ContentPackage); return; } var availableWaypoints = new List(); float minTargetDistanceSquared = minTargetDistance * minTargetDistance; for (int tries = 0; tries < 15; tries++) { scanTargets.Clear(); availableWaypoints.Clear(); availableWaypoints.AddRange(ruinWaypoints); for (int i = 0; i < targetsToScan; i++) { var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.ServerAndClient); scanTargets.Add(selectedWaypoint, false); availableWaypoints.Remove(selectedWaypoint); if (i < (targetsToScan - 1)) { availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull); availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared); if (availableWaypoints.None()) { #if DEBUG DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available on try #{tries + 1} to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})", contentPackage: Prefab.ContentPackage); #endif break; } } } if (scanTargets.Count >= targetsToScan) { #if DEBUG DebugConsole.NewMessage($"Successfully initialized a Scan mission: targets set on try #{tries + 1}", Color.Green); #endif break; } if ((tries + 1) % 5 == 0) { float reducedMinTargetDistance = (1.0f - (((tries + 1) / 5) * 0.1f)) * minTargetDistance; minTargetDistanceSquared = reducedMinTargetDistance * reducedMinTargetDistance; #if DEBUG DebugConsole.NewMessage($"Reducing minimum distance between Scan mission targets (new min: {reducedMinTargetDistance}) to reach the required target count", Color.Yellow); #endif } } if (scanTargets.Count < targetsToScan) { DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets (current targets: {scanTargets.Count}, required targets: {targetsToScan})", contentPackage: Prefab.ContentPackage); } } private void Reset() { startingItems.Clear(); parentInventoryIDs.Clear(); inventorySlotIndices.Clear(); parentItemContainerIndices.Clear(); scanners.Clear(); TargetRuin = null; scanTargets.Clear(); } private void LoadItem(XElement element, Item parent) { var itemPrefab = FindItemPrefab(element); Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); if (!position.HasValue) { return; } var item = new Item(itemPrefab, position.Value, cargoRoomSub); item.FindHull(); startingItems.Add(item); if (parent?.GetComponent() is ItemContainer itemContainer) { parentInventoryIDs.Add(item, parent.ID); parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer)); parent.Combine(item, user: null); inventorySlotIndices.Add(item, item.ParentInventory?.FindIndex(item) ?? -1); } foreach (XElement subElement in element.Elements()) { int amount = subElement.GetAttributeInt("amount", 1); for (int i = 0; i < amount; i++) { LoadItem(subElement, item); } } } private void GetScanners() { foreach (var startingItem in startingItems) { if (startingItem.GetComponent() is Scanner scanner) { scanner.OnScanStarted += OnScanStarted; if (!IsClient) { scanner.OnScanCompleted += OnScanCompleted; } scanners.Add(scanner); } } } private void OnScanStarted(Scanner scanner) { float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; foreach (var kvp in scanTargets) { if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } scanner.DisplayProgressBar = true; break; } } private void OnScanCompleted(Scanner scanner) { if (IsClient) { return; } newTargetsScanned.Clear(); float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; foreach (var kvp in scanTargets) { if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } newTargetsScanned.Add(kvp.Key); } foreach (var wp in newTargetsScanned) { scanTargets[wp] = true; } #if SERVER // Server should make sure that the clients' scan target status is in-sync GameMain.Server?.UpdateMissionState(this); #endif } private static bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) { if (scanStatus.Value) { return false; } if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; } if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; } return true; } protected override void UpdateMissionSpecific(float deltaTime) { if (IsClient) { return; } switch (State) { case 0: if (AllTargetsScanned) { State = 1; } break; } } protected override bool DetermineCompleted() => State > 0; protected override void EndMissionSpecific(bool completed) { foreach (var scanner in scanners) { if (scanner.Item != null && !scanner.Item.Removed) { scanner.OnScanStarted -= OnScanStarted; scanner.OnScanCompleted -= OnScanCompleted; scanner.Item.Remove(); } } Reset(); failed = !completed && state > 0; } } }