diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index ab071c59b1..022b72f56c 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -15,6 +15,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Reflection; using System.Security.Cryptography; using System.Text; using OpenRA.FileSystem; @@ -66,28 +67,185 @@ namespace OpenRA MissionSelector = 4 } + class MapField + { + enum Type { Normal, NodeList, MiniYaml } + readonly FieldInfo field; + readonly PropertyInfo property; + readonly Type type; + + readonly string key; + readonly string fieldName; + readonly bool required; + readonly string ignoreIfValue; + + public MapField(string key, string fieldName = null, bool required = true, string ignoreIfValue = null) + { + this.key = key; + this.fieldName = fieldName ?? key; + this.required = required; + this.ignoreIfValue = ignoreIfValue; + + field = typeof(Map).GetField(this.fieldName); + property = typeof(Map).GetProperty(this.fieldName); + if (field == null && property == null) + throw new InvalidOperationException("Map does not have a field/property " + fieldName); + + var t = field != null ? field.FieldType : property.PropertyType; + type = t == typeof(List) ? Type.NodeList : + t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal; + } + + public void Deserialize(Map map, List nodes) + { + var node = nodes.FirstOrDefault(n => n.Key == key); + if (node == null) + { + if (required) + throw new YamlException("Required field `{0}` not found in map.yaml".F(key)); + return; + } + + if (field != null) + { + if (type == Type.NodeList) + field.SetValue(map, node.Value.Nodes); + else if (type == Type.MiniYaml) + field.SetValue(map, node.Value); + else + FieldLoader.LoadField(map, fieldName, node.Value.Value); + } + + if (property != null) + { + if (type == Type.NodeList) + property.SetValue(map, node.Value.Nodes, null); + else if (type == Type.MiniYaml) + property.SetValue(map, node.Value, null); + else + FieldLoader.LoadField(map, fieldName, node.Value.Value); + } + } + + public void Serialize(Map map, List nodes) + { + var value = field != null ? field.GetValue(map) : property.GetValue(map, null); + if (type == Type.NodeList) + { + var listValue = (List)value; + if (required || listValue.Any()) + nodes.Add(new MiniYamlNode(key, null, listValue)); + } + else if (type == Type.MiniYaml) + { + var yamlValue = (MiniYaml)value; + if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Any()))) + nodes.Add(new MiniYamlNode(key, yamlValue)); + } + else + { + var formattedValue = FieldSaver.FormatValue(value); + if (required || formattedValue != ignoreIfValue) + nodes.Add(new MiniYamlNode(key, formattedValue)); + } + } + } + public class Map : IReadOnlyFileSystem { public const int SupportedMapFormat = 10; - public readonly MapGrid Grid; - readonly ModData modData; + /// Defines the order of the fields in map.yaml + static readonly MapField[] YamlFields = + { + new MapField("MapFormat"), + new MapField("RequiresMod"), + new MapField("Title"), + new MapField("Author"), + new MapField("Tileset"), + new MapField("MapSize"), + new MapField("Bounds"), + new MapField("Visibility"), + new MapField("Type"), + new MapField("LockPreview", required: false, ignoreIfValue: "False"), + new MapField("Players", "PlayerDefinitions"), + new MapField("Actors", "ActorDefinitions"), + new MapField("Rules", "RuleDefinitions", required: false), + new MapField("Sequences", "SequenceDefinitions", required: false), + new MapField("VoxelSequences", "VoxelSequenceDefinitions", required: false), + new MapField("Weapons", "WeaponDefinitions", required: false), + new MapField("Voices", "VoiceDefinitions", required: false), + new MapField("Music", "MusicDefinitions", required: false), + new MapField("Notifications", "NotificationDefinitions", required: false), + new MapField("Translations", "TranslationDefinitions", required: false) + }; - public IReadOnlyPackage Package { get; private set; } + // Format versions + public int MapFormat { get; private set; } + public readonly byte TileFormat = 2; - // Yaml map data - public string Uid { get; private set; } - public int MapFormat; - public MapVisibility Visibility = MapVisibility.Lobby; + // Standard yaml metadata public string RequiresMod; - public string Title; - public string Type = "Conquest"; public string Author; public string Tileset; public bool LockPreview; + public Rectangle Bounds; + public MapVisibility Visibility = MapVisibility.Lobby; + public string Type = "Conquest"; + + public int2 MapSize { get; private set; } + + // Player and actor yaml. Public for access by the map importers and lint checks. + public List PlayerDefinitions = new List(); + public List ActorDefinitions = new List(); + + // Custom map yaml. Public for access by the map importers and lint checks + public readonly MiniYaml RuleDefinitions; + public readonly MiniYaml SequenceDefinitions; + public readonly MiniYaml VoxelSequenceDefinitions; + public readonly MiniYaml WeaponDefinitions; + public readonly MiniYaml VoiceDefinitions; + public readonly MiniYaml MusicDefinitions; + public readonly MiniYaml NotificationDefinitions; + public readonly MiniYaml TranslationDefinitions; + + // Generated data + public readonly MapGrid Grid; + public IReadOnlyPackage Package { get; private set; } + public string Uid { get; private set; } + + public Ruleset Rules { get; private set; } public bool InvalidCustomRules { get; private set; } + /// + /// The top-left of the playable area in projected world coordinates + /// This is a hacky workaround for legacy functionality. Do not use for new code. + /// + public WPos ProjectedTopLeft { get; private set; } + + /// + /// The bottom-right of the playable area in projected world coordinates + /// This is a hacky workaround for legacy functionality. Do not use for new code. + /// + public WPos ProjectedBottomRight { get; private set; } + + public CellLayer Tiles { get; private set; } + public CellLayer Resources { get; private set; } + public CellLayer Height { get; private set; } + public CellLayer CustomTerrain { get; private set; } + + public ProjectedCellRegion ProjectedCellBounds { get; private set; } + public CellRegion AllCells { get; private set; } + public List AllEdgeCells { get; private set; } + + // Internal data + readonly ModData modData; + CellLayer cachedTerrainIndexes; + bool initializedCellProjection; + CellLayer cellProjection; + CellLayer> inverseCellProjection; + public static string ComputeUID(IReadOnlyPackage package) { // UID is calculated by taking an SHA1 of the yaml and binary data @@ -111,55 +269,6 @@ namespace OpenRA } } - public Rectangle Bounds; - - /// - /// The top-left of the playable area in projected world coordinates - /// This is a hacky workaround for legacy functionality. Do not use for new code. - /// - public WPos ProjectedTopLeft; - - /// - /// The bottom-right of the playable area in projected world coordinates - /// This is a hacky workaround for legacy functionality. Do not use for new code. - /// - public WPos ProjectedBottomRight; - - // Yaml map data - [FieldLoader.Ignore] public readonly MiniYaml RuleDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml SequenceDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml VoxelSequenceDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml WeaponDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml VoiceDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml MusicDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml NotificationDefinitions; - [FieldLoader.Ignore] public readonly MiniYaml TranslationDefinitions; - - [FieldLoader.Ignore] public List PlayerDefinitions = new List(); - [FieldLoader.Ignore] public List ActorDefinitions = new List(); - - // Binary map data - [FieldLoader.Ignore] public byte TileFormat = 2; - - public int2 MapSize; - - [FieldLoader.Ignore] public CellLayer Tiles; - [FieldLoader.Ignore] public CellLayer Resources; - [FieldLoader.Ignore] public CellLayer Height; - - [FieldLoader.Ignore] public CellLayer CustomTerrain; - [FieldLoader.Ignore] CellLayer cachedTerrainIndexes; - - [FieldLoader.Ignore] bool initializedCellProjection; - [FieldLoader.Ignore] CellLayer cellProjection; - [FieldLoader.Ignore] CellLayer> inverseCellProjection; - - public Ruleset Rules { get; private set; } - - [FieldLoader.Ignore] public ProjectedCellRegion ProjectedCellBounds; - [FieldLoader.Ignore] public CellRegion AllCells; - public List AllEdgeCells { get; private set; } - /// /// Initializes a new map created by the editor or importer. /// The map will not receive a valid UID until after it has been saved and reloaded. @@ -204,20 +313,12 @@ namespace OpenRA throw new InvalidDataException("Not a valid map\n File: {1}".F(package.Name)); var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name)); - FieldLoader.Load(this, yaml); + foreach (var field in YamlFields) + field.Deserialize(this, yaml.Nodes); if (MapFormat != SupportedMapFormat) throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(MapFormat, package.Name)); - RuleDefinitions = LoadRuleSection(yaml, "Rules"); - SequenceDefinitions = LoadRuleSection(yaml, "Sequences"); - VoxelSequenceDefinitions = LoadRuleSection(yaml, "VoxelSequences"); - WeaponDefinitions = LoadRuleSection(yaml, "Weapons"); - VoiceDefinitions = LoadRuleSection(yaml, "Voices"); - MusicDefinitions = LoadRuleSection(yaml, "Music"); - NotificationDefinitions = LoadRuleSection(yaml, "Notifications"); - TranslationDefinitions = LoadRuleSection(yaml, "Translations"); - PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players"); ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors"); @@ -284,12 +385,6 @@ namespace OpenRA Uid = ComputeUID(Package); } - MiniYaml LoadRuleSection(MiniYaml yaml, string section) - { - var node = yaml.Nodes.FirstOrDefault(n => n.Key == section); - return node != null ? node.Value : null; - } - void PostInit() { try @@ -417,49 +512,8 @@ namespace OpenRA MapFormat = SupportedMapFormat; var root = new List(); - var fields = new[] - { - "MapFormat", - "RequiresMod", - "Title", - "Author", - "Tileset", - "MapSize", - "Bounds", - "Visibility", - "Type", - }; - - foreach (var field in fields) - { - var f = GetType().GetField(field); - if (f.GetValue(this) == null) - continue; - root.Add(new MiniYamlNode(field, FieldSaver.FormatValue(this, f))); - } - - // Save LockPreview field only if it's set - if (LockPreview) - root.Add(new MiniYamlNode("LockPreview", "True")); - - root.Add(new MiniYamlNode("Players", null, PlayerDefinitions)); - root.Add(new MiniYamlNode("Actors", null, ActorDefinitions)); - - var ruleSections = new[] - { - new MiniYamlNode("Rules", RuleDefinitions), - new MiniYamlNode("Sequences", SequenceDefinitions), - new MiniYamlNode("VoxelSequences", VoxelSequenceDefinitions), - new MiniYamlNode("Weapons", WeaponDefinitions), - new MiniYamlNode("Voices", VoiceDefinitions), - new MiniYamlNode("Music", MusicDefinitions), - new MiniYamlNode("Notifications", NotificationDefinitions), - new MiniYamlNode("Translations", TranslationDefinitions) - }; - - foreach (var section in ruleSections) - if (section.Value != null && (section.Value.Value != null || section.Value.Nodes.Any())) - root.Add(section); + foreach (var field in YamlFields) + field.Serialize(this, root); // Saving to a new package: copy over all the content from the map if (Package != null && toPackage != Package)