From 180be7de07014aa33bc6066f12a0819b731c1c9d Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Tue, 10 Feb 2009 13:10:57 +0000 Subject: this is step 2 of 2 of the OpenSim.Region.Environment refactor. NOTHING has been deleted or moved off to forge at this point. what has happened is that OpenSim.Region.Environment.Modules has been split in two: - OpenSim.Region.CoreModules: all those modules that are either directly or indirectly referenced from other OpenSim packages, or that provide functionality that the OpenSim developer community considers core functionality: CoreModules/Agent/AssetTransaction CoreModules/Agent/Capabilities CoreModules/Agent/TextureDownload CoreModules/Agent/TextureSender CoreModules/Agent/TextureSender/Tests CoreModules/Agent/Xfer CoreModules/Avatar/AvatarFactory CoreModules/Avatar/Chat/ChatModule CoreModules/Avatar/Combat CoreModules/Avatar/Currency/SampleMoney CoreModules/Avatar/Dialog CoreModules/Avatar/Friends CoreModules/Avatar/Gestures CoreModules/Avatar/Groups CoreModules/Avatar/InstantMessage CoreModules/Avatar/Inventory CoreModules/Avatar/Inventory/Archiver CoreModules/Avatar/Inventory/Transfer CoreModules/Avatar/Lure CoreModules/Avatar/ObjectCaps CoreModules/Avatar/Profiles CoreModules/Communications/Local CoreModules/Communications/REST CoreModules/Framework/EventQueue CoreModules/Framework/InterfaceCommander CoreModules/Hypergrid CoreModules/InterGrid CoreModules/Scripting/DynamicTexture CoreModules/Scripting/EMailModules CoreModules/Scripting/HttpRequest CoreModules/Scripting/LoadImageURL CoreModules/Scripting/VectorRender CoreModules/Scripting/WorldComm CoreModules/Scripting/XMLRPC CoreModules/World/Archiver CoreModules/World/Archiver/Tests CoreModules/World/Estate CoreModules/World/Land CoreModules/World/Permissions CoreModules/World/Serialiser CoreModules/World/Sound CoreModules/World/Sun CoreModules/World/Terrain CoreModules/World/Terrain/DefaultEffects CoreModules/World/Terrain/DefaultEffects/bin CoreModules/World/Terrain/DefaultEffects/bin/Debug CoreModules/World/Terrain/Effects CoreModules/World/Terrain/FileLoaders CoreModules/World/Terrain/FloodBrushes CoreModules/World/Terrain/PaintBrushes CoreModules/World/Terrain/Tests CoreModules/World/Vegetation CoreModules/World/Wind CoreModules/World/WorldMap - OpenSim.Region.OptionalModules: all those modules that are not core modules: OptionalModules/Avatar/Chat/IRC-stuff OptionalModules/Avatar/Concierge OptionalModules/Avatar/Voice/AsterixVoice OptionalModules/Avatar/Voice/SIPVoice OptionalModules/ContentManagementSystem OptionalModules/Grid/Interregion OptionalModules/Python OptionalModules/SvnSerialiser OptionalModules/World/NPC OptionalModules/World/TreePopulator --- .../Scripting/WorldComm/WorldCommModule.cs | 726 +++++++++++++++++++++ 1 file changed, 726 insertions(+) create mode 100644 OpenSim/Region/CoreModules/Scripting/WorldComm/WorldCommModule.cs (limited to 'OpenSim/Region/CoreModules/Scripting/WorldComm') diff --git a/OpenSim/Region/CoreModules/Scripting/WorldComm/WorldCommModule.cs b/OpenSim/Region/CoreModules/Scripting/WorldComm/WorldCommModule.cs new file mode 100644 index 0000000..c363940 --- /dev/null +++ b/OpenSim/Region/CoreModules/Scripting/WorldComm/WorldCommModule.cs @@ -0,0 +1,726 @@ +/* + * 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 OpenSim 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 OpenMetaverse; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +// using log4net; +// using System.Reflection; + + +/***************************************************** + * + * WorldCommModule + * + * + * Holding place for world comms - basically llListen + * function implementation. + * + * lLListen(integer channel, string name, key id, string msg) + * The name, id, and msg arguments specify the filtering + * criteria. You can pass the empty string + * (or NULL_KEY for id) for these to set a completely + * open filter; this causes the listen() event handler to be + * invoked for all chat on the channel. To listen only + * for chat spoken by a specific object or avatar, + * specify the name and/or id arguments. To listen + * only for a specific command, specify the + * (case-sensitive) msg argument. If msg is not empty, + * listener will only hear strings which are exactly equal + * to msg. You can also use all the arguments to establish + * the most restrictive filtering criteria. + * + * It might be useful for each listener to maintain a message + * digest, with a list of recent messages by UUID. This can + * be used to prevent in-world repeater loops. However, the + * linden functions do not have this capability, so for now + * thats the way it works. + * Instead it blocks messages originating from the same prim. + * (not Object!) + * + * For LSL compliance, note the following: + * (Tested again 1.21.1 on May 2, 2008) + * 1. 'id' has to be parsed into a UUID. None-UUID keys are + * to be replaced by the ZeroID key. (Well, TryParse does + * that for us. + * 2. Setting up an listen event from the same script, with the + * same filter settings (including step 1), returns the same + * handle as the original filter. + * 3. (TODO) handles should be script-local. Starting from 1. + * Might be actually easier to map the global handle into + * script-local handle in the ScriptEngine. Not sure if its + * worth the effort tho. + * + * **************************************************/ + +namespace OpenSim.Region.CoreModules.Scripting.WorldComm +{ + public class WorldCommModule : IRegionModule, IWorldComm + { + // private static readonly ILog m_log = + // LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private ListenerManager m_listenerManager; + private Queue m_pending; + private Queue m_pendingQ; + private Scene m_scene; + private int m_whisperdistance = 10; + private int m_saydistance = 30; + private int m_shoutdistance = 100; + + #region IRegionModule Members + + public void Initialise(Scene scene, IConfigSource config) + { + // wrap this in a try block so that defaults will work if + // the config file doesn't specify otherwise. + int maxlisteners = 1000; + int maxhandles = 64; + try + { + m_whisperdistance = config.Configs["Chat"].GetInt("whisper_distance", m_whisperdistance); + m_saydistance = config.Configs["Chat"].GetInt("say_distance", m_saydistance); + m_shoutdistance = config.Configs["Chat"].GetInt("shout_distance", m_shoutdistance); + maxlisteners = config.Configs["Chat"].GetInt("max_listens_per_region", maxlisteners); + maxhandles = config.Configs["Chat"].GetInt("max_listens_per_script", maxhandles); + } + catch (Exception) + { + } + if (maxlisteners < 1) maxlisteners = int.MaxValue; + if (maxhandles < 1) maxhandles = int.MaxValue; + + m_scene = scene; + m_scene.RegisterModuleInterface(this); + m_listenerManager = new ListenerManager(maxlisteners, maxhandles); + m_scene.EventManager.OnChatFromClient += DeliverClientMessage; + m_scene.EventManager.OnChatBroadcast += DeliverClientMessage; + m_pendingQ = new Queue(); + m_pending = Queue.Synchronized(m_pendingQ); + } + + public void PostInitialise() + { + } + + public void Close() + { + } + + public string Name + { + get { return "WorldCommModule"; } + } + + public bool IsSharedModule + { + get { return false; } + } + + #endregion + + #region IWorldComm Members + + /// + /// Create a listen event callback with the specified filters. + /// The parameters localID,itemID are needed to uniquely identify + /// the script during 'peek' time. Parameter hostID is needed to + /// determine the position of the script. + /// + /// localID of the script engine + /// UUID of the script engine + /// UUID of the SceneObjectPart + /// channel to listen on + /// name to filter on + /// key to filter on (user given, could be totally faked) + /// msg to filter on + /// number of the scripts handle + public int Listen(uint localID, UUID itemID, UUID hostID, int channel, string name, UUID id, string msg) + { + return m_listenerManager.AddListener(localID, itemID, hostID, channel, name, id, msg); + } + + /// + /// Sets the listen event with handle as active (active = TRUE) or inactive (active = FALSE). + /// The handle used is returned from Listen() + /// + /// UUID of the script engine + /// handle returned by Listen() + /// temp. activate or deactivate the Listen() + public void ListenControl(UUID itemID, int handle, int active) + { + if (active == 1) + m_listenerManager.Activate(itemID, handle); + else if (active == 0) + m_listenerManager.Dectivate(itemID, handle); + } + + /// + /// Removes the listen event callback with handle + /// + /// UUID of the script engine + /// handle returned by Listen() + public void ListenRemove(UUID itemID, int handle) + { + m_listenerManager.Remove(itemID, handle); + } + + /// + /// Removes all listen event callbacks for the given itemID + /// (script engine) + /// + /// UUID of the script engine + public void DeleteListener(UUID itemID) + { + m_listenerManager.DeleteListener(itemID); + } + + + protected static Vector3 CenterOfRegion = new Vector3(128, 128, 20); + + public void DeliverMessage(ChatTypeEnum type, int channel, string name, UUID id, string msg) + { + Vector3 position; + SceneObjectPart source; + ScenePresence avatar; + + if ((source = m_scene.GetSceneObjectPart(id)) != null) + position = source.AbsolutePosition; + else if ((avatar = m_scene.GetScenePresence(id)) != null) + position = avatar.AbsolutePosition; + else if (ChatTypeEnum.Region == type) + position = CenterOfRegion; + else + return; + + DeliverMessage(type, channel, name, id, msg, position); + } + + /// + /// This method scans over the objects which registered an interest in listen callbacks. + /// For everyone it finds, it checks if it fits the given filter. If it does, then + /// enqueue the message for delivery to the objects listen event handler. + /// The enqueued ListenerInfo no longer has filter values, but the actually trigged values. + /// Objects that do an llSay have their messages delivered here and for nearby avatars, + /// the OnChatFromClient event is used. + /// + /// type of delvery (whisper,say,shout or regionwide) + /// channel to sent on + /// name of sender (object or avatar) + /// key of sender (object or avatar) + /// msg to sent + public void DeliverMessage(ChatTypeEnum type, int channel, string name, UUID id, string msg, Vector3 position) + { + // m_log.DebugFormat("[WorldComm] got[2] type {0}, channel {1}, name {2}, id {3}, msg {4}", + // type, channel, name, id, msg); + + // Determine which listen event filters match the given set of arguments, this results + // in a limited set of listeners, each belonging a host. If the host is in range, add them + // to the pending queue. + foreach (ListenerInfo li in m_listenerManager.GetListeners(UUID.Zero, channel, name, id, msg)) + { + // Dont process if this message is from yourself! + if (li.GetHostID().Equals(id)) + continue; + + SceneObjectPart sPart = m_scene.GetSceneObjectPart(li.GetHostID()); + if (sPart == null) + continue; + + double dis = Util.GetDistanceTo(sPart.AbsolutePosition, position); + switch (type) + { + case ChatTypeEnum.Whisper: + if (dis < m_whisperdistance) + { + lock (m_pending.SyncRoot) + { + m_pending.Enqueue(new ListenerInfo(li,name,id,msg)); + } + } + break; + + case ChatTypeEnum.Say: + if (dis < m_saydistance) + { + lock (m_pending.SyncRoot) + { + m_pending.Enqueue(new ListenerInfo(li,name,id,msg)); + } + } + break; + + case ChatTypeEnum.Shout: + if (dis < m_shoutdistance) + { + lock (m_pending.SyncRoot) + { + m_pending.Enqueue(new ListenerInfo(li,name,id,msg)); + } + } + break; + + case ChatTypeEnum.Region: + lock (m_pending.SyncRoot) + { + m_pending.Enqueue(new ListenerInfo(li,name,id,msg)); + } + break; + } + } + } + + /// + /// Are there any listen events ready to be dispatched? + /// + /// boolean indication + public bool HasMessages() + { + return (m_pending.Count > 0); + } + + /// + /// Pop the first availlable listen event from the queue + /// + /// ListenerInfo with filter filled in + public IWorldCommListenerInfo GetNextMessage() + { + ListenerInfo li = null; + + lock (m_pending.SyncRoot) + { + li = (ListenerInfo) m_pending.Dequeue(); + } + + return li; + } + + #endregion + + /******************************************************************** + * + * Listener Stuff + * + * *****************************************************************/ + + private void DeliverClientMessage(Object sender, OSChatMessage e) + { + if (null != e.Sender) + DeliverMessage(e.Type, e.Channel, e.Sender.Name, e.Sender.AgentId, e.Message, e.Position); + else + DeliverMessage(e.Type, e.Channel, e.From, UUID.Zero, e.Message, e.Position); + } + + public Object[] GetSerializationData(UUID itemID) + { + return m_listenerManager.GetSerializationData(itemID); + } + + public void CreateFromData(uint localID, UUID itemID, UUID hostID, + Object[] data) + { + m_listenerManager.AddFromData(localID, itemID, hostID, data); + } + } + + public class ListenerManager + { + private Dictionary> m_listeners = new Dictionary>(); + private int m_maxlisteners; + private int m_maxhandles; + private int m_curlisteners; + + public ListenerManager(int maxlisteners, int maxhandles) + { + m_maxlisteners = maxlisteners; + m_maxhandles = maxhandles; + m_curlisteners = 0; + } + + public int AddListener(uint localID, UUID itemID, UUID hostID, int channel, string name, UUID id, string msg) + { + // do we already have a match on this particular filter event? + List coll = GetListeners(itemID, channel, name, id, msg); + + if (coll.Count > 0) + { + // special case, called with same filter settings, return same handle + // (2008-05-02, tested on 1.21.1 server, still holds) + return coll[0].GetHandle(); + } + + if (m_curlisteners < m_maxlisteners) + { + int newHandle = GetNewHandle(itemID); + + if (newHandle > 0) + { + ListenerInfo li = new ListenerInfo(newHandle, localID, itemID, hostID, channel, name, id, msg); + + lock (m_listeners) + { + List listeners; + if (!m_listeners.TryGetValue(channel,out listeners)) + { + listeners = new List(); + m_listeners.Add(channel, listeners); + } + listeners.Add(li); + m_curlisteners++; + } + + return newHandle; + } + } + return -1; + } + + public void Remove(UUID itemID, int handle) + { + lock (m_listeners) + { + foreach (KeyValuePair> lis in m_listeners) + { + foreach (ListenerInfo li in lis.Value) + { + if (li.GetItemID().Equals(itemID) && li.GetHandle().Equals(handle)) + { + lis.Value.Remove(li); + if (lis.Value.Count == 0) + { + m_listeners.Remove(lis.Key); + m_curlisteners--; + } + // there should be only one, so we bail out early + return; + } + } + } + } + } + + public void DeleteListener(UUID itemID) + { + List emptyChannels = new List(); + List removedListeners = new List(); + + lock (m_listeners) + { + foreach (KeyValuePair> lis in m_listeners) + { + foreach (ListenerInfo li in lis.Value) + { + if (li.GetItemID().Equals(itemID)) + { + // store them first, else the enumerated bails on us + removedListeners.Add(li); + } + } + foreach (ListenerInfo li in removedListeners) + { + lis.Value.Remove(li); + m_curlisteners--; + } + removedListeners.Clear(); + if (lis.Value.Count == 0) + { + // again, store first, remove later + emptyChannels.Add(lis.Key); + } + } + foreach (int channel in emptyChannels) + { + m_listeners.Remove(channel); + } + } + } + + public void Activate(UUID itemID, int handle) + { + lock (m_listeners) + { + foreach (KeyValuePair> lis in m_listeners) + { + foreach (ListenerInfo li in lis.Value) + { + if (li.GetItemID().Equals(itemID) && li.GetHandle() == handle) + { + li.Activate(); + // only one, bail out + return; + } + } + } + } + } + + public void Dectivate(UUID itemID, int handle) + { + lock (m_listeners) + { + foreach (KeyValuePair> lis in m_listeners) + { + foreach (ListenerInfo li in lis.Value) + { + if (li.GetItemID().Equals(itemID) && li.GetHandle() == handle) + { + li.Deactivate(); + // only one, bail out + return; + } + } + } + } + } + + // non-locked access, since its always called in the context of the lock + private int GetNewHandle(UUID itemID) + { + List handles = new List(); + + // build a list of used keys for this specific itemID... + foreach (KeyValuePair> lis in m_listeners) + { + foreach (ListenerInfo li in lis.Value) + { + if (li.GetItemID().Equals(itemID)) + handles.Add(li.GetHandle()); + } + } + + // Note: 0 is NOT a valid handle for llListen() to return + for (int i = 1; i <= m_maxhandles; i++) + { + if (!handles.Contains(i)) + return i; + } + + return -1; + } + + // Theres probably a more clever and efficient way to + // do this, maybe with regex. + // PM2008: Ha, one could even be smart and define a specialized Enumerator. + public List GetListeners(UUID itemID, int channel, string name, UUID id, string msg) + { + List collection = new List(); + + lock (m_listeners) + { + List listeners; + if (!m_listeners.TryGetValue(channel,out listeners)) + { + return collection; + } + + foreach (ListenerInfo li in listeners) + { + if (!li.IsActive()) + { + continue; + } + if (!itemID.Equals(UUID.Zero) && !li.GetItemID().Equals(itemID)) + { + continue; + } + if (li.GetName().Length > 0 && !li.GetName().Equals(name)) + { + continue; + } + if (!li.GetID().Equals(UUID.Zero) && !li.GetID().Equals(id)) + { + continue; + } + if (li.GetMessage().Length > 0 && !li.GetMessage().Equals(msg)) + { + continue; + } + collection.Add(li); + } + } + return collection; + } + + public Object[] GetSerializationData(UUID itemID) + { + List data = new List(); + + foreach (List list in m_listeners.Values) + { + foreach (ListenerInfo l in list) + { + if (l.GetItemID() == itemID) + data.AddRange(l.GetSerializationData()); + } + } + return (Object[])data.ToArray(); + } + + public void AddFromData(uint localID, UUID itemID, UUID hostID, + Object[] data) + { + int idx = 0; + Object[] item = new Object[6]; + + while (idx < data.Length) + { + Array.Copy(data, idx, item, 0, 6); + + ListenerInfo info = + ListenerInfo.FromData(localID, itemID, hostID, item); + + if (!m_listeners.ContainsKey((int)item[2])) + m_listeners.Add((int)item[2], new List()); + m_listeners[(int)item[2]].Add(info); + + idx+=6; + } + } + } + + public class ListenerInfo: IWorldCommListenerInfo + { + private bool m_active; // Listener is active or not + private int m_handle; // Assigned handle of this listener + private uint m_localID; // Local ID from script engine + private UUID m_itemID; // ID of the host script engine + private UUID m_hostID; // ID of the host/scene part + private int m_channel; // Channel + private UUID m_id; // ID to filter messages from + private string m_name; // Object name to filter messages from + private string m_message; // The message + + public ListenerInfo(int handle, uint localID, UUID ItemID, UUID hostID, int channel, string name, UUID id, string message) + { + Initialise(handle, localID, ItemID, hostID, channel, name, id, message); + } + + public ListenerInfo(ListenerInfo li, string name, UUID id, string message) + { + Initialise(li.m_handle, li.m_localID, li.m_itemID, li.m_hostID, li.m_channel, name, id, message); + } + + private void Initialise(int handle, uint localID, UUID ItemID, UUID hostID, int channel, string name, + UUID id, string message) + { + m_active = true; + m_handle = handle; + m_localID = localID; + m_itemID = ItemID; + m_hostID = hostID; + m_channel = channel; + m_name = name; + m_id = id; + m_message = message; + } + + public Object[] GetSerializationData() + { + Object[] data = new Object[6]; + + data[0] = m_active; + data[1] = m_handle; + data[2] = m_channel; + data[3] = m_name; + data[4] = m_id; + data[5] = m_message; + + return data; + } + + public static ListenerInfo FromData(uint localID, UUID ItemID, UUID hostID, Object[] data) + { + ListenerInfo linfo = new ListenerInfo((int)data[1], localID, + ItemID, hostID, (int)data[2], (string)data[3], + (UUID)data[4], (string)data[5]); + linfo.m_active=(bool)data[0]; + + return linfo; + } + + public UUID GetItemID() + { + return m_itemID; + } + + public UUID GetHostID() + { + return m_hostID; + } + + public int GetChannel() + { + return m_channel; + } + + public uint GetLocalID() + { + return m_localID; + } + + public int GetHandle() + { + return m_handle; + } + + public string GetMessage() + { + return m_message; + } + + public string GetName() + { + return m_name; + } + + public bool IsActive() + { + return m_active; + } + + public void Deactivate() + { + m_active = false; + } + + public void Activate() + { + m_active = true; + } + + public UUID GetID() + { + return m_id; + } + } +} -- cgit v1.1