#region Copyright & License Information /* * Copyright 2007-2016 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.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Network; 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)); } } public class MapOptions { public bool? Creeps; public bool? AllyBuildRadius; public int? StartingCash; public string TechLevel; public bool ConfigurableStartingUnits = true; public string[] Difficulties = { }; public bool? ShortGame; public void UpdateServerSettings(Session.Global settings) { if (Creeps.HasValue) settings.Creeps = Creeps.Value; if (AllyBuildRadius.HasValue) settings.AllyBuildRadius = AllyBuildRadius.Value; if (StartingCash.HasValue) settings.StartingCash = StartingCash.Value; if (ShortGame.HasValue) settings.ShortGame = ShortGame.Value; } } public class MapVideos { public string BackgroundInfo; public string Briefing; public string GameStart; public string GameWon; public string GameLost; } [Flags] public enum MapVisibility { Lobby = 1, Shellmap = 2, MissionSelector = 4 } public class Map : IReadOnlyFileSystem { public const int SupportedMapFormat = 8; public const int MaxTilesInCircleRange = 50; public readonly MapGrid Grid; readonly ModData modData; [FieldLoader.Ignore] public readonly WVec[] SubCellOffsets; public readonly SubCell DefaultSubCell; public readonly SubCell LastSubCell; public IReadOnlyPackage Package { get; private set; } // Yaml map data public string Uid { get; private set; } public int MapFormat; public MapVisibility Visibility = MapVisibility.Lobby; public string RequiresMod; public string Title; public string Type = "Conquest"; public string Description; public string Author; public string Tileset; public bool AllowStartUnitConfig = true; public Bitmap CustomPreview; public bool InvalidCustomRules { get; private set; } public WVec OffsetOfSubCell(SubCell subCell) { if (subCell == SubCell.Invalid || subCell == SubCell.Any) return WVec.Zero; return SubCellOffsets[(int)subCell]; } [FieldLoader.LoadUsing("LoadOptions")] public MapOptions Options; static object LoadOptions(MiniYaml y) { var options = new MapOptions(); var nodesDict = y.ToDictionary(); if (nodesDict.ContainsKey("Options")) FieldLoader.Load(options, nodesDict["Options"]); return options; } [FieldLoader.LoadUsing("LoadVideos")] public MapVideos Videos; static object LoadVideos(MiniYaml y) { var videos = new MapVideos(); var nodesDict = y.ToDictionary(); if (nodesDict.ContainsKey("Videos")) FieldLoader.Load(videos, nodesDict["Videos"]); return videos; } public static string ComputeUID(IReadOnlyPackage package) { // UID is calculated by taking an SHA1 of the yaml and binary data using (var ms = new MemoryStream()) { // Read the relevant data into the buffer using (var s = package.GetStream("map.yaml")) { if (s == null) throw new FileNotFoundException("Required file map.yaml not present in this map"); s.CopyTo(ms); } using (var s = package.GetStream("map.bin")) { if (s == null) throw new FileNotFoundException("Required file map.bin not present in this map"); s.CopyTo(ms); } // Take the SHA1 ms.Seek(0, SeekOrigin.Begin); using (var csp = SHA1.Create()) return new string(csp.ComputeHash(ms).SelectMany(a => a.ToString("x2")).ToArray()); } } 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; public Lazy SpawnPoints; // Yaml map data [FieldLoader.Ignore] public List RuleDefinitions = new List(); [FieldLoader.Ignore] public List SequenceDefinitions = new List(); [FieldLoader.Ignore] public List VoxelSequenceDefinitions = new List(); [FieldLoader.Ignore] public List WeaponDefinitions = new List(); [FieldLoader.Ignore] public List VoiceDefinitions = new List(); [FieldLoader.Ignore] public List MusicDefinitions = new List(); [FieldLoader.Ignore] public List NotificationDefinitions = new List(); [FieldLoader.Ignore] public List TranslationDefinitions = new List(); [FieldLoader.Ignore] public List PlayerDefinitions = new List(); [FieldLoader.Ignore] public List ActorDefinitions = new List(); [FieldLoader.Ignore] public List SmudgeDefinitions = new List(); // Binary map data [FieldLoader.Ignore] public byte TileFormat = 2; public int2 MapSize; [FieldLoader.Ignore] public Lazy> MapTiles; [FieldLoader.Ignore] public Lazy> MapResources; [FieldLoader.Ignore] public Lazy> MapHeight; [FieldLoader.Ignore] public CellLayer CustomTerrain; [FieldLoader.Ignore] CellLayer cachedTerrainIndexes; [FieldLoader.Ignore] bool initializedCellProjection; [FieldLoader.Ignore] CellLayer cellProjection; [FieldLoader.Ignore] CellLayer> inverseCellProjection; [FieldLoader.Ignore] Lazy cachedTileSet; [FieldLoader.Ignore] Lazy rules; public Ruleset Rules { get { return rules != null ? rules.Value : null; } } public SequenceProvider SequenceProvider { get { return Rules.Sequences[Tileset]; } } [FieldLoader.Ignore] public ProjectedCellRegion ProjectedCellBounds; [FieldLoader.Ignore] public CellRegion AllCells; void AssertExists(string filename) { using (var s = Package.GetStream(filename)) if (s == null) throw new InvalidOperationException("Required file {0} not present in this map".F(filename)); } /// /// 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. /// public Map(ModData modData, TileSet tileset, int width, int height) { this.modData = modData; var size = new Size(width, height); Grid = modData.Manifest.Get(); var tileRef = new TerrainTile(tileset.Templates.First().Key, 0); Title = "Name your map here"; Description = "Describe your map here"; Author = "Your name here"; MapSize = new int2(size); Tileset = tileset.Id; Videos = new MapVideos(); Options = new MapOptions(); MapResources = Exts.Lazy(() => new CellLayer(Grid.Type, size)); MapTiles = Exts.Lazy(() => { var ret = new CellLayer(Grid.Type, size); ret.Clear(tileRef); if (Grid.MaximumTerrainHeight > 0) ret.CellEntryChanged += UpdateProjection; return ret; }); MapHeight = Exts.Lazy(() => { var ret = new CellLayer(Grid.Type, size); ret.Clear(0); if (Grid.MaximumTerrainHeight > 0) ret.CellEntryChanged += UpdateProjection; return ret; }); SpawnPoints = Exts.Lazy(() => new CPos[0]); PostInit(); } public Map(ModData modData, IReadOnlyPackage package) { this.modData = modData; Package = package; AssertExists("map.yaml"); AssertExists("map.bin"); var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name)); FieldLoader.Load(this, yaml); if (MapFormat != SupportedMapFormat) throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(MapFormat, package.Name)); SpawnPoints = Exts.Lazy(() => { var spawns = new List(); foreach (var kv in ActorDefinitions.Where(d => d.Value.Value == "mpspawn")) { var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary()); spawns.Add(s.InitDict.Get().Value(null)); } return spawns.ToArray(); }); RuleDefinitions = MiniYaml.NodesOrEmpty(yaml, "Rules"); SequenceDefinitions = MiniYaml.NodesOrEmpty(yaml, "Sequences"); VoxelSequenceDefinitions = MiniYaml.NodesOrEmpty(yaml, "VoxelSequences"); WeaponDefinitions = MiniYaml.NodesOrEmpty(yaml, "Weapons"); VoiceDefinitions = MiniYaml.NodesOrEmpty(yaml, "Voices"); MusicDefinitions = MiniYaml.NodesOrEmpty(yaml, "Music"); NotificationDefinitions = MiniYaml.NodesOrEmpty(yaml, "Notifications"); TranslationDefinitions = MiniYaml.NodesOrEmpty(yaml, "Translations"); PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players"); ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors"); SmudgeDefinitions = MiniYaml.NodesOrEmpty(yaml, "Smudges"); MapTiles = Exts.Lazy(LoadMapTiles); MapResources = Exts.Lazy(LoadResourceTiles); MapHeight = Exts.Lazy(LoadMapHeight); Grid = modData.Manifest.Get(); SubCellOffsets = Grid.SubCellOffsets; LastSubCell = (SubCell)(SubCellOffsets.Length - 1); DefaultSubCell = (SubCell)Grid.SubCellDefaultIndex; if (Package.Contains("map.png")) using (var dataStream = Package.GetStream("map.png")) CustomPreview = new Bitmap(dataStream); PostInit(); Uid = ComputeUID(Package); } void PostInit() { rules = Exts.Lazy(() => { try { return modData.RulesetCache.Load(this, this); } catch (Exception e) { InvalidCustomRules = true; Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e.Message); } return modData.DefaultRules; }); cachedTileSet = Exts.Lazy(() => Rules.TileSets[Tileset]); var tl = new MPos(0, 0).ToCPos(this); var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this); AllCells = new CellRegion(Grid.Type, tl, br); var btl = new PPos(Bounds.Left, Bounds.Top); var bbr = new PPos(Bounds.Right - 1, Bounds.Bottom - 1); SetBounds(btl, bbr); CustomTerrain = new CellLayer(this); foreach (var uv in AllCells.MapCoords) CustomTerrain[uv] = byte.MaxValue; } void InitializeCellProjection() { if (initializedCellProjection) return; initializedCellProjection = true; cellProjection = new CellLayer(this); inverseCellProjection = new CellLayer>(this); // Initialize collections foreach (var cell in AllCells) { var uv = cell.ToMPos(Grid.Type); cellProjection[uv] = new PPos[0]; inverseCellProjection[uv] = new List(); } // Initialize projections foreach (var cell in AllCells) UpdateProjection(cell); } void UpdateProjection(CPos cell) { MPos uv; if (Grid.MaximumTerrainHeight == 0) { uv = cell.ToMPos(Grid.Type); cellProjection[cell] = new[] { (PPos)uv }; var inverse = inverseCellProjection[uv]; inverse.Clear(); inverse.Add(uv); return; } if (!initializedCellProjection) InitializeCellProjection(); uv = cell.ToMPos(Grid.Type); // Remove old reverse projection foreach (var puv in cellProjection[uv]) inverseCellProjection[(MPos)puv].Remove(uv); var projected = ProjectCellInner(uv); cellProjection[uv] = projected; foreach (var puv in projected) inverseCellProjection[(MPos)puv].Add(uv); } PPos[] ProjectCellInner(MPos uv) { var mapHeight = MapHeight.Value; if (!mapHeight.Contains(uv)) return NoProjectedCells; var height = mapHeight[uv]; if (height == 0) return new[] { (PPos)uv }; // Odd-height ramps get bumped up a level to the next even height layer if ((height & 1) == 1) { var ti = cachedTileSet.Value.GetTileInfo(MapTiles.Value[uv]); if (ti != null && ti.RampType != 0) height += 1; } var candidates = new List(); // Odd-height level tiles are equally covered by four projected tiles if ((height & 1) == 1) { if ((uv.V & 1) == 1) candidates.Add(new PPos(uv.U + 1, uv.V - height)); else candidates.Add(new PPos(uv.U - 1, uv.V - height)); candidates.Add(new PPos(uv.U, uv.V - height)); candidates.Add(new PPos(uv.U, uv.V - height + 1)); candidates.Add(new PPos(uv.U, uv.V - height - 1)); } else candidates.Add(new PPos(uv.U, uv.V - height)); return candidates.Where(c => mapHeight.Contains((MPos)c)).ToArray(); } public Ruleset PreloadRules() { return rules.Value; } public void Save(IReadWritePackage toPackage) { MapFormat = SupportedMapFormat; var root = new List(); var fields = new[] { "MapFormat", "RequiresMod", "Title", "Description", "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))); } root.Add(new MiniYamlNode("Videos", FieldSaver.SaveDifferences(Videos, new MapVideos()))); root.Add(new MiniYamlNode("Options", FieldSaver.SaveDifferences(Options, new MapOptions()))); root.Add(new MiniYamlNode("Players", null, PlayerDefinitions)); root.Add(new MiniYamlNode("Actors", null, ActorDefinitions)); root.Add(new MiniYamlNode("Smudges", null, SmudgeDefinitions)); root.Add(new MiniYamlNode("Rules", null, RuleDefinitions)); root.Add(new MiniYamlNode("Sequences", null, SequenceDefinitions)); root.Add(new MiniYamlNode("VoxelSequences", null, VoxelSequenceDefinitions)); root.Add(new MiniYamlNode("Weapons", null, WeaponDefinitions)); root.Add(new MiniYamlNode("Voices", null, VoiceDefinitions)); root.Add(new MiniYamlNode("Music", null, MusicDefinitions)); root.Add(new MiniYamlNode("Notifications", null, NotificationDefinitions)); root.Add(new MiniYamlNode("Translations", null, TranslationDefinitions)); // Saving to a new package: copy over all the content from the map if (Package != null && toPackage != Package) foreach (var file in Package.Contents) toPackage.Update(file, Package.GetStream(file).ReadAllBytes()); // Update the package with the new map data var s = root.WriteToString(); toPackage.Update("map.yaml", Encoding.UTF8.GetBytes(s)); toPackage.Update("map.bin", SaveBinaryData()); Package = toPackage; // Update UID to match the newly saved data Uid = ComputeUID(toPackage); } public CellLayer LoadMapTiles() { var tiles = new CellLayer(this); 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 (Grid.MaximumTerrainHeight > 0) tiles.CellEntryChanged += UpdateProjection; return tiles; } public CellLayer LoadMapHeight() { var tiles = new CellLayer(this); using (var s = Package.GetStream("map.bin")) { var header = new BinaryDataHeader(s, MapSize); if (header.HeightsOffset > 0) { s.Position = header.HeightsOffset; for (var i = 0; i < MapSize.X; i++) for (var j = 0; j < MapSize.Y; j++) tiles[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight); } } if (Grid.MaximumTerrainHeight > 0) tiles.CellEntryChanged += UpdateProjection; return tiles; } public CellLayer LoadResourceTiles() { var resources = new CellLayer(this); using (var s = Package.GetStream("map.bin")) { var header = new BinaryDataHeader(s, MapSize); 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); } } } } return resources; } public byte[] SaveBinaryData() { var dataStream = new MemoryStream(); using (var writer = new BinaryWriter(dataStream)) { // Binary data version writer.Write(TileFormat); // Size writer.Write((ushort)MapSize.X); writer.Write((ushort)MapSize.Y); // Data offsets var tilesOffset = 17; var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0; var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17; writer.Write((uint)tilesOffset); writer.Write((uint)heightsOffset); writer.Write((uint)resourcesOffset); // Tile data if (tilesOffset != 0) { for (var i = 0; i < MapSize.X; i++) { for (var j = 0; j < MapSize.Y; j++) { var tile = MapTiles.Value[new MPos(i, j)]; writer.Write(tile.Type); writer.Write(tile.Index); } } } // Height data if (heightsOffset != 0) for (var i = 0; i < MapSize.X; i++) for (var j = 0; j < MapSize.Y; j++) writer.Write(MapHeight.Value[new MPos(i, j)]); // Resource data if (resourcesOffset != 0) { for (var i = 0; i < MapSize.X; i++) { for (var j = 0; j < MapSize.Y; j++) { var tile = MapResources.Value[new MPos(i, j)]; writer.Write(tile.Type); writer.Write(tile.Index); } } } } return dataStream.ToArray(); } public bool Contains(CPos cell) { // .ToMPos() returns the same result if the X and Y coordinates // are switched. X < Y is invalid in the RectangularIsometric coordinate system, // so we pre-filter these to avoid returning the wrong result if (Grid.Type == MapGridType.RectangularIsometric && cell.X < cell.Y) return false; return Contains(cell.ToMPos(this)); } public bool Contains(MPos uv) { // The first check ensures that the cell is within the valid map region, avoiding // potential crashes in deeper code. All CellLayers have the same geometry, and // CustomTerrain is convenient (cellProjection may be null and others are Lazy). return CustomTerrain.Contains(uv) && ContainsAllProjectedCellsCovering(uv); } bool ContainsAllProjectedCellsCovering(MPos uv) { if (Grid.MaximumTerrainHeight == 0) return Contains((PPos)uv); // If the cell has no valid projection, then we're off the map. var projectedCells = ProjectedCellsCovering(uv); if (projectedCells.Length == 0) return false; foreach (var puv in projectedCells) if (!Contains(puv)) return false; return true; } public bool Contains(PPos puv) { return Bounds.Contains(puv.U, puv.V); } public WPos CenterOfCell(CPos cell) { if (Grid.Type == MapGridType.Rectangular) return new WPos(1024 * cell.X + 512, 1024 * cell.Y + 512, 0); // Convert from isometric cell position (x, y) to world position (u, v): // (a) Consider the relationships: // - Center of origin cell is (512, 512) // - +x adds (512, 512) to world pos // - +y adds (-512, 512) to world pos // (b) Therefore: // - ax + by adds (a - b) * 512 + 512 to u // - ax + by adds (a + b) * 512 + 512 to v var z = MapHeight.Value.Contains(cell) ? 512 * MapHeight.Value[cell] : 0; return new WPos(512 * (cell.X - cell.Y + 1), 512 * (cell.X + cell.Y + 1), z); } public WPos CenterOfSubCell(CPos cell, SubCell subCell) { var index = (int)subCell; if (index >= 0 && index <= SubCellOffsets.Length) return CenterOfCell(cell) + SubCellOffsets[index]; return CenterOfCell(cell); } public WDist DistanceAboveTerrain(WPos pos) { var cell = CellContaining(pos); var delta = pos - CenterOfCell(cell); return new WDist(delta.Z); } public CPos CellContaining(WPos pos) { if (Grid.Type == MapGridType.Rectangular) return new CPos(pos.X / 1024, pos.Y / 1024); // Convert from world position to isometric cell position: // (a) Subtract (512, 512) to move the rotation center to the middle of the corner cell // (b) Rotate axes by -pi/4 // (c) Divide through by sqrt(2) to bring us to an equivalent world pos aligned with u,v axes // (d) Apply an offset so that the integer division by 1024 rounds in the right direction: // (i) u is always positive, so add 512 (which then partially cancels the -1024 term from the rotation) // (ii) v can be negative, so we need to be careful about rounding directions. We add 512 *away from 0* (negative if y > x). // (e) Divide by 1024 to bring into cell coords. var u = (pos.Y + pos.X - 512) / 1024; var v = (pos.Y - pos.X + (pos.Y > pos.X ? 512 : -512)) / 1024; return new CPos(u, v); } public PPos ProjectedCellCovering(WPos pos) { var projectedPos = pos - new WVec(0, pos.Z, pos.Z); return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type); } static readonly PPos[] NoProjectedCells = { }; public PPos[] ProjectedCellsCovering(MPos uv) { if (!initializedCellProjection) InitializeCellProjection(); if (!cellProjection.Contains(uv)) return NoProjectedCells; return cellProjection[uv]; } public List Unproject(PPos puv) { var uv = (MPos)puv; if (!initializedCellProjection) InitializeCellProjection(); if (!inverseCellProjection.Contains(uv)) return new List(); return inverseCellProjection[uv]; } public int FacingBetween(CPos cell, CPos towards, int fallbackfacing) { var delta = CenterOfCell(towards) - CenterOfCell(cell); if (delta.HorizontalLengthSquared == 0) return fallbackfacing; return delta.Yaw.Facing; } public void Resize(int width, int height) { var oldMapTiles = MapTiles.Value; var oldMapResources = MapResources.Value; var oldMapHeight = MapHeight.Value; var newSize = new Size(width, height); MapTiles = Exts.Lazy(() => CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero])); MapResources = Exts.Lazy(() => CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero])); MapHeight = Exts.Lazy(() => CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero])); MapSize = new int2(newSize); var tl = new MPos(0, 0); var br = new MPos(MapSize.X - 1, MapSize.Y - 1); AllCells = new CellRegion(Grid.Type, tl.ToCPos(this), br.ToCPos(this)); SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1)); } public void SetBounds(PPos tl, PPos br) { // The tl and br coordinates are inclusive, but the Rectangle // is exclusive. Pad the right and bottom edges to match. Bounds = Rectangle.FromLTRB(tl.U, tl.V, br.U + 1, br.V + 1); // Directly calculate the projected map corners in world units avoiding unnecessary // conversions. This abuses the definition that the width of the cell is always // 1024 units, and that the height of two rows is 2048 for classic cells and 1024 // for isometric cells. var wtop = tl.V * 1024; var wbottom = (br.V + 1) * 1024; if (Grid.Type == MapGridType.RectangularIsometric) { wtop /= 2; wbottom /= 2; } ProjectedTopLeft = new WPos(tl.U * 1024, wtop, 0); ProjectedBottomRight = new WPos(br.U * 1024 - 1, wbottom - 1, 0); ProjectedCellBounds = new ProjectedCellRegion(this, tl, br); } public void FixOpenAreas() { var r = new Random(); var tileset = Rules.TileSets[Tileset]; for (var j = Bounds.Top; j < Bounds.Bottom; j++) { for (var i = Bounds.Left; i < Bounds.Right; i++) { var type = MapTiles.Value[new MPos(i, j)].Type; var index = MapTiles.Value[new MPos(i, j)].Index; if (!tileset.Templates.ContainsKey(type)) { Console.WriteLine("Unknown Tile ID {0}".F(type)); continue; } var template = tileset.Templates[type]; if (!template.PickAny) continue; index = (byte)r.Next(0, template.TilesCount); MapTiles.Value[new MPos(i, j)] = new TerrainTile(type, index); } } } public byte GetTerrainIndex(CPos cell) { const short InvalidCachedTerrainIndex = -1; // Lazily initialize a cache for terrain indexes. if (cachedTerrainIndexes == null) { cachedTerrainIndexes = new CellLayer(this); cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex); // Invalidate the entry for a cell if anything could cause the terrain index to change. Action invalidateTerrainIndex = c => cachedTerrainIndexes[c] = InvalidCachedTerrainIndex; CustomTerrain.CellEntryChanged += invalidateTerrainIndex; MapTiles.Value.CellEntryChanged += invalidateTerrainIndex; } var uv = cell.ToMPos(this); var terrainIndex = cachedTerrainIndexes[uv]; // PERF: Cache terrain indexes per cell on demand. if (terrainIndex == InvalidCachedTerrainIndex) { var custom = CustomTerrain[uv]; terrainIndex = cachedTerrainIndexes[uv] = custom != byte.MaxValue ? custom : cachedTileSet.Value.GetTerrainIndex(MapTiles.Value[uv]); } return (byte)terrainIndex; } public TerrainTypeInfo GetTerrainInfo(CPos cell) { return cachedTileSet.Value[GetTerrainIndex(cell)]; } public CPos Clamp(CPos cell) { return Clamp(cell.ToMPos(this)).ToCPos(this); } public MPos Clamp(MPos uv) { if (Grid.MaximumTerrainHeight == 0) return (MPos)Clamp((PPos)uv); // Already in bounds, so don't need to do anything. if (ContainsAllProjectedCellsCovering(uv)) return uv; // Clamping map coordinates is trickier than it might first look! // This needs to handle three nasty cases: // * The requested cell is well outside the map region // * The requested cell is near the top edge inside the map but outside the projected layer // * The clamped projected cell lands on a cliff face with no associated map cell // // Handling these cases properly requires abuse of our knowledge of the projection transform. // // The U coordinate doesn't change significantly in the projection, so clamp this // straight away and ensure the point is somewhere inside the map uv = cellProjection.Clamp(new MPos(uv.U.Clamp(Bounds.Left, Bounds.Right), uv.V)); // Project this guessed cell and take the first available cell // If it is projected outside the layer, then make another guess. var allProjected = ProjectedCellsCovering(uv); var projected = allProjected.Any() ? allProjected.First() : new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom)); // Clamp the projected cell to the map area projected = Clamp(projected); // Project the cell back into map coordinates. // This may fail if the projected cell covered a cliff or another feature // where there is a large change in terrain height. var unProjected = Unproject(projected); if (!unProjected.Any()) { // Adjust V until we find a cell that works for (var x = 2; x <= 2 * Grid.MaximumTerrainHeight; x++) { var dv = ((x & 1) == 1 ? 1 : -1) * x / 2; var test = new PPos(projected.U, projected.V + dv); if (!Contains(test)) continue; unProjected = Unproject(test); if (unProjected.Any()) break; } // This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode. if (!unProjected.Any()) { Log.Write("debug", "Failed to clamp map cell {0} to map bounds", uv); return uv; } } return projected.V == Bounds.Bottom ? unProjected.MaxBy(x => x.V) : unProjected.MinBy(x => x.V); } public PPos Clamp(PPos puv) { var bounds = new Rectangle(Bounds.X, Bounds.Y, Bounds.Width - 1, Bounds.Height - 1); return puv.Clamp(bounds); } public CPos ChooseRandomCell(MersenneTwister rand) { List cells; do { var u = rand.Next(Bounds.Left, Bounds.Right); var v = rand.Next(Bounds.Top, Bounds.Bottom); cells = Unproject(new PPos(u, v)); } while (!cells.Any()); return cells.Random(rand).ToCPos(Grid.Type); } public CPos ChooseClosestEdgeCell(CPos cell) { return ChooseClosestEdgeCell(cell.ToMPos(Grid.Type)).ToCPos(Grid.Type); } public MPos ChooseClosestEdgeCell(MPos uv) { var allProjected = ProjectedCellsCovering(uv); PPos edge; if (allProjected.Any()) { var puv = allProjected.First(); var horizontalBound = ((puv.U - Bounds.Left) < Bounds.Width / 2) ? Bounds.Left : Bounds.Right; var verticalBound = ((puv.V - Bounds.Top) < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom; var du = Math.Abs(horizontalBound - puv.U); var dv = Math.Abs(verticalBound - puv.V); edge = du < dv ? new PPos(horizontalBound, puv.V) : new PPos(puv.U, verticalBound); } else edge = new PPos(Bounds.Left, Bounds.Top); var unProjected = Unproject(edge); if (!unProjected.Any()) { // Adjust V until we find a cell that works for (var x = 2; x <= 2 * Grid.MaximumTerrainHeight; x++) { var dv = ((x & 1) == 1 ? 1 : -1) * x / 2; var test = new PPos(edge.U, edge.V + dv); if (!Contains(test)) continue; unProjected = Unproject(test); if (unProjected.Any()) break; } // This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode. if (!unProjected.Any()) { Log.Write("debug", "Failed to find closest edge for map cell {0}", uv); return uv; } } return edge.V == Bounds.Bottom ? unProjected.MaxBy(x => x.V) : unProjected.MinBy(x => x.V); } public CPos ChooseRandomEdgeCell(MersenneTwister rand) { List cells; do { var isU = rand.Next(2) == 0; var edge = rand.Next(2) == 0; var u = isU ? rand.Next(Bounds.Left, Bounds.Right) : (edge ? Bounds.Left : Bounds.Right); var v = !isU ? rand.Next(Bounds.Top, Bounds.Bottom) : (edge ? Bounds.Top : Bounds.Bottom); cells = Unproject(new PPos(u, v)); } while (!cells.Any()); return cells.Random(rand).ToCPos(Grid.Type); } public WDist DistanceToEdge(WPos pos, WVec dir) { var projectedPos = pos - new WVec(0, pos.Z, pos.Z); var x = dir.X == 0 ? int.MaxValue : ((dir.X < 0 ? ProjectedTopLeft.X : ProjectedBottomRight.X) - projectedPos.X) / dir.X; var y = dir.Y == 0 ? int.MaxValue : ((dir.Y < 0 ? ProjectedTopLeft.Y : ProjectedBottomRight.Y) - projectedPos.Y) / dir.Y; return new WDist(Math.Min(x, y) * dir.Length); } static readonly CVec[][] TilesByDistance = InitTilesByDistance(MaxTilesInCircleRange); static CVec[][] InitTilesByDistance(int max) { var ts = new List[max + 1]; for (var i = 0; i < max + 1; i++) ts[i] = new List(); for (var j = -max; j <= max; j++) for (var i = -max; i <= max; i++) if (max * max >= i * i + j * j) ts[Exts.ISqrt(i * i + j * j, Exts.ISqrtRoundMode.Ceiling)].Add(new CVec(i, j)); // Sort each integer-distance group by the actual distance foreach (var list in ts) { list.Sort((a, b) => { var result = a.LengthSquared.CompareTo(b.LengthSquared); if (result != 0) return result; // If the lengths are equal, use other means to sort them. // Try the hash code first because it gives more // random-appearing results than X or Y that would always // prefer the leftmost/topmost position. result = a.GetHashCode().CompareTo(b.GetHashCode()); if (result != 0) return result; result = a.X.CompareTo(b.X); if (result != 0) return result; return a.Y.CompareTo(b.Y); }); } return ts.Select(list => list.ToArray()).ToArray(); } // Both ranges are inclusive because everything that calls it is designed for maxRange being inclusive: // it rounds the actual distance up to the next integer so that this call // will return any cells that intersect with the requested range circle. // The returned positions are sorted by distance from the center. public IEnumerable FindTilesInAnnulus(CPos center, int minRange, int maxRange, bool allowOutsideBounds = false) { if (maxRange < minRange) throw new ArgumentOutOfRangeException("maxRange", "Maximum range is less than the minimum range."); if (maxRange >= TilesByDistance.Length) throw new ArgumentOutOfRangeException("maxRange", "The requested range ({0}) exceeds the maximum allowed ({1})".F(maxRange, MaxTilesInCircleRange)); Func valid = Contains; if (allowOutsideBounds) valid = MapTiles.Value.Contains; for (var i = minRange; i <= maxRange; i++) { foreach (var offset in TilesByDistance[i]) { var t = offset + center; if (valid(t)) yield return t; } } } public IEnumerable FindTilesInCircle(CPos center, int maxRange, bool allowOutsideBounds = false) { return FindTilesInAnnulus(center, 0, maxRange, allowOutsideBounds); } // Placeholders for future implementation public Stream Open(string filename) { // Explicit package paths never refer to a map if (!filename.Contains("|") && Package.Contains(filename)) return Package.GetStream(filename); return modData.DefaultFileSystem.Open(filename); } public bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename) { // Packages aren't supported inside maps return modData.DefaultFileSystem.TryGetPackageContaining(path, out package, out filename); } public bool TryOpen(string filename, out Stream s) { // Explicit package paths never refer to a map if (!filename.Contains("|")) { s = Package.GetStream(filename); if (s != null) return true; } return modData.DefaultFileSystem.TryOpen(filename, out s); } public bool Exists(string filename) { // Explicit package paths never refer to a map if (!filename.Contains("|") && Package.Contains(filename)) return true; return modData.DefaultFileSystem.Exists(filename); } } }