From f0dd115a8c5c44ebde4631df3469c6e39510325f Mon Sep 17 00:00:00 2001 From: MW Date: Thu, 26 Jul 2007 17:41:31 +0000 Subject: More work on inventory and opensim library. Fixed a number of bugs in the AssetCache related to asset downloading. --- .../Framework/Communications/Cache/AssetCache.cs | 794 ++++++++++++--------- .../Communications/Cache/LibraryRootFolder.cs | 62 +- .../Communications/Cache/UserProfileCache.cs | 5 + OpenSim/Framework/General/Interfaces/IClientAPI.cs | 5 + OpenSim/Framework/General/NullClientAPI.cs | 5 + OpenSim/Framework/General/Types/AgentWearable.cs | 5 +- OpenSim/Framework/UserManager/UserManagerBase.cs | 8 + OpenSim/Region/ClientStack/ClientView.API.cs | 8 + .../Region/Examples/SimpleApp/MyNpcCharacter.cs | 5 + .../GridInterfaces/Local/LocalAssetServer.cs | 30 + bin/assets/base_skin.dat | 52 ++ bin/assets/welcomeNote.dat | 9 + 12 files changed, 640 insertions(+), 348 deletions(-) create mode 100644 bin/assets/base_skin.dat create mode 100644 bin/assets/welcomeNote.dat diff --git a/OpenSim/Framework/Communications/Cache/AssetCache.cs b/OpenSim/Framework/Communications/Cache/AssetCache.cs index a3480ec..32ba830 100644 --- a/OpenSim/Framework/Communications/Cache/AssetCache.cs +++ b/OpenSim/Framework/Communications/Cache/AssetCache.cs @@ -25,537 +25,657 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ + using System; -using System.Collections; using System.Collections.Generic; -using System.Text; using System.Reflection; using System.Threading; using libsecondlife; using libsecondlife.Packets; using OpenSim.Framework.Interfaces; -using OpenSim.Framework.Servers; using OpenSim.Framework.Types; -using OpenSim.Framework.Utilities; namespace OpenSim.Framework.Communications.Caches { public delegate void DownloadComplete(AssetCache.TextureSender sender); + /// + /// Manages local cache of assets and their sending to viewers. + /// public class AssetCache : IAssetReceiver { - // Fields - private Thread _assetCacheThread; - private IAssetServer _assetServer; - public List AssetRequests; public Dictionary Assets; - public Dictionary RequestedAssets; - public Dictionary RequestedTextures; - public Dictionary SendingTextures; - private LLUUID[] textureList; - public List TextureRequests; public Dictionary Textures; - // Methods + public List AssetRequests = new List(); //assets ready to be sent to viewers + public List TextureRequests = new List(); //textures ready to be sent + + public Dictionary RequestedAssets = new Dictionary(); //Assets requested from the asset server + public Dictionary RequestedTextures = new Dictionary(); //Textures requested from the asset server + + public Dictionary SendingTextures = new Dictionary(); + private IAssetServer _assetServer; + private Thread _assetCacheThread; + + /// + /// + /// public AssetCache(IAssetServer assetServer) { - this.AssetRequests = new List(); - this.TextureRequests = new List(); - this.RequestedAssets = new Dictionary(); - this.RequestedTextures = new Dictionary(); - this.SendingTextures = new Dictionary(); - this.textureList = new LLUUID[5]; Console.WriteLine("Creating Asset cache"); - this._assetServer = assetServer; - this._assetServer.SetReceiver(this); - this.Assets = new Dictionary(); - this.Textures = new Dictionary(); - this._assetCacheThread = new Thread(new ThreadStart(this.RunAssetManager)); + _assetServer = assetServer; + _assetServer.SetReceiver(this); + Assets = new Dictionary(); + Textures = new Dictionary(); + this._assetCacheThread = new Thread(new ThreadStart(RunAssetManager)); this._assetCacheThread.IsBackground = true; this._assetCacheThread.Start(); + } public AssetCache(string assetServerDLLName, string assetServerURL, string assetServerKey) { - this.AssetRequests = new List(); - this.TextureRequests = new List(); - this.RequestedAssets = new Dictionary(); - this.RequestedTextures = new Dictionary(); - this.SendingTextures = new Dictionary(); - this.textureList = new LLUUID[5]; Console.WriteLine("Creating Asset cache"); - this._assetServer = this.LoadAssetDll(assetServerDLLName); - this._assetServer.SetServerInfo(assetServerURL, assetServerKey); - this._assetServer.SetReceiver(this); - this.Assets = new Dictionary(); - this.Textures = new Dictionary(); - this._assetCacheThread = new Thread(new ThreadStart(this.RunAssetManager)); + _assetServer = this.LoadAssetDll(assetServerDLLName); + _assetServer.SetServerInfo(assetServerURL, assetServerKey); + _assetServer.SetReceiver(this); + Assets = new Dictionary(); + Textures = new Dictionary(); + this._assetCacheThread = new Thread(new ThreadStart(RunAssetManager)); this._assetCacheThread.IsBackground = true; this._assetCacheThread.Start(); + } - public void AddAsset(AssetBase asset) + /// + /// + /// + public void RunAssetManager() { - if (asset.Type == 0) + while (true) { - if (!this.Textures.ContainsKey(asset.FullID)) + try { - TextureImage image = new TextureImage(asset); - this.Textures.Add(image.FullID, image); - this._assetServer.UploadNewAsset(asset); + this.ProcessAssetQueue(); + this.ProcessTextureQueue(); + Thread.Sleep(500); + } + catch (Exception e) + { + Console.WriteLine(e.Message + " : " + e.StackTrace); } } - else if (!this.Assets.ContainsKey(asset.FullID)) + } + + + public AssetBase GetAsset(LLUUID assetID) + { + AssetBase asset = null; + if (this.Textures.ContainsKey(assetID)) + { + asset = this.Textures[assetID]; + } + else if (this.Assets.ContainsKey(assetID)) { - AssetInfo info = new AssetInfo(asset); - this.Assets.Add(info.FullID, info); - this._assetServer.UploadNewAsset(asset); + asset = this.Assets[assetID]; } + return asset; } - public void AddAssetRequest(IClientAPI userInfo, TransferRequestPacket transferRequest) + public void AddAsset(AssetBase asset) { - LLUUID assetID = new LLUUID(transferRequest.TransferInfo.Params, 0); - if (!this.Assets.ContainsKey(assetID)) + // Console.WriteLine("adding asset " + asset.FullID.ToStringHyphenated()); + if (asset.Type == 0) { - if (!this.RequestedAssets.ContainsKey(assetID)) - { - AssetRequest request = new AssetRequest(); - request.RequestUser = userInfo; - request.RequestAssetID = assetID; - request.TransferRequestID = transferRequest.TransferInfo.TransferID; - this.RequestedAssets.Add(assetID, request); - this._assetServer.RequestAsset(assetID, false); + //Console.WriteLine("which is a texture"); + if (!this.Textures.ContainsKey(asset.FullID)) + { //texture + TextureImage textur = new TextureImage(asset); + this.Textures.Add(textur.FullID, textur); + this._assetServer.UploadNewAsset(asset); } } else { - AssetInfo info = this.Assets[assetID]; - AssetRequest request2 = new AssetRequest(); - request2.RequestUser = userInfo; - request2.RequestAssetID = assetID; - request2.TransferRequestID = transferRequest.TransferInfo.TransferID; - request2.AssetInf = info; - if (info.Data.LongLength > 600) - { - request2.NumPackets = 1 + (((info.Data.Length - 600) + 0x3e7) / 0x3e8); - } - else + if (!this.Assets.ContainsKey(asset.FullID)) { - request2.NumPackets = 1; + AssetInfo assetInf = new AssetInfo(asset); + this.Assets.Add(assetInf.FullID, assetInf); + this._assetServer.UploadNewAsset(asset); } - this.AssetRequests.Add(request2); } } - public void AddTextureRequest(IClientAPI userInfo, LLUUID imageID) + /// + /// + /// + private void ProcessTextureQueue() { - if (!this.Textures.ContainsKey(imageID)) + if (this.TextureRequests.Count == 0) { - if (!this.RequestedTextures.ContainsKey(imageID)) - { - AssetRequest request = new AssetRequest(); - request.RequestUser = userInfo; - request.RequestAssetID = imageID; - request.IsTextureRequest = true; - this.RequestedTextures.Add(imageID, request); - this._assetServer.RequestAsset(imageID, true); - } + //no requests waiting + return; } - else + int num; + num = this.TextureRequests.Count; + + AssetRequest req; + for (int i = 0; i < num; i++) { - TextureImage image = this.Textures[imageID]; - AssetRequest request2 = new AssetRequest(); - request2.RequestUser = userInfo; - request2.RequestAssetID = imageID; - request2.IsTextureRequest = true; - request2.ImageInfo = image; - if (image.Data.LongLength > 600) + req = (AssetRequest)this.TextureRequests[i]; + if (!this.SendingTextures.ContainsKey(req.ImageInfo.FullID)) { - request2.NumPackets = 1 + (((image.Data.Length - 600) + 0x3e7) / 0x3e8); - } - else - { - request2.NumPackets = 1; + TextureSender sender = new TextureSender(req); + sender.OnComplete += this.TextureSent; + lock (this.SendingTextures) + { + this.SendingTextures.Add(req.ImageInfo.FullID, sender); + } } - this.TextureRequests.Add(request2); + } + + this.TextureRequests.Clear(); } - public void AssetNotFound(AssetBase asset) + /// + /// Event handler, called by a TextureSender object to say that texture has been sent + /// + /// + public void TextureSent(TextureSender sender) { + if (this.SendingTextures.ContainsKey(sender.request.ImageInfo.FullID)) + { + lock (this.SendingTextures) + { + this.SendingTextures.Remove(sender.request.ImageInfo.FullID); + } + } } public void AssetReceived(AssetBase asset, bool IsTexture) { - if (asset.FullID != LLUUID.Zero) + if (asset.FullID != LLUUID.Zero) // if it is set to zero then the asset wasn't found by the server { + //check if it is a texture or not + //then add to the correct cache list + //then check for waiting requests for this asset/texture (in the Requested lists) + //and move those requests into the Requests list. if (IsTexture) { TextureImage image = new TextureImage(asset); this.Textures.Add(image.FullID, image); if (this.RequestedTextures.ContainsKey(image.FullID)) { - AssetRequest request = this.RequestedTextures[image.FullID]; - request.ImageInfo = image; + AssetRequest req = this.RequestedTextures[image.FullID]; + req.ImageInfo = image; if (image.Data.LongLength > 600) { - request.NumPackets = 1 + (((image.Data.Length - 600) + 0x3e7) / 0x3e8); + //over 600 bytes so split up file + req.NumPackets = 1 + (int)(image.Data.Length - 600 + 999) / 1000; } else { - request.NumPackets = 1; + req.NumPackets = 1; } this.RequestedTextures.Remove(image.FullID); - this.TextureRequests.Add(request); + this.TextureRequests.Add(req); } } else { - AssetInfo info = new AssetInfo(asset); - this.Assets.Add(info.FullID, info); - if (this.RequestedAssets.ContainsKey(info.FullID)) + AssetInfo assetInf = new AssetInfo(asset); + this.Assets.Add(assetInf.FullID, assetInf); + if (this.RequestedAssets.ContainsKey(assetInf.FullID)) { - AssetRequest request2 = this.RequestedAssets[info.FullID]; - request2.AssetInf = info; - if (info.Data.LongLength > 600) + AssetRequest req = this.RequestedAssets[assetInf.FullID]; + req.AssetInf = assetInf; + if (assetInf.Data.LongLength > 600) { - request2.NumPackets = 1 + (((info.Data.Length - 600) + 0x3e7) / 0x3e8); + //over 600 bytes so split up file + req.NumPackets = 1 + (int)(assetInf.Data.Length - 600 + 999) / 1000; } else { - request2.NumPackets = 1; + req.NumPackets = 1; } - this.RequestedAssets.Remove(info.FullID); - this.AssetRequests.Add(request2); + this.RequestedAssets.Remove(assetInf.FullID); + this.AssetRequests.Add(req); } } } } - public AssetInfo CloneAsset(LLUUID newOwner, AssetInfo sourceAsset) + public void AssetNotFound(AssetBase asset) { - AssetInfo info = new AssetInfo(); - info.Data = new byte[sourceAsset.Data.Length]; - Array.Copy(sourceAsset.Data, info.Data, sourceAsset.Data.Length); - info.FullID = LLUUID.Random(); - info.Type = sourceAsset.Type; - info.InvType = sourceAsset.InvType; - return info; - } + //the asset server had no knowledge of requested asset - public TextureImage CloneImage(LLUUID newOwner, TextureImage source) - { - TextureImage image = new TextureImage(); - image.Data = new byte[source.Data.Length]; - Array.Copy(source.Data, image.Data, source.Data.Length); - image.FullID = LLUUID.Random(); - image.Name = source.Name; - return image; } - public AssetBase[] CreateNewInventorySet(LLUUID agentID) + #region Assets + /// + /// + /// + /// + /// + public void AddAssetRequest(IClientAPI userInfo, TransferRequestPacket transferRequest) { - AssetBase[] baseArray = new AssetBase[this.textureList.Length]; - for (int i = 0; i < this.textureList.Length; i++) + LLUUID requestID = null; + byte source = 2; + if (transferRequest.TransferInfo.SourceType == 2) + { + //direct asset request + requestID = new LLUUID(transferRequest.TransferInfo.Params, 0); + } + else if (transferRequest.TransferInfo.SourceType == 3) + { + //inventory asset request + requestID = new LLUUID(transferRequest.TransferInfo.Params, 80); + source = 3; + } + //check to see if asset is in local cache, if not we need to request it from asset server. + //Console.WriteLine("asset request " + requestID); + if (!this.Assets.ContainsKey(requestID)) { - if (this.Textures.ContainsKey(this.textureList[i])) + //not found asset + // so request from asset server + if (!this.RequestedAssets.ContainsKey(requestID)) { - baseArray[i] = this.CloneImage(agentID, this.Textures[this.textureList[i]]); - TextureImage asset = new TextureImage(baseArray[i]); - this.Textures.Add(asset.FullID, asset); - this._assetServer.UploadNewAsset(asset); + AssetRequest request = new AssetRequest(); + request.RequestUser = userInfo; + request.RequestAssetID = requestID; + request.TransferRequestID = transferRequest.TransferInfo.TransferID; + request.AssetRequestSource = source; + request.Params = transferRequest.TransferInfo.Params; + this.RequestedAssets.Add(requestID, request); + this._assetServer.RequestAsset(requestID, false); } + return; } - return baseArray; - } + //it is in our cache + AssetInfo asset = this.Assets[requestID]; - public AssetBase GetAsset(LLUUID assetID) - { - AssetBase base2 = null; - if (this.Textures.ContainsKey(assetID)) + //work out how many packets it should be sent in + // and add to the AssetRequests list + AssetRequest req = new AssetRequest(); + req.RequestUser = userInfo; + req.RequestAssetID = requestID; + req.TransferRequestID = transferRequest.TransferInfo.TransferID; + req.AssetRequestSource = source; + req.Params = transferRequest.TransferInfo.Params; + req.AssetInf = asset; + + if (asset.Data.LongLength > 600) { - return this.Textures[assetID]; + //over 600 bytes so split up file + req.NumPackets = 1 + (int)(asset.Data.Length - 600 + 999) / 1000; } - if (this.Assets.ContainsKey(assetID)) + else { - base2 = this.Assets[assetID]; + req.NumPackets = 1; } - return base2; + + this.AssetRequests.Add(req); } - private IAssetServer LoadAssetDll(string dllName) + /// + /// + /// + private void ProcessAssetQueue() { - Assembly assembly = Assembly.LoadFrom(dllName); - IAssetServer assetServer = null; - foreach (Type type in assembly.GetTypes()) + if (this.AssetRequests.Count == 0) { - if (type.IsPublic && !type.IsAbstract) - { - if (type.GetInterface("IAssetPlugin", true) != null) - { - assetServer = ((IAssetPlugin)Activator.CreateInstance(assembly.GetType(type.ToString()))).GetAssetServer(); - break; - } - } + //no requests waiting + return; } - assembly = null; - return assetServer; - } + int num; - public void LoadDefaultTextureSet() - { - this.textureList[0] = new LLUUID("00000000-0000-0000-9999-000000000001"); - this.textureList[1] = new LLUUID("00000000-0000-0000-9999-000000000002"); - this.textureList[2] = new LLUUID("00000000-0000-0000-9999-000000000003"); - this.textureList[3] = new LLUUID("00000000-0000-0000-9999-000000000004"); - this.textureList[4] = new LLUUID("00000000-0000-0000-9999-000000000005"); - for (int i = 0; i < this.textureList.Length; i++) + if (this.AssetRequests.Count < 5) { - this._assetServer.RequestAsset(this.textureList[i], true); + //lower than 5 so do all of them + num = this.AssetRequests.Count; } - } - - private void ProcessAssetQueue() - { - if (this.AssetRequests.Count != 0) + else + { + num = 5; + } + AssetRequest req; + for (int i = 0; i < num; i++) { - int num; - if (this.AssetRequests.Count < 5) + req = (AssetRequest)this.AssetRequests[i]; + //Console.WriteLine("sending asset " + req.RequestAssetID); + TransferInfoPacket Transfer = new TransferInfoPacket(); + Transfer.TransferInfo.ChannelType = 2; + Transfer.TransferInfo.Status = 0; + Transfer.TransferInfo.TargetType = 0; + if (req.AssetRequestSource == 2) { - num = this.AssetRequests.Count; + //Transfer.TransferInfo.Params = req.Params; + Transfer.TransferInfo.Params = new byte[20]; + Array.Copy(req.RequestAssetID.GetBytes(), 0, Transfer.TransferInfo.Params, 0, 16); + int assType = (int)req.AssetInf.Type; + Array.Copy(Helpers.IntToBytes(assType), 0, Transfer.TransferInfo.Params, 16, 4); } - else + else if (req.AssetRequestSource == 3) { - num = 5; + Transfer.TransferInfo.Params = req.Params; + // Transfer.TransferInfo.Params = new byte[100]; + //Array.Copy(req.RequestUser.AgentId.GetBytes(), 0, Transfer.TransferInfo.Params, 0, 16); + //Array.Copy(req.RequestUser.SessionId.GetBytes(), 0, Transfer.TransferInfo.Params, 16, 16); } - for (int i = 0; i < num; i++) + Transfer.TransferInfo.Size = (int)req.AssetInf.Data.Length; + Transfer.TransferInfo.TransferID = req.TransferRequestID; + req.RequestUser.OutPacket(Transfer); + + if (req.NumPackets == 1) { - AssetRequest request = this.AssetRequests[i]; - TransferInfoPacket newPack = new TransferInfoPacket(); - newPack.TransferInfo.ChannelType = 2; - newPack.TransferInfo.Status = 0; - newPack.TransferInfo.TargetType = 0; - newPack.TransferInfo.Params = request.RequestAssetID.GetBytes(); - newPack.TransferInfo.Size = request.AssetInf.Data.Length; - newPack.TransferInfo.TransferID = request.TransferRequestID; - request.RequestUser.OutPacket(newPack); - if (request.NumPackets == 1) + TransferPacketPacket TransferPacket = new TransferPacketPacket(); + TransferPacket.TransferData.Packet = 0; + TransferPacket.TransferData.ChannelType = 2; + TransferPacket.TransferData.TransferID = req.TransferRequestID; + TransferPacket.TransferData.Data = req.AssetInf.Data; + TransferPacket.TransferData.Status = 1; + req.RequestUser.OutPacket(TransferPacket); + } + else + { + //more than one packet so split file up , for now it can't be bigger than 2000 bytes + TransferPacketPacket TransferPacket = new TransferPacketPacket(); + TransferPacket.TransferData.Packet = 0; + TransferPacket.TransferData.ChannelType = 2; + TransferPacket.TransferData.TransferID = req.TransferRequestID; + byte[] chunk = null; + if (req.AssetInf.Data.Length <= 1000) { - TransferPacketPacket packet2 = new TransferPacketPacket(); - packet2.TransferData.Packet = 0; - packet2.TransferData.ChannelType = 2; - packet2.TransferData.TransferID = request.TransferRequestID; - packet2.TransferData.Data = request.AssetInf.Data; - packet2.TransferData.Status = 1; - request.RequestUser.OutPacket(packet2); + chunk = new byte[req.AssetInf.Data.Length]; + Array.Copy(req.AssetInf.Data, chunk, req.AssetInf.Data.Length); } else { - TransferPacketPacket packet3 = new TransferPacketPacket(); - packet3.TransferData.Packet = 0; - packet3.TransferData.ChannelType = 2; - packet3.TransferData.TransferID = request.TransferRequestID; - byte[] destinationArray = new byte[0x3e8]; - Array.Copy(request.AssetInf.Data, destinationArray, 0x3e8); - packet3.TransferData.Data = destinationArray; - packet3.TransferData.Status = 0; - request.RequestUser.OutPacket(packet3); - packet3 = new TransferPacketPacket(); - packet3.TransferData.Packet = 1; - packet3.TransferData.ChannelType = 2; - packet3.TransferData.TransferID = request.TransferRequestID; - byte[] buffer2 = new byte[request.AssetInf.Data.Length - 0x3e8]; - Array.Copy(request.AssetInf.Data, 0x3e8, buffer2, 0, buffer2.Length); - packet3.TransferData.Data = buffer2; - packet3.TransferData.Status = 1; - request.RequestUser.OutPacket(packet3); + chunk = new byte[1000]; + Array.Copy(req.AssetInf.Data, chunk, 1000); + } + + TransferPacket.TransferData.Data = chunk; + TransferPacket.TransferData.Status = 0; + req.RequestUser.OutPacket(TransferPacket); + + if (req.AssetInf.Data.Length > 1000) + { + TransferPacket = new TransferPacketPacket(); + TransferPacket.TransferData.Packet = 1; + TransferPacket.TransferData.ChannelType = 2; + TransferPacket.TransferData.TransferID = req.TransferRequestID; + byte[] chunk1 = new byte[(req.AssetInf.Data.Length - 1000)]; + Array.Copy(req.AssetInf.Data, 1000, chunk1, 0, chunk1.Length); + TransferPacket.TransferData.Data = chunk1; + TransferPacket.TransferData.Status = 1; + req.RequestUser.OutPacket(TransferPacket); } } - for (int j = 0; j < num; j++) - { - this.AssetRequests.RemoveAt(0); - } + } + + //remove requests that have been completed + for (int i = 0; i < num; i++) + { + this.AssetRequests.RemoveAt(0); + } + } - private void ProcessTextureQueue() + public AssetInfo CloneAsset(LLUUID newOwner, AssetInfo sourceAsset) + { + AssetInfo newAsset = new AssetInfo(); + newAsset.Data = new byte[sourceAsset.Data.Length]; + Array.Copy(sourceAsset.Data, newAsset.Data, sourceAsset.Data.Length); + newAsset.FullID = LLUUID.Random(); + newAsset.Type = sourceAsset.Type; + newAsset.InvType = sourceAsset.InvType; + return (newAsset); + } + #endregion + + #region Textures + /// + /// + /// + /// + /// + public void AddTextureRequest(IClientAPI userInfo, LLUUID imageID) { - if (this.TextureRequests.Count != 0) + //Console.WriteLine("texture request for " + imageID.ToStringHyphenated()); + //check to see if texture is in local cache, if not request from asset server + if (!this.Textures.ContainsKey(imageID)) { - int num = this.TextureRequests.Count; - for (int i = 0; i < num; i++) + if (!this.RequestedTextures.ContainsKey(imageID)) { - AssetRequest req = this.TextureRequests[i]; - if (!this.SendingTextures.ContainsKey(req.ImageInfo.FullID)) - { - TextureSender sender = new TextureSender(req); - sender.OnComplete += new DownloadComplete(this.TextureSent); - lock (this.SendingTextures) - { - this.SendingTextures.Add(req.ImageInfo.FullID, sender); - } - } + //not is cache so request from asset server + AssetRequest request = new AssetRequest(); + request.RequestUser = userInfo; + request.RequestAssetID = imageID; + request.IsTextureRequest = true; + this.RequestedTextures.Add(imageID, request); + this._assetServer.RequestAsset(imageID, true); } - this.TextureRequests.Clear(); + return; } - } - public void RunAssetManager() - { - Label_0000: - try + //Console.WriteLine("texture already in cache"); + TextureImage imag = this.Textures[imageID]; + AssetRequest req = new AssetRequest(); + req.RequestUser = userInfo; + req.RequestAssetID = imageID; + req.IsTextureRequest = true; + req.ImageInfo = imag; + + if (imag.Data.LongLength > 600) { - this.ProcessAssetQueue(); - this.ProcessTextureQueue(); - Thread.Sleep(500); - goto Label_0000; + //over 600 bytes so split up file + req.NumPackets = 1 + (int)(imag.Data.Length - 600 + 999) / 1000; } - catch (Exception exception) + else { - Console.WriteLine(exception.Message); - goto Label_0000; + req.NumPackets = 1; } + this.TextureRequests.Add(req); } - public void TextureSent(TextureSender sender) + public TextureImage CloneImage(LLUUID newOwner, TextureImage source) { - if (this.SendingTextures.ContainsKey(sender.request.ImageInfo.FullID)) + TextureImage newImage = new TextureImage(); + newImage.Data = new byte[source.Data.Length]; + Array.Copy(source.Data, newImage.Data, source.Data.Length); + //newImage.filename = source.filename; + newImage.FullID = LLUUID.Random(); + newImage.Name = source.Name; + return (newImage); + } + #endregion + + private IAssetServer LoadAssetDll(string dllName) + { + Assembly pluginAssembly = Assembly.LoadFrom(dllName); + IAssetServer server = null; + + foreach (Type pluginType in pluginAssembly.GetTypes()) { - lock (this.SendingTextures) + if (pluginType.IsPublic) { - this.SendingTextures.Remove(sender.request.ImageInfo.FullID); + if (!pluginType.IsAbstract) + { + Type typeInterface = pluginType.GetInterface("IAssetPlugin", true); + + if (typeInterface != null) + { + IAssetPlugin plug = (IAssetPlugin)Activator.CreateInstance(pluginAssembly.GetType(pluginType.ToString())); + server = plug.GetAssetServer(); + break; + } + + typeInterface = null; + } } } + pluginAssembly = null; + return server; + } + + public class AssetRequest + { + public IClientAPI RequestUser; + public LLUUID RequestAssetID; + public AssetInfo AssetInf; + public TextureImage ImageInfo; + public LLUUID TransferRequestID; + public long DataPointer = 0; + public int NumPackets = 0; + public int PacketCounter = 0; + public bool IsTextureRequest; + public byte AssetRequestSource = 2; + public byte[] Params = null; + //public bool AssetInCache; + //public int TimeRequested; + + public AssetRequest() + { + + } } - // Nested Types public class AssetInfo : AssetBase { - // Methods public AssetInfo() { + } public AssetInfo(AssetBase aBase) { - base.Data = aBase.Data; - base.FullID = aBase.FullID; - base.Type = aBase.Type; - base.InvType = aBase.InvType; - base.Name = aBase.Name; - base.Description = aBase.Description; + Data = aBase.Data; + FullID = aBase.FullID; + Type = aBase.Type; + InvType = aBase.InvType; + Name = aBase.Name; + Description = aBase.Description; } } - public class AssetRequest - { - // Fields - public AssetCache.AssetInfo AssetInf; - public long DataPointer; - public AssetCache.TextureImage ImageInfo; - public bool IsTextureRequest; - public int NumPackets; - public int PacketCounter; - public LLUUID RequestAssetID; - public IClientAPI RequestUser; - public LLUUID TransferRequestID; - } - public class TextureImage : AssetBase { - // Methods public TextureImage() { + } public TextureImage(AssetBase aBase) { - base.Data = aBase.Data; - base.FullID = aBase.FullID; - base.Type = aBase.Type; - base.InvType = aBase.InvType; - base.Name = aBase.Name; - base.Description = aBase.Description; + Data = aBase.Data; + FullID = aBase.FullID; + Type = aBase.Type; + InvType = aBase.InvType; + Name = aBase.Name; + Description = aBase.Description; } } public class TextureSender { - // Fields - private Thread m_thread; - public AssetCache.AssetRequest request; - - // Events + public AssetRequest request; public event DownloadComplete OnComplete; + Thread m_thread; + public TextureSender(AssetRequest req) + { + request = req; + //Console.WriteLine("creating worker thread for texture " + req.ImageInfo.FullID.ToStringHyphenated()); + //Console.WriteLine("texture data length is " + req.ImageInfo.Data.Length); + // Console.WriteLine("in " + req.NumPackets + " packets"); + //ThreadPool.QueueUserWorkItem(new WaitCallback(SendTexture), new object()); - // Methods - public TextureSender(AssetCache.AssetRequest req) + //need some sort of custom threadpool here, as using the .net one, overloads it and stops the handling of incoming packets etc + //but don't really want to create a thread for every texture download + m_thread = new Thread(new ThreadStart(SendTexture)); + m_thread.IsBackground = true; + m_thread.Start(); + } + + public void SendTexture() { - this.request = req; - this.m_thread = new Thread(new ThreadStart(this.SendTexture)); - this.m_thread.IsBackground = true; - this.m_thread.Start(); + //Console.WriteLine("starting to send sending texture " + request.ImageInfo.FullID.ToStringHyphenated()); + while (request.PacketCounter != request.NumPackets) + { + SendPacket(); + Thread.Sleep(500); + } + + //Console.WriteLine("finished sending texture " + request.ImageInfo.FullID.ToStringHyphenated()); + if (OnComplete != null) + { + OnComplete(this); + } } public void SendPacket() { - AssetCache.AssetRequest request = this.request; - if (request.PacketCounter == 0) + AssetRequest req = request; + // Console.WriteLine("sending " + req.ImageInfo.FullID); + + // if (req.ImageInfo.FullID == new LLUUID("00000000-0000-0000-5005-000000000005")) + if (req.PacketCounter == 0) { - if (request.NumPackets == 1) + //first time for this request so send imagedata packet + if (req.NumPackets == 1) { - ImageDataPacket newPack = new ImageDataPacket(); - newPack.ImageID.Packets = 1; - newPack.ImageID.ID = request.ImageInfo.FullID; - newPack.ImageID.Size = (uint)request.ImageInfo.Data.Length; - newPack.ImageData.Data = request.ImageInfo.Data; - newPack.ImageID.Codec = 2; - request.RequestUser.OutPacket(newPack); - request.PacketCounter++; + //only one packet so send whole file + ImageDataPacket im = new ImageDataPacket(); + im.ImageID.Packets = 1; + im.ImageID.ID = req.ImageInfo.FullID; + im.ImageID.Size = (uint)req.ImageInfo.Data.Length; + im.ImageData.Data = req.ImageInfo.Data; + im.ImageID.Codec = 2; + req.RequestUser.OutPacket(im); + req.PacketCounter++; + //req.ImageInfo.l= time; + //System.Console.WriteLine("sent texture: " + req.ImageInfo.FullID); + // Console.WriteLine("sending packet 1 for " + req.ImageInfo.FullID.ToStringHyphenated()); } else { - ImageDataPacket packet2 = new ImageDataPacket(); - packet2.ImageID.Packets = (ushort)request.NumPackets; - packet2.ImageID.ID = request.ImageInfo.FullID; - packet2.ImageID.Size = (uint)request.ImageInfo.Data.Length; - packet2.ImageData.Data = new byte[600]; - Array.Copy(request.ImageInfo.Data, 0, packet2.ImageData.Data, 0, 600); - packet2.ImageID.Codec = 2; - request.RequestUser.OutPacket(packet2); - request.PacketCounter++; + //more than one packet so split file up + ImageDataPacket im = new ImageDataPacket(); + im.ImageID.Packets = (ushort)req.NumPackets; + im.ImageID.ID = req.ImageInfo.FullID; + im.ImageID.Size = (uint)req.ImageInfo.Data.Length; + im.ImageData.Data = new byte[600]; + Array.Copy(req.ImageInfo.Data, 0, im.ImageData.Data, 0, 600); + im.ImageID.Codec = 2; + req.RequestUser.OutPacket(im); + req.PacketCounter++; + //req.ImageInfo.last_used = time; + //System.Console.WriteLine("sent first packet of texture: + // Console.WriteLine("sending packet 1 for " + req.ImageInfo.FullID.ToStringHyphenated()); } } else { - ImagePacketPacket packet3 = new ImagePacketPacket(); - packet3.ImageID.Packet = (ushort)request.PacketCounter; - packet3.ImageID.ID = request.ImageInfo.FullID; - int length = (request.ImageInfo.Data.Length - 600) - (0x3e8 * (request.PacketCounter - 1)); - if (length > 0x3e8) - { - length = 0x3e8; - } - packet3.ImageData.Data = new byte[length]; - Array.Copy(request.ImageInfo.Data, 600 + (0x3e8 * (request.PacketCounter - 1)), packet3.ImageData.Data, 0, length); - request.RequestUser.OutPacket(packet3); - request.PacketCounter++; + //Console.WriteLine("sending packet" + req.PacketCounter + "for " + req.ImageInfo.FullID.ToStringHyphenated()); + //send imagepacket + //more than one packet so split file up + ImagePacketPacket im = new ImagePacketPacket(); + im.ImageID.Packet = (ushort)req.PacketCounter; + im.ImageID.ID = req.ImageInfo.FullID; + int size = req.ImageInfo.Data.Length - 600 - 1000 * (req.PacketCounter - 1); + if (size > 1000) size = 1000; + im.ImageData.Data = new byte[size]; + Array.Copy(req.ImageInfo.Data, 600 + 1000 * (req.PacketCounter - 1), im.ImageData.Data, 0, size); + req.RequestUser.OutPacket(im); + req.PacketCounter++; + //req.ImageInfo.last_used = time; + //System.Console.WriteLine("sent a packet of texture: "+req.image_info.FullID); } - } - public void SendTexture() - { - while (this.request.PacketCounter != this.request.NumPackets) - { - this.SendPacket(); - Thread.Sleep(500); - } - if (this.OnComplete != null) - { - this.OnComplete(this); - } } } } } + diff --git a/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs b/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs index 18803c0..bcfce3f 100644 --- a/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs +++ b/OpenSim/Framework/Communications/Cache/LibraryRootFolder.cs @@ -9,6 +9,7 @@ namespace OpenSim.Framework.Communications.Caches public class LibraryRootFolder : InventoryFolder { private LLUUID libOwner = new LLUUID("11111111-1111-0000-0000-000100bba000"); + private InventoryFolder m_textureFolder; public LibraryRootFolder() { @@ -19,11 +20,22 @@ namespace OpenSim.Framework.Communications.Caches this.type = (short)-1; this.version = (ushort) 1; + InventoryFolder folderInfo = new InventoryFolder(); + folderInfo.agentID = libOwner; + folderInfo.folderID = new LLUUID("00000112-000f-0000-0000-000100bba001"); + folderInfo.name = "My Inventory"; + folderInfo.parentID = this.folderID; + folderInfo.type = -1; + folderInfo.version = 1; + this.SubFolders.Add(folderInfo.folderID, folderInfo); + this.m_textureFolder = folderInfo; + this.CreateLibraryItems(); } private void CreateLibraryItems() { + InventoryItemBase item = new InventoryItemBase(); item.avatarID = libOwner; item.creatorsID = libOwner; @@ -32,12 +44,12 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryDescription = "Plywood texture"; item.inventoryName = "Plywood"; item.type = 0; - item.parentFolderID = this.folderID; + item.parentFolderID = m_textureFolder.folderID; item.inventoryBasePermissions = 0x7FFFFFFF; item.inventoryEveryOnePermissions = 0x7FFFFFFF; item.inventoryCurrentPermissions = 0x7FFFFFFF; item.inventoryNextPermissions = 0x7FFFFFFF; - this.Items.Add(item.inventoryID, item); + this.m_textureFolder.Items.Add(item.inventoryID, item); item = new InventoryItemBase(); item.avatarID = libOwner; @@ -47,12 +59,12 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryDescription = "Rocks texture"; item.inventoryName = "Rocks"; item.type = 0; - item.parentFolderID = this.folderID; + item.parentFolderID = m_textureFolder.folderID; item.inventoryBasePermissions = 0x7FFFFFFF; item.inventoryEveryOnePermissions = 0x7FFFFFFF; item.inventoryCurrentPermissions = 0x7FFFFFFF; item.inventoryNextPermissions = 0x7FFFFFFF; - this.Items.Add(item.inventoryID, item); + this.m_textureFolder.Items.Add(item.inventoryID, item); item = new InventoryItemBase(); item.avatarID = libOwner; @@ -62,12 +74,12 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryDescription = "Bricks texture"; item.inventoryName = "Bricks"; item.type = 0; - item.parentFolderID = this.folderID; + item.parentFolderID = m_textureFolder.folderID; item.inventoryBasePermissions = 0x7FFFFFFF; item.inventoryEveryOnePermissions = 0x7FFFFFFF; item.inventoryCurrentPermissions = 0x7FFFFFFF; item.inventoryNextPermissions = 0x7FFFFFFF; - this.Items.Add(item.inventoryID, item); + this.m_textureFolder.Items.Add(item.inventoryID, item); item = new InventoryItemBase(); item.avatarID = libOwner; @@ -77,12 +89,12 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryDescription = "Granite texture"; item.inventoryName = "Granite"; item.type = 0; - item.parentFolderID = this.folderID; + item.parentFolderID = m_textureFolder.folderID; item.inventoryBasePermissions = 0x7FFFFFFF; item.inventoryEveryOnePermissions = 0x7FFFFFFF; item.inventoryCurrentPermissions = 0x7FFFFFFF; item.inventoryNextPermissions = 0x7FFFFFFF; - this.Items.Add(item.inventoryID, item); + this.m_textureFolder.Items.Add(item.inventoryID, item); item = new InventoryItemBase(); item.avatarID = libOwner; @@ -92,12 +104,12 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryDescription = "Hardwood texture"; item.inventoryName = "Hardwood"; item.type = 0; - item.parentFolderID = this.folderID; + item.parentFolderID = m_textureFolder.folderID; item.inventoryBasePermissions = 0x7FFFFFFF; item.inventoryEveryOnePermissions = 0x7FFFFFFF; item.inventoryCurrentPermissions = 0x7FFFFFFF; item.inventoryNextPermissions = 0x7FFFFFFF; - this.Items.Add(item.inventoryID, item); + this.m_textureFolder.Items.Add(item.inventoryID, item); item = new InventoryItemBase(); item.avatarID = libOwner; @@ -111,6 +123,36 @@ namespace OpenSim.Framework.Communications.Caches item.inventoryCurrentPermissions = 0; item.inventoryNextPermissions = 0; this.Items.Add(item.inventoryID, item); + + item = new InventoryItemBase(); + item.avatarID = libOwner; + item.creatorsID = libOwner; + item.inventoryID = new LLUUID("77c41e39-38f9-f75a-024e-585989bfabc9"); + item.assetID = new LLUUID("77c41e39-38f9-f75a-024e-585989bbabbb"); + item.inventoryDescription = "Default Skin"; + item.inventoryName = "Default Skin"; + item.type = 13; + item.parentFolderID = this.folderID; + item.inventoryCurrentPermissions = 0; + item.inventoryNextPermissions = 0; + this.Items.Add(item.inventoryID, item); + + item = new InventoryItemBase(); + item.avatarID = libOwner; + item.creatorsID = libOwner; + item.inventoryID = new LLUUID("00000000-0000-2222-4444-000000000001"); + item.assetID = new LLUUID("00000000-0000-2222-3333-000000000001"); + item.inventoryDescription = "Welcome"; + item.inventoryName = "Welcome"; + item.type = 7; + item.parentFolderID = this.folderID; + item.inventoryCurrentPermissions = (1 << 15); + item.inventoryNextPermissions = (1 << 15); + item.inventoryEveryOnePermissions = (1 << 15); + item.inventoryBasePermissions = (1 << 15); + this.Items.Add(item.inventoryID, item); + + } } diff --git a/OpenSim/Framework/Communications/Cache/UserProfileCache.cs b/OpenSim/Framework/Communications/Cache/UserProfileCache.cs index 7b4f6a5..32c5db9 100644 --- a/OpenSim/Framework/Communications/Cache/UserProfileCache.cs +++ b/OpenSim/Framework/Communications/Cache/UserProfileCache.cs @@ -116,10 +116,15 @@ namespace OpenSim.Framework.Communications.Caches public void HandleFecthInventoryDescendents(IClientAPI remoteClient, LLUUID folderID, LLUUID ownerID, bool fetchFolders, bool fetchItems, int sortOrder) { + InventoryFolder fold = null; if (folderID == libraryRoot.folderID ) { remoteClient.SendInventoryFolderDetails(libraryRoot.agentID, libraryRoot.folderID, libraryRoot.RequestListOfItems()); } + else if (( fold = libraryRoot.HasSubFolder(folderID)) != null) + { + remoteClient.SendInventoryFolderDetails(libraryRoot.agentID, folderID, fold.RequestListOfItems()); + } else if (this.UserProfiles.ContainsKey(remoteClient.AgentId)) { CachedUserInfo info = this.UserProfiles[remoteClient.AgentId]; diff --git a/OpenSim/Framework/General/Interfaces/IClientAPI.cs b/OpenSim/Framework/General/Interfaces/IClientAPI.cs index e13016b..0bfd827 100644 --- a/OpenSim/Framework/General/Interfaces/IClientAPI.cs +++ b/OpenSim/Framework/General/Interfaces/IClientAPI.cs @@ -156,6 +156,11 @@ namespace OpenSim.Framework.Interfaces get; } + LLUUID SessionId + { + get; + } + string FirstName { get; diff --git a/OpenSim/Framework/General/NullClientAPI.cs b/OpenSim/Framework/General/NullClientAPI.cs index 1b42064..aefb384 100644 --- a/OpenSim/Framework/General/NullClientAPI.cs +++ b/OpenSim/Framework/General/NullClientAPI.cs @@ -84,6 +84,11 @@ namespace OpenSim.Framework get { return m_uuid; } } + public LLUUID SessionId + { + get { return LLUUID.Zero; } + } + public virtual string FirstName { get { return ""; } diff --git a/OpenSim/Framework/General/Types/AgentWearable.cs b/OpenSim/Framework/General/Types/AgentWearable.cs index 9de25b5..8c704ce 100644 --- a/OpenSim/Framework/General/Types/AgentWearable.cs +++ b/OpenSim/Framework/General/Types/AgentWearable.cs @@ -43,13 +43,16 @@ namespace OpenSim.Framework.Types { get { - AvatarWearable[] defaultWearables = new AvatarWearable[13]; //should be 13 of these + AvatarWearable[] defaultWearables = new AvatarWearable[13]; //should be 13 of these for (int i = 0; i < 13; i++) { defaultWearables[i] = new AvatarWearable(); } defaultWearables[0].AssetID = new LLUUID("66c41e39-38f9-f75a-024e-585989bfab73"); defaultWearables[0].ItemID = new LLUUID("66c41e39-38f9-f75a-024e-585989bfaba9"); + + //defaultWearables[1].ItemID = new LLUUID("77c41e39-38f9-f75a-024e-585989bfabc9"); + //defaultWearables[1].AssetID = new LLUUID("77c41e39-38f9-f75a-024e-585989bbabbb"); return defaultWearables; } } diff --git a/OpenSim/Framework/UserManager/UserManagerBase.cs b/OpenSim/Framework/UserManager/UserManagerBase.cs index 1acafeb..bc923b6 100644 --- a/OpenSim/Framework/UserManager/UserManagerBase.cs +++ b/OpenSim/Framework/UserManager/UserManagerBase.cs @@ -361,6 +361,14 @@ namespace OpenSim.Framework.UserManagement TempHash["folder_id"] = "00000112-000f-0000-0000-000100bba000"; ArrayList temp = new ArrayList(); temp.Add(TempHash); + + TempHash = new Hashtable(); + TempHash["name"] = "Texture Library"; + TempHash["parent_id"] = "00000112-000f-0000-0000-000100bba000"; + TempHash["version"] = "1"; + TempHash["type_default"] = "-1"; + TempHash["folder_id"] = "00000112-000f-0000-0000-000100bba001"; + temp.Add(TempHash); return temp; } diff --git a/OpenSim/Region/ClientStack/ClientView.API.cs b/OpenSim/Region/ClientStack/ClientView.API.cs index feacadc..7e1820b 100644 --- a/OpenSim/Region/ClientStack/ClientView.API.cs +++ b/OpenSim/Region/ClientStack/ClientView.API.cs @@ -123,6 +123,14 @@ namespace OpenSim.Region.ClientStack } } + public LLUUID SessionId + { + get + { + return this.SessionID; + } + } + /// /// /// diff --git a/OpenSim/Region/Examples/SimpleApp/MyNpcCharacter.cs b/OpenSim/Region/Examples/SimpleApp/MyNpcCharacter.cs index b8e6af5..dd30759 100644 --- a/OpenSim/Region/Examples/SimpleApp/MyNpcCharacter.cs +++ b/OpenSim/Region/Examples/SimpleApp/MyNpcCharacter.cs @@ -103,6 +103,11 @@ namespace SimpleApp get { return myID; } } + public LLUUID SessionId + { + get { return LLUUID.Zero; } + } + public virtual string FirstName { get { return "Annoying"; } diff --git a/OpenSim/Region/GridInterfaces/Local/LocalAssetServer.cs b/OpenSim/Region/GridInterfaces/Local/LocalAssetServer.cs index 7de9d55..919ad6c 100644 --- a/OpenSim/Region/GridInterfaces/Local/LocalAssetServer.cs +++ b/OpenSim/Region/GridInterfaces/Local/LocalAssetServer.cs @@ -259,8 +259,24 @@ namespace OpenSim.Region.GridInterfaces.Local db.Commit();*/ Image = new AssetBase(); + Image.FullID = new LLUUID("77c41e39-38f9-f75a-024e-585989bbabbb"); + Image.Name = "Skin"; + Image.Type = 13; + Image.InvType = 13; + this.LoadAsset(Image, false, "base_skin.dat"); + store = new AssetStorage(); + store.Data = Image.Data; + store.Name = Image.Name; + store.UUID = Image.FullID; + db.Set(store); + db.Commit(); + + + Image = new AssetBase(); Image.FullID = new LLUUID("66c41e39-38f9-f75a-024e-585989bfab73"); Image.Name = "Shape"; + Image.Type = 13; + Image.InvType = 13; this.LoadAsset(Image, false, "base_shape.dat"); store = new AssetStorage(); store.Data = Image.Data; @@ -268,6 +284,20 @@ namespace OpenSim.Region.GridInterfaces.Local store.UUID = Image.FullID; db.Set(store); db.Commit(); + + Image = new AssetBase(); + Image.FullID = new LLUUID("00000000-0000-2222-3333-000000000001"); + Image.Name = "WelcomeNote"; + Image.Type = 7; + Image.InvType = 7; + this.LoadAsset(Image, false, "welcomeNote.dat"); + store = new AssetStorage(); + store.Data = Image.Data; + store.Name = Image.Name; + store.UUID = Image.FullID; + db.Set(store); + db.Commit(); + } private void LoadAsset(AssetBase info, bool image, string filename) diff --git a/bin/assets/base_skin.dat b/bin/assets/base_skin.dat new file mode 100644 index 0000000..2ebaed9 --- /dev/null +++ b/bin/assets/base_skin.dat @@ -0,0 +1,52 @@ +LLWearable version 22 +Sexy - Female Skin + + permissions 0 + { + base_mask 00000000 + owner_mask 00000000 + group_mask 00000000 + everyone_mask 00000000 + next_owner_mask 00000000 + creator_id 11111111-1111-0000-0000-000100bba000 + owner_id 11111111-1111-0000-0000-000100bba000 + last_owner_id 11111111-1111-0000-0000-000100bba000 + group_id 00000000-0000-0000-0000-000000000000 + } + sale_info 0 + { + sale_type not + sale_price 10 + } +type 1 +parameters 26 +108 0 +110 0 +111 0 +116 0 +117 1 +150 0 +162 0 +163 0 +165 0 +700 .01 +701 .5 +702 .26 +703 0 +704 0 +705 .5 +706 .6 +707 0 +708 0 +709 0 +710 0 +711 .5 +712 0 +713 .7 +714 0 +715 0 +775 0 +textures 3 +0 00000000-0000-0000-9999-000000000003 +5 00000000-0000-0000-9999-000000000004 +6 00000000-0000-0000-9999-000000000005 diff --git a/bin/assets/welcomeNote.dat b/bin/assets/welcomeNote.dat new file mode 100644 index 0000000..ed6358d --- /dev/null +++ b/bin/assets/welcomeNote.dat @@ -0,0 +1,9 @@ +Linden text version 2 +{ +LLEmbeddedItems version 1 +{ +count 0 +} +Text length 95 +Hello and thank you for using opensim. For more infomation visit http://openmv.org/wiki/OpenSim} + \ No newline at end of file -- cgit v1.1