diff --git a/OpenMetaverse/AssetManager.cs b/OpenMetaverse/AssetManager.cs index 131cf258..063c8c46 100644 --- a/OpenMetaverse/AssetManager.cs +++ b/OpenMetaverse/AssetManager.cs @@ -1475,316 +1475,4 @@ namespace OpenMetaverse #endregion Image Callbacks } - - #region Texture Cache - /// - /// Class that handles the local image cache - /// - public class TextureCache - { - private GridClient Client; - private Thread cleanerThread; - private System.Timers.Timer cleanerTimer; - private double pruneInterval = 1000 * 60 * 5; - - /// - /// Allows setting weather to periodicale prune the cache if it grows too big - /// Default is enabled, when caching is enabled - /// - public bool AutoPruneEnabled - { - set { - if (!Operational()) { - return; - } else { - cleanerTimer.Enabled = value; - } - } - get { return cleanerTimer.Enabled;} - } - - /// - /// How long (in ms) between cache checks (default is 5 min.) - /// - public double AutoPruneInterval - { - get { return pruneInterval; } - set - { - pruneInterval = value; - cleanerTimer.Interval = pruneInterval; - } - } - - /// - /// Default constructor - /// - /// A reference to the GridClient object - public TextureCache(GridClient client) - { - Client = client; - cleanerTimer = new System.Timers.Timer(pruneInterval); - cleanerTimer.Elapsed += new System.Timers.ElapsedEventHandler(cleanerTimer_Elapsed); - if (Operational()) { - cleanerTimer.Enabled = true; - } else { - cleanerTimer.Enabled = false; - } - } - - /// - /// Return bytes read from the local image cache, null if it does not exist - /// - /// UUID of the image we want to get - /// Raw bytes of the image, or null on failure - public byte[] GetCachedImageBytes(UUID imageID) - { - if (!Operational()) { - return null; - } - try { - Logger.DebugLog("Reading " + FileName(imageID) + " from texture cache."); - byte[] data = File.ReadAllBytes(FileName(imageID)); - return data; - } catch (Exception ex) { - Logger.Log("Failed reading image from cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); - return null; - } - } - - /// - /// Returns ImageDownload object of the - /// image from the local image cache, null if it does not exist - /// - /// UUID of the image we want to get - /// ImageDownload object containing the image, or null on failure - public ImageDownload GetCachedImage(UUID imageID) - { - if (!Operational()) - return null; - - byte[] imageData = GetCachedImageBytes(imageID); - if (imageData == null) - return null; - ImageDownload transfer = new ImageDownload(); - transfer.AssetType = AssetType.Texture; - transfer.ID = imageID; - transfer.Simulator = Client.Network.CurrentSim; - transfer.Size = imageData.Length; - transfer.Success = true; - transfer.Transferred = imageData.Length; - transfer.AssetData = imageData; - return transfer; - } - - /// - /// Constructs a file name of the cached image - /// - /// UUID of the image - /// String with the file name of the cahced image - private string FileName(UUID imageID) - { - return Client.Settings.TEXTURE_CACHE_DIR + Path.DirectorySeparatorChar + imageID.ToString(); - } - - /// - /// Saves an image to the local cache - /// - /// UUID of the image - /// Raw bytes the image consists of - /// Weather the operation was successfull - public bool SaveImageToCache(UUID imageID, byte[] imageData) - { - if (!Operational()) { - return false; - } - - try { - Logger.DebugLog("Saving " + FileName(imageID) + " to texture cache.", Client); - - if (!Directory.Exists(Client.Settings.TEXTURE_CACHE_DIR)) { - Directory.CreateDirectory(Client.Settings.TEXTURE_CACHE_DIR); - } - - File.WriteAllBytes(FileName(imageID), imageData); - } catch (Exception ex) { - Logger.Log("Failed saving image to cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); - return false; - } - - return true; - } - - /// - /// Get the file name of the asset stored with gived UUID - /// - /// UUID of the image - /// Null if we don't have that UUID cached on disk, file name if found in the cache folder - public string ImageFileName(UUID imageID) - { - if (!Operational()) - { - return null; - } - - string fileName = FileName(imageID); - - if (File.Exists(fileName)) - return fileName; - else - return null; - } - - /// - /// Checks if the image exists in the local cache - /// - /// UUID of the image - /// True is the image is stored in the cache, otherwise false - public bool HasImage(UUID imageID) - { - if (!Operational()) { - return false; - } - return File.Exists(FileName(imageID)); - } - - /// - /// Wipes out entire cache - /// - public void Clear() - { - string cacheDir = Client.Settings.TEXTURE_CACHE_DIR; - if (!Directory.Exists(cacheDir)) { - return; - } - - DirectoryInfo di = new DirectoryInfo(cacheDir); - // We save file with UUID as file name, only delete those - FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); - - int num = 0; - foreach (FileInfo file in files) { - file.Delete(); - ++num; - } - - Logger.Log("Wiped out " + num + " files from the cache directory.", Helpers.LogLevel.Debug); - } - - /// - /// Brings cache size to the 90% of the max size - /// - public void Prune() - { - string cacheDir = Client.Settings.TEXTURE_CACHE_DIR; - if (!Directory.Exists(cacheDir)) { - return; - } - DirectoryInfo di = new DirectoryInfo(cacheDir); - // We save file with UUID as file name, only count those - FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); - - long size = GetFileSize(files); - - if (size > Client.Settings.TEXTURE_CACHE_MAX_SIZE) { - Array.Sort(files, new SortFilesByAccesTimeHelper()); - long targetSize = (long)(Client.Settings.TEXTURE_CACHE_MAX_SIZE * 0.9); - int num = 0; - foreach (FileInfo file in files) { - ++num; - size -= file.Length; - file.Delete(); - if (size < targetSize) { - break; - } - } - Logger.Log(num + " files deleted from the cache, cache size now: " + NiceFileSize(size), Helpers.LogLevel.Debug); - } else { - Logger.Log("Cache size is " + NiceFileSize(size) + ", file deletion not needed", Helpers.LogLevel.Debug); - } - - } - - /// - /// Asynchronously brings cache size to the 90% of the max size - /// - public void BeginPrune() - { - // Check if the background cache cleaning thread is active first - if (cleanerThread != null && cleanerThread.IsAlive) { - return; - } - - lock (this) { - cleanerThread = new Thread(new ThreadStart(this.Prune)); - cleanerThread.IsBackground = true; - cleanerThread.Start(); - } - } - - /// - /// Adds up file sizes passes in a FileInfo array - /// - long GetFileSize(FileInfo[] files) - { - long ret = 0; - foreach (FileInfo file in files) { - ret += file.Length; - } - return ret; - } - - /// - /// Checks whether caching is enabled - /// - private bool Operational() - { - return Client.Settings.USE_TEXTURE_CACHE; - } - - /// - /// Periodically prune the cache - /// - private void cleanerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - BeginPrune(); - } - - /// - /// Nicely formats file sizes - /// - /// Byte size we want to output - /// String with humanly readable file size - private string NiceFileSize(long byteCount) - { - string size = "0 Bytes"; - if (byteCount >= 1073741824) - size = String.Format("{0:##.##}", byteCount / 1073741824) + " GB"; - else if (byteCount >= 1048576) - size = String.Format("{0:##.##}", byteCount / 1048576) + " MB"; - else if (byteCount >= 1024) - size = String.Format("{0:##.##}", byteCount / 1024) + " KB"; - else if (byteCount > 0 && byteCount < 1024) - size = byteCount.ToString() + " Bytes"; - - return size; - } - - /// - /// Helper class for sorting files by their last accessed time - /// - private class SortFilesByAccesTimeHelper : IComparer - { - int IComparer.Compare(FileInfo f1, FileInfo f2) - { - if (f1.LastAccessTime > f2.LastAccessTime) - return 1; - if (f1.LastAccessTime < f2.LastAccessTime) - return -1; - else - return 0; - } - } - } - #endregion } diff --git a/OpenMetaverse/Avatar.cs b/OpenMetaverse/Avatar.cs index e440fdd0..8bbf8487 100644 --- a/OpenMetaverse/Avatar.cs +++ b/OpenMetaverse/Avatar.cs @@ -215,6 +215,36 @@ namespace OpenMetaverse #region Properties + /// First name + public string FirstName + { + get + { + for (int i = 0; i < NameValues.Length; i++) + { + if (NameValues[i].Name == "FirstName" && NameValues[i].Type == NameValue.ValueType.String) + return (string)NameValues[i].Value; + } + + return String.Empty; + } + } + + /// Last name + public string LastName + { + get + { + for (int i = 0; i < NameValues.Length; i++) + { + if (NameValues[i].Name == "LastName" && NameValues[i].Type == NameValue.ValueType.String) + return (string)NameValues[i].Value; + } + + return String.Empty; + } + } + /// Full name public string Name { diff --git a/OpenMetaverse/TerrainCompressor.cs b/OpenMetaverse/TerrainCompressor.cs index 5a432881..e7618b49 100644 --- a/OpenMetaverse/TerrainCompressor.cs +++ b/OpenMetaverse/TerrainCompressor.cs @@ -148,6 +148,32 @@ namespace OpenMetaverse return layer; } + public static LayerDataPacket CreateLandPacket(float[] patchData, int x, int y) + { + LayerDataPacket layer = new LayerDataPacket(); + layer.LayerID.Type = (byte)TerrainPatch.LayerType.Land; + + TerrainPatch.GroupHeader header = new TerrainPatch.GroupHeader(); + header.Stride = STRIDE; + header.PatchSize = 16; + header.Type = TerrainPatch.LayerType.Land; + + byte[] data = new byte[1536]; + BitPack bitpack = new BitPack(data, 0); + bitpack.PackBits(header.Stride, 16); + bitpack.PackBits(header.PatchSize, 8); + bitpack.PackBits((int)header.Type, 8); + + CreatePatch(bitpack, patchData, x, y); + + bitpack.PackBits(END_OF_PATCHES, 8); + + layer.LayerData.Data = new byte[bitpack.BytePos + 1]; + Buffer.BlockCopy(bitpack.Data, 0, layer.LayerData.Data, 0, bitpack.BytePos + 1); + + return layer; + } + public static void CreatePatch(BitPack output, float[] patchData, int x, int y) { if (patchData.Length != 16 * 16) diff --git a/OpenMetaverse/TextureCache.cs b/OpenMetaverse/TextureCache.cs new file mode 100644 index 00000000..a543cd8b --- /dev/null +++ b/OpenMetaverse/TextureCache.cs @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * All rights reserved. + * + * - Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Neither the name of the openmetaverse.org nor the names + * of its contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace OpenMetaverse +{ + /// + /// Class that handles the local image cache + /// + public class TextureCache + { + private GridClient Client; + private Thread cleanerThread; + private System.Timers.Timer cleanerTimer; + private double pruneInterval = 1000 * 60 * 5; + + /// + /// Allows setting weather to periodicale prune the cache if it grows too big + /// Default is enabled, when caching is enabled + /// + public bool AutoPruneEnabled + { + set + { + if (!Operational()) + { + return; + } + else + { + cleanerTimer.Enabled = value; + } + } + get { return cleanerTimer.Enabled; } + } + + /// + /// How long (in ms) between cache checks (default is 5 min.) + /// + public double AutoPruneInterval + { + get { return pruneInterval; } + set + { + pruneInterval = value; + cleanerTimer.Interval = pruneInterval; + } + } + + /// + /// Default constructor + /// + /// A reference to the GridClient object + public TextureCache(GridClient client) + { + Client = client; + cleanerTimer = new System.Timers.Timer(pruneInterval); + cleanerTimer.Elapsed += new System.Timers.ElapsedEventHandler(cleanerTimer_Elapsed); + if (Operational()) + { + cleanerTimer.Enabled = true; + } + else + { + cleanerTimer.Enabled = false; + } + } + + /// + /// Return bytes read from the local image cache, null if it does not exist + /// + /// UUID of the image we want to get + /// Raw bytes of the image, or null on failure + public byte[] GetCachedImageBytes(UUID imageID) + { + if (!Operational()) + { + return null; + } + try + { + Logger.DebugLog("Reading " + FileName(imageID) + " from texture cache."); + byte[] data = File.ReadAllBytes(FileName(imageID)); + return data; + } + catch (Exception ex) + { + Logger.Log("Failed reading image from cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); + return null; + } + } + + /// + /// Returns ImageDownload object of the + /// image from the local image cache, null if it does not exist + /// + /// UUID of the image we want to get + /// ImageDownload object containing the image, or null on failure + public ImageDownload GetCachedImage(UUID imageID) + { + if (!Operational()) + return null; + + byte[] imageData = GetCachedImageBytes(imageID); + if (imageData == null) + return null; + ImageDownload transfer = new ImageDownload(); + transfer.AssetType = AssetType.Texture; + transfer.ID = imageID; + transfer.Simulator = Client.Network.CurrentSim; + transfer.Size = imageData.Length; + transfer.Success = true; + transfer.Transferred = imageData.Length; + transfer.AssetData = imageData; + return transfer; + } + + /// + /// Constructs a file name of the cached image + /// + /// UUID of the image + /// String with the file name of the cahced image + private string FileName(UUID imageID) + { + return Client.Settings.TEXTURE_CACHE_DIR + Path.DirectorySeparatorChar + imageID.ToString(); + } + + /// + /// Saves an image to the local cache + /// + /// UUID of the image + /// Raw bytes the image consists of + /// Weather the operation was successfull + public bool SaveImageToCache(UUID imageID, byte[] imageData) + { + if (!Operational()) + { + return false; + } + + try + { + Logger.DebugLog("Saving " + FileName(imageID) + " to texture cache.", Client); + + if (!Directory.Exists(Client.Settings.TEXTURE_CACHE_DIR)) + { + Directory.CreateDirectory(Client.Settings.TEXTURE_CACHE_DIR); + } + + File.WriteAllBytes(FileName(imageID), imageData); + } + catch (Exception ex) + { + Logger.Log("Failed saving image to cache (" + ex.Message + ")", Helpers.LogLevel.Warning, Client); + return false; + } + + return true; + } + + /// + /// Get the file name of the asset stored with gived UUID + /// + /// UUID of the image + /// Null if we don't have that UUID cached on disk, file name if found in the cache folder + public string ImageFileName(UUID imageID) + { + if (!Operational()) + { + return null; + } + + string fileName = FileName(imageID); + + if (File.Exists(fileName)) + return fileName; + else + return null; + } + + /// + /// Checks if the image exists in the local cache + /// + /// UUID of the image + /// True is the image is stored in the cache, otherwise false + public bool HasImage(UUID imageID) + { + if (!Operational()) + { + return false; + } + return File.Exists(FileName(imageID)); + } + + /// + /// Wipes out entire cache + /// + public void Clear() + { + string cacheDir = Client.Settings.TEXTURE_CACHE_DIR; + if (!Directory.Exists(cacheDir)) + { + return; + } + + DirectoryInfo di = new DirectoryInfo(cacheDir); + // We save file with UUID as file name, only delete those + FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); + + int num = 0; + foreach (FileInfo file in files) + { + file.Delete(); + ++num; + } + + Logger.Log("Wiped out " + num + " files from the cache directory.", Helpers.LogLevel.Debug); + } + + /// + /// Brings cache size to the 90% of the max size + /// + public void Prune() + { + string cacheDir = Client.Settings.TEXTURE_CACHE_DIR; + if (!Directory.Exists(cacheDir)) + { + return; + } + DirectoryInfo di = new DirectoryInfo(cacheDir); + // We save file with UUID as file name, only count those + FileInfo[] files = di.GetFiles("????????-????-????-????-????????????", SearchOption.TopDirectoryOnly); + + long size = GetFileSize(files); + + if (size > Client.Settings.TEXTURE_CACHE_MAX_SIZE) + { + Array.Sort(files, new SortFilesByAccesTimeHelper()); + long targetSize = (long)(Client.Settings.TEXTURE_CACHE_MAX_SIZE * 0.9); + int num = 0; + foreach (FileInfo file in files) + { + ++num; + size -= file.Length; + file.Delete(); + if (size < targetSize) + { + break; + } + } + Logger.Log(num + " files deleted from the cache, cache size now: " + NiceFileSize(size), Helpers.LogLevel.Debug); + } + else + { + Logger.Log("Cache size is " + NiceFileSize(size) + ", file deletion not needed", Helpers.LogLevel.Debug); + } + + } + + /// + /// Asynchronously brings cache size to the 90% of the max size + /// + public void BeginPrune() + { + // Check if the background cache cleaning thread is active first + if (cleanerThread != null && cleanerThread.IsAlive) + { + return; + } + + lock (this) + { + cleanerThread = new Thread(new ThreadStart(this.Prune)); + cleanerThread.IsBackground = true; + cleanerThread.Start(); + } + } + + /// + /// Adds up file sizes passes in a FileInfo array + /// + long GetFileSize(FileInfo[] files) + { + long ret = 0; + foreach (FileInfo file in files) + { + ret += file.Length; + } + return ret; + } + + /// + /// Checks whether caching is enabled + /// + private bool Operational() + { + return Client.Settings.USE_TEXTURE_CACHE; + } + + /// + /// Periodically prune the cache + /// + private void cleanerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + BeginPrune(); + } + + /// + /// Nicely formats file sizes + /// + /// Byte size we want to output + /// String with humanly readable file size + private string NiceFileSize(long byteCount) + { + string size = "0 Bytes"; + if (byteCount >= 1073741824) + size = String.Format("{0:##.##}", byteCount / 1073741824) + " GB"; + else if (byteCount >= 1048576) + size = String.Format("{0:##.##}", byteCount / 1048576) + " MB"; + else if (byteCount >= 1024) + size = String.Format("{0:##.##}", byteCount / 1024) + " KB"; + else if (byteCount > 0 && byteCount < 1024) + size = byteCount.ToString() + " Bytes"; + + return size; + } + + /// + /// Helper class for sorting files by their last accessed time + /// + private class SortFilesByAccesTimeHelper : IComparer + { + int IComparer.Compare(FileInfo f1, FileInfo f2) + { + if (f1.LastAccessTime > f2.LastAccessTime) + return 1; + if (f1.LastAccessTime < f2.LastAccessTime) + return -1; + else + return 0; + } + } + } +} diff --git a/OpenMetaverse/TexturePipeline.cs b/OpenMetaverse/TexturePipeline.cs new file mode 100644 index 00000000..f82b7abc --- /dev/null +++ b/OpenMetaverse/TexturePipeline.cs @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * All rights reserved. + * + * - Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Neither the name of the openmetaverse.org nor the names + * of its contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace OpenMetaverse +{ + /// + /// Texture request download handler, allows a configurable number of download slots + /// + public class TexturePipeline + { + class TaskInfo + { + public UUID RequestID; + public int RequestNbr; + public ImageType Type; + + public TaskInfo(UUID reqID, int reqNbr, ImageType type) + { + RequestID = reqID; + RequestNbr = reqNbr; + Type = type; + } + } + + public delegate void DownloadFinishedCallback(UUID id, bool success); + public delegate void DownloadProgressCallback(UUID image, int recieved, int total); + + /// Fired when a texture download completes + public event DownloadFinishedCallback OnDownloadFinished; + /// Fired when some texture data is received + public event DownloadProgressCallback OnDownloadProgress; + + /// For keeping track of active threads available/downloading + /// textures + public int[] ThreadpoolSlots + { + get { lock (threadpoolSlots) { return threadpoolSlots; } } + set { lock (threadpoolSlots) { threadpoolSlots = value; } } + } + + GridClient client; + /// Maximum concurrent texture requests + int maxTextureRequests; + /// Queue for image requests that have not been sent out yet + Queue> requestQueue; + /// Current texture downloads + Dictionary currentRequests; + /// Storage for completed texture downloads + Dictionary completedDownloads; + AutoResetEvent[] resetEvents; + int[] threadpoolSlots; + Thread downloadMaster; + bool running; + + /// + /// Default constructor + /// + /// Reference to SecondLife client + /// Maximum number of concurrent texture requests + public TexturePipeline(GridClient client, int maxRequests) + { + running = true; + this.client = client; + maxTextureRequests = maxRequests; + + requestQueue = new Queue>(); + currentRequests = new Dictionary(maxTextureRequests); + completedDownloads = new Dictionary(); + resetEvents = new AutoResetEvent[maxTextureRequests]; + threadpoolSlots = new int[maxTextureRequests]; + + // Pre-configure autoreset events/download slots + for (int i = 0; i < maxTextureRequests; i++) + { + resetEvents[i] = new AutoResetEvent(false); + threadpoolSlots[i] = -1; + } + + client.Assets.OnImageReceived += Assets_OnImageReceived; + client.Assets.OnImageReceiveProgress += Assets_OnImageReceiveProgress; + + // Fire up the texture download thread + downloadMaster = new Thread(new ThreadStart(DownloadThread)); + downloadMaster.Start(); + } + + public void Shutdown() + { + client.Assets.OnImageReceived -= Assets_OnImageReceived; + client.Assets.OnImageReceiveProgress -= Assets_OnImageReceiveProgress; + + requestQueue.Clear(); + + for (int i = 0; i < resetEvents.Length; i++) + if (resetEvents[i] != null) + resetEvents[i].Set(); + + running = false; + } + + /// + /// Request a texture be downloaded, once downloaded OnImageRenderReady event will be fired + /// containing texture key which can be used to retrieve texture with GetTextureToRender method + /// + /// Texture to request + /// Type of the requested texture + public void RequestTexture(UUID textureID, ImageType type) + { + if (client.Assets.Cache.HasImage(textureID)) + { + // Add to rendering dictionary + lock (completedDownloads) + { + if (!completedDownloads.ContainsKey(textureID)) + { + completedDownloads.Add(textureID, client.Assets.Cache.GetCachedImage(textureID)); + + // Let any subscribers know about it + if (OnDownloadFinished != null) + OnDownloadFinished(textureID, true); + } + else + { + // This image has already been served up, ignore this request + } + } + } + else + { + lock (requestQueue) + { + // Make sure the request isn't already queued up + foreach (KeyValuePair kvp in requestQueue) + if (kvp.Key == textureID) + return; + + // Make sure we aren't already downloading the texture + if (!currentRequests.ContainsKey(textureID)) + { + requestQueue.Enqueue(new KeyValuePair(textureID, type)); + } + } + } + } + + /// + /// retrieve texture information from dictionary + /// + /// Texture ID + /// ImageDownload object + public ImageDownload GetTextureToRender(UUID textureID) + { + ImageDownload renderable = new ImageDownload(); + lock (completedDownloads) + { + if (completedDownloads.ContainsKey(textureID)) + { + renderable = completedDownloads[textureID]; + } + else + { + Logger.Log("Requested texture data for texture that does not exist in dictionary", Helpers.LogLevel.Warning); + } + return renderable; + } + } + + /// + /// Remove no longer necessary texture from dictionary + /// + /// + public void RemoveFromPipeline(UUID textureID) + { + lock (completedDownloads) + { + if (completedDownloads.ContainsKey(textureID)) + completedDownloads.Remove(textureID); + } + } + + /// + /// Master Download Thread, Queues up downloads in the threadpool + /// + private void DownloadThread() + { + int reqNbr; + + while (running) + { + if (requestQueue.Count > 0) + { + reqNbr = -1; + // find available slot for reset event + for (int i = 0; i < threadpoolSlots.Length; i++) + { + if (threadpoolSlots[i] == -1) + { + threadpoolSlots[i] = 1; + reqNbr = i; + break; + } + } + + if (reqNbr != -1) + { + KeyValuePair request; + lock (requestQueue) + request = requestQueue.Dequeue(); + + Logger.DebugLog(String.Format("Sending Worker thread new download request {0}", reqNbr)); + ThreadPool.QueueUserWorkItem(new WaitCallback(TextureRequestDoWork), new TaskInfo(request.Key, reqNbr, request.Value)); + + continue; + } + } + + // Queue was empty, let's give up some CPU time + Thread.Sleep(500); + } + + Logger.Log("Texture pipeline shutting down", Helpers.LogLevel.Info); + } + + private void TextureRequestDoWork(Object threadContext) + { + TaskInfo ti = (TaskInfo)threadContext; + + lock (currentRequests) + { + if (currentRequests.ContainsKey(ti.RequestID)) + { + threadpoolSlots[ti.RequestNbr] = -1; + return; + } + else + { + currentRequests.Add(ti.RequestID, ti.RequestNbr); + } + } + + Logger.DebugLog(String.Format("Worker {0} Requesting {1}", ti.RequestNbr, ti.RequestID)); + + resetEvents[ti.RequestNbr].Reset(); + client.Assets.RequestImage(ti.RequestID, ti.Type); + + // don't release this worker slot until texture is downloaded or timeout occurs + if (!resetEvents[ti.RequestNbr].WaitOne(30 * 1000, false)) + { + // Timed out + Logger.Log("Worker " + ti.RequestNbr + " Timeout waiting for Texture " + ti.RequestID + " to Download", Helpers.LogLevel.Warning); + + lock (currentRequests) + currentRequests.Remove(ti.RequestID); + + if (OnDownloadFinished != null) + OnDownloadFinished(ti.RequestID, false); + } + + // free up this download slot + threadpoolSlots[ti.RequestNbr] = -1; + } + + private void Assets_OnImageReceived(ImageDownload image, AssetTexture asset) + { + int requestNbr; + bool found; + + lock (currentRequests) + found = currentRequests.TryGetValue(image.ID, out requestNbr); + + if (asset != null && found) + { + Logger.DebugLog(String.Format("Worker {0} Downloaded texture {1}", requestNbr, image.ID)); + + // Free up this slot in the ThreadPool + lock (currentRequests) + currentRequests.Remove(image.ID); + + resetEvents[requestNbr].Set(); + + if (image.Success) + { + // Add to the completed texture dictionary + lock (completedDownloads) + completedDownloads[image.ID] = image; + } + else + { + Logger.Log(String.Format("Download of texture {0} failed. NotFound={1}", image.ID, image.NotFound), + Helpers.LogLevel.Warning); + } + + // Let any subscribers know about it + if (OnDownloadFinished != null) + OnDownloadFinished(image.ID, image.Success); + } + } + + private void Assets_OnImageReceiveProgress(UUID image, int lastPacket, int recieved, int total) + { + if (OnDownloadProgress != null && currentRequests.ContainsKey(image)) + OnDownloadProgress(image, recieved, total); + } + } +} diff --git a/Programs/PrimWorkshop/TexturePipeline.cs b/Programs/PrimWorkshop/TexturePipeline.cs index d1987694..08d613c0 100644 --- a/Programs/PrimWorkshop/TexturePipeline.cs +++ b/Programs/PrimWorkshop/TexturePipeline.cs @@ -23,321 +23,5 @@ using OpenMetaverse; */ namespace PrimWorkshop { - class TaskInfo - { - public UUID RequestID; - public int RequestNbr; - - - public TaskInfo(UUID reqID, int reqNbr) - { - RequestID = reqID; - RequestNbr = reqNbr; - } - } - - /// - /// Texture request download handler, allows a configurable number of download slots - /// - public class TexturePipeline - { - private static GridClient Client; - - // queue for requested images - private Queue RequestQueue; - - // list of current requests in process - private Dictionary CurrentRequests; - - private static AutoResetEvent[] resetEvents; - - private static int[] threadpoolSlots; - - /// - /// For keeping track of active threads available/downloading textures - /// - public static int[] ThreadpoolSlots - { - get { lock (threadpoolSlots) { return threadpoolSlots; }} - set { lock (threadpoolSlots) { threadpoolSlots = value; } } - } - - // storage for images ready to render - private Dictionary RenderReady; - - // maximum allowed concurrent requests at once - const int MAX_TEXTURE_REQUESTS = 10; - - /// - /// - /// - /// - /// - public delegate void DownloadFinishedCallback(UUID id, bool success); - /// - /// - /// - /// - /// - /// - public delegate void DownloadProgressCallback(UUID image, int recieved, int total); - - /// Fired when a texture download completes - public event DownloadFinishedCallback OnDownloadFinished; - /// - public event DownloadProgressCallback OnDownloadProgress; - - private Thread downloadMaster; - private bool Running; - - private AssetManager.ImageReceivedCallback DownloadCallback; - private AssetManager.ImageReceiveProgressCallback DownloadProgCallback; - - /// - /// Default constructor - /// - /// Reference to SecondLife client - public TexturePipeline(GridClient client) - { - Running = true; - - RequestQueue = new Queue(); - CurrentRequests = new Dictionary(MAX_TEXTURE_REQUESTS); - - RenderReady = new Dictionary(); - - resetEvents = new AutoResetEvent[MAX_TEXTURE_REQUESTS]; - threadpoolSlots = new int[MAX_TEXTURE_REQUESTS]; - - // pre-configure autoreset events/download slots - for (int i = 0; i < MAX_TEXTURE_REQUESTS; i++) - { - resetEvents[i] = new AutoResetEvent(false); - threadpoolSlots[i] = -1; - } - - Client = client; - - DownloadCallback = new AssetManager.ImageReceivedCallback(Assets_OnImageReceived); - DownloadProgCallback = new AssetManager.ImageReceiveProgressCallback(Assets_OnImageReceiveProgress); - Client.Assets.OnImageReceived += DownloadCallback; - Client.Assets.OnImageReceiveProgress += DownloadProgCallback; - - // Fire up the texture download thread - downloadMaster = new Thread(new ThreadStart(DownloadThread)); - downloadMaster.Start(); - } - - public void Shutdown() - { - Client.Assets.OnImageReceived -= DownloadCallback; - Client.Assets.OnImageReceiveProgress -= DownloadProgCallback; - - RequestQueue.Clear(); - - for (int i = 0; i < resetEvents.Length; i++) - if (resetEvents[i] != null) - resetEvents[i].Set(); - - Running = false; - } - - /// - /// Request a texture be downloaded, once downloaded OnImageRenderReady event will be fired - /// containing texture key which can be used to retrieve texture with GetTextureToRender method - /// - /// id of Texture to request - public void RequestTexture(UUID textureID) - { - if (Client.Assets.Cache.HasImage(textureID)) - { - // Add to rendering dictionary - lock (RenderReady) - { - if (!RenderReady.ContainsKey(textureID)) - { - RenderReady.Add(textureID, Client.Assets.Cache.GetCachedImage(textureID)); - - // Let any subscribers know about it - if (OnDownloadFinished != null) - { - OnDownloadFinished(textureID, true); - } - } - else - { - // This image has already been served up, ignore this request - } - } - } - else - { - lock (RequestQueue) - { - // Make sure we aren't already downloading the texture - if (!RequestQueue.Contains(textureID) && !CurrentRequests.ContainsKey(textureID)) - { - RequestQueue.Enqueue(textureID); - } - } - } - } - - /// - /// retrieve texture information from dictionary - /// - /// Texture ID - /// ImageDownload object - public ImageDownload GetTextureToRender(UUID textureID) - { - ImageDownload renderable = new ImageDownload(); - lock (RenderReady) - { - if (RenderReady.ContainsKey(textureID)) - { - renderable = RenderReady[textureID]; - } - else - { - Logger.Log("Requested texture data for texture that does not exist in dictionary", Helpers.LogLevel.Warning); - } - return renderable; - } - } - - /// - /// Remove no longer necessary texture from dictionary - /// - /// - public void RemoveFromPipeline(UUID textureID) - { - lock (RenderReady) - { - if (RenderReady.ContainsKey(textureID)) - RenderReady.Remove(textureID); - } - } - - /// - /// Master Download Thread, Queues up downloads in the threadpool - /// - private void DownloadThread() - { - int reqNbr; - - while (Running) - { - if (RequestQueue.Count > 0) - { - reqNbr = -1; - // find available slot for reset event - for (int i = 0; i < threadpoolSlots.Length; i++) - { - if (threadpoolSlots[i] == -1) - { - threadpoolSlots[i] = 1; - reqNbr = i; - break; - } - } - - if (reqNbr != -1) - { - UUID requestID; - lock (RequestQueue) - requestID = RequestQueue.Dequeue(); - - Logger.DebugLog(String.Format("Sending Worker thread new download request {0}", reqNbr)); - ThreadPool.QueueUserWorkItem(new WaitCallback(textureRequestDoWork), new TaskInfo(requestID, reqNbr)); - - continue; - } - } - - // Queue was empty, let's give up some CPU time - Thread.Sleep(500); - } - } - - void textureRequestDoWork(Object threadContext) - { - TaskInfo ti = (TaskInfo)threadContext; - - lock (CurrentRequests) - { - if (CurrentRequests.ContainsKey(ti.RequestID)) - { - threadpoolSlots[ti.RequestNbr] = -1; - return; - } - else - { - CurrentRequests.Add(ti.RequestID, ti.RequestNbr); - } - } - - Logger.DebugLog(String.Format("Worker {0} Requesting {1}", ti.RequestNbr, ti.RequestID)); - - resetEvents[ti.RequestNbr].Reset(); - Client.Assets.RequestImage(ti.RequestID, ImageType.Normal); - - // don't release this worker slot until texture is downloaded or timeout occurs - if (!resetEvents[ti.RequestNbr].WaitOne(30 * 1000, false)) - { - // Timed out - Logger.Log("Worker " + ti.RequestNbr + " Timeout waiting for Texture " + ti.RequestID + " to Download", Helpers.LogLevel.Warning); - - lock (CurrentRequests) - CurrentRequests.Remove(ti.RequestID); - } - - // free up this download slot - threadpoolSlots[ti.RequestNbr] = -1; - } - - private void Assets_OnImageReceived(ImageDownload image, AssetTexture asset) - { - // Free up this slot in the ThreadPool - lock (CurrentRequests) - { - int requestNbr; - if (asset != null && CurrentRequests.TryGetValue(image.ID, out requestNbr)) - { - Logger.DebugLog(String.Format("Worker {0} Downloaded texture {1}", requestNbr, image.ID)); - resetEvents[requestNbr].Set(); - CurrentRequests.Remove(image.ID); - } - } - - if (image.Success) - { - lock (RenderReady) - { - if (!RenderReady.ContainsKey(image.ID)) - { - // Add to rendering dictionary - RenderReady.Add(image.ID, image); - } - } - } - else - { - Console.WriteLine(String.Format("Download of texture {0} failed. NotFound={1}", image.ID, image.NotFound)); - } - - // Let any subscribers know about it - if (OnDownloadFinished != null) - { - OnDownloadFinished(image.ID, image.Success); - } - } - - private void Assets_OnImageReceiveProgress(UUID image, int lastPacket, int recieved, int total) - { - if (OnDownloadProgress != null) - { - OnDownloadProgress(image, recieved, total); - } - } - } + } diff --git a/Programs/PrimWorkshop/frmBrowser.cs b/Programs/PrimWorkshop/frmBrowser.cs index 65717a22..d6569d9c 100644 --- a/Programs/PrimWorkshop/frmBrowser.cs +++ b/Programs/PrimWorkshop/frmBrowser.cs @@ -170,7 +170,7 @@ namespace PrimWorkshop // Initialize the texture download pipeline if (TextureDownloader != null) TextureDownloader.Shutdown(); - TextureDownloader = new TexturePipeline(Client); + TextureDownloader = new TexturePipeline(Client, 10); TextureDownloader.OnDownloadFinished += new TexturePipeline.DownloadFinishedCallback(TextureDownloader_OnDownloadFinished); TextureDownloader.OnDownloadProgress += new TexturePipeline.DownloadProgressCallback(TextureDownloader_OnDownloadProgress); @@ -970,7 +970,7 @@ namespace PrimWorkshop if (!Textures.ContainsKey(teFace.TextureID)) { // We haven't constructed this image in OpenGL yet, get ahold of it - TextureDownloader.RequestTexture(teFace.TextureID); + TextureDownloader.RequestTexture(teFace.TextureID, ImageType.Normal); } } } diff --git a/Programs/Simian/Extensions/AvatarManager.cs b/Programs/Simian/Extensions/AvatarManager.cs index 43553b73..1bc889f9 100644 --- a/Programs/Simian/Extensions/AvatarManager.cs +++ b/Programs/Simian/Extensions/AvatarManager.cs @@ -62,6 +62,12 @@ namespace Simian.Extensions return agent.Animations.Remove(animID); } + public bool ClearAnimations(Agent agent) + { + agent.Animations.Clear(); + return true; + } + public void SendAnimations(Agent agent) { AvatarAnimationPacket sendAnim = new AvatarAnimationPacket(); @@ -104,7 +110,7 @@ namespace Simian.Extensions // Remove the avatar from the scene SimulationObject obj; if (server.Scene.TryGetObject(agent.AgentID, out obj)) - server.Scene.ObjectRemove(this, obj); + server.Scene.ObjectRemove(this, obj.Prim.LocalID); else Logger.Log("Disconnecting an agent that is not in the scene", Helpers.LogLevel.Warning); diff --git a/Programs/Simian/Extensions/ImageDelivery.cs b/Programs/Simian/Extensions/ImageDelivery.cs index 4a6221b9..dd0ecea9 100644 --- a/Programs/Simian/Extensions/ImageDelivery.cs +++ b/Programs/Simian/Extensions/ImageDelivery.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using ExtensionLoader; using OpenMetaverse; -using OpenMetaverse.Imaging; using OpenMetaverse.Packets; namespace Simian.Extensions @@ -14,14 +13,16 @@ namespace Simian.Extensions public const int IMAGE_PACKET_SIZE = 1000; public AssetTexture Texture; + public Agent Agent; public int DiscardLevel; public float Priority; public int CurrentPacket; public int StopPacket; - public ImageDownload(AssetTexture texture, int discardLevel, float priority, int packet) + public ImageDownload(AssetTexture texture, Agent agent, int discardLevel, float priority, int packet) { Texture = texture; + Agent = agent; Update(discardLevel, priority, packet); } @@ -35,9 +36,28 @@ namespace Simian.Extensions public void Update(int discardLevel, float priority, int packet) { Priority = priority; - DiscardLevel = Utils.Clamp(discardLevel, 0, Texture.LayerInfo.Length - 1); - StopPacket = GetPacketForBytePosition(Texture.LayerInfo[(Texture.LayerInfo.Length - 1) - DiscardLevel].End); - CurrentPacket = Utils.Clamp(packet, 1, TexturePacketCount()); + + if (Texture != null) + { + if (Texture.LayerInfo != null && Texture.LayerInfo.Length > 0) + { + DiscardLevel = Utils.Clamp(discardLevel, 0, Texture.LayerInfo.Length - 1); + StopPacket = GetPacketForBytePosition(Texture.LayerInfo[(Texture.LayerInfo.Length - 1) - DiscardLevel].End); + } + else + { + DiscardLevel = 0; + StopPacket = GetPacketForBytePosition(Texture.AssetData.Length); + } + + CurrentPacket = Utils.Clamp(packet, 1, TexturePacketCount()); + } + else + { + DiscardLevel = discardLevel; + Priority = priority; + CurrentPacket = packet; + } } /// @@ -94,7 +114,7 @@ namespace Simian.Extensions { this.server = server; - server.UDP.RegisterPacketCallback(PacketType.RequestImage, new PacketCallback(RequestImageHandler)); + server.UDP.RegisterPacketCallback(PacketType.RequestImage, RequestImageHandler); } public void Stop() @@ -151,91 +171,7 @@ namespace Simian.Extensions Asset asset; if (server.Assets.TryGetAsset(block.Image, out asset) && asset is AssetTexture) { - download = new ImageDownload((AssetTexture)asset, block.DiscardLevel, block.DownloadPriority, - (int)block.Packet); - - Logger.DebugLog(String.Format( - "Starting new download for {0}, DiscardLevel: {1}, Priority: {2}, Start: {3}, End: {4}, Total: {5}", - block.Image, block.DiscardLevel, block.DownloadPriority, download.CurrentPacket, download.StopPacket, - download.TexturePacketCount())); - - // Send initial data - ImageDataPacket data = new ImageDataPacket(); - data.ImageID.Codec = (byte)ImageCodec.J2C; - data.ImageID.ID = download.Texture.AssetID; - data.ImageID.Packets = (ushort)download.TexturePacketCount(); - data.ImageID.Size = (uint)download.Texture.AssetData.Length; - - // The first bytes of the image are always sent in the ImageData packet - data.ImageData = new ImageDataPacket.ImageDataBlock(); - int imageDataSize = (download.Texture.AssetData.Length >= ImageDownload.FIRST_IMAGE_PACKET_SIZE) ? - ImageDownload.FIRST_IMAGE_PACKET_SIZE : download.Texture.AssetData.Length; - try - { - data.ImageData.Data = new byte[imageDataSize]; - Buffer.BlockCopy(download.Texture.AssetData, 0, data.ImageData.Data, 0, imageDataSize); - } - catch (Exception ex) - { - Logger.Log(String.Format("{0}: imageDataSize={1}", ex.Message, imageDataSize), - Helpers.LogLevel.Error); - } - - server.UDP.SendPacket(agent.AgentID, data, PacketCategory.Texture); - - // Check if ImagePacket packets need to be sent to complete this transfer - if (download.CurrentPacket <= download.StopPacket) - { - // Insert this download into the dictionary - lock (CurrentDownloads) - CurrentDownloads[block.Image] = download; - - // Send all of the remaining packets - ThreadPool.QueueUserWorkItem( - delegate(object obj) - { - while (download.CurrentPacket <= download.StopPacket) - { - if (download.Priority == 0.0f && download.DiscardLevel == -1) - break; - - lock (download) - { - int imagePacketSize = (download.CurrentPacket == download.TexturePacketCount() - 1) ? - download.LastPacketSize() : ImageDownload.IMAGE_PACKET_SIZE; - - ImagePacketPacket transfer = new ImagePacketPacket(); - transfer.ImageID.ID = block.Image; - transfer.ImageID.Packet = (ushort)download.CurrentPacket; - transfer.ImageData.Data = new byte[imagePacketSize]; - - try - { - Buffer.BlockCopy(download.Texture.AssetData, download.CurrentBytePosition(), - transfer.ImageData.Data, 0, imagePacketSize); - } - catch (Exception ex) - { - Logger.Log(String.Format( - "{0}: CurrentBytePosition()={1}, AssetData.Length={2} imagePacketSize={3}", - ex.Message, download.CurrentBytePosition(), download.Texture.AssetData.Length, - imagePacketSize), Helpers.LogLevel.Error); - } - - server.UDP.SendPacket(agent.AgentID, transfer, PacketCategory.Texture); - - ++download.CurrentPacket; - } - } - - Logger.DebugLog("Completed image transfer for " + block.Image.ToString()); - - // Transfer is complete, remove the reference - lock (CurrentDownloads) - CurrentDownloads.Remove(block.Image); - } - ); - } + SendTexture(agent, (AssetTexture)asset, block.DiscardLevel, (int)block.Packet, block.DownloadPriority); } else { @@ -248,5 +184,93 @@ namespace Simian.Extensions } } } + + void SendTexture(Agent agent, AssetTexture texture, int discardLevel, int packet, float priority) + { + ImageDownload download = new ImageDownload(texture, agent, discardLevel, priority, packet); + + Logger.DebugLog(String.Format( + "Starting new download for {0}, DiscardLevel: {1}, Priority: {2}, Start: {3}, End: {4}, Total: {5}", + texture.AssetID, discardLevel, priority, download.CurrentPacket, download.StopPacket, + download.TexturePacketCount())); + + // Send initial data + ImageDataPacket data = new ImageDataPacket(); + data.ImageID.Codec = (byte)ImageCodec.J2C; + data.ImageID.ID = download.Texture.AssetID; + data.ImageID.Packets = (ushort)download.TexturePacketCount(); + data.ImageID.Size = (uint)download.Texture.AssetData.Length; + + // The first bytes of the image are always sent in the ImageData packet + data.ImageData = new ImageDataPacket.ImageDataBlock(); + int imageDataSize = (download.Texture.AssetData.Length >= ImageDownload.FIRST_IMAGE_PACKET_SIZE) ? + ImageDownload.FIRST_IMAGE_PACKET_SIZE : download.Texture.AssetData.Length; + try + { + data.ImageData.Data = new byte[imageDataSize]; + Buffer.BlockCopy(download.Texture.AssetData, 0, data.ImageData.Data, 0, imageDataSize); + } + catch (Exception ex) + { + Logger.Log(String.Format("{0}: imageDataSize={1}", ex.Message, imageDataSize), + Helpers.LogLevel.Error); + } + + server.UDP.SendPacket(agent.AgentID, data, PacketCategory.Texture); + + // Check if ImagePacket packets need to be sent to complete this transfer + if (download.CurrentPacket <= download.StopPacket) + { + // Insert this download into the dictionary + lock (CurrentDownloads) + CurrentDownloads[texture.AssetID] = download; + + // Send all of the remaining packets + ThreadPool.QueueUserWorkItem( + delegate(object obj) + { + while (download.CurrentPacket <= download.StopPacket) + { + if (download.Priority == 0.0f && download.DiscardLevel == -1) + break; + + lock (download) + { + int imagePacketSize = (download.CurrentPacket == download.TexturePacketCount() - 1) ? + download.LastPacketSize() : ImageDownload.IMAGE_PACKET_SIZE; + + ImagePacketPacket transfer = new ImagePacketPacket(); + transfer.ImageID.ID = texture.AssetID; + transfer.ImageID.Packet = (ushort)download.CurrentPacket; + transfer.ImageData.Data = new byte[imagePacketSize]; + + try + { + Buffer.BlockCopy(download.Texture.AssetData, download.CurrentBytePosition(), + transfer.ImageData.Data, 0, imagePacketSize); + } + catch (Exception ex) + { + Logger.Log(String.Format( + "{0}: CurrentBytePosition()={1}, AssetData.Length={2} imagePacketSize={3}", + ex.Message, download.CurrentBytePosition(), download.Texture.AssetData.Length, + imagePacketSize), Helpers.LogLevel.Error); + } + + server.UDP.SendPacket(agent.AgentID, transfer, PacketCategory.Texture); + + ++download.CurrentPacket; + } + } + + Logger.DebugLog("Completed image transfer for " + texture.AssetID.ToString()); + + // Transfer is complete, remove the reference + lock (CurrentDownloads) + CurrentDownloads.Remove(texture.AssetID); + } + ); + } + } } } diff --git a/Programs/Simian/Extensions/Movement.cs b/Programs/Simian/Extensions/Movement.cs index 038cd3a5..24b10dc5 100644 --- a/Programs/Simian/Extensions/Movement.cs +++ b/Programs/Simian/Extensions/Movement.cs @@ -310,7 +310,6 @@ namespace Simian.Extensions if (animsChanged) server.Avatars.SendAnimations(agent); - float maxVel = AVATAR_TERMINAL_VELOCITY * seconds; // static acceleration when any control is held, otherwise none @@ -339,7 +338,6 @@ namespace Simian.Extensions else if (agent.Avatar.Position.Y > 255) agent.Avatar.Position.Y = 255f; if (agent.Avatar.Position.Z < lowerLimit) agent.Avatar.Position.Z = lowerLimit; - } } } @@ -403,6 +401,5 @@ namespace Simian.Extensions //Logger.Log(String.Format("Agent wants to set height={0}, width={1}", // heightWidth.HeightWidthBlock.Height, heightWidth.HeightWidthBlock.Width), Helpers.LogLevel.Info); } - } } diff --git a/Programs/Simian/Extensions/ObjectManager.cs b/Programs/Simian/Extensions/ObjectManager.cs index a60ff072..4cd369b1 100644 --- a/Programs/Simian/Extensions/ObjectManager.cs +++ b/Programs/Simian/Extensions/ObjectManager.cs @@ -440,7 +440,7 @@ namespace Simian.Extensions data.ProfileEnd = Primitive.UnpackEndCut(block.ProfileEnd); data.ProfileHollow = Primitive.UnpackProfileHollow(block.ProfileHollow); - server.Scene.ObjectModify(this, obj, data); + server.Scene.ObjectModify(this, obj.Prim.LocalID, data); } else { @@ -569,7 +569,7 @@ namespace Simian.Extensions server.Inventory.CreateItem(agent.AgentID, obj.Prim.Properties.Name, obj.Prim.Properties.Description, InventoryType.Object, AssetType.Object, obj.Prim.ID, trash.ID, PermissionMask.All, PermissionMask.All, agent.AgentID, obj.Prim.Properties.CreatorID, derez.AgentBlock.TransactionID, 0, true); - server.Scene.ObjectRemove(this, obj); + server.Scene.ObjectRemove(this, obj.Prim.LocalID); Logger.DebugLog(String.Format("Derezzed prim {0} to agent inventory trash", obj.Prim.LocalID)); } @@ -606,6 +606,7 @@ namespace Simian.Extensions for (int i = 0; i < update.ObjectData.Length; i++) { + bool scaled = false; MultipleObjectUpdatePacket.ObjectDataBlock block = update.ObjectData[i]; SimulationObject obj; @@ -630,6 +631,7 @@ namespace Simian.Extensions } if ((type & UpdateType.Scale) != 0) { + scaled = true; scale = new Vector3(block.Data, pos); pos += 12; @@ -637,12 +639,19 @@ namespace Simian.Extensions bool uniform = ((type & UpdateType.Uniform) != 0); } - // Although the object has already been modified, we need - // to inform the scene manager of the changes so they are - // sent to clients and propagated to other extensions - server.Scene.ObjectTransform(this, obj, position, rotation, - obj.Prim.Velocity, obj.Prim.Acceleration, obj.Prim.AngularVelocity, - scale); + if (scaled) + { + obj.Prim.Position = position; + obj.Prim.Rotation = rotation; + obj.Prim.Scale = scale; + + server.Scene.ObjectAdd(this, obj, PrimFlags.None); + } + else + { + server.Scene.ObjectTransform(this, obj.Prim.LocalID, position, rotation, + obj.Prim.Velocity, obj.Prim.Acceleration, obj.Prim.AngularVelocity); + } } else { diff --git a/Programs/Simian/Extensions/Periscope.cs b/Programs/Simian/Extensions/Periscope.cs index e680f06a..2300cec9 100644 --- a/Programs/Simian/Extensions/Periscope.cs +++ b/Programs/Simian/Extensions/Periscope.cs @@ -12,45 +12,54 @@ namespace Simian.Extensions const string FIRST_NAME = "Testing"; const string LAST_NAME = "Anvil"; const string PASSWORD = "testinganvil"; + const string SIMULATOR = "Svarga"; + + public Agent MasterAgent = null; Simian server; GridClient client; + PeriscopeImageDelivery imageDelivery; + PeriscopeMovement movement; + object loginLock = new object(); public Periscope() { - client = new GridClient(); - client.Settings.SEND_AGENT_UPDATES = false; - - client.Objects.OnNewPrim += new OpenMetaverse.ObjectManager.NewPrimCallback(Objects_OnNewPrim); - client.Terrain.OnLandPatch += new TerrainManager.LandPatchCallback(Terrain_OnLandPatch); } public void Start(Simian server) { this.server = server; + + client = new GridClient(); + Settings.LOG_LEVEL = Helpers.LogLevel.Info; + client.Settings.MULTIPLE_SIMS = false; + client.Settings.SEND_AGENT_UPDATES = false; + + client.Network.OnCurrentSimChanged += new NetworkManager.CurrentSimChangedCallback(Network_OnCurrentSimChanged); + client.Objects.OnNewPrim += new OpenMetaverse.ObjectManager.NewPrimCallback(Objects_OnNewPrim); + client.Objects.OnNewFoliage += new OpenMetaverse.ObjectManager.NewFoliageCallback(Objects_OnNewFoliage); + client.Objects.OnNewAvatar += new OpenMetaverse.ObjectManager.NewAvatarCallback(Objects_OnNewAvatar); + client.Objects.OnNewAttachment += new OpenMetaverse.ObjectManager.NewAttachmentCallback(Objects_OnNewAttachment); + client.Objects.OnObjectKilled += new OpenMetaverse.ObjectManager.KillObjectCallback(Objects_OnObjectKilled); + client.Objects.OnObjectUpdated += new OpenMetaverse.ObjectManager.ObjectUpdatedCallback(Objects_OnObjectUpdated); + client.Avatars.OnAvatarAppearance += new OpenMetaverse.AvatarManager.AvatarAppearanceCallback(Avatars_OnAvatarAppearance); + client.Terrain.OnLandPatch += new TerrainManager.LandPatchCallback(Terrain_OnLandPatch); + client.Self.OnChat += new AgentManager.ChatCallback(Self_OnChat); + client.Network.RegisterCallback(PacketType.AvatarAnimation, AvatarAnimationHandler); + client.Network.RegisterCallback(PacketType.RegionHandshake, RegionHandshakeHandler); + server.UDP.RegisterPacketCallback(PacketType.AgentUpdate, AgentUpdateHandler); + server.UDP.RegisterPacketCallback(PacketType.ChatFromViewer, ChatFromViewerHandler); - // Start the login process - Thread loginThread = new Thread(new ThreadStart( - delegate() - { - client.Network.Login(FIRST_NAME, LAST_NAME, PASSWORD, "Simian Periscope", "1.0.0"); - - if (client.Network.Connected) - { - Logger.Log("Periscope is connected: " + client.Network.LoginMessage, Helpers.LogLevel.Info); - } - else - { - Logger.Log("Periscope failed to connect to the foreign grid: " + client.Network.LoginErrorKey, Helpers.LogLevel.Error); - } - } - )); - loginThread.Start(); + imageDelivery = new PeriscopeImageDelivery(server, client); + movement = new PeriscopeMovement(server, this); } public void Stop() { + movement.Stop(); + imageDelivery.Stop(); + if (client.Network.Connected) client.Network.Logout(); } @@ -58,22 +67,211 @@ namespace Simian.Extensions void Objects_OnNewPrim(Simulator simulator, Primitive prim, ulong regionHandle, ushort timeDilation) { SimulationObject simObj = new SimulationObject(prim, server); - server.Scene.ObjectAdd(this, simObj, prim.Flags); + server.Scene.ObjectAdd(this, simObj, PrimFlags.None); + } + + void Objects_OnNewAttachment(Simulator simulator, Primitive prim, ulong regionHandle, ushort timeDilation) + { + SimulationObject simObj = new SimulationObject(prim, server); + server.Scene.ObjectAdd(this, simObj, PrimFlags.None); + } + + void Objects_OnNewAvatar(Simulator simulator, Avatar avatar, ulong regionHandle, ushort timeDilation) + { + // Add the avatar to both the agents list and the scene objects + Agent agent = new Agent(); + agent.AgentID = avatar.ID; + agent.Avatar = avatar; + agent.CurrentRegionHandle = server.RegionHandle; + agent.FirstName = avatar.FirstName; + agent.LastName = avatar.LastName; + + lock (server.Agents) + server.Agents[agent.AgentID] = agent; + + SimulationObject simObj = new SimulationObject(avatar, server); + server.Scene.ObjectAdd(this, simObj, avatar.Flags); + } + + void Objects_OnNewFoliage(Simulator simulator, Primitive foliage, ulong regionHandle, ushort timeDilation) + { + SimulationObject simObj = new SimulationObject(foliage, server); + server.Scene.ObjectAdd(this, simObj, foliage.Flags); + } + + void Objects_OnObjectUpdated(Simulator simulator, ObjectUpdate update, ulong regionHandle, ushort timeDilation) + { + server.Scene.ObjectTransform(this, update.LocalID, update.Position, update.Rotation, update.Velocity, + update.Acceleration, update.AngularVelocity); + + if (update.LocalID == client.Self.LocalID) + { + MasterAgent.Avatar.Acceleration = update.Acceleration; + MasterAgent.Avatar.AngularVelocity = update.AngularVelocity; + MasterAgent.Avatar.CollisionPlane = update.CollisionPlane; + MasterAgent.Avatar.Position = update.Position; + MasterAgent.Avatar.Rotation = update.Rotation; + MasterAgent.Avatar.Velocity = update.Velocity; + + if (update.Textures != null) + MasterAgent.Avatar.Textures = update.Textures; + } + } + + void Objects_OnObjectKilled(Simulator simulator, uint objectID) + { + server.Scene.ObjectRemove(this, objectID); + } + + void Avatars_OnAvatarAppearance(UUID avatarID, bool isTrial, Primitive.TextureEntryFace defaultTexture, + Primitive.TextureEntryFace[] faceTextures, List visualParams) + { + Agent agent; + if (server.Agents.TryGetValue(avatarID, out agent)) + { + Primitive.TextureEntry te = new Primitive.TextureEntry(defaultTexture); + te.FaceTextures = faceTextures; + + byte[] vp = (visualParams != null && visualParams.Count > 1 ? visualParams.ToArray() : null); + + Logger.Log("[Periscope] Updating foreign avatar appearance for " + avatarID.ToString(), Helpers.LogLevel.Info); + + + server.Scene.AvatarAppearance(this, agent, te, vp); + } + else + { + Logger.Log("[Periscope] Received a foreign avatar appearance for an unknown avatar", Helpers.LogLevel.Warning); + } } void Terrain_OnLandPatch(Simulator simulator, int x, int y, int width, float[] data) { - //throw new NotImplementedException(); + // TODO: When Simian gets a terrain editing interface, switch this over to + // edit the scene heightmap instead of sending packets direct to clients + int[] patches = new int[1]; + patches[0] = (y * 16) + x; + LayerDataPacket layer = TerrainCompressor.CreateLandPacket(data, x, y); + server.UDP.BroadcastPacket(layer, PacketCategory.Terrain); + } + + void Self_OnChat(string message, ChatAudibleLevel audible, ChatType type, ChatSourceType sourceType, + string fromName, UUID id, UUID ownerid, Vector3 position) + { + // TODO: Inject chat into the Scene instead of relaying it + ChatFromSimulatorPacket chat = new ChatFromSimulatorPacket(); + chat.ChatData.Audible = (byte)ChatAudibleLevel.Fully; + chat.ChatData.ChatType = (byte)type; + chat.ChatData.OwnerID = ownerid; + chat.ChatData.SourceID = id; + chat.ChatData.SourceType = (byte)sourceType; + chat.ChatData.Position = position; + chat.ChatData.FromName = Utils.StringToBytes(fromName); + chat.ChatData.Message = Utils.StringToBytes(message); + + server.UDP.BroadcastPacket(chat, PacketCategory.Transaction); + } + + void Network_OnCurrentSimChanged(Simulator PreviousSimulator) + { + Logger.Log("[Periscope] Sending bot appearance", Helpers.LogLevel.Info); + client.Appearance.SetPreviousAppearance(false); + } + + void AvatarAnimationHandler(Packet packet, Simulator simulator) + { + AvatarAnimationPacket animations = (AvatarAnimationPacket)packet; + + Agent agent; + if (server.Agents.TryGetValue(animations.Sender.ID, out agent)) + { + agent.Animations.Clear(); + + for (int i = 0; i < animations.AnimationList.Length; i++) + { + AvatarAnimationPacket.AnimationListBlock block = animations.AnimationList[i]; + agent.Animations.Add(block.AnimID, block.AnimSequenceID); + } + + server.Avatars.SendAnimations(agent); + } + + if (animations.Sender.ID == client.Self.AgentID) + { + MasterAgent.Animations.Clear(); + + for (int i = 0; i < animations.AnimationList.Length; i++) + { + AvatarAnimationPacket.AnimationListBlock block = animations.AnimationList[i]; + MasterAgent.Animations.Add(block.AnimID, block.AnimSequenceID); + } + + server.Avatars.SendAnimations(MasterAgent); + } + } + + void RegionHandshakeHandler(Packet packet, Simulator simulator) + { + RegionHandshakePacket handshake = (RegionHandshakePacket)packet; + + handshake.RegionInfo.SimOwner = (MasterAgent != null ? MasterAgent.AgentID : UUID.Zero); + + // TODO: Need more methods to manipulate the scene so we can apply these properties. + // Right now this only gets sent out to people who are logged in when the master avatar + // is already logged in + server.UDP.BroadcastPacket(handshake, PacketCategory.Transaction); + } + + #region Simian client packet handlers + + void ChatFromViewerHandler(Packet packet, Agent agent) + { + ChatFromViewerPacket chat = (ChatFromViewerPacket)packet; + + // Forward chat from the viewer to the foreign simulator + string message = String.Format("<{0} {1}> {2}", agent.FirstName, agent.LastName, + Utils.BytesToString(chat.ChatData.Message)); + + client.Self.Chat(message, chat.ChatData.Channel, (ChatType)chat.ChatData.Type); } void AgentUpdateHandler(Packet packet, Agent agent) { AgentUpdatePacket update = (AgentUpdatePacket)packet; - // Forward AgentUpdate packets with the AgentID/SessionID set to the bots ID - update.AgentData.AgentID = client.Self.AgentID; - update.AgentData.SessionID = client.Self.SessionID; - client.Network.SendPacket(update); + if (MasterAgent == null) + { + lock (loginLock) + { + // Double-checked locking to avoid hitting the loginLock each time + if (MasterAgent == null && + server.Agents.TryGetValue(update.AgentData.AgentID, out MasterAgent)) + { + Logger.Log(String.Format("[Periscope] {0} {1} is the controlling agent", + MasterAgent.FirstName, MasterAgent.LastName), Helpers.LogLevel.Info); + + LoginParams login = client.Network.DefaultLoginParams(FIRST_NAME, LAST_NAME, PASSWORD, "Simian Periscope", + "1.0.0"); + login.Start = NetworkManager.StartLocation(SIMULATOR, 128, 128, 128); + client.Network.Login(login); + + if (client.Network.Connected) + Logger.Log("[Periscope] Connected: " + client.Network.LoginMessage, Helpers.LogLevel.Info); + else + Logger.Log("[Periscope] Failed to connect to the foreign grid: " + client.Network.LoginErrorKey, Helpers.LogLevel.Error); + } + } + } + + if (MasterAgent == null || update.AgentData.AgentID == MasterAgent.AgentID) + { + // Forward AgentUpdate packets with the AgentID/SessionID set to the bots ID + update.AgentData.AgentID = client.Self.AgentID; + update.AgentData.SessionID = client.Self.SessionID; + client.Network.SendPacket(update); + } } + + #endregion Simian client packet handlers } } diff --git a/Programs/Simian/Extensions/PeriscopeImageDelivery.cs b/Programs/Simian/Extensions/PeriscopeImageDelivery.cs new file mode 100644 index 00000000..e9478c3a --- /dev/null +++ b/Programs/Simian/Extensions/PeriscopeImageDelivery.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using ExtensionLoader; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian.Extensions +{ + public class PeriscopeImageDelivery + { + Simian server; + GridClient client; + TexturePipeline pipeline; + Dictionary currentDownloads = new Dictionary(); + + public PeriscopeImageDelivery(Simian server, GridClient client) + { + this.server = server; + this.client = client; + + pipeline = new TexturePipeline(client, 10); + pipeline.OnDownloadFinished += new TexturePipeline.DownloadFinishedCallback(pipeline_OnDownloadFinished); + + server.UDP.RegisterPacketCallback(PacketType.RequestImage, RequestImageHandler); + } + + public void Stop() + { + pipeline.Shutdown(); + } + + void RequestImageHandler(Packet packet, Agent agent) + { + RequestImagePacket request = (RequestImagePacket)packet; + + for (int i = 0; i < request.RequestImage.Length; i++) + { + RequestImagePacket.RequestImageBlock block = request.RequestImage[i]; + + ImageDownload download; + bool downloadFound = currentDownloads.TryGetValue(block.Image, out download); + + if (downloadFound) + { + lock (download) + { + if (block.DiscardLevel == -1 && block.DownloadPriority == 0.0f) + Logger.DebugLog(String.Format("Image download {0} is aborting", block.Image)); + + // Update download + download.Update(block.DiscardLevel, block.DownloadPriority, (int)block.Packet); + } + } + else if (block.DiscardLevel == -1 && block.DownloadPriority == 0.0f) + { + // Aborting a download we are not tracking, ignore + } + else + { + bool bake = ((ImageType)block.Type == ImageType.Baked); + + // New download, check if we have this image + Asset asset; + if (server.Assets.TryGetAsset(block.Image, out asset) && asset is AssetTexture) + { + SendTexture(agent, (AssetTexture)asset, block.DiscardLevel, (int)block.Packet, block.DownloadPriority); + } + else + { + // We don't have this texture, add it to the download queue and see if the bot can get it for us + download = new ImageDownload(null, agent, block.DiscardLevel, block.DownloadPriority, (int)block.Packet); + lock (currentDownloads) + currentDownloads[block.Image] = download; + + pipeline.RequestTexture(block.Image, (ImageType)block.Type); + } + } + } + } + + void pipeline_OnDownloadFinished(UUID id, bool success) + { + ImageDownload download; + if (currentDownloads.TryGetValue(id, out download)) + { + lock (currentDownloads) + currentDownloads.Remove(id); + + if (success) + { + // Set the texture to the downloaded texture data + AssetTexture texture = new AssetTexture(id, pipeline.GetTextureToRender(id).AssetData); + download.Texture = texture; + + pipeline.RemoveFromPipeline(id); + + // Store this texture in the local asset store for later + server.Assets.StoreAsset(texture); + + SendTexture(download.Agent, download.Texture, download.DiscardLevel, download.CurrentPacket, download.Priority); + } + else + { + Logger.Log("[Periscope] Failed to download texture " + id.ToString(), Helpers.LogLevel.Warning); + + ImageNotInDatabasePacket notfound = new ImageNotInDatabasePacket(); + notfound.ImageID.ID = id; + server.UDP.SendPacket(download.Agent.AgentID, notfound, PacketCategory.Texture); + } + } + else + { + Logger.Log("[Periscope] Pipeline downloaded a texture we're not tracking, " + id.ToString(), Helpers.LogLevel.Warning); + } + } + + void SendTexture(Agent agent, AssetTexture texture, int discardLevel, int packet, float priority) + { + ImageDownload download = new ImageDownload(texture, agent, discardLevel, priority, packet); + + Logger.DebugLog(String.Format( + "[Periscope] Starting new texture transfer for {0}, DiscardLevel: {1}, Priority: {2}, Start: {3}, End: {4}, Total: {5}", + texture.AssetID, discardLevel, priority, download.CurrentPacket, download.StopPacket, download.TexturePacketCount())); + + // Send initial data + ImageDataPacket data = new ImageDataPacket(); + data.ImageID.Codec = (byte)ImageCodec.J2C; + data.ImageID.ID = download.Texture.AssetID; + data.ImageID.Packets = (ushort)download.TexturePacketCount(); + data.ImageID.Size = (uint)download.Texture.AssetData.Length; + + // The first bytes of the image are always sent in the ImageData packet + data.ImageData = new ImageDataPacket.ImageDataBlock(); + int imageDataSize = (download.Texture.AssetData.Length >= ImageDownload.FIRST_IMAGE_PACKET_SIZE) ? + ImageDownload.FIRST_IMAGE_PACKET_SIZE : download.Texture.AssetData.Length; + try + { + data.ImageData.Data = new byte[imageDataSize]; + Buffer.BlockCopy(download.Texture.AssetData, 0, data.ImageData.Data, 0, imageDataSize); + } + catch (Exception ex) + { + Logger.Log(String.Format("{0}: imageDataSize={1}", ex.Message, imageDataSize), + Helpers.LogLevel.Error); + } + + server.UDP.SendPacket(agent.AgentID, data, PacketCategory.Texture); + + // Check if ImagePacket packets need to be sent to complete this transfer + if (download.CurrentPacket <= download.StopPacket) + { + // Insert this download into the dictionary + lock (currentDownloads) + currentDownloads[texture.AssetID] = download; + + // Send all of the remaining packets + ThreadPool.QueueUserWorkItem( + delegate(object obj) + { + while (download.CurrentPacket <= download.StopPacket) + { + if (download.Priority == 0.0f && download.DiscardLevel == -1) + break; + + lock (download) + { + int imagePacketSize = (download.CurrentPacket == download.TexturePacketCount() - 1) ? + download.LastPacketSize() : ImageDownload.IMAGE_PACKET_SIZE; + + ImagePacketPacket transfer = new ImagePacketPacket(); + transfer.ImageID.ID = texture.AssetID; + transfer.ImageID.Packet = (ushort)download.CurrentPacket; + transfer.ImageData.Data = new byte[imagePacketSize]; + + try + { + Buffer.BlockCopy(download.Texture.AssetData, download.CurrentBytePosition(), + transfer.ImageData.Data, 0, imagePacketSize); + } + catch (Exception ex) + { + Logger.Log(String.Format( + "{0}: CurrentBytePosition()={1}, AssetData.Length={2} imagePacketSize={3}", + ex.Message, download.CurrentBytePosition(), download.Texture.AssetData.Length, + imagePacketSize), Helpers.LogLevel.Error); + } + + server.UDP.SendPacket(agent.AgentID, transfer, PacketCategory.Texture); + + ++download.CurrentPacket; + } + } + + Logger.DebugLog("Completed image transfer for " + texture.AssetID.ToString()); + + // Transfer is complete, remove the reference + lock (currentDownloads) + currentDownloads.Remove(texture.AssetID); + } + ); + } + } + } +} diff --git a/Programs/Simian/Extensions/PeriscopeMovement.cs b/Programs/Simian/Extensions/PeriscopeMovement.cs new file mode 100644 index 00000000..bd8dc9e0 --- /dev/null +++ b/Programs/Simian/Extensions/PeriscopeMovement.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using ExtensionLoader; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian.Extensions +{ + public class PeriscopeMovement + { + const int UPDATE_ITERATION = 100; //rate in milliseconds to send ObjectUpdate + const bool ENVIRONMENT_SOUNDS = true; //collision sounds, splashing, etc + const float GRAVITY = 9.8f; //meters/sec + const float WALK_SPEED = 3f; //meters/sec + const float RUN_SPEED = 5f; //meters/sec + const float FLY_SPEED = 10f; //meters/sec + const float FALL_DELAY = 0.33f; //seconds before starting animation + const float FALL_FORGIVENESS = 0.25f; //fall buffer in meters + const float JUMP_IMPULSE_VERTICAL = 8.5f; //boost amount in meters/sec + const float JUMP_IMPULSE_HORIZONTAL = 10f; //boost amount in meters/sec (no clue why this is so high) + const float INITIAL_HOVER_IMPULSE = 2f; //boost amount in meters/sec + const float PREJUMP_DELAY = 0.25f; //seconds before actually jumping + const float AVATAR_TERMINAL_VELOCITY = 54f; //~120mph + + static readonly UUID BIG_SPLASH_SOUND = new UUID("486475b9-1460-4969-871e-fad973b38015"); + + const float SQRT_TWO = 1.41421356f; + + Simian server; + Periscope periscope; + Timer updateTimer; + long lastTick; + + public int LastTick + { + get { return (int)Interlocked.Read(ref lastTick); } + set { Interlocked.Exchange(ref lastTick, value); } + } + + public PeriscopeMovement(Simian server, Periscope periscope) + { + this.server = server; + this.periscope = periscope; + + server.UDP.RegisterPacketCallback(PacketType.AgentUpdate, AgentUpdateHandler); + server.UDP.RegisterPacketCallback(PacketType.SetAlwaysRun, SetAlwaysRunHandler); + + updateTimer = new Timer(new TimerCallback(UpdateTimer_Elapsed)); + LastTick = Environment.TickCount; + updateTimer.Change(UPDATE_ITERATION, UPDATE_ITERATION); + } + + public void Stop() + { + if (updateTimer != null) + { + updateTimer.Dispose(); + updateTimer = null; + } + } + + void UpdateTimer_Elapsed(object sender) + { + int tick = Environment.TickCount; + float seconds = (float)((tick - LastTick) / 1000f); + LastTick = tick; + + lock (server.Agents) + { + foreach (Agent agent in server.Agents.Values) + { + // Don't handle movement for the master agent or foreign agents + if (agent != periscope.MasterAgent && agent.SessionID != UUID.Zero) + { + bool animsChanged = false; + + // Create forward and left vectors from the current avatar rotation + Matrix4 rotMatrix = Matrix4.CreateFromQuaternion(agent.Avatar.Rotation); + Vector3 fwd = Vector3.Transform(Vector3.UnitX, rotMatrix); + Vector3 left = Vector3.Transform(Vector3.UnitY, rotMatrix); + + // Check control flags + bool heldForward = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_AT_POS) == AgentManager.ControlFlags.AGENT_CONTROL_AT_POS; + bool heldBack = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_AT_NEG) == AgentManager.ControlFlags.AGENT_CONTROL_AT_NEG; + bool heldLeft = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_LEFT_POS) == AgentManager.ControlFlags.AGENT_CONTROL_LEFT_POS; + bool heldRight = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_LEFT_NEG) == AgentManager.ControlFlags.AGENT_CONTROL_LEFT_NEG; + bool heldTurnLeft = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_TURN_LEFT) == AgentManager.ControlFlags.AGENT_CONTROL_TURN_LEFT; + bool heldTurnRight = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_TURN_RIGHT) == AgentManager.ControlFlags.AGENT_CONTROL_TURN_RIGHT; + bool heldUp = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_UP_POS) == AgentManager.ControlFlags.AGENT_CONTROL_UP_POS; + bool heldDown = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_UP_NEG) == AgentManager.ControlFlags.AGENT_CONTROL_UP_NEG; + bool flying = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_FLY) == AgentManager.ControlFlags.AGENT_CONTROL_FLY; + bool mouselook = (agent.ControlFlags & AgentManager.ControlFlags.AGENT_CONTROL_MOUSELOOK) == AgentManager.ControlFlags.AGENT_CONTROL_MOUSELOOK; + + // direction in which the avatar is trying to move + Vector3 move = Vector3.Zero; + if (heldForward) { move.X += fwd.X; move.Y += fwd.Y; } + if (heldBack) { move.X -= fwd.X; move.Y -= fwd.Y; } + if (heldLeft) { move.X += left.X; move.Y += left.Y; } + if (heldRight) { move.X -= left.X; move.Y -= left.Y; } + if (heldUp) { move.Z += 1; } + if (heldDown) { move.Z -= 1; } + + // is the avatar trying to move? + bool moving = move != Vector3.Zero; + bool jumping = agent.TickJump != 0; + + // 2-dimensional speed multipler + float speed = seconds * (flying ? FLY_SPEED : agent.Running && !jumping ? RUN_SPEED : WALK_SPEED); + if ((heldForward || heldBack) && (heldLeft || heldRight)) + speed /= SQRT_TWO; + + // adjust multiplier for Z dimension + float oldFloor = GetLandHeightAt(agent.Avatar.Position); + float newFloor = GetLandHeightAt(agent.Avatar.Position + (move * speed)); + if (!flying && newFloor != oldFloor) + speed /= (1 + (SQRT_TWO * Math.Abs(newFloor - oldFloor))); + + // least possible distance from avatar to the ground + // TODO: calculate to get rid of "bot squat" + float lowerLimit = newFloor + agent.Avatar.Scale.Z / 2; + + // Z acceleration resulting from gravity + float gravity = 0f; + + float waterChestHeight = server.Scene.WaterHeight - (agent.Avatar.Scale.Z * .33f); + + if (flying) + { + agent.TickFall = 0; + agent.TickJump = 0; + + //velocity falloff while flying + agent.Avatar.Velocity.X *= 0.66f; + agent.Avatar.Velocity.Y *= 0.66f; + agent.Avatar.Velocity.Z *= 0.33f; + + if (agent.Avatar.Position.Z == lowerLimit) + agent.Avatar.Velocity.Z += INITIAL_HOVER_IMPULSE; + + if (move.X != 0 || move.Y != 0) + { //flying horizontally + if (server.Avatars.SetDefaultAnimation(agent, Animations.FLY)) + animsChanged = true; + } + else if (move.Z > 0) + { //flying straight up + if (server.Avatars.SetDefaultAnimation(agent, Animations.HOVER_UP)) + animsChanged = true; + } + else if (move.Z < 0) + { //flying straight down + if (server.Avatars.SetDefaultAnimation(agent, Animations.HOVER_DOWN)) + animsChanged = true; + } + else + { //hovering in the air + if (server.Avatars.SetDefaultAnimation(agent, Animations.HOVER)) + animsChanged = true; + } + } + else if (agent.Avatar.Position.Z > lowerLimit + FALL_FORGIVENESS || agent.Avatar.Position.Z <= waterChestHeight) + { //falling, floating, or landing from a jump + + if (agent.Avatar.Position.Z > server.Scene.WaterHeight) + { //above water + + move = Vector3.Zero; //override controls while drifting + agent.Avatar.Velocity *= 0.95f; //keep most of our inertia + + float fallElapsed = (float)(Environment.TickCount - agent.TickFall) / 1000f; + + if (agent.TickFall == 0 || (fallElapsed > FALL_DELAY && agent.Avatar.Velocity.Z >= 0f)) + { //just started falling + agent.TickFall = Environment.TickCount; + } + else + { + gravity = GRAVITY * fallElapsed * seconds; //normal gravity + + if (!jumping) + { //falling + if (fallElapsed > FALL_DELAY) + { //falling long enough to trigger the animation + if (server.Avatars.SetDefaultAnimation(agent, Animations.FALLDOWN)) + animsChanged = true; + } + } + } + } + } + else + { //on the ground + + agent.TickFall = 0; + + //friction + agent.Avatar.Acceleration *= 0.2f; + agent.Avatar.Velocity *= 0.2f; + + agent.Avatar.Position.Z = lowerLimit; + + if (move.Z > 0) + { //jumping + if (!jumping) + { //begin prejump + move.Z = 0; //override Z control + if (server.Avatars.SetDefaultAnimation(agent, Animations.PRE_JUMP)) + animsChanged = true; + + agent.TickJump = Environment.TickCount; + } + else if (Environment.TickCount - agent.TickJump > PREJUMP_DELAY * 1000) + { //start actual jump + + if (agent.TickJump == -1) + { + //already jumping! end current jump + agent.TickJump = 0; + return; + } + + if (server.Avatars.SetDefaultAnimation(agent, Animations.JUMP)) + animsChanged = true; + + agent.Avatar.Velocity.X += agent.Avatar.Acceleration.X * JUMP_IMPULSE_HORIZONTAL; + agent.Avatar.Velocity.Y += agent.Avatar.Acceleration.Y * JUMP_IMPULSE_HORIZONTAL; + agent.Avatar.Velocity.Z = JUMP_IMPULSE_VERTICAL * seconds; + + agent.TickJump = -1; //flag that we are currently jumping + } + else + { + move.Z = 0; //override Z control + } + } + else + { //not jumping + + agent.TickJump = 0; + + if (move.X != 0 || move.Y != 0) + { //not walking + + if (move.Z < 0) + { //crouchwalking + if (server.Avatars.SetDefaultAnimation(agent, Animations.CROUCHWALK)) + animsChanged = true; + } + else if (agent.Running) + { //running + if (server.Avatars.SetDefaultAnimation(agent, Animations.RUN)) + animsChanged = true; + } + else + { //walking + if (server.Avatars.SetDefaultAnimation(agent, Animations.WALK)) + animsChanged = true; + } + } + else + { //walking + if (move.Z < 0) + { //crouching + if (server.Avatars.SetDefaultAnimation(agent, Animations.CROUCH)) + animsChanged = true; + } + else + { //standing + if (server.Avatars.SetDefaultAnimation(agent, Animations.STAND)) + animsChanged = true; + } + } + } + } + + if (animsChanged) + server.Avatars.SendAnimations(agent); + + float maxVel = AVATAR_TERMINAL_VELOCITY * seconds; + + // static acceleration when any control is held, otherwise none + if (moving) + { + agent.Avatar.Acceleration = move * speed; + if (agent.Avatar.Acceleration.Z < -maxVel) + agent.Avatar.Acceleration.Z = -maxVel; + else if (agent.Avatar.Acceleration.Z > maxVel) + agent.Avatar.Acceleration.Z = maxVel; + } + else + { + agent.Avatar.Acceleration = Vector3.Zero; + } + + agent.Avatar.Velocity += agent.Avatar.Acceleration - new Vector3(0f, 0f, gravity); + if (agent.Avatar.Velocity.Z < -maxVel) + agent.Avatar.Velocity.Z = -maxVel; + else if (agent.Avatar.Velocity.Z > maxVel) + agent.Avatar.Velocity.Z = maxVel; + + agent.Avatar.Position += agent.Avatar.Velocity; + + if (agent.Avatar.Position.X < 0) agent.Avatar.Position.X = 0f; + else if (agent.Avatar.Position.X > 255) agent.Avatar.Position.X = 255f; + + if (agent.Avatar.Position.Y < 0) agent.Avatar.Position.Y = 0f; + else if (agent.Avatar.Position.Y > 255) agent.Avatar.Position.Y = 255f; + + if (agent.Avatar.Position.Z < lowerLimit) agent.Avatar.Position.Z = lowerLimit; + } + } + } + } + + void AgentUpdateHandler(Packet packet, Agent agent) + { + AgentUpdatePacket update = (AgentUpdatePacket)packet; + + // Don't use the local physics to update the master agent + if (agent != periscope.MasterAgent) + { + agent.Avatar.Rotation = update.AgentData.BodyRotation; + agent.ControlFlags = (AgentManager.ControlFlags)update.AgentData.ControlFlags; + agent.State = update.AgentData.State; + agent.Flags = (PrimFlags)update.AgentData.Flags; + } + + ObjectUpdatePacket fullUpdate = SimulationObject.BuildFullUpdate(agent.Avatar, + server.RegionHandle, agent.State, agent.Flags); + + server.UDP.BroadcastPacket(fullUpdate, PacketCategory.State); + } + + void SetAlwaysRunHandler(Packet packet, Agent agent) + { + SetAlwaysRunPacket run = (SetAlwaysRunPacket)packet; + + agent.Running = run.AgentData.AlwaysRun; + } + + float GetLandHeightAt(Vector3 position) + { + int x = (int)position.X; + int y = (int)position.Y; + + if (x > 255) x = 255; + else if (x < 0) x = 0; + if (y > 255) y = 255; + else if (y < 0) y = 0; + + float center = server.Scene.Heightmap[y * 256 + x]; + float distX = position.X - (int)position.X; + float distY = position.Y - (int)position.Y; + + float nearestX; + float nearestY; + + if (distX > 0) nearestX = server.Scene.Heightmap[y * 256 + x + (x < 255 ? 1 : 0)]; + else nearestX = server.Scene.Heightmap[y * 256 + x - (x > 0 ? 1 : 0)]; + + if (distY > 0) nearestY = server.Scene.Heightmap[(y + (y < 255 ? 1 : 0)) * 256 + x]; + else nearestY = server.Scene.Heightmap[(y - (y > 0 ? 1 : 0)) * 256 + x]; + + float lerpX = Utils.Lerp(center, nearestX, Math.Abs(distX)); + float lerpY = Utils.Lerp(center, nearestY, Math.Abs(distY)); + + return ((lerpX + lerpY) / 2); + } + } +} diff --git a/Programs/Simian/Extensions/SceneManager.cs b/Programs/Simian/Extensions/SceneManager.cs index 916c414e..242add02 100644 --- a/Programs/Simian/Extensions/SceneManager.cs +++ b/Programs/Simian/Extensions/SceneManager.cs @@ -61,25 +61,23 @@ namespace Simian.Extensions { // Check if the object already exists in the scene if (sceneObjects.ContainsKey(obj.Prim.ID)) - { - ObjectModify(sender, obj, obj.Prim.PrimData); - return false; - } + sceneObjects.Remove(obj.Prim.LocalID, obj.Prim.ID); - // Assign a unique LocalID to this object - obj.Prim.LocalID = (uint)Interlocked.Increment(ref currentLocalID); + if (obj.Prim.LocalID == 0) + { + // Assign a unique LocalID to this object + obj.Prim.LocalID = (uint)Interlocked.Increment(ref currentLocalID); + } if (OnObjectAdd != null) - { OnObjectAdd(sender, obj, creatorFlags); - } // Add the object to the scene dictionary sceneObjects.Add(obj.Prim.LocalID, obj.Prim.ID, obj); - // Send an update out to the creator if (server.Agents.ContainsKey(obj.Prim.OwnerID)) { + // Send an update out to the creator ObjectUpdatePacket updateToOwner = SimulationObject.BuildFullUpdate(obj.Prim, server.RegionHandle, 0, obj.Prim.Flags | creatorFlags); server.UDP.SendPacket(obj.Prim.OwnerID, updateToOwner, PacketCategory.State); @@ -100,45 +98,52 @@ namespace Simian.Extensions return true; } - public bool ObjectRemove(object sender, SimulationObject obj) + public bool ObjectRemove(object sender, uint localID) { - if (OnObjectRemove != null) + SimulationObject obj; + if (sceneObjects.TryGetValue(localID, out obj)) { - OnObjectRemove(sender, obj); + if (OnObjectRemove != null) + OnObjectRemove(sender, obj); + + sceneObjects.Remove(localID, obj.Prim.ID); + + KillObjectPacket kill = new KillObjectPacket(); + kill.ObjectData = new KillObjectPacket.ObjectDataBlock[1]; + kill.ObjectData[0] = new KillObjectPacket.ObjectDataBlock(); + kill.ObjectData[0].ID = obj.Prim.LocalID; + + server.UDP.BroadcastPacket(kill, PacketCategory.State); + return true; + } + else + { + return false; } - - sceneObjects.Remove(obj.Prim.LocalID, obj.Prim.ID); - - KillObjectPacket kill = new KillObjectPacket(); - kill.ObjectData = new KillObjectPacket.ObjectDataBlock[1]; - kill.ObjectData[0] = new KillObjectPacket.ObjectDataBlock(); - kill.ObjectData[0].ID = obj.Prim.LocalID; - - server.UDP.BroadcastPacket(kill, PacketCategory.State); - - return true; } - public void ObjectTransform(object sender, SimulationObject obj, Vector3 position, - Quaternion rotation, Vector3 velocity, Vector3 acceleration, Vector3 angularVelocity, - Vector3 scale) + public void ObjectTransform(object sender, uint localID, Vector3 position, Quaternion rotation, + Vector3 velocity, Vector3 acceleration, Vector3 angularVelocity) { - if (OnObjectTransform != null) + SimulationObject obj; + if (sceneObjects.TryGetValue(localID, out obj)) { - OnObjectTransform(sender, obj, position, rotation, velocity, - acceleration, angularVelocity, scale); + if (OnObjectTransform != null) + { + OnObjectTransform(sender, obj, position, rotation, velocity, + acceleration, angularVelocity); + } + + // Update the object + obj.Prim.Position = position; + obj.Prim.Rotation = rotation; + obj.Prim.Velocity = velocity; + obj.Prim.Acceleration = acceleration; + obj.Prim.AngularVelocity = angularVelocity; + + // Inform clients + BroadcastObjectUpdate(obj); } - - // Update the object - obj.Prim.Position = position; - obj.Prim.Rotation = rotation; - obj.Prim.Velocity = velocity; - obj.Prim.Acceleration = acceleration; - obj.Prim.AngularVelocity = angularVelocity; - obj.Prim.Scale = scale; - - // Inform clients - BroadcastObjectUpdate(obj); } public void ObjectFlags(object sender, SimulationObject obj, PrimFlags flags) @@ -170,18 +175,22 @@ namespace Simian.Extensions BroadcastObjectUpdate(obj); } - public void ObjectModify(object sender, SimulationObject obj, Primitive.ConstructionData data) + public void ObjectModify(object sender, uint localID, Primitive.ConstructionData data) { - if (OnObjectModify != null) + SimulationObject obj; + if (sceneObjects.TryGetValue(localID, out obj)) { - OnObjectModify(sender, obj, data); + if (OnObjectModify != null) + { + OnObjectModify(sender, obj, data); + } + + // Update the object + obj.Prim.PrimData = data; + + // Inform clients + BroadcastObjectUpdate(obj); } - - // Update the object - obj.Prim.PrimData = data; - - // Inform clients - BroadcastObjectUpdate(obj); } public void AvatarAppearance(object sender, Agent agent, Primitive.TextureEntry textures, byte[] visualParams) @@ -191,24 +200,28 @@ namespace Simian.Extensions OnAvatarAppearance(sender, agent, textures, visualParams); } - // Update the avatar - agent.Avatar.Textures = textures; - if (visualParams != null) - agent.VisualParams = visualParams; - - // Broadcast the object update + // Broadcast an object update for this avatar + // TODO: Is this necessary here? ObjectUpdatePacket update = SimulationObject.BuildFullUpdate(agent.Avatar, server.RegionHandle, agent.State, agent.Flags); server.UDP.BroadcastPacket(update, PacketCategory.State); - // Send the appearance packet to all other clients - AvatarAppearancePacket appearance = BuildAppearancePacket(agent); - lock (server.Agents) + // Update the avatar + agent.Avatar.Textures = textures; + if (visualParams != null && visualParams.Length > 1) + agent.VisualParams = visualParams; + + if (agent.VisualParams != null) { - foreach (Agent recipient in server.Agents.Values) + // Send the appearance packet to all other clients + AvatarAppearancePacket appearance = BuildAppearancePacket(agent); + lock (server.Agents) { - if (recipient != agent) - server.UDP.SendPacket(recipient.AgentID, appearance, PacketCategory.State); + foreach (Agent recipient in server.Agents.Values) + { + if (recipient != agent) + server.UDP.SendPacket(recipient.AgentID, appearance, PacketCategory.State); + } } } } @@ -384,13 +397,17 @@ namespace Simian.Extensions appearance.Sender.ID = agent.AgentID; appearance.Sender.IsTrial = false; - appearance.VisualParam = new AvatarAppearancePacket.VisualParamBlock[218]; - for (int i = 0; i < 218; i++) + appearance.VisualParam = new AvatarAppearancePacket.VisualParamBlock[agent.VisualParams.Length]; + for (int i = 0; i < agent.VisualParams.Length; i++) { appearance.VisualParam[i] = new AvatarAppearancePacket.VisualParamBlock(); appearance.VisualParam[i].ParamValue = agent.VisualParams[i]; } + if (agent.VisualParams.Length != 218) + Logger.Log("Built an appearance packet with VisualParams.Length=" + agent.VisualParams.Length, + Helpers.LogLevel.Warning); + return appearance; } } diff --git a/Programs/Simian/Extensions/UDPManager.cs b/Programs/Simian/Extensions/UDPManager.cs index 2f2584c1..e1a2166d 100644 --- a/Programs/Simian/Extensions/UDPManager.cs +++ b/Programs/Simian/Extensions/UDPManager.cs @@ -197,14 +197,8 @@ namespace Simian { // Look up the UDPClient this is going to UDPClient client; - if (!clients.TryGetValue(agentID, out client)) - { - Logger.Log("Attempted to send a packet to unknown UDP client " + - agentID.ToString(), Helpers.LogLevel.Warning); - return; - } - - SendPacket(client, new OutgoingPacket(packet, category)); + if (clients.TryGetValue(agentID, out client)) + SendPacket(client, new OutgoingPacket(packet, category)); } void SendPacket(UDPClient client, OutgoingPacket outgoingPacket) diff --git a/Programs/Simian/Interfaces/IAvatarProvider.cs b/Programs/Simian/Interfaces/IAvatarProvider.cs index d00b96e2..44e18d7d 100644 --- a/Programs/Simian/Interfaces/IAvatarProvider.cs +++ b/Programs/Simian/Interfaces/IAvatarProvider.cs @@ -8,6 +8,7 @@ namespace Simian bool SetDefaultAnimation(Agent agent, UUID animID); bool AddAnimation(Agent agent, UUID animID); bool RemoveAnimation(Agent agent, UUID animID); + bool ClearAnimations(Agent agent); void SendAnimations(Agent agent); void Disconnect(Agent agent); } diff --git a/Programs/Simian/Interfaces/ISceneProvider.cs b/Programs/Simian/Interfaces/ISceneProvider.cs index f06ecda8..6b21e3e1 100644 --- a/Programs/Simian/Interfaces/ISceneProvider.cs +++ b/Programs/Simian/Interfaces/ISceneProvider.cs @@ -7,7 +7,7 @@ namespace Simian public delegate void ObjectRemoveCallback(object sender, SimulationObject obj); public delegate void ObjectTransformCallback(object sender, SimulationObject obj, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 acceleration, - Vector3 angularVelocity, Vector3 scale); + Vector3 angularVelocity); public delegate void ObjectFlagsCallback(object sender, SimulationObject obj, PrimFlags flags); public delegate void ObjectImageCallback(object sender, SimulationObject obj, string mediaURL, Primitive.TextureEntry textureEntry); @@ -34,13 +34,12 @@ namespace Simian float WaterHeight { get; } bool ObjectAdd(object sender, SimulationObject obj, PrimFlags creatorFlags); - bool ObjectRemove(object sender, SimulationObject obj); - void ObjectTransform(object sender, SimulationObject obj, Vector3 position, - Quaternion rotation, Vector3 velocity, Vector3 acceleration, - Vector3 angularVelocity, Vector3 scale); + bool ObjectRemove(object sender, uint localID); + void ObjectTransform(object sender, uint localID, Vector3 position, Quaternion rotation, Vector3 velocity, + Vector3 acceleration, Vector3 angularVelocity); void ObjectFlags(object sender, SimulationObject obj, PrimFlags flags); void ObjectImage(object sender, SimulationObject obj, string mediaURL, Primitive.TextureEntry textureEntry); - void ObjectModify(object sender, SimulationObject obj, Primitive.ConstructionData data); + void ObjectModify(object sender, uint localID, Primitive.ConstructionData data); void AvatarAppearance(object sender, Agent agent, Primitive.TextureEntry textures, byte[] visualParams); diff --git a/bin/Simian.ini b/bin/Simian.ini index 42954ce0..16121cce 100644 --- a/bin/Simian.ini +++ b/bin/Simian.ini @@ -39,15 +39,15 @@ AvatarManager ; Friendship management and alerts FriendManager -; Texture downloads -ImageDelivery - ; Chat and instant messaging Messaging ; Money management and accounting functions Money +; Texture downloads +ImageDelivery + ; A simple physics engine for avatar movement. Supports walking, flying, and ; swimming as well as avatar-avatar collisions. Does not support avatar-prim ; or prim-prim collisions. @@ -67,6 +67,15 @@ RenderingPluginMesher ; Main scene graph engine. All spatial events are processed through here. SceneManager +; Periscope allows you to proxy a foreign grid simulator into the local Simian +; using a libOpenMetaverse bot. The first person to login to Simian will become +; the master agent, who's movement is tethered to the bot's movement. Any other +; agents that login can move around freely, but will only see what the master +; agent is seeing through the periscope. If you enable this extension, disable +; ImageDelivery and Movement as Periscope has its own implementations of those +; extensions +;Periscope + ; ; ---Persistence Providers--- ;