Track LocalMods as part of monolith
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user