diff --git a/AnnoMapEditor.Tests/Utils/TheoryWithGameFilesAttribute.cs b/AnnoMapEditor.Tests/Utils/TheoryWithGameFilesAttribute.cs index 9e3cc7f..9d69643 100644 --- a/AnnoMapEditor.Tests/Utils/TheoryWithGameFilesAttribute.cs +++ b/AnnoMapEditor.Tests/Utils/TheoryWithGameFilesAttribute.cs @@ -13,7 +13,7 @@ public TheoryWithGameFilesAttribute() Utilities.Settings.Instance.WaitForLoadingBlocking(); waitTimer.Stop(); - if (!Utilities.Settings.Instance.IsValidDataPath) + if (!Utilities.Settings.Instance.IsValidGamePath) { Skip = "The curring test environment has not detected the game files required for this unit test. " + $"Waited {waitTimer.ElapsedMilliseconds}ms for this information..."; diff --git a/AnnoMapEditor/App.config b/AnnoMapEditor/App.config new file mode 100644 index 0000000..120d839 --- /dev/null +++ b/AnnoMapEditor/App.config @@ -0,0 +1,30 @@ + + + + +
+ + + + + + + + + + + + + + + False + + + + + + + + + + \ No newline at end of file diff --git a/AnnoMapEditor/App.xaml b/AnnoMapEditor/App.xaml index 96f7a8f..9d091b6 100644 --- a/AnnoMapEditor/App.xaml +++ b/AnnoMapEditor/App.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AnnoMapEditor" - StartupUri="UI/Windows/Main/MainWindow.xaml"> + StartupUri="UI/Windows/Start/StartWindow.xaml"> diff --git a/AnnoMapEditor/DataArchives/Assets/Attributes/AssetTemplateAttribute.cs b/AnnoMapEditor/DataArchives/Assets/Attributes/AssetTemplateAttribute.cs deleted file mode 100644 index c0e7c4f..0000000 --- a/AnnoMapEditor/DataArchives/Assets/Attributes/AssetTemplateAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace AnnoMapEditor.DataArchives.Assets.Attributes -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] - public class AssetTemplateAttribute : Attribute - { - public string TemplateName { get; init; } - - - public AssetTemplateAttribute(string templateName) - { - TemplateName = templateName; - } - } -} diff --git a/AnnoMapEditor/DataArchives/Assets/Deserialization/AssetTemplateAttribute.cs b/AnnoMapEditor/DataArchives/Assets/Deserialization/AssetTemplateAttribute.cs new file mode 100644 index 0000000..a8d6e6f --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Deserialization/AssetTemplateAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace AnnoMapEditor.DataArchives.Assets.Deserialization +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + public class AssetTemplateAttribute : Attribute + { + public string[] TemplateNames { get; init; } + + + public AssetTemplateAttribute(params string[] templateNames) + { + TemplateNames = templateNames; + } + } +} diff --git a/AnnoMapEditor/DataArchives/Assets/Attributes/AssetReferenceAttribute.cs b/AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceAttribute.cs similarity index 54% rename from AnnoMapEditor/DataArchives/Assets/Attributes/AssetReferenceAttribute.cs rename to AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceAttribute.cs index 5406265..b0ac97d 100644 --- a/AnnoMapEditor/DataArchives/Assets/Attributes/AssetReferenceAttribute.cs +++ b/AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceAttribute.cs @@ -1,14 +1,14 @@ using System; -namespace AnnoMapEditor.DataArchives.Assets.Attributes +namespace AnnoMapEditor.DataArchives.Assets.Deserialization { [AttributeUsage(AttributeTargets.Property)] - public class AssetReferenceAttribute : Attribute + public class GuidReferenceAttribute : Attribute { public string GuidPropertyName { get; init; } - public AssetReferenceAttribute(string guidPropertyName) + public GuidReferenceAttribute(string guidPropertyName) { GuidPropertyName = guidPropertyName; } diff --git a/AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceResolverFactory.cs b/AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceResolverFactory.cs new file mode 100644 index 0000000..35db63a --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Deserialization/GuidReferenceResolverFactory.cs @@ -0,0 +1,129 @@ +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives.Assets.Repositories; +using AnnoMapEditor.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace AnnoMapEditor.DataArchives.Assets.Deserialization +{ + public class GuidReferenceResolverFactory + { + private readonly AssetRepository _assetRepository; + + private readonly Logger _logger; + + + public GuidReferenceResolverFactory(AssetRepository assetRepository) + { + _assetRepository = assetRepository; + _logger = new Logger(); + } + + + public Action? CreateResolver(PropertyInfo referenceProperty) + where TAsset : StandardAsset + { + // determine if this property is a reference to another asset + GuidReferenceAttribute? referenceAttribute = referenceProperty.GetCustomAttribute(); + if (referenceAttribute == null) + return null; + + // get the property containing the guid of the referenced asset + PropertyInfo guidProperty = typeof(TAsset).GetProperty(referenceAttribute.GuidPropertyName) + ?? throw new ArgumentException($"Invalid {nameof(GuidReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. Could not find property {referenceAttribute.GuidPropertyName}."); + + // create the resolver + if (guidProperty.PropertyType == typeof(long) || guidProperty.PropertyType == typeof(long?)) + return CreateSingleResolver(referenceProperty, guidProperty); + + else if (guidProperty.PropertyType == typeof(IEnumerable)) + return CreateEnumerableResolver(referenceProperty, guidProperty); + + else + throw new ArgumentException($"Invalid {nameof(GuidReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. Property {referenceAttribute.GuidPropertyName} must either be of type {typeof(long).FullName} or {typeof(IEnumerable).FullName}."); + } + + private Action CreateSingleResolver(PropertyInfo referenceProperty, PropertyInfo guidProperty) + where TAsset : StandardAsset + { + // validate the reference property + Type referencedType = referenceProperty.PropertyType; + if (!typeof(StandardAsset).IsAssignableFrom(referencedType)) + throw new ArgumentException($"Invalid {nameof(GuidReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. The property's type does not extend {typeof(StandardAsset).FullName}."); + + // create the delegate + return (asset) => + { + long? guid = guidProperty.GetValue(asset) as long?; + if (guid.HasValue) + { + if (TryGetReferencedAsset(guid.Value, out StandardAsset? referencedAsset, referencedType)) + referenceProperty.SetValue(asset, referencedAsset); + } + }; + } + + private bool IsValidCollectionReferenceProperty(PropertyInfo referenceProperty) + { + if (!referenceProperty.PropertyType.IsGenericType) + return false; + + // validate the reference property + Type referencedType = referenceProperty.PropertyType.GenericTypeArguments[0]; + if (!typeof(StandardAsset).IsAssignableFrom(referencedType)) + return false; + + Type enumerableType = typeof(ICollection<>).MakeGenericType(referencedType); + if (referenceProperty.PropertyType != enumerableType) + return false; + + return true; + } + + private Action CreateEnumerableResolver(PropertyInfo referenceProperty, PropertyInfo guidProperty) + where TAsset : StandardAsset + { + if (!IsValidCollectionReferenceProperty(referenceProperty)) + throw new ArgumentException($"Invalid {nameof(GuidReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. The property's type does not extend {typeof(ICollection<>)} for matching assets."); + + // create the delegate + Type referencedType = referenceProperty.PropertyType.GenericTypeArguments[0]; + Type listType = typeof(List<>).MakeGenericType(referencedType); + + return (asset) => + { + IEnumerable? guids = guidProperty.GetValue(asset) as IEnumerable; + if (guids != null) + { + IList list = (IList)Activator.CreateInstance(listType)!; + + foreach (long guid in guids) + { + if (TryGetReferencedAsset(guid, out StandardAsset? referencedAsset, referencedType)) + list.Add(referencedAsset); + } + + referenceProperty.SetValue(asset, list); + } + }; + } + + private bool TryGetReferencedAsset(long guid, [MaybeNullWhen(false)] out StandardAsset? referencedAsset, Type referencedType) + { + if (_assetRepository.TryGet(guid, out referencedAsset)) + { + if (referencedType.IsAssignableFrom(referencedAsset!.GetType())) + return true; + else + _logger.LogWarning($"Could not resolve GUID reference to type {referencedType.FullName}. Asset with GUID {guid} has type {referencedAsset.GetType().FullName} instead of expected {referencedType.FullName}."); + } + else + _logger.LogWarning($"Could not resolve GUID reference to type {referencedType.FullName}. No asset with GUID {guid} could be found."); + + return false; + } + } +} \ No newline at end of file diff --git a/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceAttribute.cs b/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceAttribute.cs new file mode 100644 index 0000000..3a2ade6 --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceAttribute.cs @@ -0,0 +1,16 @@ +using System; + +namespace AnnoMapEditor.DataArchives.Assets.Deserialization +{ + [AttributeUsage(AttributeTargets.Property)] + public class RegionIdReferenceAttribute : Attribute + { + public string RegionIdPropertyName { get; init; } + + + public RegionIdReferenceAttribute(string regionIdPropertyName) + { + RegionIdPropertyName = regionIdPropertyName; + } + } +} \ No newline at end of file diff --git a/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceResolverFactory.cs b/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceResolverFactory.cs new file mode 100644 index 0000000..0461613 --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Deserialization/RegionIdReferenceResolverFactory.cs @@ -0,0 +1,120 @@ +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives.Assets.Repositories; +using AnnoMapEditor.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AnnoMapEditor.DataArchives.Assets.Deserialization +{ + public class RegionIdReferenceResolverFactory + { + private readonly AssetRepository _assetRepository; + + private readonly Logger _logger; + + + public RegionIdReferenceResolverFactory(AssetRepository assetRepository) + { + _assetRepository = assetRepository; + _logger = new Logger(); + } + + + public Action? CreateResolver(PropertyInfo referenceProperty) + where TAsset : StandardAsset + { + // determine if this property is a reference to a RegionAsset + RegionIdReferenceAttribute? referenceAttribute = referenceProperty.GetCustomAttribute(); + if (referenceAttribute == null) + return null; + + // get the property containing the RegionId of the referenced RegionAsset + PropertyInfo regionIdProperty = typeof(TAsset).GetProperty(referenceAttribute.RegionIdPropertyName) + ?? throw new ArgumentException($"Invalid {nameof(RegionIdReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. Could not find property {referenceAttribute.RegionIdPropertyName}."); + + // create the resolver + Action resolver; + if (regionIdProperty.PropertyType == typeof(string)) + return CreateSingleResolver(referenceProperty, regionIdProperty); + + else if (regionIdProperty.PropertyType == typeof(IEnumerable)) + return CreateEnumerableResolver(referenceProperty, regionIdProperty); + + else + throw new ArgumentException($"Invalid {nameof(RegionIdReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. Property {referenceAttribute.RegionIdPropertyName} must either be of type {typeof(string).FullName} or {typeof(IEnumerable).FullName}."); + } + + private Action CreateSingleResolver(PropertyInfo referenceProperty, PropertyInfo regionIdProperty) + where TAsset : StandardAsset + { + // validate the reference property + Type referencedType = referenceProperty.PropertyType; + if (!referencedType.IsAssignableFrom(typeof(RegionAsset))) + throw new ArgumentException($"Invalid {nameof(RegionIdReferenceAttribute)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. The type {typeof(RegionAsset).FullName} does not extend the property's type."); + + // create the delegate + return (asset) => + { + string? regionId = regionIdProperty.GetValue(asset) as string; + if (regionId != null) + { + RegionAsset? regionAsset = _assetRepository.GetAll().FirstOrDefault(r => r.RegionID == regionId); + if (regionAsset != null) + referenceProperty.SetValue(asset, regionAsset); + else + _logger.LogWarning($"Could not resolve RegionID reference. No RegionAsset with RegionID {regionId} could be found."); + } + }; + } + + private bool IsValidCollectionReferenceProperty(PropertyInfo referenceProperty) + { + if (!referenceProperty.PropertyType.IsGenericType) + return false; + + // validate the reference property + Type referencedType = referenceProperty.PropertyType.GenericTypeArguments[0]; + if (!referencedType.IsAssignableFrom(typeof(RegionAsset))) + return false; + + Type enumerableType = typeof(ICollection<>).MakeGenericType(referencedType); + if (referenceProperty.PropertyType != enumerableType) + return false; + + return true; + } + + private Action CreateEnumerableResolver(PropertyInfo referenceProperty, PropertyInfo regionIdProperty) + where TAsset : StandardAsset + { + if (!IsValidCollectionReferenceProperty(referenceProperty)) + throw new ArgumentException($"Invalid {nameof(RegionIdReferenceResolverFactory)} on property {typeof(TAsset).FullName}.{referenceProperty.Name}. The property's type does not extend {typeof(ICollection<>)} accepting RegionAssets."); + + // create the delegate + Type referencedType = referenceProperty.PropertyType.GenericTypeArguments[0]; + Type listType = typeof(List<>).MakeGenericType(referencedType); + + return (asset) => + { + IEnumerable? regionIds = regionIdProperty.GetValue(asset) as IEnumerable; + if (regionIds != null) + { + List regionAssets = _assetRepository.GetAll().ToList(); + IList list = (IList)Activator.CreateInstance(listType)!; + foreach (string regionId in regionIds) + { + RegionAsset? regionAsset = regionAssets.FirstOrDefault(r => r.RegionID == regionId); + if (regionAsset != null) + list.Add(regionAsset); + else + _logger.LogWarning($"Could not resolve RegionID reference. No RegionAsset with RegionID {regionId} could be found."); + } + referenceProperty.SetValue(asset, list); + } + }; + } + } +} \ No newline at end of file diff --git a/AnnoMapEditor/DataArchives/Assets/Models/FertilityAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/FertilityAsset.cs index a368d59..32a298c 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/FertilityAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/FertilityAsset.cs @@ -1,12 +1,14 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; -using System.Windows.Media; +using AnnoMapEditor.DataArchives.Assets.Deserialization; using System.Xml.Linq; namespace AnnoMapEditor.DataArchives.Assets.Models { - [AssetTemplate("Fertility")] + [AssetTemplate(TEMPLATE_NAME)] public class FertilityAsset : StandardAsset { + public const string TEMPLATE_NAME = "Fertility"; + + public string DisplayName { get; init; } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/IslandAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/IslandAsset.cs index c782396..a293ca1 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/IslandAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/IslandAsset.cs @@ -4,6 +4,10 @@ namespace AnnoMapEditor.DataArchives.Assets.Models { + /// + /// Common base class for RandomIslandAsset and FixedIslandAsset. If an island does not belong + /// to a pool and does not require non-default properties, it may be ommitted from assets.xml. + /// public class IslandAsset { public string DisplayName { get; init; } @@ -12,7 +16,7 @@ public class IslandAsset public BitmapImage? Thumbnail { get; init; } - public Region Region { get; init; } + public RegionAsset Region { get; init; } public IEnumerable IslandDifficulty { get; init; } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/MapTemplateAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/MapTemplateAsset.cs new file mode 100644 index 0000000..8c92d95 --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Models/MapTemplateAsset.cs @@ -0,0 +1,46 @@ +using AnnoMapEditor.DataArchives.Assets.Deserialization; +using AnnoMapEditor.Mods.Enums; +using System; +using System.Xml.Linq; + +namespace AnnoMapEditor.DataArchives.Assets.Models +{ + [AssetTemplate(TEMPLATE_NAME)] + public class MapTemplateAsset : StandardAsset + { + public const string TEMPLATE_NAME = "MapTemplate"; + + + public string TemplateFilename { get; init; } + + public string? EnlargedTemplateFilename { get; init; } + + public string TemplateRegionId { get; init; } + + [RegionIdReference(nameof(TemplateRegionId))] + public RegionAsset TemplateRegion { get; set; } + + public MapType? TemplateMapType { get; init; } + + + public MapTemplateAsset(XElement valuesXml) + : base(valuesXml) + { + XElement mapTemplateValues = valuesXml.Element(TEMPLATE_NAME) + ?? throw new Exception($"XML is not a valid {nameof(MapTemplateAsset)}. It does not have '{TEMPLATE_NAME}' section in its values."); + + TemplateFilename = mapTemplateValues.Element(nameof(TemplateFilename))?.Value + ?? throw new Exception($"XML is not a valid {nameof(MapTemplateAsset)}. It does not have '{nameof(TemplateFilename)}' section in its values."); + + EnlargedTemplateFilename = mapTemplateValues.Element(nameof(EnlargedTemplateFilename))?.Value; + + // TemplateRegion defaults to Moderate. If the MapTemplate belongs to another region, + // it must have TemplateRegion set explicitly within assets.xml. + TemplateRegionId = mapTemplateValues.Element(nameof(TemplateRegion))?.Value ?? RegionAsset.REGION_MODERATE_REGIONID; + + string? templateMapTypeStr = mapTemplateValues.Element(nameof(TemplateMapType))?.Value; + if (templateMapTypeStr != null) + TemplateMapType = MapType.FromName(templateMapTypeStr); + } + } +} \ No newline at end of file diff --git a/AnnoMapEditor/DataArchives/Assets/Models/MinimapSceneAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/MinimapSceneAsset.cs index 40f6cb5..881065b 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/MinimapSceneAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/MinimapSceneAsset.cs @@ -1,17 +1,21 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; -using System; +using AnnoMapEditor.DataArchives.Assets.Deserialization; +using AnnoMapEditor.DataArchives.Assets.Repositories; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Xml.Linq; namespace AnnoMapEditor.DataArchives.Assets.Models { - [AssetTemplate("MinimapScene")] + [AssetTemplate(TEMPLATE_NAME)] public class MinimapSceneAsset : StandardAsset { - public new const long GUID = 500204; + public const string TEMPLATE_NAME = "MinimapScene"; + + public const long INSTANCE_GUID = 500204; + + + [StaticAsset(INSTANCE_GUID)] + public static MinimapSceneAsset Instance { get; set; } public List FertilityOrderGuids { get; init; } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/RandomIslandAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/RandomIslandAsset.cs index f167690..f768688 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/RandomIslandAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/RandomIslandAsset.cs @@ -1,22 +1,24 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; +using AnnoMapEditor.DataArchives.Assets.Deserialization; using AnnoMapEditor.MapTemplates.Enums; using System; using System.Collections.Generic; using System.Linq; -using System.Windows.Media.Imaging; using System.Xml.Linq; namespace AnnoMapEditor.DataArchives.Assets.Models { - [AssetTemplate("RandomIsland")] + [AssetTemplate(TEMPLATE_NAME)] public class RandomIslandAsset : StandardAsset { - public static readonly string TemplateName = "RandomIsland"; + public const string TEMPLATE_NAME = "RandomIsland"; public string FilePath { get; init; } - public Region? IslandRegion { get; init; } + public string IslandRegionId { get; init; } + + [RegionIdReference(nameof(IslandRegionId))] + public RegionAsset IslandRegion { get; init; } public IEnumerable IslandDifficulty { get; init; } @@ -26,12 +28,12 @@ public class RandomIslandAsset : StandardAsset public RandomIslandAsset(XElement valuesXml) : base(valuesXml) { - XElement randomIslandValues = valuesXml.Element(TemplateName) - ?? throw new Exception($"XML is not a valid {nameof(RandomIslandAsset)}. It does not have '{TemplateName}' section in its values."); + XElement randomIslandValues = valuesXml.Element(TEMPLATE_NAME) + ?? throw new Exception($"XML is not a valid {nameof(RandomIslandAsset)}. It does not have '{TEMPLATE_NAME}' section in its values."); - string? regionStr = randomIslandValues.Element(nameof(IslandRegion))?.Value; - if (regionStr != null) - IslandRegion = RegionFromName(regionStr); + // IslandRegion defaults to Moderate. If the MapTemplate belongs to another region, + // it must have TemplateRegion set explicitly within assets.xml. + IslandRegionId = randomIslandValues.Element(nameof(IslandRegion))?.Value ?? RegionAsset.REGION_MODERATE_REGIONID; FilePath = randomIslandValues.Element(nameof(FilePath))?.Value ?? throw new Exception($"XML is not a valid {nameof(RandomIslandAsset)}. Required attribute '{nameof(FilePath)}' not found."); @@ -58,18 +60,5 @@ public RandomIslandAsset(XElement valuesXml) else IslandType = Enumerable.Empty(); } - - - private static Region RegionFromName(string name) - { - return name switch - { - "Moderate" => Region.Moderate, - "Colony01" => Region.NewWorld, - "Arctic" => Region.Arctic, - "Africa" => Region.Enbesa, - _ => throw new NotImplementedException() - }; - } } } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/RegionAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/RegionAsset.cs index 16b1eaa..dc38804 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/RegionAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/RegionAsset.cs @@ -1,4 +1,5 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; +using AnnoMapEditor.DataArchives.Assets.Deserialization; +using AnnoMapEditor.DataArchives.Assets.Repositories; using System; using System.Collections.Generic; using System.Linq; @@ -6,21 +7,63 @@ namespace AnnoMapEditor.DataArchives.Assets.Models { - [AssetTemplate("Region")] + [AssetTemplate(TEMPLATE_NAME)] public class RegionAsset : StandardAsset { + public const string TEMPLATE_NAME = "Region"; + + public const long REGION_MODERATE_GUID = 5000000; + public const long REGION_SOUTHAMERICA_GUID = 5000001; + public const long REGION_ARCTIC_GUID = 160001; + public const long REGION_AFRICA_GUID = 114327; + + public const string REGION_MODERATE_REGIONID = "Moderate"; + + + [StaticAsset(REGION_MODERATE_GUID)] + public static RegionAsset Moderate { get; private set; } + + [StaticAsset(REGION_SOUTHAMERICA_GUID)] + public static RegionAsset SouthAmerica { get; private set; } + + [StaticAsset(REGION_ARCTIC_GUID)] + public static RegionAsset Arctic { get; private set; } + + [StaticAsset(REGION_AFRICA_GUID)] + public static RegionAsset Africa { get; private set; } + + public static IEnumerable SupportedRegions => new[] { Moderate, SouthAmerica, Arctic, Africa }; + + + /// + /// Each region has its own AmbientName, which is needed when creating the .a7t. These + /// values are missing in assets.xml. The values seen here were reverse engineered from + /// existing a7t files within the game. + /// + /// Note: Region assets do contain an attribute "Ambiente". However its value is always + /// "Region_map_global" and does not match the expected value for a7ts. + /// + private static readonly Dictionary REGION_AMBIENTE_HARDCODED = new Dictionary() + { + [REGION_MODERATE_GUID] = "Moderate_01_day_night", + [REGION_SOUTHAMERICA_GUID] = "south_america_caribic_01", + [REGION_ARCTIC_GUID] = "DLC03_01", + [REGION_AFRICA_GUID] = "Colony_02" + }; + + public string DisplayName { get; init; } public string? Ambiente { get; init; } - public string? RegionID { get; init; } + public string RegionID { get; init; } public IEnumerable AllowedFertilityGuids { get; init; } - [AssetReference(nameof(AllowedFertilityGuids))] - public IEnumerable AllowedFertilities { get; init; } - - + [GuidReference(nameof(AllowedFertilityGuids))] + public ICollection AllowedFertilities { get; init; } + + public RegionAsset(XElement valuesXml) : base(valuesXml) { @@ -31,9 +74,14 @@ public RegionAsset(XElement valuesXml) .Value! ?? "Meta"; - XElement regionElement = valuesXml.Element("Region")!; - Ambiente = regionElement.Element("Ambiente")?.Value; - RegionID = regionElement.Element("RegionID")?.Value; + XElement regionElement = valuesXml.Element(TEMPLATE_NAME)!; + + if (REGION_AMBIENTE_HARDCODED.ContainsKey(GUID)) + Ambiente = REGION_AMBIENTE_HARDCODED[GUID]; + + // The region Moderate does not have a RegionID specified in assets.xml. All other + // regions have them. + RegionID = regionElement.Element("RegionID")?.Value ?? REGION_MODERATE_REGIONID; AllowedFertilityGuids = regionElement.Element("AllowedFertilities")? .Elements("Item")? @@ -41,5 +89,21 @@ public RegionAsset(XElement valuesXml) .ToArray() ?? Array.Empty(); } + + + public static RegionAsset DetectFromPath(string filePath) + { + if (filePath.Contains("colony01") || filePath.Contains("ggj") || filePath.Contains("scenario03")) + return SouthAmerica; + else if (filePath.Contains("dlc03") || filePath.Contains("colony_03")) + return Arctic; + else if (filePath.Contains("dlc06") || filePath.Contains("colony02") || filePath.Contains("scenario02")) + return Africa; + else + return Moderate; + } + + + public override string ToString() => DisplayName; } } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/SessionAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/SessionAsset.cs new file mode 100644 index 0000000..af21350 --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Models/SessionAsset.cs @@ -0,0 +1,137 @@ +using AnnoMapEditor.DataArchives.Assets.Deserialization; +using AnnoMapEditor.DataArchives.Assets.Repositories; +using AnnoMapEditor.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace AnnoMapEditor.DataArchives.Assets.Models +{ + [AssetTemplate("SessionModerate", "SessionSouthAmerica", "SessionArctic")] + public class SessionAsset : StandardAsset + { + public const string TEMPLATE_NAME = "Session"; + + public const long SESSION_OLDWORLD_GUID = 180023; + public const long SESSION_NEWWORLD_GUID = 180025; + public const long SESSION_SUNKENTREASURES_GUID = 110934; + public const long SESSION_ARCTIC_GUID = 180045; + public const long SESSION_ENBESA_GUID = 112132; + + + [StaticAsset(SESSION_OLDWORLD_GUID)] + public static SessionAsset OldWorld { get; private set; } + + [StaticAsset(SESSION_NEWWORLD_GUID)] + public static SessionAsset NewWorld { get; private set; } + + [StaticAsset(SESSION_SUNKENTREASURES_GUID)] + public static SessionAsset CapeTrelawney { get; private set; } + + [StaticAsset(SESSION_ARCTIC_GUID)] + public static SessionAsset Arctic { get; private set; } + + [StaticAsset(SESSION_ENBESA_GUID)] + public static SessionAsset Enbesa { get; private set; } + + public static IEnumerable SupportedSessions => new[] { OldWorld, NewWorld, CapeTrelawney, Arctic, Enbesa }; + + + /// + /// The session assets for The Old World and The New World to not properly reference their + /// respective regions in assets.xml. + /// + private static readonly Dictionary REGIONID_HARDCODED = new() + { + [SESSION_OLDWORLD_GUID] = RegionAsset.REGION_MODERATE_GUID, // The Old World => Moderate + [SESSION_NEWWORLD_GUID] = RegionAsset.REGION_SOUTHAMERICA_GUID // The New World => Colony01 + }; + + + public string DisplayName { get; init; } + + public long? MapTemplateGuid { get; init; } + + [GuidReference(nameof(MapTemplateGuid))] + public MapTemplateAsset? MapTemplate { get; set; } + + public long? MapTemplateForMultiplayerGuid { get; init; } + + [GuidReference(nameof(MapTemplateForMultiplayerGuid))] + public MapTemplateAsset? MapTemplateForMultiplayer { get; set; } + + public long RegionGuid { get; init; } + + [GuidReference(nameof(RegionGuid))] + public RegionAsset Region { get; set; } + + + public SessionAsset(XElement valuesXml) + : base(valuesXml) + { + DisplayName = valuesXml.Element("Text")! + .Element("LocaText")? + .Element("English")! + .Element("Text")! + .Value! + ?? "Meta"; + + XElement sessionValues = valuesXml.Element(TEMPLATE_NAME) + ?? throw new Exception($"XML is not a valid {nameof(SessionAsset)}. It does not have '{TEMPLATE_NAME}' section in its values."); + + string? mapTemplateGuidStr = sessionValues.Element(nameof(MapTemplate))?.Value; + if (mapTemplateGuidStr != null) + { + if (long.TryParse(mapTemplateGuidStr, out long mapTemplateGuid)) + MapTemplateGuid = mapTemplateGuid; + else + throw new Exception($"XML is not a valid {nameof(SessionAsset)}. Invalid value '{mapTemplateGuidStr}' for attribute '{nameof(MapTemplate)}'."); + } + + string? mapTemplateForMultiplayerGuidStr = sessionValues.Element(nameof(MapTemplateForMultiplayer))?.Value; + if (mapTemplateForMultiplayerGuidStr != null) + { + if (long.TryParse(mapTemplateForMultiplayerGuidStr, out long mapTemplateForMultiplayerGuid)) + MapTemplateForMultiplayerGuid = mapTemplateForMultiplayerGuid; + else + throw new Exception($"XML is not a valid {nameof(SessionAsset)}. Invalid value '{mapTemplateGuidStr}' for attribute '{nameof(MapTemplateForMultiplayer)}'."); + } + + string? regionGuidStr = sessionValues.Element(nameof(Region))?.Value; + if (regionGuidStr != null) + { + if (long.TryParse(regionGuidStr, out long regionGuid)) + RegionGuid = regionGuid; + else + throw new Exception($"XML is not a valid {nameof(SessionAsset)}. Invalid value '{regionGuidStr}' for attribute '{nameof(Region)}'."); + } + // special handling for The Old World and The New World + else if (REGIONID_HARDCODED.ContainsKey(GUID)) + RegionGuid = REGIONID_HARDCODED[GUID]; + // default to Moderate + else + RegionGuid = RegionAsset.REGION_MODERATE_GUID; + } + + + public static SessionAsset DetectFromPath(string filePath) + { + if (filePath.Contains("colony01") || filePath.Contains("ggj") || filePath.Contains("scenario03")) + return NewWorld; + else if (filePath.Contains("dlc03") || filePath.Contains("colony_03")) + return Arctic; + else if (filePath.Contains("dlc06") || filePath.Contains("colony02") || filePath.Contains("scenario02")) + return Enbesa; + else if (filePath.Contains("sunken_treasures")) + return CapeTrelawney; + else + return OldWorld; + } + + + public override string ToString() => DisplayName; + } +} diff --git a/AnnoMapEditor/DataArchives/Assets/Models/SlotAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/SlotAsset.cs index d2d1d52..692468a 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/SlotAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/SlotAsset.cs @@ -1,5 +1,5 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Deserialization; +using AnnoMapEditor.DataArchives.Assets.Repositories; using System; using System.Collections.Generic; using System.Linq; @@ -7,9 +7,11 @@ namespace AnnoMapEditor.DataArchives.Assets.Models { - [AssetTemplate("Slot")] + [AssetTemplate(TEMPLATE_NAME)] public class SlotAsset : StandardAsset { + public const string TEMPLATE_NAME = "Slot"; + public const long RANDOM_MINE_OLD_WORLD_GUID = 1000029; public const long RANDOM_MINE_NEW_WORLD_GUID = 614; public const long RANDOM_MINE_ARCTIC_GUID = 116037; @@ -17,6 +19,22 @@ public class SlotAsset : StandardAsset public const long RANDOM_OIL_GUID = 100849; + [StaticAsset(RANDOM_MINE_OLD_WORLD_GUID)] + public static SlotAsset RandomMineOldWorld { get; private set; } + + [StaticAsset(RANDOM_MINE_NEW_WORLD_GUID)] + public static SlotAsset RandomMineNewWorld { get; private set; } + + [StaticAsset(RANDOM_MINE_ARCTIC_GUID)] + public static SlotAsset RandomMineArctic { get; private set; } + + [StaticAsset(RANDOM_CLAY_GUID)] + public static SlotAsset RandomClay { get; private set; } + + [StaticAsset(RANDOM_OIL_GUID)] + public static SlotAsset RandomOil { get; private set; } + + public string DisplayName { get; init; } public string? SlotType { get; init; } @@ -25,18 +43,21 @@ public class SlotAsset : StandardAsset public IEnumerable ReplacementGuids { get; init; } - [AssetReference(nameof(ReplacementGuids))] - public IEnumerable ReplacementSlotAssets { get; set; } + [GuidReference(nameof(ReplacementGuids))] + public ICollection ReplacementSlotAssets { get; set; } - public IEnumerable AssociatedRegions { get; init; } + public IEnumerable AssociatedRegionIds { get; init; } + + [RegionIdReference(nameof(AssociatedRegionIds))] + public ICollection AssociatedRegions { get; set; } public SlotAsset() : base() { DisplayName = ""; ReplacementGuids = Enumerable.Empty(); - ReplacementSlotAssets = Enumerable.Empty(); - AssociatedRegions = Enumerable.Empty(); + ReplacementSlotAssets = Array.Empty(); + AssociatedRegions = Array.Empty(); } @@ -50,7 +71,7 @@ public SlotAsset(XElement valuesXml) .Element("Text")! .Value!; - SlotType = valuesXml.Element("Slot")? + SlotType = valuesXml.Element(TEMPLATE_NAME)? .Element("SlotType")? .Value; IsRandomSlot = SlotType == "Random"; @@ -71,15 +92,12 @@ public SlotAsset(XElement valuesXml) ReplacementGuids = replacementGuids ?? dlcReplacementGuids ?? Array.Empty(); - AssociatedRegions = valuesXml.Element("Building")? + AssociatedRegionIds = valuesXml.Element("Building")? .Element("AssociatedRegions")? .Value? .Split(';') - .Select(id => Region.FromRegionId(id)) .ToArray() - ?? Array.Empty(); - - ReplacementSlotAssets = Enumerable.Empty(); + ?? Array.Empty(); } } } diff --git a/AnnoMapEditor/DataArchives/Assets/Models/StandardAsset.cs b/AnnoMapEditor/DataArchives/Assets/Models/StandardAsset.cs index 8397dab..56cbffc 100644 --- a/AnnoMapEditor/DataArchives/Assets/Models/StandardAsset.cs +++ b/AnnoMapEditor/DataArchives/Assets/Models/StandardAsset.cs @@ -1,5 +1,4 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; -using AnnoMapEditor.Utilities; +using AnnoMapEditor.Utilities; using System; using System.Windows.Media; using System.Xml.Linq; diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/AssetRepository.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/AssetRepository.cs index 6d6f7a1..00e93ce 100644 --- a/AnnoMapEditor/DataArchives/Assets/Repositories/AssetRepository.cs +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/AssetRepository.cs @@ -1,8 +1,7 @@ -using AnnoMapEditor.DataArchives.Assets.Attributes; +using AnnoMapEditor.DataArchives.Assets.Deserialization; using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.Utilities; using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -19,23 +18,33 @@ namespace AnnoMapEditor.DataArchives.Assets.Repositories { public class AssetRepository : Repository { + private const string ASSETS_XML_PATH = "data/config/export/main/asset/assets.xml"; + + private static readonly Logger _logger = new(); - private const string AssetsXmlPath = "data/config/export/main/asset/assets.xml"; private const string CachedAssetsXml = "assets.cached.xml"; - private readonly IDataArchive _dataArchive; + private readonly GuidReferenceResolverFactory _guidReferenceResolverFactory; + + private readonly RegionIdReferenceResolverFactory _regionIdReferenceResolverFactory; + private readonly Dictionary> _deserializers = new(); private readonly Dictionary>> _referenceResolvers = new(); private readonly Dictionary _assets = new(); + private readonly List _assetTypes = new(); + + public AssetRepository(IDataArchive dataArchive) { _dataArchive = dataArchive; + _guidReferenceResolverFactory = new(this); + _regionIdReferenceResolverFactory = new(this); } private StandardAsset? ConstructAssetFrom(XElement assetElement) @@ -94,21 +103,23 @@ private void RenewCache(IEnumerable assets) doc.Save(File.Create(CachedAssetsXml)); } - protected override async Task DoLoad() + public override async Task InitializeAsync() { _logger.LogInformation($"Loading assets..."); // load assets.xml Stopwatch watch = Stopwatch.StartNew(); - Stream assetsXmlStream = _dataArchive.OpenRead(AssetsXmlPath) + Stream assetsXmlStream = _dataArchive.OpenRead(ASSETS_XML_PATH) ?? throw new Exception($"Could not locate assets.xml."); + var xpath = GetXpath(); + using var hasher = SHA256.Create(); var hash = hasher.ComputeHash(assetsXmlStream); var hashB64 = Convert.ToBase64String(hash); assetsXmlStream.Seek(0, SeekOrigin.Begin); - bool validCache = UserSettings.Default.AssetsHash == hashB64; + bool validCache = UserSettings.Default.AssetsHash == hashB64 && UserSettings.Default.Xpath == xpath; _logger.LogInformation($"Computed Hash at {watch.Elapsed.TotalMilliseconds} ms"); if (validCache) @@ -125,12 +136,12 @@ protected override async Task DoLoad() } var Xml = XDocument.Load(assetsXmlStream); - var xpath = GetXpath(); var assets = Xml.XPathSelectElements(xpath); if (!validCache) { UserSettings.Default.AssetsHash = hashB64; + UserSettings.Default.Xpath = xpath; UserSettings.Default.Save(); assetsXmlStream.Dispose(); RenewCache(assets); @@ -152,7 +163,7 @@ protected override async Task DoLoad() } } - _logger.LogInformation($"Finished Deserialising at {watch.Elapsed.TotalMilliseconds} ms"); + _logger.LogInformation($"Finished Deserialising at {watch.Elapsed.TotalMilliseconds} ms."); _logger.LogInformation($"Resolving asset references..."); // resolve references @@ -163,6 +174,8 @@ protected override async Task DoLoad() resolver(asset); } + InitializeStaticAssets(); + watch.Stop(); _logger.LogInformation($"Finished loading {_assets.Count} assets at {watch.Elapsed.TotalMilliseconds} ms."); assetsXmlStream.Dispose(); @@ -223,47 +236,35 @@ public void Register() { AssetTemplateAttribute assetTemplateAttribute = typeof(TAsset).GetCustomAttribute() ?? throw new Exception($"Cannot register type '{typeof(TAsset).FullName}' as an asset model, because it lacks the {nameof(AssetTemplateAttribute)}."); - string templateName = assetTemplateAttribute.TemplateName; - + // get the deserializer ConstructorInfo deserializerConstructor = typeof(TAsset).GetConstructor(new[] { typeof(XElement) }) ?? throw new Exception($"Type {typeof(TAsset).FullName} is not a valid asset model. Asset models must have a deserialization constructor."); Func deserializer = (x) => (TAsset)deserializerConstructor.Invoke(new[] { x }); - _deserializers.Add(templateName, deserializer); + foreach (string templateName in assetTemplateAttribute.TemplateNames) + _deserializers.Add(templateName, deserializer); // prepare to resolve references foreach (PropertyInfo property in typeof(TAsset).GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - // determine if this property is a reference to another asset - AssetReferenceAttribute? referenceAttribute = property.GetCustomAttribute(); - if (referenceAttribute == null) - continue; - - // get the property containing the guid of the referenced asset - PropertyInfo guidProperty = typeof(TAsset).GetProperty(referenceAttribute.GuidPropertyName) - ?? throw new ArgumentException($"Invalid {nameof(AssetReferenceAttribute)} on type {typeof(TAsset).FullName}. Could not find property '{referenceAttribute.GuidPropertyName}'."); - - // create the resolver - Action resolver; - if (guidProperty.PropertyType == typeof(long)) - resolver = CreateEnumerableReferenceResolver(property, guidProperty); - - else if (guidProperty.PropertyType == typeof(IEnumerable)) - resolver = CreateEnumerableReferenceResolver(property, guidProperty); - - else - throw new ArgumentException($"Invalid {nameof(AssetReferenceAttribute)} on type {typeof(TAsset).FullName}. Property '{referenceAttribute.GuidPropertyName}' must either be of type {typeof(long).FullName} or {typeof(IEnumerable).FullName}."); + Action? resolver = _guidReferenceResolverFactory.CreateResolver(property) + ?? _regionIdReferenceResolverFactory.CreateResolver(property); // keep track of all resolvers - if (!_referenceResolvers.TryGetValue(typeof(TAsset), out List>? resolvers)) + if (resolver != null) { - resolvers = new(); - _referenceResolvers.Add(typeof(TAsset), resolvers); + if (!_referenceResolvers.TryGetValue(typeof(TAsset), out List>? resolvers)) + { + resolvers = new(); + _referenceResolvers.Add(typeof(TAsset), resolvers); + } + resolvers.Add(resolver); } - resolvers.Add(resolver); } + _assetTypes.Add(typeof(TAsset)); + _logger.LogInformation($"Registered asset type '{typeof(TAsset).FullName}'."); } @@ -282,61 +283,27 @@ public void Register() return null; } - private Action CreateSingleReferenceResolver(PropertyInfo referenceProperty, PropertyInfo guidProperty) where TAsset : StandardAsset + private void InitializeStaticAssets() { - // validate the reference property - Type referencedType = referenceProperty.PropertyType; - if (!typeof(StandardAsset).IsAssignableFrom(referencedType)) - throw new ArgumentException(); - - // create the delegate - return (asset) => + foreach (Type assetType in _assetTypes) { - long? guid = guidProperty.GetValue(asset) as long?; - if (guid.HasValue) + foreach (PropertyInfo staticProperty in assetType.GetProperties(BindingFlags.Static | BindingFlags.Public)) { - StandardAsset? referencedAsset = GetReferencedAsset((long) guid, referencedType); - if (referencedAsset != null) - referenceProperty.SetValue(asset, referencedAsset); - } - }; - } - + StaticAssetAttribute? staticAssetAttribute = staticProperty.GetCustomAttribute(); + if (staticAssetAttribute == null) + continue; - private Action CreateEnumerableReferenceResolver(PropertyInfo referenceProperty, PropertyInfo guidProperty) where TAsset : StandardAsset - { - // determine the type of the referenced assets - if (!referenceProperty.PropertyType.IsGenericType) - throw new ArgumentException(); - - // validate the reference property - Type referencedType = referenceProperty.PropertyType.GenericTypeArguments[0]; - if (!typeof(StandardAsset).IsAssignableFrom(referencedType)) - throw new ArgumentException(); - - Type enumerableType = typeof(IEnumerable<>).MakeGenericType(referencedType); - if (referenceProperty.PropertyType != enumerableType) - throw new ArgumentException(); - - // create the delegate - Type listType = typeof(List<>).MakeGenericType(referencedType); - return (asset) => - { - IEnumerable? guids = guidProperty.GetValue(asset) as IEnumerable; - if (guids != null) - { - IList list = (IList)Activator.CreateInstance(listType)!; - - foreach (long guid in guids) + if (TryGet(staticAssetAttribute.GUID, out StandardAsset? asset)) { - StandardAsset? referencedAsset = GetReferencedAsset((long)guid, referencedType); - if (referencedAsset != null) - list.Add(referencedAsset); - } + if (!staticProperty.PropertyType.IsAssignableFrom(asset.GetType())) + throw new Exception($"Could not resolve StaticAsset {assetType.FullName}.{staticProperty.Name}. The asset's type {asset.GetType().FullName} does not match the property's type {staticProperty.PropertyType.FullName}."); - referenceProperty.SetValue(asset, list); + staticProperty.SetValue(null, asset); + } + else + throw new Exception($"Could not resolve StaticAsset {assetType.FullName}.{staticProperty.Name}. There exists no asset with GUID {staticAssetAttribute.GUID}."); } - }; + } } } } diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/FixedIslandRepository.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/FixedIslandRepository.cs index 231607d..7b37bd2 100644 --- a/AnnoMapEditor/DataArchives/Assets/Repositories/FixedIslandRepository.cs +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/FixedIslandRepository.cs @@ -32,11 +32,10 @@ public class FixedIslandRepository : Repository, INotifyCollectionChanged, IEnum public FixedIslandRepository(IDataArchive dataArchive) { _dataArchive = dataArchive; - LoadAsync(); } - protected override async Task DoLoad() + public override async Task InitializeAsync() { _logger.LogInformation($"Begin loading fixed islands."); Stopwatch watch = Stopwatch.StartNew(); @@ -88,7 +87,7 @@ public FixedIslandAsset LoadIslandAsset(String mapFilePath) } catch (Exception e) { - throw new Exception($"Could not load a7minfo '{infoFilePath}'."); + throw new Exception($"Could not load a7minfo '{infoFilePath}'.", e); } // get the island's size in tiles @@ -142,14 +141,13 @@ public FixedIslandAsset LoadIslandAsset(String mapFilePath) } } - FixedIslandAsset fixedIsland = new() + return new() { FilePath = mapFilePath, SizeInTiles = sizeInTiles, Thumbnail = thumbnail, Slots = mineSlots }; - return fixedIsland; } public void Add(FixedIslandAsset fixedIsland) @@ -173,19 +171,6 @@ public bool TryGetByFilePath(string filePath, [NotNullWhen(false)] out FixedIsla #pragma warning restore CS8762 // Parameter must have a non-null value when exiting in some condition. } - - private int ReadSizeInTiles(string mapFilePath) - { - string infoFilePath = mapFilePath + "info"; - - IFileDBDocument? infoDoc = ReadFileDB(infoFilePath); - if (infoDoc?.Roots.FirstOrDefault(x => x.Name == "MapSize" && x.NodeType == FileDBNodeType.Attrib) is not Attrib mapSize) - return 0; - - int sizeInTiles = BitConverter.ToInt32(new ReadOnlySpan(mapSize.Content, 0, 4)); - return sizeInTiles; - } - public IFileDBDocument? ReadFileDB(string mapFilePath) { using Stream? stream = _dataArchive?.OpenRead(mapFilePath); @@ -210,31 +195,6 @@ private int ReadSizeInTiles(string mapFilePath) } } - private BitmapImage? ReadThumbnail(string mapFilePath) - { - string thumbnailPath = Path.Combine( - Path.GetDirectoryName(mapFilePath)!, - "_gamedata", - Path.GetFileNameWithoutExtension(mapFilePath), - "mapimage.png"); - - using Stream? stream = _dataArchive?.OpenRead(thumbnailPath); - if (stream == null) - { - _logger.LogWarning($"Could not load island thumbnail from '{mapFilePath}'. The file could not be found."); - return null; - } - - BitmapImage thumbnail = new(); - thumbnail.BeginInit(); - thumbnail.StreamSource = stream; - thumbnail.CacheOption = BitmapCacheOption.OnLoad; - thumbnail.EndInit(); - thumbnail.Freeze(); - - return thumbnail; - } - public IEnumerator GetEnumerator() => _islands.GetEnumerator(); diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/IslandRepository.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/IslandRepository.cs index c89fe90..47d784d 100644 --- a/AnnoMapEditor/DataArchives/Assets/Repositories/IslandRepository.cs +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/IslandRepository.cs @@ -45,8 +45,6 @@ public IslandRepository(FixedIslandRepository fixedIslandRepository, AssetReposi { _fixedIslandRepository = fixedIslandRepository; _assetRepository = assetRepository; - - LoadAsync(); } @@ -67,17 +65,9 @@ public bool TryGetByFilePath(string mapFilePath, [NotNullWhen(false)] out Island => _byFilePath.TryGetValue(mapFilePath, out islandAsset); #pragma warning restore CS8762 // Parameter must have a non-null value when exiting in some condition. - protected override async Task DoLoad() + public override Task InitializeAsync() { _logger.LogInformation($"Begin loading islands."); - _logger.LogInformation($"Waiting for random and fixed islands to be loaded."); - - // wait for both the random and fixed island repositories to be loaded - // TODO: BUG In theory both randomIslandRepositoryand fixedIslandRepository should be - // able to initialize at the same time. However, there occurs a deadlock when - // doing so. - await _assetRepository.AwaitLoadingAsync(); - await _fixedIslandRepository.AwaitLoadingAsync(); Dictionary randomByFilePath = _assetRepository .GetAll() @@ -92,7 +82,7 @@ protected override async Task DoLoad() randomByFilePath.TryGetValue(filePath, out RandomIslandAsset? randomIsland); IslandSize islandSize = IslandSize.All.FirstOrDefault(s => fixedIsland.SizeInTiles <= s.DefaultSizeInTiles)!; - + // resolve slot guids to assets ignoring WorkAreas foreach (Slot slot in fixedIsland.Slots.Values) { @@ -105,7 +95,7 @@ protected override async Task DoLoad() FilePath = filePath, DisplayName = randomIsland?.Name ?? Path.GetFileNameWithoutExtension(filePath), Thumbnail = fixedIsland.Thumbnail, - Region = randomIsland?.IslandRegion ?? Region.DetectFromPath(filePath), + Region = randomIsland?.IslandRegion ?? RegionAsset.DetectFromPath(filePath), IslandDifficulty = randomIsland?.IslandDifficulty ?? new[] { IslandDifficulty.Normal }, IslandType = randomIsland?.IslandType ?? new[] { DetectIslandTypeFromPath(filePath) }, IslandSize = new[] { islandSize }, @@ -115,6 +105,7 @@ protected override async Task DoLoad() } _logger.LogInformation($"Finished loading {_fixedIslandRepository.Count()} islands."); + return Task.CompletedTask; } diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/MapGroupRepository.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/MapGroupRepository.cs new file mode 100644 index 0000000..492af2d --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/MapGroupRepository.cs @@ -0,0 +1,49 @@ +using AnnoMapEditor.UI.Windows.Main; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace AnnoMapEditor.DataArchives.Assets.Repositories +{ + public class MapGroupRepository : Repository + { + public IEnumerable MapGroups + { + get => _mapGroups ?? throw new Exception("MapRepository has not yet been initialized."); + private set => SetProperty(ref _mapGroups, value); + } + private IEnumerable? _mapGroups; + + private readonly IDataArchive _dataArchive; + + + public MapGroupRepository(IDataArchive dataArchive) + { + _dataArchive = dataArchive; + } + + + public override Task InitializeAsync() + { + IEnumerable mapTemplatePaths = _dataArchive.Find("*.a7tinfo"); + + MapGroups = new MapGroup[] + { + new MapGroup("Campaign", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/campaign")), new(@"\/campaign_([^\/]+)\.")), + new MapGroup("Moderate, Archipelago", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_archipel")), new(@"\/([^\/]+)\.")), + new MapGroup("Moderate, Atoll", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_atoll")), new(@"\/([^\/]+)\.")), + new MapGroup("Moderate, Corners", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_corners")), new(@"\/([^\/]+)\.")), + new MapGroup("Moderate, Island Arc", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_islandarc")), new(@"\/([^\/]+)\.")), + new MapGroup("Moderate, Snowflake", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_snowflake")), new(@"\/([^\/]+)\.")), + new MapGroup("New World, Large", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_l_")), new(@"\/([^\/]+)\.")), + new MapGroup("New World, Medium", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_m_")), new(@"\/([^\/]+)\.")), + new MapGroup("New World, Small", mapTemplatePaths.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_s_")), new(@"\/([^\/]+)\.")), + new MapGroup("DLCs", mapTemplatePaths.Where(x => !x.StartsWith(@"data/sessions/")), new(@"data\/([^\/]+)\/.+\/maps\/([^\/]+)")) + //new MapGroup("Moderate", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate")), new(@"\/([^\/]+)\.")) + }; + + return Task.CompletedTask; + } + } +} diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/Repository.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/Repository.cs index c8575f8..ee58809 100644 --- a/AnnoMapEditor/DataArchives/Assets/Repositories/Repository.cs +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/Repository.cs @@ -1,46 +1,15 @@ using AnnoMapEditor.Utilities; -using System; using System.Threading.Tasks; namespace AnnoMapEditor.DataArchives.Assets.Repositories { public abstract class Repository : ObservableBase { - private Task? _loadingTask; - - public bool IsLoaded - { - get => _isLoading; - private set => SetProperty(ref _isLoading, value); - } - private bool _isLoading = false; - - public Repository() { } - public Task LoadAsync() - { - if (_loadingTask != null) - throw new InvalidOperationException(); - - _loadingTask = Task.Run(async () => { - await DoLoad(); - IsLoaded = true; - }); - return _loadingTask; - } - - protected abstract Task DoLoad(); - - public async Task AwaitLoadingAsync() - { - if (_loadingTask != null) - await _loadingTask; - else - throw new Exception($"LoadAsync has not been called."); - } + public abstract Task InitializeAsync(); } } diff --git a/AnnoMapEditor/DataArchives/Assets/Repositories/StaticAssetAttribute.cs b/AnnoMapEditor/DataArchives/Assets/Repositories/StaticAssetAttribute.cs new file mode 100644 index 0000000..385fcc5 --- /dev/null +++ b/AnnoMapEditor/DataArchives/Assets/Repositories/StaticAssetAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace AnnoMapEditor.DataArchives.Assets.Repositories +{ + public class StaticAssetAttribute : Attribute + { + public long GUID { get; init; } + + + public StaticAssetAttribute(long guid) + { + GUID = guid; + } + } +} diff --git a/AnnoMapEditor/DataArchives/DataArchive.cs b/AnnoMapEditor/DataArchives/DataArchive.cs index 1369042..672ce00 100644 --- a/AnnoMapEditor/DataArchives/DataArchive.cs +++ b/AnnoMapEditor/DataArchives/DataArchive.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -14,38 +13,6 @@ namespace AnnoMapEditor.DataArchives { public abstract class DataArchive : ObservableBase, IDataArchive { - public static readonly IDataArchive Default = new InvalidDataArchive(""); - - - public abstract bool IsValid { get; protected set; } - - public abstract string DataPath { get; } - - - public static async Task OpenAsync(string? folderPath) - { - if (folderPath is null) - return Default; - - var adjustedPath = AdjustDataPath(folderPath); - - if (adjustedPath is null) - return Default; - - IDataArchive archive; - if (File.Exists(Path.Combine(adjustedPath, "maindata/data0.rda"))) - { - RdaDataArchive rdaArchive = new RdaDataArchive(adjustedPath); - await rdaArchive.LoadAsync(); - archive = rdaArchive; - } - else - archive = new FolderDataArchive(adjustedPath); - - return archive; - } - - public abstract Stream? OpenRead(string path); public abstract IEnumerable Find(string pattern); diff --git a/AnnoMapEditor/DataArchives/DataArchiveFactory.cs b/AnnoMapEditor/DataArchives/DataArchiveFactory.cs new file mode 100644 index 0000000..1316dc0 --- /dev/null +++ b/AnnoMapEditor/DataArchives/DataArchiveFactory.cs @@ -0,0 +1,76 @@ +using AnnoMapEditor.Utilities; +using AnnoRDA; +using AnnoRDA.Builder; +using System; + +namespace AnnoMapEditor.DataArchives +{ + public class DataArchiveFactory : IDataArchiveFactory + { + private static readonly Logger _logger = new(); + + private static readonly string[] EXTENSION_WHITELIST = new[] { "*.a7tinfo", "*.png", "*.a7minfo", "*.a7t", "*.a7te", "assets.xml", "*.a7m", "*.dds" }; + + + public IDataArchive CreateDataArchive(string dataPath) + { + // RdaDataArchive + try + { + _logger.LogInformation($"Trying to load '{dataPath}' as a RdaDataArchive."); + return CreateRdaDataArchive(dataPath); + } + catch (Exception ex) + { + _logger.LogWarning(ex.Message); + } + + // FolderDataArchive + try + { + _logger.LogInformation($"Trying to load '{dataPath}' as a FolderDataArchive."); + return CreateFolderDataArchive(dataPath); + } + catch (Exception ex) + { + _logger.LogWarning(ex.Message); + } + + throw new Exception($"Could not find valid Anno 1800 data at '{dataPath}'."); + } + + public IDataArchive CreateRdaDataArchive(string dataPath) + { + _logger.LogInformation($"Discovering RDA archives at '{dataPath}'."); + + FileSystemBuilder builder = FileSystemBuilder.Create() + .FromPath(dataPath) + .OnlyArchivesMatchingWildcard("data*.rda") + .WithDefaultSorting() + .ConfigureLoadZeroByteFiles(false) + .AddWhitelisted(EXTENSION_WHITELIST); + + FileSystem fileSystem; + try + { + fileSystem = builder.Build(); + } + catch (Exception e) + { + throw new Exception($"Could not build RdaFileSystem at '{dataPath}'.", e); + } + + var loadedCount = builder.ArchiveFileNames.Count; + if (loadedCount == 0) + throw new Exception($"Could not find any .rda files at '{dataPath}'."); + + _logger.LogInformation($"Loaded {loadedCount} RDAs."); + return new RdaDataArchive(fileSystem); + } + + public IDataArchive CreateFolderDataArchive(string dataPath) + { + return new FolderDataArchive(dataPath); + } + } +} diff --git a/AnnoMapEditor/DataArchives/DataManager.cs b/AnnoMapEditor/DataArchives/DataManager.cs new file mode 100644 index 0000000..0516bad --- /dev/null +++ b/AnnoMapEditor/DataArchives/DataManager.cs @@ -0,0 +1,125 @@ +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives.Assets.Repositories; +using AnnoMapEditor.Utilities; +using System; +using System.Threading.Tasks; +using System.Windows; + +namespace AnnoMapEditor.DataArchives +{ + public class DataManager : ObservableBase + { + private const string NOT_INITIALIZED_MESSAGE = "DataManager has not yet been initialized."; + + public static readonly Logger _logger = new(); + + public static readonly DataManager Instance = new(); + + + public bool IsInitialized + { + get => _isInitialized; + private set => SetProperty(ref _isInitialized, value); + } + private bool _isInitialized = false; + + public bool IsInitializing + { + get => _isInitializing; + private set => SetProperty(ref _isInitializing, value); + } + private bool _isInitializing = false; + + public string? ErrorMessage + { + get => _errorMessage; + private set => SetProperty(ref _errorMessage, value); + } + private string? _errorMessage; + + public bool HasError => _errorMessage != null; + + + public IDataArchive DataArchive => _isInitialized && _dataArchive != null ? _dataArchive : throw new Exception(NOT_INITIALIZED_MESSAGE); + private IDataArchive? _dataArchive; + + public AssetRepository AssetRepository => _isInitialized && _assetRepository != null ? _assetRepository : throw new Exception(NOT_INITIALIZED_MESSAGE); + private AssetRepository? _assetRepository; + + public FixedIslandRepository FixedIslandRepository => _isInitialized && _fixedIslandRepository != null ? _fixedIslandRepository : throw new Exception(NOT_INITIALIZED_MESSAGE); + private FixedIslandRepository? _fixedIslandRepository; + + public IslandRepository IslandRepository => _isInitialized && _islandRepository != null ? _islandRepository : throw new Exception(NOT_INITIALIZED_MESSAGE); + private IslandRepository? _islandRepository; + + public MapGroupRepository MapGroupRepository => _isInitialized && _mapGroupRepository != null ? _mapGroupRepository : throw new Exception(NOT_INITIALIZED_MESSAGE); + private MapGroupRepository? _mapGroupRepository; + + + private DataManager() + { + + } + + + public async Task TryInitializeAsync(string dataPath) + { + UpdateStatus(isInitializing: true, isInitialized: false); + _logger.LogInformation($"Initializing DataManager at '{dataPath}'."); + + try + { + DataArchiveFactory dataArchiveFactory = new(); + _dataArchive = dataArchiveFactory.CreateDataArchive(dataPath); + + _assetRepository = new AssetRepository(_dataArchive); + _assetRepository.Register(); + _assetRepository.Register(); + _assetRepository.Register(); + _assetRepository.Register(); + _assetRepository.Register(); + _assetRepository.Register(); + _assetRepository.Register(); + await _assetRepository.InitializeAsync(); + + _fixedIslandRepository = new FixedIslandRepository(_dataArchive); + await _fixedIslandRepository.InitializeAsync(); + + _islandRepository = new IslandRepository(_fixedIslandRepository, _assetRepository); + await _islandRepository.InitializeAsync(); + + _mapGroupRepository = new MapGroupRepository(_dataArchive); + _mapGroupRepository.InitializeAsync(); + } + catch (Exception ex) + { + UpdateStatus(isInitializing: false, isInitialized: false, errorMessage: ex.Message); + _logger.LogInformation($"Could not initialize DataManager at '{dataPath}'."); + return; + } + + UpdateStatus(isInitializing: false, isInitialized: true); + _logger.LogInformation($"Successfully initialized DataManager at '{dataPath}'."); + } + + + private void Dispatch(Action action) + { + if (Application.Current?.Dispatcher != null) + Application.Current.Dispatcher.Invoke(action); + else + action(); + } + + private void UpdateStatus(bool isInitializing, bool isInitialized, string? errorMessage = null) + { + Dispatch(() => + { + IsInitializing = isInitializing; + IsInitialized = isInitialized; + ErrorMessage = errorMessage; + OnPropertyChanged(nameof(HasError)); + }); + } + } +} diff --git a/AnnoMapEditor/DataArchives/FolderDataArchive.cs b/AnnoMapEditor/DataArchives/FolderDataArchive.cs index 301ac45..d25607d 100644 --- a/AnnoMapEditor/DataArchives/FolderDataArchive.cs +++ b/AnnoMapEditor/DataArchives/FolderDataArchive.cs @@ -1,50 +1,42 @@ using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; namespace AnnoMapEditor.DataArchives { public class FolderDataArchive : DataArchive { - public override string DataPath { get; } + public string DataPath { get; } - public override bool IsValid { get; protected set; } = true; - - public FolderDataArchive(string folderPath) + public FolderDataArchive(string dataPath) { - DataPath = folderPath; + DataPath = dataPath; } public override Stream? OpenRead(string filePath) { - if (!IsValid) - return null; - Stream? stream = null; - try { - stream = File.OpenRead(System.IO.Path.Combine(DataPath, filePath)); + return File.OpenRead(Path.Combine(DataPath, filePath)); + } + catch + { + return null; } - catch { } - return stream; } public override IEnumerable Find(string pattern) { - if (!IsValid) - return Array.Empty(); - Matcher matcher = new(); matcher.AddIncludePatterns(new string[] { pattern }); PatternMatchingResult result = matcher.Execute( - new DirectoryInfoWrapper(new DirectoryInfo(DataPath))); + new DirectoryInfoWrapper(new DirectoryInfo(DataPath)) + ); return result.Files.Select(x => x.Path); } diff --git a/AnnoMapEditor/DataArchives/IDataArchive.cs b/AnnoMapEditor/DataArchives/IDataArchive.cs index 9d5581c..48f910f 100644 --- a/AnnoMapEditor/DataArchives/IDataArchive.cs +++ b/AnnoMapEditor/DataArchives/IDataArchive.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -9,11 +8,6 @@ namespace AnnoMapEditor.DataArchives { public interface IDataArchive { - bool IsValid { get; } - - string DataPath { get; } - - Stream? OpenRead(string filePath); IEnumerable Find(string pattern); diff --git a/AnnoMapEditor/DataArchives/IDataArchiveFactory.cs b/AnnoMapEditor/DataArchives/IDataArchiveFactory.cs new file mode 100644 index 0000000..2096b2d --- /dev/null +++ b/AnnoMapEditor/DataArchives/IDataArchiveFactory.cs @@ -0,0 +1,7 @@ +namespace AnnoMapEditor.DataArchives +{ + public interface IDataArchiveFactory + { + IDataArchive CreateDataArchive(string dataPath); + } +} diff --git a/AnnoMapEditor/DataArchives/InvalidDataArchive.cs b/AnnoMapEditor/DataArchives/InvalidDataArchive.cs deleted file mode 100644 index b804987..0000000 --- a/AnnoMapEditor/DataArchives/InvalidDataArchive.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Threading.Tasks; -using System.Windows.Media; -using System.Windows.Media.Imaging; - -namespace AnnoMapEditor.DataArchives -{ - public class InvalidDataArchive : IDataArchive - { - public bool IsValid { get; } = false; - public Stream? OpenRead(string filePath) => null; - public string DataPath { get; } - - - public InvalidDataArchive(string path) - { - DataPath = path; - } - - - public Task LoadAsync() - { - return Task.Run(() => { }); - } - - public IEnumerable Find(string pattern) - { - return Array.Empty(); - } - - public BitmapImage? TryLoadPng(string pngPath) - { - throw new NotImplementedException(); - } - - public ImageSource? TryLoadIcon(string iconPath, Point? desiredSize = null) - { - throw new NotImplementedException(); - } - } -} diff --git a/AnnoMapEditor/DataArchives/RdaDataArchive.cs b/AnnoMapEditor/DataArchives/RdaDataArchive.cs index 872cf82..72ad96b 100644 --- a/AnnoMapEditor/DataArchives/RdaDataArchive.cs +++ b/AnnoMapEditor/DataArchives/RdaDataArchive.cs @@ -1,91 +1,27 @@ using AnnoMapEditor.Utilities; using AnnoRDA; -using AnnoRDA.Builder; -using FileDBReader.src.XmlRepresentation; using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; namespace AnnoMapEditor.DataArchives { - public class RdaDataArchive : DataArchive, IDataArchive + public class RdaDataArchive : DataArchive { private static readonly Logger _logger = new(); - public override string DataPath { get; } - public bool IsLoaded - { - get => _isLoading; - private set => SetProperty(ref _isLoading, value); - } - private bool _isLoading = false; - - public override bool IsValid - { - get => _isValid; - protected set => SetProperty(ref _isValid, value); - } - private bool _isValid = false; + private readonly FileSystem _fileSystem; - private FileSystem _fileSystem; - - public RdaDataArchive(string folderPath) + public RdaDataArchive(FileSystem fileSystem) { - DataPath = folderPath; - IsValid = false; + _fileSystem = fileSystem; } - - public async Task LoadAsync() - { - IsLoaded = false; - IsValid = false; - _logger.LogInformation($"Discovering RDA archives at '{DataPath}'."); - var builder = FileSystemBuilder.Create() - .FromPath(System.IO.Path.Combine(DataPath, "maindata")) - .OnlyArchivesMatchingWildcard("data*.rda") - .WithDefaultSorting() - .ConfigureLoadZeroByteFiles(false) - .AddWhitelisted("*.a7tinfo", "*.png", "*.a7minfo", "*.a7t", "*.a7te", "assets.xml", "*.a7m", "*.dds"); - - await Task.Run(() => - { - try - { - _fileSystem = builder.Build(); - } - catch (Exception e){ - _logger.LogError($"error loading RDAs from {DataPath}", e); - IsValid = false; - return; - } - }); - - var loadedCount = builder.ArchiveFileNames.Count(); - if (loadedCount == 0) - { - _logger.LogWarning($"No .rda files found at {System.IO.Path.Combine(DataPath, "maindata")}"); - MessageBox.Show($"Something went wrong opening the RDA files.\n\nDo you have another Editor or the RDAExplorer open by any chance?\n\nLog file: {Log.LogFilePath}", App.TitleShort, MessageBoxButton.OK, MessageBoxImage.Exclamation); - } - _logger.LogInformation($"Loaded {loadedCount} RDAs."); - IsLoaded = true; - IsValid = true; - } - public override Stream? OpenRead(string filePath) { - if (!IsValid) - { - _logger.LogWarning($"archive not ready: {filePath}"); - return null; - } - filePath = filePath.Replace("\\", "/"); try { diff --git a/AnnoMapEditor/MapTemplates/Enums/Region.cs b/AnnoMapEditor/MapTemplates/Enums/Region.cs deleted file mode 100644 index 25ab007..0000000 --- a/AnnoMapEditor/MapTemplates/Enums/Region.cs +++ /dev/null @@ -1,126 +0,0 @@ -using AnnoMapEditor.Utilities; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace AnnoMapEditor.MapTemplates.Enums -{ - public class Region - { - private static readonly Logger _logger = new(); - - - //Technically, Cape Trelawney is in Moderate Region but has ambientName "Moderate_01_day_night_st", - //but we don't allow Mod exports for that anyways - public static readonly Region Moderate = new(5000000, "Moderate", "Moderate", "Moderate_01_day_night", allowModding:true, "moderate", - new[] { "ll", "lm", "ls", "ml", "mm", "ms", "sl", "sm", "ss" }, - new[] { "01", "02" }, - usesAllSizeIndices:false, hasMapExtension:false); - - public static readonly Region NewWorld = new(5000001, "Colony01", "New World", "south_america_caribic_01", allowModding: true, "colony01", - new[] { "s", "m", "l"}, - new[] { "01", "02", "03" }, - usesAllSizeIndices: true, hasMapExtension: true); - - //poolFolderName is manually selected, the game files don't have a special one for the arctic as it only has one map - public static readonly Region Arctic = new(160001, "Arctic", "Arctic", "DLC03_01", allowModding: false, poolFolderName:"colony03", - new[] { "sp" }, - new[] { "" }, - usesAllSizeIndices: true, hasMapExtension: false); - - public static readonly Region Enbesa = new(114327, "Africa", "Enbesa", "Colony_02", allowModding: false, "land_of_lions", - new[] { "01" }, - new[] { "", "mp" }, - usesAllSizeIndices: true, hasMapExtension: false); - - public static readonly Region[] All = new Region[] { Moderate, NewWorld, Arctic, Enbesa }; - - - public long AssetGuid { get; init; } - - public string RegionID { get; init; } - - public string Name { get; init; } - - public string AmbientName { get; init; } - - public bool AllowModding { get; init; } - - public string PoolFolderName { get; init; } - - public IReadOnlyCollection MapSizes { get; init; } - - public IReadOnlyCollection MapSizeIndices { get; init; } - - public bool UsesAllSizeIndices { get; init; } - - public bool HasMapExtension { get; init; } - - - private Region(long assetGuid, string regionId, string name, string ambientName, bool allowModding, string poolFolderName, - string[] mapSizes, string[] sizeIndices, bool usesAllSizeIndices, bool hasMapExtension) - { - AssetGuid = assetGuid; - RegionID = regionId; - Name = name; - AmbientName = ambientName; - AllowModding = allowModding; - - PoolFolderName = poolFolderName; - MapSizes = new ReadOnlyCollection(mapSizes); - MapSizeIndices = new ReadOnlyCollection(sizeIndices); - - UsesAllSizeIndices = usesAllSizeIndices; - HasMapExtension = hasMapExtension; - } - - public IEnumerable GetAllSizeCombinations() - { - if (UsesAllSizeIndices) - { - foreach (string size in MapSizes) - { - foreach (string subsize in MapSizeIndices) - { - string result = size + (string.IsNullOrEmpty(size) || string.IsNullOrEmpty(subsize) ? "" : "_") + subsize; - yield return result; - } - } - } - else - { - foreach (string size in MapSizes) - { - yield return size; - } - } - } - - public static Region DetectFromPath(string filePath) - { - if (filePath.Contains("colony01") || filePath.Contains("ggj") || filePath.Contains("scenario03")) - return NewWorld; - else if (filePath.Contains("dlc03") || filePath.Contains("colony_03")) - return Arctic; - else if (filePath.Contains("dlc06") || filePath.Contains("colony02") || filePath.Contains("scenario02")) - return Enbesa; - return Moderate; - } - - public static Region FromRegionId(string regionId) - { - Region? region = All.FirstOrDefault(t => t.RegionID == regionId); - - if (region is null) - { - _logger.LogWarning($"{regionId} is not a valid RegionID for {nameof(Region)}. Defaulting to {Moderate.RegionID}."); - region = Moderate; - } - - return region; - } - - - public override string ToString() => Name; - } -} diff --git a/AnnoMapEditor/MapTemplates/Models/FixedIslandElement.cs b/AnnoMapEditor/MapTemplates/Models/FixedIslandElement.cs index 0a8f23a..8487693 100644 --- a/AnnoMapEditor/MapTemplates/Models/FixedIslandElement.cs +++ b/AnnoMapEditor/MapTemplates/Models/FixedIslandElement.cs @@ -1,4 +1,5 @@ using Anno.FileDBModels.Anno1800.MapTemplate; +using AnnoMapEditor.DataArchives; using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.DataArchives.Assets.Repositories; using AnnoMapEditor.MapTemplates.Enums; @@ -75,21 +76,6 @@ public bool RandomizeSlots public Dictionary SlotAssignments { get; init; } = new(); - /// - /// For tracking if the island needs yet to be loaded. - /// - public bool DelayedLoading - { - get => _delayedLoading; - private set => SetProperty(ref _delayedLoading, value); - } - private bool _delayedLoading = false; - - /// - /// Used for tracking the original label during delayed loading. - /// - private string? prevLabel; - public FixedIslandElement(IslandAsset islandAsset, IslandType islandType) : base(islandType) @@ -124,24 +110,7 @@ public FixedIslandElement(Element sourceElement) : base(sourceElement) _randomizeFertilities = sourceElement.RandomizeFertilities != false; _randomizeSlots = sourceElement.MineSlotMapping == null || sourceElement.MineSlotMapping.Count == 0; - if (Settings.Instance.IsLoading) - { - DelayedLoading = true; - //Show island file if not labelled, while still loading - prevLabel = Label; - if (string.IsNullOrEmpty(Label)) - { - Label = System.IO.Path.GetFileNameWithoutExtension(islandFilePath); - } - SetDummyAsset(sourceElement); - System.Diagnostics.Debug.WriteLine($"Queueing item {sourceElement!.MapFilePath} for LoadingFinished."); - Settings.Instance.LoadingFinished += DelayedLoadAssetData; - } - else - { - LoadIslandDataFromRepository(sourceElement, false); - } - + LoadIslandDataFromRepository(sourceElement); } /// @@ -149,19 +118,15 @@ public FixedIslandElement(Element sourceElement) : base(sourceElement) /// Should only be called when the IslandRepository is actually loaded, errors otherwise. /// /// The element from the template. - /// Whether this is a delayed loading call (which means cleanup needs to be done after loading). /// The sourceElement does not have a MapFilePath. This is forbidden on FixedIslands. - /// The IslandRepository either does not exist or has not finished loading yet. /// The given MapFilePath does not match any islands in the IslandRepository. [MemberNotNull(nameof(_islandAsset))] - private void LoadIslandDataFromRepository(Element sourceElement, bool delayed) + private void LoadIslandDataFromRepository(Element sourceElement) { string islandFilePath = sourceElement.MapFilePath ?? throw new ArgumentException($"Missing property '{nameof(Element.MapFilePath)}'."); - IslandRepository islandRepository = Settings.Instance.IslandRepository - ?? throw new Exception($"No {nameof(IslandRepository)} could be found."); - if (!islandRepository.IsLoaded) - throw new Exception($"The {nameof(IslandRepository)} has not been loaded."); + + IslandRepository islandRepository = DataManager.Instance.IslandRepository; if (!islandRepository.TryGetByFilePath(islandFilePath, out var islandAsset)) throw new NullReferenceException($"Unknown island '{islandFilePath}'."); @@ -169,8 +134,7 @@ private void LoadIslandDataFromRepository(Element sourceElement, bool delayed) IslandAsset = islandAsset; //Rotation is not asset bound, thus loaded in constructor - AssetRepository assetRepository = Settings.Instance.AssetRepository - ?? throw new Exception($"The {nameof(AssetRepository)} has not been loaded."); + AssetRepository assetRepository = DataManager.Instance.AssetRepository; // fertilities // _randomizeFertilities is loaded in constructor. @@ -230,19 +194,6 @@ private void LoadIslandDataFromRepository(Element sourceElement, bool delayed) }); } } - - //Deregister when delayed loading - if (delayed) - { - Settings.Instance.LoadingFinished -= DelayedLoadAssetData; - - if(Label != prevLabel) - { - Label = prevLabel; - } - - DelayedLoading = false; - } } /// @@ -265,7 +216,7 @@ private void SetDummyAsset(Element sourceElement) FilePath = islandFilePath, DisplayName = System.IO.Path.GetFileNameWithoutExtension(islandFilePath), Thumbnail = null, - Region = Region.DetectFromPath(islandFilePath), + Region = RegionAsset.DetectFromPath(islandFilePath), IslandDifficulty = new[] { islandDifficulty }, IslandType = new[] { IslandRepository.DetectIslandTypeFromPath(islandFilePath) }, IslandSize = new[] { islandSize }, @@ -276,11 +227,6 @@ private void SetDummyAsset(Element sourceElement) IslandAsset = dummyAsset; } - private void DelayedLoadAssetData(object? sender, EventArgs _) - { - LoadIslandDataFromRepository(_sourceElement!, true); - } - protected override void ToTemplate(Element resultElement) { base.ToTemplate(resultElement); diff --git a/AnnoMapEditor/MapTemplates/Models/MapTemplate.cs b/AnnoMapEditor/MapTemplates/Models/MapTemplate.cs index 5c07a01..3f9f092 100644 --- a/AnnoMapEditor/MapTemplates/Models/MapTemplate.cs +++ b/AnnoMapEditor/MapTemplates/Models/MapTemplate.cs @@ -1,5 +1,5 @@ using Anno.FileDBModels.Anno1800.MapTemplate; -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.Utilities; using System; using System.Collections.Generic; @@ -27,12 +27,12 @@ public Rect2 PlayableArea } private Rect2 _playableAea = new(); - public Region Region + public SessionAsset Session { - get => _region; - set => SetProperty(ref _region, value); + get => _session; + set => SetProperty(ref _session, value); } - private Region _region; + private SessionAsset _session; public bool ResizingInProgress { @@ -50,15 +50,15 @@ public bool ResizingInProgress public string MapSizeText => $"Size: {Size.X}, Playable: {PlayableArea.Width}"; - public MapTemplate(Region region) + public MapTemplate(SessionAsset session) { - _region = region; + _session = session; _templateDocument = new MapTemplateDocument(); } - public MapTemplate(MapTemplateDocument document, Region region) + public MapTemplate(MapTemplateDocument document, SessionAsset session) { - _region = region; + _session = session; _size = new Vector2(document.MapTemplate?.Size); _playableAea = new Rect2(document.MapTemplate?.PlayableArea); _templateDocument = document; @@ -80,7 +80,7 @@ public MapTemplate(MapTemplateDocument document, Region region) _templateDocument.MapTemplate.TemplateElement = null; } - public MapTemplate(int mapSize, int playableSize, Region region) + public MapTemplate(int mapSize, int playableSize, SessionAsset session) { int margin = (mapSize - playableSize) / 2; @@ -94,7 +94,7 @@ public MapTemplate(int mapSize, int playableSize, Region region) } }; - _region = region; + _session = session; _size = new Vector2(_templateDocument.MapTemplate.Size); _playableAea = new Rect2(_templateDocument.MapTemplate.PlayableArea); @@ -182,7 +182,7 @@ public void ResizeAndCommitMapTemplate(int mapSize, (int x1, int y1, int x2, int _templateDocument.MapTemplate.TemplateElement = new List(Elements.Select(x => x.ToTemplate()).Where(x => x is not null)!); _templateDocument.MapTemplate.ElementCount = _templateDocument.MapTemplate.TemplateElement.Count; - if (Region.HasMapExtension && writeInitialArea) + if (Session == SessionAsset.NewWorld) _templateDocument.MapTemplate.InitialPlayableArea = _templateDocument.MapTemplate.PlayableArea; else _templateDocument.MapTemplate.InitialPlayableArea = null; diff --git a/AnnoMapEditor/MapTemplates/Pool.cs b/AnnoMapEditor/MapTemplates/Pool.cs index 8b77498..41cd44b 100644 --- a/AnnoMapEditor/MapTemplates/Pool.cs +++ b/AnnoMapEditor/MapTemplates/Pool.cs @@ -1,4 +1,5 @@ -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.MapTemplates.Enums; using System; using System.Collections.Generic; @@ -9,13 +10,13 @@ public class Pool public static readonly IEnumerable All = new List() { // Moderate - new(Region.Moderate, IslandSize.Small, + new(RegionAsset.Moderate, IslandSize.Small, "data/sessions/islands/pool/moderate/moderate_s_{0}/moderate_s_{0}.a7m", 12 ), - new(Region.Moderate, IslandSize.Medium, + new(RegionAsset.Moderate, IslandSize.Medium, "data/sessions/islands/pool/moderate/moderate_m_{0}/moderate_m_{0}.a7m", 9 ), - new(Region.Moderate, IslandSize.Large, + new(RegionAsset.Moderate, IslandSize.Large, new FilePathRange[] { new FilePathRange("data/sessions/islands/pool/moderate/moderate_l_{0}/moderate_l_{0}.a7m", 1, 14), @@ -23,19 +24,19 @@ public class Pool }), // NewWorld - new(Region.NewWorld, IslandSize.Small, + new(RegionAsset.SouthAmerica, IslandSize.Small, new FilePathRange[] { new FilePathRange("data/sessions/islands/pool/colony01/colony01_s_{0}/colony01_s_{0}.a7m", 1, 4), new FilePathRange("data/dlc12/sessions/islands/pool/colony01/colony01_s_{0}/colony01_s_{0}.a7m", 5, 3) }), - new(Region.NewWorld, IslandSize.Medium, + new(RegionAsset.SouthAmerica, IslandSize.Medium, new FilePathRange[] { new FilePathRange("data/sessions/islands/pool/colony01/colony01_m_{0}/colony01_m_{0}.a7m", 1, 6), new FilePathRange("data/dlc12/sessions/islands/pool/colony01/colony01_m_{0}/colony01_m_{0}.a7m", 7, 3) }), - new(Region.NewWorld, IslandSize.Large, + new(RegionAsset.SouthAmerica, IslandSize.Large, new FilePathRange[] { new FilePathRange("data/sessions/islands/pool/colony01/colony01_l_{0}/colony01_l_{0}.a7m", 1, 5), @@ -44,17 +45,17 @@ public class Pool }), // Arctic - new(Region.Arctic, IslandSize.Small, "data/dlc03/sessions/islands/pool/colony03_a01_{0}/colony03_a01_{0}.a7m", 8), - new(Region.Arctic, IslandSize.Medium, "data/dlc03/sessions/islands/pool/colony03_a02_{0}/colony03_a02_{0}.a7m", 4), - new(Region.Arctic, IslandSize.Large, "data/dlc03/sessions/islands/pool/moderate/moderate_l_{0}/moderate_l_{0}.a7m", 14), + new(RegionAsset.Arctic, IslandSize.Small, "data/dlc03/sessions/islands/pool/colony03_a01_{0}/colony03_a01_{0}.a7m", 8), + new(RegionAsset.Arctic, IslandSize.Medium, "data/dlc03/sessions/islands/pool/colony03_a02_{0}/colony03_a02_{0}.a7m", 4), + new(RegionAsset.Arctic, IslandSize.Large, "data/dlc03/sessions/islands/pool/moderate/moderate_l_{0}/moderate_l_{0}.a7m", 14), // Enbesa - new(Region.Enbesa, IslandSize.Small, "data/dlc06/sessions/islands/pool/colony02_s_{0}/colony02_s_{0}.a7m", new int[] { 1, 2, 3, 5 }), - new(Region.Enbesa, IslandSize.Medium, "data/dlc06/sessions/islands/pool/colony02_m_{0}/colony02_m_{0}.a7m", new int[] { 2, 4, 5, 9 }), - new(Region.Enbesa, IslandSize.Large, "data/dlc06/sessions/islands/pool/colony02_l_{0}/colony02_l_{0}.a7m", new int[] { 1, 3, 5, 6 }), + new(RegionAsset.Africa, IslandSize.Small, "data/dlc06/sessions/islands/pool/colony02_s_{0}/colony02_s_{0}.a7m", new int[] { 1, 2, 3, 5 }), + new(RegionAsset.Africa, IslandSize.Medium, "data/dlc06/sessions/islands/pool/colony02_m_{0}/colony02_m_{0}.a7m", new int[] { 2, 4, 5, 9 }), + new(RegionAsset.Africa, IslandSize.Large, "data/dlc06/sessions/islands/pool/colony02_l_{0}/colony02_l_{0}.a7m", new int[] { 1, 3, 5, 6 }), }; - private static readonly Dictionary<(Region, IslandSize), Pool> _poolsMap; + private static readonly Dictionary<(RegionAsset, IslandSize), Pool> _poolsMap; static Pool() { _poolsMap = new(); foreach (Pool pool in All) @@ -64,17 +65,17 @@ static Pool() { } - public readonly Region Region; + public readonly RegionAsset Region; public readonly IslandSize IslandSize; - public static Pool GetPool(Region region, IslandSize islandSize) + public static Pool GetPool(RegionAsset region, IslandSize islandSize) { return _poolsMap[(region, islandSize)]; } - public static string GetRandomIslandPath(Region region, IslandSize islandSize) + public static string GetRandomIslandPath(RegionAsset region, IslandSize islandSize) { // use a random Small island for IslandSize.Default if (islandSize == IslandSize.Default) @@ -119,14 +120,14 @@ public string GetPath(int i) } - public Pool(Region region, IslandSize islandSize, FilePathRange[] paths) + public Pool(RegionAsset region, IslandSize islandSize, FilePathRange[] paths) { Region = region; IslandSize = islandSize; _paths = paths; } - public Pool(Region region, IslandSize islandSize, string filePath, int size) + public Pool(RegionAsset region, IslandSize islandSize, string filePath, int size) : this(region, islandSize, new FilePathRange[] { new FilePathRange(filePath, 1, size) @@ -134,7 +135,7 @@ public Pool(Region region, IslandSize islandSize, string filePath, int size) { } - public Pool(Region region, IslandSize islandSize, string filePath, int[] ids) + public Pool(RegionAsset region, IslandSize islandSize, string filePath, int[] ids) : this(region, islandSize, new FilePathRange[] { new FilePathRange(filePath, ids) diff --git a/AnnoMapEditor/MapTemplates/Serializing/MapTemplateReader.cs b/AnnoMapEditor/MapTemplates/Serializing/MapTemplateReader.cs index 8489cc3..aaef4d0 100644 --- a/AnnoMapEditor/MapTemplates/Serializing/MapTemplateReader.cs +++ b/AnnoMapEditor/MapTemplates/Serializing/MapTemplateReader.cs @@ -1,5 +1,6 @@ using Anno.FileDBModels.Anno1800.MapTemplate; -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.Utilities; using System; using System.IO; @@ -12,19 +13,19 @@ public class MapTemplateReader { public async Task FromDataArchiveAsync(string a7tinfoPath) { - Region region = Region.DetectFromPath(a7tinfoPath); - Stream a7tinfoStream = Settings.Instance!.DataArchive.OpenRead(a7tinfoPath) + SessionAsset session = SessionAsset.DetectFromPath(a7tinfoPath); + Stream a7tinfoStream = DataManager.Instance.DataArchive.OpenRead(a7tinfoPath) ?? throw new FileNotFoundException($"Could not find file \"{a7tinfoPath}\" in DataArchive."); - return await FromBinaryStreamAsync(region, a7tinfoStream); + return await FromBinaryStreamAsync(session, a7tinfoStream); } public async Task FromFileAsync(string filePath) { string extension = Path.GetExtension(filePath); - if (extension == "a7tinfo") + if (extension == ".a7tinfo") return await FromBinaryFileAsync(filePath); - else if (extension == "xml") + else if (extension == ".xml") return await FromXmlFileAsync(filePath); else throw new ArgumentException($"Unsupported extension {extension}. Expected either a7tinfo or xml.", nameof(filePath)); @@ -32,34 +33,34 @@ public async Task FromFileAsync(string filePath) public async Task FromXmlFileAsync(string filePath) { - Region region = Region.DetectFromPath(filePath); + SessionAsset session = SessionAsset.DetectFromPath(filePath); Stream a7tinfoXmlStream = File.OpenRead(filePath); - return await FromXmlStreamAsync(region, a7tinfoXmlStream); + return await FromXmlStreamAsync(session, a7tinfoXmlStream); } public async Task FromBinaryFileAsync(string filePath) { - Region region = Region.DetectFromPath(filePath); + SessionAsset session = SessionAsset.DetectFromPath(filePath); Stream a7tinfoStream = File.OpenRead(filePath); - return await FromBinaryStreamAsync(region, a7tinfoStream); + return await FromBinaryStreamAsync(session, a7tinfoStream); } - public async Task FromBinaryStreamAsync(Region region, Stream a7tinfoStream) + public async Task FromBinaryStreamAsync(SessionAsset session, Stream a7tinfoStream) { var doc = await FileDBSerializer.ReadAsync(a7tinfoStream); if (doc is null) throw new Exception($"Could not read MapTemplate from binary stream."); - return new MapTemplate(doc, region); + return new MapTemplate(doc, session); } - public async Task FromXmlStreamAsync(Region region, Stream a7tinfoXmlStream) + public async Task FromXmlStreamAsync(SessionAsset session, Stream a7tinfoXmlStream) { var doc = await FileDBSerializer.ReadFromXmlAsync(a7tinfoXmlStream); if (doc is null) throw new Exception($"Could not read MapTemplate from XML stream."); - return new MapTemplate(doc, region); + return new MapTemplate(doc, session); } } } diff --git a/AnnoMapEditor/MapTemplates/Serializing/MapTemplateWriter.cs b/AnnoMapEditor/MapTemplates/Serializing/MapTemplateWriter.cs index 01880ce..4230f23 100644 --- a/AnnoMapEditor/MapTemplates/Serializing/MapTemplateWriter.cs +++ b/AnnoMapEditor/MapTemplates/Serializing/MapTemplateWriter.cs @@ -1,4 +1,5 @@ using AnnoMapEditor.MapTemplates.Models; +using System; using System.IO; using System.Threading.Tasks; @@ -6,22 +7,20 @@ namespace AnnoMapEditor.MapTemplates.Serializing { public class MapTemplateWriter { - public async Task ToXmlAsync(MapTemplate mapTemplate, string filePath) + public async Task WriteXmlAsync(MapTemplate mapTemplate, string filePath) { - var export = mapTemplate.ToTemplateDocument(); - if (export is null) - return; + var export = mapTemplate.ToTemplateDocument() + ?? throw new NullReferenceException($"Failed to write MapTemplate to XML. The MapTemplate could not be translated to a MapTemplateDocument."); using Stream file = File.OpenWrite(filePath); file.SetLength(0); // clear await FileDBSerializer.WriteToXmlAsync(export, file); } - public async Task ToA7tinfoAsync(MapTemplate mapTemplate, string filePath) + public async Task WriteA7tinfoAsync(MapTemplate mapTemplate, string filePath) { - var export = mapTemplate.ToTemplateDocument(); - if (export is null) - return; + var export = mapTemplate.ToTemplateDocument() + ?? throw new NullReferenceException($"Failed to write MapTemplate to a7tinfo. The MapTemplate could not be translated to a MapTemplateDocument."); var parentPath = Path.GetDirectoryName(filePath); if (parentPath is not null) diff --git a/AnnoMapEditor/MapTemplates/Validation/PoolSizeValidator.cs b/AnnoMapEditor/MapTemplates/Validation/PoolSizeValidator.cs index 277ab6a..d55c1b1 100644 --- a/AnnoMapEditor/MapTemplates/Validation/PoolSizeValidator.cs +++ b/AnnoMapEditor/MapTemplates/Validation/PoolSizeValidator.cs @@ -26,7 +26,7 @@ public MapTemplateValidatorResult Validate(MapTemplate mapTemplate) } } - int maxPoolSize = Pool.GetPool(mapTemplate.Region, _islandSize).Size; + int maxPoolSize = Pool.GetPool(mapTemplate.Session.Region, _islandSize).Size; if (islandCount <= maxPoolSize) return MapTemplateValidatorResult.Ok; else diff --git a/AnnoMapEditor/MapTemplates/Validation/SmallPoolSizeValidator.cs b/AnnoMapEditor/MapTemplates/Validation/SmallPoolSizeValidator.cs index cad0898..59d8210 100644 --- a/AnnoMapEditor/MapTemplates/Validation/SmallPoolSizeValidator.cs +++ b/AnnoMapEditor/MapTemplates/Validation/SmallPoolSizeValidator.cs @@ -1,4 +1,5 @@ -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; namespace AnnoMapEditor.MapTemplates.Validation @@ -27,14 +28,14 @@ public MapTemplateValidatorResult Validate(MapTemplate mapTemplate) } // subtract Archibald / Nate / Isabel from the counter - if ((mapTemplate.Region == Region.Moderate || mapTemplate.Region == Region.NewWorld) && thirdPartyCount > 0) + if ((mapTemplate.Session == SessionAsset.OldWorld || mapTemplate.Session == SessionAsset.NewWorld || mapTemplate.Session == SessionAsset.CapeTrelawney) && thirdPartyCount > 0) --smallIslandCount; // subtract all but one pirate island from the counter if (pirateCount > 0) smallIslandCount -= pirateCount - 1; - int maxPoolSize = Pool.GetPool(mapTemplate.Region, IslandSize.Small).Size; + int maxPoolSize = Pool.GetPool(mapTemplate.Session.Region, IslandSize.Small).Size; if (smallIslandCount <= maxPoolSize) return MapTemplateValidatorResult.Ok; else diff --git a/AnnoMapEditor/Mods/Enums/MapType.cs b/AnnoMapEditor/Mods/Enums/MapType.cs index 96e5879..5560d14 100644 --- a/AnnoMapEditor/Mods/Enums/MapType.cs +++ b/AnnoMapEditor/Mods/Enums/MapType.cs @@ -1,5 +1,4 @@ -using AnnoMapEditor.MapTemplates.Enums; -using AnnoMapEditor.Utilities; +using AnnoMapEditor.Utilities; using System.Collections.Generic; using System.Linq; @@ -9,29 +8,16 @@ public class MapType { private static readonly Logger _logger = new(); - //Old World - public static readonly MapType Archipelago = new("Archipelago", guid: "17079", assetName: "moderate_archipel"); - public static readonly MapType Atoll = new("Atoll", guid: "17080", assetName: "moderate_atoll"); - public static readonly MapType Corners = new("Corners", guid: "17082", assetName: "moderate_corners"); - public static readonly MapType IslandArc = new("Island Arc", guid: "17081", assetName: "moderate_islandarc", templateType: "Arc"); - public static readonly MapType Snowflake = new("Snowflake", guid: "17083", assetName: "moderate_snowflake"); + public static readonly MapType Archipelago = new("Archipelago", "Archipelago", guid: "17079", assetName: "moderate_archipel"); + public static readonly MapType Atoll = new("Atoll", "Atoll", guid: "17080", assetName: "moderate_atoll"); + public static readonly MapType Corners = new("Corners", "Corners", guid: "17082", assetName: "moderate_corners"); + public static readonly MapType IslandArc = new("Island Arc", "Arc", guid: "17081", assetName: "moderate_islandarc", templateType: "Arc"); + public static readonly MapType Snowflake = new("Snowflake", "Snowflake", guid: "17083", assetName: "moderate_snowflake"); - //New World - public static readonly MapType Colony01 = new("New World", guid: "", assetName: "SouthAmerica", ' ', fileNamePart: "colony01"); - - //Lists - public static readonly IEnumerable OldWorld = new[] { Archipelago, Atoll, Corners, IslandArc, Snowflake }; - public static readonly IEnumerable NewWorld = new[] { Colony01 }; - public static readonly IEnumerable All = new[] { Archipelago, Atoll, Corners, IslandArc, Snowflake, Colony01 }; - - //Region/MapType Dictionary - public static readonly Dictionary> MapTypesForRegion = new() - { - [Region.Moderate] = OldWorld, - [Region.NewWorld] = NewWorld - }; + public static readonly IEnumerable All = new[] { Archipelago, Atoll, Corners, IslandArc, Snowflake }; + public readonly string DisplayName; public readonly string Name; public readonly string Guid; public readonly string AssetName; @@ -40,8 +26,9 @@ public class MapType public readonly string TemplateType; - private MapType(string name, string guid, string assetName, char assetNameSeparator = '_', string? fileNamePart = null, string? templateType = null) + private MapType(string displayName, string name, string guid, string assetName, char assetNameSeparator = '_', string? fileNamePart = null, string? templateType = null) { + DisplayName = displayName; Name = name; Guid = guid; AssetName = assetName; diff --git a/AnnoMapEditor/Mods/Models/Mod.cs b/AnnoMapEditor/Mods/Models/Mod.cs index 1302ffc..d244964 100644 --- a/AnnoMapEditor/Mods/Models/Mod.cs +++ b/AnnoMapEditor/Mods/Models/Mod.cs @@ -1,17 +1,5 @@ -using AnnoMapEditor.MapTemplates.Enums; -using AnnoMapEditor.MapTemplates.Models; -using AnnoMapEditor.MapTemplates.Serializing; +using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Mods.Enums; -using AnnoMapEditor.Mods.Serialization; -using AnnoMapEditor.Utilities; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Windows; /* * Modloader doesn't support a7t because they are loaded as .rda archive. @@ -27,255 +15,26 @@ namespace AnnoMapEditor.Mods.Models { - internal class Mod + public class Mod { public const string AME_POOL_PATH = @"data\ame\maps\pool"; + public string Name { get; init; } + + public string? Id { get; init; } + public MapTemplate MapTemplate { get; init; } - public MapType MapType { get; init; } + public MapType? MapType { get; init; } - public Mod(MapTemplate mapTemplate, MapType? mapType) + public Mod(string name, string? modId, MapTemplate mapTemplate, MapType? mapType) { + Name = name; + Id = modId; MapTemplate = mapTemplate; MapType = mapType; } - - - public async Task Save(string modPath, string modName, string? modID) - { - string fullModName = "[Map] " + modName; - - List sizes = MapTemplate.Region.GetAllSizeCombinations().ToList(); - string size = sizes[0]; - - try - { - string? mapTypeFileName = MapType.ToFileName(); - string? mapTypeGuid = MapType.Guid; - - if (mapTypeFileName is null || mapTypeGuid is null) - throw new Exception("invalid MapType"); - - if(!MapTemplate.Region.AllowModding) - throw new Exception("not supported map region"); - - FileUtils.TryDeleteDirectory(modPath); - Directory.CreateDirectory(modPath); - - //Only write Language XML for OW Maps, as only they need naming in a menu - if (!string.IsNullOrEmpty(mapTypeGuid)) - await WriteLanguageXml(modPath, modName, mapTypeGuid); - - // write meta JSON, assets XML, .a7tinfo, .a7t and .a7te - string mapFilePath = Path.Combine(AME_POOL_PATH, MapTemplate.Region.PoolFolderName, mapTypeFileName); - string a7tBasePath = Path.Combine(modPath, $"{mapFilePath}"); - string a7tInfoPath = a7tBasePath + $"_{size}.a7tinfo"; - string a7tPath = a7tBasePath + $"_{size}.a7t"; - string a7tePath = a7tBasePath + $"_{size}.a7te"; - - await WriteMetaJson(modPath, modName, modID); - await WriteAssetsXml(modPath, fullModName, mapFilePath, MapTemplate.Region, MapType); - - MapTemplateWriter mapTemplateWriter = new(); - await mapTemplateWriter.ToA7tinfoAsync(MapTemplate, a7tBasePath + $"_{size}.a7tinfo"); - - (int x, int y, int size) playableArea = (MapTemplate.PlayableArea.X, MapTemplate.PlayableArea.Y, MapTemplate.PlayableArea.Width); - await Task.Run(() => new A7tExporter(MapTemplate.Size.X, playableArea, MapTemplate.Region).ExportA7T(a7tPath)); - await Task.Run(() => new A7teExporter(MapTemplate.Size.X).ExportA7te(a7tePath)); - - if (MapTemplate.Region.HasMapExtension) - { - File.Copy(a7tInfoPath, a7tBasePath + $"_{size}_enlarged.a7tinfo"); - File.Copy(a7tPath, a7tBasePath + $"_{size}_enlarged.a7t"); - File.Copy(a7tePath, a7tBasePath + $"_{size}_enlarged.a7te"); - } - - //copy a7tinfo, a7t and a7te for all sizes - for (int i = 1; i(File.ReadAllText(modinfoPath)); - //} - - //if (modinfo is null) - { - modinfo = new() - { - Version = "1", - ModID = string.IsNullOrEmpty(modID) ? $"ame_{MakeSafeName(modName)}_{Guid.NewGuid().ToString().Split('-').FirstOrDefault("")}" : modID, - ModName = new(modName), - Category = new("Map"), - Description = new($"Select Map Type '{modName}' to play this map.\n" + - $"World and island sizes are fixed.\n" + - $"\n" + - $"Note:\n" + - $"- Do not rename the mod folder. It will lead to a loading screen freeze.\n" + - $"- You can combine map mods as long as they do not replace the same map type.\n" + - $"\n" + - $"This mod has been created with the {App.Title}.\n" + - $"You can download the editor at:\nhttps://github.com/anno-mods/AnnoMapEditor/releases/latest"), - CreatorName = App.TitleShort, - CreatorContact = "https://github.com/anno-mods/AnnoMapEditor" - }; - } - - using StreamWriter writer = new(File.Create(modinfoPath)); - await writer.WriteAsync(JsonConvert.SerializeObject(modinfo, Newtonsoft.Json.Formatting.Indented)); - } - - private static async Task WriteLanguageXml(string modPath, string name, string guid) - { - string[] languages = new string[] { "chinese", "english", "french", "german", "italian", "japanese", "korean", "polish", "russian", "spanish", "taiwanese" }; - - foreach (var language in languages) - { - string languageXmlPath = Path.Combine(modPath, $@"data\config\gui\texts_{language}.xml"); - string? languageXmlDir = Path.GetDirectoryName(languageXmlPath); - if (languageXmlDir is not null) - Directory.CreateDirectory(languageXmlDir); - - string content = - $"\n" + - $" \n" + - $" {name}\n" + - $" \n" + - $"\n"; - - using StreamWriter writer = new(File.Create(languageXmlPath)); - await writer.WriteAsync(content); - } - } - - private static async Task WriteAssetsXml(string modPath, string fullModName, string mapFilePath, Region region, MapType mapType) - { - string assetsXmlPath = Path.Combine(modPath, @"data\config\export\main\asset\assets.xml"); - string? assetsXmlDir = Path.GetDirectoryName(assetsXmlPath); - if (assetsXmlDir is not null) - Directory.CreateDirectory(assetsXmlDir); - - string content = CreateAssetsModOps(region, mapType, mapFilePath.Replace("\\", "/")); - - using StreamWriter writer = new(File.Create(assetsXmlPath)); - await writer.WriteAsync(content); - } - - public static string CreateAssetsModOps(Region region, MapType mapType, string fullMapPath) - { - IEnumerable sizes = region.MapSizes; - // some maps have updates sizes, but make sure to only replace one - IEnumerable subSizes = region.MapSizeIndices; - - static string MakeXPath(string mapTypeName, string size, string subsize) - { - if (string.IsNullOrEmpty(subsize)) - return $"../Standard/Name='{mapTypeName}{size}'"; - else - return $"../Standard/Name='{mapTypeName}{size}_{subsize}'"; - } - - string content = "\n"; - - if (region.UsesAllSizeIndices) - { - //Single ModOp for all Sub-Sizes - foreach (var size in sizes) - { - foreach(var subsize in subSizes) - { - content += CreateModOp( - new string[] { MakeXPath(mapType.ToName(), size, subsize) }, - fullMapPath, - $"{size}_{subsize}", - region.HasMapExtension); - } - } - } - else - { - //XPath OR - foreach (var size in sizes) - { - var xpaths = subSizes.Select(x => MakeXPath(mapType.ToName(), size, x)); - - content += CreateModOp(xpaths, fullMapPath, size, region.HasMapExtension); - } - } - - - content += "\n"; - return content; - } - - private static string CreateModOp(IEnumerable xPaths, string basePath, string size, bool extension) - { - string result = CreateModOpSingle(xPaths, basePath, size, false); - if (extension) - { - result += CreateModOpSingle(xPaths, basePath, size, true); - } - return result; - } - - private static string CreateModOpSingle(IEnumerable xPaths, string basePath, string size, bool extension) - { - const string TEMPLATE = "TemplateFilename"; - const string ENLARGED_TEMPLATE = "EnlargedTemplateFilename"; - - string target = extension ? ENLARGED_TEMPLATE : TEMPLATE; - - string result = $" \n" + - $" <{target}>{basePath}_{size}{(extension ? "_enlarged" : "")}.a7t\n" + - $" \n"; - - return result; - } - - private static string MakeSafeName(string unsafeName) => new Regex(@"\W").Replace(unsafeName, "_").ToLower(); } } diff --git a/AnnoMapEditor/Mods/Serialization/A7tExporter.cs b/AnnoMapEditor/Mods/Serialization/A7tExporter.cs index 4ea7605..5726a26 100644 --- a/AnnoMapEditor/Mods/Serialization/A7tExporter.cs +++ b/AnnoMapEditor/Mods/Serialization/A7tExporter.cs @@ -1,6 +1,5 @@ using Anno.FileDBModels.Anno1800.Gamedata.Models.Shared; -using AnnoMapEditor.MapTemplates; -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; using FileDBSerializing; using FileDBSerializing.ObjectSerializer; using RDAExplorer; @@ -16,10 +15,10 @@ public class A7tExporter private readonly (int x, int y, int size) _playableArea; - private readonly Region _mapRegion; + private readonly RegionAsset _mapRegion; - public A7tExporter(int mapSize, (int x, int y, int size) playableArea, Region mapRegion) + public A7tExporter(int mapSize, (int x, int y, int size) playableArea, RegionAsset mapRegion) { _mapSize = mapSize; _playableArea = playableArea; @@ -32,7 +31,7 @@ public void ExportA7T(string a7tPath) using MemoryStream nestedDataStream = new(); //Create actual a7t File - Gamedata gameDataItem = new(_mapSize, _playableArea, _mapRegion.AmbientName, true); + Gamedata gameDataItem = new(_mapSize, _playableArea, _mapRegion.Ambiente!, true); FileDBDocumentSerializer serializer = new(new() { Version = FileDBDocumentVersion.Version1 }); IFileDBDocument generatedFileDB = serializer.WriteObjectStructureToFileDBDocument(gameDataItem); diff --git a/AnnoMapEditor/Mods/Serialization/ModWriter.cs b/AnnoMapEditor/Mods/Serialization/ModWriter.cs new file mode 100644 index 0000000..d424d28 --- /dev/null +++ b/AnnoMapEditor/Mods/Serialization/ModWriter.cs @@ -0,0 +1,336 @@ +using Anno.FileDBModels.Anno1800.Gamedata.Models.Shared; +using AnnoMapEditor.DataArchives; +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives.Assets.Repositories; +using AnnoMapEditor.MapTemplates.Serializing; +using AnnoMapEditor.Mods.Enums; +using AnnoMapEditor.Mods.Models; +using AnnoMapEditor.Utilities; +using FileDBSerializing; +using FileDBSerializing.ObjectSerializer; +using Newtonsoft.Json; +using RDAExplorer; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; + +namespace AnnoMapEditor.Mods.Serialization +{ + public class ModWriter + { + public const string AME_POOL_PATH = @"data\ame\maps\pool"; + + + private readonly MapTemplateWriter _mapTemplateWriter; + + private readonly AssetRepository _assetRepository; + + + public ModWriter() + { + _mapTemplateWriter = new(); + _assetRepository = DataManager.Instance.AssetRepository; + } + + + /// + /// Determines the list of MapTemplateAssets to be replaced for a mod targeting the given + /// session and mapType. + /// + /// + /// The way Anno 1800 chooses which template to use differs between sessions. As of GU17.1, + /// there are three scenarios we support: + /// + /// + /// 1. The Old World
+ /// The map template used for The Old World is randomly chosen out of one from a set pools. + /// Separate map pools exist for different map sizes and MapTypes. + /// To guarantee the mod's map template will be used, we replace the entire pool for the + /// given mapType.
+ /// + /// 2. The New World
+ /// Similarly to The Old World, the The New World's map template is are chosen randomly + /// from a pool. However, there exist no MapTypes for this session.
+ /// Additionally, The 13th DLC "New World Rising" introduced the concept of map extensions. + /// All new world map templates were extended to make room for the island Manola in the top + /// corner of the session.
+ /// All New World map templates and their extended versions are being targeted to be + /// replaced.
+ /// + /// 3. Cape Trelawney, + /// Arctic and Enbesa + ///
+ /// All other supported sessions reference a map template directly via the attributes + /// MapTemplate and + /// MapTemplateForMultiplayer. In + /// their case, replacing these map templates is sufficient to use the mod's map template + /// instead. + ///
+ /// + /// The MapType to be used for the mod. + /// Must not be null if session is + /// OldWorld. + /// + /// + private IList GetMapTemplatesToReplace(SessionAsset session, MapType? mapType = null) + { + // Sessions may reference the MapTemplates to be used directly via the MapTemplate and + // MapTemplateForMultiplayer attributes. + if (session.MapTemplate != null) + { + List mapTemplates = new(); + mapTemplates.Add(session.MapTemplate); + if (session.MapTemplateForMultiplayer != null) + mapTemplates.Add(session.MapTemplateForMultiplayer); + + return mapTemplates; + } + + // Otherwise get pool maps matching the Session's region. + else + { + IEnumerable mapTemplates = _assetRepository.GetAll() + .Where(m => m.TemplateRegion == session.Region) + .Where(m => m.TemplateFilename.Contains("pool")); + + // For The Old World filter according to the MapType. + if (session == SessionAsset.OldWorld) + { + if (mapType != null) + mapTemplates = mapTemplates.Where(m => m.TemplateMapType == mapType); + else + throw new ArgumentException($"Cannot determine MapTemplates to replace for session {session.GUID} \"{session.DisplayName}\" without a given {typeof(MapType).FullName}."); + } + + return mapTemplates.ToList(); + } + } + + public async Task WriteAsync(Mod mod, string modPath) + { + FileUtils.TryDeleteDirectory(modPath); + Directory.CreateDirectory(modPath); + + SessionAsset session = mod.MapTemplate.Session; + + IList mapTemplatesToReplace = GetMapTemplatesToReplace(session, mod.MapType); + + await WriteModinfoJson(mod, modPath); + + //Only write Language XML for OW Maps, as only they need naming in a menu + if (session == SessionAsset.OldWorld) + await WriteLanguageXml(modPath, mod.Name, mod.MapType!.Guid); + + // create the first copy of a7t, a7tinfo and a7te + MapTemplateAsset firstMapTemplate = mapTemplatesToReplace.First(); + string mapFileDirectory = Path.Combine(modPath, AME_POOL_PATH); + string a7tPath = Path.Combine(mapFileDirectory, Path.GetFileName(firstMapTemplate.TemplateFilename)); + string a7tinfoPath = Path.ChangeExtension(a7tPath, "a7tinfo"); + string a7tePath = Path.ChangeExtension(a7tPath, "a7te"); + + await _mapTemplateWriter.WriteA7tinfoAsync(mod.MapTemplate, Path.Combine(modPath, a7tinfoPath)); + WriteA7T(mod, a7tPath); + WriteA7te(mod.MapTemplate.Size.X, a7tePath); + + // copy a7t, a7tinfo and a7te for all MapTemplates that must be replaced + for (int i = 1; i < mapTemplatesToReplace.Count; ++i) + { + MapTemplateAsset mapTemplate = mapTemplatesToReplace[i]; + string mapFilename = Path.GetFileNameWithoutExtension(mapTemplate.TemplateFilename); + + File.Copy(a7tPath, Path.Combine(mapFileDirectory, mapFilename + ".a7t")); + File.Copy(a7tinfoPath, Path.Combine(mapFileDirectory, mapFilename + ".a7tinfo")); + File.Copy(a7tePath, Path.Combine(mapFileDirectory, mapFilename + ".a7te")); + + if (mapTemplate.EnlargedTemplateFilename != null) + { + File.Copy(a7tPath, Path.Combine(mapFileDirectory, mapFilename + "_enlarged.a7t")); + File.Copy(a7tinfoPath, Path.Combine(mapFileDirectory, mapFilename + "_enlarged.a7tinfo")); + File.Copy(a7tePath, Path.Combine(mapFileDirectory, mapFilename + "_enlarged.a7te")); + } + } + + await WriteAssetsXml(modPath, mapTemplatesToReplace); + + return true; + } + + private static async Task WriteAssetsXml(string modPath, IEnumerable mapTemplatesToReplace) + { + string assetsXmlPath = Path.Combine(modPath, @"data\config\export\main\asset\assets.xml"); + + string? assetsXmlDir = Path.GetDirectoryName(assetsXmlPath); + if (assetsXmlDir is not null) + Directory.CreateDirectory(assetsXmlDir); + + StringBuilder sb = new(); + sb.Append("\n"); + + foreach (MapTemplateAsset mapTemplateAsset in mapTemplatesToReplace) + { + string templateFilename = Path.Combine(AME_POOL_PATH, Path.GetFileName(mapTemplateAsset.TemplateFilename)); + string? enlargedTemplateFilename = mapTemplateAsset.EnlargedTemplateFilename != null ? Path.Combine(AME_POOL_PATH, Path.GetFileName(mapTemplateAsset.EnlargedTemplateFilename)) : null; + + WriteModOpReplaceTemplateFilename(sb, mapTemplateAsset.GUID, templateFilename, enlargedTemplateFilename); + + if (mapTemplateAsset.EnlargedTemplateFilename != null) + WriteModOpReplaceTemplateFilename(sb, mapTemplateAsset.GUID, templateFilename, enlargedTemplateFilename); + } + + sb.Append("\n"); + + using StreamWriter writer = new(File.Create(assetsXmlPath)); + await writer.WriteAsync(sb.ToString()); + } + + private static async Task WriteModinfoJson(Mod mod, string modPath) + { + string modDescription; + if (mod.MapTemplate.Session == SessionAsset.OldWorld) + modDescription = $"Select Map Type '{mod.Name}' to play this map.\n" + + $"World and island sizes are fixed.\n\n"; + else + modDescription = $"This mod will automatically replace {mod.MapTemplate.Session.DisplayName}.\n\n"; + + modDescription += $"Note:\n" + + $"- Do not rename the mod folder. It will lead to a loading screen freeze.\n" + + $"- You can combine map mods as long as they do not replace the same session and map type.\n" + + $"\n" + + $"This mod has been created with the {App.Title}.\n" + + $"You can download the editor at:\nhttps://github.com/anno-mods/AnnoMapEditor/releases/latest"; + + Modinfo modinfo = new() + { + Version = "1", + ModID = string.IsNullOrEmpty(mod.Id) ? $"ame_{MakeSafeName(mod.Name)}_{Guid.NewGuid().ToString().Split('-').FirstOrDefault("")}" : mod.Id, + ModName = new(mod.Name), + Category = new("Map"), + Description = new(modDescription), + CreatorName = App.TitleShort, + CreatorContact = "https://github.com/anno-mods/AnnoMapEditor" + }; + + string modinfoPath = Path.Combine(modPath, "modinfo.json"); + + using StreamWriter writer = new(File.Create(modinfoPath)); + await writer.WriteAsync(JsonConvert.SerializeObject(modinfo, Newtonsoft.Json.Formatting.Indented)); + } + + private static void WriteModOpReplaceTemplateFilename(StringBuilder sb, long mapTemplateGuid, string templateFilename, string? enlargedTemplateFilename) + { + sb.Append($" \n") + .Append($" {templateFilename.Replace('\\', '/')}\n"); + + if (enlargedTemplateFilename != null) + sb.Append($" {templateFilename.Replace('\\', '/')}\n"); + + sb.Append($" \n"); + } + + + private static void WriteA7T(Mod mod, string a7tPath) + { + using MemoryStream nestedDataStream = new(); + + // If the session has existing Gamedata, it must be updated to reflect all changes made within the AME. + (int x, int y, int size) playableArea = (mod.MapTemplate.PlayableArea.X, mod.MapTemplate.PlayableArea.Y, mod.MapTemplate.PlayableArea.Width); + Gamedata gameDataItem = new(mod.MapTemplate.Size.X, playableArea, mod.MapTemplate.Session.Region.Ambiente!, true); + + //Create actual a7t File + FileDBDocumentSerializer serializer = new(new() { Version = FileDBDocumentVersion.Version1 }); + IFileDBDocument generatedFileDB = serializer.WriteObjectStructureToFileDBDocument(gameDataItem); + + using MemoryStream fileDbStream = new(); + + DocumentWriter gamedataDocWriter = new(); + gamedataDocWriter.WriteFileDBToStream(generatedFileDB, fileDbStream); + + if (fileDbStream.Position > 0) + { + fileDbStream.Seek(0, SeekOrigin.Begin); + } + + RDABlockCreator.FileType_CompressedExtensions.Add(".data"); + + using (RDAReader rdaReader = new()) + using (BinaryReader reader = new(fileDbStream)) + { + RDAFolder rdaFolder = new(FileHeader.Version.Version_2_2); + + rdaReader.rdaFolder = rdaFolder; + DirEntry gamedataFileDirEntry = new() + { + filename = RDAFile.FileNameToRDAFileName("gamedata.data", ""), + offset = 0, + compressed = (ulong)fileDbStream.Length, + filesize = (ulong)fileDbStream.Length, + timestamp = RDAExplorer.Misc.DateTimeExtension.ToTimeStamp(DateTime.Now), + }; + + BlockInfo gamedataFileBlockInfo = new() + { + flags = 0, + fileCount = 1, + directorySize = (ulong)fileDbStream.Length, + decompressedSize = (ulong)fileDbStream.Length, + nextBlock = 0 + }; + + RDAFile rdaFile = RDAFile.FromUnmanaged(FileHeader.Version.Version_2_2, gamedataFileDirEntry, gamedataFileBlockInfo, reader, null); + rdaFolder.AddFiles(new List() { rdaFile }); + RDAWriter writer = new(rdaFolder); + bool compress = true; + writer.Write(a7tPath, FileHeader.Version.Version_2_2, compress, rdaReader, null); + + } + + RDABlockCreator.FileType_CompressedExtensions.Remove(".data"); + } + + private static void WriteA7te(int mapRadius, string a7tePath) + { + AnnoEditorLevel a7te = new(mapRadius); + + XmlSerializer a7teSerializer = new(typeof(AnnoEditorLevel)); + XmlWriterSettings xmlSettings = new() { Indent = true, IndentChars = " ", OmitXmlDeclaration = true, Async = true }; + XmlSerializerNamespaces noNamespaces = new(new XmlQualifiedName[] { XmlQualifiedName.Empty }); + + using StreamWriter streamWriter = new(a7tePath, false, Encoding.UTF8); + using XmlWriter xmlWriter = XmlWriter.Create(streamWriter, xmlSettings); + + a7teSerializer.Serialize(xmlWriter, a7te, noNamespaces); + } + + private static async Task WriteLanguageXml(string modPath, string name, string guid) + { + string[] languages = new string[] { "chinese", "english", "french", "german", "italian", "japanese", "korean", "polish", "russian", "spanish", "taiwanese" }; + + foreach (var language in languages) + { + string languageXmlPath = Path.Combine(modPath, $@"data\config\gui\texts_{language}.xml"); + string? languageXmlDir = Path.GetDirectoryName(languageXmlPath); + if (languageXmlDir is not null) + Directory.CreateDirectory(languageXmlDir); + + string content = + $"\n" + + $" \n" + + $" {name}\n" + + $" \n" + + $"\n"; + + using StreamWriter writer = new(File.Create(languageXmlPath)); + await writer.WriteAsync(content); + } + } + + + private static string MakeSafeName(string unsafeName) => new Regex(@"\W").Replace(unsafeName, "_").ToLower(); + } +} \ No newline at end of file diff --git a/AnnoMapEditor/UI/Controls/AddIsland/AddIslandViewModel.cs b/AnnoMapEditor/UI/Controls/AddIsland/AddIslandViewModel.cs index 2cfdf92..a04766b 100644 --- a/AnnoMapEditor/UI/Controls/AddIsland/AddIslandViewModel.cs +++ b/AnnoMapEditor/UI/Controls/AddIsland/AddIslandViewModel.cs @@ -65,10 +65,6 @@ public override void Move(Point delta) { EndDrag(); - // TODO: Implement proper loading! - if (Settings.Instance.IslandRepository == null) - return; - IslandAdded?.Invoke(this, new(MapElementType, IslandType, IslandSize, delta)); } } diff --git a/AnnoMapEditor/UI/Controls/Fertilities/FertilitiesViewModel.cs b/AnnoMapEditor/UI/Controls/Fertilities/FertilitiesViewModel.cs index ab2806e..371fdfb 100644 --- a/AnnoMapEditor/UI/Controls/Fertilities/FertilitiesViewModel.cs +++ b/AnnoMapEditor/UI/Controls/Fertilities/FertilitiesViewModel.cs @@ -1,5 +1,4 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.UI.Overlays; using AnnoMapEditor.UI.Overlays.SelectFertilities; @@ -14,12 +13,12 @@ public class FertilitiesViewModel { public FixedIslandElement FixedIsland { get; init; } - public Region Region { get; init; } + public RegionAsset Region { get; init; } public ObservableCollection Fertilities { get; init; } = new(); - public FertilitiesViewModel(FixedIslandElement fixedIsland, Region region) + public FertilitiesViewModel(FixedIslandElement fixedIsland, RegionAsset region) { FixedIsland = fixedIsland; Region = region; diff --git a/AnnoMapEditor/UI/Controls/Fertilities/FertilityComparer.cs b/AnnoMapEditor/UI/Controls/Fertilities/FertilityComparer.cs index 6318ff7..f1106f9 100644 --- a/AnnoMapEditor/UI/Controls/Fertilities/FertilityComparer.cs +++ b/AnnoMapEditor/UI/Controls/Fertilities/FertilityComparer.cs @@ -1,10 +1,7 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.Utilities; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace AnnoMapEditor.UI.Controls.Fertilities { @@ -20,9 +17,8 @@ public class FertilityComparer : IComparer private static readonly Dictionary _orderLookup; static FertilityComparer() { - MinimapSceneAsset minimapScene = Settings.Instance.AssetRepository!.Get(MinimapSceneAsset.GUID); int index = 0; - _orderLookup = minimapScene.FertilityOrderGuids + _orderLookup = MinimapSceneAsset.Instance.FertilityOrderGuids .ToDictionary(f => f, f => index++); } diff --git a/AnnoMapEditor/UI/Controls/Imprint.xaml b/AnnoMapEditor/UI/Controls/Imprint.xaml new file mode 100644 index 0000000..01276a9 --- /dev/null +++ b/AnnoMapEditor/UI/Controls/Imprint.xaml @@ -0,0 +1,25 @@ + + + + + + + + The editor is work in progress. + + Visit GitHub for + roadmap + and + issue reporting. + + diff --git a/AnnoMapEditor/UI/Controls/Imprint.xaml.cs b/AnnoMapEditor/UI/Controls/Imprint.xaml.cs new file mode 100644 index 0000000..21fae12 --- /dev/null +++ b/AnnoMapEditor/UI/Controls/Imprint.xaml.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using System.Windows.Controls; +using System.Windows.Navigation; + +namespace AnnoMapEditor.UI.Controls +{ + /// + /// Interaction logic for Imprint.xaml + /// + public partial class Imprint : UserControl + { + public Imprint() + { + InitializeComponent(); + } + + private void Hyperlink_OpenBrowser(object sender, RequestNavigateEventArgs e) + { + var info = new ProcessStartInfo(e.Uri.AbsoluteUri) + { + UseShellExecute = true, + }; + Process.Start(info); + } + } +} diff --git a/AnnoMapEditor/UI/Controls/IslandProperties/FixedIslandPropertiesViewModel.cs b/AnnoMapEditor/UI/Controls/IslandProperties/FixedIslandPropertiesViewModel.cs index e2817d9..b824052 100644 --- a/AnnoMapEditor/UI/Controls/IslandProperties/FixedIslandPropertiesViewModel.cs +++ b/AnnoMapEditor/UI/Controls/IslandProperties/FixedIslandPropertiesViewModel.cs @@ -1,4 +1,5 @@ -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.UI.Controls.Fertilities; using AnnoMapEditor.UI.Controls.Slots; @@ -23,7 +24,7 @@ public class FixedIslandPropertiesViewModel : ObservableBase public SlotsViewModel SlotsViewModel { get; init; } - public FixedIslandPropertiesViewModel(FixedIslandElement fixedIsland, Region region) + public FixedIslandPropertiesViewModel(FixedIslandElement fixedIsland, RegionAsset region) { FixedIsland = fixedIsland; FertilitiesViewModel = new(fixedIsland, region); diff --git a/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml b/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml new file mode 100644 index 0000000..8a6176d --- /dev/null +++ b/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml.cs b/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml.cs new file mode 100644 index 0000000..ac48fef --- /dev/null +++ b/AnnoMapEditor/UI/Controls/LoadingSpinner.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace AnnoMapEditor.UI.Controls +{ + /// + /// Interaction logic for LoadingSpinner.xaml + /// + public partial class LoadingSpinner : UserControl + { + public LoadingSpinner() + { + InitializeComponent(); + } + } +} diff --git a/AnnoMapEditor/UI/Controls/MapTemplateProperties.xaml b/AnnoMapEditor/UI/Controls/MapTemplateProperties.xaml index df8a43b..1edb85f 100644 --- a/AnnoMapEditor/UI/Controls/MapTemplateProperties.xaml +++ b/AnnoMapEditor/UI/Controls/MapTemplateProperties.xaml @@ -28,11 +28,16 @@ + ItemsSource="{Binding SupportedSessions}" + SelectedItem="{Binding SelectedSession}"> - + + + + diff --git a/AnnoMapEditor/UI/Controls/MapTemplatePropertiesViewModel.cs b/AnnoMapEditor/UI/Controls/MapTemplatePropertiesViewModel.cs index 10b6748..2f93919 100644 --- a/AnnoMapEditor/UI/Controls/MapTemplatePropertiesViewModel.cs +++ b/AnnoMapEditor/UI/Controls/MapTemplatePropertiesViewModel.cs @@ -1,7 +1,8 @@ -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Utilities; using System; +using System.Collections.Generic; namespace AnnoMapEditor.UI.Controls { @@ -19,20 +20,20 @@ public string MapSizeText } private string _mapSizeText = ""; - public Region SelectedRegion + public SessionAsset SelectedSession { - get => _selectedRegion; + get => _selectedSession; set { - if (value != _selectedRegion) + if (value != _selectedSession) { - _selectedRegion = value; - _mapTemplate.Region = value; + _selectedSession = value; + _mapTemplate.Session = value; OnSelectedRegionChanged(); } } } - private Region _selectedRegion; + private SessionAsset _selectedSession; public event EventHandler? SelectedRegionChanged; @@ -63,7 +64,7 @@ private void ResizeMapTemplateValues() _mapTemplate.PlayableArea.Y + _mapTemplate.PlayableArea.Height); } - public Region[] Regions { get; } = Region.All; + public IEnumerable SupportedSessions { get; } = SessionAsset.SupportedSessions; public int MapSize { @@ -117,7 +118,7 @@ public MapTemplatePropertiesViewModel(MapTemplate mapTemplate) _mapTemplate = mapTemplate; _mapTemplate.MapSizeConfigCommitted += HandleMapTemplateSizeCommitted; - _selectedRegion = _mapTemplate.Region; + _selectedSession = _mapTemplate.Session; MapSize = mapTemplate.Size.X; diff --git a/AnnoMapEditor/UI/Controls/MapView.xaml.cs b/AnnoMapEditor/UI/Controls/MapView.xaml.cs index 131a3a7..fc61c58 100644 --- a/AnnoMapEditor/UI/Controls/MapView.xaml.cs +++ b/AnnoMapEditor/UI/Controls/MapView.xaml.cs @@ -354,7 +354,7 @@ private void ProtoIsland_DragEnded(object? sender, DragEndedEventArgs e) // if it is a FixedIsland, let the user select the correct island if (protoIslandViewModel.MapElementType == MapElementType.FixedIsland) { - SelectIslandViewModel selectIslandViewModel = new(_mapTemplate.Region, protoIslandViewModel.Island.IslandType, protoIslandViewModel.IslandSize); + SelectIslandViewModel selectIslandViewModel = new(_mapTemplate.Session.Region, protoIslandViewModel.Island.IslandType, protoIslandViewModel.IslandSize); selectIslandViewModel.IslandSelected += (s, e) => SelectIsland_IslandSelected(s, e, protoIslandViewModel.Island.Position); OverlayService.Instance.Show(selectIslandViewModel); diff --git a/AnnoMapEditor/UI/Controls/Slots/SlotComparer.cs b/AnnoMapEditor/UI/Controls/Slots/SlotComparer.cs index bcf0c8c..b8c98e2 100644 --- a/AnnoMapEditor/UI/Controls/Slots/SlotComparer.cs +++ b/AnnoMapEditor/UI/Controls/Slots/SlotComparer.cs @@ -1,10 +1,7 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.Utilities; using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace AnnoMapEditor.UI.Controls.Slots { @@ -20,9 +17,8 @@ public class SlotComparer : IComparer private static readonly Dictionary _orderLookup; static SlotComparer() { - MinimapSceneAsset minimapScene = Settings.Instance.AssetRepository!.Get(MinimapSceneAsset.GUID); int index = 0; - _orderLookup = minimapScene.LodesOrderSlotTypes + _orderLookup = MinimapSceneAsset.Instance.LodesOrderSlotTypes .ToDictionary(f => f, f => index++); } diff --git a/AnnoMapEditor/UI/Controls/Slots/SlotsViewModel.cs b/AnnoMapEditor/UI/Controls/Slots/SlotsViewModel.cs index ff63c78..4d7ee80 100644 --- a/AnnoMapEditor/UI/Controls/Slots/SlotsViewModel.cs +++ b/AnnoMapEditor/UI/Controls/Slots/SlotsViewModel.cs @@ -1,5 +1,4 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.UI.Overlays; using AnnoMapEditor.UI.Overlays.SelectSlots; @@ -14,12 +13,12 @@ public class SlotsViewModel { public FixedIslandElement FixedIsland { get; init; } - public Region Region { get; init; } + public RegionAsset Region { get; init; } public ObservableCollection SlotCounters { get; init; } = new(); - public SlotsViewModel(FixedIslandElement fixedIsland, Region region) + public SlotsViewModel(FixedIslandElement fixedIsland, RegionAsset region) { FixedIsland = fixedIsland; Region = region; diff --git a/AnnoMapEditor/UI/Controls/StatusIndicator.xaml b/AnnoMapEditor/UI/Controls/StatusIndicator.xaml new file mode 100644 index 0000000..e9ad1c0 --- /dev/null +++ b/AnnoMapEditor/UI/Controls/StatusIndicator.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/AnnoMapEditor/UI/Controls/StatusIndicator.xaml.cs b/AnnoMapEditor/UI/Controls/StatusIndicator.xaml.cs new file mode 100644 index 0000000..4815fe4 --- /dev/null +++ b/AnnoMapEditor/UI/Controls/StatusIndicator.xaml.cs @@ -0,0 +1,47 @@ +using System.Windows; +using System.Windows.Controls; + +namespace AnnoMapEditor.UI.Controls +{ + /// + /// Interaction logic for StatusIndicator.xaml + /// + public partial class StatusIndicator : UserControl + { + public static readonly DependencyProperty IsOkProperty = + DependencyProperty.Register("IsOk", typeof(bool), + typeof(StatusIndicator), new FrameworkPropertyMetadata(false)); + + public bool IsOk + { + get { return (bool)GetValue(IsOkProperty); } + set { SetValue(IsOkProperty, value); } + } + + public static readonly DependencyProperty IsErrorProperty = + DependencyProperty.Register("IsError", typeof(bool), + typeof(StatusIndicator), new FrameworkPropertyMetadata(false)); + + public bool IsError + { + get { return (bool)GetValue(IsErrorProperty); } + set { SetValue(IsErrorProperty, value); } + } + + public static readonly DependencyProperty IsLoadingProperty = + DependencyProperty.Register("IsLoading", typeof(bool), + typeof(StatusIndicator), new FrameworkPropertyMetadata(false)); + + public bool IsLoading + { + get { return (bool) GetValue(IsLoadingProperty); } + set { SetValue(IsLoadingProperty, value); } + } + + + public StatusIndicator() + { + InitializeComponent(); + } + } +} diff --git a/AnnoMapEditor/UI/Converters/BoolToGridLengthConverter.cs b/AnnoMapEditor/UI/Converters/BoolToGridLengthConverter.cs new file mode 100644 index 0000000..16b2a1a --- /dev/null +++ b/AnnoMapEditor/UI/Converters/BoolToGridLengthConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace AnnoMapEditor.UI.Converters +{ + [ValueConversion(typeof(bool), typeof(GridLength))] + public class BoolToGridLengthConverter : IValueConverter + { + public GridLength OnTrue { get; set; } + + public GridLength OnFalse { get; set; } + + + public BoolToGridLengthConverter() + { + OnTrue = GridLength.Auto; + OnFalse = new(0); + } + + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return boolValue ? OnTrue : OnFalse; + + throw new ArgumentException("Input was not a bool."); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is GridLength gridLength) + return gridLength.Value > 0; + + throw new ArgumentException("Input was not a GridLength."); + } + } +} diff --git a/AnnoMapEditor/UI/Converters/ObjectToBool.cs b/AnnoMapEditor/UI/Converters/ObjectToBool.cs new file mode 100644 index 0000000..d529240 --- /dev/null +++ b/AnnoMapEditor/UI/Converters/ObjectToBool.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace AnnoMapEditor.UI.Converters +{ + [ValueConversion(typeof(object), typeof(bool))] + public class ObjectToBool : IValueConverter + { + public bool OnNull { get; set; } + + public ObjectToBool() + { + OnNull = false; + } + + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is null ? OnNull : !OnNull; + } + + public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModOverlay.xaml b/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModOverlay.xaml index bf3cf4e..75d552f 100644 --- a/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModOverlay.xaml +++ b/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModOverlay.xaml @@ -33,6 +33,7 @@ Works only with new save games. + @@ -42,28 +43,49 @@ + - - - - + + + + + + + + + + + + + + + + - + Select the following map type in Sandbox mode: @@ -79,10 +101,10 @@ - The map mod will automatically apply to the selected region. + Visibility="{Binding ShowMapTypeSelection, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibility}}"> + The map mod will automatically apply to the selected session. - Please make sure that only one Map Mod per non-moderate Region is active. + Please make sure that at most one Map Mod per session is active. diff --git a/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModViewModel.cs b/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModViewModel.cs index fd9207b..ae0ccb8 100644 --- a/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModViewModel.cs +++ b/AnnoMapEditor/UI/Overlays/ExportAsMod/ExportAsModViewModel.cs @@ -1,12 +1,15 @@ -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Mods.Enums; using AnnoMapEditor.Mods.Models; +using AnnoMapEditor.Mods.Serialization; using AnnoMapEditor.Utilities; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Windows; namespace AnnoMapEditor.UI.Overlays.ExportAsMod { @@ -21,27 +24,7 @@ enum ModStatus Inactive } - public MapTemplate? MapTemplate - { - get => _mapTemplate; - set - { - _mapTemplate = value; - if(_mapTemplate is not null) - { - AllowedMapTypes = MapType.MapTypesForRegion[_mapTemplate.Region]; - SelectedMapType = AllowedMapTypes.First(); - } - else - { - AllowedMapTypes = Enumerable.Empty(); - SelectedMapType = null; - } - InfoMapTypeSelection = _mapTemplate is not null && _mapTemplate.Region == Region.Moderate; - CheckExistingMod(); - } - } - MapTemplate? _mapTemplate; + public MapTemplate MapTemplate { get; init; } public bool IsSaving { @@ -67,12 +50,7 @@ public string ModName public string ModExistsWarning { get; private set; } = string.Empty; - public bool InfoMapTypeSelection - { - get => _infoMapTypeSelection; - set => SetProperty(ref _infoMapTypeSelection, value); - } - private bool _infoMapTypeSelection = true; + public bool ShowMapTypeSelection { get; init; } public string ModID { @@ -92,24 +70,21 @@ public MapType? SelectedMapType } private MapType? _mapType = null; - public IEnumerable AllowedMapTypes - { - get => _allowedMapTypes; - set => SetProperty(ref _allowedMapTypes, value); - } - private IEnumerable _allowedMapTypes = Enumerable.Empty(); + public IEnumerable AllowedMapTypes { get; init; } = MapType.All; - public ExportAsModViewModel() - { - Settings.Instance.PropertyChanged += Settings_PropertyChanged; - } - private void Settings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + public ExportAsModViewModel(MapTemplate mapTemplate) { - if (e.PropertyName == nameof(Settings.IsLoading)) - CheckExistingMod(); + MapTemplate = mapTemplate; + + ShowMapTypeSelection = mapTemplate.Session == SessionAsset.OldWorld; + if (ShowMapTypeSelection) + SelectedMapType = AllowedMapTypes.First(); + + CheckExistingMod(); } + private void CheckExistingMod() { ResultingModName = (_modName.Trim() == string.Empty ? $"Custom {_mapType}" : _modName); @@ -125,40 +100,52 @@ private void CheckExistingMod() private static ModStatus ModExists(string modName) { - if (Settings.Instance.DataPath is null) + if (Settings.Instance.GamePath is null) return ModStatus.NotFound; - string activeModPath = Path.Combine(Settings.Instance.DataPath, "mods", modName); + string activeModPath = Path.Combine(Settings.Instance.ModsPath!, modName); if (Directory.Exists(activeModPath)) return ModStatus.Active; - string inactiveModPath = Path.Combine(Settings.Instance.DataPath, "mods", "-" + modName); + string inactiveModPath = Path.Combine(Settings.Instance.ModsPath!, "-" + modName); return Directory.Exists(inactiveModPath) ? ModStatus.Inactive : ModStatus.NotFound; } public async Task Save() { - IsSaving = true; - - string? modsFolderPath = Settings.Instance.DataPath; - if (modsFolderPath is not null) - modsFolderPath = Path.Combine(modsFolderPath, "mods"); + string modsFolderPath = Settings.Instance.ModsPath + ?? throw new Exception($"ModsPath is not set."); - if (!Directory.Exists(modsFolderPath) || MapTemplate is null) - { - _logger.LogWarning("mods/ path or session not set. This shouldn't have happened."); - return false; - } + if (!Directory.Exists(modsFolderPath)) + throw new Exception($"Mods directory '{modsFolderPath}' does not exist."); - Mod mod = new(MapTemplate, SelectedMapType); + Mod mod = new(ResultingModName, ModID, MapTemplate, SelectedMapType); CheckExistingMod(); - bool result = await mod.Save(Path.Combine(modsFolderPath, ResultingFullModName), ResultingModName, ModID); - OverlayService.Instance.Close(this); + try + { + IsSaving = true; + ModWriter modWriter = new(); + await modWriter.WriteAsync(mod, Path.Combine(modsFolderPath, ResultingFullModName)); - IsSaving = false; - return result; + OverlayService.Instance.Close(this); + return true; + } + catch (UnauthorizedAccessException) + { + MessageBox.Show("Failed to save the mod.\n\nIt looks like some files are locked, possibly by another application.\n\nThe mod may be broken now.", App.TitleShort, MessageBoxButton.OK, MessageBoxImage.Exclamation); + return false; + } + catch (Exception) + { + MessageBox.Show("Failed to save the mod.\n\nThe mod may be broken now.", App.TitleShort, MessageBoxButton.OK, MessageBoxImage.Exclamation); + return false; + } + finally + { + IsSaving = false; + } } } } diff --git a/AnnoMapEditor/UI/Overlays/SelectFertilities/SelectFertilitiesViewModel.cs b/AnnoMapEditor/UI/Overlays/SelectFertilities/SelectFertilitiesViewModel.cs index 513ffbd..b37fdd5 100644 --- a/AnnoMapEditor/UI/Overlays/SelectFertilities/SelectFertilitiesViewModel.cs +++ b/AnnoMapEditor/UI/Overlays/SelectFertilities/SelectFertilitiesViewModel.cs @@ -1,9 +1,8 @@ -using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.DataArchives.Assets.Repositories; -using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Utilities; -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; @@ -27,11 +26,11 @@ public string? NameFilter } private string? _nameFilter; - public IEnumerable Regions { get; init; } = Region.All; + public IEnumerable Regions { get; init; } = RegionAsset.SupportedRegions; - private readonly Region _initialRegion; + private readonly RegionAsset _initialRegion; - public Region SelectedRegion + public RegionAsset SelectedRegion { get => _selectedRegion; set @@ -42,7 +41,7 @@ public Region SelectedRegion ShowRegionWarning = _selectedRegion != _initialRegion; } } - private Region _selectedRegion; + private RegionAsset _selectedRegion; public bool ShowRegionWarning { @@ -54,10 +53,10 @@ public bool ShowRegionWarning public ObservableCollection FertilityItems { get; init; } - public SelectFertilitiesViewModel(Region region, FixedIslandElement fixedIsland) + public SelectFertilitiesViewModel(RegionAsset region, FixedIslandElement fixedIsland) { // TODO: Should this happen here? - AssetRepository assetRepository = Settings.Instance.AssetRepository!; + AssetRepository assetRepository = DataManager.Instance.AssetRepository; FertilityItems = new(assetRepository.GetAll() .SelectMany(r => r.AllowedFertilities) .Distinct() @@ -102,17 +101,10 @@ private bool FertilityFilter(object item) FertilityAsset fertilityAsset = fertilityItem.FertilityAsset; - if (SelectedRegion != null) - { - // TODO: Should this happen here? - AssetRepository assetRepository = Settings.Instance.AssetRepository!; - RegionAsset regionAsset = assetRepository.Get(SelectedRegion.AssetGuid); - - if (!regionAsset.AllowedFertilities.Contains(fertilityAsset)) - return false; - } - - if (!string.IsNullOrEmpty(_nameFilter)) + if (SelectedRegion != null && !SelectedRegion.AllowedFertilities.Contains(fertilityAsset)) + return false; + + else if (!string.IsNullOrEmpty(_nameFilter)) { string filter = _nameFilter.ToLower(); if (fertilityAsset.Name?.ToLower().Contains(filter) != true && !fertilityAsset.DisplayName.ToLower().Contains(filter)) diff --git a/AnnoMapEditor/UI/Overlays/SelectIsland/SelectIslandViewModel.cs b/AnnoMapEditor/UI/Overlays/SelectIsland/SelectIslandViewModel.cs index e8e7b3c..f90e89a 100644 --- a/AnnoMapEditor/UI/Overlays/SelectIsland/SelectIslandViewModel.cs +++ b/AnnoMapEditor/UI/Overlays/SelectIsland/SelectIslandViewModel.cs @@ -1,4 +1,5 @@ -using AnnoMapEditor.DataArchives.Assets.Models; +using AnnoMapEditor.DataArchives; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.DataArchives.Assets.Repositories; using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.Utilities; @@ -14,7 +15,7 @@ public class SelectIslandViewModel : ObservableBase, IOverlayViewModel public event IslandSelectedEventHandler? IslandSelected; - public IslandRepository Islands { get; } = Settings.Instance.IslandRepository!; + public IslandRepository Islands { get; } = DataManager.Instance.IslandRepository; public string? PathFilter { @@ -27,11 +28,11 @@ public string? PathFilter } private string? _pathFilter; - public IEnumerable Regions { get; init; } = Region.All; + public IEnumerable Regions { get; init; } = RegionAsset.SupportedRegions; - private readonly Region _initialRegion; + private readonly RegionAsset _initialRegion; - public Region? SelectedRegion + public RegionAsset? SelectedRegion { get => _selectedRegion; set @@ -42,7 +43,7 @@ public Region? SelectedRegion ShowRegionWarning = _selectedRegion != _initialRegion; } } - private Region? _selectedRegion; + private RegionAsset? _selectedRegion; public IEnumerable IslandTypes { get; init; } = IslandType.All; @@ -106,7 +107,7 @@ public bool ShowRegionWarning private bool _showRegionWarning = false; - public SelectIslandViewModel(Region region, IslandType? islandType = null, IslandSize? islandSize = null) + public SelectIslandViewModel(RegionAsset region, IslandType? islandType = null, IslandSize? islandSize = null) { SelectedRegion = _initialRegion = region; SelectedIslandType = islandType; diff --git a/AnnoMapEditor/UI/Overlays/SelectSlots/SelectSlotsViewModel.cs b/AnnoMapEditor/UI/Overlays/SelectSlots/SelectSlotsViewModel.cs index 1fa4535..26d5d79 100644 --- a/AnnoMapEditor/UI/Overlays/SelectSlots/SelectSlotsViewModel.cs +++ b/AnnoMapEditor/UI/Overlays/SelectSlots/SelectSlotsViewModel.cs @@ -1,12 +1,10 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Utilities; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Windows; using System.Windows.Data; namespace AnnoMapEditor.UI.Overlays.SelectSlots @@ -77,11 +75,11 @@ public int Compare(Vector2? a, Vector2? b) public class SelectSlotsViewModel : ObservableBase, IOverlayViewModel { public event EventHandler>? FilterModified; - public IEnumerable Regions { get; init; } = Region.All; + public IEnumerable Regions { get; init; } = RegionAsset.SupportedRegions; - private readonly Region _initialRegion; + private readonly RegionAsset _initialRegion; - public Region SelectedRegion + public RegionAsset SelectedRegion { get => _selectedRegion; set @@ -92,7 +90,7 @@ public Region SelectedRegion ShowRegionWarning = _selectedRegion != _initialRegion; } } - private Region _selectedRegion; + private RegionAsset _selectedRegion; public bool ShowRegionWarning { @@ -141,7 +139,7 @@ public bool ShowOil public IEnumerable SlotAssignmentViewModelsRight { get; private set; } - public SelectSlotsViewModel(Region region, FixedIslandElement fixedIsland) + public SelectSlotsViewModel(RegionAsset region, FixedIslandElement fixedIsland) { _selectedRegion = _initialRegion = region; FixedIsland = fixedIsland; diff --git a/AnnoMapEditor/UI/Overlays/SelectSlots/SlotAssignmentViewModel.cs b/AnnoMapEditor/UI/Overlays/SelectSlots/SlotAssignmentViewModel.cs index 163f430..3723378 100644 --- a/AnnoMapEditor/UI/Overlays/SelectSlots/SlotAssignmentViewModel.cs +++ b/AnnoMapEditor/UI/Overlays/SelectSlots/SlotAssignmentViewModel.cs @@ -1,10 +1,8 @@ using AnnoMapEditor.DataArchives.Assets.Models; -using AnnoMapEditor.MapTemplates.Enums; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.Utilities; using System; using System.Collections.ObjectModel; -using System.Linq; using System.Windows.Data; using System.Windows.Media; @@ -62,7 +60,7 @@ public Brush BackgroundBrush } private Brush _backgroundBrush; - public Region SelectedRegion + public RegionAsset SelectedRegion { get => _selectedRegion; set @@ -71,10 +69,10 @@ public Region SelectedRegion UpdateFilter(); } } - private Region _selectedRegion; + private RegionAsset _selectedRegion; - public SlotAssignmentViewModel(SlotAssignment slotAssignment, Region selectedRegion) + public SlotAssignmentViewModel(SlotAssignment slotAssignment, RegionAsset selectedRegion) { SlotAssignment = slotAssignment; _selectedRegion = selectedRegion; @@ -96,11 +94,11 @@ private bool SlotAssetFilter(object item) throw new ArgumentException(); // Warning: Hardcoding - // The New World uses both random mine slots 614 and 1000029. However, none of the + // SouthAmerica uses both random mine slots 614 and 1000029. However, none of the // replacements for 1000029 contain the New World as an AssociatedRegion. Despite of // this, it is possible to have both 1010502 Iron Deposit and 1010507 Gold Deposit in // the New World. - if (_selectedRegion == Region.NewWorld + if (_selectedRegion == RegionAsset.SouthAmerica && SlotAssignment.Slot.ObjectGuid == SlotAsset.RANDOM_MINE_OLD_WORLD_GUID && (slotAsset.GUID == 1010501 || slotAsset.GUID == 1010507)) return true; @@ -142,13 +140,13 @@ private void UpdateBrushes() { long randomSlotGuid = SlotAssignment.Slot.ObjectGuid; - if (randomSlotGuid == SlotAsset.RANDOM_MINE_OLD_WORLD_GUID - || randomSlotGuid == SlotAsset.RANDOM_MINE_NEW_WORLD_GUID - || randomSlotGuid == SlotAsset.RANDOM_MINE_ARCTIC_GUID) + if (randomSlotGuid == SlotAsset.RandomMineOldWorld.GUID + || randomSlotGuid == SlotAsset.RandomMineNewWorld.GUID + || randomSlotGuid == SlotAsset.RandomMineArctic.GUID) PinBrush = Brushes.Gray; - else if (randomSlotGuid == SlotAsset.RANDOM_CLAY_GUID) + else if (randomSlotGuid == SlotAsset.RandomClay.GUID) PinBrush = Brushes.SandyBrown; - else if (randomSlotGuid == SlotAsset.RANDOM_OIL_GUID) + else if (randomSlotGuid == SlotAsset.RandomOil.GUID) PinBrush = Brushes.DarkSlateGray; else PinBrush = Brushes.Red; diff --git a/AnnoMapEditor/UI/Resources/Styles/Colors.xaml b/AnnoMapEditor/UI/Resources/Styles/Colors.xaml index d042621..980fe8e 100644 --- a/AnnoMapEditor/UI/Resources/Styles/Colors.xaml +++ b/AnnoMapEditor/UI/Resources/Styles/Colors.xaml @@ -9,4 +9,6 @@ #8024415F #708090 #ef182B3F + #ee0000 + #00aa00 \ No newline at end of file diff --git a/AnnoMapEditor/UI/Resources/Styles/Label.xaml b/AnnoMapEditor/UI/Resources/Styles/Label.xaml index 599ec75..9a9b9b6 100644 --- a/AnnoMapEditor/UI/Resources/Styles/Label.xaml +++ b/AnnoMapEditor/UI/Resources/Styles/Label.xaml @@ -13,30 +13,22 @@ - - + - diff --git a/AnnoMapEditor/UI/Windows/Main/DataPathStatus.cs b/AnnoMapEditor/UI/Windows/Main/DataPathStatus.cs index 7bedb5f..01ea7a4 100644 --- a/AnnoMapEditor/UI/Windows/Main/DataPathStatus.cs +++ b/AnnoMapEditor/UI/Windows/Main/DataPathStatus.cs @@ -2,7 +2,7 @@ namespace AnnoMapEditor.UI.Windows.Main { - public class DataPathStatus + public class GamePathStatus { public string Status { get; set; } = string.Empty; diff --git a/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml b/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml index 02b6c28..1067820 100644 --- a/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml +++ b/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml @@ -33,22 +33,19 @@ - - + + DataContext="{Binding MapTemplate, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" /> - - + + @@ -82,43 +79,16 @@ - + - - - - - - - The editor is work in progress. - - Visit GitHub for - roadmap - and - issue reporting. - + - + diff --git a/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml.cs b/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml.cs index c1577f0..fb74418 100644 --- a/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml.cs +++ b/AnnoMapEditor/UI/Windows/Main/MainWindow.xaml.cs @@ -1,13 +1,10 @@ using AnnoMapEditor.UI.Overlays; using AnnoMapEditor.UI.Overlays.ExportAsMod; -using AnnoMapEditor.Utilities; using Microsoft.Win32; using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Windows; -using System.Windows.Controls; namespace AnnoMapEditor.UI.Windows.Main { @@ -16,13 +13,17 @@ namespace AnnoMapEditor.UI.Windows.Main ///
public partial class MainWindow : Window { - public MainWindowViewModel ViewModel { get; } = new MainWindowViewModel(Settings.Instance); + private readonly MainWindowViewModel _viewModel; + private readonly string title; - public MainWindow() + + public MainWindow(MainWindowViewModel viewModel) { + _viewModel = viewModel; + DataContext= _viewModel; + InitializeComponent(); - DataContext = ViewModel; var exePath = Path.Join(AppContext.BaseDirectory, "AnnoMapEditor.exe"); var productVersion = ""; @@ -37,116 +38,20 @@ public MainWindow() title = $"{App.Title} {productVersion}"; Title = title; - ViewModel.PropertyChanged += ViewModel_PropertyChanged; - - CreateImportMenu(openMapMenu, ViewModel?.Maps); + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + _viewModel.PopulateOpenMapMenu(openMapMenu); } private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { - case "MapTemplate": - if (ViewModel?.MapTemplate is not null) - Title = $"{title} - {Path.GetFileName(ViewModel.MapTemplateFilePath)}"; + case nameof(MainWindowViewModel.MapTemplate): + if (_viewModel?.MapTemplate is not null) + Title = $"{title} - {Path.GetFileName(_viewModel.MapTemplateFilePath)}"; else Title = title; break; - case "Maps": - case "IsValidDataPath": - CreateImportMenu(openMapMenu, ViewModel?.Maps); - break; - } - } - - private async void OpenFile_Click(object sender, RoutedEventArgs e) - { - var picker = new OpenFileDialog - { - Filter = "Map templates (*.a7tinfo, *.xml)|*.a7tinfo;*.xml" - }; - - if (true == picker.ShowDialog()) - { - if (!Settings.Instance.IsValidDataPath) - { - int end = picker.FileName.IndexOf(@"\data\session"); - if (end == -1) - end = picker.FileName.IndexOf(@"\data\dlc"); - if (end != -1) - Settings.Instance.DataPath = picker.FileName[..end]; - } - - await ViewModel.OpenMap(picker.FileName); - } - } - - private void Configure_Click(object _, RoutedEventArgs _1) - { - var picker = new Ookii.Dialogs.Wpf.VistaFolderBrowserDialog - { - UseDescriptionForTitle = true, - Description = "Select your game (i.e. \"Anno 1800/\") folder or a folder where all .rda files are extracted into" - }; - - if (Settings.Instance.DataPath != null) - picker.SelectedPath = Settings.Instance.DataPath; - - if (true == picker.ShowDialog()) - { - Settings.Instance.DataPath = picker.SelectedPath; - } - } - - private void AutoDetect_Click(object _, RoutedEventArgs _1) - { - Settings.Instance.DataPath = Settings.GetInstallDirFromRegistry(); - } - - private void CreateImportMenu(ContextMenu parentMenu, List? mapGroups) - { - parentMenu.Items.Clear(); - - MenuItem openFile = new() { Header = "Open file..." }; - openFile.Click += OpenFile_Click; - parentMenu.Items.Add(openFile); - parentMenu.Items.Add(new Separator()); - - MenuItem newFile = new() { Header = "New Map file" }; - newFile.Click += NewMapFile_Click; - parentMenu.Items.Add(newFile); - parentMenu.Items.Add(new Separator()); - - if (mapGroups is null || mapGroups.Count == 0) - { - parentMenu.Items.Add(new MenuItem() { - Header = Settings.Instance.IsLoading ? "(loading RDA...)" : "Set game/RDA path to import", - IsEnabled = false - }); - return; - } - - foreach (var group in mapGroups) - { - MenuItem groupMenu = new() { Header = group.Name }; - - foreach (var map in group.Maps) - { - MenuItem mapMenu = new() { Header = map.Name, DataContext = map }; - mapMenu.Click += MapMenu_Click; - groupMenu.Items.Add(mapMenu); - } - - parentMenu.Items.Add(groupMenu); - } - } - - private async void MapMenu_Click(object sender, RoutedEventArgs e) - { - MapInfo? mapInfo = (sender as MenuItem)?.DataContext as MapInfo; - if (mapInfo?.FileName is not null) - { - await ViewModel.OpenMap(mapInfo.FileName, true); } } @@ -156,40 +61,23 @@ private async void ExportMap_Click(object sender, RoutedEventArgs e) { DefaultExt = ".a7tinfo", Filter = "Map template (*.a7tinfo)|*.a7tinfo|XML map template (*.xml)|*.xml", - FilterIndex = Path.GetExtension(ViewModel.MapTemplateFilePath)?.ToLower() == ".xml" ? 2 : 1, - FileName = Path.GetFileName(ViewModel.MapTemplateFilePath), + FilterIndex = Path.GetExtension(_viewModel.MapTemplateFilePath)?.ToLower() == ".xml" ? 2 : 1, + FileName = Path.GetFileName(_viewModel.MapTemplateFilePath), OverwritePrompt = true }; if (true == picker.ShowDialog() && picker.FileName is not null) { - await ViewModel.SaveMap(picker.FileName); + await _viewModel.SaveMap(picker.FileName); } } private void ExportMod_Click(object sender, RoutedEventArgs e) { - if (ViewModel.MapTemplate is null) + if (_viewModel.MapTemplate is null) return; - OverlayService.Instance.Show(new ExportAsModViewModel() - { - MapTemplate = ViewModel.MapTemplate - }); - } - - private void NewMapFile_Click(object sender, RoutedEventArgs e) - { - ViewModel.CreateNewMap(); - } - - private void Hyperlink_OpenBrowser(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) - { - var info = new ProcessStartInfo(e.Uri.AbsoluteUri) - { - UseShellExecute = true, - }; - Process.Start(info); + OverlayService.Instance.Show(new ExportAsModViewModel(_viewModel.MapTemplate)); } } } diff --git a/AnnoMapEditor/UI/Windows/Main/MainWindowViewModel.cs b/AnnoMapEditor/UI/Windows/Main/MainWindowViewModel.cs index 2b2f24d..6a3e848 100644 --- a/AnnoMapEditor/UI/Windows/Main/MainWindowViewModel.cs +++ b/AnnoMapEditor/UI/Windows/Main/MainWindowViewModel.cs @@ -1,29 +1,22 @@ using AnnoMapEditor.DataArchives; -using AnnoMapEditor.MapTemplates.Enums; +using AnnoMapEditor.DataArchives.Assets.Models; using AnnoMapEditor.MapTemplates.Models; using AnnoMapEditor.MapTemplates.Serializing; -using AnnoMapEditor.Mods.Models; using AnnoMapEditor.UI.Controls; using AnnoMapEditor.UI.Controls.IslandProperties; using AnnoMapEditor.UI.Controls.MapTemplates; using AnnoMapEditor.UI.Overlays.SelectIsland; using AnnoMapEditor.Utilities; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; +using Microsoft.Win32; using System.IO; -using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using System.Windows; -using System.Xml; +using System.Windows.Controls; namespace AnnoMapEditor.UI.Windows.Main { public class MainWindowViewModel : ObservableBase { - public MapTemplate? MapTemplate + public MapTemplate MapTemplate { get => _mapTemplate; private set @@ -33,23 +26,27 @@ private set SetProperty(ref _mapTemplate, value); SelectedIsland = null; - if(MapTemplateProperties is not null) - MapTemplateProperties.SelectedRegionChanged -= SelectedRegionChanged; - - MapTemplateProperties = value is null ? null : new(value); - OnPropertyChanged(nameof(MapTemplateProperties)); - - if(MapTemplateProperties is not null) - MapTemplateProperties.SelectedRegionChanged += SelectedRegionChanged; - - MapTemplateChecker = value is null ? null : new(value); - OnPropertyChanged(nameof(MapTemplateChecker)); + MapTemplateProperties = new(value); + MapTemplateChecker = new(value); } } } - private MapTemplate? _mapTemplate; - public MapTemplatePropertiesViewModel? MapTemplateProperties { get; private set; } - public MapTemplateCheckerViewModel? MapTemplateChecker { get; private set; } + private MapTemplate _mapTemplate; + + public MapTemplatePropertiesViewModel MapTemplateProperties + { + get => _mapTemplateProperties; + private set => SetProperty(ref _mapTemplateProperties, value); + } + private MapTemplatePropertiesViewModel _mapTemplateProperties; + + public MapTemplateCheckerViewModel MapTemplateChecker + { + get => _mapTemplateChecker; + private set => SetProperty(ref _mapTemplateChecker, value); + } + private MapTemplateCheckerViewModel _mapTemplateChecker; + public IslandElement? SelectedIsland { @@ -71,7 +68,7 @@ public IslandElement? SelectedIsland else if (value is FixedIslandElement fixedIsland) { SelectedRandomIslandPropertiesViewModel = null; - SelectedFixedIslandPropertiesViewModel = new(fixedIsland, MapTemplate!.Region); + SelectedFixedIslandPropertiesViewModel = new(fixedIsland, MapTemplate!.Session.Region); } } } @@ -111,48 +108,17 @@ public string? MapTemplateFilePath } private string? _mapTemplateFilePath; - public DataPathStatus DataPathStatus - { - get => _dataPathStatus; - private set => SetProperty(ref _dataPathStatus, value); - } - private DataPathStatus _dataPathStatus = new(); - - public ExportStatus ExportStatus - { - get => _exportStatus; - private set => SetProperty(ref _exportStatus, value); - } - private ExportStatus _exportStatus = new(); - - public List? Maps - { - get => _maps; - private set => SetProperty(ref _maps, value); - } - private List? _maps; - public Settings Settings { get; private set; } - - public MainWindowViewModel(Settings settings) + public MainWindowViewModel(MapTemplate mapTemplate) { - Settings = settings; - Settings.PropertyChanged += Settings_PropertyChanged; - - - UpdateStatusAndMenus(); + MapTemplate = mapTemplate; } - private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e) + public MainWindowViewModel() { - if (e.PropertyName == nameof(Settings.IsLoading)) - UpdateStatusAndMenus(); + CreateNewMap(); } - private void SelectedRegionChanged(object? sender, EventArgs _) - { - UpdateExportStatus(); - } public async Task OpenMap(string a7tinfoPath, bool fromArchive = false) { @@ -164,8 +130,6 @@ public async Task OpenMap(string a7tinfoPath, bool fromArchive = false) else MapTemplate = await mapTemplateReader.FromFileAsync(a7tinfoPath); - - UpdateExportStatus(); } public void CreateNewMap() @@ -174,10 +138,7 @@ public void CreateNewMap() const int DEFAULT_PLAYABLE_SIZE = 2160; MapTemplateFilePath = null; - - MapTemplate = new MapTemplate(DEFAULT_MAP_SIZE, DEFAULT_PLAYABLE_SIZE, Region.Moderate); - - UpdateExportStatus(); + MapTemplate = new MapTemplate(DEFAULT_MAP_SIZE, DEFAULT_PLAYABLE_SIZE, SessionAsset.OldWorld); } public async Task SaveMap(string filePath) @@ -189,120 +150,57 @@ public async Task SaveMap(string filePath) MapTemplateWriter mapTemplateWriter = new(); if (Path.GetExtension(filePath).ToLower() == ".a7tinfo") - await mapTemplateWriter.ToA7tinfoAsync(MapTemplate, filePath); + await mapTemplateWriter.WriteA7tinfoAsync(MapTemplate, filePath); else - await mapTemplateWriter.ToXmlAsync(MapTemplate, filePath); + await mapTemplateWriter.WriteXmlAsync(MapTemplate, filePath); } - private void UpdateExportStatus() + public void PopulateOpenMapMenu(ContextMenu menu) { - if (Settings.IsLoading) - { - // still loading - ExportStatus = new ExportStatus() - { - CanExportAsMod = false, - ExportAsModText = "(loading RDA...)" - }; - } - else if (Settings.IsValidDataPath) - { - bool supportedFormat = Mod.CanSave(MapTemplate); - bool archiveReady = Settings.DataArchive is RdaDataArchive; + menu.Items.Clear(); - ExportStatus = new ExportStatus() - { - CanExportAsMod = archiveReady && supportedFormat, - ExportAsModText = archiveReady ? supportedFormat ? "As playable mod..." : "As mod: only works with Old World maps currently" : "As mod: set game path to save" - }; - } - else + MenuItem openMapFile = new() { Header = "Open file..." }; + openMapFile.Click += (_, _) => OpenMapFileDialog(); + menu.Items.Add(openMapFile); + menu.Items.Add(new Separator()); + + MenuItem newFile = new() { Header = "New Map file" }; + newFile.Click += (_, _) => CreateNewMap(); + menu.Items.Add(newFile); + menu.Items.Add(new Separator()); + + foreach (MapGroup group in DataManager.Instance.MapGroupRepository.MapGroups) { - ExportStatus = new ExportStatus() + MenuItem groupMenu = new() { Header = group.Name }; + + foreach (MapInfo map in group.Maps) { - ExportAsModText = "As mod: set game path to save", - CanExportAsMod = false - }; + MenuItem mapMenu = new() { Header = map.Name, DataContext = map }; + mapMenu.Click += (_, _) => _ = OpenMap(map.FileName!, true); + groupMenu.Items.Add(mapMenu); + } + + menu.Items.Add(groupMenu); } } - private void UpdateStatusAndMenus() + public async void OpenMapFileDialog() { - if (Settings.IsLoading) - { - // still loading - DataPathStatus = new DataPathStatus() - { - Status = "loading RDA...", - ToolTip = "", - Configure = Visibility.Collapsed, - AutoDetect = Visibility.Collapsed, - }; - } - else if (Settings.IsValidDataPath) + var picker = new OpenFileDialog { - DataPathStatus = new DataPathStatus() - { - Status = Settings.DataArchive is RdaDataArchive ? "Game path set ✔" : "Extracted RDA path set ✔", - ToolTip = Settings.DataArchive.DataPath, - ConfigureText = "Change...", - AutoDetect = Settings.DataArchive is RdaDataArchive ? Visibility.Collapsed : Visibility.Visible, - }; - - Dictionary templateGroups = new() - { - ["DLCs"] = new(@"data\/(?!=sessions\/)([^\/]+)"), - ["Moderate"] = new(@"data\/sessions\/.+moderate"), - ["New World"] = new(@"data\/sessions\/.+colony01") - }; + Filter = "Map templates (*.a7tinfo, *.xml)|*.a7tinfo;*.xml" + }; - //load from assets instead. - var mapTemplates = Settings.DataArchive.Find("*.a7tinfo"); - - Maps = new() - { - new MapGroup("Campaign", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/campaign")), new(@"\/campaign_([^\/]+)\.")), - new MapGroup("Moderate, Archipelago", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_archipel")), new(@"\/([^\/]+)\.")), - new MapGroup("Moderate, Atoll", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_atoll")), new(@"\/([^\/]+)\.")), - new MapGroup("Moderate, Corners", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_corners")), new(@"\/([^\/]+)\.")), - new MapGroup("Moderate, Island Arc", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_islandarc")), new(@"\/([^\/]+)\.")), - new MapGroup("Moderate, Snowflake", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate/moderate_snowflake")), new(@"\/([^\/]+)\.")), - new MapGroup("New World, Large", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_l_")), new(@"\/([^\/]+)\.")), - new MapGroup("New World, Medium", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_m_")), new(@"\/([^\/]+)\.")), - new MapGroup("New World, Small", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/colony01/colony01_s_")), new(@"\/([^\/]+)\.")), - new MapGroup("DLCs", mapTemplates.Where(x => !x.StartsWith(@"data/sessions/")), new(@"data\/([^\/]+)\/.+\/maps\/([^\/]+)")) - //new MapGroup("Moderate", mapTemplates.Where(x => x.StartsWith(@"data/sessions/maps/pool/moderate")), new(@"\/([^\/]+)\.")) - }; - } - else + if (true == picker.ShowDialog()) { - DataPathStatus = new DataPathStatus() - { - Status = "⚠ Game or RDA path not valid.", - ToolTip = null, - ConfigureText = "Select...", - AutoDetect = Visibility.Visible, - }; + int end = picker.FileName.IndexOf(@"\data\session"); + if (end == -1) + end = picker.FileName.IndexOf(@"\data\dlc"); + if (end != -1) + Settings.Instance.GamePath = picker.FileName[..end]; - Maps = new(); + await OpenMap(picker.FileName); } - - UpdateExportStatus(); - } - - private IEnumerable? LoadMapsFromAssets() - { - const string assetsXmlPath = "data/config/export/main/asset/assets.xml"; - - Stopwatch stopwatch= Stopwatch.StartNew(); - using var assetStream = Settings.DataArchive.OpenRead(assetsXmlPath) - ?? throw new FileNotFoundException(assetsXmlPath); - XmlDocument doc = new XmlDocument(); - doc.Load(assetStream); - var nodes = doc.SelectNodes("//Asset[Template='MapTemplate']/Values/MapTemplate[TemplateRegion]/*[self::TemplateFilename or self::EnlargedTemplateFilename]"); - stopwatch.Stop(); - double i = stopwatch.Elapsed.TotalMilliseconds; - return nodes?.Cast().Select(x => Path.ChangeExtension(x.InnerText, "a7tinfo")); } } } diff --git a/AnnoMapEditor/UI/Windows/Main/MapInfo.cs b/AnnoMapEditor/UI/Windows/Main/MapInfo.cs index 655662e..041c090 100644 --- a/AnnoMapEditor/UI/Windows/Main/MapInfo.cs +++ b/AnnoMapEditor/UI/Windows/Main/MapInfo.cs @@ -2,8 +2,8 @@ { public class MapInfo { - public string? Name; + public string Name; - public string? FileName; + public string FileName; } } diff --git a/AnnoMapEditor/UI/Windows/Start/StartWindow.xaml b/AnnoMapEditor/UI/Windows/Start/StartWindow.xaml new file mode 100644 index 0000000..cb530d1 --- /dev/null +++ b/AnnoMapEditor/UI/Windows/Start/StartWindow.xaml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +