using Barotrauma; using Barotrauma.Extensions; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using FarseerPhysics; using HarmonyLib; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using Voronoi2; using static Barotrauma.Level; using MoreLevelContent.Shared.Utils; using MoreLevelContent.Shared.AI; using Barotrauma.MoreLevelContent.Config; namespace MoreLevelContent.Shared.Generation { public class CaveGenerationDirector : GenerationDirector { public override bool Active => true; internal static MethodInfo level_findawayfrompoint; internal static MethodInfo level_generatecave; internal static MethodInfo level_calcdistfields; internal static FieldInfo cave_genparams; internal static FieldInfo item_statusEffectList; internal static MethodInfo item_rotation; internal static PropertyInfo statusEffect_offset; internal static PropertyInfo statusEffect_characterSpawn_offset; internal static PropertyInfo subbody_visibleBorders; internal static PropertyInfo turret_aiCurrentTargetPriority; internal CaveAI ActiveThalaCave; public readonly List _InitialCaveCheckDebug = new(); public readonly List _EdgeValidtity = new(); public override void Setup() { level_generatecave = AccessTools.Method(typeof(Level), "GenerateCave"); level_findawayfrompoint = AccessTools.Method(typeof(Level), "FindPosAwayFromMainPath"); level_calcdistfields = AccessTools.Method(typeof(Level), "CalculateTunnelDistanceField"); cave_genparams = AccessTools.Field(typeof(Cave), "CaveGenerationParams"); item_rotation = AccessTools.PropertySetter(typeof(Item), "RotationRad"); item_statusEffectList = AccessTools.Field(typeof(Item), "statusEffectLists"); statusEffect_offset = AccessTools.Property(typeof(StatusEffect), "Offset"); statusEffect_characterSpawn_offset = AccessTools.Property(typeof(StatusEffect.CharacterSpawnInfo), "Offset"); subbody_visibleBorders = AccessTools.Property(typeof(SubmarineBody), "VisibleBorders"); turret_aiCurrentTargetPriority = AccessTools.Property(typeof(Turret), nameof(Turret.AICurrentTargetPriorityMultiplier)); MethodInfo Level_Generate = AccessTools.Method(typeof(Level), "Generate", new Type[] { typeof(bool), typeof(Location), typeof(Location) }); _ = Main.Harmony.Patch(Level_Generate, transpiler: new HarmonyMethod(AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.SwapCavesTranspiler)))); MethodInfo level_update = AccessTools.Method(typeof(Level), "Update"); _ = Main.Harmony.Patch(level_update, postfix: new HarmonyMethod(AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.Update)))); MethodInfo level_remove = AccessTools.Method(typeof(Level), "Remove"); _ = Main.Harmony.Patch(level_remove, postfix: new HarmonyMethod(AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.Remove)))); #if CLIENT MethodInfo submarine_cullEntities = AccessTools.Method(typeof(Submarine), nameof(Submarine.CullEntities)); _ = Main.Harmony.Patch(submarine_cullEntities, postfix: new HarmonyMethod(AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.SubmarineCullEntities)))); #endif } #if CLIENT // This could be improved with a transpiler static void SubmarineCullEntities(Camera cam, List ___visibleEntities) { if (Instance.ActiveThalaCave == null) return; Rectangle camView = cam.WorldView; int ___CullMargin = 50; camView = new Rectangle(camView.X - ___CullMargin, camView.Y + ___CullMargin, camView.Width + ___CullMargin * 2, camView.Height + ___CullMargin * 2); var caveRect = new Rectangle(Instance.ActiveThalaCave.Cave.Area.X, Instance.ActiveThalaCave.Cave.Area.Y + Instance.ActiveThalaCave.Cave.Area.Height, Instance.ActiveThalaCave.Cave.Area.Width, Instance.ActiveThalaCave.Cave.Area.Height); if (Submarine.RectsOverlap(camView, caveRect)) { foreach (var item in Instance.ActiveThalaCave.ThalamusItems) { if (item.IsVisible(camView)) { ___visibleEntities.Add(item); } } } } #endif public const float MIN_DIST_FROM_START = Sonar.DefaultSonarRange * 2; const int REQUIRED_EDGE_COUNT = 1; const float MIN_DIST_BETWEEN_ORGANS = 800; const int MAX_OFFENSE_ITEMS = 8; //8; static void Update(float deltaTime) { // Don't run the ai in editors or if we're the client if (GameMain.GameScreen.IsEditor || Main.IsClient) return; Instance.ActiveThalaCave?.Update(deltaTime); } static void Remove() { Instance.ActiveThalaCave?.Remove(); Instance.ActiveThalaCave = null; } static IEnumerable SwapCavesTranspiler(IEnumerable instructions, ILGenerator il) { var code = new List(instructions); bool finished = false; Log.Debug("transpiling..."); Instance._InitialCaveCheckDebug.Clear(); Instance._EdgeValidtity.Clear(); // This insertion point needs to be change to be between lines 1230 and 1232 for (int i = 0; i < code.Count; i++) // -1 since we will be checking i + 1 { yield return code[i]; #if CLIENT // This is very brittle and needs to be changed if (i == 2942) { Log.Debug($"Found insertion point at {i}!"); // endfinally i++; yield return code[i]; // ldc.i4.0 i++; yield return code[i]; // stloc.s i++; yield return code[i]; //br yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.TrySpawnThalaCave))); // index of cell around curIndex } #else if (!finished && code[i + 1].opcode == OpCodes.Ldc_I4_S && (sbyte)code[i + 1].operand == 13) { Log.Debug($"Found insertion point at {i}!"); yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(CaveGenerationDirector), nameof(CaveGenerationDirector.TrySpawnThalaCave))); // index of cell around curIndex finished = true; } #endif } } static void TrySpawnThalaCave() { // TODO: Thalamus Caves currently cause crashes in multiplayer, fix this if (!GameMain.IsSingleplayer) return; if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableThalamusCaves || Loaded.GenerationParams.ThalamusProbability == 0 || Instance.ActiveThalaCave != null) return; var caveParams = CaveGenerationParams.CaveParams.Where(c => { Log.Debug(c.Identifier.ToString()); return c.Identifier == "thalamuscave"; }).FirstOrDefault(); if (caveParams == null) { Log.Error("Unable to find thalacave perfab!"); return; } foreach (var cave in Loaded.Caves) { if (Vector2.DistanceSquared(cave.StartPos.ToVector2(), Loaded.StartPosition) <= MIN_DIST_FROM_START * MIN_DIST_FROM_START) { // Skip caves too close to the start of the level continue; } // find valid caves bool isValid = cave.Tunnels.Where( t => { int count = t.Cells.Where( c => { bool result = CanSeeMainPath(c, out List edges); if (result) { Instance._InitialCaveCheckDebug.Add(new CaveInitalCheckInfo(c, edges)); } return result; } ).Count(); Log.Debug($"Valid Edges: {count} Require Edges: {REQUIRED_EDGE_COUNT}"); return count >= REQUIRED_EDGE_COUNT; }).Any(); if (isValid) { Log.Debug("Valid cave found!"); if (MakeThalaCave(cave)) { cave_genparams.SetValue(cave, caveParams); Log.Debug("Updated generation params"); } return; } } Log.Debug("No valid caves found"); } static bool CanSeeMainPath(VoronoiCell cell, out List validEdges) { validEdges = new List(); // This is a quick test done to see if we're likely to have a direct LOS to the main path // We don't care if these edges are solid yet because these aren't the edges we'll be using for spawning foreach (var edge in cell.Edges.Where(e => e.NextToMainPath || e.NextToSidePath)) { validEdges.Add(edge); } return validEdges.Any(); } private static bool IsThalamus(MapEntityPrefab entityPrefab) => entityPrefab.HasSubCategory("thalamus"); private static Vector2 ClosestPathPoint(Cave cave) { var pathPoints = Loaded.PositionsOfInterest.Where(poi => poi.PositionType == PositionType.MainPath || poi.PositionType == PositionType.SidePath).ToList(); Vector2 closestPos = Vector2.Zero; float dist = float.PositiveInfinity; foreach (var point in pathPoints) { float newDist = Vector2.DistanceSquared(point.Position.ToVector2(), cave.StartPos.ToVector2()); if (newDist < dist) { closestPos = point.Position.ToVector2(); dist = newDist; } } return closestPos; } private readonly List<(Vector2, Vector2)> wallDebug = new List<(Vector2, Vector2)>(); static bool MakeThalaCave(Cave cave) { // Roll var lvlRand = MLCUtils.GetLevelRandom(); // 65% chance to check if the level can have a cave, should make it decently rare if (lvlRand.NextDouble() > 0.65 && Main.IsRelase) return false; // PoCM3hEa <- seed List caveWallCells = GetCaveWallCells(cave); Log.Debug($"Wall Cells: {caveWallCells.Count}"); // Spawn thalamus items List thalamusItems = new List(); var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); var gunPrefab = thalamusPrefabs.Where(p => p.Tags.Contains("fleshgun_cave") && p.Tags.Contains("turret")).FirstOrDefault(); var largeSpikePrefab = thalamusPrefabs.Where(p => p.Tags.Contains("fleshspike_cave")).FirstOrDefault(); var smallSpikePrefab = thalamusPrefabs.Where(p => p.Tags.Contains("fleshspikesmall_cave")).FirstOrDefault(); var spawnerPrefab = thalamusPrefabs.Where(p => p.Tags.Contains("cellspawnorgan_cave")).FirstOrDefault(); var ammosackPrefab = thalamusPrefabs.Where(p => p.Tags.Contains("fleshgunequipment_cave")).FirstOrDefault(); var storageOrgan = thalamusPrefabs.Where(p => p.Tags.Contains("storageorgan_cave")).FirstOrDefault(); var acidVent = thalamusPrefabs.Where(p => p.Tags.Contains("stomachacidvent")).FirstOrDefault(); var pathPoint = ClosestPathPoint(cave); List entranceEdges = GetEdgesFacingPoint(); // Put some debugging test criteria here to see why the walls are failing the test var insideEdges = caveWallCells.SelectMany(c => c.Edges.Where((e) => { EdgeValidity validity = new EdgeValidity(e, pathPoint); Instance._EdgeValidtity.Add(validity); return validity.IsValidEdge; })).ToList(); if (insideEdges.Count == 0) { Log.Warn("Failed to find any inside edges, spawn aborted."); return false; } GraphEdge brainEdge = null; float closestDist = float.PositiveInfinity; float curDist; foreach (var edge in insideEdges) { curDist = Vector2.DistanceSquared(edge.Center, cave.EndPos.ToVector2()); if (curDist < closestDist) { brainEdge = edge; closestDist = curDist; } } // Prevent other organs from spawning inside the brain _ = insideEdges.Remove(brainEdge); List fleshGuns = new List(); CreateOffensiveItems(); CreateDefensiveItems(); Instance.ActiveThalaCave = new CaveAI(thalamusItems, brainEdge, cave); _ = Loaded.PositionsOfInterest.RemoveAll(poi => poi.Cave == cave); return true; // Methods void CreateOffensiveItems() { Queue offensiveItems = new Queue(); // Limit offensive items to a max of 8 for (int i = 0; i < Math.Min(entranceEdges.Count, MAX_OFFENSE_ITEMS); i++) { // Always spawn a flesh gun first if (i % 2 == 0) { offensiveItems.Enqueue(SpawnFleshGun); } else { offensiveItems.Enqueue(SpawnFleshSpike); } } while (offensiveItems.Count > 0) { offensiveItems.Dequeue().Invoke(); } } void CreateDefensiveItems() { int totalSpawnLocations = insideEdges.Count; int cellSpawns = totalSpawnLocations / 4; // Spawn fleshgun ammo sacks before we spawn any cell spawners // since they're required for the fleshguns to work foreach (var fleshgun in fleshGuns) { var ammosack = SpawnOrgan(ammosackPrefab, GetEdge(insideEdges, true)); fleshgun.AddLinked(ammosack); } for (int i = 0; i < cellSpawns; i++) { if (insideEdges.Count != 0) break; if (i % 2 == 0) { SpawnCellSpawner(GetEdge(insideEdges, true)); } else { if (i % 3 == 0) { SpawnSmallFleshSpike(); } else { SpawnAcidVent(); } } } // Ensure there is always 4 organs int organCount = Math.Max(insideEdges.Count / 8, 4); // Don't let the organ count go over the remaining valid edges organCount = Math.Min(insideEdges.Count, organCount); for (int i = 0; i < organCount; i++) { if (insideEdges.Count == 0) break; _ = SpawnOrgan(storageOrgan, GetEdge(insideEdges, true)); } } void SpawnFleshGun() { Item fleshgun = new Item(gunPrefab, Vector2.Zero, null); thalamusItems.Add(fleshgun); fleshGuns.Add(fleshgun); GraphEdge edge = GetEdge(entranceEdges); if (edge == null) return; int radius = fleshgun.StaticBodyConfig.GetAttributeInt("radius", 0); Vector2 dir = MLCUtils.PositionItemOnEdge(fleshgun, edge, radius); float angle = Angle(dir); Turret turret = fleshgun.GetComponent(); turret.RotationLimits = new Vector2(-angle - 90, -angle + 90); turret.AIRange = (float)(Sonar.DefaultSonarRange * 0.8); turret.Reload = 10f; Log.Debug($"Placed fleshgun at {fleshgun.Position}"); } void SpawnSmallFleshSpike() { Item spike = new Item(smallSpikePrefab, Vector2.Zero, null); GraphEdge edge = GetEdge(insideEdges); Turret turret = ConfigureTurret(spike, edge); if (turret == null) return; turret.TargetCharacters = true; turret.TargetHumans = true; turret.TargetItems = false; Log.Debug($"Placed small spike at {spike.Position}"); } void SpawnAcidVent() { Item vent = new Item(acidVent, Vector2.Zero, null); GraphEdge edge = GetEdge(insideEdges); Turret turret = ConfigureTurret(vent, edge, 35); if (turret == null) return; turret.TargetCharacters = true; turret.TargetHumans = true; turret.TargetItems = false; Log.Debug($"Placed acid vent at {vent.Position}"); } void SpawnFleshSpike() { Item spike = new Item(largeSpikePrefab, Vector2.Zero, null); GraphEdge edge = GetEdge(entranceEdges); Turret turret = ConfigureTurret(spike, edge); if (turret == null) return; turret.TargetItems = true; turret.TargetSubmarines = true; turret.TargetCharacters = false; Log.Debug($"Placed spike at {spike.Position}"); } Turret ConfigureTurret(Item spike, GraphEdge edge, float angleRange = 1f) { thalamusItems.Add(spike); if (edge == null) return null; int height = spike.StaticBodyConfig.GetAttributeInt("height", 0); Vector2 dir = MLCUtils.PositionItemOnEdge(spike, edge, height); float angle = Angle(dir); spike.SpriteDepth = 1; Turret turret = spike.GetComponent(); turret.RotationLimits = new Vector2(-angle - angleRange, -angle + angleRange); turret.RandomMovement = false; turret.AimDelay = false; // special sauce? // turret_aiCurrentTargetPriority.SetValue(turret, 0.1f); // config status effects Dictionary> dic = (Dictionary>)item_statusEffectList.GetValue(spike); if (dic?.TryGetValue(ActionType.OnUse, out List effects) ?? false) { // Adjust offsets of on use status effects to match our angle foreach (var effect in effects) { float dist = effect.Offset.Y; float turretRot = angle; float turretRotRad = MathHelper.ToRadians(turretRot); Vector2 newOffset = new Vector2((float)Math.Cos(turretRotRad), (float)Math.Sin(turretRotRad)) * dist; statusEffect_offset.SetValue(effect, newOffset); foreach (var spawnEffect in effect.SpawnCharacters) { dist = spawnEffect.Offset.Y; newOffset = new Vector2((float)Math.Cos(turretRotRad), (float)Math.Sin(turretRotRad)) * dist; statusEffect_characterSpawn_offset.SetValue(spawnEffect, newOffset); } } } return turret; } void SpawnCellSpawner(GraphEdge edge) { Item spawner = new Item(spawnerPrefab, Vector2.Zero, null); thalamusItems.Add(spawner); Vector2 dir = MLCUtils.PositionItemOnEdge(spawner, edge, 80, true); } Item SpawnOrgan(ItemPrefab organPrefab, GraphEdge edge) { Item organ = new Item(organPrefab, Vector2.Zero, null); thalamusItems.Add(organ); Vector2 dir = MLCUtils.PositionItemOnEdge(organ, edge, 60, true); return organ; } GraphEdge GetEdge(List edges, bool removeClose = false) { if (!edges.Any()) return null; GraphEdge edge = edges.GetRandom(Rand.RandSync.ServerAndClient); _ = edges.Remove(edge); // Remove all valid edges that are too close to this edge if (removeClose) _ = edges.RemoveAll(e => Vector2.DistanceSquared(edge.Center, e.Center) < MIN_DIST_BETWEEN_ORGANS * MIN_DIST_BETWEEN_ORGANS); return edge; } float Angle(Vector2 dir) => (float)(MathUtils.VectorToAngle(dir) * 180 / Math.PI); List GetEdgesFacingPoint() { List edges = new List(); caveWallCells .ForEach(c => { edges.AddRange(c.Edges.Where(e => e.IsSolid && WideEnough(e) && FacingPathPoint(e) && CanEdgeSeePathPoint(e) ).ToList()); }); return edges; } bool FacingPathPoint(GraphEdge e) => Vector2.Dot(Vector2.Normalize(e.GetNormal(null)), Vector2.Normalize(e.Center - pathPoint)) >= 0; bool WideEnough(GraphEdge e, float size = 200) => Vector2.DistanceSquared(e.Point1, e.Point2) > size * size; bool CanEdgeSeePathPoint(GraphEdge e) { return !PhysUtil.RaycastWorld(e.SimPosition(), ConvertUnits.ToSimUnits(pathPoint), new List { }).Hit; } bool CanPosSeePathPoint(Vector2 simPos) => !PhysUtil.RaycastWorld(simPos, ConvertUnits.ToSimUnits(pathPoint), new List { }).Hit; bool InsideExtraWall(GraphEdge e) { // this doesn't work at all // SAD bool cell1 = false; bool cell2 = false; if (e.Cell1 != null) { cell1 = Loaded.ExtraWalls.Any(w => w.IsPointInside(e.Cell1.Center)); } if (e.Cell2 != null) { cell2 = Loaded.ExtraWalls.Any(w => w.IsPointInside(e.Cell2.Center)); } return cell1 || cell2; } Vector2 GetEdgeDir(GraphEdge edge) => edge.GetNormal(null); } static List GetCaveWallCells(Cave cave) { List caveWalls = new List(); foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells)) { foreach (var edge in caveCell.Edges) { if (!edge.NextToCave) { continue; } if (edge.Cell1?.CellType == CellType.Solid && !caveWalls.Contains(edge.Cell1)) { caveWalls.Add(edge.Cell1); } if (edge.Cell2?.CellType == CellType.Solid && !caveWalls.Contains(edge.Cell2)) { caveWalls.Add(edge.Cell2); } } } return caveWalls; } } public struct CaveInitalCheckInfo { public CaveInitalCheckInfo(VoronoiCell cell, List validEdges) { Cell = cell; ValidEdges = validEdges; } public List ValidEdges; public VoronoiCell Cell; public Vector2 GetEdgeDrawPosition(GraphEdge edge) { return new Vector2(edge.Center.X, -edge.Center.Y); } } public struct EdgeValidity { //e.IsSolid && // !CanEdgeSeePathPoint(e) && // WideEnough(e) && // !InsideExtraWall(e) public EdgeValidity(GraphEdge e, Vector2 pathPoint) { IsValidEdge = false; FailReason = "Valid"; Hit = default; Position = new Vector2(e.Center.X, -e.Center.Y); if (!e.IsSolid) { FailReason = "Not solid"; return; } if (CanEdgeSeePoint(e, pathPoint, out RayHit hit)) { FailReason = "Not Inside"; Hit = hit; return; } if (!WideEnough(e)) { FailReason = "Too Small"; return; } IsValidEdge = true; } public static bool CanEdgeSeePoint(GraphEdge e, Vector2 point, out RayHit hit) { hit = PhysUtil.RaycastWorld(e.SimPosition(), ConvertUnits.ToSimUnits(point), new List { }); return !hit.Hit; } public static bool WideEnough(GraphEdge e, float size = 200) => Vector2.DistanceSquared(e.Point1, e.Point2) > size * size; public RayHit Hit; public string FailReason; public bool IsValidEdge; public Vector2 Position; } public static class GraphEdgeExtensions { public static Vector2 SimPosition(this GraphEdge edge) => ConvertUnits.ToSimUnits(edge.Center); } }