/*
* Copyright (c) 2006-2014, openmetaverse.org
* 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 openmetaverse.org 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.
*/
using System;
using System.Collections.Generic;
using System.Threading;
using System.Drawing;
using OpenMetaverse;
using OpenMetaverse.Packets;
using OpenMetaverse.Imaging;
using OpenMetaverse.Assets;
using OpenMetaverse.Http;
using OpenMetaverse.StructuredData;
namespace OpenMetaverse
{
#region Enums
///
/// Index of TextureEntry slots for avatar appearances
///
public enum AvatarTextureIndex
{
Unknown = -1,
HeadBodypaint = 0,
UpperShirt,
LowerPants,
EyesIris,
Hair,
UpperBodypaint,
LowerBodypaint,
LowerShoes,
HeadBaked,
UpperBaked,
LowerBaked,
EyesBaked,
LowerSocks,
UpperJacket,
LowerJacket,
UpperGloves,
UpperUndershirt,
LowerUnderpants,
Skirt,
SkirtBaked,
HairBaked,
LowerAlpha,
UpperAlpha,
HeadAlpha,
EyesAlpha,
HairAlpha,
HeadTattoo,
UpperTattoo,
LowerTattoo,
NumberOfEntries
}
///
/// Bake layers for avatar appearance
///
public enum BakeType
{
Unknown = -1,
Head = 0,
UpperBody = 1,
LowerBody = 2,
Eyes = 3,
Skirt = 4,
Hair = 5
}
///
/// Appearance Flags, introdued with server side baking, currently unused
///
[Flags]
public enum AppearanceFlags : uint
{
None = 0
}
#endregion Enums
public class AppearanceManager
{
#region Constants
/// Mask for multiple attachments
public static readonly byte ATTACHMENT_ADD = 0x80;
/// Mapping between BakeType and AvatarTextureIndex
public static readonly byte[] BakeIndexToTextureIndex = new byte[BAKED_TEXTURE_COUNT] { 8, 9, 10, 11, 19, 20 };
/// Maximum number of concurrent downloads for wearable assets and textures
const int MAX_CONCURRENT_DOWNLOADS = 5;
/// Maximum number of concurrent uploads for baked textures
const int MAX_CONCURRENT_UPLOADS = 6;
/// Timeout for fetching inventory listings
const int INVENTORY_TIMEOUT = 1000 * 30;
/// Timeout for fetching a single wearable, or receiving a single packet response
const int WEARABLE_TIMEOUT = 1000 * 30;
/// Timeout for fetching a single texture
const int TEXTURE_TIMEOUT = 1000 * 120;
/// Timeout for uploading a single baked texture
const int UPLOAD_TIMEOUT = 1000 * 90;
/// Number of times to retry bake upload
const int UPLOAD_RETRIES = 2;
/// When changing outfit, kick off rebake after
/// 20 seconds has passed since the last change
const int REBAKE_DELAY = 1000 * 20;
/// Total number of wearables for each avatar
public const int WEARABLE_COUNT = 16;
/// Total number of baked textures on each avatar
public const int BAKED_TEXTURE_COUNT = 6;
/// Total number of wearables per bake layer
public const int WEARABLES_PER_LAYER = 9;
/// Map of what wearables are included in each bake
public static readonly WearableType[][] WEARABLE_BAKE_MAP = new WearableType[][]
{
new WearableType[] { WearableType.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Hair, WearableType.Alpha, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid },
new WearableType[] { WearableType.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Shirt, WearableType.Jacket, WearableType.Gloves, WearableType.Undershirt, WearableType.Alpha, WearableType.Invalid },
new WearableType[] { WearableType.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Pants, WearableType.Shoes, WearableType.Socks, WearableType.Jacket, WearableType.Underpants, WearableType.Alpha },
new WearableType[] { WearableType.Eyes, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid },
new WearableType[] { WearableType.Skirt, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid },
new WearableType[] { WearableType.Hair, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid }
};
/// Magic values to finalize the cache check hashes for each
/// bake
public static readonly UUID[] BAKED_TEXTURE_HASH = new UUID[]
{
new UUID("18ded8d6-bcfc-e415-8539-944c0f5ea7a6"),
new UUID("338c29e3-3024-4dbb-998d-7c04cf4fa88f"),
new UUID("91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f"),
new UUID("b2cf28af-b840-1071-3c6a-78085d8128b5"),
new UUID("ea800387-ea1a-14e0-56cb-24f2022f969a"),
new UUID("0af1ef7c-ad24-11dd-8790-001f5bf833e8")
};
/// Default avatar texture, used to detect when a custom
/// texture is not set for a face
public static readonly UUID DEFAULT_AVATAR_TEXTURE = new UUID("c228d1cf-4b5d-4ba8-84f4-899a0796aa97");
#endregion Constants
#region Structs / Classes
///
/// Contains information about a wearable inventory item
///
public class WearableData
{
/// Inventory ItemID of the wearable
public UUID ItemID;
/// AssetID of the wearable asset
public UUID AssetID;
/// WearableType of the wearable
public WearableType WearableType;
/// AssetType of the wearable
public AssetType AssetType;
/// Asset data for the wearable
public AssetWearable Asset;
public override string ToString()
{
return String.Format("ItemID: {0}, AssetID: {1}, WearableType: {2}, AssetType: {3}, Asset: {4}",
ItemID, AssetID, WearableType, AssetType, Asset != null ? Asset.Name : "(null)");
}
}
///
/// Data collected from visual params for each wearable
/// needed for the calculation of the color
///
public struct ColorParamInfo
{
public VisualParam VisualParam;
public VisualColorParam VisualColorParam;
public float Value;
public WearableType WearableType;
}
///
/// Holds a texture assetID and the data needed to bake this layer into
/// an outfit texture. Used to keep track of currently worn textures
/// and baking data
///
public struct TextureData
{
/// A texture AssetID
public UUID TextureID;
/// Asset data for the texture
public AssetTexture Texture;
/// Collection of alpha masks that needs applying
public Dictionary AlphaMasks;
/// Tint that should be applied to the texture
public Color4 Color;
/// Where on avatar does this texture belong
public AvatarTextureIndex TextureIndex;
public override string ToString()
{
return String.Format("TextureID: {0}, Texture: {1}",
TextureID, Texture != null ? Texture.AssetData.Length + " bytes" : "(null)");
}
}
#endregion Structs / Classes
#region Event delegates, Raise Events
/// The event subscribers. null if no subcribers
private EventHandler m_AgentWearablesReply;
/// Raises the AgentWearablesReply event
/// An AgentWearablesReplyEventArgs object containing the
/// data returned from the data server
protected virtual void OnAgentWearables(AgentWearablesReplyEventArgs e)
{
EventHandler handler = m_AgentWearablesReply;
if (handler != null)
handler(this, e);
}
/// Thread sync lock object
private readonly object m_AgentWearablesLock = new object();
/// Triggered when an AgentWearablesUpdate packet is received,
/// telling us what our avatar is currently wearing
/// request.
public event EventHandler AgentWearablesReply
{
add { lock (m_AgentWearablesLock) { m_AgentWearablesReply += value; } }
remove { lock (m_AgentWearablesLock) { m_AgentWearablesReply -= value; } }
}
/// The event subscribers. null if no subcribers
private EventHandler m_AgentCachedBakesReply;
/// Raises the CachedBakesReply event
/// An AgentCachedBakesReplyEventArgs object containing the
/// data returned from the data server AgentCachedTextureResponse
protected virtual void OnAgentCachedBakes(AgentCachedBakesReplyEventArgs e)
{
EventHandler handler = m_AgentCachedBakesReply;
if (handler != null)
handler(this, e);
}
/// Thread sync lock object
private readonly object m_AgentCachedBakesLock = new object();
/// Raised when an AgentCachedTextureResponse packet is
/// received, giving a list of cached bakes that were found on the
/// simulator
/// request.
public event EventHandler CachedBakesReply
{
add { lock (m_AgentCachedBakesLock) { m_AgentCachedBakesReply += value; } }
remove { lock (m_AgentCachedBakesLock) { m_AgentCachedBakesReply -= value; } }
}
/// The event subscribers. null if no subcribers
private EventHandler m_AppearanceSet;
/// Raises the AppearanceSet event
/// An AppearanceSetEventArgs object indicating if the operatin was successfull
protected virtual void OnAppearanceSet(AppearanceSetEventArgs e)
{
EventHandler handler = m_AppearanceSet;
if (handler != null)
handler(this, e);
}
/// Thread sync lock object
private readonly object m_AppearanceSetLock = new object();
///
/// Raised when appearance data is sent to the simulator, also indicates
/// the main appearance thread is finished.
///
/// request.
public event EventHandler AppearanceSet
{
add { lock (m_AppearanceSetLock) { m_AppearanceSet += value; } }
remove { lock (m_AppearanceSetLock) { m_AppearanceSet -= value; } }
}
/// The event subscribers. null if no subcribers
private EventHandler m_RebakeAvatarReply;
/// Raises the RebakeAvatarRequested event
/// An RebakeAvatarTexturesEventArgs object containing the
/// data returned from the data server
protected virtual void OnRebakeAvatar(RebakeAvatarTexturesEventArgs e)
{
EventHandler handler = m_RebakeAvatarReply;
if (handler != null)
handler(this, e);
}
/// Thread sync lock object
private readonly object m_RebakeAvatarLock = new object();
///
/// Triggered when the simulator requests the agent rebake its appearance.
///
///
public event EventHandler RebakeAvatarRequested
{
add { lock (m_RebakeAvatarLock) { m_RebakeAvatarReply += value; } }
remove { lock (m_RebakeAvatarLock) { m_RebakeAvatarReply -= value; } }
}
#endregion
#region Properties and public fields
///
/// Returns true if AppearanceManager is busy and trying to set or change appearance will fail
///
public bool ManagerBusy
{
get
{
return AppearanceThreadRunning != 0;
}
}
/// Visual parameters last sent to the sim
public byte[] MyVisualParameters = null;
/// Textures about this client sent to the sim
public Primitive.TextureEntry MyTextures = null;
#endregion Properties
#region Private Members
/// A cache of wearables currently being worn
private Dictionary Wearables = new Dictionary();
/// A cache of textures currently being worn
private TextureData[] Textures = new TextureData[(int)AvatarTextureIndex.NumberOfEntries];
/// Incrementing serial number for AgentCachedTexture packets
private int CacheCheckSerialNum = -1;
/// Incrementing serial number for AgentSetAppearance packets
private int SetAppearanceSerialNum = 0;
/// Indicates if WearablesRequest succeeded
private bool GotWearables = false;
/// Indicates whether or not the appearance thread is currently
/// running, to prevent multiple appearance threads from running
/// simultaneously
private int AppearanceThreadRunning = 0;
/// Reference to our agent
private GridClient Client;
///
/// Timer used for delaying rebake on changing outfit
///
private Timer RebakeScheduleTimer;
///
/// Main appearance thread
///
private Thread AppearanceThread;
///
/// Is server baking complete. It needs doing only once
///
private bool ServerBakingDone = false;
#endregion Private Members
///
/// Default constructor
///
/// A reference to our agent
public AppearanceManager(GridClient client)
{
Client = client;
Client.Network.RegisterCallback(PacketType.AgentWearablesUpdate, AgentWearablesUpdateHandler);
Client.Network.RegisterCallback(PacketType.AgentCachedTextureResponse, AgentCachedTextureResponseHandler);
Client.Network.RegisterCallback(PacketType.RebakeAvatarTextures, RebakeAvatarTexturesHandler);
Client.Network.EventQueueRunning += Network_OnEventQueueRunning;
Client.Network.Disconnected += Network_OnDisconnected;
}
#region Publics Methods
///
/// Obsolete method for setting appearance. This function no longer does anything.
/// Use RequestSetAppearance() to manually start the appearance thread
///
[Obsolete("Appearance is now handled automatically")]
public void SetPreviousAppearance()
{
}
///
/// Obsolete method for setting appearance. This function no longer does anything.
/// Use RequestSetAppearance() to manually start the appearance thread
///
/// Unused parameter
[Obsolete("Appearance is now handled automatically")]
public void SetPreviousAppearance(bool allowBake)
{
}
///
/// Starts the appearance setting thread
///
public void RequestSetAppearance()
{
RequestSetAppearance(false);
}
///
/// Starts the appearance setting thread
///
/// True to force rebaking, otherwise false
public void RequestSetAppearance(bool forceRebake)
{
if (Interlocked.CompareExchange(ref AppearanceThreadRunning, 1, 0) != 0)
{
Logger.Log("Appearance thread is already running, skipping", Helpers.LogLevel.Warning);
return;
}
// If we have an active delayed scheduled appearance bake, we dispose of it
if (RebakeScheduleTimer != null)
{
RebakeScheduleTimer.Dispose();
RebakeScheduleTimer = null;
}
// This is the first time setting appearance, run through the entire sequence
AppearanceThread = new Thread(
delegate()
{
bool success = true;
try
{
if (forceRebake)
{
// Set all of the baked textures to UUID.Zero to force rebaking
for (int bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
Textures[(int)BakeTypeToAgentTextureIndex((BakeType)bakedIndex)].TextureID = UUID.Zero;
}
// Is this server side baking enabled sim
if (ServerBakingRegion())
{
if (!GotWearables)
{
// Fetch a list of the current agent wearables
if (GetAgentWearables())
{
GotWearables = true;
}
}
if (!ServerBakingDone || forceRebake)
{
if (UpdateAvatarAppearance())
{
ServerBakingDone = true;
}
else
{
success = false;
}
}
}
else // Classic client side baking
{
if (!GotWearables)
{
// Fetch a list of the current agent wearables
if (!GetAgentWearables())
{
Logger.Log("Failed to retrieve a list of current agent wearables, appearance cannot be set",
Helpers.LogLevel.Error, Client);
throw new Exception("Failed to retrieve a list of current agent wearables, appearance cannot be set");
}
GotWearables = true;
}
// If we get back to server side backing region re-request server bake
ServerBakingDone = false;
// Download and parse all of the agent wearables
if (!DownloadWearables())
{
success = false;
Logger.Log("One or more agent wearables failed to download, appearance will be incomplete",
Helpers.LogLevel.Warning, Client);
}
// If this is the first time setting appearance and we're not forcing rebakes, check the server
// for cached bakes
if (SetAppearanceSerialNum == 0 && !forceRebake)
{
// Compute hashes for each bake layer and compare against what the simulator currently has
if (!GetCachedBakes())
{
Logger.Log("Failed to get a list of cached bakes from the simulator, appearance will be rebaked",
Helpers.LogLevel.Warning, Client);
}
}
// Download textures, compute bakes, and upload for any cache misses
if (!CreateBakes())
{
success = false;
Logger.Log("Failed to create or upload one or more bakes, appearance will be incomplete",
Helpers.LogLevel.Warning, Client);
}
// Send the appearance packet
RequestAgentSetAppearance();
}
}
catch (Exception e)
{
Logger.Log(
string.Format("Failed to set appearance with exception {0}", e), Helpers.LogLevel.Warning, Client);
success = false;
}
finally
{
AppearanceThreadRunning = 0;
OnAppearanceSet(new AppearanceSetEventArgs(success));
}
}
);
AppearanceThread.Name = "Appearance";
AppearanceThread.IsBackground = true;
AppearanceThread.Start();
}
///
/// Check if current region supports server side baking
///
/// True if server side baking support is detected
public bool ServerBakingRegion()
{
return Client.Network.CurrentSim != null &&
((Client.Network.CurrentSim.Protocols & RegionProtocols.AgentAppearanceService) != 0);
}
///
/// Ask the server what textures our agent is currently wearing
///
public void RequestAgentWearables()
{
AgentWearablesRequestPacket request = new AgentWearablesRequestPacket();
request.AgentData.AgentID = Client.Self.AgentID;
request.AgentData.SessionID = Client.Self.SessionID;
Client.Network.SendPacket(request);
}
///
/// Build hashes out of the texture assetIDs for each baking layer to
/// ask the simulator whether it has cached copies of each baked texture
///
public void RequestCachedBakes()
{
List hashes = new List();
// Build hashes for each of the bake layers from the individual components
lock (Wearables)
{
for (int bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
{
// Don't do a cache request for a skirt bake if we're not wearing a skirt
if (bakedIndex == (int)BakeType.Skirt && !Wearables.ContainsKey(WearableType.Skirt))
continue;
// Build a hash of all the texture asset IDs in this baking layer
UUID hash = UUID.Zero;
for (int wearableIndex = 0; wearableIndex < WEARABLES_PER_LAYER; wearableIndex++)
{
WearableType type = WEARABLE_BAKE_MAP[bakedIndex][wearableIndex];
WearableData wearable;
if (type != WearableType.Invalid && Wearables.TryGetValue(type, out wearable))
hash ^= wearable.AssetID;
}
if (hash != UUID.Zero)
{
// Hash with our secret value for this baked layer
hash ^= BAKED_TEXTURE_HASH[bakedIndex];
// Add this to the list of hashes to send out
AgentCachedTexturePacket.WearableDataBlock block = new AgentCachedTexturePacket.WearableDataBlock();
block.ID = hash;
block.TextureIndex = (byte)bakedIndex;
hashes.Add(block);
Logger.DebugLog("Checking cache for " + (BakeType)block.TextureIndex + ", hash=" + block.ID, Client);
}
}
}
// Only send the packet out if there's something to check
if (hashes.Count > 0)
{
AgentCachedTexturePacket cache = new AgentCachedTexturePacket();
cache.AgentData.AgentID = Client.Self.AgentID;
cache.AgentData.SessionID = Client.Self.SessionID;
cache.AgentData.SerialNum = Interlocked.Increment(ref CacheCheckSerialNum);
cache.WearableData = hashes.ToArray();
Client.Network.SendPacket(cache);
}
}
///
/// Returns the AssetID of the asset that is currently being worn in a
/// given WearableType slot
///
/// WearableType slot to get the AssetID for
/// The UUID of the asset being worn in the given slot, or
/// UUID.Zero if no wearable is attached to the given slot or wearables
/// have not been downloaded yet
public UUID GetWearableAsset(WearableType type)
{
WearableData wearable;
if (Wearables.TryGetValue(type, out wearable))
return wearable.AssetID;
else
return UUID.Zero;
}
///
/// Add a wearable to the current outfit and set appearance
///
/// Wearable to be added to the outfit
public void AddToOutfit(InventoryItem wearableItem)
{
List wearableItems = new List { wearableItem };
AddToOutfit(wearableItems);
}
///
/// Add a wearable to the current outfit and set appearance
///
/// Wearable to be added to the outfit
/// Should existing item on the same point or of the same type be replaced
public void AddToOutfit(InventoryItem wearableItem, bool replace)
{
List wearableItems = new List { wearableItem };
AddToOutfit(wearableItems, true);
}
///
/// Add a list of wearables to the current outfit and set appearance
///
/// List of wearable inventory items to
/// be added to the outfit
/// Should existing item on the same point or of the same type be replaced
public void AddToOutfit(List wearableItems)
{
AddToOutfit(wearableItems, true);
}
///
/// Add a list of wearables to the current outfit and set appearance
///
/// List of wearable inventory items to
/// be added to the outfit
/// Should existing item on the same point or of the same type be replaced
public void AddToOutfit(List wearableItems, bool replace)
{
List wearables = new List();
List attachments = new List();
for (int i = 0; i < wearableItems.Count; i++)
{
InventoryItem item = wearableItems[i];
if (item is InventoryWearable)
wearables.Add((InventoryWearable)item);
else if (item is InventoryAttachment || item is InventoryObject)
attachments.Add(item);
}
lock (Wearables)
{
// Add the given wearables to the wearables collection
for (int i = 0; i < wearables.Count; i++)
{
InventoryWearable wearableItem = wearables[i];
WearableData wd = new WearableData();
wd.AssetID = wearableItem.AssetUUID;
wd.AssetType = wearableItem.AssetType;
wd.ItemID = wearableItem.UUID;
wd.WearableType = wearableItem.WearableType;
Wearables[wearableItem.WearableType] = wd;
}
}
if (attachments.Count > 0)
{
AddAttachments(attachments, false, replace);
}
if (wearables.Count > 0)
{
SendAgentIsNowWearing();
DelayedRequestSetAppearance();
}
}
///
/// Remove a wearable from the current outfit and set appearance
///
/// Wearable to be removed from the outfit
public void RemoveFromOutfit(InventoryItem wearableItem)
{
List wearableItems = new List();
wearableItems.Add(wearableItem);
RemoveFromOutfit(wearableItems);
}
///
/// Removes a list of wearables from the current outfit and set appearance
///
/// List of wearable inventory items to
/// be removed from the outfit
public void RemoveFromOutfit(List wearableItems)
{
List wearables = new List();
List attachments = new List();
for (int i = 0; i < wearableItems.Count; i++)
{
InventoryItem item = wearableItems[i];
if (item is InventoryWearable)
wearables.Add((InventoryWearable)item);
else if (item is InventoryAttachment || item is InventoryObject)
attachments.Add(item);
}
bool needSetAppearance = false;
lock (Wearables)
{
// Remove the given wearables from the wearables collection
for (int i = 0; i < wearables.Count; i++)
{
InventoryWearable wearableItem = wearables[i];
if (wearables[i].AssetType != AssetType.Bodypart // Remove if it's not a body part
&& Wearables.ContainsKey(wearableItem.WearableType) // And we have that wearabe type
&& Wearables[wearableItem.WearableType].ItemID == wearableItem.UUID // And we are wearing it
)
{
Wearables.Remove(wearableItem.WearableType);
needSetAppearance = true;
}
}
}
for (int i = 0; i < attachments.Count; i++)
{
Detach(attachments[i].UUID);
}
if (needSetAppearance)
{
SendAgentIsNowWearing();
DelayedRequestSetAppearance();
}
}
///
/// Replace the current outfit with a list of wearables and set appearance
///
/// List of wearable inventory items that
/// define a new outfit
public void ReplaceOutfit(List wearableItems)
{
ReplaceOutfit(wearableItems, true);
}
///
/// Replace the current outfit with a list of wearables and set appearance
///
/// List of wearable inventory items that
/// define a new outfit
/// Check if we have all body parts, set this to false only
/// if you know what you're doing
public void ReplaceOutfit(List wearableItems, bool safe)
{
List wearables = new List();
List attachments = new List();
for (int i = 0; i < wearableItems.Count; i++)
{
InventoryItem item = wearableItems[i];
if (item is InventoryWearable)
wearables.Add((InventoryWearable)item);
else if (item is InventoryAttachment || item is InventoryObject)
attachments.Add(item);
}
if (safe)
{
// If we don't already have a the current agent wearables downloaded, updating to a
// new set of wearables that doesn't have all of the bodyparts can leave the avatar
// in an inconsistent state. If any bodypart entries are empty, we need to fetch the
// current wearables first
bool needsCurrentWearables = false;
lock (Wearables)
{
for (int i = 0; i < WEARABLE_COUNT; i++)
{
WearableType wearableType = (WearableType)i;
if (WearableTypeToAssetType(wearableType) == AssetType.Bodypart && !Wearables.ContainsKey(wearableType))
{
needsCurrentWearables = true;
break;
}
}
}
if (needsCurrentWearables && !GetAgentWearables())
{
Logger.Log("Failed to fetch the current agent wearables, cannot safely replace outfit",
Helpers.LogLevel.Error);
return;
}
}
// Replace our local Wearables collection, send the packet(s) to update our
// attachments, tell sim what we are wearing now, and start the baking process
if (!safe)
{
SetAppearanceSerialNum++;
}
ReplaceOutfit(wearables);
AddAttachments(attachments, true, false);
SendAgentIsNowWearing();
DelayedRequestSetAppearance();
}
///
/// Checks if an inventory item is currently being worn
///
/// The inventory item to check against the agent
/// wearables
/// The WearableType slot that the item is being worn in,
/// or WearbleType.Invalid if it is not currently being worn
public WearableType IsItemWorn(InventoryItem item)
{
lock (Wearables)
{
foreach (KeyValuePair entry in Wearables)
{
if (entry.Value.ItemID == item.UUID)
return entry.Key;
}
}
return WearableType.Invalid;
}
///
/// Returns a copy of the agents currently worn wearables
///
/// A copy of the agents currently worn wearables
/// Avoid calling this function multiple times as it will make
/// a copy of all of the wearable data each time
public Dictionary GetWearables()
{
lock (Wearables)
return new Dictionary(Wearables);
}
///
/// Calls either or
/// depending on the value of
/// replaceItems
///
/// List of wearable inventory items to add
/// to the outfit or become a new outfit
/// True to replace existing items with the
/// new list of items, false to add these items to the existing outfit
public void WearOutfit(List wearables, bool replaceItems)
{
List wearableItems = new List(wearables.Count);
for (int i = 0; i < wearables.Count; i++)
{
if (wearables[i] is InventoryItem)
wearableItems.Add((InventoryItem)wearables[i]);
}
if (replaceItems)
ReplaceOutfit(wearableItems);
else
AddToOutfit(wearableItems);
}
#endregion Publics Methods
#region Attachments
///
/// Adds a list of attachments to our agent
///
/// A List containing the attachments to add
/// If true, tells simulator to remove existing attachment
/// first
public void AddAttachments(List attachments, bool removeExistingFirst)
{
AddAttachments(attachments, removeExistingFirst, true);
}
///
/// Adds a list of attachments to our agent
///
/// A List containing the attachments to add
/// If true, tells simulator to remove existing attachment
/// If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)
/// first
public void AddAttachments(List attachments, bool removeExistingFirst, bool replace)
{
// Use RezMultipleAttachmentsFromInv to clear out current attachments, and attach new ones
RezMultipleAttachmentsFromInvPacket attachmentsPacket = new RezMultipleAttachmentsFromInvPacket();
attachmentsPacket.AgentData.AgentID = Client.Self.AgentID;
attachmentsPacket.AgentData.SessionID = Client.Self.SessionID;
attachmentsPacket.HeaderData.CompoundMsgID = UUID.Random();
attachmentsPacket.HeaderData.FirstDetachAll = removeExistingFirst;
attachmentsPacket.HeaderData.TotalObjects = (byte)attachments.Count;
attachmentsPacket.ObjectData = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock[attachments.Count];
for (int i = 0; i < attachments.Count; i++)
{
if (attachments[i] is InventoryAttachment)
{
InventoryAttachment attachment = (InventoryAttachment)attachments[i];
attachmentsPacket.ObjectData[i] = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock();
attachmentsPacket.ObjectData[i].AttachmentPt = replace ? (byte)attachment.AttachmentPoint : (byte)(ATTACHMENT_ADD | (byte)attachment.AttachmentPoint);
attachmentsPacket.ObjectData[i].EveryoneMask = (uint)attachment.Permissions.EveryoneMask;
attachmentsPacket.ObjectData[i].GroupMask = (uint)attachment.Permissions.GroupMask;
attachmentsPacket.ObjectData[i].ItemFlags = (uint)attachment.Flags;
attachmentsPacket.ObjectData[i].ItemID = attachment.UUID;
attachmentsPacket.ObjectData[i].Name = Utils.StringToBytes(attachment.Name);
attachmentsPacket.ObjectData[i].Description = Utils.StringToBytes(attachment.Description);
attachmentsPacket.ObjectData[i].NextOwnerMask = (uint)attachment.Permissions.NextOwnerMask;
attachmentsPacket.ObjectData[i].OwnerID = attachment.OwnerID;
}
else if (attachments[i] is InventoryObject)
{
InventoryObject attachment = (InventoryObject)attachments[i];
attachmentsPacket.ObjectData[i] = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock();
attachmentsPacket.ObjectData[i].AttachmentPt = replace ? (byte)0 : ATTACHMENT_ADD;
attachmentsPacket.ObjectData[i].EveryoneMask = (uint)attachment.Permissions.EveryoneMask;
attachmentsPacket.ObjectData[i].GroupMask = (uint)attachment.Permissions.GroupMask;
attachmentsPacket.ObjectData[i].ItemFlags = (uint)attachment.Flags;
attachmentsPacket.ObjectData[i].ItemID = attachment.UUID;
attachmentsPacket.ObjectData[i].Name = Utils.StringToBytes(attachment.Name);
attachmentsPacket.ObjectData[i].Description = Utils.StringToBytes(attachment.Description);
attachmentsPacket.ObjectData[i].NextOwnerMask = (uint)attachment.Permissions.NextOwnerMask;
attachmentsPacket.ObjectData[i].OwnerID = attachment.OwnerID;
}
else
{
Logger.Log("Cannot attach inventory item " + attachments[i].Name, Helpers.LogLevel.Warning, Client);
}
}
Client.Network.SendPacket(attachmentsPacket);
}
///
/// Attach an item to our agent at a specific attach point
///
/// A to attach
/// the on the avatar
/// to attach the item to
public void Attach(InventoryItem item, AttachmentPoint attachPoint)
{
Attach(item, attachPoint, true);
}
///
/// Attach an item to our agent at a specific attach point
///
/// A to attach
/// the on the avatar
/// If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)
/// to attach the item to
public void Attach(InventoryItem item, AttachmentPoint attachPoint, bool replace)
{
Attach(item.UUID, item.OwnerID, item.Name, item.Description, item.Permissions, item.Flags,
attachPoint, replace);
}
///
/// Attach an item to our agent specifying attachment details
///
/// The of the item to attach
/// The attachments owner
/// The name of the attachment
/// The description of the attahment
/// The to apply when attached
/// The of the attachment
/// The on the agent
/// to attach the item to
public void Attach(UUID itemID, UUID ownerID, string name, string description,
Permissions perms, uint itemFlags, AttachmentPoint attachPoint)
{
Attach(itemID, ownerID, name, description, perms, itemFlags, attachPoint, true);
}
///
/// Attach an item to our agent specifying attachment details
///
/// The of the item to attach
/// The attachments owner
/// The name of the attachment
/// The description of the attahment
/// The to apply when attached
/// The of the attachment
/// The on the agent
/// If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)
/// to attach the item to
public void Attach(UUID itemID, UUID ownerID, string name, string description,
Permissions perms, uint itemFlags, AttachmentPoint attachPoint, bool replace)
{
// TODO: At some point it might be beneficial to have AppearanceManager track what we
// are currently wearing for attachments to make enumeration and detachment easier
RezSingleAttachmentFromInvPacket attach = new RezSingleAttachmentFromInvPacket();
attach.AgentData.AgentID = Client.Self.AgentID;
attach.AgentData.SessionID = Client.Self.SessionID;
attach.ObjectData.AttachmentPt = replace ? (byte)attachPoint : (byte)(ATTACHMENT_ADD | (byte)attachPoint);
attach.ObjectData.Description = Utils.StringToBytes(description);
attach.ObjectData.EveryoneMask = (uint)perms.EveryoneMask;
attach.ObjectData.GroupMask = (uint)perms.GroupMask;
attach.ObjectData.ItemFlags = itemFlags;
attach.ObjectData.ItemID = itemID;
attach.ObjectData.Name = Utils.StringToBytes(name);
attach.ObjectData.NextOwnerMask = (uint)perms.NextOwnerMask;
attach.ObjectData.OwnerID = ownerID;
Client.Network.SendPacket(attach);
}
///
/// Detach an item from our agent using an object
///
/// An object
public void Detach(InventoryItem item)
{
Detach(item.UUID);
}
///
/// Detach an item from our agent
///
/// The inventory itemID of the item to detach
public void Detach(UUID itemID)
{
DetachAttachmentIntoInvPacket detach = new DetachAttachmentIntoInvPacket();
detach.ObjectData.AgentID = Client.Self.AgentID;
detach.ObjectData.ItemID = itemID;
Client.Network.SendPacket(detach);
}
#endregion Attachments
#region Appearance Helpers
///
/// Inform the sim which wearables are part of our current outfit
///
private void SendAgentIsNowWearing()
{
AgentIsNowWearingPacket wearing = new AgentIsNowWearingPacket();
wearing.AgentData.AgentID = Client.Self.AgentID;
wearing.AgentData.SessionID = Client.Self.SessionID;
wearing.WearableData = new AgentIsNowWearingPacket.WearableDataBlock[WEARABLE_COUNT];
lock (Wearables)
{
for (int i = 0; i < WEARABLE_COUNT; i++)
{
WearableType type = (WearableType)i;
wearing.WearableData[i] = new AgentIsNowWearingPacket.WearableDataBlock();
wearing.WearableData[i].WearableType = (byte)i;
if (Wearables.ContainsKey(type))
wearing.WearableData[i].ItemID = Wearables[type].ItemID;
else
wearing.WearableData[i].ItemID = UUID.Zero;
}
}
Client.Network.SendPacket(wearing);
}
///
/// Replaces the Wearables collection with a list of new wearable items
///
/// Wearable items to replace the Wearables collection with
private void ReplaceOutfit(List wearableItems)
{
Dictionary newWearables = new Dictionary();
lock (Wearables)
{
// Preserve body parts from the previous set of wearables. They may be overwritten,
// but cannot be missing in the new set
foreach (KeyValuePair entry in Wearables)
{
if (entry.Value.AssetType == AssetType.Bodypart)
newWearables[entry.Key] = entry.Value;
}
// Add the given wearables to the new wearables collection
for (int i = 0; i < wearableItems.Count; i++)
{
InventoryWearable wearableItem = wearableItems[i];
WearableData wd = new WearableData();
wd.AssetID = wearableItem.AssetUUID;
wd.AssetType = wearableItem.AssetType;
wd.ItemID = wearableItem.UUID;
wd.WearableType = wearableItem.WearableType;
newWearables[wearableItem.WearableType] = wd;
}
// Replace the Wearables collection
Wearables = newWearables;
}
}
///
/// Calculates base color/tint for a specific wearable
/// based on its params
///
/// All the color info gathered from wearable's VisualParams
/// passed as list of ColorParamInfo tuples
/// Base color/tint for the wearable
public static Color4 GetColorFromParams(List param)
{
// Start off with a blank slate, black, fully transparent
Color4 res = new Color4(0, 0, 0, 0);
// Apply color modification from each color parameter
foreach (ColorParamInfo p in param)
{
int n = p.VisualColorParam.Colors.Length;
Color4 paramColor = new Color4(0, 0, 0, 0);
if (n == 1)
{
// We got only one color in this param, use it for application
// to the final color
paramColor = p.VisualColorParam.Colors[0];
}
else if (n > 1)
{
// We have an array of colors in this parameter
// First, we need to find out, based on param value
// between which two elements of the array our value lands
// Size of the step using which we iterate from Min to Max
float step = (p.VisualParam.MaxValue - p.VisualParam.MinValue) / ((float)n - 1);
// Our color should land inbetween colors in the array with index a and b
int indexa = 0;
int indexb = 0;
int i = 0;
for (float a = p.VisualParam.MinValue; a <= p.VisualParam.MaxValue; a += step)
{
if (a <= p.Value)
{
indexa = i;
}
else
{
break;
}
i++;
}
// Sanity check that we don't go outside bounds of the array
if (indexa > n - 1)
indexa = n - 1;
indexb = (indexa == n - 1) ? indexa : indexa + 1;
// How far is our value from Index A on the
// line from Index A to Index B
float distance = p.Value - (float)indexa * step;
// We are at Index A (allowing for some floating point math fuzz),
// use the color on that index
if (distance < 0.00001f || indexa == indexb)
{
paramColor = p.VisualColorParam.Colors[indexa];
}
else
{
// Not so simple as being precisely on the index eh? No problem.
// We take the two colors that our param value places us between
// and then find the value for each ARGB element that is
// somewhere on the line between color1 and color2 at some
// distance from the first color
Color4 c1 = paramColor = p.VisualColorParam.Colors[indexa];
Color4 c2 = paramColor = p.VisualColorParam.Colors[indexb];
// Distance is some fraction of the step, use that fraction
// to find the value in the range from color1 to color2
paramColor = Color4.Lerp(c1, c2, distance / step);
}
// Please leave this fragment even if its commented out
// might prove useful should ($deity forbid) there be bugs in this code
//string carray = "";
//foreach (Color c in p.VisualColorParam.Colors)
//{
// carray += c.ToString() + " - ";
//}
//Logger.DebugLog("Calculating color for " + p.WearableType + " from " + p.VisualParam.Name + ", value is " + p.Value + " in range " + p.VisualParam.MinValue + " - " + p.VisualParam.MaxValue + " step " + step + " with " + n + " elements " + carray + " A: " + indexa + " B: " + indexb + " at distance " + distance);
}
// Now that we have calculated color from the scale of colors
// that visual params provided, lets apply it to the result
switch (p.VisualColorParam.Operation)
{
case VisualColorOperation.Add:
res += paramColor;
break;
case VisualColorOperation.Multiply:
res *= paramColor;
break;
case VisualColorOperation.Blend:
res = Color4.Lerp(res, paramColor, p.Value);
break;
}
}
return res;
}
///
/// Blocking method to populate the Wearables dictionary
///
/// True on success, otherwise false
bool GetAgentWearables()
{
AutoResetEvent wearablesEvent = new AutoResetEvent(false);
EventHandler wearablesCallback = ((s, e) => wearablesEvent.Set());
AgentWearablesReply += wearablesCallback;
RequestAgentWearables();
bool success = wearablesEvent.WaitOne(WEARABLE_TIMEOUT, false);
AgentWearablesReply -= wearablesCallback;
return success;
}
///
/// Blocking method to populate the Textures array with cached bakes
///
/// True on success, otherwise false
bool GetCachedBakes()
{
AutoResetEvent cacheCheckEvent = new AutoResetEvent(false);
EventHandler cacheCallback = (sender, e) => cacheCheckEvent.Set();
CachedBakesReply += cacheCallback;
RequestCachedBakes();
bool success = cacheCheckEvent.WaitOne(WEARABLE_TIMEOUT, false);
CachedBakesReply -= cacheCallback;
return success;
}
///
/// Populates textures and visual params from a decoded asset
///
/// Wearable to decode
///
/// Populates textures and visual params from a decoded asset
///
/// Wearable to decode
public static void DecodeWearableParams(WearableData wearable, ref TextureData[] textures)
{
Dictionary alphaMasks = new Dictionary();
List colorParams = new List();
// Populate collection of alpha masks from visual params
// also add color tinting information
foreach (KeyValuePair kvp in wearable.Asset.Params)
{
if (!VisualParams.Params.ContainsKey(kvp.Key)) continue;
VisualParam p = VisualParams.Params[kvp.Key];
ColorParamInfo colorInfo = new ColorParamInfo();
colorInfo.WearableType = wearable.WearableType;
colorInfo.VisualParam = p;
colorInfo.Value = kvp.Value;
// Color params
if (p.ColorParams.HasValue)
{
colorInfo.VisualColorParam = p.ColorParams.Value;
if (wearable.WearableType == WearableType.Tattoo)
{
if (kvp.Key == 1062 || kvp.Key == 1063 || kvp.Key == 1064)
{
colorParams.Add(colorInfo);
}
}
else if (wearable.WearableType == WearableType.Jacket)
{
if (kvp.Key == 809 || kvp.Key == 810 || kvp.Key == 811)
{
colorParams.Add(colorInfo);
}
}
else if (wearable.WearableType == WearableType.Hair)
{
// Param 112 - Rainbow
// Param 113 - Red
// Param 114 - Blonde
// Param 115 - White
if (kvp.Key == 112 || kvp.Key == 113 || kvp.Key == 114 || kvp.Key == 115)
{
colorParams.Add(colorInfo);
}
}
else if (wearable.WearableType == WearableType.Skin)
{
// For skin we skip makeup params for now and use only the 3
// that are used to determine base skin tone
// Param 108 - Rainbow Color
// Param 110 - Red Skin (Ruddiness)
// Param 111 - Pigment
if (kvp.Key == 108 || kvp.Key == 110 || kvp.Key == 111)
{
colorParams.Add(colorInfo);
}
}
else
{
colorParams.Add(colorInfo);
}
}
// Add alpha mask
if (p.AlphaParams.HasValue && p.AlphaParams.Value.TGAFile != string.Empty && !p.IsBumpAttribute && !alphaMasks.ContainsKey(p.AlphaParams.Value))
{
alphaMasks.Add(p.AlphaParams.Value, kvp.Value == 0 ? 0.01f : kvp.Value);
}
// Alhpa masks can also be specified in sub "driver" params
if (p.Drivers != null)
{
for (int i = 0; i < p.Drivers.Length; i++)
{
if (VisualParams.Params.ContainsKey(p.Drivers[i]))
{
VisualParam driver = VisualParams.Params[p.Drivers[i]];
if (driver.AlphaParams.HasValue && driver.AlphaParams.Value.TGAFile != string.Empty && !driver.IsBumpAttribute && !alphaMasks.ContainsKey(driver.AlphaParams.Value))
{
alphaMasks.Add(driver.AlphaParams.Value, kvp.Value == 0 ? 0.01f : kvp.Value);
}
}
}
}
}
Color4 wearableColor = Color4.White; // Never actually used
if (colorParams.Count > 0)
{
wearableColor = GetColorFromParams(colorParams);
Logger.DebugLog("Setting tint " + wearableColor + " for " + wearable.WearableType);
}
// Loop through all of the texture IDs in this decoded asset and put them in our cache of worn textures
foreach (KeyValuePair entry in wearable.Asset.Textures)
{
int i = (int)entry.Key;
// Update information about color and alpha masks for this texture
textures[i].AlphaMasks = alphaMasks;
textures[i].Color = wearableColor;
// If this texture changed, update the TextureID and clear out the old cached texture asset
if (textures[i].TextureID != entry.Value)
{
// Treat DEFAULT_AVATAR_TEXTURE as null
if (entry.Value != AppearanceManager.DEFAULT_AVATAR_TEXTURE)
textures[i].TextureID = entry.Value;
else
textures[i].TextureID = UUID.Zero;
textures[i].Texture = null;
}
}
}
///
/// Blocking method to download and parse currently worn wearable assets
///
/// True on success, otherwise false
private bool DownloadWearables()
{
bool success = true;
// Make a copy of the wearables dictionary to enumerate over
Dictionary wearables;
lock (Wearables)
wearables = new Dictionary(Wearables);
// We will refresh the textures (zero out all non bake textures)
for (int i = 0; i < Textures.Length; i++)
{
bool isBake = false;
for (int j = 0; j < BakeIndexToTextureIndex.Length; j++)
{
if (BakeIndexToTextureIndex[j] == i)
{
isBake = true;
break;
}
}
if (!isBake)
Textures[i] = new TextureData();
}
int pendingWearables = wearables.Count;
foreach (WearableData wearable in wearables.Values)
{
if (wearable.Asset != null)
{
DecodeWearableParams(wearable, ref Textures);
--pendingWearables;
}
}
if (pendingWearables == 0)
return true;
Logger.DebugLog("Downloading " + pendingWearables + " wearable assets");
Parallel.ForEach(Math.Min(pendingWearables, MAX_CONCURRENT_DOWNLOADS), wearables.Values,
delegate(WearableData wearable)
{
if (wearable.Asset == null)
{
AutoResetEvent downloadEvent = new AutoResetEvent(false);
// Fetch this wearable asset
Client.Assets.RequestAsset(wearable.AssetID, wearable.AssetType, true,
delegate(AssetDownload transfer, Asset asset)
{
if (transfer.Success && asset is AssetWearable)
{
// Update this wearable with the freshly downloaded asset
wearable.Asset = (AssetWearable)asset;
if (wearable.Asset.Decode())
{
DecodeWearableParams(wearable, ref Textures);
Logger.DebugLog("Downloaded wearable asset " + wearable.WearableType + " with " + wearable.Asset.Params.Count +
" visual params and " + wearable.Asset.Textures.Count + " textures", Client);
}
else
{
wearable.Asset = null;
Logger.Log("Failed to decode asset:" + Environment.NewLine +
Utils.BytesToString(asset.AssetData), Helpers.LogLevel.Error, Client);
}
}
else
{
Logger.Log("Wearable " + wearable.AssetID + "(" + wearable.WearableType + ") failed to download, " +
transfer.Status, Helpers.LogLevel.Warning, Client);
}
downloadEvent.Set();
}
);
if (!downloadEvent.WaitOne(WEARABLE_TIMEOUT, false))
{
Logger.Log("Timed out downloading wearable asset " + wearable.AssetID + " (" + wearable.WearableType + ")",
Helpers.LogLevel.Error, Client);
success = false;
}
--pendingWearables;
}
}
);
return success;
}
///
/// Get a list of all of the textures that need to be downloaded for a
/// single bake layer
///
/// Bake layer to get texture AssetIDs for
/// A list of texture AssetIDs to download
private List GetTextureDownloadList(BakeType bakeType)
{
List indices = BakeTypeToTextures(bakeType);
List textures = new List();
for (int i = 0; i < indices.Count; i++)
{
AvatarTextureIndex index = indices[i];
if (index == AvatarTextureIndex.Skirt && !Wearables.ContainsKey(WearableType.Skirt))
continue;
AddTextureDownload(index, textures);
}
return textures;
}
///
/// Helper method to lookup the TextureID for a single layer and add it
/// to a list if it is not already present
///
///
///
private void AddTextureDownload(AvatarTextureIndex index, List textures)
{
TextureData textureData = Textures[(int)index];
// Add the textureID to the list if this layer has a valid textureID set, it has not already
// been downloaded, and it is not already in the download list
if (textureData.TextureID != UUID.Zero && textureData.Texture == null && !textures.Contains(textureData.TextureID))
textures.Add(textureData.TextureID);
}
///
/// Blocking method to download all of the textures needed for baking
/// the given bake layers
///
/// A list of layers that need baking
/// No return value is given because the baking will happen
/// whether or not all textures are successfully downloaded
private void DownloadTextures(List bakeLayers)
{
List textureIDs = new List();
for (int i = 0; i < bakeLayers.Count; i++)
{
List layerTextureIDs = GetTextureDownloadList(bakeLayers[i]);
for (int j = 0; j < layerTextureIDs.Count; j++)
{
UUID uuid = layerTextureIDs[j];
if (!textureIDs.Contains(uuid))
textureIDs.Add(uuid);
}
}
Logger.DebugLog("Downloading " + textureIDs.Count + " textures for baking");
Parallel.ForEach(MAX_CONCURRENT_DOWNLOADS, textureIDs,
delegate(UUID textureID)
{
try
{
AutoResetEvent downloadEvent = new AutoResetEvent(false);
Client.Assets.RequestImage(textureID,
delegate(TextureRequestState state, AssetTexture assetTexture)
{
if (state == TextureRequestState.Finished)
{
assetTexture.Decode();
for (int i = 0; i < Textures.Length; i++)
{
if (Textures[i].TextureID == textureID)
Textures[i].Texture = assetTexture;
}
}
else
{
Logger.Log("Texture " + textureID + " failed to download, one or more bakes will be incomplete",
Helpers.LogLevel.Warning);
}
downloadEvent.Set();
}
);
downloadEvent.WaitOne(TEXTURE_TIMEOUT, false);
}
catch (Exception e)
{
Logger.Log(
string.Format("Download of texture {0} failed with exception {1}", textureID, e),
Helpers.LogLevel.Warning, Client);
}
}
);
}
///
/// Blocking method to create and upload baked textures for all of the
/// missing bakes
///
/// True on success, otherwise false
private bool CreateBakes()
{
bool success = true;
List pendingBakes = new List();
// Check each bake layer in the Textures array for missing bakes
for (int bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
{
AvatarTextureIndex textureIndex = BakeTypeToAgentTextureIndex((BakeType)bakedIndex);
if (Textures[(int)textureIndex].TextureID == UUID.Zero)
{
// If this is the skirt layer and we're not wearing a skirt then skip it
if (bakedIndex == (int)BakeType.Skirt && !Wearables.ContainsKey(WearableType.Skirt))
continue;
pendingBakes.Add((BakeType)bakedIndex);
}
}
if (pendingBakes.Count > 0)
{
DownloadTextures(pendingBakes);
Parallel.ForEach(Math.Min(MAX_CONCURRENT_UPLOADS, pendingBakes.Count), pendingBakes,
delegate(BakeType bakeType)
{
if (!CreateBake(bakeType))
success = false;
}
);
}
// Free up all the textures we're holding on to
for (int i = 0; i < Textures.Length; i++)
{
Textures[i].Texture = null;
}
// We just allocated and freed a ridiculous amount of memory while
// baking. Signal to the GC to clean up
GC.Collect();
return success;
}
///
/// Blocking method to create and upload a baked texture for a single
/// bake layer
///
/// Layer to bake
/// True on success, otherwise false
private bool CreateBake(BakeType bakeType)
{
List textureIndices = BakeTypeToTextures(bakeType);
Baker oven = new Baker(bakeType);
for (int i = 0; i < textureIndices.Count; i++)
{
AvatarTextureIndex textureIndex = textureIndices[i];
TextureData texture = Textures[(int)textureIndex];
texture.TextureIndex = textureIndex;
oven.AddTexture(texture);
}
int start = Environment.TickCount;
oven.Bake();
Logger.DebugLog("Baking " + bakeType + " took " + (Environment.TickCount - start) + "ms");
UUID newAssetID = UUID.Zero;
int retries = UPLOAD_RETRIES;
while (newAssetID == UUID.Zero && retries > 0)
{
newAssetID = UploadBake(oven.BakedTexture.AssetData);
--retries;
}
Textures[(int)BakeTypeToAgentTextureIndex(bakeType)].TextureID = newAssetID;
if (newAssetID == UUID.Zero)
{
Logger.Log("Failed uploading bake " + bakeType, Helpers.LogLevel.Warning);
return false;
}
return true;
}
///
/// Blocking method to upload a baked texture
///
/// Five channel JPEG2000 texture data to upload
/// UUID of the newly created asset on success, otherwise UUID.Zero
private UUID UploadBake(byte[] textureData)
{
UUID bakeID = UUID.Zero;
AutoResetEvent uploadEvent = new AutoResetEvent(false);
Client.Assets.RequestUploadBakedTexture(textureData,
delegate(UUID newAssetID)
{
bakeID = newAssetID;
uploadEvent.Set();
}
);
// FIXME: evalute the need for timeout here, RequestUploadBakedTexture() will
// timout either on Client.Settings.TRANSFER_TIMEOUT or Client.Settings.CAPS_TIMEOUT
// depending on which upload method is used.
uploadEvent.WaitOne(UPLOAD_TIMEOUT, false);
return bakeID;
}
///
/// Creates a dictionary of visual param values from the downloaded wearables
///
/// A dictionary of visual param indices mapping to visual param
/// values for our agent that can be fed to the Baker class
private Dictionary MakeParamValues()
{
Dictionary paramValues = new Dictionary(VisualParams.Params.Count);
lock (Wearables)
{
foreach (KeyValuePair kvp in VisualParams.Params)
{
// Only Group-0 parameters are sent in AgentSetAppearance packets
if (kvp.Value.Group == 0)
{
bool found = false;
VisualParam vp = kvp.Value;
// Try and find this value in our collection of downloaded wearables
foreach (WearableData data in Wearables.Values)
{
float paramValue;
if (data.Asset != null && data.Asset.Params.TryGetValue(vp.ParamID, out paramValue))
{
paramValues.Add(vp.ParamID, paramValue);
found = true;
break;
}
}
// Use a default value if we don't have one set for it
if (!found) paramValues.Add(vp.ParamID, vp.DefaultValue);
}
}
}
return paramValues;
}
///
/// Initate server baking process
///
/// True if the server baking was successful
private bool UpdateAvatarAppearance()
{
Caps caps = Client.Network.CurrentSim.Caps;
if (caps == null)
{
return false;
}
Uri url = caps.CapabilityURI("UpdateAvatarAppearance");
if (url == null)
{
return false;
}
InventoryFolder COF = GetCOF();
if (COF == null)
{
return false;
}
else
{
// TODO: create Current Outfit Folder
}
CapsClient capsRequest = new CapsClient(url);
OSDMap request = new OSDMap(1);
request["cof_version"] = COF.Version;
string msg = "Setting server side baking failed";
OSD res = capsRequest.GetResponse(request, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT * 2);
if (res != null && res is OSDMap)
{
OSDMap result = (OSDMap)res;
if (result["success"])
{
Logger.Log("Successfully set appearance", Helpers.LogLevel.Info, Client);
// TODO: Set local visual params and baked textures based on the result here
return true;
}
else
{
if (result.ContainsKey("error"))
{
msg += ": " + result["error"].AsString();
}
}
}
Logger.Log(msg, Helpers.LogLevel.Error, Client);
return false;
}
///
/// Get the latest version of COF
///
/// Current Outfit Folder (or null if getting the data failed)
private InventoryFolder GetCOF()
{
List root = null;
AutoResetEvent folderReceived = new AutoResetEvent(false);
EventHandler callback = (sender, e) =>
{
if (e.FolderID == Client.Inventory.Store.RootFolder.UUID)
{
if (e.Success)
{
root = Client.Inventory.Store.GetContents(Client.Inventory.Store.RootFolder.UUID);
}
folderReceived.Set();
}
};
Client.Inventory.FolderUpdated += callback;
Client.Inventory.RequestFolderContentsCap(Client.Inventory.Store.RootFolder.UUID, Client.Self.AgentID, true, true, InventorySortOrder.ByDate);
folderReceived.WaitOne(Client.Settings.CAPS_TIMEOUT);
Client.Inventory.FolderUpdated -= callback;
InventoryFolder COF = null;
// COF should be in the root folder. Request update to get the latest versio number
if (root != null)
{
foreach (InventoryBase baseItem in root)
{
if (baseItem is InventoryFolder && ((InventoryFolder)baseItem).PreferredType == AssetType.CurrentOutfitFolder)
{
COF = (InventoryFolder)baseItem;
break;
}
}
}
return COF;
}
///
/// Create an AgentSetAppearance packet from Wearables data and the
/// Textures array and send it
///
private void RequestAgentSetAppearance()
{
AgentSetAppearancePacket set = MakeAppearancePacket();
Client.Network.SendPacket(set);
Logger.DebugLog("Send AgentSetAppearance packet");
}
public AgentSetAppearancePacket MakeAppearancePacket()
{
AgentSetAppearancePacket set = new AgentSetAppearancePacket();
set.AgentData.AgentID = Client.Self.AgentID;
set.AgentData.SessionID = Client.Self.SessionID;
set.AgentData.SerialNum = (uint)Interlocked.Increment(ref SetAppearanceSerialNum);
// Visual params used in the agent height calculation
float agentSizeVPHeight = 0.0f;
float agentSizeVPHeelHeight = 0.0f;
float agentSizeVPPlatformHeight = 0.0f;
float agentSizeVPHeadSize = 0.5f;
float agentSizeVPLegLength = 0.0f;
float agentSizeVPNeckLength = 0.0f;
float agentSizeVPHipLength = 0.0f;
lock (Wearables)
{
#region VisualParam
int vpIndex = 0;
int nrParams;
bool wearingPhysics = false;
foreach (WearableData wearable in Wearables.Values)
{
if (wearable.WearableType == WearableType.Physics)
{
wearingPhysics = true;
break;
}
}
if (wearingPhysics)
{
nrParams = 251;
}
else
{
nrParams = 218;
}
set.VisualParam = new AgentSetAppearancePacket.VisualParamBlock[nrParams];
foreach (KeyValuePair kvp in VisualParams.Params)
{
VisualParam vp = kvp.Value;
float paramValue = 0f;
bool found = false;
// Try and find this value in our collection of downloaded wearables
foreach (WearableData data in Wearables.Values)
{
if (data.Asset != null && data.Asset.Params.TryGetValue(vp.ParamID, out paramValue))
{
found = true;
break;
}
}
// Use a default value if we don't have one set for it
if (!found)
paramValue = vp.DefaultValue;
// Only Group-0 parameters are sent in AgentSetAppearance packets
if (kvp.Value.Group == 0)
{
set.VisualParam[vpIndex] = new AgentSetAppearancePacket.VisualParamBlock();
set.VisualParam[vpIndex].ParamValue = Utils.FloatToByte(paramValue, vp.MinValue, vp.MaxValue);
++vpIndex;
}
// Check if this is one of the visual params used in the agent height calculation
switch (vp.ParamID)
{
case 33:
agentSizeVPHeight = paramValue;
break;
case 198:
agentSizeVPHeelHeight = paramValue;
break;
case 503:
agentSizeVPPlatformHeight = paramValue;
break;
case 682:
agentSizeVPHeadSize = paramValue;
break;
case 692:
agentSizeVPLegLength = paramValue;
break;
case 756:
agentSizeVPNeckLength = paramValue;
break;
case 842:
agentSizeVPHipLength = paramValue;
break;
}
if (vpIndex == nrParams) break;
}
MyVisualParameters = new byte[set.VisualParam.Length];
for (int i = 0; i < set.VisualParam.Length; i++)
{
MyVisualParameters[i] = set.VisualParam[i].ParamValue;
}
#endregion VisualParam
#region TextureEntry
Primitive.TextureEntry te = new Primitive.TextureEntry(DEFAULT_AVATAR_TEXTURE);
for (uint i = 0; i < Textures.Length; i++)
{
if ((i == 0 || i == 5 || i == 6) && Client.Settings.CLIENT_IDENTIFICATION_TAG != UUID.Zero)
{
Primitive.TextureEntryFace face = te.CreateFace(i);
face.TextureID = Client.Settings.CLIENT_IDENTIFICATION_TAG;
Logger.DebugLog("Sending client identification tag: " + Client.Settings.CLIENT_IDENTIFICATION_TAG, Client);
}
else if (Textures[i].TextureID != UUID.Zero)
{
Primitive.TextureEntryFace face = te.CreateFace(i);
face.TextureID = Textures[i].TextureID;
Logger.DebugLog("Sending texture entry for " + (AvatarTextureIndex)i + " to " + Textures[i].TextureID, Client);
}
}
set.ObjectData.TextureEntry = te.GetBytes();
MyTextures = te;
#endregion TextureEntry
#region WearableData
set.WearableData = new AgentSetAppearancePacket.WearableDataBlock[BAKED_TEXTURE_COUNT];
// Build hashes for each of the bake layers from the individual components
for (int bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
{
UUID hash = UUID.Zero;
for (int wearableIndex = 0; wearableIndex < WEARABLES_PER_LAYER; wearableIndex++)
{
WearableType type = WEARABLE_BAKE_MAP[bakedIndex][wearableIndex];
WearableData wearable;
if (type != WearableType.Invalid && Wearables.TryGetValue(type, out wearable))
hash ^= wearable.AssetID;
}
if (hash != UUID.Zero)
{
// Hash with our magic value for this baked layer
hash ^= BAKED_TEXTURE_HASH[bakedIndex];
}
// Tell the server what cached texture assetID to use for each bake layer
set.WearableData[bakedIndex] = new AgentSetAppearancePacket.WearableDataBlock();
set.WearableData[bakedIndex].TextureIndex = BakeIndexToTextureIndex[bakedIndex];
set.WearableData[bakedIndex].CacheID = hash;
Logger.DebugLog("Sending TextureIndex " + (BakeType)bakedIndex + " with CacheID " + hash, Client);
}
#endregion WearableData
#region Agent Size
// Takes into account the Shoe Heel/Platform offsets but not the HeadSize offset. Seems to work.
double agentSizeBase = 1.706;
// The calculation for the HeadSize scalar may be incorrect, but it seems to work
double agentHeight = agentSizeBase + (agentSizeVPLegLength * .1918) + (agentSizeVPHipLength * .0375) +
(agentSizeVPHeight * .12022) + (agentSizeVPHeadSize * .01117) + (agentSizeVPNeckLength * .038) +
(agentSizeVPHeelHeight * .08) + (agentSizeVPPlatformHeight * .07);
set.AgentData.Size = new Vector3(0.45f, 0.6f, (float)agentHeight);
#endregion Agent Size
if (Client.Settings.AVATAR_TRACKING)
{
Avatar me;
if (Client.Network.CurrentSim.ObjectsAvatars.TryGetValue(Client.Self.LocalID, out me))
{
me.Textures = MyTextures;
me.VisualParameters = MyVisualParameters;
}
}
}
return set;
}
private void DelayedRequestSetAppearance()
{
if (RebakeScheduleTimer == null)
{
RebakeScheduleTimer = new Timer(RebakeScheduleTimerTick);
}
try { RebakeScheduleTimer.Change(REBAKE_DELAY, Timeout.Infinite); }
catch { }
}
private void RebakeScheduleTimerTick(Object state)
{
RequestSetAppearance(true);
}
#endregion Appearance Helpers
#region Inventory Helpers
private bool GetFolderWearables(string[] folderPath, out List wearables, out List attachments)
{
UUID folder = Client.Inventory.FindObjectByPath(
Client.Inventory.Store.RootFolder.UUID, Client.Self.AgentID, String.Join("/", folderPath), INVENTORY_TIMEOUT);
if (folder != UUID.Zero)
{
return GetFolderWearables(folder, out wearables, out attachments);
}
else
{
Logger.Log("Failed to resolve outfit folder path " + folderPath, Helpers.LogLevel.Error, Client);
wearables = null;
attachments = null;
return false;
}
}
private bool GetFolderWearables(UUID folder, out List wearables, out List attachments)
{
wearables = new List();
attachments = new List();
List objects = Client.Inventory.FolderContents(folder, Client.Self.AgentID, false, true,
InventorySortOrder.ByName, INVENTORY_TIMEOUT);
if (objects != null)
{
foreach (InventoryBase ib in objects)
{
if (ib is InventoryWearable)
{
Logger.DebugLog("Adding wearable " + ib.Name, Client);
wearables.Add((InventoryWearable)ib);
}
else if (ib is InventoryAttachment)
{
Logger.DebugLog("Adding attachment (attachment) " + ib.Name, Client);
attachments.Add((InventoryItem)ib);
}
else if (ib is InventoryObject)
{
Logger.DebugLog("Adding attachment (object) " + ib.Name, Client);
attachments.Add((InventoryItem)ib);
}
else
{
Logger.DebugLog("Ignoring inventory item " + ib.Name, Client);
}
}
}
else
{
Logger.Log("Failed to download folder contents of + " + folder, Helpers.LogLevel.Error, Client);
return false;
}
return true;
}
#endregion Inventory Helpers
#region Callbacks
protected void AgentWearablesUpdateHandler(object sender, PacketReceivedEventArgs e)
{
bool changed = false;
AgentWearablesUpdatePacket update = (AgentWearablesUpdatePacket)e.Packet;
lock (Wearables)
{
#region Test if anything changed in this update
for (int i = 0; i < update.WearableData.Length; i++)
{
AgentWearablesUpdatePacket.WearableDataBlock block = update.WearableData[i];
if (block.AssetID != UUID.Zero)
{
WearableData wearable;
if (Wearables.TryGetValue((WearableType)block.WearableType, out wearable))
{
if (wearable.AssetID != block.AssetID || wearable.ItemID != block.ItemID)
{
// A different wearable is now set for this index
changed = true;
break;
}
}
else
{
// A wearable is now set for this index
changed = true;
break;
}
}
else if (Wearables.ContainsKey((WearableType)block.WearableType))
{
// This index is now empty
changed = true;
break;
}
}
#endregion Test if anything changed in this update
if (changed)
{
Logger.DebugLog("New wearables received in AgentWearablesUpdate");
Wearables.Clear();
for (int i = 0; i < update.WearableData.Length; i++)
{
AgentWearablesUpdatePacket.WearableDataBlock block = update.WearableData[i];
if (block.AssetID != UUID.Zero)
{
WearableType type = (WearableType)block.WearableType;
WearableData data = new WearableData();
data.Asset = null;
data.AssetID = block.AssetID;
data.AssetType = WearableTypeToAssetType(type);
data.ItemID = block.ItemID;
data.WearableType = type;
// Add this wearable to our collection
Wearables[type] = data;
}
}
}
else
{
Logger.DebugLog("Duplicate AgentWearablesUpdate received, discarding");
}
}
if (changed)
{
// Fire the callback
OnAgentWearables(new AgentWearablesReplyEventArgs());
}
}
protected void RebakeAvatarTexturesHandler(object sender, PacketReceivedEventArgs e)
{
RebakeAvatarTexturesPacket rebake = (RebakeAvatarTexturesPacket)e.Packet;
// allow the library to do the rebake
if (Client.Settings.SEND_AGENT_APPEARANCE)
{
RequestSetAppearance(true);
}
OnRebakeAvatar(new RebakeAvatarTexturesEventArgs(rebake.TextureData.TextureID));
}
protected void AgentCachedTextureResponseHandler(object sender, PacketReceivedEventArgs e)
{
AgentCachedTextureResponsePacket response = (AgentCachedTextureResponsePacket)e.Packet;
for (int i = 0; i < response.WearableData.Length; i++)
{
AgentCachedTextureResponsePacket.WearableDataBlock block = response.WearableData[i];
BakeType bakeType = (BakeType)block.TextureIndex;
AvatarTextureIndex index = BakeTypeToAgentTextureIndex(bakeType);
Logger.DebugLog("Cache response for " + bakeType + ", TextureID=" + block.TextureID, Client);
if (block.TextureID != UUID.Zero)
{
// A simulator has a cache of this bake layer
// FIXME: Use this. Right now we don't bother to check if this is a foreign host
string host = Utils.BytesToString(block.HostName);
Textures[(int)index].TextureID = block.TextureID;
}
else
{
// The server does not have a cache of this bake layer
// FIXME:
}
}
OnAgentCachedBakes(new AgentCachedBakesReplyEventArgs());
}
private void Network_OnEventQueueRunning(object sender, EventQueueRunningEventArgs e)
{
if (e.Simulator == Client.Network.CurrentSim && Client.Settings.SEND_AGENT_APPEARANCE)
{
// Update appearance each time we enter a new sim and capabilities have been retrieved
Client.Appearance.RequestSetAppearance();
}
}
private void Network_OnDisconnected(object sender, DisconnectedEventArgs e)
{
if (RebakeScheduleTimer != null)
{
RebakeScheduleTimer.Dispose();
RebakeScheduleTimer = null;
}
if (AppearanceThread != null)
{
if (AppearanceThread.IsAlive)
{
AppearanceThread.Abort();
}
AppearanceThread = null;
AppearanceThreadRunning = 0;
}
}
#endregion Callbacks
#region Static Helpers
///
/// Converts a WearableType to a bodypart or clothing WearableType
///
/// A WearableType
/// AssetType.Bodypart or AssetType.Clothing or AssetType.Unknown
public static AssetType WearableTypeToAssetType(WearableType type)
{
switch (type)
{
case WearableType.Shape:
case WearableType.Skin:
case WearableType.Hair:
case WearableType.Eyes:
return AssetType.Bodypart;
case WearableType.Shirt:
case WearableType.Pants:
case WearableType.Shoes:
case WearableType.Socks:
case WearableType.Jacket:
case WearableType.Gloves:
case WearableType.Undershirt:
case WearableType.Underpants:
case WearableType.Skirt:
case WearableType.Tattoo:
case WearableType.Alpha:
case WearableType.Physics:
return AssetType.Clothing;
default:
return AssetType.Unknown;
}
}
///
/// Converts a BakeType to the corresponding baked texture slot in AvatarTextureIndex
///
/// A BakeType
/// The AvatarTextureIndex slot that holds the given BakeType
public static AvatarTextureIndex BakeTypeToAgentTextureIndex(BakeType index)
{
switch (index)
{
case BakeType.Head:
return AvatarTextureIndex.HeadBaked;
case BakeType.UpperBody:
return AvatarTextureIndex.UpperBaked;
case BakeType.LowerBody:
return AvatarTextureIndex.LowerBaked;
case BakeType.Eyes:
return AvatarTextureIndex.EyesBaked;
case BakeType.Skirt:
return AvatarTextureIndex.SkirtBaked;
case BakeType.Hair:
return AvatarTextureIndex.HairBaked;
default:
return AvatarTextureIndex.Unknown;
}
}
///
/// Gives the layer number that is used for morph mask
///
/// >A BakeType
/// Which layer number as defined in BakeTypeToTextures is used for morph mask
public static AvatarTextureIndex MorphLayerForBakeType(BakeType bakeType)
{
// Indexes return here correspond to those returned
// in BakeTypeToTextures(), those two need to be in sync.
// Which wearable layer is used for morph is defined in avatar_lad.xml
// by looking for that has defined in it, and
// looking up which wearable is defined in that layer. Morph mask
// is never combined, it's always a straight copy of one single clothing
// item's alpha channel per bake.
switch (bakeType)
{
case BakeType.Head:
return AvatarTextureIndex.Hair; // hair
case BakeType.UpperBody:
return AvatarTextureIndex.UpperShirt; // shirt
case BakeType.LowerBody:
return AvatarTextureIndex.LowerPants; // lower pants
case BakeType.Skirt:
return AvatarTextureIndex.Skirt; // skirt
case BakeType.Hair:
return AvatarTextureIndex.Hair; // hair
default:
return AvatarTextureIndex.Unknown;
}
}
///
/// Converts a BakeType to a list of the texture slots that make up that bake
///
/// A BakeType
/// A list of texture slots that are inputs for the given bake
public static List BakeTypeToTextures(BakeType bakeType)
{
List textures = new List();
switch (bakeType)
{
case BakeType.Head:
textures.Add(AvatarTextureIndex.HeadBodypaint);
textures.Add(AvatarTextureIndex.HeadTattoo);
textures.Add(AvatarTextureIndex.Hair);
textures.Add(AvatarTextureIndex.HeadAlpha);
break;
case BakeType.UpperBody:
textures.Add(AvatarTextureIndex.UpperBodypaint);
textures.Add(AvatarTextureIndex.UpperTattoo);
textures.Add(AvatarTextureIndex.UpperGloves);
textures.Add(AvatarTextureIndex.UpperUndershirt);
textures.Add(AvatarTextureIndex.UpperShirt);
textures.Add(AvatarTextureIndex.UpperJacket);
textures.Add(AvatarTextureIndex.UpperAlpha);
break;
case BakeType.LowerBody:
textures.Add(AvatarTextureIndex.LowerBodypaint);
textures.Add(AvatarTextureIndex.LowerTattoo);
textures.Add(AvatarTextureIndex.LowerUnderpants);
textures.Add(AvatarTextureIndex.LowerSocks);
textures.Add(AvatarTextureIndex.LowerShoes);
textures.Add(AvatarTextureIndex.LowerPants);
textures.Add(AvatarTextureIndex.LowerJacket);
textures.Add(AvatarTextureIndex.LowerAlpha);
break;
case BakeType.Eyes:
textures.Add(AvatarTextureIndex.EyesIris);
textures.Add(AvatarTextureIndex.EyesAlpha);
break;
case BakeType.Skirt:
textures.Add(AvatarTextureIndex.Skirt);
break;
case BakeType.Hair:
textures.Add(AvatarTextureIndex.Hair);
textures.Add(AvatarTextureIndex.HairAlpha);
break;
}
return textures;
}
#endregion Static Helpers
}
#region AppearanceManager EventArgs Classes
/// Contains the Event data returned from the data server from an AgentWearablesRequest
public class AgentWearablesReplyEventArgs : EventArgs
{
/// Construct a new instance of the AgentWearablesReplyEventArgs class
public AgentWearablesReplyEventArgs()
{
}
}
/// Contains the Event data returned from the data server from an AgentCachedTextureResponse
public class AgentCachedBakesReplyEventArgs : EventArgs
{
/// Construct a new instance of the AgentCachedBakesReplyEventArgs class
public AgentCachedBakesReplyEventArgs()
{
}
}
/// Contains the Event data returned from an AppearanceSetRequest
public class AppearanceSetEventArgs : EventArgs
{
private readonly bool m_success;
/// Indicates whether appearance setting was successful
public bool Success { get { return m_success; } }
///
/// Triggered when appearance data is sent to the sim and
/// the main appearance thread is done.
/// Indicates whether appearance setting was successful
public AppearanceSetEventArgs(bool success)
{
this.m_success = success;
}
}
/// Contains the Event data returned from the data server from an RebakeAvatarTextures
public class RebakeAvatarTexturesEventArgs : EventArgs
{
private readonly UUID m_textureID;
/// The ID of the Texture Layer to bake
public UUID TextureID { get { return m_textureID; } }
///
/// Triggered when the simulator sends a request for this agent to rebake
/// its appearance
///
/// The ID of the Texture Layer to bake
public RebakeAvatarTexturesEventArgs(UUID textureID)
{
this.m_textureID = textureID;
}
}
#endregion
}