diff --git a/OpenSim/Data/MSSQL/MSSQLSimulationData.cs b/OpenSim/Data/MSSQL/MSSQLSimulationData.cs index 0d09be6361..145b9c0aad 100644 --- a/OpenSim/Data/MSSQL/MSSQLSimulationData.cs +++ b/OpenSim/Data/MSSQL/MSSQLSimulationData.cs @@ -530,43 +530,52 @@ ELSE /// public double[,] LoadTerrain(UUID regionID) { - double[,] terrain = new double[(int)Constants.RegionSize, (int)Constants.RegionSize]; - terrain.Initialize(); + double[,] ret = null; + TerrainData terrData = LoadTerrain(regionID, (int)Constants.RegionSize, (int)Constants.RegionSize, (int)Constants.RegionHeight); + if (terrData != null) + ret = terrData.GetDoubles(); + return ret; + } + + // Returns 'null' if region not found + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + TerrainData terrData = null; string sql = "select top 1 RegionUUID, Revision, Heightfield from terrain where RegionUUID = @RegionUUID order by Revision desc"; using (SqlConnection conn = new SqlConnection(m_connectionString)) - using (SqlCommand cmd = new SqlCommand(sql, conn)) { - // MySqlParameter param = new MySqlParameter(); - cmd.Parameters.Add(_Database.CreateParameter("@RegionUUID", regionID)); - conn.Open(); - using (SqlDataReader reader = cmd.ExecuteReader()) + using (SqlCommand cmd = new SqlCommand(sql, conn)) { - int rev; - if (reader.Read()) + // MySqlParameter param = new MySqlParameter(); + cmd.Parameters.Add(_Database.CreateParameter("@RegionUUID", regionID)); + conn.Open(); + using (SqlDataReader reader = cmd.ExecuteReader()) { - MemoryStream str = new MemoryStream((byte[])reader["Heightfield"]); - BinaryReader br = new BinaryReader(str); - for (int x = 0; x < (int)Constants.RegionSize; x++) + if (reader.Read()) { - for (int y = 0; y < (int)Constants.RegionSize; y++) - { - terrain[x, y] = br.ReadDouble(); - } + int rev = (int)reader["Revision"]; + byte[] blob = (byte[])reader["Heightfield"]; + terrData = TerrainData.CreateFromDatabaseBlobFactory(pSizeX, pSizeY, pSizeZ, rev, blob); } - rev = (int)reader["Revision"]; + else + { + _Log.Info("[REGION DB]: No terrain found for region"); + return null; + } + _Log.Info("[REGION DB]: Loaded terrain"); } - else - { - _Log.Info("[REGION DB]: No terrain found for region"); - return null; - } - _Log.Info("[REGION DB]: Loaded terrain revision r" + rev); } } - return terrain; + return terrData; + } + + // Legacy entry point for when terrain was always a 256x256 hieghtmap + public void StoreTerrain(double[,] ter, UUID regionID) + { + StoreTerrain(new HeightmapTerrainData(ter), regionID); } /// @@ -574,10 +583,8 @@ ELSE /// /// terrain map data. /// regionID. - public void StoreTerrain(double[,] terrain, UUID regionID) + public void StoreTerrain(TerrainData terrData, UUID regionID) { - int revision = Util.UnixTimeSinceEpoch(); - //Delete old terrain map string sql = "delete from terrain where RegionUUID=@RegionUUID"; using (SqlConnection conn = new SqlConnection(m_connectionString)) @@ -590,17 +597,23 @@ ELSE sql = "insert into terrain(RegionUUID, Revision, Heightfield) values(@RegionUUID, @Revision, @Heightfield)"; + int terrainDBRevision; + Array terrainDBblob; + terrData.GetDatabaseBlob(out terrainDBRevision, out terrainDBblob); + using (SqlConnection conn = new SqlConnection(m_connectionString)) - using (SqlCommand cmd = new SqlCommand(sql, conn)) { - cmd.Parameters.Add(_Database.CreateParameter("@RegionUUID", regionID)); - cmd.Parameters.Add(_Database.CreateParameter("@Revision", revision)); - cmd.Parameters.Add(_Database.CreateParameter("@Heightfield", serializeTerrain(terrain))); - conn.Open(); - cmd.ExecuteNonQuery(); + using (SqlCommand cmd = new SqlCommand(sql, conn)) + { + cmd.Parameters.Add(_Database.CreateParameter("@RegionUUID", regionID)); + cmd.Parameters.Add(_Database.CreateParameter("@Revision", terrainDBRevision)); + cmd.Parameters.Add(_Database.CreateParameter("@Heightfield", terrainDBblob)); + conn.Open(); + cmd.ExecuteNonQuery(); + } } - _Log.Info("[REGION DB]: Stored terrain revision r " + revision); + _Log.Info("[REGION DB]: Stored terrain"); } /// @@ -1344,6 +1357,7 @@ VALUES #region Private Methods + /* /// /// Serializes the terrain data for storage in DB. /// @@ -1367,6 +1381,7 @@ VALUES return str.ToArray(); } + */ /// /// Stores new regionsettings. diff --git a/OpenSim/Data/MySQL/MySQLSimulationData.cs b/OpenSim/Data/MySQL/MySQLSimulationData.cs index cc844aef76..f910e443fc 100644 --- a/OpenSim/Data/MySQL/MySQLSimulationData.cs +++ b/OpenSim/Data/MySQL/MySQLSimulationData.cs @@ -48,8 +48,18 @@ namespace OpenSim.Data.MySQL public class MySQLSimulationData : ISimulationDataStore { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static string LogHeader = "[REGION DB MYSQL]"; private string m_connectionString; + + /// + /// This lock was being used to serialize database operations when the connection was shared, but this has + /// been unnecessary for a long time after we switched to using MySQL's underlying connection pooling instead. + /// FIXME: However, the locks remain in many places since they are effectively providing a level of + /// transactionality. This should be replaced by more efficient database transactions which would not require + /// unrelated operations to block each other or unrelated operations on the same tables from blocking each + /// other. + /// private object m_dbLock = new object(); protected virtual Assembly Assembly @@ -91,7 +101,7 @@ namespace OpenSim.Data.MySQL } catch (Exception e) { - m_log.Error("[REGION DB]: MySQL error in ExecuteReader: " + e.Message); + m_log.ErrorFormat("{0} MySQL error in ExecuteReader: {1}", LogHeader, e); throw; } @@ -574,12 +584,16 @@ namespace OpenSim.Data.MySQL } } - public virtual void StoreTerrain(double[,] ter, UUID regionID) + // Legacy entry point for when terrain was always a 256x256 hieghtmap + public void StoreTerrain(double[,] ter, UUID regionID) + { + StoreTerrain(new HeightmapTerrainData(ter), regionID); + } + + public void StoreTerrain(TerrainData terrData, UUID regionID) { Util.FireAndForget(delegate(object x) { - double[,] oldTerrain = LoadTerrain(regionID); - m_log.Info("[REGION DB]: Storing terrain"); lock (m_dbLock) @@ -601,8 +615,12 @@ namespace OpenSim.Data.MySQL "Revision, Heightfield) values (?RegionUUID, " + "1, ?Heightfield)"; - cmd2.Parameters.AddWithValue("RegionUUID", regionID.ToString()); - cmd2.Parameters.AddWithValue("Heightfield", SerializeTerrain(ter, oldTerrain)); + int terrainDBRevision; + Array terrainDBblob; + terrData.GetDatabaseBlob(out terrainDBRevision, out terrainDBblob); + + cmd2.Parameters.AddWithValue("Revision", terrainDBRevision); + cmd2.Parameters.AddWithValue("Heightfield", terrainDBblob); ExecuteNonQuery(cmd); ExecuteNonQuery(cmd2); @@ -618,9 +636,20 @@ namespace OpenSim.Data.MySQL }); } + // Legacy region loading public virtual double[,] LoadTerrain(UUID regionID) { - double[,] terrain = null; + double[,] ret = null; + TerrainData terrData = LoadTerrain(regionID, (int)Constants.RegionSize, (int)Constants.RegionSize, (int)Constants.RegionHeight); + if (terrData != null) + ret = terrData.GetDoubles(); + return ret; + } + + // Returns 'null' if region not found + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + TerrainData terrData = null; lock (m_dbLock) { @@ -640,32 +669,15 @@ namespace OpenSim.Data.MySQL while (reader.Read()) { int rev = Convert.ToInt32(reader["Revision"]); - - terrain = new double[(int)Constants.RegionSize, (int)Constants.RegionSize]; - terrain.Initialize(); - - using (MemoryStream mstr = new MemoryStream((byte[])reader["Heightfield"])) - { - using (BinaryReader br = new BinaryReader(mstr)) - { - for (int x = 0; x < (int)Constants.RegionSize; x++) - { - for (int y = 0; y < (int)Constants.RegionSize; y++) - { - terrain[x, y] = br.ReadDouble(); - } - } - } - - m_log.InfoFormat("[REGION DB]: Loaded terrain revision r{0}", rev); - } + byte[] blob = (byte[])reader["Heightfield"]; + terrData = TerrainData.CreateFromDatabaseBlobFactory(pSizeX, pSizeY, pSizeZ, rev, blob); } } } } } - return terrain; + return terrData; } public virtual void RemoveLandObject(UUID globalID) diff --git a/OpenSim/Data/Null/NullSimulationData.cs b/OpenSim/Data/Null/NullSimulationData.cs index 15824a9ebe..339e7f45b0 100644 --- a/OpenSim/Data/Null/NullSimulationData.cs +++ b/OpenSim/Data/Null/NullSimulationData.cs @@ -132,15 +132,33 @@ namespace OpenSim.Data.Null return new List(); } - Dictionary m_terrains = new Dictionary(); - public void StoreTerrain(double[,] ter, UUID regionID) + Dictionary m_terrains = new Dictionary(); + public void StoreTerrain(TerrainData ter, UUID regionID) { if (m_terrains.ContainsKey(regionID)) m_terrains.Remove(regionID); m_terrains.Add(regionID, ter); } + // Legacy. Just don't do this. + public void StoreTerrain(double[,] ter, UUID regionID) + { + TerrainData terrData = new HeightmapTerrainData(ter); + StoreTerrain(terrData, regionID); + } + + // Legacy. Just don't do this. + // Returns 'null' if region not found public double[,] LoadTerrain(UUID regionID) + { + if (m_terrains.ContainsKey(regionID)) + { + return m_terrains[regionID].GetDoubles(); + } + return null; + } + + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) { if (m_terrains.ContainsKey(regionID)) { diff --git a/OpenSim/Data/PGSQL/PGSQLSimulationData.cs b/OpenSim/Data/PGSQL/PGSQLSimulationData.cs index 3243635a3a..cd02e058bc 100644 --- a/OpenSim/Data/PGSQL/PGSQLSimulationData.cs +++ b/OpenSim/Data/PGSQL/PGSQLSimulationData.cs @@ -46,6 +46,7 @@ namespace OpenSim.Data.PGSQL public class PGSQLSimulationData : ISimulationDataStore { private const string _migrationStore = "RegionStore"; + private const string LogHeader = "[REGION DB PGSQL]"; // private static FileSystemDataStore Instance = new FileSystemDataStore(); private static readonly ILog _Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); @@ -523,8 +524,17 @@ namespace OpenSim.Data.PGSQL /// public double[,] LoadTerrain(UUID regionID) { - double[,] terrain = new double[(int)Constants.RegionSize, (int)Constants.RegionSize]; - terrain.Initialize(); + double[,] ret = null; + TerrainData terrData = LoadTerrain(regionID, (int)Constants.RegionSize, (int)Constants.RegionSize, (int)Constants.RegionHeight); + if (terrData != null) + ret = terrData.GetDoubles(); + return ret; + } + + // Returns 'null' if region not found + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + TerrainData terrData = null; string sql = @"select ""RegionUUID"", ""Revision"", ""Heightfield"" from terrain where ""RegionUUID"" = :RegionUUID order by ""Revision"" desc limit 1; "; @@ -540,16 +550,9 @@ namespace OpenSim.Data.PGSQL int rev; if (reader.Read()) { - MemoryStream str = new MemoryStream((byte[])reader["Heightfield"]); - BinaryReader br = new BinaryReader(str); - for (int x = 0; x < (int)Constants.RegionSize; x++) - { - for (int y = 0; y < (int)Constants.RegionSize; y++) - { - terrain[x, y] = br.ReadDouble(); - } - } - rev = (int)reader["Revision"]; + rev = Convert.ToInt32(reader["Revision"]); + byte[] blob = (byte[])reader["Heightfield"]; + terrData = TerrainData.CreateFromDatabaseBlobFactory(pSizeX, pSizeY, pSizeZ, rev, blob); } else { @@ -560,7 +563,13 @@ namespace OpenSim.Data.PGSQL } } - return terrain; + return terrData; + } + + // Legacy entry point for when terrain was always a 256x256 heightmap + public void StoreTerrain(double[,] terrain, UUID regionID) + { + StoreTerrain(new HeightmapTerrainData(terrain), regionID); } /// @@ -568,35 +577,38 @@ namespace OpenSim.Data.PGSQL /// /// terrain map data. /// regionID. - public void StoreTerrain(double[,] terrain, UUID regionID) + public void StoreTerrain(TerrainData terrData, UUID regionID) { - int revision = Util.UnixTimeSinceEpoch(); - //Delete old terrain map string sql = @"delete from terrain where ""RegionUUID""=:RegionUUID"; using (NpgsqlConnection conn = new NpgsqlConnection(m_connectionString)) - using (NpgsqlCommand cmd = new NpgsqlCommand(sql, conn)) { - cmd.Parameters.Add(_Database.CreateParameter("RegionUUID", regionID)); - conn.Open(); - cmd.ExecuteNonQuery(); + using (NpgsqlCommand cmd = new NpgsqlCommand(sql, conn)) + { + cmd.Parameters.Add(_Database.CreateParameter("RegionUUID", regionID)); + conn.Open(); + cmd.ExecuteNonQuery(); + } } - - _Log.Info("[REGION DB]: Deleted terrain revision r " + revision); + int terrainDBRevision; + Array terrainDBblob; + terrData.GetDatabaseBlob(out terrainDBRevision, out terrainDBblob); sql = @"insert into terrain(""RegionUUID"", ""Revision"", ""Heightfield"") values(:RegionUUID, :Revision, :Heightfield)"; using (NpgsqlConnection conn = new NpgsqlConnection(m_connectionString)) - using (NpgsqlCommand cmd = new NpgsqlCommand(sql, conn)) { - cmd.Parameters.Add(_Database.CreateParameter("RegionUUID", regionID)); - cmd.Parameters.Add(_Database.CreateParameter("Revision", revision)); - cmd.Parameters.Add(_Database.CreateParameter("Heightfield", serializeTerrain(terrain))); - conn.Open(); - cmd.ExecuteNonQuery(); + using (NpgsqlCommand cmd = new NpgsqlCommand(sql, conn)) + { + cmd.Parameters.Add(_Database.CreateParameter("RegionUUID", regionID)); + cmd.Parameters.Add(_Database.CreateParameter("Revision", terrainDBRevision)); + cmd.Parameters.Add(_Database.CreateParameter("Heightfield", terrainDBblob)); + conn.Open(); + cmd.ExecuteNonQuery(); + } } - _Log.Info("[REGION DB]: Stored terrain revision r " + revision); + _Log.Info("[REGION DB]: Stored terrain revision r " + terrainDBRevision); } /// @@ -1349,6 +1361,7 @@ namespace OpenSim.Data.PGSQL #region Private Methods + /* /// /// Serializes the terrain data for storage in DB. /// @@ -1372,6 +1385,7 @@ namespace OpenSim.Data.PGSQL return str.ToArray(); } + */ /// /// Stores new regionsettings. diff --git a/OpenSim/Data/SQLite/SQLiteSimulationData.cs b/OpenSim/Data/SQLite/SQLiteSimulationData.cs index 4d6a80ad22..d28c2277e0 100644 --- a/OpenSim/Data/SQLite/SQLiteSimulationData.cs +++ b/OpenSim/Data/SQLite/SQLiteSimulationData.cs @@ -51,6 +51,7 @@ namespace OpenSim.Data.SQLite public class SQLiteSimulationData : ISimulationDataStore { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly string LogHeader = "[REGION DB SQLLITE]"; private const string primSelect = "select * from prims"; private const string shapeSelect = "select * from primshapes"; @@ -819,12 +820,18 @@ namespace OpenSim.Data.SQLite prim.Inventory.RestoreInventoryItems(inventory); } + // Legacy entry point for when terrain was always a 256x256 hieghtmap + public void StoreTerrain(double[,] ter, UUID regionID) + { + StoreTerrain(new HeightmapTerrainData(ter), regionID); + } + /// /// Store a terrain revision in region storage /// /// terrain heightfield /// region UUID - public void StoreTerrain(double[,] ter, UUID regionID) + public void StoreTerrain(TerrainData terrData, UUID regionID) { lock (ds) { @@ -853,11 +860,17 @@ namespace OpenSim.Data.SQLite String sql = "insert into terrain(RegionUUID, Revision, Heightfield)" + " values(:RegionUUID, :Revision, :Heightfield)"; + int terrainDBRevision; + Array terrainDBblob; + terrData.GetDatabaseBlob(out terrainDBRevision, out terrainDBblob); + + m_log.DebugFormat("{0} Storing terrain revision r {1}", LogHeader, terrainDBRevision); + using (SqliteCommand cmd = new SqliteCommand(sql, m_conn)) { cmd.Parameters.Add(new SqliteParameter(":RegionUUID", regionID.ToString())); - cmd.Parameters.Add(new SqliteParameter(":Revision", revision)); - cmd.Parameters.Add(new SqliteParameter(":Heightfield", serializeTerrain(ter))); + cmd.Parameters.Add(new SqliteParameter(":Revision", terrainDBRevision)); + cmd.Parameters.Add(new SqliteParameter(":Heightfield", terrainDBblob)); cmd.ExecuteNonQuery(); } } @@ -870,11 +883,20 @@ namespace OpenSim.Data.SQLite /// Heightfield data public double[,] LoadTerrain(UUID regionID) { + double[,] ret = null; + TerrainData terrData = LoadTerrain(regionID, (int)Constants.RegionSize, (int)Constants.RegionSize, (int)Constants.RegionHeight); + if (terrData != null) + ret = terrData.GetDoubles(); + return ret; + } + + // Returns 'null' if region not found + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + TerrainData terrData = null; + lock (ds) { - double[,] terret = new double[(int)Constants.RegionSize, (int)Constants.RegionSize]; - terret.Initialize(); - String sql = "select RegionUUID, Revision, Heightfield from terrain" + " where RegionUUID=:RegionUUID order by Revision desc"; @@ -887,21 +909,9 @@ namespace OpenSim.Data.SQLite int rev = 0; if (row.Read()) { - // TODO: put this into a function - using (MemoryStream str = new MemoryStream((byte[])row["Heightfield"])) - { - using (BinaryReader br = new BinaryReader(str)) - { - for (int x = 0; x < (int)Constants.RegionSize; x++) - { - for (int y = 0; y < (int)Constants.RegionSize; y++) - { - terret[x, y] = br.ReadDouble(); - } - } - } - } rev = Convert.ToInt32(row["Revision"]); + byte[] blob = (byte[])row["Heightfield"]; + terrData = TerrainData.CreateFromDatabaseBlobFactory(pSizeX, pSizeY, pSizeZ, rev, blob); } else { @@ -912,8 +922,8 @@ namespace OpenSim.Data.SQLite m_log.Debug("[SQLITE REGION DB]: Loaded terrain revision r" + rev.ToString()); } } - return terret; } + return terrData; } public void RemoveLandObject(UUID globalID) @@ -2016,6 +2026,7 @@ namespace OpenSim.Data.SQLite return entry; } + /* /// /// /// @@ -2033,6 +2044,7 @@ namespace OpenSim.Data.SQLite return str.ToArray(); } + */ // private void fillTerrainRow(DataRow row, UUID regionUUID, int rev, double[,] val) // { diff --git a/OpenSim/Framework/TerrainData.cs b/OpenSim/Framework/TerrainData.cs new file mode 100644 index 0000000000..6b1be4e9fa --- /dev/null +++ b/OpenSim/Framework/TerrainData.cs @@ -0,0 +1,464 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +using OpenMetaverse; + +using log4net; + +namespace OpenSim.Framework +{ + public abstract class TerrainData + { + // Terrain always is a square + public int SizeX { get; protected set; } + public int SizeY { get; protected set; } + public int SizeZ { get; protected set; } + + // A height used when the user doesn't specify anything + public const float DefaultTerrainHeight = 21f; + + public abstract float this[int x, int y] { get; set; } + // Someday terrain will have caves + public abstract float this[int x, int y, int z] { get; set; } + + public abstract bool IsTaintedAt(int xx, int yy); + public abstract bool IsTaintedAt(int xx, int yy, bool clearOnTest); + public abstract void TaintAllTerrain(); + public abstract void ClearTaint(); + + public abstract void ClearLand(); + public abstract void ClearLand(float height); + + // Return a representation of this terrain for storing as a blob in the database. + // Returns 'true' to say blob was stored in the 'out' locations. + public abstract bool GetDatabaseBlob(out int DBFormatRevisionCode, out Array blob); + + // Given a revision code and a blob from the database, create and return the right type of TerrainData. + // The sizes passed are the expected size of the region. The database info will be used to + // initialize the heightmap of that sized region with as much data is in the blob. + // Return created TerrainData or 'null' if unsuccessful. + public static TerrainData CreateFromDatabaseBlobFactory(int pSizeX, int pSizeY, int pSizeZ, int pFormatCode, byte[] pBlob) + { + // For the moment, there is only one implementation class + return new HeightmapTerrainData(pSizeX, pSizeY, pSizeZ, pFormatCode, pBlob); + } + + // return a special compressed representation of the heightmap in ints + public abstract int[] GetCompressedMap(); + public abstract float CompressionFactor { get; } + + public abstract float[] GetFloatsSerialized(); + public abstract double[,] GetDoubles(); + public abstract TerrainData Clone(); + } + + // The terrain is stored in the database as a blob with a 'revision' field. + // Some implementations of terrain storage would fill the revision field with + // the time the terrain was stored. When real revisions were added and this + // feature removed, that left some old entries with the time in the revision + // field. + // Thus, if revision is greater than 'RevisionHigh' then terrain db entry is + // left over and it is presumed to be 'Legacy256'. + // Numbers are arbitrary and are chosen to to reduce possible mis-interpretation. + // If a revision does not match any of these, it is assumed to be Legacy256. + public enum DBTerrainRevision + { + // Terrain is 'double[256,256]' + Legacy256 = 11, + // Terrain is 'int32, int32, float[,]' where the ints are X and Y dimensions + // The dimensions are presumed to be multiples of 16 and, more likely, multiples of 256. + Variable2D = 22, + // Terrain is 'int32, int32, int32, int16[]' where the ints are X and Y dimensions + // and third int is the 'compression factor'. The heights are compressed as + // "int compressedHeight = (int)(height * compressionFactor);" + // The dimensions are presumed to be multiples of 16 and, more likely, multiples of 256. + Compressed2D = 27, + // A revision that is not listed above or any revision greater than this value is 'Legacy256'. + RevisionHigh = 1234 + } + + // Version of terrain that is a heightmap. + // This should really be 'LLOptimizedHeightmapTerrainData' as it includes knowledge + // of 'patches' which are 16x16 terrain areas which can be sent separately to the viewer. + // The heighmap is kept as an array of integers. The integer values are converted to + // and from floats by TerrainCompressionFactor. + public class HeightmapTerrainData : TerrainData + { + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static string LogHeader = "[HEIGHTMAP TERRAIN DATA]"; + + // TerrainData.this[x, y] + public override float this[int x, int y] + { + get { return FromCompressedHeight(m_heightmap[x, y]); } + set { + int newVal = ToCompressedHeight(value); + if (m_heightmap[x, y] != newVal) + { + m_heightmap[x, y] = newVal; + m_taint[x / Constants.TerrainPatchSize, y / Constants.TerrainPatchSize] = true; + } + } + } + + // TerrainData.this[x, y, z] + public override float this[int x, int y, int z] + { + get { return this[x, y]; } + set { this[x, y] = value; } + } + + // TerrainData.ClearTaint + public override void ClearTaint() + { + SetAllTaint(false); + } + + // TerrainData.TaintAllTerrain + public override void TaintAllTerrain() + { + SetAllTaint(true); + } + + private void SetAllTaint(bool setting) + { + for (int ii = 0; ii < m_taint.GetLength(0); ii++) + for (int jj = 0; jj < m_taint.GetLength(1); jj++) + m_taint[ii, jj] = setting; + } + + // TerrainData.ClearLand + public override void ClearLand() + { + ClearLand(DefaultTerrainHeight); + } + // TerrainData.ClearLand(float) + public override void ClearLand(float pHeight) + { + int flatHeight = ToCompressedHeight(pHeight); + for (int xx = 0; xx < SizeX; xx++) + for (int yy = 0; yy < SizeY; yy++) + m_heightmap[xx, yy] = flatHeight; + } + + // Return 'true' of the patch that contains these region coordinates has been modified. + // Note that checking the taint clears it. + // There is existing code that relies on this feature. + public override bool IsTaintedAt(int xx, int yy, bool clearOnTest) + { + int tx = xx / Constants.TerrainPatchSize; + int ty = yy / Constants.TerrainPatchSize; + bool ret = m_taint[tx, ty]; + if (ret && clearOnTest) + m_taint[tx, ty] = false; + return ret; + } + + // Old form that clears the taint flag when we check it. + public override bool IsTaintedAt(int xx, int yy) + { + return IsTaintedAt(xx, yy, true /* clearOnTest */); + } + + // TerrainData.GetDatabaseBlob + // The user wants something to store in the database. + public override bool GetDatabaseBlob(out int DBRevisionCode, out Array blob) + { + bool ret = false; + if (SizeX == Constants.RegionSize && SizeY == Constants.RegionSize) + { + DBRevisionCode = (int)DBTerrainRevision.Legacy256; + blob = ToLegacyTerrainSerialization(); + ret = true; + } + else + { + DBRevisionCode = (int)DBTerrainRevision.Compressed2D; + blob = ToCompressedTerrainSerialization(); + ret = true; + } + return ret; + } + + // TerrainData.CompressionFactor + private float m_compressionFactor = 100.0f; + public override float CompressionFactor { get { return m_compressionFactor; } } + + // TerrainData.GetCompressedMap + public override int[] GetCompressedMap() + { + int[] newMap = new int[SizeX * SizeY]; + + int ind = 0; + for (int xx = 0; xx < SizeX; xx++) + for (int yy = 0; yy < SizeY; yy++) + newMap[ind++] = m_heightmap[xx, yy]; + + return newMap; + + } + // TerrainData.Clone + public override TerrainData Clone() + { + HeightmapTerrainData ret = new HeightmapTerrainData(SizeX, SizeY, SizeZ); + ret.m_heightmap = (int[,])this.m_heightmap.Clone(); + return ret; + } + + // TerrainData.GetFloatsSerialized + // This one dimensional version is ordered so height = map[y*sizeX+x]; + // DEPRECATED: don't use this function as it does not retain the dimensions of the terrain + // and the caller will probably do the wrong thing if the terrain is not the legacy 256x256. + public override float[] GetFloatsSerialized() + { + int points = SizeX * SizeY; + float[] heights = new float[points]; + + int idx = 0; + for (int jj = 0; jj < SizeY; jj++) + for (int ii = 0; ii < SizeX; ii++) + { + heights[idx++] = FromCompressedHeight(m_heightmap[ii, jj]); + } + + return heights; + } + + // TerrainData.GetDoubles + public override double[,] GetDoubles() + { + double[,] ret = new double[SizeX, SizeY]; + for (int xx = 0; xx < SizeX; xx++) + for (int yy = 0; yy < SizeY; yy++) + ret[xx, yy] = FromCompressedHeight(m_heightmap[xx, yy]); + + return ret; + } + + + // ============================================================= + + private int[,] m_heightmap; + // Remember subregions of the heightmap that has changed. + private bool[,] m_taint; + + // To save space (especially for large regions), keep the height as a short integer + // that is coded as the float height times the compression factor (usually '100' + // to make for two decimal points). + public int ToCompressedHeight(double pHeight) + { + return (int)(pHeight * CompressionFactor); + } + + public float FromCompressedHeight(int pHeight) + { + return ((float)pHeight) / CompressionFactor; + } + + // To keep with the legacy theme, create an instance of this class based on the + // way terrain used to be passed around. + public HeightmapTerrainData(double[,] pTerrain) + { + SizeX = pTerrain.GetLength(0); + SizeY = pTerrain.GetLength(1); + SizeZ = (int)Constants.RegionHeight; + m_compressionFactor = 100.0f; + + m_heightmap = new int[SizeX, SizeY]; + for (int ii = 0; ii < SizeX; ii++) + { + for (int jj = 0; jj < SizeY; jj++) + { + m_heightmap[ii, jj] = ToCompressedHeight(pTerrain[ii, jj]); + + } + } + // m_log.DebugFormat("{0} new by doubles. sizeX={1}, sizeY={2}, sizeZ={3}", LogHeader, SizeX, SizeY, SizeZ); + + m_taint = new bool[SizeX / Constants.TerrainPatchSize, SizeY / Constants.TerrainPatchSize]; + ClearTaint(); + } + + // Create underlying structures but don't initialize the heightmap assuming the caller will immediately do that + public HeightmapTerrainData(int pX, int pY, int pZ) + { + SizeX = pX; + SizeY = pY; + SizeZ = pZ; + m_compressionFactor = 100.0f; + m_heightmap = new int[SizeX, SizeY]; + m_taint = new bool[SizeX / Constants.TerrainPatchSize, SizeY / Constants.TerrainPatchSize]; + // m_log.DebugFormat("{0} new by dimensions. sizeX={1}, sizeY={2}, sizeZ={3}", LogHeader, SizeX, SizeY, SizeZ); + ClearTaint(); + ClearLand(0f); + } + + public HeightmapTerrainData(int[] cmap, float pCompressionFactor, int pX, int pY, int pZ) : this(pX, pY, pZ) + { + m_compressionFactor = pCompressionFactor; + int ind = 0; + for (int xx = 0; xx < SizeX; xx++) + for (int yy = 0; yy < SizeY; yy++) + m_heightmap[xx, yy] = cmap[ind++]; + // m_log.DebugFormat("{0} new by compressed map. sizeX={1}, sizeY={2}, sizeZ={3}", LogHeader, SizeX, SizeY, SizeZ); + } + + // Create a heighmap from a database blob + public HeightmapTerrainData(int pSizeX, int pSizeY, int pSizeZ, int pFormatCode, byte[] pBlob) : this(pSizeX, pSizeY, pSizeZ) + { + switch ((DBTerrainRevision)pFormatCode) + { + case DBTerrainRevision.Compressed2D: + FromCompressedTerrainSerialization(pBlob); + m_log.DebugFormat("{0} HeightmapTerrainData create from Compressed2D serialization. Size=<{1},{2}>", LogHeader, SizeX, SizeY); + break; + default: + FromLegacyTerrainSerialization(pBlob); + m_log.DebugFormat("{0} HeightmapTerrainData create from legacy serialization. Size=<{1},{2}>", LogHeader, SizeX, SizeY); + break; + } + } + + // Just create an array of doubles. Presumes the caller implicitly knows the size. + public Array ToLegacyTerrainSerialization() + { + Array ret = null; + + using (MemoryStream str = new MemoryStream((int)Constants.RegionSize * (int)Constants.RegionSize * sizeof(double))) + { + using (BinaryWriter bw = new BinaryWriter(str)) + { + for (int xx = 0; xx < Constants.RegionSize; xx++) + { + for (int yy = 0; yy < Constants.RegionSize; yy++) + { + double height = this[xx, yy]; + if (height == 0.0) + height = double.Epsilon; + bw.Write(height); + } + } + } + ret = str.ToArray(); + } + return ret; + } + + // Just create an array of doubles. Presumes the caller implicitly knows the size. + public void FromLegacyTerrainSerialization(byte[] pBlob) + { + // In case database info doesn't match real terrain size, initialize the whole terrain. + ClearLand(); + + using (MemoryStream mstr = new MemoryStream(pBlob)) + { + using (BinaryReader br = new BinaryReader(mstr)) + { + for (int xx = 0; xx < (int)Constants.RegionSize; xx++) + { + for (int yy = 0; yy < (int)Constants.RegionSize; yy++) + { + float val = (float)br.ReadDouble(); + if (xx < SizeX && yy < SizeY) + m_heightmap[xx, yy] = ToCompressedHeight(val); + } + } + } + ClearTaint(); + } + } + + // See the reader below. + public Array ToCompressedTerrainSerialization() + { + Array ret = null; + using (MemoryStream str = new MemoryStream((3 * sizeof(Int32)) + (SizeX * SizeY * sizeof(Int16)))) + { + using (BinaryWriter bw = new BinaryWriter(str)) + { + bw.Write((Int32)DBTerrainRevision.Compressed2D); + bw.Write((Int32)SizeX); + bw.Write((Int32)SizeY); + bw.Write((Int32)CompressionFactor); + for (int yy = 0; yy < SizeY; yy++) + for (int xx = 0; xx < SizeX; xx++) + { + bw.Write((Int16)m_heightmap[xx, yy]); + } + } + ret = str.ToArray(); + } + return ret; + } + + // Initialize heightmap from blob consisting of: + // int32, int32, int32, int32, int16[] + // where the first int32 is format code, next two int32s are the X and y of heightmap data and + // the forth int is the compression factor for the following int16s + // This is just sets heightmap info. The actual size of the region was set on this instance's + // creation and any heights not initialized by theis blob are set to the default height. + public void FromCompressedTerrainSerialization(byte[] pBlob) + { + Int32 hmFormatCode, hmSizeX, hmSizeY, hmCompressionFactor; + + using (MemoryStream mstr = new MemoryStream(pBlob)) + { + using (BinaryReader br = new BinaryReader(mstr)) + { + hmFormatCode = br.ReadInt32(); + hmSizeX = br.ReadInt32(); + hmSizeY = br.ReadInt32(); + hmCompressionFactor = br.ReadInt32(); + + m_compressionFactor = hmCompressionFactor; + + // In case database info doesn't match real terrain size, initialize the whole terrain. + ClearLand(); + + for (int yy = 0; yy < hmSizeY; yy++) + { + for (int xx = 0; xx < hmSizeX; xx++) + { + Int16 val = br.ReadInt16(); + if (xx < SizeX && yy < SizeY) + m_heightmap[xx, yy] = val; + } + } + } + ClearTaint(); + + m_log.InfoFormat("{0} Read compressed 2d heightmap. Heightmap size=<{1},{2}>. Region size=<{3},{4}>. CompFact={5}", + LogHeader, hmSizeX, hmSizeY, SizeX, SizeY, hmCompressionFactor); + } + } + } +} diff --git a/OpenSim/Region/CoreModules/World/Terrain/FileLoaders/GenericSystemDrawing.cs b/OpenSim/Region/CoreModules/World/Terrain/FileLoaders/GenericSystemDrawing.cs index d78ade5fa7..d5c77ec3e1 100644 --- a/OpenSim/Region/CoreModules/World/Terrain/FileLoaders/GenericSystemDrawing.cs +++ b/OpenSim/Region/CoreModules/World/Terrain/FileLoaders/GenericSystemDrawing.cs @@ -67,7 +67,7 @@ namespace OpenSim.Region.CoreModules.World.Terrain.FileLoaders { using (Bitmap bitmap = new Bitmap(filename)) { - ITerrainChannel retval = new TerrainChannel(true); + ITerrainChannel retval = new TerrainChannel(w, h); for (int x = 0; x < retval.Width; x++) { diff --git a/OpenSim/Region/CoreModules/World/Terrain/TerrainModule.cs b/OpenSim/Region/CoreModules/World/Terrain/TerrainModule.cs index 4d738a58ec..9a88804a72 100644 --- a/OpenSim/Region/CoreModules/World/Terrain/TerrainModule.cs +++ b/OpenSim/Region/CoreModules/World/Terrain/TerrainModule.cs @@ -71,6 +71,10 @@ namespace OpenSim.Region.CoreModules.World.Terrain private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); +#pragma warning disable 414 + private static readonly string LogHeader = "[TERRAIN MODULE]"; +#pragma warning restore 414 + private readonly Commander m_commander = new Commander("terrain"); private readonly Dictionary m_floodeffects = @@ -81,8 +85,8 @@ namespace OpenSim.Region.CoreModules.World.Terrain private readonly Dictionary m_painteffects = new Dictionary(); - private ITerrainChannel m_channel; private Dictionary m_plugineffects; + private ITerrainChannel m_channel; private ITerrainChannel m_revert; private Scene m_scene; private volatile bool m_tainted; @@ -90,6 +94,85 @@ namespace OpenSim.Region.CoreModules.World.Terrain private String m_InitialTerrain = "pinhead-island"; + // If true, send terrain patch updates to clients based on their view distance + private bool m_sendTerrainUpdatesByViewDistance = true; + + // Class to keep the per client collection of terrain patches that must be sent. + // A patch is set to 'true' meaning it should be sent to the client. Once the + // patch packet is queued to the client, the bit for that patch is set to 'false'. + private class PatchUpdates + { + private bool[,] updated; // for each patch, whether it needs to be sent to this client + private int updateCount; // number of patches that need to be sent + public ScenePresence Presence; // a reference to the client to send to + public PatchUpdates(TerrainData terrData, ScenePresence pPresence) + { + updated = new bool[terrData.SizeX / Constants.TerrainPatchSize, terrData.SizeY / Constants.TerrainPatchSize]; + updateCount = 0; + Presence = pPresence; + // Initially, send all patches to the client + SetAll(true); + } + // Returns 'true' if there are any patches marked for sending + public bool HasUpdates() + { + return (updateCount > 0); + } + public void SetByXY(int x, int y, bool state) + { + this.SetByPatch(x / Constants.TerrainPatchSize, y / Constants.TerrainPatchSize, state); + } + public bool GetByPatch(int patchX, int patchY) + { + return updated[patchX, patchY]; + } + public void SetByPatch(int patchX, int patchY, bool state) + { + bool prevState = updated[patchX, patchY]; + if (!prevState && state) + updateCount++; + if (prevState && !state) + updateCount--; + updated[patchX, patchY] = state; + } + public void SetAll(bool state) + { + updateCount = 0; + for (int xx = 0; xx < updated.GetLength(0); xx++) + for (int yy = 0; yy < updated.GetLength(1); yy++) + updated[xx, yy] = state; + if (state) + updateCount = updated.GetLength(0) * updated.GetLength(1); + } + // Logically OR's the terrain data's patch taint map into this client's update map. + public void SetAll(TerrainData terrData) + { + if (updated.GetLength(0) != (terrData.SizeX / Constants.TerrainPatchSize) + || updated.GetLength(1) != (terrData.SizeY / Constants.TerrainPatchSize)) + { + throw new Exception( + String.Format("{0} PatchUpdates.SetAll: patch array not same size as terrain. arr=<{1},{2}>, terr=<{3},{4}>", + LogHeader, updated.GetLength(0), updated.GetLength(1), + terrData.SizeX / Constants.TerrainPatchSize, terrData.SizeY / Constants.TerrainPatchSize) + ); + } + for (int xx = 0; xx < terrData.SizeX; xx += Constants.TerrainPatchSize) + { + for (int yy = 0; yy < terrData.SizeY; yy += Constants.TerrainPatchSize) + { + // Only set tainted. The patch bit may be set if the patch was to be sent later. + if (terrData.IsTaintedAt(xx, yy, false)) + { + this.SetByXY(xx, yy, true); + } + } + } + } + } + + // The flags of which terrain patches to send for each of the ScenePresence's + private Dictionary m_perClientPatchUpdates = new Dictionary(); + /// /// Human readable list of terrain file extensions that are supported. /// @@ -118,7 +201,10 @@ namespace OpenSim.Region.CoreModules.World.Terrain { IConfig terrainConfig = config.Configs["Terrain"]; if (terrainConfig != null) + { m_InitialTerrain = terrainConfig.GetString("InitialTerrain", m_InitialTerrain); + m_sendTerrainUpdatesByViewDistance = terrainConfig.GetBoolean("SendTerrainUpdatesByViewDistance", m_sendTerrainUpdatesByViewDistance); + } } public void AddRegion(Scene scene) @@ -130,22 +216,24 @@ namespace OpenSim.Region.CoreModules.World.Terrain { if (m_scene.Heightmap == null) { - m_channel = new TerrainChannel(m_InitialTerrain); + m_channel = new TerrainChannel(m_InitialTerrain, (int)m_scene.RegionInfo.RegionSizeX, + (int)m_scene.RegionInfo.RegionSizeY, + (int)m_scene.RegionInfo.RegionSizeZ); m_scene.Heightmap = m_channel; - m_revert = new TerrainChannel(); UpdateRevertMap(); } else { m_channel = m_scene.Heightmap; - m_revert = new TerrainChannel(); UpdateRevertMap(); } m_scene.RegisterModuleInterface(this); m_scene.EventManager.OnNewClient += EventManager_OnNewClient; + m_scene.EventManager.OnClientClosed += EventManager_OnClientClosed; m_scene.EventManager.OnPluginConsole += EventManager_OnPluginConsole; m_scene.EventManager.OnTerrainTick += EventManager_OnTerrainTick; + m_scene.EventManager.OnFrame += EventManager_OnFrame; } InstallDefaultEffects(); @@ -184,8 +272,10 @@ namespace OpenSim.Region.CoreModules.World.Terrain // remove the commands m_scene.UnregisterModuleCommander(m_commander.Name); // remove the event-handlers + m_scene.EventManager.OnFrame -= EventManager_OnFrame; m_scene.EventManager.OnTerrainTick -= EventManager_OnTerrainTick; m_scene.EventManager.OnPluginConsole -= EventManager_OnPluginConsole; + m_scene.EventManager.OnClientClosed -= EventManager_OnClientClosed; m_scene.EventManager.OnNewClient -= EventManager_OnNewClient; // remove the interface m_scene.UnregisterModuleInterface(this); @@ -230,11 +320,11 @@ namespace OpenSim.Region.CoreModules.World.Terrain try { ITerrainChannel channel = loader.Value.LoadFile(filename); - if (channel.Width != Constants.RegionSize || channel.Height != Constants.RegionSize) + if (channel.Width != m_scene.RegionInfo.RegionSizeX || channel.Height != m_scene.RegionInfo.RegionSizeY) { // TerrainChannel expects a RegionSize x RegionSize map, currently throw new ArgumentException(String.Format("wrong size, use a file with size {0} x {1}", - Constants.RegionSize, Constants.RegionSize)); + m_scene.RegionInfo.RegionSizeX, m_scene.RegionInfo.RegionSizeY)); } m_log.DebugFormat("[TERRAIN]: Loaded terrain, wd/ht: {0}/{1}", channel.Width, channel.Height); m_scene.Heightmap = channel; @@ -261,7 +351,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain String.Format("Unable to load heightmap: {0}", e.Message)); } } - CheckForTerrainUpdates(); m_log.Info("[TERRAIN]: File (" + filename + ") loaded successfully"); return; } @@ -309,12 +398,18 @@ namespace OpenSim.Region.CoreModules.World.Terrain LoadFromStream(filename, URIFetch(pathToTerrainHeightmap)); } + public void LoadFromStream(string filename, Stream stream) + { + LoadFromStream(filename, Vector3.Zero, 0f, Vector2.Zero, stream); + } + /// /// Loads a terrain file from a stream and installs it in the scene. /// /// Filename to terrain file. Type is determined by extension. /// - public void LoadFromStream(string filename, Stream stream) + public void LoadFromStream(string filename, Vector3 displacement, + float radianRotation, Vector2 rotationDisplacement, Stream stream) { foreach (KeyValuePair loader in m_loaders) { @@ -325,8 +420,7 @@ namespace OpenSim.Region.CoreModules.World.Terrain try { ITerrainChannel channel = loader.Value.LoadStream(stream); - m_scene.Heightmap = channel; - m_channel = channel; + m_channel.Merge(channel, displacement, radianRotation, rotationDisplacement); UpdateRevertMap(); } catch (NotImplementedException) @@ -337,7 +431,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain } } - CheckForTerrainUpdates(); m_log.Info("[TERRAIN]: File (" + filename + ") loaded successfully"); return; } @@ -406,9 +499,46 @@ namespace OpenSim.Region.CoreModules.World.Terrain } } + // Someone diddled terrain outside the normal code paths. Set the taintedness for all clients. + // ITerrainModule.TaintTerrain() public void TaintTerrain () { - CheckForTerrainUpdates(); + lock (m_perClientPatchUpdates) + { + // Set the flags for all clients so the tainted patches will be sent out + foreach (PatchUpdates pups in m_perClientPatchUpdates.Values) + { + pups.SetAll(m_scene.Heightmap.GetTerrainData()); + } + } + } + + // ITerrainModule.PushTerrain() + public void PushTerrain(IClientAPI pClient) + { + if (m_sendTerrainUpdatesByViewDistance) + { + ScenePresence presence = m_scene.GetScenePresence(pClient.AgentId); + if (presence != null) + { + lock (m_perClientPatchUpdates) + { + PatchUpdates pups; + if (!m_perClientPatchUpdates.TryGetValue(pClient.AgentId, out pups)) + { + // There is a ScenePresence without a send patch map. Create one. + pups = new PatchUpdates(m_scene.Heightmap.GetTerrainData(), presence); + m_perClientPatchUpdates.Add(presence.UUID, pups); + } + pups.SetAll(true); + } + } + } + else + { + // The traditional way is to call into the protocol stack to send them all. + pClient.SendLayerData(new float[10]); + } } #region Plugin Loading Methods @@ -532,6 +662,7 @@ namespace OpenSim.Region.CoreModules.World.Terrain /// public void UpdateRevertMap() { + /* int x; for (x = 0; x < m_channel.Width; x++) { @@ -541,6 +672,8 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_revert[x, y] = m_channel[x, y]; } } + */ + m_revert = m_channel.MakeCopy(); } /// @@ -567,8 +700,8 @@ namespace OpenSim.Region.CoreModules.World.Terrain { ITerrainChannel channel = loader.Value.LoadFile(filename, offsetX, offsetY, fileWidth, fileHeight, - (int) Constants.RegionSize, - (int) Constants.RegionSize); + (int) m_scene.RegionInfo.RegionSizeX, + (int) m_scene.RegionInfo.RegionSizeY); m_scene.Heightmap = channel; m_channel = channel; UpdateRevertMap(); @@ -615,8 +748,8 @@ namespace OpenSim.Region.CoreModules.World.Terrain { loader.Value.SaveFile(m_channel, filename, offsetX, offsetY, fileWidth, fileHeight, - (int)Constants.RegionSize, - (int)Constants.RegionSize); + (int)m_scene.RegionInfo.RegionSizeX, + (int)m_scene.RegionInfo.RegionSizeY); MainConsole.Instance.OutputFormat( "Saved terrain from ({0},{1}) to ({2},{3}) from {4} to {5}", @@ -633,8 +766,45 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_scene.RegionInfo.RegionName, filename, m_supportFileExtensionsForTileSave); } + /// + /// Called before processing of every simulation frame. + /// This is used to check to see of any of the terrain is tainted and, if so, schedule + /// updates for all the presences. + /// This also checks to see if there are updates that need to be sent for each presence. + /// This is where the logic is to send terrain updates to clients. + /// + private void EventManager_OnFrame() + { + TerrainData terrData = m_channel.GetTerrainData(); + + bool shouldTaint = false; + for (int x = 0; x < terrData.SizeX; x += Constants.TerrainPatchSize) + { + for (int y = 0; y < terrData.SizeY; y += Constants.TerrainPatchSize) + { + if (terrData.IsTaintedAt(x, y)) + { + // Found a patch that was modified. Push this flag into the clients. + SendToClients(terrData, x, y); + shouldTaint = true; + } + } + } + + // This event also causes changes to be sent to the clients + CheckSendingPatchesToClients(); + + // If things changes, generate some events + if (shouldTaint) + { + m_scene.EventManager.TriggerTerrainTainted(); + m_tainted = true; + } + } + /// /// Performs updates to the region periodically, synchronising physics and other heightmap aware sections + /// Called infrequently (like every 5 seconds or so). Best used for storing terrain. /// private void EventManager_OnTerrainTick() { @@ -644,8 +814,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_scene.PhysicsScene.SetTerrain(m_channel.GetFloatsSerialised()); m_scene.SaveTerrain(); - m_scene.EventManager.TriggerTerrainUpdate(); - // Clients who look at the map will never see changes after they looked at the map, so i've commented this out. //m_scene.CreateTerrainTexture(true); } @@ -687,54 +855,48 @@ namespace OpenSim.Region.CoreModules.World.Terrain } /// - /// Checks to see if the terrain has been modified since last check - /// but won't attempt to limit those changes to the limits specified in the estate settings - /// currently invoked by the command line operations in the region server only + /// Installs terrain brush hook to IClientAPI /// - private void CheckForTerrainUpdates() + /// + private void EventManager_OnClientClosed(UUID client, Scene scene) { - CheckForTerrainUpdates(false); - } - - /// - /// Checks to see if the terrain has been modified since last check. - /// If it has been modified, every all the terrain patches are sent to the client. - /// If the call is asked to respect the estate settings for terrain_raise_limit and - /// terrain_lower_limit, it will clamp terrain updates between these values - /// currently invoked by client_OnModifyTerrain only and not the Commander interfaces - /// should height map deltas be limited to the estate settings limits - /// - private void CheckForTerrainUpdates(bool respectEstateSettings) - { - bool shouldTaint = false; - float[] serialised = m_channel.GetFloatsSerialised(); - int x; - for (x = 0; x < m_channel.Width; x += Constants.TerrainPatchSize) + ScenePresence presence = scene.GetScenePresence(client); + if (presence != null) { - int y; - for (y = 0; y < m_channel.Height; y += Constants.TerrainPatchSize) - { - if (m_channel.Tainted(x, y)) - { - // if we should respect the estate settings then - // fixup and height deltas that don't respect them - if (respectEstateSettings && LimitChannelChanges(x, y)) - { - // this has been vetoed, so update - // what we are going to send to the client - serialised = m_channel.GetFloatsSerialised(); - } + presence.ControllingClient.OnModifyTerrain -= client_OnModifyTerrain; + presence.ControllingClient.OnBakeTerrain -= client_OnBakeTerrain; + presence.ControllingClient.OnLandUndo -= client_OnLandUndo; + presence.ControllingClient.OnUnackedTerrain -= client_OnUnackedTerrain; + } - SendToClients(serialised, x, y); - shouldTaint = true; + lock (m_perClientPatchUpdates) + m_perClientPatchUpdates.Remove(client); + } + + /// + /// Scan over changes in the terrain and limit height changes. This enforces the + /// non-estate owner limits on rate of terrain editting. + /// Returns 'true' if any heights were limited. + /// + private bool EnforceEstateLimits() + { + TerrainData terrData = m_channel.GetTerrainData(); + + bool wasLimited = false; + for (int x = 0; x < terrData.SizeX; x += Constants.TerrainPatchSize) + { + for (int y = 0; y < terrData.SizeY; y += Constants.TerrainPatchSize) + { + if (terrData.IsTaintedAt(x, y, false /* clearOnTest */)) + { + // If we should respect the estate settings then + // fixup and height deltas that don't respect them. + // Note that LimitChannelChanges() modifies the TerrainChannel with the limited height values. + wasLimited |= LimitChannelChanges(terrData, x, y); } } } - if (shouldTaint) - { - m_scene.EventManager.TriggerTerrainTainted(); - m_tainted = true; - } + return wasLimited; } /// @@ -742,11 +904,11 @@ namespace OpenSim.Region.CoreModules.World.Terrain /// are all within the current estate limits /// true if changes were limited, false otherwise /// - private bool LimitChannelChanges(int xStart, int yStart) + private bool LimitChannelChanges(TerrainData terrData, int xStart, int yStart) { bool changesLimited = false; - double minDelta = m_scene.RegionInfo.RegionSettings.TerrainLowerLimit; - double maxDelta = m_scene.RegionInfo.RegionSettings.TerrainRaiseLimit; + float minDelta = (float)m_scene.RegionInfo.RegionSettings.TerrainLowerLimit; + float maxDelta = (float)m_scene.RegionInfo.RegionSettings.TerrainRaiseLimit; // loop through the height map for this patch and compare it against // the revert map @@ -754,19 +916,18 @@ namespace OpenSim.Region.CoreModules.World.Terrain { for (int y = yStart; y < yStart + Constants.TerrainPatchSize; y++) { - - double requestedHeight = m_channel[x, y]; - double bakedHeight = m_revert[x, y]; - double requestedDelta = requestedHeight - bakedHeight; + float requestedHeight = terrData[x, y]; + float bakedHeight = (float)m_revert[x, y]; + float requestedDelta = requestedHeight - bakedHeight; if (requestedDelta > maxDelta) { - m_channel[x, y] = bakedHeight + maxDelta; + terrData[x, y] = bakedHeight + maxDelta; changesLimited = true; } else if (requestedDelta < minDelta) { - m_channel[x, y] = bakedHeight + minDelta; //as lower is a -ve delta + terrData[x, y] = bakedHeight + minDelta; //as lower is a -ve delta changesLimited = true; } } @@ -794,14 +955,154 @@ namespace OpenSim.Region.CoreModules.World.Terrain /// A copy of the terrain as a 1D float array of size w*h /// The patch corner to send /// The patch corner to send - private void SendToClients(float[] serialised, int x, int y) + private void SendToClients(TerrainData terrData, int x, int y) { - m_scene.ForEachClient( - delegate(IClientAPI controller) - { controller.SendLayerData( - x / Constants.TerrainPatchSize, y / Constants.TerrainPatchSize, serialised); + if (m_sendTerrainUpdatesByViewDistance) + { + // Add that this patch needs to be sent to the accounting for each client. + lock (m_perClientPatchUpdates) + { + m_scene.ForEachScenePresence(presence => + { + PatchUpdates thisClientUpdates; + if (!m_perClientPatchUpdates.TryGetValue(presence.UUID, out thisClientUpdates)) + { + // There is a ScenePresence without a send patch map. Create one. + thisClientUpdates = new PatchUpdates(terrData, presence); + m_perClientPatchUpdates.Add(presence.UUID, thisClientUpdates); + } + thisClientUpdates.SetByXY(x, y, true); + } + ); + } + } + else + { + // Legacy update sending where the update is sent out as soon as noticed + // We know the actual terrain data passed is ignored. This kludge saves changing IClientAPI. + //float[] heightMap = terrData.GetFloatsSerialized(); + float[] heightMap = new float[10]; + m_scene.ForEachClient( + delegate(IClientAPI controller) + { + controller.SendLayerData(x / Constants.TerrainPatchSize, + y / Constants.TerrainPatchSize, + heightMap); } - ); + ); + } + } + + private class PatchesToSend : IComparable + { + public int PatchX; + public int PatchY; + public float Dist; + public PatchesToSend(int pX, int pY, float pDist) + { + PatchX = pX; + PatchY = pY; + Dist = pDist; + } + public int CompareTo(PatchesToSend other) + { + return Dist.CompareTo(other.Dist); + } + } + + // Called each frame time to see if there are any patches to send to any of the + // ScenePresences. + // Loop through all the per-client info and send any patches necessary. + private void CheckSendingPatchesToClients() + { + lock (m_perClientPatchUpdates) + { + foreach (PatchUpdates pups in m_perClientPatchUpdates.Values) + { + if (pups.HasUpdates()) + { + // There is something that could be sent to this client. + List toSend = GetModifiedPatchesInViewDistance(pups); + if (toSend.Count > 0) + { + // m_log.DebugFormat("{0} CheckSendingPatchesToClient: sending {1} patches to {2} in region {3}", + // LogHeader, toSend.Count, pups.Presence.Name, m_scene.RegionInfo.RegionName); + // Sort the patches to send by the distance from the presence + toSend.Sort(); + /* + foreach (PatchesToSend pts in toSend) + { + pups.Presence.ControllingClient.SendLayerData(pts.PatchX, pts.PatchY, null); + // presence.ControllingClient.SendLayerData(xs.ToArray(), ys.ToArray(), null, TerrainPatch.LayerType.Land); + } + */ + + int[] xPieces = new int[toSend.Count]; + int[] yPieces = new int[toSend.Count]; + float[] patchPieces = new float[toSend.Count * 2]; + int pieceIndex = 0; + foreach (PatchesToSend pts in toSend) + { + patchPieces[pieceIndex++] = pts.PatchX; + patchPieces[pieceIndex++] = pts.PatchY; + } + pups.Presence.ControllingClient.SendLayerData(-toSend.Count, 0, patchPieces); + } + } + } + } + } + + private List GetModifiedPatchesInViewDistance(PatchUpdates pups) + { + List ret = new List(); + + ScenePresence presence = pups.Presence; + if (presence == null) + return ret; + + // Compute the area of patches within our draw distance + int startX = (((int) (presence.AbsolutePosition.X - presence.DrawDistance))/Constants.TerrainPatchSize) - 2; + startX = Math.Max(startX, 0); + startX = Math.Min(startX, (int)m_scene.RegionInfo.RegionSizeX/Constants.TerrainPatchSize); + int startY = (((int) (presence.AbsolutePosition.Y - presence.DrawDistance))/Constants.TerrainPatchSize) - 2; + startY = Math.Max(startY, 0); + startY = Math.Min(startY, (int)m_scene.RegionInfo.RegionSizeY/Constants.TerrainPatchSize); + int endX = (((int) (presence.AbsolutePosition.X + presence.DrawDistance))/Constants.TerrainPatchSize) + 2; + endX = Math.Max(endX, 0); + endX = Math.Min(endX, (int)m_scene.RegionInfo.RegionSizeX/Constants.TerrainPatchSize); + int endY = (((int) (presence.AbsolutePosition.Y + presence.DrawDistance))/Constants.TerrainPatchSize) + 2; + endY = Math.Max(endY, 0); + endY = Math.Min(endY, (int)m_scene.RegionInfo.RegionSizeY/Constants.TerrainPatchSize); + // m_log.DebugFormat("{0} GetModifiedPatchesInViewDistance. rName={1}, ddist={2}, apos={3}, start=<{4},{5}>, end=<{6},{7}>", + // LogHeader, m_scene.RegionInfo.RegionName, + // presence.DrawDistance, presence.AbsolutePosition, + // startX, startY, endX, endY); + for (int x = startX; x < endX; x++) + { + for (int y = startY; y < endY; y++) + { + //Need to make sure we don't send the same ones over and over + Vector3 presencePos = presence.AbsolutePosition; + Vector3 patchPos = new Vector3(x * Constants.TerrainPatchSize, y * Constants.TerrainPatchSize, presencePos.Z); + if (pups.GetByPatch(x, y)) + { + //Check which has less distance, camera or avatar position, both have to be done. + //Its not a radius, its a diameter and we add 50 so that it doesn't look like it cuts off + if (Util.DistanceLessThan(presencePos, patchPos, presence.DrawDistance + 50) + || Util.DistanceLessThan(presence.CameraPosition, patchPos, presence.DrawDistance + 50)) + { + //They can see it, send it to them + pups.SetByPatch(x, y, false); + float dist = Vector3.DistanceSquared(presencePos, patchPos); + ret.Add(new PatchesToSend(x, y, dist)); + //Wait and send them all at once + // pups.client.SendLayerData(x, y, null); + } + } + } + } + return ret; } private void client_OnModifyTerrain(UUID user, float height, float seconds, byte size, byte action, @@ -846,7 +1147,9 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_painteffects[(StandardTerrainEffects) action].PaintEffect( m_channel, allowMask, west, south, height, size, seconds); - CheckForTerrainUpdates(!god); //revert changes outside estate limits + //revert changes outside estate limits + if (!god) + EnforceEstateLimits(); } } else @@ -884,10 +1187,11 @@ namespace OpenSim.Region.CoreModules.World.Terrain if (allowed) { StoreUndoState(); - m_floodeffects[(StandardTerrainEffects) action].FloodEffect( - m_channel, fillArea, size); + m_floodeffects[(StandardTerrainEffects) action].FloodEffect(m_channel, fillArea, size); - CheckForTerrainUpdates(!god); //revert changes outside estate limits + //revert changes outside estate limits + if (!god) + EnforceEstateLimits(); } } else @@ -911,7 +1215,9 @@ namespace OpenSim.Region.CoreModules.World.Terrain protected void client_OnUnackedTerrain(IClientAPI client, int patchX, int patchY) { //m_log.Debug("Terrain packet unacked, resending patch: " + patchX + " , " + patchY); - client.SendLayerData(patchX, patchY, m_scene.Heightmap.GetFloatsSerialised()); + // SendLayerData does not use the heightmap parameter. This kludge is so as to not change IClientAPI. + float[] heightMap = new float[10]; + client.SendLayerData(patchX, patchY, heightMap); } private void StoreUndoState() @@ -938,7 +1244,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain private void InterfaceLoadFile(Object[] args) { LoadFromFile((string) args[0]); - CheckForTerrainUpdates(); } private void InterfaceLoadTileFile(Object[] args) @@ -948,7 +1253,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain (int) args[2], (int) args[3], (int) args[4]); - CheckForTerrainUpdates(); } private void InterfaceSaveFile(Object[] args) @@ -977,7 +1281,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain for (y = 0; y < m_channel.Height; y++) m_channel[x, y] = m_revert[x, y]; - CheckForTerrainUpdates(); } private void InterfaceFlipTerrain(Object[] args) @@ -986,28 +1289,28 @@ namespace OpenSim.Region.CoreModules.World.Terrain if (direction.ToLower().StartsWith("y")) { - for (int x = 0; x < Constants.RegionSize; x++) + for (int x = 0; x < m_channel.Width; x++) { - for (int y = 0; y < Constants.RegionSize / 2; y++) + for (int y = 0; y < m_channel.Height / 2; y++) { double height = m_channel[x, y]; - double flippedHeight = m_channel[x, (int)Constants.RegionSize - 1 - y]; + double flippedHeight = m_channel[x, (int)m_channel.Height - 1 - y]; m_channel[x, y] = flippedHeight; - m_channel[x, (int)Constants.RegionSize - 1 - y] = height; + m_channel[x, (int)m_channel.Height - 1 - y] = height; } } } else if (direction.ToLower().StartsWith("x")) { - for (int y = 0; y < Constants.RegionSize; y++) + for (int y = 0; y < m_channel.Height; y++) { - for (int x = 0; x < Constants.RegionSize / 2; x++) + for (int x = 0; x < m_channel.Width / 2; x++) { double height = m_channel[x, y]; - double flippedHeight = m_channel[(int)Constants.RegionSize - 1 - x, y]; + double flippedHeight = m_channel[(int)m_channel.Width - 1 - x, y]; m_channel[x, y] = flippedHeight; - m_channel[(int)Constants.RegionSize - 1 - x, y] = height; + m_channel[(int)m_channel.Width - 1 - x, y] = height; } } @@ -1016,9 +1319,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain { m_log.Error("Unrecognised direction - need x or y"); } - - - CheckForTerrainUpdates(); } private void InterfaceRescaleTerrain(Object[] args) @@ -1076,7 +1376,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain } } - CheckForTerrainUpdates(); } } @@ -1087,7 +1386,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain for (x = 0; x < m_channel.Width; x++) for (y = 0; y < m_channel.Height; y++) m_channel[x, y] += (double) args[0]; - CheckForTerrainUpdates(); } private void InterfaceMultiplyTerrain(Object[] args) @@ -1096,7 +1394,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain for (x = 0; x < m_channel.Width; x++) for (y = 0; y < m_channel.Height; y++) m_channel[x, y] *= (double) args[0]; - CheckForTerrainUpdates(); } private void InterfaceLowerTerrain(Object[] args) @@ -1105,17 +1402,15 @@ namespace OpenSim.Region.CoreModules.World.Terrain for (x = 0; x < m_channel.Width; x++) for (y = 0; y < m_channel.Height; y++) m_channel[x, y] -= (double) args[0]; - CheckForTerrainUpdates(); } - private void InterfaceFillTerrain(Object[] args) + public void InterfaceFillTerrain(Object[] args) { int x, y; for (x = 0; x < m_channel.Width; x++) for (y = 0; y < m_channel.Height; y++) m_channel[x, y] = (double) args[0]; - CheckForTerrainUpdates(); } private void InterfaceMinTerrain(Object[] args) @@ -1128,7 +1423,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_channel[x, y] = Math.Max((double)args[0], m_channel[x, y]); } } - CheckForTerrainUpdates(); } private void InterfaceMaxTerrain(Object[] args) @@ -1141,7 +1435,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain m_channel[x, y] = Math.Min((double)args[0], m_channel[x, y]); } } - CheckForTerrainUpdates(); } private void InterfaceShowDebugStats(Object[] args) @@ -1204,7 +1497,6 @@ namespace OpenSim.Region.CoreModules.World.Terrain if (m_plugineffects.ContainsKey(firstArg)) { m_plugineffects[firstArg].RunEffect(m_channel); - CheckForTerrainUpdates(); } else { diff --git a/OpenSim/Region/Framework/Interfaces/ISimulationDataService.cs b/OpenSim/Region/Framework/Interfaces/ISimulationDataService.cs index 3e97a7ae3d..13358cb360 100644 --- a/OpenSim/Region/Framework/Interfaces/ISimulationDataService.cs +++ b/OpenSim/Region/Framework/Interfaces/ISimulationDataService.cs @@ -68,13 +68,22 @@ namespace OpenSim.Region.Framework.Interfaces /// /// HeightField data /// region UUID + void StoreTerrain(TerrainData terrain, UUID regionID); + + // Legacy version kept for downward compabibility void StoreTerrain(double[,] terrain, UUID regionID); /// /// Load the latest terrain revision from region storage /// /// the region UUID + /// the X dimension of the region being filled + /// the Y dimension of the region being filled + /// the Z dimension of the region being filled /// Heightfield data + TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ); + + // Legacy version kept for downward compabibility double[,] LoadTerrain(UUID regionID); void StoreLandObject(ILandObject Parcel); diff --git a/OpenSim/Region/Framework/Interfaces/ISimulationDataStore.cs b/OpenSim/Region/Framework/Interfaces/ISimulationDataStore.cs index 17bd48be67..e09f775642 100644 --- a/OpenSim/Region/Framework/Interfaces/ISimulationDataStore.cs +++ b/OpenSim/Region/Framework/Interfaces/ISimulationDataStore.cs @@ -79,13 +79,22 @@ namespace OpenSim.Region.Framework.Interfaces /// /// HeightField data /// region UUID + void StoreTerrain(TerrainData terrain, UUID regionID); + + // Legacy version kept for downward compabibility void StoreTerrain(double[,] terrain, UUID regionID); /// /// Load the latest terrain revision from region storage /// /// the region UUID + /// the X dimension of the terrain being filled + /// the Y dimension of the terrain being filled + /// the Z dimension of the terrain being filled /// Heightfield data + TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ); + + // Legacy version kept for downward compabibility double[,] LoadTerrain(UUID regionID); void StoreLandObject(ILandObject Parcel); @@ -136,4 +145,5 @@ namespace OpenSim.Region.Framework.Interfaces void Shutdown(); } + } diff --git a/OpenSim/Region/Framework/Interfaces/ITerrainChannel.cs b/OpenSim/Region/Framework/Interfaces/ITerrainChannel.cs index e467701351..f660b8dbe0 100644 --- a/OpenSim/Region/Framework/Interfaces/ITerrainChannel.cs +++ b/OpenSim/Region/Framework/Interfaces/ITerrainChannel.cs @@ -25,13 +25,23 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +using OpenSim.Framework; +using OpenMetaverse; + namespace OpenSim.Region.Framework.Interfaces { public interface ITerrainChannel { - int Height { get; } + int Width { get;} // X dimension + int Height { get;} // Y dimension + int Altitude { get;} // Z dimension + double this[int x, int y] { get; set; } - int Width { get; } + + float GetHeightAtXYZ(float x, float y, float z); + + // Return the packaged terrain data for passing into lower levels of communication + TerrainData GetTerrainData(); /// /// Squash the entire heightmap into a single dimensioned array @@ -40,9 +50,14 @@ namespace OpenSim.Region.Framework.Interfaces float[] GetFloatsSerialised(); double[,] GetDoubles(); + + // Check if a location has been updated. Clears the taint flag as a side effect. bool Tainted(int x, int y); + ITerrainChannel MakeCopy(); string SaveToXmlString(); void LoadFromXmlString(string data); + // Merge some terrain into this channel + void Merge(ITerrainChannel newTerrain, Vector3 displacement, float radianRotation, Vector2 rotationDisplacement); } } diff --git a/OpenSim/Region/Framework/Scenes/Scene.cs b/OpenSim/Region/Framework/Scenes/Scene.cs index 03270d7db0..f5458c1292 100644 --- a/OpenSim/Region/Framework/Scenes/Scene.cs +++ b/OpenSim/Region/Framework/Scenes/Scene.cs @@ -1935,7 +1935,7 @@ namespace OpenSim.Region.Framework.Scenes { try { - double[,] map = SimulationDataService.LoadTerrain(RegionInfo.RegionID); + TerrainData map = SimulationDataService.LoadTerrain(RegionInfo.RegionID, (int)RegionInfo.RegionSizeX, (int)RegionInfo.RegionSizeY, (int)RegionInfo.RegionSizeZ); if (map == null) { // This should be in the Terrain module, but it isn't because @@ -1946,7 +1946,7 @@ namespace OpenSim.Region.Framework.Scenes m_InitialTerrain = terrainConfig.GetString("InitialTerrain", m_InitialTerrain); m_log.InfoFormat("[TERRAIN]: No default terrain. Generating a new terrain {0}.", m_InitialTerrain); - Heightmap = new TerrainChannel(m_InitialTerrain); + Heightmap = new TerrainChannel(m_InitialTerrain, (int)RegionInfo.RegionSizeX, (int)RegionInfo.RegionSizeY, (int)RegionInfo.RegionSizeZ); SimulationDataService.StoreTerrain(Heightmap.GetDoubles(), RegionInfo.RegionID); } diff --git a/OpenSim/Region/Framework/Scenes/TerrainChannel.cs b/OpenSim/Region/Framework/Scenes/TerrainChannel.cs index b6e0a97fce..3d563a6523 100644 --- a/OpenSim/Region/Framework/Scenes/TerrainChannel.cs +++ b/OpenSim/Region/Framework/Scenes/TerrainChannel.cs @@ -25,13 +25,20 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +using System; +using System.IO; +using System.Text; +using System.Reflection; +using System.Xml; +using System.Xml.Serialization; + +using OpenSim.Data; using OpenSim.Framework; using OpenSim.Region.Framework.Interfaces; -using System; -using System.Text; -using System.Xml; -using System.IO; -using System.Xml.Serialization; + +using OpenMetaverse; + +using log4net; namespace OpenSim.Region.Framework.Scenes { @@ -40,140 +47,136 @@ namespace OpenSim.Region.Framework.Scenes /// public class TerrainChannel : ITerrainChannel { - private readonly bool[,] taint; - private double[,] map; + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static string LogHeader = "[TERRAIN CHANNEL]"; + protected TerrainData m_terrainData; + + public int Width { get { return m_terrainData.SizeX; } } // X dimension + // Unfortunately, for historical reasons, in this module 'Width' is X and 'Height' is Y + public int Height { get { return m_terrainData.SizeY; } } // Y dimension + public int Altitude { get { return m_terrainData.SizeZ; } } // Y dimension + + // Default, not-often-used builder public TerrainChannel() { - map = new double[Constants.RegionSize, Constants.RegionSize]; - taint = new bool[Constants.RegionSize / 16, Constants.RegionSize / 16]; - - PinHeadIsland(); + m_terrainData = new HeightmapTerrainData((int)Constants.RegionSize, (int)Constants.RegionSize, (int)Constants.RegionHeight); + FlatLand(); + // PinHeadIsland(); } - public TerrainChannel(String type) + // Create terrain of given size + public TerrainChannel(int pX, int pY) { - map = new double[Constants.RegionSize, Constants.RegionSize]; - taint = new bool[Constants.RegionSize / 16, Constants.RegionSize / 16]; + m_terrainData = new HeightmapTerrainData(pX, pY, (int)Constants.RegionHeight); + } + // Create terrain of specified size and initialize with specified terrain. + // TODO: join this with the terrain initializers. + public TerrainChannel(String type, int pX, int pY, int pZ) + { + m_terrainData = new HeightmapTerrainData(pX, pY, pZ); if (type.Equals("flat")) FlatLand(); else PinHeadIsland(); } - public TerrainChannel(double[,] import) + // Create channel passed a heightmap and expected dimensions of the region. + // The heightmap might not fit the passed size so accomodations must be made. + public TerrainChannel(double[,] pM, int pSizeX, int pSizeY, int pAltitude) { - map = import; - taint = new bool[import.GetLength(0),import.GetLength(1)]; + int hmSizeX = pM.GetLength(0); + int hmSizeY = pM.GetLength(1); + + m_terrainData = new HeightmapTerrainData(pSizeX, pSizeY, pAltitude); + + for (int xx = 0; xx < pSizeX; xx++) + for (int yy = 0; yy < pSizeY; yy++) + if (xx > hmSizeX || yy > hmSizeY) + m_terrainData[xx, yy] = TerrainData.DefaultTerrainHeight; + else + m_terrainData[xx, yy] = (float)pM[xx, yy]; } - public TerrainChannel(bool createMap) + public TerrainChannel(TerrainData pTerrData) { - if (createMap) - { - map = new double[Constants.RegionSize,Constants.RegionSize]; - taint = new bool[Constants.RegionSize / 16,Constants.RegionSize / 16]; - } - } - - public TerrainChannel(int w, int h) - { - map = new double[w,h]; - taint = new bool[w / 16,h / 16]; + m_terrainData = pTerrData; } #region ITerrainChannel Members - public int Width - { - get { return map.GetLength(0); } - } - - public int Height - { - get { return map.GetLength(1); } - } - + // ITerrainChannel.MakeCopy() public ITerrainChannel MakeCopy() { - TerrainChannel copy = new TerrainChannel(false); - copy.map = (double[,]) map.Clone(); - - return copy; + return this.Copy(); } + // ITerrainChannel.GetTerrainData() + public TerrainData GetTerrainData() + { + return m_terrainData; + } + + // ITerrainChannel.GetFloatsSerialized() + // This one dimensional version is ordered so height = map[y*sizeX+x]; + // DEPRECATED: don't use this function as it does not retain the dimensions of the terrain + // and the caller will probably do the wrong thing if the terrain is not the legacy 256x256. public float[] GetFloatsSerialised() { - // Move the member variables into local variables, calling - // member variables 256*256 times gets expensive - int w = Width; - int h = Height; - float[] heights = new float[w * h]; + return m_terrainData.GetFloatsSerialized(); + } + + // ITerrainChannel.GetDoubles() + public double[,] GetDoubles() + { + double[,] heights = new double[Width, Height]; - int i, j; // map coordinates int idx = 0; // index into serialized array - for (i = 0; i < h; i++) + for (int ii = 0; ii < Width; ii++) { - for (j = 0; j < w; j++) + for (int jj = 0; jj < Height; jj++) { - heights[idx++] = (float)map[j, i]; + heights[ii, jj] = (double)m_terrainData[ii, jj]; + idx++; } } return heights; } - public double[,] GetDoubles() - { - return map; - } - + // ITerrainChannel.this[x,y] public double this[int x, int y] { - get - { - if (x < 0) x = 0; - if (y < 0) y = 0; - if (x >= (int)Constants.RegionSize) x = (int)Constants.RegionSize - 1; - if (y >= (int)Constants.RegionSize) y = (int)Constants.RegionSize - 1; - - return map[x, y]; + get { + if (x < 0 || x >= Width || y < 0 || y >= Height) + return 0; + return (double)m_terrainData[x, y]; } set { - // Will "fix" terrain hole problems. Although not fantastically. if (Double.IsNaN(value) || Double.IsInfinity(value)) return; - if (map[x, y] != value) - { - taint[x / 16, y / 16] = true; - map[x, y] = value; - } + m_terrainData[x, y] = (float)value; } } + // ITerrainChannel.GetHieghtAtXYZ(x, y, z) + public float GetHeightAtXYZ(float x, float y, float z) + { + if (x < 0 || x >= Width || y < 0 || y >= Height) + return 0; + return m_terrainData[(int)x, (int)y]; + } + + // ITerrainChannel.Tainted() public bool Tainted(int x, int y) { - if (taint[x / 16, y / 16]) - { - taint[x / 16, y / 16] = false; - return true; - } - return false; - } - - #endregion - - public TerrainChannel Copy() - { - TerrainChannel copy = new TerrainChannel(false); - copy.map = (double[,]) map.Clone(); - - return copy; + return m_terrainData.IsTaintedAt(x, y); } + // ITerrainChannel.SaveToXmlString() public string SaveToXmlString() { XmlWriterSettings settings = new XmlWriterSettings(); @@ -189,13 +192,7 @@ namespace OpenSim.Region.Framework.Scenes } } - private void WriteXml(XmlWriter writer) - { - writer.WriteStartElement(String.Empty, "TerrainMap", String.Empty); - ToXml(writer); - writer.WriteEndElement(); - } - + // ITerrainChannel.LoadFromXmlString() public void LoadFromXmlString(string data) { StringReader sr = new StringReader(data); @@ -207,12 +204,124 @@ namespace OpenSim.Region.Framework.Scenes sr.Close(); } - private void ReadXml(XmlReader reader) + // ITerrainChannel.Merge + public void Merge(ITerrainChannel newTerrain, Vector3 displacement, float radianRotation, Vector2 rotationDisplacement) { - reader.ReadStartElement("TerrainMap"); - FromXml(reader); + m_log.DebugFormat("{0} Merge. inSize=<{1},{2}>, disp={3}, rot={4}, rotDisp={5}, outSize=<{6},{7}>", LogHeader, + newTerrain.Width, newTerrain.Height, + displacement, radianRotation, rotationDisplacement, + m_terrainData.SizeX, m_terrainData.SizeY); + for (int xx = 0; xx < newTerrain.Width; xx++) + { + for (int yy = 0; yy < newTerrain.Height; yy++) + { + int dispX = (int)displacement.X; + int dispY = (int)displacement.Y; + float newHeight = (float)newTerrain[xx, yy] + displacement.Z; + if (radianRotation == 0) + { + // If no rotation, place the new height in the specified location + dispX += xx; + dispY += yy; + if (dispX >= 0 && dispX < m_terrainData.SizeX && dispY >= 0 && dispY < m_terrainData.SizeY) + { + m_terrainData[dispX, dispY] = newHeight; + } + } + else + { + // If rotating, we have to smooth the result because the conversion + // to ints will mean heightmap entries will not get changed + // First compute the rotation location for the new height. + dispX += (int)(rotationDisplacement.X + + ((float)xx - rotationDisplacement.X) * Math.Cos(radianRotation) + - ((float)yy - rotationDisplacement.Y) * Math.Sin(radianRotation) ); + + dispY += (int)(rotationDisplacement.Y + + ((float)xx - rotationDisplacement.X) * Math.Sin(radianRotation) + + ((float)yy - rotationDisplacement.Y) * Math.Cos(radianRotation) ); + + if (dispX >= 0 && dispX < m_terrainData.SizeX && dispY >= 0 && dispY < m_terrainData.SizeY) + { + float oldHeight = m_terrainData[dispX, dispY]; + // Smooth the heights around this location if the old height is far from this one + for (int sxx = dispX - 2; sxx < dispX + 2; sxx++) + { + for (int syy = dispY - 2; syy < dispY + 2; syy++) + { + if (sxx >= 0 && sxx < m_terrainData.SizeX && syy >= 0 && syy < m_terrainData.SizeY) + { + if (sxx == dispX && syy == dispY) + { + // Set height for the exact rotated point + m_terrainData[dispX, dispY] = newHeight; + } + else + { + if (Math.Abs(m_terrainData[sxx, syy] - newHeight) > 1f) + { + // If the adjacent height is far off, force it to this height + m_terrainData[sxx, syy] = newHeight; + } + } + } + } + } + } + + if (dispX >= 0 && dispX < m_terrainData.SizeX && dispY >= 0 && dispY < m_terrainData.SizeY) + { + m_terrainData[dispX, dispY] = (float)newTerrain[xx, yy]; + } + } + } + } } + #endregion + + public TerrainChannel Copy() + { + TerrainChannel copy = new TerrainChannel(); + copy.m_terrainData = m_terrainData.Clone(); + return copy; + } + + private void WriteXml(XmlWriter writer) + { + if (Width == Constants.RegionSize && Height == Constants.RegionSize) + { + // Downward compatibility for legacy region terrain maps. + // If region is exactly legacy size, return the old format XML. + writer.WriteStartElement(String.Empty, "TerrainMap", String.Empty); + ToXml(writer); + writer.WriteEndElement(); + } + else + { + // New format XML that includes width and length. + writer.WriteStartElement(String.Empty, "TerrainMap2", String.Empty); + ToXml2(writer); + writer.WriteEndElement(); + } + } + + private void ReadXml(XmlReader reader) + { + // Check the first element. If legacy element, use the legacy reader. + if (reader.IsStartElement("TerrainMap")) + { + reader.ReadStartElement("TerrainMap"); + FromXml(reader); + } + else + { + reader.ReadStartElement("TerrainMap2"); + FromXml2(reader); + } + } + + // Write legacy terrain map. Presumed to be 256x256 of data encoded as floats in a byte array. private void ToXml(XmlWriter xmlWriter) { float[] mapData = GetFloatsSerialised(); @@ -226,12 +335,15 @@ namespace OpenSim.Region.Framework.Scenes serializer.Serialize(xmlWriter, buffer); } + // Read legacy terrain map. Presumed to be 256x256 of data encoded as floats in a byte array. private void FromXml(XmlReader xmlReader) { XmlSerializer serializer = new XmlSerializer(typeof(byte[])); byte[] dataArray = (byte[])serializer.Deserialize(xmlReader); int index = 0; + m_terrainData = new HeightmapTerrainData(Height, Width, (int)Constants.RegionHeight); + for (int y = 0; y < Height; y++) { for (int x = 0; x < Width; x++) @@ -244,35 +356,63 @@ namespace OpenSim.Region.Framework.Scenes } } + private class TerrainChannelXMLPackage + { + public int Version; + public int SizeX; + public int SizeY; + public int SizeZ; + public float CompressionFactor; + public int[] Map; + public TerrainChannelXMLPackage(int pX, int pY, int pZ, float pCompressionFactor, int[] pMap) + { + Version = 1; + SizeX = pX; + SizeY = pY; + SizeZ = pZ; + CompressionFactor = pCompressionFactor; + Map = pMap; + } + } + + // New terrain serialization format that includes the width and length. + private void ToXml2(XmlWriter xmlWriter) + { + TerrainChannelXMLPackage package = new TerrainChannelXMLPackage(Width, Height, Altitude, m_terrainData.CompressionFactor, + m_terrainData.GetCompressedMap()); + XmlSerializer serializer = new XmlSerializer(typeof(TerrainChannelXMLPackage)); + serializer.Serialize(xmlWriter, package); + } + + // New terrain serialization format that includes the width and length. + private void FromXml2(XmlReader xmlReader) + { + XmlSerializer serializer = new XmlSerializer(typeof(TerrainChannelXMLPackage)); + TerrainChannelXMLPackage package = (TerrainChannelXMLPackage)serializer.Deserialize(xmlReader); + m_terrainData = new HeightmapTerrainData(package.Map, package.CompressionFactor, package.SizeX, package.SizeY, package.SizeZ); + } + + // Fill the heightmap with the center bump terrain private void PinHeadIsland() { - int x; - for (x = 0; x < Constants.RegionSize; x++) + for (int x = 0; x < Width; x++) { - int y; - for (y = 0; y < Constants.RegionSize; y++) + for (int y = 0; y < Height; y++) { - map[x, y] = TerrainUtil.PerlinNoise2D(x, y, 2, 0.125) * 10; - double spherFacA = TerrainUtil.SphericalFactor(x, y, Constants.RegionSize / 2.0, Constants.RegionSize / 2.0, 50) * 0.01; - double spherFacB = TerrainUtil.SphericalFactor(x, y, Constants.RegionSize / 2.0, Constants.RegionSize / 2.0, 100) * 0.001; - if (map[x, y] < spherFacA) - map[x, y] = spherFacA; - if (map[x, y] < spherFacB) - map[x, y] = spherFacB; + m_terrainData[x, y] = (float)TerrainUtil.PerlinNoise2D(x, y, 2, 0.125) * 10; + float spherFacA = (float)(TerrainUtil.SphericalFactor(x, y, m_terrainData.SizeX / 2.0, m_terrainData.SizeY / 2.0, 50) * 0.01d); + float spherFacB = (float)(TerrainUtil.SphericalFactor(x, y, m_terrainData.SizeX / 2.0, m_terrainData.SizeY / 2.0, 100) * 0.001d); + if (m_terrainData[x, y]< spherFacA) + m_terrainData[x, y]= spherFacA; + if (m_terrainData[x, y]< spherFacB) + m_terrainData[x, y] = spherFacB; } } } private void FlatLand() { - int x; - for (x = 0; x < Constants.RegionSize; x++) - { - int y; - for (y = 0; y < Constants.RegionSize; y++) - map[x, y] = 21; - } + m_terrainData.ClearLand(); } - } } diff --git a/OpenSim/Services/Connectors/Simulation/SimulationDataService.cs b/OpenSim/Services/Connectors/Simulation/SimulationDataService.cs index 96c02d90fc..475983816c 100644 --- a/OpenSim/Services/Connectors/Simulation/SimulationDataService.cs +++ b/OpenSim/Services/Connectors/Simulation/SimulationDataService.cs @@ -100,6 +100,11 @@ namespace OpenSim.Services.Connectors return m_database.LoadObjects(regionUUID); } + public void StoreTerrain(TerrainData terrain, UUID regionID) + { + m_database.StoreTerrain(terrain, regionID); + } + public void StoreTerrain(double[,] terrain, UUID regionID) { m_database.StoreTerrain(terrain, regionID); @@ -110,6 +115,11 @@ namespace OpenSim.Services.Connectors return m_database.LoadTerrain(regionID); } + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + return m_database.LoadTerrain(regionID, pSizeX, pSizeY, pSizeZ); + } + public void StoreLandObject(ILandObject Parcel) { m_database.StoreLandObject(Parcel); diff --git a/OpenSim/Tests/Common/Mock/MockRegionDataPlugin.cs b/OpenSim/Tests/Common/Mock/MockRegionDataPlugin.cs index 5c1ec0bebd..3ab90207a5 100644 --- a/OpenSim/Tests/Common/Mock/MockRegionDataPlugin.cs +++ b/OpenSim/Tests/Common/Mock/MockRegionDataPlugin.cs @@ -69,11 +69,21 @@ namespace OpenSim.Data.Null m_store.StoreTerrain(terrain, regionID); } + public void StoreTerrain(TerrainData terrain, UUID regionID) + { + m_store.StoreTerrain(terrain, regionID); + } + public double[,] LoadTerrain(UUID regionID) { return m_store.LoadTerrain(regionID); } + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + return m_store.LoadTerrain(regionID, pSizeX, pSizeY, pSizeZ); + } + public void StoreLandObject(ILandObject Parcel) { m_store.StoreLandObject(Parcel); @@ -159,7 +169,7 @@ namespace OpenSim.Data.Null protected Dictionary m_sceneObjectParts = new Dictionary(); protected Dictionary> m_primItems = new Dictionary>(); - protected Dictionary m_terrains = new Dictionary(); + protected Dictionary m_terrains = new Dictionary(); protected Dictionary m_landData = new Dictionary(); public void Initialise(string dbfile) @@ -304,15 +314,28 @@ namespace OpenSim.Data.Null return new List(objects.Values); } - public void StoreTerrain(double[,] ter, UUID regionID) + public void StoreTerrain(TerrainData ter, UUID regionID) { m_terrains[regionID] = ter; } + public void StoreTerrain(double[,] ter, UUID regionID) + { + m_terrains[regionID] = new HeightmapTerrainData(ter); + } + + public TerrainData LoadTerrain(UUID regionID, int pSizeX, int pSizeY, int pSizeZ) + { + if (m_terrains.ContainsKey(regionID)) + return m_terrains[regionID]; + else + return null; + } + public double[,] LoadTerrain(UUID regionID) { if (m_terrains.ContainsKey(regionID)) - return m_terrains[regionID]; + return m_terrains[regionID].GetDoubles(); else return null; }