Files
libremetaverse/libsecondlife/libsecondlife.Utilities/Utilities.cs
John Hurliman 6e7af40682 * Fixed a crash when attempting to teleport with CAPS disabled
* The meaning of Settings.ALWAYS_DECODE_OBJECTS has changed slightly, agent updates are now always decoded regardless of this setting. Clients that are not doing object tracking and have this set to false should see less memory usage
* NameValue is now a struct
* LLObject.NameValues is now an array to help with performance and serialization

git-svn-id: http://libopenmetaverse.googlecode.com/svn/trunk@1135 52acb1d6-8a22-11de-b505-999d5b087335
2007-04-20 16:03:24 +00:00

768 lines
29 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using libsecondlife;
using libsecondlife.Packets;
namespace libsecondlife.Utilities
{
/// <summary>
///
/// </summary>
public enum WaterType
{
/// <summary></summary>
Unknown,
/// <summary></summary>
Dry,
/// <summary></summary>
Waterfront,
/// <summary></summary>
Underwater
}
public static class Realism
{
public readonly static LLUUID TypingAnimation = new LLUUID("c541c47f-e0c0-058b-ad1a-d6ae3a4584d9");
/// <summary>
/// A psuedo-realistic chat function that uses the typing sound and
/// animation, types at three characters per second, and randomly
/// pauses. This function will block until the message has been sent
/// </summary>
/// <param name="client">A reference to the client that will chat</param>
/// <param name="message">The chat message to send</param>
public static void Chat(SecondLife client, string message)
{
Chat(client, message, MainAvatar.ChatType.Normal, 3);
}
/// <summary>
/// A psuedo-realistic chat function that uses the typing sound and
/// animation, types at a given rate, and randomly pauses. This
/// function will block until the message has been sent
/// </summary>
/// <param name="client">A reference to the client that will chat</param>
/// <param name="message">The chat message to send</param>
/// <param name="type">The chat type (usually Normal, Whisper or Shout)</param>
/// <param name="cps">Characters per second rate for chatting</param>
public static void Chat(SecondLife client, string message, MainAvatar.ChatType type, int cps)
{
Random rand = new Random();
int characters = 0;
bool typing = true;
// Start typing
client.Self.Chat(String.Empty, 0, MainAvatar.ChatType.StartTyping);
client.Self.AnimationStart(TypingAnimation);
while (characters < message.Length)
{
if (!typing)
{
// Start typing again
client.Self.Chat(String.Empty, 0, MainAvatar.ChatType.StartTyping);
client.Self.AnimationStart(TypingAnimation);
typing = true;
}
else
{
// Randomly pause typing
if (rand.Next(10) >= 9)
{
client.Self.Chat(String.Empty, 0, MainAvatar.ChatType.StopTyping);
client.Self.AnimationStop(TypingAnimation);
typing = false;
}
}
// Sleep for a second and increase the amount of characters we've typed
System.Threading.Thread.Sleep(1000);
characters += cps;
}
// Send the message
client.Self.Chat(message, 0, type);
// Stop typing
client.Self.Chat(String.Empty, 0, MainAvatar.ChatType.StopTyping);
client.Self.AnimationStop(TypingAnimation);
}
}
public class ConnectionManager
{
private SecondLife Client;
private ulong SimHandle;
private LLVector3 Position = LLVector3.Zero;
private System.Timers.Timer CheckTimer;
public ConnectionManager(SecondLife client, int timerFrequency)
{
Client = client;
CheckTimer = new System.Timers.Timer(timerFrequency);
CheckTimer.Elapsed += new System.Timers.ElapsedEventHandler(CheckTimer_Elapsed);
}
public static bool PersistentLogin(SecondLife client, string firstName, string lastName, string password,
string userAgent, string start, string author)
{
int unknownLogins = 0;
Start:
if (client.Network.Login(firstName, lastName, password, userAgent, start, author))
{
client.Log("Logged in to " + client.Network.CurrentSim, Helpers.LogLevel.Info);
return true;
}
else
{
if (client.Network.LoginErrorKey == "god")
{
client.Log("Grid is down, waiting 10 minutes", Helpers.LogLevel.Warning);
LoginWait(10);
goto Start;
}
else if (client.Network.LoginErrorKey == "key")
{
client.Log("Bad username or password, giving up on login", Helpers.LogLevel.Error);
return false;
}
else if (client.Network.LoginErrorKey == "presence")
{
client.Log("Server is still logging us out, waiting 1 minute", Helpers.LogLevel.Warning);
LoginWait(1);
goto Start;
}
else if (client.Network.LoginErrorKey == "disabled")
{
client.Log("This account has been banned! Giving up on login", Helpers.LogLevel.Error);
return false;
}
else if (client.Network.LoginErrorKey == "timed out")
{
client.Log("Login request timed out, waiting 1 minute", Helpers.LogLevel.Warning);
LoginWait(1);
goto Start;
}
else
{
++unknownLogins;
if (unknownLogins < 5)
{
client.Log("Unknown login error, waiting 2 minutes: " + client.Network.LoginErrorKey + ": " +
client.Network.LoginMessage, Helpers.LogLevel.Warning);
LoginWait(2);
goto Start;
}
else
{
client.Log("Too many unknown login error codes, giving up", Helpers.LogLevel.Error);
return false;
}
}
}
}
public void StayInSim(ulong handle, LLVector3 desiredPosition)
{
SimHandle = handle;
Position = desiredPosition;
CheckTimer.Start();
}
private static void LoginWait(int minutes)
{
Thread.Sleep(1000 * 60 * minutes);
}
private void CheckTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
if (SimHandle != 0)
{
if (Client.Network.CurrentSim.Handle != 0 &&
Client.Network.CurrentSim.Handle != SimHandle)
{
// Attempt to move to our target sim
Client.Self.Teleport(SimHandle, Position);
}
}
}
}
/// <summary>
/// Maintains a cache of avatars and does blocking lookups for avatar data
/// </summary>
public class AvatarTracker
{
protected SecondLife Client;
protected Dictionary<LLUUID, Avatar> avatars = new Dictionary<LLUUID, Avatar>();
protected Dictionary<LLUUID, ManualResetEvent> NameLookupEvents = new Dictionary<LLUUID, ManualResetEvent>();
protected Dictionary<LLUUID, ManualResetEvent> StatisticsLookupEvents = new Dictionary<LLUUID, ManualResetEvent>();
protected Dictionary<LLUUID, ManualResetEvent> PropertiesLookupEvents = new Dictionary<LLUUID, ManualResetEvent>();
protected Dictionary<LLUUID, ManualResetEvent> InterestsLookupEvents = new Dictionary<LLUUID, ManualResetEvent>();
protected Dictionary<LLUUID, ManualResetEvent> GroupsLookupEvents = new Dictionary<LLUUID, ManualResetEvent>();
public AvatarTracker(SecondLife client)
{
Client = client;
Client.Avatars.OnAvatarNames += new AvatarManager.AvatarNamesCallback(Avatars_OnAvatarNames);
Client.Avatars.OnAvatarInterests += new AvatarManager.AvatarInterestsCallback(Avatars_OnAvatarInterests);
Client.Avatars.OnAvatarProperties += new AvatarManager.AvatarPropertiesCallback(Avatars_OnAvatarProperties);
Client.Avatars.OnAvatarStatistics += new AvatarManager.AvatarStatisticsCallback(Avatars_OnAvatarStatistics);
Client.Avatars.OnAvatarGroups += new AvatarManager.AvatarGroupsCallback(Avatars_OnAvatarGroups);
//Client.Objects.OnNewAvatar += new ObjectManager.NewAvatarCallback(Objects_OnNewAvatar);
//Client.Objects.OnObjectUpdated += new ObjectManager.ObjectUpdatedCallback(Objects_OnObjectUpdated);
}
/// <summary>
/// Check if a particular avatar is in the local cache
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public bool Contains(LLUUID id)
{
return avatars.ContainsKey(id);
}
public Dictionary<LLUUID, Avatar> SimLocalAvatars()
{
Dictionary<LLUUID, Avatar> local = new Dictionary<LLUUID, Avatar>();
lock (avatars)
{
foreach (Avatar avatar in avatars.Values)
{
if (avatar.CurrentSim == Client.Network.CurrentSim)
local[avatar.ID] = avatar;
}
}
return local;
}
/// <summary>
/// Get an avatar's name, either from the cache or request it.
/// This function is blocking
/// </summary>
/// <param name="id">Avatar key to look up</param>
/// <returns>The avatar name, or String.Empty if the lookup failed</returns>
public string GetAvatarName(LLUUID id)
{
// Short circuit the cache lookup in GetAvatarNames
if (Contains(id))
return LocalAvatarNameLookup(id);
// Add to the dictionary
lock (NameLookupEvents) NameLookupEvents.Add(id, new ManualResetEvent(false));
// Call function
Client.Avatars.RequestAvatarName(id);
// Start blocking while we wait for this name to be fetched
NameLookupEvents[id].WaitOne(5000, false);
// Clean up
lock (NameLookupEvents) NameLookupEvents.Remove(id);
// Return
return LocalAvatarNameLookup(id);
}
public bool GetAvatarProfile(LLUUID id, out Avatar.Interests interests, out Avatar.AvatarProperties properties,
out Avatar.Statistics statistics, out List<LLUUID> groups)
{
// Do a local lookup first
if (avatars.ContainsKey(id) && avatars[id].ProfileProperties.BornOn != null &&
avatars[id].ProfileProperties.BornOn != String.Empty)
{
interests = avatars[id].ProfileInterests;
properties = avatars[id].ProfileProperties;
statistics = avatars[id].ProfileStatistics;
groups = avatars[id].Groups;
return true;
}
// Create the ManualResetEvents
lock (PropertiesLookupEvents)
if (!PropertiesLookupEvents.ContainsKey(id))
PropertiesLookupEvents[id] = new ManualResetEvent(false);
lock (InterestsLookupEvents)
if (!InterestsLookupEvents.ContainsKey(id))
InterestsLookupEvents[id] = new ManualResetEvent(false);
lock (StatisticsLookupEvents)
if (!StatisticsLookupEvents.ContainsKey(id))
StatisticsLookupEvents[id] = new ManualResetEvent(false);
lock (GroupsLookupEvents)
if (!GroupsLookupEvents.ContainsKey(id))
GroupsLookupEvents[id] = new ManualResetEvent(false);
// Request the avatar profile
Client.Avatars.RequestAvatarProperties(id);
// Wait for all of the events to complete
PropertiesLookupEvents[id].WaitOne(5000, false);
InterestsLookupEvents[id].WaitOne(5000, false);
StatisticsLookupEvents[id].WaitOne(5000, false);
GroupsLookupEvents[id].WaitOne(5000, false);
// Destroy the ManualResetEvents
lock (PropertiesLookupEvents)
PropertiesLookupEvents.Remove(id);
lock (InterestsLookupEvents)
InterestsLookupEvents.Remove(id);
lock (StatisticsLookupEvents)
StatisticsLookupEvents.Remove(id);
lock (GroupsLookupEvents)
GroupsLookupEvents.Remove(id);
// If we got a filled in profile return everything
if (avatars.ContainsKey(id) && avatars[id].ProfileProperties.BornOn != null &&
avatars[id].ProfileProperties.BornOn != String.Empty)
{
interests = avatars[id].ProfileInterests;
properties = avatars[id].ProfileProperties;
statistics = avatars[id].ProfileStatistics;
groups = avatars[id].Groups;
return true;
}
else
{
interests = new Avatar.Interests();
properties = new Avatar.AvatarProperties();
statistics = new Avatar.Statistics();
groups = null;
return false;
}
}
/// <summary>
/// This function will only check if the avatar name exists locally,
/// it will not do any networking calls to fetch the name
/// </summary>
/// <returns>The avatar name, or an empty string if it's not found</returns>
protected string LocalAvatarNameLookup(LLUUID id)
{
lock (avatars)
{
if (avatars.ContainsKey(id))
return avatars[id].Name;
else
return String.Empty;
}
}
private void Avatars_OnAvatarNames(Dictionary<LLUUID, string> names)
{
lock (avatars)
{
foreach (KeyValuePair<LLUUID, string> kvp in names)
{
if (!avatars.ContainsKey(kvp.Key) || avatars[kvp.Key] == null)
avatars[kvp.Key] = new Avatar();
// FIXME: Change this to .name when we move inside libsecondlife
avatars[kvp.Key].Name = kvp.Value;
if (NameLookupEvents.ContainsKey(kvp.Key))
NameLookupEvents[kvp.Key].Set();
}
}
}
void Avatars_OnAvatarStatistics(LLUUID avatarID, Avatar.Statistics statistics)
{
lock (avatars)
{
if (!avatars.ContainsKey(avatarID))
avatars[avatarID] = new Avatar();
avatars[avatarID].ProfileStatistics = statistics;
}
if (StatisticsLookupEvents.ContainsKey(avatarID))
StatisticsLookupEvents[avatarID].Set();
}
void Avatars_OnAvatarProperties(LLUUID avatarID, Avatar.AvatarProperties properties)
{
lock (avatars)
{
if (!avatars.ContainsKey(avatarID))
avatars[avatarID] = new Avatar();
avatars[avatarID].ProfileProperties = properties;
}
if (PropertiesLookupEvents.ContainsKey(avatarID))
PropertiesLookupEvents[avatarID].Set();
}
void Avatars_OnAvatarInterests(LLUUID avatarID, Avatar.Interests interests)
{
lock (avatars)
{
if (!avatars.ContainsKey(avatarID))
avatars[avatarID] = new Avatar();
avatars[avatarID].ProfileInterests = interests;
}
if (InterestsLookupEvents.ContainsKey(avatarID))
InterestsLookupEvents[avatarID].Set();
}
void Avatars_OnAvatarGroups(LLUUID avatarID, AvatarGroupsReplyPacket.GroupDataBlock[] groups)
{
List<LLUUID> groupList = new List<LLUUID>();
foreach (AvatarGroupsReplyPacket.GroupDataBlock block in groups)
{
// TODO: We just toss away all the other information here, seems like a waste...
groupList.Add(block.GroupID);
}
lock (avatars)
{
if (!avatars.ContainsKey(avatarID))
avatars[avatarID] = new Avatar();
avatars[avatarID].Groups = groupList;
}
if (GroupsLookupEvents.ContainsKey(avatarID))
GroupsLookupEvents[avatarID].Set();
}
}
/// <summary>
///
/// </summary>
public class ParcelDownloader
{
/// <summary>
///
/// </summary>
/// <param name="simulator">Simulator where the parcels are located</param>
/// <param name="Parcels">Mapping of parcel LocalIDs to Parcel objects</param>
public delegate void ParcelsDownloadedCallback(Simulator simulator, Dictionary<int, Parcel> Parcels, int[,] map);
/// <summary>
///
/// </summary>
public event ParcelsDownloadedCallback OnParcelsDownloaded;
private SecondLife Client;
/// <summary>Dictionary of 64x64 arrays of parcels which have been successfully downloaded
/// for each simulator (and their LocalID's, 0 = Null)</summary>
private Dictionary<Simulator, int[,]> ParcelMarked = new Dictionary<Simulator, int[,]>();
private Dictionary<Simulator, Dictionary<int, Parcel>> Parcels = new Dictionary<Simulator, Dictionary<int, Parcel>>();
private List<Simulator> active_sims = new List<Simulator>();
/// <summary>
/// Default constructor
/// </summary>
/// <param name="client">A reference to the SecondLife client</param>
public ParcelDownloader(SecondLife client)
{
Client = client;
Client.Parcels.OnParcelProperties += new ParcelManager.ParcelPropertiesCallback(Parcels_OnParcelProperties);
Client.Parcels.OnAccessListReply += new ParcelManager.ParcelAccessListReplyCallback(Parcels_OnParcelAccessList);
}
public void DownloadSimParcels(Simulator simulator)
{
lock (active_sims)
{
if (active_sims.Contains(simulator))
{
Client.Log("DownloadSimParcels(" + simulator + ") called more than once?", Helpers.LogLevel.Error);
return;
}
active_sims.Add(simulator);
}
lock (ParcelMarked)
{
if (!ParcelMarked.ContainsKey(simulator))
{
ParcelMarked[simulator] = new int[64, 64];
Parcels[simulator] = new Dictionary<int, Parcel>();
}
}
Client.Parcels.PropertiesRequest(simulator, 0.0f, 0.0f, 0.0f, 0.0f, 0, false);
}
/// <summary>
///
/// </summary>
/// <param name="map"></param>
/// <param name="localid"></param>
/// <returns></returns>
public float GetHeightRange(int[,] map, int localid)
{
float min = Single.MaxValue;
float max = 0.0f;
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
if (map[y, x] == localid)
{
for (int y1 = 0; y1 < 4; y1++)
{
for (int x1 = 0; x1 < 4; x1++)
{
float height;
int tries = 0;
CheckHeight:
if (Client.Terrain.TerrainHeightAtPoint(Client.Network.CurrentSim.Handle,
x * 4 + x1, y * 4 + y1, out height))
{
if (height < min)
min = height;
if (height > max)
max = height;
}
else if (tries > 4)
{
Client.Log("Too many tries on this terrain block, skipping",
Helpers.LogLevel.Warning);
continue;
}
else
{
Client.Log(String.Format("Terrain height is null at {0},{1} retrying",
x * 4 + x1, y * 4 + y1), Helpers.LogLevel.Info);
// Terrain at this point hasn't been downloaded, move the camera to this spot
// and try again
Client.Self.Status.Camera.CameraCenter.X = (float)(x * 4 + x1);
Client.Self.Status.Camera.CameraCenter.Y = (float)(y * 4 + y1);
Client.Self.Status.Camera.CameraCenter.Z = Client.Self.Position.Z;
Client.Self.Status.SendUpdate(true);
Thread.Sleep(1000);
goto CheckHeight;
}
}
}
}
}
}
if (min != Single.MaxValue)
{
return max - min;
}
else
{
Client.Log("Error decoding terrain for parcel " + localid, Helpers.LogLevel.Error);
return Single.NaN;
}
}
/// <summary>
///
/// </summary>
/// <param name="map"></param>
/// <param name="localid"></param>
/// <returns></returns>
public WaterType GetWaterType(int[,] map, int localid)
{
if (!Client.Settings.STORE_LAND_PATCHES)
{
Client.Log("GetWaterType() will not work without Settings.STORE_LAND_PATCHES set to true",
Helpers.LogLevel.Error);
return WaterType.Unknown;
}
bool underwater = false;
bool abovewater = false;
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
if (map[y, x] == localid)
{
for (int y1 = 0; y1 < 4; y1++)
{
for (int x1 = 0; x1 < 4; x1++)
{
float height;
int tries = 0;
CheckHeight:
tries++;
if (Client.Terrain.TerrainHeightAtPoint(Client.Network.CurrentSim.Handle,
x * 4 + x1, y * 4 + y1, out height))
{
if (height < Client.Network.CurrentSim.WaterHeight)
{
underwater = true;
}
else
{
abovewater = true;
}
}
else if (tries > 4)
{
Client.Log("Too many tries on this terrain block, skipping",
Helpers.LogLevel.Warning);
continue;
}
else
{
Client.Log(String.Format("Terrain height is null at {0},{1} retrying",
x * 4 + x1, y * 4 + y1), Helpers.LogLevel.Info);
// Terrain at this point hasn't been downloaded, move the camera to this spot
// and try again
Client.Self.Status.Camera.CameraCenter.X = (float)(x * 4 + x1);
Client.Self.Status.Camera.CameraCenter.Y = (float)(y * 4 + y1);
Client.Self.Status.Camera.CameraCenter.Z = Client.Self.Position.Z;
Client.Self.Status.SendUpdate(true);
Thread.Sleep(1000);
goto CheckHeight;
}
}
}
}
}
}
if (underwater && abovewater)
{
return WaterType.Waterfront;
}
else if (abovewater)
{
return WaterType.Dry;
}
else if (underwater)
{
return WaterType.Underwater;
}
else
{
Client.Log("Error decoding terrain for parcel " + localid, Helpers.LogLevel.Error);
return WaterType.Unknown;
}
}
public int GetRectangularDeviation(LLVector3 aabbmin, LLVector3 aabbmax, int area)
{
int xlength = (int)(aabbmax.X - aabbmin.X);
int ylength = (int)(aabbmax.Y - aabbmin.Y);
int aabbarea = xlength * ylength;
return (aabbarea - area) / 16;
}
private void Parcels_OnParcelAccessList(Simulator simulator, int sequenceID, int localID, uint flags,
List<ParcelManager.ParcelAccessEntry> accessEntries)
{
Parcels[simulator][localID].AccessList = accessEntries;
}
private void Parcels_OnParcelProperties(Parcel parcel, ParcelManager.ParcelResult result, int sequenceID,
bool snapSelection)
{
// Check if this is for a simulator we're concerned with
if (!active_sims.Contains(parcel.Simulator)) return;
// Warn about parcel property request errors and bail out
if (result == ParcelManager.ParcelResult.NoData)
{
Client.Log("ParcelDownloader received a NoData response, sequenceID " + sequenceID,
Helpers.LogLevel.Warning);
return;
}
// Warn about unexpected data and bail out
if (!ParcelMarked.ContainsKey(parcel.Simulator))
{
Client.Log("ParcelDownloader received unexpected parcel data for " + parcel.Simulator,
Helpers.LogLevel.Warning);
return;
}
int x, y, index, bit;
int[,] markers = ParcelMarked[parcel.Simulator];
// Add this parcel to the dictionary of LocalID -> Parcel mappings
lock (Parcels[parcel.Simulator])
if (!Parcels[parcel.Simulator].ContainsKey(parcel.LocalID))
Parcels[parcel.Simulator][parcel.LocalID] = parcel;
// Request the access list for this parcel
Client.Parcels.AccessListRequest(parcel.Simulator, parcel.LocalID,
ParcelManager.AccessList.Both, 0);
// Mark this area as downloaded
for (y = 0; y < 64; y++)
{
for (x = 0; x < 64; x++)
{
if (markers[y, x] == 0)
{
index = (y * 64) + x;
bit = index % 8;
index >>= 3;
if ((parcel.Bitmap[index] & (1 << bit)) != 0)
markers[y, x] = parcel.LocalID;
}
}
}
// Request parcel information for the next missing area
for (y = 0; y < 64; y++)
{
for (x = 0; x < 64; x++)
{
if (markers[y, x] == 0)
{
Client.Parcels.PropertiesRequest(parcel.Simulator,
(y + 1) * 4.0f, (x + 1) * 4.0f,
y * 4.0f, x * 4.0f, 0, false);
return;
}
}
}
// If we get here, there are no more zeroes in the markers map
lock (active_sims)
{
active_sims.Remove(parcel.Simulator);
if (OnParcelsDownloaded != null)
{
// This map is complete, fire callback
try { OnParcelsDownloaded(parcel.Simulator, Parcels[parcel.Simulator], markers); }
catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); }
}
}
}
}
}