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 { /// 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 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; internal ManualResetEvent HeaderReceivedEvent = new ManualResetEvent(false); } /// /// /// public class AssetTransfer : 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; } /// /// /// public class ImageTransfer : Transfer { public ushort PacketCount = 0; public int Codec = 0; public bool NotFound = false; internal int InitialDataSize = 0; } /// /// /// public class AssetManager { /// /// /// /// public delegate void AssetReceivedCallback(AssetTransfer asset); /// /// /// public event AssetReceivedCallback OnAssetReceived; 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)); // Xfer packets for uploading large assets //Client.Network.RegisterCallback(PacketType.AssetUploadComplete, new NetworkManager.PacketCallback(AssetUploadCompleteHandler)); //Client.Network.RegisterCallback(PacketType.ConfirmXferPacket, new NetworkManager.PacketCallback(ConfirmXferPacketHandler)); //Client.Network.RegisterCallback(PacketType.RequestXfer, new NetworkManager.PacketCallback(RequestXferHandler)); } /// /// /// /// /// /// public void RequestAsset(LLUUID assetID, AssetType type, float priority) { AssetTransfer transfer = new AssetTransfer(); transfer.ID = LLUUID.Random(); transfer.AssetID = assetID; transfer.Priority = priority; 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 = 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 /// Try 101.0f public void RequestInventoryAsset(LLUUID assetID, LLUUID itemID, LLUUID taskID, LLUUID ownerID, AssetType type, float priority) { AssetTransfer transfer = new AssetTransfer(); transfer.ID = LLUUID.Random(); transfer.AssetID = assetID; transfer.Priority = priority; 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 = 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() { ; } private void TransferInfoHandler(Packet packet, Simulator simulator) { if (OnAssetReceived != null) { TransferInfoPacket info = (TransferInfoPacket)packet; if (Transfers.ContainsKey(info.TransferInfo.TransferID)) { AssetTransfer transfer = 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); // Better than returning null transfer.AssetData = new byte[0]; // 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) { if (OnAssetReceived != null) { TransferPacketPacket asset = (TransferPacketPacket)packet; if (Transfers.ContainsKey(asset.TransferData.TransferID)) { AssetTransfer transfer = 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; 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 if (asset.TransferData.Data.Length == 1000 || transfer.Transferred + asset.TransferData.Data.Length >= transfer.Size) { Array.Copy(asset.TransferData.Data, 0, transfer.AssetData, 1000 * (asset.TransferData.Packet - 1), asset.TransferData.Data.Length); transfer.Transferred += asset.TransferData.Data.Length; } else { Client.Log("Received a TransferPacket with a data length of " + asset.TransferData.Data.Length + " bytes! Bailing out...", Helpers.LogLevel.Error); lock (Transfers) Transfers.Remove(transfer.ID); // fire the even with out transfer that contains Success = false; try { OnAssetReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } return; } 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); try { OnAssetReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } else { Client.Log("Received a TransferPacket packet for an asset we didn't request, TransferID: " + asset.TransferData.TransferID, Helpers.LogLevel.Warning); } } } } public class ImageManager { /// /// /// /// public delegate void ImageReceivedCallback(ImageTransfer image); public event ImageReceivedCallback OnImageReceived; private SecondLife Client; private Dictionary Transfers = new Dictionary(); /// /// Default constructor /// /// A reference to the SecondLife client to use public ImageManager(SecondLife client) { Client = client; 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)); } /// /// Initiate an image download. This is an asynchronous function /// /// The image to download public void RequestImage(LLUUID imageID, float priority) { if (!Transfers.ContainsKey(imageID)) { ImageTransfer transfer = new ImageTransfer(); 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 = 0; request.RequestImage[0].DownloadPriority = priority; request.RequestImage[0].Packet = 0; request.RequestImage[0].Image = imageID; request.RequestImage[0].Type = 0; // TODO: What is this? Client.Network.SendPacket(request); } else { Client.Log("RequestImage() called for an image we are already downloading, ignoring", Helpers.LogLevel.Info); } } /// /// 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; Client.DebugLog("Received first " + data.ImageData.Data.Length + " bytes for image " + data.ImageID.ID.ToStringHyphenated()); if (Transfers.ContainsKey(data.ImageID.ID)) { ImageTransfer transfer = 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; transfer.HeaderReceivedEvent.Set(); // Check if we downloaded the full image if (transfer.Transferred >= transfer.Size) { lock (Transfers) Transfers.Remove(transfer.ID); transfer.Success = true; if (OnImageReceived != null) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } else { Client.Log("Received an ImageData packet for an image we didn't request, ID: " + data.ImageID.ID, Helpers.LogLevel.Warning); } } /// /// 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; if (Transfers.ContainsKey(image.ImageID.ID)) { ImageTransfer transfer = 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); lock (Transfers) Transfers.Remove(transfer.ID); // Fire the event with our transfer that contains Success = false; if (OnImageReceived != null) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } return; } } // 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; lock (Transfers) Transfers.Remove(transfer.ID); if (OnImageReceived != null) { try { OnImageReceived(transfer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } else { Client.Log("Received an ImagePacket packet for an image we didn't request, ID: " + image.ImageID.ID, Helpers.LogLevel.Warning); } } /// /// The requested image does not exist on the asset server /// public void ImageNotInDatabaseHandler(Packet packet, Simulator simulator) { ImageNotInDatabasePacket notin = (ImageNotInDatabasePacket)packet; if (Transfers.ContainsKey(notin.ImageID.ID)) { ImageTransfer transfer = Transfers[notin.ImageID.ID]; transfer.NotFound = true; lock (Transfers) Transfers.Remove(transfer.ID); // Fire the event with our transfer that contains Success = false; if (OnImageReceived != null) { OnImageReceived(transfer); } } else { Client.Log("Received an ImageNotInDatabase packet for an image we didn't request, ID: " + notin.ImageID.ID, Helpers.LogLevel.Warning); } } } }