522 lines
21 KiB
C#
522 lines
21 KiB
C#
/*
|
|
* Copyright (c) Contributors, http://opensimulator.org/
|
|
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
|
*
|
|
* 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.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of the OpenSimulator Project 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 DEVELOPERS ``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 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.Reflection;
|
|
using log4net;
|
|
using Nini.Config;
|
|
using OpenMetaverse;
|
|
using OpenSim.Framework;
|
|
|
|
using System.Threading;
|
|
using System.Timers;
|
|
using System.Collections.Generic;
|
|
|
|
using OpenSim.Region.Framework.Interfaces;
|
|
using OpenSim.Region.Framework.Scenes;
|
|
using OpenSim.Services.Interfaces;
|
|
|
|
namespace OpenSim.Region.CoreModules.Avatar.AvatarFactory
|
|
{
|
|
public class AvatarFactoryModule : IAvatarFactory, IRegionModule
|
|
{
|
|
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
private Scene m_scene = null;
|
|
|
|
private int m_savetime = 5; // seconds to wait before saving changed appearance
|
|
private int m_sendtime = 2; // seconds to wait before sending changed appearance
|
|
|
|
private int m_checkTime = 500; // milliseconds to wait between checks for appearance updates
|
|
private System.Timers.Timer m_updateTimer = new System.Timers.Timer();
|
|
private Dictionary<UUID,long> m_savequeue = new Dictionary<UUID,long>();
|
|
private Dictionary<UUID,long> m_sendqueue = new Dictionary<UUID,long>();
|
|
|
|
private object m_setAppearanceLock = new object();
|
|
|
|
#region RegionModule Members
|
|
|
|
public void Initialise(Scene scene, IConfigSource config)
|
|
{
|
|
scene.RegisterModuleInterface<IAvatarFactory>(this);
|
|
scene.EventManager.OnNewClient += NewClient;
|
|
|
|
if (config != null)
|
|
{
|
|
IConfig sconfig = config.Configs["Startup"];
|
|
if (sconfig != null)
|
|
{
|
|
m_savetime = Convert.ToInt32(sconfig.GetString("DelayBeforeAppearanceSave",Convert.ToString(m_savetime)));
|
|
m_sendtime = Convert.ToInt32(sconfig.GetString("DelayBeforeAppearanceSend",Convert.ToString(m_sendtime)));
|
|
m_log.WarnFormat("[AVFACTORY] configured for {0} save and {1} send",m_savetime,m_sendtime);
|
|
}
|
|
}
|
|
|
|
if (m_scene == null)
|
|
m_scene = scene;
|
|
}
|
|
|
|
public void PostInitialise()
|
|
{
|
|
m_updateTimer.Enabled = false;
|
|
m_updateTimer.AutoReset = true;
|
|
m_updateTimer.Interval = m_checkTime; // 500 milliseconds wait to start async ops
|
|
m_updateTimer.Elapsed += new ElapsedEventHandler(HandleAppearanceUpdateTimer);
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
}
|
|
|
|
public string Name
|
|
{
|
|
get { return "Default Avatar Factory"; }
|
|
}
|
|
|
|
public bool IsSharedModule
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
public void NewClient(IClientAPI client)
|
|
{
|
|
client.OnRequestWearables += SendWearables;
|
|
client.OnSetAppearance += SetAppearance;
|
|
client.OnAvatarNowWearing += AvatarIsWearing;
|
|
}
|
|
|
|
public void RemoveClient(IClientAPI client)
|
|
{
|
|
// client.OnAvatarNowWearing -= AvatarIsWearing;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Check for the existence of the baked texture assets.
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
public bool ValidateBakedTextureCache(IClientAPI client)
|
|
{
|
|
return ValidateBakedTextureCache(client, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check for the existence of the baked texture assets. Request a rebake
|
|
/// unless checkonly is true.
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="checkonly"></param>
|
|
private bool ValidateBakedTextureCache(IClientAPI client, bool checkonly)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(client.AgentId);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: SetAppearance unable to find presence for {0}", client.AgentId);
|
|
return false;
|
|
}
|
|
|
|
bool defonly = true; // are we only using default textures
|
|
|
|
// Process the texture entry
|
|
for (int i = 0; i < AvatarAppearance.BAKE_INDICES.Length; i++)
|
|
{
|
|
int idx = AvatarAppearance.BAKE_INDICES[i];
|
|
Primitive.TextureEntryFace face = sp.Appearance.Texture.FaceTextures[idx];
|
|
|
|
// if there is no texture entry, skip it
|
|
if (face == null)
|
|
continue;
|
|
|
|
// if the texture is one of the "defaults" then skip it
|
|
// this should probably be more intelligent (skirt texture doesnt matter
|
|
// if the avatar isnt wearing a skirt) but if any of the main baked
|
|
// textures is default then the rest should be as well
|
|
if (face.TextureID == UUID.Zero || face.TextureID == AppearanceManager.DEFAULT_AVATAR_TEXTURE)
|
|
continue;
|
|
|
|
defonly = false; // found a non-default texture reference
|
|
|
|
if (!CheckBakedTextureAsset(client, face.TextureID, idx))
|
|
{
|
|
// the asset didn't exist if we are only checking, then we found a bad
|
|
// one and we're done otherwise, ask for a rebake
|
|
if (checkonly)
|
|
return false;
|
|
|
|
m_log.InfoFormat("[AVFACTORY]: missing baked texture {0}, requesting rebake", face.TextureID);
|
|
|
|
client.SendRebakeAvatarTextures(face.TextureID);
|
|
}
|
|
}
|
|
|
|
m_log.DebugFormat("[AVFACTORY]: Completed texture check for {0}", client.AgentId);
|
|
|
|
// If we only found default textures, then the appearance is not cached
|
|
return (defonly ? false : true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set appearance data (texture asset IDs and slider settings) received from the client
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="texture"></param>
|
|
/// <param name="visualParam"></param>
|
|
public void SetAppearance(IClientAPI client, Primitive.TextureEntry textureEntry, byte[] visualParams)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(client.AgentId);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: SetAppearance unable to find presence for {0}", client.AgentId);
|
|
return;
|
|
}
|
|
|
|
// m_log.InfoFormat("[AVFACTORY]: start SetAppearance for {0}", client.AgentId);
|
|
|
|
// TODO: This is probably not necessary any longer, just assume the
|
|
// textureEntry set implies that the appearance transaction is complete
|
|
bool changed = false;
|
|
|
|
// Process the texture entry transactionally, this doesn't guarantee that Appearance is
|
|
// going to be handled correctly but it does serialize the updates to the appearance
|
|
lock (m_setAppearanceLock)
|
|
{
|
|
// Process the visual params, this may change height as well
|
|
if (visualParams != null)
|
|
{
|
|
changed = sp.Appearance.SetVisualParams(visualParams);
|
|
if (sp.Appearance.AvatarHeight > 0)
|
|
sp.SetHeight(sp.Appearance.AvatarHeight);
|
|
}
|
|
|
|
// Process the baked texture array
|
|
if (textureEntry != null)
|
|
{
|
|
changed = sp.Appearance.SetTextureEntries(textureEntry) || changed;
|
|
|
|
m_log.InfoFormat("[AVFACTORY]: received texture update for {0}", client.AgentId);
|
|
Util.FireAndForget(delegate(object o) { ValidateBakedTextureCache(client, false); });
|
|
|
|
// This appears to be set only in the final stage of the appearance
|
|
// update transaction. In theory, we should be able to do an immediate
|
|
// appearance send and save here.
|
|
|
|
}
|
|
// save only if there were changes, send no matter what (doesn't hurt to send twice)
|
|
if (changed)
|
|
QueueAppearanceSave(client.AgentId);
|
|
|
|
QueueAppearanceSend(client.AgentId);
|
|
}
|
|
|
|
// m_log.WarnFormat("[AVFACTORY]: complete SetAppearance for {0}:\n{1}",client.AgentId,sp.Appearance.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for the existance of a baked texture asset and
|
|
/// requests the viewer rebake if the asset is not found
|
|
/// </summary>
|
|
/// <param name="client"></param>
|
|
/// <param name="textureID"></param>
|
|
/// <param name="idx"></param>
|
|
private bool CheckBakedTextureAsset(IClientAPI client, UUID textureID, int idx)
|
|
{
|
|
if (m_scene.AssetService.Get(textureID.ToString()) == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: Missing baked texture {0} ({1}) for avatar {2}",
|
|
textureID, idx, client.Name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#region UpdateAppearanceTimer
|
|
|
|
/// <summary>
|
|
/// Queue up a request to send appearance, makes it possible to
|
|
/// accumulate changes without sending out each one separately.
|
|
/// </summary>
|
|
public void QueueAppearanceSend(UUID agentid)
|
|
{
|
|
// 10000 ticks per millisecond, 1000 milliseconds per second
|
|
long timestamp = DateTime.Now.Ticks + Convert.ToInt64(m_sendtime * 1000 * 10000);
|
|
lock (m_sendqueue)
|
|
{
|
|
m_sendqueue[agentid] = timestamp;
|
|
m_updateTimer.Start();
|
|
}
|
|
//m_log.DebugFormat("[AVFACTORY]: Queue appearance send for {0} at {1} (now is {2})", agentid, timestamp,DateTime.Now.Ticks);
|
|
}
|
|
|
|
public void QueueAppearanceSave(UUID agentid)
|
|
{
|
|
// m_log.WarnFormat("[AVFACTORY]: Queue appearance save for {0}", agentid);
|
|
|
|
// 10000 ticks per millisecond, 1000 milliseconds per second
|
|
long timestamp = DateTime.Now.Ticks + Convert.ToInt64(m_savetime * 1000 * 10000);
|
|
lock (m_savequeue)
|
|
{
|
|
m_savequeue[agentid] = timestamp;
|
|
m_updateTimer.Start();
|
|
}
|
|
}
|
|
|
|
public void RefreshAppearance(UUID agentid)
|
|
{
|
|
m_log.DebugFormat("[AVFACTORY]: FireAndForget called for RefreshAppearance on agentid {0}", agentid);
|
|
Util.FireAndForget(delegate(object o)
|
|
{
|
|
int maxtries = 10;
|
|
int trycount = maxtries;
|
|
int interval = 5000;
|
|
ScenePresence sp;
|
|
while (!m_scene.TryGetScenePresence(agentid, out sp))
|
|
{
|
|
//m_log.WarnFormat("[AVFACTORY]: RefreshAppearance unable to find presence for {0}", agentid);
|
|
Thread.Sleep(interval);
|
|
if (trycount-- <= 0)
|
|
{
|
|
m_log.ErrorFormat("[AVFACTORY]: RefreshAppearance unable to find presence for {0} after {1} attempts at {2}ms intervals", agentid, maxtries, interval);
|
|
return;
|
|
}
|
|
}
|
|
trycount = maxtries;
|
|
AvatarAppearance appearance = null;
|
|
while (appearance == null)
|
|
{
|
|
try
|
|
{
|
|
appearance = m_scene.AvatarService.GetAppearance(agentid);
|
|
}
|
|
catch (System.Net.WebException e)
|
|
{
|
|
if (trycount-- <= 0)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: RefreshAppearance unable to get appearance from AvatarService for {0} after {1} attempts at {2}ms intervals: {3}", agentid, maxtries, interval, e.Message);
|
|
return;
|
|
}
|
|
}
|
|
Thread.Sleep(interval);
|
|
}
|
|
|
|
if (appearance.Texture != null && appearance.VisualParams != null)
|
|
{
|
|
sp.Appearance = appearance;
|
|
if (sp.Appearance.AvatarHeight > 0)
|
|
sp.SetHeight(sp.Appearance.AvatarHeight);
|
|
QueueAppearanceSend(agentid);
|
|
}
|
|
else
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: RefreshAppearance received null appearance data from grid for {0}", agentid);
|
|
}
|
|
}, null);
|
|
}
|
|
|
|
private void HandleAppearanceSend(UUID agentid)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(agentid);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: Agent {0} no longer in the scene", agentid);
|
|
return;
|
|
}
|
|
|
|
// m_log.WarnFormat("[AVFACTORY]: Handle appearance send for {0}", agentid);
|
|
|
|
// Send the appearance to everyone in the scene
|
|
sp.SendAppearanceToAllOtherAgents();
|
|
|
|
// Send animations back to the avatar as well
|
|
sp.Animator.SendAnimPack();
|
|
}
|
|
|
|
private void HandleAppearanceSave(UUID agentid)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(agentid);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: Agent {0} no longer in the scene", agentid);
|
|
return;
|
|
}
|
|
if (sp.IsSyncedAvatar)
|
|
return;
|
|
|
|
// m_log.WarnFormat("[AVFACTORY] avatar {0} save appearance",agentid);
|
|
|
|
// Disable saving of appearance for demonstrations
|
|
// m_scene.AvatarService.SetAppearance(agentid, sp.Appearance);
|
|
// REGION SYNC
|
|
// If this is a client manager, we have received new appearance from a client and saved
|
|
// it to the avatar service. Now let's tell the parent scene about it.
|
|
if (m_scene.IsSyncedClient())
|
|
m_scene.RegionSyncClientModule.SendAppearanceToScene(agentid);
|
|
}
|
|
|
|
private void HandleAppearanceUpdateTimer(object sender, EventArgs ea)
|
|
{
|
|
long now = DateTime.Now.Ticks;
|
|
|
|
lock (m_sendqueue)
|
|
{
|
|
Dictionary<UUID, long> sends = new Dictionary<UUID, long>(m_sendqueue);
|
|
foreach (KeyValuePair<UUID, long> kvp in sends)
|
|
{
|
|
if (kvp.Value < now)
|
|
{
|
|
m_log.DebugFormat("[AVFACTORY]: send appearance for {0} at time {1}", kvp.Key, now);
|
|
Util.FireAndForget(delegate(object o) { HandleAppearanceSend(kvp.Key); });
|
|
m_sendqueue.Remove(kvp.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
lock (m_savequeue)
|
|
{
|
|
Dictionary<UUID, long> saves = new Dictionary<UUID, long>(m_savequeue);
|
|
foreach (KeyValuePair<UUID, long> kvp in saves)
|
|
{
|
|
if (kvp.Value < now)
|
|
{
|
|
Util.FireAndForget(delegate(object o) { HandleAppearanceSave(kvp.Key); });
|
|
m_savequeue.Remove(kvp.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_savequeue.Count == 0 && m_sendqueue.Count == 0)
|
|
m_updateTimer.Stop();
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Tell the client for this scene presence what items it should be wearing now
|
|
/// </summary>
|
|
public void SendWearables(IClientAPI client)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(client.AgentId);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: SendWearables unable to find presence for {0}", client.AgentId);
|
|
return;
|
|
}
|
|
|
|
// m_log.WarnFormat("[AVFACTORY]: Received request for wearables of {0}", client.AgentId);
|
|
|
|
client.SendWearables(sp.Appearance.Wearables, sp.Appearance.Serial++);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update what the avatar is wearing using an item from their inventory.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
public void AvatarIsWearing(IClientAPI client, AvatarWearingArgs e)
|
|
{
|
|
ScenePresence sp = m_scene.GetScenePresence(client.AgentId);
|
|
if (sp == null)
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: AvatarIsWearing unable to find presence for {0}", client.AgentId);
|
|
return;
|
|
}
|
|
|
|
// m_log.WarnFormat("[AVFACTORY]: AvatarIsWearing called for {0}", client.AgentId);
|
|
|
|
// we need to clean out the existing textures
|
|
sp.Appearance.ResetAppearance();
|
|
|
|
// operate on a copy of the appearance so we don't have to lock anything yet
|
|
AvatarAppearance avatAppearance = new AvatarAppearance(sp.Appearance, false);
|
|
|
|
foreach (AvatarWearingArgs.Wearable wear in e.NowWearing)
|
|
{
|
|
if (wear.Type < AvatarWearable.MAX_WEARABLES)
|
|
avatAppearance.Wearables[wear.Type].Add(wear.ItemID, UUID.Zero);
|
|
}
|
|
|
|
avatAppearance.GetAssetsFrom(sp.Appearance);
|
|
|
|
// This could take awhile since it needs to pull inventory
|
|
SetAppearanceAssets(sp.UUID, ref avatAppearance);
|
|
|
|
lock (m_setAppearanceLock)
|
|
{
|
|
// Update only those fields that we have changed. This is important because the viewer
|
|
// often sends AvatarIsWearing and SetAppearance packets at once, and AvatarIsWearing
|
|
// shouldn't overwrite the changes made in SetAppearance.
|
|
sp.Appearance.Wearables = avatAppearance.Wearables;
|
|
sp.Appearance.Texture = avatAppearance.Texture;
|
|
|
|
// We don't need to send the appearance here since the "iswearing" will trigger a new set
|
|
// of visual param and baked texture changes. When those complete, the new appearance will be sent
|
|
|
|
QueueAppearanceSave(client.AgentId);
|
|
}
|
|
}
|
|
|
|
private void SetAppearanceAssets(UUID userID, ref AvatarAppearance appearance)
|
|
{
|
|
IInventoryService invService = m_scene.InventoryService;
|
|
|
|
if (invService.GetRootFolder(userID) != null)
|
|
{
|
|
for (int i = 0; i < AvatarWearable.MAX_WEARABLES; i++)
|
|
{
|
|
for (int j = 0; j < appearance.Wearables[j].Count; j++)
|
|
{
|
|
if (appearance.Wearables[i][j].ItemID == UUID.Zero)
|
|
continue;
|
|
|
|
// Ignore ruth's assets
|
|
if (appearance.Wearables[i][j].ItemID == AvatarWearable.DefaultWearables[i][0].ItemID)
|
|
continue;
|
|
InventoryItemBase baseItem = new InventoryItemBase(appearance.Wearables[i][j].ItemID, userID);
|
|
baseItem = invService.GetItem(baseItem);
|
|
|
|
if (baseItem != null)
|
|
{
|
|
appearance.Wearables[i].Add(appearance.Wearables[i][j].ItemID, baseItem.AssetID);
|
|
}
|
|
else
|
|
{
|
|
m_log.ErrorFormat(
|
|
"[AVFACTORY]: Can't find inventory item {0} for {1}, setting to default",
|
|
appearance.Wearables[i][j].ItemID, (WearableType)i);
|
|
|
|
appearance.Wearables[i].RemoveItem(appearance.Wearables[i][j].ItemID);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_log.WarnFormat("[AVFACTORY]: user {0} has no inventory, appearance isn't going to work", userID);
|
|
}
|
|
}
|
|
}
|
|
}
|