PageRenderTime 57ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/OpenSim/Region/CoreModules/Scripting/WorldComm/WorldCommModule.cs

https://bitbucket.org/VirtualReality/taiga
C# | 722 lines | 481 code | 88 blank | 153 comment | 58 complexity | bd24d2f2c3b5ebb3ddf4fdaa741e059e MD5 | raw file
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections;
  29. using System.Collections.Generic;
  30. using Nini.Config;
  31. using OpenMetaverse;
  32. using OpenSim.Framework;
  33. using OpenSim.Region.Framework.Interfaces;
  34. using OpenSim.Region.Framework.Scenes;
  35. // using log4net;
  36. // using System.Reflection;
  37. /*****************************************************
  38. *
  39. * WorldCommModule
  40. *
  41. *
  42. * Holding place for world comms - basically llListen
  43. * function implementation.
  44. *
  45. * lLListen(integer channel, string name, key id, string msg)
  46. * The name, id, and msg arguments specify the filtering
  47. * criteria. You can pass the empty string
  48. * (or NULL_KEY for id) for these to set a completely
  49. * open filter; this causes the listen() event handler to be
  50. * invoked for all chat on the channel. To listen only
  51. * for chat spoken by a specific object or avatar,
  52. * specify the name and/or id arguments. To listen
  53. * only for a specific command, specify the
  54. * (case-sensitive) msg argument. If msg is not empty,
  55. * listener will only hear strings which are exactly equal
  56. * to msg. You can also use all the arguments to establish
  57. * the most restrictive filtering criteria.
  58. *
  59. * It might be useful for each listener to maintain a message
  60. * digest, with a list of recent messages by UUID. This can
  61. * be used to prevent in-world repeater loops. However, the
  62. * linden functions do not have this capability, so for now
  63. * thats the way it works.
  64. * Instead it blocks messages originating from the same prim.
  65. * (not Object!)
  66. *
  67. * For LSL compliance, note the following:
  68. * (Tested again 1.21.1 on May 2, 2008)
  69. * 1. 'id' has to be parsed into a UUID. None-UUID keys are
  70. * to be replaced by the ZeroID key. (Well, TryParse does
  71. * that for us.
  72. * 2. Setting up an listen event from the same script, with the
  73. * same filter settings (including step 1), returns the same
  74. * handle as the original filter.
  75. * 3. (TODO) handles should be script-local. Starting from 1.
  76. * Might be actually easier to map the global handle into
  77. * script-local handle in the ScriptEngine. Not sure if its
  78. * worth the effort tho.
  79. *
  80. * **************************************************/
  81. namespace OpenSim.Region.CoreModules.Scripting.WorldComm
  82. {
  83. public class WorldCommModule : IRegionModule, IWorldComm
  84. {
  85. // private static readonly ILog m_log =
  86. // LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
  87. private ListenerManager m_listenerManager;
  88. private Queue m_pending;
  89. private Queue m_pendingQ;
  90. private Scene m_scene;
  91. private int m_whisperdistance = 10;
  92. private int m_saydistance = 30;
  93. private int m_shoutdistance = 100;
  94. #region IRegionModule Members
  95. public void Initialise(Scene scene, IConfigSource config)
  96. {
  97. // wrap this in a try block so that defaults will work if
  98. // the config file doesn't specify otherwise.
  99. int maxlisteners = 1000;
  100. int maxhandles = 64;
  101. try
  102. {
  103. m_whisperdistance = config.Configs["Chat"].GetInt("whisper_distance", m_whisperdistance);
  104. m_saydistance = config.Configs["Chat"].GetInt("say_distance", m_saydistance);
  105. m_shoutdistance = config.Configs["Chat"].GetInt("shout_distance", m_shoutdistance);
  106. maxlisteners = config.Configs["LL-Functions"].GetInt("max_listens_per_region", maxlisteners);
  107. maxhandles = config.Configs["LL-Functions"].GetInt("max_listens_per_script", maxhandles);
  108. }
  109. catch (Exception)
  110. {
  111. }
  112. if (maxlisteners < 1) maxlisteners = int.MaxValue;
  113. if (maxhandles < 1) maxhandles = int.MaxValue;
  114. m_scene = scene;
  115. m_scene.RegisterModuleInterface<IWorldComm>(this);
  116. m_listenerManager = new ListenerManager(maxlisteners, maxhandles);
  117. m_scene.EventManager.OnChatFromClient += DeliverClientMessage;
  118. m_scene.EventManager.OnChatBroadcast += DeliverClientMessage;
  119. m_pendingQ = new Queue();
  120. m_pending = Queue.Synchronized(m_pendingQ);
  121. }
  122. public void PostInitialise()
  123. {
  124. }
  125. public void Close()
  126. {
  127. }
  128. public string Name
  129. {
  130. get { return "WorldCommModule"; }
  131. }
  132. public bool IsSharedModule
  133. {
  134. get { return false; }
  135. }
  136. #endregion
  137. #region IWorldComm Members
  138. /// <summary>
  139. /// Create a listen event callback with the specified filters.
  140. /// The parameters localID,itemID are needed to uniquely identify
  141. /// the script during 'peek' time. Parameter hostID is needed to
  142. /// determine the position of the script.
  143. /// </summary>
  144. /// <param name="localID">localID of the script engine</param>
  145. /// <param name="itemID">UUID of the script engine</param>
  146. /// <param name="hostID">UUID of the SceneObjectPart</param>
  147. /// <param name="channel">channel to listen on</param>
  148. /// <param name="name">name to filter on</param>
  149. /// <param name="id">key to filter on (user given, could be totally faked)</param>
  150. /// <param name="msg">msg to filter on</param>
  151. /// <returns>number of the scripts handle</returns>
  152. public int Listen(uint localID, UUID itemID, UUID hostID, int channel, string name, UUID id, string msg)
  153. {
  154. return m_listenerManager.AddListener(localID, itemID, hostID, channel, name, id, msg);
  155. }
  156. /// <summary>
  157. /// Sets the listen event with handle as active (active = TRUE) or inactive (active = FALSE).
  158. /// The handle used is returned from Listen()
  159. /// </summary>
  160. /// <param name="itemID">UUID of the script engine</param>
  161. /// <param name="handle">handle returned by Listen()</param>
  162. /// <param name="active">temp. activate or deactivate the Listen()</param>
  163. public void ListenControl(UUID itemID, int handle, int active)
  164. {
  165. if (active == 1)
  166. m_listenerManager.Activate(itemID, handle);
  167. else if (active == 0)
  168. m_listenerManager.Dectivate(itemID, handle);
  169. }
  170. /// <summary>
  171. /// Removes the listen event callback with handle
  172. /// </summary>
  173. /// <param name="itemID">UUID of the script engine</param>
  174. /// <param name="handle">handle returned by Listen()</param>
  175. public void ListenRemove(UUID itemID, int handle)
  176. {
  177. m_listenerManager.Remove(itemID, handle);
  178. }
  179. /// <summary>
  180. /// Removes all listen event callbacks for the given itemID
  181. /// (script engine)
  182. /// </summary>
  183. /// <param name="itemID">UUID of the script engine</param>
  184. public void DeleteListener(UUID itemID)
  185. {
  186. m_listenerManager.DeleteListener(itemID);
  187. }
  188. protected static Vector3 CenterOfRegion = new Vector3(128, 128, 20);
  189. public void DeliverMessage(ChatTypeEnum type, int channel, string name, UUID id, string msg)
  190. {
  191. Vector3 position;
  192. SceneObjectPart source;
  193. ScenePresence avatar;
  194. if ((source = m_scene.GetSceneObjectPart(id)) != null)
  195. position = source.AbsolutePosition;
  196. else if ((avatar = m_scene.GetScenePresence(id)) != null)
  197. position = avatar.AbsolutePosition;
  198. else if (ChatTypeEnum.Region == type)
  199. position = CenterOfRegion;
  200. else
  201. return;
  202. DeliverMessage(type, channel, name, id, msg, position);
  203. }
  204. /// <summary>
  205. /// This method scans over the objects which registered an interest in listen callbacks.
  206. /// For everyone it finds, it checks if it fits the given filter. If it does, then
  207. /// enqueue the message for delivery to the objects listen event handler.
  208. /// The enqueued ListenerInfo no longer has filter values, but the actually trigged values.
  209. /// Objects that do an llSay have their messages delivered here and for nearby avatars,
  210. /// the OnChatFromClient event is used.
  211. /// </summary>
  212. /// <param name="type">type of delvery (whisper,say,shout or regionwide)</param>
  213. /// <param name="channel">channel to sent on</param>
  214. /// <param name="name">name of sender (object or avatar)</param>
  215. /// <param name="id">key of sender (object or avatar)</param>
  216. /// <param name="msg">msg to sent</param>
  217. public void DeliverMessage(ChatTypeEnum type, int channel, string name, UUID id, string msg, Vector3 position)
  218. {
  219. // m_log.DebugFormat("[WorldComm] got[2] type {0}, channel {1}, name {2}, id {3}, msg {4}",
  220. // type, channel, name, id, msg);
  221. // Determine which listen event filters match the given set of arguments, this results
  222. // in a limited set of listeners, each belonging a host. If the host is in range, add them
  223. // to the pending queue.
  224. foreach (ListenerInfo li in m_listenerManager.GetListeners(UUID.Zero, channel, name, id, msg))
  225. {
  226. // Dont process if this message is from yourself!
  227. if (li.GetHostID().Equals(id))
  228. continue;
  229. SceneObjectPart sPart = m_scene.GetSceneObjectPart(li.GetHostID());
  230. if (sPart == null)
  231. continue;
  232. double dis = Util.GetDistanceTo(sPart.AbsolutePosition, position);
  233. switch (type)
  234. {
  235. case ChatTypeEnum.Whisper:
  236. if (dis < m_whisperdistance)
  237. QueueMessage(new ListenerInfo(li, name, id, msg));
  238. break;
  239. case ChatTypeEnum.Say:
  240. if (dis < m_saydistance)
  241. QueueMessage(new ListenerInfo(li, name, id, msg));
  242. break;
  243. case ChatTypeEnum.Shout:
  244. if (dis < m_shoutdistance)
  245. QueueMessage(new ListenerInfo(li, name, id, msg));
  246. break;
  247. case ChatTypeEnum.Region:
  248. QueueMessage(new ListenerInfo(li, name, id, msg));
  249. break;
  250. }
  251. }
  252. }
  253. protected void QueueMessage(ListenerInfo li)
  254. {
  255. lock (m_pending.SyncRoot)
  256. {
  257. m_pending.Enqueue(li);
  258. }
  259. }
  260. /// <summary>
  261. /// Are there any listen events ready to be dispatched?
  262. /// </summary>
  263. /// <returns>boolean indication</returns>
  264. public bool HasMessages()
  265. {
  266. return (m_pending.Count > 0);
  267. }
  268. /// <summary>
  269. /// Pop the first availlable listen event from the queue
  270. /// </summary>
  271. /// <returns>ListenerInfo with filter filled in</returns>
  272. public IWorldCommListenerInfo GetNextMessage()
  273. {
  274. ListenerInfo li = null;
  275. lock (m_pending.SyncRoot)
  276. {
  277. li = (ListenerInfo)m_pending.Dequeue();
  278. }
  279. return li;
  280. }
  281. #endregion
  282. /********************************************************************
  283. *
  284. * Listener Stuff
  285. *
  286. * *****************************************************************/
  287. private void DeliverClientMessage(Object sender, OSChatMessage e)
  288. {
  289. if (null != e.Sender)
  290. DeliverMessage(e.Type, e.Channel, e.Sender.Name, e.Sender.AgentId, e.Message, e.Position);
  291. else
  292. DeliverMessage(e.Type, e.Channel, e.From, UUID.Zero, e.Message, e.Position);
  293. }
  294. public Object[] GetSerializationData(UUID itemID)
  295. {
  296. return m_listenerManager.GetSerializationData(itemID);
  297. }
  298. public void CreateFromData(uint localID, UUID itemID, UUID hostID,
  299. Object[] data)
  300. {
  301. m_listenerManager.AddFromData(localID, itemID, hostID, data);
  302. }
  303. }
  304. public class ListenerManager
  305. {
  306. private Dictionary<int, List<ListenerInfo>> m_listeners = new Dictionary<int, List<ListenerInfo>>();
  307. private int m_maxlisteners;
  308. private int m_maxhandles;
  309. private int m_curlisteners;
  310. public ListenerManager(int maxlisteners, int maxhandles)
  311. {
  312. m_maxlisteners = maxlisteners;
  313. m_maxhandles = maxhandles;
  314. m_curlisteners = 0;
  315. }
  316. public int AddListener(uint localID, UUID itemID, UUID hostID, int channel, string name, UUID id, string msg)
  317. {
  318. // do we already have a match on this particular filter event?
  319. List<ListenerInfo> coll = GetListeners(itemID, channel, name, id, msg);
  320. if (coll.Count > 0)
  321. {
  322. // special case, called with same filter settings, return same handle
  323. // (2008-05-02, tested on 1.21.1 server, still holds)
  324. return coll[0].GetHandle();
  325. }
  326. if (m_curlisteners < m_maxlisteners)
  327. {
  328. lock (m_listeners)
  329. {
  330. int newHandle = GetNewHandle(itemID);
  331. if (newHandle > 0)
  332. {
  333. ListenerInfo li = new ListenerInfo(newHandle, localID, itemID, hostID, channel, name, id, msg);
  334. List<ListenerInfo> listeners;
  335. if (!m_listeners.TryGetValue(channel,out listeners))
  336. {
  337. listeners = new List<ListenerInfo>();
  338. m_listeners.Add(channel, listeners);
  339. }
  340. listeners.Add(li);
  341. m_curlisteners++;
  342. return newHandle;
  343. }
  344. }
  345. }
  346. return -1;
  347. }
  348. public void Remove(UUID itemID, int handle)
  349. {
  350. lock (m_listeners)
  351. {
  352. foreach (KeyValuePair<int,List<ListenerInfo>> lis in m_listeners)
  353. {
  354. foreach (ListenerInfo li in lis.Value)
  355. {
  356. if (li.GetItemID().Equals(itemID) && li.GetHandle().Equals(handle))
  357. {
  358. lis.Value.Remove(li);
  359. if (lis.Value.Count == 0)
  360. {
  361. m_listeners.Remove(lis.Key);
  362. m_curlisteners--;
  363. }
  364. // there should be only one, so we bail out early
  365. return;
  366. }
  367. }
  368. }
  369. }
  370. }
  371. public void DeleteListener(UUID itemID)
  372. {
  373. List<int> emptyChannels = new List<int>();
  374. List<ListenerInfo> removedListeners = new List<ListenerInfo>();
  375. lock (m_listeners)
  376. {
  377. foreach (KeyValuePair<int,List<ListenerInfo>> lis in m_listeners)
  378. {
  379. foreach (ListenerInfo li in lis.Value)
  380. {
  381. if (li.GetItemID().Equals(itemID))
  382. {
  383. // store them first, else the enumerated bails on us
  384. removedListeners.Add(li);
  385. }
  386. }
  387. foreach (ListenerInfo li in removedListeners)
  388. {
  389. lis.Value.Remove(li);
  390. m_curlisteners--;
  391. }
  392. removedListeners.Clear();
  393. if (lis.Value.Count == 0)
  394. {
  395. // again, store first, remove later
  396. emptyChannels.Add(lis.Key);
  397. }
  398. }
  399. foreach (int channel in emptyChannels)
  400. {
  401. m_listeners.Remove(channel);
  402. }
  403. }
  404. }
  405. public void Activate(UUID itemID, int handle)
  406. {
  407. lock (m_listeners)
  408. {
  409. foreach (KeyValuePair<int,List<ListenerInfo>> lis in m_listeners)
  410. {
  411. foreach (ListenerInfo li in lis.Value)
  412. {
  413. if (li.GetItemID().Equals(itemID) && li.GetHandle() == handle)
  414. {
  415. li.Activate();
  416. // only one, bail out
  417. return;
  418. }
  419. }
  420. }
  421. }
  422. }
  423. public void Dectivate(UUID itemID, int handle)
  424. {
  425. lock (m_listeners)
  426. {
  427. foreach (KeyValuePair<int,List<ListenerInfo>> lis in m_listeners)
  428. {
  429. foreach (ListenerInfo li in lis.Value)
  430. {
  431. if (li.GetItemID().Equals(itemID) && li.GetHandle() == handle)
  432. {
  433. li.Deactivate();
  434. // only one, bail out
  435. return;
  436. }
  437. }
  438. }
  439. }
  440. }
  441. // non-locked access, since its always called in the context of the lock
  442. private int GetNewHandle(UUID itemID)
  443. {
  444. List<int> handles = new List<int>();
  445. // build a list of used keys for this specific itemID...
  446. foreach (KeyValuePair<int,List<ListenerInfo>> lis in m_listeners)
  447. {
  448. foreach (ListenerInfo li in lis.Value)
  449. {
  450. if (li.GetItemID().Equals(itemID))
  451. handles.Add(li.GetHandle());
  452. }
  453. }
  454. // Note: 0 is NOT a valid handle for llListen() to return
  455. for (int i = 1; i <= m_maxhandles; i++)
  456. {
  457. if (!handles.Contains(i))
  458. return i;
  459. }
  460. return -1;
  461. }
  462. // Theres probably a more clever and efficient way to
  463. // do this, maybe with regex.
  464. // PM2008: Ha, one could even be smart and define a specialized Enumerator.
  465. public List<ListenerInfo> GetListeners(UUID itemID, int channel, string name, UUID id, string msg)
  466. {
  467. List<ListenerInfo> collection = new List<ListenerInfo>();
  468. lock (m_listeners)
  469. {
  470. List<ListenerInfo> listeners;
  471. if (!m_listeners.TryGetValue(channel,out listeners))
  472. {
  473. return collection;
  474. }
  475. foreach (ListenerInfo li in listeners)
  476. {
  477. if (!li.IsActive())
  478. {
  479. continue;
  480. }
  481. if (!itemID.Equals(UUID.Zero) && !li.GetItemID().Equals(itemID))
  482. {
  483. continue;
  484. }
  485. if (li.GetName().Length > 0 && !li.GetName().Equals(name))
  486. {
  487. continue;
  488. }
  489. if (!li.GetID().Equals(UUID.Zero) && !li.GetID().Equals(id))
  490. {
  491. continue;
  492. }
  493. if (li.GetMessage().Length > 0 && !li.GetMessage().Equals(msg))
  494. {
  495. continue;
  496. }
  497. collection.Add(li);
  498. }
  499. }
  500. return collection;
  501. }
  502. public Object[] GetSerializationData(UUID itemID)
  503. {
  504. List<Object> data = new List<Object>();
  505. lock (m_listeners)
  506. {
  507. foreach (List<ListenerInfo> list in m_listeners.Values)
  508. {
  509. foreach (ListenerInfo l in list)
  510. {
  511. if (l.GetItemID() == itemID)
  512. data.AddRange(l.GetSerializationData());
  513. }
  514. }
  515. }
  516. return (Object[])data.ToArray();
  517. }
  518. public void AddFromData(uint localID, UUID itemID, UUID hostID,
  519. Object[] data)
  520. {
  521. int idx = 0;
  522. Object[] item = new Object[6];
  523. while (idx < data.Length)
  524. {
  525. Array.Copy(data, idx, item, 0, 6);
  526. ListenerInfo info =
  527. ListenerInfo.FromData(localID, itemID, hostID, item);
  528. lock (m_listeners)
  529. {
  530. if (!m_listeners.ContainsKey((int)item[2]))
  531. m_listeners.Add((int)item[2], new List<ListenerInfo>());
  532. m_listeners[(int)item[2]].Add(info);
  533. }
  534. idx+=6;
  535. }
  536. }
  537. }
  538. public class ListenerInfo: IWorldCommListenerInfo
  539. {
  540. private bool m_active; // Listener is active or not
  541. private int m_handle; // Assigned handle of this listener
  542. private uint m_localID; // Local ID from script engine
  543. private UUID m_itemID; // ID of the host script engine
  544. private UUID m_hostID; // ID of the host/scene part
  545. private int m_channel; // Channel
  546. private UUID m_id; // ID to filter messages from
  547. private string m_name; // Object name to filter messages from
  548. private string m_message; // The message
  549. public ListenerInfo(int handle, uint localID, UUID ItemID, UUID hostID, int channel, string name, UUID id, string message)
  550. {
  551. Initialise(handle, localID, ItemID, hostID, channel, name, id, message);
  552. }
  553. public ListenerInfo(ListenerInfo li, string name, UUID id, string message)
  554. {
  555. Initialise(li.m_handle, li.m_localID, li.m_itemID, li.m_hostID, li.m_channel, name, id, message);
  556. }
  557. private void Initialise(int handle, uint localID, UUID ItemID, UUID hostID, int channel, string name,
  558. UUID id, string message)
  559. {
  560. m_active = true;
  561. m_handle = handle;
  562. m_localID = localID;
  563. m_itemID = ItemID;
  564. m_hostID = hostID;
  565. m_channel = channel;
  566. m_name = name;
  567. m_id = id;
  568. m_message = message;
  569. }
  570. public Object[] GetSerializationData()
  571. {
  572. Object[] data = new Object[6];
  573. data[0] = m_active;
  574. data[1] = m_handle;
  575. data[2] = m_channel;
  576. data[3] = m_name;
  577. data[4] = m_id;
  578. data[5] = m_message;
  579. return data;
  580. }
  581. public static ListenerInfo FromData(uint localID, UUID ItemID, UUID hostID, Object[] data)
  582. {
  583. ListenerInfo linfo = new ListenerInfo((int)data[1], localID,
  584. ItemID, hostID, (int)data[2], (string)data[3],
  585. (UUID)data[4], (string)data[5]);
  586. linfo.m_active=(bool)data[0];
  587. return linfo;
  588. }
  589. public UUID GetItemID()
  590. {
  591. return m_itemID;
  592. }
  593. public UUID GetHostID()
  594. {
  595. return m_hostID;
  596. }
  597. public int GetChannel()
  598. {
  599. return m_channel;
  600. }
  601. public uint GetLocalID()
  602. {
  603. return m_localID;
  604. }
  605. public int GetHandle()
  606. {
  607. return m_handle;
  608. }
  609. public string GetMessage()
  610. {
  611. return m_message;
  612. }
  613. public string GetName()
  614. {
  615. return m_name;
  616. }
  617. public bool IsActive()
  618. {
  619. return m_active;
  620. }
  621. public void Deactivate()
  622. {
  623. m_active = false;
  624. }
  625. public void Activate()
  626. {
  627. m_active = true;
  628. }
  629. public UUID GetID()
  630. {
  631. return m_id;
  632. }
  633. }
  634. }