using System; using System.Collections.Generic; using System.Threading; using libsecondlife; using libsecondlife.Packets; namespace libsecondlife.Utilities.Assets { /// /// The different types of assets in Second Life /// public enum AssetType { /// Unknown asset type Unknown = -1, /// Texture asset, stores in JPEG2000 J2C stream format Texture = 0, /// Sound asset Sound = 1, /// Calling card for another avatar CallingCard = 2, /// Link to a location in world Landmark = 3, /// Legacy script asset, you should never see one of these [Obsolete] Script = 4, /// Collection of textures and parameters that can be /// worn by an avatar Clothing = 5, /// Primitive that can contain textures, sounds, /// scripts and more Object = 6, /// Notecard asset Notecard = 7, /// Holds a collection of inventory items Folder = 8, /// Root inventory folder RootFolder = 9, /// Linden scripting language script LSLText = 10, /// LSO bytecode for a script LSLBytecode = 11, /// Uncompressed TGA texture TextureTGA = 12, /// Collection of textures and shape parameters that can /// be worn Bodypart = 13, /// Trash folder TrashFolder = 14, /// Snapshot folder SnapshotFolder = 15, /// Lost and found folder LostAndFoundFolder = 16, /// Uncompressed sound SoundWAV = 17, /// Uncompressed TGA non-square image, not to be used as a /// texture ImageTGA = 18, /// Compressed JPEG non-square image, not to be used as a /// texture ImageJPEG = 19, /// Animation Animation = 20, /// Sequence of animations, sounds, chat, and pauses Gesture = 21, /// Simstate file Simstate = 22, } /// /// /// public enum StatusCode { /// OK OK = 0, /// Transfer completed Done = 1, /// Skip = 2, /// Abort = 3, /// Unknown error occurred Error = -1, /// Equivalent to a 404 error UnknownSource = -2, /// Client does not have permission for that resource InsufficientPermissiosn = -3, /// Unknown status Unknown = -4 } /// /// /// public enum ChannelType : int { /// Unknown = 0, /// Unknown Misc = 1, /// Virtually all asset transfers use this channel Asset = 2 } /// /// /// public enum SourceType : int { /// Unknown = 0, /// Arbitrary system files off the server [Obsolete] File = 1, /// Asset from the asset server Asset = 2, /// Inventory item SimInventoryItem = 3, /// SimEstate = 4 } /// /// /// public enum TargetType : int { /// Unknown = 0, /// File, /// VFile } /// /// /// public enum ImageType : byte { /// Normal = 0, /// Baked = 1 } /// /// /// public class Transfer { public LLUUID ID = LLUUID.Zero; public int Size = 0; public byte[] AssetData = new byte[0]; public int Transferred = 0; public bool Success = false; } /// /// /// public class AssetDownload : Transfer { public LLUUID AssetID = LLUUID.Zero; public ChannelType Channel = ChannelType.Unknown; public SourceType Source = SourceType.Unknown; public TargetType Target = TargetType.Unknown; public StatusCode Status = StatusCode.Unknown; public float Priority = 0.0f; internal ManualResetEvent HeaderReceivedEvent = new ManualResetEvent(false); } /// /// /// public class ImageDownload : Transfer { public ushort PacketCount = 0; public int Codec = 0; public bool NotFound = false; internal int InitialDataSize = 0; internal ManualResetEvent HeaderReceivedEvent = new ManualResetEvent(false); } /// /// /// public class AssetUpload : Transfer { public LLUUID AssetID = LLUUID.Zero; public AssetType Type = AssetType.Unknown; public ulong XferID = 0; public uint PacketNum = 0; } /// /// /// public class AssetManager { /// /// /// /// public delegate void AssetReceivedCallback(AssetDownload asset); /// /// /// /// public delegate void ImageReceivedCallback(ImageDownload image); /// /// /// /// public delegate void AssetUploadedCallback(AssetUpload upload); /// /// /// public event AssetReceivedCallback OnAssetReceived; /// /// /// public event ImageReceivedCallback OnImageReceived; /// /// /// public event AssetUploadedCallback OnAssetUploaded; private SecondLife Client; private Dictionary Transfers = new Dictionary(); /// /// Default constructor /// /// A reference to the SecondLife client object public AssetManager(SecondLife client) { Client = client; // Transfer packets for downloading large assets Client.Network.RegisterCallback(PacketType.TransferInfo, new NetworkManager.PacketCallback(TransferInfoHandler)); Client.Network.RegisterCallback(PacketType.TransferPacket, new NetworkManager.PacketCallback(TransferPacketHandler)); // Image downloading packets Client.Network.RegisterCallback(PacketType.ImageData, new NetworkManager.PacketCallback(ImageDataHandler)); Client.Network.RegisterCallback(PacketType.ImagePacket, new NetworkManager.PacketCallback(ImagePacketHandler)); Client.Network.RegisterCallback(PacketType.ImageNotInDatabase, new NetworkManager.PacketCallback(ImageNotInDatabaseHandler)); // Xfer packets for uploading large assets Client.Network.RegisterCallback(PacketType.RequestXfer, new NetworkManager.PacketCallback(RequestXferHandler)); Client.Network.RegisterCallback(PacketType.ConfirmXferPacket, new NetworkManager.PacketCallback(ConfirmXferPacketHandler)); Client.Network.RegisterCallback(PacketType.AssetUploadComplete, new NetworkManager.PacketCallback(AssetUploadCompleteHandler)); } /// /// /// /// /// /// public void RequestAsset(LLUUID assetID, AssetType type, bool priority) { AssetDownload transfer = new AssetDownload(); transfer.ID = LLUUID.Random(); transfer.AssetID = assetID; transfer.Priority = 100.0f + (priority ? 1.0f : 0.0f); transfer.Channel = ChannelType.Asset; transfer.Source = SourceType.Asset; // Add this transfer to the dictionary lock (Transfers) Transfers[transfer.ID] = transfer; // Build the request packet and send it TransferRequestPacket request = new TransferRequestPacket(); request.TransferInfo.ChannelType = (int)transfer.Channel; request.TransferInfo.Priority = transfer.Priority; request.TransferInfo.SourceType = (int)transfer.Source; request.TransferInfo.TransferID = transfer.ID; byte[] paramField = new byte[20]; Array.Copy(assetID.GetBytes(), 0, paramField, 0, 16); Array.Copy(Helpers.IntToBytes((int)type), 0, paramField, 16, 4); request.TransferInfo.Params = paramField; Client.Network.SendPacket(request); } /// /// /// /// Use LLUUID.Zero if you do not have the /// asset ID but have all the necessary permissions /// The item ID of this asset in the inventory /// Use LLUUID.Zero if you are not requesting an /// asset from an object inventory /// The owner of this asset /// Asset type /// Whether to prioritize this asset download or not public void RequestInventoryAsset(LLUUID assetID, LLUUID itemID, LLUUID taskID, LLUUID ownerID, AssetType type, bool priority) { AssetDownload transfer = new AssetDownload(); transfer.ID = LLUUID.Random(); transfer.AssetID = assetID; transfer.Priority = 100.0f + (priority ? 1.0f : 0.0f); transfer.Channel = ChannelType.Asset; transfer.Source = SourceType.SimInventoryItem; // Add this transfer to the dictionary lock (Transfers) Transfers[transfer.ID] = transfer; // Build the request packet and send it TransferRequestPacket request = new TransferRequestPacket(); request.TransferInfo.ChannelType = (int)transfer.Channel; request.TransferInfo.Priority = transfer.Priority; request.TransferInfo.SourceType = (int)transfer.Source; request.TransferInfo.TransferID = transfer.ID; byte[] paramField = new byte[100]; Array.Copy(Client.Network.AgentID.GetBytes(), 0, paramField, 0, 16); Array.Copy(Client.Network.SessionID.GetBytes(), 0, paramField, 16, 16); Array.Copy(ownerID.GetBytes(), 0, paramField, 32, 16); Array.Copy(taskID.GetBytes(), 0, paramField, 48, 16); Array.Copy(itemID.GetBytes(), 0, paramField, 64, 16); Array.Copy(assetID.GetBytes(), 0, paramField, 80, 16); Array.Copy(Helpers.IntToBytes((int)type), 0, paramField, 96, 4); request.TransferInfo.Params = paramField; Client.Network.SendPacket(request); } public void RequestEstateAsset() { throw new Exception("This function is not implemented yet!"); } /// /// Initiate an image download. This is an asynchronous function /// /// The image to download /// /// /// public void RequestImage(LLUUID imageID, ImageType type, float priority, int discardLevel) { if (!Transfers.ContainsKey(imageID)) { ImageDownload transfer = new ImageDownload(); transfer.ID = imageID; // Add this transfer to the dictionary lock (Transfers) Transfers[transfer.ID] = transfer; // Build and send the request packet RequestImagePacket request = new RequestImagePacket(); request.AgentData.AgentID = Client.Network.AgentID; request.AgentData.SessionID = Client.Network.SessionID; request.RequestImage = new RequestImagePacket.RequestImageBlock[1]; request.RequestImage[0] = new RequestImagePacket.RequestImageBlock(); request.RequestImage[0].DiscardLevel = (sbyte)discardLevel; request.RequestImage[0].DownloadPriority = priority; request.RequestImage[0].Packet = 0; request.RequestImage[0].Image = imageID; request.RequestImage[0].Type = (byte)type; Client.Network.SendPacket(request); } else { Client.Log("RequestImage() called for an image we are already downloading, ignoring", Helpers.LogLevel.Info); } } /// /// /// /// Usually a randomly generated UUID /// /// /// /// /// public void RequestUpload(LLUUID transactionID, AssetType type, byte[] data, bool tempFile, bool storeLocal, bool isPriority) { if (!Transfers.ContainsKey(transactionID)) { LLUUID assetID; if (transactionID != LLUUID.Zero) assetID = transactionID.Combine(Client.Network.SecureSessionID); else assetID = LLUUID.Zero; AssetUpload upload = new AssetUpload(); upload.AssetData = data; upload.ID = transactionID; upload.Size = data.Length; // Build and send the upload packet AssetUploadRequestPacket request = new AssetUploadRequestPacket(); request.AssetBlock.StoreLocal = storeLocal; request.AssetBlock.Tempfile = tempFile; request.AssetBlock.TransactionID = transactionID; request.AssetBlock.Type = (sbyte)type; if (data.Length + 100 < Client.Settings.MAX_PACKET_SIZE) { // The whole asset will fit in this packet, makes things easy request.AssetBlock.AssetData = data; upload.Transferred = data.Length; } else { // Asset is too big, send in multiple packets request.AssetBlock.AssetData = new byte[0]; } Client.Network.SendPacket(request); } else { Client.Log("RequestUpload() called for an asset we are already uploading, ignoring", Helpers.LogLevel.Info); } } private void SendNextUploadPacket(AssetUpload upload) { SendXferPacketPacket send = new SendXferPacketPacket(); send.XferID.ID = upload.XferID; send.XferID.Packet = upload.PacketNum++; send.DataPacket.Data = new byte[1000]; Array.Copy(upload.AssetData, upload.Transferred, send.DataPacket.Data, 0, 1000); upload.Transferred += 1000; Client.Network.SendPacket(send); // FIXME: Trigger uploaded event } private void TransferInfoHandler(Packet packet, Simulator simulator) { if (OnAssetReceived != null) { TransferInfoPacket info = (TransferInfoPacket)packet; if (Transfers.ContainsKey(info.TransferInfo.TransferID)) { AssetDownload transfer = (AssetDownload)Transfers[info.TransferInfo.TransferID]; transfer.Channel = (ChannelType)info.TransferInfo.ChannelType; transfer.Status = (StatusCode)info.TransferInfo.Status; transfer.Target = (TargetType)info.TransferInfo.TargetType; transfer.Size = info.TransferInfo.Size; // TODO: Once we support mid-transfer status checking and aborting this // will need to become smarter if (transfer.Status != StatusCode.OK) { lock (Transfers) Transfers.Remove(transfer.ID); // No data could have been received before the TransferInfo packet transfer.AssetData = null; // Fire the event with our transfer that contains Success = false; try { OnAssetReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } else { transfer.AssetData = new byte[transfer.Size]; if (transfer.Source == SourceType.Asset && info.TransferInfo.Params.Length == 20) { transfer.AssetID = new LLUUID(info.TransferInfo.Params, 0); // TODO: Set the authoritative asset type here as well } else if (transfer.Source == SourceType.SimInventoryItem && info.TransferInfo.Params.Length == 100) { transfer.AssetID = new LLUUID(info.TransferInfo.Params, 80); // TODO: Set the authoritative asset type here as well } else { Client.Log("Received a TransferInfo packet with a SourceType of " + transfer.Source.ToString() + " and a Params field length of " + info.TransferInfo.Params.Length, Helpers.LogLevel.Warning); } } } else { Client.Log("Received a TransferInfo packet for an asset we didn't request, TransferID: " + info.TransferInfo.TransferID, Helpers.LogLevel.Warning); } } } private void TransferPacketHandler(Packet packet, Simulator simulator) { TransferPacketPacket asset = (TransferPacketPacket)packet; if (Transfers.ContainsKey(asset.TransferData.TransferID)) { AssetDownload transfer = (AssetDownload)Transfers[asset.TransferData.TransferID]; if (transfer.Size == 0) { // We haven't received the header yet, block until it's received or times out transfer.HeaderReceivedEvent.WaitOne(1000 * 20, false); if (transfer.Size == 0) { Client.Log("Timed out while waiting for the asset header to download for " + transfer.ID.ToStringHyphenated(), Helpers.LogLevel.Warning); lock (Transfers) Transfers.Remove(transfer.ID); // Fire the event with our transfer that contains Success = false; if (OnAssetReceived != null) { try { OnAssetReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } return; } } // This assumes that every transfer packet except the last one is exactly 1000 bytes, // hopefully that is a safe assumption to make Array.Copy(asset.TransferData.Data, 0, transfer.AssetData, 1000 * asset.TransferData.Packet, asset.TransferData.Data.Length); transfer.Transferred += asset.TransferData.Data.Length; Client.DebugLog("Received " + asset.TransferData.Data.Length + "/" + transfer.Transferred + "/" + transfer.Size + " bytes for asset " + transfer.ID.ToStringHyphenated()); // Check if we downloaded the full asset if (transfer.Transferred >= transfer.Size) { transfer.Success = true; lock (Transfers) Transfers.Remove(transfer.ID); if (OnAssetReceived != null) { try { OnAssetReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } } private void RequestXferHandler(Packet packet, Simulator simulator) { AssetUpload upload = null; RequestXferPacket request = (RequestXferPacket)packet; Console.WriteLine(request.ToString()); // The Xfer system sucks. This will thankfully die soon when uploads are // moved to HTTP lock (Transfers) { // Associate the XferID with an upload. If an upload is initiated // before the previous one is associated with an XferID one or both // of them will undoubtedly fail foreach (Transfer transfer in Transfers.Values) { if (transfer.GetType() == typeof(AssetUpload)) { if (((AssetUpload)transfer).XferID == 0) { // First match, use it upload = (AssetUpload)transfer; upload.XferID = request.XferID.ID; break; } } } } if (upload != null) { // Add this transfer to the dictionary. Create a UUID out of the ulong XferID LLUUID transferID = new LLUUID(upload.XferID); lock (Transfers) Transfers[transferID] = upload; SendNextUploadPacket(upload); } } private void ConfirmXferPacketHandler(Packet packet, Simulator simulator) { ConfirmXferPacketPacket confirm = (ConfirmXferPacketPacket)packet; // Building a new UUID every time an ACK is received for an upload is a horrible // thing, but this whole Xfer system is horrible LLUUID transferID = new LLUUID(confirm.XferID.ID); if (Transfers.ContainsKey(transferID)) { SendNextUploadPacket((AssetUpload)Transfers[transferID]); } } private void AssetUploadCompleteHandler(Packet packet, Simulator simulator) { if (OnAssetUploaded != null) { bool found = false; KeyValuePair foundTransfer = new KeyValuePair(); AssetUploadCompletePacket complete = (AssetUploadCompletePacket)packet; // Xfer system sucks really really bad. Where is the damn XferID? lock (Transfers) { foreach (KeyValuePair transfer in Transfers) { if (transfer.Value.GetType() == typeof(AssetUpload)) { AssetUpload upload = (AssetUpload)transfer.Value; if ((upload).AssetID == complete.AssetBlock.UUID) { found = true; foundTransfer = transfer; upload.Success = complete.AssetBlock.Success; upload.Type = (AssetType)complete.AssetBlock.Type; found = true; break; } } } } if (found) { lock (Transfers) Transfers.Remove(foundTransfer.Key); try { OnAssetUploaded((AssetUpload)foundTransfer.Value); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } /// /// 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 ImageDataHandler(Packet packet, Simulator simulator) { ImageDataPacket data = (ImageDataPacket)packet; ImageDownload transfer = null; Client.DebugLog("Received first " + data.ImageData.Data.Length + " bytes for image " + data.ImageID.ID.ToStringHyphenated()); lock (Transfers) { if (Transfers.ContainsKey(data.ImageID.ID)) { transfer = (ImageDownload)Transfers[data.ImageID.ID]; transfer.Codec = data.ImageID.Codec; transfer.PacketCount = data.ImageID.Packets; transfer.Size = (int)data.ImageID.Size; transfer.AssetData = new byte[transfer.Size]; Array.Copy(data.ImageData.Data, transfer.AssetData, data.ImageData.Data.Length); transfer.InitialDataSize = data.ImageData.Data.Length; transfer.Transferred += data.ImageData.Data.Length; // Check if we downloaded the full image if (transfer.Transferred >= transfer.Size) { Transfers.Remove(transfer.ID); transfer.Success = true; } } } if (transfer != null) { if (OnImageReceived != null && transfer.Transferred >= transfer.Size) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } transfer.HeaderReceivedEvent.Set(); } } /// /// Handles the remaining Image data that did not fit in the initial ImageData packet /// public void ImagePacketHandler(Packet packet, Simulator simulator) { ImagePacketPacket image = (ImagePacketPacket)packet; ImageDownload transfer = null; lock (Transfers) { if (Transfers.ContainsKey(image.ImageID.ID)) { transfer = (ImageDownload)Transfers[image.ImageID.ID]; if (transfer.Size == 0) { // We haven't received the header yet, block until it's received or times out transfer.HeaderReceivedEvent.WaitOne(1000 * 20, false); if (transfer.Size == 0) { Client.Log("Timed out while waiting for the image header to download for " + transfer.ID.ToStringHyphenated(), Helpers.LogLevel.Warning); transfer.Success = false; Transfers.Remove(transfer.ID); goto Callback; } } // The header is downloaded, we can insert this data in to the proper position Array.Copy(image.ImageData.Data, 0, transfer.AssetData, transfer.InitialDataSize + (1000 * (image.ImageID.Packet - 1)), image.ImageData.Data.Length); transfer.Transferred += image.ImageData.Data.Length; Client.DebugLog("Received " + image.ImageData.Data.Length + "/" + transfer.Transferred + "/" + transfer.Size + " bytes for image " + image.ImageID.ID.ToStringHyphenated()); // Check if we downloaded the full image if (transfer.Transferred >= transfer.Size) { transfer.Success = true; Transfers.Remove(transfer.ID); } } } Callback: if (transfer != null && OnImageReceived != null && (transfer.Transferred >= transfer.Size || transfer.Size == 0)) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } /// /// The requested image does not exist on the asset server /// public void ImageNotInDatabaseHandler(Packet packet, Simulator simulator) { ImageNotInDatabasePacket notin = (ImageNotInDatabasePacket)packet; ImageDownload transfer = null; lock (Transfers) { if (Transfers.ContainsKey(notin.ImageID.ID)) { transfer = (ImageDownload)Transfers[notin.ImageID.ID]; transfer.NotFound = true; Transfers.Remove(transfer.ID); } } // Fire the event with our transfer that contains Success = false; if (transfer != null && OnImageReceived != null) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } }