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,17 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MoreLevelContent.Shared.Utils
{
internal static class AfflictionHelper
{
internal static bool TryGetAffliction(string identifier, out AfflictionPrefab affliction)
{
affliction = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == identifier);
return affliction != null;
}
}
}
@@ -0,0 +1,90 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma.MoreLevelContent.Shared.Utils
{
public static class CharacterUtils
{
internal static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null)
{
CharacterInfo characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
characterInfo.TeamID = teamType;
if (positionToStayIn == null)
{
positionToStayIn =
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
humanPrefab.GiveItems(spawnedCharacter, submarine, null, Rand.RandSync.ServerAndClient);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
internal static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, CharacterTeamType teamType, Vector2 spawnPosition, bool createNetEvent = true)
{
CharacterInfo characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
characterInfo.TeamID = teamType;
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: createNetEvent);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter);
humanPrefab.GiveItems(spawnedCharacter, null, null, Rand.RandSync.ServerAndClient, createNetworkEvents: createNetEvent);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
internal static HumanPrefab GetHumanPrefabFromElement(XElement element)
{
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error");
return null;
}
Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
return null;
}
return humanPrefab;
}
public static XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier, Random rand)
{
// look for the element that is closest to our difficulty, with some randomness
XElement bestElement = null;
float bestValue = float.MaxValue;
foreach (XElement element in parentElement.Elements())
{
float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier, rand);
if (applicabilityValue < bestValue)
{
bestElement = element;
bestValue = applicabilityValue;
}
}
return bestElement;
}
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand) => Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Utils
{
public static class CommandUtils
{
public static void AddCommand(string cmdName, string cmdHelp, LuaCsAction callback, LuaCsFunc args = null, bool isCheat = false) =>
GameMain.LuaCs.Game.AddCommand(cmdName, cmdHelp, callback, args, isCheat);
}
}
@@ -0,0 +1,54 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Steam;
using MoreLevelContent.Shared;
using System;
using System.Linq;
public class CompatabilityHelper : Singleton<CompatabilityHelper>
{
public bool ReactorModInstalled { get; private set; }
public bool HazardousReactorsInstalled { get; private set; }
public bool DynamicEuropaInstalled { get; private set; }
internal Faction BanditFaction
{
get
{
_Bandits ??= GameMain.GameSession.Campaign.GetFaction("bandits");
return _Bandits;
}
}
private Faction _Bandits;
internal static void SetupHazReactor(Reactor reactor)
{
Item reactorItem = reactor.Item;
reactorItem.InvulnerableToDamage = true;
// tell bots not to stay in the hull the reactor is in
if (reactorItem.CurrentHull != null) reactorItem.CurrentHull.AvoidStaying = true;
}
public override void Setup()
{
HazardousReactorsInstalled = CheckInstalled(2547888957);
if (ItemPrefab.Prefabs.TryGet("reactor1", out ItemPrefab prefab))
{
ReactorModInstalled = prefab.IsOverride;
}
DynamicEuropaInstalled = CheckInstalled(2532991202);
Log.Debug(
$"-= MLC Compatability =-\n" +
$"- HazReactorsInstalled: {HazardousReactorsInstalled}\n" +
$"- ReactorModInstalled: {ReactorModInstalled}\n" +
$"- DynamicEuropa: {DynamicEuropaInstalled}");
}
bool CheckInstalled(ulong workshopID) => ContentPackageManager.EnabledPackages.All.Any(p => p.TryExtractSteamWorkshopId(out SteamWorkshopId idOut) && idOut.Value == workshopID);
}
+82
View File
@@ -0,0 +1,82 @@
using Barotrauma;
using HarmonyLib;
using System.Reflection;
using System;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Reflection.Emit;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Generation.Pirate;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
namespace MoreLevelContent.Shared.Utils
{
internal class Hooks : Singleton<Hooks>
{
public delegate void UpdateAction(float deltaTime, Camera cam);
internal event Action<Structure, float, Character> OnStructureDamaged;
private List<UpdateAction> _UpdateActions = new();
#if CLIENT
internal event Action<SpriteBatch, Camera> OnDebugDraw;
internal event Action<Sonar, Vector2, float> OnUpdateSonarDisruption;
private FieldInfo Sonar_activePingsCount;
#endif
//float deltaTime, Camera cam
public override void Setup()
{
var Structure_SetDamage = typeof(HumanAIController).GetMethod(nameof(HumanAIController.StructureDamaged), BindingFlags.Public | BindingFlags.Static);
_ = Main.Harmony.Patch(Structure_SetDamage, prefix: new HarmonyMethod(typeof(Hooks), nameof(StructureDamaged)));
MethodInfo level_update = AccessTools.Method(typeof(Level), "Update");
_ = Main.Harmony.Patch(level_update, postfix: new HarmonyMethod(AccessTools.Method(typeof(Hooks), nameof(Hooks.Update))));
#if CLIENT
var Level_DrawDebugOverlay = typeof(Level).GetMethod(nameof(Level.DrawDebugOverlay));
_ = Main.Harmony.Patch(Level_DrawDebugOverlay, postfix: new HarmonyMethod(typeof(Hooks), nameof(DebugDraw)));
var Sonar_UpdateDisruptions = AccessTools.Method(typeof(Sonar), "UpdateDisruptions");//typeof(Sonar).GetMethod("UpdateDisruptions");
Sonar_activePingsCount = AccessTools.Field(typeof(Sonar), "activePingsCount");
if (Sonar_UpdateDisruptions == null) Log.Error("\n\n\n\nNull");
_ = Main.Harmony.Patch(Sonar_UpdateDisruptions, postfix: new HarmonyMethod(typeof(Hooks), nameof(SonarDisruption)));
#endif
Log.Debug("Patched Hooks");
}
public void AddUpdateAction(UpdateAction action)
{
_UpdateActions.Add(action);
}
private static void StructureDamaged(Structure structure, float damageAmount, Character character) => Instance.OnStructureDamaged?.Invoke(structure, damageAmount, character);
private static void Update(float deltaTime, Camera cam)
{
foreach (var item in Instance._UpdateActions)
{
item.Invoke(deltaTime, cam);
}
}
#if CLIENT
private static void DebugDraw(SpriteBatch spriteBatch, Camera cam)
{
if (!GameMain.DebugDraw) return;
Instance.OnDebugDraw?.Invoke(spriteBatch, cam);
}
private static void SonarDisruption(Sonar __instance, Vector2 pingSource, float worldPingRadius)
{
int activePingsCount = (int)Instance.Sonar_activePingsCount.GetValue(__instance);
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{
Instance.OnUpdateSonarDisruption?.Invoke(__instance, pingSource, worldPingRadius);
}
}
#endif
}
}
@@ -0,0 +1,151 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Voronoi2;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Utils
{
public static class MLCUtils
{
internal static string GetRandomTag(string baseTag)
{
int maxIndex = 1;
while (TextManager.ContainsTag(baseTag + maxIndex))
{
maxIndex++;
}
return "mlc.lostcargo.tooslow" + Rand.Range(0, maxIndex);
}
internal static string GetRandomTag(string baseTag, LevelData data)
{
Random rand = new MTRandom(ToolBox.StringToInt(data.Seed));
int maxIndex = 1;
while (TextManager.ContainsTag(baseTag + maxIndex))
{
maxIndex++;
}
return baseTag + rand.Next(0, maxIndex);
}
internal static Vector2 PositionItemOnEdge(Item target, GraphEdge edge, float height, bool setRotation = false)
{
Vector2 dir = Vector2.Normalize(edge.GetNormal(edge.Cell1 ?? edge.Cell2));
float angle = Angle(dir) - 90;
Vector2 pos = ConvertUnits.ToSimUnits(edge.Center + (edge.GetNormal(edge.Cell1 ?? edge.Cell2) * height));
SetItemPosition(target, pos, setRotation ? MathHelper.ToRadians(angle) : 0);
target.Rotation = -angle;
return dir;
}
internal static void SetItemPosition(Item target, Vector2 simPos, float rot) => target.SetTransform(simPos - (target.Submarine?.SimPosition ?? Vector2.Zero), rot, false);
internal static float Angle(Vector2 dir) => (float)(MathUtils.VectorToAngle(dir) * 180 / Math.PI);
internal static Random GetLevelRandom()
{
if (Level.Loaded == null)
{
Log.Error("Level was null when we tried to get a random instance!");
return null;
}
return new MTRandom(ToolBox.StringToInt(Level.Loaded.LevelData.Seed));
}
internal static Location FindUnlockLocation(FindLocationInfo info)
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign)
{
Log.Warn("Not campaign mode, can't find location");
return null;
}
if (info.MinDistance <= 1)
{
return campaign.Map.CurrentLocation;
}
var currentLocation = campaign.Map.CurrentLocation;
int distance = 0;
HashSet<Location> checkedLocations = new HashSet<Location>();
HashSet<Location> pendingLocations = new HashSet<Location>() { currentLocation };
do
{
List<Location> currentLocations = pendingLocations.ToList();
pendingLocations.Clear();
foreach (var location in currentLocations)
{
checkedLocations.Add(location);
if (IsLocationValid(currentLocation, location, distance, info))
{
return location;
}
else
{
foreach (LocationConnection connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (checkedLocations.Contains(otherLocation)) { continue; }
pendingLocations.Add(otherLocation);
}
}
}
distance++;
} while (pendingLocations.Any());
return null;
}
internal static bool IsLocationValid(Location currentLocation, Location location, int distance, FindLocationInfo info)
{
if (!info.RequiredFaction.IsEmpty)
{
if (location.Faction?.Prefab.Identifier != info.RequiredFaction &&
location.SecondaryFaction?.Prefab.Identifier != info.RequiredFaction)
{
return false;
}
}
if (info.AllowedLocationTypes != null && info.AllowedLocationTypes.Count() > 0 && !info.AllowedLocationTypes.Contains(location.Type.Identifier) && !(location.HasOutpost() && info.AllowedLocationTypes.Contains(Tags.AnyOutpost)))
{
return false;
}
if (distance < info.MinDistance)
{
return false;
}
if (info.MustBeFurtherOnMap && location.MapPosition.X < currentLocation.MapPosition.X)
{
return false;
}
if (info.MustBeHidden && !location.Discovered)
{
return false;
}
return true;
}
internal struct FindLocationInfo
{
public int MinDistance;
public int MaxDistance;
public bool MustBeFurtherOnMap;
public IEnumerable<Identifier> AllowedLocationTypes;
public Identifier RequiredFaction;
public bool MustBeHidden;
}
internal static Random GetRandomFromString(string seed)
{
return new MTRandom(ToolBox.StringToInt(seed));
}
}
}
@@ -0,0 +1,544 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Voronoi2;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Utils
{
public class TrackingSonarMarker
{
public (LocalizedString Label, Vector2 Position) CurrentPosition { get; private set; }
private readonly LocalizedString label;
public TrackingSonarMarker(float updateInterval, Func<Vector2> getPositionFunc, LocalizedString sonarLabel)
{
_updateInterval = updateInterval;
_getPositionFunc = getPositionFunc;
label = sonarLabel;
Init();
}
internal TrackingSonarMarker(float updateInterval, Submarine submarine, LocalizedString sonarLabel)
{
_updateInterval = updateInterval;
_getPositionFunc = () => submarine.WorldPosition;
label = sonarLabel;
Init();
}
readonly float _updateInterval;
readonly Func<Vector2> _getPositionFunc;
private float _timeSinceLastUpdate;
private void Init() => CurrentPosition = (label, _getPositionFunc.Invoke());
public void Update(float delta)
{
_timeSinceLastUpdate += delta;
if (_timeSinceLastUpdate > _updateInterval)
{
_timeSinceLastUpdate = 0;
CurrentPosition = (label, _getPositionFunc.Invoke());
}
}
}
public class StructureDamageTracker : IDisposable
{
public event Action ThresholdCrossed;
public event OnStructureTakeDamage DamageAfterThreshold;
public delegate void OnStructureTakeDamage(float amount);
const float MaxDamagePerSecond = 5.0f;
const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
private readonly Submarine _sub;
private readonly float _threshold;
private readonly float _decayPerSec;
private readonly float _decayDelay;
private float _accumulatedDamage;
private bool _threshholdCrossed = false;
private float _decayTimer;
internal StructureDamageTracker(Submarine subToTrack, Func<float, float> resetDamageFunc, float threshold = 20f, float decay = 1f, float decayDelay = 5f)
{
Hooks.Instance.OnStructureDamaged += OnStructureDamaged;
_sub = subToTrack;
_threshold = threshold;
_decayPerSec = decay * (float)Timing.Step;
_decayDelay = decayDelay;
_resetDamageFunc = resetDamageFunc;
}
internal StructureDamageTracker(Submarine subToTrack, float threshold = 20f, float decay = 1f, float decayDelay = 5f)
{
Hooks.Instance.OnStructureDamaged += OnStructureDamaged;
_sub = subToTrack;
_threshold = threshold;
_decayPerSec = decay * (float)Timing.Step;
_decayDelay = decayDelay;
_resetDamageFunc = (float val) => 0;
}
readonly Func<float, float> _resetDamageFunc;
public void Update()
{
if (_accumulatedDamage > 0 && _decayTimer <= 0)
{
_accumulatedDamage -= _decayPerSec;
}
if (_decayTimer > 0)
{
_decayTimer -= (float)Timing.Step;
}
}
private void OnStructureDamaged(Structure structure, float damageAmount, Character character)
{
if (character == null || !character.IsPlayer) { return; }
if (structure?.Submarine == null || structure.Submarine != _sub) { return; }
// ignore interior walls so gun fights don't cause rep loss
if (structure.Prefab.Tags.Contains("inner")) { return; }
_accumulatedDamage += MathHelper.Clamp(damageAmount, 0, MaxDamagePerFrame);
_decayTimer = _decayDelay;
if (_accumulatedDamage < _threshold) return;
if (!_threshholdCrossed)
{
ThresholdCrossed?.Invoke();
_accumulatedDamage = _resetDamageFunc.Invoke(_accumulatedDamage);
_threshholdCrossed = true;
Log.Debug($"Reset accumulated damage to {_accumulatedDamage}");
return;
}
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null)
{
DamageAfterThreshold?.Invoke(damageAmount);
}
}
public void Dispose()
{
// Don't know if I really need to do this but why not
Hooks.Instance.OnStructureDamaged -= OnStructureDamaged;
GC.SuppressFinalize(this);
}
}
public static class MissionUtils
{
internal static bool TryGetInterestingPosition(PositionType positionType, float minDistFromSubs, out Point position)
{
if (!Loaded.PositionsOfInterest.Any())
{
position = new Point(Loaded.Size.X / 2, Loaded.Size.Y / 2);
Log.Debug("Failed to find point, default to middle of level");
return false;
}
List<InterestingPosition> suitablePositions = Loaded.PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType));
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
{
suitablePositions.RemoveAll(p => Loaded.IsPositionInsideWall(p.Position.ToVector2()));
}
if (!suitablePositions.Any())
{
Log.Debug("Failed to find point, no positions not inside walls");
position = Loaded.PositionsOfInterest[Rand.Int(Loaded.PositionsOfInterest.Count, Rand.RandSync.ServerAndClient)].Position;
return false;
}
List<InterestingPosition> farEnoughPositions = new List<InterestingPosition>(suitablePositions);
if (minDistFromSubs > 0.0f)
{
int beforeFilter = farEnoughPositions.Count;
int filtered = farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), Loaded.StartPosition) < minDistFromSubs * minDistFromSubs);
Log.Debug($"Removed {filtered} positions, after filer {farEnoughPositions.Count}, before: {beforeFilter}");
}
if (!farEnoughPositions.Any())
{
string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
Log.Error(errorMsg);
float maxDist = 0.0f;
position = suitablePositions.First().Position;
foreach (InterestingPosition pos in suitablePositions)
{
float dist = Submarine.Loaded.Sum(s =>
Submarine.MainSubs.Contains(s) ? Vector2.DistanceSquared(s.WorldPosition, pos.Position.ToVector2()) : 0.0f);
if (dist > maxDist)
{
position = pos.Position;
maxDist = dist;
}
}
return false;
}
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, Rand.RandSync.ServerAndClient)].Position;
Log.Debug("Found position!");
return true;
}
public static Vector2 GetPosInRect(Rectangle rect, Random rand)
{
float x = rand.Next(0, rect.Width);
float y = rand.Next(0, rect.Height);
y = Math.Clamp(y, rect.Height / 3, rect.Height / 2);
return new Vector2(rect.X + x, rect.Y + y);
}
static FieldInfo _waypointTagsField;
internal static void TagSubmarineWaypoints(this Submarine submarine, string tag)
{
if (_waypointTagsField == null)
{
_waypointTagsField = typeof(WayPoint).GetField("tags", BindingFlags.Instance | BindingFlags.NonPublic);
}
var validWaypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == submarine && wp.SpawnType == SpawnType.Human);
foreach (var waypoint in validWaypoints)
{
HashSet<Identifier> tags = (HashSet<Identifier>)_waypointTagsField.GetValue(waypoint);
_ = tags.Add(tag);
}
}
}
// We copy the whole method over here just so we can add a check
// to see if we're too close to a beacon station because
// trying to patch local methods through IL code is fuckin ass
public static class SubPlacementUtils
{
private static List<Rectangle> BlockedRects = new List<Rectangle>();
public static void ClearBlockedRects() => BlockedRects.Clear();
internal static Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type, PlacementType placementType = PlacementType.Bottom) => SpawnSubOnPath(subName, contentFile.Path.Value, type, placementType);
internal static Submarine SpawnSubOnPath(string subName, string path, SubmarineType type, PlacementType placementType = PlacementType.Bottom)
{
var tempSW = new Stopwatch();
FieldInfo _levelCells = AccessTools.Field(typeof(Level), "cells");
List<VoronoiCell> cells = (List<VoronoiCell>)_levelCells.GetValue(Loaded);
// Min distance between a sub and the start/end/other sub.
const float minDistance = Sonar.DefaultSonarRange;
var waypoints = WayPoint.WayPointList.Where(wp =>
wp.Submarine == null &&
wp.SpawnType == SpawnType.Path &&
wp.WorldPosition.X < Loaded.EndExitPosition.X &&
!Loaded.IsCloseToStart(wp.WorldPosition, minDistance) &&
!Loaded.IsCloseToEnd(wp.WorldPosition, minDistance)).ToList();
var subDoc = SubmarineInfo.OpenFile(path);
Rectangle subBorders = Submarine.GetBorders(subDoc.Root);
SubmarineInfo info = new SubmarineInfo(path)
{
Type = type
};
// Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by.
Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000);
var positions = new List<Vector2>();
var rects = new List<Rectangle>();
int maxAttempts = 50;
int attemptsLeft = maxAttempts;
bool success = false;
Vector2 spawnPoint = Vector2.Zero;
var allCells = Loaded.GetAllCells();
while (attemptsLeft > 0)
{
if (attemptsLeft < maxAttempts)
{
Debug.WriteLine($"Failed to position the sub {subName}. Trying again.");
}
attemptsLeft--;
if (TryGetSpawnPoint(out spawnPoint))
{
success = TryPositionSub(subBorders, subName, placementType, ref spawnPoint);
if (success)
{
break;
}
else
{
positions.Clear();
}
}
else
{
DebugConsole.NewMessage($"Failed to find any spawn point for the sub: {subName} (No valid waypoints left).", Color.Red);
break;
}
}
tempSW.Stop();
if (success)
{
Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)");
tempSW.Restart();
Submarine sub = new Submarine(info);
tempSW.Stop();
Debug.WriteLine($"Sub {sub.Info.Name} loaded in {tempSW.ElapsedMilliseconds} (ms)");
sub.SetPosition(spawnPoint);
BlockedRects.Add(sub.GetDockedBorders());
return sub;
}
else
{
DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds} (ms).", Color.Red);
return null;
}
bool TryPositionSub(Rectangle subBorders, string subName, PlacementType placement, ref Vector2 spawnPoint)
{
positions.Add(spawnPoint);
bool bottomFound = TryRaycast(subBorders, placement, ref spawnPoint);
positions.Add(spawnPoint);
bool leftSideBlocked = IsSideBlocked(subBorders, false);
bool rightSideBlocked = IsSideBlocked(subBorders, true);
int step = 5;
if (rightSideBlocked && !leftSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
}
else if (leftSideBlocked && !rightSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
}
else if (!bottomFound)
{
if (!leftSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, -step);
}
else if (!rightSideBlocked)
{
bottomFound = TryMove(subBorders, placement, ref spawnPoint, step);
}
else
{
Debug.WriteLine($"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));
Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls.");
}
else if (!bottomFound)
{
Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground.");
}
else
{
var sp = spawnPoint;
if (Loaded.Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < minDistance * minDistance))
{
Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s).");
return false;
}
if (Loaded.BeaconStation != null)
{
if (Vector2.DistanceSquared(Loaded.BeaconStation.WorldPosition, sp) < minDistance * minDistance)
{
return false;
}
}
if (Vector2.DistanceSquared(Loaded.StartPosition, sp) < minDistance * minDistance)
{
return false;
}
if (Vector2.DistanceSquared(Loaded.EndPosition, sp) < minDistance * minDistance)
{
return false;
}
}
return !isBlocked && bottomFound;
bool TryMove(Rectangle subBorders, PlacementType placement, ref Vector2 spawnPoint, float amount)
{
float maxMovement = 5000;
float totalAmount = 0;
bool foundBottom = TryRaycast(subBorders, placement, ref spawnPoint);
while (!IsSideBlocked(subBorders, amount > 0))
{
foundBottom = TryRaycast(subBorders, placement, 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 TryGetSpawnPoint(out Vector2 spawnPoint)
{
spawnPoint = Vector2.Zero;
while (waypoints.Any())
{
var wp = waypoints.GetRandom(Rand.RandSync.ServerAndClient);
waypoints.Remove(wp);
if (!IsBlocked(wp.WorldPosition, paddedDimensions))
{
spawnPoint = wp.WorldPosition;
return true;
}
}
return false;
}
bool TryRaycast(Rectangle subBorders, PlacementType placement, 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, placement == PlacementType.Bottom ? -1 : Loaded.Size.Y + 1),
customPredicate: f => f.Body == Loaded.TopBarrier || f.Body == Loaded.BottomBarrier || (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 * (placement == PlacementType.Bottom ? 1 : -1));
hit = true;
}
}
float highestPoint = placement == PlacementType.Bottom ? positions.Max(p => p.Y) : positions.Min(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);
if (Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds)))
{
return true;
}
if (Loaded.Caves.Any(c =>
ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) ||
ToolBox.GetWorldBounds(c.StartPos, new Point(1500)).IntersectsWorld(bounds)))
{
return true;
}
return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v)));
}
}
internal static void PositionSubmarine(Submarine submarine, PositionType positionType)
{
float dist = Sonar.DefaultSonarRange * 2;
if (MissionUtils.TryGetInterestingPosition(positionType, dist, out Point point))
{
Vector2 spawnPos = point.ToVector2();
Point subSize = submarine.GetDockedBorders().Size;
int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead
spawnPos = submarine.FindSpawnPos(spawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance));
submarine.SetPosition(spawnPos);
}
}
internal static void SetCrushDepth(Submarine sub, bool inf = false)
{
if (inf)
{
sub.SetCrushDepth(float.MaxValue);
return;
}
float depth = Math.Max(sub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
depth = Math.Max(depth, Loaded.GetRealWorldDepth(sub.Position.Y) + 1000);
sub.SetCrushDepth(depth);
}
}
}
@@ -0,0 +1,119 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Voronoi2;
namespace MoreLevelContent.Shared.Utils
{
public static class PhysUtil
{
// public static RayHit RayCastFirst(Vector2 point_0, Vector2 point_1)
// {
// RayHit hit = new RayHit() { Hit = false };
// Func<Fixture, Vector2, Vector2, float, float> get_first_callback = delegate (Fixture fixture, Vector2 point, Vector2 normal, float fraction)
// {
// hit = new RayHit(fixture, point, normal);
// return 0;
// };
//
// // Summary:
// // Ray-cast the world for all fixtures in the path of the ray. Your callback
// // controls whether you get the closest point, any point, or n-points. The
// // ray-cast ignores shapes that contain the starting point. Inside the callback:
// // return -1: ignore this fixture and continue
// // return 0: terminate the ray cast
// // return fraction: clip the ray to this point
// // return 1: don't clip the ray and continue
// GameMain.World.RayCast(get_first_callback, point_0, point_1);
// return hit;
// }
public static RayHit RaycastWorld(Vector2 start_simpos, Vector2 end_simpos, IEnumerable<Body> ignoredBodies = null)
{
RayHit hit = new RayHit() { Hit = false };
GameMain.World.RayCast((fixture, point, normal, fraction) =>
{
if (!CheckFixtureCollision(fixture, ignoredBodies, Physics.CollisionLevel | Physics.CollisionWall, true, CheckForWalls)) return -1;
hit.Hit = true;
hit.Body = fixture.Body;
return 0;
}, start_simpos, end_simpos, Physics.CollisionLevel | Physics.CollisionWall);
return hit;
}
public static bool WorldPositionClear(Vector2 simPos)
{
var aabb = new FarseerPhysics.Collision.AABB(simPos + Vector2.One, simPos - Vector2.One);
bool clear = true;
GameMain.World.QueryAABB((fixture) =>
{
if (!CheckFixtureCollision(fixture, null, Physics.CollisionLevel | Physics.CollisionWall, true, CheckForWalls)) return true;
clear = false;
return false;
}, ref aabb);
return clear;
}
static bool CheckForWalls(Fixture f) => (f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static) ||
!Level.Loaded.ExtraWalls.Any(w => w.Body == f.Body);
private static bool CheckFixtureCollision(Fixture fixture, IEnumerable<Body> ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate<Fixture> customPredicate = null)
{
if (fixture == null ||
(ignoreSensors && fixture.IsSensor) ||
fixture.CollisionCategories == Category.None ||
fixture.CollisionCategories == Physics.CollisionItem)
{
return false;
}
if (customPredicate != null && !customPredicate(fixture))
{
return false;
}
if (collisionCategory != null &&
!fixture.CollisionCategories.HasFlag((Category)collisionCategory) &&
!((Category)collisionCategory).HasFlag(fixture.CollisionCategories))
{
return false;
}
if (ignoredBodies != null && ignoredBodies.Contains(fixture.Body))
{
return false;
}
if (fixture.Body.UserData is Structure structure)
{
if (structure.IsPlatform && collisionCategory != null && !((Category)collisionCategory).HasFlag(Physics.CollisionPlatform))
{
return false;
}
}
return true;
}
}
public struct RayHit
{
public bool Hit;
public Body Body;
public RayHit(Body body)
{
Body = body;
Hit = true;
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Utils
{
public abstract class Singleton<T> where T : class
{
/// <summary>
/// Static instance. Needs to use lambda expression
/// to construct an instance (since constructor is private).
/// </summary>
private static readonly Lazy<T> sInstance = new Lazy<T>(() => CreateInstanceOfT());
/// <summary>
/// Gets the instance of this singleton.
/// </summary>
public static T Instance => sInstance.Value;
/// <summary>
/// Creates an instance of T via reflection since T's constructor is expected to be private.
/// </summary>
/// <returns></returns>
private static T CreateInstanceOfT() => Activator.CreateInstance(typeof(T), true) as T;
public abstract void Setup();
}
}
@@ -0,0 +1,68 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using HarmonyLib;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using static Barotrauma.Items.Components.Sonar;
namespace MoreLevelContent.Shared.Utils
{
public class SonarExtensions : Singleton<SonarExtensions>
{
private readonly List<SonarDisturbance> _Disturbances = new();
public override void Setup()
{
#if CLIENT
Hooks.Instance.OnUpdateSonarDisruption += Instance_OnUpdateSonarDisruption;
Dictionary<BlipType, Color[]> sonarBlips = (Dictionary<BlipType, Color[]>)ReflectionInfo.Instance.blipColorGradient.GetValue(null);
if (!sonarBlips.ContainsKey((BlipType)5))
{
sonarBlips.Add((BlipType)5, new Color[] { Color.TransparentBlack, new Color(0, 68, 65) * 0.9f, new Color(0, 68, 65) * 0.65f, new Color(0, 68, 65) * 0.25f });
}
#endif
}
internal void Add(Item source, float strength)
{
_Disturbances.Add(new SonarDisturbance(source, strength));
}
internal void Remove(Item source)
{
var dist = _Disturbances.Find(d => d.Item == source);
if (dist != null)
{
_Disturbances.Remove(dist);
}
}
private void Instance_OnUpdateSonarDisruption(Barotrauma.Items.Components.Sonar sonar, Vector2 pingSource, float worldPingRadius)
{
for (int i = 0; i < _Disturbances.Count; i++)
{
var disturbance = _Disturbances[i];
if (disturbance.Item == null || disturbance.Item.Removed)
{
_Disturbances.Remove(disturbance);
continue;
}
MLCExtensions.AddSonarDisruption(sonar, pingSource, disturbance.Position, disturbance.Strength);
}
}
private class SonarDisturbance
{
public SonarDisturbance(Item source, float strength)
{
Item = source;
Strength = strength;
}
internal Item Item { get; private set; }
public float Strength { get; private set; }
public Vector2 Position => Item?.WorldPosition ?? Vector2.Zero;
}
}
}
@@ -0,0 +1,110 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using HarmonyLib;
using Microsoft.Xna.Framework;
using Steamworks.Ugc;
using System;
using System.Collections.Generic;
using System.Reflection;
using static Barotrauma.Items.Components.Sonar;
namespace MoreLevelContent.Shared.Utils
{
internal static class MLCExtensions
{
// Private field access
internal static float GetMinRotation(this Turret turret) => (float)ReflectionInfo.Instance.minRotation.GetValue(turret);
internal static float GetMaxRotation(this Turret turret) => (float)ReflectionInfo.Instance.maxRotation.GetValue(turret);
internal static List<(Vector2 pos, float strength)> GetDisruptedDirections(this Sonar sonar) => (List<(Vector2 pos, float strength)>)ReflectionInfo.Instance.disruptedDirections.GetValue(sonar);
internal static void AddSonarDisruption(this Sonar sonar, Vector2 pingSource, Vector2 disruptionPos, float disruptionStrength)
{
#if CLIENT
disruptionStrength = Math.Min(disruptionStrength, 10.0f);
Vector2 dir = disruptionPos - pingSource;
float disruptionDist = Vector2.Distance(pingSource, disruptionPos);
sonar.GetDisruptedDirections().Add(((disruptionPos - pingSource) / disruptionDist, disruptionStrength));
for (int i = 0; i < disruptionStrength * 10.0f; i++)
{
Vector2 pos = disruptionPos + Rand.Vector(Rand.Range(0.0f, Level.GridCellSize * 4 * disruptionStrength));
if (Vector2.Dot(pos - pingSource, -dir) > 1.0f - disruptionStrength) { continue; }
var blip = new SonarBlip(
pos,
MathHelper.Lerp(0.1f, 1.5f, Math.Min(disruptionStrength, 1.0f)),
Rand.Range(0.2f, 1.0f + disruptionStrength),
BlipType.Disruption);
List<SonarBlip> blips = (List<SonarBlip>)ReflectionInfo.Instance.sonarBlips.GetValue(sonar);
blips.Add(blip);
}
#endif
}
#if CLIENT
// Still doesn't work
internal static void AddSonarCircle(this Sonar sonar, Vector2 pingSource, BlipType type, int blipCount = 10, float range = 0)
{
Vector2 sonarPos = sonar.Item.Submarine != null && sonar.Item.body == null ? sonar.Item.Submarine.WorldPosition : sonar.Item.WorldPosition;
Vector2 targetVector = pingSource - sonarPos;
if (targetVector.LengthSquared() > MathUtils.Pow2(range)) return;
// Don't display pings if we're in sonar range
if (targetVector.LengthSquared() < MathUtils.Pow2(DefaultSonarRange)) return;
float dist = targetVector.Length();
Vector2 targetDir = targetVector / dist;
for (int i = 0; i < blipCount; i++)
{
float angle = Rand.Range(-0.5f, 0.5f);
Vector2 blipDir = MathUtils.RotatePoint(targetDir, angle);
Vector2 invBlipDir = MathUtils.RotatePoint(targetDir, -angle);
var longRangeBlip = new SonarBlip(sonarPos + blipDir * sonar.Range * 0.9f, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), type)
{
Velocity = -invBlipDir * (MathUtils.Round(Rand.Range(8000.0f, 15000.0f), 2000.0f) - Math.Abs(angle * angle * 10000.0f)),
Rotation = (float)Math.Atan2(-invBlipDir.Y, invBlipDir.X),
Alpha = MathUtils.Pow2((range - dist) / range) * 0.25f
};
longRangeBlip.Size.Y *= 4.0f;
List<SonarBlip> blips = (List<SonarBlip>)ReflectionInfo.Instance.sonarBlips.GetValue(sonar);
blips.Add(longRangeBlip);
}
// for (int i = 0; i < amount; i++)
// {
// Vector2 dir = Rand.Vector(1.0f);
// var longRangeBlip = new SonarBlip(pingSource, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), type)
// {
// Velocity = dir * MathUtils.Round(Rand.Range(4000.0f, 6000.0f), 1000.0f),
// Rotation = (float)Math.Atan2(-dir.Y, dir.X)
// };
// longRangeBlip.Size.Y *= 4.0f;
// List<SonarBlip> blips = (List<SonarBlip>)ReflectionInfo.Instance.sonarBlips.GetValue(sonar);
// blips.Add(longRangeBlip);
// }
}
#endif
}
public class ReflectionInfo : Singleton<ReflectionInfo>
{
public FieldInfo minRotation;
public FieldInfo maxRotation;
public FieldInfo disruptedDirections;
public FieldInfo sonarBlips;
public FieldInfo blipColorGradient;
public override void Setup()
{
// Fields
minRotation = AccessTools.Field(typeof(Turret), "minRotation");
maxRotation = AccessTools.Field(typeof(Turret), "maxRotation");
disruptedDirections = AccessTools.Field(typeof(Sonar), "disruptedDirections");
sonarBlips = AccessTools.Field(typeof(Sonar), "sonarBlips");
blipColorGradient = AccessTools.Field(typeof(Sonar), "blipColorGradient");
Log.Debug("Setup field references");
}
}
}