/* * 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 }