/* * Copyright (c) 2006, Second Life Reverse Engineering Team * 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.Timers; using System.Threading; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Net; using System.Net.Sockets; using System.Globalization; using System.IO; using Nwc.XmlRpc; using Nii.JSON; using libsecondlife.Packets; namespace libsecondlife { /// /// This exception is thrown whenever a network operation is attempted /// without a network connection. /// public class NotConnectedException : ApplicationException { } /// /// Capabilities is the name of the bi-directional HTTP REST protocol that /// Second Life uses to communicate transactions such as teleporting or /// asset transfers /// public class Caps { /// /// Triggered when an event is received via the EventQueueGet capability; /// /// /// public delegate void EventQueueCallback(string message, object body); /// Reference to the SecondLife client this system is connected to public SecondLife Client; /// Reference to the simulator this system is connected to public Simulator Simulator; /// public string SeedCapsURI { get { return Seedcaps; } } internal bool Dead = false; internal string Seedcaps; private StringDictionary Capabilities = new StringDictionary(); private string EventQueueCap = String.Empty; private Thread EventQueueThread; private WebRequest EventQueueRequest = null; private List Callbacks; /// /// Default constructor /// /// /// /// /// internal Caps(SecondLife client, Simulator simulator, string seedcaps, List callbacks) { Client = client; Simulator = simulator; Seedcaps = seedcaps; Callbacks = callbacks; ArrayList req = new ArrayList(); req.Add("MapLayer"); req.Add("MapLayerGod"); req.Add("NewAgentInventory"); req.Add("EventQueueGet"); byte[] data = LLSD.LLSDSerialize(req); WebRequest request = WebRequest.Create(seedcaps); request.Method = "POST"; request.ContentLength = data.Length; Stream reqStream = request.GetRequestStream(); reqStream.Write(data, 0, data.Length); reqStream.Close(); request.BeginGetResponse(new AsyncCallback(InitialResponse), request); } public void Disconnect() { Dead = true; if (EventQueueRequest != null) EventQueueRequest.Abort(); } private void RunEventQueue() { // Make a new request Hashtable req = new Hashtable(); req["ack"] = null; req["done"] = false; byte[] data = LLSD.LLSDSerialize(req); EventQueueRequest = WebRequest.Create(EventQueueCap); EventQueueRequest.Method = "POST"; EventQueueRequest.ContentLength = data.Length; Stream reqStream = EventQueueRequest.GetRequestStream(); reqStream.Write(data, 0, data.Length); reqStream.Close(); EventQueueRequest.BeginGetResponse(new AsyncCallback(EventQueueResponse), EventQueueRequest); } private void InitialResponse(IAsyncResult result) { WebRequest request = (WebRequest)result.AsyncState; byte[] buffer = null; try { HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result); BinaryReader reader = new BinaryReader(response.GetResponseStream()); buffer = reader.ReadBytes((int)response.ContentLength); response.Close(); } catch (WebException e) { Client.Log("CAPS probe error: " + e.Message, Helpers.LogLevel.Warning); } if (buffer != null) { Hashtable resp = (Hashtable)LLSD.LLSDDeserialize(buffer); foreach (string cap in resp.Keys) { Client.DebugLog(String.Format("Got cap {0}: {1}", cap, (string)resp[cap])); Capabilities[cap] = (string)resp[cap]; } if (Capabilities.ContainsKey("EventQueueGet")) { EventQueueCap = Capabilities["EventQueueGet"]; Client.Log("Running event queue", Helpers.LogLevel.Info); EventQueueThread = new Thread(new ThreadStart(RunEventQueue)); EventQueueThread.Start(); } } } private void EventQueueResponse(IAsyncResult result) { byte[] buffer = null; long ack = 0; try { HttpWebResponse response = (HttpWebResponse)EventQueueRequest.EndGetResponse(result); BinaryReader reader = new BinaryReader(response.GetResponseStream()); buffer = reader.ReadBytes((int)response.ContentLength); response.Close(); } catch (WebException e) { Client.DebugLog("EventQueue: " + e.Message); } if (buffer != null) { Hashtable resp = (Hashtable)LLSD.LLSDDeserialize(buffer); ArrayList events = (ArrayList)resp["events"]; ack = (long)resp["id"]; foreach (Hashtable evt in events) { string msg = (string)evt["message"]; object body = (object)evt["body"]; Client.DebugLog("Event " + msg + ":" + Environment.NewLine + LLSD.LLSDDump(body, 0)); if (!Dead) { foreach (EventQueueCallback callback in Callbacks) { try { callback(msg, body); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } } if (!Dead) { // Make a new request Hashtable req = new Hashtable(); if (ack != 0) req["ack"] = ack; else req["ack"] = null; req["done"] = false; byte[] data = LLSD.LLSDSerialize(req); EventQueueRequest = WebRequest.Create(EventQueueCap); EventQueueRequest.Method = "POST"; EventQueueRequest.ContentLength = data.Length; Stream reqStream = EventQueueRequest.GetRequestStream(); reqStream.Write(data, 0, data.Length); reqStream.Close(); EventQueueRequest.BeginGetResponse(new AsyncCallback(EventQueueResponse), EventQueueRequest); } } } /// /// NetworkManager is responsible for managing the network layer of /// libsecondlife. It tracks all the server connections, serializes /// outgoing traffic and deserializes incoming traffic, and provides /// instances of delegates for network-related events. /// public class NetworkManager { /// /// Explains why a simulator or the grid disconnected from us /// public enum DisconnectType { /// The client requested the logout or simulator disconnect ClientInitiated, /// The server notified us that it is disconnecting ServerInitiated, /// Either a socket was closed or network traffic timed out NetworkTimeout } /// /// Coupled with RegisterCallback(), this is triggered whenever a packet /// of a registered type is received /// /// /// public delegate void PacketCallback(Packet packet, Simulator simulator); /// /// Triggered when a simulator other than the simulator that is currently /// being occupied disconnects for whatever reason /// /// The simulator that disconnected, which will become a null /// reference after the callback is finished /// Enumeration explaining the reason for the disconnect public delegate void SimDisconnectedCallback(Simulator simulator, DisconnectType reason); /// /// Triggered when we are logged out of the grid due to a simulator request, /// client request, network timeout, or any other cause /// /// Enumeration explaining the reason for the disconnect /// If we were logged out by the simulator, this /// is a message explaining why public delegate void DisconnectedCallback(DisconnectType reason, string message); /// /// Triggered when CurrentSim changes /// /// A reference to the old value of CurrentSim public delegate void CurrentSimChangedCallback(Simulator PreviousSimulator); /// /// An event for the connection to a simulator other than the currently /// occupied one disconnecting /// public event SimDisconnectedCallback OnSimDisconnected; /// /// An event for being logged out either through client request, server /// forced, or network error /// public event DisconnectedCallback OnDisconnected; /// /// An event for when CurrentSim changes /// public event CurrentSimChangedCallback OnCurrentSimChanged; /// The permanent UUID for the logged in avatar public LLUUID AgentID = LLUUID.Zero; /// Temporary UUID assigned to this session, used for /// verifying our identity in packets public LLUUID SessionID = LLUUID.Zero; /// Shared secret UUID that is never sent over the wire public LLUUID SecureSessionID = LLUUID.Zero; /// Uniquely identifier associated with our connections to /// simulators public uint CircuitCode; /// String holding a descriptive error on login failure, empty /// otherwise public string LoginError = String.Empty; /// The simulator that the logged in avatar is currently /// occupying public Simulator CurrentSim = null; /// The capabilities for the current simulator public Caps CurrentCaps = null; /// The complete dictionary of all the login values returned /// by the RPC login server, converted to native data types wherever /// possible public Dictionary LoginValues = new Dictionary(); /// /// Shows whether the network layer is logged in to the grid or not /// public bool Connected { get { return connected; } } /// internal Dictionary> Callbacks = new Dictionary>(); private SecondLife Client; private List Simulators = new List(); private System.Timers.Timer DisconnectTimer; private System.Timers.Timer LogoutTimer; private bool connected = false; private List EventQueueCallbacks = new List(); private ManualResetEvent LogoutReplyEvent = new ManualResetEvent(false); /// /// Default constructor /// /// Reference to the SecondLife client public NetworkManager(SecondLife client) { Client = client; CurrentSim = null; // Register the internal callbacks RegisterCallback(PacketType.RegionHandshake, new PacketCallback(RegionHandshakeHandler)); RegisterCallback(PacketType.StartPingCheck, new PacketCallback(StartPingCheckHandler)); RegisterCallback(PacketType.ParcelOverlay, new PacketCallback(ParcelOverlayHandler)); RegisterCallback(PacketType.EnableSimulator, new PacketCallback(EnableSimulatorHandler)); RegisterCallback(PacketType.KickUser, new PacketCallback(KickUserHandler)); RegisterCallback(PacketType.LogoutReply, new PacketCallback(LogoutReplyHandler)); // The proper timeout for this will get set at Login DisconnectTimer = new System.Timers.Timer(); DisconnectTimer.Elapsed += new ElapsedEventHandler(DisconnectTimer_Elapsed); } /// /// Register an event handler for a packet. This is a low level event /// interface and should only be used if you are doing something not /// supported in libsecondlife /// /// Packet type to trigger events for /// Callback to fire when a packet of this type /// is received public void RegisterCallback(PacketType type, PacketCallback callback) { if (!Callbacks.ContainsKey(type)) { Callbacks[type] = new List(); } List callbackArray = Callbacks[type]; callbackArray.Add(callback); } /// /// Unregister an event handler for a packet. This is a low level event /// interface and should only be used if you are doing something not /// supported in libsecondlife /// /// Packet type this callback is registered with /// Callback to stop firing events for public void UnregisterCallback(PacketType type, PacketCallback callback) { if (!Callbacks.ContainsKey(type)) { Client.Log("Trying to unregister a callback for packet " + type.ToString() + " when no callbacks are setup for that packet", Helpers.LogLevel.Info); return; } List callbackArray = Callbacks[type]; if (callbackArray.Contains(callback)) { callbackArray.Remove(callback); } else { Client.Log("Trying to unregister a non-existant callback for packet " + type.ToString(), Helpers.LogLevel.Info); } } /// /// Register a CAPS event handler /// /// Callback to fire when a CAPS event is received public void RegisterEventCallback(Caps.EventQueueCallback callback) { EventQueueCallbacks.Add(callback); } /// /// Send a packet to the simulator the avatar is currently occupying /// /// Packet to send public void SendPacket(Packet packet) { if (CurrentSim != null && CurrentSim.Connected) CurrentSim.SendPacket(packet, true); } /// /// Send a packet to a specified simulator /// /// Packet to send /// Simulator to send the packet to public void SendPacket(Packet packet, Simulator simulator) { if (simulator != null) simulator.SendPacket(packet, true); } /// /// Send a raw byte array as a packet to the current simulator /// /// Byte array containing a packet /// Whether to set the second, third, and fourth /// bytes of the payload to the current sequence number public void SendPacket(byte[] payload, bool setSequence) { if (CurrentSim != null) CurrentSim.SendPacket(payload, setSequence); } /// /// Send a raw byte array as a packet to the specified simulator /// /// Byte array containing a packet /// Simulator to send the packet to /// Whether to set the second, third, and fourth /// bytes of the payload to the current sequence number public void SendPacket(byte[] payload, Simulator simulator, bool setSequence) { if (simulator != null) simulator.SendPacket(payload, setSequence); } /// /// Build a start location URI for passing to the Login function /// /// Name of the simulator to start in /// X coordinate to start at /// Y coordinate to start at /// Z coordinate to start at /// String with a URI that can be used to login to a specified /// location public static string StartLocation(string sim, int x, int y, int z) { // uri:sim name&x&y&z return "uri:" + sim.ToLower() + "&" + x + "&" + y + "&" + z; } /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// public Dictionary DefaultLoginValues(string firstName, string lastName, string password, string mac, string startLocation, int major, int minor, int patch, int build, string platform, string viewerDigest, string userAgent, string author, bool md5pass) { Dictionary values = new Dictionary(); values["first"] = firstName; values["last"] = lastName; values["passwd"] = md5pass ? password : Helpers.MD5(password); values["start"] = startLocation; values["major"] = major; values["minor"] = minor; values["patch"] = patch; values["build"] = build; values["platform"] = platform; values["mac"] = mac; values["agree_to_tos"] = "true"; values["read_critical"] = "true"; values["viewer_digest"] = viewerDigest; values["user-agent"] = userAgent + " (" + Client.Settings.VERSION + ")"; values["author"] = author; // Build the options array List optionsArray = new List(); optionsArray.Add("inventory-root"); optionsArray.Add("inventory-skeleton"); optionsArray.Add("inventory-lib-root"); optionsArray.Add("inventory-lib-owner"); optionsArray.Add("inventory-skel-lib"); optionsArray.Add("initial-outfit"); optionsArray.Add("gestures"); optionsArray.Add("event_categories"); optionsArray.Add("event_notifications"); optionsArray.Add("classified_categories"); optionsArray.Add("buddy-list"); optionsArray.Add("ui-config"); optionsArray.Add("login-flags"); optionsArray.Add("global-textures"); values["options"] = optionsArray; return values; } /// /// Assigned by the OnConnected event. Raised when login was a success /// /// Reference to the SecondLife class that called the event public delegate void ConnectedCallback(object sender); /// /// Event raised when the client was able to connected successfully. /// /// Uses the ConnectedCallback delegate. public event ConnectedCallback OnConnected; /// /// Assigned by the OnLogoutReply callback. Raised upone receipt of a LogoutReply packet during logout process. /// /// public delegate void LogoutCallback(List inventoryItems); /// /// Event raised when a logout is confirmed by the simulator /// public event LogoutCallback OnLogoutReply; /// /// Simplified login that takes the most common fields as parameters /// and uses defaults for the rest /// /// Account first name /// Account last name /// Account password /// Client application name and version /// Client application author /// Whether the login was successful or not. On failure the /// LoginError string will contain the error public bool Login(string firstName, string lastName, string password, string userAgent, string author) { Dictionary loginParams = DefaultLoginValues(firstName, lastName, password, "00:00:00:00:00:00", "last", 1, 50, 50, 50, "Win", "0", userAgent, author, false); return Login(loginParams); } /// /// /// /// /// /// /// /// /// /// public bool Login(string firstName, string lastName, string password, string userAgent, string author, bool md5pass) { Dictionary loginParams = DefaultLoginValues(firstName, lastName, password, "00:00:00:00:00:00", "last", 1, 50, 50, 50, "Win", "0", userAgent, author, md5pass); return Login(loginParams); } /// /// Simplified login that takes the most common fields along with a /// starting location URI, and can accept an MD5 string instead of a /// plaintext password /// /// Account first name /// Account last name /// Account password or MD5 hash of the password /// such as $1$1682a1e45e9f957dcdf0bb56eb43319c /// Client application name and version /// Starting location URI that can be built with /// StartLocation() /// Client application author /// If true, the password field contains /// Whether the login was successful or not. On failure the /// LoginError string will contain the error public bool Login(string firstName, string lastName, string password, string userAgent, string start, string author, bool md5pass) { Dictionary loginParams = DefaultLoginValues(firstName, lastName, password, "00:00:00:00:00:00", start, 1, 50, 50, 50, "Win", "0", userAgent, author, md5pass); return Login(loginParams); } /// /// Login that takes a custom built dictionary of login parameters and /// values /// /// Dictionary of login parameters and values /// that can be created with DefaultLoginValues() /// Whether the login was successful or not. On failure the /// LoginError string will contain the error public bool Login(Dictionary loginParams) { return Login(loginParams, Client.Settings.LOGIN_SERVER, "login_to_simulator"); } /// /// Login that takes a custom built dictionary of login parameters and /// values and the URL of the login server /// /// Dictionary of login parameters and values /// that can be created with DefaultLoginValues() /// URL of the login server to authenticate with /// Whether the login was successful or not. On failure the /// LoginError string will contain the error public bool Login(Dictionary loginParams, string url) { return Login(loginParams, url, "login_to_simulator"); } /// /// Login that takes a custom built dictionary of login parameters and /// values, URL of the login server, and the name of the XML-RPC method /// to use /// /// Dictionary of login parameters and values /// that can be created with DefaultLoginValues() /// URL of the login server to authenticate with /// The XML-RPC method to execute on the login /// server. This is generally login_to_simulator /// Whether the login was successful or not. On failure the /// LoginError string will contain the error public bool Login(Dictionary loginParams, string url, string method) { XmlRpcResponse result; XmlRpcRequest xmlrpc; Hashtable loginValues; // Re-read the timeout value for the DisconnectTimer DisconnectTimer.Interval = Client.Settings.SIMULATOR_TIMEOUT; // Clear possible old values from the last login LoginValues.Clear(); // Rebuild the Dictionary<> in to a Hashtable for compatibility with XmlRpcCS loginValues = new Hashtable(loginParams.Count); foreach (KeyValuePair kvp in loginParams) { if (kvp.Value is IList) { IList list = ((IList)kvp.Value); ArrayList array = new ArrayList(list.Count); foreach (object obj in list) { array.Add(obj); } loginValues[kvp.Key] = array; } else { loginValues[kvp.Key] = kvp.Value; } } // Build the XML-RPC request xmlrpc = new XmlRpcRequest(); xmlrpc.MethodName = method; xmlrpc.Params.Clear(); xmlrpc.Params.Add(loginValues); try { result = (XmlRpcResponse)xmlrpc.Send(url, Client.Settings.LOGIN_TIMEOUT); } catch (Exception e) { LoginError = "XML-RPC Error: " + e.Message; return false; } if (result.IsFault) { Client.Log("Fault " + result.FaultCode + ": " + result.FaultString, Helpers.LogLevel.Error); LoginError = "XML-RPC Fault: " + result.FaultCode + ": " + result.FaultString; return false; } Hashtable values = (Hashtable)result.Value; foreach (DictionaryEntry entry in values) { LoginValues[(string)entry.Key] = entry.Value; } if ((string)LoginValues["login"] == "indeterminate") { string nexturl = (string)LoginValues["next_url"]; string nextmethod = (string)LoginValues["next_method"]; string message = (string)LoginValues["message"]; Client.Log("Login redirected: " + nexturl + ", message: " + message, Helpers.LogLevel.Info); return Login(loginParams, nexturl, nextmethod); } else if ((string)LoginValues["login"] == "false") { LoginError = LoginValues["reason"] + ": " + LoginValues["message"]; return false; } else if ((string)LoginValues["login"] != "true") { LoginError = "Unknown error"; return false; } System.Text.RegularExpressions.Regex LLSDtoJSON = new System.Text.RegularExpressions.Regex(@"('|r([0-9])|r(\-))"); string json; Dictionary jsonObject = null; LLVector3 vector = LLVector3.Zero; LLVector3 posVector = LLVector3.Zero; LLVector3 lookatVector = LLVector3.Zero; ulong regionHandle = 0; try { if (LoginValues.ContainsKey("look_at")) { // Replace LLSD variables with object representations // Convert LLSD string to JSON json = "{vector:" + LLSDtoJSON.Replace((string)LoginValues["look_at"], "$2") + "}"; // Convert JSON string to a JSON object jsonObject = JsonFacade.fromJSON(json); JSONArray jsonVector = (JSONArray)jsonObject["vector"]; // Convert the JSON object to an LLVector3 vector = new LLVector3(Convert.ToSingle(jsonVector[0], CultureInfo.InvariantCulture), Convert.ToSingle(jsonVector[1], CultureInfo.InvariantCulture), Convert.ToSingle(jsonVector[2], CultureInfo.InvariantCulture)); LoginValues["look_at"] = vector; } } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Warning); LoginValues["look_at"] = null; } try { if (LoginValues.ContainsKey("home")) { Dictionary home; // Convert LLSD string to JSON json = LLSDtoJSON.Replace((string)LoginValues["home"], "$2"); // Convert JSON string to an object jsonObject = JsonFacade.fromJSON(json); // Create the position vector JSONArray array = (JSONArray)jsonObject["position"]; posVector = new LLVector3(Convert.ToSingle(array[0], CultureInfo.InvariantCulture), Convert.ToSingle(array[1], CultureInfo.InvariantCulture), Convert.ToSingle(array[2], CultureInfo.InvariantCulture)); // Create the look_at vector array = (JSONArray)jsonObject["look_at"]; lookatVector = new LLVector3(Convert.ToSingle(array[0], CultureInfo.InvariantCulture), Convert.ToSingle(array[1], CultureInfo.InvariantCulture), Convert.ToSingle(array[2], CultureInfo.InvariantCulture)); // Create the regionhandle array = (JSONArray)jsonObject["region_handle"]; regionHandle = Helpers.UIntsToLong((uint)(int)array[0], (uint)(int)array[1]); Client.Self.Position = posVector; Client.Self.LookAt = lookatVector; // Create a dictionary to hold the home values home = new Dictionary(); home["position"] = posVector; home["look_at"] = lookatVector; home["region_handle"] = regionHandle; LoginValues["home"] = home; } } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Warning); LoginValues["home"] = null; } try { this.AgentID = new LLUUID((string)LoginValues["agent_id"]); this.SessionID = new LLUUID((string)LoginValues["session_id"]); this.SecureSessionID = new LLUUID((string)LoginValues["secure_session_id"]); Client.Self.ID = this.AgentID; // Names are wrapped in quotes now, have to strip those Client.Self.FirstName = ((string)LoginValues["first_name"]).Trim(new char[] { '"' }); Client.Self.LastName = ((string)LoginValues["last_name"]).Trim(new char[] { '"' }); Client.Self.LookAt = vector; Client.Self.HomePosition = posVector; Client.Self.HomeLookAt = lookatVector; // Get Inventory Root Folder ArrayList alInventoryRoot = (ArrayList)LoginValues["inventory-root"]; Hashtable htInventoryRoot = (Hashtable)alInventoryRoot[0]; Client.Self.InventoryRootFolderUUID = new LLUUID((string)htInventoryRoot["folder_id"]); // Set the Circuit Code CircuitCode = (uint)(int)LoginValues["circuit_code"]; // Connect to the sim given in the login reply if (Connect(IPAddress.Parse((string)LoginValues["sim_ip"]), (ushort)(int)LoginValues["sim_port"], true, (string)LoginValues["seed_capability"]) == null) { LoginError = "Unable to connect to the simulator"; return false; } // Send a couple packets that are useful right after login SendInitialPackets(); // Fire an event for connecting to the grid if (OnConnected != null) { try { OnConnected(this.Client); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } return true; } catch (Exception e) { Client.Log("Login error: " + e.ToString(), Helpers.LogLevel.Error); return false; } } /// /// Connect to a simulator /// /// IP address to connect to /// Port to connect to /// Whether to set CurrentSim to this new /// connection, use this if the avatar is moving in to this simulator /// URL of the capabilities server to use for /// this sim connection /// A Simulator object on success, otherwise null public Simulator Connect(IPAddress ip, ushort port, bool setDefault, string seedcaps) { Simulator simulator = new Simulator(Client, ip, (int)port, setDefault); if (!simulator.Connected) { simulator = null; return null; } lock (Simulators) Simulators.Add(simulator); // Mark that we are connected to the grid (in case we weren't before) connected = true; // Start a timer that checks if we've been disconnected DisconnectTimer.Start(); if (setDefault) { Simulator oldSim = CurrentSim; lock (Simulators) CurrentSim = simulator; if (CurrentCaps != null) CurrentCaps.Disconnect(); CurrentCaps = null; if (seedcaps != null && seedcaps.Length > 0) CurrentCaps = new Caps(Client, simulator, seedcaps, EventQueueCallbacks); if (OnCurrentSimChanged != null && simulator != oldSim) { try { OnCurrentSimChanged(oldSim); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } return simulator; } /// /// Initiate a blocking logout request. This will return when the logout /// handshake has completed or when Settings.LOGOUT_TIMEOUT has expired /// and a LogoutDemand packet has been sent /// public void Logout() { LogoutReplyEvent.Reset(); RequestLogout(); LogoutReplyEvent.WaitOne(Client.Settings.LOGOUT_TIMEOUT, false); } /// /// Initiate the logout process (three step process!) /// public void RequestLogout() { // This will catch a Logout when the client is not logged in if (CurrentSim == null || !connected) { LogoutReplyEvent.Set(); return; } Client.Log("Logging out", Helpers.LogLevel.Info); DisconnectTimer.Stop(); // Send a logout request to the current sim LogoutRequestPacket logout = new LogoutRequestPacket(); logout.AgentData.AgentID = AgentID; logout.AgentData.SessionID = SessionID; CurrentSim.SendPacket(logout, true); LogoutTimer = new System.Timers.Timer(Client.Settings.LOGOUT_TIMEOUT); LogoutTimer.AutoReset = false; LogoutTimer.Elapsed += new ElapsedEventHandler(LogoutTimer_Elapsed); LogoutTimer.Start(); } /// /// Uses a LogoutDemand packet to force initiate a logout /// public void ForceLogout() { Client.Log("Forcing a logout", Helpers.LogLevel.Info); DisconnectTimer.Stop(); // Insist on shutdown LogoutDemandPacket logoutDemand = new LogoutDemandPacket(); logoutDemand.LogoutBlock.SessionID = SessionID; CurrentSim.SendPacket(logoutDemand, true); FinalizeLogout(); } /// /// /// /// public void DisconnectSim(Simulator sim) { if (sim != null) { sim.Disconnect(); // Fire the SimDisconnected event if a handler is registered if (OnSimDisconnected != null) { try { OnSimDisconnected(sim, DisconnectType.NetworkTimeout); } catch (Exception e) { Client.Log("Caught an exception in OnSimDisconnected(): " + e.ToString(), Helpers.LogLevel.Error); } } lock (Simulators) Simulators.Remove(sim); } else { Client.Log("DisconnectSim() called with a null Simulator reference", Helpers.LogLevel.Warning); } } /// /// Finalize the logout procedure. Close down sockets, etc. /// private void FinalizeLogout() { LogoutTimer.Stop(); // Shutdown the network layer Shutdown(); if (OnDisconnected != null) { try { OnDisconnected(DisconnectType.ClientInitiated, ""); } catch (Exception e) { Client.Log("Caught an exception in OnDisconnected(): " + e.ToString(), Helpers.LogLevel.Error); } } // In case we are blocking in Logout() LogoutReplyEvent.Set(); } /// /// Shutdown will disconnect all the sims except for the current sim /// first, and then kill the connection to CurrentSim. /// private void Shutdown() { Client.Log("NetworkManager shutdown initiated", Helpers.LogLevel.Info); lock (Simulators) { // Disconnect all simulators except the current one for (int i = 0; i < Simulators.Count; i++) { if (Simulators[i] != null && Simulators[i] != CurrentSim) { DisconnectSim(Simulators[i]); // Fire the SimDisconnected event if a handler is registered if (OnSimDisconnected != null) { try { OnSimDisconnected(Simulators[i], DisconnectType.NetworkTimeout); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } } Simulators.Clear(); } if (CurrentSim != null) { Simulator oldSim = CurrentSim; DisconnectSim(CurrentSim); lock (Simulators) CurrentSim = null; if (OnCurrentSimChanged != null) { try { OnCurrentSimChanged(oldSim); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } } if (CurrentCaps != null) { CurrentCaps.Disconnect(); CurrentCaps = null; } connected = false; } private void SendInitialPackets() { // Request the economy data SendPacket(new EconomyDataRequestPacket()); // Send an AgentUpdate packet to truly move our avatar in to the sim MainAvatar.AgentUpdateFlags controlFlags = MainAvatar.AgentUpdateFlags.AGENT_CONTROL_FINISH_ANIM; LLVector3 position = new LLVector3(128, 128, 32); LLVector3 forwardAxis = new LLVector3(0, 0.999999f, 0); LLVector3 leftAxis = new LLVector3(0.999999f, 0, 0); LLVector3 upAxis = new LLVector3(0, 0, 0.999999f); Client.Self.UpdateCamera(controlFlags, position, forwardAxis, leftAxis, upAxis, LLQuaternion.Identity, LLQuaternion.Identity, 384.0f, true); } /// /// Triggered if a LogoutReply is not received /// private void LogoutTimer_Elapsed(object sender, ElapsedEventArgs ev) { LogoutTimer.Stop(); Client.Log("Logout due to timeout on server acknowledgement", Helpers.LogLevel.Debug); ForceLogout(); } private void DisconnectTimer_Elapsed(object sender, ElapsedEventArgs ev) { if (connected) { if (CurrentSim == null) { DisconnectTimer.Stop(); connected = false; return; } // If the current simulator is disconnected, shutdown+callback+return if (CurrentSim.DisconnectCandidate) { Client.Log("Network timeout for the current simulator (" + CurrentSim.ToString() + "), logging out", Helpers.LogLevel.Warning); DisconnectTimer.Stop(); connected = false; // Shutdown the network layer Shutdown(); if (OnDisconnected != null) { try { OnDisconnected(DisconnectType.NetworkTimeout, ""); } catch (Exception e) { Client.Log("Caught an exception in OnDisconnected(): " + e.ToString(), Helpers.LogLevel.Error); } } // We're completely logged out and shut down, leave this function return; } List disconnectedSims = null; // Check all of the connected sims for disconnects lock (Simulators) { for (int i = 0; i < Simulators.Count; i++) { if (Simulators[i].DisconnectCandidate) { if (disconnectedSims == null) disconnectedSims = new List(); disconnectedSims.Add(Simulators[i]); } else { Simulators[i].DisconnectCandidate = true; } } } // Actually disconnect each sim we detected as disconnected if (disconnectedSims != null) { for (int i = 0; i < disconnectedSims.Count; i++) { if (disconnectedSims[i] != null) { // This sim hasn't received any network traffic since the // timer last elapsed, consider it disconnected Client.Log("Network timeout for simulator " + disconnectedSims[i].ToString() + ", disconnecting", Helpers.LogLevel.Warning); DisconnectSim(disconnectedSims[i]); } } } } } /// /// Called to deal with LogoutReply packet and fires off callback /// /// Full packet of type LogoutReplyPacket /// private void LogoutReplyHandler(Packet packet, Simulator simulator) { LogoutReplyPacket logout = (LogoutReplyPacket)packet; if ((logout.AgentData.SessionID == SessionID) && (logout.AgentData.AgentID == AgentID)) { Client.Log("Logout negotiated with server", Helpers.LogLevel.Debug); // Deal with callbacks, if any if (OnLogoutReply != null) { List itemIDs = new List(); foreach (LogoutReplyPacket.InventoryDataBlock InventoryData in logout.InventoryData) { itemIDs.Add(InventoryData.ItemID); } try { OnLogoutReply(itemIDs); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } FinalizeLogout(); } else { Client.Log("Invalid Session or Agent ID received in Logout Reply... ignoring", Helpers.LogLevel.Warning); } } private void StartPingCheckHandler(Packet packet, Simulator simulator) { StartPingCheckPacket incomingPing = (StartPingCheckPacket)packet; CompletePingCheckPacket ping = new CompletePingCheckPacket(); ping.PingID.PingID = incomingPing.PingID.PingID; // TODO: We can use OldestUnacked to correct transmission errors SendPacket(ping, simulator); } private void RegionHandshakeHandler(Packet packet, Simulator simulator) { RegionHandshakePacket handshake = (RegionHandshakePacket)packet; simulator.ID = handshake.RegionInfo.CacheID; // TODO: What do we need these for? RegionFlags probably contains good stuff //handshake.RegionInfo.BillableFactor; //handshake.RegionInfo.RegionFlags; //handshake.RegionInfo.SimAccess; simulator.IsEstateManager = handshake.RegionInfo.IsEstateManager; simulator.Name = Helpers.FieldToString(handshake.RegionInfo.SimName); simulator.SimOwner = handshake.RegionInfo.SimOwner; simulator.TerrainBase0 = handshake.RegionInfo.TerrainBase0; simulator.TerrainBase1 = handshake.RegionInfo.TerrainBase1; simulator.TerrainBase2 = handshake.RegionInfo.TerrainBase2; simulator.TerrainBase3 = handshake.RegionInfo.TerrainBase3; simulator.TerrainDetail0 = handshake.RegionInfo.TerrainDetail0; simulator.TerrainDetail1 = handshake.RegionInfo.TerrainDetail1; simulator.TerrainDetail2 = handshake.RegionInfo.TerrainDetail2; simulator.TerrainDetail3 = handshake.RegionInfo.TerrainDetail3; simulator.TerrainHeightRange00 = handshake.RegionInfo.TerrainHeightRange00; simulator.TerrainHeightRange01 = handshake.RegionInfo.TerrainHeightRange01; simulator.TerrainHeightRange10 = handshake.RegionInfo.TerrainHeightRange10; simulator.TerrainHeightRange11 = handshake.RegionInfo.TerrainHeightRange11; simulator.TerrainStartHeight00 = handshake.RegionInfo.TerrainStartHeight00; simulator.TerrainStartHeight01 = handshake.RegionInfo.TerrainStartHeight01; simulator.TerrainStartHeight10 = handshake.RegionInfo.TerrainStartHeight10; simulator.TerrainStartHeight11 = handshake.RegionInfo.TerrainStartHeight11; simulator.WaterHeight = handshake.RegionInfo.WaterHeight; Client.Log("Received a region handshake for " + simulator.ToString(), Helpers.LogLevel.Info); // Send a RegionHandshakeReply RegionHandshakeReplyPacket reply = new RegionHandshakeReplyPacket(); reply.AgentData.AgentID = AgentID; reply.AgentData.SessionID = SessionID; reply.RegionInfo.Flags = 0; SendPacket(reply, simulator); // We're officially connected to this sim simulator.connected = true; simulator.ConnectedEvent.Set(); } private void ParcelOverlayHandler(Packet packet, Simulator simulator) { ParcelOverlayPacket overlay = (ParcelOverlayPacket)packet; if (overlay.ParcelData.SequenceID >= 0 && overlay.ParcelData.SequenceID <= 3) { Array.Copy(overlay.ParcelData.Data, 0, simulator.ParcelOverlay, overlay.ParcelData.SequenceID * 1024, 1024); simulator.ParcelOverlaysReceived++; if (simulator.ParcelOverlaysReceived > 3) { // TODO: ParcelOverlaysReceived should become internal, and reset to zero every // time it hits four. Also need a callback here } } else { Client.Log("Parcel overlay with sequence ID of " + overlay.ParcelData.SequenceID + " received from " + simulator.ToString(), Helpers.LogLevel.Warning); } } private void EnableSimulatorHandler(Packet packet, Simulator simulator) { // TODO: Actually connect to the simulator // TODO: Sending ConfirmEnableSimulator completely screws things up. :-? // Respond to the simulator connection request //Packet replyPacket = Packets.Network.ConfirmEnableSimulator(Protocol, AgentID, SessionID); //SendPacket(replyPacket, circuit); } private void KickUserHandler(Packet packet, Simulator simulator) { string message = Helpers.FieldToString(((KickUserPacket)packet).UserInfo.Reason); // Shutdown the network layer Shutdown(); if (OnDisconnected != null) { try { OnDisconnected(DisconnectType.ServerInitiated, message); } catch (Exception e) { Client.Log("Caught an exception in OnDisconnected(): " + e.ToString(), Helpers.LogLevel.Error); } } } } /// /// Throttles the network traffic for various different traffic types. /// Access this class through SecondLife.Throttle /// public class AgentThrottle { /// Maximum bytes per second for resending unacknowledged packets public float Resend { get { return resend; } set { if (value > 150000.0f) resend = 150000.0f; else if (value < 10000.0f) resend = 10000.0f; else resend = value; } } /// Maximum bytes per second for LayerData terrain public float Land { get { return land; } set { if (value > 170000.0f) land = 170000.0f; else if (value < 0.0f) land = 0.0f; // We don't have control of these so allow throttling to 0 else land = value; } } /// Maximum bytes per second for LayerData wind data public float Wind { get { return wind; } set { if (value > 34000.0f) wind = 34000.0f; else if (value < 0.0f) wind = 0.0f; // We don't have control of these so allow throttling to 0 else wind = value; } } /// Maximum bytes per second for LayerData clouds public float Cloud { get { return cloud; } set { if (value > 34000.0f) cloud = 34000.0f; else if (value < 0.0f) cloud = 0.0f; // We don't have control of these so allow throttling to 0 else cloud = value; } } /// Unknown, includes object data public float Task { get { return task; } set { if (value > 446000.0f) task = 446000.0f; else if (value < 4000.0f) task = 4000.0f; else task = value; } } /// Maximum bytes per second for textures public float Texture { get { return texture; } set { if (value > 446000.0f) texture = 446000.0f; else if (value < 4000.0f) texture = 4000.0f; else texture = value; } } /// Maximum bytes per second for downloaded assets public float Asset { get { return asset; } set { if (value > 220000.0f) asset = 220000.0f; else if (value < 10000.0f) asset = 10000.0f; else asset = value; } } /// Maximum bytes per second the entire connection, divided up /// between invidiual streams using default multipliers public float Total { get { return Resend + Land + Wind + Cloud + Task + Texture + Asset; } set { // These sane initial values were pulled from the Second Life client Resend = (value * 0.1f); Land = (float)(value * 0.52f / 3f); Wind = (float)(value * 0.05f); Cloud = (float)(value * 0.05f); Task = (float)(value * 0.704f / 3f); Texture = (float)(value * 0.704f / 3f); Asset = (float)(value * 0.484f / 3f); } } private SecondLife Client; private float resend; private float land; private float wind; private float cloud; private float task; private float texture; private float asset; /// /// Default constructor, uses a default high total of 1500 KBps (1536000) /// public AgentThrottle(SecondLife client) { Client = client; Total = 1536000.0f; } /// /// Constructor that decodes an existing AgentThrottle packet in to /// individual values /// /// Reference to the throttle data in an AgentThrottle /// packet /// Offset position to start reading at in the /// throttle data /// This is generally not needed in libsecondlife clients as /// the server will never send a throttle packet to the client public AgentThrottle(byte[] data, int pos) { int i; if (!BitConverter.IsLittleEndian) for (i = 0; i < 7; i++) Array.Reverse(data, pos + i * 4, 4); Resend = BitConverter.ToSingle(data, pos); pos += 4; Land = BitConverter.ToSingle(data, pos); pos += 4; Wind = BitConverter.ToSingle(data, pos); pos += 4; Cloud = BitConverter.ToSingle(data, pos); pos += 4; Task = BitConverter.ToSingle(data, pos); pos += 4; Texture = BitConverter.ToSingle(data, pos); pos += 4; Asset = BitConverter.ToSingle(data, pos); } /// /// Send an AgentThrottle packet to the server using the current values /// public void Set() { AgentThrottlePacket throttle = new AgentThrottlePacket(); throttle.AgentData.AgentID = Client.Network.AgentID; throttle.AgentData.SessionID = Client.Network.SessionID; throttle.AgentData.CircuitCode = Client.Network.CircuitCode; throttle.Throttle.GenCounter = 0; throttle.Throttle.Throttles = this.ToBytes(); Client.Network.SendPacket(throttle); } /// /// Convert the current throttle values to a byte array that can be put /// in an AgentThrottle packet /// /// Byte array containing all the throttle values public byte[] ToBytes() { byte[] data = new byte[7 * 4]; int i = 0; BitConverter.GetBytes(Resend).CopyTo(data, i); i += 4; BitConverter.GetBytes(Land).CopyTo(data, i); i += 4; BitConverter.GetBytes(Wind).CopyTo(data, i); i += 4; BitConverter.GetBytes(Cloud).CopyTo(data, i); i += 4; BitConverter.GetBytes(Task).CopyTo(data, i); i += 4; BitConverter.GetBytes(Texture).CopyTo(data, i); i += 4; BitConverter.GetBytes(Asset).CopyTo(data, i); i += 4; if (!BitConverter.IsLittleEndian) for (i = 0; i < 7; i++) Array.Reverse(data, i * 4, 4); return data; } } }