/* * Copyright (c) 2006, Second Life Reverse Engineering Team * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the Second Life Reverse Engineering Team nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ //#define DEBUG_PACKETS using System; using System.Collections.Generic; using System.IO; using System.Threading; using libsecondlife; using libsecondlife.Packets; using libsecondlife.InventorySystem; namespace libsecondlife.AssetSystem { public delegate void ImageRetrievedCallback(LLUUID id, byte[] data, bool cached, string statusmsg); //this delegate is called when an image completed. /// /// Manages the uploading and downloading of Images from SecondLife /// public class ImageManager { private SecondLife slClient; public enum CacheTypes {None, Memory, Disk}; private CacheTypes CacheType; private string CacheDirectory = "ImageCache"; private Dictionary CacheTable = new Dictionary(); private List CachedDiskIndex = new List(); private ImagePacketHelpers ImagePacketHelper; private Dictionary htDownloadRequests = new Dictionary(); public ImageRetrievedCallback OnImageRetrieved; private class TransferRequest { public ManualResetEvent ReceivedHeaderPacket = new ManualResetEvent(false); public ManualResetEvent Completed = new ManualResetEvent(false); public bool Status; public string StatusMsg; public uint Size; public uint Received; public uint LastPacket; public byte[] AssetData; public int BaseDataReceived; public TransferRequest() { Status = false; StatusMsg = ""; AssetData = null; BaseDataReceived = 0; } } /// /// /// public ImageManager(SecondLife client) { Init(client, CacheTypes.None, null); } /// /// /// /// The type of Cache system to use for images. public ImageManager(SecondLife client, CacheTypes ctype) { Init(client, ctype, null); } /// /// /// /// The type of Cache system to use for images. /// The directory to use for disk based caching. public ImageManager(SecondLife client, CacheTypes ctype, String directory) { Init(client, ctype, directory); } /// /// /// /// The type of Cache system to use for images. /// The directory to use for disk based caching. private void Init(SecondLife client, CacheTypes ctype, string directory) { slClient = client; // Setup Image Caching CacheType = ctype; if (ctype == CacheTypes.Disk) { if (directory != null) { CacheDirectory = directory; } try { if (!Directory.Exists(CacheDirectory)) { Directory.CreateDirectory(CacheDirectory); } } catch (Exception e) { slClient.Log("Disk Cache directory could not be established, defaulting to Memory Cache: " + Environment.NewLine + e.ToString(), Helpers.LogLevel.Warning); CacheType = CacheTypes.Memory; } } // Image Packet Helpers ImagePacketHelper = new ImagePacketHelpers(client); // Image Callbacks slClient.Network.RegisterCallback(PacketType.ImageData, new NetworkManager.PacketCallback(ImageDataCallbackHandler)); slClient.Network.RegisterCallback(PacketType.ImagePacket, new NetworkManager.PacketCallback(ImagePacketCallbackHandler)); slClient.Network.RegisterCallback(PacketType.ImageNotInDatabase, new NetworkManager.PacketCallback(ImageNotInDatabaseCallbackHandler)); } private void CacheImage(LLUUID ImageID, byte[] ImageData) { switch (CacheType) { case CacheTypes.Memory: CacheTable[ImageID] = ImageData; break; case CacheTypes.Disk: String filepath = Path.Combine(CacheDirectory, ImageID.ToStringHyphenated()); File.WriteAllBytes(filepath, ImageData); CachedDiskIndex.Add(ImageID); break; default: break; } } private byte[] CachedImage(LLUUID ImageID) { switch (CacheType) { case CacheTypes.Memory: if (CacheTable.ContainsKey(ImageID)) { return CacheTable[ImageID]; } else { return null; } case CacheTypes.Disk: String filepath = Path.Combine(CacheDirectory, ImageID.ToStringHyphenated()); if (File.Exists(filepath)) { return File.ReadAllBytes(filepath); } else { return null; } default: return null; } } public bool isCachedImage(LLUUID ImageID) { if (ImageID == null) { throw new Exception("Don't go calling isCachedImage() with a null..."); } switch (CacheType) { case CacheTypes.Memory: return CacheTable.ContainsKey(ImageID); case CacheTypes.Disk: if (CachedDiskIndex.Contains(ImageID)) { return true; } else { String filepath = Path.Combine(CacheDirectory, ImageID.ToStringHyphenated()); if (File.Exists(filepath)) { CachedDiskIndex.Add(ImageID); return true; } else { return false; } } default: return false; } } public bool isDownloadingImages() { bool isDownloading = false; lock (htDownloadRequests) { isDownloading = htDownloadRequests.Count > 0 ? true : false; } return isDownloading; } /// /// Requests an image from SecondLife and blocks until it's received. /// /// The Image's AssetID public byte[] RequestImage(LLUUID ImageID) { byte[] imgData = CachedImage(ImageID); if (imgData != null) { return imgData; } TransferRequest tr; lock (htDownloadRequests) { if (htDownloadRequests.ContainsKey(ImageID) == false) { tr = new TransferRequest(); tr.Size = int.MaxValue; // Number of bytes expected tr.Received = 0; // Number of bytes received tr.LastPacket = Helpers.GetUnixTime(); // last time we recevied a packet for this request htDownloadRequests[ImageID] = tr; Packet packet = ImagePacketHelper.RequestImage(ImageID); slClient.Network.SendPacket(packet); } else { tr = htDownloadRequests[ImageID]; } } // Wait for transfer to complete. tr.Completed.WaitOne(); if( tr.Status == true ) { return tr.AssetData; } else { throw new Exception( "RequestImage: " + tr.StatusMsg ); } } /// /// Requests an image from SecondLife. /// /// The Image's AssetID public void RequestImageAsync(LLUUID ImageID) { if (ImageID == null) { throw new Exception("WTF!!! Don't request Image Assets by passing in an ImageID of null"); } byte[] imgData = CachedImage(ImageID); if (imgData != null) { FireImageRetrieved(ImageID, imgData, true); } lock (htDownloadRequests) { if (htDownloadRequests.ContainsKey(ImageID) == false) { TransferRequest tr = new TransferRequest(); tr.Size = int.MaxValue; // Number of bytes expected tr.Received = 0; // Number of bytes received tr.LastPacket = Helpers.GetUnixTime(); // last time we recevied a packet for this request htDownloadRequests[ImageID] = tr; Packet packet = ImagePacketHelper.RequestImage(ImageID); slClient.Network.SendPacket(packet); } } } /// /// Handles the Image Data packet, which includes the ID, and Size of the image, /// along with the first block of data for the image. If the image is small enough /// there will be no additional packets. /// /// /// public void ImageDataCallbackHandler(Packet packet, Simulator simulator) { #if DEBUG_PACKETS slClient.DebugLog(packet); #endif ImageDataPacket reply = (ImageDataPacket)packet; LLUUID ImageID = reply.ImageID.ID; // unused? ushort Packets = reply.ImageID.Packets; uint Size = reply.ImageID.Size; byte[] Data = reply.ImageData.Data; // Lookup the request that this packet is for TransferRequest tr; lock (htDownloadRequests) { try { tr = htDownloadRequests[ImageID]; } catch (Exception) { // Received a packet for an image we didn't request... return; } } // Initialize the request so that it's data buffer is the right size for the image tr.Size = Size; tr.AssetData = new byte[tr.Size]; tr.BaseDataReceived = Data.Length; // Copy the first block of image data into the request. Array.Copy(Data, 0, tr.AssetData, tr.Received, Data.Length); tr.Received += (uint)Data.Length; // Mark that the TransferRequest has received this header packet tr.ReceivedHeaderPacket.Set(); // If we've gotten all the data, mark it completed. if( tr.Received >= tr.Size ) { tr.Status = true; tr.Completed.Set(); // Fire off image downloaded event CacheImage(ImageID, tr.AssetData); FireImageRetrieved(ImageID, tr.AssetData, false); } } /// /// Handles the remaining Image data that did not fit in the initial ImageData packet /// /// /// public void ImagePacketCallbackHandler(Packet packet, Simulator simulator) { #if DEBUG_PACKETS slClient.DebugLog(packet); #endif ImagePacketPacket reply = (ImagePacketPacket)packet; LLUUID ImageID = reply.ImageID.ID; // Lookup the request for this packet TransferRequest tr; lock (htDownloadRequests) { tr = (TransferRequest)htDownloadRequests[ImageID]; } if( tr == null ) { // Received a packet that doesn't belong to any requests in our queue, strange... return; } // TODO: Received data should probably be put into a temporary collection that's indected by ImageID.Packet // then once we've received all data packets, it should be re-assembled into a complete array and marked // completed. // FIXME: Sometimes this gets called before ImageDataCallbackHandler, when that // happens tr.AssetData will be null. Implimenting the above TODO should fix this. // Wait until we've received the header packet for this image, which creates the AssetData array tr.ReceivedHeaderPacket.WaitOne(); // Add this packet's data to the request. Array.Copy(reply.ImageData.Data, 0, tr.AssetData, tr.BaseDataReceived + (1000 * (reply.ImageID.Packet - 1)), reply.ImageData.Data.Length); tr.Received += (uint)reply.ImageData.Data.Length; // If we've gotten all the data, mark it completed. if( tr.Received >= tr.Size ) { tr.Status = true; tr.Completed.Set(); // Fire off image downloaded event CacheImage(ImageID, tr.AssetData); FireImageRetrieved(ImageID, tr.AssetData, false); } } /// /// /// public void ImageNotInDatabaseCallbackHandler(Packet packet, Simulator simulator) { #if DEBUG_PACKETS slClient.DebugLog(packet); #endif ImageNotInDatabasePacket reply = (ImageNotInDatabasePacket)packet; LLUUID ImageID = reply.ImageID.ID; // Lookup the request for this packet TransferRequest tr; lock (htDownloadRequests) { tr = (TransferRequest)htDownloadRequests[ImageID]; } if( tr == null ) { // Received a packet that doesn't belong to any requests in our queue, strange... return; } tr.Status = false; tr.StatusMsg = "Image not in database"; tr.Completed.Set(); // Fire off image downloaded event FireImageRetrieved(ImageID, null, false, tr.StatusMsg); } private void FireImageRetrieved(LLUUID ImageID, byte[] ImageData, bool cached) { FireImageRetrieved(ImageID, ImageData, cached, ""); } private void FireImageRetrieved(LLUUID ImageID, byte[] ImageData, bool cached, string status) { if (OnImageRetrieved != null) { OnImageRetrieved(ImageID, ImageData, cached, status); } } } }