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,82 @@
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Barotrauma;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using MoreLevelContent.Shared.Utils;
using System.Reflection.Metadata;
using MoreLevelContent.Shared.Generation;
namespace MoreLevelContent.Shared.AI
{
partial class CaveAI
{
private CoroutineHandle fadeOutRoutine;
partial void FadeOutColors()
{
if (fadeOutRoutine != null)
{
CoroutineManager.StopCoroutines(fadeOutRoutine);
}
fadeOutRoutine = CoroutineManager.StartCoroutine(FadeOutColors(Config.DeadEntityColorFadeOutTime));
}
private IEnumerable<CoroutineStatus> FadeOutColors(float time)
{
float timer = 0;
while (timer < time)
{
timer += CoroutineManager.DeltaTime;
float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer));
foreach (var item in ThalamusItems)
{
if (item.Prefab.BrokenSprites.None())
{
Color c = item.Prefab.SpriteColor;
item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f);
}
}
yield return CoroutineStatus.Running;
}
yield return CoroutineStatus.Success;
}
public void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch sb, Camera cam)
{
float lineThickness = 1f / Screen.Selected.Cam.Zoom;
sb.DrawPoint(new Vector2(Cave.StartPos.X, -Cave.StartPos.Y), Color.Pink, 10 / Screen.Selected.Cam.Zoom);
foreach (var turret in turrets)
{
const float coneRadius = 300.0f;
float radians = turret.GetMaxRotation() - turret.GetMinRotation();
float circleRadius = coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale;
sb.DrawSector(turret.GetDrawPos(), circleRadius, radians, (int)Math.Abs(90 * radians), GUIStyle.Green, offset: turret.GetMinRotation(), thickness: lineThickness);
Dictionary<ActionType, List<StatusEffect>> dic = (Dictionary<ActionType, List<StatusEffect>>)CaveGenerationDirector.item_statusEffectList.GetValue(turret.Item);
if (dic?.TryGetValue(ActionType.OnUse, out List<StatusEffect> effects) ?? false)
{
foreach (var effect in effects)
{
var pos = turret.Item.Position + new Vector2(effect.Offset.X, effect.Offset.Y);
pos = new Vector2(pos.X, -pos.Y);
GUI.DrawRectangle(sb, pos, 100, 100, 0, Color.Aqua, thickness: lineThickness);
foreach (var spawnEffect in effect.SpawnCharacters)
{
var pos2 = turret.Item.Position + new Vector2(spawnEffect.Offset.X, spawnEffect.Offset.Y);
pos2 = new Vector2(pos2.X, -pos2.Y);
GUI.DrawRectangle(sb, pos2, 50, 50, 0, Color.Orange, thickness: lineThickness);
}
}
}
}
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
IsAlive = msg.ReadBoolean();
}
}
}
@@ -0,0 +1,118 @@
using Barotrauma.MoreLevelContent.Client.UI;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using Barotrauma.MoreLevelContent.Shared.Config;
using MoreLevelContent;
using Barotrauma.Networking;
using MoreLevelContent.Networking;
namespace Barotrauma.MoreLevelContent.Config
{
/// <summary>
/// Client
/// </summary>
partial class ConfigManager : Singleton<ConfigManager>
{
public static bool ShouldDisplayPatchNotes = false;
private void SetupClient()
{
CommandUtils.AddCommand(
"mlc_config",
"Toggle the display of the config editor",
ToggleGUI);
// Exit if we're in an editor
if (Screen.Selected.IsEditor) return;
if (GameMain.IsSingleplayer) return; // We don't need to do any of this if we're in singleplayer
NetUtil.Register(NetEvent.CONFIG_WRITE_CLIENT, ClientRead);
if (!GameMain.Client.IsServerOwner) RequestConfig();
else ClientWrite();
}
public void SetConfig(MLCConfig config)
{
this.Config = config;
Log.Debug("[CLIENT] Config Updated");
Log.Verbose(Config.ToString());
if (!GameMain.IsSingleplayer) UpdateConfig();
SaveConfig();
}
private void RequestConfig()
{
IWriteMessage outMsg = NetUtil.CreateNetMsg(NetEvent.CONFIG_REQUEST);
outMsg.WriteString(Main.Version);
GameMain.LuaCs.Networking.Send(outMsg);
Log.Verbose("Requested config from server...");
}
private void ClientWrite()
{
// Always allow the server owner to write
if (!GameMain.Client.HasPermission(ClientPermissions.ManageSettings) && !GameMain.Client.IsServerOwner)
{
Log.Error("No Perms!");
return;
}
IWriteMessage outMsg = NetUtil.CreateNetMsg(NetEvent.CONFIG_WRITE_SERVER);
outMsg.WriteString(Main.Version);
WriteConfig(ref outMsg);
GameMain.LuaCs.Networking.Send(outMsg);
Log.Debug("Sent config packet to server!");
}
private void ClientRead(object[] args)
{
Log.Debug("Got config packet!");
IReadMessage inMsg = (IReadMessage)args[0];
ReadNetConfig(ref inMsg);
}
private void UpdateConfig()
{
if (!GameMain.Client.HasPermission(ClientPermissions.ManageSettings)) return;
ClientWrite();
}
private void DisplayPatchNotes(bool force = false)
{
// REMEMBER TO CHANGE THIS BACK
if (Config.Version != Main.Version || force || Main.IsNightly)
{
ShouldDisplayPatchNotes = true;
}
}
public bool SettingsOpen
{
get => _settingsOpen;
set
{
if (value == _settingsOpen) { return; }
if (value)
{
_settingsMenu = new GUIFrame(new RectTransform(Vector2.One, Screen.Selected.Frame.RectTransform, Anchor.Center), style: null);
_ = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, _settingsMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
var settingsMenuInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), _settingsMenu.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) });
_ = ConfigMenu.Create(settingsMenuInner.RectTransform);
Log.Verbose("Opened Settings");
}
else
{
ConfigMenu.Instance?.Close();
_settingsMenu.Parent.RemoveChild(_settingsMenu);
_settingsMenu = null;
Log.Verbose("Closed Settings");
}
_settingsOpen = value;
}
}
private static bool _settingsOpen;
private static GUIFrame _settingsMenu;
private void ToggleGUI(object[] args) => SettingsOpen = !SettingsOpen;
}
}
@@ -0,0 +1,123 @@
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Networking;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using Barotrauma;
using Barotrauma.Networking;
using MoreLevelContent.Shared.Data;
using System.Reflection.Emit;
using System.Xml.Linq;
using static Barotrauma.Networking.MessageFragment;
namespace MoreLevelContent.Shared.Generation
{
// Client
public partial class MapDirector : Singleton<MapDirector>
{
private FieldInfo _notificationList;
private Type _notification;
private MethodInfo _addMethod;
private ConstructorInfo _notifConstructor;
private FieldInfo _mapAnimField;
private ConstructorInfo _mapAnimConstructor;
private MapSyncState SyncState = MapSyncState.Syncing;
partial void SetupProjSpecific()
{
NetUtil.Register(NetEvent.MAP_CONNECTION_EQUALITYCHECK_SENDCLIENT, ConnectionEqualityCheck);
NetUtil.Register(NetEvent.EVENT_REVEALMAPFEATURE, NotifyRevealMapFeature);
_notificationList = AccessTools.Field(typeof(Map), "mapNotifications");
_notification = typeof(Map).GetNestedType("MapNotification", BindingFlags.NonPublic);
_addMethod = AccessTools.Method(_notificationList.FieldType, "Add");
_notifConstructor = AccessTools.Constructor(_notification, new Type[] { typeof(string), typeof(GUIFont), _notificationList.FieldType, typeof(Location) });
_mapAnimField = AccessTools.Field(typeof(Map), "mapAnimQueue");
var mapAnimType = typeof(Map).GetNestedType("MapAnim", BindingFlags.NonPublic);
_mapAnimConstructor = AccessTools.Constructor(mapAnimType);
var singlePlayerCampaign_Start = typeof(SinglePlayerCampaign).GetMethod(nameof(SinglePlayerCampaign.Start));
_ = Main.Harmony.Patch(singlePlayerCampaign_Start, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnSinglePlayerMapLoad), BindingFlags.Static | BindingFlags.NonPublic)));
NetUtil.Register(NetEvent.MAP_SEND_STATE, ReceiveMapState);
}
private static void OnSinglePlayerMapLoad(SinglePlayerCampaign __instance) => OnMapLoad(__instance.Map);
private void ReceiveMapState(object[] args)
{
Log.Debug("Got map state packet");
IReadMessage inMsg = (IReadMessage)args[0];
MapSyncState mapState = (MapSyncState)inMsg.ReadByte();
if (mapState != MapSyncState.MapSynced)
{
Log.Debug($"Map did not sync, state: {mapState}");
SyncState = mapState;
return;
}
byte activeBeacons = inMsg.ReadByte();
for (int i = 0; i < activeBeacons; i++)
{
int connectionID = inMsg.ReadUInt16();
int stepsLeft = inMsg.ReadByte();
if (!IdConnectionLookup.TryGetValue(connectionID, out var connection))
{
DebugConsole.ThrowError($"More Level Content tried to add an active distress beacon on connection with ID '{connectionID}' but found no connection on the clients id connection lookup! Do we have connections in the lookup? {IdConnectionLookup.Count > 0}s");
continue;
}
connection.LevelData.MLC().HasDistress = true;
connection.LevelData.MLC().DistressStepsLeft = stepsLeft;
}
Log.Debug("Synced map state!");
}
internal partial void RoundEnd(CampaignMode.TransitionType transitionType)
{
if (transitionType == CampaignMode.TransitionType.None)
{
Log.Debug($"Cleaned connection lookup");
// Reset connection lookup
if (_validatedConnectionLookup)
{
_validatedConnectionLookup = false;
IdConnectionLookup.Clear();
ConnectionIdLookup.Clear();
}
}
}
public void AddNewsStory(string message)
{
object list = _notificationList.GetValue(GameMain.GameSession.Map);
var notification = _notifConstructor.Invoke(new object[] {
message,
GUIStyle.SubHeadingFont,
list,
null
});
_ = _addMethod.Invoke(list, new object[] { notification });
_notificationList.SetValue(GameMain.GameSession.Map, list);
}
public void AddMapAnimation() => throw new NotImplementedException();
void NotifyRevealMapFeature(object[] args)
{
IReadMessage inMsg = (IReadMessage)args[0];
Identifier featureName = inMsg.ReadIdentifier();
Int32 conId = inMsg.ReadInt32();
LocationConnection con = IdConnectionLookup[conId];
MapFeatureModule.TryGetFeature(featureName, out MapFeature feature);
RevealMapFeatureAction.ShowNotification(feature, con);
}
}
}
@@ -0,0 +1,33 @@
using Barotrauma.Networking;
using Barotrauma;
using MoreLevelContent.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Shared.Generation
{
// Client
internal partial class OldDistressMapModule
{
protected override void InitProjSpecific()
{
if (GameMain.IsMultiplayer) NetUtil.Register(NetEvent.MAP_SEND_NEWDISTRESS, CreateDistress);
}
internal void CreateDistress(object[] args)
{
IReadMessage inMsg = (IReadMessage)args[0];
int id = (int)inMsg.ReadUInt32();
byte steps = inMsg.ReadByte();
LocationConnection connection = MapDirector.IdConnectionLookup[id];
CreateDistress(connection, steps);
}
}
internal partial class DistressMapModule : TimedEventMapModule
{
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Shared.Generation
{
// Client
internal partial class LostCargoMapModule : TimedEventMapModule
{
}
}
@@ -0,0 +1,26 @@
using Barotrauma;
using Barotrauma.Networking;
using MoreLevelContent.Networking;
namespace MoreLevelContent.Shared.Generation
{
// Client
abstract partial class TimedEventMapModule
{
protected override void InitProjSpecific()
{
if (GameMain.IsMultiplayer) NetUtil.Register(EventCreated, CreateEvent);
}
internal void CreateEvent(object[] args)
{
IReadMessage inMsg = (IReadMessage)args[0];
int id = (int)inMsg.ReadUInt32();
byte steps = inMsg.ReadByte();
LocationConnection connection = MapDirector.IdConnectionLookup[id];
CreateEvent(connection, steps);
}
}
}
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Client.UI;
using Barotrauma.MoreLevelContent.Config;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System.Linq;
using System.Reflection;
namespace MoreLevelContent
{
/// <summary>
/// Client
/// </summary>
partial class Main
{
public void InitClient()
{
MapUI.Instance.Setup();
Hooks.Instance.OnDebugDraw += ClientDebugDraw.Draw;
SonarExtensions.Instance.Setup();
GameMain.LuaCs.Hook.Add("roundStart", OpenPatchNotes);
// Exit if we're in an editor
if (Screen.Selected.IsEditor) return;
MethodInfo info = typeof(GUI).GetMethod("TogglePauseMenu", BindingFlags.Static | BindingFlags.Public);
Patch(info, postfix: new HarmonyMethod(AccessTools.Method(typeof(Main), "AddSettingsButton")));
}
private static void AddSettingsButton()
{
if (!GUI.PauseMenuOpen) return; // don't try to add the button when the pause menu doesn't exist
var target = GUI.PauseMenu.Children.ToList()[1].Children.First();
var button = new GUIButton(new RectTransform(new Vector2(1, 0.1f), target.RectTransform), TextManager.Get("mlc.configshort"), style: "GUIButtonSmall")
{
OnClicked = (GUIButton obj, object o) =>
{
GUI.TogglePauseMenu();
return Instance.OpenConfig(obj, o);
},
};
}
object OpenPatchNotes(object[] args)
{
if (!ConfigManager.ShouldDisplayPatchNotes) return null;
CoroutineManager.Invoke(() => PatchNotes.Open(), delay: 5.0f);
return null;
}
private bool OpenConfig(GUIButton button, object obj)
{
ConfigManager.Instance.SettingsOpen = true;
return false;
}
}
}
@@ -0,0 +1,14 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Text;
namespace MoreLevelContent.Missions
{
partial class BeaconConstMission : Mission
{
public override bool DisplayAsCompleted => State > 0;
public override bool DisplayAsFailed => false;
}
}
@@ -0,0 +1,20 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Client
internal partial class CablePuzzleMission : Mission
{
public override bool DisplayAsCompleted => State == 2;
public override bool DisplayAsFailed => false;
}
}
@@ -0,0 +1,24 @@
using Barotrauma;
using Barotrauma.Networking;
using MoreLevelContent.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Client
partial class DistressEscortMission : DistressMission
{
public override bool DisplayAsFailed => State == 1;
public override void ClientReadInitial(IReadMessage msg)
{
base.ClientReadInitial(msg);
missionNPCs.Read(msg);
InitCharacters();
}
}
}
@@ -0,0 +1,100 @@
using Barotrauma;
using Barotrauma.Networking;
using MoreLevelContent.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
partial class DistressGhostshipMission : DistressMission
{
private readonly Identifier EXPLORE_SUB = "MLCDISTRESS_GHOSTSHIP_EXPLORE_SUB";
private readonly Identifier SALVAGE_SUB = "MLCDISTRESS_GHOSTSHIP_SALVAGE";
public override bool DisplayAsFailed => false;
public override bool DisplayAsCompleted => State >= 2;
public override void ClientReadInitial(IReadMessage msg)
{
base.ClientReadInitial(msg);
Log.Debug("message read init");
missionNPCs.Read(msg);
}
public override RichString GetMissionRewardText(Submarine sub) => !SubSalvaged ? base.GetMissionRewardText(sub) : GetBaseMissionRewardText(sub);
private GhostshipState CurrentState = GhostshipState.WaitForBoardSub;
private ObjectiveManager.Segment ExploreSegment;
private List<Hull> HullToExplore = new();
private List<Hull> ExploredHulls = new();
private enum GhostshipState
{
WaitForBoardSub,
WaitForExplore,
WaitForSalvage,
Salvage
}
private bool _salvaged = false;
partial void UpdateProjSpecific(float deltaTime)
{
if (SubSalvaged && !_salvaged)
{
ObjectiveManager.CompleteSegment(SALVAGE_SUB);
_salvaged = true;
CoroutineManager.StartCoroutine(_showMessageBox(TextManager.Get("missionheader0.distress_ghostship"), TextManager.Get("dgs.inrageforsalvage")));
}
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;
}
switch (State)
{
case 2:
if (CurrentState == GhostshipState.WaitForSalvage)
{
return;
}
if (CurrentState == GhostshipState.WaitForExplore)
{
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
if (HullToExplore.Contains(crewMember.CurrentHull))
{
if (!ExploredHulls.Contains(crewMember.CurrentHull))
{
ExploredHulls.Add(crewMember.CurrentHull);
}
}
}
if (ExploredHulls.Count >= HullToExplore.Count / 2)
{
CurrentState = GhostshipState.WaitForSalvage;
ObjectiveManager.CompleteSegment(EXPLORE_SUB);
ObjectiveManager.TriggerSegment(ObjectiveManager.Segment.CreateObjectiveSegment(SALVAGE_SUB, "dgs.obj.optionalsalvage"));
}
return;
}
HullToExplore = ghostship.GetHulls(false).Where(h => h.AvoidStaying == false).ToList();
CurrentState = GhostshipState.WaitForExplore;
ExploreSegment = ObjectiveManager.Segment.CreateObjectiveSegment(EXPLORE_SUB, "dgs.obj.exploreship");
ExploreSegment.CanBeCompleted = true;
ObjectiveManager.TriggerSegment(ExploreSegment);
Log.Debug("Triggered state");
break;
}
}
}
}
@@ -0,0 +1,19 @@
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Client
abstract partial class DistressMission : Mission
{
public override bool DisplayAsCompleted => false;
// Hide reward until the end of the round
public override RichString GetMissionRewardText(Submarine sub) => RichString.Rich(TextManager.GetWithVariable("missionreward", "[reward]", $"‖color:gui.orange‖{(GameMain.GameSession.RoundEnding || DisplayReward ? Reward : "???")}‖end‖"));
protected RichString GetBaseMissionRewardText(Submarine sub) => base.GetMissionRewardText(sub);
}
}
@@ -0,0 +1,35 @@
using Barotrauma;
using Barotrauma.Networking;
using HarmonyLib;
using MoreLevelContent.Shared;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Client
partial class DistressSubmarineMission : DistressMission
{
public override bool DisplayAsFailed => false;
public override RichString GetMissionRewardText(Submarine sub) => State == 0 ? base.GetMissionRewardText(sub) : GetBaseMissionRewardText(sub);
public override void ClientReadInitial(IReadMessage msg)
{
base.ClientReadInitial(msg);
missionNPCs.Read(msg);
foreach (var character in missionNPCs.characters)
{
int reward = msg.ReadUInt16();
rewardLookup.Add(character, reward);
character.Info.Title = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", reward));
}
}
}
}
@@ -0,0 +1,48 @@
using Barotrauma.Networking;
using Barotrauma;
using MoreLevelContent.Shared;
namespace MoreLevelContent.Missions
{
// Client
partial class MissionNPCCollection
{
internal void Read(IReadMessage msg)
{
bool hasCharacters = msg.ReadBoolean();
if (!hasCharacters)
{
Log.Debug("Mission has no characters");
return;
}
byte characterCount = msg.ReadByte();
for (int i = 0; i < characterCount; i++)
{
Character character = Character.ReadSpawnData(msg);
bool allowOrdering = msg.ReadBoolean();
characters.Add(character);
if (allowOrdering)
{
_ = GameMain.GameSession.CrewManager.AddCharacterToCrewList(character);
Log.InternalDebug($"Added character {character.Name} to crew list");
}
ushort itemCount = msg.ReadUInt16();
for (int j = 0; j < itemCount; j++)
{
Item.ReadSpawnData(msg);
}
}
if (characters.Contains(null))
{
throw new System.Exception("Error in EscortMission.ClientReadInitial: character list contains null (mission: " + mission.Prefab.Identifier + ")");
}
if (characters.Count != characterCount)
{
throw new System.Exception("Error in EscortMission.ClientReadInitial: character count does not match the server count (" + characterCount + " != " + characters.Count + "mission: " + mission.Prefab.Identifier + ")");
}
InitCharacters();
}
}
}
@@ -0,0 +1,11 @@
using Barotrauma;
namespace MoreLevelContent.Missions
{
// Client
internal partial class TriangulationMission : Mission
{
public override bool DisplayAsCompleted => false;
public override bool DisplayAsFailed => false;
}
}
@@ -0,0 +1,18 @@
using Barotrauma;
using Barotrauma.Networking;
namespace MoreLevelContent.Networking
{
/// <summary>
/// Client
/// </summary>
public static partial class NetUtil
{
internal static void SendServer(IWriteMessage outMsg, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable)
{
if (GameMain.IsSingleplayer) return;
GameMain.LuaCs.Networking.Send(outMsg, deliveryMethod);
}
}
}
@@ -0,0 +1,36 @@
using Barotrauma;
using Microsoft.Xna.Framework.Graphics;
using MoreLevelContent.Shared.Generation;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using Barotrauma.Items.Components;
using MoreLevelContent.Shared;
using System;
public static class ClientDebugDraw
{
internal static void Draw(SpriteBatch spriteBatch, Camera cam)
{
if (Level.Loaded != null)
{
// spriteBatch.DrawCircle(new Vector2(Level.Loaded.StartPosition.X, -Level.Loaded.StartPosition.Y), CaveGenerationDirector.MIN_DIST_FROM_START, 16, Color.Red, thickness: 100);
// spriteBatch.DrawLine(new Vector2(0, -Level.Loaded.StartPosition.Y + (Sonar.DefaultSonarRange / 2)), new Vector2(int.MaxValue, -Level.Loaded.StartPosition.Y + (Sonar.DefaultSonarRange / 2)), Color.Yellow, thickness: 2 / Screen.Selected.Cam.Zoom * GUI.Scale);
}
foreach (var item in CaveGenerationDirector.Instance._InitialCaveCheckDebug)
{
GUI.DrawString(spriteBatch, new Vector2(item.Cell.Center.X, -item.Cell.Center.Y), "Cell", Color.Azure);
}
foreach (var (from, to) in MissionGenerationDirector.DebugPoints)
{
var from1 = new Vector2(from.X, -from.Y);
var to1 = new Vector2(to.X, -to.Y);
spriteBatch.DrawCircle(from1, 20, 8, Color.Aqua);
spriteBatch.DrawCircle(to1, 10, 8, Color.Yellow);
GUI.DrawLine(spriteBatch, from1, to1, Color.Red, width: 5);
}
if (CaveGenerationDirector.Instance.ActiveThalaCave != null) CaveGenerationDirector.Instance.ActiveThalaCave.DebugDraw(spriteBatch, cam);
}
}
+354
View File
@@ -0,0 +1,354 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.MoreLevelContent.Shared.Config;
using Barotrauma.MoreLevelContent.Config;
using MoreLevelContent.Shared;
using OpenAL;
namespace Barotrauma.MoreLevelContent.Client.UI
{
public class ConfigMenu
{
public static ConfigMenu Instance { get; private set; }
private MLCConfig unsavedConfig;
private readonly GUIFrame mainFrame;
private readonly GUILayoutGroup tabber;
private readonly GUIFrame contentFrame;
private readonly GUILayoutGroup bottom;
private readonly Dictionary<Tab, (GUIButton Button, GUIFrame Content)> tabContents;
public ConfigMenu(RectTransform mainParent)
{
unsavedConfig = ConfigManager.Instance.Config;
mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent));
var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center),
isHorizontal: false, childAnchor: Anchor.TopRight);
_ = new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.Get("mlc.config"),
font: GUIStyle.LargeFont);
var tabberAndContentLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.86f), mainLayout.RectTransform),
isHorizontal: true);
void tabberPadding()
=> new GUIFrame(new RectTransform((0.01f, 1.0f), tabberAndContentLayout.RectTransform), style: null);
tabberPadding();
tabber = new GUILayoutGroup(new RectTransform((0.06f, 1.0f), tabberAndContentLayout.RectTransform), isHorizontal: false) { AbsoluteSpacing = GUI.IntScale(5f) };
tabberPadding();
tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>();
contentFrame = new GUIFrame(new RectTransform((0.92f, 1.0f), tabberAndContentLayout.RectTransform),
style: "InnerFrame");
bottom = new GUILayoutGroup(new RectTransform((contentFrame.RectTransform.RelativeSize.X, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f };
var tabToSelect = Tab.Debug;
tabToSelect = MakePermissionLockedTabs(tabToSelect);
CreateDebugTab();
CreateBottomButtons();
SelectTab(tabToSelect);
}
private Tab MakePermissionLockedTabs(Tab defaultTab)
{
// If we're not in single player
if (!GameMain.IsSingleplayer)
{
// and we don't have perms
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings))
return defaultTab; // don't create perm locked tabs
}
CreateGeneralTab();
CreatePirateOutpostTab();
return Tab.General;
}
GUITextBlock moveRuinsChanceDisplay;
GUITextBlock maxDistressCountDisplay;
GUITextBlock distressSpawnChanceDisplay;
private void CreateGeneralTab()
{
GUIFrame content = CreateNewContentFrame(Tab.General);
var (left, right) = CreateSidebars(content);
Tickbox(left,
TextManager.Get("mlc.settings.enablethalamuscave"),
TextManager.Get("mlc.settings.enablethalamuscavetooltip"),
unsavedConfig.NetworkedConfig.GeneralConfig.EnableThalamusCaves,
(v) => unsavedConfig.NetworkedConfig.GeneralConfig.EnableThalamusCaves = v);
Tickbox(left,
TextManager.Get("mlc.settings.enabledistressmissions"),
TextManager.Get("mlc.settings.enabledistressmissionstooltip"),
unsavedConfig.NetworkedConfig.GeneralConfig.EnableDistressMissions,
(v) => unsavedConfig.NetworkedConfig.GeneralConfig.EnableDistressMissions = v);
Tickbox(left,
TextManager.Get("mlc.settings.enablemapfeature"),
TextManager.Get("mlc.settings.enablemapfeaturetooltip"),
unsavedConfig.NetworkedConfig.GeneralConfig.EnableMapFeatures,
(v) => unsavedConfig.NetworkedConfig.GeneralConfig.EnableMapFeatures = v);
Tickbox(left,
TextManager.Get("mlc.settings.enablerelaystation"),
TextManager.Get("mlc.settings.enablerelaystationtooltip"),
unsavedConfig.NetworkedConfig.GeneralConfig.EnableRelayStations,
(v) => unsavedConfig.NetworkedConfig.GeneralConfig.EnableRelayStations = v);
Tickbox(left,
TextManager.Get("mlc.settings.enableconstructionsites"),
TextManager.Get("mlc.settings.enableconstructionsitestooltip"),
unsavedConfig.NetworkedConfig.GeneralConfig.EnableConstructionSites,
(v) => unsavedConfig.NetworkedConfig.GeneralConfig.EnableConstructionSites = v);
var moveRuinsChance = Label(left, TextManager.Get("mlc.settings.moveruins"), GUIStyle.SubHeadingFont);
moveRuinsChanceDisplay = TextBlock(moveRuinsChance, TextManager.Get("mlc.settings.moveruinstooltip"));
Slider(left, (0, 100), 100, (v) => $"{Round(v)}%",
unsavedConfig.NetworkedConfig.GeneralConfig.RuinMoveChance,
(v) => UpdateRuinMoveChance(v));
var maxActiveDistress = Label(left, TextManager.Get("mlc.settings.maxdistresscount"), GUIStyle.SubHeadingFont);
maxDistressCountDisplay = TextBlock(maxActiveDistress, TextManager.Get("mlc.settings.maxdistresscounttooltip"));
Slider(left, (0, 100), 100, (v) => $"{Round(v)}",
unsavedConfig.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons,
(v) => UpdateMaxDistress(v));
var distressSpawnChance = Label(left, TextManager.Get("mlc.settings.spawndistresschance"), GUIStyle.SubHeadingFont);
distressSpawnChanceDisplay = TextBlock(distressSpawnChance, TextManager.Get("mlc.settings.spawndistresschancetooltip"));
Slider(left, (0, 100), 100, (v) => $"{Round(v)}",
unsavedConfig.NetworkedConfig.GeneralConfig.DistressSpawnChance,
(v) => UpdateDistressSpawnChance(v));
void UpdateRuinMoveChance(float v)
{
unsavedConfig.NetworkedConfig.GeneralConfig.RuinMoveChance = Round(v);
moveRuinsChanceDisplay.Text = TextManager.GetWithVariable("mlc.settings.spawnchance", "[chance]", Round(v).ToString()); ;
}
void UpdateMaxDistress(float v)
{
unsavedConfig.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons = Round(v);
maxDistressCountDisplay.Text = TextManager.GetWithVariable("mlc.settings.maxactive", "[max]", Round(v).ToString()); ;
}
void UpdateDistressSpawnChance(float v)
{
unsavedConfig.NetworkedConfig.GeneralConfig.DistressSpawnChance = Round(v);
distressSpawnChanceDisplay.Text = TextManager.GetWithVariable("mlc.settings.spawnchance", "[chance]", Round(v).ToString());
}
UpdateRuinMoveChance(unsavedConfig.NetworkedConfig.GeneralConfig.RuinMoveChance);
UpdateMaxDistress(unsavedConfig.NetworkedConfig.GeneralConfig.MaxActiveDistressBeacons);
UpdateDistressSpawnChance(unsavedConfig.NetworkedConfig.GeneralConfig.DistressSpawnChance);
GUITextBlock TextBlock(GUITextBlock container, RichString tooltip)
{
return new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), container.RectTransform), "", textAlignment: Alignment.CenterRight)
{
ToolTip = tooltip
};
}
}
GUITextBlock pirateSpawnChanceDisplay;
private void CreatePirateOutpostTab()
{
GUIFrame content = CreateNewContentFrame(Tab.PirateOutpost);
var (left, right) = CreateSidebars(content);
Tickbox(left,
TextManager.Get("mlc.settings.enablepiratebase"),
TextManager.Get("mlc.settings.enablepiratebasetooltip"),
unsavedConfig.NetworkedConfig.PirateConfig.EnablePirateBases,
(v) => unsavedConfig.NetworkedConfig.PirateConfig.EnablePirateBases = v);
// If the pirate outpost is displayed on sonar
Tickbox(left,
TextManager.Get("mlc.config.piratedisplaysonar"),
TextManager.Get("mlc.config.piratedisplaysonartooltip"),
unsavedConfig.NetworkedConfig.PirateConfig.DisplaySonarMarker,
(v) => unsavedConfig.NetworkedConfig.PirateConfig.DisplaySonarMarker = v);
// If the pirate difficulty should scale with server memebers
Tickbox(left,
TextManager.Get("mlc.config.piratescalediff"),
TextManager.Get("mlc.config.piratescaledifftooltip"),
unsavedConfig.NetworkedConfig.PirateConfig.AddDiffPerPlayer,
(v) => unsavedConfig.NetworkedConfig.PirateConfig.AddDiffPerPlayer = v);
}
private void CreateDebugTab()
{
GUIFrame content = CreateNewContentFrame(Tab.Debug);
var (left, right) = CreateSidebars(content);
Tickbox(left, TextManager.Get("mlc.config.debugverbose"), TextManager.Get("mlc.config.debugverbosetooltip"), unsavedConfig.Client.Verbose, (v) => unsavedConfig.Client.Verbose = v);
Tickbox(left, TextManager.Get("mlc.config.debuginternal"), TextManager.Get("mlc.config.debuginternaltooltip"), unsavedConfig.Client.Internal, (v) => unsavedConfig.Client.Internal = v);
GUIButton showPatchNotes = new GUIButton(NewItemRectT(left), text: "Patch Notes")
{
OnClicked = (btn, obj) =>
{
MoreLevelContent.Client.UI.PatchNotes.Open();
return false;
}
};
}
private void CreateBottomButtons()
{
GUIButton cancelButton =
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Cancel")
{
OnClicked = (btn, obj) =>
{
Close();
return false;
}
};
GUIButton applyButton =
new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: "Apply")
{
OnClicked = (btn, obj) =>
{
ConfigManager.Instance.SetConfig(unsavedConfig);
mainFrame.Flash(color: GUIStyle.Green);
return false;
}
};
}
private void Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action<bool> setter)
{
var tickbox = new GUITickBox(NewItemRectT(parent), label)
{
Selected = currentValue,
ToolTip = tooltip,
OnSelected = (tb) =>
{
setter(tb.Selected);
return true;
}
};
}
private int Round(float v) => (int)MathF.Round(v);
private void Slider(GUILayoutGroup parent, Vector2 range, int steps, Func<float, string> labelFunc, float currentValue, Action<float> setter, LocalizedString tooltip = null)
{
var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true);
var slider = new GUIScrollBar(new RectTransform((0.82f, 1.0f), layout.RectTransform), style: "GUISlider")
{
Range = range,
BarScrollValue = currentValue,
Step = 1.0f / (steps - 1),
BarSize = 1.0f / steps
};
if (tooltip != null)
{
slider.ToolTip = tooltip;
}
var label = new GUITextBlock(new RectTransform((0.18f, 1.0f), layout.RectTransform),
labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center);
slider.OnMoved = (sb, val) =>
{
label.Text = labelFunc(sb.BarScrollValue);
setter(sb.BarScrollValue);
return true;
};
}
private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) => new GUITextBlock(NewItemRectT(parent), str, font: font);
private static RectTransform NewItemRectT(GUILayoutGroup parent)
=> new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft);
private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false)
{
GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true);
GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false);
var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null);
if (split)
{
_ = new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform),
onDraw: (sb, c) => sb.DrawLine((c.Rect.Center.X, c.Rect.Top), (c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f));
}
GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false);
return (left, right);
}
private GUIFrame CreateNewContentFrame(Tab tab)
{
var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null);
AddButtonToTabber(tab, content);
return content;
}
private void AddButtonToTabber(Tab tab, GUIFrame content)
{
var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: $"SettingsMenuTab.{tab}")
{
ToolTip = TextManager.Get($"SettingsTab.{tab}"),
OnClicked = (b, _) =>
{
SelectTab(tab);
return false;
}
};
button.RectTransform.MaxSize = RectTransform.MaxPoint;
button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint);
tabContents.Add(tab, (button, content));
}
public void SelectTab(Tab tab)
{
SwitchContent(tabContents[tab].Content);
tabber.Children.ForEach(c =>
{
if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; }
});
}
private void SwitchContent(GUIFrame newContent)
{
contentFrame.Children.ForEach(c => c.Visible = false);
newContent.Visible = true;
}
public static ConfigMenu Create(RectTransform mainParent)
{
Instance?.Close();
Instance = new ConfigMenu(mainParent);
return Instance;
}
public void Close()
{
mainFrame.Parent.RemoveChild(mainFrame);
Instance = null;
ConfigManager.Instance.SettingsOpen = false;
}
public enum Tab
{
General,
PirateOutpost,
Debug
}
}
}
+273
View File
@@ -0,0 +1,273 @@
using Barotrauma.MoreLevelContent.Shared.Utils;
using System.Reflection;
using MoreLevelContent;
using HarmonyLib;
using MoreLevelContent.Shared.Data;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Generation;
using System.Threading;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection.Emit;
using Barotrauma.MoreLevelContent.Config;
namespace Barotrauma.MoreLevelContent.Client.UI
{
public class MapUI : Singleton<MapUI>
{
static FieldInfo zoomLevel;
static FieldInfo tooltipField;
static FieldInfo pendingSubInfoField;
static MethodInfo isInFogOfWar;
static bool _DrawingConnections = false;
public override void Setup()
{
var drawConnection = typeof(Map).GetMethod("DrawConnection", BindingFlags.NonPublic | BindingFlags.Instance);
var draw = AccessTools.Method(typeof(Map), nameof(Map.Draw));
zoomLevel = typeof(Map).GetField("zoom", BindingFlags.Instance | BindingFlags.NonPublic);
tooltipField = typeof(Map).GetField("tooltip", BindingFlags.Instance | BindingFlags.NonPublic);
pendingSubInfoField = AccessTools.Field(typeof(Map), "pendingSubInfo");
isInFogOfWar = AccessTools.Method(typeof(Map), "IsInFogOfWar");
_ = Main.Harmony.Patch(draw, transpiler: new HarmonyMethod(AccessTools.Method(typeof(MapUI), nameof(TranspileMapDraw))));
_ = Main.Harmony.Patch(drawConnection, postfix: new HarmonyMethod(GetType().GetMethod(nameof(OnDrawConnection), BindingFlags.NonPublic | BindingFlags.Static)));
}
private static SubmarineInfo.PendingSubInfo pendingSubInfo;
private static IEnumerable<CodeInstruction> TranspileMapDraw(IEnumerable<CodeInstruction> instructions, ILGenerator il)
{
Log.Debug("Transpiling map draw...");
bool finished = false;
var code = new List<CodeInstruction>(instructions);
for (int i = 0; i < code.Count; i++)
{
if (finished == false && code[i].opcode == OpCodes.Stloc_S && code[i].operand.ToString() == "Barotrauma.LocationConnection (32)")
{
finished = true;
yield return code[i];
yield return new CodeInstruction(OpCodes.Ldloc_S, code[i].operand); // Location connection
yield return new CodeInstruction(OpCodes.Ldarg_0); // Map
yield return new CodeInstruction(OpCodes.Ldarg_2); // Sprite batch
yield return new CodeInstruction(OpCodes.Ldloc_1); // View area
yield return new CodeInstruction(OpCodes.Ldloc_S, (byte)4); // View Offset
yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(MapUI), nameof(DrawRevealedFeatures)));
yield return new CodeInstruction(OpCodes.Ldloc_S, code[i].operand);
}
yield return code[i];
}
if (!finished) Log.Error("Failed to find map transpile injection point!");
}
private static void DrawRevealedFeatures(LocationConnection connection, Map map, SpriteBatch spriteBatch, Rectangle viewArea, Vector2 viewOffset)
{
// Skip if we don't have a feature or pirate base
if (!CheckValid()) return;
// Both sides are in fog of war
bool inFow = (bool)isInFogOfWar.Invoke(map, new object[] { connection.Locations[0] }) && (bool)isInFogOfWar.Invoke(map, new object[] { connection.Locations[1] });
if (inFow)
{
DrawCustomConnections(spriteBatch, connection, viewArea, viewOffset, map, true);
//Log.Debug("Drew custom connection");
}
bool CheckValid()
{
var feature = connection.LevelData.MLC().MapFeatureData;
var pirateBase = connection.LevelData.MLC().PirateData;
if (pirateBase.HasPirateBase && pirateBase.Revealed) return true;
// Not valid if we don't have a feature
if (!feature.HasFeature) return false;
// Not valid if the feature isn't revealed
if (!feature.Revealed) return false;
// Not valid if the feature starts revealed
if (!feature.Feature.Display.HideUntilRevealed) return false;
return true;
}
}
private static void OnDrawConnection(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Map __instance, bool __state)
{
if (__state) return;
DrawCustomConnections(spriteBatch, connection, viewArea, viewOffset, __instance, false);
}
private static void DrawCustomConnections(SpriteBatch spriteBatch, LocationConnection connection, Rectangle viewArea, Vector2 viewOffset, Map __instance, bool drawInFOW)
{
_DrawingConnections = false;
if (connection == null || spriteBatch == null) return;
LevelData_MLCData data = connection.LevelData.MLC();
Vector2? connectionStart = null;
Vector2? connectionEnd = null;
Vector2 rectCenter = viewArea.Center.ToVector2();
int startIndex = connection.CrackSegments.Count > 2 ? 1 : 0;
int endIndex = connection.CrackSegments.Count > 2 ? connection.CrackSegments.Count - 1 : connection.CrackSegments.Count;
float zoom = (float)zoomLevel.GetValue(__instance);
int iconCount, iconIndex = GetIconIndex(connection);
for (int i = startIndex; i < endIndex; i++)
{
var segment = connection.CrackSegments[i];
Vector2 start = rectCenter + (segment[0] + viewOffset) * zoom;
Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom;
connectionEnd = end;
if (!connectionStart.HasValue) { connectionStart = start; }
}
iconCount = GetIconCount(__instance, connection);
if (drawInFOW)
{
iconCount = 0;
DrawMapFeature(data);
DrawPirateBase();
return;
}
if (data.HasBeaconConstruction && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableConstructionSites)
{
LocalizedString localizedString = TextManager.GetWithVariable("mlc.beaconconsttooltip", "[requestedsupplies]", data.GetRequestedSupplies());
DrawIcon("BeaconConst", (int)(28 * zoom), RichString.Rich(localizedString));
}
if (data.HasDistress && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableDistressMissions)
{
string tooltip = "mlc.distresstooltip";
string iconStyle = "DistressBeacon";
if (data.DistressStepsLeft <= 3)
{
tooltip = "mlc.distresstooltipfaint";
iconStyle = "DistressBeaconFaint";
}
LocalizedString localizedString = TextManager.Get(tooltip);
DrawIcon(iconStyle, (int)(28 * zoom), RichString.Rich(localizedString));
}
if (data.HasLostCargo)
{
DrawIcon("LostCargo", (int)(28 * zoom), RichString.Rich(TextManager.Get("mlc.lostcargotooltip")));
}
if (data.HasRelayStation && ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableRelayStations)
{
var iconName = data.RelayStationStatus == RelayStationStatus.Active ? "RelayStationActive" : "RelayStationInactive";
var locString = data.RelayStationStatus == RelayStationStatus.Active ? "mlc.relaystationtooltip.active" : "mlc.relaystationtooltip.inactive";
LocalizedString localizedString = TextManager.Get(locString);
DrawIcon(iconName, (int)(28 * zoom), RichString.Rich(localizedString));
}
DrawPirateBase();
DrawMapFeature(data);
void DrawMapFeature(LevelData_MLCData data)
{
if (!ConfigManager.Instance.Config.NetworkedConfig.GeneralConfig.EnableMapFeatures) return;
if (data.MapFeatureData.Name.IsEmpty) return;
if (!data.MapFeatureData.Revealed && !GameMain.DebugDraw && !Commands.DisplayAllMapLocations) return;
if (!MapFeatureModule.TryGetFeature(data.MapFeatureData.Name, out MapFeature feature))
{
Log.Error($"Failed to find map feature with identifier {data.MapFeatureData.Name}!!");
return;
}
var tooltip = TextManager.Get(feature.Display.Tooltip);
if (GameMain.DebugDraw)
{
tooltip = $"{tooltip.Value} + {data.MapFeatureData.Revealed}";
}
DrawIcon(feature.Display.Icon, (int)(28 * zoom), RichString.Rich(tooltip));
}
void DrawPirateBase()
{
if (ConfigManager.Instance.Config.NetworkedConfig.PirateConfig.EnablePirateBases &&
data.PirateData.HasPirateBase &&
(GameMain.DebugDraw || Commands.DisplayAllMapLocations || data.PirateData.Revealed))
{ } else { return; }
LocalizedString text = "";
switch (data.PirateData.Status)
{
case PirateOutpostStatus.Active:
text = TextManager.Get("piratebase.active");
break;
case PirateOutpostStatus.Destroyed:
text = TextManager.Get("piratebase.destroyed");
break;
case PirateOutpostStatus.Husked:
text = TextManager.Get("piratebase.husked");
break;
}
if (GameMain.DebugDraw)
{
text += $" Revealed: {data.PirateData.Revealed}";
}
DrawIcon(data.PirateData.Status == PirateOutpostStatus.Active ? "PirateBase" : "PirateBaseDestroyed", (int)(28 * zoom), RichString.Rich(text));
}
void DrawIcon(string iconStyle, int iconSize, RichString tooltipText)
{
Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2;
Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize;
iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex;
var style = GUIStyle.GetComponentStyle(iconStyle);
if (style == null)
{
Log.Error($"Unable to find icon style {style}");
return;
}
bool mouseOn = Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize && IsPreferredTooltip(iconPos, __instance);
Sprite iconSprite = style.GetDefaultSprite();
iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f,
scale: iconSize / iconSprite.size.X);
if (mouseOn)
{
tooltipField.SetValue(__instance, (new Rectangle((iconPos - Vector2.One * iconSize / 2).ToPoint(), new Point(iconSize)), tooltipText));
}
iconIndex++;
}
bool IsPreferredTooltip(Vector2 tooltipPos, Map map) => tooltipField.GetValue(map) == null || Vector2.DistanceSquared(tooltipPos, PlayerInput.MousePosition) < Vector2.DistanceSquared((tooltipField.GetValue(map) as (Rectangle targetArea, RichString tip)?).Value.targetArea.Center.ToVector2(), PlayerInput.MousePosition);
int GetIconCount(Map __instance, LocationConnection connection)
{
int iconCount = 0;
float subCrushDepth = SubmarineInfo.GetSubCrushDepth(SubmarineSelection.CurrentOrPendingSubmarine(), ref pendingSubInfo);
if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth)
{
iconIndex++;
iconCount++;
}
else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth)
{
iconIndex++;
iconCount++;
}
return iconCount;
}
int GetIconIndex(LocationConnection connection)
{
int index = 0;
if (connection.LevelData.HasBeaconStation) index++;
if (connection.Locked) index++;
if (connection.LevelData.HasHuntingGrounds) index++;
return index;
}
}
}
}
@@ -0,0 +1,73 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Barotrauma.MoreLevelContent.Config;
using MoreLevelContent;
namespace Barotrauma.MoreLevelContent.Client.UI
{
public class PatchNotes
{
private readonly GUIFrame mainFrame;
private readonly GUIFrame backgroundBlocker;
private readonly GUIFrame contentFrame;
private readonly GUILayoutGroup bottom;
public static PatchNotes Instance { get; private set; }
public PatchNotes()
{
backgroundBlocker = new GUIFrame(new RectTransform(Vector2.One, Screen.Selected.Frame.RectTransform, Anchor.Center), style: null);
_ = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, backgroundBlocker.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
var mainParent = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.55f), backgroundBlocker.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) }).RectTransform;
mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent));
var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center),
isHorizontal: false, childAnchor: Anchor.TopRight);
_ = new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.GetWithVariable("mlc.patchnote", "[version]", Main.Version),
font: GUIStyle.LargeFont);
// Padding
_ = new GUIFrame(new RectTransform((0.01f, 0.01f), mainLayout.RectTransform), style: null);
contentFrame = new GUIFrame(new RectTransform((1.0f, 0.8f), mainLayout.RectTransform),
style: "InnerFrame");
_ = new GUITextBlock(new RectTransform((1.0f, 1.0f), contentFrame.RectTransform), TextManager.Get("mlc.patchnotes").Value ?? "hot spicy meme action", textAlignment: Alignment.TopLeft);
// Padding
_ = new GUIFrame(new RectTransform((0.01f, 0.01f), mainLayout.RectTransform), style: null);
bottom = new GUILayoutGroup(new RectTransform((1.0f, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f };
CreateBottomButton();
}
private void CreateBottomButton()
{
GUIButton cancelButton =
new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), bottom.RectTransform), text: "close")
{
OnClicked = (btn, obj) =>
{
Close();
return false;
}
};
}
public static void Open()
{
Instance?.Close();
Instance = new PatchNotes();
ConfigManager.ShouldDisplayPatchNotes = false;
}
public void Close()
{
mainFrame.Parent.RemoveChild(mainFrame);
backgroundBlocker.Parent.RemoveChild(backgroundBlocker);
if (Instance == this) { Instance = null; }
}
}
}
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RunConfig>
<Server>Standard</Server>
<Client>Standard</Client>
<UseNonPublicizedAssemblies>true</UseNonPublicizedAssemblies>
<UseInternalAssemblyName>true</UseInternalAssemblyName>
</RunConfig>
@@ -0,0 +1,112 @@
using Barotrauma.MoreLevelContent.Shared.Utils;
using MoreLevelContent.Shared;
using Barotrauma.MoreLevelContent.Shared.Config;
using MoreLevelContent;
using Barotrauma.Networking;
using MoreLevelContent.Networking;
using System.Collections.Generic;
using System.Linq;
using System;
// ISSUE WITH LEVEL GEN, IT'S THE RUIN MOVE
namespace Barotrauma.MoreLevelContent.Config
{
/// <summary>
/// Server
/// </summary>
partial class ConfigManager : Singleton<ConfigManager>
{
private void SetupServer()
{
NetUtil.Register(NetEvent.CONFIG_WRITE_SERVER, ServerRead);
NetUtil.Register(NetEvent.CONFIG_REQUEST, ConfigRequest);
// Always init the server with a default config, the first client to join with admin perms will set the config
// This was due to some issue with reading the config file on the server iirc
// Maybe revist this in the future?
Log.Debug("Setting up server config...");
// Only setup default config on non-dedicated servers, on servers hosted through
// the in game menu, the owner of the server will send the config to use
// where as on dedicated servers they will never get a config sent and thus will
// always have the default config loaded
if (!Main.IsDedicatedServer)
{
DefaultConfig();
} else
{
LoadConfig();
}
}
private readonly List<AccountId> correctInstalls = new List<AccountId>();
#region Networking
private void ServerRead(object[] args)
{
try
{
IReadMessage inMsg = (IReadMessage)args[0];
Client c = (Client)args[1];
Log.Debug($"Got config from client {c.Name}");
if (!c.HasPermission(ClientPermissions.ManageSettings))
{
Log.Error("No Perms!");
return;
}
if (!CheckClientVersion(c, inMsg.ReadString()))
{
Log.Debug($"Ignored config from {c.Name} due to them using the wrong version!");
return;
}
ReadNetConfig(ref inMsg);
ServerWrite();
} catch(Exception err)
{
Log.Debug(err.ToString());
}
}
private void ServerWrite()
{
Log.Debug("Propagating config to all clients...");
IWriteMessage outMsg = NetUtil.CreateNetMsg(NetEvent.CONFIG_WRITE_CLIENT);
WriteConfig(ref outMsg);
NetUtil.SendAll(outMsg);
}
private void ConfigRequest(object[] args)
{
IReadMessage inMsg = (IReadMessage)args[0];
Client c = (Client)args[1];
string version = inMsg.ReadString();
if (!CheckClientVersion(c, version)) return; // Exit if the client doesn't have the right version
IWriteMessage outMsg = NetUtil.CreateNetMsg(NetEvent.CONFIG_WRITE_CLIENT);
WriteConfig(ref outMsg);
NetUtil.SendClient(outMsg, c.Connection);
Log.Debug($"Sent config to client {c.Name}");
}
#endregion
private bool CheckClientVersion(Client client, string clientVersion)
{
if (!client.AccountId.TryUnwrap(out AccountId account)) return false;
if (correctInstalls.Contains(account)) return true;
if (clientVersion != Main.Version)
{
GameMain.Server.SendDirectChatMessage(
TextManager.GetServerMessage($"mlc.server.wrongversionclient~[clientversion]={clientVersion}~[serverversion]={Main.Version}").Value,
client,
ChatMessageType.ServerMessageBox);
GameMain.Server.SendChatMessage(TextManager.GetServerMessage($"mlc.server.wrongversion~[client]={client.Name}~[clientversion]={clientVersion}~[serverversion]={Main.Version}").Value);
return false;
}
GameMain.Server.SendChatMessage(TextManager.GetServerMessage($"mlc.server.installed~[client]={client.Name}").Value);
correctInstalls.Add(account);
return true;
}
}
}
@@ -0,0 +1,81 @@
using Barotrauma;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Networking;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation.Pirate;
using System;
using System.Linq;
namespace MoreLevelContent.Shared.Generation
{
// Server
public partial class MapDirector : Singleton<MapDirector>
{
partial void SetupProjSpecific()
{
NetUtil.Register(NetEvent.MAP_CONNECTION_EQUALITYCHECK_REQUEST, RequestConnectionEquality);
NetUtil.Register(NetEvent.MAP_REQUEST_STATE, RespondToMapStateRequest);
}
private void RespondToMapStateRequest(object[] args)
{
Client client = (Client)args[1];
IWriteMessage outMsg = NetUtil.CreateNetMsg(NetEvent.MAP_SEND_STATE);
WriteMsg();
NetUtil.SendClient(outMsg, client.Connection);
Log.Debug($"Sent map state request to {client.Name}");
void WriteMsg()
{
if (GameMain.GameSession.Campaign is not MultiPlayerCampaign campaign)
{
outMsg.WriteByte((byte)MapSyncState.NotCampaign);
return;
}
if (campaign.Map == null)
{
outMsg.WriteByte((byte)MapSyncState.MapNotCreated);
return;
}
outMsg.WriteByte((byte)MapSyncState.MapSynced);
var activeDistressBeacons = campaign.Map.Connections.Where(c => c.LevelData.MLC().HasDistress);
var count = activeDistressBeacons.Count();
if (count > byte.MaxValue)
{
DebugConsole.ThrowError("More Level Content detected more than 255 active distress beacons when trying to respond to a client map state request, what did you do??? This won't work, please reduce the numer!");
return;
}
outMsg.WriteByte((byte)count);
foreach (var connection in activeDistressBeacons)
{
int id = ConnectionIdLookup[connection];
outMsg.WriteInt16((short)id);
outMsg.WriteByte((byte)connection.LevelData.MLC().DistressStepsLeft);
}
}
}
internal void NotifyMapFeatureRevealed(LocationConnection con, MapFeatureData feature)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
NotifyMapFeatureRevealed(client, con, feature);
}
}
internal partial void RoundEnd(CampaignMode.TransitionType transitionType) { }
private void NotifyMapFeatureRevealed(Client client, LocationConnection con, MapFeatureData feature)
{
Int32 conId = MapDirector.ConnectionIdLookup[con];
var msg = NetUtil.CreateNetMsg(NetEvent.EVENT_REVEALMAPFEATURE);
msg.WriteIdentifier(feature.Name);
msg.WriteInt32(conId);
NetUtil.SendClient(msg, client.Connection);
}
}
}
@@ -0,0 +1,30 @@
using Barotrauma.Networking;
using Barotrauma;
using MoreLevelContent.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MoreLevelContent.Shared.Data;
using Barotrauma.MoreLevelContent.Config;
namespace MoreLevelContent.Shared.Generation
{
// Server
internal partial class OldDistressMapModule
{
protected override void InitProjSpecific() => NetUtil.Register(NetEvent.COMMAND_CREATEDISTRESS, Command_CreateDistress);
internal void Command_CreateDistress(object[] args)
{
Log.Debug("Got command");
ForceDistress();
}
}
internal partial class DistressMapModule : TimedEventMapModule
{
protected override void InitProjSpecific() { }
}
}
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Shared.Generation
{
// Server
internal partial class LostCargoMapModule : TimedEventMapModule
{
protected override void InitProjSpecific() { }
}
}
@@ -0,0 +1,84 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Utils;
using System;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Items
{
// Server
internal partial class SimpleStore : Powered, IServerSerializable, IClientSerializable
{
public void ServerEventRead(IReadMessage msg, Client c)
{
uint recipeHash = msg.ReadUInt32();
int amountToFabricate = msg.ReadRangedInteger(1, MaxAmountToFabricate);
item.CreateServerEvent(this);
if (!item.CanClientAccess(c)) { return; }
AmountToFabricate = amountToFabricate;
if (recipeHash == 0)
{
CancelFabricating(c.Character);
}
else
{
//if already fabricating the selected item, return
if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; }
if (recipeHash == 0) { return; }
amountRemaining = AmountToFabricate;
StartFabricating(fabricationRecipes[recipeHash], c.Character);
}
}
private ulong serverEventId = 0;
private readonly struct EventData : IEventData
{
public readonly ulong ServerEventId;
public readonly SimpleStoreState State;
public EventData(ulong serverEventId, SimpleStoreState state)
{
//ensuring the uniqueness of this event is
//required for the fabricator to sync correctly;
//otherwise, the event manager would incorrectly
//assume that the client actually has the latest state
ServerEventId = serverEventId;
State = state;
}
}
public override IEventData ServerGetEventData()
=> new EventData(serverEventId, State);
public override bool ValidateEventData(NetEntityEvent.IData data)
=> TryExtractEventData<EventData>(data, out _);
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
var componentData = ExtractEventData<EventData>(extraData);
msg.WriteByte((byte)componentData.State);
msg.WriteRangedInteger(AmountToFabricate, 0, MaxAmountToFabricate);
msg.WriteRangedInteger(amountRemaining, 0, MaxAmountToFabricate);
msg.WriteSingle(timeUntilReady);
uint recipeHash = fabricatedItem?.RecipeHash ?? 0;
msg.WriteUInt32(recipeHash);
UInt16 userId = fabricatedItem is null || user is null ? (UInt16)0 : user.ID;
msg.WriteUInt16(userId);
msg.WriteUInt16((ushort)fabricationLimits.Count);
foreach (var kvp in fabricationLimits)
{
msg.WriteUInt32(kvp.Key);
msg.WriteUInt32((uint)kvp.Value);
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
using Barotrauma;
using Barotrauma.Networking;
using HarmonyLib;
using MoreLevelContent.Networking;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Generation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
namespace MoreLevelContent
{
// Server
partial class Main
{
public static bool IsDedicatedServer => GameMain.Server.OwnerConnection == null;
public static bool CurrentGameModeValid = true;
public void InitServer()
{
Log.Debug("Init Server");
Harmony.Patch(AccessTools.PropertySetter(typeof(NetLobbyScreen), nameof(NetLobbyScreen.SelectedModeIndex)), postfix: new HarmonyMethod(AccessTools.Method(typeof(Main), nameof(Main.OnGameModeChange))));
OnGameModeChange(GameMain.NetLobbyScreen);
if (PreventRoundEnd)
{
Main.Harmony.Patch(AccessTools.Method(typeof(GameServer), nameof(GameServer.Update)), transpiler: new HarmonyMethod(AccessTools.Method(typeof(Main), nameof(Main.PatchEndRound))));
}
}
static IEnumerable<CodeInstruction> PatchEndRound(IEnumerable<CodeInstruction> instructions, ILGenerator il)
{
Log.Debug(">>>> Starting end round transpile");
var code = new List<CodeInstruction>(instructions);
for (int i = 0; i < code.Count; i++)
{
if (i >= 514 && i <= 519)
{
Log.Debug($"nop {i}");
code[i].opcode = OpCodes.Nop;
}
yield return code[i];
}
}
static void OnGameModeChange(NetLobbyScreen __instance)
{
var gameMode = __instance.GameModes[__instance.SelectedModeIndex];
CurrentGameModeValid = gameMode.GameModeType == typeof(MultiPlayerCampaign);
var validClients = GameMain.Server.ConnectedClients.Where(c => c.HasPermission(ClientPermissions.SelectMode));
if (!CurrentGameModeValid)
{
foreach (var client in validClients)
{
GameMain.Server.SendDirectChatMessage(
TextManager.GetServerMessage($"mlc.gamemodewarning.description").Value,
client,
ChatMessageType.ServerMessageBox);
}
}
}
static void SetRoundEndDelay()
{
Log.Debug("called");
var endRoundDelay = AccessTools.PropertySetter(typeof(GameServer), nameof(GameServer.EndRoundDelay));
var endRoundTimer = AccessTools.PropertySetter(typeof(GameServer), nameof(GameServer.EndRoundTimer));
endRoundTimer.Invoke(GameMain.Server, new object[] { 0 });
endRoundDelay.Invoke(GameMain.Server, new object[] { 1000f });
Log.Debug($"{GameMain.Server.EndRoundDelay} {GameMain.Server.EndRoundTimer}");
}
}
}
@@ -0,0 +1,15 @@
using Barotrauma;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Server
internal partial class CablePuzzleMission : Mission
{
}
}
@@ -0,0 +1,21 @@
using Barotrauma;
using Barotrauma.Networking;
using MoreLevelContent.Shared.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Server
partial class DistressEscortMission : DistressMission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
base.ServerWriteInitial(msg, c);
missionNPCs.Write(msg);
}
}
}
@@ -0,0 +1,19 @@
using Barotrauma.Networking;
using MoreLevelContent.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
partial class DistressGhostshipMission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
base.ServerWriteInitial(msg, c);
missionNPCs.Write(msg);
}
}
}
@@ -0,0 +1,23 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MoreLevelContent.Missions
{
// Server
partial class DistressSubmarineMission : DistressMission
{
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
base.ServerWriteInitial(msg, c);
missionNPCs.Write(msg);
foreach (var character in rewardLookup.Keys)
{
msg.WriteUInt16((ushort)rewardLookup[character]);
}
}
}
}
@@ -0,0 +1,33 @@
using Barotrauma.Networking;
using Barotrauma;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MoreLevelContent.Shared.Data;
namespace MoreLevelContent.Missions
{
// Server
partial class MissionNPCCollection
{
internal void Write(IWriteMessage msg)
{
msg.WriteBoolean(characters.Count > 0);
if (characters.Count == 0) return;
msg.WriteByte((byte)characters.Count);
foreach (Character character in characters)
{
character.WriteSpawnData(msg, character.ID, restrictMessageSize: false);
msg.WriteBoolean(character.MLC().NPCElement.GetAttributeBool("allowordering", false));
msg.WriteUInt16((ushort)characterItems[character].Count());
foreach (Item item in characterItems[character])
{
item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0, item.ParentInventory?.FindIndex(item) ?? -1);
}
}
}
}
}
@@ -0,0 +1,9 @@
using Barotrauma;
namespace MoreLevelContent.Missions
{
// Server
internal partial class TriangulationMission : Mission
{
}
}
@@ -0,0 +1,34 @@
using Barotrauma;
using Barotrauma.Networking;
namespace MoreLevelContent.Networking
{
/// <summary>
/// Server
/// </summary>
public static partial class NetUtil
{
/// <summary>
/// Send a message to the specified client
/// </summary>
/// <param name="outMsg"></param>
/// <param name="connection"></param>
/// <param name="deliveryMethod"></param>
internal static void SendClient(IWriteMessage outMsg, NetworkConnection connection, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable)
{
if (GameMain.IsSingleplayer) return;
GameMain.LuaCs.Networking.Send(outMsg, connection, deliveryMethod);
}
/// <summary>
/// Send message to all connected clients
/// </summary>
/// <param name="outMsg"></param>
/// <param name="deliveryMethod"></param>
internal static void SendAll(IWriteMessage outMsg, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable)
{
if (GameMain.IsSingleplayer) return;
GameMain.LuaCs.Networking.Send(outMsg, null, deliveryMethod);
}
}
}
@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More