370 lines
16 KiBLFS
C#
Executable File
370 lines
16 KiBLFS
C#
Executable File
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
|
|
}
|
|
}
|