diff options
author | Melanie | 2013-07-24 04:45:07 +0100 |
---|---|---|
committer | Melanie | 2013-07-24 04:45:07 +0100 |
commit | e82d4154a2e348e4a38f01cb1877878c94569bba (patch) | |
tree | 7b27e6d30d04f6089fd17d5779cfb0a81e50d508 /OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs | |
parent | Merge branch 'master' into careminster (diff) | |
parent | For unknown user issue, bump GUN7 to GUN8 and UMMAU3 to UMMAU4 to assess what... (diff) | |
download | opensim-SC-e82d4154a2e348e4a38f01cb1877878c94569bba.zip opensim-SC-e82d4154a2e348e4a38f01cb1877878c94569bba.tar.gz opensim-SC-e82d4154a2e348e4a38f01cb1877878c94569bba.tar.bz2 opensim-SC-e82d4154a2e348e4a38f01cb1877878c94569bba.tar.xz |
Merge branch 'master' into careminster
Conflicts:
OpenSim/Framework/Servers/HttpServer/PollServiceRequestManager.cs
OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs
OpenSim/Region/ClientStack/Linden/UDP/LLUDPServer.cs
OpenSim/Region/Framework/Scenes/Scene.PacketHandlers.cs
OpenSim/Region/Framework/Scenes/ScenePresence.cs
OpenSim/Region/Physics/Manager/PhysicsActor.cs
OpenSim/Region/Physics/Manager/PhysicsScene.cs
Diffstat (limited to 'OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs')
-rw-r--r-- | OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs | 242 |
1 files changed, 153 insertions, 89 deletions
diff --git a/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs b/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs index ac5e77e..0e20e38 100644 --- a/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs +++ b/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs | |||
@@ -96,6 +96,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
96 | public event Action<IClientAPI, bool> OnCompleteMovementToRegion; | 96 | public event Action<IClientAPI, bool> OnCompleteMovementToRegion; |
97 | public event UpdateAgent OnPreAgentUpdate; | 97 | public event UpdateAgent OnPreAgentUpdate; |
98 | public event UpdateAgent OnAgentUpdate; | 98 | public event UpdateAgent OnAgentUpdate; |
99 | public event UpdateAgent OnAgentCameraUpdate; | ||
99 | public event AgentRequestSit OnAgentRequestSit; | 100 | public event AgentRequestSit OnAgentRequestSit; |
100 | public event AgentSit OnAgentSit; | 101 | public event AgentSit OnAgentSit; |
101 | public event AvatarPickerRequest OnAvatarPickerRequest; | 102 | public event AvatarPickerRequest OnAvatarPickerRequest; |
@@ -368,7 +369,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
368 | /// This does mean that agent updates must be processed synchronously, at least for each client, and called methods | 369 | /// This does mean that agent updates must be processed synchronously, at least for each client, and called methods |
369 | /// cannot retain a reference to it outside of that method. | 370 | /// cannot retain a reference to it outside of that method. |
370 | /// </remarks> | 371 | /// </remarks> |
371 | private AgentUpdateArgs m_lastAgentUpdateArgs; | 372 | private AgentUpdateArgs m_thisAgentUpdateArgs = new AgentUpdateArgs(); |
372 | 373 | ||
373 | protected Dictionary<PacketType, PacketProcessor> m_packetHandlers = new Dictionary<PacketType, PacketProcessor>(); | 374 | protected Dictionary<PacketType, PacketProcessor> m_packetHandlers = new Dictionary<PacketType, PacketProcessor>(); |
374 | protected Dictionary<string, GenericMessage> m_genericPacketHandlers = new Dictionary<string, GenericMessage>(); //PauPaw:Local Generic Message handlers | 375 | protected Dictionary<string, GenericMessage> m_genericPacketHandlers = new Dictionary<string, GenericMessage>(); //PauPaw:Local Generic Message handlers |
@@ -505,6 +506,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
505 | m_udpServer = udpServer; | 506 | m_udpServer = udpServer; |
506 | m_udpClient = udpClient; | 507 | m_udpClient = udpClient; |
507 | m_udpClient.OnQueueEmpty += HandleQueueEmpty; | 508 | m_udpClient.OnQueueEmpty += HandleQueueEmpty; |
509 | m_udpClient.HasUpdates += HandleHasUpdates; | ||
508 | m_udpClient.OnPacketStats += PopulateStats; | 510 | m_udpClient.OnPacketStats += PopulateStats; |
509 | 511 | ||
510 | m_prioritizer = new Prioritizer(m_scene); | 512 | m_prioritizer = new Prioritizer(m_scene); |
@@ -4164,8 +4166,14 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
4164 | 4166 | ||
4165 | void HandleQueueEmpty(ThrottleOutPacketTypeFlags categories) | 4167 | void HandleQueueEmpty(ThrottleOutPacketTypeFlags categories) |
4166 | { | 4168 | { |
4169 | // if (!m_udpServer.IsRunningOutbound) | ||
4170 | // return; | ||
4171 | |||
4167 | if ((categories & ThrottleOutPacketTypeFlags.Task) != 0) | 4172 | if ((categories & ThrottleOutPacketTypeFlags.Task) != 0) |
4168 | { | 4173 | { |
4174 | // if (!m_udpServer.IsRunningOutbound) | ||
4175 | // return; | ||
4176 | |||
4169 | if (m_maxUpdates == 0 || m_LastQueueFill == 0) | 4177 | if (m_maxUpdates == 0 || m_LastQueueFill == 0) |
4170 | { | 4178 | { |
4171 | m_maxUpdates = m_udpServer.PrimUpdatesPerCallback; | 4179 | m_maxUpdates = m_udpServer.PrimUpdatesPerCallback; |
@@ -4191,6 +4199,27 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
4191 | ImageManager.ProcessImageQueue(m_udpServer.TextureSendLimit); | 4199 | ImageManager.ProcessImageQueue(m_udpServer.TextureSendLimit); |
4192 | } | 4200 | } |
4193 | 4201 | ||
4202 | internal bool HandleHasUpdates(ThrottleOutPacketTypeFlags categories) | ||
4203 | { | ||
4204 | bool hasUpdates = false; | ||
4205 | |||
4206 | if ((categories & ThrottleOutPacketTypeFlags.Task) != 0) | ||
4207 | { | ||
4208 | if (m_entityUpdates.Count > 0) | ||
4209 | hasUpdates = true; | ||
4210 | else if (m_entityProps.Count > 0) | ||
4211 | hasUpdates = true; | ||
4212 | } | ||
4213 | |||
4214 | if ((categories & ThrottleOutPacketTypeFlags.Texture) != 0) | ||
4215 | { | ||
4216 | if (ImageManager.HasUpdates()) | ||
4217 | hasUpdates = true; | ||
4218 | } | ||
4219 | |||
4220 | return hasUpdates; | ||
4221 | } | ||
4222 | |||
4194 | public void SendAssetUploadCompleteMessage(sbyte AssetType, bool Success, UUID AssetFullID) | 4223 | public void SendAssetUploadCompleteMessage(sbyte AssetType, bool Success, UUID AssetFullID) |
4195 | { | 4224 | { |
4196 | AssetUploadCompletePacket newPack = new AssetUploadCompletePacket(); | 4225 | AssetUploadCompletePacket newPack = new AssetUploadCompletePacket(); |
@@ -5058,7 +5087,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
5058 | SceneObjectPart part = (SceneObjectPart)entity; | 5087 | SceneObjectPart part = (SceneObjectPart)entity; |
5059 | 5088 | ||
5060 | attachPoint = part.ParentGroup.AttachmentPoint; | 5089 | attachPoint = part.ParentGroup.AttachmentPoint; |
5061 | 5090 | attachPoint = ((attachPoint % 16) * 16 + (attachPoint / 16)); | |
5062 | // m_log.DebugFormat( | 5091 | // m_log.DebugFormat( |
5063 | // "[LLCLIENTVIEW]: Sending attachPoint {0} for {1} {2} to {3}", | 5092 | // "[LLCLIENTVIEW]: Sending attachPoint {0} for {1} {2} to {3}", |
5064 | // attachPoint, part.Name, part.LocalId, Name); | 5093 | // attachPoint, part.Name, part.LocalId, Name); |
@@ -5086,7 +5115,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
5086 | pos += 4; | 5115 | pos += 4; |
5087 | 5116 | ||
5088 | // Avatar/CollisionPlane | 5117 | // Avatar/CollisionPlane |
5089 | data[pos++] = (byte)((attachPoint % 16) * 16 + (attachPoint / 16)); ; | 5118 | data[pos++] = (byte) attachPoint; |
5090 | if (avatar) | 5119 | if (avatar) |
5091 | { | 5120 | { |
5092 | data[pos++] = 1; | 5121 | data[pos++] = 1; |
@@ -5618,83 +5647,137 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
5618 | 5647 | ||
5619 | #region Packet Handlers | 5648 | #region Packet Handlers |
5620 | 5649 | ||
5650 | public int TotalAgentUpdates { get; set; } | ||
5651 | |||
5621 | #region Scene/Avatar | 5652 | #region Scene/Avatar |
5622 | 5653 | ||
5623 | private bool HandleAgentUpdate(IClientAPI sener, Packet packet) | 5654 | // Threshold for body rotation to be a significant agent update |
5655 | private const float QDELTA = 0.000001f; | ||
5656 | // Threshold for camera rotation to be a significant agent update | ||
5657 | private const float VDELTA = 0.01f; | ||
5658 | |||
5659 | /// <summary> | ||
5660 | /// This checks the update significance against the last update made. | ||
5661 | /// </summary> | ||
5662 | /// <remarks>Can only be called by one thread at a time</remarks> | ||
5663 | /// <returns></returns> | ||
5664 | /// <param name='x'></param> | ||
5665 | public bool CheckAgentUpdateSignificance(AgentUpdatePacket.AgentDataBlock x) | ||
5624 | { | 5666 | { |
5625 | if (OnAgentUpdate != null) | 5667 | return CheckAgentMovementUpdateSignificance(x) || CheckAgentCameraUpdateSignificance(x); |
5626 | { | 5668 | } |
5627 | AgentUpdatePacket agentUpdate = (AgentUpdatePacket)packet; | ||
5628 | 5669 | ||
5629 | #region Packet Session and User Check | 5670 | /// <summary> |
5630 | if (agentUpdate.AgentData.SessionID != SessionId || agentUpdate.AgentData.AgentID != AgentId) | 5671 | /// This checks the movement/state update significance against the last update made. |
5631 | { | 5672 | /// </summary> |
5632 | PacketPool.Instance.ReturnPacket(packet); | 5673 | /// <remarks>Can only be called by one thread at a time</remarks> |
5633 | return false; | 5674 | /// <returns></returns> |
5634 | } | 5675 | /// <param name='x'></param> |
5635 | #endregion | 5676 | private bool CheckAgentMovementUpdateSignificance(AgentUpdatePacket.AgentDataBlock x) |
5677 | { | ||
5678 | float qdelta1 = 1 - (float)Math.Pow(Quaternion.Dot(x.BodyRotation, m_thisAgentUpdateArgs.BodyRotation), 2); | ||
5679 | //qdelta2 = 1 - (float)Math.Pow(Quaternion.Dot(x.HeadRotation, m_thisAgentUpdateArgs.HeadRotation), 2); | ||
5680 | |||
5681 | bool movementSignificant = | ||
5682 | (qdelta1 > QDELTA) // significant if body rotation above threshold | ||
5683 | // Ignoring head rotation altogether, because it's not being used for anything interesting up the stack | ||
5684 | // || (qdelta2 > QDELTA * 10) // significant if head rotation above threshold | ||
5685 | || (x.ControlFlags != m_thisAgentUpdateArgs.ControlFlags) // significant if control flags changed | ||
5686 | || (x.ControlFlags != (byte)AgentManager.ControlFlags.NONE) // significant if user supplying any movement update commands | ||
5687 | || (x.Far != m_thisAgentUpdateArgs.Far) // significant if far distance changed | ||
5688 | || (x.Flags != m_thisAgentUpdateArgs.Flags) // significant if Flags changed | ||
5689 | || (x.State != m_thisAgentUpdateArgs.State) // significant if Stats changed | ||
5690 | ; | ||
5691 | //if (movementSignificant) | ||
5692 | //{ | ||
5693 | //m_log.DebugFormat("[LLCLIENTVIEW]: Bod {0} {1}", | ||
5694 | // qdelta1, qdelta2); | ||
5695 | //m_log.DebugFormat("[LLCLIENTVIEW]: St {0} {1} {2} {3}", | ||
5696 | // x.ControlFlags, x.Flags, x.Far, x.State); | ||
5697 | //} | ||
5698 | return movementSignificant; | ||
5699 | } | ||
5636 | 5700 | ||
5637 | bool update = false; | 5701 | /// <summary> |
5638 | AgentUpdatePacket.AgentDataBlock x = agentUpdate.AgentData; | 5702 | /// This checks the camera update significance against the last update made. |
5639 | 5703 | /// </summary> | |
5640 | if (m_lastAgentUpdateArgs != null) | 5704 | /// <remarks>Can only be called by one thread at a time</remarks> |
5641 | { | 5705 | /// <returns></returns> |
5642 | // These should be ordered from most-likely to | 5706 | /// <param name='x'></param> |
5643 | // least likely to change. I've made an initial | 5707 | private bool CheckAgentCameraUpdateSignificance(AgentUpdatePacket.AgentDataBlock x) |
5644 | // guess at that. | 5708 | { |
5645 | update = | 5709 | float vdelta1 = Vector3.Distance(x.CameraAtAxis, m_thisAgentUpdateArgs.CameraAtAxis); |
5646 | ( | 5710 | float vdelta2 = Vector3.Distance(x.CameraCenter, m_thisAgentUpdateArgs.CameraCenter); |
5647 | (x.BodyRotation != m_lastAgentUpdateArgs.BodyRotation) || | 5711 | float vdelta3 = Vector3.Distance(x.CameraLeftAxis, m_thisAgentUpdateArgs.CameraLeftAxis); |
5648 | (x.CameraAtAxis != m_lastAgentUpdateArgs.CameraAtAxis) || | 5712 | float vdelta4 = Vector3.Distance(x.CameraUpAxis, m_thisAgentUpdateArgs.CameraUpAxis); |
5649 | (x.CameraCenter != m_lastAgentUpdateArgs.CameraCenter) || | ||
5650 | (x.CameraLeftAxis != m_lastAgentUpdateArgs.CameraLeftAxis) || | ||
5651 | (x.CameraUpAxis != m_lastAgentUpdateArgs.CameraUpAxis) || | ||
5652 | (x.ControlFlags != m_lastAgentUpdateArgs.ControlFlags) || | ||
5653 | (x.ControlFlags != 0) || | ||
5654 | (x.Far != m_lastAgentUpdateArgs.Far) || | ||
5655 | (x.Flags != m_lastAgentUpdateArgs.Flags) || | ||
5656 | (x.State != m_lastAgentUpdateArgs.State) || | ||
5657 | (x.HeadRotation != m_lastAgentUpdateArgs.HeadRotation) || | ||
5658 | (x.SessionID != m_lastAgentUpdateArgs.SessionID) || | ||
5659 | (x.AgentID != m_lastAgentUpdateArgs.AgentID) | ||
5660 | ); | ||
5661 | } | ||
5662 | else | ||
5663 | { | ||
5664 | m_lastAgentUpdateArgs = new AgentUpdateArgs(); | ||
5665 | update = true; | ||
5666 | } | ||
5667 | 5713 | ||
5668 | if (update) | 5714 | bool cameraSignificant = |
5669 | { | 5715 | (vdelta1 > VDELTA) || |
5670 | // m_log.DebugFormat("[LLCLIENTVIEW]: Triggered AgentUpdate for {0}", sener.Name); | 5716 | (vdelta2 > VDELTA) || |
5717 | (vdelta3 > VDELTA) || | ||
5718 | (vdelta4 > VDELTA) | ||
5719 | ; | ||
5671 | 5720 | ||
5672 | m_lastAgentUpdateArgs.AgentID = x.AgentID; | 5721 | //if (cameraSignificant) |
5673 | m_lastAgentUpdateArgs.BodyRotation = x.BodyRotation; | 5722 | //{ |
5674 | m_lastAgentUpdateArgs.CameraAtAxis = x.CameraAtAxis; | 5723 | //m_log.DebugFormat("[LLCLIENTVIEW]: Cam1 {0} {1}", |
5675 | m_lastAgentUpdateArgs.CameraCenter = x.CameraCenter; | 5724 | // x.CameraAtAxis, x.CameraCenter); |
5676 | m_lastAgentUpdateArgs.CameraLeftAxis = x.CameraLeftAxis; | 5725 | //m_log.DebugFormat("[LLCLIENTVIEW]: Cam2 {0} {1}", |
5677 | m_lastAgentUpdateArgs.CameraUpAxis = x.CameraUpAxis; | 5726 | // x.CameraLeftAxis, x.CameraUpAxis); |
5678 | m_lastAgentUpdateArgs.ControlFlags = x.ControlFlags; | 5727 | //} |
5679 | m_lastAgentUpdateArgs.Far = x.Far; | ||
5680 | m_lastAgentUpdateArgs.Flags = x.Flags; | ||
5681 | m_lastAgentUpdateArgs.HeadRotation = x.HeadRotation; | ||
5682 | m_lastAgentUpdateArgs.SessionID = x.SessionID; | ||
5683 | m_lastAgentUpdateArgs.State = x.State; | ||
5684 | 5728 | ||
5685 | UpdateAgent handlerAgentUpdate = OnAgentUpdate; | 5729 | return cameraSignificant; |
5686 | UpdateAgent handlerPreAgentUpdate = OnPreAgentUpdate; | 5730 | } |
5687 | 5731 | ||
5688 | if (handlerPreAgentUpdate != null) | 5732 | private bool HandleAgentUpdate(IClientAPI sener, Packet packet) |
5689 | OnPreAgentUpdate(this, m_lastAgentUpdateArgs); | 5733 | { |
5734 | // We got here, which means that something in agent update was significant | ||
5690 | 5735 | ||
5691 | if (handlerAgentUpdate != null) | 5736 | AgentUpdatePacket agentUpdate = (AgentUpdatePacket)packet; |
5692 | OnAgentUpdate(this, m_lastAgentUpdateArgs); | 5737 | AgentUpdatePacket.AgentDataBlock x = agentUpdate.AgentData; |
5693 | 5738 | ||
5694 | handlerAgentUpdate = null; | 5739 | if (x.AgentID != AgentId || x.SessionID != SessionId) |
5695 | handlerPreAgentUpdate = null; | 5740 | return false; |
5696 | } | 5741 | |
5697 | } | 5742 | // Before we update the current m_thisAgentUpdateArgs, let's check this again |
5743 | // to see what exactly changed | ||
5744 | bool movement = CheckAgentMovementUpdateSignificance(x); | ||
5745 | bool camera = CheckAgentCameraUpdateSignificance(x); | ||
5746 | |||
5747 | m_thisAgentUpdateArgs.AgentID = x.AgentID; | ||
5748 | m_thisAgentUpdateArgs.BodyRotation = x.BodyRotation; | ||
5749 | m_thisAgentUpdateArgs.CameraAtAxis = x.CameraAtAxis; | ||
5750 | m_thisAgentUpdateArgs.CameraCenter = x.CameraCenter; | ||
5751 | m_thisAgentUpdateArgs.CameraLeftAxis = x.CameraLeftAxis; | ||
5752 | m_thisAgentUpdateArgs.CameraUpAxis = x.CameraUpAxis; | ||
5753 | m_thisAgentUpdateArgs.ControlFlags = x.ControlFlags; | ||
5754 | m_thisAgentUpdateArgs.Far = x.Far; | ||
5755 | m_thisAgentUpdateArgs.Flags = x.Flags; | ||
5756 | m_thisAgentUpdateArgs.HeadRotation = x.HeadRotation; | ||
5757 | m_thisAgentUpdateArgs.SessionID = x.SessionID; | ||
5758 | m_thisAgentUpdateArgs.State = x.State; | ||
5759 | |||
5760 | UpdateAgent handlerAgentUpdate = OnAgentUpdate; | ||
5761 | UpdateAgent handlerPreAgentUpdate = OnPreAgentUpdate; | ||
5762 | UpdateAgent handlerAgentCameraUpdate = OnAgentCameraUpdate; | ||
5763 | |||
5764 | // Was there a significant movement/state change? | ||
5765 | if (movement) | ||
5766 | { | ||
5767 | if (handlerPreAgentUpdate != null) | ||
5768 | OnPreAgentUpdate(this, m_thisAgentUpdateArgs); | ||
5769 | |||
5770 | if (handlerAgentUpdate != null) | ||
5771 | OnAgentUpdate(this, m_thisAgentUpdateArgs); | ||
5772 | } | ||
5773 | // Was there a significant camera(s) change? | ||
5774 | if (camera) | ||
5775 | if (handlerAgentCameraUpdate != null) | ||
5776 | handlerAgentCameraUpdate(this, m_thisAgentUpdateArgs); | ||
5777 | |||
5778 | handlerAgentUpdate = null; | ||
5779 | handlerPreAgentUpdate = null; | ||
5780 | handlerAgentCameraUpdate = null; | ||
5698 | 5781 | ||
5699 | PacketPool.Instance.ReturnPacket(packet); | 5782 | PacketPool.Instance.ReturnPacket(packet); |
5700 | 5783 | ||
@@ -12813,7 +12896,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
12813 | OutPacket(dialog, ThrottleOutPacketType.Task); | 12896 | OutPacket(dialog, ThrottleOutPacketType.Task); |
12814 | } | 12897 | } |
12815 | 12898 | ||
12816 | public void StopFlying(ISceneEntity p) | 12899 | public void SendAgentTerseUpdate(ISceneEntity p) |
12817 | { | 12900 | { |
12818 | if (p is ScenePresence) | 12901 | if (p is ScenePresence) |
12819 | { | 12902 | { |
@@ -12827,25 +12910,6 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
12827 | 12910 | ||
12828 | Vector3 pos = presence.AbsolutePosition; | 12911 | Vector3 pos = presence.AbsolutePosition; |
12829 | 12912 | ||
12830 | if (presence.Appearance.AvatarHeight != 127.0f) | ||
12831 | pos += new Vector3(0f, 0f, (presence.Appearance.AvatarHeight/6f)); | ||
12832 | else | ||
12833 | pos += new Vector3(0f, 0f, (1.56f/6f)); | ||
12834 | |||
12835 | presence.AbsolutePosition = pos; | ||
12836 | |||
12837 | // attach a suitable collision plane regardless of the actual situation to force the LLClient to land. | ||
12838 | // Collision plane below the avatar's position a 6th of the avatar's height is suitable. | ||
12839 | // Mind you, that this method doesn't get called if the avatar's velocity magnitude is greater then a | ||
12840 | // certain amount.. because the LLClient wouldn't land in that situation anyway. | ||
12841 | |||
12842 | // why are we still testing for this really old height value default??? | ||
12843 | if (presence.Appearance.AvatarHeight != 127.0f) | ||
12844 | presence.CollisionPlane = new Vector4(0, 0, 0, pos.Z - presence.Appearance.AvatarHeight/6f); | ||
12845 | else | ||
12846 | presence.CollisionPlane = new Vector4(0, 0, 0, pos.Z - (1.56f/6f)); | ||
12847 | |||
12848 | |||
12849 | ImprovedTerseObjectUpdatePacket.ObjectDataBlock block = | 12913 | ImprovedTerseObjectUpdatePacket.ObjectDataBlock block = |
12850 | CreateImprovedTerseBlock(p, false); | 12914 | CreateImprovedTerseBlock(p, false); |
12851 | 12915 | ||