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,342 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System;
using MoreLevelContent.Shared.Utils;
using static Barotrauma.Level;
using Voronoi2;
using MoreLevelContent.Shared.Generation;
using HarmonyLib;
namespace MoreLevelContent.Shared.AI
{
public class CaveAiConfig
{
public Identifier Entity => "thalamus";
public Identifier DefensiveAgent => "Leucocyte";
public string OffensiveAgent => "Terminalcell";
public string Brain => "thalamusbrain_cave";
public string Spawner => "cellspawnorgan_cave";
public float AgentSpawnDelay => 10;
public float AgentSpawnDelayRandomFactor => 0.25f;
public float AgentSpawnDelayDifficultyMultiplier => 1.0f;
public float AgentSpawnCountDifficultyMultiplier => 1.0f;
public int MaxAgentCount => 30;
public bool KillAgentsWhenEntityDies => true;
public float DeadEntityColorMultiplier => 0.5f;
public float DeadEntityColorFadeOutTime => 1;
}
partial class CaveAI : IServerSerializable
{
public bool IsAlive { get; private set; }
public readonly List<Item> ThalamusItems;
public readonly Cave Cave;
private readonly List<Turret> turrets = new List<Turret>();
private readonly List<Item> spawnOrgans = new List<Item>();
private readonly List<VoronoiCell> spawnPoints = new List<VoronoiCell>();
private readonly Item brain;
// Auto operate turrets need to have a submarine to work
public readonly Submarine DummySub;
private bool initialCellsSpawned;
public readonly CaveAiConfig Config = new CaveAiConfig();
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public CaveAI(List<Item> allThalamusItems, GraphEdge spawnEdge, Cave cave)
{
Log.Debug($"it {allThalamusItems == null} se: {spawnEdge == null} cave: {cave == null}");
this.Cave = cave;
DummySub = new Submarine(new SubmarineInfo(), showErrorMessages: false)
{
TeamID = CharacterTeamType.None,
ShowSonarMarker = false
};
DummySub.PhysicsBody.BodyType = FarseerPhysics.BodyType.Static;
DummySub.Info.Type = SubmarineType.EnemySubmarine;
allThalamusItems.ForEach(i => i.Submarine = DummySub);
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
var brainPrefab = thalamusPrefabs.Where(p => p.Tags.Contains(Config.Brain)).FirstOrDefault();
if (brainPrefab == null)
{
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
return;
}
ThalamusItems = allThalamusItems;
brain = new Item(brainPrefab, Vector2.Zero, null);
ThalamusItems.Add(brain);
_ = MLCUtils.PositionItemOnEdge(brain, spawnEdge, 120, true);
// Setup spawner organs
spawnPoints = cave.Tunnels.SelectMany(t => t.Cells.Where(c => c.CellType != CellType.Solid && c.CellType != CellType.Removed)).ToList();
foreach (var item in allThalamusItems)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
turret.AutoOperate = false;
}
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
{
spawnOrgans.Add(item);
}
}
}
// need to setup positions for initial cells to spawn
IsAlive = true;
ClearCave();
}
private readonly List<Item> destroyedOrgans = new List<Item>();
public void Update(float deltaTime)
{
// General AI management
if (!IsAlive) { return; }
if (Cave == null)
{
Remove();
return;
}
if (brain == null || brain.Removed || brain.Condition <= 0)
{
Kill();
return;
}
// Manage organs
destroyedOrgans.Clear();
foreach (var organ in spawnOrgans)
{
if (organ.Condition <= 0)
{
destroyedOrgans.Add(organ);
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
// Manage agro
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Cave.StartPos.ToVector2()) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Cave.StartPos.ToVector2()) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
if (!IsClient)
{
if (!initialCellsSpawned)
{
SpawnInitialCells();
}
UpdateReinforcements(deltaTime);
}
}
private void ClearCave()
{
var wallsNearCave = Loaded.ExtraWalls.Where(w =>
w.Cells.Any(c => c.IsDestructible &&
(Cave.Area.Contains(c.Center) ||
Vector2.DistanceSquared(Cave.StartPos.ToVector2(), c.Center) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)));
foreach (var wall in wallsNearCave)
{
if (wall is DestructibleLevelWall destructible)
{
destructible.Destroy();
destructible.NetworkUpdatePending = true;
}
}
}
private void SpawnInitialCells()
{
int closeBrainCells = Rand.Range(5, 8);
for (int i = 0; i < closeBrainCells; i++)
{
if (!TrySpawnCell(out _, brain)) { break; }
}
int initalCells = Rand.Range(5, MaxCellCount);
for (int i = 0; i < initalCells; i++)
{
if (!TrySpawnCell(out _)) { break; }
}
initialCellsSpawned = true;
}
public void Kill()
{
ThalamusItems.ForEach(i => i.Condition = 0);
foreach (var turret in turrets)
{
// Snap all tendons
foreach (Item item in turret.ActiveProjectiles)
{
if (item.GetComponent<Projectile>()?.IsStuckToTarget ?? false)
{
item.Condition = 0;
}
}
}
FadeOutColors();
protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
if (!IsClient)
{
if (Config != null)
{
if (Config.KillAgentsWhenEntityDies)
{
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
{
foreach (var character in Character.CharacterList)
{
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
// but as long as spawning is handled via status effects, I don't know if there is any better way.
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
// And if there was, the distance check should prevent killing the agents of a different organism.
if (character.SpeciesName == Config.OffensiveAgent)
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
if (Vector2.DistanceSquared(character.WorldPosition, Cave.StartPos.ToVector2()) < maxDistance * maxDistance)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
}
}
}
}
}
protectiveCells.Clear();
IsAlive = false;
}
partial void FadeOutColors();
public void Remove()
{
Kill();
ThalamusItems?.Clear();
Log.Debug("Removed thalacave");
}
public void RemoveThalamusItems()
{
foreach (MapEntity thalamusItem in ThalamusItems)
{
if (thalamusItem.Removed) continue;
thalamusItem.Remove();
}
}
// The client doesn't use these, so we don't have to sync them.
private readonly List<Character> protectiveCells = new List<Character>();
private float cellSpawnTimer;
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
private int CalculateCellCount(int minValue, int maxValue)
{
if (maxValue == 0) { return 0; }
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
}
private float GetSpawnTime()
{
float randomFactor = Config.AgentSpawnDelayRandomFactor;
float delay = Config.AgentSpawnDelay;
float min = delay;
float max = delay * 6;
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
}
void UpdateReinforcements(float deltaTime)
{
if (spawnOrgans.Count == 0) { return; }
cellSpawnTimer -= deltaTime;
if (cellSpawnTimer < 0)
{
TrySpawnCell(out _, spawnOrgans.GetRandomUnsynced());
cellSpawnTimer = GetSpawnTime();
}
}
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
{
cell = null;
if (protectiveCells.Count >= MaxCellCount) { return false; }
Vector2 worldSpawnPosition = targetEntity == null ? spawnPoints.GetRandomUnsynced().Center : targetEntity.WorldPosition;
// Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs.
cell = Character.Create(Config.DefensiveAgent, worldSpawnPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
protectiveCells.Add(cell);
cell.OnDeath += OnCellDeath;
cellSpawnTimer = GetSpawnTime();
return true;
}
void OperateTurrets(float deltaTime)
{
foreach (var turret in turrets)
{
turret.UpdateAutoOperate(deltaTime, true, Config.Entity);
}
}
void OnCellDeath(Character character, CauseOfDeath causeOfDeath) => protectiveCells.Remove(character);
#if SERVER
public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null)
{
msg.WriteBoolean(IsAlive);
}
#endif
}
}
@@ -0,0 +1,53 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using HarmonyLib;
using System.Reflection;
namespace MoreLevelContent.Shared.AI
{
public class MLCAIObjectiveManager : Singleton<MLCAIObjectiveManager>
{
public override void Setup()
{
MethodInfo info = AccessTools.Method(typeof(AIObjectiveManager), nameof(AIObjectiveManager.CreateObjective));
Main.Patch(info, postfix: new HarmonyMethod(typeof(MLCAIObjectiveManager), nameof(MLCAIObjectiveManager.AIObjectiveManager_CreateObjective)));
Log.Debug("Setup AI override");
}
/*
*
*
Exception: Object reference not set to an instance of an object. (System.NullReferenceException)
Target site: Void AIObjectiveManager_CreateObjective(Barotrauma.AIObjective ByRef, Barotrauma.AIObjectiveManager, Barotrauma.Character, Barotrauma.Order)
Stack trace:
at MoreLevelContent.Shared.AI.MLCAIObjectiveManager.AIObjectiveManager_CreateObjective(AIObjective& __result, AIObjectiveManager __instance, Character ___character, Order order)
at Barotrauma.AIObjectiveManager.CreateObjective_Patch1
*
*
*
*/
internal static void AIObjectiveManager_CreateObjective(ref AIObjective __result, AIObjectiveManager __instance, Character ___character, Order order, float priorityModifier)
{
if (order == null || order.IsDismissal) { return; }
AIObjective newObjective;
switch (order.Identifier.Value.ToLowerInvariant())
{
case "traitorinjectitem":
newObjective = new AITraitorObjectiveInjectItem(___character, __instance, priorityModifier, order.Option, order.GetTargetItems(order.Option));
break;
case "fightintrudersanysub":
newObjective = new AIFightIntrudersAnySubObjective(___character, __instance, priorityModifier);
break;
default:
return;
}
if (newObjective != null)
{
newObjective.Identifier = order.Identifier;
}
__result = newObjective;
}
}
}
@@ -0,0 +1,43 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using static Barotrauma.AIObjectiveIdle;
namespace MoreLevelContent.Shared.AI
{
// Targets list not populating is probably the issue
internal class AIFightIntrudersAnySubObjective : AIObjectiveFightIntruders
{
public AIFightIntrudersAnySubObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
}
public override bool AllowInAnySub => true;
public override void FindTargets()
{
foreach (Character target in GetList())
{
if (!IsValidTarget(target)) { continue; }
if (!character.CanSeeTarget(target)) { continue; }
if (!ignoreList.Contains(target))
{
Targets.Add(target);
if (Targets.Count > MaxTargets)
{
break;
}
}
}
}
}
}
@@ -0,0 +1,422 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using static Barotrauma.AIObjectiveIdle;
namespace MoreLevelContent.Shared.AI
{
internal class AITraitorObjectiveInjectItem : AIObjective
{
public override Identifier Identifier { get; set; } = "traitorinject".ToIdentifier();
// public override bool IsLoop { get => true; set => throw new NotImplementedException(); }
public override bool CanBeCompleted => true;
public AITraitorObjectiveInjectItem(Character character, AIObjectiveManager objectiveManager, float priorityModifier, Identifier option = default, ImmutableArray<Identifier> targetItems = default) : base(character, objectiveManager, priorityModifier, option)
{
targetItemIdentifier = targetItems.GetRandomUnsynced();
// Pick our hated job
hatedJob = JobPrefab.Prefabs.Where(p => !p.HiddenJob).GetRandomUnsynced().Identifier;
Log.Debug($"Hated Job: {hatedJob}");
character.IsEscorted = true; // lets them wander on the players sub
// for testing, makes them walk around
// like normal people
if (!Main.IsRelase)
{
var idleObjective = objectiveManager.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
{
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.Active;
}
}
actCasualTimer = 5;//Rand.Range(60, 120, Rand.RandSync.Unsynced);
ForceWalkPermanently = true;
character.OnAttacked += OnAttacked;
}
private bool HasItem => targetItem != null && character.Inventory.Contains(targetItem);
private bool IsActingCasual => actCasualTimer > 0;
private readonly Identifier targetItemIdentifier;
private readonly Identifier hatedJob;
const string TraitorTeamChangeIdentifier = "traitor";
const float CloseEnoughToInject = 100.0f;
const float InjectDelay = 0.5f;
private float actCasualTimer;
private bool _hasDoneSusAction = false;
private bool _victimWasUsingItemWhenPicked = false;
private bool _injectedVictim = false;
private float _injectTimer = InjectDelay;
readonly List<Character> previousVictims = new List<Character>();
AIObjectiveGetItem findItemTask;
AIObjectiveGoTo gotoVictimTask;
Item targetItem;
Character victim;
// Set the priority of this to be low if we're cuffed or we're still acting /casual/
public override float GetPriority()
{
Priority = IsActingCasual ? 0 : AIObjectiveManager.RunPriority - 0.5f;
return Priority;
}
private void OnAttacked(Character attacker, AttackResult attackResult)
{
if (attacker == null) return;
if (_hasDoneSusAction && attacker.IsOnPlayerTeam && (attackResult.Damage > 1))
{
// we're made! switch team and start attacking
if (!character.HasTeamChange(TraitorTeamChangeIdentifier))
{
_ = character.TryAddNewTeamChange(TraitorTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful));
var fight = objectiveManager.GetObjective<AIFightIntrudersAnySubObjective>();
fight.ForceHighestPriority = true;
objectiveManager.AddObjective(fight);
character.Speak(TextManager.Get($"dialog.{character.JobIdentifier}.found").Value);
Abandon = true;
}
}
}
public override void Update(float deltaTime)
{
if (character.Submarine == null) return;
if (!character.Submarine.IsConnectedTo(Submarine.MainSub)) return;
if (actCasualTimer > 0) actCasualTimer -= deltaTime;
base.Update(deltaTime);
}
AIObjectiveEscapeHandcuffs _EscapeHandcuffsSubObjective;
public override void Act(float deltaTime)
{
// don't do anything if we're cuffed
if (character.LockHands)
{
// If we're cuffed, try to break out
_ = TryAddSubObjective(ref _EscapeHandcuffsSubObjective, () => new AIObjectiveEscapeHandcuffs(character, objectiveManager));
return;
}
// don't do anything if we're not in the main sub and not docked to it
if (!character.Submarine.IsConnectedTo(Submarine.MainSub)) return;
// We're on the submarine, lets act casual for awhile
if (IsActingCasual) return;
// Time to spring into action
if (!HasItem)
{
// We don't have the target item yet, lets try to find it
FindTargetItem();
return;
}
// We've found our target item, lets find a target to inject it into
if (victim == null || victim.IsDead || victim.Removed)
{
victim = FindVictim();
previousVictims.Add(victim);
_victimWasUsingItemWhenPicked = victim.SelectedItem != null;
DebugSpeak($"Picked new victim: {victim.Name}");
_ = TryAddSubObjective(ref gotoVictimTask, () =>
new AIObjectiveGoTo(victim, character, objectiveManager, closeEnough: CloseEnoughToInject)
{
ForceWalkPermanently = true
},
() => GotToVictim(),
() => CouldntGetToVictim());
}
// Wait until we finish all the sub-objectives before doing anything
if (subObjectives.Any()) return;
if (!character.CanInteractWith(victim))
{
// Go to the victim and select it
RemoveSubObjective(ref gotoVictimTask);
_ = TryAddSubObjective(ref gotoVictimTask, () => new AIObjectiveGoTo(victim, character, objectiveManager, closeEnough: CloseEnoughToInject)
{
ForceWalkPermanently = true
},
onCompleted: () => GotToVictim(),
onAbandon: () => CouldntGetToVictim()
);
return;
}
// We're at the target, time to posion them!
if (!_injectedVictim) InjectItem(deltaTime);
}
// Leave the sceen of the crime
private void Scram()
{
character.DeselectCharacter();
Hull targetHull = GetEscapeHull();
_ = TryAddSubObjective(ref gotoVictimTask, () =>
{
DebugSpeak("Getting outta dodge!");
return new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
ForceWalkPermanently = true
};
},
onCompleted: () =>
{
float time = Rand.Range(60, 120, Rand.RandSync.Unsynced);
actCasualTimer = time;
DebugSpeak($"Time to act casual for {time}");
_victimWasUsingItemWhenPicked = false;
_injectedVictim = false;
_injectTimer = InjectDelay;
victim = null;
targetItem = null;
RemoveSubObjective(ref findItemTask);
RemoveSubObjective(ref gotoVictimTask);
}
);
}
private Hull GetEscapeHull()
{
List<Hull> potentialEscapeHulls = new List<Hull>();
List<float> hullWeights = new List<float>();
if (character.Submarine == null) return null;
foreach (var hull in character.Submarine.GetHulls(true))
{
// taken form AIObjectiveIdle
if (hull == null || hull.AvoidStaying || hull.IsWetRoom) { continue; }
// Ignore very narrow hulls.
if (hull.RectWidth < 200) { continue; }
// Ignore hulls that are too low to stand inside.
if (character.AnimController is HumanoidAnimController animController)
{
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
{
continue;
}
}
if (!potentialEscapeHulls.Contains(hull))
{
float weight = hull.RectWidth;
// prefer distant hulls
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist));
// prefer hulls with less water
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
weight *= distanceFactor * waterFactor;
potentialEscapeHulls.Add(hull);
hullWeights.Add(weight);
}
}
return !potentialEscapeHulls.Any() ? null : ToolBox.SelectWeightedRandom(potentialEscapeHulls, hullWeights, Rand.RandSync.Unsynced);
}
private void InjectItem(float deltaTime)
{
SteeringManager.Reset();
if (character.SelectedCharacter != victim)
{
character.SelectCharacter(victim);
}
if (_injectTimer > 0.0f)
{
_injectTimer -= deltaTime;
return;
}
_injectTimer = InjectDelay;
targetItem.ApplyTreatment(character, victim, victim.AnimController.MainLimb);
_injectedVictim = true;
DebugSpeak("Injected the item!");
// We did it, time to get outta here!
Scram();
}
private void GotToVictim()
{
// We got to them
RemoveSubObjective(ref gotoVictimTask);
// If they were using an item when we picked them, check if they're still using it
if (_victimWasUsingItemWhenPicked && victim.SelectedItem == null)
{
// They're not using it, abort
_victimWasUsingItemWhenPicked = false;
victim = null;
DebugSpeak($"They're not using the item anymore, abort!");
return;
}
}
private void CouldntGetToVictim()
{
// We couldn't get to them
RemoveSubObjective(ref gotoVictimTask);
DebugSpeak($"Couldn't get to victim: {victim.Name}! Picking a new one...");
_victimWasUsingItemWhenPicked = false;
victim = null;
return;
}
private void FindTargetItem()
{
if (targetItem == null)
{
_ = TryAddSubObjective(ref findItemTask, () =>
{
DebugSpeak("Going to find the poison :)");
return new AIObjectiveGetItem(character, targetItemIdentifier, objectiveManager, spawnItemIfNotFound: false, checkInventory: true)
{
ForceWalkPermanently = true,
AllowStealing = true,
SpeakIfFails = false
};
}, FoundItem, FailedToFindItem);
void FoundItem()
{
RemoveSubObjective(ref findItemTask);
_hasDoneSusAction = true;
TrySetTargetItem(character.Inventory.FindItemByIdentifier(targetItemIdentifier, true));
DebugSpeak("Found the poison");
actCasualTimer = 5; // act casual for a bit
}
void FailedToFindItem()
{
// Wait for a bit before trying to find it again
actCasualTimer = 10;
// Try to spawn the item in a traitor pannel
ItemPrefab prefab = FindItemPrefab(targetItemIdentifier);
if (!TryFindSuitableContainer(out Item container))
{
// We couldn't find a spot to spawn the item, just spawn it in our inventory
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory);
DebugSpeak("Couldn't find a place to spawn it, spawning it in my inventory!");
return;
}
// We found a spot to spawn the item in, lets spawn it there
Entity.Spawner.AddItemToSpawnQueue(prefab, container.OwnInventory);
DebugSpeak("Spawned the item in a hidden container!");
}
}
}
protected ItemPrefab FindItemPrefab(Identifier identifier) => (ItemPrefab)MapEntityPrefab.List.FirstOrDefault(prefab => prefab is ItemPrefab && prefab.Identifier == identifier);
private void TrySetTargetItem(Item item)
{
if (targetItem == item)
{
Log.Debug("Failed to get item!");
return;
}
targetItem = item;
}
private Character FindVictim()
{
return Character.CharacterList.Where(c => Filter(c)).OrderByDescending(c => CheckPriority(c)).First();
bool Filter(Character c) =>
!c.Removed &&
!c.IsDead &&
c.IsHuman &&
c.IsOnPlayerTeam &&
c.Submarine != null &&
c.Submarine.IsConnectedTo(Submarine.MainSub);
int CheckPriority(Character potentialVictim)
{
int priority = 0;
// Things that increase priority
if (potentialVictim.IsPlayer) priority += 2;
if (potentialVictim.JobIdentifier == hatedJob) priority += 2;
if (potentialVictim.SelectedItem != null)
{
if (potentialVictim.SelectedItem.Tags.Contains("turret")) priority += 3;
if (potentialVictim.SelectedItem.Tags.Contains("navterminal")) priority += 2;
if (potentialVictim.SelectedItem.Tags.Contains("fabricator")) priority++;
}
// Things that decrease priority
if (potentialVictim.SelectedItem == null) priority--;
if (potentialVictim.IsUnconscious) priority--;
if (potentialVictim.IsIncapacitated) priority--;
if (HumanAIController.GetHullSafety(potentialVictim.CurrentHull, potentialVictim) > HumanAIController.HULL_SAFETY_THRESHOLD) priority -= 4;
if (previousVictims.Contains(potentialVictim)) priority -= 4;
if (potentialVictim.IsBot)
{
// Reduce priority of bots more if we're in multiplayer
if (GameMain.IsMultiplayer && potentialVictim.IsBot) priority = 0;
else priority--;
}
return priority;
}
}
private bool TryFindSuitableContainer(out Item container)
{
List<Item> suitableItems = new List<Item>();
List<Identifier> allowedContainerIdentifiers = new List<Identifier>()
{
"loosevent",
"loosepanel"
};
foreach (Item item in Item.ItemList)
{
if (item.HiddenInGame || item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
if (item.Submarine == null || !item.Submarine.IsConnectedTo(character.Submarine))
{
continue;
}
if (item.GetComponent<ItemContainer>() != null && allowedContainerIdentifiers.Contains(((MapEntity)item).Prefab.Identifier))
{
if ((!item.OwnInventory.IsFull()))
{
suitableItems.Add(item);
}
}
}
container = suitableItems.GetRandomUnsynced();
return container != null;
}
protected void DebugSpeak(string msg)
{
if (!Main.IsRelase)
{
character.Speak(msg);
}
}
// never abort
//protected bool CheckObjectiveSpecific() => false;
public override bool CheckObjectiveState() => throw new NotImplementedException();
}
}
+203
View File
@@ -0,0 +1,203 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Microsoft.Xna.Framework;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Generation.Pirate;
using MoreLevelContent.Shared.Store;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
namespace MoreLevelContent
{
public class Commands : Singleton<Commands>
{
public static bool DisplayAllMapLocations = false;
public override void Setup()
{
CommandUtils.AddCommand("mlc_debugmissions", "Prints a debug output of all active missions", _debugMissions);
CommandUtils.AddCommand("mlc_dumppirateoutposts", "Dumps the file paths of all pirate outposts", _dumpPirateOutposts, isCheat: true);
CommandUtils.AddCommand("mlc_stepworld", "Fakes a world step", _stepWorld, isCheat: true);
CommandUtils.AddCommand("mlc_createdistressbeacon", "Tries to create a new distress beacon", _createDistress, isCheat: true);
CommandUtils.AddCommand("mlc_forcedistress", "Toggles forcing every level to spawn a distress mission, does nothing in multiplayer", _forceDistress, isCheat: true);
CommandUtils.AddCommand("mlc_forcepirate", "Toggles forcing a specific pirate base to spawn", _forcePirate, isCheat: true);
CommandUtils.AddCommand("mlc_toggleMapDisplay", "Toggles if all map locations should be shown, even if they are not discovered yet", _toggleMapDisplay, isCheat: true);
CommandUtils.AddCommand("mlc_showpatchnotes", "Displays the patch notes", _showPatchnotes);
CommandUtils.AddCommand("mlc_leveldatadebug", "Displays debug info on the current level's generation data", _isDistressActive);
}
private void _motionToggle(object[] args)
{
Log.Debug($"Checking {Item.ItemList.Count} items...");
Stopwatch sw = new Stopwatch();
Stopwatch totalTime = new Stopwatch();
int duration = 500;
totalTime.Start();
sw.Start();
foreach (Item item in Item.ItemList)
{
item.Update((float)(Timing.Step), GameMain.GameScreen.Cam);
if (sw.ElapsedTicks > duration)
{
MotionSensor sensor = item.GetComponent<MotionSensor>();
if (sensor == null) continue;
Log.Debug($"Disabling item: {item.Name} : {item.Prefab.Identifier} width: {sensor.RangeX} height: {sensor.RangeY} with interval {sensor.UpdateInterval} (in room {item.CurrentHull?.RoomName})");
sensor.UpdateInterval = 1;
}
sw.Restart();
}
}
private void _itemSpotCheck(object[] args)
{
Log.Debug("Preforming spot check...");
string[] arg = (string[])args[0];
int duration = 1;
if (arg.Length > 0 && !int.TryParse(arg[0], out duration));
bool listAll = false;
if (arg.Length > 0 && !bool.TryParse(arg[0], out listAll));
Log.Debug($"Checking {Item.ItemList.Count} items...");
Stopwatch sw = new Stopwatch();
Stopwatch totalTime = new Stopwatch();
totalTime.Start();
sw.Start();
foreach (Item item in Item.ItemList)
{
item.Update((float)(Timing.Step), GameMain.GameScreen.Cam);
if (sw.ElapsedTicks > duration || listAll)
{
Log.Debug($"-- Item: {item.Name} : {item.Prefab.Identifier} (in room {item.CurrentHull?.RoomName}) from package {item.Prefab.ContentPackage.Name} took {sw.ElapsedTicks} to update!");
}
sw.Restart();
}
Log.Debug($"Done: {totalTime.ElapsedMilliseconds}!");
}
private void _isDistressActive(object[] args)
{
if (Level.Loaded == null)
{
Log.Debug("No level loaded");
return;
}
Log.Debug($"HasDistress: {Level.Loaded.LevelData.MLC().HasDistress}, Steps left: {Level.Loaded.LevelData.MLC().DistressStepsLeft}");
}
private void _showPatchnotes(object[] args)
{
#if CLIENT
Barotrauma.MoreLevelContent.Client.UI.PatchNotes.Open();
#endif
}
private void _toggleMapDisplay(object[] args) => DisplayAllMapLocations = !DisplayAllMapLocations;
private void _debugMissions(object[] args)
{
foreach (var item in GameMain.GameSession.Missions)
{
Log.Debug(
$"MISSION DEBUG PRINTOUT\n" +
$"Name: {item.Name.Value}\n" +
$"Sonar: {item.SonarLabels.FirstOrDefault().Label}\n" +
$"Sonar Position: {item.SonarLabels.FirstOrDefault().Position}\n" +
$"Beacon: {Level.Loaded.MLC().BeaconConstructionStation.WorldPosition}");
}
}
private void _forceDistress(object[] args)
{//ForcedMissionIdentifier
if (GameMain.IsMultiplayer) return;
string additional = "";
string[] arg = (string[])args[0];
string identifier = arg.Length == 2 ? arg[1] : "";
if (arg.Length == 0)
{
Log.Debug("Missing arguments");
return;
}
if (!bool.TryParse(arg[0], out bool force))
{
Log.Debug("First argument must be a boolean");
return;
}
MapDirector.Instance.SetForcedDistressMission(force, identifier);
DebugConsole.NewMessage((force ? "Enabled" : "Disabled") + " forceing of distress mission " + identifier, Color.White);
}
private void _forcePirate(object[] args)
{//ForcedMissionIdentifier
string additional = "";
string[] arg = (string[])args[0];
string identifier = arg.Length == 2 ? arg[1] : "";
if (arg.Length == 0) return;
if (!bool.TryParse(arg[0], out bool force)) return;
PirateOutpostDirector.Instance.ForceSpawn = force;
PirateOutpostDirector.Instance.ForcedPirateOutpost = identifier;
if (!identifier.IsNullOrEmpty())
{
additional = ", all outpost will be " + identifier;
}
if (!force)
{
PirateOutpostDirector.Instance.ForcedPirateOutpost = "";
additional = ", all outpost will be random";
}
DebugConsole.NewMessage((force ? "Enabled" : "Disabled") + " forceing of pirate outposts" + additional, Color.White);
}
private void _dumpPirateOutposts(object[] args)
{
PirateStore.Instance.DumpPirateOutposts();
}
private void _stepWorld(object[] args)
{
if (!Main.IsCampaign) return;
#if CLIENT
NetUtil.SendServer(NetUtil.CreateNetMsg(NetEvent.COMMAND_STEPWORLD));
MapDirector.ForceWorldStep();
#endif
}
private void _createDistress(object[] args)
{
if (!Main.IsCampaign)
{
Log.Error($"Can't create a distress beacon when not in campaign! ({GameMain.GameSession?.Campaign != null} || {GameMain.IsSingleplayer}) -> {GameMain.GameSession?.Campaign != null || GameMain.IsSingleplayer}");
return;
}
if (Main.IsClient) return;
Log.Debug("Creating distress");
MapDirector.Instance.ForceDistress();
}
void _createDistressClient()
{
#if CLIENT
if (!GameMain.Client.HasPermission(Barotrauma.Networking.ClientPermissions.ManageRound))
{
Log.Error("No Perms");
return;
}
Log.Debug("Sent create distress request");
NetUtil.SendServer(NetUtil.CreateNetMsg(NetEvent.COMMAND_CREATEDISTRESS));
#endif
}
}
}
@@ -0,0 +1,408 @@
using Barotrauma.MoreLevelContent.Shared.Config;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Networking;
using MoreLevelContent;
using MoreLevelContent.Shared;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Xml.Linq;
namespace Barotrauma.MoreLevelContent.Config
{
/// <summary>
/// Shared
/// </summary>
partial class ConfigManager : Singleton<ConfigManager>
{
public override void Setup()
{
#if CLIENT
LoadConfig();
SetupClient();
#elif SERVER
SetupServer();
#endif
}
private void LoadConfig()
{
if (LuaCsFile.Exists(configFilepath))
{
try
{
Config = MLCLuaCsConfig.Load<MLCConfig>(configFilepath);
if (Config.Version != Main.Version)
{
MigrateConfig();
Log.Debug("Migrated Config");
}
#if CLIENT
DisplayPatchNotes();
SetConfig(Config);
#endif
Config.Version = Main.Version;
SaveConfig();
return;
} catch
{
Log.Warn("Failed to load config!");
DefaultConfig();
}
} else
{
Log.Debug("File doesn't exist");
DefaultConfig();
}
}
private void MigrateConfig()
{
Config.NetworkedConfig.GeneralConfig.EnableThalamusCaves = true;
Config.NetworkedConfig.GeneralConfig.DistressSpawnChance = 35;
Config.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons = 5;
Config.NetworkedConfig.PirateConfig.PeakSpawnChance = 35;
Config.NetworkedConfig.PirateConfig.EnablePirateBases = true;
Config.NetworkedConfig.GeneralConfig.EnableConstructionSites = true;
Config.NetworkedConfig.GeneralConfig.EnableDistressMissions = true;
Config.NetworkedConfig.GeneralConfig.EnableMapFeatures = true;
Config.NetworkedConfig.GeneralConfig.EnableRelayStations = true;
}
private void DefaultConfig()
{
Log.Debug("Defaulting config...");
Config = MLCConfig.GetDefault();
#if CLIENT
SaveConfig(); // Only save the default config on the client, look into changing this for dedicated servers
DisplayPatchNotes(true);
#endif
}
private void SaveConfig()
{
MLCLuaCsConfig.Save(configFilepath, Config);
Log.Debug("Saved config to disk!");
}
private void ReadNetConfig(ref IReadMessage inMsg)
{
try
{
Config.NetworkedConfig = INetSerializableStruct.Read<NetworkedConfig>(inMsg);
} catch(Exception err)
{
Log.Debug(err.ToString());
}
}
private void WriteConfig(ref IWriteMessage outMsg) =>
(Config.NetworkedConfig as INetSerializableStruct).Write(outMsg);
private static readonly string configFilepath = $"{ACsMod.GetStoreFolder<Main>()}/MLCConfig.xml";
public MLCConfig Config;
}
/// <summary>
/// Literally a direct copy of the class "LuaCsConfig" because it doesn't exist anymore and i do not care enough to
/// change this mod to use the new correct config system because i couldn't find any docs on it and its 1:21am rn
/// </summary>
class MLCLuaCsConfig
{
private enum ValueType
{
None,
Text,
Integer,
Decimal,
Boolean,
Collection,
Object,
Enum
}
private static Type[] LoadDocTypes(XElement typesElem)
{
var result = new List<Type>();
var loadedTypes = AssemblyLoadContext.All
.Where(alc => alc != AssemblyLoadContext.Default)
.SelectMany(alc => alc.Assemblies)
.SelectMany(asm => asm.GetTypes())
.ToImmutableArray();
foreach (var elem in typesElem.Elements())
{
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
if (!typesFound.Any())
{
ModUtils.Logging.PrintError(
$"{nameof(MLCLuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
continue;
}
result.AddRange(typesFound);
}
return result.ToArray();
}
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
{
return types.Select(t => new XElement("Type", t.ToString()));
}
private static Type GetTypeAttr(Type[] types, XElement elem)
{
var idx = elem.GetAttributeInt("Type", -1);
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
return types[idx];
}
private static ValueType GetValueType(XElement elem)
{
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
if (result != null) return (ValueType)result;
else return ValueType.None;
}
private static object ParseValue(Type[] types, XElement elem)
{
var type = GetValueType(elem);
if (elem.IsEmpty) return null;
if (type == ValueType.Enum)
{
var tType = GetTypeAttr(types, elem);
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
else return null;
}
if (type == ValueType.Collection)
{
var tType = GetTypeAttr(types, elem);
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
var gArg = tInt.GetGenericArguments()[0];
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
object result = null;
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var param = c.GetParameters();
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
});
if (ctor != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
result = ctor.Invoke(new object[] { castElems });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Add") return false;
var param = m.GetParameters();
return param.Count() == 1 && param[0].ParameterType == gArg;
});
if (ctor != null && addMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(null);
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
}
}
if (result == null)
{
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
{
if (m.Name != "Set") return false;
var param = m.GetParameters();
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
});
if (ctor != null || setMethod != null)
{
var elements = elem.Elements().Select(x => ParseValue(types, x));
result = ctor.Invoke(new object[] { elements.Count() });
int i = 0;
foreach (var el in elements)
{
setMethod.Invoke(result, new object[] { i, el });
i++;
}
}
}
return result;
}
else if (type == ValueType.Text) return elem.Value;
else if (type == ValueType.Integer)
{
int.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Decimal)
{
float.TryParse(elem.Value, out var num);
return num;
}
else if (type == ValueType.Boolean)
{
bool.TryParse(elem.Value, out var boolean);
return boolean;
}
else if (type == ValueType.Object)
{
var tType = GetTypeAttr(types, elem);
if (tType == null) return null;
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
object result = null;
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
if (ctor == null)
{
if (!tType.IsValueType) return null;
result = Activator.CreateInstance(tType);
}
else result = ctor.Invoke(null);
foreach (var el in elem.Elements())
{
var value = ParseValue(types, el);
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
if (field != null) field.SetValue(result, value);
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
if (property != null) property.SetValue(result, value);
}
return result;
}
else return elem.Value;
}
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
{
if (!types.Contains(type)) types.Add(type);
elem.SetAttributeValue("Type", types.IndexOf(type));
}
private static XElement ParseObject(List<Type> types, string name, object value)
{
XElement result = new XElement(name);
if (value != null)
{
var tType = value.GetType();
if (tType.IsEnum)
{
result.SetAttributeValue("Value", ValueType.Enum);
AddTypeAttr(types, tType, result);
result.Value = Enum.GetName(tType, value) ?? "";
}
else if (value is string str)
{
result.SetAttributeValue("Value", ValueType.Text);
result.Value = str;
}
else if (value is int integer)
{
result.SetAttributeValue("Value", ValueType.Integer);
result.Value = integer.ToString();
}
else if (value is float || value is double)
{
result.SetAttributeValue("Value", ValueType.Decimal);
result.Value = value.ToString();
}
else if (value is bool boolean)
{
result.SetAttributeValue("Value", ValueType.Boolean);
result.Value = boolean.ToString();
}
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
result.SetAttributeValue("Value", ValueType.Collection);
AddTypeAttr(types, tType, result);
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
while (enumerator.MoveNext())
{
var elVal = ParseObject(types, "Item", enumerator.Current);
result.Add(elVal);
}
}
else if (tType.IsClass || tType.IsValueType)
{
result.SetAttributeValue("Value", ValueType.Object);
AddTypeAttr(types, tType, result);
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
foreach (var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
}
else
{
result.SetAttributeValue("Value", ValueType.None);
result.Value = value.ToString();
}
}
return result;
}
public static T Load<T>(FileStream file)
{
var doc = XDocument.Load(file);
var rootElems = doc.Root.Elements().ToArray();
var types = rootElems[0];
var elem = rootElems[1];
var dict = ParseValue(LoadDocTypes(types), elem);
if (dict.GetType() == typeof(T)) return (T)dict;
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
}
public static void Save(FileStream file, object obj)
{
var types = new List<Type>();
var elem = ParseObject(types, "Root", obj);
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
var doc = new XDocument(root);
doc.Save(file);
}
public static T Load<T>(string path)
{
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
}
public static void Save(string path, object obj)
{
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
}
}
}
@@ -0,0 +1,23 @@
using Barotrauma;
public struct ClientConfig
{
public bool Verbose;
public bool Internal;
public static ClientConfig GetDefault()
{
return new ClientConfig()
{
Verbose = false,
Internal = false
};
}
public override string ToString() =>
$"\n-Debug Config-\n" +
$"Verbose: {Verbose}\n" +
$"Internal: {Internal}\n";
}
@@ -0,0 +1,42 @@
using Barotrauma.Networking;
namespace Barotrauma.MoreLevelContent.Shared.Config
{
[NetworkSerialize]
public struct LevelConfig : INetSerializableStruct
{
// public bool MoveRuins;
public bool EnableDistressMissions;
public bool EnableConstructionSites;
public bool EnableRelayStations;
public bool EnableMapFeatures;
public bool EnableThalamusCaves;
public int RuinMoveChance;
public int MaxActiveDistressBeacons;
public int DistressSpawnChance;
public float DistressSpawnPercentage => DistressSpawnChance / 100f;
public static LevelConfig GetDefault()
{
LevelConfig config = new LevelConfig
{
EnableThalamusCaves = true,
RuinMoveChance = 25,
MaxActiveDistressBeacons = 5,
DistressSpawnChance = 35,
EnableConstructionSites = true,
EnableDistressMissions = true,
EnableMapFeatures = true,
EnableRelayStations = true
};
return config;
}
public override string ToString() =>
$"\n-General Config-\n" +
$"RuinMoveChance: {RuinMoveChance}";
}
}
@@ -0,0 +1,29 @@
using Barotrauma.Networking;
using MoreLevelContent;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Config
{
public struct MLCConfig
{
public NetworkedConfig NetworkedConfig;
public ClientConfig Client;
public string Version;
public static MLCConfig GetDefault()
{
MLCConfig config = new MLCConfig
{
NetworkedConfig = NetworkedConfig.GetDefault(),
Client = ClientConfig.GetDefault(),
Version = Main.Version
};
return config;
}
public override string ToString() =>
$"\n_= MLC CONFIG =_" + NetworkedConfig.ToString() + Client.ToString();
}
}
@@ -0,0 +1,27 @@
using Barotrauma.Networking;
using System;
namespace Barotrauma.MoreLevelContent.Shared.Config
{
[NetworkSerialize]
public struct NetworkedConfig : INetSerializableStruct
{
[NetworkSerialize]
public PirateConfig PirateConfig;
[NetworkSerialize]
public LevelConfig GeneralConfig;
public static NetworkedConfig GetDefault()
{
NetworkedConfig config = new NetworkedConfig
{
PirateConfig = PirateConfig.GetDefault(),
GeneralConfig = LevelConfig.GetDefault()
};
return config;
}
public override string ToString() => PirateConfig.ToString() + GeneralConfig.ToString();
}
}
@@ -0,0 +1,45 @@
using Barotrauma.Networking;
using MoreLevelContent.Shared;
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Config
{
[NetworkSerialize]
public struct PirateConfig : INetSerializableStruct
{
public bool EnablePirateBases;
public Int32 BasePirateSpawnChance;
public Int32 PeakSpawnChance;
public Int32 BaseHuskChance;
public Single SpawnChanceNoise;
public Single DifficultyNoise;
public bool AddDiffPerPlayer;
public bool DisplaySonarMarker;
public static PirateConfig GetDefault()
{
PirateConfig config = new PirateConfig()
{
PeakSpawnChance = 25,
BasePirateSpawnChance = 0,
BaseHuskChance = 1,
SpawnChanceNoise = 10.0f,
DifficultyNoise = 10.0f,
AddDiffPerPlayer = true,
DisplaySonarMarker = false,
EnablePirateBases = true
};
return config;
}
public override string ToString() =>
$"\n-Pirate Config-\n" +
$"Spawn Chance: {BasePirateSpawnChance}\n" +
$"Husk Chance: {BaseHuskChance}\n" +
$"Spawn Noise: {SpawnChanceNoise}\n" +
$"Diff Noise: {DifficultyNoise}\n" +
$"Add Diff: {AddDiffPerPlayer}";
}
}
@@ -0,0 +1,30 @@
using Barotrauma;
using MoreLevelContent.Missions;
using System;
using System.Collections.Generic;
namespace MoreLevelContent.Shared.Content
{
public class CustomMissions
{
public static readonly Dictionary<CustomMissionType, Type> MissionDefs = new()
{
{ CustomMissionType.BeaconConstruction, typeof(BeaconConstMission) },
{ CustomMissionType.DistressEscort, typeof(DistressEscortMission) },
{ CustomMissionType.DistressSubmarine, typeof(DistressSubmarineMission) },
{ CustomMissionType.DistressGhostship, typeof(DistressGhostshipMission) },
{ CustomMissionType.CablePuzzle, typeof(CablePuzzleMission) }
};
}
public enum CustomMissionType
{
BeaconConstruction,
DistressEscort,
DistressSubmarine,
DistressGhostship,
CablePuzzle
//DistressOutpost
}
}
@@ -0,0 +1,10 @@
using Barotrauma;
using MoreLevelContent.Missions;
using System;
using System.Collections.Generic;
namespace MoreLevelContent.Custom.Missions
{
}
@@ -0,0 +1,30 @@
using Barotrauma;
using System.Runtime.CompilerServices;
using System;
using System.Xml.Linq;
namespace MoreLevelContent.Shared.Data
{
class Character_MLCData
{
public XElement NPCElement;
public bool IsDistressShuttle;
public bool IsDistressDiver;
}
public static partial class MLCData
{
private static readonly ConditionalWeakTable<Character, Character_MLCData> character_data = new();
internal static Character_MLCData MLC(this Character characterData) => character_data.GetOrCreateValue(characterData);
internal static void AddData(this Character characterData, Character_MLCData additional)
{
try
{
character_data.Add(characterData, additional);
}
catch (Exception e) { Log.Error(e.ToString()); }
}
}
}
@@ -0,0 +1,60 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace MoreLevelContent.Shared.Data
{
public class DataBase
{
public void SaveData(XElement saveFile)
{
var saveFields = GetSaveFields();
foreach (var field in saveFields)
{
saveFile.SetAttributeValue(field.Name, field.GetValue(this));
}
SaveSpecific(saveFile);
}
public void LoadData(XElement saveFile)
{
var saveFields = GetSaveFields();
foreach (var field in saveFields)
{
XAttribute attr = saveFile.GetAttribute(field.Name);
if (attr != null)
{
if (field.FieldType.IsEnum)
{
field.SetValue(this, Enum.Parse(field.FieldType, attr.Value));
continue;
}
field.SetValue(this, Convert.ChangeType(attr.Value, field.FieldType));
}
else
{
field.SetValue(this, ((AttributeSaveData)field.GetCustomAttribute(typeof(AttributeSaveData))).DefaultFieldValue);
}
}
LoadSpecific(saveFile);
}
protected virtual void LoadSpecific(XElement saveFile) { }
protected virtual void SaveSpecific(XElement saveFile) { }
private IEnumerable<FieldInfo> GetSaveFields() => GetType().GetFields().Where(f => f.IsDefined(typeof(AttributeSaveData), false));
}
/// <summary>
/// Simple data that can be stringified into an xml attribute
/// </summary>
public class AttributeSaveData : Attribute
{
public AttributeSaveData(object defaultValue) => DefaultFieldValue = defaultValue;
public object DefaultFieldValue;
}
}
@@ -0,0 +1,218 @@
using Barotrauma;
using Microsoft.CodeAnalysis;
using MoreLevelContent.Shared.Generation;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
namespace MoreLevelContent.Shared.Data
{
public class LevelData_MLCData : DataBase
{
[AttributeSaveData(false)]
public bool HasBeaconConstruction;
[AttributeSaveData(false)]
public bool HasDistress;
[AttributeSaveData(7)]
public int DistressStepsLeft;
[AttributeSaveData(false)]
public bool HasPirateActivity;
[AttributeSaveData(false)]
public bool HasBlackMarket;
[AttributeSaveData(RelayStationStatus.None)]
public RelayStationStatus RelayStationStatus;
[AttributeSaveData(false)]
public bool HasLostCargo;
[AttributeSaveData(4)]
public int CargoStepsLeft;
[AttributeSaveData(0)]
public int RequestedU;
[AttributeSaveData(0)]
public int RequestedS;
[AttributeSaveData(0)]
public int RequestedE;
[AttributeSaveData(TriangulationTarget.None)]
public TriangulationTarget TriangulationTarget;
public MapFeatureData MapFeatureData;
internal PirateData PirateData;
public LevelData_MLCData()
{
MapFeatureData = new MapFeatureData();
PirateData = new PirateData();
}
public bool HasRelayStation => RelayStationStatus != RelayStationStatus.None;
public LocalizedString GetRequestedSupplies()
{
List<LocalizedString> requestedSuppliesList = new();
// Utility
if (RequestedU > 0)
{
requestedSuppliesList.Add(TextManager.GetWithVariable("mlc.beaconconstutility", "[count]", RequestedU.ToString()));
}
// Structural
if (RequestedS > 0)
{
requestedSuppliesList.Add(TextManager.GetWithVariable("mlc.beaconconststructural", "[count]", RequestedS.ToString()));
}
// Electrical
if (RequestedE > 0)
{
requestedSuppliesList.Add(TextManager.GetWithVariable("mlc.beaconconstelectrical", "[count]", RequestedE.ToString()));
}
switch (requestedSuppliesList.Count)
{
case 1:
return TextManager.GetWithVariable("mlc.beaconconstone", "[supply1]", requestedSuppliesList[0]);
case 2:
return TextManager.GetWithVariables("mlc.beaconconsttwo", ("[supply1]", requestedSuppliesList[0]), ("[supply2]", requestedSuppliesList[1]));
case 3:
return TextManager.GetWithVariables("mlc.beaconconstthree", ("[supply1]", requestedSuppliesList[0]), ("[supply2]", requestedSuppliesList[1]), ("[supply3]", requestedSuppliesList[2]));
default:
Log.Error($"Invalid amount of requested supplies {requestedSuppliesList.Count}");
return null;
}
}
protected override void LoadSpecific(XElement saveFile)
{
var pirateData = saveFile.GetChildElement("PirateData");
if (pirateData != null)
{
PirateData = new PirateData()
{
Status = pirateData.GetAttributeEnum("status", PirateOutpostStatus.None),
Difficulty = pirateData.GetAttributeFloat("difficulty", 0),
Revealed = pirateData.GetAttributeBool("revealed", false)
};
}
var mapFeatureData = saveFile.GetChildElement("MapFeatureData");
if (mapFeatureData != null)
{
MapFeatureData = new MapFeatureData()
{
Name = mapFeatureData.GetAttributeIdentifier("name", null),
Revealed = mapFeatureData.GetAttributeBool("revealed", false)
};
if (MapFeatureModule.TryGetFeature(MapFeatureData.Name, out MapFeature feature))
{
MapFeatureData.Feature = feature;
}
}
}
protected override void SaveSpecific(XElement saveFile)
{
var pirateData = new XElement("PirateData",
new XAttribute("status", PirateData.Status),
new XAttribute("difficulty", PirateData.Difficulty),
new XAttribute("revealed", PirateData.Revealed));
var mapFeatureData = new XElement("MapFeatureData",
new XAttribute("name", MapFeatureData.Name),
new XAttribute("revealed", MapFeatureData.Revealed));
saveFile.Add(pirateData);
saveFile.Add(mapFeatureData);
}
}
public class MapFeatureData
{
public Identifier Name;
public bool Revealed;
internal MapFeature Feature;
public bool HasFeature => Feature != null;
}
public static partial class MLCData
{
private static readonly ConditionalWeakTable<LevelData, LevelData_MLCData> levelData_data = new();
internal static LevelData_MLCData MLC(this LevelData levelData) => levelData_data.GetOrCreateValue(levelData);
internal static void AddData(this LevelData levelData, LevelData_MLCData additional)
{
try
{
levelData_data.Add(levelData, additional);
} catch(Exception e) { Log.Error(e.ToString()); }
}
}
public enum PirateOutpostStatus
{
None,
Active,
Destroyed,
Husked
}
public enum RelayStationStatus
{
None,
Inactive,
Active
}
public enum TriangulationTarget
{
None,
MapFeature,
PirateBase,
Treasure
}
internal class PirateData
{
public PirateData()
{
Status = PirateOutpostStatus.None;
Difficulty = 0;
Revealed = false;
}
public PirateData(PirateSpawnData spawnData)
{
Difficulty = 0;
Status = PirateOutpostStatus.None;
Revealed = false;
if (spawnData.WillSpawn)
{
Status = PirateOutpostStatus.Active;
Difficulty = spawnData.PirateDifficulty;
if (spawnData.Husked)
{
Status = PirateOutpostStatus.Husked;
}
}
}
public PirateOutpostStatus Status;
public float Difficulty;
public bool Revealed;
public bool HasPirateBase => Status != PirateOutpostStatus.None;
}
}
@@ -0,0 +1,48 @@
using Barotrauma;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace MoreLevelContent.Shared.Data
{
class Level_MLCData : DataBase
{
public ContentFile RelayStationFile;
public Submarine BeaconConstructionStation;
public Submarine RelayStation;
public Item DropOffPoint;
public bool CheckSuppliesDelivered()
{
if (DropOffPoint == null)
{
Log.Error("No drop off point specified!!");
return false;
}
int uCount = DropOffPoint.ContainedItems.Where(it => it.Tags.Contains("supply_utility")).Count();
int sCount = DropOffPoint.ContainedItems.Where(it => it.Tags.Contains("supply_structural")).Count();
int eCount = DropOffPoint.ContainedItems.Where(it => it.Tags.Contains("supply_electrical")).Count();
return
uCount >= Level.Loaded.LevelData.MLC().RequestedU && // electrical
sCount >= Level.Loaded.LevelData.MLC().RequestedS && // structural
eCount >= Level.Loaded.LevelData.MLC().RequestedE; // electrical
}
}
public static partial class MLCData
{
private static readonly ConditionalWeakTable<Level, Level_MLCData> level_data = new();
internal static Level_MLCData MLC(this Level levelData) => level_data.GetOrCreateValue(levelData);
internal static void AddData(this Level levelData, Level_MLCData additional)
{
try
{
level_data.Add(levelData, additional);
}
catch (Exception e) { Log.Error(e.ToString()); }
}
}
}
@@ -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;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Items
{
internal class SonarGuide : Powered
{
[Serialize(30.0f, IsPropertySaveable.Yes, description: "How often the guide sends out a ping."), Editable]
public float PingInterval { get; private set; }
[Serialize(30000.0f, IsPropertySaveable.Yes, description: "How far away this guide can be detected from, 10000.0f is the default sonar range."), Editable]
public float Range { get; private set; }
private float _Interval;
public SonarGuide(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
#if CLIENT
if (Voltage >= MinVoltage)
{
if (_Interval > 0)
{
_Interval -= deltaTime;
return;
}
_Interval = PingInterval;
foreach (Item item in Item.ItemList)
{
item.GetComponent<Sonar>()?.AddSonarCircle(Item.WorldPosition, (Sonar.BlipType)5, blipCount: 100, range: Range);
}
}
#endif
}
}
}
@@ -0,0 +1,41 @@
using Barotrauma;
using Barotrauma.Items.Components;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Utils;
using System.Xml.Linq;
namespace MoreLevelContent.Items
{
internal class SonarJammer : Powered
{
private readonly int _Strength;
private bool _ActiveDisturbance;
public SonarJammer(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
_Strength = element.GetAttributeInt("strength", 100);
}
public override void Update(float deltaTime, Camera cam)
{
UpdateOnActiveEffects(deltaTime);
#if CLIENT
if (Voltage >= MinVoltage && !_ActiveDisturbance)
{
SonarExtensions.Instance.Add(Item, _Strength);
_ActiveDisturbance = true;
Log.Debug("Added Disturbance");
}
if (Voltage < MinVoltage && _ActiveDisturbance)
{
SonarExtensions.Instance.Remove(Item);
_ActiveDisturbance = false;
Log.Debug("Removed Disturbance");
}
#endif
}
}
}
+57
View File
@@ -0,0 +1,57 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Config;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
namespace MoreLevelContent.Shared
{
public static class Log
{
private static readonly bool verbose = true;
public static void Debug(string msg) => LogBase("MLC D ", msg, "null", Color.MediumPurple);
public static void Warn(string msg) => LogBase("MLC W ", msg, "null", Color.Yellow);
public static void Error(string msg) => LogBase("MLC E ", msg, "null", Color.Red);
public static void InternalDebug(string msg)
{
if (!ConfigManager.Instance.Config.Client.Internal) return;
LogBase("MLC ID", msg, "null", Color.Purple);
}
public static void Verbose(string msg)
{
if (!ConfigManager.Instance.Config.Client.Verbose) return;
LogBase("MLC V", msg, "null", Color.LightGray);
}
private static void LogBase(string prefix, string message, string empty, Color col)
{
if (message == null) { message = empty; }
string str = message.ToString();
for (int i = 0; i < str.Length; i += 1024)
{
string subStr = str.Substring(i, Math.Min(1024, str.Length - i));
#if SERVER
if (GameMain.Server != null)
{
foreach (var c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", "[SERVER] " + subStr, ChatMessageType.Console, null, textColor: Color.MediumPurple), c);
}
GameServer.Log("[SERVER] " + prefix + subStr, ServerLog.MessageType.ServerMessage);
}
#endif
}
#if SERVER
DebugConsole.NewMessage("[SERVER] " + message.ToString(), Color.White);
#else
DebugConsole.NewMessage("[CLIENT] " + message.ToString(), col);
#endif
}
}
}
+127
View File
@@ -0,0 +1,127 @@
using Barotrauma;
using Barotrauma.LuaCs;
using Barotrauma.MoreLevelContent.Config;
using HarmonyLib;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.AI;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.XML;
using System;
using System.Collections.Generic;
using System.Reflection;
using static Barotrauma.CampaignMode;
namespace MoreLevelContent
{
/// <summary>
/// Shared
/// </summary>
partial class Main : ACsMod
{
public static bool IsCampaign => GameMain.GameSession?.Campaign != null || GameMain.IsSingleplayer;
public static bool IsRunning => GameMain.GameSession?.IsRunning ?? false;
public static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
public const string GUID = "com.dak.mlc";
public static bool IsRelase = true;
public static bool IsNightly = false;
public static bool PreventRoundEnd = false;
public static Main Instance;
public static string Version = "1.0.0";
private static LevelContentProducer levelContentProducer;
internal static Harmony Harmony;
public Main()
{
Instance = this;
Log.Debug("Mod Init");
ConfigManager.Instance.Setup();
Init();
#if SERVER
InitServer();
#elif CLIENT
InitClient();
#endif
}
public static void Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null, HarmonyMethod finalizer = null)
{
Harmony.Patch(original, prefix, postfix, transpiler, finalizer, null);
}
public void Init()
{
Log.Verbose("Reflecting methods...");
var level_onCreateWrecks = typeof(Level).GetMethod("CreateWrecks", BindingFlags.NonPublic | BindingFlags.Instance);
var level_onSpawnNPC = typeof(Level).GetMethod(nameof(Level.SpawnNPCs));
var level_generate = typeof(Level).GetMethod(nameof(Level.Generate));
var gameSession_before_startRound = typeof(GameSession).GetMethod(nameof(GameSession.StartRound), new Type[] { typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo) });
var eventManager_TriggerOnEndRoundActions = AccessTools.Method(typeof(GameSession), "EndRound");
Harmony = new Harmony("com.mlc.dak");
MoveRuins.Init();
Hooks.Instance.Setup();
levelContentProducer = new LevelContentProducer();
MapDirector.Instance.Setup();
XMLManager.Instance.Setup();
InjectionManager.Instance.Setup();
Commands.Instance.Setup();
CompatabilityHelper.Instance.Setup();
ReflectionInfo.Instance.Setup();
MLCAIObjectiveManager.Instance.Setup();
if (!levelContentProducer.Active)
{
Log.Error("Level content producer is disabled!");
return;
}
Log.Verbose("Patching...");
Patch(level_onCreateWrecks, postfix: AccessTools.Method(typeof(Main), nameof(OnCreateWrecks)));
Patch(level_onSpawnNPC, prefix: AccessTools.Method(typeof(Main), nameof(OnSpawnNPC)));
Patch(level_generate, prefix: AccessTools.Method(typeof(Main), nameof(OnLevelGenerate)));
Patch(gameSession_before_startRound, prefix: AccessTools.Method(typeof(Main), nameof(OnBeforeStartRound)));
_ = Harmony.Patch(eventManager_TriggerOnEndRoundActions, postfix: new HarmonyMethod(AccessTools.Method(typeof(Main), nameof(Main.OnRoundEnd))));
Log.Verbose("Done!");
}
public static void OnCreateWrecks()
{
levelContentProducer.CreateWrecks();
}
public static void OnSpawnNPC()
{
levelContentProducer.SpawnNPCs();
}
public static void OnLevelGenerate(LevelData levelData, bool mirror)
{
levelContentProducer.LevelGenerate(levelData, mirror);
MapDirector.Instance.OnLevelGenerate(levelData, mirror);
}
public static void OnBeforeStartRound()
{
levelContentProducer.StartRound();
}
public static void OnRoundEnd(CampaignMode.TransitionType transitionType)
{
levelContentProducer.EndRound();
MapDirector.Instance.RoundEnd(transitionType);
}
public override void Stop() => Main.Harmony.UnpatchSelf();
}
}
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
namespace MoreLevelContent.Missions
{
partial class BeaconConstMission : Mission
{
private readonly LocalizedString sonarLabel;
private readonly int PriceUtility;
private readonly int PriceStructure;
private readonly int PriceElectric;
public BeaconConstMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
sonarLabel = TextManager.Get("beaconconsonarlabel");
var supplyCosts = prefab.ConfigElement.GetChildElement("supplycosts");
PriceUtility = GetPrice("supply_utility");
PriceStructure = GetPrice("supply_structural");
PriceElectric = GetPrice("supply_electrical");
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
description = descriptionWithoutReward.Replace("[reward]", rewardText);
int GetPrice(string supplyType) => supplyCosts.GetChildElement(supplyType).GetAttributeInt("price", 0);
}
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
public override int Reward => GetReward();
public override float GetBaseReward(Submarine sub) => GetReward();
private int GetReward()
{
LocationConnection connection = Locations[0].Connections.Find(lc => lc.Locations.Contains(Locations[1]));
var levelData = connection.LevelData.MLC();
int reward = (int)(((PriceUtility * levelData.RequestedU) +
(PriceStructure * levelData.RequestedS) +
(PriceElectric * levelData.RequestedE)) * 2.5);
return reward;
}
public override void StartMissionSpecific(Level level) => description = description.Replace("[requestedsupplies]", level.LevelData.MLC().GetRequestedSupplies());
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (level.MLC().BeaconConstructionStation == null)
{
yield break;
}
Vector2 worldPos = level.MLC().BeaconConstructionStation.WorldPosition;
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, worldPos);
}
}
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
if (State == 0 && level.MLC().CheckSuppliesDelivered())
{
State = 1;
}
}
public override void EndMissionSpecific(bool completed)
{
if (completed && level.LevelData != null)
{
level.LevelData.IsBeaconActive = true;
level.LevelData.HasBeaconStation = true;
level.LevelData.MLC().HasBeaconConstruction = false;
}
}
public override void AdjustLevelData(LevelData levelData) => levelData.MLC().HasBeaconConstruction = true;
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => level.MLC().CheckSuppliesDelivered();
}
}
@@ -0,0 +1,338 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Resources;
using System.Xml.Linq;
namespace MoreLevelContent.Missions
{
// Shared
internal partial class CablePuzzleMission : Mission
{
/// <summary>
/// This sucks ass but we should /NEVER/ have more than one relay station in one level
/// </summary>
public static SubmarineFile SubmarineFile { get; set; }
const float INTERVAL = 2.5f;
const int RANGE_MIN = 50;
const int RANGE_MAX = 200;
const int REQUIRED_CYCLES = 2;
const float REQUIRED_CYCLE_TIME = 1;
const double CYCLE_GRACE_PERIOD = Timing.Step * 2;
private readonly LocalizedString defaultSonarLabel;
private readonly string terminalTag;
private readonly XElement _SubmarineConfig;
private Submarine _Station;
private LevelData _LevelData;
private float _Timer = INTERVAL;
public CablePuzzleMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
_SubmarineConfig = prefab.ConfigElement.GetChildElement("Submarine");
defaultSonarLabel = TextManager.Get("relaysonarlabel");
terminalTag = _SubmarineConfig.GetAttributeString("welcomemsg", "relayrepair.terminal");
SubmarineFile = null;
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (_Station == null) yield break;
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel, _Station.WorldPosition);
}
}
public override void SetLevel(LevelData level)
{
if (_LevelData != null)
{
//level already set
return;
}
_LevelData = level;
SetSub(_SubmarineConfig, Prefab);
}
public static void SetSub(XElement config, MissionPrefab prefab)
{
ContentPath subPath = config.GetAttributeContentPath("path", prefab.ContentPackage);
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the relay station mission \"{prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
SubmarineFile = file;
Log.Debug("Set relay station sub file");
}
private MemoryComponent _WpInput;
private MemoryComponent _WpTarget;
private LightComponent _WpLight;
private Terminal _WpHint;
private readonly List<SignalOperation> _Operations = new();
private readonly SignalOperation[] _Sequence = new SignalOperation[4];
private enum OperationType
{
Add,
Sub,
Mul,
Div
}
private struct SignalOperation
{
public SignalOperation(OperationType type, int value)
{
_Value = value;
_Operation = type;
}
private readonly OperationType _Operation;
private readonly int _Value;
public override string ToString() => $"{_Operation} {_Value}";
public int Run(int input)
{
switch (_Operation)
{
case OperationType.Add:
return input += _Value;
case OperationType.Sub:
return input -= _Value;
case OperationType.Mul:
return input *= _Value;
case OperationType.Div:
return input /= _Value;
}
throw new NotImplementedException();
}
}
public override void StartMissionSpecific(Level level)
{
_Station = level.MLC().RelayStation;
if (_Station == null)
{
Log.Error("Failed to spawn relay station!!");
return;
}
if (IsClient) return;
var items = _Station.GetItems(false);
var rand = MLCUtils.GetRandomFromString(_LevelData.Seed);
var memoryComps = new List<MemoryComponent>();
foreach (var item in items)
{
if (item.HasTag("wp_input")) _WpInput = item.GetComponent<MemoryComponent>();
if (item.HasTag("wp_target")) _WpTarget = item.GetComponent<MemoryComponent>();
if (item.HasTag("wp_light")) _WpLight = item.GetComponent<LightComponent>();
if (item.HasTag("wp_hint")) _WpHint = item.GetComponent<Terminal>();
if (item.HasTag("wp_add")) AddOperation(OperationType.Add);
if (item.HasTag("wp_sub")) AddOperation(OperationType.Sub);
if (item.HasTag("wp_mul")) AddOperation(OperationType.Mul);
void AddOperation(OperationType type, int low = 5, int high = 25)
{
var comp = item.GetComponent<MemoryComponent>();
if (comp == null)
{
Log.Error("Failed to find a memory component on tagged item");
return;
}
int val = rand.Next(low, high);
comp.Value = val.ToString();
memoryComps.Add(comp);
_Operations.Add(new SignalOperation(type, val));
}
}
if (_Operations.Count == 0)
{
Log.Error("Failed to collect any operations!");
return;
}
// Build random sequence
_Operations.Shuffle(rand);
_Sequence[0] = _Operations[0];
_Sequence[1] = _Operations[1];
_Sequence[2] = _Operations[2];
_Sequence[3] = _Operations[3];
int[] steps0 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
int[] steps1 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
int[] steps2 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
_WpHint.ShowMessage = TextManager.GetWithVariables("relayrepair.terminal",
("[version]", $"${Main.Version}"),
("[station]", $"{rand.Next(100, 999)}"),
("[input0]", $"{steps0[0].ToString().PadLeft(3, '0')}"),
("[input1]", $"{steps1[0].ToString().PadLeft(3, '0')}"),
("[input2]", $"{steps2[0].ToString().PadLeft(3, '0')}"),
("[step10]", $"{steps0[1].ToString().PadLeft(3, '0')}"),
("[step20]", $"{steps0[2].ToString().PadLeft(3, '0')}"),
("[step30]", $"{steps0[3].ToString().PadLeft(3, '0')}"),
("[step11]", $"{steps1[1].ToString().PadLeft(3, '0')}"),
("[step21]", $"{steps1[2].ToString().PadLeft(3, '0')}"),
("[step31]", $"{steps1[3].ToString().PadLeft(3, '0')}"),
("[step12]", $"{steps2[1].ToString().PadLeft(3, '0')}"),
("[step22]", $"{steps2[2].ToString().PadLeft(3, '0')}"),
("[step32]", $"{steps2[3].ToString().PadLeft(3, '0')}"),
("[output0]", $"{steps0[4]}"),
("[output1]", $"{steps1[4]}"),
("[output2]", $"{steps2[4]}")
).Value;
Log.Debug("Sub created");
#if SERVER
// Have the server send these changes to the client
_WpHint.SyncHistory();
foreach (var comp in memoryComps)
{
comp.Item.CreateServerEvent(comp);
}
#endif
}
private int GetStepForInput(int input, int stepCount)
{
if (stepCount > _Sequence.Length)
{
Log.Error($"Requested sequence step ({stepCount}) is bigger then the sequence length ({_Sequence.Length})");
return 0;
}
for (int i = 0; i < stepCount; i++)
{
var operation = _Sequence[i];
input = operation.Run(input);
}
return input;
}
private int[] GetStepsForInput(int input)
{
int[] output = new int[_Sequence.Length + 1];
output[0] = input;
for (int i = 0; i < _Sequence.Length; i++)
{
input = _Sequence[i].Run(input);
output[i + 1] = input;
}
return output;
}
double successTimer = 0;
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) return;
if (_Station == null) return;
if (State == 0)
{
if (CrewInSub())
{
State = 1;
}
}
// Crew has entered the relay and is fixing it
if (State >= 1)
{
// We have the correct value
if (_WpLight.IsOn)
{
successTimer = 2f;
} else if (successTimer > 0)
{
successTimer -= deltaTime;
}
State = successTimer > 0 ? 2 : 1;
}
// Return if timer is counting
if (_Timer > 0)
{
_Timer -= deltaTime;
return;
}
var input = Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced);
_WpInput.Value = input.ToString();
_WpTarget.Value = GetStepForInput(input, 4).ToString();
#if SERVER
// Have the server send the info to the client
_WpInput.Item.CreateServerEvent(_WpInput);
_WpTarget.Item.CreateServerEvent(_WpTarget);
#endif
_Timer = INTERVAL;
bool CrewInSub()
{
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
if (crewMember.Submarine == _Station)
{
return true;
}
}
return false;
}
}
public override void EndMissionSpecific(bool completed)
{
if (completed && level.LevelData != null)
{
level.LevelData.MLC().RelayStationStatus = RelayStationStatus.Active;
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
}
}
@@ -0,0 +1,241 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.Level;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressEscortMission : DistressMission
{
private readonly XElement localCharacterConfig;
private readonly LocalizedString sonarLabel;
private readonly PositionType spawnPositionType;
private readonly bool hostile;
private readonly MissionNPCCollection missionNPCs;
private int calculatedReward;
private Submarine missionSub;
private Vector2 spawnPosition;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State != 1)
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, spawnPosition);
}
}
public DistressEscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
missionSub = sub;
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
sonarLabel = TextManager.Get("missionname.distressmission");
spawnPositionType = localCharacterConfig.GetAttributeEnum("spawntype", PositionType.Cave);
hostile = localCharacterConfig.GetAttributeBool("hostile", false);
missionNPCs = new(this, localCharacterConfig);
CalculateReward();
}
private void CalculateReward()
{
if (missionSub == null)
{
calculatedReward = Prefab.Reward;
return;
}
// Disabled for now, because they make balancing the missions a pain.
int multiplier = 1;//CalculateScalingEscortedCharacterCount();
calculatedReward = Prefab.Reward * multiplier;
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override float GetBaseReward(Submarine sub)
{
if (sub != missionSub)
{
missionSub = sub;
CalculateReward();
}
return calculatedReward;
}
private void InitEscort()
{
missionNPCs.Clear();
if (!Loaded.TryGetInterestingPosition(false, spawnPositionType, 0, out InterestingPosition _spawnPos))
{
Log.Error($"Failed to find a spawn position of type {spawnPositionType}! Falling back to any position.");
bool failed = Loaded.TryGetInterestingPosition(false, PositionType.MainPath | PositionType.SidePath | PositionType.Cave | PositionType.Wreck, 0, out _spawnPos);
if (failed)
{
Log.Error("Could not find ANY position to spawn distress humans at!");
spawnPosition = Vector2.Zero;
}
}
spawnPosition = _spawnPos.Position.ToVector2();
CharacterTeamType team = hostile ? CharacterTeamType.None : CharacterTeamType.FriendlyNPC;
missionNPCs.CreateHumansAtPosition(team, spawnPosition, OnCharacterCreated);
void OnCharacterCreated(Character character, XElement characterMissionConfig)
{
character.MLC().NPCElement = characterMissionConfig;
if (character.AIController is not HumanAIController humanAI) return;
// Only turn on the mental state for non-hostile divers
// so that sometimes you'll get insane divers that attack you
if (!hostile) humanAI.InitMentalStateManager();
// Force the AI to wait in the location so if they get spooked by damage they
// don't try to swim cross map to your sub to find saftey
var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
humanAI.SetForcedOrder(waitOrder);
int minMoney = characterMissionConfig.GetAttributeInt("minmoney", 0);
int maxMoney = characterMissionConfig.GetAttributeInt("maxmoney", 0);
if (maxMoney > 0)
{
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
character.Wallet.Give(money);
Log.InternalDebug($"Gave {money} to {character.Name}");
}
if (characterMissionConfig.GetAttributeBool("allowordering", false))
{
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(character);
#endif
}
}
}
private void InitCharacters()
{
spawnPosition = missionNPCs[0].WorldPosition;
}
public override void StartMissionSpecific(Level level)
{
if (missionNPCs.characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({missionNPCs.characters.Count})");
#else
DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
missionNPCs.characters.Clear();
#endif
}
if (localCharacterConfig == null)
{
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
return;
}
// to ensure single missions run without issues, default to mainsub
if (missionSub == null)
{
missionSub = Submarine.MainSub;
CalculateReward();
}
if (!IsClient)
{
InitEscort();
InitCharacters();
}
}
const float MINDIST = 2000f;
bool triggered = false;
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) return;
// Exit if we're client or if we're already active or if all of the characters are dead
if (missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c)))
{
State = 1;
}
if (triggered || missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c))) return;
foreach (var aiCharacter in missionNPCs.characters.Where(c => MissionNPCCollection.IsAlive(c)))
{
if (!triggered && ShouldActivate())
{
Activate();
return;
}
bool ShouldActivate()
{
bool shouldActivate = GameSession.GetSessionCrewCharacters(CharacterType.Player).Any(c => MissionNPCCollection.Close(c, aiCharacter, MINDIST));
return shouldActivate;
}
}
}
/// <summary>
/// Removes the forced wait order from the AI
/// and forces them into combat with the closest player
/// if they're hostile
/// </summary>
private void Activate()
{
triggered = true;
Log.Debug("Activated NPC");
foreach (var aiCharacter in missionNPCs.characters)
{
if (aiCharacter.AIController is HumanAIController humanAI)
{
humanAI.ClearForcedOrder();
if (hostile)
{
Character target = MissionNPCCollection.GetClosest(aiCharacter);
if (target != null) humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Offensive, target);
}
}
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
if (hostile)
{
return triggered;
}
return missionNPCs.characters.All(c => MissionNPCCollection.Survived(c));
}
return false;
}
public override void EndMissionSpecific(bool completed)
{
if (!IsClient)
{
missionNPCs.End(completed);
}
missionNPCs.Clear();
base.EndMissionSpecific(completed);
}
}
}
@@ -0,0 +1,526 @@
using Barotrauma;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using HarmonyLib;
using System;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System.Collections.Generic;
using Steamworks.Data;
using Barotrauma.Networking;
using static MoreLevelContent.Shared.Generation.MissionGenerationDirector;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressGhostshipMission : DistressMission
{
private readonly LocalizedString defaultSonarLabel;
private readonly XElement localCharacterConfig;
private readonly XElement submarineConfig;
private readonly XElement decalConfig;
private readonly XElement damageDevices;
private readonly XElement removeItems;
private readonly XElement tagDevices;
private readonly MissionNPCCollection missionNPCs;
private readonly bool AllowStealing;
private readonly TravelTarget travelTarget;
private readonly bool reactorActive;
private readonly Level.PositionType spawnPosition;
private Submarine ghostship;
private LevelData levelData;
private TrackingSonarMarker trackingSonarMarker;
private StructureDamageTracker damageTracker;
private int salvagedReward = 0;
enum TravelTarget
{
Start,
Maintain,
End
}
public DistressGhostshipMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
// Config
submarineConfig = prefab.ConfigElement.GetChildElement("submarines");
localCharacterConfig = prefab.ConfigElement.GetChildElement("characters");
removeItems = prefab.ConfigElement.GetChildElement("removeitems");
decalConfig = prefab.ConfigElement.GetChildElement("decals");
damageDevices = prefab.ConfigElement.GetChildElement("damageDevices");
tagDevices = prefab.ConfigElement.GetChildElement("tagDevices");
// Top level attributes
travelTarget = submarineConfig.GetAttributeEnum("TravelTarget", TravelTarget.Maintain);
spawnPosition = submarineConfig.GetAttributeEnum("SpawnPosition", Level.PositionType.MainPath);
reactorActive = submarineConfig.GetAttributeBool("ReactorActive", true);
AllowStealing = submarineConfig.GetAttributeBool("AllowStealing", true);
// General
defaultSonarLabel = TextManager.Get("missionname.distressmission");
missionNPCs = new(this, localCharacterConfig);
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
public override int Reward
{
get
{
if (_SubWasSalvaged) return salvagedReward;
return 0;
}
}
private bool _SubWasSalvaged = false;
private bool SubSalvaged => ghostship.AtEndExit || ghostship.AtStartExit;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (ghostship == null) yield break;
yield return trackingSonarMarker.CurrentPosition;
}
}
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
List<(float, ContentPath)> submarines = new List<(float, ContentPath)>();
foreach (var sub in submarineConfig.GetChildElements("sub"))
{
ContentPath path = sub.GetAttributeContentPath("path", Prefab.ContentPackage);
int commenness = sub.GetAttributeInt("commonness", 0);
submarines.Add((commenness, path));
}
ContentPath subPath = ToolBox.SelectWeightedRandom(submarines, s => s.Item1, Rand.RandSync.ServerAndClient).Item2;
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
{
File = file,
Callback = OnSubCreated,
SpawnPosition = SubSpawnPosition.Path,
AutoFill = true,
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
AllowStealing = AllowStealing,
SkipItemChance = 0.75f
});
}
void OnSubCreated(Submarine submarine)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
ghostship = submarine;
ghostship.FlipX();
submarine.ShowSonarMarker = false;
submarine.TeamID = CharacterTeamType.FriendlyNPC;
ghostship.Info.Type = (SubmarineType)7;
submarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
SubPlacementUtils.PositionSubmarine(submarine, Level.PositionType.MainPath);
SubPlacementUtils.SetCrushDepth(submarine);
salvagedReward = (int)Math.Round(ghostship.Info.Price * 0.85f);
double minFlood = submarineConfig.GetAttributeDouble("minfloodpercentage", 0);
double maxFlood = submarineConfig.GetAttributeDouble("maxfloodpercentage", 0);
string[] floodHulls = submarineConfig.GetAttributeStringArray("floodtargets", new string[] { }, convertToLowerInvariant: true);
if (floodHulls.Length > 0 && minFlood >= 0 && maxFlood > 0)
{
List<Hull> validHulls = ghostship.GetHulls(false).Where(h => floodHulls.Contains(h.RoomName.ToLowerInvariant())).ToList();
foreach (var target in validHulls)
{
target.WaterVolume = (float)(target.Volume * ((rand.NextDouble() * (maxFlood - minFlood)) + minFlood));
Log.Debug($"Flooded hull {target.RoomName}");
}
} else
{
Log.Debug("No hulls to flood");
}
// tag all sub waypoints
submarine.TagSubmarineWaypoints("distress_ghostship");
if (tagDevices != null)
{
foreach (var item in tagDevices.Elements())
{
Identifier targetItem = item.GetAttributeIdentifier("identifier", null);
Identifier tag = item.GetAttributeIdentifier("tag", null);
var items = submarine.GetItems(false).Where(i => i.Prefab.Identifier == targetItem);
items.ForEach((i) =>
{
i.AddTag(tag);
Log.Debug(i.Tags);
});
}
}
// Init tracking sonar marker
trackingSonarMarker = new TrackingSonarMarker(30, submarine, Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel);
}
private void SpawnCharacters()
{
missionNPCs.CreateHumansInSubmarine(ghostship, onCharacterCreated: (character, config) =>
{
if (character.AIController is not HumanAIController humanAI) return;
bool alive = config.GetAttributeBool("alive", true);
if (!alive)
{
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
}
else
{
humanAI.InitMentalStateManager();
}
int minMoney = config.GetAttributeInt("minmoney", 0);
int maxMoney = config.GetAttributeInt("maxmoney", 0);
if (maxMoney > 0)
{
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
character.Wallet.Give(money);
Log.InternalDebug($"Gave {money} to {character.Name}");
}
foreach (var affliction in config.GetChildElements("affliction"))
{
string identifier = affliction.GetAttributeString("identifier", null);
LimbType limb = affliction.GetAttributeEnum("limb", LimbType.None);
float strength = affliction.GetAttributeFloat("strength", 1);
bool targetRandomLimb = affliction.GetAttributeBool("randomLimb", false);
bool randomStrength = affliction.GetAttributeBool("randomStrength", false);
int count = affliction.GetAttributeInt("count", 1);
if (AfflictionHelper.TryGetAffliction(identifier, out AfflictionPrefab prefab))
{
for (int i = 0; i < count; i++)
{
Limb targetLimb = character.AnimController.MainLimb;
if (limb != LimbType.None) targetLimb = character.AnimController.GetLimb(limb);
if (targetRandomLimb) targetLimb = character.AnimController.Limbs.GetRandomUnsynced();
if (randomStrength) strength = Rand.Range(10f, 70f, Rand.RandSync.Unsynced);
character.CharacterHealth.ApplyAffliction(targetLimb, new Affliction(prefab, strength));
}
}
else
{
Log.Error($"Unable to get affliction with identifier {identifier}");
}
}
character.CharacterHealth.ForceUpdateVisuals();
});
}
private void SpawnDecals()
{
if (decalConfig == null) return;
foreach (XElement item in decalConfig.GetChildElements("item"))
{
string prefab = item.GetAttributeString("prefab", "");
string preferedHullName = item.GetAttributeString("preferedhull", "");
int count = item.GetAttributeInt("count", 0);
if (count == 0 || string.IsNullOrWhiteSpace(prefab)) continue;
PlaceDecals(prefab, preferedHullName, count);
}
}
private void PlaceDecals(string decalName, string preferedHull, int count)
{
try
{
bool hasPreferedHull = !string.IsNullOrWhiteSpace(preferedHull);
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
List<Hull> filteredHulls = ghostship.GetHulls(false).Where(h => !h.RoomName.Contains("ballast") && !h.RoomName.Contains("airlock") && !h.IsWetRoom).ToList();
var preferedHulls = filteredHulls.Where(h => h.RoomName.ToLowerInvariant() == preferedHull.ToLowerInvariant());
for (int i = 0; i < count; i++)
{
Hull hull = filteredHulls.ToList().GetRandom(rand);
if (hasPreferedHull && preferedHull.Any())
{
hull = preferedHulls.ToList().GetRandom(rand);
Log.Debug($"Set prefered hull, roomname {hull.RoomName}");
}
Vector2 pos = new Vector2(hull.WorldPosition.X + rand.Next(-hull.RectWidth / 2, hull.RectWidth / 2), hull.WorldPosition.Y + rand.Next(-hull.RectHeight / 2, hull.RectHeight / 2));
Decal decal = hull.AddDecal(decalName, pos, 1.0f, false);
}
} catch(Exception e)
{
Log.Error(e.ToString());
}
}
private void DamageDevices()
{
if (damageDevices == null) return;
foreach (XElement device in damageDevices.GetChildElements("item"))
{
Identifier tag = device.GetAttributeIdentifier("tag", "");
int condition = device.GetAttributeInt("condition", 0);
int amount = device.GetAttributeInt("amount", 1);
bool all = device.GetAttributeBool("all", false);
if (tag.IsEmpty) continue;
DamageDevice(tag, condition, amount, all);
}
}
private void DamageDevice(Identifier tag, int condition, int amount, bool all)
{
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var validItems = ghostship.GetItems(false).Where(i => i.IsPlayerTeamInteractable && i.HasTag(tag)).ToList();
if (!validItems.Any()) return;
if (all)
{
validItems.ForEach(i => Damage(i));
return;
}
while(amount > 0 && validItems.Any())
{
amount--;
Item target = validItems.GetRandom(rand);
_ = validItems.Remove(target);
Damage(target);
}
void Damage(Item item)
{
if (item.GetComponent<Repairable>() is Repairable repairable)
{
item.Condition = condition;
}
Log.Debug("Damaged Device");
}
}
public override void StartMissionSpecific(Level level)
{
if (!IsClient)
{
StartServer();
InitShip();
SpawnCharacters();
}
}
const float MAX_REP_LOSS = 20f;
float _LostRep = 0;
void StartServer()
{
// Reputation stuff
damageTracker = new StructureDamageTracker(ghostship, 2.0f, 1f, 2f);
damageTracker.DamageAfterThreshold += DamageTracker_DamageAfterThreshold;
damageTracker.ThresholdCrossed += DamageTracker_ThresholdCrossed;
_LostRep = 0;
}
private void DamageTracker_ThresholdCrossed()
{
#if SERVER
GameMain.Server?.SendChatMessage(TextManager.GetServerMessage("distress.ghostship.damagenotification")?.Value, ChatMessageType.Default);
#endif
#if CLIENT
if (GameMain.IsSingleplayer)
{
GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(
TextManager.Get("mlc.info1")?.Value, TextManager.Get("distress.ghostship.damagenotification")?.Value,
ChatMessageType.MessageBox, null);
}
#endif
}
private void DamageTracker_DamageAfterThreshold(float amount)
{
if (_LostRep >= MAX_REP_LOSS) return;
var reputationLoss = amount * Reputation.ReputationLossPerWallDamage;
reputationLoss = Math.Min(reputationLoss, 10); // clamp rep loss to a value 0-10
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
_LostRep += reputationLoss;
}
void InitShip()
{
ghostship.NeutralizeBallast();
var ghostshipItems = ghostship.GetItems(alsoFromConnectedSubs: false);
if (ghostshipItems.Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
Item reactorItem = reactor.Item;
ItemContainer container = reactorItem.GetComponent<ItemContainer>();
if (reactorActive)
{
reactor.PowerUpImmediately();
reactor.FuelConsumptionRate = 0;
}
// ItemPrefab rod = ItemPrefab.Find(null, "fuelrod".ToIdentifier());
// Make sure the reactor doesn't explode or irradiate the bots
if (CompatabilityHelper.Instance.ReactorModInstalled) CompatabilityHelper.SetupHazReactor(reactor);
Repairable repairable = reactor.Item.GetComponent<Repairable>();
if (repairable != null)
{
repairable.DeteriorationSpeed = 0.0f;
}
}
// make sure shit doesn't break by itself
ghostshipItems.FindAll(i => i.HasTag("junctionbox") || i.HasTag("oxygengenerator")).ForEach(i =>
{
if (i.GetComponent<Repairable>() is Repairable repairable)
{
repairable.DeteriorationSpeed = 0;
}
});
Item sonarItem = Item.ItemList.Find(it => it.Submarine == ghostship && it.GetComponent<Sonar>() != null);
if (sonarItem == null)
{
DebugConsole.ThrowError($"No sonar found in the beacon station \"{ghostship.Info.Name}\"!");
return;
}
var steering = sonarItem.GetComponent<Steering>();
steering.AutoPilot = true;
switch (travelTarget)
{
case TravelTarget.Start:
steering.SetDestinationLevelStart();
break;
case TravelTarget.Maintain:
steering.MaintainPos = true;
steering.PosToMaintain = ghostship.WorldPosition;
break;
case TravelTarget.End:
steering.SetDestinationLevelEnd();
break;
}
}
readonly float detectDist = Sonar.DefaultSonarRange;
private bool finalSubSetup = false;
const float FINAL_SETUP_DELAY = 5;
float setupDelay = FINAL_SETUP_DELAY;
partial void UpdateProjSpecific(float deltaTime);
public override void UpdateMissionSpecific(float deltaTime)
{
if (ghostship == null) return;
if (!finalSubSetup && !IsClient && setupDelay <= 0)
{
SpawnDecals();
DamageDevices();
if (removeItems != null)
{
foreach (XElement item in removeItems.Elements())
{
Identifier tagToRemove = item.GetAttributeIdentifier("tag", null);
if (tagToRemove != null)
{
foreach (var itemToRemove in ghostship.GetItems(false).FindAll(i => i.HasTag(tagToRemove)))
{
Entity.Spawner.AddItemToRemoveQueue(itemToRemove);
}
}
}
}
Log.InternalDebug("Preformed final sub setup");
finalSubSetup = true;
}
if (setupDelay > 0)
{
setupDelay -= deltaTime;
}
trackingSonarMarker.Update(deltaTime);
UpdateProjSpecific(deltaTime);
bool crewMemberInSub = CrewInSub();
switch (State)
{
case 0:
float dist = Vector2.DistanceSquared(ghostship.WorldPosition, Submarine.MainSub.WorldPosition);
if (dist < detectDist * detectDist || crewMemberInSub)
{
State = 1;
Log.InternalDebug("State -> 1");
}
break;
case 1:
if (crewMemberInSub)
{
State = 2;
Log.InternalDebug("State -> 2");
}
break;
default:
break;
}
if (SubSalvaged) _SubWasSalvaged = true;
if (IsClient) return;
damageTracker.Update();
bool CrewInSub()
{
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
if (crewMember.Submarine == ghostship)
{
return true;
}
}
return false;
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
public override void EndMissionSpecific(bool completed)
{
base.EndMissionSpecific(completed);
missionNPCs.Clear();
}
}
}
@@ -0,0 +1,27 @@
using Barotrauma;
using MoreLevelContent.Shared.Data;
using System;
using System.Linq;
using System.Reflection;
namespace MoreLevelContent.Missions
{
// Shared
abstract partial class DistressMission : Mission
{
protected bool DisplayReward;
public DistressMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) => DisplayReward = prefab.ConfigElement.GetAttributeBool("displayreward", false);
public override void EndMissionSpecific(bool completed)
{
failed = !completed;
if (completed || Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit)
{
if (level?.LevelData != null && Prefab.Tags.Contains("distress"))
{
level.LevelData.MLC().HasDistress = false;
}
}
}
}
}
@@ -0,0 +1,465 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static HarmonyLib.Code;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressSubmarineMission : DistressMission
{
private class MonsterSet
{
public readonly HashSet<(CharacterPrefab character, Point amountRange)> MonsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>();
public float Commonness;
public MonsterSet(XElement element) => Commonness = element.GetAttributeFloat("commonness", 100.0f);
}
private readonly XElement localCharacterConfig;
private readonly List<MonsterSet> monsterSets = new List<MonsterSet>();
private readonly XElement submarineTypeConfig;
private readonly LocalizedString sonarLabel;
private readonly LocalizedString successCrew;
private readonly LocalizedString successSub;
private readonly LocalizedString revealedFailure;
private readonly MissionNPCCollection missionNPCs;
private readonly Dictionary<Character, int> rewardLookup = new();
private Submarine lostSubmarine;
private LevelData levelData;
private bool swarmSpawned;
private bool outsideOfSonarRange;
private bool playerSubClose;
private TrackingSonarMarker trackingSonarMarker;
private bool SubSalvaged => lostSubmarine.AtEndExit || lostSubmarine.AtStartExit;
private bool CrewResuced => missionNPCs.AnyHumanSurvived;
public DistressSubmarineMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
// Setup submarine
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
submarineTypeConfig = prefab.ConfigElement.GetChildElement("Submarine");
// Setup text
sonarLabel = TextManager.Get("missionname.distressmission");
successCrew = TextManager.Get(prefab.ConfigElement.GetAttributeString("crewsurviveidentifier", "distress.submarinesuccess.crew"));
successSub = TextManager.Get(prefab.ConfigElement.GetAttributeString("subsalvagedidentifer", "distress.submarinesuccess.sub"));
revealedFailure = TextManager.Get(prefab.ConfigElement.GetAttributeString("revealedFailureidentifer", "distress.submarinefail"));
// Setup monsters, copied from beacon station code
swarmSpawned = false;
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
if (!monsterSets.Any())
{
monsterSets.Add(new MonsterSet(monsterElement));
}
LoadMonsters(monsterElement, monsterSets[0]);
}
foreach (var monsterSetElement in prefab.ConfigElement.GetChildElements("monsters"))
{
monsterSets.Add(new MonsterSet(monsterSetElement));
foreach (var monsterElement in monsterSetElement.GetChildElements("monster"))
{
LoadMonsters(monsterElement, monsterSets.Last());
}
}
missionNPCs = new(this, localCharacterConfig);
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
// Allow override the default sonar label with a sonarlabel="" attribute
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
// Display a different failure message if the shuttle was located
public override LocalizedString FailureMessage => state > 0 ? revealedFailure : base.FailureMessage;
// Display a different success message if the crew is dead or alive at the end of the level
public override LocalizedString SuccessMessage => state < 2 ? FormatReward(successCrew) : FormatReward(successSub);
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (lostSubmarine == null) yield break;
if (outsideOfSonarRange)
{
if (trackingSonarMarker == null) yield break;
yield return trackingSonarMarker.CurrentPosition;
}
}
}
#region Reward
LocalizedString FormatReward(LocalizedString input)
{
string msg = input.Value;
msg = msg.Replace("[reward]", GetReward(null).ToString());
return msg;
}
public override float GetBaseReward(Submarine sub) => Completed ? GetRewardCompleted() : GetRewardInLevel();
int survivingCrewPayout = 0;
void CalculateSurvivingPayout(out int payout)
{
payout = 0;
foreach (var character in missionNPCs.characters)
{
if (MissionNPCCollection.Survived(character))
{
payout += rewardLookup[character];
}
}
}
int GetRewardCompleted()
{
int reward = survivingCrewPayout;
if (SubSalvaged)
{
reward += Prefab.Reward;
Log.Debug("Sub salvaged");
}
Log.Debug($"Get reward completed: {reward}");
return reward;
}
int GetRewardInLevel()
{
int reward = Prefab.Reward;
if (missionNPCs?.characters?.Count > 0)
{
foreach (var character in missionNPCs.characters)
{
// Add payout for each living character
if (MissionNPCCollection.IsAlive(character))
{
reward += rewardLookup[character];
Log.Debug($"Added {rewardLookup[character]} to reward");
}
else Log.Debug("Character is dead");
}
}
else Log.Debug("No Characters");
Log.Debug($"Reward: {reward}");
return reward;
}
#endregion
private void LoadMonsters(XElement monsterElement, MonsterSet set)
{
Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = monsterElement.GetAttributeInt("amount", 1);
}
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
_ = set.MonsterPrefabs.Add((characterPrefab, new Point(min, max)));
}
else
{
DebugConsole.ThrowError($"Error in distress submarine mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
ContentPath subPath = submarineTypeConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
{
File = file,
Callback = OnSubCreated,
AllowStealing = true,
AutoFill = true,
IgnoreCrushDpeth = true,
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
SpawnPosition = MissionGenerationDirector.SubSpawnPosition.Path,
SkipItemChance = 0.5f
});
}
void OnSubCreated(Submarine submarine)
{
if (submarine == null)
{
State = -1;
DebugConsole.ThrowError("Distress submarine failed to spawn! Mission will now fail.");
return;
}
lostSubmarine = submarine;
lostSubmarine.Info.Type = SubmarineType.Player;
lostSubmarine.TeamID = CharacterTeamType.FriendlyNPC;
lostSubmarine.ShowSonarMarker = false;
lostSubmarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
SubPlacementUtils.PositionSubmarine(lostSubmarine, Level.PositionType.SidePath | Level.PositionType.MainPath);
//make the shuttle resist at least it's spawn position + 1000m
SubPlacementUtils.SetCrushDepth(lostSubmarine);
// tag all sub waypoints
lostSubmarine.TagSubmarineWaypoints("distress_shuttle");
// Init sonar tracking
trackingSonarMarker = new TrackingSonarMarker(30, lostSubmarine, Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel);
// TriggerEvents(0);
}
public override void StartMissionSpecific(Level level)
{
if (lostSubmarine == null) return;
if (!IsClient) StartServer();
}
void StartServer()
{
SinkSub();
lostSubmarine.EnableMaintainPosition();
Item sonarItem = Item.ItemList.Find(it => it.Submarine == lostSubmarine && it.GetComponent<Sonar>() != null);
if (sonarItem != null)
{
// Always allow the lost sub sonar to run so it attracts monsters :)
Powered sonarPower = sonarItem.GetComponent<Powered>();
sonarPower.MinVoltage = 0;
var sonar = sonarItem.GetComponent<Sonar>();
var steering = sonarItem.GetComponent<Steering>();
sonar.CurrentMode = Sonar.Mode.Active;
// Notify clients of the sonar's state
#if SERVER
sonar.Item.CreateServerEvent(sonar);
#endif
}
bool givenCharge = false;
// Drain all of the batteries on the shuttle
foreach (var item in lostSubmarine.GetItems(alsoFromConnectedSubs: false).Where(i => i.HasTag("battery") && !i.NonInteractable))
{
if (item.GetComponent<PowerContainer>() is PowerContainer powerContainer)
{
// Allow fast recharging
powerContainer.MaxRechargeSpeed = powerContainer.Capacity;
// Drain batteries, give them a little bit of power
powerContainer.Charge = givenCharge ? 0 : 10;
givenCharge = true;
}
}
// TODO: Drain any rods that are inside a reactor, if the shuttle has one
// Init NPCS
missionNPCs.CreateHumansInSubmarine(lostSubmarine, onCharacterCreated: (character, config) =>
{
int payout = config.GetAttributeInt("payout", 0);
rewardLookup.Add(character, payout);
character.Info.Title = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", payout));
((HumanAIController)character.AIController).InitMentalStateManager();
if (config.GetAttributeBool("isCaptain", false))
{
// Set shuttle captain orders
var fightIntruders = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
var repairBrokenDevices = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
var fixLeaks = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
character.SetOrder(fixLeaks, true, false);
character.SetOrder(fightIntruders, true, false);
character.SetOrder(repairBrokenDevices, true, false);
Log.InternalDebug("Updated captain orders");
}
});
void SinkSub()
{
HashSet<Hull> ballastHulls = new HashSet<Hull>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine != lostSubmarine) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null || item.CurrentHull == null) { continue; }
if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
pump.FlowPercentage = 0.0f;
_ = ballastHulls.Add(item.CurrentHull);
}
foreach (Hull hull in ballastHulls)
{
hull.WaterVolume = hull.Volume;
}
}
}
// state 0 = init
// state 1 = crew alive, escort to end
// state 2 = crew dead, escort sub to end
readonly float spawnDist = Sonar.DefaultSonarRange * 2;
private bool _salvedState = false;
private bool _migrate = false;
public override void UpdateMissionSpecific(float deltaTime)
{
if (State == -1 || lostSubmarine == null) return;
UpdateLastPing(deltaTime);
#if CLIENT
if (SubSalvaged && _salvedState != SubSalvaged)
{
CoroutineManager.StartCoroutine(_showMessageBox(TextManager.Get("missionheader1.distress_shiprescue"), TextManager.Get("distress.lostshuttle.atend")));
_salvedState = SubSalvaged;
}
IEnumerable<CoroutineStatus> _showMessageBox(LocalizedString header, LocalizedString message)
{
while (GUIMessageBox.VisibleBox?.UserData is RoundSummary)
{
yield return new WaitForSeconds(1.0f);
}
CreateMessageBox(header, message);
yield return CoroutineStatus.Success;
}
#endif
if (IsClient) return;
switch (state)
{
// init
case 0:
float dist = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition);
if (dist > spawnDist * spawnDist) return;
if (!swarmSpawned) SpawnSwarm();
if (playerSubClose && swarmSpawned)
{
State = missionNPCs.AnyHumanAlive ? 1 : 2;
}
break;
// crew alive
case 1:
if (!missionNPCs.AnyHumanAlive)
{
State = 2;
}
break;
// crew dead
case 2:
break;
}
}
readonly float sonarClose = Sonar.DefaultSonarRange / 0.8f;
void UpdateLastPing(float deltaTime)
{
if (Submarine.MainSub == null)
{
Log.Warn($"Skipped updating last ping as main sub was null");
return;
}
outsideOfSonarRange = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) > Sonar.DefaultSonarRange * Sonar.DefaultSonarRange;
playerSubClose = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) < sonarClose * sonarClose;
trackingSonarMarker.Update(deltaTime);
}
void SpawnSwarm()
{
swarmSpawned = true;
// Find spawn position for the monsters
if (monsterSets.Count == 0) return;
Vector2 spawnPos = lostSubmarine.WorldPosition;
spawnPos.Y += lostSubmarine.GetDockedBorders().Height * 1.5f;
var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced);
foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs)
{
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
_ = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos, (Character character) =>
{
if (character.AIController is EnemyAIController controller)
{
AITarget target = missionNPCs.characters.GetRandomUnsynced().AiTarget;
if (target != null) controller.SelectTarget(target);
}
});
}, Rand.Range(0f, amount));
}
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
CalculateSurvivingPayout(out survivingCrewPayout);
return SubSalvaged || CrewResuced;
}
public override void EndMissionSpecific(bool completed)
{
if (!IsClient) missionNPCs.End(completed);
missionNPCs.Clear();
base.EndMissionSpecific(completed);
}
}
}
@@ -0,0 +1,12 @@
using Barotrauma;
using System;
namespace MoreLevelContent.Missions
{
// Shared
partial class LostCargoMission
{
}
}
@@ -0,0 +1,266 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Networking;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Missions
{
// Shared
partial class MissionNPCCollection
{
internal readonly List<Character> characters = new();
internal readonly Dictionary<Character, List<Item>> characterItems = new();
internal bool AllHumansAlive => characters.All(c => IsAlive(c));
internal bool AnyHumanAlive => characters.Any(c => IsAlive(c));
internal bool AnyHumanSurvived => characters.Any(c => Survived(c));
internal int AliveHumans => characters.Where(c => IsAlive(c)).Count();
internal delegate void OnCharacterCreated(Character character, XElement missionCharacterConfig);
internal Character this[int index] => characters[index];
internal MissionNPCCollection(Mission mission, XElement characterConfig)
{
this.mission = mission;
this.characterConfig = characterConfig;
}
private readonly Mission mission;
private readonly XElement characterConfig;
public void Clear()
{
characters.Clear();
characterItems.Clear();
}
public void End(bool completed)
{
foreach (Character character in characters)
{
if (character.Inventory == null) { continue; }
foreach (Item item in character.Inventory.AllItemsMod)
{
//item didn't spawn with the characters -> drop it
if (!characterItems.Any(c => c.Value.Contains(item)))
{
item.Drop(character);
}
}
}
// characters that survived will take their items with them, in case players tried to be crafty and steal them
// this needs to run here in case players abort the mission by going back home
foreach (var characterItem in characterItems)
{
if (Survived(characterItem.Key) || !completed)
{
foreach (Item item in characterItem.Value)
{
if (!item.Removed)
{
item.Remove();
}
}
}
}
}
internal void CreateHumansInSubmarine(Submarine submarine, CharacterTeamType team = CharacterTeamType.FriendlyNPC, OnCharacterCreated onCharacterCreated = null)
{
if (characterConfig == null)
{
Log.Warn("No characters");
return;
}
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, submarine);
Rand.RandSync randSync = Rand.RandSync.Unsynced;
List<(HumanPrefab prefab, XElement config)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
foreach (XElement element in characterConfig.Elements())
{
var humanPrefab = GetHumanPrefabFromElement(element);
humanPrefabsToSpawn.Add((humanPrefab, element));
}
foreach (var (prefab, config) in humanPrefabsToSpawn)
{
var humanPrefab = prefab;
XElement characterSpecificConfig = config;
if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
var jobPrefab = humanPrefab.GetJobPrefab(randSync);
var stayPos = explicitStayInHullPos;
if (jobPrefab != null)
{
stayPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, submarine) ?? explicitStayInHullPos;
}
XElement additionalItemsElement = config?.GetChildElement("additionalitems");
ContentXElement additionalItems = new ContentXElement(null, additionalItemsElement);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, team, stayPos, additionalItems: additionalItemsElement != null ? additionalItems.Elements() : null);
spawnedCharacter.EnableDespawn = false; // don't let mission npcs despawn
spawnedCharacter.GiveIdCardTags(stayPos, false);
onCharacterCreated?.Invoke(spawnedCharacter, characterSpecificConfig);
spawnedCharacter.MLC().NPCElement = characterSpecificConfig;
#if CLIENT
if (GameMain.IsSingleplayer)
{
if (characterSpecificConfig.GetAttributeBool("allowordering", false))
{
_ = GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
}
#endif
}
Log.Debug("end");
InitCharacters();
}
internal Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, bool giveTags = true, IEnumerable<ContentXElement> additionalItems = null)
{
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.Unsynced);
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, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
_ = humanPrefab.GiveItems(spawnedCharacter, submarine, null, createNetworkEvents: false);
foreach (var item in spawnedCharacter.Inventory.AllItems)
{
IdCard card = item.GetComponent<IdCard>();
if (card == null) continue;
card.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
}
if (additionalItems != null)
{
foreach (var additionalItem in additionalItems)
{
int amount = additionalItem.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
HumanPrefab.InitializeItem(spawnedCharacter, additionalItem, submarine, humanPrefab, createNetworkEvents: false);
}
}
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
internal void GiveCharacterItem(Character character, ContentXElement itemElement, bool createNetworkEvents = true)
{
HumanPrefab.InitializeItem(character, itemElement, null, character.HumanPrefab, createNetworkEvents: createNetworkEvents);
characterItems[character] = character.Inventory.FindAllItems(recursive: true);
}
internal void CreateHumansAtPosition(CharacterTeamType team, Vector2 position, OnCharacterCreated onCharacterCreated)
{
List<(HumanPrefab, XElement)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
foreach (XElement element in characterConfig?.Elements())
{
var humanPrefab = GetHumanPrefabFromElement(element);
humanPrefabsToSpawn.Add((humanPrefab, element));
}
foreach (var prefabToSpawn in humanPrefabsToSpawn)
{
var humanPrefab = prefabToSpawn.Item1;
XElement characterMissionConfig = prefabToSpawn.Item2;
Character character = CreateHumanAtPosition(humanPrefab, team, position);
character.EnableDespawn = false; // don't let mission npcs despawn
onCharacterCreated.Invoke(character, characterMissionConfig);
character.MLC().NPCElement = characterMissionConfig;
}
InitCharacters();
}
internal Character CreateHumanAtPosition(HumanPrefab humanPrefab, CharacterTeamType team, Vector2 spawnPosition)
{
Character character = CharacterUtils.CreateHuman(humanPrefab, characters, characterItems, team, spawnPosition, false);
return character;
}
private HumanPrefab GetHumanPrefabFromElement(XElement element)
{
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + mission.Prefab.Identifier + "\" - use character identifiers instead of names to configure the characters.");
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;
}
private void InitCharacters()
{
Log.Debug("characterCount");
int i = 0;
foreach (XElement element in characterConfig.Elements())
{
characters[i].IsEscorted = false;
Color col = element.GetAttributeColor("color", Color.LightGreen);
characters[i].UniqueNameColor = col;
Log.Debug($"Set color to {col} for character {characters[i].Name}");
i++;
}
}
internal static bool IsAlive(Character character) => character != null && !character.Removed && !character.IsDead;
internal static bool IsCaptured(Character character) => character.LockHands;
internal static bool Survived(Character character)
{
return IsAlive(character) && character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine));
}
internal static bool Close(Character target, Character ai, float closeDistance)
{
float dist = Vector2.DistanceSquared(ai.WorldPosition, target.WorldPosition);
return dist < closeDistance * closeDistance;
}
internal static Character GetClosest(Character ai)
{
float closest = float.MaxValue;
Character target = null;
foreach (Character player in Character.CharacterList.Where(c => c.IsPlayer))
{
if (player.IsDead) continue; // skip dead players
float dist = Vector2.DistanceSquared(player.WorldPosition, ai.WorldPosition);
if (dist < closest)
{
closest = dist;
target = player;
}
}
return target;
}
}
}
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Missions
{
class MissionTest
{
}
}
@@ -0,0 +1,25 @@
using Barotrauma;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System.Collections.Generic;
namespace MoreLevelContent.Missions
{
// Shared
// partial class PirateOutpostMission : Mission
// {
// public override bool DisplayAsCompleted => throw new NotImplementedException();
//
// public override bool DisplayAsFailed => throw new NotImplementedException();
//
// public override bool DetermineCompleted() => throw new NotImplementedException();
// }
}
@@ -0,0 +1,60 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Linq;
namespace MoreLevelContent.Missions
{
// Shared
internal partial class TriangulationMission : Mission
{
private readonly LocalizedString sonarLabel0;
private readonly LocalizedString sonarLabel1;
private readonly LocalizedString sonarLabel2;
private LevelData levelData;
public TriangulationMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
sonarLabel0 = TextManager.Get("tri-point-1");
sonarLabel1 = TextManager.Get("tri-point-1");
sonarLabel2 = TextManager.Get("tri-point-1");
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
//public override bool DetermineCompleted() => false;
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => false;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels => base.SonarLabels;
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
switch (level.MLC().TriangulationTarget)
{
case TriangulationTarget.None:
break;
case TriangulationTarget.MapFeature:
break;
case TriangulationTarget.PirateBase:
break;
case TriangulationTarget.Treasure:
break;
default:
break;
}
}
}
}
@@ -0,0 +1,107 @@
using Barotrauma;
using Barotrauma.Networking;
using System;
namespace MoreLevelContent.Networking
{
/// <summary>
/// Shared
/// </summary>
public static partial class NetUtil
{
internal static IWriteMessage CreateNetMsg(NetEvent target) => GameMain.LuaCs.Networking.Start(Enum.GetName(typeof(NetEvent), target));
/// <summary>
/// Register a method to run when the specified NetEvent happens
/// </summary>
/// <param name="target"></param>
/// <param name="netEvent"></param>
public static void Register(NetEvent target, LuaCsAction netEvent)
{
if (GameMain.IsSingleplayer) return;
GameMain.LuaCs.Networking.Receive(Enum.GetName(typeof(NetEvent), target), netEvent);
}
}
/// <summary>
/// Events that are sent over the network
/// </summary>
public enum NetEvent
{
/// <summary>
/// Send a config message to the server
/// </summary>
CONFIG_WRITE_SERVER,
/// <summary>
/// Send a config message to the clients
/// </summary>
CONFIG_WRITE_CLIENT,
/// <summary>
/// Request the current config from the server
/// </summary>
CONFIG_REQUEST,
/// <summary>
/// Used to test if the target has the mod installed
/// </summary>
PING_CLIENT,
/// <summary>
/// Used to reply to the server's ping
/// </summary>
PONG_SERVER,
/// <summary>
/// Send the location of a new distress signal
/// </summary>
MAP_SEND_NEWDISTRESS,
/// <summary>
/// Send the location of a new lost cargo mission
/// </summary>
MAP_SEND_NEWCARGO,
/// <summary>
/// Call the create distress method on the server
/// </summary>
COMMAND_CREATEDISTRESS,
/// <summary>
/// Fakes a world step
/// </summary>
COMMAND_STEPWORLD,
/// <summary>
/// Request a map connection equality check from the server
/// </summary>
MAP_CONNECTION_EQUALITYCHECK_REQUEST,
/// <summary>
/// Sends the result to the client
/// </summary>
MAP_CONNECTION_EQUALITYCHECK_SENDCLIENT,
/// <summary>
/// Reveals the specified map feature
/// </summary>
EVENT_REVEALMAPFEATURE,
/// <summary>
/// Updates the status of a pirate base
/// </summary>
PIRATEBASE_STATUS,
/// <summary>
/// Request information on custom data added to the campaign map by mlc
/// </summary>
MAP_REQUEST_STATE,
/// <summary>
/// Information on the current map state, distress beacons, pirate state, etc
/// </summary>
MAP_SEND_STATE
}
}
@@ -0,0 +1,57 @@
using Barotrauma;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.XML;
namespace Barotrauma
{
/// <summary>
/// Changes what map feature the current level has.
/// </summary>
[InjectScriptedEvent]
internal class AlterMapFeatureAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes, description: "The Identifier this levels map feature should change to.")]
public Identifier MapFeatureIdentifier { get; set; }
public AlterMapFeatureAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MapFeatureIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MapFeatureIdentifier has not been configured.",
contentPackage: element.ContentPackage);
}
}
private bool isFinished = false;
public override bool IsFinished(ref string goToLabel)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
// This is going to break people mid-joining
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (MapFeatureModule.TryGetFeature(MapFeatureIdentifier, out var feature))
{
var data = Level.Loaded?.LevelData?.MLC();
if (data != null)
{
data.MapFeatureData.Name = feature.Name;
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(AlterMapFeatureAction)}";
}
}
}
@@ -0,0 +1,91 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.XML;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
/// <summary>
/// Remove the fog of war from a nearby area
/// </summary>
[InjectScriptedEvent]
internal class RevealMapAreaAction : EventAction
{
private bool isFinished = false;
private readonly Random random;
public RevealMapAreaAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
//the action chooses the same mission if
// 1. event seed is the same (based on level seed, changes when events are completed)
// 2. event is the same (two different events shouldn't choose the same mission)
// 3. the MissionAction is the same (two different actions in the same event shouldn't choose the same mission)
// Taken from MissionAction
random = new MTRandom(
parentEvent.RandomSeed +
ToolBox.StringToInt(ParentEvent.Prefab.Identifier.Value) +
ParentEvent.Actions.Count);
}
public override void Update(float deltaTime)
{
isFinished = true;
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
Location loc = MLCUtils.FindUnlockLocation(new MLCUtils.FindLocationInfo()
{
MinDistance = 3,
MustBeFurtherOnMap = true,
MustBeHidden = true
});
if (loc == null)
{
Log.Error("Failed to find a location to unlock");
return;
}
campaign.Map.Discover(loc, false);
// Probably have to do some syncing here, maybe not
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
#if CLIENT
ShowNotification(loc);
#endif
}
}
#if CLIENT
public static void ShowNotification(Location loc)
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) return;
_ = new GUIMessageBox(TextManager.Get("mapupdate.generic.header"), TextManager.GetWithVariable("mapupdate.revealed.location", "[location1]", loc.DisplayName),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, iconStyle: "", relativeSize: new Vector2(0.2f, 0.06f), minSize: new Point(64, 74));
}
#endif
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(RevealMapFeatureAction)}";
}
}
}
@@ -0,0 +1,149 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.XML;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
/// <summary>
/// Finds and reveals a hidden map feature.
/// </summary>
[InjectScriptedEvent]
internal class RevealMapFeatureAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes, description: "The Identifier of the map feature to search for.")]
public Identifier MapFeatureIdentifier { get; set; }
private bool isFinished = false;
public RevealMapFeatureAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MapFeatureIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MapFeatureIdentifier has not been configured.",
contentPackage: element.ContentPackage);
}
}
public override void Update(float deltaTime)
{
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
LocationConnection featureConnection;
try
{
featureConnection = FindConnectionWithMapFeature(MapFeatureIdentifier);
} catch(Exception e)
{
isFinished = true;
Log.Error($"RevealMapFeatureAction crashed! {e.Message}");
return;
}
if (featureConnection != null)
{
MapFeatureModule.TryGetFeature(featureConnection.LevelData.MLC().MapFeatureData.Name, out MapFeature feature);
// Probably have to do some syncing here, maybe not
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
#if CLIENT
ShowNotification(feature, featureConnection);
#else
MapDirector.Instance.NotifyMapFeatureRevealed(featureConnection, featureConnection.LevelData.MLC().MapFeatureData);
featureConnection.LevelData.MLC().MapFeatureData.Revealed = true;
#endif
}
}
isFinished = true;
}
#if CLIENT
public static void ShowNotification(MapFeature feature, LocationConnection featureConnection)
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) return;
_ = new GUIMessageBox(TextManager.GetWithVariable("mapfeature.revealed.header", "[feature]", feature.Display.DisplayName), TextManager.Get("mapfeature.revealed.description"),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, iconStyle: feature.Display.Icon, relativeSize: new Vector2(0.2f, 0.06f), minSize: new Point(64, 74));
featureConnection.LevelData.MLC().MapFeatureData.Revealed = true;
}
#endif
private LocationConnection FindConnectionWithMapFeature(Identifier name)
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) return null;
LocationConnection currentConnection;
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
var start = Level.Loaded.StartLocation;
var end = Level.Loaded.EndLocation;
currentConnection = start.Connections.Where(c => c.OtherLocation(start) == end).FirstOrDefault();
} else
{
currentConnection = campaign.CurrentLocation.Connections.FirstOrDefault();
}
HashSet<LocationConnection> checkedConnections = new HashSet<LocationConnection>();
HashSet<LocationConnection> pendingConnections = new HashSet<LocationConnection>() { currentConnection };
do
{
List<LocationConnection> currentConnections = pendingConnections.ToList();
pendingConnections.Clear();
foreach (var connection in currentConnections)
{
checkedConnections.Add(connection);
var data = connection.LevelData.MLC();
if (data == null)
{
Log.Error("Missing data");
continue;
}
MapFeatureData feature = data.MapFeatureData;
if (feature.Name == name && !feature.Revealed)
{
return connection;
}
else
{
foreach (Location location in connection.Locations)
{
foreach (var item in location.Connections)
{
if (checkedConnections.Contains(item)) { continue; }
pendingConnections.Add(item);
}
}
}
}
} while (pendingConnections.Any());
return null;
}
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(RevealMapFeatureAction)} -> ({(MapFeatureIdentifier)})";
}
}
}
@@ -0,0 +1,133 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Generation.Pirate;
using MoreLevelContent.Shared.XML;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
namespace Barotrauma
{
/// <summary>
/// Changes what map feature the current level has.
/// </summary>
[InjectScriptedEvent]
internal class RevealPirateBaseAction //: BinaryOptionAction
{
/*
public RevealPirateBaseAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
private LocationConnection FindPirateBase()
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) return null;
LocationConnection startSearchConnection;
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
var start = Level.Loaded.StartLocation;
var end = Level.Loaded.EndLocation;
startSearchConnection = start.Connections.Where(c => c.OtherLocation(start) == end).FirstOrDefault();
}
else
{
startSearchConnection = campaign.CurrentLocation.Connections.FirstOrDefault();
}
HashSet<LocationConnection> checkedConnections = new HashSet<LocationConnection>();
HashSet<LocationConnection> pendingConnections = new HashSet<LocationConnection>() { startSearchConnection };
do
{
List<LocationConnection> currentConnections = pendingConnections.ToList();
pendingConnections.Clear();
foreach (var connection in currentConnections)
{
checkedConnections.Add(connection);
var data = connection.LevelData.MLC();
if (data == null)
{
Log.Error("Missing data");
continue;
}
var pirateData = data.PirateData;
// Don't use the current connection, only look for active and not revealed bases
if (startSearchConnection != connection && pirateData.Status == PirateOutpostStatus.Active && !pirateData.Revealed)
{
Log.Debug("Found connection with pirate base");
return connection;
}
else
{
foreach (Location location in connection.Locations)
{
foreach (var item in location.Connections)
{
if (checkedConnections.Contains(item)) { continue; }
pendingConnections.Add(item);
}
}
}
}
} while (pendingConnections.Any());
return null;
}
protected override bool? DetermineSuccess()
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) return false;
try
{
LocationConnection connection = FindPirateBase();
if (connection != null)
{
#if CLIENT
ShowNotification(connection);
#endif
if (!Main.IsClient)
{
var data = connection.LevelData.MLC().PirateData;
data.Revealed = true;
PirateOutpostDirector.UpdateStatus(data, connection);
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
Log.Debug("Updated status of pirate base to revealed");
}
return true;
}
}
catch(Exception e)
{
Log.Error($"RevealMapFeatureAction crashed! {e.Message}");
}
return false;
}
#if CLIENT
public static void ShowNotification(LocationConnection connection)
{
if (GameMain.GameSession.GameMode is not CampaignMode) return;
_ = new GUIMessageBox
(TextManager.Get("mapupdate.generic.header"),
RichString.Rich(TextManager.GetWithVariables("mapupdate.piratebase.revealed",
("[location1]", $"‖color:gui.orange‖{connection.Locations[0].DisplayName}‖end‖"),
("[location2]", $"‖color:gui.orange‖{connection.Locations[1].DisplayName}‖end‖"))),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, iconStyle: "PirateBase", relativeSize: new Vector2(0.2f, 0.06f), minSize: new Point(64, 74));
}
#endif
*/
}
}
@@ -0,0 +1,16 @@
using Barotrauma;
using MoreLevelContent.Shared.XML;
namespace MoreLevelContent
{
[InjectScriptedEvent]
internal class SpawnCreatureNearbyAction : EventAction
{
public SpawnCreatureNearbyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
public override bool IsFinished(ref string goToLabel) => throw new System.NotImplementedException();
public override void Reset() => throw new System.NotImplementedException();
}
}
@@ -0,0 +1,104 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.XML;
using System.Collections.Generic;
using System.Linq;
using static Barotrauma.Level;
namespace Barotrauma
{
[InjectScriptedEvent]
internal class TeleportCharacterAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the target(s) to teleport.")]
public Identifier TargetTag { get; set; }
public TeleportCharacterAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
private bool isFinished;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
// Try to find a ruin to tp to
Submarine wreck = null;
Submarine ruin = null;
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Ruin)
{
ruin = sub;
break;
}
if (sub.Info.Type == SubmarineType.Wreck)
{
wreck = sub;
}
}
try
{
Tp();
} catch { Teleport(Vector2.Zero); }
isFinished = true;
void Tp()
{
if (ruin != null)
{
Teleport(ruin);
return;
}
// If we can't find a ruin, try to find a wreck
if (wreck != null)
{
Teleport(wreck);
return;
}
// If both those fail, try to find an abyss cave
if (Loaded != null && Loaded.TryGetInterestingPosition(false, PositionType.AbyssCave, Sonar.DefaultSonarRange, out InterestingPosition pos, suppressWarning: true))
{
Teleport(pos.Position.ToVector2());
return;
}
// Try to find a cave, main path or side path position
if (Loaded != null && Loaded.TryGetInterestingPosition(false, PositionType.Cave | PositionType.MainPath | PositionType.SidePath, Sonar.DefaultSonarRange, out InterestingPosition pos2, suppressWarning: true))
{
Teleport(pos2.Position.ToVector2());
return;
}
// If all else fails, teleport to a random waypoint or 0,0
Teleport(WayPoint.GetRandom(SpawnType.Path)?.WorldPosition ?? Vector2.Zero);
}
}
void Teleport(Submarine target)
{
var wp = WayPoint.GetRandom(sub: target);
Teleport(wp.WorldPosition);
}
void Teleport(Vector2 worldPos)
{
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (target is Character c)
{
c.TeleportTo(worldPos);
}
}
}
public override bool IsFinished(ref string goToLabel)
{
return isFinished;
}
public override void Reset() { isFinished = false; }
}
}
@@ -0,0 +1,29 @@
using Barotrauma;
using Barotrauma.Extensions;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MoreLevelContent.Shared.Store
{
public class BeaconConstStore : StoreBase<BeaconConstStore>
{
private List<OutpostModuleFile> ConstBeacons = new();
public override void Setup() => HasContent = FindConstBeacons();
internal OutpostModuleFile GetBeaconForLevel()
{
Random rand = MLCUtils.GetLevelRandom();
return ConstBeacons.GetRandom(rand);
}
bool FindConstBeacons()
{
ConstBeacons = GetOutpostModuleFilesWithLocation("mlc_BeaconConstruction");
return ConstBeacons.Count() > 0;
// ConstBeacons.Sort();
}
}
}
@@ -0,0 +1,9 @@
namespace MoreLevelContent.Shared.Store
{
public class LevelDataStore : StoreBase<LevelDataStore>
{
public override void Setup() => throw new System.NotImplementedException();
}
}
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Barotrauma;
using Barotrauma.Extensions;
using MoreLevelContent.Shared.Generation;
using static Barotrauma.Level;
namespace MoreLevelContent.Shared.Store
{
public class PirateStore : StoreBase<PirateStore>
{
private List<PirateOutpostDef> pirateOutposts;
private List<PirateNPCSetDef> pirateSets;
public override void Setup()
{
pirateOutposts = new List<PirateOutpostDef>();
pirateSets = new List<PirateNPCSetDef>();
HasContent = FindAndScoreOutpostFiles() && FindAndScoreNPCs();
}
internal PirateOutpostDef FindOutpostWithPath(string path)
{
return pirateOutposts.Find(p => p.SubInfo.FilePath == path);
}
internal void DumpPirateOutposts()
{
if (pirateOutposts.Count == 0)
{
Log.Warn("No pirate outposts found!");
return;
}
foreach (var item in pirateOutposts)
{
Log.Debug(item.SubInfo.FilePath);
}
}
public PirateNPCSetDef GetNPCSetForDiff(float diff, string seed) => GetElementWithPreferedDifficulty(diff, pirateSets, seed);
internal PirateOutpostDef GetPirateOutpostForDiff(float diff, string seed) => GetElementWithPreferedDifficulty(diff, pirateOutposts, seed);
private bool FindAndScoreOutpostFiles()
{
Log.Debug("Collecting pirate outposts...");
var pirateOutpostSets = MissionPrefab.Prefabs.Where(m => m.Tags.Contains("pirateoutpostset"));
Log.Debug($"outposts: {pirateOutpostSets.Count()}");
foreach (var item in pirateOutpostSets)
{
foreach (var outpost in item.ConfigElement.GetChildElements("PirateOutpost"))
{
var path = outpost.GetAttributeContentPath("path");
var min = outpost.GetAttributeInt("mindiff", 0);
var max = outpost.GetAttributeInt("maxdiff", 100);
var placement = outpost.GetAttributeEnum("placement", PlacementType.Bottom);
SubmarineInfo subInfo = new SubmarineInfo(path.Value);
pirateOutposts.Add(new PirateOutpostDef(subInfo, min, max, placement));
}
}
pirateOutposts = pirateOutposts.OrderBy(o => o.SubInfo.Name).ToList();
foreach (var item in pirateOutposts)
{
Log.Verbose(item.DifficultyRange.ToString());
}
if (pirateOutposts.Count > 0)
{
Log.Debug($"Collected {pirateOutposts.Count} pirate outposts");
return true;
}
else
{
Log.Error("Failed to find any pirate outposts!!!");
return false;
}
}
private bool FindAndScoreNPCs()
{
List<MissionPrefab> pirateMissions = MissionPrefab.Prefabs.Where(m => m.Identifier.StartsWith("mlc_mp")).ToList();
foreach (MissionPrefab prefab in pirateMissions)
{
pirateSets.Add(new PirateNPCSetDef(prefab, prefab.Name.Value));
}
Log.Verbose("Sorting sets by their diff ranges...");
pirateSets.Sort();
if (pirateSets.Count == 0)
{
Log.Error("Failed to find pirates to spawn :(");
return false;
}
Log.Verbose($"Collected {pirateSets.Count} pirate NPC sets to choose from.");
return true;
}
}
}
@@ -0,0 +1,67 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Generation.Pirate;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MoreLevelContent.Shared.Store
{
public abstract class StoreBase<T> : Singleton<T> where T : class
{
public static bool HasContent { get; protected set; }
protected Element GetElementWithPreferedDifficulty<Element>(float preferedDifficulty, List<Element> elements, string seed, float maxDifference = 20f) where Element : DefWithDifficultyRange
{
Log.InternalDebug($"Looking for {typeof(Element).Name} with perfered difficulty of {preferedDifficulty}...");
List<Element> filtered = elements;
try
{
filtered = elements.Where(e => MathF.Abs(e.AverageDifficulty - preferedDifficulty) < maxDifference).ToList();
} catch(Exception e)
{
Log.Error(e.ToString());
}
if (filtered.Count == 0)
{
Log.Warn($"Failed to find element of type '{nameof(Element)}' with prefered difficulty of {preferedDifficulty} with a max differential of {maxDifference}!");
filtered = elements;
}
Log.Verbose($"Filtered sets of '{nameof(Element)}' to choose from {filtered.Count}");
filtered = filtered.OrderBy(e => e.AverageDifficulty).ToList();
var rand = new MTRandom(ToolBox.StringToInt(seed));
Element selectedElement = ToolBox.SelectWeightedRandom(filtered, (elm) =>
{
return elm.AverageDifficulty > preferedDifficulty
? preferedDifficulty / elm.AverageDifficulty
: elm.AverageDifficulty / preferedDifficulty;
}, rand);
return selectedElement;
}
internal List<OutpostModuleFile> GetOutpostModuleFilesWithLocation(string locationType)
{
var outpostModuleFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostModuleFile>())
.OrderBy(f => f.UintIdentifier).ToList();
List<OutpostModuleFile> modulesWithTag = new();
foreach (var outpostModuleFile in outpostModuleFiles)
{
SubmarineInfo subInfo = new SubmarineInfo(outpostModuleFile.Path.Value);
if (subInfo.OutpostModuleInfo != null)
{
if (subInfo.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType))
modulesWithTag.Add(outpostModuleFile);
}
}
return modulesWithTag;
}
}
}
@@ -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");
}
}
}
+136
View File
@@ -0,0 +1,136 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using HarmonyLib;
using MoreLevelContent.Custom.Missions;
using MoreLevelContent.Shared.Content;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
namespace MoreLevelContent.Shared.XML
{
[AttributeUsage(AttributeTargets.Class)]
public class InjectScriptedEvent : Attribute { }
public class InjectionManager : Singleton<InjectionManager>
{
FieldInfo missionPrefab_constructor;
static ImmutableDictionary<string, Type> _CustomScriptedEvents;
private static Assembly _BaroAsm;
public override void Setup()
{
missionPrefab_constructor = AccessTools.Field(typeof(MissionPrefab), "constructor");
var npcConversation_GetCurrentFlags = AccessTools.Method(typeof(NPCConversation), "GetCurrentFlags");
var eventAction_Instantiate = AccessTools.Method(typeof(EventAction), "Instantiate");
_BaroAsm = Assembly.GetAssembly(typeof(EventAction));
_ = Main.Harmony.Patch(npcConversation_GetCurrentFlags, postfix: new HarmonyMethod(AccessTools.Method(typeof(InjectionManager), nameof(AddNPCConcersationFlags))));
_ = Main.Harmony.Patch(eventAction_Instantiate, prefix: new HarmonyMethod(AccessTools.Method(typeof(InjectionManager), nameof(InjectEventActions))));
// Collect scripted events
Dictionary<string, Type> customEventDict = new()
{
{ nameof(AlterMapFeatureAction), typeof(AlterMapFeatureAction) },
{ nameof(RevealMapAreaAction), typeof(RevealMapAreaAction) },
{ nameof(RevealMapFeatureAction), typeof(RevealMapFeatureAction) },
{ nameof(RevealPirateBaseAction), typeof(RevealPirateBaseAction) },
{ nameof(TeleportCharacterAction), typeof(TeleportCharacterAction) }
};
_CustomScriptedEvents = customEventDict.ToImmutableDictionary();
InjectMissions();
}
// This should be a transpiler but I don't want to spend the time doing that and I don't think any other mod is ever going to add custom scripted events
// We'll fix this incompatability when it happens!
private static bool InjectEventActions(ScriptedEvent scriptedEvent, ContentXElement element, ref EventAction __result)
{
Type actionType;
__result = null;
try
{
Identifier typeName = element.Name.ToString().ToIdentifier();
if (typeName == "TutorialSegmentAction")
{
typeName = nameof(EventObjectiveAction).ToIdentifier();
}
else if (typeName == "TutorialHighlightAction")
{
typeName = nameof(HighlightAction).ToIdentifier();
}
if (!_CustomScriptedEvents.TryGetValue(typeName.ToString(), out actionType))
{
actionType = _BaroAsm.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true);
//actionType = _BaroAsm.GetType("Barotrauma." + typeName.ToString(), throwOnError: true, ignoreCase: true);
}
if (actionType == null) { throw new NullReferenceException(); }
}
catch(Exception e)
{
Log.Error(e.Message);
DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".",
contentPackage: element.ContentPackage);
return false;
}
ConstructorInfo constructor = actionType.GetConstructor(new[] { typeof(ScriptedEvent), typeof(ContentXElement) });
try
{
if (constructor == null)
{
throw new Exception($"Error in scripted event \"{scriptedEvent.Prefab.Identifier}\" - could not find a constructor for the EventAction \"{actionType}\".");
}
__result = constructor.Invoke(new object[] { scriptedEvent, element }) as EventAction;
return false;
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString(),
contentPackage: element.ContentPackage);
return false;
}
}
private static void AddNPCConcersationFlags(ref List<Identifier> __result, Character speaker)
{
if (speaker == null) return;
if (speaker.MLC().IsDistressShuttle) __result.Add("DistressShuttle");
if (speaker.MLC().IsDistressDiver) __result.Add("DistressDiver");
if (speaker.TeamID == CharacterTeamType.FriendlyNPC)
{
if (speaker.Submarine == MapFeatureModule.MapFeatureSub)
{
__result.Add(MapFeatureModule.CurrentMapFeature);
}
if (Submarine.MainSub != null && speaker.Submarine == Submarine.MainSub)
{
__result.Add("MainSub");
}
}
}
private void InjectMissions()
{
Log.Debug("Injecting custom missions");
foreach (MissionPrefab prefab in MissionPrefab.Prefabs)
{
Identifier customType = prefab.ConfigElement.GetAttributeIdentifier("customType", Identifier.Empty);
if(!Enum.TryParse(customType.Value, true, out CustomMissionType type)) continue;
missionPrefab_constructor.SetValue(prefab, CustomMissions.MissionDefs[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) }));
Log.Verbose($"Updated consturctor for mission {prefab.Name} to type {type}");
}
}
}
}
@@ -0,0 +1,72 @@
using System;
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared.Utils;
using Barotrauma;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using System.IO;
using System.Xml;
namespace MoreLevelContent.Shared.XML
{
public class XMLManager : Singleton<XMLManager>
{
internal List<CustomMission> CustomMissions = new();
public override void Setup()
{
List<ContentFile> otherFiles = new List<ContentFile>();
foreach (var package in ContentPackageManager.EnabledPackages.All)
{
var a = package.Files.Where(f => f.GetType() == typeof(OtherFile));
otherFiles.AddRange(a);
}
Log.Debug($"Collected {otherFiles.Count} other files to check");
foreach (ContentFile file in otherFiles)
{
// Skip non-xml files
if (Path.GetExtension(file.Path.RawValue) != ".xml") continue;
XDocument doc = null;
try
{
doc = XDocument.Parse(LuaCsFile.Read(file.Path.Value));
} catch(Exception e)
{
Log.Error($"Failed to load file at path {file.Path.Value} due to {e.Message}");
continue;
}
if (doc == null) { continue; }
ContentXElement contentElement = doc.Root.FromPackage(file.ContentPackage);
var tags = contentElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
if (!tags.Contains("MLC")) continue; // skip non-MLC elements
foreach (string tag in tags)
{
switch (tag)
{
case "mission":
CustomMissions.Add(new CustomMission()
{
File = new MissionsFile(file.ContentPackage, file.Path),
ContentXElement = contentElement
});
Log.Debug("Found custom mission");
break;
case "MLC": // Ignore MLC tag
break;
default:
Log.Debug($"Unknown tag: {tag}");
break;
}
}
}
}
}
internal class CustomMission
{
public ContentXElement ContentXElement;
public MissionsFile File;
}
}