diff --git a/OpenSim/Capabilities/Handlers/GetTexture/GetTextureHandler.cs b/OpenSim/Capabilities/Handlers/GetTexture/GetTextureHandler.cs index c275d87c34..6437d0b7b0 100644 --- a/OpenSim/Capabilities/Handlers/GetTexture/GetTextureHandler.cs +++ b/OpenSim/Capabilities/Handlers/GetTexture/GetTextureHandler.cs @@ -68,7 +68,7 @@ namespace OpenSim.Capabilities.Handlers ret["content_type"] = "text/plain"; ret["keepalive"] = false; ret["reusecontext"] = false; - + ret["int_bytes"] = 0; string textureStr = (string)request["texture_id"]; string format = (string)request["format"]; @@ -237,6 +237,7 @@ namespace OpenSim.Capabilities.Handlers { response["int_response_code"] = (int)System.Net.HttpStatusCode.OK; response["bin_response_data"] = texture.Data; + response["int_bytes"] = texture.Data.Length; } else { @@ -246,6 +247,7 @@ namespace OpenSim.Capabilities.Handlers byte[] d = new byte[len]; Array.Copy(texture.Data, start, d, 0, len); response["bin_response_data"] = d; + response["int_bytes"] = len; } // response.Body.Write(texture.Data, start, len); } @@ -266,6 +268,8 @@ namespace OpenSim.Capabilities.Handlers response["content_type"] = "image/" + format; response["bin_response_data"] = texture.Data; + response["int_bytes"] = texture.Data.Length; + // response.Body.Write(texture.Data, 0, texture.Data.Length); } diff --git a/OpenSim/Framework/IClientAPI.cs b/OpenSim/Framework/IClientAPI.cs index a1df6378e8..1c6685aec6 100644 --- a/OpenSim/Framework/IClientAPI.cs +++ b/OpenSim/Framework/IClientAPI.cs @@ -1053,7 +1053,7 @@ namespace OpenSim.Framework event MuteListEntryRemove OnRemoveMuteListEntry; event GodlikeMessage onGodlikeMessage; event GodUpdateRegionInfoUpdate OnGodUpdateRegionInfoUpdate; - + event GenericCall2 OnUpdateThrottles; /// /// Set the debug level at which packet output should be printed to console. /// diff --git a/OpenSim/Region/ClientStack/Linden/Caps/GetTextureModule.cs b/OpenSim/Region/ClientStack/Linden/Caps/GetTextureModule.cs index 24a019016f..d1ec4aa5aa 100644 --- a/OpenSim/Region/ClientStack/Linden/Caps/GetTextureModule.cs +++ b/OpenSim/Region/ClientStack/Linden/Caps/GetTextureModule.cs @@ -58,6 +58,13 @@ namespace OpenSim.Region.ClientStack.Linden public Hashtable request; } + public class aPollResponse + { + public Hashtable response; + public int bytes; + } + + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private Scene m_scene; @@ -72,6 +79,8 @@ namespace OpenSim.Region.ClientStack.Linden private static OpenMetaverse.BlockingQueue m_queue = new OpenMetaverse.BlockingQueue(); + private Dictionary m_pollservices = new Dictionary(); + #region ISharedRegionModule Members public void Initialise(IConfigSource source) @@ -88,6 +97,7 @@ namespace OpenSim.Region.ClientStack.Linden { m_scene.EventManager.OnRegisterCaps -= RegisterCaps; m_scene.EventManager.OnDeregisterCaps -= DeregisterCaps; + m_scene.EventManager.OnThrottleUpdate -= ThrottleUpdate; m_scene = null; } @@ -98,6 +108,7 @@ namespace OpenSim.Region.ClientStack.Linden m_scene.EventManager.OnRegisterCaps += RegisterCaps; m_scene.EventManager.OnDeregisterCaps += DeregisterCaps; + m_scene.EventManager.OnThrottleUpdate += ThrottleUpdate; if (m_workerThreads == null) { @@ -115,6 +126,56 @@ namespace OpenSim.Region.ClientStack.Linden } } } + private int ExtractImageThrottle(byte[] pthrottles) + { + + byte[] adjData; + int pos = 0; + + if (!BitConverter.IsLittleEndian) + { + byte[] newData = new byte[7 * 4]; + Buffer.BlockCopy(pthrottles, 0, newData, 0, 7 * 4); + + for (int i = 0; i < 7; i++) + Array.Reverse(newData, i * 4, 4); + + adjData = newData; + } + else + { + adjData = pthrottles; + } + + // 0.125f converts from bits to bytes + //int resend = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + //pos += 4; + // int land = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + //pos += 4; + // int wind = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + // pos += 4; + // int cloud = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + // pos += 4; + // int task = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + // pos += 4; + pos = pos + 20; + int texture = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); //pos += 4; + //int asset = (int)(BitConverter.ToSingle(adjData, pos) * 0.125f); + return texture; + } + + // Now we know when the throttle is changed by the client in the case of a root agent or by a neighbor region in the case of a child agent. + public void ThrottleUpdate(ScenePresence p) + { + byte[] throttles = p.ControllingClient.GetThrottlesPacked(1); + UUID user = p.UUID; + int imagethrottle = ExtractImageThrottle(throttles); + PollServiceTextureEventArgs args; + if (m_pollservices.TryGetValue(user,out args)) + { + args.UpdateThrottle(imagethrottle); + } + } public void PostInitialise() { @@ -142,20 +203,25 @@ namespace OpenSim.Region.ClientStack.Linden { private List requests = new List(); - private Dictionary responses = - new Dictionary(); + private Dictionary responses = + new Dictionary(); private Scene m_scene; - + private CapsDataThrottler m_throttler = new CapsDataThrottler(100000, 1400000,10000); public PollServiceTextureEventArgs(UUID pId, Scene scene) : base(null, null, null, null, pId, int.MaxValue) { m_scene = scene; - + // x is request id, y is userid HasEvents = (x, y) => { lock (responses) - return responses.ContainsKey(x); + { + bool ret = m_throttler.hasEvents(x, responses); + m_throttler.ProcessTime(); + return ret; + + } }; GetEvents = (x, y) => { @@ -163,7 +229,7 @@ namespace OpenSim.Region.ClientStack.Linden { try { - return responses[x]; + return responses[x].response; } finally { @@ -171,14 +237,14 @@ namespace OpenSim.Region.ClientStack.Linden } } }; - + // x is request id, y is request data hashtable Request = (x, y) => { aPollRequest reqinfo = new aPollRequest(); reqinfo.thepoll = this; reqinfo.reqID = x; reqinfo.request = y; - + m_queue.Enqueue(reqinfo); }; @@ -220,16 +286,29 @@ namespace OpenSim.Region.ClientStack.Linden response["content_type"] = "text/plain"; response["keepalive"] = false; response["reusecontext"] = false; - + lock (responses) - responses[requestID] = response; + responses[requestID] = new aPollResponse() {bytes = 0, response = response}; return; } - + response = m_getTextureHandler.Handle(requestinfo.request); lock (responses) - responses[requestID] = response; + { + responses[requestID] = new aPollResponse() + { + bytes = (int) response["int_bytes"], + response = response + }; + + } + m_throttler.ProcessTime(); + } + + internal void UpdateThrottle(int pimagethrottle) + { + m_throttler.ThrottleBytes = pimagethrottle; } } @@ -254,19 +333,23 @@ namespace OpenSim.Region.ClientStack.Linden protocol = "https"; } caps.RegisterHandler("GetTexture", String.Format("{0}://{1}:{2}{3}", protocol, hostName, port, capUrl)); - + m_pollservices.Add(agentID, args); m_capsDict[agentID] = capUrl; } private void DeregisterCaps(UUID agentID, Caps caps) { string capUrl; - + PollServiceTextureEventArgs args; if (m_capsDict.TryGetValue(agentID, out capUrl)) { MainServer.Instance.RemoveHTTPHandler("", capUrl); m_capsDict.Remove(agentID); } + if (m_pollservices.TryGetValue(agentID, out args)) + { + m_pollservices.Remove(agentID); + } } private void DoTextureRequests() @@ -279,4 +362,79 @@ namespace OpenSim.Region.ClientStack.Linden } } } + + internal sealed class CapsDataThrottler + { + + private volatile int currenttime = 0; + private volatile int lastTimeElapsed = 0; + private volatile int BytesSent = 0; + private int oversizedImages = 0; + public CapsDataThrottler(int pBytes, int max, int min) + { + ThrottleBytes = pBytes; + lastTimeElapsed = Util.EnvironmentTickCount(); + } + public bool hasEvents(UUID key, Dictionary responses) + { + PassTime(); + // Note, this is called IN LOCK + bool haskey = responses.ContainsKey(key); + if (!haskey) + { + return false; + } + GetTextureModule.aPollResponse response; + if (responses.TryGetValue(key,out response)) + { + + // Normal + if (BytesSent + response.bytes <= ThrottleBytes) + { + BytesSent += response.bytes; + //TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + 1000, unlockyn = false }; + //m_actions.Add(timeBasedAction); + return true; + } + // Big textures + else if (response.bytes > ThrottleBytes && oversizedImages <= ((ThrottleBytes%50000) + 1)) + { + Interlocked.Increment(ref oversizedImages); + BytesSent += response.bytes; + //TimeBasedAction timeBasedAction = new TimeBasedAction { byteRemoval = response.bytes, requestId = key, timeMS = currenttime + (((response.bytes % ThrottleBytes)+1)*1000) , unlockyn = false }; + //m_actions.Add(timeBasedAction); + return true; + } + else + { + return false; + } + } + + return haskey; + } + public void ProcessTime() + { + PassTime(); + } + + + private void PassTime() + { + currenttime = Util.EnvironmentTickCount(); + int timeElapsed = Util.EnvironmentTickCountSubtract(currenttime, lastTimeElapsed); + //processTimeBasedActions(responses); + if (Util.EnvironmentTickCountSubtract(currenttime, timeElapsed) >= 1000) + { + lastTimeElapsed = Util.EnvironmentTickCount(); + BytesSent -= ThrottleBytes; + if (BytesSent < 0) BytesSent = 0; + if (BytesSent < ThrottleBytes) + { + oversizedImages = 0; + } + } + } + public int ThrottleBytes; + } } diff --git a/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs b/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs index 81167ece84..5551f510ba 100644 --- a/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs +++ b/OpenSim/Region/ClientStack/Linden/UDP/LLClientView.cs @@ -296,6 +296,7 @@ namespace OpenSim.Region.ClientStack.LindenUDP public event MuteListEntryRemove OnRemoveMuteListEntry; public event GodlikeMessage onGodlikeMessage; public event GodUpdateRegionInfoUpdate OnGodUpdateRegionInfoUpdate; + public event GenericCall2 OnUpdateThrottles; #endregion Events @@ -6753,6 +6754,11 @@ namespace OpenSim.Region.ClientStack.LindenUDP #endregion m_udpClient.SetThrottles(atpack.Throttle.Throttles); + GenericCall2 handler = OnUpdateThrottles; + if (handler != null) + { + handler(); + } return true; } @@ -11906,6 +11912,11 @@ namespace OpenSim.Region.ClientStack.LindenUDP public void SetChildAgentThrottle(byte[] throttles) { m_udpClient.SetThrottles(throttles); + GenericCall2 handler = OnUpdateThrottles; + if (handler != null) + { + handler(); + } } /// diff --git a/OpenSim/Region/CoreModules/Agent/AssetTransaction/AssetXferUploader.cs b/OpenSim/Region/CoreModules/Agent/AssetTransaction/AssetXferUploader.cs index f6dd5af2d3..0aa46931fb 100644 --- a/OpenSim/Region/CoreModules/Agent/AssetTransaction/AssetXferUploader.cs +++ b/OpenSim/Region/CoreModules/Agent/AssetTransaction/AssetXferUploader.cs @@ -136,18 +136,27 @@ namespace OpenSim.Region.CoreModules.Agent.AssetTransaction if (XferID == xferID) { - if (m_asset.Data.Length > 1) + lock (this) { - byte[] destinationArray = new byte[m_asset.Data.Length + data.Length]; - Array.Copy(m_asset.Data, 0, destinationArray, 0, m_asset.Data.Length); - Array.Copy(data, 0, destinationArray, m_asset.Data.Length, data.Length); - m_asset.Data = destinationArray; - } - else - { - byte[] buffer2 = new byte[data.Length - 4]; - Array.Copy(data, 4, buffer2, 0, data.Length - 4); - m_asset.Data = buffer2; + int assetLength = m_asset.Data.Length; + int dataLength = data.Length; + + if (m_asset.Data.Length > 1) + { + byte[] destinationArray = new byte[assetLength + dataLength]; + Array.Copy(m_asset.Data, 0, destinationArray, 0, assetLength); + Array.Copy(data, 0, destinationArray, assetLength, dataLength); + m_asset.Data = destinationArray; + } + else + { + if (dataLength > 4) + { + byte[] buffer2 = new byte[dataLength - 4]; + Array.Copy(data, 4, buffer2, 0, dataLength - 4); + m_asset.Data = buffer2; + } + } } ourClient.SendConfirmXfer(xferID, packetID); diff --git a/OpenSim/Region/Framework/Scenes/EventManager.cs b/OpenSim/Region/Framework/Scenes/EventManager.cs index 8691b91d3f..5b1c9f4a1c 100644 --- a/OpenSim/Region/Framework/Scenes/EventManager.cs +++ b/OpenSim/Region/Framework/Scenes/EventManager.cs @@ -816,6 +816,10 @@ namespace OpenSim.Region.Framework.Scenes public event ParcelPrimCountTainted OnParcelPrimCountTainted; public event GetScriptRunning OnGetScriptRunning; + public delegate void ThrottleUpdate(ScenePresence scenePresence); + + public event ThrottleUpdate OnThrottleUpdate; + /// /// RegisterCapsEvent is called by Scene after the Caps object /// has been instantiated and before it is return to the @@ -3130,5 +3134,14 @@ namespace OpenSim.Region.Framework.Scenes } } } + + public void TriggerThrottleUpdate(ScenePresence scenePresence) + { + ThrottleUpdate handler = OnThrottleUpdate; + if (handler != null) + { + handler(scenePresence); + } + } } } diff --git a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs index 74d2629489..b9f9c86cd1 100644 --- a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs +++ b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs @@ -112,7 +112,7 @@ namespace OpenSim.Region.Framework.Scenes private long timeLastChanged = 0; private long m_maxPersistTime = 0; private long m_minPersistTime = 0; - private Random m_rand; +// private Random m_rand; private List m_linkedAvatars = new List(); /// @@ -130,6 +130,7 @@ namespace OpenSim.Region.Framework.Scenes { if (value) { + if (m_isBackedUp) { m_scene.SceneGraph.FireChangeBackup(this); @@ -139,19 +140,21 @@ namespace OpenSim.Region.Framework.Scenes timeFirstChanged = DateTime.Now.Ticks; if (m_rootPart != null && m_rootPart.UUID != null && m_scene != null) { +/* if (m_rand == null) { byte[] val = new byte[16]; m_rootPart.UUID.ToBytes(val, 0); m_rand = new Random(BitConverter.ToInt32(val, 0)); } - + */ if (m_scene.GetRootAgentCount() == 0) { //If the region is empty, this change has been made by an automated process //and thus we delay the persist time by a random amount between 1.5 and 2.5. - float factor = 1.5f + (float)(m_rand.NextDouble()); +// float factor = 1.5f + (float)(m_rand.NextDouble()); + float factor = 2.0f; m_maxPersistTime = (long)((float)m_scene.m_persistAfter * factor); m_minPersistTime = (long)((float)m_scene.m_dontPersistBefore * factor); } @@ -159,8 +162,10 @@ namespace OpenSim.Region.Framework.Scenes { //If the region is not empty, we want to obey the minimum and maximum persist times //but add a random factor so we stagger the object persistance a little - m_maxPersistTime = (long)((float)m_scene.m_persistAfter * (1.0d - (m_rand.NextDouble() / 5.0d))); //Multiply by 1.0-1.5 - m_minPersistTime = (long)((float)m_scene.m_dontPersistBefore * (1.0d + (m_rand.NextDouble() / 2.0d))); //Multiply by 0.8-1.0 +// m_maxPersistTime = (long)((float)m_scene.m_persistAfter * (1.0d - (m_rand.NextDouble() / 5.0d))); //Multiply by 1.0-1.5 +// m_minPersistTime = (long)((float)m_scene.m_dontPersistBefore * (1.0d + (m_rand.NextDouble() / 2.0d))); //Multiply by 0.8-1.0 + m_maxPersistTime = m_scene.m_persistAfter; + m_minPersistTime = m_scene.m_dontPersistBefore; } } } diff --git a/OpenSim/Region/Framework/Scenes/ScenePresence.cs b/OpenSim/Region/Framework/Scenes/ScenePresence.cs index 2c18397301..25a53b4142 100644 --- a/OpenSim/Region/Framework/Scenes/ScenePresence.cs +++ b/OpenSim/Region/Framework/Scenes/ScenePresence.cs @@ -794,6 +794,7 @@ namespace OpenSim.Region.Framework.Scenes ControllingClient.OnChangeAnim += avnHandleChangeAnim; ControllingClient.OnForceReleaseControls += HandleForceReleaseControls; ControllingClient.OnAutoPilotGo += MoveToTarget; + ControllingClient.OnUpdateThrottles += RaiseUpdateThrottles; // ControllingClient.OnChildAgentStatus += new StatusChange(this.ChildStatusChange); // ControllingClient.OnStopMovement += new GenericCall2(this.StopMovement); @@ -3174,6 +3175,10 @@ namespace OpenSim.Region.Framework.Scenes } private static Vector3 marker = new Vector3(-1f, -1f, -1f); + private void RaiseUpdateThrottles() + { + m_scene.EventManager.TriggerThrottleUpdate(this); + } /// /// This updates important decision making data about a child agent /// The main purpose is to figure out what objects to send to a child agent that's in a neighboring region diff --git a/OpenSim/Region/OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs b/OpenSim/Region/OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs index c363fd33be..254eeb4d89 100644 --- a/OpenSim/Region/OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs +++ b/OpenSim/Region/OptionalModules/Agent/InternetRelayClientView/Server/IRCClientView.cs @@ -873,6 +873,7 @@ namespace OpenSim.Region.OptionalModules.Agent.InternetRelayClientView.Server public event MuteListEntryRemove OnRemoveMuteListEntry; public event GodlikeMessage onGodlikeMessage; public event GodUpdateRegionInfoUpdate OnGodUpdateRegionInfoUpdate; + public event GenericCall2 OnUpdateThrottles; #pragma warning restore 67 diff --git a/OpenSim/Region/OptionalModules/PrimLimitsModule/PrimLimitsModule.cs b/OpenSim/Region/OptionalModules/PrimLimitsModule/PrimLimitsModule.cs index 59ff9b8a19..39cabb52b6 100644 --- a/OpenSim/Region/OptionalModules/PrimLimitsModule/PrimLimitsModule.cs +++ b/OpenSim/Region/OptionalModules/PrimLimitsModule/PrimLimitsModule.cs @@ -121,34 +121,40 @@ namespace OpenSim.Region.OptionalModules private bool CanObjectEnter(UUID objectID, bool enteringRegion, Vector3 newPoint, Scene scene) { - if ((newPoint.X > 257f || newPoint.X < -1f || newPoint.Y > 257f || newPoint.Y < -1f)) + if (newPoint.X < -1f || newPoint.X > (float)(Constants.RegionSize + 1) || + newPoint.Y < -1f || newPoint.Y > (float)(Constants.RegionSize + 1)) return true; SceneObjectPart obj = scene.GetSceneObjectPart(objectID); - Vector3 oldPoint = obj.GroupPosition; - int objectCount = obj.ParentGroup.PrimCount; - ILandObject oldParcel = scene.LandChannel.GetLandObject(oldPoint.X, oldPoint.Y); + + if (obj == null) + return false; + + // Prim counts are determined by the location of the root prim. if we're + // moving a child prim, just let it pass + if (!obj.IsRoot) + { + return true; + } + ILandObject newParcel = scene.LandChannel.GetLandObject(newPoint.X, newPoint.Y); if (newParcel == null) return true; - int usedPrims = newParcel.PrimCounts.Total; - int simulatorCapacity = newParcel.GetSimulatorMaxPrimCount(); + Vector3 oldPoint = obj.GroupPosition; + ILandObject oldParcel = scene.LandChannel.GetLandObject(oldPoint.X, oldPoint.Y); // The prim hasn't crossed a region boundry so we don't need to worry // about prim counts here - if(oldParcel.Equals(newParcel)) + if(oldParcel != null && oldParcel.Equals(newParcel)) { return true; } - // Prim counts are determined by the location of the root prim. if we're - // moving a child prim, just let it pass - if(!obj.IsRoot) - { - return true; - } + int objectCount = obj.ParentGroup.PrimCount; + int usedPrims = newParcel.PrimCounts.Total; + int simulatorCapacity = newParcel.GetSimulatorMaxPrimCount(); // TODO: Add Special Case here for temporary prims diff --git a/OpenSim/Region/OptionalModules/World/NPC/NPCAvatar.cs b/OpenSim/Region/OptionalModules/World/NPC/NPCAvatar.cs index 5471afaf8f..3a03101751 100644 --- a/OpenSim/Region/OptionalModules/World/NPC/NPCAvatar.cs +++ b/OpenSim/Region/OptionalModules/World/NPC/NPCAvatar.cs @@ -473,7 +473,7 @@ namespace OpenSim.Region.OptionalModules.World.NPC public event MuteListEntryRemove OnRemoveMuteListEntry; public event GodlikeMessage onGodlikeMessage; public event GodUpdateRegionInfoUpdate OnGodUpdateRegionInfoUpdate; - + public event GenericCall2 OnUpdateThrottles; #pragma warning restore 67 #endregion diff --git a/OpenSim/Region/Physics/UbitOdePlugin/ODEPrim.cs b/OpenSim/Region/Physics/UbitOdePlugin/ODEPrim.cs index 76e42d4417..dc247a9fa2 100644 --- a/OpenSim/Region/Physics/UbitOdePlugin/ODEPrim.cs +++ b/OpenSim/Region/Physics/UbitOdePlugin/ODEPrim.cs @@ -1403,8 +1403,8 @@ namespace OpenSim.Region.Physics.OdePlugin if (vertexCount == 0 || indexCount == 0) { - m_log.WarnFormat("[PHYSICS]: Invalid mesh data on OdePrim {0}, mesh {1}", - Name, _pbs.SculptEntry ? _pbs.SculptTexture.ToString() : "primMesh"); + m_log.WarnFormat("[PHYSICS]: Invalid mesh data on OdePrim {0}, mesh {1} at {2}", + Name, _pbs.SculptEntry ? _pbs.SculptTexture.ToString() : "primMesh",_position.ToString()); m_hasOBB = false; m_OBBOffset = Vector3.Zero; diff --git a/OpenSim/Tests/Common/Mock/TestClient.cs b/OpenSim/Tests/Common/Mock/TestClient.cs index ebdfb4f95e..5ad3c9f635 100644 --- a/OpenSim/Tests/Common/Mock/TestClient.cs +++ b/OpenSim/Tests/Common/Mock/TestClient.cs @@ -320,7 +320,7 @@ namespace OpenSim.Tests.Common.Mock public event MuteListEntryRemove OnRemoveMuteListEntry; public event GodlikeMessage onGodlikeMessage; public event GodUpdateRegionInfoUpdate OnGodUpdateRegionInfoUpdate; - + public event GenericCall2 OnUpdateThrottles; #pragma warning restore 67 ///