From 8f9a726465c5bb7528d6ae7d74f20818d4ee3094 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Thu, 30 May 2013 19:27:20 +0100 Subject: [PATCH 01/18] If on a sit request we sit the avatar on a different prim in a linkset for some reason (e.g. because it has a sit target), then send the actual sit prim UUID to the viewer rather than the requested one. This purports to fix the issue described in http://opensimulator.org/mantis/view.php?id=6653 where the camera can end up following the requested sit prim rather than the actual. The original spot was by Vegaslon, this commit just goes about it in a slightly different way This commit also makes m_requestedSitTargetUUID to be the actual UUID, which is consistent with m_requestedSitTargetID which was already doing this. However, this adjustment has no practical effect since we only currently need to know that there's any requested sit UUID at all, not which one it is. --- OpenSim/Region/Framework/Scenes/ScenePresence.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenSim/Region/Framework/Scenes/ScenePresence.cs b/OpenSim/Region/Framework/Scenes/ScenePresence.cs index ab7fd5b61b..e8aa52e07f 100644 --- a/OpenSim/Region/Framework/Scenes/ScenePresence.cs +++ b/OpenSim/Region/Framework/Scenes/ScenePresence.cs @@ -2138,9 +2138,9 @@ namespace OpenSim.Region.Framework.Scenes forceMouselook = part.GetForceMouselook(); ControllingClient.SendSitResponse( - targetID, offset, sitOrientation, false, cameraAtOffset, cameraEyeOffset, forceMouselook); + part.UUID, offset, sitOrientation, false, cameraAtOffset, cameraEyeOffset, forceMouselook); - m_requestedSitTargetUUID = targetID; + m_requestedSitTargetUUID = part.UUID; HandleAgentSit(ControllingClient, UUID); @@ -2165,7 +2165,7 @@ namespace OpenSim.Region.Framework.Scenes if (part != null) { m_requestedSitTargetID = part.LocalId; - m_requestedSitTargetUUID = targetID; + m_requestedSitTargetUUID = part.UUID; // m_log.DebugFormat("[SIT]: Client requested Sit Position: {0}", offset); From 12a3b855619735b2e36a67ab99027029c8b57260 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Thu, 30 May 2013 22:20:02 +0100 Subject: [PATCH 02/18] Fix passing of voice distance attenuation to the Vivox voice server. Because of a typo, this wasn't being done at all - now the 'default' value as described in OpenSimDefaults.ini of 10m is passed (vivox_channel_clamping_distance) Thanks to Ai Austin for spotting this. --- .../OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs index db4869d022..2d65530afe 100644 --- a/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs +++ b/OpenSim/Region/OptionalModules/Avatar/Voice/VivoxVoice/VivoxVoiceModule.cs @@ -836,7 +836,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.Voice.VivoxVoice requrl = String.Format("{0}&chan_roll_off={1}", requrl, m_vivoxChannelRollOff); requrl = String.Format("{0}&chan_dist_model={1}", requrl, m_vivoxChannelDistanceModel); requrl = String.Format("{0}&chan_max_range={1}", requrl, m_vivoxChannelMaximumRange); - requrl = String.Format("{0}&chan_ckamping_distance={1}", requrl, m_vivoxChannelClampingDistance); + requrl = String.Format("{0}&chan_clamping_distance={1}", requrl, m_vivoxChannelClampingDistance); XmlElement resp = VivoxCall(requrl, true); if (XmlFind(resp, "response.level0.body.chan_uri", out channelUri)) From 6b88a665d3a3196288b5aa16eb23f7673c2433ac Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Thu, 30 May 2013 22:43:52 +0100 Subject: [PATCH 03/18] minor: fix warnings in GodsModule that were due to duplicate using statements --- .../CoreModules/Avatar/Gods/GodsModule.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/OpenSim/Region/CoreModules/Avatar/Gods/GodsModule.cs b/OpenSim/Region/CoreModules/Avatar/Gods/GodsModule.cs index 16673ec448..a542d626ed 100644 --- a/OpenSim/Region/CoreModules/Avatar/Gods/GodsModule.cs +++ b/OpenSim/Region/CoreModules/Avatar/Gods/GodsModule.cs @@ -26,29 +26,25 @@ */ using System; -using System.Collections.Generic; -using Nini.Config; -using OpenMetaverse; -using OpenSim.Framework; -using OpenSim.Region.Framework.Scenes; -using OpenSim.Region.Framework.Interfaces; -using System; -using System.Reflection; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; -using System.Reflection; using System.IO; +using System.Reflection; using System.Web; using System.Xml; using log4net; - using Mono.Addins; - +using Nini.Config; +using OpenMetaverse; using OpenMetaverse.Messages.Linden; using OpenMetaverse.StructuredData; +using OpenSim.Framework; using OpenSim.Framework.Capabilities; using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Region.Framework.Interfaces; using Caps = OpenSim.Framework.Capabilities.Caps; using OSDArray = OpenMetaverse.StructuredData.OSDArray; using OSDMap = OpenMetaverse.StructuredData.OSDMap; From 328883700a15e4216bf7b251ac099d38f413375e Mon Sep 17 00:00:00 2001 From: BlueWall Date: Mon, 13 May 2013 22:11:28 -0400 Subject: [PATCH 04/18] UserProfiles UserProfiles for Robust and Standalone. Includes service and connectors for Robust and standalone opensim plus matching region module. --- OpenSim/Data/IProfilesData.cs | 29 + OpenSim/Data/MySQL/MySQLUserProfilesData.cs | 1023 ++++++++++++ .../MySQL/Resources/UserProfiles.migrations | 83 + OpenSim/Framework/UserProfiles.cs | 90 ++ .../Avatar/Profile/BasicProfileModule.cs | 176 --- .../Avatar/UserProfiles/UserProfileModule.cs | 1386 +++++++++++++++++ .../LocalUserProfilesServiceConnector.cs | 226 +++ .../Grid/LocalGridServiceConnector.cs | 1 + .../Profiles/UserProfilesConnector.cs | 92 ++ .../Handlers/Profiles/UserProfilesHandlers.cs | 434 ++++++ .../Interfaces/IUserProfilesService.cs | 48 + .../UserProfilesService.cs | 160 ++ .../UserProfilesServiceBase.cs | 59 + bin/OpenSim.ini.example | 6 + bin/OpenSimDefaults.ini | 13 + bin/Robust.HG.ini.example | 12 +- bin/Robust.ini.example | 11 + .../StandaloneCommon.ini.example | 16 + bin/config-include/StandaloneHypergrid.ini | 2 +- prebuild.xml | 38 + 20 files changed, 3727 insertions(+), 178 deletions(-) create mode 100644 OpenSim/Data/IProfilesData.cs create mode 100644 OpenSim/Data/MySQL/MySQLUserProfilesData.cs create mode 100644 OpenSim/Data/MySQL/Resources/UserProfiles.migrations create mode 100644 OpenSim/Framework/UserProfiles.cs delete mode 100644 OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs create mode 100644 OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs create mode 100644 OpenSim/Region/CoreModules/ServiceConnectorsIn/UserProfiles/LocalUserProfilesServiceConnector.cs create mode 100644 OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs create mode 100644 OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs create mode 100644 OpenSim/Services/Interfaces/IUserProfilesService.cs create mode 100644 OpenSim/Services/UserProfilesService/UserProfilesService.cs create mode 100644 OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs diff --git a/OpenSim/Data/IProfilesData.cs b/OpenSim/Data/IProfilesData.cs new file mode 100644 index 0000000000..eeccbf6965 --- /dev/null +++ b/OpenSim/Data/IProfilesData.cs @@ -0,0 +1,29 @@ +using System; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; + +namespace OpenSim.Data +{ + + public interface IProfilesData + { + OSDArray GetClassifiedRecords(UUID creatorId); + bool UpdateClassifiedRecord(UserClassifiedAdd ad, ref string result); + bool DeleteClassifiedRecord(UUID recordId); + OSDArray GetAvatarPicks(UUID avatarId); + UserProfilePick GetPickInfo(UUID avatarId, UUID pickId); + bool UpdatePicksRecord(UserProfilePick pick); + bool DeletePicksRecord(UUID pickId); + bool GetAvatarNotes(ref UserProfileNotes note); + bool UpdateAvatarNotes(ref UserProfileNotes note, ref string result); + bool GetAvatarProperties(ref UserProfileProperties props, ref string result); + bool UpdateAvatarProperties(ref UserProfileProperties props, ref string result); + bool UpdateAvatarInterests(UserProfileProperties up, ref string result); + bool GetClassifiedInfo(ref UserClassifiedAdd ad, ref string result); + bool GetUserAppData(ref UserAppData props, ref string result); + bool SetUserAppData(UserAppData props, ref string result); + OSDArray GetUserImageAssets(UUID avatarId); + } +} + diff --git a/OpenSim/Data/MySQL/MySQLUserProfilesData.cs b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs new file mode 100644 index 0000000000..09bd44814e --- /dev/null +++ b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs @@ -0,0 +1,1023 @@ +using System; +using System.Data; +using System.Reflection; +using OpenSim.Data; +using OpenSim.Framework; +using MySql.Data.MySqlClient; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using log4net; + +namespace OpenSim.Data.MySQL +{ + public class UserProfilesData: IProfilesData + { + static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + #region Properites + string ConnectionString + { + get; set; + } + + protected object Lock + { + get; set; + } + + protected virtual Assembly Assembly + { + get { return GetType().Assembly; } + } + + #endregion Properties + + #region class Member Functions + public UserProfilesData(string connectionString) + { + ConnectionString = connectionString; + Init(); + } + + void Init() + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + + Migration m = new Migration(dbcon, Assembly, "UserProfiles"); + m.Update(); + } + } + #endregion Member Functions + + #region Classifieds Queries + /// + /// Gets the classified records. + /// + /// + /// Array of classified records + /// + /// + /// Creator identifier. + /// + public OSDArray GetClassifiedRecords(UUID creatorId) + { + OSDArray data = new OSDArray(); + + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + string query = "SELECT classifieduuid, name FROM classifieds WHERE creatoruuid = ?Id"; + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", creatorId); + using( MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.Default)) + { + if(reader.HasRows) + { + while (reader.Read()) + { + OSDMap n = new OSDMap(); + UUID Id = UUID.Zero; + + string Name = null; + try + { + UUID.TryParse(Convert.ToString( reader["classifieduuid"]), out Id); + Name = Convert.ToString(reader["name"]); + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": UserAccount exception {0}", e.Message); + } + n.Add("classifieduuid", OSD.FromUUID(Id)); + n.Add("name", OSD.FromString(Name)); + data.Add(n); + } + } + } + } + } + return data; + } + + public bool UpdateClassifiedRecord(UserClassifiedAdd ad, ref string result) + { + string query = string.Empty; + + + query += "INSERT INTO classifieds ("; + query += "`classifieduuid`,"; + query += "`creatoruuid`,"; + query += "`creationdate`,"; + query += "`expirationdate`,"; + query += "`category`,"; + query += "`name`,"; + query += "`description`,"; + query += "`parceluuid`,"; + query += "`parentestate`,"; + query += "`snapshotuuid`,"; + query += "`simname`,"; + query += "`posglobal`,"; + query += "`parcelname`,"; + query += "`classifiedflags`,"; + query += "`priceforlisting`) "; + query += "VALUES ("; + query += "?ClassifiedId,"; + query += "?CreatorId,"; + query += "?CreatedDate,"; + query += "?ExpirationDate,"; + query += "?Category,"; + query += "?Name,"; + query += "?Description,"; + query += "?ParcelId,"; + query += "?ParentEstate,"; + query += "?SnapshotId,"; + query += "?SimName,"; + query += "?GlobalPos,"; + query += "?ParcelName,"; + query += "?Flags,"; + query += "?ListingPrice ) "; + query += "ON DUPLICATE KEY UPDATE "; + query += "category=?Category, "; + query += "expirationdate=?ExpirationDate, "; + query += "name=?Name, "; + query += "description=?Description, "; + query += "parentestate=?ParentEstate, "; + query += "posglobal=?GlobalPos, "; + query += "parcelname=?ParcelName, "; + query += "classifiedflags=?Flags, "; + query += "priceforlisting=?ListingPrice, "; + query += "snapshotuuid=?SnapshotId"; + + if(string.IsNullOrEmpty(ad.ParcelName)) + ad.ParcelName = "Unknown"; + if(ad.ParcelId == null) + ad.ParcelId = UUID.Zero; + if(string.IsNullOrEmpty(ad.Description)) + ad.Description = "No Description"; + + DateTime epoch = new DateTime(1970, 1, 1); + DateTime now = DateTime.Now; + TimeSpan epochnow = now - epoch; + TimeSpan duration; + DateTime expiration; + TimeSpan epochexp; + + if(ad.Flags == 2) + { + duration = new TimeSpan(7,0,0,0); + expiration = now.Add(duration); + epochexp = expiration - epoch; + } + else + { + duration = new TimeSpan(365,0,0,0); + expiration = now.Add(duration); + epochexp = expiration - epoch; + } + ad.CreationDate = (int)epochnow.TotalSeconds; + ad.ExpirationDate = (int)epochexp.TotalSeconds; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?ClassifiedId", ad.ClassifiedId.ToString()); + cmd.Parameters.AddWithValue("?CreatorId", ad.CreatorId.ToString()); + cmd.Parameters.AddWithValue("?CreatedDate", ad.CreationDate.ToString()); + cmd.Parameters.AddWithValue("?ExpirationDate", ad.ExpirationDate.ToString()); + cmd.Parameters.AddWithValue("?Category", ad.Category.ToString()); + cmd.Parameters.AddWithValue("?Name", ad.Name.ToString()); + cmd.Parameters.AddWithValue("?Description", ad.Description.ToString()); + cmd.Parameters.AddWithValue("?ParcelId", ad.ParcelId.ToString()); + cmd.Parameters.AddWithValue("?ParentEstate", ad.ParentEstate.ToString()); + cmd.Parameters.AddWithValue("?SnapshotId", ad.SnapshotId.ToString ()); + cmd.Parameters.AddWithValue("?SimName", ad.SimName.ToString()); + cmd.Parameters.AddWithValue("?GlobalPos", ad.GlobalPos.ToString()); + cmd.Parameters.AddWithValue("?ParcelName", ad.ParcelName.ToString()); + cmd.Parameters.AddWithValue("?Flags", ad.Flags.ToString()); + cmd.Parameters.AddWithValue("?ListingPrice", ad.Price.ToString ()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": ClassifiedesUpdate exception {0}", e.Message); + result = e.Message; + return false; + } + return true; + } + + public bool DeleteClassifiedRecord(UUID recordId) + { + string query = string.Empty; + + query += "DELETE FROM classifieds WHERE "; + query += "classifieduuid = ?ClasifiedId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?ClassifiedId", recordId.ToString()); + + lock(Lock) + { + cmd.ExecuteNonQuery(); + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": DeleteClassifiedRecord exception {0}", e.Message); + return false; + } + return true; + } + + public bool GetClassifiedInfo(ref UserClassifiedAdd ad, ref string result) + { + string query = string.Empty; + + query += "SELECT * FROM classifieds WHERE "; + query += "classifieduuid = ?AdId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?AdId", ad.ClassifiedId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader()) + { + if(reader.Read ()) + { + ad.CreatorId = new UUID(reader.GetGuid("creatoruuid")); + ad.ParcelId = new UUID(reader.GetGuid("parceluuid")); + ad.SnapshotId = new UUID(reader.GetGuid("snapshotuuid")); + ad.CreationDate = Convert.ToInt32(reader["creationdate"]); + ad.ExpirationDate = Convert.ToInt32(reader["expirationdate"]); + ad.ParentEstate = Convert.ToInt32(reader["parentestate"]); + ad.Flags = (byte)reader.GetUInt32("classifiedflags"); + ad.Category = reader.GetInt32("category"); + ad.Price = reader.GetInt16("priceforlisting"); + ad.Name = reader.GetString("name"); + ad.Description = reader.GetString("description"); + ad.SimName = reader.GetString("simname"); + ad.GlobalPos = reader.GetString("posglobal"); + ad.ParcelName = reader.GetString("parcelname"); + + } + } + } + dbcon.Close(); + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": GetPickInfo exception {0}", e.Message); + } + return true; + } + #endregion Classifieds Queries + + #region Picks Queries + public OSDArray GetAvatarPicks(UUID avatarId) + { + string query = string.Empty; + + query += "SELECT `pickuuid`,`name` FROM userpicks WHERE "; + query += "creatoruuid = ?Id"; + OSDArray data = new OSDArray(); + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", avatarId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader()) + { + if(reader.HasRows) + { + while (reader.Read()) + { + OSDMap record = new OSDMap(); + + record.Add("pickuuid",OSD.FromString((string)reader["pickuuid"])); + record.Add("name",OSD.FromString((string)reader["name"])); + data.Add(record); + } + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": GetAvatarPicks exception {0}", e.Message); + } + return data; + } + + public UserProfilePick GetPickInfo(UUID avatarId, UUID pickId) + { + string query = string.Empty; + UserProfilePick pick = new UserProfilePick(); + + query += "SELECT * FROM userpicks WHERE "; + query += "creatoruuid = ?CreatorId AND "; + query += "pickuuid = ?PickId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?CreatorId", avatarId.ToString()); + cmd.Parameters.AddWithValue("?PickId", pickId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader()) + { + if(reader.HasRows) + { + reader.Read(); + + string description = (string)reader["description"]; + + if (string.IsNullOrEmpty(description)) + description = "No description given."; + + UUID.TryParse((string)reader["pickuuid"], out pick.PickId); + UUID.TryParse((string)reader["creatoruuid"], out pick.CreatorId); + UUID.TryParse((string)reader["parceluuid"], out pick.ParcelId); + UUID.TryParse((string)reader["snapshotuuid"], out pick.SnapshotId); + pick.GlobalPos = (string)reader["posglobal"]; + bool.TryParse((string)reader["toppick"], out pick.TopPick); + bool.TryParse((string)reader["enabled"], out pick.Enabled); + pick.Name = (string)reader["name"]; + pick.Desc = description; + pick.User = (string)reader["user"]; + pick.OriginalName = (string)reader["originalname"]; + pick.SimName = (string)reader["simname"]; + pick.SortOrder = (int)reader["sortorder"]; + } + } + } + dbcon.Close(); + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": GetPickInfo exception {0}", e.Message); + } + return pick; + } + + public bool UpdatePicksRecord(UserProfilePick pick) + { + string query = string.Empty; + + query += "INSERT INTO userpicks VALUES ("; + query += "?PickId,"; + query += "?CreatorId,"; + query += "?TopPick,"; + query += "?ParcelId,"; + query += "?Name,"; + query += "?Desc,"; + query += "?SnapshotId,"; + query += "?User,"; + query += "?Original,"; + query += "?SimName,"; + query += "?GlobalPos,"; + query += "?SortOrder,"; + query += "?Enabled) "; + query += "ON DUPLICATE KEY UPDATE "; + query += "parceluuid=?ParcelId,"; + query += "name=?Name,"; + query += "description=?Desc,"; + query += "snapshotuuid=?SnapshotId,"; + query += "pickuuid=?PickId,"; + query += "posglobal=?GlobalPos"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?PickId", pick.PickId.ToString()); + cmd.Parameters.AddWithValue("?CreatorId", pick.CreatorId.ToString()); + cmd.Parameters.AddWithValue("?TopPick", pick.TopPick.ToString()); + cmd.Parameters.AddWithValue("?ParcelId", pick.ParcelId.ToString()); + cmd.Parameters.AddWithValue("?Name", pick.Name.ToString()); + cmd.Parameters.AddWithValue("?Desc", pick.Desc.ToString()); + cmd.Parameters.AddWithValue("?SnapshotId", pick.SnapshotId.ToString()); + cmd.Parameters.AddWithValue("?User", pick.User.ToString()); + cmd.Parameters.AddWithValue("?Original", pick.OriginalName.ToString()); + cmd.Parameters.AddWithValue("?SimName",pick.SimName.ToString()); + cmd.Parameters.AddWithValue("?GlobalPos", pick.GlobalPos); + cmd.Parameters.AddWithValue("?SortOrder", pick.SortOrder.ToString ()); + cmd.Parameters.AddWithValue("?Enabled", pick.Enabled.ToString()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": UpdateAvatarNotes exception {0}", e.Message); + return false; + } + return true; + } + + public bool DeletePicksRecord(UUID pickId) + { + string query = string.Empty; + + query += "DELETE FROM userpicks WHERE "; + query += "pickuuid = ?PickId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?PickId", pickId.ToString()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": DeleteUserPickRecord exception {0}", e.Message); + return false; + } + return true; + } + #endregion Picks Queries + + #region Avatar Notes Queries + public bool GetAvatarNotes(ref UserProfileNotes notes) + { // WIP + string query = string.Empty; + + query += "SELECT `notes` FROM usernotes WHERE "; + query += "useruuid = ?Id AND "; + query += "targetuuid = ?TargetId"; + OSDArray data = new OSDArray(); + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", notes.UserId.ToString()); + cmd.Parameters.AddWithValue("?TargetId", notes.TargetId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + reader.Read(); + notes.Notes = OSD.FromString((string)reader["notes"]); + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": GetAvatarNotes exception {0}", e.Message); + } + return true; + } + + public bool UpdateAvatarNotes(ref UserProfileNotes note, ref string result) + { + string query = string.Empty; + bool remove; + + if(string.IsNullOrEmpty(note.Notes)) + { + remove = true; + query += "DELETE FROM usernotes WHERE "; + query += "useruuid=?UserId AND "; + query += "targetuuid=?TargetId"; + } + else + { + remove = false; + query += "INSERT INTO usernotes VALUES ( "; + query += "?UserId,"; + query += "?TargetId,"; + query += "?Notes )"; + query += "ON DUPLICATE KEY "; + query += "UPDATE "; + query += "notes=?Notes"; + } + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + if(!remove) + cmd.Parameters.AddWithValue("?Notes", note.Notes); + cmd.Parameters.AddWithValue("?TargetId", note.TargetId.ToString ()); + cmd.Parameters.AddWithValue("?UserId", note.UserId.ToString()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": UpdateAvatarNotes exception {0}", e.Message); + return false; + } + return true; + + } + #endregion Avatar Notes Queries + + #region Avatar Properties + public bool GetAvatarProperties(ref UserProfileProperties props, ref string result) + { + string query = string.Empty; + + query += "SELECT * FROM userprofile WHERE "; + query += "useruuid = ?Id"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", props.UserId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": Getting data for {0}.", props.UserId); + reader.Read(); + props.WebUrl = (string)reader["profileURL"]; + UUID.TryParse((string)reader["profileImage"], out props.ImageId); + props.AboutText = (string)reader["profileAboutText"]; + UUID.TryParse((string)reader["profileFirstImage"], out props.FirstLifeImageId); + props.FirstLifeText = (string)reader["profileFirstText"]; + UUID.TryParse((string)reader["profilePartner"], out props.PartnerId); + props.WantToMask = (int)reader["profileWantToMask"]; + props.WantToText = (string)reader["profileWantToText"]; + props.SkillsMask = (int)reader["profileSkillsMask"]; + props.SkillsText = (string)reader["profileSkillsText"]; + props.Language = (string)reader["profileLanguages"]; + } + else + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": No data for {0}", props.UserId); + + props.WebUrl = string.Empty; + props.ImageId = UUID.Zero; + props.AboutText = string.Empty; + props.FirstLifeImageId = UUID.Zero; + props.FirstLifeText = string.Empty; + props.PartnerId = UUID.Zero; + props.WantToMask = 0; + props.WantToText = string.Empty; + props.SkillsMask = 0; + props.SkillsText = string.Empty; + props.Language = string.Empty; + + query = "INSERT INTO userprofile (`useruuid`) VALUES (?userId)"; + + dbcon.Close(); + dbcon.Open(); + + using (MySqlCommand put = new MySqlCommand(query, dbcon)) + { + put.Parameters.AddWithValue("?userId", props.UserId.ToString()); + put.ExecuteNonQuery(); + } + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": Requst properties exception {0}", e.Message); + result = e.Message; + return false; + } + return true; + } + + public bool UpdateAvatarProperties(ref UserProfileProperties props, ref string result) + { + string query = string.Empty; + + query += "UPDATE userprofile SET "; + query += "profileURL=?profileURL, "; + query += "profileImage=?image, "; + query += "profileAboutText=?abouttext,"; + query += "profileFirstImage=?firstlifeimage,"; + query += "profileFirstText=?firstlifetext "; + query += "WHERE useruuid=?uuid"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?profileURL", props.WebUrl); + cmd.Parameters.AddWithValue("?image", props.ImageId.ToString()); + cmd.Parameters.AddWithValue("?abouttext", props.AboutText); + cmd.Parameters.AddWithValue("?firstlifeimage", props.FirstLifeImageId.ToString()); + cmd.Parameters.AddWithValue("?firstlifetext", props.FirstLifeText); + cmd.Parameters.AddWithValue("?uuid", props.UserId.ToString()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": AgentPropertiesUpdate exception {0}", e.Message); + + return false; + } + return true; + } + #endregion Avatar Properties + + #region Avatar Interests + public bool UpdateAvatarInterests(UserProfileProperties up, ref string result) + { + string query = string.Empty; + + query += "UPDATE userprofile SET "; + query += "profileWantToMask=?WantMask, "; + query += "profileWantToText=?WantText,"; + query += "profileSkillsMask=?SkillsMask,"; + query += "profileSkillsText=?SkillsText, "; + query += "profileLanguages=?Languages "; + query += "WHERE useruuid=?uuid"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?WantMask", up.WantToMask); + cmd.Parameters.AddWithValue("?WantText", up.WantToText); + cmd.Parameters.AddWithValue("?SkillsMask", up.SkillsMask); + cmd.Parameters.AddWithValue("?SkillsText", up.SkillsText); + cmd.Parameters.AddWithValue("?Languages", up.Language); + cmd.Parameters.AddWithValue("?uuid", up.UserId.ToString()); + + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": AgentInterestsUpdate exception {0}", e.Message); + result = e.Message; + return false; + } + return true; + } + #endregion Avatar Interests + + public OSDArray GetUserImageAssets(UUID avatarId) + { + OSDArray data = new OSDArray(); + string query = "SELECT `snapshotuuid` FROM {0} WHERE `creatoruuid` = ?Id"; + + // Get classified image assets + + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + + using (MySqlCommand cmd = new MySqlCommand(string.Format (query,"`classifieds`"), dbcon)) + { + cmd.Parameters.AddWithValue("?Id", avatarId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + while (reader.Read()) + { + data.Add(new OSDString((string)reader["snapshotuuid"].ToString ())); + } + } + } + } + + dbcon.Close(); + dbcon.Open(); + + using (MySqlCommand cmd = new MySqlCommand(string.Format (query,"`userpicks`"), dbcon)) + { + cmd.Parameters.AddWithValue("?Id", avatarId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + while (reader.Read()) + { + data.Add(new OSDString((string)reader["snapshotuuid"].ToString ())); + } + } + } + } + + dbcon.Close(); + dbcon.Open(); + + query = "SELECT `profileImage`, `profileFirstImage` FROM `userprofile` WHERE `useruuid` = ?Id"; + + using (MySqlCommand cmd = new MySqlCommand(string.Format (query,"`userpicks`"), dbcon)) + { + cmd.Parameters.AddWithValue("?Id", avatarId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + while (reader.Read()) + { + data.Add(new OSDString((string)reader["profileImage"].ToString ())); + data.Add(new OSDString((string)reader["profileFirstImage"].ToString ())); + } + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": GetAvatarNotes exception {0}", e.Message); + } + return data; + } + + #region User Preferences + public OSDArray GetUserPreferences(UUID avatarId) + { + string query = string.Empty; + + query += "SELECT imviaemail,visible,email FROM "; + query += "usersettings WHERE "; + query += "useruuid = ?Id"; + + OSDArray data = new OSDArray(); + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", avatarId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader()) + { + if(reader.HasRows) + { + reader.Read(); + OSDMap record = new OSDMap(); + + record.Add("imviaemail",OSD.FromString((string)reader["imviaemail"])); + record.Add("visible",OSD.FromString((string)reader["visible"])); + record.Add("email",OSD.FromString((string)reader["email"])); + data.Add(record); + } + else + { + using (MySqlCommand put = new MySqlCommand(query, dbcon)) + { + query = "INSERT INTO usersettings VALUES "; + query += "(?Id,'false','false', '')"; + + lock(Lock) + { + put.ExecuteNonQuery(); + } + } + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": Get preferences exception {0}", e.Message); + } + return data; + } + + public bool UpdateUserPreferences(bool emailIm, bool visible, UUID avatarId ) + { + string query = string.Empty; + + query += "UPDATE userpsettings SET "; + query += "imviaemail=?ImViaEmail, "; + query += "visible=?Visible,"; + query += "WHERE useruuid=?uuid"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?ImViaEmail", emailIm.ToString().ToLower ()); + cmd.Parameters.AddWithValue("?WantText", visible.ToString().ToLower ()); + cmd.Parameters.AddWithValue("?uuid", avatarId.ToString()); + + lock(Lock) + { + cmd.ExecuteNonQuery(); + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": AgentInterestsUpdate exception {0}", e.Message); + return false; + } + return true; + } + #endregion User Preferences + + #region Integration + public bool GetUserAppData(ref UserAppData props, ref string result) + { + string query = string.Empty; + + query += "SELECT * FROM `userdata` WHERE "; + query += "UserId = ?Id AND "; + query += "TagId = ?TagId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?Id", props.UserId.ToString()); + cmd.Parameters.AddWithValue ("?TagId", props.TagId.ToString()); + + using (MySqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow)) + { + if(reader.HasRows) + { + reader.Read(); + props.DataKey = (string)reader["DataKey"]; + props.DataVal = (string)reader["DataVal"]; + } + else + { + query += "INSERT INTO userdata VALUES ( "; + query += "?UserId,"; + query += "?TagId,"; + query += "?DataKey,"; + query += "?DataVal) "; + + using (MySqlCommand put = new MySqlCommand(query, dbcon)) + { + put.Parameters.AddWithValue("?Id", props.UserId.ToString()); + put.Parameters.AddWithValue("?TagId", props.TagId.ToString()); + put.Parameters.AddWithValue("?DataKey", props.DataKey.ToString()); + put.Parameters.AddWithValue("?DataVal", props.DataVal.ToString()); + + lock(Lock) + { + put.ExecuteNonQuery(); + } + } + } + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": Requst application data exception {0}", e.Message); + result = e.Message; + return false; + } + return true; + } + + public bool SetUserAppData(UserAppData props, ref string result) + { + string query = string.Empty; + + query += "UPDATE userdata SET "; + query += "TagId = ?TagId, "; + query += "DataKey = ?DataKey, "; + query += "DataVal = ?DataVal WHERE "; + query += "UserId = ?UserId AND "; + query += "TagId = ?TagId"; + + try + { + using (MySqlConnection dbcon = new MySqlConnection(ConnectionString)) + { + dbcon.Open(); + using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) + { + cmd.Parameters.AddWithValue("?UserId", props.UserId.ToString()); + cmd.Parameters.AddWithValue("?TagId", props.TagId.ToString ()); + cmd.Parameters.AddWithValue("?DataKey", props.DataKey.ToString ()); + cmd.Parameters.AddWithValue("?DataVal", props.DataKey.ToString ()); + + lock(Lock) + { + cmd.ExecuteNonQuery(); + } + } + } + } + catch (Exception e) + { + m_log.DebugFormat("[PROFILES_DATA]" + + ": SetUserData exception {0}", e.Message); + return false; + } + return true; + } + #endregion Integration + } +} + diff --git a/OpenSim/Data/MySQL/Resources/UserProfiles.migrations b/OpenSim/Data/MySQL/Resources/UserProfiles.migrations new file mode 100644 index 0000000000..c29f1abf3e --- /dev/null +++ b/OpenSim/Data/MySQL/Resources/UserProfiles.migrations @@ -0,0 +1,83 @@ +:VERSION 1 # ------------------------------- + +begin; + +CREATE TABLE IF NOT EXISTS `classifieds` ( + `classifieduuid` char(36) NOT NULL, + `creatoruuid` char(36) NOT NULL, + `creationdate` int(20) NOT NULL, + `expirationdate` int(20) NOT NULL, + `category` varchar(20) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text NOT NULL, + `parceluuid` char(36) NOT NULL, + `parentestate` int(11) NOT NULL, + `snapshotuuid` char(36) NOT NULL, + `simname` varchar(255) NOT NULL, + `posglobal` varchar(255) NOT NULL, + `parcelname` varchar(255) NOT NULL, + `classifiedflags` int(8) NOT NULL, + `priceforlisting` int(5) NOT NULL, + PRIMARY KEY (`classifieduuid`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + + +CREATE TABLE IF NOT EXISTS `usernotes` ( + `useruuid` varchar(36) NOT NULL, + `targetuuid` varchar(36) NOT NULL, + `notes` text NOT NULL, + UNIQUE KEY `useruuid` (`useruuid`,`targetuuid`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + + +CREATE TABLE IF NOT EXISTS `userpicks` ( + `pickuuid` varchar(36) NOT NULL, + `creatoruuid` varchar(36) NOT NULL, + `toppick` enum('true','false') NOT NULL, + `parceluuid` varchar(36) NOT NULL, + `name` varchar(255) NOT NULL, + `description` text NOT NULL, + `snapshotuuid` varchar(36) NOT NULL, + `user` varchar(255) NOT NULL, + `originalname` varchar(255) NOT NULL, + `simname` varchar(255) NOT NULL, + `posglobal` varchar(255) NOT NULL, + `sortorder` int(2) NOT NULL, + `enabled` enum('true','false') NOT NULL, + PRIMARY KEY (`pickuuid`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + + +CREATE TABLE IF NOT EXISTS `userprofile` ( + `useruuid` varchar(36) NOT NULL, + `profilePartner` varchar(36) NOT NULL, + `profileAllowPublish` binary(1) NOT NULL, + `profileMaturePublish` binary(1) NOT NULL, + `profileURL` varchar(255) NOT NULL, + `profileWantToMask` int(3) NOT NULL, + `profileWantToText` text NOT NULL, + `profileSkillsMask` int(3) NOT NULL, + `profileSkillsText` text NOT NULL, + `profileLanguages` text NOT NULL, + `profileImage` varchar(36) NOT NULL, + `profileAboutText` text NOT NULL, + `profileFirstImage` varchar(36) NOT NULL, + `profileFirstText` text NOT NULL, + PRIMARY KEY (`useruuid`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + +commit; + +:VERSION 2 # ------------------------------- + +begin; +CREATE TABLE IF NOT EXISTS `userdata` ( + `UserId` char(36) NOT NULL, + `TagId` varchar(64) NOT NULL, + `DataKey` varchar(255), + `DataVal` varchar(255), + PRIMARY KEY (`UserId`,`TagId`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1; + +commit; + diff --git a/OpenSim/Framework/UserProfiles.cs b/OpenSim/Framework/UserProfiles.cs new file mode 100644 index 0000000000..aabbb84538 --- /dev/null +++ b/OpenSim/Framework/UserProfiles.cs @@ -0,0 +1,90 @@ +using System; +using OpenMetaverse; + +namespace OpenSim.Framework +{ + public class UserClassifiedAdd + { + public UUID ClassifiedId = UUID.Zero; + public UUID CreatorId = UUID.Zero; + public int CreationDate = 0; + public int ExpirationDate = 0; + public int Category = 0; + public string Name = string.Empty; + public string Description = string.Empty; + public UUID ParcelId = UUID.Zero; + public int ParentEstate = 0; + public UUID SnapshotId = UUID.Zero; + public string SimName = string.Empty; + public string GlobalPos = "<0,0,0>"; + public string ParcelName = string.Empty; + public byte Flags = 0; + public int Price = 0; + } + + public class UserProfileProperties + { + public UUID UserId = UUID.Zero; + public UUID PartnerId = UUID.Zero; + public bool PublishProfile = false; + public bool PublishMature = false; + public string WebUrl = string.Empty; + public int WantToMask = 0; + public string WantToText = string.Empty; + public int SkillsMask = 0; + public string SkillsText = string.Empty; + public string Language = string.Empty; + public UUID ImageId = UUID.Zero; + public string AboutText = string.Empty; + public UUID FirstLifeImageId = UUID.Zero; + public string FirstLifeText = string.Empty; + } + + public class UserProfilePick + { + public UUID PickId = UUID.Zero; + public UUID CreatorId = UUID.Zero; + public bool TopPick = false; + public string Name = string.Empty; + public string OriginalName = string.Empty; + public string Desc = string.Empty; + public UUID ParcelId = UUID.Zero; + public UUID SnapshotId = UUID.Zero; + public string User = string.Empty; + public string SimName = string.Empty; + public string GlobalPos = "<0,0,0>"; + public int SortOrder = 0; + public bool Enabled = false; + } + + public class UserProfileNotes + { + public UUID UserId; + public UUID TargetId; + public string Notes; + } + + public class UserAccountProperties + { + public string EmailAddress = string.Empty; + public string Firstname = string.Empty; + public string LastName = string.Empty; + public string Password = string.Empty; + public string UserId = string.Empty; + } + + public class UserAccountAuth + { + public string UserId = UUID.Zero.ToString(); + public string Password = string.Empty; + } + + public class UserAppData + { + public string TagId = string.Empty; + public string DataKey = string.Empty; + public string UserId = UUID.Zero.ToString(); + public string DataVal = string.Empty; + } +} + diff --git a/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs b/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs deleted file mode 100644 index bf24030771..0000000000 --- a/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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; -using System.Collections.Generic; -using System.Globalization; -using System.Reflection; - -using OpenMetaverse; -using log4net; -using Nini.Config; -using Mono.Addins; - -using OpenSim.Framework; -using OpenSim.Region.Framework.Interfaces; -using OpenSim.Region.Framework.Scenes; -using OpenSim.Services.Interfaces; - -namespace OpenSim.Region.CoreModules.Avatar.Profile -{ - [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "BasicProfileModule")] - public class BasicProfileModule : IProfileModule, ISharedRegionModule - { - private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - - // - // Module vars - // - private List m_Scenes = new List(); - private bool m_Enabled = false; - - #region ISharedRegionModule - - public void Initialise(IConfigSource config) - { - m_log.DebugFormat("[PROFILE MODULE]: Basic Profile Module enabled"); - m_Enabled = true; - } - - public void AddRegion(Scene scene) - { - if (!m_Enabled) - return; - - lock (m_Scenes) - { - if (!m_Scenes.Contains(scene)) - { - m_Scenes.Add(scene); - // Hook up events - scene.EventManager.OnNewClient += OnNewClient; - scene.RegisterModuleInterface(this); - } - } - } - - public void RegionLoaded(Scene scene) - { - if (!m_Enabled) - return; - } - - public void RemoveRegion(Scene scene) - { - if (!m_Enabled) - return; - - lock (m_Scenes) - { - m_Scenes.Remove(scene); - } - } - - public void PostInitialise() - { - } - - public void Close() - { - } - - public string Name - { - get { return "BasicProfileModule"; } - } - - public Type ReplaceableInterface - { - get { return typeof(IProfileModule); } - } - - #endregion - - /// New Client Event Handler - private void OnNewClient(IClientAPI client) - { - //Profile - client.OnRequestAvatarProperties += RequestAvatarProperties; - } - - public void RequestAvatarProperties(IClientAPI remoteClient, UUID avatarID) - { - IScene s = remoteClient.Scene; - if (!(s is Scene)) - return; - -// Scene scene = (Scene)s; - - string profileUrl = String.Empty; - string aboutText = String.Empty; - string firstLifeAboutText = String.Empty; - UUID image = UUID.Zero; - UUID firstLifeImage = UUID.Zero; - UUID partner = UUID.Zero; - uint wantMask = 0; - string wantText = String.Empty; - uint skillsMask = 0; - string skillsText = String.Empty; - string languages = String.Empty; - - UserAccount account = m_Scenes[0].UserAccountService.GetUserAccount(m_Scenes[0].RegionInfo.ScopeID, avatarID); - - string name = "Avatar"; - int created = 0; - if (account != null) - { - name = account.FirstName + " " + account.LastName; - created = account.Created; - } - Byte[] charterMember = Utils.StringToBytes(name); - - profileUrl = "No profile data"; - aboutText = string.Empty; - firstLifeAboutText = string.Empty; - image = UUID.Zero; - firstLifeImage = UUID.Zero; - partner = UUID.Zero; - - remoteClient.SendAvatarProperties(avatarID, aboutText, - Util.ToDateTime(created).ToString( - "M/d/yyyy", CultureInfo.InvariantCulture), - charterMember, firstLifeAboutText, - (uint)(0 & 0xff), - firstLifeImage, image, profileUrl, partner); - - //Viewer expects interest data when it asks for properties. - remoteClient.SendAvatarInterestsReply(avatarID, wantMask, wantText, - skillsMask, skillsText, languages); - } - - } -} diff --git a/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs b/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs new file mode 100644 index 0000000000..563617d6b8 --- /dev/null +++ b/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs @@ -0,0 +1,1386 @@ +/* + * 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.IO; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Xml; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using log4net; +using Nini.Config; +using Nwc.XmlRpc; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; +using Mono.Addins; +using OpenSim.Services.Connectors.Hypergrid; + +namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles +{ + [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "UserProfilesModule")] + public class UserProfileModule : IProfileModule, INonSharedRegionModule + { + /// + /// Logging + /// + static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + // The pair of Dictionaries are used to handle the switching of classified ads + // by maintaining a cache of classified id to creator id mappings and an interest + // count. The entries are removed when the interest count reaches 0. + Dictionary classifiedCache = new Dictionary(); + Dictionary classifiedInterest = new Dictionary(); + + public Scene Scene + { + get; private set; + } + + /// + /// Gets or sets the ConfigSource. + /// + /// + /// The configuration + /// + public IConfigSource Config { + get; + set; + } + + /// + /// Gets or sets the URI to the profile server. + /// + /// + /// The profile server URI. + /// + public string ProfileServerUri { + get; + set; + } + + IProfileModule ProfileModule + { + get; set; + } + + IUserManagement UserManagementModule + { + get; set; + } + + /// + /// Gets or sets a value indicating whether this + /// is enabled. + /// + /// + /// true if enabled; otherwise, false. + /// + public bool Enabled { + get; + set; + } + + #region IRegionModuleBase implementation + /// + /// This is called to initialize the region module. For shared modules, this is called exactly once, after + /// creating the single (shared) instance. For non-shared modules, this is called once on each instance, after + /// the instace for the region has been created. + /// + /// + /// Source. + /// + public void Initialise(IConfigSource source) + { + Config = source; + + IConfig profileConfig = Config.Configs["Profile"]; + + if (profileConfig == null) + { + Enabled = false; + return; + } + + // If we find ProfileURL then we configure for FULL support + // else we setup for BASIC support + ProfileServerUri = profileConfig.GetString("ProfileURL", ""); + if (ProfileServerUri == "") + { + m_log.Info("[PROFILES] UserProfiles module is activated in BASIC mode"); + Enabled = false; + return; + } + else + { + m_log.Info("[PROFILES] UserProfiles module is activated in FULL mode"); + Enabled = true; + } + } + + /// + /// Adds the region. + /// + /// + /// Scene. + /// + public void AddRegion(Scene scene) + { + Scene = scene; + Scene.RegisterModuleInterface(this); + Scene.EventManager.OnNewClient += OnNewClient; + Scene.EventManager.OnMakeRootAgent += HandleOnMakeRootAgent; + + UserManagementModule = Scene.RequestModuleInterface(); + } + + void HandleOnMakeRootAgent (ScenePresence obj) + { + GetImageAssets(((IScenePresence)obj).UUID); + } + + /// + /// Removes the region. + /// + /// + /// Scene. + /// + public void RemoveRegion(Scene scene) + { + } + + /// + /// This will be called once for every scene loaded. In a shared module this will be multiple times in one + /// instance, while a nonshared module instance will only be called once. This method is called after AddRegion + /// has been called in all modules for that scene, providing an opportunity to request another module's + /// interface, or hook an event from another module. + /// + /// + /// Scene. + /// + public void RegionLoaded(Scene scene) + { + } + + /// + /// If this returns non-null, it is the type of an interface that this module intends to register. This will + /// cause the loader to defer loading of this module until all other modules have been loaded. If no other + /// module has registered the interface by then, this module will be activated, else it will remain inactive, + /// letting the other module take over. This should return non-null ONLY in modules that are intended to be + /// easily replaceable, e.g. stub implementations that the developer expects to be replaced by third party + /// provided modules. + /// + /// + /// The replaceable interface. + /// + public Type ReplaceableInterface + { + get { return typeof(IProfileModule); } + } + + /// + /// Called as the instance is closed. + /// + public void Close() + { + } + + /// + /// The name of the module + /// + /// + /// Gets the module name. + /// + public string Name + { + get { return "UserProfileModule"; } + } + #endregion IRegionModuleBase implementation + + #region Region Event Handlers + /// + /// Raises the new client event. + /// + /// + /// Client. + /// + void OnNewClient(IClientAPI client) + { + // Basic or Full module? + if(!Enabled) + { + client.OnRequestAvatarProperties += BasicRequestProperties; + return; + } + + //Profile + client.OnRequestAvatarProperties += RequestAvatarProperties; + client.OnUpdateAvatarProperties += AvatarPropertiesUpdate; + client.OnAvatarInterestUpdate += AvatarInterestsUpdate; + + // Classifieds + client.AddGenericPacketHandler("avatarclassifiedsrequest", ClassifiedsRequest); + client.OnClassifiedInfoUpdate += ClassifiedInfoUpdate; + client.OnClassifiedInfoRequest += ClassifiedInfoRequest; + client.OnClassifiedDelete += ClassifiedDelete; + + // Picks + client.AddGenericPacketHandler("avatarpicksrequest", PicksRequest); + client.AddGenericPacketHandler("pickinforequest", PickInfoRequest); + client.OnPickInfoUpdate += PickInfoUpdate; + client.OnPickDelete += PickDelete; + + // Notes + client.AddGenericPacketHandler("avatarnotesrequest", NotesRequest); + client.OnAvatarNotesUpdate += NotesUpdate; + } + #endregion Region Event Handlers + + #region Classified + /// + /// + /// Handles the avatar classifieds request. + /// + /// + /// Sender. + /// + /// + /// Method. + /// + /// + /// Arguments. + /// + public void ClassifiedsRequest(Object sender, string method, List args) + { + if (!(sender is IClientAPI)) + return; + + IClientAPI remoteClient = (IClientAPI)sender; + + UUID targetID; + UUID.TryParse(args[0], out targetID); + + // Can't handle NPC yet... + ScenePresence p = FindPresence(targetID); + + if (null != p) + { + if (p.PresenceType == PresenceType.Npc) + return; + } + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(targetID, out serverURI); + UUID creatorId = UUID.Zero; + + OSDMap parameters= new OSDMap(); + UUID.TryParse(args[0], out creatorId); + parameters.Add("creatorId", OSD.FromUUID(creatorId)); + OSD Params = (OSD)parameters; + if(!JsonRpcRequest(ref Params, "avatarclassifiedsrequest", serverURI, UUID.Random().ToString())) + { + // Error Handling here! + // if(parameters.ContainsKey("message") + } + + parameters = (OSDMap)Params; + + OSDArray list = (OSDArray)parameters["result"]; + + Dictionary classifieds = new Dictionary(); + + foreach(OSD map in list) + { + OSDMap m = (OSDMap)map; + UUID cid = m["classifieduuid"].AsUUID(); + string name = m["name"].AsString(); + + classifieds[cid] = name; + + if(!classifiedCache.ContainsKey(cid)) + { + classifiedCache.Add(cid,creatorId); + classifiedInterest.Add(cid, 0); + } + + classifiedInterest[cid] ++; + } + + remoteClient.SendAvatarClassifiedReply(new UUID(args[0]), classifieds); + } + + public void ClassifiedInfoRequest(UUID queryClassifiedID, IClientAPI remoteClient) + { + UUID target = remoteClient.AgentId; + UserClassifiedAdd ad = new UserClassifiedAdd(); + ad.ClassifiedId = queryClassifiedID; + + if(classifiedCache.ContainsKey(queryClassifiedID)) + { + target = classifiedCache[queryClassifiedID]; + + if(classifiedInterest[queryClassifiedID] -- == 0) + { + lock(classifiedCache) + { + lock(classifiedInterest) + { + classifiedInterest.Remove(queryClassifiedID); + } + classifiedCache.Remove(queryClassifiedID); + } + } + } + + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(target, out serverURI); + + object Ad = (object)ad; + if(!JsonRpcRequest(ref Ad, "classifieds_info_query", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error getting classified info", false); + return; + } + ad = (UserClassifiedAdd) Ad; + + if(ad.CreatorId == UUID.Zero) + return; + + Vector3 globalPos = new Vector3(); + Vector3.TryParse(ad.GlobalPos, out globalPos); + + remoteClient.SendClassifiedInfoReply(ad.ClassifiedId, ad.CreatorId, (uint)ad.CreationDate, (uint)ad.ExpirationDate, + (uint)ad.Category, ad.Name, ad.Description, ad.ParcelId, (uint)ad.ParentEstate, + ad.SnapshotId, ad.SimName, globalPos, ad.ParcelName, ad.Flags, ad.Price); + + } + + /// + /// Classifieds info update. + /// + /// + /// Queryclassified I. + /// + /// + /// Query category. + /// + /// + /// Query name. + /// + /// + /// Query description. + /// + /// + /// Query parcel I. + /// + /// + /// Query parent estate. + /// + /// + /// Query snapshot I. + /// + /// + /// Query global position. + /// + /// + /// Queryclassified flags. + /// + /// + /// Queryclassified price. + /// + /// + /// Remote client. + /// + public void ClassifiedInfoUpdate(UUID queryclassifiedID, uint queryCategory, string queryName, string queryDescription, UUID queryParcelID, + uint queryParentEstate, UUID querySnapshotID, Vector3 queryGlobalPos, byte queryclassifiedFlags, + int queryclassifiedPrice, IClientAPI remoteClient) + { + UserClassifiedAdd ad = new UserClassifiedAdd(); + + Scene s = (Scene) remoteClient.Scene; + Vector3 pos = remoteClient.SceneAgent.AbsolutePosition; + ILandObject land = s.LandChannel.GetLandObject(pos.X, pos.Y); + ScenePresence p = FindPresence(remoteClient.AgentId); + Vector3 avaPos = p.AbsolutePosition; + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + if (land == null) + { + ad.ParcelName = string.Empty; + } + else + { + ad.ParcelName = land.LandData.Name; + } + + ad.CreatorId = remoteClient.AgentId; + ad.ClassifiedId = queryclassifiedID; + ad.Category = Convert.ToInt32(queryCategory); + ad.Name = queryName; + ad.Description = queryDescription; + ad.ParentEstate = Convert.ToInt32(queryParentEstate); + ad.SnapshotId = querySnapshotID; + ad.SimName = remoteClient.Scene.RegionInfo.RegionName; + ad.GlobalPos = queryGlobalPos.ToString (); + ad.Flags = queryclassifiedFlags; + ad.Price = queryclassifiedPrice; + ad.ParcelId = p.currentParcelUUID; + + object Ad = ad; + + OSD X = OSD.SerializeMembers(Ad); + + if(!JsonRpcRequest(ref Ad, "classified_update", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error updating classified", false); + } + } + + /// + /// Classifieds delete. + /// + /// + /// Query classified I. + /// + /// + /// Remote client. + /// + public void ClassifiedDelete(UUID queryClassifiedID, IClientAPI remoteClient) + { + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + UUID classifiedId; + OSDMap parameters= new OSDMap(); + UUID.TryParse(queryClassifiedID.ToString(), out classifiedId); + parameters.Add("classifiedId", OSD.FromUUID(classifiedId)); + OSD Params = (OSD)parameters; + if(!JsonRpcRequest(ref Params, "classified_delete", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error classified delete", false); + } + + parameters = (OSDMap)Params; + } + #endregion Classified + + #region Picks + /// + /// Handles the avatar picks request. + /// + /// + /// Sender. + /// + /// + /// Method. + /// + /// + /// Arguments. + /// + public void PicksRequest(Object sender, string method, List args) + { + if (!(sender is IClientAPI)) + return; + + IClientAPI remoteClient = (IClientAPI)sender; + + UUID targetId; + UUID.TryParse(args[0], out targetId); + + // Can't handle NPC yet... + ScenePresence p = FindPresence(targetId); + + if (null != p) + { + if (p.PresenceType == PresenceType.Npc) + return; + } + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(targetId, out serverURI); + + OSDMap parameters= new OSDMap(); + parameters.Add("creatorId", OSD.FromUUID(targetId)); + OSD Params = (OSD)parameters; + if(!JsonRpcRequest(ref Params, "avatarpicksrequest", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error requesting picks", false); + return; + } + + parameters = (OSDMap)Params; + + OSDArray list = (OSDArray)parameters["result"]; + + Dictionary picks = new Dictionary(); + + foreach(OSD map in list) + { + OSDMap m = (OSDMap)map; + UUID cid = m["pickuuid"].AsUUID(); + string name = m["name"].AsString(); + + m_log.DebugFormat("[PROFILES]: PicksRequest {0}", name); + + picks[cid] = name; + } + remoteClient.SendAvatarPicksReply(new UUID(args[0]), picks); + } + + /// + /// Handles the pick info request. + /// + /// + /// Sender. + /// + /// + /// Method. + /// + /// + /// Arguments. + /// + public void PickInfoRequest(Object sender, string method, List args) + { + if (!(sender is IClientAPI)) + return; + + UUID targetID; + UUID.TryParse(args[0], out targetID); + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(targetID, out serverURI); + IClientAPI remoteClient = (IClientAPI)sender; + + UserProfilePick pick = new UserProfilePick(); + UUID.TryParse(args[0], out pick.CreatorId); + UUID.TryParse(args[1], out pick.PickId); + + + object Pick = (object)pick; + if(!JsonRpcRequest(ref Pick, "pickinforequest", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error selecting pick", false); + } + pick = (UserProfilePick) Pick; + if(pick.SnapshotId == UUID.Zero) + { + // In case of a new UserPick, the data may not be ready and we would send wrong data, skip it... + m_log.DebugFormat("[PROFILES]: PickInfoRequest: SnapshotID is {0}", UUID.Zero.ToString()); + return; + } + + Vector3 globalPos; + Vector3.TryParse(pick.GlobalPos,out globalPos); + + m_log.DebugFormat("[PROFILES]: PickInfoRequest: {0} : {1}", pick.Name.ToString(), pick.SnapshotId.ToString()); + + remoteClient.SendPickInfoReply(pick.PickId,pick.CreatorId,pick.TopPick,pick.ParcelId,pick.Name, + pick.Desc,pick.SnapshotId,pick.User,pick.OriginalName,pick.SimName, + globalPos,pick.SortOrder,pick.Enabled); + } + + /// + /// Updates the userpicks + /// + /// + /// Remote client. + /// + /// + /// Pick I. + /// + /// + /// the creator of the pick + /// + /// + /// Top pick. + /// + /// + /// Name. + /// + /// + /// Desc. + /// + /// + /// Snapshot I. + /// + /// + /// Sort order. + /// + /// + /// Enabled. + /// + public void PickInfoUpdate(IClientAPI remoteClient, UUID pickID, UUID creatorID, bool topPick, string name, string desc, UUID snapshotID, int sortOrder, bool enabled) + { + + m_log.DebugFormat("[PROFILES]: Start PickInfoUpdate Name: {0} PickId: {1} SnapshotId: {2}", name, pickID.ToString(), snapshotID.ToString()); + UserProfilePick pick = new UserProfilePick(); + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + ScenePresence p = FindPresence(remoteClient.AgentId); + + Vector3 avaPos = p.AbsolutePosition; + // Getting the global position for the Avatar + Vector3 posGlobal = new Vector3(remoteClient.Scene.RegionInfo.RegionLocX*Constants.RegionSize + avaPos.X, + remoteClient.Scene.RegionInfo.RegionLocY*Constants.RegionSize + avaPos.Y, + avaPos.Z); + + string landOwnerName = string.Empty; + ILandObject land = p.Scene.LandChannel.GetLandObject(avaPos.X, avaPos.Y); + if(land.LandData.IsGroupOwned) + { + IGroupsModule groupMod = p.Scene.RequestModuleInterface(); + UUID groupId = land.LandData.GroupID; + GroupRecord groupRecord = groupMod.GetGroupRecord(groupId); + landOwnerName = groupRecord.GroupName; + } + else + { + IUserAccountService accounts = p.Scene.RequestModuleInterface(); + UserAccount user = accounts.GetUserAccount(p.Scene.RegionInfo.ScopeID, land.LandData.OwnerID); + landOwnerName = user.Name; + } + + pick.PickId = pickID; + pick.CreatorId = creatorID; + pick.TopPick = topPick; + pick.Name = name; + pick.Desc = desc; + pick.ParcelId = p.currentParcelUUID; + pick.SnapshotId = snapshotID; + pick.User = landOwnerName; + pick.SimName = remoteClient.Scene.RegionInfo.RegionName; + pick.GlobalPos = posGlobal.ToString(); + pick.SortOrder = sortOrder; + pick.Enabled = enabled; + + object Pick = (object)pick; + if(!JsonRpcRequest(ref Pick, "picks_update", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error updating pick", false); + } + + m_log.DebugFormat("[PROFILES]: Finish PickInfoUpdate {0} {1}", pick.Name, pick.PickId.ToString()); + } + + /// + /// Delete a Pick + /// + /// + /// Remote client. + /// + /// + /// Query pick I. + /// + public void PickDelete(IClientAPI remoteClient, UUID queryPickID) + { + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + OSDMap parameters= new OSDMap(); + parameters.Add("pickId", OSD.FromUUID(queryPickID)); + OSD Params = (OSD)parameters; + if(!JsonRpcRequest(ref Params, "picks_delete", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error picks delete", false); + } + } + #endregion Picks + + #region Notes + /// + /// Handles the avatar notes request. + /// + /// + /// Sender. + /// + /// + /// Method. + /// + /// + /// Arguments. + /// + public void NotesRequest(Object sender, string method, List args) + { + UserProfileNotes note = new UserProfileNotes(); + + if (!(sender is IClientAPI)) + return; + + IClientAPI remoteClient = (IClientAPI)sender; + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + note.TargetId = remoteClient.AgentId; + UUID.TryParse(args[0], out note.UserId); + + object Note = (object)note; + if(!JsonRpcRequest(ref Note, "avatarnotesrequest", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error requesting note", false); + } + note = (UserProfileNotes) Note; + + remoteClient.SendAvatarNotesReply(note.TargetId, note.Notes); + } + + /// + /// Avatars the notes update. + /// + /// + /// Remote client. + /// + /// + /// Query target I. + /// + /// + /// Query notes. + /// + public void NotesUpdate(IClientAPI remoteClient, UUID queryTargetID, string queryNotes) + { + UserProfileNotes note = new UserProfileNotes(); + + note.UserId = remoteClient.AgentId; + note.TargetId = queryTargetID; + note.Notes = queryNotes; + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + object Note = note; + if(!JsonRpcRequest(ref Note, "avatar_notes_update", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error updating note", false); + } + } + #endregion Notes + + #region Avatar Properties + /// + /// Update the avatars interests . + /// + /// + /// Remote client. + /// + /// + /// Wantmask. + /// + /// + /// Wanttext. + /// + /// + /// Skillsmask. + /// + /// + /// Skillstext. + /// + /// + /// Languages. + /// + public void AvatarInterestsUpdate(IClientAPI remoteClient, uint wantmask, string wanttext, uint skillsmask, string skillstext, string languages) + { + UserProfileProperties prop = new UserProfileProperties(); + + prop.UserId = remoteClient.AgentId; + prop.WantToMask = (int)wantmask; + prop.WantToText = wanttext; + prop.SkillsMask = (int)skillsmask; + prop.SkillsText = skillstext; + prop.Language = languages; + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + object Param = prop; + if(!JsonRpcRequest(ref Param, "avatar_interests_update", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error updating interests", false); + } + } + + public void BasicRequestProperties(IClientAPI remoteClient, UUID avatarID) + { + IScene s = remoteClient.Scene; + if (!(s is Scene)) + return; + + string profileUrl = String.Empty; + string aboutText = String.Empty; + string firstLifeAboutText = String.Empty; + UUID image = UUID.Zero; + UUID firstLifeImage = UUID.Zero; + UUID partner = UUID.Zero; + uint wantMask = 0; + string wantText = String.Empty; + uint skillsMask = 0; + string skillsText = String.Empty; + string languages = String.Empty; + + UserAccount account = Scene.UserAccountService.GetUserAccount(Scene.RegionInfo.ScopeID, avatarID); + + string name = "Avatar"; + int created = 0; + if (account != null) + { + name = account.FirstName + " " + account.LastName; + created = account.Created; + } + Byte[] charterMember = Utils.StringToBytes(name); + + profileUrl = "No profile data"; + aboutText = string.Empty; + firstLifeAboutText = string.Empty; + image = UUID.Zero; + firstLifeImage = UUID.Zero; + partner = UUID.Zero; + + remoteClient.SendAvatarProperties(avatarID, aboutText, + Util.ToDateTime(created).ToString( + "M/d/yyyy", CultureInfo.InvariantCulture), + charterMember, firstLifeAboutText, + (uint)(0 & 0xff), + firstLifeImage, image, profileUrl, partner); + + //Viewer expects interest data when it asks for properties. + remoteClient.SendAvatarInterestsReply(avatarID, wantMask, wantText, + skillsMask, skillsText, languages); + } + + /// + /// Requests the avatar properties. + /// + /// + /// Remote client. + /// + /// + /// Avatar I. + /// + public void RequestAvatarProperties(IClientAPI remoteClient, UUID avatarID) + { + if ( String.IsNullOrEmpty(avatarID.ToString()) || String.IsNullOrEmpty(remoteClient.AgentId.ToString())) + { + // Looking for a reason that some viewers are sending null Id's + m_log.DebugFormat("[PROFILES]: This should not happen remoteClient.AgentId {0} - avatarID {1}", remoteClient.AgentId, avatarID); + return; + } + + // Can't handle NPC yet... + ScenePresence p = FindPresence(avatarID); + + if (null != p) + { + if (p.PresenceType == PresenceType.Npc) + return; + } + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(avatarID, out serverURI); + + UserAccount account = null; + Dictionary userInfo; + + if (!foreign) + { + account = Scene.UserAccountService.GetUserAccount(Scene.RegionInfo.ScopeID, avatarID); + } + else + { + userInfo = new Dictionary(); + } + + Byte[] charterMember = new Byte[1]; + string born = String.Empty; + uint flags = 0x00; + + if (null != account) + { + if (account.UserTitle == "") + { + charterMember[0] = (Byte)((account.UserFlags & 0xf00) >> 8); + } + else + { + charterMember = Utils.StringToBytes(account.UserTitle); + } + + born = Util.ToDateTime(account.Created).ToString( + "M/d/yyyy", CultureInfo.InvariantCulture); + flags = (uint)(account.UserFlags & 0xff); + } + else + { + if (GetUserAccountData(avatarID, out userInfo) == true) + { + if ((string)userInfo["user_title"] == "") + { + charterMember[0] = (Byte)(((Byte)userInfo["user_flags"] & 0xf00) >> 8); + } + else + { + charterMember = Utils.StringToBytes((string)userInfo["user_title"]); + } + + int val_born = (int)userInfo["user_created"]; + born = Util.ToDateTime(val_born).ToString( + "M/d/yyyy", CultureInfo.InvariantCulture); + + // picky, picky + int val_flags = (int)userInfo["user_flags"]; + flags = (uint)(val_flags & 0xff); + } + } + + UserProfileProperties props = new UserProfileProperties(); + string result = string.Empty; + + props.UserId = avatarID; + GetProfileData(ref props, out result); + + remoteClient.SendAvatarProperties(props.UserId, props.AboutText, born, charterMember , props.FirstLifeText, flags, + props.FirstLifeImageId, props.ImageId, props.WebUrl, props.PartnerId); + + + remoteClient.SendAvatarInterestsReply(props.UserId, (uint)props.WantToMask, props.WantToText, (uint)props.SkillsMask, + props.SkillsText, props.Language); + } + + /// + /// Updates the avatar properties. + /// + /// + /// Remote client. + /// + /// + /// New profile. + /// + public void AvatarPropertiesUpdate(IClientAPI remoteClient, UserProfileData newProfile) + { + if (remoteClient.AgentId == newProfile.ID) + { + UserProfileProperties prop = new UserProfileProperties(); + + prop.UserId = remoteClient.AgentId; + prop.WebUrl = newProfile.ProfileUrl; + prop.ImageId = newProfile.Image; + prop.AboutText = newProfile.AboutText; + prop.FirstLifeImageId = newProfile.FirstLifeImage; + prop.FirstLifeText = newProfile.FirstLifeAboutText; + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(remoteClient.AgentId, out serverURI); + + object Prop = prop; + + if(!JsonRpcRequest(ref Prop, "avatar_properties_update", serverURI, UUID.Random().ToString())) + { + remoteClient.SendAgentAlertMessage( + "Error updating properties", false); + } + + RequestAvatarProperties(remoteClient, newProfile.ID); + } + } + + + /// + /// Gets the profile data. + /// + /// + /// The profile data. + /// + /// + /// User I. + /// + bool GetProfileData(ref UserProfileProperties properties, out string message) + { + // Can't handle NPC yet... + ScenePresence p = FindPresence(properties.UserId); + + if (null != p) + { + if (p.PresenceType == PresenceType.Npc) + { + message = "Id points to NPC"; + return false; + } + } + + string serverURI = string.Empty; + bool foreign = GetUserProfileServerURI(properties.UserId, out serverURI); + + // This is checking a friend on the home grid + // Not HG friend + if ( String.IsNullOrEmpty(serverURI)) + { + message = "No Presence - foreign friend"; + return false; + } + + object Prop = (object)properties; + JsonRpcRequest(ref Prop, "avatar_properties_request", serverURI, UUID.Random().ToString()); + properties = (UserProfileProperties)Prop; + + message = "Success"; + return true; + } + #endregion Avatar Properties + + #region Utils + bool GetImageAssets(UUID avatarId) + { + string profileServerURI = string.Empty; + string assetServerURI = string.Empty; + + bool foreign = GetUserProfileServerURI(avatarId, out profileServerURI); + + if(!foreign) + return true; + + assetServerURI = UserManagementModule.GetUserServerURL(avatarId, "AssetServerURI"); + + OSDMap parameters= new OSDMap(); + parameters.Add("avatarId", OSD.FromUUID(avatarId)); + OSD Params = (OSD)parameters; + if(!JsonRpcRequest(ref Params, "image_assets_request", profileServerURI, UUID.Random().ToString())) + { + // Error Handling here! + // if(parameters.ContainsKey("message") + return false; + } + + parameters = (OSDMap)Params; + + OSDArray list = (OSDArray)parameters["result"]; + + foreach(OSD asset in list) + { + OSDString assetId = (OSDString)asset; + + Scene.AssetService.Get(string.Format("{0}/{1}",assetServerURI, assetId.AsString()), this, + delegate (string assetID, Object s, AssetBase a) + { + // m_log.DebugFormat("[PROFILES]: Getting Image Assets {0}", assetID); + return; + }); + } + return true; + } + + /// + /// Gets the user account data. + /// + /// + /// The user profile data. + /// + /// + /// If set to true user I. + /// + /// + /// If set to true user info. + /// + bool GetUserAccountData(UUID userID, out Dictionary userInfo) + { + Dictionary info = new Dictionary(); + + if (UserManagementModule.IsLocalGridUser(userID)) + { + // Is local + IUserAccountService uas = Scene.UserAccountService; + UserAccount account = uas.GetUserAccount(Scene.RegionInfo.ScopeID, userID); + + info["user_flags"] = account.UserFlags; + info["user_created"] = account.Created; + + if (!String.IsNullOrEmpty(account.UserTitle)) + info["user_title"] = account.UserTitle; + else + info["user_title"] = ""; + + userInfo = info; + + return false; + } + else + { + // Is Foreign + string home_url = UserManagementModule.GetUserServerURL(userID, "HomeURI"); + + if (String.IsNullOrEmpty(home_url)) + { + info["user_flags"] = 0; + info["user_created"] = 0; + info["user_title"] = "Unavailable"; + + userInfo = info; + return true; + } + + UserAgentServiceConnector uConn = new UserAgentServiceConnector(home_url); + + Dictionary account = uConn.GetUserInfo(userID); + + if (account.Count > 0) + { + if (account.ContainsKey("user_flags")) + info["user_flags"] = account["user_flags"]; + else + info["user_flags"] = ""; + + if (account.ContainsKey("user_created")) + info["user_created"] = account["user_created"]; + else + info["user_created"] = ""; + + info["user_title"] = "HG Visitor"; + } + else + { + info["user_flags"] = 0; + info["user_created"] = 0; + info["user_title"] = "HG Visitor"; + } + userInfo = info; + return true; + } + } + + /// + /// Gets the user profile server UR. + /// + /// + /// The user profile server UR. + /// + /// + /// If set to true user I. + /// + /// + /// If set to true server UR. + /// + bool GetUserProfileServerURI(UUID userID, out string serverURI) + { + bool local; + local = UserManagementModule.IsLocalGridUser(userID); + + if (!local) + { + serverURI = UserManagementModule.GetUserServerURL(userID, "ProfileServerURI"); + // Is Foreign + return true; + } + else + { + serverURI = ProfileServerUri; + // Is local + return false; + } + } + + /// + /// Finds the presence. + /// + /// + /// The presence. + /// + /// + /// Client I. + /// + ScenePresence FindPresence(UUID clientID) + { + ScenePresence p; + + p = Scene.GetScenePresence(clientID); + if (p != null && !p.IsChildAgent) + return p; + + return null; + } + #endregion Util + + #region Web Util + /// + /// Sends json-rpc request with a serializable type. + /// + /// + /// OSD Map. + /// + /// + /// Serializable type . + /// + /// + /// Json-rpc method to call. + /// + /// + /// URI of json-rpc service. + /// + /// + /// Id for our call. + /// + bool JsonRpcRequest(ref object parameters, string method, string uri, string jsonId) + { + if (jsonId == null) + throw new ArgumentNullException ("jsonId"); + if (uri == null) + throw new ArgumentNullException ("uri"); + if (method == null) + throw new ArgumentNullException ("method"); + if (parameters == null) + throw new ArgumentNullException ("parameters"); + + // Prep our payload + OSDMap json = new OSDMap(); + + json.Add("jsonrpc", OSD.FromString("2.0")); + json.Add("id", OSD.FromString(jsonId)); + json.Add("method", OSD.FromString(method)); + // Experiment + json.Add("params", OSD.SerializeMembers(parameters)); + + string jsonRequestData = OSDParser.SerializeJsonString(json); + byte[] content = Encoding.UTF8.GetBytes(jsonRequestData); + + HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri); + // webRequest.Credentials = new NetworkCredential(rpcUser, rpcPass); + webRequest.ContentType = "application/json-rpc"; + webRequest.Method = "POST"; + + Stream dataStream = webRequest.GetRequestStream(); + dataStream.Write(content, 0, content.Length); + dataStream.Close(); + + WebResponse webResponse = null; + try + { + webResponse = webRequest.GetResponse(); + } + catch (WebException e) + { + Console.WriteLine("Web Error" + e.Message); + Console.WriteLine ("Please check input"); + return false; + } + + byte[] buf = new byte[8192]; + Stream rstream = webResponse.GetResponseStream(); + OSDMap mret = (OSDMap)OSDParser.DeserializeJson(rstream); + + if(mret.ContainsKey("error")) + return false; + + // get params... + OSD.DeserializeMembers(ref parameters, (OSDMap) mret["result"]); + return true; + } + + /// + /// Sends json-rpc request with OSD parameter. + /// + /// + /// The rpc request. + /// + /// + /// data - incoming as parameters, outgong as result/error + /// + /// + /// Json-rpc method to call. + /// + /// + /// URI of json-rpc service. + /// + /// + /// If set to true json identifier. + /// + bool JsonRpcRequest(ref OSD data, string method, string uri, string jsonId) + { + OSDMap map = new OSDMap(); + + map["jsonrpc"] = "2.0"; + if(string.IsNullOrEmpty(jsonId)) + map["id"] = UUID.Random().ToString(); + else + map["id"] = jsonId; + + map["method"] = method; + map["params"] = data; + + string jsonRequestData = OSDParser.SerializeJsonString(map); + byte[] content = Encoding.UTF8.GetBytes(jsonRequestData); + + HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri); + webRequest.ContentType = "application/json-rpc"; + webRequest.Method = "POST"; + + Stream dataStream = webRequest.GetRequestStream(); + dataStream.Write(content, 0, content.Length); + dataStream.Close(); + + WebResponse webResponse = null; + try + { + webResponse = webRequest.GetResponse(); + } + catch (WebException e) + { + Console.WriteLine("Web Error" + e.Message); + Console.WriteLine ("Please check input"); + return false; + } + + byte[] buf = new byte[8192]; + Stream rstream = webResponse.GetResponseStream(); + + OSDMap response = new OSDMap(); + response = (OSDMap)OSDParser.DeserializeJson(rstream); + if(response.ContainsKey("error")) + { + data = response["error"]; + return false; + } + + data = response; + + return true; + } + #endregion Web Util + } +} diff --git a/OpenSim/Region/CoreModules/ServiceConnectorsIn/UserProfiles/LocalUserProfilesServiceConnector.cs b/OpenSim/Region/CoreModules/ServiceConnectorsIn/UserProfiles/LocalUserProfilesServiceConnector.cs new file mode 100644 index 0000000000..323535ab07 --- /dev/null +++ b/OpenSim/Region/CoreModules/ServiceConnectorsIn/UserProfiles/LocalUserProfilesServiceConnector.cs @@ -0,0 +1,226 @@ + +/* + * 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 log4net; +using Mono.Addins; +using Nini.Config; +using System; +using System.Collections.Generic; +using System.Reflection; +using OpenSim.Framework; +using OpenSim.Framework.Console; +using OpenSim.Server.Base; +using OpenSim.Server.Handlers; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Framework.Servers; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; +using GridRegion = OpenSim.Services.Interfaces.GridRegion; +using OpenMetaverse; + +namespace OpenSim.Region.CoreModules.ServiceConnectorsOut.Profile +{ + [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "LocalUserProfilesServicesConnector")] + public class LocalUserProfilesServicesConnector : ISharedRegionModule + { + private static readonly ILog m_log = + LogManager.GetLogger( + MethodBase.GetCurrentMethod().DeclaringType); + + private Dictionary regions = new Dictionary(); + + public IUserProfilesService ServiceModule + { + get; private set; + } + + public bool Enabled + { + get; private set; + } + + public string Name + { + get + { + return "LocalUserProfilesServicesConnector"; + } + } + + public string ConfigName + { + get; private set; + } + + public Type ReplaceableInterface + { + get { return null; } + } + + public LocalUserProfilesServicesConnector() + { + m_log.Debug("[LOCAL USERPROFILES SERVICE CONNECTOR]: LocalUserProfileServicesConnector no params"); + } + + public LocalUserProfilesServicesConnector(IConfigSource source) + { + m_log.Debug("[LOCAL USERPROFILES SERVICE CONNECTOR]: LocalUserProfileServicesConnector instantiated directly."); + InitialiseService(source); + } + + public void InitialiseService(IConfigSource source) + { + ConfigName = "UserProfilesService"; + + // Instantiate the request handler + IHttpServer Server = MainServer.Instance; + + IConfig config = source.Configs[ConfigName]; + if (config == null) + { + m_log.Error("[LOCAL USERPROFILES SERVICE CONNECTOR]: UserProfilesService missing from OpenSim.ini"); + return; + } + + if(!config.GetBoolean("Enabled",false)) + { + Enabled = false; + return; + } + + Enabled = true; + + string serviceDll = config.GetString("LocalServiceModule", + String.Empty); + + if (serviceDll == String.Empty) + { + m_log.Error("[LOCAL USERPROFILES SERVICE CONNECTOR]: No LocalServiceModule named in section UserProfilesService"); + return; + } + + Object[] args = new Object[] { source, ConfigName }; + ServiceModule = + ServerUtils.LoadPlugin(serviceDll, + args); + + if (ServiceModule == null) + { + m_log.Error("[LOCAL USERPROFILES SERVICE CONNECTOR]: Can't load user profiles service"); + return; + } + + Enabled = true; + + JsonRpcProfileHandlers handler = new JsonRpcProfileHandlers(ServiceModule); + + Server.AddJsonRPCHandler("avatarclassifiedsrequest", handler.AvatarClassifiedsRequest); + Server.AddJsonRPCHandler("classified_update", handler.ClassifiedUpdate); + Server.AddJsonRPCHandler("classifieds_info_query", handler.ClassifiedInfoRequest); + Server.AddJsonRPCHandler("classified_delete", handler.ClassifiedDelete); + Server.AddJsonRPCHandler("avatarpicksrequest", handler.AvatarPicksRequest); + Server.AddJsonRPCHandler("pickinforequest", handler.PickInfoRequest); + Server.AddJsonRPCHandler("picks_update", handler.PicksUpdate); + Server.AddJsonRPCHandler("picks_delete", handler.PicksDelete); + Server.AddJsonRPCHandler("avatarnotesrequest", handler.AvatarNotesRequest); + Server.AddJsonRPCHandler("avatar_notes_update", handler.NotesUpdate); + Server.AddJsonRPCHandler("avatar_properties_request", handler.AvatarPropertiesRequest); + Server.AddJsonRPCHandler("avatar_properties_update", handler.AvatarPropertiesUpdate); + Server.AddJsonRPCHandler("avatar_interests_update", handler.AvatarInterestsUpdate); + Server.AddJsonRPCHandler("image_assets_request", handler.AvatarImageAssetsRequest); + Server.AddJsonRPCHandler("user_data_request", handler.RequestUserAppData); + Server.AddJsonRPCHandler("user_data_update", handler.UpdateUserAppData); + + } + + #region ISharedRegionModule implementation + + void ISharedRegionModule.PostInitialise() + { + if(!Enabled) + return; + } + + #endregion + + #region IRegionModuleBase implementation + + void IRegionModuleBase.Initialise(IConfigSource source) + { + IConfig moduleConfig = source.Configs["Modules"]; + if (moduleConfig != null) + { + string name = moduleConfig.GetString("UserProfilesServices", ""); + if (name == Name) + { + InitialiseService(source); + m_log.Info("[LOCAL USERPROFILES SERVICE CONNECTOR]: Local user profiles connector enabled"); + } + } + } + + void IRegionModuleBase.Close() + { + return; + } + + void IRegionModuleBase.AddRegion(Scene scene) + { + if (!Enabled) + return; + + lock (regions) + { + if (regions.ContainsKey(scene.RegionInfo.RegionID)) + m_log.ErrorFormat("[LOCAL USERPROFILES SERVICE CONNECTOR]: simulator seems to have more than one region with the same UUID. Please correct this!"); + else + regions.Add(scene.RegionInfo.RegionID, scene); + } + } + + void IRegionModuleBase.RemoveRegion(Scene scene) + { + if (!Enabled) + return; + + lock (regions) + { + if (regions.ContainsKey(scene.RegionInfo.RegionID)) + regions.Remove(scene.RegionInfo.RegionID); + } + } + + void IRegionModuleBase.RegionLoaded(Scene scene) + { + if (!Enabled) + return; + } + #endregion + } +} \ No newline at end of file diff --git a/OpenSim/Region/CoreModules/ServiceConnectorsOut/Grid/LocalGridServiceConnector.cs b/OpenSim/Region/CoreModules/ServiceConnectorsOut/Grid/LocalGridServiceConnector.cs index c32820e94d..3849a3fb91 100644 --- a/OpenSim/Region/CoreModules/ServiceConnectorsOut/Grid/LocalGridServiceConnector.cs +++ b/OpenSim/Region/CoreModules/ServiceConnectorsOut/Grid/LocalGridServiceConnector.cs @@ -56,6 +56,7 @@ namespace OpenSim.Region.CoreModules.ServiceConnectorsOut.Grid public LocalGridServicesConnector() { + m_log.Debug("[LOCAL GRID SERVICE CONNECTOR]: LocalGridServicesConnector no parms."); } public LocalGridServicesConnector(IConfigSource source) diff --git a/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs b/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs new file mode 100644 index 0000000000..4ad729748b --- /dev/null +++ b/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs @@ -0,0 +1,92 @@ +using System; +using System.Reflection; +using Nini.Config; +using OpenSim.Server.Base; +using OpenSim.Services.Interfaces; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Framework; +using OpenSim.Server.Handlers.Base; +using log4net; + +namespace OpenSim.Server.Handlers.Profiles +{ + public class UserProfilesConnector: ServiceConnector + { + static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + + // Our Local Module + public IUserProfilesService ServiceModule + { + get; private set; + } + + // The HTTP server. + public IHttpServer Server + { + get; private set; + } + + public string ConfigName + { + get; private set; + } + + public bool Enabled + { + get; private set; + } + + public UserProfilesConnector(IConfigSource config, IHttpServer server, string configName) : + base(config, server, configName) + { + ConfigName = "UserProfilesService"; + if(!string.IsNullOrEmpty(configName)) + ConfigName = configName; + + IConfig serverConfig = config.Configs[ConfigName]; + if (serverConfig == null) + throw new Exception(String.Format("No section {0} in config file", ConfigName)); + + if(!serverConfig.GetBoolean("Enabled",false)) + { + Enabled = false; + return; + } + + Enabled = true; + + Server = server; + + string service = serverConfig.GetString("LocalServiceModule", String.Empty); + + Object[] args = new Object[] { config, ConfigName }; + ServiceModule = ServerUtils.LoadPlugin(service, args); + + JsonRpcProfileHandlers handler = new JsonRpcProfileHandlers(ServiceModule); + + Server.AddJsonRPCHandler("avatarclassifiedsrequest", handler.AvatarClassifiedsRequest); + Server.AddJsonRPCHandler("classified_update", handler.ClassifiedUpdate); + Server.AddJsonRPCHandler("classifieds_info_query", handler.ClassifiedInfoRequest); + Server.AddJsonRPCHandler("classified_delete", handler.ClassifiedDelete); + Server.AddJsonRPCHandler("avatarpicksrequest", handler.AvatarPicksRequest); + Server.AddJsonRPCHandler("pickinforequest", handler.PickInfoRequest); + Server.AddJsonRPCHandler("picks_update", handler.PicksUpdate); + Server.AddJsonRPCHandler("picks_delete", handler.PicksDelete); + Server.AddJsonRPCHandler("avatarnotesrequest", handler.AvatarNotesRequest); + Server.AddJsonRPCHandler("avatar_notes_update", handler.NotesUpdate); + Server.AddJsonRPCHandler("avatar_properties_request", handler.AvatarPropertiesRequest); + Server.AddJsonRPCHandler("avatar_properties_update", handler.AvatarPropertiesUpdate); + Server.AddJsonRPCHandler("avatar_interests_update", handler.AvatarInterestsUpdate); + Server.AddJsonRPCHandler("image_assets_request", handler.AvatarImageAssetsRequest); +// Server.AddJsonRPCHandler("user_preferences_request", handler.UserPreferencesRequest); +// Server.AddJsonRPCHandler("user_preferences_update", handler.UserPreferencesUpdate); +// Server.AddJsonRPCHandler("user_account_create", handler.UserAccountCreate); +// Server.AddJsonRPCHandler("user_account_auth", handler.UserAccountAuth); +// Server.AddJsonRPCHandler("user_account_test", handler.UserAccountTest); + Server.AddJsonRPCHandler("user_data_request", handler.RequestUserAppData); + Server.AddJsonRPCHandler("user_data_update", handler.UpdateUserAppData); + } + } +} + diff --git a/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs b/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs new file mode 100644 index 0000000000..93da102b8e --- /dev/null +++ b/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs @@ -0,0 +1,434 @@ +using System; +using System.Reflection; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using log4net; +using OpenSim.Services.Interfaces; +using OpenSim.Framework.Servers.HttpServer; +using OpenSim.Framework; + +namespace OpenSim.Server.Handlers +{ + public class UserProfilesHandlers + { + public UserProfilesHandlers () + { + } + } + + public class JsonRpcProfileHandlers + { + static readonly ILog m_log = + LogManager.GetLogger( + MethodBase.GetCurrentMethod().DeclaringType); + + public IUserProfilesService Service + { + get; private set; + } + + public JsonRpcProfileHandlers(IUserProfilesService service) + { + Service = service; + } + + #region Classifieds + /// + /// Request avatar's classified ads. + /// + /// + /// An array containing all the calassified uuid and it's name created by the creator id + /// + /// + /// Our parameters are in the OSDMap json["params"] + /// + /// + /// If set to true response. + /// + public bool AvatarClassifiedsRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + m_log.DebugFormat ("Classified Request"); + return false; + } + + OSDMap request = (OSDMap)json["params"]; + UUID creatorId = new UUID(request["creatorId"].AsString()); + + + OSDArray data = (OSDArray) Service.AvatarClassifiedsRequest(creatorId); + response.Result = data; + + return true; + } + + public bool ClassifiedUpdate(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "Error parsing classified update request"; + m_log.DebugFormat ("Classified Update Request"); + return false; + } + + string result = string.Empty; + UserClassifiedAdd ad = new UserClassifiedAdd(); + object Ad = (object)ad; + OSD.DeserializeMembers(ref Ad, (OSDMap)json["params"]); + if(Service.ClassifiedUpdate(ad, ref result)) + { + response.Result = OSD.SerializeMembers(ad); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + + public bool ClassifiedDelete(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + m_log.DebugFormat ("Classified Delete Request"); + return false; + } + + OSDMap request = (OSDMap)json["params"]; + UUID classifiedId = new UUID(request["classifiedID"].AsString()); + + OSDMap res = new OSDMap(); + res["result"] = OSD.FromString("success"); + response.Result = res; + + return true; + + } + + public bool ClassifiedInfoRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Classified Info Request"); + return false; + } + + string result = string.Empty; + UserClassifiedAdd ad = new UserClassifiedAdd(); + object Ad = (object)ad; + OSD.DeserializeMembers(ref Ad, (OSDMap)json["params"]); + if(Service.ClassifiedInfoRequest(ref ad, ref result)) + { + response.Result = OSD.SerializeMembers(ad); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + #endregion Classifieds + + #region Picks + public bool AvatarPicksRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + m_log.DebugFormat ("Avatar Picks Request"); + return false; + } + + OSDMap request = (OSDMap)json["params"]; + UUID creatorId = new UUID(request["creatorId"].AsString()); + + + OSDArray data = (OSDArray) Service.AvatarPicksRequest(creatorId); + response.Result = data; + + return true; + } + + public bool PickInfoRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Avatar Picks Info Request"); + return false; + } + + string result = string.Empty; + UserProfilePick pick = new UserProfilePick(); + object Pick = (object)pick; + OSD.DeserializeMembers(ref Pick, (OSDMap)json["params"]); + if(Service.PickInfoRequest(ref pick, ref result)) + { + response.Result = OSD.SerializeMembers(pick); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + + public bool PicksUpdate(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Avatar Picks Update Request"); + return false; + } + + string result = string.Empty; + UserProfilePick pick = new UserProfilePick(); + object Pick = (object)pick; + OSD.DeserializeMembers(ref Pick, (OSDMap)json["params"]); + if(Service.PicksUpdate(ref pick, ref result)) + { + response.Result = OSD.SerializeMembers(pick); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = "unable to update pick"; + + return false; + } + + public bool PicksDelete(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + m_log.DebugFormat ("Avatar Picks Delete Request"); + return false; + } + + OSDMap request = (OSDMap)json["params"]; + UUID pickId = new UUID(request["pickId"].AsString()); + if(Service.PicksDelete(pickId)) + return true; + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = "data error removing record"; + return false; + } + #endregion Picks + + #region Notes + public bool AvatarNotesRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "Params missing"; + m_log.DebugFormat ("Avatar Notes Request"); + return false; + } + + string result = string.Empty; + UserProfileNotes note = new UserProfileNotes(); + object Note = (object)note; + OSD.DeserializeMembers(ref Note, (OSDMap)json["params"]); + if(Service.AvatarNotesRequest(ref note)) + { + response.Result = OSD.SerializeMembers(note); + return true; + } + + object Notes = (object) note; + OSD.DeserializeMembers(ref Notes, (OSDMap)json["params"]); + return true; + } + + public bool NotesUpdate(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "No parameters"; + m_log.DebugFormat ("Avatar Notes Update Request"); + return false; + } + + string result = string.Empty; + UserProfileNotes note = new UserProfileNotes(); + object Notes = (object) note; + OSD.DeserializeMembers(ref Notes, (OSDMap)json["params"]); + if(Service.NotesUpdate(ref note, ref result)) + { + response.Result = OSD.SerializeMembers(note); + return true; + } + return true; + } + #endregion Notes + + #region Profile Properties + public bool AvatarPropertiesRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Avatar Properties Request"); + return false; + } + + string result = string.Empty; + UserProfileProperties props = new UserProfileProperties(); + object Props = (object)props; + OSD.DeserializeMembers(ref Props, (OSDMap)json["params"]); + if(Service.AvatarPropertiesRequest(ref props, ref result)) + { + response.Result = OSD.SerializeMembers(props); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + + public bool AvatarPropertiesUpdate(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Avatar Properties Update Request"); + return false; + } + + string result = string.Empty; + UserProfileProperties props = new UserProfileProperties(); + object Props = (object)props; + OSD.DeserializeMembers(ref Props, (OSDMap)json["params"]); + if(Service.AvatarPropertiesUpdate(ref props, ref result)) + { + response.Result = OSD.SerializeMembers(props); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + #endregion Profile Properties + + #region Interests + public bool AvatarInterestsUpdate(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("Avatar Interests Update Request"); + return false; + } + + string result = string.Empty; + UserProfileProperties props = new UserProfileProperties(); + object Props = (object)props; + OSD.DeserializeMembers(ref Props, (OSDMap)json["params"]); + if(Service.AvatarInterestsUpdate(props, ref result)) + { + response.Result = OSD.SerializeMembers(props); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + #endregion Interests + + #region Utility + public bool AvatarImageAssetsRequest(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + m_log.DebugFormat ("Avatar Image Assets Request"); + return false; + } + + OSDMap request = (OSDMap)json["params"]; + UUID avatarId = new UUID(request["avatarId"].AsString()); + + OSDArray data = (OSDArray) Service.AvatarImageAssetsRequest(avatarId); + response.Result = data; + + return true; + } + #endregion Utiltiy + + #region UserData + public bool RequestUserAppData(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("User Application Service URL Request: No Parameters!"); + return false; + } + + string result = string.Empty; + UserAppData props = new UserAppData(); + object Props = (object)props; + OSD.DeserializeMembers(ref Props, (OSDMap)json["params"]); + if(Service.RequestUserAppData(ref props, ref result)) + { + OSDMap res = new OSDMap(); + res["result"] = OSD.FromString("success"); + res["token"] = OSD.FromString (result); + response.Result = res; + + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + + public bool UpdateUserAppData(OSDMap json, ref JsonRpcResponse response) + { + if(!json.ContainsKey("params")) + { + response.Error.Code = ErrorCode.ParseError; + response.Error.Message = "no parameters supplied"; + m_log.DebugFormat ("User App Data Update Request"); + return false; + } + + string result = string.Empty; + UserAppData props = new UserAppData(); + object Props = (object)props; + OSD.DeserializeMembers(ref Props, (OSDMap)json["params"]); + if(Service.SetUserAppData(props, ref result)) + { + response.Result = OSD.SerializeMembers(props); + return true; + } + + response.Error.Code = ErrorCode.InternalError; + response.Error.Message = string.Format("{0}", result); + return false; + } + #endregion UserData + } +} + diff --git a/OpenSim/Services/Interfaces/IUserProfilesService.cs b/OpenSim/Services/Interfaces/IUserProfilesService.cs new file mode 100644 index 0000000000..12fc986a76 --- /dev/null +++ b/OpenSim/Services/Interfaces/IUserProfilesService.cs @@ -0,0 +1,48 @@ +using System; +using OpenSim.Framework; +using OpenMetaverse; +using OpenMetaverse.StructuredData; + +namespace OpenSim.Services.Interfaces +{ + public interface IUserProfilesService + { + #region Classifieds + OSD AvatarClassifiedsRequest(UUID creatorId); + bool ClassifiedUpdate(UserClassifiedAdd ad, ref string result); + bool ClassifiedInfoRequest(ref UserClassifiedAdd ad, ref string result); + bool ClassifiedDelete(UUID recordId); + #endregion Classifieds + + #region Picks + OSD AvatarPicksRequest(UUID creatorId); + bool PickInfoRequest(ref UserProfilePick pick, ref string result); + bool PicksUpdate(ref UserProfilePick pick, ref string result); + bool PicksDelete(UUID pickId); + #endregion Picks + + #region Notes + bool AvatarNotesRequest(ref UserProfileNotes note); + bool NotesUpdate(ref UserProfileNotes note, ref string result); + #endregion Notes + + #region Profile Properties + bool AvatarPropertiesRequest(ref UserProfileProperties prop, ref string result); + bool AvatarPropertiesUpdate(ref UserProfileProperties prop, ref string result); + #endregion Profile Properties + + #region Interests + bool AvatarInterestsUpdate(UserProfileProperties prop, ref string result); + #endregion Interests + + #region Utility + OSD AvatarImageAssetsRequest(UUID avatarId); + #endregion Utility + + #region UserData + bool RequestUserAppData(ref UserAppData prop, ref string result); + bool SetUserAppData(UserAppData prop, ref string result); + #endregion UserData + } +} + diff --git a/OpenSim/Services/UserProfilesService/UserProfilesService.cs b/OpenSim/Services/UserProfilesService/UserProfilesService.cs new file mode 100644 index 0000000000..959c661b5f --- /dev/null +++ b/OpenSim/Services/UserProfilesService/UserProfilesService.cs @@ -0,0 +1,160 @@ +using System; +using System.Reflection; +using System.Text; +using Nini.Config; +using log4net; +using OpenSim.Server.Base; +using OpenSim.Services.Interfaces; +using OpenSim.Services.UserAccountService; +using OpenSim.Data; +using OpenMetaverse; +using OpenMetaverse.StructuredData; +using OpenSim.Framework; + +namespace OpenSim.Services.ProfilesService +{ + public class UserProfilesService: UserProfilesServiceBase, IUserProfilesService + { + static readonly ILog m_log = + LogManager.GetLogger( + MethodBase.GetCurrentMethod().DeclaringType); + + IUserAccountService userAccounts; + IAuthenticationService authService; + + public UserProfilesService(IConfigSource config, string configName): + base(config, configName) + { + IConfig Config = config.Configs[configName]; + if (Config == null) + { + m_log.Warn("[PROFILES]: No configuration found!"); + return; + } + Object[] args = null; + + args = new Object[] { config }; + string accountService = Config.GetString("UserAccountService", String.Empty); + if (accountService != string.Empty) + userAccounts = ServerUtils.LoadPlugin(accountService, args); + + args = new Object[] { config }; + string authServiceConfig = Config.GetString("AuthenticationServiceModule", String.Empty); + if (accountService != string.Empty) + authService = ServerUtils.LoadPlugin(authServiceConfig, args); + } + + #region Classifieds + public OSD AvatarClassifiedsRequest(UUID creatorId) + { + OSDArray records = ProfilesData.GetClassifiedRecords(creatorId); + + return records; + } + + public bool ClassifiedUpdate(UserClassifiedAdd ad, ref string result) + { + if(!ProfilesData.UpdateClassifiedRecord(ad, ref result)) + { + return false; + } + result = "success"; + return true; + } + + public bool ClassifiedDelete(UUID recordId) + { + if(ProfilesData.DeleteClassifiedRecord(recordId)) + return true; + + return false; + } + + public bool ClassifiedInfoRequest(ref UserClassifiedAdd ad, ref string result) + { + if(ProfilesData.GetClassifiedInfo(ref ad, ref result)) + return true; + + return false; + } + #endregion Classifieds + + #region Picks + public OSD AvatarPicksRequest(UUID creatorId) + { + OSDArray records = ProfilesData.GetAvatarPicks(creatorId); + + return records; + } + + public bool PickInfoRequest(ref UserProfilePick pick, ref string result) + { + pick = ProfilesData.GetPickInfo(pick.CreatorId, pick.PickId); + result = "OK"; + return true; + } + + public bool PicksUpdate(ref UserProfilePick pick, ref string result) + { + return ProfilesData.UpdatePicksRecord(pick); + } + + public bool PicksDelete(UUID pickId) + { + return ProfilesData.DeletePicksRecord(pickId); + } + #endregion Picks + + #region Notes + public bool AvatarNotesRequest(ref UserProfileNotes note) + { + return ProfilesData.GetAvatarNotes(ref note); + } + + public bool NotesUpdate(ref UserProfileNotes note, ref string result) + { + return ProfilesData.UpdateAvatarNotes(ref note, ref result); + } + #endregion Notes + + #region Profile Properties + public bool AvatarPropertiesRequest(ref UserProfileProperties prop, ref string result) + { + return ProfilesData.GetAvatarProperties(ref prop, ref result); + } + + public bool AvatarPropertiesUpdate(ref UserProfileProperties prop, ref string result) + { + return ProfilesData.UpdateAvatarProperties(ref prop, ref result); + } + #endregion Profile Properties + + #region Interests + public bool AvatarInterestsUpdate(UserProfileProperties prop, ref string result) + { + return ProfilesData.UpdateAvatarInterests(prop, ref result); + } + #endregion Interests + + #region Utility + public OSD AvatarImageAssetsRequest(UUID avatarId) + { + OSDArray records = ProfilesData.GetUserImageAssets(avatarId); + return records; + } + #endregion Utility + + #region UserData + public bool RequestUserAppData(ref UserAppData prop, ref string result) + { + return ProfilesData.GetUserAppData(ref prop, ref result); + } + + public bool SetUserAppData(UserAppData prop, ref string result) + { + return true; + } + #endregion UserData + } +} + diff --git a/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs b/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs new file mode 100644 index 0000000000..dc0be038c5 --- /dev/null +++ b/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Reflection; +using Nini.Config; +using log4net; +using OpenSim.Services.Base; +using OpenSim.Data; + +namespace OpenSim.Services.ProfilesService +{ + public class UserProfilesServiceBase: ServiceBase + { + static readonly ILog m_log = + LogManager.GetLogger( + MethodBase.GetCurrentMethod().DeclaringType); + + public IProfilesData ProfilesData; + + public string ConfigName + { + get; private set; + } + + public UserProfilesServiceBase(IConfigSource config, string configName): + base(config) + { + if(string.IsNullOrEmpty(configName)) + { + m_log.WarnFormat("[PROFILES]: Configuration section not given!"); + return; + } + + string dllName = String.Empty; + string connString = null; + string realm = String.Empty; + + IConfig dbConfig = config.Configs["DatabaseService"]; + if (dbConfig != null) + { + if (dllName == String.Empty) + dllName = dbConfig.GetString("StorageProvider", String.Empty); + if (string.IsNullOrEmpty(connString)) + connString = dbConfig.GetString("ConnectionString", String.Empty); + } + + IConfig ProfilesConfig = config.Configs[configName]; + if (ProfilesConfig != null) + { + connString = ProfilesConfig.GetString("ConnectionString", connString); + realm = ProfilesConfig.GetString("Realm", realm); + } + + ProfilesData = LoadPlugin(dllName, new Object[] { connString }); + if (ProfilesData == null) + throw new Exception("Could not find a storage interface in the given module"); + + } + } +} + diff --git a/bin/OpenSim.ini.example b/bin/OpenSim.ini.example index 5e486d4d2e..38e2a075a9 100644 --- a/bin/OpenSim.ini.example +++ b/bin/OpenSim.ini.example @@ -1032,6 +1032,12 @@ ;# {InitialTerrain} {} {Initial terrain type} {pinhead-island flat} pinhead-island ; InitialTerrain = "pinhead-island" +[Profile] + ;# {ProfileURL} {} {Set url to UserProfilesService} {} + ;; Set the value of the url to your UserProfilesService + ;; If un-set / "" the module is disabled + ;; ProfileURL = http://127.0.0.1:8002 + [Architecture] ;# {Include-Architecture} {} {Choose one of the following architectures} {config-include/Standalone.ini config-include/StandaloneHypergrid.ini config-include/Grid.ini config-include/GridHypergrid.ini config-include/SimianGrid.ini config-include/HyperSimianGrid.ini} config-include/Standalone.ini ;; Uncomment one of the following includes as required. For instance, to create a standalone OpenSim, diff --git a/bin/OpenSimDefaults.ini b/bin/OpenSimDefaults.ini index 81843b1c34..237f684577 100644 --- a/bin/OpenSimDefaults.ini +++ b/bin/OpenSimDefaults.ini @@ -377,6 +377,19 @@ AllowRegionRestartFromClient = true +[UserProfiles] + ;# {ProfileURL} {} {Set url to UserProfilesService} {} + ;; Set the value of the url to your UserProfilesService + ;; If un-set / "" the module is disabled + ;; If the ProfileURL is not set, then very BASIC + ;; profile support will be configured. If the ProfileURL is set to a + ;; valid URL, then full profile support will be configured. The URL + ;; points to your grid's Robust user profiles service + ;; + ; ProfileURL = http://127.0.0.1:9000 + + + [SMTP] enabled = false diff --git a/bin/Robust.HG.ini.example b/bin/Robust.HG.ini.example index bc2b4cfb29..d9f1ca1725 100644 --- a/bin/Robust.HG.ini.example +++ b/bin/Robust.HG.ini.example @@ -71,10 +71,12 @@ HGInventoryServiceConnector = "HGInventoryService@8002/OpenSim.Server.Handlers.d HGAssetServiceConnector = "HGAssetService@8002/OpenSim.Server.Handlers.dll:AssetServiceConnector" ;; Uncomment this if you want Groups V2, HG to work ; HGGroupsServiceConnector = "8002/OpenSim.Addons.Groups.dll:HGGroupsServiceRobustConnector" - ;; Additions for other add-on modules. For example: ;; WifiServerConnector = "8002/Diva.Wifi.dll:WifiServerConnector" +;; Uncomment for UserProfiles see [UserProfilesService] to configure... +; UserProfilesServiceConnector = "8002/OpenSim.Server.Handlers.dll:UserProfilesConnector" + ; * This is common for all services, it's the network setup for the entire ; * server instance, if none is specified above ; * @@ -595,4 +597,12 @@ HGAssetServiceConnector = "HGAssetService@8002/OpenSim.Server.Handlers.dll:Asset ;; Can overwrite the default in [Hypergrid], but probably shouldn't ; HomeURI = "http://127.0.0.1:8002" +[UserProfilesService] + LocalServiceModule = "OpenSim.Services.UserProfilesService.dll:UserProfilesService" + Enabled = false + ;; Configure this for separate profiles database + ;; ConnectionString = "Data Source=localhost;Database=opensim;User ID=opensim;Password=*****;Old Guids=true;" + ;; Realm = UserProfiles + UserAccountService = OpenSim.Services.UserAccountService.dll:UserAccountService + AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" diff --git a/bin/Robust.ini.example b/bin/Robust.ini.example index 1d66b7fcfd..7d6492bf94 100644 --- a/bin/Robust.ini.example +++ b/bin/Robust.ini.example @@ -51,6 +51,8 @@ MapGetServiceConnector = "8002/OpenSim.Server.Handlers.dll:MapGetServiceConnecto ;; Uncomment this if you want Groups V2 to work ;GroupsServiceConnector = "8003/OpenSim.Addons.Groups.dll:GroupsServiceRobustConnector" +;; Uncomment for UserProfiles see [UserProfilesService] to configure... +; UserProfilesServiceConnector = "8002/OpenSim.Server.Handlers.dll:UserProfilesConnector" ; * This is common for all services, it's the network setup for the entire ; * server instance, if none is specified above @@ -391,4 +393,13 @@ MapGetServiceConnector = "8002/OpenSim.Server.Handlers.dll:MapGetServiceConnecto ; password help: optional: page providing password assistance for users of your grid ;password = http://127.0.0.1/password +[UserProfilesService] + LocalServiceModule = "OpenSim.Services.UserProfilesService.dll:UserProfilesService" + Enabled = false + ;; Configure this for separate profiles database + ;; ConnectionString = "Data Source=localhost;Database=opensim;User ID=opensim;Password=*****;Old Guids=true;" + ;; Realm = UserProfiles + UserAccountService = OpenSim.Services.UserAccountService.dll:UserAccountService + AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + diff --git a/bin/config-include/StandaloneCommon.ini.example b/bin/config-include/StandaloneCommon.ini.example index 254724417f..8c23c41418 100644 --- a/bin/config-include/StandaloneCommon.ini.example +++ b/bin/config-include/StandaloneCommon.ini.example @@ -352,3 +352,19 @@ ;; If appearance is restricted, which accounts' appearances are allowed to be exported? ;; Comma-separated list of account names AccountForAppearance = "Test User, Astronaut Smith" + +;; UserProfiles Service +;; +;; To use, set Enabled to true then configure for your site... +[UserProfilesService] + LocalServiceModule = "OpenSim.Services.UserProfilesService.dll:UserProfilesService" + Enabled = false + + ;; Configure this for separate databse + ; ConnectionString = "Data Source=localhost;Database=opensim;User ID=opensim;Password=***;Old Guids=true;" + ; Realm = UserProfiles + + UserAccountService = OpenSim.Services.UserAccountService.dll:UserAccountService + AuthenticationServiceModule = "OpenSim.Services.AuthenticationService.dll:PasswordAuthenticationService" + + diff --git a/bin/config-include/StandaloneHypergrid.ini b/bin/config-include/StandaloneHypergrid.ini index ba92030132..39c33e8ca2 100644 --- a/bin/config-include/StandaloneHypergrid.ini +++ b/bin/config-include/StandaloneHypergrid.ini @@ -19,6 +19,7 @@ GridUserServices = "LocalGridUserServicesConnector" SimulationServices = "RemoteSimulationConnectorModule" AvatarServices = "LocalAvatarServicesConnector" + UserProfilesServices = "LocalUserProfilesServicesConnector" MapImageService = "MapImageServiceModule" EntityTransferModule = "HGEntityTransferModule" InventoryAccessModule = "HGInventoryAccessModule" @@ -184,7 +185,6 @@ UserAgentService = "OpenSim.Services.HypergridService.dll:UserAgentService" InGatekeeper = True - ;; This should always be the very last thing on this file [Includes] Include-Common = "config-include/StandaloneCommon.ini" diff --git a/prebuild.xml b/prebuild.xml index 5967ef6ebb..03cac764ce 100644 --- a/prebuild.xml +++ b/prebuild.xml @@ -281,6 +281,7 @@ + @@ -1239,6 +1240,42 @@ + + + + ../../../bin/ + + + + + ../../../bin/ + + + + ../../../bin/ + + + + + + + + + + + + + + + + + + + + + + + @@ -1977,6 +2014,7 @@ + From 46335b103e1f993c6aa77e46f89e5ee0d6d7497e Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Thu, 30 May 2013 23:51:35 +0100 Subject: [PATCH 05/18] If an exception occurs in the AsyncCommandManager loop, spit it out to log rather than silently swallowing it. This might help diagnose the cause of http://opensimulator.org/mantis/view.php?id=6651 where sometimes scripts fail to start on region start. --- .../Api/Implementation/AsyncCommandManager.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs index 1c59624d35..352e3163db 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs @@ -47,7 +47,7 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// public class AsyncCommandManager { -// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static Thread cmdHandlerThread; private static int cmdHandlerThreadCycleSleepms; @@ -183,17 +183,15 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api { try { - while (true) - { - Thread.Sleep(cmdHandlerThreadCycleSleepms); + Thread.Sleep(cmdHandlerThreadCycleSleepms); - DoOneCmdHandlerPass(); + DoOneCmdHandlerPass(); - Watchdog.UpdateThread(); - } + Watchdog.UpdateThread(); } - catch + catch (Exception e) { + m_log.Error("[ASYNC COMMAND MANAGER]: Exception in command handler pass: ", e); } } } From 3aa2fb99289162b4338b7550a39411bebd74d97b Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 30 May 2013 09:19:42 -0700 Subject: [PATCH 06/18] BulletSim: remove unuseful BulletSim parameters from OpenSimDefaults.ini and replace with things someone might actually want to tune (avatar height, ...). --- bin/OpenSimDefaults.ini | 57 ++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/bin/OpenSimDefaults.ini b/bin/OpenSimDefaults.ini index 81843b1c34..6cdf146a24 100644 --- a/bin/OpenSimDefaults.ini +++ b/bin/OpenSimDefaults.ini @@ -921,56 +921,49 @@ ;force_simple_prim_meshing = true [BulletSim] - ; World parameters + ; All the BulletSim parameters can be displayed with the console command "physics get all" + ; and all are defined in the source file OpenSim/Regions/Physics/BulletSPlugin/BSParam.cs. ; There are two bullet physics libraries, bulletunmanaged is the default and is a native c++ dll - ; bulletxna is a managed C# dll. They have comparible functionality.. the c++ is much faster. - + ; bulletxna is a managed C# dll. They have comparible functionality but the c++ one is much faster. BulletEngine = "bulletunmanaged" ; BulletEngine = "bulletxna" - ; Terrain Implementation {1|0} 0 for HeightField, 1 for Mesh terrain. If you're using the bulletxna engine, - ; you will want to switch to the heightfield option - TerrainImplementation = 1 - ; TerrainImplementation = 0 + ; Terrain Implementation + TerrainImplementation = 1 ; 0=Heightfield, 1=mesh + ; For mesh terrain, the detail of the created mesh. '1' gives 256x256 (heightfield resolution). '2' + ; gives 512x512. Etc. Cannot be larger than '4'. Higher magnification uses lots of memory. + TerrainMeshMagnification = 2 - Gravity = -9.80665 + ; Avatar physics height adjustments. http://opensimulator.org/wiki/BulletSim#Adjusting_Avatar_Height + AvatarHeightLowFudge = -0.2 ; Adjustment at low end of height range + AvatarHeightMidFudge = 0.1 ; Adjustment at mid point of avatar height range + AvatarHeightHighFudge = 0.1 ; Adjustment at high end of height range - TerrainFriction = 0.30 - TerrainHitFraction = 0.8 - TerrainRestitution = 0 - TerrainCollisionMargin = 0.04 - - AvatarFriction = 0.2 - AvatarStandingFriction = 0.95 - AvatarRestitution = 0.0 - AvatarDensity = 3.5 - AvatarCapsuleWidth = 0.6 - AvatarCapsuleDepth = 0.45 - AvatarCapsuleHeight = 1.5 - AvatarContactProcessingThreshold = 0.1 - - MaxObjectMass = 10000.01 - - CollisionMargin = 0.04 - - ; Linkset implmentation + ; Default linkset implmentation + ; 'Constraint' uses physics constraints to hold linkset together. 'Compound' builds a compound + ; shape from the children shapes to create a single physical shape. 'Compound' uses a lot less CPU time. LinkImplementation = 1 ; 0=constraint, 1=compound - ; Whether to mesh sculpties + ; If 'true', turn scuplties into meshes MeshSculptedPrim = true ; If 'true', force simple prims (box and sphere) to be meshed + ; If 'false', the Bullet native special case shape is used for square rectangles and even dimensioned spheres ForceSimplePrimMeshing = false - ; Bullet step parameters - MaxSubSteps = 10 - FixedTimeStep = .01667 + ; If 'true', when creating meshes, remove all triangles that have two equal vertexes. + ; Happens often in sculpties. If turned off, there will be some doorways that cannot be walked through. + ShouldRemoveZeroWidthTriangles = true + ; If 'true', use convex hull definition in mesh asset if present. + ShouldUseAssetHulls = true + + ; If there are thousands of physical objects, these maximums should be increased. MaxCollisionsPerFrame = 2048 MaxUpdatesPerFrame = 8192 - ; Detailed physics debug logging + ; Detailed physics debug logging. Very verbose. PhysicsLoggingEnabled = False PhysicsLoggingDir = "." VehicleLoggingEnabled = False From 439f11cc3cf2ab8f7fdc1d8746f1d8ab44c911eb Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 30 May 2013 09:21:58 -0700 Subject: [PATCH 07/18] Add region heartbeat start event to complement heartbeat end event. This allows object modification before the usual heartbeat operation. --- .../Region/Framework/Scenes/EventManager.cs | 23 +++++++++++++++++++ OpenSim/Region/Framework/Scenes/Scene.cs | 2 ++ 2 files changed, 25 insertions(+) diff --git a/OpenSim/Region/Framework/Scenes/EventManager.cs b/OpenSim/Region/Framework/Scenes/EventManager.cs index 59d01489d0..a246319c52 100644 --- a/OpenSim/Region/Framework/Scenes/EventManager.cs +++ b/OpenSim/Region/Framework/Scenes/EventManager.cs @@ -969,6 +969,8 @@ namespace OpenSim.Region.Framework.Scenes public delegate void RegionStarted(Scene scene); public event RegionStarted OnRegionStarted; + public delegate void RegionHeartbeatStart(Scene scene); + public event RegionHeartbeatStart OnRegionHeartbeatStart; public delegate void RegionHeartbeatEnd(Scene scene); public event RegionHeartbeatEnd OnRegionHeartbeatEnd; @@ -3068,6 +3070,27 @@ namespace OpenSim.Region.Framework.Scenes } } + public void TriggerRegionHeartbeatStart(Scene scene) + { + RegionHeartbeatStart handler = OnRegionHeartbeatStart; + + if (handler != null) + { + foreach (RegionHeartbeatStart d in handler.GetInvocationList()) + { + try + { + d(scene); + } + catch (Exception e) + { + m_log.ErrorFormat("[EVENT MANAGER]: Delegate for OnRegionHeartbeatStart failed - continuing {0} - {1}", + e.Message, e.StackTrace); + } + } + } + } + public void TriggerRegionHeartbeatEnd(Scene scene) { RegionHeartbeatEnd handler = OnRegionHeartbeatEnd; diff --git a/OpenSim/Region/Framework/Scenes/Scene.cs b/OpenSim/Region/Framework/Scenes/Scene.cs index 5dea6349cd..0743ce7bdd 100644 --- a/OpenSim/Region/Framework/Scenes/Scene.cs +++ b/OpenSim/Region/Framework/Scenes/Scene.cs @@ -1517,6 +1517,8 @@ namespace OpenSim.Region.Framework.Scenes try { + EventManager.TriggerRegionHeartbeatStart(this); + // Apply taints in terrain module to terrain in physics scene if (Frame % m_update_terrain == 0) { From 48a175eff760e04f8096acd404058755d7c2919c Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 30 May 2013 14:30:45 -0700 Subject: [PATCH 08/18] Add methods to Animation and AnimationSet for easier manipulation and display of groups of animations (Equal(), ToString(), FromOSDArray(), ...). No functional change to animations. --- OpenSim/Framework/Animation.cs | 20 ++++ .../Scenes/Animation/AnimationSet.cs | 110 ++++++++++++++++++ .../Animation/DefaultAvatarAnimations.cs | 26 +++++ 3 files changed, 156 insertions(+) diff --git a/OpenSim/Framework/Animation.cs b/OpenSim/Framework/Animation.cs index 232f5a1893..8bdf8f4373 100644 --- a/OpenSim/Framework/Animation.cs +++ b/OpenSim/Framework/Animation.cs @@ -120,5 +120,25 @@ namespace OpenSim.Framework sequenceNum = args["seq_num"].AsInteger(); } + public override bool Equals(object obj) + { + Animation other = obj as Animation; + if (other != null) + { + return (other.AnimID == this.AnimID + && other.SequenceNum == this.SequenceNum + && other.ObjectID == this.ObjectID); + } + + return base.Equals(obj); + } + + public override string ToString() + { + return "AnimID=" + AnimID.ToString() + + "/seq=" + SequenceNum.ToString() + + "/objID=" + ObjectID.ToString(); + } + } } diff --git a/OpenSim/Region/Framework/Scenes/Animation/AnimationSet.cs b/OpenSim/Region/Framework/Scenes/Animation/AnimationSet.cs index 66edfed393..5dee64d0b5 100644 --- a/OpenSim/Region/Framework/Scenes/Animation/AnimationSet.cs +++ b/OpenSim/Region/Framework/Scenes/Animation/AnimationSet.cs @@ -28,8 +28,11 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Text; using log4net; using OpenMetaverse; +using OpenMetaverse.StructuredData; + using OpenSim.Framework; using Animation = OpenSim.Framework.Animation; @@ -60,6 +63,12 @@ namespace OpenSim.Region.Framework.Scenes.Animation ResetDefaultAnimation(); } + public AnimationSet(OSDArray pArray) + { + ResetDefaultAnimation(); + FromOSDArray(pArray); + } + public bool HasAnimation(UUID animID) { if (m_defaultAnimation.AnimID == animID) @@ -218,5 +227,106 @@ namespace OpenSim.Region.Framework.Scenes.Animation foreach (OpenSim.Framework.Animation anim in theArray) m_animations.Add(anim); } + + // Create representation of this AnimationSet as an OSDArray. + // First two entries in the array are the default and implicitDefault animations + // followed by the other animations. + public OSDArray ToOSDArray() + { + OSDArray ret = new OSDArray(); + ret.Add(DefaultAnimation.PackUpdateMessage()); + ret.Add(ImplicitDefaultAnimation.PackUpdateMessage()); + + foreach (OpenSim.Framework.Animation anim in m_animations) + ret.Add(anim.PackUpdateMessage()); + + return ret; + } + + public void FromOSDArray(OSDArray pArray) + { + this.Clear(); + + if (pArray.Count >= 1) + { + m_defaultAnimation = new OpenSim.Framework.Animation((OSDMap)pArray[0]); + } + if (pArray.Count >= 2) + { + m_implicitDefaultAnimation = new OpenSim.Framework.Animation((OSDMap)pArray[1]); + } + for (int ii = 2; ii < pArray.Count; ii++) + { + m_animations.Add(new OpenSim.Framework.Animation((OSDMap)pArray[ii])); + } + } + + // Compare two AnimationSets and return 'true' if the default animations are the same + // and all of the animations in the list are equal. + public override bool Equals(object obj) + { + AnimationSet other = obj as AnimationSet; + if (other != null) + { + if (this.DefaultAnimation.Equals(other.DefaultAnimation) + && this.ImplicitDefaultAnimation.Equals(other.ImplicitDefaultAnimation)) + { + // The defaults are the same. Is the list of animations the same? + OpenSim.Framework.Animation[] thisAnims = this.ToArray(); + OpenSim.Framework.Animation[] otherAnims = other.ToArray(); + if (thisAnims.Length == 0 && otherAnims.Length == 0) + return true; // the common case + if (thisAnims.Length == otherAnims.Length) + { + // Do this the hard way but since the list is usually short this won't take long. + foreach (OpenSim.Framework.Animation thisAnim in thisAnims) + { + bool found = false; + foreach (OpenSim.Framework.Animation otherAnim in otherAnims) + { + if (thisAnim.Equals(otherAnim)) + { + found = true; + break; + } + } + if (!found) + { + // If anything is not in the other list, these are not equal + return false; + } + } + // Found everything in the other list. Since lists are equal length, they must be equal. + return true; + } + } + return false; + } + // Don't know what was passed, but the base system will figure it out for me. + return base.Equals(obj); + } + + public override string ToString() + { + StringBuilder buff = new StringBuilder(); + buff.Append("dflt="); + buff.Append(DefaultAnimation.ToString()); + buff.Append(",iDflt="); + if (DefaultAnimation == ImplicitDefaultAnimation) + buff.Append("same"); + else + buff.Append(ImplicitDefaultAnimation.ToString()); + if (m_animations.Count > 0) + { + buff.Append(",anims="); + foreach (OpenSim.Framework.Animation anim in m_animations) + { + buff.Append("<"); + buff.Append(anim.ToString()); + buff.Append(">,"); + } + } + return buff.ToString(); + } } } diff --git a/OpenSim/Region/Framework/Scenes/Animation/DefaultAvatarAnimations.cs b/OpenSim/Region/Framework/Scenes/Animation/DefaultAvatarAnimations.cs index c2b0468067..b79dd8f3da 100644 --- a/OpenSim/Region/Framework/Scenes/Animation/DefaultAvatarAnimations.cs +++ b/OpenSim/Region/Framework/Scenes/Animation/DefaultAvatarAnimations.cs @@ -104,5 +104,31 @@ namespace OpenSim.Region.Framework.Scenes.Animation return UUID.Zero; } + + /// + /// Get the name of the animation given a UUID. If there is no matching animation + /// return the UUID as a string. + /// + public static string GetDefaultAnimationName(UUID uuid) + { + string ret = "unknown"; + if (AnimsUUID.ContainsValue(uuid)) + { + foreach (KeyValuePair kvp in AnimsUUID) + { + if (kvp.Value == uuid) + { + ret = kvp.Key; + break; + } + } + } + else + { + ret = uuid.ToString(); + } + + return ret; + } } } \ No newline at end of file From 4d32ca19bf27048105aeb01c67f0f9647ed3e700 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 30 May 2013 19:15:14 -0700 Subject: [PATCH 09/18] Trigger OnScenePresenceUpdated when the avatar's animations change. --- .../Scenes/Animation/ScenePresenceAnimator.cs | 17 ++++++++++++++--- .../Region/Framework/Scenes/ScenePresence.cs | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/OpenSim/Region/Framework/Scenes/Animation/ScenePresenceAnimator.cs b/OpenSim/Region/Framework/Scenes/Animation/ScenePresenceAnimator.cs index e92a087625..a701a79081 100644 --- a/OpenSim/Region/Framework/Scenes/Animation/ScenePresenceAnimator.cs +++ b/OpenSim/Region/Framework/Scenes/Animation/ScenePresenceAnimator.cs @@ -92,7 +92,9 @@ namespace OpenSim.Region.Framework.Scenes.Animation GetAnimName(animID), animID, m_scenePresence.Name); if (m_animations.Add(animID, m_scenePresence.ControllingClient.NextAnimationSequenceNumber, objectID)) + { SendAnimPack(); + } } // Called from scripts @@ -131,7 +133,9 @@ namespace OpenSim.Region.Framework.Scenes.Animation GetAnimName(animID), animID, m_scenePresence.Name); if (m_animations.Remove(animID, allowNoDefault)) + { SendAnimPack(); + } } // Called from scripts @@ -163,8 +167,10 @@ namespace OpenSim.Region.Framework.Scenes.Animation /// The movement animation is reserved for "main" animations /// that are mutually exclusive, e.g. flying and sitting. /// - public void TrySetMovementAnimation(string anim) + /// 'true' if the animation was updated + public bool TrySetMovementAnimation(string anim) { + bool ret = false; if (!m_scenePresence.IsChildAgent) { // m_log.DebugFormat( @@ -181,6 +187,7 @@ namespace OpenSim.Region.Framework.Scenes.Animation // 16384 is CHANGED_ANIMATION m_scenePresence.SendScriptEventToAttachments("changed", new Object[] { (int)Changed.ANIMATION}); SendAnimPack(); + ret = true; } } else @@ -189,6 +196,7 @@ namespace OpenSim.Region.Framework.Scenes.Animation "[SCENE PRESENCE ANIMATOR]: Tried to set movement animation {0} on child presence {1}", anim, m_scenePresence.Name); } + return ret; } /// @@ -422,8 +430,10 @@ namespace OpenSim.Region.Framework.Scenes.Animation /// /// Update the movement animation of this avatar according to its current state /// - public void UpdateMovementAnimations() + /// 'true' if the animation was changed + public bool UpdateMovementAnimations() { + bool ret = false; lock (m_animations) { string newMovementAnimation = DetermineMovementAnimation(); @@ -437,9 +447,10 @@ namespace OpenSim.Region.Framework.Scenes.Animation // Only set it if it's actually changed, give a script // a chance to stop a default animation - TrySetMovementAnimation(CurrentMovementAnimation); + ret = TrySetMovementAnimation(CurrentMovementAnimation); } } + return ret; } public UUID[] GetAnimationArray() diff --git a/OpenSim/Region/Framework/Scenes/ScenePresence.cs b/OpenSim/Region/Framework/Scenes/ScenePresence.cs index e8aa52e07f..b8ff7f772f 100644 --- a/OpenSim/Region/Framework/Scenes/ScenePresence.cs +++ b/OpenSim/Region/Framework/Scenes/ScenePresence.cs @@ -2039,6 +2039,7 @@ namespace OpenSim.Region.Framework.Scenes } Animator.TrySetMovementAnimation("STAND"); + TriggerScenePresenceUpdated(); } private SceneObjectPart FindNextAvailableSitTarget(UUID targetID) @@ -2432,6 +2433,7 @@ namespace OpenSim.Region.Framework.Scenes } Animator.TrySetMovementAnimation(sitAnimation); SendAvatarDataToAllAgents(); + TriggerScenePresenceUpdated(); } } @@ -2440,6 +2442,7 @@ namespace OpenSim.Region.Framework.Scenes // m_updateCount = 0; // Kill animation update burst so that the SIT_G.. will stick.. m_AngularVelocity = Vector3.Zero; Animator.TrySetMovementAnimation("SIT_GROUND_CONSTRAINED"); + TriggerScenePresenceUpdated(); SitGround = true; RemoveFromPhysicalScene(); } @@ -2456,11 +2459,13 @@ namespace OpenSim.Region.Framework.Scenes public void HandleStartAnim(IClientAPI remoteClient, UUID animID) { Animator.AddAnimation(animID, UUID.Zero); + TriggerScenePresenceUpdated(); } public void HandleStopAnim(IClientAPI remoteClient, UUID animID) { Animator.RemoveAnimation(animID, false); + TriggerScenePresenceUpdated(); } /// @@ -3465,7 +3470,8 @@ namespace OpenSim.Region.Framework.Scenes // if (m_updateCount > 0) // { - Animator.UpdateMovementAnimations(); + if (Animator.UpdateMovementAnimations()) + TriggerScenePresenceUpdated(); // m_updateCount--; // } From 5b3a443125e19d9cda0b6ce5f8b6a44116c9d50a Mon Sep 17 00:00:00 2001 From: BlueWall Date: Thu, 30 May 2013 22:52:17 -0400 Subject: [PATCH 10/18] Trigger Jenkins build --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 552cdef866..118fe71d3e 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,4 @@ project can always be found at http://opensimulator.org. Thanks for trying OpenSim, we hope it is a pleasant experience. + From bf035233232885b798a56a1687319ba167e4bf60 Mon Sep 17 00:00:00 2001 From: BlueWall Date: Fri, 31 May 2013 10:40:47 -0400 Subject: [PATCH 11/18] Fill in fields with default values on profile creation --- OpenSim/Data/MySQL/MySQLUserProfilesData.cs | 50 ++++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/OpenSim/Data/MySQL/MySQLUserProfilesData.cs b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs index 09bd44814e..5d7149bc03 100644 --- a/OpenSim/Data/MySQL/MySQLUserProfilesData.cs +++ b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs @@ -622,7 +622,7 @@ namespace OpenSim.Data.MySQL { m_log.DebugFormat("[PROFILES_DATA]" + ": No data for {0}", props.UserId); - + props.WebUrl = string.Empty; props.ImageId = UUID.Zero; props.AboutText = string.Empty; @@ -634,8 +634,38 @@ namespace OpenSim.Data.MySQL props.SkillsMask = 0; props.SkillsText = string.Empty; props.Language = string.Empty; + props.PublishProfile = false; + props.PublishMature = false; - query = "INSERT INTO userprofile (`useruuid`) VALUES (?userId)"; + query = "INSERT INTO userprofile ("; + query += "useruuid, "; + query += "profilePartner, "; + query += "profileAllowPublish, "; + query += "profileMaturePublish, "; + query += "profileURL, "; + query += "profileWantToMask, "; + query += "profileWantToText, "; + query += "profileSkillsMask, "; + query += "profileSkillsText, "; + query += "profileLanguages, "; + query += "profileImage, "; + query += "profileAboutText, "; + query += "profileFirstImage, "; + query += "profileFirstText) VALUES ("; + query += "?userId, "; + query += "?profilePartner, "; + query += "?profileAllowPublish, "; + query += "?profileMaturePublish, "; + query += "?profileURL, "; + query += "?profileWantToMask, "; + query += "?profileWantToText, "; + query += "?profileSkillsMask, "; + query += "?profileSkillsText, "; + query += "?profileLanguages, "; + query += "?profileImage, "; + query += "?profileAboutText, "; + query += "?profileFirstImage, "; + query += "?profileFirstText)"; dbcon.Close(); dbcon.Open(); @@ -643,6 +673,20 @@ namespace OpenSim.Data.MySQL using (MySqlCommand put = new MySqlCommand(query, dbcon)) { put.Parameters.AddWithValue("?userId", props.UserId.ToString()); + put.Parameters.AddWithValue("?profilePartner", props.PartnerId.ToString()); + put.Parameters.AddWithValue("?profileAllowPublish", props.PublishProfile); + put.Parameters.AddWithValue("?profileMaturePublish", props.PublishMature); + put.Parameters.AddWithValue("?profileURL", props.WebUrl); + put.Parameters.AddWithValue("?profileWantToMask", props.WantToMask); + put.Parameters.AddWithValue("?profileWantToText", props.WantToText); + put.Parameters.AddWithValue("?profileSkillsMask", props.SkillsMask); + put.Parameters.AddWithValue("?profileSkillsText", props.SkillsText); + put.Parameters.AddWithValue("?profileLanguages", props.Language); + put.Parameters.AddWithValue("?profileImage", props.ImageId.ToString()); + put.Parameters.AddWithValue("?profileAboutText", props.AboutText); + put.Parameters.AddWithValue("?profileFirstImage", props.FirstLifeImageId.ToString()); + put.Parameters.AddWithValue("?profileFirstText", props.FirstLifeText); + put.ExecuteNonQuery(); } } @@ -665,6 +709,7 @@ namespace OpenSim.Data.MySQL string query = string.Empty; query += "UPDATE userprofile SET "; + query += "profilePartner=?profilePartner, "; query += "profileURL=?profileURL, "; query += "profileImage=?image, "; query += "profileAboutText=?abouttext,"; @@ -680,6 +725,7 @@ namespace OpenSim.Data.MySQL using (MySqlCommand cmd = new MySqlCommand(query, dbcon)) { cmd.Parameters.AddWithValue("?profileURL", props.WebUrl); + cmd.Parameters.AddWithValue("?profilePartner", props.PartnerId.ToString()); cmd.Parameters.AddWithValue("?image", props.ImageId.ToString()); cmd.Parameters.AddWithValue("?abouttext", props.AboutText); cmd.Parameters.AddWithValue("?firstlifeimage", props.FirstLifeImageId.ToString()); From 00c1586ff8cd0abb2270a109087857adefe3b5c2 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Fri, 31 May 2013 18:12:36 +0100 Subject: [PATCH 12/18] refactor: Remove unused AsyncCommandManager.PleaseShutdown --- .../Api/Implementation/AsyncCommandManager.cs | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs index 352e3163db..c71b5717cd 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs @@ -377,23 +377,5 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api } } } - - #region Check llRemoteData channels - - #endregion - - #region Check llListeners - - #endregion - - /// - /// If set to true then threads and stuff should try to make a graceful exit - /// - public bool PleaseShutdown - { - get { return _PleaseShutdown; } - set { _PleaseShutdown = value; } - } - private bool _PleaseShutdown = false; } -} +} \ No newline at end of file From 921ad8704e08fccc994f7907e7cb1e9372e355b9 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Fri, 31 May 2013 22:50:15 +0100 Subject: [PATCH 13/18] Lock areas of AsyncCommandManager where multiple threads could try to access/update the same static structures simultaneously. This is possible where there is more than one scene (multiple copies of the same script engine) and/or more than one script engine being used. These operations are not thread safe and could be leading to the exceptions/problems seen in http://opensimulator.org/mantis/view.php?id=6651 This also prevents a small race condition where more than one AsyncLSLCmdHandlerThread could be started. --- .../Api/Implementation/AsyncCommandManager.cs | 292 +++++++++++------- 1 file changed, 179 insertions(+), 113 deletions(-) diff --git a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs index c71b5717cd..72e841507d 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs @@ -52,6 +52,15 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api private static Thread cmdHandlerThread; private static int cmdHandlerThreadCycleSleepms; + /// + /// Lock for reading/writing static components of AsyncCommandManager. + /// + /// + /// This lock exists so that multiple threads from different engines and/or different copies of the same engine + /// are prevented from running non-thread safe code (e.g. read/write of lists) concurrently. + /// + private static object staticLock = new object(); + private static List m_Scenes = new List(); private static List m_ScriptEngines = new List(); @@ -74,37 +83,65 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api public Dataserver DataserverPlugin { - get { return m_Dataserver[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_Dataserver[m_ScriptEngine]; + } } public Timer TimerPlugin { - get { return m_Timer[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_Timer[m_ScriptEngine]; + } } public HttpRequest HttpRequestPlugin { - get { return m_HttpRequest[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_HttpRequest[m_ScriptEngine]; + } } public Listener ListenerPlugin { - get { return m_Listener[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_Listener[m_ScriptEngine]; + } } public SensorRepeat SensorRepeatPlugin { - get { return m_SensorRepeat[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_SensorRepeat[m_ScriptEngine]; + } } public XmlRequest XmlRequestPlugin { - get { return m_XmlRequest[m_ScriptEngine]; } + get + { + lock (staticLock) + return m_XmlRequest[m_ScriptEngine]; + } } public IScriptEngine[] ScriptEngines { - get { return m_ScriptEngines.ToArray(); } + get + { + lock (staticLock) + return m_ScriptEngines.ToArray(); + } } public AsyncCommandManager(IScriptEngine _ScriptEngine) @@ -112,29 +149,36 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api m_ScriptEngine = _ScriptEngine; m_Scene = m_ScriptEngine.World; - if (m_Scenes.Count == 0) - ReadConfig(); + // If there is more than one scene in the simulator or multiple script engines are used on the same region + // then more than one thread could arrive at this block of code simultaneously. However, it cannot be + // executed concurrently both because concurrent list operations are not thread-safe and because of other + // race conditions such as the later check of cmdHandlerThread == null. + lock (staticLock) + { + if (m_Scenes.Count == 0) + ReadConfig(); - if (!m_Scenes.Contains(m_Scene)) - m_Scenes.Add(m_Scene); - if (!m_ScriptEngines.Contains(m_ScriptEngine)) - m_ScriptEngines.Add(m_ScriptEngine); + if (!m_Scenes.Contains(m_Scene)) + m_Scenes.Add(m_Scene); + if (!m_ScriptEngines.Contains(m_ScriptEngine)) + m_ScriptEngines.Add(m_ScriptEngine); - // Create instances of all plugins - if (!m_Dataserver.ContainsKey(m_ScriptEngine)) - m_Dataserver[m_ScriptEngine] = new Dataserver(this); - if (!m_Timer.ContainsKey(m_ScriptEngine)) - m_Timer[m_ScriptEngine] = new Timer(this); - if (!m_HttpRequest.ContainsKey(m_ScriptEngine)) - m_HttpRequest[m_ScriptEngine] = new HttpRequest(this); - if (!m_Listener.ContainsKey(m_ScriptEngine)) - m_Listener[m_ScriptEngine] = new Listener(this); - if (!m_SensorRepeat.ContainsKey(m_ScriptEngine)) - m_SensorRepeat[m_ScriptEngine] = new SensorRepeat(this); - if (!m_XmlRequest.ContainsKey(m_ScriptEngine)) - m_XmlRequest[m_ScriptEngine] = new XmlRequest(this); + // Create instances of all plugins + if (!m_Dataserver.ContainsKey(m_ScriptEngine)) + m_Dataserver[m_ScriptEngine] = new Dataserver(this); + if (!m_Timer.ContainsKey(m_ScriptEngine)) + m_Timer[m_ScriptEngine] = new Timer(this); + if (!m_HttpRequest.ContainsKey(m_ScriptEngine)) + m_HttpRequest[m_ScriptEngine] = new HttpRequest(this); + if (!m_Listener.ContainsKey(m_ScriptEngine)) + m_Listener[m_ScriptEngine] = new Listener(this); + if (!m_SensorRepeat.ContainsKey(m_ScriptEngine)) + m_SensorRepeat[m_ScriptEngine] = new SensorRepeat(this); + if (!m_XmlRequest.ContainsKey(m_ScriptEngine)) + m_XmlRequest[m_ScriptEngine] = new XmlRequest(this); - StartThread(); + StartThread(); + } } private static void StartThread() @@ -198,25 +242,28 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api private static void DoOneCmdHandlerPass() { - // Check HttpRequests - m_HttpRequest[m_ScriptEngines[0]].CheckHttpRequests(); - - // Check XMLRPCRequests - m_XmlRequest[m_ScriptEngines[0]].CheckXMLRPCRequests(); - - foreach (IScriptEngine s in m_ScriptEngines) + lock (staticLock) { - // Check Listeners - m_Listener[s].CheckListeners(); + // Check HttpRequests + m_HttpRequest[m_ScriptEngines[0]].CheckHttpRequests(); - // Check timers - m_Timer[s].CheckTimerEvents(); + // Check XMLRPCRequests + m_XmlRequest[m_ScriptEngines[0]].CheckXMLRPCRequests(); - // Check Sensors - m_SensorRepeat[s].CheckSenseRepeaterEvents(); + foreach (IScriptEngine s in m_ScriptEngines) + { + // Check Listeners + m_Listener[s].CheckListeners(); - // Check dataserver - m_Dataserver[s].ExpireRequests(); + // Check timers + m_Timer[s].CheckTimerEvents(); + + // Check Sensors + m_SensorRepeat[s].CheckSenseRepeaterEvents(); + + // Check dataserver + m_Dataserver[s].ExpireRequests(); + } } } @@ -229,32 +276,33 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api { // m_log.DebugFormat("[ASYNC COMMAND MANAGER]: Removing facilities for script {0}", itemID); - // Remove a specific script - - // Remove dataserver events - m_Dataserver[engine].RemoveEvents(localID, itemID); - - // Remove from: Timers - m_Timer[engine].UnSetTimerEvents(localID, itemID); - - // Remove from: HttpRequest - IHttpRequestModule iHttpReq = engine.World.RequestModuleInterface(); - if (iHttpReq != null) - iHttpReq.StopHttpRequestsForScript(itemID); - - IWorldComm comms = engine.World.RequestModuleInterface(); - if (comms != null) - comms.DeleteListener(itemID); - - IXMLRPC xmlrpc = engine.World.RequestModuleInterface(); - if (xmlrpc != null) + lock (staticLock) { - xmlrpc.DeleteChannels(itemID); - xmlrpc.CancelSRDRequests(itemID); - } + // Remove dataserver events + m_Dataserver[engine].RemoveEvents(localID, itemID); - // Remove Sensors - m_SensorRepeat[engine].UnSetSenseRepeaterEvents(localID, itemID); + // Remove from: Timers + m_Timer[engine].UnSetTimerEvents(localID, itemID); + + // Remove from: HttpRequest + IHttpRequestModule iHttpReq = engine.World.RequestModuleInterface(); + if (iHttpReq != null) + iHttpReq.StopHttpRequestsForScript(itemID); + + IWorldComm comms = engine.World.RequestModuleInterface(); + if (comms != null) + comms.DeleteListener(itemID); + + IXMLRPC xmlrpc = engine.World.RequestModuleInterface(); + if (xmlrpc != null) + { + xmlrpc.DeleteChannels(itemID); + xmlrpc.CancelSRDRequests(itemID); + } + + // Remove Sensors + m_SensorRepeat[engine].UnSetSenseRepeaterEvents(localID, itemID); + } } /// @@ -264,10 +312,13 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// public static SensorRepeat GetSensorRepeatPlugin(IScriptEngine engine) { - if (m_SensorRepeat.ContainsKey(engine)) - return m_SensorRepeat[engine]; - else - return null; + lock (staticLock) + { + if (m_SensorRepeat.ContainsKey(engine)) + return m_SensorRepeat[engine]; + else + return null; + } } /// @@ -277,10 +328,13 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// public static Dataserver GetDataserverPlugin(IScriptEngine engine) { - if (m_Dataserver.ContainsKey(engine)) - return m_Dataserver[engine]; - else - return null; + lock (staticLock) + { + if (m_Dataserver.ContainsKey(engine)) + return m_Dataserver[engine]; + else + return null; + } } /// @@ -290,10 +344,13 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// public static Timer GetTimerPlugin(IScriptEngine engine) { - if (m_Timer.ContainsKey(engine)) - return m_Timer[engine]; - else - return null; + lock (staticLock) + { + if (m_Timer.ContainsKey(engine)) + return m_Timer[engine]; + else + return null; + } } /// @@ -303,38 +360,44 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// public static Listener GetListenerPlugin(IScriptEngine engine) { - if (m_Listener.ContainsKey(engine)) - return m_Listener[engine]; - else - return null; + lock (staticLock) + { + if (m_Listener.ContainsKey(engine)) + return m_Listener[engine]; + else + return null; + } } public static Object[] GetSerializationData(IScriptEngine engine, UUID itemID) { List data = new List(); - Object[] listeners = m_Listener[engine].GetSerializationData(itemID); - if (listeners.Length > 0) + lock (staticLock) { - data.Add("listener"); - data.Add(listeners.Length); - data.AddRange(listeners); - } + Object[] listeners = m_Listener[engine].GetSerializationData(itemID); + if (listeners.Length > 0) + { + data.Add("listener"); + data.Add(listeners.Length); + data.AddRange(listeners); + } - Object[] timers=m_Timer[engine].GetSerializationData(itemID); - if (timers.Length > 0) - { - data.Add("timer"); - data.Add(timers.Length); - data.AddRange(timers); - } + Object[] timers=m_Timer[engine].GetSerializationData(itemID); + if (timers.Length > 0) + { + data.Add("timer"); + data.Add(timers.Length); + data.AddRange(timers); + } - Object[] sensors = m_SensorRepeat[engine].GetSerializationData(itemID); - if (sensors.Length > 0) - { - data.Add("sensor"); - data.Add(sensors.Length); - data.AddRange(sensors); + Object[] sensors = m_SensorRepeat[engine].GetSerializationData(itemID); + if (sensors.Length > 0) + { + data.Add("sensor"); + data.Add(sensors.Length); + data.AddRange(sensors); + } } return data.ToArray(); @@ -359,20 +422,23 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api idx+=len; + lock (staticLock) + { switch (type) { - case "listener": - m_Listener[engine].CreateFromData(localID, itemID, - hostID, item); - break; - case "timer": - m_Timer[engine].CreateFromData(localID, itemID, - hostID, item); - break; - case "sensor": - m_SensorRepeat[engine].CreateFromData(localID, - itemID, hostID, item); - break; + case "listener": + m_Listener[engine].CreateFromData(localID, itemID, + hostID, item); + break; + case "timer": + m_Timer[engine].CreateFromData(localID, itemID, + hostID, item); + break; + case "sensor": + m_SensorRepeat[engine].CreateFromData(localID, + itemID, hostID, item); + break; + } } } } From 217c7d11401dbf15ada3de750e7ad0df85a02894 Mon Sep 17 00:00:00 2001 From: "Justin Clark-Casey (justincc)" Date: Fri, 31 May 2013 23:00:10 +0100 Subject: [PATCH 14/18] Remove unnecessary m_scenes and m_scene from AsyncCommandManager. These were private and the sole point of use (to know when to load config for the first time) can be done by looking at script engines instead. --- .../Shared/Api/Implementation/AsyncCommandManager.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs index 72e841507d..998f40bcb5 100644 --- a/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs +++ b/OpenSim/Region/ScriptEngine/Shared/Api/Implementation/AsyncCommandManager.cs @@ -61,12 +61,10 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api /// private static object staticLock = new object(); - private static List m_Scenes = new List(); private static List m_ScriptEngines = new List(); public IScriptEngine m_ScriptEngine; - private IScene m_Scene; private static Dictionary m_Dataserver = new Dictionary(); @@ -147,7 +145,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api public AsyncCommandManager(IScriptEngine _ScriptEngine) { m_ScriptEngine = _ScriptEngine; - m_Scene = m_ScriptEngine.World; // If there is more than one scene in the simulator or multiple script engines are used on the same region // then more than one thread could arrive at this block of code simultaneously. However, it cannot be @@ -155,11 +152,9 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api // race conditions such as the later check of cmdHandlerThread == null. lock (staticLock) { - if (m_Scenes.Count == 0) + if (m_ScriptEngines.Count == 0) ReadConfig(); - if (!m_Scenes.Contains(m_Scene)) - m_Scenes.Add(m_Scene); if (!m_ScriptEngines.Contains(m_ScriptEngine)) m_ScriptEngines.Add(m_ScriptEngine); From ba2f13db63a58698ca47e9ba51a1a1509b838a77 Mon Sep 17 00:00:00 2001 From: BlueWall Date: Fri, 31 May 2013 18:48:01 -0400 Subject: [PATCH 15/18] Adding back the BasicProfileModule --- .../Avatar/Profile/BasicProfileModule.cs | 176 ++++++++++++++++++ .../Avatar/UserProfiles/UserProfileModule.cs | 88 ++------- bin/OpenSim.ini.example | 4 +- 3 files changed, 193 insertions(+), 75 deletions(-) create mode 100644 OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs diff --git a/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs b/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs new file mode 100644 index 0000000000..bf24030771 --- /dev/null +++ b/OpenSim/Region/CoreModules/Avatar/Profile/BasicProfileModule.cs @@ -0,0 +1,176 @@ +/* + * 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; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; + +using OpenMetaverse; +using log4net; +using Nini.Config; +using Mono.Addins; + +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; +using OpenSim.Services.Interfaces; + +namespace OpenSim.Region.CoreModules.Avatar.Profile +{ + [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "BasicProfileModule")] + public class BasicProfileModule : IProfileModule, ISharedRegionModule + { + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + // + // Module vars + // + private List m_Scenes = new List(); + private bool m_Enabled = false; + + #region ISharedRegionModule + + public void Initialise(IConfigSource config) + { + m_log.DebugFormat("[PROFILE MODULE]: Basic Profile Module enabled"); + m_Enabled = true; + } + + public void AddRegion(Scene scene) + { + if (!m_Enabled) + return; + + lock (m_Scenes) + { + if (!m_Scenes.Contains(scene)) + { + m_Scenes.Add(scene); + // Hook up events + scene.EventManager.OnNewClient += OnNewClient; + scene.RegisterModuleInterface(this); + } + } + } + + public void RegionLoaded(Scene scene) + { + if (!m_Enabled) + return; + } + + public void RemoveRegion(Scene scene) + { + if (!m_Enabled) + return; + + lock (m_Scenes) + { + m_Scenes.Remove(scene); + } + } + + public void PostInitialise() + { + } + + public void Close() + { + } + + public string Name + { + get { return "BasicProfileModule"; } + } + + public Type ReplaceableInterface + { + get { return typeof(IProfileModule); } + } + + #endregion + + /// New Client Event Handler + private void OnNewClient(IClientAPI client) + { + //Profile + client.OnRequestAvatarProperties += RequestAvatarProperties; + } + + public void RequestAvatarProperties(IClientAPI remoteClient, UUID avatarID) + { + IScene s = remoteClient.Scene; + if (!(s is Scene)) + return; + +// Scene scene = (Scene)s; + + string profileUrl = String.Empty; + string aboutText = String.Empty; + string firstLifeAboutText = String.Empty; + UUID image = UUID.Zero; + UUID firstLifeImage = UUID.Zero; + UUID partner = UUID.Zero; + uint wantMask = 0; + string wantText = String.Empty; + uint skillsMask = 0; + string skillsText = String.Empty; + string languages = String.Empty; + + UserAccount account = m_Scenes[0].UserAccountService.GetUserAccount(m_Scenes[0].RegionInfo.ScopeID, avatarID); + + string name = "Avatar"; + int created = 0; + if (account != null) + { + name = account.FirstName + " " + account.LastName; + created = account.Created; + } + Byte[] charterMember = Utils.StringToBytes(name); + + profileUrl = "No profile data"; + aboutText = string.Empty; + firstLifeAboutText = string.Empty; + image = UUID.Zero; + firstLifeImage = UUID.Zero; + partner = UUID.Zero; + + remoteClient.SendAvatarProperties(avatarID, aboutText, + Util.ToDateTime(created).ToString( + "M/d/yyyy", CultureInfo.InvariantCulture), + charterMember, firstLifeAboutText, + (uint)(0 & 0xff), + firstLifeImage, image, profileUrl, partner); + + //Viewer expects interest data when it asks for properties. + remoteClient.SendAvatarInterestsReply(avatarID, wantMask, wantText, + skillsMask, skillsText, languages); + } + + } +} diff --git a/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs b/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs index 563617d6b8..5b228ee6d4 100644 --- a/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs +++ b/OpenSim/Region/CoreModules/Avatar/UserProfiles/UserProfileModule.cs @@ -124,8 +124,9 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles public void Initialise(IConfigSource source) { Config = source; + ReplaceableInterface = typeof(IProfileModule); - IConfig profileConfig = Config.Configs["Profile"]; + IConfig profileConfig = Config.Configs["UserProfiles"]; if (profileConfig == null) { @@ -135,18 +136,16 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles // If we find ProfileURL then we configure for FULL support // else we setup for BASIC support - ProfileServerUri = profileConfig.GetString("ProfileURL", ""); + ProfileServerUri = profileConfig.GetString("ProfileServiceURL", ""); if (ProfileServerUri == "") { - m_log.Info("[PROFILES] UserProfiles module is activated in BASIC mode"); Enabled = false; return; } - else - { - m_log.Info("[PROFILES] UserProfiles module is activated in FULL mode"); - Enabled = true; - } + + m_log.Debug("[PROFILES]: Full Profiles Enabled"); + ReplaceableInterface = null; + Enabled = true; } /// @@ -157,6 +156,9 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles /// public void AddRegion(Scene scene) { + if(!Enabled) + return; + Scene = scene; Scene.RegisterModuleInterface(this); Scene.EventManager.OnNewClient += OnNewClient; @@ -178,6 +180,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles /// public void RemoveRegion(Scene scene) { + if(!Enabled) + return; } /// @@ -191,6 +195,8 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles /// public void RegionLoaded(Scene scene) { + if(!Enabled) + return; } /// @@ -206,7 +212,7 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles /// public Type ReplaceableInterface { - get { return typeof(IProfileModule); } + get; private set; } /// @@ -237,13 +243,6 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles /// void OnNewClient(IClientAPI client) { - // Basic or Full module? - if(!Enabled) - { - client.OnRequestAvatarProperties += BasicRequestProperties; - return; - } - //Profile client.OnRequestAvatarProperties += RequestAvatarProperties; client.OnUpdateAvatarProperties += AvatarPropertiesUpdate; @@ -839,63 +838,6 @@ namespace OpenSim.Region.OptionalModules.Avatar.UserProfiles } } - public void BasicRequestProperties(IClientAPI remoteClient, UUID avatarID) - { - IScene s = remoteClient.Scene; - if (!(s is Scene)) - return; - - string profileUrl = String.Empty; - string aboutText = String.Empty; - string firstLifeAboutText = String.Empty; - UUID image = UUID.Zero; - UUID firstLifeImage = UUID.Zero; - UUID partner = UUID.Zero; - uint wantMask = 0; - string wantText = String.Empty; - uint skillsMask = 0; - string skillsText = String.Empty; - string languages = String.Empty; - - UserAccount account = Scene.UserAccountService.GetUserAccount(Scene.RegionInfo.ScopeID, avatarID); - - string name = "Avatar"; - int created = 0; - if (account != null) - { - name = account.FirstName + " " + account.LastName; - created = account.Created; - } - Byte[] charterMember = Utils.StringToBytes(name); - - profileUrl = "No profile data"; - aboutText = string.Empty; - firstLifeAboutText = string.Empty; - image = UUID.Zero; - firstLifeImage = UUID.Zero; - partner = UUID.Zero; - - remoteClient.SendAvatarProperties(avatarID, aboutText, - Util.ToDateTime(created).ToString( - "M/d/yyyy", CultureInfo.InvariantCulture), - charterMember, firstLifeAboutText, - (uint)(0 & 0xff), - firstLifeImage, image, profileUrl, partner); - - //Viewer expects interest data when it asks for properties. - remoteClient.SendAvatarInterestsReply(avatarID, wantMask, wantText, - skillsMask, skillsText, languages); - } - - /// - /// Requests the avatar properties. - /// - /// - /// Remote client. - /// - /// - /// Avatar I. - /// public void RequestAvatarProperties(IClientAPI remoteClient, UUID avatarID) { if ( String.IsNullOrEmpty(avatarID.ToString()) || String.IsNullOrEmpty(remoteClient.AgentId.ToString())) diff --git a/bin/OpenSim.ini.example b/bin/OpenSim.ini.example index 38e2a075a9..3015c088f1 100644 --- a/bin/OpenSim.ini.example +++ b/bin/OpenSim.ini.example @@ -1032,11 +1032,11 @@ ;# {InitialTerrain} {} {Initial terrain type} {pinhead-island flat} pinhead-island ; InitialTerrain = "pinhead-island" -[Profile] +[UserProfiles] ;# {ProfileURL} {} {Set url to UserProfilesService} {} ;; Set the value of the url to your UserProfilesService ;; If un-set / "" the module is disabled - ;; ProfileURL = http://127.0.0.1:8002 + ;; ProfileServiceURL = http://127.0.0.1:8002 [Architecture] ;# {Include-Architecture} {} {Choose one of the following architectures} {config-include/Standalone.ini config-include/StandaloneHypergrid.ini config-include/Grid.ini config-include/GridHypergrid.ini config-include/SimianGrid.ini config-include/HyperSimianGrid.ini} config-include/Standalone.ini From d7fa9f671eefebd49c0e8f56e56088b0c0b3d93c Mon Sep 17 00:00:00 2001 From: BlueWall Date: Fri, 31 May 2013 22:03:27 -0400 Subject: [PATCH 16/18] Adding standard OpenSim header to source files --- OpenSim/Data/IProfilesData.cs | 27 +++++++++++++++++++ OpenSim/Data/MySQL/MySQLUserProfilesData.cs | 27 +++++++++++++++++++ OpenSim/Framework/UserProfiles.cs | 27 +++++++++++++++++++ .../Profiles/UserProfilesConnector.cs | 27 +++++++++++++++++++ .../Handlers/Profiles/UserProfilesHandlers.cs | 27 +++++++++++++++++++ .../Interfaces/IUserProfilesService.cs | 27 +++++++++++++++++++ .../UserProfilesService.cs | 27 +++++++++++++++++++ .../UserProfilesServiceBase.cs | 27 +++++++++++++++++++ 8 files changed, 216 insertions(+) diff --git a/OpenSim/Data/IProfilesData.cs b/OpenSim/Data/IProfilesData.cs index eeccbf6965..0de7f68af9 100644 --- a/OpenSim/Data/IProfilesData.cs +++ b/OpenSim/Data/IProfilesData.cs @@ -1,3 +1,30 @@ +/* + * 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 OpenMetaverse; using OpenMetaverse.StructuredData; diff --git a/OpenSim/Data/MySQL/MySQLUserProfilesData.cs b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs index 5d7149bc03..4c6c8e3aa2 100644 --- a/OpenSim/Data/MySQL/MySQLUserProfilesData.cs +++ b/OpenSim/Data/MySQL/MySQLUserProfilesData.cs @@ -1,3 +1,30 @@ +/* + * 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.Data; using System.Reflection; diff --git a/OpenSim/Framework/UserProfiles.cs b/OpenSim/Framework/UserProfiles.cs index aabbb84538..61335917e1 100644 --- a/OpenSim/Framework/UserProfiles.cs +++ b/OpenSim/Framework/UserProfiles.cs @@ -1,3 +1,30 @@ +/* + * 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 OpenMetaverse; diff --git a/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs b/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs index 4ad729748b..5a24ee323e 100644 --- a/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs +++ b/OpenSim/Server/Handlers/Profiles/UserProfilesConnector.cs @@ -1,3 +1,30 @@ +/* + * 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.Reflection; using Nini.Config; diff --git a/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs b/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs index 93da102b8e..f5f0794653 100644 --- a/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs +++ b/OpenSim/Server/Handlers/Profiles/UserProfilesHandlers.cs @@ -1,3 +1,30 @@ +/* + * 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.Reflection; using OpenMetaverse; diff --git a/OpenSim/Services/Interfaces/IUserProfilesService.cs b/OpenSim/Services/Interfaces/IUserProfilesService.cs index 12fc986a76..319d3075dd 100644 --- a/OpenSim/Services/Interfaces/IUserProfilesService.cs +++ b/OpenSim/Services/Interfaces/IUserProfilesService.cs @@ -1,3 +1,30 @@ +/* + * 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 OpenSim.Framework; using OpenMetaverse; diff --git a/OpenSim/Services/UserProfilesService/UserProfilesService.cs b/OpenSim/Services/UserProfilesService/UserProfilesService.cs index 959c661b5f..d00f34d650 100644 --- a/OpenSim/Services/UserProfilesService/UserProfilesService.cs +++ b/OpenSim/Services/UserProfilesService/UserProfilesService.cs @@ -1,3 +1,30 @@ +/* + * 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.Reflection; using System.Text; diff --git a/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs b/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs index dc0be038c5..927f7c93e0 100644 --- a/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs +++ b/OpenSim/Services/UserProfilesService/UserProfilesServiceBase.cs @@ -1,3 +1,30 @@ +/* + * 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.Reflection; using Nini.Config; From 07058b044be59b6e07efedeca36b2b464e984195 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Sat, 1 Jun 2013 14:52:44 -0700 Subject: [PATCH 17/18] BulletSim: experimental movement of physics execution off of heartbeat thread. Off by default until more testing. Setting "[BulletSim]UseSeparatePhysicsThread=true" causes the physics engine to be called on its own thread and the heartbeat thread only handles the reporting of property updates and collisions. Physics frame rate is about right but physics execution time goes to zero as accounted by the heartbeat loop. --- .../Region/Physics/BulletSPlugin/BSParam.cs | 8 + .../Region/Physics/BulletSPlugin/BSPrim.cs | 3 +- .../Region/Physics/BulletSPlugin/BSScene.cs | 283 +++++++++++++----- 3 files changed, 215 insertions(+), 79 deletions(-) diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSParam.cs b/OpenSim/Region/Physics/BulletSPlugin/BSParam.cs index 2651e3b5f6..afd547a088 100755 --- a/OpenSim/Region/Physics/BulletSPlugin/BSParam.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSParam.cs @@ -54,6 +54,9 @@ public static class BSParam // =================== // From: + public static bool UseSeparatePhysicsThread { get; private set; } + public static float PhysicsTimeStep { get; private set; } + // Level of Detail values kept as float because that's what the Meshmerizer wants public static float MeshLOD { get; private set; } public static float MeshCircularLOD { get; private set; } @@ -354,6 +357,11 @@ public static class BSParam // v = value (appropriate type) private static ParameterDefnBase[] ParameterDefinitions = { + new ParameterDefn("UseSeparatePhysicsThread", "If 'true', the physics engine runs independent from the simulator heartbeat", + false ), + new ParameterDefn("PhysicsTimeStep", "If separate thread, seconds to simulate each interval", + 0.1f ), + new ParameterDefn("MeshSculptedPrim", "Whether to create meshes for sculpties", true, (s) => { return ShouldMeshSculptedPrim; }, diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs b/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs index e11e365b2d..95bdc7b32f 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs @@ -1513,7 +1513,8 @@ public class BSPrim : BSPhysObject CurrentEntityProperties = entprop; // Note that BSPrim can be overloaded by BSPrimLinkable which controls updates from root and children prims. - base.RequestPhysicsterseUpdate(); + + PhysScene.PostUpdate(this); } } } diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs b/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs index 39f5b0a93b..423c38914e 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs @@ -56,12 +56,23 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters public string BulletEngineName { get; private set; } public BSAPITemplate PE; + // If the physics engine is running on a separate thread + public Thread m_physicsThread; + public Dictionary PhysObjects; public BSShapeCollection Shapes; // Keeping track of the objects with collisions so we can report begin and end of a collision public HashSet ObjectsWithCollisions = new HashSet(); public HashSet ObjectsWithNoMoreCollisions = new HashSet(); + + // All the collision processing is protected with this lock object + public Object CollisionLock = new Object(); + + // Properties are updated here + public Object UpdateLock = new Object(); + public HashSet ObjectsWithUpdates = new HashSet(); + // Keep track of all the avatars so we can send them a collision event // every tick so OpenSim will update its animation. private HashSet m_avatars = new HashSet(); @@ -77,12 +88,19 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters public BSConstraintCollection Constraints { get; private set; } // Simulation parameters + internal float m_physicsStepTime; // if running independently, the interval simulated by default + internal int m_maxSubSteps; internal float m_fixedTimeStep; - internal long m_simulationStep = 0; - internal float NominalFrameRate { get; set; } + + internal float m_simulatedTime; // the time simulated previously. Used for physics framerate calc. + + internal long m_simulationStep = 0; // The current simulation step. public long SimulationStep { get { return m_simulationStep; } } - internal float LastTimeStep { get; private set; } + + internal float LastTimeStep { get; private set; } // The simulation time from the last invocation of Simulate() + + internal float NominalFrameRate { get; set; } // Parameterized ideal frame rate that simulation is scaled to // Physical objects can register for prestep or poststep events public delegate void PreStepAction(float timeStep); @@ -90,7 +108,7 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters public event PreStepAction BeforeStep; public event PostStepAction AfterStep; - // A value of the time now so all the collision and update routines do not have to get their own + // A value of the time 'now' so all the collision and update routines do not have to get their own // Set to 'now' just before all the prims and actors are called for collisions and updates public int SimulationNowTime { get; private set; } @@ -188,6 +206,9 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters PhysObjects = new Dictionary(); Shapes = new BSShapeCollection(this); + m_simulatedTime = 0f; + LastTimeStep = 0.1f; + // Allocate pinned memory to pass parameters. UnmanagedParams = new ConfigurationParameters[1]; @@ -227,10 +248,20 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters TerrainManager = new BSTerrainManager(this); TerrainManager.CreateInitialGroundPlaneAndTerrain(); + // Put some informational messages into the log file. m_log.WarnFormat("{0} Linksets implemented with {1}", LogHeader, (BSLinkset.LinksetImplementation)BSParam.LinksetImplementation); InTaintTime = false; m_initialized = true; + + // If the physics engine runs on its own thread, start same. + if (BSParam.UseSeparatePhysicsThread) + { + // The physics simulation should happen independently of the heartbeat loop + m_physicsThread = new Thread(BulletSPluginPhysicsThread); + m_physicsThread.Name = BulletEngineName; + m_physicsThread.Start(); + } } // All default parameter values are set here. There should be no values set in the @@ -270,6 +301,7 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters } else { + // Nothing in the configuration INI file so assume unmanaged and other defaults. BulletEngineName = "BulletUnmanaged"; m_physicsLoggingEnabled = false; VehicleLoggingEnabled = false; @@ -317,6 +349,7 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters switch (selectionName) { + case "bullet": case "bulletunmanaged": ret = new BSAPIUnman(engineName, this); break; @@ -494,25 +527,41 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters #endregion // Prim and Avatar addition and removal #region Simulation - // Simulate one timestep + + // Call from the simulator to send physics information to the simulator objects. + // This pushes all the collision and property update events into the objects in + // the simulator and, since it is on the heartbeat thread, there is an implicit + // locking of those data structures from other heartbeat events. + // If the physics engine is running on a separate thread, the update information + // will be in the ObjectsWithCollions and ObjectsWithUpdates structures. public override float Simulate(float timeStep) + { + if (!BSParam.UseSeparatePhysicsThread) + { + DoPhysicsStep(timeStep); + } + return SendUpdatesToSimulator(timeStep); + } + + // Call the physics engine to do one 'timeStep' and collect collisions and updates + // into ObjectsWithCollisions and ObjectsWithUpdates data structures. + private void DoPhysicsStep(float timeStep) { // prevent simulation until we've been initialized - if (!m_initialized) return 5.0f; + if (!m_initialized) return; LastTimeStep = timeStep; int updatedEntityCount = 0; int collidersCount = 0; - int beforeTime = 0; + int beforeTime = Util.EnvironmentTickCount(); int simTime = 0; - // update the prim states while we know the physics engine is not busy int numTaints = _taintOperations.Count; - InTaintTime = true; // Only used for debugging so locking is not necessary. + // update the prim states while we know the physics engine is not busy ProcessTaints(); // Some of the physical objects requre individual, pre-step calls @@ -535,18 +584,8 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters int numSubSteps = 0; try { - if (PhysicsLogging.Enabled) - beforeTime = Util.EnvironmentTickCount(); - numSubSteps = PE.PhysicsStep(World, timeStep, m_maxSubSteps, m_fixedTimeStep, out updatedEntityCount, out collidersCount); - if (PhysicsLogging.Enabled) - { - simTime = Util.EnvironmentTickCountSubtract(beforeTime); - DetailLog("{0},Simulate,call, frame={1}, nTaints={2}, simTime={3}, substeps={4}, updates={5}, colliders={6}, objWColl={7}", - DetailLogZero, m_simulationStep, numTaints, simTime, numSubSteps, - updatedEntityCount, collidersCount, ObjectsWithCollisions.Count); - } } catch (Exception e) { @@ -558,77 +597,62 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters collidersCount = 0; } + // Make the physics engine dump useful statistics periodically if (PhysicsMetricDumpFrames != 0 && ((m_simulationStep % PhysicsMetricDumpFrames) == 0)) PE.DumpPhysicsStatistics(World); // Get a value for 'now' so all the collision and update routines don't have to get their own. SimulationNowTime = Util.EnvironmentTickCount(); - // If there were collisions, process them by sending the event to the prim. - // Collisions must be processed before updates. - if (collidersCount > 0) + // Send collision information to the colliding objects. The objects decide if the collision + // is 'real' (like linksets don't collide with themselves) and the individual objects + // know if the simulator has subscribed to collisions. + lock (CollisionLock) { - for (int ii = 0; ii < collidersCount; ii++) + if (collidersCount > 0) { - uint cA = m_collisionArray[ii].aID; - uint cB = m_collisionArray[ii].bID; - Vector3 point = m_collisionArray[ii].point; - Vector3 normal = m_collisionArray[ii].normal; - float penetration = m_collisionArray[ii].penetration; - SendCollision(cA, cB, point, normal, penetration); - SendCollision(cB, cA, point, -normal, penetration); - } - } - - // The above SendCollision's batch up the collisions on the objects. - // Now push the collisions into the simulator. - if (ObjectsWithCollisions.Count > 0) - { - foreach (BSPhysObject bsp in ObjectsWithCollisions) - if (!bsp.SendCollisions()) + for (int ii = 0; ii < collidersCount; ii++) { - // If the object is done colliding, see that it's removed from the colliding list - ObjectsWithNoMoreCollisions.Add(bsp); - } - } - - // This is a kludge to get avatar movement updates. - // The simulator expects collisions for avatars even if there are have been no collisions. - // The event updates avatar animations and stuff. - // If you fix avatar animation updates, remove this overhead and let normal collision processing happen. - foreach (BSPhysObject bsp in m_avatars) - if (!ObjectsWithCollisions.Contains(bsp)) // don't call avatars twice - bsp.SendCollisions(); - - // Objects that are done colliding are removed from the ObjectsWithCollisions list. - // Not done above because it is inside an iteration of ObjectWithCollisions. - // This complex collision processing is required to create an empty collision - // event call after all real collisions have happened on an object. This enables - // the simulator to generate the 'collision end' event. - if (ObjectsWithNoMoreCollisions.Count > 0) - { - foreach (BSPhysObject po in ObjectsWithNoMoreCollisions) - ObjectsWithCollisions.Remove(po); - ObjectsWithNoMoreCollisions.Clear(); - } - // Done with collisions. - - // If any of the objects had updated properties, tell the object it has been changed by the physics engine - if (updatedEntityCount > 0) - { - for (int ii = 0; ii < updatedEntityCount; ii++) - { - EntityProperties entprop = m_updateArray[ii]; - BSPhysObject pobj; - if (PhysObjects.TryGetValue(entprop.ID, out pobj)) - { - pobj.UpdateProperties(entprop); + uint cA = m_collisionArray[ii].aID; + uint cB = m_collisionArray[ii].bID; + Vector3 point = m_collisionArray[ii].point; + Vector3 normal = m_collisionArray[ii].normal; + float penetration = m_collisionArray[ii].penetration; + SendCollision(cA, cB, point, normal, penetration); + SendCollision(cB, cA, point, -normal, penetration); } } } + // If any of the objects had updated properties, tell the managed objects about the update + // and remember that there was a change so it will be passed to the simulator. + lock (UpdateLock) + { + if (updatedEntityCount > 0) + { + for (int ii = 0; ii < updatedEntityCount; ii++) + { + EntityProperties entprop = m_updateArray[ii]; + BSPhysObject pobj; + if (PhysObjects.TryGetValue(entprop.ID, out pobj)) + { + pobj.UpdateProperties(entprop); + } + } + } + } + + // Some actors want to know when the simulation step is complete. TriggerPostStepEvent(timeStep); + simTime = Util.EnvironmentTickCountSubtract(beforeTime); + if (PhysicsLogging.Enabled) + { + DetailLog("{0},DoPhysicsStep,call, frame={1}, nTaints={2}, simTime={3}, substeps={4}, updates={5}, colliders={6}, objWColl={7}", + DetailLogZero, m_simulationStep, numTaints, simTime, numSubSteps, + updatedEntityCount, collidersCount, ObjectsWithCollisions.Count); + } + // The following causes the unmanaged code to output ALL the values found in ALL the objects in the world. // Only enable this in a limited test world with few objects. if (m_physicsPhysicalDumpEnabled) @@ -637,7 +661,84 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters // The physics engine returns the number of milliseconds it simulated this call. // These are summed and normalized to one second and divided by 1000 to give the reported physics FPS. // Multiply by a fixed nominal frame rate to give a rate similar to the simulator (usually 55). - return (float)numSubSteps * m_fixedTimeStep * 1000f * NominalFrameRate; + m_simulatedTime += (float)numSubSteps * m_fixedTimeStep * 1000f * NominalFrameRate; + } + + // Called by a BSPhysObject to note that it has changed properties and this information + // should be passed up to the simulator at the proper time. + // Note: this is called by the BSPhysObject from invocation via DoPhysicsStep() above so + // this is is under UpdateLock. + public void PostUpdate(BSPhysObject updatee) + { + ObjectsWithUpdates.Add(updatee); + } + + // The simulator thinks it is physics time so return all the collisions and position + // updates that were collected in actual physics simulation. + private float SendUpdatesToSimulator(float timeStep) + { + if (!m_initialized) return 5.0f; + + DetailLog("{0},SendUpdatesToSimulator,collisions={1},updates={2},simedTime={3}", + BSScene.DetailLogZero, ObjectsWithCollisions.Count, ObjectsWithUpdates.Count, m_simulatedTime); + // Push the collisions into the simulator. + lock (CollisionLock) + { + if (ObjectsWithCollisions.Count > 0) + { + foreach (BSPhysObject bsp in ObjectsWithCollisions) + if (!bsp.SendCollisions()) + { + // If the object is done colliding, see that it's removed from the colliding list + ObjectsWithNoMoreCollisions.Add(bsp); + } + } + + // This is a kludge to get avatar movement updates. + // The simulator expects collisions for avatars even if there are have been no collisions. + // The event updates avatar animations and stuff. + // If you fix avatar animation updates, remove this overhead and let normal collision processing happen. + foreach (BSPhysObject bsp in m_avatars) + if (!ObjectsWithCollisions.Contains(bsp)) // don't call avatars twice + bsp.SendCollisions(); + + // Objects that are done colliding are removed from the ObjectsWithCollisions list. + // Not done above because it is inside an iteration of ObjectWithCollisions. + // This complex collision processing is required to create an empty collision + // event call after all real collisions have happened on an object. This allows + // the simulator to generate the 'collision end' event. + if (ObjectsWithNoMoreCollisions.Count > 0) + { + foreach (BSPhysObject po in ObjectsWithNoMoreCollisions) + ObjectsWithCollisions.Remove(po); + ObjectsWithNoMoreCollisions.Clear(); + } + } + + // Call the simulator for each object that has physics property updates. + HashSet updatedObjects = null; + lock (UpdateLock) + { + if (ObjectsWithUpdates.Count > 0) + { + updatedObjects = ObjectsWithUpdates; + ObjectsWithUpdates = new HashSet(); + } + } + if (updatedObjects != null) + { + foreach (BSPhysObject obj in updatedObjects) + { + obj.RequestPhysicsterseUpdate(); + } + updatedObjects.Clear(); + } + + // Return the framerate simulated to give the above returned results. + // (Race condition here but this is just bookkeeping so rare mistakes do not merit a lock). + float simTime = m_simulatedTime; + m_simulatedTime = 0f; + return simTime; } // Something has collided @@ -656,7 +757,7 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters return; } - // The terrain is not in the physical object list so 'collidee' can be null when Collide() is called. + // Note: the terrain is not in the physical object list so 'collidee' can be null when Collide() is called. BSPhysObject collidee = null; PhysObjects.TryGetValue(collidingWith, out collidee); @@ -664,13 +765,39 @@ public sealed class BSScene : PhysicsScene, IPhysicsParameters if (collider.Collide(collidingWith, collidee, collidePoint, collideNormal, penetration)) { - // If a collision was posted, remember to send it to the simulator + // If a collision was 'good', remember to send it to the simulator ObjectsWithCollisions.Add(collider); } return; } + public void BulletSPluginPhysicsThread() + { + while (m_initialized) + { + int beginSimulationRealtimeMS = Util.EnvironmentTickCount(); + DoPhysicsStep(BSParam.PhysicsTimeStep); + int simulationRealtimeMS = Util.EnvironmentTickCountSubtract(beginSimulationRealtimeMS); + int simulationTimeVsRealtimeDifferenceMS = ((int)(BSParam.PhysicsTimeStep*1000f)) - simulationRealtimeMS; + + if (simulationTimeVsRealtimeDifferenceMS > 0) + { + // The simulation of the time interval took less than realtime. + // Do a sleep for the rest of realtime. + DetailLog("{0},BulletSPluginPhysicsThread,sleeping={1}", BSScene.DetailLogZero, simulationTimeVsRealtimeDifferenceMS); + Thread.Sleep(simulationTimeVsRealtimeDifferenceMS); + } + else + { + // The simulation took longer than realtime. + // Do some scaling of simulation time. + // TODO. + DetailLog("{0},BulletSPluginPhysicsThread,longerThanRealtime={1}", BSScene.DetailLogZero, simulationTimeVsRealtimeDifferenceMS); + } + } + } + #endregion // Simulation public override void GetResults() { } From 43d804b998e6f37bbae771711af0ae1ffcc84cbd Mon Sep 17 00:00:00 2001 From: Diva Canto Date: Tue, 4 Jun 2013 12:27:22 -0700 Subject: [PATCH 18/18] New HttpServer_OpenSim.dll with increased limits on number of connections, requests, etc. --- bin/HttpServer_OpenSim.dll | Bin 119808 -> 116224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/HttpServer_OpenSim.dll b/bin/HttpServer_OpenSim.dll index e15493d338184e902a38476f1020f6f4cdfc3144..38a4cb795c87f11e4245ee1000ca9fae17f64b05 100755 GIT binary patch literal 116224 zcmc${34mNhwKraU@9lfLZ+B0or)RonOJ)*Cxb!l~WFTZA3p<2;PeArHLA<=%VN25? zfd{A%Bp`wTRDv6#C?K1n2#6xc14LvWSsvn!h>C(Q{C~ew_jdP80`k81z5mzA)U7&o z>eQ)IRj2lf6ajC*Kv@>(R*voxH3% zcv|3}6dZHv;Bm)%>|_3l!DCMt3{L;p;IfYm?!51TgQxn(pD>ikxLby#_uECOy<(>F z7T&yn6x&P6n4F3wl=^;5DO*E_UVwcN_LuM_ceERtZ`g!?e$^m=_y}PT zZ1snM!;d*}^5TXirbRj0U$3yd1ShH3eKMsX^-`Ql=u z>2G*lO1Z^LjcOj>I*Nd0cyS;a9%Nt})o;P9Lz-4`<)%1h^_GVBB%{XAMP<$~)g@tG z8Xii49e{(=f-DTbfLKD{P6X~b4tNZKr;G!hyE))R1b%89<_((zeuKc<$6@}2z$eB5 zUnB4j|1iWD9LmW00Pvtw-AZK>@I-)fR)>}JCc&1Uk+KG7AWHviP_lv=f_Ip~Rrq)V z@b}LrIN=Y{UZ>qh<*-fvNJe%I(I&&Iyr<=#1-OtJS}>@9zZR7+mWwB{@w~YGDL~BG zr9KOgtnH%#vB!I6?fi_F3Ps4mRL4UW5Ng&O81lD3P}lAnN||jge$_B3k(ebu|i|PpII>-EP!%^%*d~B;s##eSt zNpF7nh|yUzEDzcRSkT0O>f?||dbBO&_$9!cY8mFr zL{Y`iOe(dd>RYJVc7sR)Q)dmtoQtc|3SI?>{tyh5gGr&%TxFP-Od#^1K(V~(@bzZE zFw8odDXVzirv7nnxCH?!QxInvgI6GXO)tlE&NaN58VP#XLTt6@i_U;moMIH}VxgYB zhHT?!Ldr*gi|RHAxzhC0hO)~A{q?s6Y}G6n-gYp`so)f1&eoWj5R>0cF|TQ08a0QN zM=1MqNGI0{GX1%L6~}_+5i}P~PjwGar$p`IOpW4iProxFd?A>LT=VJIS-d=Qi*H-# zvci}wO>b8X_7@qkMcg6AVEsyWJ& zjUt9(WHa-bJIof8U1o<_7YhJrF*QIexf!y$AN*;1sd_mx0WscI*C*sIiSt}nr0RA>)t%3m~6m8b(G2!YJQNbb-^>=6T+v4T49lXpm ziQDq`05}()y&5UXN=a~2ai_!RFntS3&TFV4+khf|luvi=iuc}oZ?r2LKOGe&mb0^V*+CnFa_7DPnhkVY z=LdDNp_ZyEey`9+RgjmVuEf_tf(#Z(*GCz3E4JviVpJq`ox+m!{!-YS?jiby^jE>Wn%>AHhl6qtF)VuwSaI05srjWSUS=ky0eyl6d`2V_6k@ZUd1`1Fs(XJGEo+%buzzv#f$gc|IHHQ?01VL|C$ zf^2~luPqK$=RgsyX43?ggojVzn*tBeCT4JPBQ+++*6wm<_5qd?vbHm9_&=7lks*^X zVaIIA5kMR4v{IJhz)byW7>7M##>;KyREe>Bsee2oW?$1%K6u0UKFhw&IxCBIAyFd*H!V$Dp%| zzP<49r3Meu6=BEfwgxB;CLd|I(0Kq;b|o9U2$-dNxF>=7N-M}E!OzLCqwhSD3JP#m zQ54`6b)%(QmNK1q)kD`LUBwX5&dh!o)i!I@CdE`3eMhhqVbLlqM`%p~{sWQ!+=Fz0 zi4maOA3Pudghmi^p%qSP4LTW^KMzL;CP)p!o%cTqlxZf12Ud+3%Vf+jEsQE>4-9&z z0ww2U9bJScW}RRd#Dnc%_#cB`FpqH|Ki$%VU?+Mt0J%ig%HmJ>_D=(f_ir#Rfhvdv zdombPCt>SMT4~03{KAqC4iVD!0??`4pG|mGS`x6VfF(*tHtwAcXA;xy6RB{tZCC5oBq8ZO7{WB1U|1^q5!beMCPt93#y^Zg^mn<$egM*RU z)`3S<5MMC{j;~Kar8NfGx2$hTYxPV}s!ydgUpXzyJFZb96I0XP*7i{8*L~(yLt}7R57Y`>-5mx{wlaD zsnBsQ9K~0Rd>Eby_g4cpP5Tdi9IDq}(;DJDIE+pq$bOiqt_BYikfz!2QAUjJfXb)TQMm>$^UkESA!_o3ULXTxa zF9Ou|K2C}{&bd`K9_F-BRgCqsFSN7T`cy41aH&51X=Nf{jC~B~aL)8*%@v(Mgxs0o zHg%|NQ|;M4R0;<#HAr7)xL5!vMF6oxg$(pe|^w`uGdio-35Hkzm>mJc0sy5Z7-9i{;uC;a06?A70I>jo(Y6MN1po|6H9#x?j1aH@ zlA{HcD`FUfa*Ze!AZ{N4!~y_wxp<;cqsYq>^w-}Jg3oV^6$`~<$NHFn`a8jw^_pc7 zm0bEUhPMIvge;wqLYAHc;L3tq+8f0ZrPdwDjP#*}TPhCkVnEPh?n_Kag^i!aa!0xw zSOb}%RB(F(13#ICy+}IFA(%cLYTB1D8(m+>%^~Vz?zqTFe725M8Ocz!N+hg^z~4$u@PG)-{H=9-rwt$r5yiqI5Wv|Ihjl5b8e|Wo6NfFbCw-U zMU?2O`Di52Z*nmta$UfE^BbL`xaoXFyjE!+o?4ZdkO#e#S}0 z)|VMwPzUOh#^4aLST`z?zTgDVGPNK?cbxHlDB`=ps3I!2j_F3UgjdnJkXGH%((7uN zzO;&Wd>~@WDxF~GW;p6BFqUH*?xQ#24ot_C`e$J&$k>J5>HNUZby>$p?z-)-kOefmG+Fc`)#iF`41o%nDm_La&3c|AhjU)huDk z_%@Ws4&_60x?Gj8eXO~K^DRFFZk}-jeG|f-hbdyw`Bm904M)$E77NW8LQVx1weX?{ zV_KvUvKf}0I7Vv&#p_VN&=o}4k+E^w@zD};v8T7gX5->!5@ynW8GK6S$!raLP;K4H zi}MVX{!zo9lCBtP-$1nf_ORi1f1@nom;`z?d@HrnS%p&ff5< zJUN=qCeSR0Gvcj@nPE6Z2VLG{fQRh46Q82A8j?LMP5I$tvsUm4N@puL*`kP_q$^qK z=}1bta!~f!xJ*ULPtbPxkqGuummiUC_KMHuGPP3kKnU{e~Zkj;nLG)t?!oqHY z4p&-5yx#$ukKu%@y^e!mF)#12JQa79!d|F;ACA?cNKOoO=N^cxp_G3?-19c$Q(sIp ztG*{b$Pcci{Tt-g+tk5LI;Q^;$fQbJIJ(bCediiABG{q{?drWi5OFv<&z~C2I@0q@ zH&=Y1LK&%!*{m_GTRK*RQb)Y)Xhx{D$u6iSOqF`19Ommj2^Q_ zSY}gB*U%ILih>#t;PGUa~6tdycKFUPj5DQ?4I zr8c~$n7P#3Qn5GNvXZdw%XFyn0^JT$$FB(EF)NNCV}#;nV%$RO%tnlzo9fIc(x+OR zb6eD7enz-}X`|}4>PyW}znZ?M=7(z_2Y{c6E%IZX-%0ws1BY|D;JZjLW!i7pj=eP0 zyjUpQ5^za@KwJv+D#QR}gw?LnKkKI-!3JmVV4^8Uh-@B<*}@uGJhtP=&mzI;qUwR1i- zYq<6qzPYr|h@!ldHF;=qJrUvVH)97q=j+OVYdSIXcZooJCk5xXd= ze<$?dRw$Gjx(jZ7DS!=O9ZH8)UqZy*Dte^uN^FrK)MReh>?P4tyD%4bN92o)*Bvs< zri1|qf-2Ye;$XK-XwyQfvZn%NLk%*$f?E9{Vs&jz#D5SD+y4>FOQxXa{U5oj4cqBXcoN$0B25>R%6XFv^mhpd0KuIt=J(hQk?bjTxACo#X!) z*xKhrqehHNmz9&+mS3;ysqi}l`d}XEBNErryRepyH9xjxZrQR^qwS3)^Vg()7aevb ziSGXdI4l>2f{IXEd;pz7YBb%XlLw9S!XyP>VmF6V;KGU$3gxN`UujsnxIuQ>K_%9AVQf%l%EoXpj9;L zrwU3nLfMWe=!3bd30g&?{xLy8OQa*3O_cv23PG!A)P16a8n?Os`y@b-PPQ9skVY5z z9ye;s0G(mROQ_M;gJn7iv3Yssy&@3VUI_EMwr*z+lY{ic)dex7rb=Cu z2n)duh|rW4PNFpj1{;0i6dFcT8oZfXhYa=vM~ua9aaZpG+bg4fH2V!MDWpL%A?+*I z>6?V^97m|>4hdmzl^y_PlEQ>64h*M)R2DT=;@TX4Tfw&_JwekQl4Wa;nv8){IO^%0 ziFM|DuhgwzErJ)kC*WUhvU=4xnr}ATnx;`>Nq2myaAF5();H~u^rZ^cq&#Yn|1-pZ zA|~@b2e2aHXVh4COy6Vni{0W5T-yxx?1;Fk^jO9@EC=3`pn#R1W$=o)kkYI}`afp` zP>#APJq4GKF@el9oQB|1^Ton)?x?flN}>9qE!i4;_l=+jl8<{+Sf&bYS4x9>M~r;R ziZ4M^VCAgY3lRp3_UpI}8vA-?nE8?ocVWpo3=QL0r{YEobR$VIhl%K2fX_{W>OuS+ zW+=r?%1`5O_8S(WwsZzhY!tN{ku)ik!7m$$#P))xk&LnfU5U7;GPjnvFyA@g4mAT$ z$VGv7flW)u>6&)|V*+nOm<=6p7o7p80XklpJUV(Y)^xI#v($eE@p#h!H*^xPKZ11( zqh+VCRCY0Coh5~(nXc6A=@C_?+a2+KnyFnXXr}WsO$e0vDjMIIW^fAMpRgm zm_FD*f3SfL+K~8CojnKupqOG! zl-|fTdzO$MFC8m0d!i;8m9w@ngp(+U|AZnoYRgd-MWO2*=K0|1u}O=h(UPG~7uTy8 zu&K3!alkO~lpc3C3^%i2K0?*PuZ3_oy+eBQo8X^+<{Cg4nQr5LTWfE6?n|Q1MNzG2Hm_C+QnQG2G^x$!4(47sNMY zGgxM%EMV)F;{>o^qLbuc>y=qVSoDTpX;(GAM)9^ng+}2n2zy~yMUv2LWpz$VHNYxf z*1{s(7G^fJ4!a54<(nBw$k!c-7qHBi%BHYF0y8o5nXH_O37ht(B59BoI&69;WMbjn z?{MD|#K1KGsCN08);cOFe%v7*l%ZuLhy8~LM>w=B5R8KOWFnv8l=g8)jP9{vBA*O> z0w*8oM8w9u2V3L26{>@7FkEg=@}7lG;kKAq+Bk>JNWwvvM3r3I8TlpoeZO$ef5>oX z=MWz|NG6hX@B5 zAwD+BE%DhpK0*;6xZX5<|kXGQp8IWaoR(cEifdi#W^7e=UJL0lIQAZ5T-x8X~5hp_~~+H+uGqaS&Op+Qm4 zOHLHk&ll91n6&#FrQ1Ua(NwJAcrPFc!3aw0I1;F&u8uA0aIQKVAq)PC@bJ*f#Ry&p z5fUNlCO91btVTE)?@mEFN=%17Y_=U@m8}G>&29X<(D@LkV_36F79 zzTG^-ft@lf3qaoGWR~?w&t1Nt_AAFQslzrQt`4IGzp&6@2y_(HH<}f9rD1B8f*YdJ zss$o#ssC+|mGZ4KR;H7Npp$g1TTQ{ehg!#y;9U&d;uO=M^mt}4u2vdKRgk~o97%8R zJ;b5MRSzNO$oq@jw@uC$xik(wB`IQ8QGG~)BD>xO@3jS|^Qcgf3A@uysK!YpDsV*-~Z8~ji zG-F-5J}9*Ymqf&R0ysJMgua3Nc%I>D(%c5Iuf(_%y(3hPR|)zpL2;fV4i&`j0091* z^pu)|869ff;e`ISqasn0a2p_11^T4)jxQD69B19+zTV|w+#%K96I1j}oq;h&b1px+ z_Xr`v+SJKX{dBvR7pgk3TIuBLY#Sg~mML9bsU4{l*w4x#UUbE>x~Op<&GhT+)ok*( zpVw;i8^cY;%sFG+6mnf^eG9Xzz5*<*9yG&iB-AXVsjhk7od{IOt;x`n^CAx%CXeKy zuR*w!xN;4G8BFX6=WeZ_5?PIS9om*H-o^Y_^-K7v(Ok6-hTh(ihwvL9Jqi%KRi6Ti z_gNM^Oln(s?@C(HI5Ap&FXNZZnpoJMz8?C9Kf}8UxB;r|nEMn2xD$bUHZLy_;4uWE zibZGj1&GYu6nGJV;A{-$4VwdggTULzVg7_bOgN1R4(&7MJ_U(=A*2}W1|z&vK!7n` zbC7Iab0EMz0$VOw5a9Sr76e!&CX~z=nx7}I<&p)#9Dm7z0LNdlAV5sFkGW()fTt4J za>;@K$6vA_!10$X2ypx*3j!Q}$$|jKU$P*;@s}(JaQr0;0z8|yB(z+zAi&QO*mB8& z0LNdlAi#eRv*nTn0gk_9L4e~fSr|2?v!=_icO43AS?)*ZriQ7pw#WuUENYnEweXc= zIOvL^Y*Rn%gKvp#^(ItO!PzJ>VS^&oD#8(>DCaF36|ojEebpZb`Ya3(=eZR$SZYXU z{}I4+sWaif0Shi{kp6Jl!U9e{+}iS=t?RU0%0XD&r*YKaR?YR{kD?|{2Tg3LLR)Hk z*T4{M9lDuB9O^(x#84D^yY(gniZ8qgp%dChJExIp)K7aew;|X)g`Vo4$onQ2M)U(( z8Qugu5#UURX?t(NvYERij2hKgc|hMOq0zYMx@8+T)aL)_ZV68D>h(Z%iwH$X8^B9diz|3+zuy1(A5bZ z1RophZaG4@!Cw6-$WBTbRx!(VhD>of2)27drZ{VH1ZW?>Xi zSU(R_mXU5np{Qck&0bBA!0Xov$`>M(+lcZOqa$b)jrw&&(d(zdBj8dN8w+~Zdhk2} zlOVi!@LJ@D#^rK`vFcay6R&@opPHB(?F~^B{*_<_tNArXfIie;5G^hOLhr)69yo1Q z@Ye>#HmiOGu!;wSr`&GR z%!5p?@Gnyi7_Yo1d5q5CT4@(8)i+PQDd_7&U{;M%tr=sIb34t)*JnK$-+ER za4`KQ6mW?F&xdYsK(a}}X`z6Gd=8|gc@~T81?&KuGm)OWYn-zl-rip!C=BkdfxX=6 z@GJ+j-(L;zv=?7T>O;K05n$A=h9}N}-AK8=4N#~5cX||{GwEge|9~@XB>bCzQAsBX z{yPA?2nNuUM%GR%VRc!&{43}j-4||6 zFDX}~=y}f?i(mzthQzCH6ECh&ODRqIlR(DiVVkdkQ=DAWdXqKw2luOeQEy}1tF;1* zk1+o=#opj5FdxSCKC&3y6~fT*+wafXCSDEJY+;RO>8K#^t5npkiIu6bgAwg0q-h;K$(U8SfeUU6W zB`U};nxouCWJXc9%*E{p>0<7WP|U@}H#ZkIKHT$&ky8QxD%`dbA1i-Ld>CtTu8~m0 zXKyaP@!=%C4+zI`I@)nH3-L8?@UhC+k*H!a;;BL?;hi;lR|3|^Hh=?cN@M>YHJdKW zjQef6qF)cEk8UMe`u>D(z``vbXb?CJ7U88vd&3t0h1OO_lSHep+x-0%4tBaMB46WR z?KZLkiDKHK##l_>1od(ZJZL8d+;R<#un?yMT7wRL$);!zvEViu8whp!Y6V_ABwER@ zqxVe>{Y8ereuyrX7_$`$z5OM6f-G_Q6=Zw`E@P@a!m{0RNtNND8*J;vn(2o36DpM+ zQzIDOLvU6daOEEd8?La@ARlvR*p`wn4sp;@@E(Py*yEx79l&wB$&UbU>_>T^A*;sz zlGhs`Tuf6BZAH>WF+;B_`m(w4)j2h8fQsZOX;FfpX1Rk>O!WYZ8 zlE}_5kz>|tQXh{sCn(Cr}Q611>`dPHqewvBu3qA!@4GC@}JEs6IJRjSFZTi>{ zgd@dvc*I(l+aa~%Y5Ke{`@i6XeH-Z9kf%qRJVhIQnwyO^FH@r<&6+Xt)I7M1)arOf z)PxQ4ZPN@CTG@JNpyP;DngP2SJK?xPLIZi>xJE2IOEFjVAEKa!*y?4}(czhG=D~4*%_D!_D2mW# zWb7A66NfttuNyWV3F*Pt??aIQ4EvGv;SOVC9%Gs4aEA!V$8fosIgn33;~!9y7e&0} z)b59CS)CW$iA*U}pFmw*P5mk2@R*?06cA3!Kt+1b!W%Ujgm}BfBg{vw_i!%|8bP;? zsQ?R?M*{UZpf)gDijh_SI&qM0&O7Zzq2L!zEz!#eo&{Z@6!u`F_+D+qXQ9r#*>FK6 zevJXm`b{K_{lnf1h@Ejzhn$D+_K>kzp0fFirW#w~+>+jUi()`dB;Lx*;2(&o(Lpq7 zOr!J=iGypdj2fdw7m*ip^+RIC;FL*R*&(9#v^`uC!Eq<|DnS#g`QS(H(SaCm2Xc7G z`O;;To?yWM1Cuyz>hgYtl<3fUe=N>rZfEmK6;xX8iEUnG11K*o*$wb;|7*+TO^th1 z9tiFMeVv?i0_zNtibTQOkmwGh+mJv~5;`f1gPjH&>~OQ4TRbtir3o6t;Xc7OlN-oZ zGtK(0mIX+&Ovs_pl4RkK;Q$CeUPC?Mc0PBc-T4w72;u0ABgsKQf0C)i-h1s4 ztcSNwLnaxeA&E33yR8jLaG%uNL*BN+*i9{D9jmdFNR*~O8PRD#r#}UD@MNEku2M`c zW_l8+I)QPC&CwBQ z5b6s#2w^Rx!rsT-quGRKkK!NrU%KpZ+I(zb>OV#Y-{j#NbNHaI<vO#~K{#bN!J`N!O9B30 z0I4KbeTz{euoFBRd8MV0VrOciu_culhW9%L=?#7d#00M5WR>Q5RPQaoF^^#;bMe63 zQk@oef*d{)k5n1C`$n)522QIcoS-lAZ(+#^wrE1Pj;#J4Nj>REhet?e_4e2@hE>?P zqUR}e+m}(r?esOv=*eRo<7gD|MUGs;q+x znEe`VZ_fX-@b{)8e3~qs3%w=vFZd?lQhfc7foOBHcSPg123@Hv15Q*~Do*eq{IKx$ zL<6Oi*1#z6^9>&-c(-ZSHJ|0nR#=(keUw=vKT&Ty%}#Bst_=`)2iOYN6?v<|z~FT_ zF1Q}Rb|v@7;D)YzSOIZ08@46IXYUD`2IdEk1s3buz?6}A0(%tF4i9YaVnVbA{-)vY zc>I;lMwPr6xE0J1%C1UW=7c~)+)f_Pv5`!b4vuh0Z~n-ntR;KYaS#^un|1^w`c@uNF!Lkog}d{Y9m#s=sM&M&%U8=L$Zz086Bp-J>tNq z$sIjXdz=1zKqPtouQ1%uo8a@q*b%M+(0c`nfUz0X;{O*W@>&s_LksW{wLi}9Ne*)CHc2iW-muh~`h*CzGaf+}h5NTjJhTji-?%kAiG(^{! zuRxw6@N#Dqm>`pP@3 zTVFH$T7=)X$!j#4CKJ4y3D*T2l=OFoMHdI&Esj=i7XbX%z_zz5Y+S|I4PT?i^7+wO zQ$h^c77-Q&fOcn~Jzz{LC6fBI>=#GfX1BFb_1Qk_F=&rEI-Eb_c=)UfntZb?K+Ydf zo{iBxBVn?*f;%l);h<49ZR$SoDAJ2kPlp{!ul4RZ85d_%d5-48P-w)D(RTEsn0V<& zi3R+zox}7YbqelxSPUlWXw{Gn;_HQ3$UV5YXxD{A@i?<`Plm@7maHKQjp$#7+_!e?y7_P`@!L(f6)xQ z|K`0#k8OioJV44WNjIkOTH_hTSXH|Z0HJA{>VSc=E_ct3oBG%a?Ob#2v(j*3&f`v& zJ*$2fKE(@z`S3Dogn0)d3Zq82SoPo?1)83m8I};-RU#IhkL;847j2r|pY1F6*G6lH z+=V1-hlRMTrUge4gNun6)XR}E%ahY$agPVZ*jmk`01@BajF<6=1%U5F0I>k@-3TC- zxyIn9QO$4#Oeg9xFLM#6z7!ukvfM2dZpkmk?lz}|PHD)76RYPpToBPcAaB%Y2I~NC zZpvwZ^rjraGwa_YH;bcP1$4<)1=&OR4={Eh^EzDDpMtcuJp6Ewh+cx6jMVdlSYL1@ z;xuXm29Lt3XKv*cgmCVfq4H2#67Cp;TOxhvREsx8-{eku_vwV}kIPgKMmYABll~#J z@j9eKVHrsKOX#+7KdEdXh~Oy=$3D)Egq~Qz5s>BbQbg9)n}e=J>~sDd!bO;+AcF4q z835&y7VmrI6-lCa;abdA(FDRnN8`v0J(H4FdYIjt-wqOY-))rts{a5V)*tyEX8K0}(Wuc=Z(HPcZT8)d1UCn@l*ys6OkxeXM!J0d z1H<1gjAxI64I@wN2?Q6|kyCI1JQ~2tD|(PSzcI+QD=AAVo;)_fgzLWEBsC?RM{d4P zabL)(d}Y3YMd)Dg6wROQfIQP(b##qnd}{|MG+;P|7vSxW)4UO~j*H~aZFm*nbxp&o z+jcNuy&Y~0Sf6b{GB63snK;`H-fMuv>)6?cn+1M`C?o%mD9#W@xaS9#W6SBtO6#Vd zS6i0=^`;-4c1KP2^!gv?RIVPE#^hFwG+I#_r#E4nO?UkL(;feC>5ji|y4hcu3?2_b zx{>Aj=?B@y0;af)WheQ1iy*e*%wS)*8WV`;!+|VEUd9zhAL`JSyu1Z|I(^un+P_40 z$6)YYLjPnv{)eMoJx-mAsoeyC8ysBUZt@CuOgH2Dc7&YyI1Jn*Hx(z@L<$>1`!*go zUTFA^Ep|>&L~s@f*k#gU1MaveykF*{9YTK%BNd&Nm+@V~auBd(l@`tnxV|74zsAcB zG)?yrn4CWn5$lQ1s0jZ6knu&BD+n59mG@nBKdM2T^YbmwdZg@lzeosk?!hQilPEZl zfy9pqcVk1MR~A$@B>H3l1QYP6A9kr^$9p9pCb~6p5>G%RyESsMMHI&y`A(1+HzWkz16z?PYoCqFQS z7rsj2=rd{<++($Zlk079Zy~DpW*&G5^9Bvv|A@afyPRvo0R_pwezHBAiTKcLGcz&O zxDx_}`P^o=z8mskl0rF;ARXwHh@6|Uq?|XArEH)Qsf3eh)aSoJ_`|~Rhcu+m@J(v$ z@ieL7eyFt3^5};tixQdHYd|R}>k_VoYj8RYdGf<(dGf=-E_X*SDH*L3k>&1qxyy=Ll)DkEbx<=zVtQ<--~8xI>T5jFX~A(3d1|Kh z9`GI16+h>;aOi5IlY7f{K!K67Oc?8pd5`5IkV}|Jn|bfWEDzej)lHO!>TT9nJNRPL z--;I1n*Q} zFQ0kGptDhK!+wXlyQ;WcC%0&wUJw8c!py$PK zIK|YWXc200l3B$Xo(!|N8wmYCQ+*Ed_z2{&>6>T=K^`3JHg{CX+YxT~G8z^ww+FY5 zypQHTjdEN)fg(w0IByQH#WJjE&zl;C_i#-P-BJ9+)g1ta5 zYw2gzMqQ_jJrtPM!t*}tNA`nJtVS(Zqb6W|F=k-L0(eGc-wdNQP2MQ7ixKLxNpzf;vw|KE?h`UgZ=|CjN9@_#9QGkAu@QBEGW zuu>yKrte@C>V3t6c?(PZ@A38b58x~D|B$ba()-IR`7imp@_#=ct>A-L{NOB=JjGT5 z`TgInXDwyT3f@0^EnV!S2wFa~8jm&^;*9$rBw;i(to?XOpauUNC;A8}qgiPj%1pulF0f+?vDhz3lI8B1N3Cxu>_x6i6sZkZltV9YM++s2- z!DG|(^f5Qi8yjU(g0~mrKEURrgzVWiaO30T`Fgz_@li1e17g8-ZPY?O2ECj*6o<(- z$mGRI@xu78#S0@MaZx9lJjU%Z7&$ck)9Jz-uRaTM6x&uGGr`dl5yNR5vZQ#}XxO8@ zZZHjnJv^U+AtW4*o;Q+rTUbaYPE?r#pytJ1RGD4bM4%729RNp@ey|>)eDpnSoU%BZ zAAOV$3x1e6`6$fl8Q{vb<2(_L6X0hFkEF3%L<0c<)b}5Tl)4b`1Ibx>D}4*ipBXiD z1uR^6=^YQ9Thz|*Xc3Pr%cc}LIsGH@d?=B)f4hd%9m9N=48QV9x3*=4ujI=)34PdiJ=cvf%1Y?h4CkE9bmQ#rT} zVMcb=WqWEa9`5{PN%vPmEcqE`X9@L#fV>Z0-3;~@#r6K8ByPD)VhoR?;x=v@jVJsS zu#KYkv~^s+nTzaJBmG8PrpAIqnGboQM``P$7FA%!{ncQjaySzBIxP8N@bWk!gZn-p zTAYMMVP|#mR)GUI?f9A z9&`u0g0^=a_%W(~0|(mQJB|$A0e59SkuGf@NwuhUfII(oNiM4i9||1fKA6F^KNZj8 zdx9g`5a{>7hwnOxK1#Eh5bNXnkHwg#!G?D-@CVf*`05}T1gTy{@@=q%Pmy7N0fgyd zGvUGtb{X3G#jj4o=m`!4Kwk?hi7eu|DU^(q6I=ifzs^#^7X$H5fVCb4Z505~jW>Mf z;@6?pfr`*~3bG!20&+c0>H*7|E_@5n=~8*O6Lb5RL$`xUAYIj*)tSTjZ$>jUmuxNZ=YmmO;xo|UdaG&0wioVy#zej> z@!^Us?qEy8dkwrR&!Ael8}pE3wABfZLg=c2?OI+oj=_kJQ?F1(m{hj&Ymu55cTL-= zcAT<#~(bjR1f-saWP3cH$hOii6`Z*B514FPvOrW06B}VrJhVb+h zcuKeOWS6(nq_s$gdr)vmTi50m5@}TIl;*xr%yL_HcwhH+c!C*f0RH~KIPL>X}XHS{@F!?{6? zN8PZe%Pw_h?bEMDAuAiBJE5XoK*^$pReo&-7lH4=zd(|@fyWjGdvqdi=%eN``hw3g zU5;)=*i?=xIWxBdI#CA74O%w2cXlDZ&}fYx)ky)aPvScfU*N)qt!Vhe{#jMQ-#Hg9{NW=wu^Fx{<7N3EGd-FyHEKUaig}L7JCod*XbD6SPoP%)1VFs&gN*xM zMmp>|(RCn<32_UeK9=f#3!WN0h@A!J`d@Fjwe0+x0pNJiR-Xc2{QV2Q9({kl;?++g z;yN*mnwU)vL%^oI5wDdu9h&*w;LTvGBEL{9*we%G`rlw`I>WRxU>QFAMn0OD)a#>> zqqrYQXErnxb2xHyHBQ{o*}jvgBc$)ua@KAiEU);2N|D zlYQ%3gRhfC4w!suJ#(0Z=7Agtg&eY*lf(FM!p8fBvvJwfs%Jq>PF-KdvU?$_?P`Jq zUT0J?KY2ZwqwZbxT!fFS7(;x51m3{Ml<*BY#E*nGF!72Y5KVH2U@ge}ri`XwD!{RMvF^-+8{?tGbFZXyv2#$<5?40Vc>5k}G214)OEz8AI#?rqe>1?`9t zQ|))~sVrk&qL+OQg=#}zT}Os428RC#Y=m4JvFJZaNWuRmKBXD|-GI$aX>tGiBxL&E z;RkOMA~tN`_RxKD{rffSQrdT8^YQ}%{M%`Urq4$U`fP6m6JX>)PG6UIkhY-FQW@U<&%ow5B{-?mhXeNA)6z_+SG8zen+mW~cD{NU6PU2_@w|`Wm}?ZU2<)u~VlFdoBoQMx%Iw#zWmU=jhb|^w0W=}pa$2% z9_i6-rEE|vEqW9UBlQQNAADxH84eZIUGah8;_~k5$KFRCc-8{vV3~RrVBU@9>bu*o zXxKhYTT!&km9%~i582_1XEfyNUquIQEhf;v8iv7ZgOJZDjQi&!FZeTRuOd|y534G) zgd}iZmxK}$cv*uEeQN-CSr3PPSQg=#&-~4qqfB%g8gfKm3+>S#$I7u22#_P8kR#AH zZI8x>6FGk9aBpljK0-e@zGk@A_%;bgO|FM(qHBMT6u3-hp-wGftoj>FX%e19zUlki z2@-gnsWw)<9ijZZfkKeL>x|G?_4f$nDWXuxtR_g{^*06O9}&tgh$4!Spj9;Ls1l)K zw1U0SKa+`P%Wvq4;1mM+W^Fv+Gvwlr%Q!w8;mUovZQ+)^uMo;DdsxlcZ} znaW2HqYqty-eCy7mEcDS9&UoCZgg%T{67dE83X?o;pmgN{&d3S%EtO{7*Sqlpy^T9 zq5=7nFl5(%E8n<0V_wG~Ra%wb^UGF6EUSzfNmzasK`p;a%p6QEb=lLP3T3``U#TTP z^ijg?fH(7nn~1iw{H+OU+^vZ&q4fL*jt>emAp9Sbr!W!**7#gdvx}H-)3Nu#qijPAzuv z@F8P7w(YSg)Jf&Kp^XfC4cJ=27^@ml>7u~}KASIKL=>eF3u`I*1>W7D#{K~+j+TO0 z0ATqmezJA$S&F7EaJqDEN7_K8h(v;70RU~N28acKzeWJDh>YVRW0Xpar6%g6pMD^e za2fBaQVuJiPGWKwr%{udZdUhVwp|N>kM>3K?hSTAxd>+uwa^>)l5De$>N7xu`H3C~ z@210<3ev_ihpc`!2Bm|$q8xb-0Qlmiz(GHr(zKU^Le-*W?&B`OzTkXBW(bCCNs!@Y zbd{FW#HgS!oL`1~(CA36#$TKc@=m(sg>_aiu`QLDxLtlMu1Quz)NH!3rO!d@6{6;6 ze9x&E32Y!5-ZF#@<6au_2y$UT)l|LDA(>&4oTWNR!cKWl&6en_@&a8vDzh|D6Var# z{nBOHwG^GZ@ns(uT1siaB;ak3jts?ckX$6XJz53yR-|V11o7c$->F8@V|sLTu*U|e zbftDxisp7@E@?0B{yT&nonA zvVqPOSF>z=9yXy=O0^b)i%1sZb6uZr z(d2owlEd@CA+j)>_yAVVohy3>R{&9;`Nft++zt?)0KlHb_|!^zT1zNwU&KJ_7{skF*h=)`^lqXLr+4)w z0;O4+4t6l&gg%}HMh}H2^))uOFlqn}`6l~<h|x?Ko>h_n%NbyQZ21fWn0ahI^&TcI^r5ZBS-tSq z)x6xg8eW?=o$-7Dl?_*rQx}#8L*?^3NWWeEM?F;FvH>lkSB;VgwZ$NRk4 zhh}#Q9~Rw47q8j#8$y%N05^MnGn?QsJ-=lVFqu}n!#ZUsGD->euN_?oyRFPSkif% zni1pc6!J5?#}UgYH0uV$^Fla|$GmJJ8bez$FAJ`)7x{V2o=exg3jK(7U5;ky{bb!3PSe62xDGP-yiYZra8GAD#xxsF?()2)6T)Okk^=0n{t z5ii}k;R~xSI*ziMFi*#^t05~R!6!&PhfGAS2&Co24;OL6=bJ^>t%p>Rjp+Y$!D0q4 zMx0r4V34gbe!R<(0EC*R!uT*TD!gc(&5n&mx zjq@u7r3l^r+lE80&q;agRs-8L>&aiyXrGPvtG9qWW}4u({aayA;U4@-UkJDjfFQXI z3IIDU`ide-UQ~fij z^WKi6|0Zl%96h%a{++d(uY~E*`QM0T`nzu<+O!E}P=Wx}cKH%YMn21t+Yle7CUd`9EYmF>z02ZQ$w*{%Zc-(m`xAh^4On)c{11rAVeuvUYl zwanVwr{k{7=5WeCm(686@RI#>EVvYMiI8>wyFke0@kn&uyN6cXpWy!ykyPSXYQ^$z z-uoWF6+RN3-*8n! zPKl)?stxd!NVs*Q&>D%(Kpn=rm+Vb=(O{Nk5j=}uI})@#E-es6$CtN6R;~FS-xm3P z3i4fVIk&mY=jOU+Uygk3==ScTE0axkWISx2aD#)1zMSa0!k7l|0|09&^^|6G^yqZ7 zW!torCS}vG{)xGQrA1f2Ld{|phVRiS`!Q11)0(nuda8dM(7KoU$K#Vt`wt@cG;s7I zeCHmEOhSHEqOqbv61XOo9Z)T`ZtDbK}`!{Y^{ZFWU4uF@Tc8G0xLbPW6 z`B>@U$bOPB|BFT&f6Vmc zop4RLp`OAm2;f(@B@5I?!ufDqoA?k?QfJ({ZJ;8?SV1ddu>cT@0Aj(k0b3DbqMT_= zu>fF30I>jIMF6n?5RU+2iD=oyKO5Dbkd*}dja4a_GJlxDd|MS0hW2WL1YUQE60LXG za7e2m7DOQ54MvTGO#flf5~qkoMJwiMA4H$uVB;pa!6)d9N4XW(N^)UA8fskkoA-rZakgW7HUp*kR9hp8env$r}=s3)#yU(Td{UxUi4?u%SnmxW6FW%hWYe2#y0GGdKyR=xwulAZeO< zgnEa8$-_;t*EX?#Mz&3z5iD2i51j+vO$4+W(ah29E6+SKKAd^P-O$~^En@F6^f(lk zMW5M5PtiY?X2jBrpv>CpShPiT$^}EnXAv~ae(9gsA7hI!z?fwu2 zcYovu7G9UN3j@<&`cHyyys{l6w?iNPWF)z3WLqQ1(De{pSiA|k8je0A$R#76Ha#jZ zS zWN!*!jR5&nXC~U=rtb}!d6iR{Wcx3%zVA(5v%y5LYUuvf&zT_!jX7i6WVxHPwKHT! z=$CrKWNgNdm4x%joCo61G8Z3Yn2Ys6()lBJ8?ND+I<6ow$`JmBS*%SCc6g@zkv&93FeM5Ua>latswD&^@J@LX8F6i3tIHopbz zL24AVOzGi7r8CJQhE8q^__jo0(VmDZIZ#=&6TYpUVLb2}vh%oKr*xtAHEJyOSbXCL z_gG9pim*lyI(chHHam2INPH|7bSBYw_kv1kX#^))W(3_wa;eeWq5sqPKY_uTX0wHH+|#}( z<0U7bo_!cEq|m?-a>0SbP$$~vxN zt8Ou;h`F0n2IOStV3;cea<-Tki+L4HSKaPxfbR^P@w8PvaZ18gUrAgC^B0MS6RvtE z@dnH(N#ZO|lK!WXr1s4u@$Ya6`IDG8rVj?Sm(us8ZS`)NVLzWC&b^tNGq(DrnAHiy zUk%g6E9SJ%$q{FNF<%gK_b&SWy08J}Xx|&)WT5}eK3ARHe^|e(uIMNHiAlFka@ET) zZT0a1(m!Cb>$Is8ryM@TRl7}PsFhQhivRMMqSwklD%>8Ow7@3N$nqiyQ(tlzVx~3fLV-X%`ArfjF@-L zBDIHLKQQ)l@!ewzA?fXi(^IH6WVWqdnoXYnDW)@raaF`TNX)~;Jb5nR|2FsLOozI9?%KJwx>+DI=P^Y) z!L-#OVt!^GW4T_;uZj5}OjjMX{TqpM)u*;s>h+jT`_0=k)E#0zw>^1yYkQ`pIG^yN z=I=P)R_BR%t(ad2+*QAy&(doypjm=xs|5=fFuwOZG0YOpoXJ(KbFYaDj|YscyI zGA7$F%cZUd<{@%lzopv5uLf5fnxb7?7h+~E30F>BUxTX~zIkzdPd$ZkaU5EQR9;fQ zg6XI}6~j>fZTQ9E>K9BWR)}Q~<5av2*{-&Z(GPMJ*9>uG;UbmUaB(fY1S)*r>M5+5 zcjG+=hC4ELa*TTs;R1IxaSh`BQ>JKrj9d@$raHK85}H$RN0SNHU7!@lYW;CS^M3eE zMEXyb_@0sY%FyPd@+%2f#yMuX-jW#Rt8aMo&DFv!^=^&O!y4KAMf|Bhe| z!xe|?d*WIsuHDto1alAL5GeY+)GOk*9IiNAzZaTo;F<`1{YPnms#9DGOy=LQsz+Q0!WG9^o5?Ze*a;@ne!7|?erLfIhieaU zT?|(SD>;kBb-lRG!fojccdLXuTkR))_e!|4)v4lo#ANxrSY0Wc{X+aMSJ#W*>n3yh zdfdrOD({+<=MCr~GkqEBSMa+*Jt(eTxL#0SR*#6QY(0hb+pnm{#I>!(vh+3ejJOtA zOzq9k8KiQs#ay{n{VhgWpNw#Eb%!!AhuW^r6xW>?bCYhPdX%caQI^?#KK$YsV>Zrs#e(SNzV1Kc)Vz9#FdpXBWVw)I)ffid<@u00Kw|$c{3gP6i1Pee&>5TD4(RTH+#5XZEQ)vE1xSkT*2AE1cJC~eIiERhJ2b{@jvKmP2 z1#>}SKbXnnI+$B0zm3{CoV*M6qmn;|eStd^i>ZTP9UMA+1Vt!W4 z4~vQI`01xXIhB46W|x?QViv`mDdqy0d9_pepTJp~At%SejHy#HPs2P%%ylp)tFL5! zY)n>_6yW*k=U(SD2I4 zaE@}`HTPH0Uy^&zjM95;?oIQM)g#$A%~|TE+#HM2o)7bu+^$wk-JK(E_vY@xjd?fa zYN4-&Zz=G}%IvxeYvx*dpLHLz4p}|eMRS^%B{4_DoC9-~TG;iXn7@O0Q|?cQ<;bqT z!aQEgqmutX*z>w5+l$1!M9iyTPFB}+T^2uN_1$8Br0eVP$?CbTTjDYGnLN{btC)|$ zoUEL}ui=|5`~kkYj7==O3Hwli5}RKj{$66W+!2DL>xmZ@6 zJIA!M>ee|9%-80$!|a~hW3%UeBFrgLO8?M(8t|E$v(#UDUbJ-%PFAsArcURejyKWE zJi1MC?Z~c|5lZKBXD|8b@8$B+*1f+1d`KWS^)hE4=)DY_za-{60_o^8oXKitA5%I{ z%w77J%dA1e>LY!`4`2?f^ThYpeOm&5djHlix9=Z7M1S!Ha$26R3>@sogm?*F6hO~9in zviIStdpo2%dq_eO)&#;97D2*lfCR!4c1T!MG&Jdkv~<#;y8}c;iC`4Ph0(!%#0A{Y z;J7Q}fHG0XebiBATwuhg<32KOqvQ9UQ+03OB;e0^{?8X4PS<-*-C9qbI<@reURNPJ zr#F>nG1KL}sXTA?ekXp6_`LU#F8SiS-v5ptDF*b(ap#MYKEps~_M!Mn$EXEHicNjI z?sRc^pR?TE#Bcjh?w&kKXGGprh(96kMw}_l%A>L^$vX?2D$nhplmdD`kLVY9e?gr1 ze2UW>G+%`CDVGbFUdHsQ{BfRqaU+NK<(GQYh+eqqpWsMwET8ffeV_3l_rCOODyS6a z^!*7>sb{zMH4^g0wS5zDK((uH7U%;^pW>J=F+I|^JNT+SzvxTl`L1sRS}CDlAMm^N zD@aHe1^osgd}hHx&<_ee1gC8>QKOjX@5g_LT!!_hT8!^MF_HRiI;cw1*&}U}sdv`) zr?$GRKefPb@sy?cJNq}|DM|d0J1#$ zavn{zdcVs+il0A_TB8IsHacjGd@%meL>e7bk^zGbRw0$)+srvh)K+uAQN6#1 zV?NAO*~wPKT(W7-5V9l{pku^oL)yA5+;qW^JxL2UT?m>lE*|nWSzSocn?wErT~e)g z#}H~sva$JM&yWYO&i;onTw-mCIR&d^#K&&}r5rY~x*B@e%n-d#xkhafh}O7g;|HLG zujJ88*DPLv&b!1v7#)=FU^^yG{EJbm=l%E+c>Monv?WKl%YaY>{55Md)J7vAo2`($ z+`SAwPNhO(DV}qSPy6w6qS@^Q`jJtSxXK;CKF5dxlC=sdS&}HG%@ENf?sM1k@8Gg5 z?mc&teHz&s*XG`aw?gdUm~E0|St4fu#cY!#%Mqg)sdDCui8lHzy!q;i>LVkn9 z5k^PkA3R+k`&!Akj|O2!k@Ev8fb3)&m0<-nK_PceLZK`aczY?8%$=Xm0@-Yh3LzUJ zDl{sB47=d`*P48D!nty&h2r)mG{sQz(+OK8ozc=?Yw}w_BNPDoIpGReQ}x%HoRxTm zJlRIW6R(%pO5wlO* zc8JO(VT==dHCmD6Hck=7U`l3(2tqblq-(SuXo|?wXiHMMF-;8B=*pxlpj$P%9x-Q% zJsRB%G+W@kd-%Ib+?>?Ym?vr(?GO(o4KNmob8U38u@qxi<-RXzvT=r3!A-hD>`N*) zs>HuEI+nD+@TzO!NYj(N#8@dV98~|zMbd>vodQ5XptUyIl2j;hcb5NJh1@?Se~#Vde`}PUa>O`S z{8yv?DWBtny9+BxvwI9=8$~Lk&F&eHohPz2s)Fo1k*Cr6loD~i7@?jM7p1fr=ZlLq zx+0|vHs=zJu3_|Njc#J(Vl`|Mw=z1Y(S0dj8cpI0M$PUYQvPjRAimY8OX`1s_y^oE zs=B5c=4Rn$^=)?dPIUvNE5svp3%91pwza=$p;j48Z8Ms+tW{o~I>fx#M%Sl~H@7h& zoxeSGGM!fOUu*KcsbvoIaB88v+?M@0wME3xyQwAi8x2~M|Cu_=yuyz8P3nB}N*l$c zRXR{++G*x?Tb7?zbM%1@=i(MK~-`*qc)rk7`9`Ue7 z)VF)YlNwRq?h(&uM16a&cv&Or+k3@Z8d2ZgCk|^weS4qyQX}fy7V#aUCighBS&LAH zzs*BEy;tDw3jUhhGa=h6x-x3>P`}?Vx-)8aFYH<(?iWQGtwhZG#R3&ml1}_yyrmK8 z#Dn5~oCA@sK|1lE_`8jo%twT4ypoYlJSx6CMIq9OM@0tqvFW+^^cL(gjbxMvCi6QO2kl9#V$ukD^|q3o>$D`^AFE6th`ekkQ9= zK-{F!r5W3>cl44%(7G0JP`quU0j@vML-=crTjeQ`&nSSgve7fl^_+#`Zbd487N6M9 z70BmBb_t1_kmfwsi=vDX)u__-vdAkXSu^`KuZUq9QJSxa`5KYu@v2y=5qTc3iWM4> z=kXWpf~!=>^Z1L{tFNs!nOTK6fsh@{YXq_r0Vw(J%qp3~O_Uv*!la$0{OX-j{ic z>k}J2k-6J}$ou`&lCdY}M6*mAB|n?F$Mu;NGx;r`&&4t>8EM@t^9!+BA$L+%iTILZ zT63y}Xioiu>t9+%bLv6YF&ojG`kj?S+&l5FyS^9ooMs!=N3Xkn6lt?ayhAL``iJXh zv7C__#X>G%q*_TxA0zdgkYPtmQ?7EvH09a0%!o7P4f;9K!8m!VMx@E{a=${X1#bB{ zqs?MX*7+`v{8pp0ff6MDbhc2vnJ7ohrc$fkOq44asd8emi$ANCEJUr85SOfF)Jhp~ zxIxJ=Tjlksd2v~C9jBt|o+CHes72(+R)ySmWfjU?d03;3o)lNE>^_ImQDx~a2Qg|E zRKM=>OhzLkiDYOnUIZazF{WnKe zYDDY5x$%-j;O%8p6? zXAF`58AI#fW3IMiUJM^IhF(kCgp(bs{|4qZJJ5*SZ4N|ZEEbc-SPT`TECD-}8M%cr zh8E?vIAmw!ZtrrI9dmW=H4d~s_eKXgKldUt7V{#s%NqMR8q+aEW7VUBb5P`>d8O5&5a^zp>p*L{uJQwAnqS`z$jme`3_;p|Uhc z_i5CIZ5}F1gM5b3W-+(>KfA1xuQ0kwEbIPNm-X_$Iwtks2I)CnJxBe%L8dcmaiD_j6>WM%1_G%H-(8XdP>gfyQOBzv6ZP2NE7WtOJr~Ji8jHOH zoY%@_jL0vT7=NuC5FlBTc&X>i_#5PCMkHGhf0L}V(V6kL$N-~eH_e8-WmF@Y4R_0v z&!SYC-G_UYh&$v2jlSwR%e+IDF=`SuBRLDK4*V*y+$io^f0@@>UYE|xJ%kPy7G$LDmpR81M$E@~!MvGjc z(eCjgvqfI4Vv6_N*TlET5xhx9`^mS)|6ZQ15U;r&kTn|J*Xz#s2V}iQPxSgd&<2fO z>UAqnlR{`gtZyxgg7sf(d<=cx>vMsxqVivBvb*;Y!$xu0yET_zs4(S+VDKv&x(E09mfyKNL7{|EVuu0ybQq1-QD)@TEx zH#NGTcZt|94>M|&v?IS?jt)`oG#(1%Q?k@X|A;>*=i2C#_-EucMx>w5dCMt zp`<15Pvw4%f_+k4U&$9Wil8k2lCLwuP6lNEk|pb{8a*8MwVY+6{c-=66^xX({f%6% zkRVU|8~N-8e$IVIpAzwdd_|*&`^++bkZ&{EA^y}SvkFY8EqFY z_Gxw}7?o#}O!@kWM)5g>c3@Op>P|L(JeSZx`8Sk0)rf9XvVZp3?n*UYXLL~hh~7*y z%FZL1>Z5dH0V8E;(~a90HHnjZe`%x}dl=FBy*0k8G4p(Su1(U&$uR0|)C%;SMl^CV zjhsy?CXJjdV~a*Ka=IA@HKLJ|ZJgUA@Yg13zh~eeRwH zf6E9vQB;FZokGNjO9~r6ri9W^=CF6BfALC<K*68KDBL?2-q|u=~oWL2IH987e ze}mE|D^-^NclQ7rZA>`grZ@U1*{vBLI?yW5=k7tqHkAs>Ilwi@xWPs^SL5fbwe}E= zXs!LVdzg(bPyNA7FR-FjIjyzFo1=_9b}9w3$VhJ{DV?CE#E&rw7^zuwY`YvL*r^0l z<2=PSx;!=2Q(~j*Q+s))w|lPKmIYG-Vz!MgPaWu)W25U+Pxj0=He5_)QPy;Uah*c$ z-T8%bk#UPcT+T(tql{EJD~zJ8)^pQ5ON^-sxu40O?OAHfVbqGVy7P>s#@md@XIShx z)fjOJJ=ZGegz9wTb&cqR>P$mks$_H~EV$Q7Uv+SK|p7hiw$~)ES#Dvt&)4s4??$g)UCG(sPb+F{AC`;)EMK=NngEp=595 z-|5+6+-RfwJr^6NU1`al@LXoB(db>o+-_{J(fyv^8mDYmF(ZA_jq8mmjFh#x-k8UT z>hQehdgDABz2>>exXwl&h+B=ljMRv`-3a`K(rk8T_r1k+yRlZIetmZXU8>PZeGBC- z<0_5D^=%QmxZi0!%|x^t8{N|7VZ*n>l1)f>#Q4!hBgLb})z?_E(uBv1ao1XCcES_J zF@@aY`z=d&()dxMihk8V1Fqwk?qI({`J^#iqjUPTh$oG)8eIa}AC1Wx?S$-)#!O2F zPj$bsKqK;0_ZyFD^l-n=#Q|f#Mo;xSVjM7@(}>PYpECZc5uKSnWgOLr&P<;+zS4-! zOrJJ>RtQ!GMxY7(OhX2u^fS~Qwm&}RJIIIPiqIKO?zIC?8R*Nky{f5Lmlxwl#9u>{O=w<|=R z_)){h=%8F&Fb*iHWs3`5ceNQ?6mp+c@La;j#y*YC2KuLQOe6B(KQT&nahh)O;6F9C zY4mx)7YUyk2Q~T+(C5Z~6oS@GjQ_$IvYVd+S`hy);|v>}8UKxOC8Hf8yZ84A|1tL2 z$V~jn_|!&eiNfr82c>dQ(7MBAe$P43&exjUICC(^+%66=I$fjW5|YtVG;S074@``q z-2NvecClp1v+^rF5%{%X; z{0_=RKpEzKMry5=ZN8#Wb$?%Ct~vBBirFTqEIrH@6!K76dYi7hS>~a#^f8MSl2nI0 zb2{fobr+fW=4zHvIiC>uX1$GqiG9tpZ4^l?Ft^y~+{6Lq6^vR1?a2)`A7yk zIoQm+hjMRrf8Kv9zTH1ZqaT2Vm|HYTAFx#pHTP-M3uu`6qe3XlC5gk$WA~~$&^}+0 z`J+a(&sSu|-AA&6a_E5T6GxkaG%6WzYvNdQGovQ4aKK%O1aY zfULwU(C8>+CFTf5ZIbp|OU)Y@HHohWj5nv4pFTi2(603TiBrwJ4-#sVv==+gy!9aq zo!kW{zcy+SbIrpV(O&F4^G6#sn)A&S5A$;#+F@N}UaJx9uvVC_Xhb?#X}TX#F-Zp( zn^E*aOtUXK2vPbE;gW{(FWn!uM73+lBqh4x$tQcA4A_g-awrxE$ytIbFMNHWzftIg*X za=$<5zgOCPte@q_9QQnOhmPdFUKAWNz1p&S695;~LR9Y`yuNMsyAvHpKyfzcvq@ z!-h?dMzlVPnB5pPyBiBjfO>0mVc{$@Vh&U>`J^doj<6ASmrbumG#(mEpF$owv28GK z)QC=O*O{eU78(!F_gHVuIdRt&-jEPO&*iikjppn89QEeHq;t%LPf@7}9f>>N^f6Ln z>;m&@CF51(1?H_9(K-4B<~Y>WAtMsx$^ zLi315bOYr=^Ph~GMB0!t(QM*+1^%}1=x;XXFlv+35*L}LGuk1>4mmaHBJ*;MN`_P= zZ8i64G;@eQ=~6T2Af>WHR1dj4>2mYpKM~q4qC>7uy23oB(YZjsF`sKyvQ?g&l74Hx zqY&TxyVm@i5#4j#m2|E7qed%mC-HhS@fmt zV(*Ybd7JquqwV6cA^VeVGv_=@sWgeVhD>(vGC$SG9Ex33v;H|HOB;%tN9NUxc8H!s zTa)fE_i5SSp=->$%q`DT%w|zM^rfV`%-Jsx+Aih{-R`=}+{8$&SME0d!$^(kyG`>& zevU`S-DZ}JTEyLEKN}5j-DA?XCeIj(7as^es440|rC7&wBl9od@Fh&IX3q7XrnrMS z^z89ccxb4BG#_Uv-Op5EQ+&^yU*;RbD89-!7B)pZ_);V@?Zz}#aw&Q-ho~V6I6Ty* zhB%4CqgkrLPC9}4rJ$ym$uXVMUmu>w=`7-OmT@{x4*i;jOLXRM>=bhwb5v=sK^VUU z-6>Azr;g9Da#X49;?(wn;vE~HapE~nSxKqIOhIv6g5tz+oGv=f>);sTZBQwk;Wp-c z#?miAP4Sb1qw1@q6gEZTa7sb(ovq;vQ(Kw8s;$&hC(^O*oLA=_>ER2A zV~E#4O>IRLhwQnHM>v-g)lNw%reyucuibL^!{KD}hhme~6l4RX=*V&L zsENZ<2BZ|z;FzW!L7#9sUpeSE2pht65~W6AXgk7&whV(fOdL~81hs6PiZh-0^RZ0~ zKEi6AQtABfX`YD`3=!ZoX{_Nr(_BXss=XPc0RX7D#_ET65H zBYuBJSb15Bqr68&bsWTmeNgf3Fh(nLV(ozM^0M?y=EuU8573e0I!+48GOJ!wwz`f} zBaUUs$(u5G?l57UXj~}U)sZiS;!qrXJC;+3a0!*a@yp>&oUe*Qt!mXmahf4D#TB5I zy;A%em`_p{OXIYaQ~g45j%T}0WCPVx?Jc2I-#GRq@Gcaj883E$y2U-99&sP%@(`#Y z9s@Nwj*H{OahxtXj`EsR`l?No-B2F>A2=OK8Q)K3s`TOi&=ziIHPU-=SjGP{r$Avt zyuo2rJEx^Ok>CCf$8TdwYYNMwQ0oil468=yF^(AvpLmA-FUM3PH>Szf8p@fnvlkLZ z#^$5r^W%7y#QTp}_b5K*JB)N|mUO19?4HV|9P|Thuy5^&?Ts zKT-Vu)f-posQ!wrUo34`BlQ1eC8AQG^iQ-}i5F@$M7bEemN9uPSYHE(NY$pn@ z;?&kKRT@sa6L|YGj{@>!B=1)kBJ7A0TN9Zqby>R^h@+l8-cH3vmQpV2Da$KXo^~uv zYn`Ra6Pt^rh=sB8<+5$$(kctn*~dOnt(|;ln#vRC$oX}%f9$E_l`|$)C3k6`L3s!< zYH+>G<9HOb$}>b0+RzlLoT?VvxK(#BpR~>;ZblgII5`2|>E}4ZRE87h9xEZ<$G|af z=Uh~{r9;>hO8PMKovrN*@8=Re3u?0e;}UNo9LII+qHCezJIi^ZuruWog_Y;5d^4pj zO7}W?>Hq)u@E%bViE9+o7Fbui?pC-M992TtWR^~2 zx{&#&olI-t6(``3ziRoSs&>v<_?Ks1HN)aJz>q>6sO6=vV~%P)XLtkiH-bu@HKg7d zu=+XTr3Lk zzd!i>!S4_LAp9SSch-#rIvFQ?C*yN0Vik+G@Y}X2_@9M0zzvmikCNr7#WZER@v}r5T?Vv5P|(&VqilA=PHZqyZR6I;dlqGvph z3HqK^y0NL~s^qO=Z^E^pmlypGl)md25Z4y1F>$-G==S7*c(|wu&puJ~XmYyoOwk{b zmy1`6o&w$Ic@~^^iuNb16-SF+PtHf~hmr>yaifu!NCk!Khx>7*M)l~=5Oc%muQ~pY z$>}It3%%_F(p)YlZli&-kZHQHmr^k9!CQ6GjfY0xo;;rUKF-%Cno%Pk*DEOKJj=%= z@rk1u{ZoQ`Q!&bE2JjTVS)tQ>rzo02r9C?Z2TtH05Q$?hNb#V)h4O&titu3ck(ArT zuzvsTLek41?KS4AlC@9%IrcXXLHb7OA<;axL>$6Boig#2aVv*+k9`~b zd&ZWD?~Q$9kEDKO90Y$4*L08g^Vr{~NRv|VnExF6VJZ%>(J%Ok3DB)Z?{Pna(~R#& z2E>qY;E0jq64LTb>MyEYYT5xYd0bZ7V`9#@-W)DqIy7yqSTb%@+Jl_hKBnnL)i`?U z%Q1m8(yryHUa@-IIbBF6w_)^Bf9(^S#)Z;wJcry5a!>7J9eYqyCB4COPudII`!8@G z?c--(=cnFg-8jn9Pq}A5<(@spDO?TdJ!wC(^hbX77(aVV>qfI@eEQX5|G1KLmn6MP zHzxF6mQEvXS^6!;^W)Od?|a8QntY7)oWjo`CVl%O-FS1{ACkwLpNxAwz0{;{PL`Xg z0-`ex>-Ch5P|TseMO*IQ7d z{$>GFxIYLNX-+V83*t|nKiD@;NsNGwdN?$kC{JSPAtyK953e= z_sVP%$FgUcO`@i_036}IKVdv><{UA`OS*+ddQSPCQ;gd{oMSQPScY$LKb2gDlE0f$ z#{D~o`-RGXZDuLLH-lbOyiJr!8mIW)A$qM+l4e$NeJdr|+e&^G-yme^(TpRRKIHOk zW)Qh_OI^&YJ1l@q<)a?v9x zt}R^mEnMrZVkgqv!nMAU>FrGSaGX6H=XP$H+a+0*J(A{#J)HjSlI+!k+!y<}FAnN* zJ~Z)Iw_Mkg6MyV>kW0IVOZx(+{5q%nI;ZnGr}H+a|2pUOx}>@0ZEnNY`KdF#8J-cD{=3nLU|M= zyeI8bF5#!#Vq3*+MTuyuhbJYZZ50P5eVM&gJT&o#?6s=JTyIap%^Z1jQeuu*{yZri zG-)!0$pU!g;K@Ykj=fjTn>;vYyNgG+Y@U2l&Mh3ii_OsSk@W~r;+ z_!d#>A`6_(y742o!;ff(9;t_T#yMnMknw2FW3F`xW#UJ!h0CB(<}yeRk1;2SIZ4b( z7q3m8?U}7DG^huQSsIYJC3G8>Mrc6xEx9;1)1cmb4rRM5_c_;` zlD)YL5%UT9{S0?7k!XbJxuEnGuO{IwDFLPXaRH_a`36Togi8LATg#wK~IVA+I@`<8w zY6`sU(fDb&p<)W?2vH7NBo={=gHETw*FFoh1mBBI5%hig6fu)y&f%B~IA$fsT*fh% zBc>ak;JctT;(gGd_z1LKdxKn-NGg5DYvAj#&Niq z!zGeZD3g@JOnkG+E#^o{p+*+QrHG)M3|cQ2fkx%|py$f#K$Faa9+cS?OhTDm8G(}#i5~-bU*3W%_g%k{-cNuD>s3JmGYJ_IDC-fPNw^r`jR;vrZR<| zb)^s|pTnh0y-a<~Z{qMl4jys%^Ef`!Ql?&}O-yf5oP3J2 zq94&sOm{Nf&-5_Uqy4BCL;=O|6;L>@KZQ$}-rk=$1p|og96r|;`k;Ke-npYlPFv;ndp{MqIpv|HKr?=ZeqHV>3*iJ*%b43rmi^@_A%YU)HRnl zW%G$1T|jipLZV_3rR-Wn;XDqPFx|o&Q9;r?rUexgr=*fOOSwEu4>J|Zn9sC?>59{c zvx(_Wru$X+bmH%4dYGwL&d)L}VY-6pCZ;=??pO2-ig}o+IFp}Zx`OE@raPJLXL^{a zsN$GROPH?k5~m=-?ZEUX)4P*jPCk;nIOXD$Yf~Occ`7A8b$;s7)SA>isn4c35~ypZ;=sTl#0|Bf6G$o!NCk*E75Rrt7_3-|hNU z*Nlv!j13uEGVaTGF5^hX*BS22!pu>blQJta&(7SN`9$V3naNo*v!YpNXZ$tx8Hr5_uIQHyL!d~4N)N~v;#n_3PC zfoW)g8HhOxp2TeI+s(nA-Ez#VwP=+PUeCA|DMYb{w@!=~jp7vix@<9aawg(El9S*M zOcoc2>Ec49(2Nu=LMv`X>-`3;ca>O*+4xkk6TioBD_U|l_Vwucei!#l(EaWWpg*|j zOC^0hbP0E6F66bjrPqrb0jHUfFv=CHHHDqEUX{r$kQoMleP&Fmp8ABP83_+S`g!J~ zb~-s(R9Y41fhvgOh;ukbg|m@?%YQZ zR;}@HZmex*bE}?}z7Ntr$ z4@y&oRr<~xRhz54oaw8SJEy7gQsq%)P$g6v_;L@bi>muYy(nL$fk%2h5|44->i}p* z!keI_y-8<&+ndUN6VuyzQ~U{i`ns*Q?Vm>+XM9VaTkSYXpU33?6&xjJj4X9YpkTrq45-oRyMIuj`ZHDEdEmx1u>cHoyuvbWPf4Or4FaHh*{;LHFu zU^BB3b1Z0@Xpy*#FYW`Cu!*^lE&)x$X+?MN_kv2;$sXX-xl5YZV)O)mGpK~!>;?X5 zpl)%Y(HqicPy-ga55gB4dEj3JYKUq<@Ba6Jy2WpezL4$!m0}gnX>i{RG)?R@3Lw21 z)ZjB42`k?poV$zxklqa{Ve5YeCb*3q~RM&x7LkD^Po! z#e=4amkj#-mKQ;#Xy9Irg1Uvr91dv$sKoE%oP_x6KqcmrkqB=9#T)w3lLls&Q3!9u zyklUN8IADyXhj3F4be@QgK*-ESqR?|z#JsSWtf2sv5o2FnA0TQ)msYAl~@Bxd|!g- zRhZ)p%t=JA#!Q9x>odIuzoum1+a5%(#oQ8)Zh__u)?cth<{gm-}&nDI^py#sxSH}``YcvtBep!@N=Lk8CPUhtnr zzZv2wP$~X|UNW$MPPA3527LzmE{1qPtO0!keTCn+05x!y76SbTdczR!qX(oo3TlWC z(CYZD5>P{Yh?X|QXJ})bp`&#T;gK6b6Xf}zsS@w*!FvcdgZ7XY;;EjXhUg_PLbx|5 z#)aI9Fy0o3Z~zfi{uRmFJxLFZ$h|I-VFX?rc325;4ERfOx}j@sd5+SX?RyJUI!)bLilv1 zXUKaHK2zQceif)8R>&4mue=|0rF;PVYEVP?IIhU63A*Mk}& zBI%c`8sr}ljxt?`GcM>Y(?E&l}mIiS#0ycbrA^W<{~Z)AEt-v27`rmN?{ zX_7A@yji}C@C8h_&IVfvl?9${hp2Yd+%D{cG)8fW|rnq){Ep(Y!qlmb7| zjBvUUhj3S>nMOP~8KAhQVR#VEVmi!7M0li;3_9CLg>(+6A?6zC2+spWj~f}FD~&AB zY9kxeXV9;5`I)XZdVsTvX^qhf`~avS&NBKSyoPD5kq=IgX`Rsz{17Nw(&&%ybxd1~ zf#BT7^Z{cq_z!}j-Hag!KW+>|_%WtW7$DAXj6(R2p!l61V>H4C zK=I2M##n^k0hQt~PB{$m9;k$$c?!Zun7(gJ1m`Fy#;-9M;WnmU7^UER&h$%TD)?W4 z8sckX8p6kzeq+o4=if}fHOj&Nj_D7^Z1BHl`d?!%_`;kI&d&x>iBkxPpSWI#uxVC+ zy3EDk#DSvk%%uo-1x2sn&5TlHGVNxb4o()+Z1W88b3hZ|{hL^6+$iHfua)a0e%lM- z{?L{|O4*&VC*}E+H&eb$@uVI| zU6giu+J>};(vGBkoc2wcnVyuMnLaE1>-0fgZ_fBb#?u+W%t+?NnOA1sk@;5UXPIVJ zQP!laud=?+GP*T&+uH5QZm)KGx7#P(MrY5+UXXoz_MYt7ITblC=Vat=%H7<3NB8Hu z|D*fAy1RO0_1N3vFFg+RINZaviS9(GKl=Q8(o`JPLDTI&=sSGBjy(07gYDy;-nU~s zf5v4Kh?fa};Z)4+r{VufSbh3C$+aM#ZbpsdPrL@-wKShfFxuq@Ux@qTNG`;y{OJ;} zX?!S7#XA(|&wofB>iUrU5dS?H56L0;KQ8kjIV*FxYkAIa*Gl}a$r*w02!xAVx8ZXS zc)!Qz?{glKr{q2)=i>jU?hnb&dQe`Xcy7qo5cE$HGo#V^3V(Q=KU_7x-d|S{s2%PL z2AQp*IARQ^$cl?iS$`zd5U%!54+Q->X8H6`xVFq+9rF3Z&c__=h*%sBI-^Y6Q0=de z210et2W)0U;MtV{pU+=s^TgC(z+V?Ft)>V4zG>?SgjYu#35fDVsGNf_oG_QUK2#U+ zPYs3E1b}Dyy(sGOQW(C#8;+nBbN$hpkWYU^L^-v;Hzh&sjV(+@jZHdI&VucmTTHrD%9 zYUOqH4N*>tL!3Fag5qJV==X;GK1W|U*;W1i1oifZQEopC9&0>8U)BX^TsnKKA{q+&F_L|OFuI+`%p(74 z|AuxPN|h24^H-jQSR5=1ti}lF&@6NPwV`mM?s+R|E~GWybgdK=Q$oJRSo@Vv4b?(Z z(KU8a=Y^14L!B?Cpmxa8mPLM8u81FxY-rSd*}ha(#P(b&2^+mB~9V71yBSYuJwzr3BgO!ro!HyY6!++<3JEatLW+h{KG zdxN!Bxt%?(dRD8I^&my)z*^*QhnFb}r57%m&=Tw|_)o|966kSZM z@mH_mV(L=Os?Y%{x@|ngG-XpoIjlgq&Kp!yT&&o&7L#0a3|ti;eg!&OH`MEkC)3Wio&vw*1ZM`QMot)^J*k5-iigQ4{mn2_sy-f$x{ zm!=H-j=LC69U@LBt_thP-I^>=NS+pu%8HXkRc)ZwPn{UyT;|%S)aNVnukvCFi}7^x zE%L9@%_mAD5r6H4$ZlRtMLEs;A~o zN|J|>l_DBYjabaJ4UwsEQ$!UxD_jfGADX_kKsCCkits2AXpwcHH7ceSQ3J#Lfa}Ac zWvK^(c9(0rQ(86o#uH_S}Ga`h_l2SgzrHt6)Gmlq%~DlQ@qt{ z(B9Jn{-943v#=AJQ&G>6&O_soyY{@qe0KdJVnHYp6${{JREE9Px~&?*VN3?fqf*_- zW70ASUY`%mucsICq2LROFf7SBe`mh)Nwgq6OS8|E#;6}IRJA{_F7|L|fg>xL^E*DE zOVLh5k0|f5W9$xGYTwwJkA=}A!0xvY-I zr-j3zaD;3MRF-o<;nna*n)&`=& zGEB<#@B4m#s5PE;6kR zFkf&nb(r{82E@0*#o7_-WY>yyj7|c}bFxw>uj9F)yfo5SSB*Zy?1(84;i=wWa3w6L znBrd@sMFD=)%kRYO2i|Lio*fou=`mZ3NC@$OKUC?cI0s#YZ~fkX5u>PF!Svto>hIl z;^S$_ovWAnYVR7DFUc(S&Es1P8|v zhu3age#cslt4NXU)~AI9wpL&|@D_?`KV!gUr>Njq^JJ<`6e?MhGHHd(0lesEa3$q}BE$Y9rc z>+AgvuXav|wpr9pnOX~av2g^=pW%?6i)pp>QS?4f*HDZ) z^3JLPtLw0GSLIbs_#CV$)a+bN6C}z)z0915s0^uD&$(XZeE{CX8BQ_T{X5RAs+T$^ z@+`^8wD*NNI+n3A>2f$+Qb+cjHG6m*nzkVdCono69tjuB#~-xahjoK8Xf|L+&X1L( zUpdO!|C{1qmHUJp-N~==Z>Xm&08V|H4qKmP{*?`@v3SIqcBwa9M{5dpAH;M_=|Ko0 zQEwd_RdgQhOu@@gp1TTE(bASDZ>;Al{87wK*cV~HgCIE!f)_00bvQNz1s2=(IFGHb z*h5o(T8G`MO7Ch>MXPcsHf?C35Kf5sfhHm6Mu+miplfPV(6K9`*VYaWO)u@Cl^VAh zb*A1PB{SfF)NKSU17?XWOmdiC9tR||!n|ZrNp>2hEYk^e7U~o_2^@uT7RbtwEi3Ow zYRjv_Vdce=Q>++dfO)Mj$G-~B792QbXV^oIK{fs``Ba8@x|v@WbFSytsYk?o?i1{u z(PoKSwNM*qzlH1_uaGG7DPg~N4LgLzPI0GNQhQ>R-(YETIi^O1)KXQcZfv6w^B(oT zR{c8PQs_?^HXe97h67?_8eXx-Fjn;VYR98U+Slp0*6GNe0smpHzp}>PZY!gFc3o)w z(i(5HytXVvL(M9EtRBunwadI?4%8H?y40vDRLmd8Z8dd>5F2XLZROY*P(8zH&ZCtz zTF=cCE{~As&vM5Q&_Rm8n_Vn-4t|nA_qd(uQ)5JqM)alyD^7(U+M!$mX9+u$j!UK| zJfTaZ@^$9Px=!gMP*EQYM6o8rJW|ga=Hx|K4o+3AI_scChi$H^8c<9kJXoo`s&kaH z$67}FIhc>QG|JOnT=)jucZ|6H;qV(7CBA`RO@&&L#VN92p4Rk=y>|B{Z zv~l>{XGPhNv&J zzD}(~J7&~=0udFFpyg|$bdeYmmDTpyg{TVZQw)UcQ=lpwPtxjF^*cSSGme>5+y3yt ziuidq30odY#TGjm^XpVPu!YfZBd@?T!;&B?z0+s!3S&Krit_eRN1nEf9ofasOgrh8 zQz)u@>f{tP^C=awjsU4PI;@;Mtzwp(RHXyUj^2S!Nhv3XHznyw2UOmHAg5JSc(K-l z9xbken)`SovaX>X%|$zG*eECGiPNZB6%N(nR993*L;Mlp6~1+bq;y+Hq3kO;7KJQp zp^l%#bR9`W=peNAK(KBfjx(x;pf`*%kdnhqiJ(|Gwj%dCT3thhU~KKuunUbb0-tOZ z``oCOI!(~NH-j0mp9+_D>lI>(A z_M0`t`kY3wP@DAp80}m|wXeVo!gI?;&-6xWrUc-E-~|!o5tJd|Q{v)SdY!a0Cc;>= z8oH{1^}#;h)o!%X1i!5n*vsLA73XQl|Gi6%nTPx$n)y1eCDa0lJ7-G6s#Rz;(p^U| zwzoci)hftXu7kxYUZ)yj>50v^&TREqC%(>2CkiF9*8!D$rln4sU>^B!#9k$+CAtq) z3)Jy}ml`E}dxC>CwEJCE%QrH}t_7lg_AIIU=~$A19YBj)S0*TeZE@7u*p$!)u?V|_ zp(zc~C>;2zI-HfO=?WJTBCxAo-Y?{yLT2@}7_IV!YQ2FvZe2C9mBqBelnM26b&hx-JV#oDLkR?%aM z!|kUutF+WcWMg#ItePo>coB5j zh0h(SY+Qt;&@;>%a6o7$Fj%Xxo+q6wt8Lq7XSM5b+tks3+7dY$&uSJmpX)N%Ko`Kq zN_ojqr7dBFl+S1Js#dbW;f0vmcH2NpdZJZk|w;n3RRdUi- z-O)}~Y>B5!&(C77gl;47C{RIC{Y7v+qJEfNc5O-6o)e3+fE?3U1cp>XKIlUi;6eZ$ zvlQF8cQWr~d=49R2pk-P(o6dhMCokp5-=nxGT($sc5nJ}z> zaNLWNaop(}IYNDmRNM75`k)38Kc)$LE2TU#Z7oi1X{QD^$L7F1!@lE)97n_}>6RPr z*2F?=ZsD$kR>2=!Ni!;)hw}uFrl<2VRoDPs*0c|<+511ScxfP7V;|~Kl2fdF30ONx zwEN`^`&9|%VzE~ln&;mTZ5Njq))gxlivaFl!j062dST@~V5-r(%-TH@>(zD6db-|O z3nN7yOVD25)9NuGXfbJ-9R_5RBK34Dq@MDqR@LK?p>TX2Kv3$D=-fE zCNZ}&ossi~v|jG&9l~ns;2J$lr`ZkYH}ci3{e3*EHx;>?R$1c}4Nc9g4zC0jHnjpP z_*H=o!W+>eg|>g&A8rNs{;?f_E^p|rfpbaMzV)#gX%+OVQLLhcKKxe!5h#M*3r5rM&|n?o#?<%A zkUpJ`ucV{h2stC{{0Ue#Iv20k>!JwsUF`!~+mH0gDI)XZY(B!;!sx>3M%P3{J^4YH zYOuwKT;MB`whQNG9x~#CZ#1Q#MG`6?eEJ5z9=6a}YllZ|@O0SU(L$^UDNi~y;%E*` z3P4rDA|cwynhEa}nHufPKl-gQB;frz?mgrK$prBR{R z$aS=$wfe_us|D6NR=1g2cG8+wsAWBkMT|_E+=wF_OA5-Yn$K>~L25BUhDcziF`_ph z)toBSbVmaKLztfkvpZ=oLSRaulGAmEH`LYgU4Nh0fWR;2wg2guvEBws*Au@ zEoLPxHhg0Kl0>`d%@R%x)vXGwZouC83B;Je>KvxmdJf&>gA>&`x2d7}Mii~)I3jFb z`24lr@S5X11>=WqhzVdqmjogKY~sauGa3TE7_v&DO=V{RIWC=qdZNR{Ih^}LcdO~N z2q)C5X%QGpL-EiZSUY3>>o7giypF`f71%i8`B5>r4Hx-?-VGdz#FEqVDn)~6WdLVK zjy9bMBS%TazBt6rUFk|3h~o|&+bFFZ(9a0XFyt;N)hUn6!?qL@i!V2D4%kf&aSGat z2-U+ch9`lg8jL!5BkToXUp1n1YyK+QTE)Ch`Uj2RTNJbXjTFW)2B)>Sa{4&jDx;}? zkvg5Bb9Wu2kIE1V$5H2M^~UWh+8!-~V;0+5vz+c2M>p<_U}z;RCb{jzRHYF1x`NWe z!mbWmvaBg63LW)vTj-5>d}(5(KOAdxD<^87SZ`(8y)&LpSlNYA4i7RyFn9_@(25voJmBG>qmYbU#3*w{fO;@^Vdtrx5gcE zJ3C^vI9oTB{2&-ODGvl;tmM~>2k~>y8a0}0Mm=D zDB{`|3RNZxLZP6#M~QQ$@KW3bEhm>t2|3wV1MBbZC?~l*va|-%a0Pqa(}N9>8ZPR* z(0Xd4F-2NgXlD-N4Ao`jvI-FtK9ma;a%#X<- z7@2g==usoamQE=x9X4g$$ljTwe7_i!0h(F5eUW3j3RKx`0Mwi^r2`KwqpVzSj_483*gyE(Syux|jc zJ*2rG`OrF*f~5Z(sDQClmx8>JD5t>3Z4{^TSLA+Gum4Kr7=;e3hL1_+wH&97D&#mU zw+_hbd}=>EhLs~rwN_i6u=6n%8iK|z)CI=3x<;`5hz)SQJg+PZFCeGDkYV>)XorM& z+Dc+@6QYx2&4G)@%M2dh5L;MKlZ9;y$=#vu;ahVvrq>y0d+2E;^b)=a@79p}hHqZ2 z=TD4y`0BVhWjj~wx?@6accDVy#8q9e(7u+vJfLh#E2fs0hLwCXl1*M^2p19IPx03kaEi$LI%?-#O_2D`8>=jR8(HnD zi83rHXakLW8}%h+FD6JwyHoLCsQGT7wH-u$UV9#m1?VU%A|`RT2s((IVhRtX7h1-2FIS*e!&U5 z&p+2&PuP}P#o(P9Dh3yhVq#LXTmv}xu?J|Bvof=F;6rtWYN#Ejq%qd7G6f?Y6F?C+ z8lO&3*{q`$?d$S)5ozH`4k#}2XjA1lw)2-_ElW!RP6tyVZ3DsLv&G;M4>Q1@Q)n(A zBhDN8^bO5eu_%oonT0QiJpQ zi5eE)wBtQvuAU7X{wOcrCxpFBAx3)f7N~lJ19)GN7aY6^8gGg^iFRCo{P;f% z?;(ofZBW=(qxTiXNAY$iJefsO3Z2Byd+}r)UUOIjPA$?C88Nxdi&r5Le-@rujW_|s z#cvNFrIUo_dFT@c4nJkL64Lq(u{19p+lQz}`xG=U9$QMCQrEorYSbnoM0%`rn&VlG zH&q2Fzy2p`lMQGKE0&DcvHR5OwxnmhXeVkBimBw(Uc?psekoNIN227|gcG9AiQ;N` zJmvCB@w7aC19}3rJz+1eLreMamNuNhqQumn)u2K2GHC^R`IHltr2Uii(3kaiGK{A} ztO-%1!TWZ2DdUOKI1!&p1JjSxd_u&XMKu_9qPS&`S2f~UErp+s8a)-yVvC&9 zrntZADGzR`A|~=nq)`w=P6)&H)*y9k$l#ND7n;?BK5>QvtXhL&N5DG;ZBIE@1Jn3e zgR&IcHKuaVb2KV^c>7adv=xnyC_;I7O65*`kxU$FaYk8$@RonXRC5Y*O~lKh+#)Cm z+42D7EQ#xjx6cX_|1*m5T%KKWmU@a=mm%#{%%!G2ptL)bp|lvKBO9Rd%wyZ2T7}Y~ zJO&{5l}Mu=HK$aa@sWO?Urs>`rv4)vsq&}(r<6PLP|rU9M^30`r`(z6ObaE-E=F4g zQB&$68bcA)VrUcUF~mrul$>FTPkb6nVbl=g3$Iy-wMzu4b<{5!S$T*m3r)o^w=1rlEYuX2=XFShx|z|)ND*8qLy=xEh`NkwdO>;oOLd0MD40l zqH!8Rn^P^q$d9y|+NOOfWRsK~!RQT4Lrm4uv7D{cs4i=f9?dsVjLAIFF~3;*#`JkO z-m*3g{fMI_{3px6<(>Gy<+KECqVzux@Bfr14MgswMKsQ-k7=ZxxLz`DIo?YuO#Fu* zLo=O-r?NtyJi}2U<_siFB5%^^0mUpPj&C9J3Yi+qhds#D_U$50or!o+9>VLnMX8MRoGPocKgkQCUR7F28lb$yJUmI(uZD9YL^hlBT+I;FM}Eqs zx*Sid6v?_$S{-wxGN~3(vn`D+O7dfk*r1DT7QAwy(Nuz7IOkr@;-0qweY0OP+H)iMH2`bb7 z*WT5@R(0L?bMO1O_dVO!?_J{(m-on{Kw=tf;}kcLz=#7bus~vJlM)rgoEQSF2{xG3 zEY13^+o+@;TC*kEs;OGHrBmBMKO~}M6=<(_x(M}|7=NZZDLg<3-iLbpT|oM6@BMhfzA`+CQDFb1^l}i5FcPB2b`EQS zM(QHQ7#l6{saNMF-4lCiI%p4Qi|K6;0*%Hr#+re3qc2NdV+)RY&`jd99S88)@y@Cz z@OjKnVvZ-waD2ZL_b%uh)2r{OZS<|wt|c7w-nbU)TghgE4zqnM@6I)!oJdz70s z-XoZapHr!=%~oP5tw9r8sc@YGbx0rD#N$}a4E~8GU9}t3qjlvvk`cr*J>06Nn%mIs zP&*!jKSADZwz&IIPnhXt(n9Ww+seojZ)W6!`1XRmOUGO_iMgnm$*rpGY)f#|#pB4J9JOsV9XHDzXgkq5!_7$=?K82{>YItJ9v&6_Ls_DYenf0GWz}jSs~zdZ z(t9xn#$X$$K{cmG8Bg11VqfDpN+u5Oh=h~ADjPMF9><#mJ7$SQT3e$LA9>dLLhWa2 zGv?M-yxw%rp;mvox#yDAkKDL@+Iv$!R($6j?YOIXrgYCGvySZn1*}h$j!Z4LX^*rw zO{q!n&UoJ7J517}z@7}Nb)r_bGD+TGyg_;KaUZ)#>A6mf7(VuNj)O-_1RIWFX(UBD9c$Q zcg?uBJS-NYYnxzzb_;QE3(nvXyo(+|vuAGSW&BHSr&dbniSIeM!`jrNz{b%Q3&_8|gUs5}tX!hgnmn>zW)IZYrDqcR zy4?o>@ne8UCBf`G2yA~4zsw*L@3+UEe>0?{S2MjWUw|_3Z5X#u2b!+a;*qJ(Ywk!m zvf1W*2rD2ZrofNnKSHk_(lPkSd}qq`c^s|8GxX?-orc7v_brl4INve{K>zbNdJJL| z#klF)i&;3&V;E}&un=c5=5TwAa-75mOR|?(Cp=xQR=rSii|lB?f2D^AUh3xl4mCZW zq5Y9Tp+2T3NzYb2bHof?*2Yf2+L&86U{$0Ny;|}IrGWNDwUSwHTi>wN$Y{K3ay2>L z>ZE3B5*|W$N`3d;ld@7fBUeuOOXQ>dC>l2qqwEQs?B17JzuKUii|B1k9_yg{+tGya zH2rdF4dv7{wsf{xfr^@vv9);2J0{SFRHB8onx5L3{6Or|Qd%9U?U*r1xRMbIxkzJa zv({pcTsnj3f|}WKfo4q1t;FCB-j}sKIfj{7@It}nYs^&pSjWahiI+i~Yc@l34z-3B zdXGSL3KU7Ao1~6&X?$h+-V>J+4}=_JBQ{Ek#R%hRS|R1nvu2HP&Nr`=Efa04-iL*X zvSzbM7Ju5}e+0qlr8ASp%=>Hh$U;bL{V&YdvlNe}i}?Z@F&UA5!eYd(SZIxR8l3b2 zW9Dv@(bsrCoxRt%pEcI1GOA2!`OQAe9;lvC%xVi}5hRnp_vU6?Is)?%ocRgtsy#{T zUH^Du(_a!`TM1yUra`^#r4&_Uc85ZB@)_fX2%tGYP{!J^9U zM=No>pWG&6YG6-iThqS;oXU>rm!&9Ua^(Fn>W;{JyG5dCKbk^o;%;zEMF2+}5;Q#Z zR!?uKdc$R$T7DI@X|zRl@Al0hLxGN#pN{ELyS?}3rn(`(*ivG`oNg->wyG4R4{4jw z@0VhV>1K@W0R~>n;E3Euhe)@Jnro1-ssHrN07~$0maR4k6B4`2&zLNaG33Hq#MYMI zgwl82XkSl7B`ro4*=F~R_v@@~V|!|rrF(8FW*mnKqn&W4_bQ3$R(2s9uwLNk<_y;E zfcQP*P(|G3QXh*$SNf@gW2o&lW$rL@2CJ{l zN`?IH7;fIFPfd4oz40^$YP>mFrO9zbgT3t3=tIqY>{z_h4)DG?zK?K@YI>bZa+5t& z!O7?pCDW^Fni@oHxYT9_j8oSIi#oW^*9T(%%S zr-L|--q3N$1!IP;^fOn$>^m`xcOA)-S<`MM$;g0rE&X`zc#XoV;O@jAva4yo3z0;D zlz0NaraSGUU}tw#^E06!Z_f)f^qok*39OCuV+6st)2vHSe;c>;&$~Bc#C$aIznFjx{6W zDBs*JGdlyzw;crJ%+XMi`*of5$WF05!s!Ik{~5%D$@Y*X0!2y>^JtQV+d)i`yayBU zNXwv1M}%{^Cvj|zliM*jfO&2QVl+7clwfn>ESQW+f=orQ$<9Kkspif6pY5kvWyQ57 zN|jIT-|+B-iWxBl}(Igm@-j1M;(|H}@=WKz^1vH64lhHsv%fL=EP|u;5V6at2D1DRjeWhAi%+Z=a^_ zcwmF-h{}uR3L?;JQxHiVM7EFNEw|h_SJRvBqwsYW@D857tQEY;#((PYrH*H0a}j@F1D+zc&bHe+UBy{do#F!&glqN422}N}%|Wu7Tzd=- zin)&>rLD;$6Bviw532W2;2y(#YBEHPw^hjL8LZl6K(8ue3Ub6FYbsMVT|uKoJTl7Q zU3r|wB^iOdg}##9Bxk7!pFs;X=(+*K;MX27(KR!TGXs3ZX~O}9*>2i#HYRTfV-gf= z+=JORP-!QB$7z&v3OM4N9?LFQa~2$)mXs(zBO3USuw8RK#ZoQg{>DO7+2QYzmW;p>x1MIwI4mm>T-=rQxOHQP`j(6G|SQXiAPA>~rDb#{M4 zc}TF{c8}KbLo**s`vdfs>?tOzq$~_N7h@__Ib3~`Z{wO+Lq%ut2DNj5=O)Nc93@eX zbURp+aGcdjcF%?!t>^bzIB9G-?qvdrtFswx?tZnKo5?6M)NT~YqFpPosk|tdX}fdK zzI{#e>^pCM2JP{HU><**i7%lr#loHu>XzV6Xrp*S*V%pp#6mCAzV9u#SV$T}c zydO^zk@Dn2)kg!+8OgcrClJ%=siW3b`=q^|6eFHAM&)??)kxJT*k@B{?2@-=B|x!1 zy_ZzO_o0)QR2r>zqrSig(8N@-*w-{uZ4vW4;clzWrt@&WXqwlrmpU8t?NuYf(F`&(^PV*4n)IBBKjJPQC zq`T}$uW!dyjNz!8NIi*G%Zyejk*t%N53xpV1Z^=hB;<9awx)8A!#eOJpiQ5#y=ooq zn$WH`YZ&WRrS${l$`D%4 znjS@@w99g>z`TPnr*Yl9!;$zM?M}TR{HkhFTD>N_dtx1a)5E<~!mHT>dM#%7K~V7n zs8c=7xoMS?ndp6~&Lf|pN7D&oj>hmv9dBZWkkj8G{RuaD#qM{k0heQy>j@sTEE87}Ozm zBfxHLY5525@OLISiQRG%)xKxGg*MDeJ)T$cxs^*$FF7^hiS#4HMsjj#&564DRjZ^_ z0VRLoa^vfv^AYi$A(^_}oX0|0ln8_@Tbd-+$Wc4a-g#_2=b2 ztB>{9viY!np;*hfVg04>P`Hq<^$9Rox9*Q@5ApTe)GGf;P1h06fsutAdWds;&A1IV7dw8kE1Yw ziu|}7H7-uVZy1I49y0`M8}U=k`MyANpoj(ch6l0Q*OTJKg00<{!)ognh>C3>khF{M z7XdV$H|Btf`UN-d1CiUZ20vMc7nhd|FRaYwZ~-27c_tGCK{fDA7IzfTB*;spTadac zDmzRr2*QWM`tMZSPLoH&_@Q9jfc#QHA2$e(1UUn&9x0fN6Hc+GXMlQ?NK4*bpr7>s z7grFPa{>o4_igD@UMhvjuE7Ari8^k|gf`oiRFUnDIpH`!I*H#?_?;AtPLgB%f-t~k z@E;#5g2l_i9QDic@RB~fL~g&#%S(dC%W{2*3{bhHtCkJt@S1{f4r73|=EyxGQXJvP zE=hWV&*c+*wN+lpwY(h+RAeJuOxO>Y9ZUCWPTHL(8;_8%3EYXh<&O!bQR7HguxuLG1xzL$rx4TqH@UPdGuj{^>$3=LAs30fkjciGI406t9Ui zy%_7HEAjmrrr3Zt7>5MH$0DP5T1u> zQ{5)Gdxp)P!zJ295I)Pt&%t&lb27$VlQK1FFW>$Zb{qO|# zE7oW)dRZh`&^6^TF6K*}roa~ZQ>f9B@F#1al|5*IBX9%QgIZ-FR_Y7Zp4VSfF5nI3 zJg`<9^#B(YKtMGIqUS(;=+sRPb^RvCtTyNY-{4YqlWQE+8iiPE6hz308A?l6Eg#Nl z(F#9m!#UCex<6L@Za62@g8HEJP%k>-rQiZ`6v#TXig1I(ghGd^1qDtM3#t!+@qmC( zAF4qS9pZ;h1Lm2EA7r3Pr5Y4yiUXE(Y!uufnDX+N2qq9MqKZ4|;)Gk^4T%3U6V|^D zUI2K23@Z*`y+D%d7Ls*Ba)4BSp8R<)U%t!|{e*oiY|9ygs}La~O40UZHLce%OQ%Q! zN-yJ~!AkY#MWU|@j@HT5CrgHhpiM;g9tv;4SOKuB!XpM2o@Q*cp(#3KW zNyZ=Fm}cbN`A?Qa|+W8_XEBq+(b8@bBazE zK$;Q`V$6UEAPyb`u9~A81?pems>}5^>Yq33;OW+lc^1xYo(?dc-#dZt63R0AbjZ-4Faxr6>ESG@E5m*4eh{SO~l_smC59sBERFNTH8 z+pqp)`grxnzkA!H=|acwikJAKniwQPAZx;haaOkU7J5$i(&=s!oA^|oWvNo?sw=A_Lw4|n!+ma z?0Z=hp$&cX zP>v2}jRUU$k`SFD=VJ_p2e_{AiU_5nk1<}3E>vj;syVC)m zH2a`$sfT^gl`d2CSu6_RCNeWiaDD$CuLH2zj!8*}NH1b(P0mB&1jx^XvqY;0HlIbs zN-xtGFg*Y!^fN%0i&O&5^yk1q(1K5c!Vm|LeG1g|Q1dZ>Nz~82!!cESPF?LJzweWu zatKL1e)>#}pLr)AvxP%T(4@p$kMU%I;cUS}g>(m#5ziQ!H!P1T+UFe=SOb~F^Kt>! z3`VDS+yySAll*#ozo^v+F;EQ^W(cs0K0Z6)C9MFoeHR!IobLOf2b|&ueB}GCU*1N) z3Vi*7{_iX~nboGW8vfpDlPx+3Yk<82{8uR2v76Va>K>>UL6|>(7=tZINci>o*qrWeE zz-1VHTdYPH{Uf;mi^wSM3o$9Kk&G2sb?6@0T?qOdyhM-~uFf1ZSQz~i^sEcF1THS5 zv5*6LBVUXQ7R3Jz1YT<(@R}mf-T(|Tj3wNV2Pk!cHYKPseF9TLdcso7oD684@Vuai zv5QqI^F{KXn4tL%k6yNxOBgJg$A4Ra4x(S?spG@w2cqK(QUW%lwjBLST+0$`Z}Wgl z?IuHM0Az=R1ZcAYN~6+&B{7ln74U>Dr#fge;F_V3SUhS~n#YQVB~STPH3F3i(iCvc z%F%yPf-0;B@}nOy^#W(V0)D9!Xz40q4>@3^1JCs5euVc7?!xHD72jXsbuf+!mV534JW#p)070|buVMV|$wJQlaLO`%xI(Hjt|0NNyaBk+ph5ubVv zDa;JbU3sjl8_UPLxThp-=$UnS`EapN&B4e+l3|9FbeL6IBCjl_YXOfE`vt(xLR`jh z)LhXUB>)`FgbOl0tlolXBWdrGFId%?((`<1==lY!=NI72mlwMv8{G8{kX-4ufVz0X zbT42ewG^Er=EnMKKF~^i1h+tzjTlt;I3BY8ny@PuD6uQRM{>c0i@hLjP+sg?@0BT? z=flM^%{i?5Vl`Y`A1-bP7YD+{;c#&zj5fqF;m11Ihpl%*Lh~HrPcTzB<4!#sQ2VsxE&TtXrLYd^0Dz2>lbzIAf+sj@~yo}y(aVsjr#Zg%| z5^-4lfnF%A*#Jqi#r=GVVZ}nJ?5vAL51|)yME$y4UzY0yh-_owKFZPFFzTaqU##|d zdAtj6LHHR%ro+{r_lsz_F^o2e&eR~ufIY{m)6!|Fb8L(1+e%R}BD@14= zVI%*m`-Oszj=M;D^33%}ayLfvx;{X8C|rl^5K<{&f&aAcI`K#Dm4$2(6}Tmg_}^NH znzax$QGoGPHEvDd-V{cAs7Gs{twfN5{AZ{WBX#1UanKon+b26f{;1Q7J)dQuQ`6C*#=FQ0=zeYSS$bi%%x?{M*HYYFtZel-@HXxcX}@N=nJDS@n`SZkvvD~8h*hzG){`bWLDw@ktD`M-WO zjHa<5Vd!a4Z5n8u4yRhjL1jhOglG&4X)L~4QO=5{Q}?DN*?M=iv(kc(3DZL;e|=b8 zh6-+s$)N!oUTH;yuuhJx*Y!X32JDCz&iin>pv6Ina4G{g5|INOQ0fx69PslIAi!+7 z(0E0-nE0VCz7Q0P-Hs_HUx$I{j@wOUrDMX~Q~2u0laJRX`K3*#Pt`{6!x0$AtZRJV zNbQ?nsx56)yK~c~E!uMHqjw$Oyk+Y6!0_}VcMfd6bNcweBh!y=8Q8M<-J3U0-8p>x z(RbsI7&;x}4G(VOKVy8yFw&QTFNM_%^(Wo8@tZpFy}yl5(ZweYpZ?HUS*4IGo8-9D&Z!YG(wf7I+n0)7VPrUFHchPp79l@6%9ZOXo#aBN(K5_EY z;Oz7xCi@TZ!Z6ormTM@q`6;E^;p2pMo<6f{W@i5szB=L5*+=oA{>KKVXK*GTKR;b> zYWGU%7nlE!7;sDpVBLZf3eC`w`wdT%E~#g`J@`HJeaGx0Q^!C1GMiC)$V}k8>_gb6 zelJS8oqZJd2XJ=cL1}q4`|tn7M4JAxie+Q)*%zJFvZaGiU@Q64!+U(&7X<)vVy@){_tcj1u$%n|RR^D!& zqo9+XALD6R{!t6QqYeM~GXqS6>KIEudx034#W^8j^f4Sh!C6|tQgdC+ddS)_9gH!N~Hs-z>Oq?$=6z xR{U>~KBJ&AkNwyagEerno_MpAYwbr|zhysv>zGaqzG>TAHp4Fl{y&U?{{|S%zI6Zq literal 119808 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 h+gU