using Barotrauma.Extensions; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System.Collections.Generic; using Barotrauma.Networking; using System.Linq; using System; namespace Barotrauma { class WreckAI : IServerSerializable { public Submarine Wreck { get; private set; } public bool IsAlive { get; private set; } private readonly List allItems; private readonly List thalamusItems; private readonly List turrets = new List(); private readonly List wayPoints = new List(); private readonly List hulls = new List(); private readonly List spawnOrgans = new List(); private readonly Item brain; private bool initialCellsSpawned; public readonly WreckAIConfig Config; private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; public WreckAI(Submarine wreck, Item brain, List items = null) { Config = WreckAIConfig.GetRandom(); if (Config == null) { DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); Kill(); return; } allItems = items ?? wreck.GetItems(false); thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus")); foreach (Item item in allItems) { if (thalamusItems.Contains(item)) { // Ensure that thalamus items are visible item.HiddenInGame = false; } else { // Load regular turrets var turret = item.GetComponent(); if (turret != null) { foreach (var linkedItem in item.GetLinkedEntities()) { var container = linkedItem.GetComponent(); if (container == null) { continue; } for (int i = 0; i < container.Inventory.Capacity; i++) { if (container.Inventory.Items[i] != null) { continue; } if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) { Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, wreck); if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) { item.Remove(); } } } } } } } this.brain = brain; Wreck = wreck; foreach (var item in Wreck.GetItems(false)) { var turret = item.GetComponent(); if (turret != null) { turrets.Add(turret); } if (item.HasTag("cellspawnorgan")) { if (!spawnOrgans.Contains(item)) { spawnOrgans.Add(item); } } } wayPoints.AddRange(Wreck.GetWaypoints(false)); hulls.AddRange(Wreck.GetHulls(false)); IsAlive = true; } private readonly List destroyedOrgans = new List(); public void Update(float deltaTime) { if (!IsAlive || Wreck == null || Wreck.Removed) { cells.ForEach(c => c.OnDeath -= OnCellDeath); return; } if (brain == null || brain.Removed || brain.Condition <= 0) { Kill(); } destroyedOrgans.Clear(); foreach (var organ in spawnOrgans) { if (organ.Condition <= 0) { destroyedOrgans.Add(organ); } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); bool someoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; foreach (Submarine submarine in Submarine.Loaded) { if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) { someoneNearby = true; break; } } foreach (Character c in Character.CharacterList) { if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) { someoneNearby = true; break; } } if (!someoneNearby) { return; } OperateTurrets(deltaTime); if (!IsClient) { if (!initialCellsSpawned) { SpawnInitialCells(); } UpdateReinforcements(deltaTime); } } private void SpawnInitialCells() { int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom); if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel) { for (int i = 0; i < brainRoomCells; i++) { if (!TrySpawnCell(out _, brain.CurrentHull)) { break; } } } int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside); for (int i = 0; i < cellsInside; i++) { if (!TrySpawnCell(out _)) { break; } } int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - cells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) { ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); if (targetEntity == null) { break; } if (!TrySpawnCell(out _, targetEntity)) { break; } } initialCellsSpawned = true; } public void Kill() { if (!IsClient) { brain.Condition = 0; } IsAlive = false; } // The client doesn't use these, so we don't have to sync them. private readonly List cells = new List(); // Intentionally contains duplicates. private readonly List populatedHulls = new List(); private float cellSpawnTimer; private float CellSpawnTime => Config.CellSpawnTime; private float CellSpawnRandomFactor => Config.CellSpawnRandomFactor; private int MinCellsPerBrainRoom => Config.MinCellsPerBrainRoom; private int MaxCellsPerRoom => Config.MaxCellsPerRoom; private int MinCellsOutside => Config.MinCellsOutside; private int MaxCellsOutside => Config.MaxCellsOutside; private int MinCellsInside => Config.MinCellsInside; private int MaxCellsInside => Config.MaxCellsInside; private int MaxCellCount => Config.MaxCellCount; private float MinWaterLevel => Config.MinWaterLevel; void UpdateReinforcements(float deltaTime) { if (cells.Count >= MaxCellCount) { return; } cellSpawnTimer -= deltaTime; if (cellSpawnTimer < 0) { TrySpawnCell(out _, spawnOrgans.GetRandom()); cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); } } bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) { cell = null; if (cells.Count >= MaxCellCount) { return false; } if (targetEntity == null) { targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; } if (targetEntity == null) { return false; } if (targetEntity is Hull h) { populatedHulls.Add(h); } else if (targetEntity is WayPoint wp && wp.CurrentHull != null) { populatedHulls.Add(wp.CurrentHull); } // Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs. cell = Character.Create("Leucocyte", targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); cells.Add(cell); cell.OnDeath += OnCellDeath; cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); return true; } void OperateTurrets(float deltaTime) { foreach (var turret in turrets) { // Never target other creatures than humans with the turrets. turret.ThalamusOperate(deltaTime, !turret.Item.HasTag("ignorecharacters"), targetOtherCreatures: false, !turret.Item.HasTag("ignoresubmarines"), turret.Item.HasTag("ignoreaimdelay")); } } void OnCellDeath(Character character, CauseOfDeath causeOfDeath) { cells.Remove(character); } #if SERVER public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null) { msg.Write(IsAlive); } #endif #if CLIENT public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { IsAlive = msg.ReadBoolean(); } #endif } }