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); }
}
}
}
}