Unstable 0.17.0.0

This commit is contained in:
Markus Isberg
2022-02-26 02:43:01 +09:00
parent a83f375681
commit 3974067915
913 changed files with 32472 additions and 32364 deletions
@@ -0,0 +1,113 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class AfflictionsFile : ContentFile
{
private readonly static ImmutableHashSet<Type> afflictionTypes;
static AfflictionsFile()
{
afflictionTypes = ReflectionUtils.GetDerivedNonAbstract<Affliction>()
.ToImmutableHashSet();
}
public AfflictionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
private void ParseElement(ContentXElement element, bool overriding)
{
Identifier elementName = element.NameAsIdentifier();
if (element.IsOverride())
{
element.Elements().ForEach(s => ParseElement(s, overriding: true));
}
else if (elementName == "Afflictions")
{
element.Elements().ForEach(s => ParseElement(s, overriding: overriding));
}
else if (elementName == "cprsettings")
{
var cprSettings = new CPRSettings(element, this);
CPRSettings.Prefabs.Add(cprSettings, overriding);
}
else if (elementName == "damageoverlay")
{
#if CLIENT
var damageOverlay = new CharacterHealth.DamageOverlayPrefab(element, this);
CharacterHealth.DamageOverlayPrefab.Prefabs.Add(damageOverlay, overriding);
#endif
}
else
{
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError(
$"No identifier defined for the affliction '{elementName}' in file '{Path}'");
return;
}
if (AfflictionPrefab.Prefabs.ContainsKey(identifier))
{
if (overriding)
{
DebugConsole.NewMessage(
$"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'",
Color.Yellow);
}
else
{
DebugConsole.ThrowError(
$"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'");
return;
}
}
var type = afflictionTypes.FirstOrDefault(t =>
t.Name == elementName
|| t.Name == $"Affliction{elementName}".ToIdentifier())
?? typeof(Affliction);
var prefab = CreatePrefab(element, type);
AfflictionPrefab.Prefabs.Add(prefab, overriding);
}
}
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc?.Root is null) { return; }
ParseElement(doc.Root.FromPackage(ContentPackage), overriding: false);
}
private AfflictionPrefab CreatePrefab(ContentXElement element, Type type)
{
if (type == typeof(AfflictionHusk)) { return new AfflictionPrefabHusk(element, this, type); }
return new AfflictionPrefab(element, this, type);
}
public override void UnloadFile()
{
#if CLIENT
CharacterHealth.DamageOverlayPrefab.Prefabs.RemoveByFile(this);
#endif
CPRSettings.Prefabs.RemoveByFile(this);
AfflictionPrefab.Prefabs.RemoveByFile(this);
}
public override void Sort()
{
#if CLIENT
CharacterHealth.DamageOverlayPrefab.Prefabs.Sort();
#endif
CPRSettings.Prefabs.Sort();
AfflictionPrefab.Prefabs.SortAll();
}
}
}
@@ -0,0 +1,9 @@
namespace Barotrauma
{
sealed class BackgroundCreaturePrefabsFile : OtherFile
{
public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
//this content type only comes into play when a level is generated, so LoadFile and UnloadFile don't have anything to do
}
}
@@ -0,0 +1,19 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage, AlternativeContentTypeNames("MapCreature")]
sealed class BallastFloraFile : GenericPrefabFile<BallastFloraPrefab>
{
public BallastFloraFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior";
protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors";
protected override PrefabCollection<BallastFloraPrefab> prefabs => BallastFloraPrefab.Prefabs;
protected override BallastFloraPrefab CreatePrefab(ContentXElement element)
{
return new BallastFloraPrefab(element, this);
}
}
}
@@ -0,0 +1,8 @@
namespace Barotrauma
{
[RequiredByCorePackage]
public class BeaconStationFile : BaseSubFile
{
public BeaconStationFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class CaveGenerationParametersFile : GenericPrefabFile<CaveGenerationParams>
{
public CaveGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "cave";
protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters";
protected override PrefabCollection<CaveGenerationParams> prefabs => CaveGenerationParams.CaveParams;
protected override CaveGenerationParams CreatePrefab(ContentXElement element)
{
return new CaveGenerationParams(element, this);
}
}
}
@@ -0,0 +1,100 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class CharacterFile : ContentFile
{
public CharacterFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
DebugConsole.ThrowError($"Loading character file failed: {Path}");
return;
}
if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this)))
{
DebugConsole.ThrowError($"Duplicate path: {Path}");
return;
}
var mainElement = doc.Root.FromPackage(ContentPackage);
bool isOverride = mainElement.IsOverride();
if (isOverride) { mainElement = mainElement.FirstElement(); }
if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier n)) { return; }
var prefab = new CharacterPrefab(mainElement, this);
CharacterPrefab.Prefabs.Add(prefab, isOverride);
}
public override void UnloadFile()
{
CharacterPrefab.Prefabs.RemoveByFile(this);
}
public override void Sort()
{
CharacterPrefab.Prefabs.SortAll();
}
public override void Preload(Action<Sprite> addPreloadedSprite)
{
#if CLIENT
CharacterPrefab characterPrefab = CharacterPrefab.FindByFilePath(Path.Value);
if (characterPrefab?.ConfigElement == null)
{
throw new Exception($"Failed to load the character config file from {Path}!");
}
var mainElement = characterPrefab.ConfigElement;
mainElement.GetChildElements("sound").ForEach(e => RoundSound.Load(e));
if (!CharacterPrefab.CheckSpeciesName(mainElement, this, out Identifier speciesName)) { return; }
bool humanoid = mainElement.GetAttributeBool("humanoid", false);
RagdollParams ragdollParams;
try
{
if (humanoid)
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(speciesName);
}
else
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName);
}
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e);
return;
}
if (ragdollParams != null)
{
HashSet<string> texturePaths = new HashSet<string>
{
ragdollParams.Texture
};
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
{
if (!string.IsNullOrEmpty(limb.normalSpriteParams?.Texture)) { texturePaths.Add(limb.normalSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.deformSpriteParams?.Texture)) { texturePaths.Add(limb.deformSpriteParams.Texture); }
if (!string.IsNullOrEmpty(limb.damagedSpriteParams?.Texture)) { texturePaths.Add(limb.damagedSpriteParams.Texture); }
foreach (var decorativeSprite in limb.decorativeSpriteParams)
{
if (!string.IsNullOrEmpty(decorativeSprite.Texture)) { texturePaths.Add(decorativeSprite.Texture); }
}
}
foreach (string texturePath in texturePaths)
{
addPreloadedSprite(new Sprite(texturePath, Vector2.Zero));
}
}
#endif
}
}
}
@@ -0,0 +1,117 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class NotSyncedInMultiplayer : Attribute { }
/// <summary>
/// Base class for content file types, which are loaded
/// from filelist.xml via reflection.
/// PLEASE AVOID INHERITING FROM THIS CLASS DIRECTLY.
/// Inheriting from GenericPrefabFile&lt;T&gt; is likely what
/// you want.
/// </summary>
public abstract class ContentFile
{
public class TypeInfo
{
public readonly Type Type;
public readonly bool RequiredByCorePackage;
public readonly bool NotSyncedInMultiplayer;
public readonly ImmutableHashSet<Type>? AlternativeTypes;
public readonly ImmutableHashSet<Identifier> Names;
public TypeInfo(Type type)
{
Type = type;
var reqByCoreAttribute = type.GetCustomAttribute<RequiredByCorePackage>();
RequiredByCorePackage = reqByCoreAttribute != null;
var notSyncedInMultiplayerAttribute = type.GetCustomAttribute<NotSyncedInMultiplayer>();
NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null;
AlternativeTypes = reqByCoreAttribute?.AlternativeTypes;
HashSet<Identifier> names = new HashSet<Identifier> { type.Name.RemoveFromEnd("File").ToIdentifier() };
if (type.GetCustomAttribute<AlternativeContentTypeNames>()?.Names is { } altNames)
{
names.UnionWith(altNames);
}
Names = names.ToImmutableHashSet();
}
public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) =>
(ContentFile?)Activator.CreateInstance(Type, contentPackage, path);
}
public readonly static ImmutableHashSet<TypeInfo> Types;
static ContentFile()
{
Types = ReflectionUtils.GetDerivedNonAbstract<ContentFile>()
.Select(t => new TypeInfo(t))
.ToImmutableHashSet();
}
public static Result<ContentFile, string> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
Result<ContentFile, string> fail(string error)
=> Result<ContentFile, string>.Failure(error);
Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
var filePath = element.GetAttributeContentPath("file", contentPackage);
if (type is null)
{
return fail($"Invalid content type \"{elemName}\"");
}
if (filePath is null)
{
return fail($"No content path defined for file of type \"{elemName}\"");
}
try
{
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, string>.Success(file);
}
catch (Exception e)
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}\n{e.StackTrace.CleanupStackTrace()}");
}
}
protected ContentFile(ContentPackage contentPackage, ContentPath path)
{
ContentPackage = contentPackage;
Path = path;
Hash = CalculateHash();
}
public readonly ContentPackage ContentPackage;
public readonly ContentPath Path;
public readonly Md5Hash Hash;
public abstract void LoadFile();
public abstract void UnloadFile();
public abstract void Sort();
public virtual void Preload(Action<Sprite> addPreloadedSprite) { }
public virtual Md5Hash CalculateHash()
{
return Md5Hash.CalculateForFile(Path.Value, Md5Hash.StringHashOptions.IgnoreWhitespace);
}
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class CorpsesFile : GenericPrefabFile<CorpsePrefab>
{
public CorpsesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse";
protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses";
protected override PrefabCollection<CorpsePrefab> prefabs => CorpsePrefab.Prefabs;
protected override CorpsePrefab CreatePrefab(ContentXElement element)
{
return new CorpsePrefab(element, this);
}
}
}
@@ -0,0 +1,22 @@
namespace Barotrauma
{
public sealed class DecalsFile : ContentFile
{
public DecalsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
DecalManager.LoadFromFile(this);
}
public override void UnloadFile()
{
DecalManager.RemoveByFile(this);
}
public override void Sort()
{
DecalManager.SortAll();
}
}
}
@@ -0,0 +1,8 @@
namespace Barotrauma
{
[RequiredByCorePackage]
public class EnemySubmarineFile : BaseSubFile
{
public EnemySubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma
{
sealed class EventManagerSettingsFile : GenericPrefabFile<EventManagerSettings>
{
public EventManagerSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings";
protected override PrefabCollection<EventManagerSettings> prefabs => EventManagerSettings.Prefabs;
protected override EventManagerSettings CreatePrefab(ContentXElement element)
{
return new EventManagerSettings(element, this);
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class FactionsFile : GenericPrefabFile<FactionPrefab>
{
public FactionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "faction";
protected override bool MatchesPlural(Identifier identifier) => identifier == "factions";
protected override PrefabCollection<FactionPrefab> prefabs => FactionPrefab.Prefabs;
protected override FactionPrefab CreatePrefab(ContentXElement element)
{
return new FactionPrefab(element, this);
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
abstract class GenericPrefabFile<T> : ContentFile where T : Prefab
{
protected GenericPrefabFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected abstract bool MatchesSingular(Identifier identifier);
protected abstract bool MatchesPlural(Identifier identifier);
protected abstract PrefabCollection<T> prefabs { get; }
protected abstract T CreatePrefab(ContentXElement element);
private void LoadFromXElement(ContentXElement parentElement, bool overriding)
{
Identifier elemName = parentElement.NameAsIdentifier();
var childElements = parentElement.Elements()
#if DEBUG
.OrderBy(e => Rand.Int(int.MaxValue, Rand.RandSync.Unsynced)).ToArray()
#endif
;
if (parentElement.IsOverride())
{
foreach (var element in childElements)
{
LoadFromXElement(element, true);
}
}
else if (elemName == "clear")
{
prefabs.AddOverrideFile(this);
}
else if (MatchesSingular(elemName))
{
T prefab = CreatePrefab(parentElement);
prefabs.Add(prefab, overriding);
}
else if (MatchesPlural(elemName))
{
foreach (var element in childElements)
{
LoadFromXElement(element, overriding);
}
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
public override sealed void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var rootElement = doc.Root.FromPackage(ContentPackage);
LoadFromXElement(rootElement, false);
}
public override sealed void UnloadFile()
{
prefabs.RemoveByFile(this);
}
public sealed override void Sort()
{
prefabs.SortAll();
}
}
}
@@ -0,0 +1,10 @@
namespace Barotrauma
{
[NotSyncedInMultiplayer]
public abstract class HashlessFile : ContentFile
{
public HashlessFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public sealed override Md5Hash CalculateHash() => Md5Hash.Blank;
}
}
@@ -0,0 +1,17 @@
using System.Xml.Linq;
namespace Barotrauma
{
sealed class ItemAssemblyFile : GenericPrefabFile<ItemAssemblyPrefab>
{
public ItemAssemblyFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly";
protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies";
protected override PrefabCollection<ItemAssemblyPrefab> prefabs => ItemAssemblyPrefab.Prefabs;
protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element)
{
return new ItemAssemblyPrefab(element, this);
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class ItemFile : GenericPrefabFile<ItemPrefab>
{
public ItemFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "items";
protected override PrefabCollection<ItemPrefab> prefabs => ItemPrefab.Prefabs;
protected override ItemPrefab CreatePrefab(ContentXElement element)
{
return new ItemPrefab(element, this);
}
}
}
@@ -0,0 +1,60 @@
using Barotrauma;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class JobsFile : ContentFile
{
public JobsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
LoadElements(doc.Root.FromPackage(ContentPackage), false);
}
private void LoadElements(ContentXElement mainElement, bool isOverride)
{
foreach (var element in mainElement.Elements())
{
if (element.NameAsIdentifier() == "nojob")
{
JobPrefab.NoJobElement ??= element;
}
else if (element.NameAsIdentifier() == "ItemRepairPriorities")
{
foreach (var subElement in element.Elements())
{
ItemRepairPriority prio = new ItemRepairPriority(subElement, this);
ItemRepairPriority.Prefabs.Add(prio, isOverride);
}
}
else if (element.IsOverride())
{
LoadElements(element, true);
}
else
{
var job = new JobPrefab(element, this);
JobPrefab.Prefabs.Add(job, isOverride);
}
}
}
public override void UnloadFile()
{
JobPrefab.Prefabs.RemoveByFile(this);
ItemRepairPriority.Prefabs.RemoveByFile(this);
}
public override void Sort()
{
JobPrefab.Prefabs.SortAll();
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
sealed class LevelGenerationParametersFile : ContentFile
{
public LevelGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
private void LoadBiomes(ContentXElement element, bool isOverride)
{
foreach (var subElement in element.Elements())
{
Biome biome = new Biome(subElement, this);
Biome.Prefabs.Add(biome, isOverride);
}
}
private void LoadLevelGenerationParams(ContentXElement element, bool isOverride)
{
LevelGenerationParams lParams = new LevelGenerationParams(element, this);
LevelGenerationParams.LevelParams.Add(lParams, isOverride);
}
private void LoadSubElements(ContentXElement element, bool overridePropagation)
{
foreach (var subElement in element.Elements())
{
if (subElement.IsOverride())
{
LoadSubElements(subElement, true);
}
else if (subElement.NameAsIdentifier() == "clear")
{
LevelGenerationParams.LevelParams.AddOverrideFile(this);
Biome.Prefabs.AddOverrideFile(this);
}
else if (subElement.NameAsIdentifier() == "biomes")
{
LoadBiomes(subElement, overridePropagation);
}
else
{
LoadLevelGenerationParams(subElement, overridePropagation);
}
}
}
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc is null) { return; }
LoadSubElements(doc.Root.FromPackage(ContentPackage), false);
}
public override void UnloadFile()
{
LevelGenerationParams.LevelParams.RemoveByFile(this);
Biome.Prefabs.RemoveByFile(this);
}
public override void Sort()
{
LevelGenerationParams.LevelParams.SortAll();
Biome.Prefabs.SortAll();
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class LevelObjectPrefabsFile : GenericPrefabFile<LevelObjectPrefab>
{
public LevelObjectPrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects";
protected override PrefabCollection<LevelObjectPrefab> prefabs => LevelObjectPrefab.Prefabs;
protected override LevelObjectPrefab CreatePrefab(ContentXElement element)
{
return new LevelObjectPrefab(element, this);
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class LocationTypesFile : GenericPrefabFile<LocationType>
{
public LocationTypesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes";
protected override PrefabCollection<LocationType> prefabs => LocationType.Prefabs;
protected override LocationType CreatePrefab(ContentXElement element)
{
return new LocationType(element, this);
}
}
}
@@ -0,0 +1,34 @@
using System.Xml.Linq;
namespace Barotrauma
{
sealed class MapGenerationParametersFile : ContentFile
{
public MapGenerationParametersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
DebugConsole.ThrowError($"Loading map generation parameters file failed: {Path}");
return;
}
var mainElement = doc.Root.FromPackage(ContentPackage);
bool isOverride = mainElement.IsOverride();
if (isOverride) { mainElement = mainElement.FirstElement(); }
var prefab = new MapGenerationParams(mainElement, this);
MapGenerationParams.Params.Add(prefab, isOverride);
}
public override void UnloadFile()
{
MapGenerationParams.Params.RemoveByFile(this);
}
public override void Sort()
{
MapGenerationParams.Params.Sort();
}
}
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class MissionsFile : GenericPrefabFile<MissionPrefab>
{
/*private readonly static ImmutableHashSet<Type> missionTypes;
static MissionsFile()
{
missionTypes = ReflectionUtils.GetDerivedNonAbstract<Mission>()
.ToImmutableHashSet();
}*/
public MissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier)
=> !MatchesPlural(identifier);
/*missionTypes.Any(t => identifier == t.Name)
|| identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/
protected override bool MatchesPlural(Identifier identifier) => identifier == "missions";
protected override PrefabCollection<MissionPrefab> prefabs => MissionPrefab.Prefabs;
protected override MissionPrefab CreatePrefab(ContentXElement element)
{
return new MissionPrefab(element, this);
}
}
}
@@ -0,0 +1,44 @@
using System.Xml.Linq;
namespace Barotrauma
{
sealed class NPCConversationsFile : ContentFile
{
public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var mainElement = doc.Root.FromPackage(ContentPackage);
bool allowOverriding = doc.Root.IsOverride();
if (allowOverriding)
{
mainElement = mainElement.FirstElement();
}
var npcConversationCollection = new NPCConversationCollection(this, mainElement);
if (!NPCConversationCollection.Collections.ContainsKey(npcConversationCollection.Language))
{
NPCConversationCollection.Collections.Add(npcConversationCollection.Language, new PrefabCollection<NPCConversationCollection>());
}
NPCConversationCollection.Collections[npcConversationCollection.Language].Add(npcConversationCollection, allowOverriding);
}
public override void UnloadFile()
{
foreach (var collection in NPCConversationCollection.Collections.Values)
{
collection.RemoveByFile(this);
}
}
public override void Sort()
{
foreach (var collection in NPCConversationCollection.Collections.Values)
{
collection.SortAll();
}
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class NPCSetsFile : GenericPrefabFile<NPCSet>
{
public NPCSetsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset";
protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets";
protected override PrefabCollection<NPCSet> prefabs => NPCSet.Sets;
protected override NPCSet CreatePrefab(ContentXElement element)
{
return new NPCSet(element, this);
}
}
}
@@ -0,0 +1,70 @@
using System.Xml.Linq;
namespace Barotrauma
{
#warning TODO: this is almost a GenericPrefabFile. Must refactor further.
[RequiredByCorePackage]
sealed class OrdersFile : ContentFile
{
public OrdersFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public void LoadFromXElement(ContentXElement parentElement, bool overriding)
{
Identifier elemName = new Identifier(parentElement.Name.ToString());
if (parentElement.IsOverride())
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, true);
}
}
else if (elemName == "order")
{
OrderPrefab prefab = new OrderPrefab(parentElement, this);
OrderPrefab.Prefabs.Add(prefab, overriding);
}
else if (elemName == "ordercategory")
{
OrderCategoryIcon prefab = new OrderCategoryIcon(parentElement, this);
OrderCategoryIcon.OrderCategoryIcons.Add(prefab, overriding);
}
else if (elemName == "orders")
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, overriding);
}
}
else if (elemName == "clear")
{
OrderCategoryIcon.OrderCategoryIcons.AddOverrideFile(this);
OrderPrefab.Prefabs.AddOverrideFile(this);
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
public override sealed void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var rootElement = doc.Root.FromPackage(ContentPackage);
LoadFromXElement(rootElement, false);
}
public override sealed void UnloadFile()
{
OrderCategoryIcon.OrderCategoryIcons.RemoveByFile(this);
OrderPrefab.Prefabs.RemoveByFile(this);
}
public override sealed void Sort()
{
OrderCategoryIcon.OrderCategoryIcons.SortAll();
OrderPrefab.Prefabs.SortAll();
}
}
}
@@ -0,0 +1,16 @@
using Barotrauma;
namespace Barotrauma
{
[AlternativeContentTypeNames("None")]
public class OtherFile : HashlessFile
{
public OtherFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
//this content type is completely ignored by the game so LoadFile and UnloadFile don't do anything
public sealed override void LoadFile() { }
public sealed override void UnloadFile() { }
public sealed override void Sort() { }
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage(alternativeTypes: typeof(OutpostFile))]
sealed class OutpostConfigFile : GenericPrefabFile<OutpostGenerationParams>
{
public OutpostConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters";
protected override PrefabCollection<OutpostGenerationParams> prefabs => OutpostGenerationParams.OutpostParams;
protected override OutpostGenerationParams CreatePrefab(ContentXElement element)
{
return new OutpostGenerationParams(element, this);
}
}
}
@@ -0,0 +1,7 @@
namespace Barotrauma
{
public class OutpostFile : BaseSubFile
{
public OutpostFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,7 @@
namespace Barotrauma
{
public class OutpostModuleFile : BaseSubFile
{
public OutpostModuleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,31 @@
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Particles;
#endif
namespace Barotrauma
{
[RequiredByCorePackage]
[NotSyncedInMultiplayer]
#if CLIENT
sealed class ParticlesFile : GenericPrefabFile<ParticlePrefab>
{
public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles";
protected override PrefabCollection<ParticlePrefab> prefabs => ParticlePrefab.Prefabs;
protected override ParticlePrefab CreatePrefab(ContentXElement element)
{
return new ParticlePrefab(element, this);
}
public override Md5Hash CalculateHash() => Md5Hash.Blank;
}
#else
sealed class ParticlesFile : OtherFile
{
public ParticlesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { } //this content type doesn't do anything on a server
}
#endif
}
@@ -0,0 +1,91 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class RandomEventsFile : ContentFile
{
public RandomEventsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public void LoadFromXElement(ContentXElement parentElement, bool overriding)
{
Identifier elemName = new Identifier(parentElement.Name.ToString());
if (parentElement.IsOverride())
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, true);
}
}
else if (elemName == "randomevents")
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, overriding);
}
}
else if (elemName == "eventprefabs")
{
foreach (var subElement in parentElement.Elements())
{
var prefab = new EventPrefab(subElement, this);
EventPrefab.Prefabs.Add(prefab, overriding);
}
}
else if (elemName == "eventsprites")
{
#if CLIENT
foreach (var subElement in parentElement.Elements())
{
var prefab = new EventSprite(subElement, this);
EventSprite.Prefabs.Add(prefab, overriding);
}
#endif
}
else if (elemName == "eventset")
{
var prefab = new EventSet(parentElement, this);
EventSet.Prefabs.Add(prefab, overriding);
}
else if (elemName == "clear")
{
EventPrefab.Prefabs.AddOverrideFile(this);
EventSet.Prefabs.AddOverrideFile(this);
#if CLIENT
EventSprite.Prefabs.AddOverrideFile(this);
#endif
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var rootElement = doc.Root.FromPackage(ContentPackage);
LoadFromXElement(rootElement, false);
}
public override void UnloadFile()
{
EventPrefab.Prefabs.RemoveByFile(this);
EventSet.Prefabs.RemoveByFile(this);
#if CLIENT
EventSprite.Prefabs.RemoveByFile(this);
#endif
}
public override void Sort()
{
EventPrefab.Prefabs.SortAll();
EventSet.Prefabs.SortAll();
#if CLIENT
EventSprite.Prefabs.SortAll();
#endif
}
}
}
@@ -0,0 +1,19 @@
using Barotrauma.RuinGeneration;
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class RuinConfigFile : GenericPrefabFile<RuinGenerationParams>
{
public RuinConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters";
protected override PrefabCollection<RuinGenerationParams> prefabs => RuinGenerationParams.RuinParams;
protected override RuinGenerationParams CreatePrefab(ContentXElement element)
{
return new RuinGenerationParams(element, this);
}
}
}
@@ -0,0 +1,33 @@
using System.Xml.Linq;
namespace Barotrauma
{
sealed class SkillSettingsFile : ContentFile
{
public SkillSettingsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var mainElement = doc.Root.FromPackage(ContentPackage);
bool allowOverriding = mainElement.IsOverride();
if (allowOverriding)
{
mainElement = mainElement.FirstElement();
}
var prefab = new SkillSettings(mainElement, this);
SkillSettings.Prefabs.Add(prefab, allowOverriding);
}
public override void UnloadFile()
{
SkillSettings.Prefabs.RemoveByFile(this);
}
public override void Sort()
{
SkillSettings.Prefabs.Sort();
}
}
}
@@ -0,0 +1,38 @@
using System;
using System.Collections.Immutable;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
#if CLIENT
sealed class SoundsFile : GenericPrefabFile<SoundPrefab>
{
public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override PrefabCollection<SoundPrefab> prefabs => SoundPrefab.Prefabs;
protected override SoundPrefab CreatePrefab(ContentXElement element)
{
var elemName = element.NameAsIdentifier();
if (SoundPrefab.TagToDerivedPrefab.ContainsKey(elemName))
{
return Activator.CreateInstance(SoundPrefab.TagToDerivedPrefab[elemName], new object[] { element, this }) as SoundPrefab;
}
return new SoundPrefab(element, this);
}
protected override bool MatchesPlural(Identifier identifier) => identifier == "sounds";
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
public override Md5Hash CalculateHash() => Md5Hash.Blank;
}
#else
sealed class SoundsFile : OtherFile
{
public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
#endif
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class StructureFile : GenericPrefabFile<StructurePrefab>
{
public StructureFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures";
protected override PrefabCollection<StructurePrefab> prefabs => StructurePrefab.Prefabs;
protected override StructurePrefab CreatePrefab(ContentXElement element)
{
return new StructurePrefab(element, this);
}
}
}
@@ -0,0 +1,38 @@
using System;
using System.Security.Cryptography;
namespace Barotrauma
{
public abstract class BaseSubFile : ContentFile
{
protected BaseSubFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path)
{
using var md5 = MD5.Create();
#warning TODO: this doesn't account for collisions, this should probably be using the PrefabCollection class like everything else
UintIdentifier = ToolBox.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5);
}
public readonly UInt32 UintIdentifier;
public override void LoadFile()
{
SubmarineInfo.RefreshSavedSub(Path.Value);
}
public override void UnloadFile()
{
SubmarineInfo.RefreshSavedSub(Path.Value);
}
public override void Sort()
{
//Overrides for subs don't exist! Should we change this?
}
}
[NotSyncedInMultiplayer]
public class SubmarineFile : BaseSubFile
{
public SubmarineFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class TalentTreesFile : GenericPrefabFile<TalentTree>
{
public TalentTreesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees";
protected override PrefabCollection<TalentTree> prefabs => TalentTree.JobTalentTrees;
protected override TalentTree CreatePrefab(ContentXElement element)
{
return new TalentTree(element, this);
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class TalentsFile : GenericPrefabFile<TalentPrefab>
{
public TalentsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "talent";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talents";
protected override PrefabCollection<TalentPrefab> prefabs => TalentPrefab.TalentPrefabs;
protected override TalentPrefab CreatePrefab(ContentXElement element)
{
return new TalentPrefab(element, this);
}
}
}
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
public sealed class TextFile : ContentFile
{
public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public override void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
var mainElement = doc.Root.FromPackage(ContentPackage);
var languageName = mainElement.GetAttributeIdentifier("language", TextManager.DefaultLanguage.Value);
LanguageIdentifier language = languageName.ToLanguageIdentifier();
if (!TextManager.TextPacks.ContainsKey(language))
{
TextManager.TextPacks.TryAdd(language, ImmutableHashSet<TextPack>.Empty);
}
var newPack = new TextPack(this, mainElement, language);
var newHashSet = TextManager.TextPacks[language].Add(newPack);
TextManager.TextPacks.TryRemove(language, out _);
TextManager.TextPacks.TryAdd(language, newHashSet);
TextManager.IncrementLanguageVersion();
}
public override void UnloadFile()
{
foreach (var kvp in TextManager.TextPacks.ToArray())
{
var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet();
TextManager.TextPacks.TryRemove(kvp.Key, out _);
if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); }
}
TextManager.IncrementLanguageVersion();
}
public override void Sort()
{
//Overrides for text packs don't exist! Should we change this?
}
}
}
@@ -0,0 +1,26 @@
using System.Xml.Linq;
#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing.
#if CLIENT
using PrefabType = Barotrauma.TraitorMissionPrefab;
#elif SERVER
using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry;
#endif
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class TraitorMissionsFile : GenericPrefabFile<PrefabType>
{
public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission";
protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions";
protected override PrefabCollection<PrefabType> prefabs => PrefabType.Prefabs;
protected override PrefabType CreatePrefab(ContentXElement element)
{
return new PrefabType(element, this);
}
}
}
@@ -0,0 +1,100 @@
using System;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
#if CLIENT
public sealed class UIStyleFile : HashlessFile
{
public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
public void LoadFromXElement(ContentXElement parentElement, bool overriding)
{
Identifier elemName = parentElement.NameAsIdentifier();
Identifier elemNameWithFontSuffix = elemName.AppendIfMissing("Font");
if (parentElement.IsOverride())
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, true);
}
}
else if (GUIStyle.Fonts.ContainsKey(elemNameWithFontSuffix))
{
GUIFontPrefab prefab = new GUIFontPrefab(parentElement, this);
GUIStyle.Fonts[elemNameWithFontSuffix].Prefabs.Add(prefab, overriding);
}
else if (GUIStyle.Sprites.ContainsKey(elemName))
{
GUISpritePrefab prefab = new GUISpritePrefab(parentElement, this);
GUIStyle.Sprites[elemName].Prefabs.Add(prefab, overriding);
}
else if (GUIStyle.SpriteSheets.ContainsKey(elemName))
{
GUISpriteSheetPrefab prefab = new GUISpriteSheetPrefab(parentElement, this);
GUIStyle.SpriteSheets[elemName].Prefabs.Add(prefab, overriding);
}
else if (GUIStyle.Colors.ContainsKey(elemName))
{
GUIColorPrefab prefab = new GUIColorPrefab(parentElement, this);
GUIStyle.Colors[elemName].Prefabs.Add(prefab, overriding);
}
else if (elemName == "cursor")
{
GUICursorPrefab prefab = new GUICursorPrefab(parentElement, this);
GUIStyle.CursorSprite.Prefabs.Add(prefab, overriding);
}
else if (elemName == "style")
{
foreach (var element in parentElement.Elements())
{
LoadFromXElement(element, overriding);
}
}
else
{
GUIComponentStyle prefab = new GUIComponentStyle(parentElement, this);
GUIStyle.ComponentStyles.Add(prefab, overriding);
}
}
public override sealed void LoadFile()
{
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return; }
var rootElement = doc.Root.FromPackage(ContentPackage);
LoadFromXElement(rootElement, false);
}
public override sealed void UnloadFile()
{
GUIStyle.ComponentStyles.RemoveByFile(this);
GUIStyle.CursorSprite.Prefabs.RemoveByFile(this);
GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.RemoveByFile(this));
GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.RemoveByFile(this));
GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.RemoveByFile(this));
GUIStyle.Colors.Values.ForEach(p => p.Prefabs.RemoveByFile(this));
}
public override sealed void Sort()
{
GUIStyle.ComponentStyles.SortAll();
GUIStyle.CursorSprite.Prefabs.Sort();
GUIStyle.Fonts.Values.ForEach(p => p.Prefabs.Sort());
GUIStyle.Sprites.Values.ForEach(p => p.Prefabs.Sort());
GUIStyle.SpriteSheets.Values.ForEach(p => p.Prefabs.Sort());
GUIStyle.Colors.Values.ForEach(p => p.Prefabs.Sort());
}
}
#else
public sealed class UIStyleFile : OtherFile
{
public UIStyleFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
#endif
}
@@ -0,0 +1,31 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class UpgradeModulesFile : GenericPrefabFile<UpgradeContentPrefab>
{
public UpgradeModulesFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) =>
identifier == "upgrademodule" ||
identifier == "upgradecategory";
protected override bool MatchesPlural(Identifier identifier) =>
identifier == "upgrademodules";
protected override PrefabCollection<UpgradeContentPrefab> prefabs => UpgradeContentPrefab.PrefabsAndCategories;
protected override UpgradeContentPrefab CreatePrefab(ContentXElement element)
{
Identifier elemName = element.NameAsIdentifier();
if (elemName == "upgradecategory")
{
return new UpgradeCategory(element, this);
}
else
{
return new UpgradePrefab(element, this);
}
}
}
}
@@ -0,0 +1,18 @@
using System.Xml.Linq;
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class WreckAIConfigFile : GenericPrefabFile<WreckAIConfig>
{
public WreckAIConfigFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs";
protected override PrefabCollection<WreckAIConfig> prefabs => WreckAIConfig.Prefabs;
protected override WreckAIConfig CreatePrefab(ContentXElement element)
{
return new WreckAIConfig(element, this);
}
}
}
@@ -0,0 +1,7 @@
namespace Barotrauma
{
public class WreckFile : BaseSubFile
{
public WreckFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
}
}
@@ -0,0 +1,306 @@
#nullable enable
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Steam;
namespace Barotrauma
{
public abstract class ContentPackage
{
#warning TODO: make this independent of the current version
public static readonly Version MinimumHashCompatibleVersion = GameMain.Version;
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
SaveUtil.SaveFolder,
"WorkshopMods",
"Installed");
public const string FileListFileName = "filelist.xml";
public const string DefaultModVersion = "1.0.0";
public readonly string Name;
public readonly ImmutableArray<string> AltNames;
public readonly string Path;
public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? "";
public readonly UInt64 SteamWorkshopId;
public readonly Version GameVersion;
public readonly string ModVersion;
public readonly Md5Hash Hash;
public readonly DateTime? InstallTime;
public readonly ImmutableArray<ContentFile> Files;
public readonly ImmutableArray<string> Errors;
public async Task<bool> IsUpToDate()
{
if (SteamWorkshopId != 0 && InstallTime.HasValue)
{
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= InstallTime;
}
return true;
}
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
#warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do
public readonly bool HasMultiplayerIncompatibleContent;
protected ContentPackage(XDocument doc, string path)
{
Path = path.CleanUpPathCrossPlatform();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
Name = rootElement.GetAttributeString("name", "").Trim();
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
if (rootElement.Attribute("installtime") != null)
{
InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0));
}
else
{
InstallTime = null;
}
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
Files = fileResults
.OfType<Success<ContentFile, string>>()
.Select(f => f.Value)
.ToImmutableArray();
Errors = fileResults
.OfType<Failure<ContentFile, string>>()
.Select(f => f.Error)
.ToImmutableArray();
HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer);
Hash = CalculateHash();
var expectedHash = rootElement.GetAttributeString("expectedhash", "");
if (HashMismatches(expectedHash))
{
DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})");
}
}
public bool HashMismatches(string expectedHash)
=> GameVersion >= MinimumHashCompatibleVersion &&
!expectedHash.IsNullOrWhiteSpace() &&
!expectedHash.Equals(Hash.StringRepresentation, StringComparison.OrdinalIgnoreCase);
public IEnumerable<T> GetFiles<T>() where T : ContentFile => Files.Where(f => f is T).Cast<T>();
public IEnumerable<ContentFile> GetFiles(Type type)
=> !type.IsSubclassOf(typeof(ContentFile))
? throw new ArgumentException($"Type must be subclass of ContentFile, got {type.Name}")
: Files.Where(f => f.GetType() == type || f.GetType().IsSubclassOf(type));
public bool NameMatches(Identifier name)
=> Name == name || AltNames.Any(n => n == name);
public bool NameMatches(string name)
=> NameMatches(name.ToIdentifier());
public static ContentPackage? TryLoad(string path)
{
XDocument doc = XMLExtensions.TryLoadXml(path);
try
{
if (doc.Root.GetAttributeBool("corepackage", false))
{
return new CorePackage(doc, path);
}
else
{
return new RegularPackage(doc, path);
}
}
catch (Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
}
}
public Md5Hash CalculateHash(bool logging = false)
{
using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
if (logging)
{
DebugConsole.NewMessage("****************************** Calculating content package hash " + Name);
}
foreach (ContentFile file in Files)
{
try
{
var hash = file.Hash;
if (logging)
{
DebugConsole.NewMessage(" " + file.Path + ": " + hash.StringRepresentation);
}
incrementalHash.AppendData(hash.ByteRepresentation);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e);
break;
}
}
var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset());
if (logging)
{
DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.StringRepresentation);
}
return md5Hash;
}
protected void AssertCondition(bool condition, string errorMsg)
{
if (!condition)
{
throw new InvalidOperationException($"Failed to load \"{Name ?? Path}\": {errorMsg}");
}
}
public void LoadFilesOfType<T>() where T : ContentFile
{
Files.Where(f => f is T).ForEach(f => f.LoadFile());
}
public void UnloadFilesOfType<T>() where T : ContentFile
{
Files.Where(f => f is T).ForEach(f => f.UnloadFile());
}
public enum LoadResult
{
Success,
Failure
}
public LoadResult LoadPackage()
{
foreach (var p in LoadPackageEnumerable())
{
if (p.Exception != null) { return LoadResult.Failure; }
}
return LoadResult.Success;
}
public IEnumerable<ContentPackageManager.LoadProgress> LoadPackageEnumerable()
{
ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
=> Files.Where(predicate.Invoke).ToArray()
#if DEBUG
//The game should be able to work just fine with a completely arbitrary file load order.
//To make sure we don't mess this up, debug builds randomize it so it has a higher chance
//of breaking anything that's not implemented correctly.
.Randomize()
#endif
;
IEnumerable<ContentPackageManager.LoadProgress> loadFiles(ContentFile[] filesToLoad, int indexOffset)
{
for (int i = 0; i < filesToLoad.Length; i++)
{
Exception? exception = null;
try
{
//do not allow exceptions thrown here to crash the game
filesToLoad[i].LoadFile();
}
catch (Exception e)
{
exception = e;
}
if (exception != null)
{
yield return ContentPackageManager.LoadProgress.Failure(exception);
break;
}
yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length);
}
}
//Load the UI files first. This is to allow the game to render
//the text in the loading screen as soon as possible.
var priorityFiles = getFilesToLoad(f => f is UIStyleFile);
var remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
var loadEnumerable =
loadFiles(priorityFiles, 0)
.Concat(loadFiles(remainder, priorityFiles.Length));
foreach (var p in loadEnumerable)
{
if (p.Exception != null)
{
HandleLoadException(p.Exception);
yield return p;
break;
}
yield return p;
}
}
protected abstract void HandleLoadException(Exception e);
public void UnloadPackage()
{
Files.ForEach(f => f.UnloadFile());
}
public override int GetHashCode()
{
byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4));
return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3];
}
public static bool PathAllowedAsLocalModFile(string path)
{
#if DEBUG
if (GameMain.VanillaContent.Files.Any(f => f.Path == path))
{
//file is in vanilla package, this is allowed
return true;
}
#endif
while (true)
{
string temp = Barotrauma.IO.Path.GetDirectoryName(path) ?? "";
if (string.IsNullOrEmpty(temp)) { break; }
path = temp;
}
return path == LocalModsDir;
}
}
}
@@ -0,0 +1,52 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[AttributeUsage(AttributeTargets.Class)]
public class RequiredByCorePackage : Attribute
{
public readonly ImmutableHashSet<Type> AlternativeTypes;
public RequiredByCorePackage(params Type[] alternativeTypes)
{
AlternativeTypes = alternativeTypes.ToImmutableHashSet();
}
}
[AttributeUsage(AttributeTargets.Class)]
public class AlternativeContentTypeNames : Attribute
{
public readonly ImmutableHashSet<Identifier> Names;
public AlternativeContentTypeNames(params string[] names)
{
Names = names.ToIdentifiers().ToImmutableHashSet();
}
}
public class CorePackage : ContentPackage
{
public CorePackage(XDocument doc, string path) : base(doc, path)
{
AssertCondition(doc.Root.GetAttributeBool("corepackage", false),
"Expected a core package, got a regular package");
var missingFileTypes = ContentFile.Types.Where(
t => t.RequiredByCorePackage
&& !Files.Any(f => t.Type == f.GetType()
|| t.AlternativeTypes.Contains(f.GetType())));
AssertCondition(!missingFileTypes.Any(),
"Core package requires at least one of the following content types: " +
string.Join(", ", missingFileTypes.Select(t => t.Type.Name)));
}
protected override void HandleLoadException(Exception e)
{
throw new Exception($"An exception was thrown while loading \"{Name}\"", e);
}
}
}
@@ -0,0 +1,19 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
{
public class RegularPackage : ContentPackage
{
public RegularPackage(XDocument doc, string path) : base(doc, path)
{
AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package");
}
protected override void HandleLoadException(Exception e)
{
UnloadPackage();
DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e);
}
}
}
@@ -0,0 +1,461 @@
#nullable enable
using Barotrauma.Extensions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
public static partial class ContentPackageManager
{
public const string CopyIndicatorFileName = ".copying";
public const string VanillaFileList = "Content/ContentPackages/Vanilla.xml";
public const string CorePackageElementName = "corepackage";
public const string RegularPackagesElementName = "regularpackages";
public const string RegularPackagesSubElementName = "package";
public static class EnabledPackages
{
public static CorePackage? Core { get; private set; } = null;
private static readonly List<RegularPackage> regular = new List<RegularPackage>();
public static IReadOnlyList<RegularPackage> Regular => regular;
public static IEnumerable<ContentPackage> All =>
Core != null
? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular)
: Enumerable.Empty<ContentPackage>();
private static class BackupPackages
{
public static CorePackage? Core;
public static ImmutableArray<RegularPackage>? Regular;
}
public static void SetCore(CorePackage newCore) => SetCoreEnumerable(newCore).Consume();
public static IEnumerable<LoadProgress> SetCoreEnumerable(CorePackage newCore)
{
var oldCore = Core;
if (newCore == oldCore) { yield break; }
Core?.UnloadPackage();
Core = newCore;
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; }
ThrowIfDuplicates(All);
yield return new LoadProgress(1.0f);
}
public static void ReloadCore()
{
if (Core == null) { return; }
Core.UnloadPackage();
Core.LoadPackage();
ThrowIfDuplicates(All);
}
public static void EnableRegular(RegularPackage p)
{
if (regular.Contains(p)) { return; }
var newRegular = regular.ToList();
newRegular.Add(p);
SetRegular(newRegular);
}
public static void SetRegular(IReadOnlyList<RegularPackage> newRegular)
=> SetRegularEnumerable(newRegular).Consume();
public static IEnumerable<LoadProgress> SetRegularEnumerable(IReadOnlyList<RegularPackage> inNewRegular)
{
if (ReferenceEquals(inNewRegular, regular)) { yield break; }
if (inNewRegular.SequenceEqual(regular)) { yield break; }
ThrowIfDuplicates(inNewRegular);
var newRegular = inNewRegular.ToList();
IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
toUnload.ForEach(r => r.UnloadPackage());
Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
for (int i = 0; i < toLoad.Length; i++)
{
var package = toLoad[i];
loadingRange = new Range<float>(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length);
foreach (var progress in package.LoadPackageEnumerable())
{
if (progress.Exception != null)
{
//If an exception was thrown while loading this package, refuse to add it to the list of enabled packages
newRegular.Remove(package);
break;
}
yield return progress.Transform(loadingRange);
}
}
regular.Clear(); regular.AddRange(newRegular);
SortContent();
yield return new LoadProgress(1.0f);
}
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
{
var contentPackages = pkgs as IList<ContentPackage> ?? pkgs.ToArray();
if (contentPackages.Any(p1 => contentPackages.AtLeast(2, p2 => p1 == p2)))
{
throw new InvalidOperationException($"Input contains duplicate packages");
}
}
private class TypeComparer<T> : IEqualityComparer<T>
{
public bool Equals([AllowNull] T x, [AllowNull] T y)
{
if (x is null || y is null)
{
return x is null == y is null;
}
return x.GetType() == y.GetType();
}
public int GetHashCode([DisallowNull] T obj)
{
return obj.GetType().GetHashCode();
}
}
public static void SortContent()
{
ThrowIfDuplicates(All);
All
.SelectMany(r => r.Files)
.Distinct(new TypeComparer<ContentFile>())
.ForEach(f => f.Sort());
}
public static int IndexOf(ContentPackage contentPackage)
{
if (contentPackage is CorePackage core)
{
if (core == Core) { return 0; }
return -1;
}
else if (contentPackage is RegularPackage reg)
{
return Regular.IndexOf(reg) + 1;
}
return -1;
}
public static void DisableRemovedMods()
{
if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
{
SetCore(ContentPackageManager.CorePackages.First());
}
SetRegular(Regular.Where(p => ContentPackageManager.RegularPackages.Contains(p)).ToArray());
}
public static void BackUp()
{
if (BackupPackages.Core != null || BackupPackages.Regular != null)
{
throw new InvalidOperationException("Tried to back up enabled packages multiple times");
}
BackupPackages.Core = Core;
BackupPackages.Regular = Regular.ToImmutableArray();
}
public static void Restore()
{
if (BackupPackages.Core == null || BackupPackages.Regular == null)
{
DebugConsole.AddWarning("Tried to restore enabled packages multiple times/without performing a backup");
return;
}
SetCore(BackupPackages.Core);
SetRegular(BackupPackages.Regular);
BackupPackages.Core = null;
BackupPackages.Regular = null;
}
}
public sealed partial class PackageSource : ICollection<ContentPackage>
{
private readonly Predicate<string>? skipPredicate;
public PackageSource(string dir, Predicate<string>? skipPredicate)
{
this.skipPredicate = skipPredicate;
directory = dir;
Directory.CreateDirectory(directory);
}
public void SwapPackage(ContentPackage oldPackage, ContentPackage newPackage)
{
bool contains = false;
if (oldPackage is CorePackage oldCore && corePackages.Contains(oldCore))
{
corePackages.Remove(oldCore);
contains = true;
}
else if (oldPackage is RegularPackage oldRegular && regularPackages.Contains(oldRegular))
{
regularPackages.Remove(oldRegular);
contains = true;
}
if (contains)
{
if (newPackage is CorePackage newCore)
{
corePackages.Add(newCore);
}
else if (newPackage is RegularPackage newRegular)
{
regularPackages.Add(newRegular);
}
}
}
public void Refresh()
{
//remove packages that have been deleted from the directory
corePackages.RemoveWhere(p => !File.Exists(p.Path));
regularPackages.RemoveWhere(p => !File.Exists(p.Path));
//load packages that have been added to the directory
var subDirs = Directory.GetDirectories(directory);
foreach (string subDir in subDirs)
{
var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; }
if (File.Exists(fileListPath))
{
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath);
if (newPackage is CorePackage corePackage)
{
corePackages.Add(corePackage);
}
else if (newPackage is RegularPackage regularPackage)
{
regularPackages.Add(regularPackage);
}
if (!(newPackage is null))
{
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
}
}
}
private readonly string directory;
private readonly HashSet<RegularPackage> regularPackages = new HashSet<RegularPackage>();
public IEnumerable<RegularPackage> Regular => regularPackages;
private readonly HashSet<CorePackage> corePackages = new HashSet<CorePackage>();
public IEnumerable<CorePackage> Core => corePackages;
public IEnumerator<ContentPackage> GetEnumerator()
{
foreach (var core in Core) { yield return core; }
foreach (var regular in Regular) { yield return regular; }
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
void ICollection<ContentPackage>.Add(ContentPackage item) => throw new InvalidOperationException();
void ICollection<ContentPackage>.Clear() => throw new InvalidOperationException();
public bool Contains(ContentPackage item)
=> item switch
{
CorePackage core => corePackages.Contains(core),
RegularPackage regular => this.regularPackages.Contains(regular),
_ => throw new ArgumentException($"Expected regular or core package, got {item.GetType().Name}")
};
void ICollection<ContentPackage>.CopyTo(ContentPackage[] array, int arrayIndex)
{
foreach (var package in corePackages)
{
array[arrayIndex] = package;
arrayIndex++;
}
foreach (var package in regularPackages)
{
array[arrayIndex] = package;
arrayIndex++;
}
}
bool ICollection<ContentPackage>.Remove(ContentPackage item) => throw new InvalidOperationException();
public int Count => corePackages.Count + regularPackages.Count;
public bool IsReadOnly => true;
}
public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null);
public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath);
public static CorePackage? VanillaCorePackage { get; private set; } = null;
public static IEnumerable<CorePackage> CorePackages
=> (VanillaCorePackage is null
? Enumerable.Empty<CorePackage>()
: VanillaCorePackage.ToEnumerable())
.CollectionConcat(LocalPackages.Core.CollectionConcat(WorkshopPackages.Core));
public static IEnumerable<RegularPackage> RegularPackages
=> LocalPackages.Regular.CollectionConcat(WorkshopPackages.Regular);
public static IEnumerable<ContentPackage> AllPackages
=> LocalPackages.CollectionConcat(WorkshopPackages);
public static void UpdateContentPackageList()
{
LocalPackages.Refresh();
WorkshopPackages.Refresh();
EnabledPackages.DisableRemovedMods();
}
public static ContentPackage? ReloadContentPackage(ContentPackage p)
{
ContentPackage? newPackage = ContentPackage.TryLoad(p.Path);
if (newPackage is CorePackage core)
{
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
}
else if (newPackage is RegularPackage regular)
{
int index = EnabledPackages.Regular.IndexOf(p);
if (index >= 0)
{
var newRegular = EnabledPackages.Regular.ToArray();
newRegular[index] = regular;
EnabledPackages.SetRegular(newRegular);
}
}
if (newPackage != null)
{
LocalPackages.SwapPackage(p, newPackage);
WorkshopPackages.SwapPackage(p, newPackage);
}
EnabledPackages.DisableRemovedMods();
return newPackage;
}
public readonly struct LoadProgress
{
public readonly float Value;
public readonly Exception? Exception;
public LoadProgress(float value)
{
Value = value;
Exception = null;
}
private LoadProgress(Exception exception)
{
Value = -1f;
Exception = exception;
}
public static LoadProgress Failure(Exception exception)
=> new LoadProgress(exception);
public LoadProgress Transform(Range<float> range)
=> Exception != null
? this
: new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value));
}
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
}
public static IEnumerable<LoadProgress> Init()
{
Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
SteamManager.Workshop.DeleteFailedCopies();
UpdateContentPackageList();
if (VanillaCorePackage is null) { LoadVanillaFileList(); }
CorePackage enabledCorePackage = VanillaCorePackage!;
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
#if CLIENT
TaskPool.Add("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { });
#else
#warning TODO: implement Workshop updates for servers at some point
#endif
var contentPackagesElement = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath)?.Root
?.GetChildElement("ContentPackages");
if (contentPackagesElement != null)
{
T? findPackage<T>(IEnumerable<T> packages, XElement? elem) where T : ContentPackage
{
if (elem is null) { return null; }
string name = elem.GetAttributeString("name", "");
string path = elem.GetAttributeStringUnrestricted("path", "").CleanUpPathCrossPlatform(correctFilenameCase: false);
return
packages.FirstOrDefault(p => p.Path.Equals(path, StringComparison.OrdinalIgnoreCase))
?? packages.FirstOrDefault(p => p.NameMatches(name));
}
var corePackageElement = contentPackagesElement.GetChildElement(CorePackageElementName);
enabledCorePackage = findPackage(CorePackages, corePackageElement) ?? VanillaCorePackage!;
var regularPackagesElement = contentPackagesElement.GetChildElement(RegularPackagesElementName);
if (regularPackagesElement != null)
{
XElement[] regularPackageElements = regularPackagesElement.GetChildElements(RegularPackagesSubElementName).ToArray();
for (int i = 0; i < regularPackageElements.Length; i++)
{
var regularPackage = findPackage(RegularPackages, regularPackageElements[i]);
if (regularPackage != null) { enabledRegularPackages.Add(regularPackage); }
}
}
}
int pkgCount = 1 + enabledRegularPackages.Count; //core + regular
loadingRange = new Range<float>(0.01f, 1.0f / pkgCount);
foreach (var p in EnabledPackages.SetCoreEnumerable(enabledCorePackage))
{
yield return p.Transform(loadingRange);
}
loadingRange = new Range<float>(1.0f / pkgCount, 1.0f);
foreach (var p in EnabledPackages.SetRegularEnumerable(enabledRegularPackages))
{
yield return p.Transform(loadingRange);
}
yield return new LoadProgress(1.0f);
}
}
}
@@ -0,0 +1,149 @@
#nullable enable
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Barotrauma.IO;
namespace Barotrauma
{
public sealed class ContentPath
{
public readonly static ContentPath Empty = new ContentPath(null, "");
public const string ModDirStr = "%ModDir%";
public const string OtherModDirFmt = "%ModDir:{0}%";
private static readonly Regex OtherModDirRegex = new Regex(
string.Format(OtherModDirFmt, "(.+?)"));
public readonly string? RawValue;
public readonly ContentPackage? ContentPackage;
private string? cachedValue;
private string? cachedFullPath;
public string Value
{
get
{
if (RawValue.IsNullOrEmpty()) { return ""; }
if (!cachedValue.IsNullOrEmpty()) { return cachedValue!; }
string? modName = ContentPackage?.Name;
var otherMods = OtherModDirRegex.Matches(RawValue ?? throw new NullReferenceException($"{nameof(RawValue)} is null."))
.Select(m => m.Groups[1].Value.Trim().ToIdentifier())
.Distinct().Where(id => !id.IsEmpty && id != modName).ToHashSet();
cachedValue = RawValue!;
if (!(ContentPackage is null))
{
string modPath = Path.GetDirectoryName(ContentPackage.Path)!;
cachedValue = cachedValue
.Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase)
.Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase);
if (ContentPackage.SteamWorkshopId != 0)
{
cachedValue = cachedValue
.Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase);
}
}
var allPackages = ContentPackageManager.AllPackages;
foreach (Identifier otherModName in otherMods)
{
if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; }
ContentPackage? otherMod =
allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId)
?? allPackages.FirstOrDefault(p => p.Name == otherModName)
?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName))
?? throw new MissingContentPackageException(ContentPackage, otherModName.Value);
cachedValue = cachedValue.Replace(string.Format(OtherModDirFmt, otherModName.Value), Path.GetDirectoryName(otherMod.Path));
}
cachedValue = cachedValue.CleanUpPath();
return cachedValue;
}
}
public string FullPath
{
get
{
if (cachedFullPath.IsNullOrEmpty())
{
if (Value.IsNullOrEmpty())
{
return "";
}
cachedFullPath = Path.GetFullPath(Value).CleanUpPathCrossPlatform(correctFilenameCase: false);
}
return cachedFullPath!;
}
}
private ContentPath(ContentPackage? contentPackage, string? rawValue)
{
ContentPackage = contentPackage;
RawValue = rawValue;
cachedValue = null;
cachedFullPath = null;
}
public static ContentPath FromRaw(string? rawValue)
=> new ContentPath(null, rawValue);
public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue)
=> new ContentPath(contentPackage, rawValue);
public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue)
{
throw new NotImplementedException();
}
private static bool StringEquality(string? a, string? b)
=> (a.IsNullOrEmpty() && b.IsNullOrEmpty()) ||
string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""),
Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""),
StringComparison.OrdinalIgnoreCase);
public static bool operator==(ContentPath a, ContentPath b)
=> StringEquality(a.Value, b.Value);
public static bool operator!=(ContentPath a, ContentPath b) => !(a == b);
public static bool operator==(ContentPath a, string? b)
=> StringEquality(a.Value, b);
public static bool operator!=(ContentPath a, string? b) => !(a == b);
public static bool operator==(string? a, ContentPath b)
=> StringEquality(a, b.Value);
public static bool operator!=(string? a, ContentPath b) => !(a == b);
protected bool Equals(ContentPath other)
{
return RawValue == other.RawValue && Equals(ContentPackage, other.ContentPackage) && cachedValue == other.cachedValue && cachedFullPath == other.cachedFullPath;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ContentPath)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath);
}
public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value);
public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value);
public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
public override string? ToString() => Value;
}
}
@@ -0,0 +1,130 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
public sealed class ContentXElement
{
public ContentPackage? ContentPackage { get; private set; }
public readonly XElement Element;
public ContentXElement(ContentPackage? contentPackage, XElement element)
{
ContentPackage = contentPackage;
Element = element;
}
public static implicit operator XElement?(ContentXElement? cxe) => cxe?.Element;
//public static implicit operator ContentXElement?(XElement? xe) => xe is null ? null : new ContentXElement(null, xe);
public XName Name => Element.Name;
public Identifier NameAsIdentifier() => Element.NameAsIdentifier();
public string BaseUri => Element.BaseUri;
public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null.");
public ContentXElement? FirstElement() => Elements().FirstOrDefault();
public ContentXElement? Parent => Element.Parent is null ? null : new ContentXElement(ContentPackage, Element.Parent);
public bool HasElements => Element.HasElements;
public bool IsOverride() => Element.IsOverride();
public bool ComesAfter(ContentXElement other) => Element.ComesAfter(other.Element);
public ContentXElement? GetChildElement(string name)
=> Element.GetChildElement(name) is { } elem ? new ContentXElement(ContentPackage, elem) : null;
public IEnumerable<ContentXElement> Elements()
=> Element.Elements().Select(e => new ContentXElement(ContentPackage, e));
public IEnumerable<ContentXElement> ElementsBeforeSelf()
=> Element.ElementsBeforeSelf().Select(e => new ContentXElement(ContentPackage, e));
public IEnumerable<ContentXElement> Descendants()
=> Element.Descendants().Select(e => new ContentXElement(ContentPackage, e));
public IEnumerable<ContentXElement> GetChildElements(string name)
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase));
public XAttribute? Attribute(string name) => Element.Attribute(name);
public XAttribute? GetAttribute(string name) => Element.GetAttribute(name);
public IEnumerable<XAttribute> Attributes() => Element.Attributes();
public IEnumerable<XAttribute> Attributes(string name) => Element.Attributes(name);
public string ElementInnerText() => Element.ElementInnerText();
public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def);
public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def);
public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage);
public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def);
public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def);
public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def);
public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def);
public float[]? GetAttributeFloatArray(string key, float[]? def) => Element.GetAttributeFloatArray(key, def);
public float GetAttributeFloat(float def, params string[] keys) => Element.GetAttributeFloat(def, keys);
public bool GetAttributeBool(string key, bool def) => Element.GetAttributeBool(key, def);
public Point GetAttributePoint(string key, in Point def) => Element.GetAttributePoint(key, def);
public Vector2 GetAttributeVector2(string key, in Vector2 def) => Element.GetAttributeVector2(key, def);
public Vector4 GetAttributeVector4(string key, in Vector4 def) => Element.GetAttributeVector4(key, def);
public Color GetAttributeColor(string key, in Color def) => Element.GetAttributeColor(key, def);
public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key);
public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def);
public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def);
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
public Identifier VariantOf() => Element.VariantOf();
public bool DoesAttributeReferenceFileNameAlone(string key) => Element.DoesAttributeReferenceFileNameAlone(key);
public string ParseContentPathFromUri() => Element.ParseContentPathFromUri();
public void SetAttributeValue(string key, string val) => Element.SetAttributeValue(key, val);
public void Add(ContentXElement elem)
{
Element.Add(elem.Element);
elem.ContentPackage = ContentPackage;
#warning TODO: update %ModDir% instances in case the content package changes
}
public void AddFirst(ContentXElement elem)
{
Element.AddFirst(elem.Element);
elem.ContentPackage = ContentPackage;
#warning TODO: update %ModDir% instances in case the content package changes
}
public void AddAfterSelf(ContentXElement elem)
{
Element.AddAfterSelf(elem.Element);
elem.ContentPackage = ContentPackage;
#warning TODO: update %ModDir% instances in case the content package changes
}
public void Remove() => Element.Remove();
}
public static class ContentXElementExtensions
{
public static ContentXElement FromPackage(this XElement element, ContentPackage? contentPackage)
=> new ContentXElement(contentPackage, element);
public static IEnumerable<ContentXElement> Elements(this IEnumerable<ContentXElement> elements)
=> elements.SelectMany(e => e.Elements());
}
}
@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
{
// Identifier struct to eliminate case-sensitive comparisons
public readonly struct Identifier : IComparable, IEquatable<Identifier>
{
public readonly static Identifier Empty = default;
private readonly static int emptyHash = "".GetHashCode(StringComparison.OrdinalIgnoreCase);
private readonly string? value;
private readonly Lazy<int>? hashCode;
public string Value => value ?? "";
public int HashCode => hashCode?.Value ?? emptyHash;
public Identifier(string? str)
{
value = str;
hashCode = new Lazy<int>(() => (str ?? "").GetHashCode(StringComparison.OrdinalIgnoreCase));
}
public bool IsEmpty => Value.IsNullOrEmpty();
public Identifier IfEmpty(in Identifier id)
=> IsEmpty ? id : this;
public Identifier Replace(in Identifier subStr, in Identifier newStr)
=> Replace(subStr.Value ?? "", newStr.Value ?? "");
public Identifier Replace(string subStr, string newStr)
=> (Value?.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public Identifier Remove(Identifier subStr)
=> Remove(subStr.Value ?? "");
public Identifier Remove(string subStr)
=> (Value?.Remove(subStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public override bool Equals(object? obj) =>
obj switch
{
Identifier i => this == i,
string s => this == s,
_ => base.Equals(obj)
};
public bool StartsWith(string str) => Value?.StartsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool StartsWith(Identifier id) => StartsWith(id.Value ?? "");
public bool EndsWith(string str) => Value?.EndsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool EndsWith(Identifier id) => EndsWith(id.Value ?? "");
public Identifier AppendIfMissing(string suffix)
=> EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier();
public Identifier RemoveFromEnd(string suffix)
=> (Value?.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public bool Contains(string str) => Value?.Contains(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool Contains(in Identifier id) => Contains(id.Value ?? "");
public override string ToString() => Value ?? "";
public override int GetHashCode() => HashCode;
public int CompareTo(object? obj)
{
return string.Compare(Value, obj?.ToString() ?? "", StringComparison.InvariantCultureIgnoreCase);
}
public bool Equals([AllowNull] Identifier other)
{
return this == other;
}
private static bool StringEquality(string? a, string? b)
=> (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
public static bool operator ==(in Identifier a, in Identifier b) =>
StringEquality(a.Value, b.Value);
public static bool operator !=(in Identifier a, in Identifier b) =>
!(a == b);
public static bool operator ==(in Identifier identifier, string? str) =>
StringEquality(identifier.Value, str);
public static bool operator !=(in Identifier identifier, string? str) =>
!(identifier == str);
public static bool operator ==(string? str, in Identifier identifier) =>
identifier == str;
public static bool operator !=(string? str, in Identifier identifier) =>
!(identifier == str);
public static bool operator ==(in Identifier? a, in Identifier? b) =>
StringEquality(a?.Value, b?.Value);
public static bool operator !=(in Identifier? a, in Identifier? b) =>
!(a == b);
public static bool operator ==(in Identifier? a, string? b) =>
StringEquality(a?.Value, b);
public static bool operator !=(in Identifier? a, string? b) =>
!(a == b);
public static bool operator ==(string str, in Identifier? identifier) =>
identifier == str;
public static bool operator !=(string str, in Identifier? identifier) =>
!(identifier == str);
}
public static class IdentifierExtensions
{
public static IEnumerable<Identifier> ToIdentifiers(this IEnumerable<string> strings)
{
foreach (string s in strings)
{
if (string.IsNullOrEmpty(s)) { continue; }
yield return new Identifier(s);
}
}
public static Identifier[] ToIdentifiers(this string[] strings)
=> ((IEnumerable<string>)strings).ToIdentifiers().ToArray();
public static Identifier ToIdentifier(this string? s)
{
return new Identifier(s);
}
public static Identifier ToIdentifier<T>(this T t) where T: notnull
{
return t.ToString().ToIdentifier();
}
public static bool Contains(this ISet<Identifier> set, string identifier)
{
return set.Contains(identifier.ToIdentifier());
}
public static bool ContainsKey<T>(this IReadOnlyDictionary<Identifier, T> dictionary, string key)
{
return dictionary.ContainsKey(key.ToIdentifier());
}
}
}
@@ -0,0 +1,17 @@
#nullable enable
using System;
namespace Barotrauma
{
public sealed class MissingContentPackageException : Exception
{
public override string Message { get; }
public MissingContentPackageException(ContentPackage? whoAsked, string? missingPackage)
{
Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " +
$"with name or ID \"{missingPackage ?? "[NULL]"}\" " +
$"that is not currently installed.";
}
}
}