// Copies climate data from source to destination if destination is ocean
        // Used to dilate climate data around shorelines as a pre-process
        private static void TransferLandToOcean(ContentReader contentReader, ref byte[] dstClimateArray, int srcX, int srcY, int dstX, int dstY)
        {
            const int oceanClimate = 223;

            // Source must be land
            int srcOffset = srcY * PakFile.pakWidthValue + (srcX + 1);
            int srcClimate = contentReader.MapFileReader.ClimateFile.Buffer[srcOffset];
            if (srcClimate == oceanClimate)
                return;

            // Destination must be ocean
            int dstOffset = dstY * PakFile.pakWidthValue + (dstX + 1);
            int dstClimate = contentReader.MapFileReader.ClimateFile.Buffer[dstOffset];
            if (dstClimate == oceanClimate)
            {
                dstClimateArray[dstOffset] = (byte)srcClimate;
            }
        }
        /// <summary>
        /// initializes resources (mapDistanceSquaredFromWater, mapDistanceSquaredFromLocations, mapMultipliers) and smoothes small height map
        /// </summary>
        public static void InitImprovedWorldTerrain(ContentReader contentReader)
        {
            if (!init)
            {
                #if CREATE_PERSISTENT_LOCATION_RANGE_MAPS
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;

                    mapLocationRangeX = new byte[width * height];
                    mapLocationRangeY = new byte[width * height];

                    //int y = 204;
                    //int x = 718;
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            //MapPixelData MapData = TerrainHelper.GetMapPixelData(contentReader, x, y);
                            //if (MapData.hasLocation)
                            //{
                            //    int locationRangeX = (int)MapData.locationRect.xMax - (int)MapData.locationRect.xMin;
                            //    int locationRangeY = (int)MapData.locationRect.yMax - (int)MapData.locationRect.yMin;
                            //}

                            ContentReader.MapSummary mapSummary;
                            int regionIndex = -1, mapIndex = -1;
                            bool hasLocation = contentReader.HasLocation(x, y, out mapSummary);
                            if (hasLocation)
                            {   
                                regionIndex = mapSummary.RegionIndex;
                                mapIndex = mapSummary.MapIndex;
                                DFLocation location = contentReader.MapFileReader.GetLocation(regionIndex, mapIndex);
                                byte locationRangeX = location.Exterior.ExteriorData.Width;
                                byte locationRangeY = location.Exterior.ExteriorData.Height;

                                mapLocationRangeX[y * width + x] = locationRangeX;
                                mapLocationRangeY[y * width + x] = locationRangeY;
                            }
                        }
                    }
              
                    // save to files
                    FileStream ostream;
                    ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathMapLocationRangeX), FileMode.Create, FileAccess.Write);
                    BinaryWriter writerMapLocationRangeX = new BinaryWriter(ostream, Encoding.UTF8);
                    writerMapLocationRangeX.Write(mapLocationRangeX, 0, width * height);
                    writerMapLocationRangeX.Close();
                    ostream.Close();

                    ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathMapLocationRangeY), FileMode.Create, FileAccess.Write);
                    BinaryWriter writerMapLocationRangeY = new BinaryWriter(ostream, Encoding.UTF8);
                    writerMapLocationRangeY.Write(mapLocationRangeY, 0, width * height);
                    writerMapLocationRangeY.Close();
                    ostream.Close();
                }
                #else
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;

                    mapLocationRangeX = new byte[width * height];
                    mapLocationRangeY = new byte[width * height];

                    MemoryStream istream;
                    TextAsset assetMapLocationRangeX = Resources.Load<TextAsset>(filenameMapLocationRangeX);
                    if (assetMapLocationRangeX != null)
                    {
                        istream = new MemoryStream(assetMapLocationRangeX.bytes);
                        BinaryReader readerMapLocationRangeX = new BinaryReader(istream, Encoding.UTF8);
                        readerMapLocationRangeX.Read(mapLocationRangeX, 0, width * height);
                        readerMapLocationRangeX.Close();
                        istream.Close();
                    }

                    TextAsset assetMapLocationRangeY = Resources.Load<TextAsset>(filenameMapLocationRangeY);
                    if (assetMapLocationRangeY)
                    {
                        istream = new MemoryStream(assetMapLocationRangeY.bytes);
                        BinaryReader readerMapLocationRangeY = new BinaryReader(istream, Encoding.UTF8);
                        readerMapLocationRangeY.Read(mapLocationRangeY, 0, width * height);
                        readerMapLocationRangeY.Close();
                        istream.Close();
                    }

                    //FileStream istream;
                    //istream = new FileStream(filepathMapLocationRangeX, FileMode.Open, FileAccess.Read);
                    //BinaryReader readerMapLocationRangeX = new BinaryReader(istream, Encoding.UTF8);
                    //readerMapLocationRangeX.Read(mapLocationRangeX, 0, width * height);
                    //readerMapLocationRangeX.Close();
                    //istream.Close();

                    //istream = new FileStream(filepathMapLocationRangeY, FileMode.Open, FileAccess.Read);
                    //BinaryReader readerMapLocationRangeY = new BinaryReader(istream, Encoding.UTF8);
                    //readerMapLocationRangeY.Read(mapLocationRangeY, 0, width * height);
                    //readerMapLocationRangeY.Close();
                    //istream.Close();
                }
                #endif

                if (mapDistanceSquaredFromWater == null)
                {
                    byte[] heightMapArray = contentReader.WoodsFileReader.Buffer.Clone() as byte[];
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            if (heightMapArray[y * width + x] <= 2)
                                heightMapArray[y * width + x] = 1;
                            else
                                heightMapArray[y * width + x] = 0;
                        }
                    }
                    //now set image borders to "water" (this is a workaround to prevent mountains to become too high in north-east and south-east edge of map)
                    for (int y = 0; y < height; y++)
                    {
                        heightMapArray[y * width + 0] = 1;
                        heightMapArray[y * width + width - 1] = 1;
                    }
                    for (int x = 0; x < width; x++)
                    {
                        heightMapArray[0 * width + x] = 1;
                        heightMapArray[(height - 1) * width + x] = 1;
                    }

                    mapDistanceSquaredFromWater = imageDistanceTransform(heightMapArray, width, height, 1);

                    heightMapArray = null;
                }

                if (mapDistanceSquaredFromLocations == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapLocations = new byte[width * height];

                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            ContentReader.MapSummary summary;
                            if (contentReader.HasLocation(x + 1, height - 1 - y, out summary))
                                mapLocations[y * width + x] = 1;
                            else
                                mapLocations[y * width + x] = 0;
                        }
                    }
                    mapDistanceSquaredFromLocations = imageDistanceTransform(mapLocations, width, height, 1);
                }



                if (mapMultipliers == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapMultipliers = new float[width * height];

                    // compute the multiplier and store it in mapMultipliers
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            float distanceFromWater = (float)Math.Sqrt(mapDistanceSquaredFromWater[y * WoodsFile.mapWidthValue + x]);
                            float distanceFromLocation = (float)Math.Sqrt(mapDistanceSquaredFromLocations[y * WoodsFile.mapWidthValue + x]);
                            float multiplierLocation = (distanceFromLocation * extraExaggerationFactorLocationDistance + 1.0f); // terrain distant from location gets extra exaggeration
                            if (distanceFromWater < minDistanceFromWaterForExtraExaggeration) // except if it is near water
                                multiplierLocation = 1.0f;
                            mapMultipliers[y * width + x] = (Math.Min(maxHeightsExaggerationMultiplier, multiplierLocation * Math.Max(1.0f, distanceFromWater * exaggerationFactorWaterDistance)));
                        }
                    }

                    // multipliedMap gets smoothed
                    float[] newmapMultipliers = mapMultipliers.Clone() as float[];
                    float[,] weights = { { 0.0625f, 0.125f, 0.0625f }, { 0.125f, 0.25f, 0.125f }, { 0.0625f, 0.125f, 0.0625f } };
                    for (int y = 1; y < height - 1; y++)
                    {
                        for (int x = 1; x < width - 1; x++)
                        {
                            if (mapDistanceSquaredFromLocations[y * width + x] <= 2) // at and around locations ( <= 2 ... only map pixels in 8-connected neighborhood (distanceFromLocationMaps stores squared distances...))
                            {
                                newmapMultipliers[y * width + x] =
                                    weights[0, 0] * mapMultipliers[(y - 1) * width + (x - 1)] + weights[0, 1] * mapMultipliers[(y - 1) * width + (x)] + weights[0, 2] * mapMultipliers[(y - 1) * width + (x + 1)] +
                                    weights[1, 0] * mapMultipliers[(y - 0) * width + (x - 1)] + weights[1, 1] * mapMultipliers[(y - 0) * width + (x)] + weights[1, 2] * mapMultipliers[(y - 0) * width + (x + 1)] +
                                    weights[2, 0] * mapMultipliers[(y + 1) * width + (x - 1)] + weights[2, 1] * mapMultipliers[(y + 1) * width + (x)] + weights[2, 2] * mapMultipliers[(y + 1) * width + (x + 1)];
                            }
                        }
                    }
                    mapMultipliers = newmapMultipliers;

                    newmapMultipliers = null;
                    weights = null;
                }

                //the height map gets smoothed as well
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    byte[] heightMapBuffer = contentReader.WoodsFileReader.Buffer.Clone() as byte[];
                    int[,] intWeights = { { 1, 2, 1 }, { 2, 4, 2 }, { 1, 2, 1 } };
                    for (int y = 1; y < height - 1; y++)
                    {
                        for (int x = 1; x < width - 1; x++)
                        {
                            if (mapDistanceSquaredFromWater[y * width + x] > 0) // check if squared distance from water is greater than zero -> if it is no water pixel
                            {
                                int value =
                                    intWeights[0, 0] * (int)heightMapBuffer[(y - 1) * width + (x - 1)] + intWeights[0, 1] * (int)heightMapBuffer[(y - 1) * width + (x)] + intWeights[0, 2] * (int)heightMapBuffer[(y - 1) * width + (x + 1)] +
                                    intWeights[1, 0] * (int)heightMapBuffer[(y - 0) * width + (x - 1)] + intWeights[1, 1] * (int)heightMapBuffer[(y - 0) * width + (x)] + intWeights[1, 2] * (int)heightMapBuffer[(y - 0) * width + (x + 1)] +
                                    intWeights[2, 0] * (int)heightMapBuffer[(y + 1) * width + (x - 1)] + intWeights[2, 1] * (int)heightMapBuffer[(y + 1) * width + (x)] + intWeights[2, 2] * (int)heightMapBuffer[(y + 1) * width + (x + 1)];

                                heightMapBuffer[y * width + x] = (byte)(value / 16);
                            }
                        }
                    }
                    contentReader.WoodsFileReader.Buffer = heightMapBuffer;

                    heightMapBuffer = null;
                    intWeights = null;
                }

                // build tree coverage map
                if (mapTreeCoverage == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapTreeCoverage = new byte[width * height];

                    #if !LOAD_TREE_COVERAGE_MAP
                    {
                        float startTreeCoverageAtElevation = ImprovedTerrainSampler.baseHeightScale * 2.0f; // ImprovedTerrainSampler.scaledBeachElevation;
                        float minTreeCoverageSaturated = ImprovedTerrainSampler.baseHeightScale * 6.0f;
                        float maxTreeCoverageSaturated = ImprovedTerrainSampler.baseHeightScale * 60.0f;
                        float endTreeCoverageAtElevation = ImprovedTerrainSampler.baseHeightScale * 80.0f;
                        //float maxElevation = 0.0f;
                        for (int y = 0; y < height; y++)
                        {
                            for (int x = 0; x < width; x++)
                            {
                                int readIndex = (height - 1 - y) * width + x;
                                float w = 0.0f;

                                //float elevation = ((float)contentReader.WoodsFileReader.Buffer[(height - 1 - y) * width + x]) / 255.0f; // *mapMultipliers[index];
                                float elevation = ((float)contentReader.WoodsFileReader.Buffer[readIndex]) * mapMultipliers[readIndex];

                                //maxElevation = Math.Max(maxElevation, elevation);
                                if ((elevation > minTreeCoverageSaturated) && (elevation < maxTreeCoverageSaturated))
                                {
                                    w = 1.0f;
                                }
                                else if ((elevation >= startTreeCoverageAtElevation) && (elevation <= minTreeCoverageSaturated))
                                {
                                    w = (elevation - startTreeCoverageAtElevation) / (minTreeCoverageSaturated - startTreeCoverageAtElevation);
                                }
                                else if ((elevation >= maxTreeCoverageSaturated) && (elevation <= endTreeCoverageAtElevation))
                                {
                                    w = 1.0f - ((elevation - maxTreeCoverageSaturated) / (endTreeCoverageAtElevation - maxTreeCoverageSaturated));
                                }

                                //w = 0.65f * w + 0.35f * Math.Min(6.0f, (float)Math.Sqrt(mapDistanceSquaredFromLocations[y * width + x])) / 6.0f;

                                mapTreeCoverage[(y) * width + x] = Convert.ToByte(w * 255.0f);

                                //if (elevation>0.05f)
                                //    mapTreeCoverage[index] = Convert.ToByte(250); //w * 255.0f);
                                //else mapTreeCoverage[index] = Convert.ToByte(0);

                                //if (elevation >= startTreeCoverageAtElevation)
                                //{
                                //    mapTreeCoverage[(y) * width + x] = Convert.ToByte(255.0f);
                                //} else{
                                //    mapTreeCoverage[(y) * width + x] = Convert.ToByte(0.0f);
                                //}
                            }
                        }
                    }
                    #else
                    {
                        MemoryStream istream;
                        TextAsset assetMapTreeCoverage = Resources.Load<TextAsset>(filenameTreeCoverageMap);
                        if (assetMapTreeCoverage)
                        {
                            istream = new MemoryStream(assetMapTreeCoverage.bytes);
                            BinaryReader readerMapTreeCoverage = new BinaryReader(istream, Encoding.UTF8);
                            readerMapTreeCoverage.Read(mapTreeCoverage, 0, width * height);
                            readerMapTreeCoverage.Close();
                            istream.Close();
                        }
                    }
                    #endif

                    #if CREATE_PERSISTENT_TREE_COVERAGE_MAP
                    {
                        FileStream ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathOutTreeCoverageMap), FileMode.Create, FileAccess.Write);
                        BinaryWriter writerMapTreeCoverage = new BinaryWriter(ostream, Encoding.UTF8);
                        writerMapTreeCoverage.Write(mapTreeCoverage, 0, width * height);
                        writerMapTreeCoverage.Close();
                        ostream.Close();
                    }
                    #endif
                    //Debug.Log(string.Format("max elevation: {0}", maxElevation));
                }
                
                init = true;
            }
        }
        /// <summary>
        /// If a location map pixel is on a gradient greater than threshold, then
        /// smooth surrounding Moore neighbourhood with location height
        /// </summary>
        public static void SmoothLocationNeighbourhood(ContentReader contentReader, int threshold = 20)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            // Get in-memory height array
            byte[] heightArray = contentReader.WoodsFileReader.Buffer;

            // Search for locations
            for (int y = 1; y < WoodsFile.mapHeightValue - 1; y++)
            {
                for (int x = 1; x < WoodsFile.mapWidthValue - 1; x++)
                {
                    ContentReader.MapSummary summary;
                    if (contentReader.HasLocation(x, y, out summary))
                    {
                        // Use Sobel filter for gradient
                        float x0y0 = heightArray[y * WoodsFile.mapWidthValue + x];
                        float x1y0 = heightArray[y * WoodsFile.mapWidthValue + (x + 1)];
                        float x0y1 = heightArray[(y + 1) * WoodsFile.mapWidthValue + x];
                        float gradient = GetGradient(x0y0, x1y0, x0y1);
                        if (gradient > threshold)
                        {
                            AverageHeights(ref heightArray, x, y);
                        }
                    }
                }
            }

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to smooth location neighbourhoods: {0}ms", totalTime), true);
        }
        // Get noise sample at coordinates
        private static float GetNoise(
            ContentReader reader,
            float x,
            float y,
            float frequency,
            float amplitude,
            float persistance,
            int octaves)
        {
            float finalValue = 0f;
            for (int i = 0; i < octaves; ++i)
            {
                finalValue += reader.Noise.Generate(x * frequency, y * frequency) * amplitude;
                frequency *= 2.0f;
                amplitude *= persistance;
            }

            return Mathf.Clamp(finalValue, -1, 1);
        }
        /// <summary>
        /// Gets map pixel data for any location in world.
        /// </summary>
        public static MapPixelData GetMapPixelData(ContentReader contentReader, int mapPixelX, int mapPixelY)
        {
            // Read general data from world maps
            int worldHeight = contentReader.WoodsFileReader.GetHeightMapValue(mapPixelX, mapPixelY);
            int worldClimate = contentReader.MapFileReader.GetClimateIndex(mapPixelX, mapPixelY);
            int worldPolitic = contentReader.MapFileReader.GetPoliticIndex(mapPixelX, mapPixelY);

            // Get location if present
            int id = -1, regionIndex = -1, mapIndex = -1;
            string locationName = string.Empty;
            ContentReader.MapSummary mapSummary = new ContentReader.MapSummary();
            bool hasLocation = contentReader.HasLocation(mapPixelX, mapPixelY, out mapSummary);
            if (hasLocation)
            {
                id = mapSummary.ID;
                regionIndex = mapSummary.RegionIndex;
                mapIndex = mapSummary.MapIndex;
                DFLocation location = contentReader.MapFileReader.GetLocation(regionIndex, mapIndex);
                locationName = location.Name;
            }

            // Create map pixel data
            MapPixelData mapPixel = new MapPixelData()
            {
                inWorld = true,
                mapPixelX = mapPixelX,
                mapPixelY = mapPixelY,
                worldHeight = worldHeight,
                worldClimate = worldClimate,
                worldPolitic = worldPolitic,
                hasLocation = hasLocation,
                mapRegionIndex = regionIndex,
                mapLocationIndex = mapIndex,
                locationID = id,
                locationName = locationName,
            };

            return mapPixel;
        }
        // Set texture and height data for city tiles
        public static void SetLocationTiles(ContentReader contentReader, ref MapPixelData mapPixel)
        {
            const int tileDim = 16;
            const int chunkDim = 8;

            // Get location
            DFLocation location = contentReader.MapFileReader.GetLocation(mapPixel.mapRegionIndex, mapPixel.mapLocationIndex);

            // Centre location tiles inside terrain area
            int startX = ((chunkDim * tileDim) - location.Exterior.ExteriorData.Width * tileDim) / 2;
            int startY = ((chunkDim * tileDim) - location.Exterior.ExteriorData.Height * tileDim) / 2;

            // Full 8x8 locations have "terrain blend space" around walls to smooth down random terrain towards flat area.
            // This is indicated by texture index > 55 (ground texture range is 0-55), larger values indicate blend space.
            // We need to know rect of actual city area so we can use blend space outside walls.
            int xmin = terrainSampleDim, ymin = terrainSampleDim;
            int xmax = 0, ymax = 0;

            // Iterate blocks of this location
            for (int blockY = 0; blockY < location.Exterior.ExteriorData.Height; blockY++)
            {
                for (int blockX = 0; blockX < location.Exterior.ExteriorData.Width; blockX++)
                {
                    // Get block data
                    DFBlock block;
                    string blockName = contentReader.MapFileReader.GetRmbBlockName(ref location, blockX, blockY);
                    if (!contentReader.GetBlock(blockName, out block))
                        continue;

                    // Copy ground tile info
                    for (int tileY = 0; tileY < tileDim; tileY++)
                    {
                        for (int tileX = 0; tileX < tileDim; tileX++)
                        {
                            DFBlock.RmbGroundTiles tile = block.RmbBlock.FldHeader.GroundData.GroundTiles[tileX, (tileDim - 1) - tileY];
                            int xpos = startX + blockX * tileDim + tileX;
                            int ypos = startY + blockY * tileDim + tileY;
                            int offset = (ypos * terrainSampleDim) + xpos;

                            int record = tile.TextureRecord;
                            if (tile.TextureRecord < 56)
                            {
                                // Track interior bounds of location tiled area
                                if (xpos < xmin) xmin = xpos;
                                if (xpos > xmax) xmax = xpos;
                                if (ypos < ymin) ymin = ypos;
                                if (ypos > ymax) ymax = ypos;

                                // Store texture data from block
                                mapPixel.samples[offset].record = record;
                                mapPixel.samples[offset].flip = tile.IsFlipped;
                                mapPixel.samples[offset].rotate = tile.IsRotated;
                                mapPixel.samples[offset].location = true;
                            }
                        }
                    }
                }
            }

            // Update location rect with extra clearance
            const int extraClearance = 2;
            Rect locationRect = new Rect();
            locationRect.xMin = xmin - extraClearance;
            locationRect.xMax = xmax + extraClearance;
            locationRect.yMin = ymin - extraClearance;
            locationRect.yMax = ymax + extraClearance;
            mapPixel.locationRect = locationRect;
        }
        /// <summary>
        /// Generate initial samples from any map pixel coordinates in world range.
        /// Also sets location height in mapPixelData for location positioning.
        /// </summary>
        public static void GenerateSamples(ContentReader contentReader, ref MapPixelData mapPixel)
        {
            // Raise start event
            RaiseOnGenerateSamplesStartEvent();

            // Divisor ensures continuous 0-1 range of tile samples
            float div = (float)terrainTileDim / 3f;

            // Read neighbouring height samples for this map pixel
            int mx = mapPixel.mapPixelX;
            int my = mapPixel.mapPixelY;
            byte[,] shm = contentReader.WoodsFileReader.GetHeightMapValuesRange(mx - 2, my - 2, 4);
            byte[,] lhm = contentReader.WoodsFileReader.GetLargeHeightMapValuesRange(mx - 1, my, 3);

            // Raise new samples event
            RaiseOnNewHeightSamplesEvent();

            // Extract height samples for all chunks
            float averageHeight = 0;
            float maxHeight = float.MinValue;
            float baseHeight, noiseHeight;
            float x1, x2, x3, x4;
            int dim = terrainSampleDim;
            mapPixel.samples = new WorldSample[dim * dim];
            for (int y = 0; y < dim; y++)
            {
                for (int x = 0; x < dim; x++)
                {
                    float rx = (float)x / div;
                    float ry = (float)y / div;
                    int ix = Mathf.FloorToInt(rx);
                    int iy = Mathf.FloorToInt(ry);
                    float sfracx = (float)x / (float)(dim - 1);
                    float sfracy = (float)y / (float)(dim - 1);
                    float fracx = (float)(x - ix * div) / div;
                    float fracy = (float)(y - iy * div) / div;
                    float scaledHeight = 0;

                    //// TEST: Point sample small height map for base terrain
                    //baseHeight = shm[2, 2];
                    //scaledHeight += baseHeight * baseHeightScale;

                    // Bicubic sample small height map for base terrain elevation
                    x1 = CubicInterpolator(shm[0, 3], shm[1, 3], shm[2, 3], shm[3, 3], sfracx);
                    x2 = CubicInterpolator(shm[0, 2], shm[1, 2], shm[2, 2], shm[3, 2], sfracx);
                    x3 = CubicInterpolator(shm[0, 1], shm[1, 1], shm[2, 1], shm[3, 1], sfracx);
                    x4 = CubicInterpolator(shm[0, 0], shm[1, 0], shm[2, 0], shm[3, 0], sfracx);
                    baseHeight = CubicInterpolator(x1, x2, x3, x4, sfracy);
                    scaledHeight += baseHeight * baseHeightScale;

                    // Bicubic sample large height map for noise mask over terrain features
                    x1 = CubicInterpolator(lhm[ix, iy + 0], lhm[ix + 1, iy + 0], lhm[ix + 2, iy + 0], lhm[ix + 3, iy + 0], fracx);
                    x2 = CubicInterpolator(lhm[ix, iy + 1], lhm[ix + 1, iy + 1], lhm[ix + 2, iy + 1], lhm[ix + 3, iy + 1], fracx);
                    x3 = CubicInterpolator(lhm[ix, iy + 2], lhm[ix + 1, iy + 2], lhm[ix + 2, iy + 2], lhm[ix + 3, iy + 2], fracx);
                    x4 = CubicInterpolator(lhm[ix, iy + 3], lhm[ix + 1, iy + 3], lhm[ix + 2, iy + 3], lhm[ix + 3, iy + 3], fracx);
                    noiseHeight = CubicInterpolator(x1, x2, x3, x4, fracy);
                    scaledHeight += noiseHeight * noiseMapScale;

                    // TODO: Developers must be able to override above settings via event or some other mechanism
                    // Will implement this before final 1.3 version

                    // Additional noise mask for small terrain features at ground level
                    float latitude = mapPixel.mapPixelX * MapsFile.WorldMapTileDim + x;
                    float longitude = MapsFile.MaxWorldTileCoordZ - mapPixel.mapPixelY * MapsFile.WorldMapTileDim + y;
                    float lowFreq = GetNoise(contentReader, latitude, longitude, 0.1f, 0.5f, 0.5f, 1);
                    float highFreq = GetNoise(contentReader, latitude, longitude, 6f, 0.5f, 0.5f, 1);
                    scaledHeight += (lowFreq * highFreq) * extraNoiseScale;

                    // Clamp lower values to ocean elevation
                    if (scaledHeight < scaledOceanElevation)
                        scaledHeight = scaledOceanElevation;

                    // Accumulate average height
                    averageHeight += scaledHeight;

                    // Get max height
                    if (scaledHeight > maxHeight)
                        maxHeight = scaledHeight;

                    // Set sample
                    mapPixel.samples[y * dim + x] = new WorldSample()
                    {
                        scaledHeight = scaledHeight,
                        record = 2,
                    };
                }
            }

            // Average and max heights are passed back for locations
            mapPixel.averageHeight = averageHeight /= (float)(dim * dim);
            mapPixel.maxHeight = maxHeight;

            // Raise end event
            RaiseOnGenerateSamplesEndEvent();
        }
        // Flattens location terrain and blends flat area with surrounding terrain
        // Not entirely happy with this, need to revisit later
        public static void FlattenLocationTerrain(ContentReader contentReader, ref MapPixelData mapPixel)
        {
            // Get range between bounds of sample data and interior location rect
            // The location rect is always smaller than the sample area
            float leftRange = 1f / (mapPixel.locationRect.xMin);
            float topRange = 1f / (mapPixel.locationRect.yMin);
            float rightRange = 1f / (terrainSampleDim - mapPixel.locationRect.xMax);
            float bottomRange = 1f / (terrainSampleDim - mapPixel.locationRect.yMax);

            float desiredHeight = mapPixel.averageHeight;
            float strength = 0;
            float u, v;
            for (int y = 1; y < terrainSampleDim - 1; y++)
            {
                for (int x = 1; x < terrainSampleDim - 1; x++)
                {
                    // Create a height scale from location to edge of terrain using
                    // linear interpolation on straight edges and bilinear in corners
                    if (x <= mapPixel.locationRect.xMin && y >= mapPixel.locationRect.yMin && y <= mapPixel.locationRect.yMax)
                    {
                        strength = x * leftRange;
                    }
                    else if (x >= mapPixel.locationRect.xMax && y >= mapPixel.locationRect.yMin && y <= mapPixel.locationRect.yMax)
                    {
                        strength = (terrainSampleDim - x) * rightRange;
                    }
                    else if (y <= mapPixel.locationRect.yMin && x >= mapPixel.locationRect.xMin && x <= mapPixel.locationRect.xMax)
                    {
                        strength = y * topRange;
                    }
                    else if (y >= mapPixel.locationRect.yMax && x >= mapPixel.locationRect.xMin && x <= mapPixel.locationRect.xMax)
                    {
                        strength = (terrainSampleDim - y) * bottomRange;
                    }
                    else if (x <= mapPixel.locationRect.xMin && y <= mapPixel.locationRect.yMin)
                    {
                        u = x * leftRange;
                        v = y * topRange;
                        strength = BilinearInterpolator(0, 0, 0, 1, u, v);
                    }
                    else if (x >= mapPixel.locationRect.xMax && y <= mapPixel.locationRect.yMin)
                    {
                        u = (terrainSampleDim - x) * rightRange;
                        v = y * topRange;
                        strength = BilinearInterpolator(0, 0, 0, 1, u, v);
                    }
                    else if (x <= mapPixel.locationRect.xMin && y >= mapPixel.locationRect.yMax)
                    {
                        u = x * leftRange;
                        v = (terrainSampleDim - y) * bottomRange;
                        strength = BilinearInterpolator(0, 0, 0, 1, u, v);
                    }
                    else if (x >= mapPixel.locationRect.xMax && y >= mapPixel.locationRect.yMax)
                    {
                        u = (terrainSampleDim - x) * rightRange;
                        v = (terrainSampleDim - y) * bottomRange;
                        strength = BilinearInterpolator(0, 0, 0, 1, u, v);
                    }

                    // Apply a little noise to gradient so it doesn't look perfectly smooth
                    // Noise strength is the inverse of scalemap strength
                    float extraNoise = GetNoise(contentReader, x, y, 0.1f, 0.5f, 0.5f, 1) * extraNoiseScale * (1f - strength);

                    int offset = y * terrainSampleDim + x;
                    float curHeight = mapPixel.samples[offset].scaledHeight;
                    if (!mapPixel.samples[offset].location)
                    {
                        mapPixel.samples[offset].scaledHeight = (desiredHeight * strength) + (curHeight * (1 - strength)) + extraNoise;
                    }
                    else
                    {
                        mapPixel.samples[offset].scaledHeight = desiredHeight;
                    }
                }
            }
        }
        /// <summary>
        /// Terrain interpolation causes Daggerfall's square coastline to become nicely raised and curvy.
        /// A side effect of this is that underwater climate areas are raised above sea-level.
        /// This function dilates coastal land climate into nearby ocean to hide this issue.
        /// Intended to be called once at startup. Modifies runtime copy of CLIMATE.PAK buffer.
        /// </summary>
        public static void DilateCoastalClimate(ContentReader contentReader, int passes)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            for (int pass = 0; pass < passes; pass++)
            {
                // Get clone of in-memory climate array
                byte[] climateArray = contentReader.MapFileReader.ClimateFile.Buffer.Clone() as byte[];

                // Dilate coastal areas
                for (int y = 1; y < WoodsFile.mapHeightValue - 1; y++)
                {
                    for (int x = 1; x < WoodsFile.mapWidthValue - 1; x++)
                    {
                        // Transfer climate of this pixel to any ocean pixel in Moore neighbourhood
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x - 1, y - 1);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x, y - 1);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x + 1, y - 1);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x - 1, y);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x + 1, y);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x - 1, y + 1);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x, y + 1);
                        TransferLandToOcean(contentReader, ref climateArray, x, y, x + 1, y + 1);
                    }
                }

                // Store modified climate array
                contentReader.MapFileReader.ClimateFile.Buffer = climateArray;
            }

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to dilate coastal climates: {0}ms", totalTime), true);
        }
 private void SetupContentReaders(bool force = false)
 {
     if (reader == null || force)
     {
         // Ensure content readers available even when path not valid
         if (isPathValidated)
         {
             DaggerfallUnity.LogMessage(string.Format("Setting up content readers with arena2 path '{0}'.", Arena2Path));
             reader = new ContentReader(Arena2Path);
         }
         else
         {
             DaggerfallUnity.LogMessage(string.Format("Setting up content readers without arena2 path. Not all features will be available."));
             reader = new ContentReader(string.Empty);
         }
     }
 }
        private void SetupContentReaders(bool force = false)
        {
            if (reader == null || force)
            {
                // Ensure content readers available even when path not valid
                if (isPathValidated)
                {
                    DaggerfallUnity.LogMessage(string.Format("Setting up content readers with arena2 path '{0}'.", Arena2Path));
                    reader = new ContentReader(Arena2Path);
                }
                else
                {
                    DaggerfallUnity.LogMessage(string.Format("Setting up content readers without arena2 path. Not all features will be available."));
                    reader = new ContentReader(string.Empty);
                }

                // Allow external code to set their own terrain sampler at start
                RaiseOnSetTerrainSamplerEvent();
            }
        }