/* * ClientAO.cs: GridProxy application that acts as a client side animation overrider. * The application will start and stop animations corresponding to the movements * of the avatar on screen. * * Copyright (c) 2007 Gilbert Roulot * 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 Second Life Reverse Engineering Team 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; using System.Collections.Generic; using System.Net; using System.IO; using System.Reflection; using OpenMetaverse; using OpenMetaverse.Packets; using OpenMetaverse.StructuredData; using Nwc.XmlRpc; using GridProxy; public class ClientAO : ProxyPlugin { private ProxyFrame frame; private Proxy proxy; private UUID[] wetikonanims = { Animations.WALK, Animations.RUN, Animations.CROUCHWALK, Animations.FLY, Animations.TURNLEFT, Animations.TURNRIGHT, Animations.JUMP, Animations.HOVER_UP, Animations.CROUCH, Animations.HOVER_DOWN, Animations.STAND, Animations.STAND_1, Animations.STAND_2, Animations.STAND_3, Animations.STAND_4, Animations.HOVER, Animations.SIT, Animations.PRE_JUMP, Animations.FALLDOWN, Animations.LAND, Animations.STANDUP, Animations.FLYSLOW, Animations.SIT_GROUND_staticRAINED, UUID.Zero, //swimming doesnt exist UUID.Zero, UUID.Zero, UUID.Zero }; private string[] wetikonanimnames = { "walk", "run", "crouch walk", "fly", "turn left", "turn right", "jump", "hover up", "crouch", "hover down", "stand", "stand 2", "stand 3", "stand 4", "stand 5", "hover", "sit", "pre jump", "fall down", "land", "stand up", "fly slow", "sit on ground", "swim (ignored)", //swimming doesnt exist "swim (ignored)", "swim (ignored)", "swim (ignored)" }; private Dictionary animuid2name; //private Assembly libslAssembly; #region Packet delegates members private PacketDelegate _packetDelegate; private PacketDelegate packetDelegate { get { if (_packetDelegate == null) { _packetDelegate = new PacketDelegate(AnimationPacketHandler); } return _packetDelegate; } } private PacketDelegate _inventoryPacketDelegate; private PacketDelegate inventoryPacketDelegate { get { if (_inventoryPacketDelegate == null) { _inventoryPacketDelegate = new PacketDelegate(InventoryDescendentsHandler); } return _inventoryPacketDelegate; } } private PacketDelegate _transferPacketDelegate; private PacketDelegate transferPacketDelegate { get { if (_transferPacketDelegate == null) { _transferPacketDelegate = new PacketDelegate(TransferPacketHandler); } return _transferPacketDelegate; } } // private PacketDelegate _transferInfoDelegate; // private PacketDelegate transferInfoDelegate // { // get // { // if (_transferInfoDelegate == null) // { // _transferInfoDelegate = new PacketDelegate(TransferInfoHandler); // } // return _transferInfoDelegate; // } // } #endregion //map of built in SL animations and their overrides private Dictionary overrides = new Dictionary(); //list of animations currently running private Dictionary SignaledAnimations = new Dictionary(); //playing status of animations'override animation private Dictionary overrideanimationisplaying; //Current inventory path search string[] searchPath; //Search level int searchLevel; //Current folder UUID currentFolder; // Number of directory descendents received int nbdescendantsreceived; //List of items in the current folder Dictionary currentFolderItems; //Asset download request ID UUID assetdownloadID; //Downloaded bytes so far int downloadedbytes; //size of download int downloadsize; //data buffer byte[] buffer; public ClientAO(ProxyFrame frame) { this.frame = frame; this.proxy = frame.proxy; } //Initialise the plugin public override void Init() { //libslAssembly = Assembly.Load("libsecondlife"); //if (libslAssembly == null) throw new Exception("Assembly load exception"); // build the table of /command delegates InitializeCommandDelegates(); SayToUser("ClientAO loaded"); } // InitializeCommandDelegates: configure ClientAO's commands private void InitializeCommandDelegates() { //The ClientAO responds to command beginning with /ao frame.AddCommand("/ao", new ProxyFrame.CommandDelegate(CmdAO)); } //Process commands from the user private void CmdAO(string[] words) { if (words.Length < 2) { SayToUser("Usage: /ao on/off/notecard path"); } else if (words[1] == "on") { //Turn AO on AOOn(); SayToUser("AO started"); } else if (words[1] == "off") { //Turn AO off AOOff(); SayToUser("AO stopped"); } else { //Load notecard from path //exemple: /ao Objects/My AOs/wetikon/config.txt string[] tmp = new string[words.Length - 1]; //join the arguments together with spaces, to //take care of folder and item names with spaces in them for (int i = 1; i < words.Length; i++) { tmp[i - 1] = words[i]; } // add a delegate to monitor inventory infos proxy.AddDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); RequestFindObjectByPath(frame.InventoryRoot, String.Join(" ", tmp)); } } private void AOOn() { // add a delegate to track agent movements proxy.AddDelegate(PacketType.AvatarAnimation, Direction.Incoming, this.packetDelegate); } private void AOOff() { // remove the delegate to track agent movements proxy.RemoveDelegate(PacketType.AvatarAnimation, Direction.Incoming, this.packetDelegate); //Stop all override animations foreach (UUID tmp in overrides.Values) { Animate(tmp, false); } } // Inventory functions //start requesting an item by its path public void RequestFindObjectByPath(UUID baseFolder, string path) { if (path == null || path.Length == 0) throw new ArgumentException("Empty path is not supported"); currentFolder = baseFolder; //split path by '/' searchPath = path.Split('/'); //search for first element in the path searchLevel = 0; // Start the search RequestFolderContents(baseFolder, true, (searchPath.Length == 1) ? true : false, InventorySortOrder.ByName); } //request a folder content public void RequestFolderContents(UUID folder, bool folders, bool items, InventorySortOrder order) { //empty the dictionnary containing current folder items by name currentFolderItems = new Dictionary(); //reset the number of descendants received nbdescendantsreceived = 0; //build a packet to request the content FetchInventoryDescendentsPacket fetch = new FetchInventoryDescendentsPacket(); fetch.AgentData.AgentID = frame.AgentID; fetch.AgentData.SessionID = frame.SessionID; fetch.InventoryData.FetchFolders = folders; fetch.InventoryData.FetchItems = items; fetch.InventoryData.FolderID = folder; fetch.InventoryData.OwnerID = frame.AgentID; //is it correct? fetch.InventoryData.SortOrder = (int)order; //send packet to SL proxy.InjectPacket(fetch, Direction.Outgoing); } //process the reply from SL private Packet InventoryDescendentsHandler(Packet packet, IPEndPoint sim) { bool intercept = false; InventoryDescendentsPacket reply = (InventoryDescendentsPacket)packet; if (reply.AgentData.Descendents > 0 && reply.AgentData.FolderID == currentFolder) { //SayToUser("nb descendents: " + reply.AgentData.Descendents); //this packet concerns the folder we asked for if (reply.FolderData[0].FolderID != UUID.Zero && searchLevel < searchPath.Length - 1) { nbdescendantsreceived += reply.FolderData.Length; //SayToUser("nb received: " + nbdescendantsreceived); //folders are present, and we are not at end of path. //look at them for (int i = 0; i < reply.FolderData.Length; i++) { //SayToUser("Folder: " + Utils.BytesToString(reply.FolderData[i].Name)); if (searchPath[searchLevel] == Utils.BytesToString(reply.FolderData[i].Name)) { //We found the next folder in the path currentFolder = reply.FolderData[i].FolderID; if (searchLevel < searchPath.Length - 1) { // ask for next item in path searchLevel++; RequestFolderContents(currentFolder, true, (searchLevel < searchPath.Length - 1) ? false : true, InventorySortOrder.ByName); //Jump to end goto End; } } } if (nbdescendantsreceived >= reply.AgentData.Descendents) { //We have not found the folder. The user probably mistyped it SayToUser("Didn't find folder " + searchPath[searchLevel]); //Stop looking at packets proxy.RemoveDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); } } else if (searchLevel < searchPath.Length - 1) { //There are no folders in the packet ; but we are looking for one! //We have not found the folder. The user probably mistyped it SayToUser("Didn't find folder " + searchPath[searchLevel]); //Stop looking at packets proxy.RemoveDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); } else { //There are folders in the packet. And we are at the end of //the path, count their number in nbdescendantsreceived nbdescendantsreceived += reply.FolderData.Length; //SayToUser("nb received: " + nbdescendantsreceived); } if (reply.ItemData[0].ItemID != UUID.Zero && searchLevel == searchPath.Length - 1) { //there are items returned and we are looking for one //(end of search path) //count them nbdescendantsreceived += reply.ItemData.Length; //SayToUser("nb received: " + nbdescendantsreceived); for (int i = 0; i < reply.ItemData.Length; i++) { //we are going to store info on all items. we'll need //it to get the asset ID of animations refered to by the //configuration notecard if (reply.ItemData[i].ItemID != UUID.Zero) { InventoryItem item = CreateInventoryItem((InventoryType)reply.ItemData[i].InvType, reply.ItemData[i].ItemID); item.ParentUUID = reply.ItemData[i].FolderID; item.CreatorID = reply.ItemData[i].CreatorID; item.AssetType = (AssetType)reply.ItemData[i].Type; item.AssetUUID = reply.ItemData[i].AssetID; item.CreationDate = Utils.UnixTimeToDateTime((uint)reply.ItemData[i].CreationDate); item.Description = Utils.BytesToString(reply.ItemData[i].Description); item.Flags = (uint)reply.ItemData[i].Flags; item.Name = Utils.BytesToString(reply.ItemData[i].Name); item.GroupID = reply.ItemData[i].GroupID; item.GroupOwned = reply.ItemData[i].GroupOwned; item.Permissions = new Permissions( reply.ItemData[i].BaseMask, reply.ItemData[i].EveryoneMask, reply.ItemData[i].GroupMask, reply.ItemData[i].NextOwnerMask, reply.ItemData[i].OwnerMask); item.SalePrice = reply.ItemData[i].SalePrice; item.SaleType = (SaleType)reply.ItemData[i].SaleType; item.OwnerID = reply.AgentData.OwnerID; //SayToUser("item in folder: " + item.Name); //Add the item to the name -> item hash currentFolderItems.Add(item.Name, item); } } if (nbdescendantsreceived >= reply.AgentData.Descendents) { //We have received all the items in the last folder //Let's look for the item we are looking for if (currentFolderItems.ContainsKey(searchPath[searchLevel])) { //We found what we where looking for //Stop looking at packets proxy.RemoveDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); //Download the notecard assetdownloadID = RequestInventoryAsset(currentFolderItems[searchPath[searchLevel]]); } else { //We didnt find the item, the user probably mistyped its name SayToUser("Didn't find notecard " + searchPath[searchLevel]); //TODO: keep looking for a moment, or else reply packets may still //come in case of a very large inventory folder //Stop looking at packets proxy.RemoveDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); } } } else if (searchLevel == searchPath.Length - 1 && nbdescendantsreceived >= reply.AgentData.Descendents) { //There are no items in the packet, but we are looking for one! //We didnt find the item, the user probably mistyped its name SayToUser("Didn't find notecard " + searchPath[searchLevel]); //TODO: keep looking for a moment, or else reply packets may still //come in case of a very large inventory folder //Stop looking at packets proxy.RemoveDelegate(PacketType.InventoryDescendents, Direction.Incoming, this.inventoryPacketDelegate); } //Intercept the packet, it was a reply to our request. No need //to confuse the actual SL client intercept = true; } End: if (intercept) { //stop packet return null; } else { //let packet go to client return packet; } } public static InventoryItem CreateInventoryItem(InventoryType type, UUID id) { switch (type) { case InventoryType.Texture: return new InventoryTexture(id); case InventoryType.Sound: return new InventorySound(id); case InventoryType.CallingCard: return new InventoryCallingCard(id); case InventoryType.Landmark: return new InventoryLandmark(id); case InventoryType.Object: return new InventoryObject(id); case InventoryType.Notecard: return new InventoryNotecard(id); case InventoryType.Category: return new InventoryCategory(id); case InventoryType.LSL: return new InventoryLSL(id); case InventoryType.Snapshot: return new InventorySnapshot(id); case InventoryType.Attachment: return new InventoryAttachment(id); case InventoryType.Wearable: return new InventoryWearable(id); case InventoryType.Animation: return new InventoryAnimation(id); case InventoryType.Gesture: return new InventoryGesture(id); default: return new InventoryItem(type, id); } } //Ask for download of an item public UUID RequestInventoryAsset(InventoryItem item) { // Build the request packet and send it TransferRequestPacket request = new TransferRequestPacket(); request.TransferInfo.ChannelType = (int)ChannelType.Asset; request.TransferInfo.Priority = 101.0f; request.TransferInfo.SourceType = (int)SourceType.SimInventoryItem; UUID transferID = UUID.Random(); request.TransferInfo.TransferID = transferID; byte[] paramField = new byte[100]; Buffer.BlockCopy(frame.AgentID.GetBytes(), 0, paramField, 0, 16); Buffer.BlockCopy(frame.SessionID.GetBytes(), 0, paramField, 16, 16); Buffer.BlockCopy(item.OwnerID.GetBytes(), 0, paramField, 32, 16); Buffer.BlockCopy(UUID.Zero.GetBytes(), 0, paramField, 48, 16); Buffer.BlockCopy(item.UUID.GetBytes(), 0, paramField, 64, 16); Buffer.BlockCopy(item.AssetUUID.GetBytes(), 0, paramField, 80, 16); Buffer.BlockCopy(Utils.IntToBytes((int)item.AssetType), 0, paramField, 96, 4); request.TransferInfo.Params = paramField; // add a delegate to monitor configuration notecards download proxy.AddDelegate(PacketType.TransferPacket, Direction.Incoming, this.transferPacketDelegate); //send packet to SL proxy.InjectPacket(request, Direction.Outgoing); //so far we downloaded 0 bytes downloadedbytes = 0; //the total size of the download is yet unknown downloadsize = 0; //A 100K buffer should be enough for everyone buffer = new byte[1024 * 100]; //Return the transfer ID return transferID; } // SayToUser: send a message to the user as in-world chat private void SayToUser(string message) { ChatFromSimulatorPacket packet = new ChatFromSimulatorPacket(); packet.ChatData.FromName = Utils.StringToBytes("ClientAO"); packet.ChatData.SourceID = UUID.Random(); packet.ChatData.OwnerID = frame.AgentID; packet.ChatData.SourceType = (byte)2; packet.ChatData.ChatType = (byte)1; packet.ChatData.Audible = (byte)1; packet.ChatData.Position = new Vector3(0, 0, 0); packet.ChatData.Message = Utils.StringToBytes(message); proxy.InjectPacket(packet, Direction.Incoming); } //start or stop an animation public void Animate(UUID animationuuid, bool run) { AgentAnimationPacket animate = new AgentAnimationPacket(); animate.Header.Reliable = true; animate.AgentData.AgentID = frame.AgentID; animate.AgentData.SessionID = frame.SessionID; //We send one animation animate.AnimationList = new AgentAnimationPacket.AnimationListBlock[1]; animate.AnimationList[0] = new AgentAnimationPacket.AnimationListBlock(); animate.AnimationList[0].AnimID = animationuuid; animate.AnimationList[0].StartAnim = run; animate.PhysicalAvatarEventList = new AgentAnimationPacket.PhysicalAvatarEventListBlock[0]; //SayToUser("anim " + animname(animationuuid) + " " + run); proxy.InjectPacket(animate, Direction.Outgoing); } //return the name of an animation by its UUID // private string animname(UUID arg) // { // return animuid2name[arg]; // } //handle animation packets from simulator private Packet AnimationPacketHandler(Packet packet, IPEndPoint sim) { AvatarAnimationPacket animation = (AvatarAnimationPacket)packet; if (animation.Sender.ID == frame.AgentID) { //the received animation packet is about our Agent, handle it lock (SignaledAnimations) { // Reset the signaled animation list SignaledAnimations.Clear(); //fill it with the fresh list from simulator for (int i = 0; i < animation.AnimationList.Length; i++) { UUID animID = animation.AnimationList[i].AnimID; int sequenceID = animation.AnimationList[i].AnimSequenceID; // Add this animation to the list of currently signaled animations SignaledAnimations[animID] = sequenceID; //SayToUser("Animation: " + animname(animID)); } } //we now have a list of currently running animations //Start override animations if necessary foreach (UUID key in overrides.Keys) { //For each overriden animation key, test if its override is running if (SignaledAnimations.ContainsKey(key) && (!overrideanimationisplaying[key] )) { //An overriden animation is present and its override animation //isnt currently playing //Start the override animation //SayToUser("animation " + animname(key) + " started, will override with " + animname(overrides[key])); overrideanimationisplaying[key] = true; Animate(overrides[key], true); } else if ((!SignaledAnimations.ContainsKey(key)) && overrideanimationisplaying[key]) { //an override animation is currently playing, but it's overriden //animation is not. //stop the override animation //SayToUser("animation " + animname(key) + " stopped, will override with " + animname(overrides[key])); overrideanimationisplaying[key] = false; Animate(overrides[key], false); } } } //Let the packet go to the client return packet; } //handle packets that contain info about the notecard data transfer // private Packet TransferInfoHandler(Packet packet, IPEndPoint simulator) // { // TransferInfoPacket info = (TransferInfoPacket)packet; // // if (info.TransferInfo.TransferID == assetdownloadID) // { // //this is our requested tranfer, handle it // downloadsize = info.TransferInfo.Size; // // if ((StatusCode)info.TransferInfo.Status != StatusCode.OK) // { // SayToUser("Failed to read notecard"); // } // if (downloadedbytes >= downloadsize) // { // //Download already completed! // downloadCompleted(); // } // //intercept packet // return null; // } // return packet; // } //handle packets which contain the notecard data private Packet TransferPacketHandler(Packet packet, IPEndPoint simulator) { TransferPacketPacket asset = (TransferPacketPacket)packet; if (asset.TransferData.TransferID == assetdownloadID) { Buffer.BlockCopy(asset.TransferData.Data, 0, buffer, 1000 * asset.TransferData.Packet, asset.TransferData.Data.Length); downloadedbytes += asset.TransferData.Data.Length; // Check if we downloaded the full asset if (downloadedbytes >= downloadsize) { downloadCompleted(); } //Intercept packet return null; } return packet; } private void downloadCompleted() { //We have the notecard. //Stop looking at transfer packets proxy.RemoveDelegate(PacketType.TransferPacket, Direction.Incoming, this.transferPacketDelegate); //crop the buffer size byte[] tmp = new byte[downloadedbytes]; Buffer.BlockCopy(buffer, 0, tmp, 0, downloadedbytes); buffer = tmp; String notecardtext = getNotecardText(Utils.BytesToString(buffer)); //Load config, wetikon format loadWetIkon(notecardtext); } private void loadWetIkon(string config) { //Reinitialise override table overrides = new Dictionary(); overrideanimationisplaying = new Dictionary(); animuid2name = new Dictionary(); foreach (UUID key in wetikonanims ) { animuid2name[key] = wetikonanimnames[Array.IndexOf(wetikonanims, key)]; } //list of animations in wetikon //read every second line in the config char[] sep = { '\n' }; string[] lines = config.Split(sep); int length = lines.Length; int i = 1; while (i < length) { //Read animation name and look it up string animname = lines[i].Trim(); //SayToUser("anim: " + animname); if (animname != "") { if (currentFolderItems.ContainsKey(animname)) { UUID over = currentFolderItems[animname].AssetUUID; UUID orig = wetikonanims[((i + 1) / 2) - 1]; //put it in overrides animuid2name[over] = animname; overrides[orig] = over; overrideanimationisplaying[orig] = false; //SayToUser(wetikonanimnames[((i + 1) / 2) - 1] + " overriden by " + animname + " ( " + over + ")"); } else { //Not found SayToUser(animname + " not found."); } } i += 2; } SayToUser("Notecard read, " + overrides.Count + " animations found"); } private string getNotecardText(string data) { // Version 1 format: // Linden text version 1 // { // // Text length // // } // Version 2 format: (NOTE: Imports identically to version 1) // Linden text version 2 // { // // Text length // // } int i = 0; char[] sep = { '\n' }; string[] lines = data.Split(sep); int length = lines.Length; string result = ""; //check format if (!lines[i].StartsWith("Linden text version ")) { SayToUser("error"); return ""; } //{ i++; if (lines[i] != "{") { SayToUser("error"); return ""; } i++; if (lines[i] != "LLEmbeddedItems version 1") { SayToUser("error"); return ""; } //{ i++; if (lines[i] != "{") { SayToUser("error"); return ""; } //count ... i++; if (!lines[i].StartsWith("count ")) { SayToUser("error"); return ""; } //} i++; if (lines[i] != "}") { SayToUser("error"); return ""; } //Text length ... i++; if (!lines[i].StartsWith("Text length ")) { SayToUser("error"); return ""; } i++; while (i < length) { result += lines[i] + "\n"; i++; } result = result.Substring(0, result.Length - 3); return result; } }