diff options
Diffstat (limited to '')
-rw-r--r-- | OpenSim/Region/Environment/Modules/World/WorldMap/WorldMapModule.cs | 312 |
1 files changed, 261 insertions, 51 deletions
diff --git a/OpenSim/Region/Environment/Modules/World/WorldMap/WorldMapModule.cs b/OpenSim/Region/Environment/Modules/World/WorldMap/WorldMapModule.cs index 215f3fa..9b4a997 100644 --- a/OpenSim/Region/Environment/Modules/World/WorldMap/WorldMapModule.cs +++ b/OpenSim/Region/Environment/Modules/World/WorldMap/WorldMapModule.cs | |||
@@ -33,6 +33,7 @@ using System.Drawing.Imaging; | |||
33 | using System.IO; | 33 | using System.IO; |
34 | using System.Net; | 34 | using System.Net; |
35 | using System.Reflection; | 35 | using System.Reflection; |
36 | using System.Threading; | ||
36 | using OpenMetaverse; | 37 | using OpenMetaverse; |
37 | using OpenMetaverse.Imaging; | 38 | using OpenMetaverse.Imaging; |
38 | using OpenMetaverse.StructuredData; | 39 | using OpenMetaverse.StructuredData; |
@@ -60,6 +61,8 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
60 | 61 | ||
61 | private static readonly string m_mapLayerPath = "0001/"; | 62 | private static readonly string m_mapLayerPath = "0001/"; |
62 | 63 | ||
64 | private OpenSim.Framework.BlockingQueue<MapRequestState> requests = new OpenSim.Framework.BlockingQueue<MapRequestState>(); | ||
65 | |||
63 | //private IConfig m_config; | 66 | //private IConfig m_config; |
64 | private Scene m_scene; | 67 | private Scene m_scene; |
65 | private List<MapBlockData> cachedMapBlocks = new List<MapBlockData>(); | 68 | private List<MapBlockData> cachedMapBlocks = new List<MapBlockData>(); |
@@ -67,7 +70,11 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
67 | private byte[] myMapImageJPEG; | 70 | private byte[] myMapImageJPEG; |
68 | private bool m_Enabled = false; | 71 | private bool m_Enabled = false; |
69 | private Dictionary<UUID, MapRequestState> m_openRequests = new Dictionary<UUID, MapRequestState>(); | 72 | private Dictionary<UUID, MapRequestState> m_openRequests = new Dictionary<UUID, MapRequestState>(); |
70 | 73 | private Dictionary<string, int> m_blacklistedurls = new Dictionary<string, int>(); | |
74 | private Dictionary<ulong, int> m_blacklistedregions = new Dictionary<ulong, int>(); | ||
75 | private Dictionary<ulong, string> m_cachedRegionMapItemsAddress = new Dictionary<ulong, string>(); | ||
76 | private Thread mapItemReqThread; | ||
77 | private volatile bool threadrunning = false; | ||
71 | 78 | ||
72 | //private int CacheRegionsDistance = 256; | 79 | //private int CacheRegionsDistance = 256; |
73 | 80 | ||
@@ -93,12 +100,15 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
93 | 100 | ||
94 | m_scene.AddHTTPHandler(regionimage, OnHTTPGetMapImage); | 101 | m_scene.AddHTTPHandler(regionimage, OnHTTPGetMapImage); |
95 | m_scene.AddLLSDHandler("/MAP/MapItems/" + scene.RegionInfo.RegionHandle.ToString(),HandleRemoteMapItemRequest); | 102 | m_scene.AddLLSDHandler("/MAP/MapItems/" + scene.RegionInfo.RegionHandle.ToString(),HandleRemoteMapItemRequest); |
96 | //QuadTree.Subdivide(); | 103 | |
97 | //QuadTree.Subdivide(); | ||
98 | 104 | ||
99 | scene.EventManager.OnRegisterCaps += OnRegisterCaps; | 105 | scene.EventManager.OnRegisterCaps += OnRegisterCaps; |
100 | scene.EventManager.OnNewClient += OnNewClient; | 106 | scene.EventManager.OnNewClient += OnNewClient; |
101 | scene.EventManager.OnClientClosed += ClientLoggedOut; | 107 | scene.EventManager.OnClientClosed += ClientLoggedOut; |
108 | scene.EventManager.OnMakeChildAgent += MakeChildAgent; | ||
109 | scene.EventManager.OnAvatarEnteringNewParcel += AvatarEnteringParcel; | ||
110 | |||
111 | |||
102 | } | 112 | } |
103 | public void PostInitialise() | 113 | public void PostInitialise() |
104 | { | 114 | { |
@@ -220,33 +230,80 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
220 | } | 230 | } |
221 | #region EventHandlers | 231 | #region EventHandlers |
222 | 232 | ||
223 | 233 | /// <summary> | |
234 | /// Registered for event | ||
235 | /// </summary> | ||
236 | /// <param name="client"></param> | ||
224 | private void OnNewClient(IClientAPI client) | 237 | private void OnNewClient(IClientAPI client) |
225 | { | 238 | { |
226 | // All friends establishment protocol goes over instant message | ||
227 | // There's no way to send a message from the sim | ||
228 | // to a user to 'add a friend' without causing dialog box spam | ||
229 | // | ||
230 | // The base set of friends are added when the user signs on in their XMLRPC response | ||
231 | // Generated by LoginService. The friends are retreived from the database by the UserManager | ||
232 | |||
233 | // Subscribe to instant messages | ||
234 | |||
235 | //client.OnInstantMessage += OnInstantMessage; | ||
236 | //client.OnApproveFriendRequest += OnApprovedFriendRequest; | ||
237 | //client.OnDenyFriendRequest += OnDenyFriendRequest; | ||
238 | //client.OnTerminateFriendship += OnTerminateFriendship; | ||
239 | |||
240 | //doFriendListUpdateOnline(client.AgentId); | ||
241 | client.OnRequestMapBlocks += RequestMapBlocks; | 239 | client.OnRequestMapBlocks += RequestMapBlocks; |
242 | client.OnMapItemRequest += HandleMapItemRequest; | 240 | client.OnMapItemRequest += HandleMapItemRequest; |
243 | } | 241 | } |
242 | |||
243 | /// <summary> | ||
244 | /// Client logged out, check to see if there are any more root agents in the simulator | ||
245 | /// If not, stop the mapItemRequest Thread | ||
246 | /// Event handler | ||
247 | /// </summary> | ||
248 | /// <param name="AgentId">AgentID that logged out</param> | ||
244 | private void ClientLoggedOut(UUID AgentId) | 249 | private void ClientLoggedOut(UUID AgentId) |
245 | { | 250 | { |
251 | |||
252 | List<ScenePresence> presences = m_scene.GetAvatars(); | ||
253 | int rootcount = 0; | ||
254 | for (int i=0;i<presences.Count;i++) | ||
255 | { | ||
256 | if (presences[i] != null) | ||
257 | { | ||
258 | if (!presences[i].IsChildAgent) | ||
259 | rootcount++; | ||
260 | } | ||
261 | } | ||
262 | if (rootcount <= 1) | ||
263 | StopThread(); | ||
264 | |||
246 | 265 | ||
247 | } | 266 | } |
248 | #endregion | 267 | #endregion |
249 | 268 | ||
269 | /// <summary> | ||
270 | /// Starts the MapItemRequest Thread | ||
271 | /// Note that this only gets started when there are actually agents in the region | ||
272 | /// Additionally, it gets stopped when there are none. | ||
273 | /// </summary> | ||
274 | /// <param name="o"></param> | ||
275 | private void StartThread(object o) | ||
276 | { | ||
277 | if (threadrunning) return; | ||
278 | |||
279 | m_log.Warn("[WorldMap]: Starting remote MapItem request thread"); | ||
280 | threadrunning = true; | ||
281 | mapItemReqThread = new Thread(new ThreadStart(process)); | ||
282 | mapItemReqThread.IsBackground = true; | ||
283 | mapItemReqThread.Name = "MapItemRequestThread"; | ||
284 | mapItemReqThread.Priority = ThreadPriority.BelowNormal; | ||
285 | mapItemReqThread.SetApartmentState(ApartmentState.MTA); | ||
286 | mapItemReqThread.Start(); | ||
287 | ThreadTracker.Add(mapItemReqThread); | ||
288 | } | ||
289 | |||
290 | /// <summary> | ||
291 | /// Enqueues a 'stop thread' MapRequestState. Causes the MapItemRequest thread to end | ||
292 | /// </summary> | ||
293 | private void StopThread() | ||
294 | { | ||
295 | MapRequestState st = new MapRequestState(); | ||
296 | st.agentID=UUID.Zero; | ||
297 | st.EstateID=0; | ||
298 | st.flags=0; | ||
299 | st.godlike=false; | ||
300 | st.itemtype=0; | ||
301 | st.regionhandle=0; | ||
302 | |||
303 | requests.Enqueue(st); | ||
304 | } | ||
305 | |||
306 | |||
250 | public virtual void HandleMapItemRequest(IClientAPI remoteClient, uint flags, | 307 | public virtual void HandleMapItemRequest(IClientAPI remoteClient, uint flags, |
251 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) | 308 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) |
252 | { | 309 | { |
@@ -257,6 +314,7 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
257 | { | 314 | { |
258 | if (regionhandle == 0 || regionhandle == m_scene.RegionInfo.RegionHandle) | 315 | if (regionhandle == 0 || regionhandle == m_scene.RegionInfo.RegionHandle) |
259 | { | 316 | { |
317 | // Local Map Item Request | ||
260 | List<ScenePresence> avatars = m_scene.GetAvatars(); | 318 | List<ScenePresence> avatars = m_scene.GetAvatars(); |
261 | int tc = System.Environment.TickCount; | 319 | int tc = System.Environment.TickCount; |
262 | List<mapItemReply> mapitems = new List<mapItemReply>(); | 320 | List<mapItemReply> mapitems = new List<mapItemReply>(); |
@@ -295,29 +353,62 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
295 | } | 353 | } |
296 | else | 354 | else |
297 | { | 355 | { |
298 | //RegionInfo mreg = m_scene.SceneGridService.RequestNeighbouringRegionInfo(regionhandle); | 356 | // Remote Map Item Request |
299 | //if (mreg != null) | 357 | |
300 | //{ | 358 | |
301 | // string httpserver = "http://" + mreg.ExternalEndPoint.Address.ToString() + ":" + mreg.HttpPort + "/MAP/MapItems/" + regionhandle.ToString(); | 359 | // ensures that the blockingqueue doesn't get borked if the GetAgents() timing changes. |
302 | 360 | // Note that we only start up a remote mapItem Request thread if there's users who could | |
303 | // RequestMapItems(httpserver,remoteClient.AgentId,flags,EstateID,godlike,itemtype,regionhandle); | 361 | // be making requests |
304 | 362 | if (!threadrunning) | |
305 | //} | 363 | { |
364 | m_log.Warn("[WorldMap]: Starting new remote request thread manually. This means that AvatarEnteringParcel never fired! This needs to be fixed! Don't Mantis this, as the developers can see it in this message"); | ||
365 | StartThread(new object()); | ||
366 | } | ||
367 | |||
368 | RequestMapItems("",remoteClient.AgentId,flags,EstateID,godlike,itemtype,regionhandle); | ||
369 | |||
306 | } | 370 | } |
307 | } | 371 | } |
308 | 372 | ||
309 | } | 373 | } |
310 | 374 | ||
311 | public delegate LLSDMap RequestMapItemsDelegate(string httpserver, UUID id, uint flags, | 375 | /// <summary> |
312 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle); | 376 | /// Processing thread main() loop for doing remote mapitem requests |
377 | /// </summary> | ||
378 | public void process() | ||
379 | { | ||
380 | while (true) | ||
381 | { | ||
382 | MapRequestState st = requests.Dequeue(); | ||
383 | |||
384 | // end gracefully | ||
385 | if (st.agentID == UUID.Zero) | ||
386 | { | ||
387 | ThreadTracker.Remove(mapItemReqThread); | ||
388 | break; | ||
389 | } | ||
390 | LLSDMap response = RequestMapItemsAsync("", st.agentID, st.flags, st.EstateID, st.godlike, st.itemtype, st.regionhandle); | ||
391 | RequestMapItemsCompleted(response); | ||
392 | } | ||
393 | threadrunning = false; | ||
394 | m_log.Warn("[WorldMap]: Remote request thread exiting"); | ||
395 | } | ||
313 | 396 | ||
314 | private void RequestMapItemsCompleted(IAsyncResult iar) | 397 | /// <summary> |
398 | /// Enqueues the map item request into the processing thread | ||
399 | /// </summary> | ||
400 | /// <param name="state"></param> | ||
401 | public void EnqueueMapItemRequest(MapRequestState state) | ||
315 | { | 402 | { |
316 | 403 | requests.Enqueue(state); | |
317 | RequestMapItemsDelegate icon = (RequestMapItemsDelegate)iar.AsyncState; | 404 | } |
318 | LLSDMap response = icon.EndInvoke(iar); | ||
319 | |||
320 | 405 | ||
406 | /// <summary> | ||
407 | /// Sends the mapitem response to the IClientAPI | ||
408 | /// </summary> | ||
409 | /// <param name="response">The LLSDMap Response for the mapitem</param> | ||
410 | private void RequestMapItemsCompleted(LLSDMap response) | ||
411 | { | ||
321 | 412 | ||
322 | UUID requestID = response["requestID"].AsUUID(); | 413 | UUID requestID = response["requestID"].AsUUID(); |
323 | 414 | ||
@@ -364,19 +455,99 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
364 | } | 455 | } |
365 | } | 456 | } |
366 | } | 457 | } |
458 | |||
459 | /// <summary> | ||
460 | /// Enqueue the MapItem request for remote processing | ||
461 | /// </summary> | ||
462 | /// <param name="httpserver">blank string, we discover this in the process</param> | ||
463 | /// <param name="id">Agent ID that we are making this request on behalf</param> | ||
464 | /// <param name="flags">passed in from packet</param> | ||
465 | /// <param name="EstateID">passed in from packet</param> | ||
466 | /// <param name="godlike">passed in from packet</param> | ||
467 | /// <param name="itemtype">passed in from packet</param> | ||
468 | /// <param name="regionhandle">Region we're looking up</param> | ||
367 | public void RequestMapItems(string httpserver, UUID id, uint flags, | 469 | public void RequestMapItems(string httpserver, UUID id, uint flags, |
368 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) | 470 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) |
369 | { | 471 | { |
370 | //m_log.Info("[INTER]: " + debugRegionName + ": SceneCommunicationService: Sending InterRegion Notification that region is up " + region.RegionName); | 472 | MapRequestState st = new MapRequestState(); |
371 | RequestMapItemsDelegate d = RequestMapItemsAsync; | 473 | st.agentID = id; |
372 | d.BeginInvoke(httpserver, id,flags,EstateID,godlike,itemtype,regionhandle,RequestMapItemsCompleted, d); | 474 | st.flags = flags; |
373 | //bool val = m_commsProvider.InterRegion.RegionUp(new SerializableRegionInfo(region)); | 475 | st.EstateID = EstateID; |
476 | st.godlike = godlike; | ||
477 | st.itemtype = itemtype; | ||
478 | st.regionhandle = regionhandle; | ||
479 | EnqueueMapItemRequest(st); | ||
480 | |||
374 | } | 481 | } |
482 | |||
483 | /// <summary> | ||
484 | /// Does the actual remote mapitem request | ||
485 | /// This should be called from an asynchronous thread | ||
486 | /// Request failures get blacklisted until region restart so we don't | ||
487 | /// continue to spend resources trying to contact regions that are down. | ||
488 | /// </summary> | ||
489 | /// <param name="httpserver">blank string, we discover this in the process</param> | ||
490 | /// <param name="id">Agent ID that we are making this request on behalf</param> | ||
491 | /// <param name="flags">passed in from packet</param> | ||
492 | /// <param name="EstateID">passed in from packet</param> | ||
493 | /// <param name="godlike">passed in from packet</param> | ||
494 | /// <param name="itemtype">passed in from packet</param> | ||
495 | /// <param name="regionhandle">Region we're looking up</param> | ||
496 | /// <returns></returns> | ||
375 | private LLSDMap RequestMapItemsAsync(string httpserver, UUID id, uint flags, | 497 | private LLSDMap RequestMapItemsAsync(string httpserver, UUID id, uint flags, |
376 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) | 498 | uint EstateID, bool godlike, uint itemtype, ulong regionhandle) |
377 | { | 499 | { |
500 | bool blacklisted = false; | ||
501 | lock (m_blacklistedregions) | ||
502 | { | ||
503 | if (m_blacklistedregions.ContainsKey(regionhandle)) | ||
504 | blacklisted = true; | ||
505 | |||
506 | } | ||
507 | |||
508 | if (blacklisted) | ||
509 | return new LLSDMap(); | ||
510 | |||
378 | UUID requestID = UUID.Random(); | 511 | UUID requestID = UUID.Random(); |
379 | 512 | lock (m_cachedRegionMapItemsAddress) | |
513 | { | ||
514 | if (m_cachedRegionMapItemsAddress.ContainsKey(regionhandle)) | ||
515 | httpserver = m_cachedRegionMapItemsAddress[regionhandle]; | ||
516 | } | ||
517 | if (httpserver.Length == 0) | ||
518 | { | ||
519 | RegionInfo mreg = m_scene.SceneGridService.RequestNeighbouringRegionInfo(regionhandle); | ||
520 | if (mreg != null) | ||
521 | { | ||
522 | httpserver = "http://" + mreg.ExternalEndPoint.Address.ToString() + ":" + mreg.HttpPort + "/MAP/MapItems/" + regionhandle.ToString(); | ||
523 | lock (m_cachedRegionMapItemsAddress) | ||
524 | { | ||
525 | if (!m_cachedRegionMapItemsAddress.ContainsKey(regionhandle)) | ||
526 | m_cachedRegionMapItemsAddress.Add(regionhandle, httpserver); | ||
527 | } | ||
528 | } | ||
529 | else | ||
530 | { | ||
531 | lock (m_blacklistedregions) | ||
532 | { | ||
533 | if (!m_blacklistedregions.ContainsKey(regionhandle)) | ||
534 | m_blacklistedregions.Add(regionhandle, System.Environment.TickCount); | ||
535 | } | ||
536 | m_log.WarnFormat("[WorldMap]: Blacklisted region {0}", regionhandle.ToString()); | ||
537 | } | ||
538 | } | ||
539 | |||
540 | blacklisted = false; | ||
541 | lock (m_blacklistedurls) | ||
542 | { | ||
543 | if (m_blacklistedurls.ContainsKey(httpserver)) | ||
544 | blacklisted = true; | ||
545 | } | ||
546 | |||
547 | // Can't find the http server | ||
548 | if (httpserver.Length == 0 || blacklisted) | ||
549 | return new LLSDMap(); | ||
550 | |||
380 | MapRequestState mrs = new MapRequestState(); | 551 | MapRequestState mrs = new MapRequestState(); |
381 | mrs.agentID = id; | 552 | mrs.agentID = id; |
382 | mrs.EstateID = EstateID; | 553 | mrs.EstateID = EstateID; |
@@ -413,30 +584,43 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
413 | { | 584 | { |
414 | m_log.InfoFormat("[WorldMap] Bad send on GetMapItems {0}", ex.Message); | 585 | m_log.InfoFormat("[WorldMap] Bad send on GetMapItems {0}", ex.Message); |
415 | responseMap["connect"] = LLSD.FromBoolean(false); | 586 | responseMap["connect"] = LLSD.FromBoolean(false); |
587 | lock (m_blacklistedurls) | ||
588 | { | ||
589 | if (!m_blacklistedurls.ContainsKey(httpserver)) | ||
590 | m_blacklistedurls.Add(httpserver, System.Environment.TickCount); | ||
591 | } | ||
592 | |||
593 | m_log.WarnFormat("[WorldMap]: Blacklisted {0}", httpserver); | ||
416 | 594 | ||
417 | return responseMap; | 595 | return responseMap; |
418 | } | 596 | } |
419 | 597 | ||
420 | //m_log.Info("[OGP] waiting for a reply after rez avatar send"); | ||
421 | string response_mapItems_reply = null; | 598 | string response_mapItems_reply = null; |
422 | { // get the response | 599 | { // get the response |
423 | try | 600 | try |
424 | { | 601 | { |
425 | WebResponse webResponse = mapitemsrequest.GetResponse(); | 602 | WebResponse webResponse = mapitemsrequest.GetResponse(); |
426 | if (webResponse == null) | 603 | if (webResponse != null) |
427 | { | 604 | { |
428 | //m_log.Info("[OGP:] Null reply on rez_avatar post"); | 605 | StreamReader sr = new StreamReader(webResponse.GetResponseStream()); |
606 | response_mapItems_reply = sr.ReadToEnd().Trim(); | ||
607 | } | ||
608 | else | ||
609 | { | ||
610 | return new LLSDMap(); | ||
429 | } | 611 | } |
430 | |||
431 | StreamReader sr = new StreamReader(webResponse.GetResponseStream()); | ||
432 | response_mapItems_reply = sr.ReadToEnd().Trim(); | ||
433 | //m_log.InfoFormat("[OGP]: rez_avatar reply was {0} ", response_mapItems_reply); | ||
434 | |||
435 | } | 612 | } |
436 | catch (WebException) | 613 | catch (WebException) |
437 | { | 614 | { |
438 | //m_log.InfoFormat("[OGP]: exception on read after send of rez avatar {0}", ex.Message); | 615 | |
439 | responseMap["connect"] = LLSD.FromBoolean(false); | 616 | responseMap["connect"] = LLSD.FromBoolean(false); |
617 | lock (m_blacklistedurls) | ||
618 | { | ||
619 | if (!m_blacklistedurls.ContainsKey(httpserver)) | ||
620 | m_blacklistedurls.Add(httpserver, System.Environment.TickCount); | ||
621 | } | ||
622 | |||
623 | m_log.WarnFormat("[WorldMap]: Blacklisted {0}", httpserver); | ||
440 | 624 | ||
441 | return responseMap; | 625 | return responseMap; |
442 | } | 626 | } |
@@ -655,7 +839,34 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
655 | } | 839 | } |
656 | return responsemap; | 840 | return responsemap; |
657 | } | 841 | } |
842 | |||
843 | private void AvatarEnteringParcel(ScenePresence avatar, int localLandID, UUID regionID) | ||
844 | { | ||
845 | // You may ask, why this is in a threadpool to start with.. | ||
846 | // The reason is so we don't cause the thread to freeze waiting | ||
847 | // for the 1 second it costs to start a thread manually. | ||
848 | |||
849 | if (!threadrunning) | ||
850 | ThreadPool.QueueUserWorkItem(new WaitCallback(this.StartThread)); | ||
851 | } | ||
852 | |||
853 | private void MakeChildAgent(ScenePresence avatar) | ||
854 | { | ||
855 | List<ScenePresence> presences = m_scene.GetAvatars(); | ||
856 | int rootcount = 0; | ||
857 | for (int i = 0; i < presences.Count; i++) | ||
858 | { | ||
859 | if (presences[i] != null) | ||
860 | { | ||
861 | if (!presences[i].IsChildAgent) | ||
862 | rootcount++; | ||
863 | } | ||
864 | } | ||
865 | if (rootcount <= 1) | ||
866 | StopThread(); | ||
867 | } | ||
658 | } | 868 | } |
869 | |||
659 | public struct MapRequestState | 870 | public struct MapRequestState |
660 | { | 871 | { |
661 | public UUID agentID; | 872 | public UUID agentID; |
@@ -665,6 +876,5 @@ namespace OpenSim.Region.Environment.Modules.World.WorldMap | |||
665 | public uint itemtype; | 876 | public uint itemtype; |
666 | public ulong regionhandle; | 877 | public ulong regionhandle; |
667 | } | 878 | } |
668 | 879 | ||
669 | |||
670 | } | 880 | } |