/* * Copyright (c) 2006-2016, openmetaverse.co * 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.co 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.Concurrent; using System.Collections.Generic; using System.Threading; using System.Linq; using System.Threading.Tasks; using OpenMetaverse.Packets; using OpenMetaverse.Imaging; using OpenMetaverse.Assets; using OpenMetaverse.Http; using OpenMetaverse.StructuredData; using LibreMetaverse; 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, // pre-composited UpperBaked, // pre-composited LowerBaked, // pre-composited EyesBaked, // pre-composited LowerSocks, UpperJacket, LowerJacket, UpperGloves, UpperUndershirt, LowerUnderpants, Skirt, SkirtBaked, // pre-composited HairBaked, // pre-composited LowerAlpha, UpperAlpha, HeadAlpha, EyesAlpha, HairAlpha, HeadTattoo, UpperTattoo, LowerTattoo, HeadUniversalTattoo, UpperUniversalTattoo, LowerUniversalTattoo, SkirtTattoo, HairTattoo, EyesTattoo, LeftArmTattoo, LeftLegTattoo, Aux1Tattoo, Aux2Tattoo, Aux3Tattoo, LeftArmBaked, // pre-composited LegLegBaked, // pre-composited Aux1Baked, // pre-composited Aux2Baked, // pre-composited Aux3Baked, // pre-composited NumberOfEntries } /// /// Bake layers for avatar appearance /// public enum BakeType { Unknown = -1, Head = 0, UpperBody = 1, LowerBody = 2, Eyes = 3, Skirt = 4, Hair = 5, BakedLeftArm, BakedLeftLeg, BakedAux1, BakedAux2, BakedAux3 } /// /// Appearance Flags, introdued with server side baking, currently unused /// [Flags] public enum AppearanceFlags : uint { None = 0 } #endregion Enums [Serializable] public class AppearanceManagerException : Exception { public AppearanceManagerException(string message) : base(message) { } } 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 allowed for each avatar public const int WEARABLE_COUNT_MAX = 60; /// 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.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Hair, WearableType.Alpha, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid }, new[] { WearableType.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Shirt, WearableType.Jacket, WearableType.Gloves, WearableType.Undershirt, WearableType.Alpha, WearableType.Invalid }, new[] { WearableType.Shape, WearableType.Skin, WearableType.Tattoo, WearableType.Pants, WearableType.Shoes, WearableType.Socks, WearableType.Jacket, WearableType.Underpants, WearableType.Alpha }, new[] { WearableType.Eyes, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid }, new[] { WearableType.Skirt, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid, WearableType.Invalid }, new[] { 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("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) { m_AgentWearablesReply?.Invoke(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) { m_AgentCachedBakesReply?.Invoke(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) { m_AppearanceSet?.Invoke(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) { m_RebakeAvatarReply?.Invoke(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 => AppearanceThreadRunning != 0; /// Visual parameters last sent to the sim public byte[] MyVisualParameters; /// Textures about this client sent to the sim public Primitive.TextureEntry MyTextures; #endregion Properties #region Private Members /// A cache of wearables currently being worn private MultiValueDictionary Wearables = new MultiValueDictionary(); /// A cache of attachments currently being worn private ConcurrentDictionary Attachments = new ConcurrentDictionary(); /// 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; /// /// Main appearance cancellation token source /// private CancellationTokenSource CancellationTokenSource; /// /// Is server baking complete. It needs doing only once /// private bool ServerBakingDone = false; private static readonly ParallelOptions _parallelOptions = new ParallelOptions() { MaxDegreeOfParallelism = MAX_CONCURRENT_DOWNLOADS }; #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; } CancellationTokenSource = new CancellationTokenSource(); // This is the first time setting appearance, run through the entire sequence AppearanceThread = new Thread( () => { var cancellationToken = CancellationTokenSource.Token; 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; } // FIXME: we really need to make this better... cancellationToken.ThrowIfCancellationRequested(); // Retrieve the worn attachments. if (!GetAgentAttachments()) { Logger.Log( "Failed to retrieve a list of current agent attachments, appearance cannot be set", Helpers.LogLevel.Error, Client); throw new AppearanceManagerException( "Failed to retrieve a list of current agent attachments, appearance cannot be set"); } // Is this server side baking enabled sim if (ServerBakingRegion()) { if (!GotWearables) { // Fetch a list of the current agent wearables if (GetAgentWearables()) { GotWearables = true; } } cancellationToken.ThrowIfCancellationRequested(); if (!ServerBakingDone || forceRebake) { if (UpdateAvatarAppearanceAsync(cancellationToken).Result) { 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 AppearanceManagerException( "Failed to retrieve a list of current agent wearables, appearance cannot be set"); } GotWearables = true; } cancellationToken.ThrowIfCancellationRequested(); // 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); } cancellationToken.ThrowIfCancellationRequested(); // 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); } } cancellationToken.ThrowIfCancellationRequested(); // 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); } cancellationToken.ThrowIfCancellationRequested(); // Send the appearance packet RequestAgentSetAppearance(); } } catch (Exception e) { if (e is OperationCanceledException) { Logger.Log( "Setting appearance cancelled.", Helpers.LogLevel.Debug, Client); } else { Logger.Log( $"Failed to set appearance with exception {e}", Helpers.LogLevel.Warning, Client); } success = false; } finally { AppearanceThreadRunning = 0; OnAppearanceSet(new AppearanceSetEventArgs(success)); } } ) { Name = "Appearance", 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 { AgentData = { AgentID = Client.Self.AgentID, 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() { var 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]; if (type == WearableType.Invalid) continue; hash = Wearables.Where(e => e.Key == type) .SelectMany(e => e.Value).Aggregate(hash, (current, worn) => current ^ worn.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 { ID = hash, 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) return; AgentCachedTexturePacket cache = new AgentCachedTexturePacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, SerialNum = Interlocked.Increment(ref CacheCheckSerialNum) }, WearableData = hashes.ToArray() }; Client.Network.SendPacket(cache); } /// /// OBSOLETE! Returns the AssetID of the first 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 [Obsolete] public UUID GetWearableAsset(WearableType type) { return Wearables.TryGetValue(type, out var wearableList) ? wearableList.First().AssetID : UUID.Zero; } public IEnumerable GetWearableAssets(WearableType type) { return Wearables.Where(e => e.Key == type).SelectMany(e => e.Value) .Select(wearable => wearable.AssetID); } /// /// Add a wearable to the current outfit and set appearance /// /// Wearable to be added to the outfit public void AddToOutfit(InventoryItem wearableItem) { var 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) { var wearableItems = new List { wearableItem }; AddToOutfit(wearableItems, replace); } /// /// Add a list of wearables to the current outfit and set appearance /// /// List of wearable inventory items to /// be added to the outfit 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) { var wearables = wearableItems.OfType() .ToList(); var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject) .ToList(); lock (Wearables) { // Add the given wearables to the wearables collection foreach (InventoryWearable wearableItem in wearables) { var wd = new WearableData { AssetID = wearableItem.AssetUUID, AssetType = wearableItem.AssetType, ItemID = wearableItem.UUID, WearableType = wearableItem.WearableType }; if (replace || wearableItem.AssetType == AssetType.Bodypart) { // Dump everything from the key Wearables.Remove(wearableItem.WearableType); } Wearables.Add(wearableItem.WearableType, wd); } } if (attachments.Any()) { AddAttachments(attachments.ToList(), false, replace); } if (wearables.Any()) { 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 { 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) { var wearables = wearableItems.OfType() .ToList(); var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject) .ToList(); bool needSetAppearance = false; lock (Wearables) { // Remove the given wearables from the wearables collection foreach (InventoryWearable wearable in wearables) { if (wearable.AssetType != AssetType.Bodypart // Remove if it's not a body part && Wearables.ContainsKey(wearable.WearableType)) // And we have that wearable type { var worn = Wearables.Where(e => e.Key == wearable.WearableType) .SelectMany(e => e.Value); WearableData wearableData = worn.FirstOrDefault(item => item.ItemID == wearable.UUID); if (wearableData == null) continue; Wearables.Remove(wearable.WearableType, wearableData); needSetAppearance = true; } } } foreach (var attachment in attachments) { Detach(attachment.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) { var wearables = wearableItems.OfType() .ToList(); var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject) .ToList(); 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++; } try { ReplaceOutfit(wearables); AddAttachments(attachments, true, false); SendAgentIsNowWearing(); DelayedRequestSetAppearance(); } catch (AppearanceManagerException e) { Logger.Log(e.Message, Helpers.LogLevel.Error, Client); } } /// /// 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 (var wearableType in Wearables) { if (wearableType.Value.Any(wearable => wearable.ItemID == item.UUID)) { return wearableType.Key; } } } return WearableType.Invalid; } /// /// Returns a collection 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 IEnumerable GetWearables() { lock (Wearables) { // ToList will copy the IEnumerable return Wearables.SelectMany(e => e.Value).ToList(); } } public MultiValueDictionary GetWearablesByType() { lock (Wearables) { return new MultiValueDictionary(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) { var wearableItems = wearables.OfType().ToList(); 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 /// 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 = true) { // Use RezMultipleAttachmentsFromInv to clear out current attachments, and attach new ones RezMultipleAttachmentsFromInvPacket attachmentsPacket = new RezMultipleAttachmentsFromInvPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, HeaderData = { CompoundMsgID = UUID.Random(), FirstDetachAll = removeExistingFirst, TotalObjects = (byte) attachments.Count }, ObjectData = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock[attachments.Count] }; if (removeExistingFirst) { Attachments.Clear(); } for (int i = 0; i < attachments.Count; i++) { if (attachments[i] is InventoryAttachment) { InventoryAttachment attachment = (InventoryAttachment)attachments[i]; attachmentsPacket.ObjectData[i] = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock { AttachmentPt = replace ? (byte)attachment.AttachmentPoint : (byte)(ATTACHMENT_ADD | (byte)attachment.AttachmentPoint), EveryoneMask = (uint)attachment.Permissions.EveryoneMask, GroupMask = (uint)attachment.Permissions.GroupMask, ItemFlags = (uint)attachment.Flags, ItemID = attachment.UUID, Name = Utils.StringToBytes(attachment.Name), Description = Utils.StringToBytes(attachment.Description), NextOwnerMask = (uint)attachment.Permissions.NextOwnerMask, OwnerID = attachment.OwnerID }; Attachments.AddOrUpdate(attachments[i].UUID, attachment.AttachmentPoint, (id, point) => attachment.AttachmentPoint); } else if (attachments[i] is InventoryObject) { InventoryObject attachment = (InventoryObject)attachments[i]; attachmentsPacket.ObjectData[i] = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock { AttachmentPt = replace ? (byte)0 : ATTACHMENT_ADD, EveryoneMask = (uint)attachment.Permissions.EveryoneMask, GroupMask = (uint)attachment.Permissions.GroupMask, ItemFlags = (uint)attachment.Flags, ItemID = attachment.UUID, Name = Utils.StringToBytes(attachment.Name), Description = Utils.StringToBytes(attachment.Description), NextOwnerMask = (uint)attachment.Permissions.NextOwnerMask, OwnerID = attachment.OwnerID }; Attachments.AddOrUpdate(attachments[i].UUID, attachment.AttachPoint, (id, point) => attachment.AttachPoint); } 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 { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, ObjectData = { AttachmentPt = replace ? (byte) attachPoint : (byte) (ATTACHMENT_ADD | (byte) attachPoint), Description = Utils.StringToBytes(description), EveryoneMask = (uint) perms.EveryoneMask, GroupMask = (uint) perms.GroupMask, ItemFlags = itemFlags, ItemID = itemID, Name = Utils.StringToBytes(name), NextOwnerMask = (uint) perms.NextOwnerMask, OwnerID = ownerID } }; Attachments.AddOrUpdate(itemID, attachPoint, (id, point) => attachPoint); 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) { var detach = new DetachAttachmentIntoInvPacket { ObjectData = { AgentID = Client.Self.AgentID, ItemID = itemID } }; Attachments.TryRemove(itemID, out _); Client.Network.SendPacket(detach); } /// /// Retrieves the currently worn attachments. /// removed async and Task due to no await. /// /// private bool GetAgentAttachments() { var objectsPrimitives = Client.Network.CurrentSim.ObjectsPrimitives.Copy(); // No primitives found. if (objectsPrimitives.Count == 0) { return true; } // Build a list of objects that are attached to the avatar. var primitives = objectsPrimitives.Where(primitive => primitive.Value.ParentID == Client.Self.LocalID) .Select(primitive => primitive.Value); var enumerable = primitives as Primitive[] ?? primitives.ToArray(); if (enumerable.Length == 0) { return true; } foreach (var primitive in enumerable) { // Find the inventory UUID from the primitive name-value collection. var nameValue = primitive.NameValues.SingleOrDefault(item => item.Name.Equals("AttachItemID")); if (nameValue.Equals(default(NameValue))) { continue; } // Retrieve the inventory item UUID from the name values. var inventoryItemId = (string)nameValue.Value; if (string.IsNullOrEmpty(inventoryItemId) || !UUID.TryParse(inventoryItemId, out var itemID)) { return false; } // Determine the attachment point from the primitive. var attachmentPoint = (AttachmentPoint)(((primitive.PrimData.State & 0xF0) >> 4) | ((primitive.PrimData.State & ~0xF0) << 4)); // Add or update the attachment list. Attachments.AddOrUpdate(itemID, attachmentPoint, (id, point) => attachmentPoint); } return true; } /// /// Returns a collection 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 IEnumerable GetAttachments() { foreach (var item in Attachments) { yield return Client.Inventory.Store[item.Key] as InventoryItem; } } public Dictionary GetAttachmentsByItemId() { return Attachments.ToDictionary(k => k.Key, v => v.Value); } public MultiValueDictionary GetAttachmentsByAttachmentPoint() { var attachmentsByPoint = new MultiValueDictionary(); foreach (var item in Attachments) { // If the item is already retrieved then speed this up. if (Client.Inventory.Store.Contains(item.Key)) { attachmentsByPoint.Add(item.Value, Client.Inventory.Store[item.Key] as InventoryItem); continue; } // Otherwise, retrieve the item off the asset server. var inventoryItem = Client.Inventory.FetchItem(item.Key, Client.Self.AgentID, 1000 * 10); attachmentsByPoint.Add(item.Value, inventoryItem); } return attachmentsByPoint; } public Dictionary GetAttachmentsByInventoryItem() { var attachmentsByInventoryItem = new Dictionary(); foreach (var item in Attachments) { // If the item is already retrieved then speed this up. if (Client.Inventory.Store.Contains(item.Key)) { attachmentsByInventoryItem.Add(Client.Inventory.Store[item.Key] as InventoryItem, item.Value); continue; } // Otherwise, retrieve the item off the asset server. var inventoryItem = Client.Inventory.FetchItem(item.Key, Client.Self.AgentID, 1000 * 10); attachmentsByInventoryItem.Add(inventoryItem, item.Value); } return attachmentsByInventoryItem; } #endregion Attachments #region Appearance Helpers /// /// Inform the sim which wearables are part of our current outfit /// private void SendAgentIsNowWearing() { AgentIsNowWearingPacket wearing = new AgentIsNowWearingPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID }, 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 { WearableType = (byte)i, // This appears to be hacked on SL server side to support multi-layers ItemID = Wearables.ContainsKey(type) ? (Wearables[type].First()?.ItemID ?? UUID.Zero) : 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) { // *TODO: This could use some love. We need to sanitize wearable layers, and this may not be // the most efficient way of doing that. var newWearables = new MultiValueDictionary(); var bodyparts = 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 (var wearableType in Wearables) { foreach (var entry in wearableType.Value) { if (entry.AssetType == AssetType.Bodypart) { bodyparts[wearableType.Key] = entry; } } } // Add the given wearables to the new wearables collection foreach (var wearableItem in wearableItems) { WearableData wd = new WearableData { AssetID = wearableItem.AssetUUID, AssetType = wearableItem.AssetType, ItemID = wearableItem.UUID, WearableType = wearableItem.WearableType }; // Body cannot be layered. Overwrite when multiple are selected. if (wearableItem.AssetType == AssetType.Bodypart) { bodyparts[wearableItem.WearableType] = wd; } else { newWearables.Add(wearableItem.WearableType, wd); } } // merge bodyparts into new wearable list foreach (var bodypart in bodyparts) { newWearables.Add(bodypart.Key, bodypart.Value); } // heavy handed body part sanity check if (newWearables.ContainsKey(WearableType.Shape) && newWearables.ContainsKey(WearableType.Skin) && newWearables.ContainsKey(WearableType.Eyes) && newWearables.ContainsKey(WearableType.Hair)) { // Replace the Wearables collection Wearables = newWearables; } else { throw new AppearanceManagerException( "Wearables collection does not contain all required body parts; appearance cannot be set"); } } } /// /// 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 - 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 = p.VisualColorParam.Colors[indexa]; Color4 c2 = 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 /// Texture data public static void DecodeWearableParams(WearableData wearable, ref TextureData[] textures) { var alphaMasks = new Dictionary(); var colorParams = new List(); // Populate collection of alpha masks from visual params // also add color tinting information foreach (var kvp in wearable.Asset.Params) { if (!VisualParams.Params.ContainsKey(kvp.Key)) continue; VisualParam p = VisualParams.Params[kvp.Key]; ColorParamInfo colorInfo = new ColorParamInfo { WearableType = wearable.WearableType, VisualParam = p, Value = kvp.Value }; // Color params if (p.ColorParams.HasValue) { colorInfo.VisualColorParam = p.ColorParams.Value; switch (wearable.WearableType) { case WearableType.Tattoo: if (kvp.Key == 1062 || kvp.Key == 1063 || kvp.Key == 1064) { colorParams.Add(colorInfo); } break; case WearableType.Jacket: if (kvp.Key == 809 || kvp.Key == 810 || kvp.Key == 811) { colorParams.Add(colorInfo); } break; case 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); } break; case 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); } break; default: colorParams.Add(colorInfo); break; } } // 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) { foreach (int t in p.Drivers) { if (VisualParams.Params.ContainsKey(t)) { VisualParam driver = VisualParams.Params[t]; if (driver.AlphaParams.HasValue && driver.AlphaParams.Value.TGAFile != string.Empty && !driver.IsBumpAttribute && !alphaMasks.ContainsKey(driver.AlphaParams.Value)) { alphaMasks.Add(driver.AlphaParams.Value, Math.Abs(kvp.Value) < float.Epsilon ? 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 (var 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 textures[i].TextureID = entry.Value != AppearanceManager.DEFAULT_AVATAR_TEXTURE ? entry.Value : 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 var wearables = new List(GetWearables()); // We will refresh the textures (zero out all non bake textures) for (int i = 0; i < Textures.Length; i++) { bool isBake = BakeIndexToTextureIndex.Any(t => t == i); if (!isBake) Textures[i] = new TextureData(); } int pendingWearables = wearables.Count; foreach (var wearable in wearables) { if (wearable.Asset == null) continue; DecodeWearableParams(wearable, ref Textures); --pendingWearables; } if (pendingWearables == 0) return true; Logger.DebugLog("Downloading " + pendingWearables + " wearable assets"); Parallel.ForEach(wearables, _parallelOptions, wearable => { if (wearable.Asset != null) return; 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 assetWearable) { // Update this wearable with the freshly downloaded asset wearable.Asset = assetWearable; 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(assetWearable.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 IEnumerable GetTextureDownloadList(BakeType bakeType) { var indices = BakeTypeToTextures(bakeType); var textures = new List(); foreach (AvatarTextureIndex index in indices) { if (index == AvatarTextureIndex.Skirt && !Wearables.ContainsKey(WearableType.Skirt)) continue; 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); } return textures; } /// /// 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(); foreach (BakeType t in bakeLayers) { var layerTextureIDs = GetTextureDownloadList(t); foreach (UUID uuid in layerTextureIDs) { if (!textureIDs.Contains(uuid)) textureIDs.Add(uuid); } } Logger.DebugLog("Downloading " + textureIDs.Count + " textures for baking"); Parallel.ForEach(textureIDs, _parallelOptions, 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( $"Download of texture {textureId} failed with exception {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)) { Logger.DebugLog("texture: " + (AvatarTextureIndex)textureIndex + " skipping not attached"); continue; } Logger.DebugLog("texture: " + (AvatarTextureIndex)textureIndex + " is needed adding to pending Bakes"); pendingBakes.Add((BakeType)bakedIndex); } else { Logger.DebugLog("texture: " + (AvatarTextureIndex)textureIndex + " is ready"); } } if (pendingBakes.Any()) { DownloadTextures(pendingBakes); Parallel.ForEach(pendingBakes, _parallelOptions, 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; } 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) { var textureIndices = BakeTypeToTextures(bakeType); var oven = new Baker(bakeType); foreach (AvatarTextureIndex textureIndex in textureIndices) { 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; } int bakeIndex = (int)BakeTypeToAgentTextureIndex(bakeType); Logger.DebugLog("Saving back to " + (AvatarTextureIndex)bakeIndex); Textures[bakeIndex].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; var 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() { var paramValues = new Dictionary(VisualParams.Params.Count); lock (Wearables) { foreach (var kvp in VisualParams.Params) { // Only Group-0 parameters are sent in AgentSetAppearance packets if (kvp.Value.Group != 0) continue; bool found = false; VisualParam vp = kvp.Value; // Try and find this value in our collection of downloaded wearables foreach (var wearableType in Wearables) { foreach (var data in wearableType.Value) { float paramValue; if (data.Asset != null && data.Asset.Params.TryGetValue(vp.ParamID, out paramValue)) { paramValues.Add(vp.ParamID, paramValue); found = true; break; } } if (found) break; } // Use a default value if we don't have one set for it if (!found) paramValues.Add(vp.ParamID, vp.DefaultValue); } } return paramValues; } /// /// Initiate server baking process /// /// True if the server baking was successful private async Task UpdateAvatarAppearanceAsync(CancellationToken cancellationToken) { Caps caps = Client.Network.CurrentSim.Caps; if (caps == null) { return false; } Uri cap = caps.CapabilityURI("UpdateAvatarAppearance"); if (cap == null) { return false; } InventoryFolder currentoutfitfolder = GetCOF(); if (currentoutfitfolder == null) { return false; } else { // TODO: create Current Outfit Folder } OSDMap request = new OSDMap(1) { ["cof_version"] = currentoutfitfolder.Version }; string msg = "Server side baking failed"; OSD res = null; await Client.HttpCapsClient.PostRequestAsync(cap, OSDFormat.Xml, request, cancellationToken, (response, data, error) => res = OSDParser.Deserialize(data)); if (res is OSDMap result) { 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; } 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; var folderReceived = new AutoResetEvent(false); EventHandler UpdatedCallback = (sender, e) => { if (e.FolderID != Client.Inventory.Store.RootFolder.UUID) return; if (e.Success) { root = Client.Inventory.Store.GetContents(Client.Inventory.Store.RootFolder.UUID); } folderReceived.Set(); }; Client.Inventory.FolderUpdated += UpdatedCallback; Client.Inventory.RequestFolderContentsCap(Client.Inventory.Store.RootFolder.UUID, Client.Self.AgentID, true, true, InventorySortOrder.ByDate); folderReceived.WaitOne(Client.Settings.CAPS_TIMEOUT); Client.Inventory.FolderUpdated -= UpdatedCallback; InventoryFolder COF = null; // COF should be in the root folder. Request update to get the latest versio number if (root == null) return COF; foreach (var baseItem in root) { if (baseItem is InventoryFolder folder && folder.PreferredType == FolderType.CurrentOutfit) { COF = folder; 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 { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, 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; bool wearingPhysics = Wearables.ContainsKey(WearableType.Physics); var nrParams = wearingPhysics ? 251 : 218; set.VisualParam = new AgentSetAppearancePacket.VisualParamBlock[nrParams]; foreach (var kvp in VisualParams.Params) { VisualParam vp = kvp.Value; float paramValue = 0f; bool found = false; foreach (var wearableList in Wearables) { if (wearableList.Value.Any(wearable => wearable.Asset != null && wearable.Asset.Params .TryGetValue(vp.ParamID, out paramValue))) { found = true; break; } } // Try and find this value in our collection of downloaded wearables // 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 { 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++) { if (set.VisualParam[i] != null) { 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++) { Primitive.TextureEntryFace face = te.CreateFace(i); if (Textures[i].TextureID != UUID.Zero) { face.TextureID = Textures[i].TextureID; Logger.DebugLog("Sending texture entry for " + (AvatarTextureIndex)i + " to " + Textures[i].TextureID, Client); } else { Logger.DebugLog("Skipping texture entry for " + (AvatarTextureIndex)i + " its null", 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]; if (type == WearableType.Invalid) continue; hash = Wearables.Where(e => e.Key == type) .SelectMany(e => e.Value).Aggregate(hash, (current, worn) => current ^ worn.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 { TextureIndex = BakeIndexToTextureIndex[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); } 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(); var objects = Client.Inventory.FolderContents(folder, Client.Self.AgentID, false, true, InventorySortOrder.ByName, INVENTORY_TIMEOUT); if (objects != null) { foreach (var ib in objects) { if (ib is InventoryWearable wearable) { Logger.DebugLog("Adding wearable " + wearable.Name, Client); wearables.Add(wearable); } else if (ib is InventoryAttachment attachment) { Logger.DebugLog("Adding attachment (attachment) " + attachment.Name, Client); attachments.Add(attachment); } else if (ib is InventoryObject inventoryObject) { Logger.DebugLog("Adding attachment (object) " + inventoryObject.Name, Client); attachments.Add(inventoryObject); } 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; var update = (AgentWearablesUpdatePacket)e.Packet; lock (Wearables) { #region Test if anything changed in this update foreach (var block in update.WearableData) { if (block.AssetID != UUID.Zero) { WearableType type = (WearableType)block.WearableType; if (Wearables.ContainsKey(type)) { // HACK: I'm so tired and this is so bad. bool match = false; foreach (var wearable in Wearables.Where(w => w.Key == type).SelectMany(w => w.Value)) { if (wearable.AssetID == block.AssetID || wearable.ItemID == block.ItemID) { match = true; } } changed = !match; if (changed) 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(); foreach (AgentWearablesUpdatePacket.WearableDataBlock block in update.WearableData) { if (block.AssetID == UUID.Zero) continue; WearableType type = (WearableType)block.WearableType; WearableData data = new WearableData { Asset = null, AssetID = block.AssetID, AssetType = WearableTypeToAssetType(type), ItemID = block.ItemID, WearableType = type }; // Add this wearable to our collection Wearables.Add(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) { var response = (AgentCachedTextureResponsePacket)e.Packet; foreach (var block in response.WearableData) { 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 (CancellationTokenSource != null) { CancellationTokenSource.Cancel(); CancellationTokenSource.Dispose(); CancellationTokenSource = null; } if (AppearanceThread != null) { 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; case BakeType.BakedLeftArm: return AvatarTextureIndex.LeftArmBaked; case BakeType.BakedLeftLeg: return AvatarTextureIndex.LegLegBaked; case BakeType.BakedAux1: return AvatarTextureIndex.Aux1Baked; case BakeType.BakedAux2: return AvatarTextureIndex.Aux2Baked; case BakeType.BakedAux3: return AvatarTextureIndex.Aux3Baked; 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 case BakeType.BakedLeftArm: return AvatarTextureIndex.LeftArmTattoo; case BakeType.BakedLeftLeg: return AvatarTextureIndex.LeftLegTattoo; case BakeType.BakedAux1: return AvatarTextureIndex.Aux1Tattoo; case BakeType.BakedAux2: return AvatarTextureIndex.Aux2Tattoo; case BakeType.BakedAux3: return AvatarTextureIndex.Aux3Tattoo; 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) { var 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 { } /// Contains the Event data returned from the data server from an AgentCachedTextureResponse public class AgentCachedBakesReplyEventArgs : EventArgs { } /// Contains the Event data returned from an AppearanceSetRequest public class AppearanceSetEventArgs : EventArgs { /// Indicates whether appearance setting was successful public bool Success { get; } /// /// 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.Success = success; } } /// Contains the Event data returned from the data server from an RebakeAvatarTextures public class RebakeAvatarTexturesEventArgs : EventArgs { /// The ID of the Texture Layer to bake public UUID TextureID { get; } /// /// 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.TextureID = textureID; } } #endregion }