context: #region Copyright & License Information
  /*
   * Copyright 2007-2017 The OpenRA Developers (see AUTHORS)
   * This file is part of OpenRA, which is free software. It is made
   * available to you under the terms of the GNU General Public License
   * as published by the Free Software Foundation, either version 3 of
   * the License, or (at your option) any later version. For more
   * information, see COPYING.
   */
  #endregion
  
  using System;
  using System.Collections.Generic;
  using System.Drawing;
  using System.Drawing.Imaging;
  using System.IO;
  using System.Linq;
  using System.Reflection;
  using System.Text;
  using OpenRA.FileSystem;
  using OpenRA.Graphics;
  using OpenRA.Support;
  using OpenRA.Traits;
  
  namespace OpenRA
  {
  	struct BinaryDataHeader
  	{
  		public readonly byte Format;
  		public readonly uint TilesOffset;
  		public readonly uint HeightsOffset;
  		public readonly uint ResourcesOffset;
  
  		public BinaryDataHeader(Stream s, int2 expectedSize)
  		{
  			Format = s.ReadUInt8();
  			var width = s.ReadUInt16();
  			var height = s.ReadUInt16();
  			if (width != expectedSize.X || height != expectedSize.Y)
  				throw new InvalidDataException("Invalid tile data");
  
  			if (Format == 1)
  			{
  				TilesOffset = 5;
  				HeightsOffset = 0;
  				ResourcesOffset = (uint)(3 * width * height + 5);
  			}
  			else if (Format == 2)
  			{
  				TilesOffset = s.ReadUInt32();
  				HeightsOffset = s.ReadUInt32();
  				ResourcesOffset = s.ReadUInt32();
  			}
  			else
  				throw new InvalidDataException("Unknown binary map format '{0}'".F(Format));
  		}
  	}
  
  	[Flags]
  	public enum MapVisibility
  	{
  		Lobby = 1,
  		Shellmap = 2,
  		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<MiniYamlNode>) ? Type.NodeList :
  				t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
  		}
  
  		public void Deserialize(Map map, List<MiniYamlNode> 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<MiniYamlNode> nodes)
  		{
  			var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
  			if (type == Type.NodeList)
  			{
  				var listValue = (List<MiniYamlNode>)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 = 11;
  
  		/// <summary>Defines the order of the fields in map.yaml</summary>
  		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("Categories"),
  			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("ModelSequences", "ModelSequenceDefinitions", 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)
  		};
  
  		// Format versions
  		public int MapFormat { get; private set; }
  		public readonly byte TileFormat = 2;
  
  		// Standard yaml metadata
  		public string RequiresMod;
  		public string Title;
  		public string Author;
  		public string Tileset;
  		public bool LockPreview;
  		public Rectangle Bounds;
  		public MapVisibility Visibility = MapVisibility.Lobby;
  		public string[] Categories = { "Conquest" };
  
  		public int2 MapSize { get; private set; }
  
  		// Player and actor yaml. Public for access by the map importers and lint checks.
  		public List<MiniYamlNode> PlayerDefinitions = new List<MiniYamlNode>();
  		public List<MiniYamlNode> ActorDefinitions = new List<MiniYamlNode>();
  
  		// Custom map yaml. Public for access by the map importers and lint checks
  		public readonly MiniYaml RuleDefinitions;
  		public readonly MiniYaml SequenceDefinitions;
  		public readonly MiniYaml ModelSequenceDefinitions;
  		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; }
  
  		/// <summary>
  		/// 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.
  		/// </summary>
  		public WPos ProjectedTopLeft { get; private set; }
  
  		/// <summary>
  		/// 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.
  		/// </summary>
  		public WPos ProjectedBottomRight { get; private set; }
  
  		public CellLayer<TerrainTile> Tiles { get; private set; }
  		public CellLayer<ResourceTile> Resources { get; private set; }
  		public CellLayer<byte> Height { get; private set; }
  		public CellLayer<byte> CustomTerrain { get; private set; }
  
  		public ProjectedCellRegion ProjectedCellBounds { get; private set; }
  		public CellRegion AllCells { get; private set; }
  		public List<CPos> AllEdgeCells { get; private set; }
  
  		// Internal data
  		readonly ModData modData;
  		CellLayer<short> cachedTerrainIndexes;
  		bool initializedCellProjection;
  		CellLayer<PPos[]> cellProjection;
  		CellLayer<List<MPos>> inverseCellProjection;
  		CellLayer<byte> projectedHeight;
  
  		public static string ComputeUID(IReadOnlyPackage package)
  		{
  			// UID is calculated by taking an SHA1 of the yaml and binary data
  			var requiredFiles = new[] { "map.yaml", "map.bin" };
  			var contents = package.Contents.ToList();
  			foreach (var required in requiredFiles)
  				if (!contents.Contains(required))
  					throw new FileNotFoundException("Required file {0} not present in this map".F(required));
  
  			using (var ms = new MemoryStream())
  			{
  				foreach (var filename in contents)
  					if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua"))
  						using (var s = package.GetStream(filename))
  							s.CopyTo(ms);
  
  				// Take the SHA1
  				ms.Seek(0, SeekOrigin.Begin);
  				return CryptoUtil.SHA1Hash(ms);
  			}
  		}
  
  		/// <summary>
  		/// 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.
  		/// </summary>
  		public Map(ModData modData, TileSet tileset, int width, int height)
  		{
  			this.modData = modData;
  			var size = new Size(width, height);
  			Grid = modData.Manifest.Get<MapGrid>();
  			var tileRef = new TerrainTile(tileset.Templates.First().Key, 0);
  
  			Title = "Name your map here";
  			Author = "Your name here";
  
  			MapSize = new int2(size);
  			Tileset = tileset.Id;
  
  			// Empty rules that can be added to by the importers.
  			// Will be dropped on save if nothing is added to it
  			RuleDefinitions = new MiniYaml("");
  
  			Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
  			Resources = new CellLayer<ResourceTile>(Grid.Type, size);
  			Height = new CellLayer<byte>(Grid.Type, size);
  			if (Grid.MaximumTerrainHeight > 0)
  			{
  				Height.CellEntryChanged += UpdateProjection;
  				Tiles.CellEntryChanged += UpdateProjection;
  			}
  
  			Tiles.Clear(tileRef);
  
  			PostInit();
  		}
  
  		public Map(ModData modData, IReadOnlyPackage package)
  		{
  			this.modData = modData;
  			Package = package;
  
  			if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
  				throw new InvalidDataException("Not a valid map\n File: {0}".F(package.Name));
  
  			var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
  			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));
  
  			PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
  			ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
  
  			Grid = modData.Manifest.Get<MapGrid>();
  
  			var size = new Size(MapSize.X, MapSize.Y);
  			Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
  			Resources = new CellLayer<ResourceTile>(Grid.Type, size);
  			Height = new CellLayer<byte>(Grid.Type, size);
  
  			using (var s = Package.GetStream("map.bin"))
  			{
  				var header = new BinaryDataHeader(s, MapSize);
  				if (header.TilesOffset > 0)
  				{
  					s.Position = header.TilesOffset;
  					for (var i = 0; i < MapSize.X; i++)
  					{
  						for (var j = 0; j < MapSize.Y; j++)
  						{
  							var tile = s.ReadUInt16();
  							var index = s.ReadUInt8();
  
  							// TODO: Remember to remove this when rewriting tile variants / PickAny
  							if (index == byte.MaxValue)
  								index = (byte)(i % 4 + (j % 4) * 4);
  
  							Tiles[new MPos(i, j)] = new TerrainTile(tile, index);
  						}
  					}
  				}
  
  				if (header.ResourcesOffset > 0)
  				{
  					s.Position = header.ResourcesOffset;
  					for (var i = 0; i < MapSize.X; i++)
  					{
  						for (var j = 0; j < MapSize.Y; j++)
  						{
  							var type = s.ReadUInt8();
  							var density = s.ReadUInt8();
  							Resources[new MPos(i, j)] = new ResourceTile(type, density);
  						}
  					}
  				}
  
  				if (header.HeightsOffset > 0)
  				{
  					s.Position = header.HeightsOffset;
  					for (var i = 0; i < MapSize.X; i++)
  						for (var j = 0; j < MapSize.Y; j++)
  							Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
  				}
  			}
  
  			if (Grid.MaximumTerrainHeight > 0)
  			{
  				Tiles.CellEntryChanged += UpdateProjection;
  				Height.CellEntryChanged += UpdateProjection;
  			}
  
  			PostInit();
  
  			Uid = ComputeUID(Package);
  		}
  
  		void PostInit()
  		{
  			try
  			{
  				Rules = Ruleset.Load(modData, this, Tileset, RuleDefinitions, WeaponDefinitions,
  					VoiceDefinitions, NotificationDefinitions, MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions);
  			}
  			catch (Exception e)
  			{
  				Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e);
  				InvalidCustomRules = true;
  				Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset);
  			}
  
  			Rules.Sequences.Preload();
  
  			var tl = new MPos(0, 0).ToCPos(this);
  			var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
  
gt: 			AllCells = new CellRegion(Grid.Type, tl, br);