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,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);
}
}