Track LocalMods as part of monolith

This commit is contained in:
2026-06-08 18:50:16 +03:00
parent 143f2fed7c
commit 1b214b44c2
1287 changed files with 139255 additions and 1 deletions
@@ -0,0 +1,633 @@
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<CaveGenerationDirector>
{
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<CaveInitalCheckInfo> _InitialCaveCheckDebug = new();
public readonly List<EdgeValidity> _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<MapEntity> ___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<CodeInstruction> SwapCavesTranspiler(IEnumerable<CodeInstruction> instructions, ILGenerator il)
{
var code = new List<CodeInstruction>(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<GraphEdge> 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<GraphEdge> validEdges)
{
validEdges = new List<GraphEdge>();
// 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<VoronoiCell> caveWallCells = GetCaveWallCells(cave);
Log.Debug($"Wall Cells: {caveWallCells.Count}");
// Spawn thalamus items
List<Item> thalamusItems = new List<Item>();
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<GraphEdge> 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<Item> fleshGuns = new List<Item>();
CreateOffensiveItems();
CreateDefensiveItems();
Instance.ActiveThalaCave = new CaveAI(thalamusItems, brainEdge, cave);
_ = Loaded.PositionsOfInterest.RemoveAll(poi => poi.Cave == cave);
return true;
// Methods
void CreateOffensiveItems()
{
Queue<Action> offensiveItems = new Queue<Action>();
// 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>();
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>();
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<ActionType, List<StatusEffect>> dic = (Dictionary<ActionType, List<StatusEffect>>)item_statusEffectList.GetValue(spike);
if (dic?.TryGetValue(ActionType.OnUse, out List<StatusEffect> 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<GraphEdge> 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<GraphEdge> GetEdgesFacingPoint()
{
List<GraphEdge> edges = new List<GraphEdge>();
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<Body> { }).Hit;
}
bool CanPosSeePathPoint(Vector2 simPos) => !PhysUtil.RaycastWorld(simPos, ConvertUnits.ToSimUnits(pathPoint), new List<Body> { }).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<VoronoiCell> GetCaveWallCells(Cave cave)
{
List<VoronoiCell> caveWalls = new List<VoronoiCell>();
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<GraphEdge> validEdges)
{
Cell = cell;
ValidEdges = validEdges;
}
public List<GraphEdge> 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<Body> { });
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);
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace MoreLevelContent.Shared.Generation
{
public abstract class DefWithDifficultyRange : IComparable<DefWithDifficultyRange>
{
protected DefWithDifficultyRange(string stringContainingDiff) => DifficultyRange = new DifficultyRange(stringContainingDiff);
protected DefWithDifficultyRange(float min, float max) => DifficultyRange = new DifficultyRange(min, max);
public float MinDifficulty => DifficultyRange.MinDiff;
public float MaxDifficulty => DifficultyRange.MaxDiff;
public float AverageDifficulty => (MinDifficulty + MaxDifficulty) / 2;
public DifficultyRange DifficultyRange { get; protected set; }
public int CompareTo([AllowNull] DefWithDifficultyRange other) => other == null ? -1 : other.MinDifficulty < MinDifficulty ? -1 : other.MinDifficulty == MinDifficulty ? 0 : 1;
}
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace MoreLevelContent.Shared.Generation
{
public struct DifficultyRange
{
public float MinDiff;
public float MaxDiff;
private static readonly Regex diffRegex = new Regex("diff_([0-9.]+)-([0-9.]+)");
public DifficultyRange(float min, float max)
{
MinDiff = min;
MaxDiff = max;
}
public DifficultyRange(string name)
{
Match match = diffRegex.Match(name);
// Exit if the sub has no difficulty range defined
if (match.Groups.Count < 2)
{
MinDiff = 0;
MaxDiff = 0;
Log.Warn($"Element with name {name} has no diff range defined. Will only spawn when at 0% diff!");
return;
}
string diffStr1 = match.Groups[1].Value;
string diffStr2 = match.Groups[2].Value;
MinDiff = float.Parse(diffStr1);
MaxDiff = float.Parse(diffStr2);
}
public override string ToString() => $"{MinDiff} - {MaxDiff}";
public bool IsInRangeOf(float diff) => MinDiff <= diff && diff < MaxDiff;
}
}
@@ -0,0 +1,13 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation
{
public class PirateNPCSetDef : DefWithDifficultyRange
{
internal readonly MissionPrefab Prefab;
internal PirateNPCSetDef(MissionPrefab prefab, string stringContainingDiff) : base(stringContainingDiff) => Prefab = prefab;
}
}
@@ -0,0 +1,20 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Generation
{
internal class PirateOutpostDef : DefWithDifficultyRange
{
internal SubmarineInfo SubInfo;
internal PlacementType PlacementType;
internal PirateOutpostDef(SubmarineInfo subInfo, float min, float max, PlacementType placementType) : base(min, max)
{
SubInfo = subInfo;
PlacementType = placementType;
}
}
}
@@ -0,0 +1,11 @@
using Barotrauma.MoreLevelContent.Shared.Utils;
namespace MoreLevelContent.Shared.Generation
{
public abstract class Director<DirectorType, ModuleType> : Singleton<DirectorType>
where DirectorType : class
where ModuleType : DirectorModule<ModuleType>
{
}
}
@@ -0,0 +1,7 @@
namespace MoreLevelContent.Shared.Generation
{
public class DirectorModule<T>
{
}
}
@@ -0,0 +1,50 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.Generation.Interfaces;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using static Barotrauma.Level;
using static HarmonyLib.Code;
namespace MoreLevelContent.Shared.Generation
{
public abstract class GenerationDirector<T> : Singleton<T>, IActive where T : class
{
static GenerationDirector()
{
_autofill = typeof(AutoItemPlacer).GetMethod("CreateAndPlace", BindingFlags.NonPublic | BindingFlags.Static);
if (_autofill == null)
{
Log.Error("Unable to reflect");
}
}
private static readonly MethodInfo _autofill;
public abstract bool Active { get; }
internal Submarine SpawnSubOnPath(string name, string path, bool ignoreCrushDepth = false, SubmarineType submarineType = SubmarineType.EnemySubmarine, PlacementType placementType = PlacementType.Bottom)
{
Submarine placedSub = SubPlacementUtils.SpawnSubOnPath(name, path, submarineType, placementType);
if (placedSub == null)
{
Log.Error("SpawnSubOnPath failed to spawn wanted sub.");
return null;
}
SubPlacementUtils.SetCrushDepth(placedSub, ignoreCrushDepth);
return placedSub;
}
internal Submarine SpawnSubOnPath(string name, ContentFile sub, bool ignoreCrushDepth = false, SubmarineType submarineType = SubmarineType.EnemySubmarine, PlacementType placementType = PlacementType.Bottom)
{
Submarine placedSub = SubPlacementUtils.SpawnSubOnPath(name, sub, submarineType, placementType);
SubPlacementUtils.SetCrushDepth(placedSub, ignoreCrushDepth);
return placedSub;
}
internal void AutofillSub(Submarine sub, float skipChance = 0.5f) => _autofill.Invoke(null, new object[] { sub.ToEnumerable(), null, skipChance });
}
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation.Interfaces
{
public interface IActive
{
bool Active { get; }
}
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation.Interfaces
{
interface IGenerateNPCs
{
void SpawnNPCs();
}
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation.Interfaces
{
interface IGenerateSubmarine
{
void GenerateSub();
}
}
@@ -0,0 +1,12 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation.Interfaces
{
interface ILevelStartGenerate
{
internal void OnLevelGenerationStart(LevelData levelData, bool mirror);
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Shared.Generation.Interfaces
{
interface IRoundStatus
{
void BeforeRoundStart();
void RoundEnd();
}
}
@@ -0,0 +1,115 @@
using Barotrauma;
using MoreLevelContent.Shared.Generation.Interfaces;
using MoreLevelContent.Shared.Generation.Pirate;
using MoreLevelContent.Shared.Utils;
using System.Collections.Generic;
namespace MoreLevelContent.Shared.Generation
{
public class LevelContentProducer : IActive
{
/// <summary>
/// If the the generator has outposts to spawn or not
/// </summary>
public bool Active { get; private set; }
public LevelContentProducer()
{
Log.Verbose("LevelContentProducer::ctr..");
AddDirector(PirateOutpostDirector.Instance);
AddDirector(MissionGenerationDirector.Instance);
AddDirector(CaveGenerationDirector.Instance);
// AddDirector(PirateEncounterDirector.Instance);
}
private readonly List<IGenerateSubmarine> submarineGenerators = new List<IGenerateSubmarine>();
private readonly List<IGenerateNPCs> npcGenerators = new List<IGenerateNPCs>();
private readonly List<ILevelStartGenerate> levelStartGenerators = new List<ILevelStartGenerate>();
private readonly List<IRoundStatus> roundStart = new List<IRoundStatus>();
public void Cleanup() => Log.Verbose("LevelContentProducer::Cleanup");
public void AddDirector<Director>(GenerationDirector<Director> director) where Director : class
{
director.Setup();
if (!director.Active)
{
Log.Error($"Did not add director {director} as it was not active!");
}
Active = true;
if (director is IGenerateSubmarine)
{
submarineGenerators.Add(director as IGenerateSubmarine);
Log.Verbose($"Added {director} to Submarine generators");
}
if (director is IGenerateNPCs)
{
npcGenerators.Add(director as IGenerateNPCs);
Log.Verbose($"Added {director} to NPC generators");
}
if (director is ILevelStartGenerate)
{
levelStartGenerators.Add(director as ILevelStartGenerate);
Log.Verbose($"Added {director} to level start generators");
}
if (director is IRoundStatus)
{
roundStart.Add(director as IRoundStatus);
Log.Verbose($"Added {director} to round start generators");
}
}
internal void LevelGenerate(LevelData levelData, bool mirror)
{
SubPlacementUtils.ClearBlockedRects();
Log.Verbose("Called level generate");
foreach (ILevelStartGenerate levelStart in levelStartGenerators)
{
levelStart.OnLevelGenerationStart(levelData, mirror);
}
}
public void StartRound()
{
Log.Verbose("Called start round");
foreach (IRoundStatus generator in roundStart)
{
generator.BeforeRoundStart();
}
}
public void EndRound()
{
Log.Verbose("Called end round");
foreach (IRoundStatus generator in roundStart)
{
generator.RoundEnd();
}
}
public void CreateWrecks()
{
Log.Verbose("Called create wrecks");
foreach (IGenerateSubmarine generateSubmarine in submarineGenerators)
{
generateSubmarine.GenerateSub();
}
}
public void SpawnNPCs()
{
Log.Verbose("Called spawn NPCS");
foreach (IGenerateNPCs generateNPCs in npcGenerators)
{
generateNPCs.SpawnNPCs();
}
}
}
}
@@ -0,0 +1,307 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.Data;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using MoreLevelContent.Networking;
using Barotrauma.Networking;
using System.Reflection.Emit;
using System.Diagnostics;
namespace MoreLevelContent.Shared.Generation
{
// Shared
public partial class MapDirector : Singleton<MapDirector>
{
internal static readonly Dictionary<Int32, LocationConnection> IdConnectionLookup = new();
internal static readonly Dictionary<LocationConnection, Int32> ConnectionIdLookup = new();
#if CLIENT
private static bool _validatedConnectionLookup = false;
#endif
public override void Setup()
{
// Map
var map_ctr_loadFromFile = AccessTools.Constructor(typeof(Map), new Type[] { typeof(CampaignMode), typeof(XElement) });
var map_ctr_createNewMap = AccessTools.Constructor(typeof(Map), new Type[] { typeof(CampaignMode), typeof(string) });
var map_save = typeof(Map).GetMethod(nameof(Map.Save));
var map_progressworld = AccessTools.Method(typeof(Map), "ProgressWorld", new Type[] { typeof(CampaignMode) });
// Leveldata
var leveldata_ctr_load = typeof(LevelData).GetConstructor(new Type[] { typeof(XElement), typeof(float?), typeof(bool) });
var leveldata_ctr_generate = typeof(LevelData).GetConstructor(new Type[] { typeof(LocationConnection) });
var leveldata_save = typeof(LevelData).GetMethod(nameof(LevelData.Save));
// GameSession
var gamesession_StartRound = typeof(GameSession).GetMethod(nameof(GameSession.StartRound), BindingFlags.Public | BindingFlags.Instance, new Type[] { typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo) });
var campaignmode_AddExtraMissions = typeof(CampaignMode).GetMethod(nameof(CampaignMode.AddExtraMissions));
// level generate
Check(map_ctr_loadFromFile, "Map Created From File");
Check(map_ctr_createNewMap, "Map Created From Seed");
Check(map_save, "map_save");
Check(map_progressworld, "map_progressworld");
Check(leveldata_ctr_load, "leveldata_ctr_load");
Check(leveldata_ctr_generate, "leveldata_ctr_generate");
Check(leveldata_save, "leveldata_save");
Check(gamesession_StartRound, "gamesession_startround");
Check(campaignmode_AddExtraMissions, "campaignmode_addextramissions");
// Map data
_ = Main.Harmony.Patch(map_ctr_loadFromFile, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnMapLoad), BindingFlags.Static | BindingFlags.NonPublic)));
_ = Main.Harmony.Patch(map_ctr_createNewMap, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnMapLoad), BindingFlags.Static | BindingFlags.NonPublic)));
// Level data
_ = Main.Harmony.Patch(leveldata_ctr_load, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnLevelDataLoad), BindingFlags.Static | BindingFlags.NonPublic)));
_ = Main.Harmony.Patch(leveldata_ctr_generate, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnLevelDataGenerate), BindingFlags.Static | BindingFlags.NonPublic)));
_ = Main.Harmony.Patch(leveldata_save, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnLevelDataSave), BindingFlags.Static | BindingFlags.NonPublic)));
// Campaign
_ = Main.Harmony.Patch(campaignmode_AddExtraMissions, postfix: new HarmonyMethod(AccessTools.Method(typeof(MapDirector), nameof(OnAddExtraMissions))));
_ = Main.Harmony.Patch(gamesession_StartRound, prefix: new HarmonyMethod(AccessTools.Method(typeof(MapDirector), nameof(OnPreRoundStart))));
_ = Main.Harmony.Patch(gamesession_StartRound, postfix: new HarmonyMethod(AccessTools.Method(typeof(MapDirector), nameof(OnPostRoundStart))));
extraMissions = AccessTools.Field(typeof(CampaignMode), "extraMissions");
_ = Main.Harmony.Patch(map_progressworld, postfix: new HarmonyMethod(AccessTools.Method(typeof(MapDirector), nameof(OnProgressWorld))));
Modules.Add(new ConstructionMapModule());
Modules.Add(new DistressMapModule());
Modules.Add(new PirateOutpostMapModule());
Modules.Add(new CablePuzzleMapModule());
Modules.Add(new MapFeatureModule());
Log.Debug("Map direction setup");
SetupProjSpecific();
}
public void ForceDistress()
{
var distressModule = (DistressMapModule)Modules.Find(m => m.GetType() == typeof(DistressMapModule));
distressModule.TrySpawnEvent(GameMain.GameSession.Map, true);
}
public void SetForcedDistressMission(bool force, string identifier)
{
var distressModule = (DistressMapModule)Modules.Find(m => m.GetType() == typeof(DistressMapModule));
distressModule.ForceSpawnMission = force;
distressModule.ForcedMissionIdentifier = identifier;
}
partial void SetupProjSpecific();
public enum MapSyncState
{
Syncing,
NotCampaign,
MapNotCreated,
MapSynced
}
#if CLIENT
private void ConnectionEqualityCheck(object[] args)
{
Log.Debug("Got map connection equality check!");
IReadMessage inMsg = (IReadMessage)args[0];
UInt32 connectionCount = inMsg.ReadUInt32();
if (connectionCount != IdConnectionLookup.Keys.Count)
{
KickClient($"The connection lookup generated on your client did not match the one on the server (Client Key Count: {IdConnectionLookup.Keys.Count}, Server Key Count: {connectionCount})");
return;
}
for (int i = 0; i < connectionCount - 1; i++)
{
Int32 key = inMsg.ReadInt32();
if (!IdConnectionLookup.ContainsKey(key))
{
KickClient($"The connection lookup generated on your client did not match the one on the server (Client did not contain server key {key})");
return;
}
}
Log.Debug("Equality good! Requesting map sync");
NetUtil.SendServer(NetUtil.CreateNetMsg(NetEvent.MAP_REQUEST_STATE));
}
private void KickClient(string reason)
{
Log.Error(reason);
_ = new GUIMessageBox(TextManager.Get("Error"), TextManager.GetWithVariables("MessageReadError", ("[message]", $"MLC ERROR: {reason}")))
{
DisplayInLoadingScreens = true
};
GameMain.Client.Quit();
}
#endif
#if SERVER
private void RequestConnectionEquality(object[] args)
{
if (GameMain.GameSession.GameMode.GetType() != typeof(MultiPlayerCampaign)) return;
Log.Debug("Got request for quality check");
Client c = (Client)args[1];
if (IdConnectionLookup.Count == 0)
{
c.Kick("Client requested the map equality check before the server generated it. This means the campaign map did not exist on the server when the client requested this request. Are you playing campaign mode?");
return;
}
IWriteMessage msg = NetUtil.CreateNetMsg(NetEvent.MAP_CONNECTION_EQUALITYCHECK_SENDCLIENT);
msg.WriteUInt32((uint)IdConnectionLookup.Keys.Count); // write the total count
foreach (var key in IdConnectionLookup.Keys)
{
msg.WriteUInt32((uint)key);
}
NetUtil.SendClient(msg, c.Connection);
}
#endif
internal partial void RoundEnd(CampaignMode.TransitionType transitionType);
private void Check(object info, string name)
{
if (info == null) Log.Error(name);
}
internal FieldInfo extraMissions;
internal List<MapModule> Modules = new();
private static void OnPreRoundStart(GameSession __instance, LevelData levelData)
{
foreach (var item in Instance.Modules)
{
item.OnPreRoundStart(levelData);
}
}
private static void OnPostRoundStart(GameSession __instance, LevelData levelData)
{
foreach (var item in Instance.Modules)
{
item.OnPostRoundStart(levelData);
}
}
private static void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
{
foreach (var item in Instance.Modules)
{
item.OnAddExtraMissions(__instance, levelData);
}
}
private static void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
{
foreach (var item in Instance.Modules)
{
item.OnLevelDataGenerate(__instance, locationConnection);
}
}
public static void ForceWorldStep() => OnProgressWorld(GameMain.GameSession.Map);
private static void OnProgressWorld(Map __instance)
{
foreach (var item in Instance.Modules)
{
item.OnProgressWorld(__instance);
}
}
private static void OnLevelDataLoad(LevelData __instance, XElement element)
{
LevelData_MLCData data = new();
data.LoadData(element);
__instance.AddData(data);
foreach (var item in Instance.Modules)
{
item.OnLevelDataLoad(__instance, element);
}
}
private static void OnLevelDataSave(LevelData __instance, XElement parentElement)
{
XElement levelData = (XElement)parentElement.LastNode;
LevelData_MLCData data = __instance.MLC();
data.SaveData(levelData);
foreach (var item in Instance.Modules)
{
item.OnLevelDataSave(__instance, parentElement);
}
}
private static void OnMapLoad(Map __instance)
{
Log.Debug("OnMapLoad:Postfix");
IdConnectionLookup.Clear();
ConnectionIdLookup.Clear();
// Generate location connection lookup
GenerateConnectionLookup(__instance);
#if CLIENT
if (!_validatedConnectionLookup && GameMain.IsMultiplayer)
{
_validatedConnectionLookup = true;
NetUtil.SendServer(NetUtil.CreateNetMsg(NetEvent.MAP_CONNECTION_EQUALITYCHECK_REQUEST));
Log.Debug("Sent request for connection equality");
} else
{
Log.Debug($"Skipped validating the connection lookup: {_validatedConnectionLookup}, {GameMain.IsMultiplayer}");
}
#endif
foreach (var item in Instance.Modules)
{
item.OnMapLoad(__instance);
}
}
private static void OnMapSave()
{
Log.Debug("OnMapSave");
}
internal void OnLevelGenerate(LevelData levelData, bool mirror)
{
foreach (var item in Modules)
{
item.OnLevelGenerate(levelData, mirror);
}
}
private static void GenerateConnectionLookup(Map map)
{
for (int i = 0; i < map.Connections.Count; i++)
{
var connection = map.Connections[i];
if (IdConnectionLookup.ContainsKey(i) || ConnectionIdLookup.ContainsKey(connection)) continue; // skip duplicate entries
IdConnectionLookup.Add(i, connection);
ConnectionIdLookup.Add(connection, i);
}
Log.Debug("Generated map connection lookup");
}
}
internal static class MapExtensions
{
internal static int GetZoneIndex(this Location location, Map map)
{
float zoneWidth = MapGenerationParams.Instance.Width / MapGenerationParams.Instance.DifficultyZones;
return MathHelper.Clamp((int)Math.Floor(location.MapPosition.X / zoneWidth) + 1, 1, MapGenerationParams.Instance.DifficultyZones);
}
}
}
@@ -0,0 +1,98 @@
using Barotrauma;
using MoreLevelContent.Missions;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Xml.Linq;
using System;
using System.Linq;
using Barotrauma.MoreLevelContent.Config;
namespace MoreLevelContent.Shared.Generation
{
internal partial class CablePuzzleMapModule : MapModule
{
protected override void InitProjSpecific() { }
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
{
CablePuzzleMission.SubmarineFile = null; // Clear the sub file at the start of every level
if (levelData.Type == LevelData.LevelType.Outpost)
{
Log.Debug("Ignored level due to being an outpost");
return; // Ignore outpost levels
}
LevelData_MLCData data = levelData.MLC();
if (data.RelayStationStatus == RelayStationStatus.None || !ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableRelayStations)
{
Log.Debug("No relay station");
return; // Do nothing if we don't have a relay station
}
var missions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("relayrepair")).OrderBy(m => m.UintIdentifier);
if (!missions.Any())
{
Log.Error("Failed to find any cable puzzle missions!");
return;
}
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var cablePuzzleMissionPrefab = ToolBox.SelectWeightedRandom(missions, p => p.Commonness, rand);
// Add the mission if the station is inactive
if (!__instance.Missions.Any(m => m.Prefab.Tags.Contains("relayrepair")) && data.RelayStationStatus == RelayStationStatus.Inactive)
{
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(__instance);
Mission inst = cablePuzzleMissionPrefab.Instantiate(__instance.Map.SelectedConnection.Locations, Submarine.MainSub);
_extraMissions.Add(inst);
Instance.extraMissions.SetValue(__instance, _extraMissions);
Log.Debug("Added relay staion mission to extra missions!");
return;
}
// Otherwise the station is active, so we need to assign the sub file
var configElement = cablePuzzleMissionPrefab.ConfigElement.GetChildElement("Submarine");
CablePuzzleMission.SetSub(configElement, cablePuzzleMissionPrefab);
Log.Debug("Set the relay station sub for a completed relay station");
}
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
{
LevelData_MLCData levelData = __instance.MLC();
if (levelData.HasBeaconConstruction) return; // Ignore levels with a construction site
RollForRelay(__instance, levelData, locationConnection);
}
// Map Migration
public override void OnMapLoad(Map __instance)
{
if (!__instance.Connections.Any(c => c.LevelData.MLC().HasRelayStation))
{
Log.Debug("Map has no relay stations, adding some...");
for (int i = 0; i < __instance.Connections.Count; i++)
{
var connection = __instance.Connections[i];
// See if we should generate a construction site
LevelData_MLCData extraData = connection.LevelData.MLC();
RollForRelay(connection.LevelData, extraData, connection);
}
}
else
{
Log.Debug("Map has relay stations");
}
}
private void RollForRelay(LevelData levelData, LevelData_MLCData extraData, LocationConnection locationConnection)
{
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
if (!levelData.HasBeaconStation && !levelData.MLC().HasBeaconConstruction)
{
double roll = rand.NextDouble();
// Relay stations have a 10% chance to spawn on any connection
extraData.RelayStationStatus = roll < 0.10f ? RelayStationStatus.Inactive : RelayStationStatus.None;
}
}
}
}
@@ -0,0 +1,135 @@
using Barotrauma;
using MoreLevelContent.Missions;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Xml.Linq;
using System;
using System.Linq;
using Barotrauma.MoreLevelContent.Config;
namespace MoreLevelContent.Shared.Generation
{
internal partial class ConstructionMapModule : MapModule
{
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
{
if (levelData.Type == LevelData.LevelType.Outpost) return; // Ignore outpost levels
LevelData_MLCData data = levelData.MLC();
if (data.HasBeaconConstruction && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableConstructionSites)
{
var constructionMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("beaconconstruction")).OrderBy(m => m.UintIdentifier);
if (constructionMissions.Any())
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(constructionMissions, p => (float)p.Commonness, rand);
if (!__instance.Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(__instance);
Mission inst = beaconMissionPrefab.Instantiate(__instance.Map.SelectedConnection.Locations, Submarine.MainSub);
_extraMissions.Add(inst);
Instance.extraMissions.SetValue(__instance, _extraMissions);
Log.Debug("Added beacon construction mission to extra missions!");
}
}
else
{
Log.Error("Failed to find any beacon construction missions!");
}
}
}
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
{
LevelData_MLCData levelData = __instance.MLC();
if (levelData.HasRelayStation) return;
TrySpawnBeaconConstruction(__instance, levelData, locationConnection);
}
public override void OnMapLoad(Map __instance)
{
if (!__instance.Connections.Any(c => c.LevelData.MLC().HasBeaconConstruction))
{
Log.Debug("Map has no construction sites, adding some...");
for (int i = 0; i < __instance.Connections.Count; i++)
{
var connection = __instance.Connections[i];
// See if we should generate a construction site
LevelData_MLCData extraData = connection.LevelData.MLC();
TrySpawnBeaconConstruction(connection.LevelData, extraData, connection);
}
}
else
{
Log.Debug("Map has construction sites");
}
}
private void TrySpawnBeaconConstruction(LevelData levelData, LevelData_MLCData extraData, LocationConnection locationConnection)
{
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
// Place some beacon stations
if (!levelData.IsBeaconActive)
{
double roll = rand.NextDouble();
double chance = locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Min();
extraData.HasBeaconConstruction = roll < (chance / 1.2); // construction sites have half the chance to spawn as regular beacon stations
if (extraData.HasBeaconConstruction)
{
CreateBeaconConstruction(levelData, rand, extraData);
levelData.HasBeaconStation = false;
}
}
}
private void CreateBeaconConstruction(LevelData __instance, MTRandom rand, LevelData_MLCData levelData)
{
List<SupplyType> possibleSupplies = new();
AddSupply(SupplyType.Electric, 4);
AddSupply(SupplyType.Structure, 4);
AddSupply(SupplyType.Utility, 4);
int diffClamped = (int)(__instance.Difficulty / 10);
// Always request at least one
int totalRequested = 1 + rand.Next(diffClamped + 1);
for (int i = 0; i < totalRequested; i++)
{
int index = rand.Next(possibleSupplies.Count);
SupplyType requestedSupply = possibleSupplies[index];
possibleSupplies.RemoveAt(index);
switch (requestedSupply)
{
case SupplyType.Electric:
levelData.RequestedE++;
break;
case SupplyType.Structure:
levelData.RequestedS++;
break;
case SupplyType.Utility:
levelData.RequestedU++;
break;
}
}
Log.Debug("Created a beacon construction mission");
void AddSupply(SupplyType type, int count)
{
for (int i = 0; i < count; i++)
{
possibleSupplies.Add(type);
}
}
if (levelData.HasBeaconConstruction) __instance.HasBeaconStation = false;
}
protected override void InitProjSpecific() { }
enum SupplyType
{
Electric,
Structure,
Utility
}
}
}
@@ -0,0 +1,233 @@
using Barotrauma.Networking;
using Barotrauma;
using System.Collections.Generic;
using System;
using System.Linq;
using MoreLevelContent.Shared.Data;
using Barotrauma.MoreLevelContent.Config;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared.Utils;
namespace MoreLevelContent.Shared.Generation
{
// Shared
internal partial class OldDistressMapModule : MapModule
{
public static bool ForceSpawnDistress = false;
public static string ForcedMissionIdentifier = "";
private readonly List<Mission> _internalMissionStore = new();
private static OldDistressMapModule _instance;
private static bool _spawnStartingBeacon = false;
const int MAX_DISTRESS_CREATE_ATTEMPTS = 5;
const int DISTRESS_MIN_DIST = 1;
const int DISTRESS_MAX_DIST = 3;
public OldDistressMapModule()
{
_instance = this;
InitProjSpecific();
}
internal void UpdateDistressBeacons(Map __instance)
{
foreach (LocationConnection connection in __instance.Connections.Where(c => c.LevelData.MLC().HasDistress))
{
// skip locations that are close
if (GameMain.GameSession.Campaign.Map.CurrentLocation.Connections.Contains(connection)) continue;
// TODO: When multiple world steps happen at once in a long mission
// this cause a distress to skip from active -> faint -> off before
// the player has seen any notification of it. There could be a way
// of avoiding this by counting the world steps before doing this
// instead of doing it every world step
var levelData = connection.LevelData.MLC();
levelData.DistressStepsLeft--;
if (levelData.DistressStepsLeft <= 0)
{
levelData.HasDistress = false;
SendDistressUpdate("mlc.distress.lost", connection);
}
if (levelData.DistressStepsLeft == 3) SendDistressUpdate("mlc.distress.faint", connection);
}
}
private void SendDistressUpdate(string updateType, LocationConnection connection)
{
#if CLIENT
string msg = TextManager.GetWithVariables(updateType, ("[location1]", $"‖color:gui.orange‖{connection.Locations[0].DisplayName}‖end‖"), ("[location2]", $"‖color:gui.orange‖{connection.Locations[1].DisplayName}‖end‖")).Value;
SendChatUpdate(msg);
#endif
}
public override void OnPreRoundStart(LevelData levelData)
{
_internalMissionStore.Clear();
if (levelData == null) return;
if (!Main.IsCampaign) return;
TrySpawnDistress(GameMain.GameSession.Map, _spawnStartingBeacon);
_spawnStartingBeacon = false;
if (!levelData.MLC().HasDistress && !ForceSpawnDistress)
{
Log.Debug("Level has no distress mission");
return;
}
if (TryGetMissionByTag("distress", levelData, out MissionPrefab prefab, ForcedMissionIdentifier))
{
Log.Debug("Adding distress mission");
Mission inst = prefab.Instantiate(GameMain.GameSession.Map.SelectedConnection.Locations, Submarine.MainSub);
AddExtraMission(inst); // weird
_internalMissionStore.Add(inst);
Log.Debug("Added distress mission to extra missions!");
} else
{
Log.Error("Failed to find any distress missions!");
}
}
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
{
if (!_internalMissionStore.Any()) return;
foreach (Mission mission in _internalMissionStore)
{
AddExtraMission(mission);
}
_internalMissionStore.Clear();
}
private void AddExtraMission(Mission mission)
{
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(GameMain.GameSession.GameMode);
_extraMissions.Add(mission);
Instance.extraMissions.SetValue(GameMain.GameSession.GameMode, _extraMissions);
}
public override void OnProgressWorld(Map __instance) => UpdateDistressBeacons(__instance);
private void TrySpawnDistress(Map __instance, bool force = false)
{
if (Main.IsClient) return;
if (__instance == null || __instance.Connections.Count == 0)
{
Log.Debug("Skipped trying to create a distress beacon as there was no map connections");
return;
}
// Check if we're at the max
int activeDistressCalls = __instance.Connections.Where(c => c.LevelData.MLC().HasDistress).Count();
if (activeDistressCalls > ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons)
{
if (force)
{
Log.Debug("Ignoring max distress cap due to force creation");
} else
{
Log.Debug($"Skipped creating new distress due to being at the limit ({ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons})");
return;
}
}
// If we're not, lets roll to see if we should make a new distress signal
float chance = Rand.Value(Rand.RandSync.Unsynced);
Log.InternalDebug($"{chance} >= {ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage} ({chance >= ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage})");
if (chance >= ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage && !force) return;
// Lets get a random instance to use
int seed = Rand.GetRNG(Rand.RandSync.Unsynced).Next();
Random rand = new MTRandom(seed);
// Find a location connection to spawn a distress beacon at
int dist = Rand.Range(DISTRESS_MIN_DIST, DISTRESS_MAX_DIST, Rand.RandSync.Unsynced);
LocationConnection targetConnection = WalkConnection(__instance.CurrentLocation, rand, dist);
int stepsLeft = rand.Next(4, 8);
if (!MapDirector.ConnectionIdLookup.ContainsKey(targetConnection)) return; // how does this happen?
CreateDistress(targetConnection, stepsLeft);
#if SERVER
if (GameMain.IsMultiplayer)
{
// inform clients of the new distress beacon
IWriteMessage msg = NetUtil.CreateNetMsg(NetEvent.MAP_SEND_NEWDISTRESS);
msg.WriteUInt32((uint)MapDirector.ConnectionIdLookup[targetConnection]);
msg.WriteByte((byte)stepsLeft);
NetUtil.SendAll(msg);
}
#endif
}
private void CreateDistress(LocationConnection connection, int stepsLeft)
{
connection.LevelData.MLC().HasDistress = true;
connection.LevelData.MLC().DistressStepsLeft = stepsLeft;
SendDistressUpdate("mlc.distress.new", connection);
}
internal static void ForceDistress()
{
Log.Debug("Force creating distress beacon");
_instance.TrySpawnDistress(GameMain.GameSession.Map, true);
}
}
internal partial class DistressMapModule : TimedEventMapModule
{
protected override NetEvent EventCreated => NetEvent.MAP_SEND_NEWDISTRESS;
protected override string NewEventText => "distress.new";
protected override string EventTag => "distress";
protected override int MaxActiveEvents => ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons;
protected override float EventSpawnChance => ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.DistressSpawnPercentage;
protected override int MinDistance => 1;
protected override int MaxDistance => 3;
protected override int MinEventDuration => 4;
protected override int MaxEventDuration => 8;
protected override bool ShouldSpawnEventAtStart => true;
protected override void HandleEventCreation(LevelData_MLCData data, int eventDuration)
{
data.HasDistress = true;
data.DistressStepsLeft = eventDuration;
}
protected override bool TryGetMissionPrefab(LevelData levelData, out MissionPrefab prefab)
{
prefab = null;
if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableDistressMissions) return false;
if (!ForcedMissionIdentifier.IsNullOrEmpty()) return base.TryGetMissionPrefab(levelData, out prefab);
var orderedMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(EventTag) && m.IsAllowedDifficulty(levelData.Difficulty)).OrderBy(m => m.UintIdentifier);
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
prefab = ToolBox.SelectWeightedRandom(orderedMissions, p => p.Commonness, rand);
return prefab != null;
}
protected override void HandleUpdate(LevelData_MLCData data, LocationConnection connection)
{
if (!data.HasDistress) return;
data.DistressStepsLeft--;
if (data.DistressStepsLeft == 3) AddNewsStory("distress.faint", connection);
if (data.DistressStepsLeft <= 0)
{
data.HasDistress = false;
AddNewsStory("distress.lost", connection);
}
}
protected override bool LevelHasEvent(LevelData_MLCData data) => data.HasDistress && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableDistressMissions;
}
}
@@ -0,0 +1,57 @@
using Barotrauma;
using MoreLevelContent.Missions;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Xml.Linq;
using System;
using System.Linq;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared.Utils;
namespace MoreLevelContent.Shared.Generation
{
internal partial class LostCargoMapModule : TimedEventMapModule
{
protected override NetEvent EventCreated => NetEvent.MAP_SEND_NEWCARGO;
//protected override NetEvent EventUpdated => throw new NotImplementedException();
protected override string NewEventText => "mlc.lostcargo.new";
protected override string EventTag => "lostcargo";
protected override int MaxActiveEvents => 5;
protected override float EventSpawnChance => 1;
protected override int MinDistance => 1;
protected override int MaxDistance => 2;
protected override int MinEventDuration => 4;
protected override int MaxEventDuration => 6;
protected override bool ShouldSpawnEventAtStart => true;
protected override void HandleEventCreation(LevelData_MLCData data, int eventDuration)
{
data.HasLostCargo = true;
data.CargoStepsLeft = eventDuration;
}
protected override void HandleUpdate(LevelData_MLCData data, LocationConnection connection)
{
data.CargoStepsLeft--;
if (data.CargoStepsLeft <= 0)
{
data.HasLostCargo = false;
string textTag = MLCUtils.GetRandomTag("mlc.lostcargo.tooslow", connection.LevelData);
AddNewsStory(textTag, connection);
}
}
protected override bool LevelHasEvent(LevelData_MLCData data) => data.HasLostCargo;
}
}
@@ -0,0 +1,369 @@
using Barotrauma;
using System.Collections.Generic;
using System.Xml.Linq;
using System;
using System.Linq;
using MoreLevelContent.Shared.Utils;
using static Barotrauma.Level;
using MoreLevelContent.Shared.Data;
using System.Globalization;
using static MoreLevelContent.Shared.Generation.MissionGenerationDirector;
using Barotrauma.Items.Components;
using Steamworks.Ugc;
using Microsoft.Xna.Framework;
using System.Reflection.Metadata.Ecma335;
using Barotrauma.MoreLevelContent.Config;
namespace MoreLevelContent.Shared.Generation
{
internal partial class MapFeatureModule : MapModule
{
private static List<MapFeature> _Features = new();
private static Dictionary<Identifier, MapFeature> _IdentifierToFeature = new();
private List<Location> _DisallowedLocations;
public static Submarine MapFeatureSub { get; private set; }
public static Identifier CurrentMapFeature { get; private set; }
public static MapFeature Feature { get; private set; }
protected override void InitProjSpecific()
{
// Build table of map features
_Features.Clear();
_DisallowedLocations = new();
var features = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("mapfeatureset"));
var featureEvents = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("mapfeatureeventset"));
// Parse map features
var featureDict = new Dictionary<Identifier, MapFeature>();
foreach (var item in features)
{
var config = item.ConfigElement;
foreach (var elm in config.GetChildElements("MapFeature"))
{
var feature = new MapFeature(elm, item.ContentPackage);
if (featureDict.ContainsKey(feature.Name))
{
DebugConsole.ThrowError($"ContentPackage {item.ContentPackage.Name} contains a duplicate map feature with identifier {feature.Name}, skipping...");
continue;
}
featureDict.Add(feature.Name, feature);
}
}
_IdentifierToFeature = featureDict;
_Features = featureDict.Values.OrderBy(f => f.Name).ToList();
foreach (var featureEvent in featureEvents)
{
var config = featureEvent.ConfigElement;
foreach (var eventElement in config.GetChildElements("Events"))
{
var targets = eventElement.GetAttributeIdentifierArray("features", Array.Empty<Identifier>(), true);
foreach (var target in targets)
{
if (!_IdentifierToFeature.TryGetValue(target, out MapFeature feature))
{
DebugConsole.ThrowError($"MLC: Tried to add a event set to unknown map feature {target}", contentPackage: featureEvent.ContentPackage);
continue;
}
feature.AddEventSet(eventElement, featureEvent.ContentPackage);
}
}
}
Hooks.Instance.AddUpdateAction(Update);
Log.Debug($"Collected {_Features.Count} map features");
}
void Update(float deltaTime, Camera cam)
{
if (Loaded == null) return;
if (MapFeatureSub == null) return;
if (Loaded.LevelData.MLC().MapFeatureData.Revealed) return;
if (GameSession.GetSessionCrewCharacters(CharacterType.Player).Any(c => c.Submarine == MapFeatureSub))
{
Loaded.LevelData.MLC().MapFeatureData.Revealed = true;
}
}
public static bool TryGetFeature(Identifier name, out MapFeature feature)
{
feature = null;
if (name.IsEmpty) return false;
if (!_IdentifierToFeature.ContainsKey(name))
{
DebugConsole.ThrowError($"No map feature found with identifier '{name}'");
return false;
}
feature = _IdentifierToFeature[name];
return true;
}
public override void OnLevelGenerate(LevelData levelData, bool mirror)
{
Feature = null;
MapFeatureSub = null;
var data = levelData.MLC();
if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableMapFeatures) return;
if (data.MapFeatureData.Name.IsEmpty) return;
if (!TryGetFeature(data.MapFeatureData.Name, out MapFeature feature))
{
Log.Error($"Tried to spawn non-existant map feature with identifier {data.MapFeatureData.Name}");
return;
}
Feature = feature;
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == feature.SubFile).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {feature.SubFile}");
return;
}
// We need a custom placement thing for this
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
{
AutoFill = true,
File = file,
IgnoreCrushDpeth = true,
PlacementType = feature.PlacementType,
AllowStealing = false,
SpawnPosition = feature.SpawnLocation,
Callback = OnSubSpawned
});
void OnSubSpawned(Submarine sub)
{
Log.Debug("Spawned map feature sub");
MapFeatureSub = sub;
CurrentMapFeature = feature.Name;
SubPlacementUtils.SetCrushDepth(sub, true);
sub.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Static;
sub.TeamID = CharacterTeamType.FriendlyNPC;
sub.Info.Type = SubmarineType.Outpost;
sub.GodMode = true;
sub.ShowSonarMarker = false;
}
}
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection)
{
RollForFeature(__instance, locationConnection);
}
public override void OnMapLoad(Map __instance)
{
if (!__instance.Connections.Any(c => !c.LevelData.MLC().MapFeatureData.Name.IsEmpty))
{
Log.Debug("Map has no map features, adding some...");
for (int i = 0; i < __instance.Connections.Count; i++)
{
var connection = __instance.Connections[i];
RollForFeature(connection.LevelData, connection);
}
}
else
{
Log.Debug("Map has map features");
}
}
public override void OnPostRoundStart(LevelData levelData)
{
if (levelData == null) return;
if (levelData.Type == LevelData.LevelType.Outpost) return;
var data = levelData.MLC();
if (data == null) return;
if (!TryGetFeature(data.MapFeatureData.Name, out MapFeature feature))
{
return;
}
if (MapFeatureSub == null)
{
DebugConsole.ThrowError("MLC: This level calls for a map feature but no map feature sub was spawned!");
return;
}
// Set allow stealing
if (!feature.AllowStealing)
{
foreach (var item in MapFeatureSub.GetItems(true))
{
if (item.Container?.Prefab.AllowStealingContainedItems ?? false) continue;
item.AllowStealing = false;
item.SpawnedInCurrentOutpost = true;
}
}
// No damaging map features
MapFeatureSub.GodMode = true;
if (GameMain.GameSession?.EventManager == null)
{
Log.Error("Event manager was null");
return;
}
if (feature.PossibleEvents.Count == 0) return;
var rand = new MTRandom(GameMain.GameSession.EventManager.RandomSeed);
var mapEvent = ToolBox.SelectWeightedRandom(feature.PossibleEvents, e => e.Commonness, rand);
if (rand.NextDouble() > mapEvent.Probability) return;
EventPrefab eventPrefab = EventSet.GetAllEventPrefabs().Where(p => p.Identifier == mapEvent.EventIdentifier).Distinct().OrderBy(p => p.Identifier).FirstOrDefault();
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Map Feature \"{feature.Name}\" failed to trigger an event (couldn't find an event with the identifier \"{mapEvent.EventIdentifier}\").",
contentPackage: feature.Package);
return;
}
if (GameMain.GameSession?.EventManager != null)
{
_ = CoroutineManager.StartCoroutine(SpawnMapFeatureEvent(eventPrefab));
}
}
const float WAIT_TIME = 5;
private IEnumerable<CoroutineStatus> SpawnMapFeatureEvent(EventPrefab prefab)
{
float timer = 0;
while(timer < WAIT_TIME)
{
timer += CoroutineManager.DeltaTime;
yield return CoroutineStatus.Running;
}
var newEvent = prefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
yield return CoroutineStatus.Success;
}
void RollForFeature(LevelData data, LocationConnection connection)
{
try
{
// Check if there's already a map featue nearby
if (connection.Locations.Any(l => _DisallowedLocations.Contains(l)))
{
return;
}
var rand = MLCUtils.GetRandomFromString(data.Seed);
int zoneIndex = connection.Locations[0].GetZoneIndex(GameMain.GameSession.Map);
var validFeatures = _Features.Where(f => f.CommonnessPerZone.ContainsKey(zoneIndex));
if (!validFeatures.Any()) return;
// Select feature to try and spawn
MapFeature feature = ToolBox.SelectWeightedRandom(validFeatures, f => f.CommonnessPerZone[zoneIndex], rand);
// Roll for spawn
if (feature.Chance > rand.NextDouble())
{
data.MLC().MapFeatureData.Name = feature.Name;
data.MLC().MapFeatureData.Revealed = !feature.Display.HideUntilRevealed;
_DisallowedLocations.AddRange(connection.Locations);
}
}
catch { }
}
}
internal class MapFeature
{
public MapFeature(XElement element, ContentPackage package)
{
Package = package;
SubFile = element.GetAttributeContentPath("path", package);
Name = element.GetAttributeIdentifier("identifier", "");
SpawnLocation = element.GetAttributeEnum("spawnPosition", SubSpawnPosition.PathWall);
PlacementType = element.GetAttributeEnum("placement", PlacementType.Bottom);
Chance = element.GetAttributeFloat("chance", 0);
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
ParseCommonnessPerZone(commonnessPerZoneStrs);
AllowStealing = element.GetAttributeBool("allowstealing", true);
Display = new MapFeatureDisplay(element.GetChildElement("Display"), Name);
PossibleEvents = new();
}
public ContentPackage Package { get; private set; }
public ContentPath SubFile { get; private set; }
public Identifier Name { get; private set; }
public SubSpawnPosition SpawnLocation { get; private set; }
public PlacementType PlacementType { get; private set; }
public float Chance { get; private set; }
public Dictionary<int, float> CommonnessPerZone { get; private set; }
public bool AllowStealing { get; private set; }
public MapFeatureDisplay Display { get; private set; }
public List<MapFeatureEvent> PossibleEvents { get; private set; }
public struct MapFeatureDisplay
{
public MapFeatureDisplay(XElement element, Identifier name)
{
Icon = element.GetAttributeString("icon", "");
Tooltip = element.GetAttributeString("tooltip", "");
HideUntilRevealed = element.GetAttributeBool("hideuntilrevealed", false);
DisplayName = TextManager.Get($"mapfeature.{name}.name");
}
public string Icon { get; private set; }
public string Tooltip { get; private set; }
public bool HideUntilRevealed { get; private set; }
public LocalizedString DisplayName { get; private set; }
}
public struct MapFeatureEvent
{
public MapFeatureEvent(XElement element, ContentPackage package)
{
Probability = element.GetAttributeFloat("probability", 0);
Commonness = element.GetAttributeFloat("commonness", 0);
EventIdentifier = element.GetAttributeIdentifier("identifier", "");
if (EventIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Map feature EventSet missing identifier!", contentPackage: package);
}
}
public float Probability { get; private set; }
public float Commonness { get; private set; }
public Identifier EventIdentifier { get; private set; }
}
void ParseCommonnessPerZone(string[] array)
{
CommonnessPerZone = new();
foreach (string commonnessPerZoneStr in array)
{
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
if (splitCommonnessPerZone.Length != 2 ||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
{
DebugConsole.ThrowError("Failed to read commonness values for map feature \"" + Name + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
break;
}
CommonnessPerZone[zoneIndex] = zoneCommonness;
}
}
public void AddEventSet(XElement element, ContentPackage package)
{
foreach (var item in element.GetChildElements("ScriptedEvent"))
{
PossibleEvents.Add(new MapFeatureEvent(item, package));
}
}
}
[Flags]
public enum SpawnLocation
{
Wreck = 1,
Cave = 2,
Abyss = 4,
AbyssIsland = 8
}
}
@@ -0,0 +1,158 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Shared.Generation
{
internal abstract class MapModule
{
public MapModule() => InitProjSpecific();
protected MapDirector Instance => MapDirector.Instance;
protected abstract void InitProjSpecific();
protected static bool TryGetMissionByTag(string tag, LevelData data, out MissionPrefab missionPrefab, string forceMission = "")
{
var orderedMissions = MissionPrefab.Prefabs.Where(m => m.Tags.Contains(tag)).OrderBy(m => m.UintIdentifier);
if (!string.IsNullOrEmpty(forceMission))
{
orderedMissions = orderedMissions.Where(m => m.Identifier == forceMission).OrderBy(m => m.UintIdentifier);
}
Random rand = new MTRandom(ToolBox.StringToInt(data.Seed));
missionPrefab = ToolBox.SelectWeightedRandom(orderedMissions, p => p.Commonness, rand);
return missionPrefab != null;
}
protected LocationConnection WalkConnection(Location start, Random rand, int preferedWalkDistance)
{
// Since we do a connection step at the end of the process, there's one step implict in every walk
// so we subtract a step here
int actualWalkDist = preferedWalkDistance - 1;
if (actualWalkDist <= 0)
{
return GetConnectionWeighted(start, rand);
}
Location location = WalkLocation(start, rand, actualWalkDist);
return GetConnectionWeighted(location, rand);
}
protected Location WalkLocation(Location start, Random rand, int preferedWalkDistance, LocationConnection from = null)
{
var filteredConnections = start.Connections.Where(c => c != from);
if (!filteredConnections.Any())
{
return start;
}
LocationConnection connectionToTravel = ToolBox.SelectWeightedRandom(
filteredConnections.ToList(),
filteredConnections.Select(c => GetConnectionWeight(start, c)).ToList(),
rand);
Location walkedLocation = connectionToTravel.OtherLocation(start);
preferedWalkDistance--;
// if we haven't walked our wanted dist or
if (preferedWalkDistance > 0) walkedLocation = WalkLocation(walkedLocation, rand, preferedWalkDistance);
return walkedLocation;
}
static LocationConnection GetConnectionWeighted(Location location, Random rand)
{
LocationConnection connectionToTravel = ToolBox.SelectWeightedRandom(
location.Connections,
location.Connections.Select(c => GetConnectionWeight(location, c)).ToList(),
rand);
return connectionToTravel;
}
static float GetConnectionWeight(Location location, LocationConnection c)
{
// get the destination of this connection
Location destination = c.OtherLocation(location);
if (destination == null) { return 0; }
float minWeight = 0.0001f;
float lowWeight = 0.2f;
float normalWeight = 1.0f;
float maxWeight = 2.0f;
// prefer connections we haven't passed through
float weight = c.Passed ? lowWeight : normalWeight;
if (location.Biome.AllowedZones.Contains(1))
{
// In the first biome, give a stronger preference for locations that are farther to the right)
float diff = destination.MapPosition.X - location.MapPosition.X;
if (diff < 0)
{
weight *= 0.1f;
}
else
{
float maxRelevantDiff = 300;
weight = MathHelper.Lerp(weight, maxWeight, MathUtils.InverseLerp(0, maxRelevantDiff, diff));
}
}
else if (destination.MapPosition.X > location.MapPosition.X)
{
weight *= 2.0f;
}
if (destination.IsRadiated())
{
weight *= 0.001f;
}
// Prefer locations that have been revealed
if (!destination.Discovered)
{
weight *= 0.5f;
}
return MathHelper.Clamp(weight, minWeight, maxWeight);
}
protected void SendChatUpdate(string msg)
{
#if CLIENT
if (GameMain.Client != null)
{
GameMain.Client.AddChatMessage(msg, Barotrauma.Networking.ChatMessageType.Default, TextManager.Get("mlc.navigationannouce").Value);
}
else
{
GameMain.GameSession?.GameMode.CrewManager.AddSinglePlayerChatMessage(
TextManager.Get("mlc.navigationannouce").Value,
msg,
Barotrauma.Networking.ChatMessageType.Default,
sender: null);
}
#endif
}
protected void AddNewsStory(string tag, LocationConnection connection)
{
#if CLIENT
string randomTag = MLCUtils.GetRandomTag(tag, connection.LevelData);
string msg = TextManager.GetWithVariables(randomTag, ("[location1]", $"‖color:gui.orange‖{connection.Locations[0].DisplayName}‖end‖"), ("[location2]", $"‖color:gui.orange‖{connection.Locations[1].DisplayName}‖end‖")).Value;
Log.Debug($"Added text tag {randomTag} : {msg} to news ticket");
MapDirector.Instance.AddNewsStory(msg);
#endif
}
public virtual void OnAddExtraMissions(CampaignMode __instance, LevelData levelData) { }
public virtual void OnPreRoundStart(LevelData levelData) { }
public virtual void OnPostRoundStart(LevelData levelData) { }
public virtual void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection) { }
public virtual void OnProgressWorld(Map __instance) { }
public virtual void OnLevelDataLoad(LevelData __instance, XElement element) { }
public virtual void OnLevelDataSave(LevelData __instance, XElement parentElement) { }
public virtual void OnMapLoad(Map __instance) { }
public virtual void OnLevelGenerate(LevelData levelData, bool mirror) { }
}
}
@@ -0,0 +1,174 @@
using Barotrauma;
using MoreLevelContent.Missions;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Xml.Linq;
using System;
using System.Linq;
using MoreLevelContent.Shared.Generation.Pirate;
using Microsoft.Xna.Framework;
namespace MoreLevelContent.Shared.Generation
{
internal partial class PirateOutpostMapModule : MapModule
{
List<Location> _DisallowedLocations;
protected override void InitProjSpecific()
{
_DisallowedLocations = new();
}
public override void OnLevelDataGenerate(LevelData __instance, LocationConnection locationConnection) => SetPirateData(__instance, __instance.MLC(), locationConnection);
public override void OnMapLoad(Map __instance)
{
// Map has no pirate outposts, lets generate some
if (!__instance.Connections.Any(c => c.LevelData.MLC().PirateData.HasPirateBase))
{
Log.Debug("Map has no pirate bases, adding some...");
for (int i = 0; i < __instance.Connections.Count; i++)
{
var connection = __instance.Connections[i];
SetPirateData(connection.LevelData, connection.LevelData.MLC(), connection);
}
} else
{
Log.Debug("Map has pirate bases");
}
}
void SetPirateData(LevelData levelData, LevelData_MLCData additionalData, LocationConnection locationConnection)
{
PirateSpawnData spawnData = new PirateSpawnData(levelData, locationConnection);
// Prevent pirate outposts from spawning too clustered together
if (spawnData.WillSpawn && locationConnection.Locations.Any(l => _DisallowedLocations.Contains(l)))
{
// Unless they're husked, then that's fine
if (!spawnData.Husked)
{
spawnData.WillSpawn = false;
spawnData.Husked = false;
}
}
// Add nearby locations to disallowed list
if (spawnData.WillSpawn)
{
_DisallowedLocations.AddRange(locationConnection.Locations);
}
additionalData.PirateData = new PirateData(spawnData);
}
}
internal class PirateSpawnData
{
public PirateSpawnData(LevelData levelData, LocationConnection connection)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
UpdatePirateSpawnData(rand, levelData, connection);
int spawnInt = rand.Next(100);
int huskInt = rand.Next(100);
WillSpawn = _ModifiedSpawnChance > spawnInt;
Husked = _ModifiedHuskChance > huskInt;
}
public bool WillSpawn { get; set; }
public bool Husked { get; set; }
public float PirateDifficulty { get; private set; }
public override string ToString() => $"Will Spawn: {WillSpawn}, Is Husked: {Husked}";
private float _ModifiedSpawnChance;
private float _ModifiedHuskChance;
private void UpdatePirateSpawnData(Random rand, LevelData levelData, LocationConnection connection)
{
var levelDiff = levelData.Difficulty;
float a = PirateOutpostDirector.Config.PeakSpawnChance;
float b = a / 2500;
float c = MathF.Pow(levelDiff - 50.0f, 2);
var spawnChance = (-b * c) + a;
var huskChance = MathF.Max(PirateOutpostDirector.Config.BaseHuskChance, levelDiff / 10);
ModifyChances();
_ModifiedSpawnChance = spawnChance;
_ModifiedHuskChance = huskChance;
float difficultyNoise = Math.Abs(MathHelper.Lerp(-PirateOutpostDirector.Config.DifficultyNoise, PirateOutpostDirector.Config.DifficultyNoise, (float)rand.NextDouble()));
PirateDifficulty = levelDiff + difficultyNoise;
void ModifyChances()
{
// Don't spawn bases on routes with an abyss creature
if (levelData.HasHuntingGrounds)
{
spawnChance = 0;
Log.Debug("Set spawn chance to 0 due to hunting grounds");
return;
}
foreach (var location in connection.Locations)
{
var identifier = location.Type.Identifier;
if (CompatabilityHelper.Instance.DynamicEuropaInstalled)
{
// Double spawn chance on routes leading to pirate outposts
ModifySpawn("PirateOutpost", 2);
// Don't spawn on areas leading to military
ModifySpawn("Camp", 0);
ModifySpawn("Base", 0);
ModifySpawn("Blockade", 0);
ModifyHusk("HuskgroundsDE", 10f);
ModifyHusk("OuterHuskLair", 5f);
}
// Increased chance to spawn next to natural formations
ModifySpawn("None", 1.5f);
// Increased chance to spawn next to abandoned outposts
ModifySpawn("Abandoned", 1.3f);
// Never spawn if one of the connections is a military outpost
ModifySpawn("Military", 0);
// No chance if city
ModifySpawn("City", 0f);
// Slightly reduced chance if leading to a outpost
ModifySpawn("Outpost", 0.25f);
// Slightly reduced chance if leading to a research outpost
ModifySpawn("Research", 0.25f);
// Slightly reduced chance if leading to a research outpost
ModifySpawn("Mine", 0.25f);
// Never spawn leading to the end
ModifySpawn("EndLocation", 0);
void ModifySpawn(string input, float multi)
{
if (identifier == input) spawnChance *= multi;
//Log.Debug($"M SC {input}: {spawnChance}");
}
void ModifyHusk(string input, float multi)
{
if (identifier == input) spawnChance *= multi;
//Log.Debug($"M HC {input}: {spawnChance}");
}
}
}
}
}
}
@@ -0,0 +1,188 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Config;
using Barotrauma.Networking;
using FarseerPhysics.Collision;
using Microsoft.Xna.Framework;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Shared.Generation
{
abstract partial class TimedEventMapModule : MapModule
{
public TimedEventMapModule()
{
InitProjSpecific();
}
public string ForcedMissionIdentifier = "";
public bool ForceSpawnMission = false;
// Networking
protected abstract NetEvent EventCreated { get; }
//protected abstract NetEvent EventUpdated { get; }
// Text
protected abstract string NewEventText { get; }
//protected abstract string UpdatedEventText { get; }
protected abstract string EventTag { get; }
// Config
protected abstract int MaxActiveEvents { get; }
protected abstract float EventSpawnChance { get; }
protected abstract int MinDistance { get; }
protected abstract int MaxDistance { get; }
protected abstract int MinEventDuration { get; }
protected abstract int MaxEventDuration { get; }
protected abstract bool ShouldSpawnEventAtStart { get; }
protected bool SpawnedEventAtStart
{
get => GameMain.GameSession.Campaign.CampaignMetadata.GetBoolean($"{EventTag}SpawnedStart", false);
set => GameMain.GameSession.Campaign.CampaignMetadata.SetValue($"{EventTag}SpawnedStart", value);
}
private readonly List<Mission> _internalMissionStore = new();
public override void OnProgressWorld(Map __instance)
{
foreach (LocationConnection connection in __instance.Connections)
{
// skip locations that are close
if (GameMain.GameSession.Campaign.Map.CurrentLocation.Connections.Contains(connection)) continue;
HandleUpdate(connection.LevelData.MLC(), connection);
}
if (ShouldSpawnEventAtStart && !SpawnedEventAtStart)
{
TrySpawnEvent(GameMain.GameSession.Map, true);
SpawnedEventAtStart = true;
}
else
{
TrySpawnEvent(GameMain.GameSession.Map, false);
}
}
public override void OnPreRoundStart(LevelData levelData)
{
if (levelData == null) return;
if (!Main.IsCampaign) return;
if (!LevelHasEvent(levelData.MLC()) && !ForceSpawnMission)
{
Log.Debug($"Level has no {EventTag}");
return;
}
// Never try to spawn a timed event on an outpost level
if (levelData.Type == LevelData.LevelType.Outpost) return;
if (TryGetMissionPrefab(levelData, out MissionPrefab prefab))
{
Log.Debug($"Adding {EventTag} mission");
Mission inst = prefab.Instantiate(GameMain.GameSession.Map.SelectedConnection.Locations, Submarine.MainSub);
AddExtraMission(inst); // we have to double add missions to make them work correctly
_internalMissionStore.Add(inst);
Log.Debug($"Added {EventTag} mission to extra missions!");
}
else
{
Log.Error($"Failed to find any {EventTag} missions!");
}
}
protected virtual bool TryGetMissionPrefab(LevelData levelData, out MissionPrefab prefab)
{
return TryGetMissionByTag(EventTag, levelData, out prefab, ForcedMissionIdentifier);
}
protected void AddExtraMission(Mission mission)
{
List<Mission> _extraMissions = (List<Mission>)Instance.extraMissions.GetValue(GameMain.GameSession.GameMode);
_extraMissions.Add(mission);
Instance.extraMissions.SetValue(GameMain.GameSession.GameMode, _extraMissions);
}
public override void OnAddExtraMissions(CampaignMode __instance, LevelData levelData)
{
if (!_internalMissionStore.Any()) return;
foreach (Mission mission in _internalMissionStore)
{
AddExtraMission(mission);
}
_internalMissionStore.Clear();
}
protected abstract void HandleUpdate(LevelData_MLCData data, LocationConnection connection);
public void TrySpawnEvent(Map __instance, bool force = false)
{
if (Main.IsClient) return;
// Check if we're at the max
int activeEvents = __instance.Connections.Where(c => LevelHasEvent(c.LevelData.MLC())).Count();
if (activeEvents > MaxActiveEvents)
{
if (force)
{
Log.Debug($"Ignoring max {EventTag} cap due to force creation");
}
else
{
Log.Verbose($"Skipped creating new {EventTag} due to being at the limit ({MaxActiveEvents})");
return;
}
}
// If we're not, lets roll to see if we should make a new distress signal
float chance = Rand.Value(Rand.RandSync.Unsynced);
Log.InternalDebug($"{chance} <= {EventSpawnChance} ({chance <= EventSpawnChance}) for {EventTag}");
if (chance >= EventSpawnChance && !force) return;
// Lets get a random instance to use
int seed = Rand.GetRNG(Rand.RandSync.Unsynced).Next();
Random rand = new MTRandom(seed);
// Find a location connection to spawn a distress beacon at
int wantedEventSpawnDistance = Rand.Range(MinDistance, MaxDistance, Rand.RandSync.Unsynced);
LocationConnection targetConnection = WalkConnection(__instance.CurrentLocation, rand, wantedEventSpawnDistance);
if (targetConnection == null)
{
Log.Warn($"Failed to spawn new {EventTag} due to target connection being null.");
return;
}
int duration = rand.Next(MinEventDuration, MaxEventDuration);
if (!MapDirector.ConnectionIdLookup.ContainsKey(targetConnection)) return; // how does this happen?
CreateEvent(targetConnection, duration);
#if SERVER
if (GameMain.IsMultiplayer)
{
// inform clients of the new distress beacon
IWriteMessage msg = NetUtil.CreateNetMsg(NetEvent.MAP_SEND_NEWDISTRESS);
msg.WriteUInt32((uint)MapDirector.ConnectionIdLookup[targetConnection]);
msg.WriteByte((byte)duration);
NetUtil.SendAll(msg);
}
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
{
campaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
#endif
}
protected abstract bool LevelHasEvent(LevelData_MLCData data);
protected void CreateEvent(LocationConnection connection, int eventDuration)
{
HandleEventCreation(connection.LevelData.MLC(), eventDuration);
AddNewsStory(NewEventText, connection);
}
protected abstract void HandleEventCreation(LevelData_MLCData data, int eventDuration);
}
}
@@ -0,0 +1,631 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Config;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using MoreLevelContent.Missions;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation.Interfaces;
using MoreLevelContent.Shared.Store;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Voronoi2;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Generation
{
public class MissionGenerationDirector : GenerationDirector<MissionGenerationDirector>, IGenerateSubmarine, IGenerateNPCs
{
public override bool Active => true;
readonly Queue<SubmarineSpawnRequest> SubCreationQueue = new();
readonly Queue<DecoSpawnRequest> DecoCreationQueue = new();
readonly Queue<(Submarine Sub, float SkipChance)> AutoFillQueue = new();
internal delegate void OnSubmarineCreated(Submarine createdSubmarine);
internal delegate void OnDecoCreated(List<Submarine> decoItems, Cave decoratedCave);
public static List<(Vector2, Vector2)> DebugPoints = new();
internal static void RequestSubmarine(SubmarineSpawnRequest info) =>
Instance.SubCreationQueue.Enqueue(info);
internal static void RequestStaticSubmarine(ContentFile contentFile, OnSubmarineCreated onSubmarineCreated, bool autoFill = true) =>
Instance.RequestStaticSub(contentFile, onSubmarineCreated, autoFill);
internal static void RequestSubmarine(ContentFile contentFile, OnSubmarineCreated onSubmarineCreated, bool autoFill = true) =>
Instance.RequestSub(contentFile, onSubmarineCreated, autoFill);
internal static void RequestDecorate(List<ContentFile> files, OnDecoCreated onDecoCreated, bool autoFill = false) => Instance.RequestDeco(files, onDecoCreated, autoFill);
struct DecoSpawnRequest
{
public List<ContentFile> ContentFiles;
public OnDecoCreated Callback;
public bool AutoFill;
public DecoSpawnRequest(List<ContentFile> contentFiles, OnDecoCreated callback, bool autoFill)
{
ContentFiles = contentFiles;
Callback = callback;
AutoFill = autoFill;
}
}
internal struct SubmarineSpawnRequest
{
public ContentFile File;
public OnSubmarineCreated Callback;
public bool AutoFill = false;
public bool AllowStealing = true;
public AutoFillPrefix Prefix = AutoFillPrefix.None;
public SubSpawnPosition SpawnPosition = SubSpawnPosition.PathWall;
public PlacementType PlacementType = PlacementType.Bottom;
public bool IgnoreCrushDpeth = true;
public float SkipItemChance = 0.5f;
public SubmarineSpawnRequest()
{
File = null;
Callback = null;
}
public enum AutoFillPrefix
{
None,
Wreck,
Abandoned
}
}
void RequestStaticSub(ContentFile contentFile, OnSubmarineCreated onSubmarineCreated, bool autoFill)
{
SubCreationQueue.Enqueue(new SubmarineSpawnRequest()
{
File = contentFile,
Callback = onSubmarineCreated,
AutoFill = autoFill
});
Log.Debug("Enqueued spawn request for submarine");
}
void RequestSub(ContentFile contentFile, OnSubmarineCreated onSubmarineCreated, bool autoFill)
{
SubCreationQueue.Enqueue(new SubmarineSpawnRequest() {
File = contentFile,
Callback = onSubmarineCreated,
AutoFill = autoFill,
SpawnPosition = SubSpawnPosition.PathWall
});
Log.Debug("Enqueued spawn request for submarine on path");
}
void RequestDeco(List<ContentFile> files, OnDecoCreated onDecoCreated, bool autoFill)
{
DecoCreationQueue.Enqueue(new DecoSpawnRequest(files, onDecoCreated, autoFill));
Log.Debug($"Enqueued spawn request for cave decoration with {files.Count} items");
}
public void GenerateSub()
{
SpawnConstructionSite();
SpawnRelayStation();
SpawnRequestedSubs();
DecorateCaves();
}
void SpawnRequestedSubs()
{
DebugPoints.Clear();
while (SubCreationQueue.Count > 0)
{
SubmarineSpawnRequest request = SubCreationQueue.Dequeue();
string subName = System.IO.Path.GetFileNameWithoutExtension(request.File.Path.Value);
Submarine submarine;
if (request.SpawnPosition == SubSpawnPosition.PathWall)
{
submarine = SpawnSubOnPath(subName, request.File, ignoreCrushDepth: request.IgnoreCrushDpeth, placementType: request.PlacementType);
if (submarine == null)
{
Log.Error("Failed to spawn submarine at requested location, spawning it anywhere...");
submarine = SpawnSub(request.File);
if (Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out var potentialSpawnPos))
{
submarine.SetPosition(submarine.FindSpawnPos(potentialSpawnPos.Position.ToVector2()));
} else
{
submarine.SetPosition(submarine.FindSpawnPos(Level.Loaded.EndPosition));
}
}
} else
{
if (request.SpawnPosition == SubSpawnPosition.AbyssIsland)
{
submarine = PositionAbyssCave(request);
} else
{
submarine = SpawnSub(request.File);
}
}
if (submarine != null)
{
Log.Debug($"Spawned requested submarine {subName}");
request.Callback.Invoke(submarine);
if (request.AutoFill)
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine != submarine) { continue; }
if (item.NonInteractable) { continue; }
item.AllowStealing = request.AllowStealing;
if (item.GetRootInventoryOwner() is Character) { continue; }
IEnumerable<Identifier> splitTags = item.Tags.Split(',').ToIdentifiers();
int len = splitTags.Count();
foreach (var tag in splitTags)
{
if (request.Prefix != SubmarineSpawnRequest.AutoFillPrefix.None)
{
item.AddTag($"{request.Prefix}{tag}");
}
}
foreach (var container in item.GetComponents<ItemContainer>())
{
container.AutoFill = true;
}
}
Log.Debug("Filed auto fill request for submarine");
AutoFillQueue.Enqueue((submarine, request.SkipItemChance));
}
}
else
{
Log.Error($"Failed to spawn requested submarine!");
}
}
}
void SpawnConstructionSite()
{
if (Level.Loaded.LevelData.MLC().HasBeaconConstruction && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableConstructionSites)
{
Submarine beacon = SpawnSubOnPath("Construction Site", BeaconConstStore.Instance.GetBeaconForLevel(), ignoreCrushDepth: true, SubmarineType.EnemySubmarine);
beacon.PhysicsBody.BodyType = BodyType.Static;
beacon.Info.Type = SubmarineType.BeaconStation;
beacon.ShowSonarMarker = false;
Level.Loaded.MLC().BeaconConstructionStation = beacon;
Item storageItem = Item.ItemList.Find(it => it.Submarine == beacon && it.GetComponent<ItemContainer>() != null && it.Tags.Contains("dropoff"));
if (storageItem == null)
{
Log.Error($"Unable to find the drop off point for beacon construction {beacon.Info.Name}!");
return;
}
Level.Loaded.MLC().DropOffPoint = storageItem;
}
}
void SpawnRelayStation()
{
if (Loaded.LevelData.MLC().RelayStationStatus == RelayStationStatus.None) return;
Log.Debug($"Trying to spawn relay station for status {Loaded.LevelData.MLC().RelayStationStatus}");
if (CablePuzzleMission.SubmarineFile == null)
{
Log.Debug("Sub file was null, how did this happen? Skipping attempting to make the relay station.");
return;
}
Submarine relayStation = SpawnSubOnPath("Relay Station", CablePuzzleMission.SubmarineFile, ignoreCrushDepth: true, SubmarineType.EnemySubmarine, PlacementType.Top);
if (relayStation == null)
{
Log.Error("Failed to spawn relay station");
return;
}
Log.Debug("Spawned relay station");
relayStation.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Static;
relayStation.TeamID = CharacterTeamType.FriendlyNPC;
relayStation.ShowSonarMarker = false;
Loaded.MLC().RelayStation = relayStation;
}
void DecorateCaves()
{
Log.Debug("Decorating Caves");
while (DecoCreationQueue.Count > 0)
{
var request = DecoCreationQueue.Dequeue();
Cave cave = Loaded.Caves.GetRandom(Rand.RandSync.ServerAndClient);
List<Submarine> deco = new();
foreach (var file in request.ContentFiles)
{
try
{
Log.Debug($"Trying to spawn deco {file.Path}");
var tunnel = cave.Tunnels.GetRandom(Rand.RandSync.ServerAndClient);
var pos = tunnel.WayPoints.GetRandom(Rand.RandSync.ServerAndClient);
Submarine sub = SpawnSubAtPosition("Cave Deco", file, pos.Position);
if (sub != null) deco.Add(sub);
}
catch (Exception e)
{
Log.Error(e.ToString());
}
}
request.Callback?.Invoke(deco, cave);
}
}
public override void Setup() => BeaconConstStore.Instance.Setup();
public void SpawnNPCs()
{
Log.Debug("Auto filling subs");
if (AutoFillQueue.Count == 0) Log.Debug("No subs in autofill queue");
while (AutoFillQueue.Count > 0)
{
try
{
(Submarine, float)request = AutoFillQueue.Dequeue();
AutofillSub(request.Item1, request.Item2);
Log.Debug("Auto filled submarine");
} catch(Exception e)
{
Log.Debug(e.ToString());
}
}
}
void PositionAbyss(Submarine sub)
{
Log.Error("Position Type Abyss is not implemented");
}
Submarine PositionAbyssCave(SubmarineSpawnRequest request)
{
var subDoc = SubmarineInfo.OpenFile(request.File.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
SubmarineInfo info = new SubmarineInfo(request.File.Path.Value);
int maxAttempts = 25;
int attemptsLeft = maxAttempts;
var rand = MLCUtils.GetLevelRandom();
var validIslands = Loaded.AbyssIslands.Where(i => !Loaded.Caves.Any(c => c.Area.Intersects(i.Area))).ToList();
if (!validIslands.Any())
{
// If we found NO islands, tolerate spawning on caves
validIslands = Loaded.AbyssIslands;
}
Vector2 startPoint = default;
bool foundPos = false;
int offset = 1;
int dir = request.PlacementType == PlacementType.Bottom ? 1 : -1;
SpawnOnIsland(validIslands);
if (!foundPos)
{
Log.Error("Failed to find a spawn position");
return null;
}
Submarine sub = new Submarine(info);
sub.SetPosition(startPoint, forceUndockFromStaticSubmarines: false);
return sub;
void SpawnOnIsland(List<AbyssIsland> islands)
{
var island = validIslands.GetRandom(rand);
if (island == null)
{
Log.Debug("Failed to find a valid island to spawn on");
return;
}
startPoint = island.Area.Center.ToVector2();
// Check if position is overlapping
while (attemptsLeft > 0)
{
if (TryPosition())
{
foundPos = true;
return;
}
offset++;
}
// We found no position for this island
// Remove it and try again if we still have islands left
_ = validIslands.Remove(island);
attemptsLeft = maxAttempts;
if (islands.Count == 0)
{
Log.Error("NO valid abyss islands found :((");
return;
}
SpawnOnIsland(islands);
bool TryPosition()
{
float halfHeight = subBorders.Height / 10;
float startY = startPoint.Y + (halfHeight * offset * dir);
float x1 = startPoint.X - (subBorders.Width / 2);
float x2 = startPoint.X;
float x3 = startPoint.X + (subBorders.Width / 2);
Vector2 rayStart = new Vector2(x2, startY);
Vector2 to = new Vector2(x2, startY - (halfHeight * dir));
DebugPoints.Add((rayStart, to));
Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
customPredicate: f => f.Body?.UserData is VoronoiCell cell,
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
allowInsideFixture: true) != null)
{
return false;
}
else
{
startPoint = new Vector2(to.X, to.Y + ((subBorders.Height / 2) * dir));
//for (int i = 0; i < 50; i++)
//{
// if (Slam()) break;
//}
return true;
}
bool Slam()
{
if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
customPredicate: f => f.Body?.UserData is VoronoiCell cell,
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
allowInsideFixture: true) != null)
{
return false;
} else
{
return true;
}
}
}
}
}
private Submarine SpawnSub(ContentFile contentFile)
{
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value);
Submarine sub = new Submarine(info);
return sub;
}
private Submarine SpawnSubAtPosition(string subName, ContentFile contentFile, Vector2 spawnPoint)
{
var tempSW = new Stopwatch();
// Min distance between a sub and the start/end/other sub.
var subDoc = SubmarineInfo.OpenFile(contentFile.Path.Value);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
Point paddedDimensions = new Point(subBorders.Width, subBorders.Height);
var positions = new List<Vector2>();
var rects = new List<Rectangle>();
int maxAttempts = 50;
int attemptsLeft = maxAttempts;
bool success = true;
var allCells = Loaded.GetAllCells();
while (attemptsLeft > 0)
{
if (attemptsLeft < maxAttempts)
{
Log.Debug($"Failed to position the sub {subName}. Trying again.");
}
attemptsLeft--;
success = TryPositionSub(subBorders, subName, ref spawnPoint);
if (success)
{
break;
}
else
{
positions.Clear();
}
}
tempSW.Stop();
if (success)
{
Log.Debug($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
tempSW.Restart();
try
{
SubmarineInfo info = new SubmarineInfo(contentFile.Path.Value);
Submarine sub = new Submarine(info);
tempSW.Stop();
Log.Debug($"Sub {sub.Info.Name} loaded in {tempSW.ElapsedMilliseconds} (ms)");
sub.PhysicsBody.BodyType = BodyType.Static;
sub.SetPosition(spawnPoint);
return sub;
}
catch (Exception e)
{
Log.Error(e.ToString());
return null;
}
}
else
{
Log.Error($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).");
return null;
}
bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint)
{
positions.Add(spawnPoint);
bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint);
positions.Add(spawnPoint);
bool leftSideBlocked = IsSideBlocked(subBorders, false);
bool rightSideBlocked = IsSideBlocked(subBorders, true);
int step = 5;
if (rightSideBlocked && !leftSideBlocked)
{
bottomFound = TryMove(subBorders, ref spawnPoint, -step);
}
else if (leftSideBlocked && !rightSideBlocked)
{
bottomFound = TryMove(subBorders, ref spawnPoint, step);
}
else if (!bottomFound)
{
if (!leftSideBlocked)
{
bottomFound = TryMove(subBorders, ref spawnPoint, -step);
}
else if (!rightSideBlocked)
{
bottomFound = TryMove(subBorders, ref spawnPoint, step);
}
else
{
Log.Debug($"Invalid position {spawnPoint}. Does not touch the ground.");
return false;
}
}
positions.Add(spawnPoint);
bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50));
if (isBlocked)
{
rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size));
Log.Debug($"Invalid position {spawnPoint}. Blocked by level walls.");
}
else if (!bottomFound)
{
Log.Debug($"Invalid position {spawnPoint}. Does not touch the ground.");
}
else
{
var sp = spawnPoint;
}
return !isBlocked && bottomFound;
bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount)
{
float maxMovement = 5000;
float totalAmount = 0;
bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint);
while (!IsSideBlocked(subBorders, amount > 0))
{
foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint);
totalAmount += amount;
spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y);
if (Math.Abs(totalAmount) > maxMovement)
{
Debug.WriteLine($"Moving the sub {subName} failed.");
break;
}
}
return foundBottom;
}
}
bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint)
{
// Shoot five rays and pick the highest hit point.
int rayCount = 5;
var positions = new Vector2[rayCount];
bool hit = false;
for (int i = 0; i < rayCount; i++)
{
float quarterWidth = subBorders.Width * 0.25f;
Vector2 rayStart = spawnPoint;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y);
break;
case 3:
rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y);
break;
case 4:
rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y);
break;
}
var simPos = ConvertUnits.ToSimUnits(rayStart);
var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1),
customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !Loaded.ExtraWalls.Any(w => w.Body == f.Body),
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
if (body != null)
{
positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2);
hit = true;
}
}
float highestPoint = positions.Max(p => p.Y);
spawnPoint = new Vector2(spawnPoint.X, highestPoint);
return hit;
}
bool IsSideBlocked(Rectangle subBorders, bool front)
{
// Shoot three rays and check whether any of them hits.
int rayCount = 3;
Vector2 halfSize = subBorders.Size.ToVector2() / 2;
Vector2 quarterSize = halfSize / 2;
var positions = new Vector2[rayCount];
for (int i = 0; i < rayCount; i++)
{
float dir = front ? 1 : -1;
Vector2 rayStart;
Vector2 to;
switch (i)
{
case 1:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
break;
case 2:
rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y);
to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y);
break;
case 0:
default:
rayStart = spawnPoint;
to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y);
break;
}
Vector2 simPos = ConvertUnits.ToSimUnits(rayStart);
if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to),
customPredicate: f => f.Body?.UserData is VoronoiCell cell,
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
{
return true;
}
}
return false;
}
bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1)
{
float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared();
Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size);
return Loaded.GetAllCells().Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
}
}
public enum SubSpawnPosition
{
Path,
PathWall,
Abyss,
AbyssIsland
}
}
}
@@ -0,0 +1,31 @@
using MoreLevelContent.Shared.Generation.Interfaces;
using Barotrauma;
using System.Linq;
using static Barotrauma.CheckMissionAction;
namespace MoreLevelContent.Shared.Generation.Pirate
{
public class PirateEncounterDirector : GenerationDirector<PirateEncounterDirector>
{
public override bool Active => false;
private MissionPrefab pirateMission;
public override void Setup()
{
pirateMission = MissionPrefab.Prefabs.Find(m => m.Type == "Pirate");
if (pirateMission == null)
{
Log.Error("Couldn't find a pirate mission!");
}
}
//public void BeforeRoundStart()
//{
// Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
// var mission = pirateMission.Instantiate(locations, Submarine.MainSub);
// var missionList = GameMain.GameSession.GameMode.Missions.ToList();
//}
//
//public void RoundEnd() { }
}
}
@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Store;
using MoreLevelContent.Shared.Utils;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Generation.Pirate
{
internal class PirateOutpost
{
public PirateBaseRelationStatus Status { get; private set; }
private readonly List<Character> characters;
private readonly Dictionary<Character, List<Item>> characterItems;
private readonly PirateNPCSetDef selectedPirateSet;
private readonly PirateOutpostDef _SelectedSubmarine;
private readonly float _Difficulty;
private Submarine _Sub;
private Character _Commander;
readonly PirateData _Data;
private bool _Generated = false;
private bool _Revealed = false;
public PirateOutpost(PirateData data, string filePath, string seed)
{
characters = new List<Character>();
characterItems = new Dictionary<Character, List<Item>>();
selectedPirateSet = PirateStore.Instance.GetNPCSetForDiff(data.Difficulty, seed);
_SelectedSubmarine = filePath.IsNullOrEmpty()
? PirateStore.Instance.GetPirateOutpostForDiff(data.Difficulty, seed)
: PirateStore.Instance.FindOutpostWithPath(filePath);
Log.Verbose($"Selected NPC set {selectedPirateSet.Prefab.Name}");
Log.Verbose($"Selected outpost {_SelectedSubmarine.SubInfo.FilePath}");
_Difficulty = data.Difficulty;
_Data = data;
_Revealed = data.Revealed;
}
public void Update(float deltaTime)
{
if (Main.IsClient || _Revealed) return;
if (_Sub == null) return;
float minDist = Sonar.DefaultSonarRange / 2f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, _Sub.WorldPosition) < minDist * minDist)
{
_Revealed = true;
Log.Debug("Revealed pirate base");
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, _Sub.WorldPosition) < minDist * minDist)
{
_Revealed = true;
Log.Debug("Revealed pirate base");
break;
}
}
}
public void Generate()
{
if (_Generated) return;
_Generated = true;
characters.Clear();
characterItems.Clear();
_Sub = PirateOutpostDirector.Instance.SpawnSubOnPath("Pirate Outpost", _SelectedSubmarine.SubInfo.FilePath, ignoreCrushDepth: true, placementType: _SelectedSubmarine.PlacementType);
if (_Sub == null)
{
Log.Error("Failed to place pirate outpost! Skipping...");
return;
}
_Sub.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static;
_Sub.Info.DisplayName = TextManager.Get("mlc.pirateoutpost");
_Sub.ShowSonarMarker = PirateOutpostDirector.Config.DisplaySonarMarker;
if (CompatabilityHelper.Instance.DynamicEuropaInstalled)
{
SetupDE();
} else
{
Status = PirateBaseRelationStatus.Hostile;
}
_Sub.TeamID = CharacterTeamType.None;
_Sub.Info.Type = SubmarineType.EnemySubmarine;
switch (Status)
{
case PirateBaseRelationStatus.Neutral:
// ToDo: Allow bribing the pirates
break;
case PirateBaseRelationStatus.Friendly:
_Sub.TeamID = CharacterTeamType.FriendlyNPC;
break;
case PirateBaseRelationStatus.Hostile:
default:
break;
}
Log.InternalDebug($"Spawned a pirate base with name {_Sub.Info.Name}");
void SetupDE()
{
switch (CompatabilityHelper.Instance.BanditFaction.Reputation.Value)
{
case >= +13:
Status = PirateBaseRelationStatus.Friendly;
break;
case <= -13:
Status = PirateBaseRelationStatus.Hostile;
break;
default:
Status = PirateBaseRelationStatus.Neutral;
break;
}
Log.Debug($"Base status: {Status}, rep: {CompatabilityHelper.Instance.BanditFaction.Reputation.Value}");
}
}
StructureDamageTracker damageTracker;
private bool threshholdCrossed;
void SetupActive()
{
if (_Sub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.PowerUpImmediately();
reactor.FuelConsumptionRate = 0; // never run out of fuel
// Make sure the reactor doesn't explode lol
if (CompatabilityHelper.Instance.HazardousReactorsInstalled)
{
reactor.Item.InvulnerableToDamage = true;
// If we have a different reactor mod installed (e.g. immersive repairs) make the reactor not degrade
} else if (CompatabilityHelper.Instance.ReactorModInstalled)
{
Repairable repair = reactor.Item.GetComponent<Repairable>();
if (repair != null)
{
repair.DeteriorationSpeed = 0;
repair.MinDeteriorationDelay = float.PositiveInfinity;
repair.MinDeteriorationCondition = 100;
}
}
}
if (Status != PirateBaseRelationStatus.Hostile)
{
damageTracker = new StructureDamageTracker(_Sub);
damageTracker.ThresholdCrossed += DamageTracker_ThresholdCrossed;
damageTracker.DamageAfterThreshold += DamageTracker_DamageAfterThreshold;
threshholdCrossed = false;
}
}
private void DamageTracker_DamageAfterThreshold(float amount)
{
}
private void DamageTracker_ThresholdCrossed()
{
// Send message
threshholdCrossed = true;
Log.Debug("Threshold Crossed");
}
void SetupDestroyed()
{
_Sub.Info.Type = SubmarineType.Outpost;
if (Main.IsClient) return;
var baseItems = _Sub.GetItems(alsoFromConnectedSubs: false);
if (baseItems.Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.AutoTemp = false;
reactor.PowerOn = false;
}
var waypoints = _Sub.GetWaypoints(false).Where(wp => wp.SpawnType == SpawnType.Human || wp.SpawnType == SpawnType.Cargo);
foreach (var wp in waypoints)
{
Level.Loaded.PositionsOfInterest.Add(new Level.InterestingPosition(wp.WorldPosition.ToPoint(), Level.PositionType.Wreck));
}
//break powered items
foreach (Item item in baseItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
{
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.8f)
{
item.Condition *= Rand.Range(0f, 0.2f, Rand.RandSync.Unsynced);
}
}
// min walls to damage
var walls = Structure.WallList.Where(s => s.Submarine == _Sub);
int wallCount = walls.Count();
int damagedWallCount = Rand.Range(wallCount / 2, wallCount, Rand.RandSync.Unsynced);
var avaliableWalls = walls.ToList();
for (int i = 0; i < damagedWallCount; i++)
{
var targetWall = avaliableWalls.GetRandom(Rand.RandSync.Unsynced);
_ = avaliableWalls.Remove(targetWall);
int sectionsToDamage = Rand.Range(targetWall.SectionCount / 4, targetWall.SectionCount, Rand.RandSync.Unsynced);
while (sectionsToDamage > 0)
{
sectionsToDamage--;
targetWall.AddDamage(sectionsToDamage, Rand.Range(targetWall.MaxHealth * 0.75f, targetWall.MaxHealth, Rand.RandSync.Unsynced));
}
}
}
public void Populate()
{
if (_Data.Status == PirateOutpostStatus.Active)
{
SetupActive();
}
else
{
SetupDestroyed();
}
// Don't spawn crew on destroyed outposts
if (_Data.Status == PirateOutpostStatus.Destroyed) return;
bool commanderAssigned = false;
Log.InternalDebug("Spawning Pirates");
if (_Sub == null)
{
Log.Error("Pirate outpost was null! Aborting NPC spawn...");
return;
}
// Don't spawn more pirates than there are diving suits on the outpost
int maxSpawns = Item.ItemList.Where(it => it.Submarine == _Sub && (it.Prefab.Identifier == "divingsuitlocker2" || it.Prefab.Identifier == "divingsuitlocker")).Count();
int currentSpawns = 0;
XElement characterConfig = selectedPirateSet.Prefab.ConfigElement.GetChildElement("Characters");
XElement characterTypeConfig = selectedPirateSet.Prefab.ConfigElement.GetChildElement("CharacterTypes");
float addedMissionDifficultyPerPlayer = selectedPirateSet.Prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
int playerCount = 1;
#if SERVER
playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count();
#endif
if (!PirateOutpostDirector.Config.AddDiffPerPlayer) addedMissionDifficultyPerPlayer = 0;
float enemyCreationDifficulty = _Difficulty + (playerCount * addedMissionDifficultyPerPlayer);
Random rand = new MTRandom(ToolBox.StringToInt(Level.Loaded.Seed));
foreach (XElement element in characterConfig.Elements())
{
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
// Set hard cap on pirates to prevent lag when there's a lot of mods installed
amountCreated = Math.Max(amountCreated, 8);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType =
characterTypeConfig.Elements()
.Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty))
.FirstOrDefault();
if (characterType == null)
{
DebugConsole.NewMessage($"No characters defined in the loaded XML!!");
return;
}
//TODO: Varient elements don't seem to work
XElement variantElement = CharacterUtils.GetRandomDifficultyModifiedElement(characterType, _Difficulty, 25f, rand);
if (variantElement == null)
{
Log.Error("Varient element was null!");
continue;
}
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
// don't spawn more than the max diving suits on the outpost
if (currentSpawns > maxSpawns && (commanderAssigned || isCommander)) break;
HumanPrefab character = CharacterUtils.GetHumanPrefabFromElement(variantElement);
if (character == null)
{
Log.Error($"Character was null!\nTYPE: {characterType}, VARIANT: {variantElement}");
continue;
}
var team = Status == PirateBaseRelationStatus.Friendly ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None;
Character spawnedCharacter = CharacterUtils.CreateHuman(character, characters, characterItems, _Sub, team, null);
if (CompatabilityHelper.Instance.DynamicEuropaInstalled) DESetup();
if (!commanderAssigned)
{
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.InitShipCommandManager();
commanderAssigned = true;
_Commander = spawnedCharacter;
Log.Verbose("Spawned Commader");
}
}
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.Prefab.Identifier == "idcard")
{
item.AddTag("id_pirate");
}
// Why are you stealing from your friends :(
if (Status == PirateBaseRelationStatus.Friendly)
{
item.AllowStealing = false;
item.SpawnedInCurrentOutpost = true;
}
}
currentSpawns++;
void DESetup()
{
spawnedCharacter.Faction = "bandits";
}
}
}
if (Status == PirateBaseRelationStatus.Friendly)
{
foreach (var item in _Sub.GetItems(true))
{
if (item.Container?.Prefab.AllowStealingContainedItems ?? false) continue;
item.AllowStealing = false;
item.SpawnedInCurrentOutpost = true;
}
_Sub.TeamID = CharacterTeamType.FriendlyNPC;
}
HuskOutpost();
}
internal void OnRoundEnd(LevelData levelData)
{
if (Main.IsClient)
{
Log.Debug("Was client");
return;
}
if (_Sub == null) return;
#if CLIENT
bool success = GameMain.GameSession.CrewManager!.GetCharacters().Any(c => !c.IsDead);
#else
bool success =
GameMain.Server != null &&
GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif
if (!success)
{
Log.Debug("Did not succeed");
return;
}
if (_Revealed) levelData.MLC().PirateData.Revealed = true;
if (levelData.MLC().PirateData.Status == PirateOutpostStatus.Destroyed)
{
Log.Debug("Base was destroyed");
return;
}
try
{
// If more than half of the crew or the commander is dead / incapacited / arrested, the outpost is destroyed
bool crewStatus = characters.Select(c => c.IsDead || c.Removed || c.IsIncapacitated || c.IsHandcuffed).Count() > characters.Count / 2;
if (_Commander.IsDead || _Commander.Removed || _Commander.IsHandcuffed || crewStatus)
{
levelData.MLC().PirateData.Status = PirateOutpostStatus.Destroyed;
Log.Debug("base destroyed");
}
else
{
Log.Debug($"Base still active: {crewStatus} dead: {_Commander.IsDead} removed: {_Commander.Removed} handcuffed: {_Commander.IsHandcuffed}");
}
LocationConnection con = Level.Loaded.StartLocation.Connections.Where(c => c.OtherLocation(Level.Loaded.StartLocation) == Level.Loaded.EndLocation).First();
PirateOutpostDirector.UpdateStatus(levelData.MLC().PirateData, con);
} catch(Exception e)
{
DebugConsole.ThrowError("Error in pirate outpost OnRoundEnd", e);
}
}
private void HuskOutpost()
{
if (_Data.Status != PirateOutpostStatus.Husked) return;
Log.InternalDebug("You've met with a terrible fate, haven't you?");
if (!AfflictionHelper.TryGetAffliction("huskinfection", out AfflictionPrefab husk))
{
Log.Error("Couldn't get the husk affliction!!!");
return;
}
foreach (Character character in characters)
{
var huskAffliction = new Affliction(husk, 200);
character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, huskAffliction);
character.CharacterHealth.Update((float)Timing.Step);
character.Kill(CauseOfDeathType.Affliction, huskAffliction);
}
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand) =>
Math.Max(
(int)Math.Round(
minAmount +
((maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-25, 25, (float)rand.NextDouble())) / 100)
),
minAmount);
}
public enum PirateBaseRelationStatus
{
Hostile,
Neutral,
Friendly
}
}
@@ -0,0 +1,110 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Config;
using Barotrauma.MoreLevelContent.Shared.Config;
using Barotrauma.Networking;
using HarmonyLib;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation.Interfaces;
using MoreLevelContent.Shared.Store;
using MoreLevelContent.Shared.Utils;
using System;
using System.Reflection;
namespace MoreLevelContent.Shared.Generation.Pirate
{
public class PirateOutpostDirector : GenerationDirector<PirateOutpostDirector>, IGenerateSubmarine, IGenerateNPCs, ILevelStartGenerate, IRoundStatus
{
public string ForcedPirateOutpost = "";
public bool ForceSpawn { get; set; } = false;
public bool ForceHusk { get; set; } = false;
public static PirateConfig Config => ConfigManager.Instance.Config.NetworkedConfig.PirateConfig;
private PirateOutpost _PirateOutpost;
public override bool Active => PirateStore.HasContent;
public override void Setup()
{
PirateStore.Instance.Setup();
Hooks.Instance.AddUpdateAction(Update);
#if CLIENT
NetUtil.Register(NetEvent.PIRATEBASE_STATUS, StatusUpdated);
#endif
}
internal static void UpdateStatus(PirateData data, LocationConnection con)
{
#if SERVER
Log.Debug("Send status");
var msg = NetUtil.CreateNetMsg(NetEvent.PIRATEBASE_STATUS);
Int32 id = MapDirector.ConnectionIdLookup[con];
msg.WriteInt32(id);
msg.WriteBoolean(data.Revealed);
msg.WriteInt16((short)data.Status);
NetUtil.SendAll(msg);
#endif
}
#if CLIENT
public void StatusUpdated(object[] args)
{
IReadMessage inMsg = (IReadMessage)args[0];
int conId = inMsg.ReadInt32();
bool revealed = inMsg.ReadBoolean();
PirateOutpostStatus status = (PirateOutpostStatus)inMsg.ReadInt16();
// Look up connection
var connection = MapDirector.IdConnectionLookup[conId];
connection.LevelData.MLC().PirateData.Revealed = revealed;
connection.LevelData.MLC().PirateData.Status = status;
Log.Debug("Updated pirate status");
}
#endif
static void Update(float deltaTime, Camera cam)
{
if (Instance._PirateOutpost != null)
{
Instance._PirateOutpost.Update(deltaTime);
}
}
void ILevelStartGenerate.OnLevelGenerationStart(LevelData levelData, bool _)
{
_PirateOutpost = null;
// Prevent an outpost from spawning if the mission is a pirate
// It will brick the pirates if it does
if (!Screen.Selected.IsEditor) // Don't check in editor
{
foreach (Mission mission in GameMain.GameSession.GameMode!.Missions)
{
if (mission is PirateMission) return;
}
}
if (levelData.MLC().PirateData.HasPirateBase && ConfigManager.Instance.Config.NetworkedConfig.PirateConfig.EnablePirateBases)
{
_PirateOutpost = new PirateOutpost(levelData.MLC().PirateData, ForcedPirateOutpost, levelData.Seed);
Log.Verbose("Set pirate outpost");
}
}
public void GenerateSub() => _PirateOutpost?.Generate();
public void SpawnNPCs() => _PirateOutpost?.Populate();
public void BeforeRoundStart() { }
public void RoundEnd()
{
Log.Debug("Pirate director round end");
if (_PirateOutpost != null)
{
_PirateOutpost.OnRoundEnd(Level.Loaded.LevelData);
_PirateOutpost = null;
} else
{
Log.Debug("Pirate outpost not set");
}
}
}
}
@@ -0,0 +1,52 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Config;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Generation
{
public static class MoveRuins
{
public static void Init()
{
if (GameMain.IsMultiplayer) return;
var level_FindPosAwayFromMainPath = typeof(Level).GetMethod("FindPosAwayFromMainPath", BindingFlags.NonPublic | BindingFlags.Instance);
// Main.Patch(level_FindPosAwayFromMainPath, prefix: AccessTools.Method(typeof(MoveRuins), nameof(MoveRuinSpawnPos)));
}
readonly static Point ruinSize = new Point(5000);
public static object MoveRuinSpawnPos(object self, Dictionary<string, object> args)
{
// Broken in multiplayer
if (GameMain.IsMultiplayer) return null;
// Exit if caves haven't been generated yet
if (Loaded.Caves.Count < Loaded.GenerationParams.CaveCount) return null;
Random rand = new MTRandom(ToolBox.StringToInt(Loaded.Seed));
// Roll for move
if (rand.Next(100) > ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.RuinMoveChance) return null;
Log.Debug("Moving the ruins...");
// Generate ruin point
int limitLeft = Math.Max((int)Loaded.StartPosition.X, ruinSize.X / 2);
int limitRight = Math.Min((int)Loaded.EndPosition.X, Loaded.Size.X - (ruinSize.X / 2));
Point ruinPos = new Point(rand.Next(limitLeft, limitRight), rand.Next(Loaded.AbyssArea.Top + 5000, Loaded.AbyssArea.Bottom - 5000));
// Move the ruins above the sea floor, copied from Level.cs Line 1709
ruinPos.Y = Math.Max(ruinPos.Y, (int)Loaded.GetBottomPosition(ruinPos.X).Y + 500);
ruinPos.Y = Math.Max(ruinPos.Y, (int)Loaded.GetBottomPosition(ruinPos.X + 5000).Y + 500);
Log.Debug($"Ruins spawn point: {ruinPos}");
return ruinPos;
}
}
}