Files
libremetaverse/LibreMetaverse/AppearanceManager.cs

2979 lines
126 KiB
C#

/*
* Copyright (c) 2006-2016, openmetaverse.co
* Copyright (c) 2024-2025, Sjofn LLC.
* 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.Net.Http;
using System.Threading.Tasks;
using OpenMetaverse.Packets;
using OpenMetaverse.Imaging;
using OpenMetaverse.Assets;
using OpenMetaverse.StructuredData;
using LibreMetaverse;
namespace OpenMetaverse
{
#region Enums
/// <summary>
/// Index of TextureEntry slots for avatar appearances
/// </summary>
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
}
/// <summary>
/// Bake layers for avatar appearance
/// </summary>
public enum BakeType
{
Unknown = -1,
Head = 0,
UpperBody = 1,
LowerBody = 2,
Eyes = 3,
Skirt = 4,
Hair = 5,
BakedLeftArm,
BakedLeftLeg,
BakedAux1,
BakedAux2,
BakedAux3
}
/// <summary>
/// Appearance Flags, introduced with server side baking, currently unused
/// </summary>
[Flags]
public enum AppearanceFlags : uint
{
None = 0
}
#endregion Enums
public class AppearanceManagerException : Exception
{
public AppearanceManagerException(string message)
: base(message) { }
}
public class AppearanceManager
{
#region Constants
/// <summary>Mask for multiple attachments</summary>
public static readonly byte ATTACHMENT_ADD = 0x80;
/// <summary>Mapping between BakeType and AvatarTextureIndex</summary>
public static readonly byte[] BakeIndexToTextureIndex = new byte[BAKED_TEXTURE_COUNT] { 8, 9, 10, 11, 19, 20 };
/// <summary>Maximum number of concurrent downloads for wearable assets and textures</summary>
private const int MAX_CONCURRENT_DOWNLOADS = 5;
/// <summary>Maximum number of concurrent uploads for baked textures</summary>
private const int MAX_CONCURRENT_UPLOADS = 6;
/// <summary>Timeout for fetching inventory listings</summary>
private readonly TimeSpan INVENTORY_TIMEOUT = TimeSpan.FromSeconds(30);
/// <summary>Timeout for fetching a single wearable, or receiving a single packet response</summary>
private readonly TimeSpan WEARABLE_TIMEOUT = TimeSpan.FromSeconds(30);
/// <summary>Timeout for fetching a single texture</summary>
private readonly TimeSpan TEXTURE_TIMEOUT = TimeSpan.FromSeconds(120);
/// <summary>Timeout for uploading a single baked texture</summary>
private readonly TimeSpan UPLOAD_TIMEOUT = TimeSpan.FromSeconds(90);
/// <summary>Number of times to retry bake upload</summary>
private const int UPLOAD_RETRIES = 2;
/// <summary>When changing outfit, kick off rebake after REBAKE_DELAY has passed since the last change</summary>
private const int REBAKE_DELAY = 1000 * 5;
/// <summary>Total number of wearables allowed for each avatar</summary>
public const int WEARABLE_COUNT_MAX = 60;
/// <summary>Total number of wearables for each avatar</summary>
public const int WEARABLE_COUNT = 16;
/// <summary>Total number of baked textures on each avatar</summary>
public const int BAKED_TEXTURE_COUNT = 6;
/// <summary>Total number of wearables per bake layer</summary>
public const int WEARABLES_PER_LAYER = 9;
/// <summary>Map of what wearables are included in each bake</summary>
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 }
};
/// <summary>Magic values to finalize the cache check hashes for each
/// bake</summary>
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")
};
/// <summary>Default avatar texture, used to detect when a custom
/// texture is not set for a face</summary>
public static readonly UUID DEFAULT_AVATAR_TEXTURE = new UUID("c228d1cf-4b5d-4ba8-84f4-899a0796aa97");
#endregion Constants
#region Structs / Classes
/// <summary>
/// Contains information about a wearable inventory item
/// </summary>
public class WearableData
{
/// <summary>Inventory ItemID of the wearable</summary>
public UUID ItemID;
/// <summary>AssetID of the wearable asset</summary>
public UUID AssetID;
/// <summary>WearableType of the wearable</summary>
public WearableType WearableType;
/// <summary>AssetType of the wearable</summary>
public AssetType AssetType;
/// <summary>Asset data for the wearable</summary>
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)");
}
}
/// <summary>
/// Data collected from visual params for each wearable
/// needed for the calculation of the color
/// </summary>
public struct ColorParamInfo
{
public VisualParam VisualParam;
public VisualColorParam VisualColorParam;
public float Value;
public WearableType WearableType;
}
/// <summary>
/// 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
/// </summary>
public struct TextureData
{
/// <summary>A texture AssetID</summary>
public UUID TextureID;
/// <summary>Asset data for the texture</summary>
public AssetTexture Texture;
/// <summary>Collection of alpha masks that needs applying</summary>
public Dictionary<VisualAlphaParam, float> AlphaMasks;
/// <summary>Tint that should be applied to the texture</summary>
public Color4 Color;
/// <summary>Where on avatar does this texture belong</summary>
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
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<AgentWearablesReplyEventArgs> m_AgentWearablesReply;
/// <summary>Raises the AgentWearablesReply event</summary>
/// <param name="e">An AgentWearablesReplyEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnAgentWearables(AgentWearablesReplyEventArgs e)
{
m_AgentWearablesReply?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_AgentWearablesLock = new object();
/// <summary>Triggered when a legacy AgentWearablesUpdate packet is received
/// or a request has been made for COF contents has populated <see cref="Wearables"/>
/// telling us what the avatar is currently wearing</summary>
/// <seealso cref="RequestAgentWorn" />
/// <seealso cref="RequestAgentWearablesLLUDP"/>
public event EventHandler<AgentWearablesReplyEventArgs> AgentWearablesReply
{
add { lock (m_AgentWearablesLock) { m_AgentWearablesReply += value; } }
remove { lock (m_AgentWearablesLock) { m_AgentWearablesReply -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<AgentCachedBakesReplyEventArgs> m_AgentCachedBakesReply;
/// <summary>Raises the CachedBakesReply event</summary>
/// <param name="e">An AgentCachedBakesReplyEventArgs object containing the
/// data returned from the data server AgentCachedTextureResponse</param>
protected virtual void OnAgentCachedBakes(AgentCachedBakesReplyEventArgs e)
{
m_AgentCachedBakesReply?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_AgentCachedBakesLock = new object();
/// <summary>Raised when an AgentCachedTextureResponse packet is
/// received, giving a list of cached bakes that were found on the
/// simulator
/// <seealso cref="RequestCachedBakes"/> request.</summary>
public event EventHandler<AgentCachedBakesReplyEventArgs> CachedBakesReply
{
add { lock (m_AgentCachedBakesLock) { m_AgentCachedBakesReply += value; } }
remove { lock (m_AgentCachedBakesLock) { m_AgentCachedBakesReply -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<AppearanceSetEventArgs> m_AppearanceSet;
/// <summary>Raises the AppearanceSet event</summary>
/// <param name="e">An AppearanceSetEventArgs object indicating if the operation was successful</param>
protected virtual void OnAppearanceSet(AppearanceSetEventArgs e)
{
m_AppearanceSet?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_AppearanceSetLock = new object();
/// <summary>
/// Raised when appearance data is sent to the simulator, also indicates
/// the main appearance thread is finished.
/// </summary>
/// <seealso cref="RequestAgentSetAppearance"/> request.
public event EventHandler<AppearanceSetEventArgs> AppearanceSet
{
add { lock (m_AppearanceSetLock) { m_AppearanceSet += value; } }
remove { lock (m_AppearanceSetLock) { m_AppearanceSet -= value; } }
}
/// <summary>The event subscribers. null if no subscribers</summary>
private EventHandler<RebakeAvatarTexturesEventArgs> m_RebakeAvatarReply;
/// <summary>Raises the RebakeAvatarRequested event</summary>
/// <param name="e">An RebakeAvatarTexturesEventArgs object containing the
/// data returned from the data server</param>
protected virtual void OnRebakeAvatar(RebakeAvatarTexturesEventArgs e)
{
m_RebakeAvatarReply?.Invoke(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_RebakeAvatarLock = new object();
/// <summary>
/// Triggered when the simulator requests the agent rebake its appearance.
/// </summary>
/// <seealso cref="RebakeAvatarRequest"/>
public event EventHandler<RebakeAvatarTexturesEventArgs> RebakeAvatarRequested
{
add { lock (m_RebakeAvatarLock) { m_RebakeAvatarReply += value; } }
remove { lock (m_RebakeAvatarLock) { m_RebakeAvatarReply -= value; } }
}
#endregion
#region Properties and public fields
/// <summary>
/// Returns true if AppearanceManager is busy and trying to set or change appearance will fail
/// </summary>
public bool ManagerBusy => AppearanceThreadRunning != 0;
/// <summary>Visual parameters last sent to the sim</summary>
public byte[] MyVisualParameters;
/// <summary>Textures about this client sent to the sim</summary>
public Primitive.TextureEntry MyTextures;
#endregion Properties
#region Private Members
/// <summary>A cache of wearables currently being worn</summary>
private MultiValueDictionary<WearableType, WearableData> Wearables = new MultiValueDictionary<WearableType, WearableData>();
/// <summary>A cache of attachments currently being worn</summary>
private readonly ConcurrentDictionary<UUID, AttachmentPoint> Attachments = new ConcurrentDictionary<UUID, AttachmentPoint>();
/// <summary>A cache of textures currently being worn</summary>
private TextureData[] Textures = new TextureData[(int)AvatarTextureIndex.NumberOfEntries];
/// <summary>Incrementing serial number for AgentCachedTexture packets</summary>
private int CacheCheckSerialNum = -1;
/// <summary>Incrementing serial number for AgentSetAppearance packets</summary>
private int SetAppearanceSerialNum = 0;
/// <summary>Indicates whether the appearance thread is currently
/// running, to prevent multiple appearance threads from running
/// simultaneously</summary>
private int AppearanceThreadRunning = 0;
/// <summary>Reference to our agent</summary>
private readonly GridClient Client;
/// <summary>
/// Timer used for delaying rebake on changing outfit
/// </summary>
private Timer RebakeScheduleTimer;
/// <summary>
/// Main appearance thread
/// </summary>
private Thread AppearanceThread;
/// <summary>
/// Main appearance cancellation token source
/// </summary>
private CancellationTokenSource AppearanceCts;
/// <summary>
/// Is server baking complete. It needs doing only once
/// </summary>
private bool ServerBakingDone = false;
private static readonly ParallelOptions _parallelOptions = new ParallelOptions()
{
MaxDegreeOfParallelism = MAX_CONCURRENT_DOWNLOADS
};
#endregion Private Members
/// <summary>
/// Default constructor
/// </summary>
/// <param name="client">A reference to our agent</param>
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.Objects.ObjectUpdate += Objects_AttachmentUpdate;
Client.Network.Disconnected += Network_OnDisconnected;
Client.Network.SimChanged += Network_OnSimChanged;
}
#region Publics Methods
/// <summary>
/// Starts the appearance setting thread
/// </summary>
/// <param name="forceRebake">True to force rebaking, otherwise false</param>
public void RequestSetAppearance(bool forceRebake = false)
{
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;
}
AppearanceCts = new CancellationTokenSource();
// This is the first time setting appearance, run through the entire sequence
AppearanceThread = new Thread(
() =>
{
var cancellationToken = AppearanceCts.Token;
var success = true;
try
{
if (forceRebake)
{
// Set all the baked textures to UUID.Zero to force rebaking
for (var 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 (!GatherAgentAttachments())
{
Logger.Log(
"Failed to retrieve a list of current agent attachments, appearance cannot be set",
Helpers.LogLevel.Warning,
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 (!Wearables.Any())
{
// Fetch a list of the current agent wearables
GatherAgentWearables();
}
cancellationToken.ThrowIfCancellationRequested();
if (!ServerBakingDone || forceRebake)
{
if (UpdateAvatarAppearanceAsync(cancellationToken).Result)
{
ServerBakingDone = true;
}
else
{
success = false;
}
}
}
else // Classic client side baking
{
if (!Wearables.Any())
{
// Fetch a list of the current agent wearables
if (!GatherAgentWearables())
{
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");
}
}
cancellationToken.ThrowIfCancellationRequested();
// If we get back to server side baking region re-request server bake
ServerBakingDone = false;
// Download and parse all 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 a rebake, 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();
}
/// <summary>
/// Check if current region supports server side baking
/// </summary>
/// <returns>True if server side baking support is detected</returns>
public bool ServerBakingRegion()
{
return Client.Network.CurrentSim != null &&
((Client.Network.CurrentSim.Protocols & RegionProtocols.AgentAppearanceService) != 0);
}
/// <summary>
/// Returns a List of worn items in COF. Also populates <see cref="Wearables"/>
/// and <see cref="Attachments"/> before firing <see cref="OnAgentWearables"/>
/// </summary>
/// <returns><see cref="List{T}"/> of <see cref="InventoryBase"/> in COF</returns>
public List<InventoryBase> RequestAgentWorn()
{
var cof = GetCurrentOutfitFolder(CancellationToken.None).Result;
if (cof == null)
{
Logger.Log("Could not retrieve Current Outfit folder", Helpers.LogLevel.Warning, Client);
return null;
}
var contents = Client.Inventory.FolderContents(cof.UUID, cof.OwnerID, true, true,
InventorySortOrder.ByDate, TimeSpan.FromMinutes(1), true);
var wearables = new MultiValueDictionary<WearableType, WearableData>();
foreach (var item in contents)
{
switch (item)
{
case InventoryWearable wearable:
{
var w = wearable;
if (wearable.IsLink() && Client.Inventory.Store.Contains(wearable.ActualUUID))
{
w = Client.Inventory.Store[wearable.ActualUUID] as InventoryWearable;
}
wearables.Add(w.WearableType, new WearableData()
{
ItemID = w.UUID,
AssetID = w.ActualUUID,
AssetType = w.AssetType,
WearableType = w.WearableType
});
break;
}
case InventoryAttachment attachment:
{
var a = attachment;
if (attachment.IsLink() && Client.Inventory.Store.Contains(attachment.ActualUUID))
{
a = Client.Inventory.Store[attachment.ActualUUID] as InventoryAttachment;
}
Attachments.AddOrUpdate(a.ActualUUID, a.AttachmentPoint, (id, point) => a.AttachmentPoint);
break;
}
case InventoryObject attachedObject:
{
var a = attachedObject;
if (attachedObject.IsLink() && Client.Inventory.Store.Contains(attachedObject.ActualUUID))
{
a = Client.Inventory.Store[attachedObject.ActualUUID] as InventoryObject;
}
Attachments.AddOrUpdate(a.ActualUUID, a.AttachPoint, (id, point) => a.AttachPoint);
break;
}
}
}
lock (Wearables) { Wearables = wearables; }
OnAgentWearables(new AgentWearablesReplyEventArgs());
return contents;
}
/// <summary>
/// Ask the server what textures our agent is currently wearing
/// </summary>
[Obsolete("Second Life sends dummy information back post SSB. Use RequestAgentWorn")]
public void RequestAgentWearablesLLUDP()
{
var request = new AgentWearablesRequestPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
}
};
Client.Network.SendPacket(request);
}
/// <summary>
/// Build hashes out of the texture assetIDs for each baking layer to
/// ask the simulator whether it has cached copies of each baked texture
/// </summary>
public void RequestCachedBakes()
{
var hashes = new List<AgentCachedTexturePacket.WearableDataBlock>();
// Build hashes for each of the bake layers from the individual components
lock (Wearables)
{
for (var 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
var hash = UUID.Zero;
for (var wearableIndex = 0; wearableIndex < WEARABLES_PER_LAYER; wearableIndex++)
{
var 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
var 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;
var cache = new AgentCachedTexturePacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID,
SerialNum = Interlocked.Increment(ref CacheCheckSerialNum)
},
WearableData = hashes.ToArray()
};
Client.Network.SendPacket(cache);
}
/// <summary>
/// OBSOLETE! Returns the AssetID of the first asset that is currently
/// being worn in a given WearableType slot
/// </summary>
/// <param name="type">WearableType slot to get the AssetID for</param>
/// <returns>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</returns>
[Obsolete("Returns the first asset currently being worn, prefer GetWearableAssets")]
public UUID GetWearableAsset(WearableType type)
{
return Wearables.TryGetValue(type, out var wearableList)
? wearableList.First().AssetID
: UUID.Zero;
}
public IEnumerable<UUID> GetWearableAssets(WearableType type)
{
return Wearables.Where(e => e.Key == type)
.SelectMany(e => e.Value)
.Select(wearable => wearable.AssetID);
}
/// <summary>
/// Add a wearable to the current outfit and set appearance
/// </summary>
/// <param name="wearableItem">Wearable to be added to the outfit</param>
/// <param name="replace">Should existing item on the same point or of the same type be replaced</param>
public void AddToOutfit(InventoryItem wearableItem, bool replace = true)
{
var wearableItems = new List<InventoryItem> { wearableItem };
AddToOutfit(wearableItems, replace);
}
/// <summary>
/// Add a list of wearables to the current outfit and set appearance
/// </summary>
/// <param name="wearableItems">List of wearable inventory items to
/// be added to the outfit</param>
/// <param name="replace">Should existing item on the same point or of the same type be replaced</param>
public void AddToOutfit(List<InventoryItem> wearableItems, bool replace = true)
{
var wearables = wearableItems.OfType<InventoryWearable>().ToList();
var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject).ToList();
lock (Wearables)
{
// Add the given wearables to the wearables collection
foreach (var 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();
}
}
/// <summary>
/// Remove a wearable from the current outfit and set appearance
/// </summary>
/// <param name="wearableItem">Wearable to be removed from the outfit</param>
public void RemoveFromOutfit(InventoryItem wearableItem)
{
var wearableItems = new List<InventoryItem> { wearableItem };
RemoveFromOutfit(wearableItems);
}
/// <summary>
/// Removes a list of wearables from the current outfit and set appearance
/// </summary>
/// <param name="wearableItems">List of wearable inventory items to
/// be removed from the outfit</param>
public void RemoveFromOutfit(List<InventoryItem> wearableItems)
{
var wearables = wearableItems.OfType<InventoryWearable>()
.ToList();
var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject)
.ToList();
var needSetAppearance = false;
lock (Wearables)
{
// Remove the given wearables from the wearables collection
foreach (var 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);
var 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();
}
}
/// <summary>
/// Replace the current outfit with a list of wearables and set appearance
/// </summary>
/// <param name="wearableItems">List of wearable inventory items that
/// define a new outfit</param>
public void ReplaceOutfit(List<InventoryItem> wearableItems)
{
ReplaceOutfit(wearableItems, true);
}
/// <summary>
/// Replace the current outfit with a list of wearables and set appearance
/// </summary>
/// <param name="wearableItems">List of wearable inventory items that
/// define a new outfit</param>
/// <param name="safe">Check if we have all body parts, set this to false only
/// if you know what you're doing</param>
public void ReplaceOutfit(List<InventoryItem> wearableItems, bool safe)
{
var wearables = wearableItems.OfType<InventoryWearable>().ToList();
var attachments = wearableItems.Where(item => item is InventoryAttachment || item is InventoryObject).ToList();
if (safe)
{
// If we don't already have the current agent wearables downloaded, updating to a
// new set of wearables that doesn't have all bodyparts can leave the avatar
// in an inconsistent state. If any bodypart entries are empty, we need to fetch the
// current wearables first
var needsCurrentWearables = false;
lock (Wearables)
{
for (var i = 0; i < WEARABLE_COUNT; i++)
{
var wearableType = (WearableType)i;
if (WearableTypeToAssetType(wearableType) == AssetType.Bodypart
&& !Wearables.ContainsKey(wearableType))
{
needsCurrentWearables = true;
break;
}
}
}
if (needsCurrentWearables && !GatherAgentWearables())
{
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);
}
}
/// <summary>
/// Checks if an inventory item is currently being worn
/// </summary>
/// <param name="item">The <see cref="InventoryItem"/> to check against the agent wearables</param>
/// <returns>The <see cref="WearableType"/> slot that the item is being worn in,
/// or <see cref="WearableType.Invalid"/> if it is not currently being worn</returns>
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;
}
/// <summary>
/// Checks if an inventory item is currently being worn
/// </summary>
/// <param name="itemId"><see cref="UUID"/> if <see cref="InventoryItem"/> to check</param>
/// <returns>True if worn</returns>
public bool IsItemWorn(UUID itemId)
{
lock (Wearables)
{
if (Wearables.Any(wearableType => wearableType.Value
.Any(wearable => wearable.ItemID == itemId)))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns a collection of the agents currently worn wearables
/// </summary>
/// <returns>A copy of the agents currently worn wearables</returns>
/// <remarks>Avoid calling this function multiple times as it will make
/// a copy of all wearable data each time</remarks>
public IEnumerable<WearableData> GetWearables()
{
lock (Wearables)
{
// ToList will copy the IEnumerable
return Wearables.SelectMany(e => e.Value).ToList();
}
}
public MultiValueDictionary<WearableType, WearableData> GetWearablesByType()
{
lock (Wearables)
{
return new MultiValueDictionary<WearableType, WearableData>(Wearables);
}
}
/// <summary>
/// Calls either <see cref="AppearanceManager.ReplaceOutfit"/> or
/// <see cref="AppearanceManager.AddToOutfit"/> depending on the value of
/// replaceItems
/// </summary>
/// <param name="wearables">List of wearable inventory items to add
/// to the outfit or become a new outfit</param>
/// <param name="replaceItems">True to replace existing items with the
/// new list of items, false to add these items to the existing outfit</param>
public void WearOutfit(List<InventoryBase> wearables, bool replaceItems)
{
var wearableItems = wearables.OfType<InventoryItem>().ToList();
if (replaceItems)
ReplaceOutfit(wearableItems);
else
AddToOutfit(wearableItems);
}
#endregion Publics Methods
#region Attachments
/// <summary>
/// Adds a list of attachments to our agent
/// </summary>
/// <param name="attachments">A List containing the attachments to add</param>
/// <param name="removeExistingFirst">If true, tells simulator to remove existing attachment
/// <param name="replace">If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)</param>
/// first</param>
public void AddAttachments(List<InventoryItem> attachments, bool removeExistingFirst, bool replace = true)
{
// Use RezMultipleAttachmentsFromInv to clear out current attachments, and attach new ones
var 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 (var i = 0; i < attachments.Count; i++)
{
if (attachments[i] is InventoryAttachment)
{
var 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 = attachment.Flags,
ItemID = attachment.ActualUUID,
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)
{
var 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 = attachment.Flags,
ItemID = attachment.ActualUUID,
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);
}
/// <summary>
/// Attach an item to our agent at a specific attach point
/// </summary>
/// <param name="item">A <see cref="OpenMetaverse.InventoryItem"/> to attach</param>
/// <param name="attachPoint">the <see cref="OpenMetaverse.AttachmentPoint"/> on the avatar
/// <param name="replace">If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)</param>
/// to attach the item to</param>
public void Attach(InventoryItem item, AttachmentPoint attachPoint, bool replace = true)
{
Attach(item.ActualUUID, item.OwnerID, item.Name, item.Description, item.Permissions, item.Flags,
attachPoint, replace);
}
/// <summary>
/// Attach an item to our agent specifying attachment details
/// </summary>
/// <param name="itemID">The <see cref="OpenMetaverse.UUID"/> of the item to attach</param>
/// <param name="ownerID">The <see cref="OpenMetaverse.UUID"/> attachments owner</param>
/// <param name="name">The name of the attachment</param>
/// <param name="description">The description of the attachment</param>
/// <param name="perms">The <see cref="OpenMetaverse.Permissions"/> to apply when attached</param>
/// <param name="itemFlags">The <see cref="OpenMetaverse.InventoryItemFlags"/> of the attachment</param>
/// <param name="attachPoint">The <see cref="OpenMetaverse.AttachmentPoint"/> on the agent
/// <param name="replace">If true replace existing attachment on this attachment point, otherwise add to it (multi-attachments)</param>
/// to attach the item to</param>
public void Attach(UUID itemID, UUID ownerID, string name, string description,
Permissions perms, uint itemFlags, AttachmentPoint attachPoint, bool replace = true)
{
var 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);
}
/// <summary>
/// Detach an item from our agent by <see cref="OpenMetaverse.InventoryItem"/>
/// </summary>
/// <param name="item"><see cref="OpenMetaverse.InventoryItem"/> to detach</param>
public void Detach(InventoryItem item)
{
Detach(item.UUID);
}
/// <summary>
/// Detach an item from our agent
/// </summary>
/// <param name="itemID">The inventory itemID of the item to detach</param>
public void Detach(UUID itemID)
{
var detach = new DetachAttachmentIntoInvPacket
{
ObjectData =
{
AgentID = Client.Self.AgentID,
ItemID = itemID
}
};
Attachments.TryRemove(itemID, out _);
Client.Network.SendPacket(detach);
}
/// <summary>
/// Populates currently worn attachments.
/// </summary>
/// <returns>True on success retrieving attachments</returns>
private bool GatherAgentAttachments()
{
var objectsPrimitives = Client.Network.CurrentSim.ObjectsPrimitives;
// No primitives found.
if (objectsPrimitives.IsEmpty)
{
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();
foreach (var primitive in enumerable)
{
// Find the inventory UUID from the primitive name-value collection.
if (primitive == null || primitive.NameValues == null || !primitive.NameValues.Any()) { continue; }
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 = primitive.PrimData.AttachmentPoint;
// Add or update the attachment list.
Attachments.AddOrUpdate(itemID, attachmentPoint, (id, point) => attachmentPoint);
}
return true;
}
public bool isItemAttached(InventoryItem item)
{
return isItemAttached(item.ActualUUID);
}
public bool isItemAttached(UUID key)
{
return Attachments.ContainsKey(key);
}
/// <summary>
/// Returns a collection of the agents currently worn attachments from the cached inventory
/// </summary>
/// <returns>A copy of the agents currently worn attachments</returns>
/// <remarks>Avoid calling this function multiple times as it will make
/// a copy of all wearable data each time</remarks>
public IEnumerable<InventoryItem> GetAttachments()
{
return Attachments.Select(item => Client.Inventory.Store[item.Key] as InventoryItem);
}
public Dictionary<UUID, AttachmentPoint> GetAttachmentsByItemId()
{
return Attachments.ToDictionary(k => k.Key, v => v.Value);
}
public MultiValueDictionary<AttachmentPoint, InventoryItem> GetAttachmentsByAttachmentPoint()
{
var attachmentsByPoint = new MultiValueDictionary<AttachmentPoint, InventoryItem>();
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.FetchItemHttpAsync(item.Key, Client.Self.AgentID).Result;
attachmentsByPoint.Add(item.Value, inventoryItem);
}
return attachmentsByPoint;
}
public Dictionary<InventoryItem, AttachmentPoint> GetAttachmentsByInventoryItem()
{
var attachmentsByInventoryItem = new Dictionary<InventoryItem, AttachmentPoint>();
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.FetchItemHttpAsync(item.Key, Client.Self.AgentID).Result;
if (inventoryItem != null)
{
attachmentsByInventoryItem.Add(inventoryItem, item.Value);
}
}
return attachmentsByInventoryItem;
}
#endregion Attachments
#region Appearance Helpers
/// <summary>
/// Inform the sim which wearables are part of our current outfit
/// </summary>
private void SendAgentIsNowWearing()
{
var wearing = new AgentIsNowWearingPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
WearableData = new AgentIsNowWearingPacket.WearableDataBlock[WEARABLE_COUNT]
};
lock (Wearables)
{
for (var i = 0; i < WEARABLE_COUNT; i++)
{
var 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.TryGetValue(type, out var wearable) ?
(wearable.First()?.ItemID ?? UUID.Zero)
: UUID.Zero
};
}
}
Client.Network.SendPacket(wearing);
}
/// <summary>
/// Replaces the Wearables collection with a list of new wearable items
/// </summary>
/// <param name="wearableItems">Wearable items to replace the Wearables collection with</param>
private void ReplaceOutfit(List<InventoryWearable> 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<WearableType, WearableData>();
var bodyparts = new Dictionary<WearableType, WearableData>();
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)
{
var 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");
}
}
}
/// <summary>
/// Calculates base color/tint for a specific wearable based on its params
/// </summary>
/// <param name="param">All the color info gathered from wearable VisualParams
/// passed as list of ColorParamInfo tuples</param>
/// <returns>Base color/tint for the wearable</returns>
public static Color4 GetColorFromParams(List<ColorParamInfo> param)
{
// Start off with a blank slate, black, fully transparent
var res = new Color4(0, 0, 0, 0);
// Apply color modification from each color parameter
foreach (var p in param)
{
var n = p.VisualColorParam.Colors.Length;
var 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
var step = (p.VisualParam.MaxValue - p.VisualParam.MinValue) / ((float)n - 1);
// Our color should land in between colors in the array with index a and b
var indexa = 0;
var indexb = 0;
var i = 0;
for (var 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
var 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
var c1 = p.VisualColorParam.Colors[indexa];
var 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;
}
/// <summary>
/// Blocking method to populate the Wearables dictionary
/// </summary>
/// <returns>True on success, otherwise false</returns>
private bool GatherAgentWearables()
{
var wearablesEvent = new AutoResetEvent(false);
EventHandler<AgentWearablesReplyEventArgs> WearablesCallback = ((s, e) => wearablesEvent.Set());
AgentWearablesReply += WearablesCallback;
RequestAgentWorn();
var success = wearablesEvent.WaitOne(WEARABLE_TIMEOUT, false);
AgentWearablesReply -= WearablesCallback;
return success;
}
/// <summary>
/// Blocking method to populate the Textures array with cached bakes
/// </summary>
/// <returns>True on success, otherwise false</returns>
private bool GetCachedBakes()
{
var cacheCheckEvent = new AutoResetEvent(false);
EventHandler<AgentCachedBakesReplyEventArgs> CacheCallback = (sender, e) => cacheCheckEvent.Set();
CachedBakesReply += CacheCallback;
RequestCachedBakes();
var success = cacheCheckEvent.WaitOne(WEARABLE_TIMEOUT, false);
CachedBakesReply -= CacheCallback;
return success;
}
/// <summary>
/// Populates textures and visual params from a decoded asset
/// </summary>
/// <param name="wearable">Wearable to decode</param>
/// <param name="textures">Texture data</param>
public static void DecodeWearableParams(WearableData wearable, ref TextureData[] textures)
{
var alphaMasks = new Dictionary<VisualAlphaParam, float>();
var colorParams = new List<ColorParamInfo>();
// Populate collection of alpha masks from visual params
// also add color tinting information
foreach (var kvp in wearable.Asset.Params)
{
if (!VisualParams.Params.TryGetValue(kvp.Key, out var p)) continue;
var 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);
}
// Alpha masks can also be specified in sub "driver" params
if (p.Drivers != null)
{
foreach (var t in p.Drivers)
{
if (VisualParams.Params.TryGetValue(t, out var driver))
{
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);
}
}
}
}
}
var wearableColor = Color4.White; // Never actually used
if (colorParams.Count > 0)
{
wearableColor = GetColorFromParams(colorParams);
Logger.DebugLog($"Setting tint {wearableColor} for {wearable.WearableType}");
}
// Loop through all the texture IDs in this decoded asset and put them in our cache of worn textures
foreach (var entry in wearable.Asset.Textures)
{
var 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;
}
}
}
/// <summary>
/// Blocking method to download and parse currently worn wearable assets
/// </summary>
/// <returns>True on success, otherwise false</returns>
private bool DownloadWearables()
{
var success = true;
// Make a copy of the wearables dictionary to enumerate over
var wearables = new List<WearableData>(GetWearables());
// We will refresh the textures (zero out all non bake textures)
for (var i = 0; i < Textures.Length; i++)
{
var isBake = BakeIndexToTextureIndex.Any(t => t == i);
if (!isBake)
Textures[i] = new TextureData();
}
var pendingWearables = wearables.Count;
foreach (var wearable in wearables.Where(wearable => wearable.Asset != null))
{
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;
var 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;
}
/// <summary>
/// Get a list of all textures that need to be downloaded for a single bake layer
/// </summary>
/// <param name="bakeType">Bake layer to get texture AssetIDs for</param>
/// <returns>A list of texture AssetIDs to download</returns>
private IEnumerable<UUID> GetTextureDownloadList(BakeType bakeType)
{
var indices = BakeTypeToTextures(bakeType);
var textures = new List<UUID>();
foreach (var textureData in from index in indices
where index != AvatarTextureIndex.Skirt || Wearables.ContainsKey(WearableType.Skirt)
select Textures[(int)index] into textureData
where textureData.TextureID != UUID.Zero && textureData.Texture == null
&& !textures.Contains(textureData.TextureID) select textureData)
{
textures.Add(textureData.TextureID);
}
return textures;
}
/// <summary>
/// Blocking method to download all textures needed for baking
/// the given bake layers
/// </summary>
/// <param name="bakeLayers">A list of layers that need baking</param>
/// <remarks>No return value is given because the baking will happen
/// whether all textures are successfully downloaded</remarks>
private void DownloadTextures(List<BakeType> bakeLayers)
{
var textureIDs = new List<UUID>();
foreach (var uuid in from t in bakeLayers
select GetTextureDownloadList(t) into layerTextureIDs
from uuid in layerTextureIDs where !textureIDs.Contains(uuid) select uuid)
{
textureIDs.Add(uuid);
}
Logger.DebugLog("Downloading " + textureIDs.Count + " textures for baking");
Parallel.ForEach(textureIDs, _parallelOptions,
textureId =>
{
try
{
var downloadEvent = new AutoResetEvent(false);
Client.Assets.RequestImage(textureId,
delegate (TextureRequestState state, AssetTexture assetTexture)
{
if (state == TextureRequestState.Finished)
{
assetTexture.Decode();
for (var 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);
}
}
);
}
/// <summary>
/// Blocking method to create and upload baked textures for all missing bakes
/// </summary>
/// <returns>True on success, otherwise false</returns>
private bool CreateBakes()
{
var success = true;
var pendingBakes = new List<BakeType>();
// Check each bake layer in the Textures array for missing bakes
for (var bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
{
var 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: {textureIndex} skipping not attached");
continue;
}
Logger.DebugLog($"texture: {textureIndex} is needed adding to pending Bakes");
pendingBakes.Add((BakeType)bakedIndex);
}
else
{
Logger.DebugLog($"texture: {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 (var i = 0; i < Textures.Length; i++)
{
Textures[i].Texture = null;
}
return success;
}
/// <summary>
/// Blocking method to create and upload a baked texture for a single
/// bake layer
/// </summary>
/// <param name="bakeType">Layer to bake</param>
/// <returns>True on success, otherwise false</returns>
private bool CreateBake(BakeType bakeType)
{
var textureIndices = BakeTypeToTextures(bakeType);
var oven = new Baker(bakeType);
foreach (var textureIndex in textureIndices)
{
var texture = Textures[(int)textureIndex];
texture.TextureIndex = textureIndex;
oven.AddTexture(texture);
}
var start = Environment.TickCount;
oven.Bake();
Logger.DebugLog($"Baking {bakeType} took {Environment.TickCount - start}ms");
var newAssetID = UUID.Zero;
var retries = UPLOAD_RETRIES;
while (newAssetID == UUID.Zero && retries > 0)
{
newAssetID = UploadBake(oven.BakedTexture.AssetData);
--retries;
}
var 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;
}
/// <summary>
/// Blocking method to upload a baked texture
/// </summary>
/// <param name="textureData">Five channel JPEG2000 texture data to upload</param>
/// <returns>UUID of the newly created asset on success, otherwise UUID.Zero</returns>
private UUID UploadBake(byte[] textureData)
{
var bakeID = UUID.Zero;
var uploadEvent = new AutoResetEvent(false);
Client.Assets.RequestUploadBakedTexture(textureData,
delegate (UUID newAssetID)
{
bakeID = newAssetID;
uploadEvent.Set();
}
);
// FIXME: evaluate the need for timeout here, RequestUploadBakedTexture() will
// timeout 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;
}
/// <summary>
/// Creates a dictionary of visual param values from the downloaded wearables
/// </summary>
/// <returns>A dictionary of visual param indices mapping to visual param
/// values for our agent that can be fed to the Baker class</returns>
private Dictionary<int, float> MakeParamValues()
{
var paramValues = new Dictionary<int, float>(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;
var found = false;
var 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;
}
private Avatar GetOwnAvatar()
{
Client.Network.CurrentSim.ObjectsAvatars.TryGetValue(Client.Self.LocalID, out var av);
return av;
}
/// <summary>
/// Initiate server baking process
/// </summary>
/// <returns>True if the server baking was successful</returns>
private async Task<bool> UpdateAvatarAppearanceAsync(CancellationToken cancellationToken, int totalRetries = 3)
{
while (true)
{
if (cancellationToken.IsCancellationRequested) { return false; }
if (totalRetries < 0)
{
return false;
}
var cap = Client.Network.CurrentSim.Caps?.CapabilityURI("UpdateAvatarAppearance");
if (cap == null)
{
Logger.Log("Could not retrieve UpdateAvatarAppearance region capability",
Helpers.LogLevel.Warning, Client);
return false;
}
var currentOutfitFolder = await GetCurrentOutfitFolder(cancellationToken);
if (currentOutfitFolder == null)
{
Logger.Log("Could not retrieve Current Outfit folder",
Helpers.LogLevel.Warning, Client);
return false;
}
else
{
// TODO: create Current Outfit Folder
}
Logger.Log($"Requesting bake for COF version {currentOutfitFolder.Version}",
Helpers.LogLevel.Info, Client);
var request = new OSDMap(1) { ["cof_version"] = currentOutfitFolder.Version };
OSD res = null;
var maxRetries = 1000; // About a minute. (50,000ms)
while (maxRetries-- > 0)
{
if (!Client.Network.Connected)
{
await Task.Delay(50, cancellationToken);
}
else
{
if (GetOwnAvatar() == null)
{
await Task.Delay(50, cancellationToken);
}
else
{
break;
}
}
}
await Client.HttpCapsClient.PostRequestAsync(cap, OSDFormat.Xml, request, cancellationToken, (response, data, error) =>
{
if (data != null)
{
res = OSDParser.Deserialize(data);
}
if (error != null)
{
Logger.Log($"UpdateAvatarAppearance failed. Server responded: {error.Message}",
Helpers.LogLevel.Warning, Client);
}
});
if (!(res is OSDMap result)) { return false; }
if (result.ContainsKey("success") && result["success"].AsBoolean())
{
var visualParams = result["visual_params"].AsBinary();
var textures = (result["textures"] as OSDArray)?.Select(arrayEntry => arrayEntry.AsUUID()).ToArray();
var cofVersion = result["cof_version"].AsInteger();
MyVisualParameters = visualParams;
if (textures != null && textures.Length > 20)
{
if ((textures[8] == UUID.Zero || textures[9] == UUID.Zero || textures[10] == UUID.Zero || textures[11] == UUID.Zero) || (textures[8] == DEFAULT_AVATAR_TEXTURE || textures[9] == DEFAULT_AVATAR_TEXTURE || textures[10] == DEFAULT_AVATAR_TEXTURE || textures[11] == DEFAULT_AVATAR_TEXTURE))
{
// This hasn't actually baked. Retry after a delay.
await Task.Delay(REBAKE_DELAY, cancellationToken);
totalRetries = totalRetries - 1;
continue;
}
}
try
{
var selfPrim = GetOwnAvatar();
if (selfPrim == null)
{
Logger.Log("Unable to find avatar to set appearance information", Helpers.LogLevel.Error, Client);
}
else
{
var selfAvatarTextures = new Primitive.TextureEntry(UUID.Zero);
if (textures != null)
{
for (var i = 0; i < textures.Length; i++)
{
selfAvatarTextures.FaceTextures[i] = new Primitive.TextureEntryFace(null) { TextureID = textures[i] };
}
selfPrim.Textures = selfAvatarTextures;
MyTextures = selfAvatarTextures;
}
selfPrim.VisualParameters = visualParams;
selfPrim.AppearanceVersion = 1;
selfPrim.COFVersion = cofVersion;
selfPrim.AppearanceFlags = 0;
var appearance = new AvatarAppearanceEventArgs(Client.Network.CurrentSim,
Client.Self.AgentID,
false,
selfPrim.Textures.DefaultTexture,
selfPrim.Textures.FaceTextures,
selfPrim.VisualParameters.ToList(), 1,
cofVersion,
AppearanceFlags.None,
selfPrim.ChildCount);
Client.Avatars.TriggerAvatarAppearanceMessage(appearance);
}
}
catch (Exception e)
{
Logger.Log($"Error applying textures to avatar object: {e.Message}", Helpers.LogLevel.Error, Client);
throw;
}
Logger.Log("Returning appearance information from server-side bake request.", Helpers.LogLevel.Info, Client);
return true;
}
if (result.ContainsKey("expected"))
{
Logger.Log($"Server expected {result["expected"].AsInteger()} as COF version. " +
$"Version {currentOutfitFolder.Version} was sent.",
Helpers.LogLevel.Warning, Client);
await SyncCofVersion(cancellationToken);
--totalRetries;
continue;
}
if (result.ContainsKey("error"))
{
var er = result["error"].AsString();
if (string.IsNullOrEmpty(er))
{
Logger.Log($"UpdateAvatarAppearance failed. Server responded with: '{result["error"].AsString()}'",
Helpers.LogLevel.Warning, Client);
}
}
Logger.Log($"Avatar appearance update failed on {totalRetries} attempt.", Helpers.LogLevel.Info, Client);
await Task.Delay(REBAKE_DELAY, cancellationToken);
--totalRetries;
}
}
/// <summary>
/// Get the latest version of COF
/// </summary>
/// <returns>Current Outfit Folder (or null if getting the data failed)</returns>
public async Task<InventoryFolder> GetCurrentOutfitFolder(CancellationToken cancellationToken = default)
{
// COF should be in the root folder. Request update to get the latest version number
List<InventoryBase> root = await Client.Inventory.RequestFolderContents(Client.Inventory.Store.RootFolder.UUID,
Client.Self.AgentID, true, false, InventorySortOrder.ByDate,
cancellationToken);
if (root == null) { return null; }
foreach (var baseItem in root)
{
if (baseItem is InventoryFolder folder && folder.PreferredType == FolderType.CurrentOutfit)
{
return folder;
}
}
return null;
}
public AgentSetAppearancePacket MakeAppearancePacket()
{
var 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
var agentSizeVPHeight = 0.0f;
var agentSizeVPHeelHeight = 0.0f;
var agentSizeVPPlatformHeight = 0.0f;
var agentSizeVPHeadSize = 0.5f;
var agentSizeVPLegLength = 0.0f;
var agentSizeVPNeckLength = 0.0f;
var agentSizeVPHipLength = 0.0f;
lock (Wearables)
{
#region VisualParam
var vpIndex = 0;
var wearingPhysics = Wearables.ContainsKey(WearableType.Physics);
var nrParams = wearingPhysics ? 251 : 218;
set.VisualParam = new AgentSetAppearancePacket.VisualParamBlock[nrParams];
foreach (var kvp in VisualParams.Params)
{
var vp = kvp.Value;
var paramValue = 0f;
var found = Wearables.Any(wearableList => wearableList.Value.Any(wearable => wearable.Asset != null && wearable.Asset.Params.TryGetValue(vp.ParamID, out paramValue)));
// 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 (var i = 0; i < set.VisualParam.Length; i++)
{
if (set.VisualParam[i] != null)
{
MyVisualParameters[i] = set.VisualParam[i].ParamValue;
}
}
#endregion VisualParam
#region TextureEntry
var te = new Primitive.TextureEntry(DEFAULT_AVATAR_TEXTURE);
for (uint i = 0; i < Textures.Length; i++)
{
var 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 (var bakedIndex = 0; bakedIndex < BAKED_TEXTURE_COUNT; bakedIndex++)
{
var hash = UUID.Zero;
for (var wearableIndex = 0; wearableIndex < WEARABLES_PER_LAYER; wearableIndex++)
{
var 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.
const double agentSizeBase = 1.706;
// The calculation for the HeadSize scalar may be incorrect, but it seems to work
var 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;
}
public void SendOutfitToCurrentSimulator()
{
var blocks = new List<RezMultipleAttachmentsFromInvPacket.ObjectDataBlock>();
var worn = RequestAgentWorn();
Logger.Log($"{worn.Count} inventory items in 'Current Outfit' folder", Helpers.LogLevel.Info, Client);
foreach (var inventoryBase in worn.Where(inventoryBase => inventoryBase != null))
{
Logger.Log($"'{inventoryBase.Name}' found in 'Current Outfit' folder ({inventoryBase.GetType().Name})", Helpers.LogLevel.Info, Client);
switch (inventoryBase)
{
case InventoryAttachment attachment:
{
var block = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock
{
AttachmentPt = (byte)(ATTACHMENT_ADD | (byte)attachment.AttachmentPoint),
EveryoneMask = (uint)attachment.Permissions.EveryoneMask,
GroupMask = (uint)attachment.Permissions.GroupMask,
ItemFlags = attachment.Flags,
ItemID = attachment.ActualUUID,
Name = Utils.StringToBytes(attachment.Name),
Description = Utils.StringToBytes(attachment.Description),
NextOwnerMask = (uint)attachment.Permissions.NextOwnerMask,
OwnerID = attachment.OwnerID
};
Logger.Log($"Wearing attachment {attachment.UUID} ({attachment.Name})", Helpers.LogLevel.Debug, Client);
blocks.Add(block);
break;
}
case InventoryObject attachmentIO:
{
var block = new RezMultipleAttachmentsFromInvPacket.ObjectDataBlock
{
AttachmentPt = ATTACHMENT_ADD,
EveryoneMask = (uint)attachmentIO.Permissions.EveryoneMask,
GroupMask = (uint)attachmentIO.Permissions.GroupMask,
ItemFlags = attachmentIO.Flags,
ItemID = attachmentIO.ActualUUID,
Name = Utils.StringToBytes(attachmentIO.Name),
Description = Utils.StringToBytes(attachmentIO.Description),
NextOwnerMask = (uint)attachmentIO.Permissions.NextOwnerMask,
OwnerID = attachmentIO.OwnerID
};
Logger.Log($"Wearing object {attachmentIO.UUID} ({attachmentIO.Name})", Helpers.LogLevel.Debug, Client);
blocks.Add(block);
break;
}
}
}
var attachmentsPacket = new RezMultipleAttachmentsFromInvPacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
},
HeaderData =
{
CompoundMsgID = UUID.Random(),
FirstDetachAll = true,
TotalObjects = (byte)blocks.Count
},
ObjectData = blocks.ToArray()
};
Client.Network.SendPacket(attachmentsPacket);
}
/// <summary>
/// Create an AgentSetAppearance packet from Wearables data and the
/// Textures array and send it
/// </summary>
private void RequestAgentSetAppearance()
{
var set = MakeAppearancePacket();
Client.Network.SendPacket(set);
Logger.DebugLog("Send AgentSetAppearance packet");
}
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);
}
private async Task SyncCofVersion(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) { return; }
Uri capability = Client.Network.CurrentSim.Caps.CapabilityURI("IncrementCOFVersion");
if (capability == null)
{
Logger.Log("Region returned no IncrementCOFVersion capability", Helpers.LogLevel.Warning, Client);
return;
}
Logger.Log("Requesting COF version be incremented by the server", Helpers.LogLevel.Debug, Client);
using (var request = new HttpRequestMessage(HttpMethod.Get, capability))
{
using (var reply = await Client.HttpCapsClient.SendAsync(request, cancellationToken))
{
if (!reply.IsSuccessStatusCode)
{
Logger.Log($"Failed to increment COF version: {reply.ReasonPhrase}",
Helpers.LogLevel.Warning);
}
else
{
var data = await reply.Content.ReadAsStringAsync();
if (OSDParser.Deserialize(data) is OSDMap map)
{
var version = map["version"].AsInteger();
Logger.Log($"Slamming {version} version to Current Outfit Folder",
Helpers.LogLevel.Info, Client);
var cof = await GetCurrentOutfitFolder(cancellationToken);
cof.Version = version;
}
}
}
}
}
#endregion Appearance Helpers
#region Inventory Helpers
private bool GetFolderWearables(string[] folderPath, out List<InventoryWearable> wearables, out List<InventoryItem> attachments)
{
var 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<InventoryWearable> wearables, out List<InventoryItem> attachments)
{
wearables = new List<InventoryWearable>();
attachments = new List<InventoryItem>();
var objects = Client.Inventory.FolderContents(folder, Client.Self.AgentID, false, true,
InventorySortOrder.ByName, INVENTORY_TIMEOUT);
if (objects != null)
{
foreach (var ib in objects)
{
switch (ib)
{
case InventoryWearable wearable:
Logger.DebugLog($"Adding wearable {wearable.Name}", Client);
wearables.Add(wearable);
break;
case InventoryAttachment attachment:
Logger.DebugLog($"Adding attachment (attachment) {attachment.Name}", Client);
attachments.Add(attachment);
break;
case InventoryObject inventoryObject:
Logger.DebugLog($"Adding attachment (object) {inventoryObject.Name}", Client);
attachments.Add(inventoryObject);
break;
default:
Logger.DebugLog($"Ignoring inventory item {ib.Name}", Client);
break;
}
}
}
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)
{
var update = (AgentWearablesUpdatePacket)e.Packet;
Logger.DebugLog("Received AgentWearablesUpdate");
// At one point this was necessary, but Second Life now sends dummy items back...
// So let's just ignore the dumdum, k?
// Fire the callback
OnAgentWearables(new AgentWearablesReplyEventArgs());
}
protected void RebakeAvatarTexturesHandler(object sender, PacketReceivedEventArgs e)
{
var 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)
{
var bakeType = (BakeType)block.TextureIndex;
var 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
var 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_OnDisconnected(object sender, DisconnectedEventArgs e)
{
if (RebakeScheduleTimer != null)
{
RebakeScheduleTimer.Dispose();
RebakeScheduleTimer = null;
}
if (AppearanceCts != null)
{
AppearanceCts.Cancel();
AppearanceCts.Dispose();
AppearanceCts = null;
}
if (AppearanceThread != null)
{
AppearanceThread = null;
AppearanceThreadRunning = 0;
}
}
private void Network_OnSimChanged(object sender, SimChangedEventArgs e)
{
Client.Network.CurrentSim.Caps.CapabilitiesReceived += Simulator_OnCapabilitiesReceived;
}
private void Objects_AttachmentUpdate(object sender, PrimEventArgs e)
{
Primitive prim = e.Prim;
if (Client.Self.LocalID == 0
|| prim.ParentID != Client.Self.LocalID
|| prim.NameValues == null
|| !prim.IsAttachment
|| !e.IsNew)
{
return;
}
// Updates Attachment points as soon as the data arrives
for (int i = 0; i < prim.NameValues.Length; ++i)
{
if (prim.NameValues[i].Name != "AttachItemID")
{
continue;
}
UUID inventoryID = new UUID(prim.NameValues[i].Value.ToString());
if (Attachments.TryGetValue(inventoryID, out AttachmentPoint attachmentPoint))
{
Attachments.AddOrUpdate(inventoryID, attachmentPoint, (id, value) => prim.PrimData.AttachmentPoint);
}
break;
}
}
private void Simulator_OnCapabilitiesReceived(object sender, CapabilitiesReceivedEventArgs e)
{
e.Simulator.Caps.CapabilitiesReceived -= Simulator_OnCapabilitiesReceived;
if (e.Simulator == Client.Network.CurrentSim && Client.Settings.SEND_AGENT_APPEARANCE)
{
bool updateSucceeded = UpdateAvatarAppearanceAsync(CancellationToken.None).Result;
if (updateSucceeded)
{
ThreadPool.QueueUserWorkItem((o) => { SendOutfitToCurrentSimulator(); });
}
}
}
#endregion Callbacks
#region Static Helpers
/// <summary>
/// Converts a WearableType to a bodypart or clothing WearableType
/// </summary>
/// <param name="type">A WearableType</param>
/// <returns>AssetType.Bodypart or AssetType.Clothing or AssetType.Unknown</returns>
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;
}
}
/// <summary>
/// Converts a BakeType to the corresponding baked texture slot in AvatarTextureIndex
/// </summary>
/// <param name="index">A BakeType</param>
/// <returns>The AvatarTextureIndex slot that holds the given BakeType</returns>
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;
}
}
/// <summary>
/// Gives the layer number that is used for morph mask
/// </summary>
/// <param name="bakeType">>A BakeType</param>
/// <returns>Which layer number as defined in BakeTypeToTextures is used for morph mask</returns>
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 <layer> that has <morph_mask> 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;
}
}
/// <summary>
/// Converts a BakeType to a list of the texture slots that make up that bake
/// </summary>
/// <param name="bakeType">A BakeType</param>
/// <returns>A list of texture slots that are inputs for the given bake</returns>
public static List<AvatarTextureIndex> BakeTypeToTextures(BakeType bakeType)
{
var textures = new List<AvatarTextureIndex>();
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
/// <summary>Contains the Event data returned from the data server from an AgentWearablesRequest</summary>
public class AgentWearablesReplyEventArgs : EventArgs
{
}
/// <summary>Contains the Event data returned from the data server from an AgentCachedTextureResponse</summary>
public class AgentCachedBakesReplyEventArgs : EventArgs
{
}
/// <summary>Contains the Event data returned from an AppearanceSetRequest</summary>
public class AppearanceSetEventArgs : EventArgs
{
/// <summary>Indicates whether appearance setting was successful</summary>
public bool Success { get; }
/// <summary>
/// Triggered when appearance data is sent to the sim and
/// the main appearance thread is done.</summary>
/// <param name="success">Indicates whether appearance setting was successful</param>
public AppearanceSetEventArgs(bool success)
{
this.Success = success;
}
}
/// <summary>Contains the Event data returned from the data server from an RebakeAvatarTextures</summary>
public class RebakeAvatarTexturesEventArgs : EventArgs
{
/// <summary>The ID of the Texture Layer to bake</summary>
public UUID TextureID { get; }
/// <summary>
/// Triggered when the simulator sends a request for this agent to rebake
/// its appearance
/// </summary>
/// <param name="textureID">The ID of the Texture Layer to bake</param>
public RebakeAvatarTexturesEventArgs(UUID textureID)
{
this.TextureID = textureID;
}
}
#endregion
}