diff --git a/libsecondlife-cs/Avatar.cs b/libsecondlife-cs/Avatar.cs index 4ce22f7f..d93890c6 100644 --- a/libsecondlife-cs/Avatar.cs +++ b/libsecondlife-cs/Avatar.cs @@ -129,8 +129,8 @@ namespace libsecondlife /// Avatar interests including spoken languages, skills, and "want to" /// choices public Interests ProfileInterests = new Interests(); - /// Region the avatar is in - public Region CurrentRegion = null; + /// Simulator the avatar is in + public Simulator CurrentSim = null; #endregion Public Members diff --git a/libsecondlife-cs/MainAvatar.cs b/libsecondlife-cs/MainAvatar.cs index 6e3265da..05379d6f 100644 --- a/libsecondlife-cs/MainAvatar.cs +++ b/libsecondlife-cs/MainAvatar.cs @@ -1032,7 +1032,7 @@ namespace libsecondlife public void AutoPilotLocal(int localX, int localY, float z) { uint x, y; - Helpers.LongToUInts(Client.Network.CurrentSim.Region.Handle, out x, out y); + Helpers.LongToUInts(Client.Network.CurrentSim.Handle, out x, out y); AutoPilot((ulong)(x + localX), (ulong)(y + localY), z); } @@ -1448,7 +1448,7 @@ namespace libsecondlife this.Position = movement.Data.Position; this.LookAt = movement.Data.LookAt; - simulator.Region.Handle = movement.Data.RegionHandle; + simulator.Handle = movement.Data.RegionHandle; } /// diff --git a/libsecondlife-cs/NetworkManager.cs b/libsecondlife-cs/NetworkManager.cs index 0f655ccc..abe2cc07 100644 --- a/libsecondlife-cs/NetworkManager.cs +++ b/libsecondlife-cs/NetworkManager.cs @@ -62,8 +62,8 @@ namespace libsecondlife /// Reference to the SecondLife client this system is connected to public SecondLife Client; - /// Reference to the region this system is connected to - public Region Region; + /// Reference to the simulator this system is connected to + public Simulator Simulator; internal bool Dead = false; @@ -76,13 +76,14 @@ namespace libsecondlife /// Default constructor /// /// - /// /// /// - public Caps(SecondLife client, Region region, string seedcaps, List callbacks) + public Caps(SecondLife client, Simulator simulator, string seedcaps, List callbacks) { - Client = client; Region = region; - this.Seedcaps = seedcaps; Callbacks = callbacks; + Client = client; + Simulator = simulator; + Seedcaps = seedcaps; + Callbacks = callbacks; ArrayList req = new ArrayList(); req.Add("MapLayer"); req.Add("MapLayerGod"); @@ -107,7 +108,7 @@ namespace libsecondlife } else { - Client.Log("Disabling caps for " + Region.ToString(), Helpers.LogLevel.Info); + Client.Log("Disabling caps for " + Simulator.ToString(), Helpers.LogLevel.Info); Dead = true; } } @@ -156,7 +157,7 @@ namespace libsecondlife } else { - Client.Log("Disabling caps for " + Region.ToString(), Helpers.LogLevel.Info); + Client.Log("Disabling caps for " + Simulator.ToString(), Helpers.LogLevel.Info); Dead = true; } } @@ -211,566 +212,6 @@ namespace libsecondlife } } - /// - /// Simulator is a wrapper for a network connection to a simulator and the - /// Region class representing the block of land in the metaverse - /// - public class Simulator - { - /// A public reference to the client that this Simulator object - /// is attached to - public SecondLife Client; - /// The Region class that this Simulator wraps - public Region Region; - /// Current time dilation of this simulator - public float Dilation = 0.0f; - - /// - /// The ID number associated with this particular connection to the - /// simulator, used to emulate TCP connections. This is used - /// internally for packets that have a CircuitCode field - /// - public uint CircuitCode - { - get { return circuitCode; } - set { circuitCode = value; } - } - /// - /// The IP address and port of the server - /// - public IPEndPoint IPEndPoint - { - get { return ipEndPoint; } - } - /// - /// A boolean representing whether there is a working connection to the - /// simulator or not - /// - public bool Connected - { - get { return connected; } - } - - /// Used internally to track sim disconnections - internal bool DisconnectCandidate = false; - - private NetworkManager Network; - private Dictionary> Callbacks; - private uint Sequence = 0; - private object SequenceLock = new object(); - private byte[] RecvBuffer = new byte[4096]; - private byte[] ZeroBuffer = new byte[8192]; - private byte[] ZeroOutBuffer = new byte[4096]; - private Socket Connection = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - private AsyncCallback ReceivedData; - // Packets we sent out that need ACKs from the simulator - private Dictionary NeedAck = new Dictionary(); - // Sequence numbers of packets we've received from the simulator - private Queue Inbox; - // ACKs that are queued up to be sent to the simulator - private Dictionary PendingAcks = new Dictionary(); - private bool connected = false; - private uint circuitCode; - private IPEndPoint ipEndPoint; - private EndPoint endPoint; - private System.Timers.Timer AckTimer; - private ManualResetEvent ConnectedEvent = new ManualResetEvent(false); - - - /// - /// Constructor for Simulator - /// - /// - /// - /// - /// - /// - public Simulator(SecondLife client, Dictionary> callbacks, - uint circuit, IPAddress ip, int port) - { - Client = client; - Network = client.Network; - Callbacks = callbacks; - Region = new Region(client); - circuitCode = circuit; - Inbox = new Queue(Client.Settings.INBOX_SIZE); - AckTimer = new System.Timers.Timer(Client.Settings.NETWORK_TICK_LENGTH); - AckTimer.Elapsed += new ElapsedEventHandler(AckTimer_Elapsed); - - // Initialize the callback for receiving a new packet - ReceivedData = new AsyncCallback(OnReceivedData); - - Client.Log("Connecting to " + ip.ToString() + ":" + port, Helpers.LogLevel.Info); - - try - { - // Create an endpoint that we will be communicating with (need it in two - // types due to .NET weirdness) - ipEndPoint = new IPEndPoint(ip, port); - endPoint = (EndPoint)ipEndPoint; - - // Associate this simulator's socket with the given ip/port and start listening - Connection.Connect(endPoint); - Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); - - // Send the UseCircuitCode packet to initiate the connection - UseCircuitCodePacket use = new UseCircuitCodePacket(); - use.CircuitCode.Code = circuitCode; - use.CircuitCode.ID = Network.AgentID; - use.CircuitCode.SessionID = Network.SessionID; - - // Start the ACK timer - AckTimer.Start(); - - // Send the initial packet out - SendPacket(use, true); - - ConnectedEvent.Reset(); - ConnectedEvent.WaitOne(Client.Settings.SIMULATOR_TIMEOUT, false); - } - catch (Exception e) - { - Client.Log(e.ToString(), Helpers.LogLevel.Error); - } - } - - /// - /// Disconnect a Simulator - /// - public void Disconnect() - { - if (connected) - { - connected = false; - AckTimer.Stop(); - - // Send the CloseCircuit notice - CloseCircuitPacket close = new CloseCircuitPacket(); - - if (Connection.Connected) - { - try - { - Connection.Send(close.ToBytes()); - } - catch (SocketException) - { - // There's a high probability of this failing if the network is - // disconnecting, so don't even bother logging the error - } - } - - try - { - // Shut the socket communication down - Connection.Shutdown(SocketShutdown.Both); - } - catch (SocketException) - { - } - } - } - - /// - /// Sends a packet - /// - /// Packet to be sent - /// Increment sequence number? - public void SendPacket(Packet packet, bool incrementSequence) - { - byte[] buffer; - int bytes; - - if (!connected && packet.Type != PacketType.UseCircuitCode) - { - Client.Log("Trying to send a " + packet.Type.ToString() + " packet when the socket is closed", - Helpers.LogLevel.Info); - - return; - } - - if (packet.Header.AckList.Length > 0) - { - // Scrub any appended ACKs since all of the ACK handling is done here - packet.Header.AckList = new uint[0]; - } - packet.Header.AppendedAcks = false; - - // Keep track of when this packet was sent out - packet.TickCount = Environment.TickCount; - - if (incrementSequence) - { - // Set the sequence number - lock (SequenceLock) - { - if (Sequence > Client.Settings.MAX_SEQUENCE) - Sequence = 1; - else - Sequence++; - packet.Header.Sequence = Sequence; - } - - if (packet.Header.Reliable) - { - lock (NeedAck) - { - if (!NeedAck.ContainsKey(packet.Header.Sequence)) - { - NeedAck.Add(packet.Header.Sequence, packet); - } - else - { - Client.Log("Attempted to add a duplicate sequence number (" + - packet.Header.Sequence + ") to the NeedAck dictionary for packet type " + - packet.Type.ToString(), Helpers.LogLevel.Warning); - } - } - - // Don't append ACKs to resent packets, in case that's what was causing the - // delivery to fail - if (!packet.Header.Resent) - { - // Append any ACKs that need to be sent out to this packet - lock (PendingAcks) - { - if (PendingAcks.Count > 0 && PendingAcks.Count < Client.Settings.MAX_APPENDED_ACKS && - packet.Type != PacketType.PacketAck && - packet.Type != PacketType.LogoutRequest) - { - packet.Header.AckList = new uint[PendingAcks.Count]; - - int i = 0; - - foreach (uint ack in PendingAcks.Values) - { - packet.Header.AckList[i] = ack; - i++; - } - - PendingAcks.Clear(); - packet.Header.AppendedAcks = true; - } - } - } - } - } - - // Serialize the packet - buffer = packet.ToBytes(); - bytes = buffer.Length; - - try - { - // Zerocode if needed - if (packet.Header.Zerocoded) - { - lock (ZeroOutBuffer) - { - bytes = Helpers.ZeroEncode(buffer, bytes, ZeroOutBuffer); - Connection.Send(ZeroOutBuffer, bytes, SocketFlags.None); - } - } - else - { - Connection.Send(buffer, bytes, SocketFlags.None); - } - } - catch (SocketException) - { - Client.Log("Tried to send a " + packet.Type.ToString() + " on a closed socket", - Helpers.LogLevel.Warning); - - Disconnect(); - } - } - - /// - /// Send a raw byte array payload as a packet - /// - /// The packet payload - /// Whether the second, third, and fourth bytes - /// should be modified to the current stream sequence number - public void SendPacket(byte[] payload, bool setSequence) - { - if (connected) - { - try - { - if (setSequence && payload.Length > 3) - { - lock (SequenceLock) - { - payload[1] = (byte)(Sequence >> 16); - payload[2] = (byte)(Sequence >> 8); - payload[3] = (byte)(Sequence % 256); - Sequence++; - } - } - - Connection.Send(payload, payload.Length, SocketFlags.None); - } - catch (SocketException e) - { - Client.Log(e.ToString(), Helpers.LogLevel.Error); - } - } - else - { - Client.Log("Attempted to send a " + payload.Length + " byte payload when " + - "we are disconnected", Helpers.LogLevel.Warning); - } - } - - /// - /// Returns Simulator Name as a String - /// - /// - public override string ToString() - { - return Region.Name + " (" + ipEndPoint.ToString() + ")"; - } - - /// - /// Sends out pending acknowledgements - /// - private void SendAcks() - { - lock (PendingAcks) - { - if (connected && PendingAcks.Count > 0) - { - if (PendingAcks.Count > 250) - { - // FIXME: Handle the odd case where we have too many pending ACKs queued up - Client.Log("Too many ACKs queued up!", Helpers.LogLevel.Error); - return; - } - - int i = 0; - PacketAckPacket acks = new PacketAckPacket(); - acks.Packets = new PacketAckPacket.PacketsBlock[PendingAcks.Count]; - - foreach (uint ack in PendingAcks.Values) - { - acks.Packets[i] = new PacketAckPacket.PacketsBlock(); - acks.Packets[i].ID = ack; - i++; - } - - acks.Header.Reliable = false; - SendPacket(acks, true); - - PendingAcks.Clear(); - } - } - } - /// - /// Resend unacknowledged packets - /// - private void ResendUnacked() - { - if (connected) - { - int now = Environment.TickCount; - - lock (NeedAck) - { - foreach (Packet packet in NeedAck.Values) - { - if (now - packet.TickCount > Client.Settings.RESEND_TIMEOUT) - { - Client.Log("Resending " + packet.Type.ToString() + " packet (" + packet.Header.Sequence + - "), " + (now - packet.TickCount) + "ms have passed", Helpers.LogLevel.Info); - - packet.Header.Resent = true; - SendPacket(packet, false); - } - } - } - } - } - /// - /// Callback handler for incomming data - /// - /// - private void OnReceivedData(IAsyncResult result) - { - Packet packet = null; - int numBytes; - - // If we're receiving data the sim connection is open - connected = true; - ConnectedEvent.Set(); - - // Update the disconnect flag so this sim doesn't time out - DisconnectCandidate = false; - - lock (RecvBuffer) - { - // Retrieve the incoming packet - try - { - numBytes = Connection.EndReceiveFrom(result, ref endPoint); - - int packetEnd = numBytes - 1; - packet = Packet.BuildPacket(RecvBuffer, ref packetEnd, ZeroBuffer); - - Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); - } - catch (SocketException) - { - Client.Log(endPoint.ToString() + " socket is closed, shutting down " + this.Region.Name, - Helpers.LogLevel.Info); - - connected = false; - Network.DisconnectSim(this); - return; - } - } - - // Fail-safe check - if (packet == null) - { - Client.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning); - return; - } - - // Track the sequence number for this packet if it's marked as reliable - if (packet.Header.Reliable) - { - if (PendingAcks.Count > Client.Settings.MAX_PENDING_ACKS) - { - SendAcks(); - } - - // Check if we already received this packet - if (Inbox.Contains(packet.Header.Sequence)) - { - Client.Log("Received a duplicate " + packet.Type.ToString() + ", sequence=" + - packet.Header.Sequence + ", resent=" + ((packet.Header.Resent) ? "Yes" : "No") + - ", Inbox.Count=" + Inbox.Count + ", NeedAck.Count=" + NeedAck.Count, - Helpers.LogLevel.Info); - - // Send an ACK for this packet immediately - //SendAck(packet.Header.Sequence); - - // TESTING: Try just queuing up ACKs for resent packets instead of immediately triggering an ACK - lock (PendingAcks) - { - uint sequence = (uint)packet.Header.Sequence; - if (!PendingAcks.ContainsKey(sequence)) { PendingAcks[sequence] = sequence; } - } - - // Avoid firing a callback twice for the same packet - return; - } - else - { - lock (PendingAcks) - { - uint sequence = (uint)packet.Header.Sequence; - if (!PendingAcks.ContainsKey(sequence)) { PendingAcks[sequence] = sequence; } - } - } - } - - // Add this packet to our inbox - lock (Inbox) - { - while (Inbox.Count >= Client.Settings.INBOX_SIZE) - { - Inbox.Dequeue(); - Inbox.Dequeue(); - } - Inbox.Enqueue(packet.Header.Sequence); - } - - // Handle appended ACKs - if (packet.Header.AppendedAcks) - { - lock (NeedAck) - { - foreach (uint ack in packet.Header.AckList) - { - NeedAck.Remove(ack); - } - } - } - - // Handle PacketAck packets - if (packet.Type == PacketType.PacketAck) - { - PacketAckPacket ackPacket = (PacketAckPacket)packet; - - lock (NeedAck) - { - foreach (PacketAckPacket.PacketsBlock block in ackPacket.Packets) - { - NeedAck.Remove(block.ID); - } - } - } - - - // Fire the registered packet events - #region FireCallbacks - if (Callbacks.ContainsKey(packet.Type)) - { - List callbackArray = Callbacks[packet.Type]; - - // Fire any registered callbacks - foreach (NetworkManager.PacketCallback callback in callbackArray) - { - if (callback != null) - { - try - { - callback(packet, this); - } - catch (Exception e) - { - Client.Log("Caught an exception in a packet callback: " + e.ToString(), - Helpers.LogLevel.Error); - } - } - } - } - - if (Callbacks.ContainsKey(PacketType.Default)) - { - List callbackArray = Callbacks[PacketType.Default]; - - // Fire any registered callbacks - foreach (NetworkManager.PacketCallback callback in callbackArray) - { - if (callback != null) - { - try - { - callback(packet, this); - } - catch (Exception e) - { - Client.Log("Caught an exception in a packet callback: " + e.ToString(), - Helpers.LogLevel.Error); - } - } - } - } - #endregion FireCallbacks - } - - private void AckTimer_Elapsed(object sender, ElapsedEventArgs ea) - { - if (connected) - { - SendAcks(); - ResendUnacked(); - } - } - } - /// /// NetworkManager is responsible for managing the network layer of /// libsecondlife. It tracks all the server connections, serializes @@ -879,8 +320,10 @@ namespace libsecondlife /// public bool Connected { get { return connected; } } + /// + internal Dictionary> Callbacks = new Dictionary>(); + private SecondLife Client; - private Dictionary> Callbacks = new Dictionary>(); private List Simulators = new List(); private System.Timers.Timer DisconnectTimer; private System.Timers.Timer LogoutTimer; @@ -985,7 +428,7 @@ namespace libsecondlife /// Simulator to send the packet to public void SendPacket(Packet packet, Simulator simulator) { - if (simulator != null && simulator.Connected) + if (simulator != null) simulator.SendPacket(packet, true); } @@ -997,7 +440,7 @@ namespace libsecondlife /// bytes of the payload to the current sequence number public void SendPacket(byte[] payload, bool setSequence) { - if (connected && CurrentSim != null) + if (CurrentSim != null) CurrentSim.SendPacket(payload, setSequence); } @@ -1010,7 +453,7 @@ namespace libsecondlife /// bytes of the payload to the current sequence number public void SendPacket(byte[] payload, Simulator simulator, bool setSequence) { - if (connected && simulator != null) + if (simulator != null) simulator.SendPacket(payload, setSequence); } @@ -1150,6 +593,7 @@ namespace libsecondlife /// 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. /// @@ -1417,70 +861,26 @@ namespace libsecondlife Client.Self.HomeLookAt = lookatVector; // Get Inventory Root Folder - Client.Log("Pulling root folder UUID from login data.", Helpers.LogLevel.Debug); ArrayList alInventoryRoot = (ArrayList)LoginValues["inventory-root"]; Hashtable htInventoryRoot = (Hashtable)alInventoryRoot[0]; Client.Self.InventoryRootFolderUUID = new LLUUID((string)htInventoryRoot["folder_id"]); - // Connect to the sim given in the login reply - Simulator simulator = new Simulator(Client, this.Callbacks, (uint)(int)LoginValues["circuit_code"], - IPAddress.Parse((string)LoginValues["sim_ip"]), (int)LoginValues["sim_port"]); - if (!simulator.Connected) + if (Connect(IPAddress.Parse((string)LoginValues["sim_ip"]), (ushort)(int)LoginValues["sim_port"], + (uint)(int)LoginValues["circuit_code"], true, (string)LoginValues["seed_capability"]) == null) { LoginError = "Unable to connect to the simulator"; return false; } - Simulator oldSim = CurrentSim; - CurrentSim = simulator; - - // Simulator is successfully connected, add it to the list and set it as default - Simulators.Add(simulator); - - // Mark that we are now officially connected to the grid - connected = true; - - // Start a timer that checks if we've been disconnected - DisconnectTimer.Start(); - - if (LoginValues.ContainsKey("seed_capability") && (string)LoginValues["seed_capability"] != "") - { - CurrentCaps = new Caps(Client, simulator.Region, (string)LoginValues["seed_capability"], EventQueueCallbacks); - } - - // Move our agent in to the sim to complete the connection - Client.Self.CompleteAgentMovement(simulator); - // 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("Caught an exception in the OnConnected() callback: " + e.ToString(), - Helpers.LogLevel.Error); - } - } - - // Fire an event that the current simulator has changed - if (OnCurrentSimChanged != null) - { - try - { - OnCurrentSimChanged(oldSim); - } - catch (Exception e) - { - Client.Log("Caught an exception in OnCurrentSimChanged(): " + e.ToString(), - Helpers.LogLevel.Error); - } + try { OnConnected(this.Client); } + catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } return true; @@ -1503,7 +903,7 @@ namespace libsecondlife /// A Simulator object on success, otherwise null public Simulator Connect(IPAddress ip, ushort port, uint circuitCode, bool setDefault, string seedcaps) { - Simulator simulator = new Simulator(Client, this.Callbacks, circuitCode, ip, (int)port); + Simulator simulator = new Simulator(Client, circuitCode, ip, (int)port, setDefault); if (!simulator.Connected) { @@ -1511,10 +911,7 @@ namespace libsecondlife return null; } - lock (Simulators) - { - Simulators.Add(simulator); - } + lock (Simulators) Simulators.Add(simulator); // Mark that we are connected to the grid (in case we weren't before) connected = true; @@ -1526,11 +923,18 @@ namespace libsecondlife { Simulator oldSim = CurrentSim; CurrentSim = simulator; + if (CurrentCaps != null) CurrentCaps.Disconnect(); CurrentCaps = null; - if (seedcaps != null && seedcaps != "") - CurrentCaps = new Caps(Client, simulator.Region, seedcaps, EventQueueCallbacks); - if (OnCurrentSimChanged != null && simulator != oldSim) OnCurrentSimChanged(oldSim); + + 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; @@ -1617,10 +1021,7 @@ namespace libsecondlife } } - lock (Simulators) - { - Simulators.Remove(sim); - } + lock (Simulators) Simulators.Remove(sim); } else { @@ -1723,7 +1124,7 @@ namespace libsecondlife // Request the economy data SendPacket(new EconomyDataRequestPacket()); - // FIXME: A movement class should be handling this + // 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); @@ -1758,7 +1159,7 @@ namespace libsecondlife if (CurrentSim.DisconnectCandidate) { Client.Log("Network timeout for the current simulator (" + - CurrentSim.Region.Name + "), logging out", Helpers.LogLevel.Warning); + CurrentSim.ToString() + "), logging out", Helpers.LogLevel.Warning); DisconnectTimer.Stop(); connected = false; @@ -1815,7 +1216,7 @@ namespace libsecondlife { // This sim hasn't received any network traffic since the // timer last elapsed, consider it disconnected - Client.Log("Network timeout for simulator " + sim.Region.Name + + Client.Log("Network timeout for simulator " + sim.ToString() + ", disconnecting", Helpers.LogLevel.Warning); DisconnectSim(sim); @@ -1868,11 +1269,43 @@ namespace libsecondlife // TODO: We can use OldestUnacked to correct transmission errors - SendPacket((Packet)ping, simulator); + 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; @@ -1880,37 +1313,9 @@ namespace libsecondlife reply.RegionInfo.Flags = 0; SendPacket(reply, simulator); - RegionHandshakePacket handshake = (RegionHandshakePacket)packet; - - simulator.Region.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.Region.IsEstateManager = handshake.RegionInfo.IsEstateManager; - simulator.Region.Name = Helpers.FieldToString(handshake.RegionInfo.SimName); - simulator.Region.SimOwner = handshake.RegionInfo.SimOwner; - simulator.Region.TerrainBase0 = handshake.RegionInfo.TerrainBase0; - simulator.Region.TerrainBase1 = handshake.RegionInfo.TerrainBase1; - simulator.Region.TerrainBase2 = handshake.RegionInfo.TerrainBase2; - simulator.Region.TerrainBase3 = handshake.RegionInfo.TerrainBase3; - simulator.Region.TerrainDetail0 = handshake.RegionInfo.TerrainDetail0; - simulator.Region.TerrainDetail1 = handshake.RegionInfo.TerrainDetail1; - simulator.Region.TerrainDetail2 = handshake.RegionInfo.TerrainDetail2; - simulator.Region.TerrainDetail3 = handshake.RegionInfo.TerrainDetail3; - simulator.Region.TerrainHeightRange00 = handshake.RegionInfo.TerrainHeightRange00; - simulator.Region.TerrainHeightRange01 = handshake.RegionInfo.TerrainHeightRange01; - simulator.Region.TerrainHeightRange10 = handshake.RegionInfo.TerrainHeightRange10; - simulator.Region.TerrainHeightRange11 = handshake.RegionInfo.TerrainHeightRange11; - simulator.Region.TerrainStartHeight00 = handshake.RegionInfo.TerrainStartHeight00; - simulator.Region.TerrainStartHeight01 = handshake.RegionInfo.TerrainStartHeight01; - simulator.Region.TerrainStartHeight10 = handshake.RegionInfo.TerrainStartHeight10; - simulator.Region.TerrainStartHeight11 = handshake.RegionInfo.TerrainStartHeight11; - simulator.Region.WaterHeight = handshake.RegionInfo.WaterHeight; - - Client.Log("Received a region handshake for " + simulator.Region.Name, Helpers.LogLevel.Info); + // We're officially connected to this sim + simulator.connected = true; + simulator.ConnectedEvent.Set(); } private void ParcelOverlayHandler(Packet packet, Simulator simulator) @@ -1919,11 +1324,11 @@ namespace libsecondlife if (overlay.ParcelData.SequenceID >= 0 && overlay.ParcelData.SequenceID <= 3) { - Array.Copy(overlay.ParcelData.Data, 0, simulator.Region.ParcelOverlay, + Array.Copy(overlay.ParcelData.Data, 0, simulator.ParcelOverlay, overlay.ParcelData.SequenceID * 1024, 1024); - simulator.Region.ParcelOverlaysReceived++; + simulator.ParcelOverlaysReceived++; - if (simulator.Region.ParcelOverlaysReceived > 3) + if (simulator.ParcelOverlaysReceived > 3) { // TODO: ParcelOverlaysReceived should become internal, and reset to zero every // time it hits four. Also need a callback here @@ -1932,7 +1337,7 @@ namespace libsecondlife else { Client.Log("Parcel overlay with sequence ID of " + overlay.ParcelData.SequenceID + - " received from " + simulator.Region.Name, Helpers.LogLevel.Warning); + " received from " + simulator.ToString(), Helpers.LogLevel.Warning); } } diff --git a/libsecondlife-cs/ObjectManager.cs b/libsecondlife-cs/ObjectManager.cs index fef43588..ac1781a2 100644 --- a/libsecondlife-cs/ObjectManager.cs +++ b/libsecondlife-cs/ObjectManager.cs @@ -1290,7 +1290,7 @@ namespace libsecondlife // Set this avatar online and in a region avatar.Online = true; - avatar.CurrentRegion = simulator.Region; + avatar.CurrentSim = simulator; // Textures avatar.Textures = new Primitive.TextureEntry(block.TextureEntry, 0, block.TextureEntry.Length); diff --git a/libsecondlife-cs/Region.cs b/libsecondlife-cs/Region.cs deleted file mode 100644 index be5a35fe..00000000 --- a/libsecondlife-cs/Region.cs +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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.Collections.Generic; -using libsecondlife.Packets; - -namespace libsecondlife -{ - /// - /// Represents a region (also known as a sim) in Second Life. - /// - public class Region - { - /// - public LLUUID ID = LLUUID.Zero; - /// - public ulong Handle; - /// - public string Name = ""; - /// - public byte[] ParcelOverlay = new byte[4096]; - /// - public int ParcelOverlaysReceived; - /// - public float TerrainHeightRange00; - /// - public float TerrainHeightRange01; - /// - public float TerrainHeightRange10; - /// - public float TerrainHeightRange11; - /// - public float TerrainStartHeight00; - /// - public float TerrainStartHeight01; - /// - public float TerrainStartHeight10; - /// - public float TerrainStartHeight11; - /// - public float WaterHeight; - /// - public LLUUID SimOwner = LLUUID.Zero; - /// - public LLUUID TerrainBase0 = LLUUID.Zero; - /// - public LLUUID TerrainBase1 = LLUUID.Zero; - /// - public LLUUID TerrainBase2 = LLUUID.Zero; - /// - public LLUUID TerrainBase3 = LLUUID.Zero; - /// - public LLUUID TerrainDetail0 = LLUUID.Zero; - /// - public LLUUID TerrainDetail1 = LLUUID.Zero; - /// - public LLUUID TerrainDetail2 = LLUUID.Zero; - /// - public LLUUID TerrainDetail3 = LLUUID.Zero; - /// - public bool IsEstateManager; - /// - public EstateTools Estate; - - /// - /// This may cause your code to block while the GridRegion data is fetched for the first time - private GridRegion _GridRegionData = null; - public GridRegion GridRegionData - { - get - { - if (_GridRegionData == null) - { - if ((Name != null) && (!Name.Equals(""))) - { - _GridRegionData = Client.Grid.GetGridRegion(Client.Network.CurrentSim.Region.Name); - } - } - return _GridRegionData; - } - } - - private SecondLife Client; - - /// - /// - /// - /// - public Region(SecondLife client) - { - Estate = new EstateTools(client); - Client = client; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public Region(SecondLife client, LLUUID id, ulong handle, string name, float[] heightList, - LLUUID simOwner, LLUUID[] terrainImages, bool isEstateManager) - { - Client = client; - Estate = new EstateTools(client); - ID = id; - Handle = handle; - Name = name; - ParcelOverlay = new byte[4096]; - - TerrainHeightRange00 = heightList[0]; - TerrainHeightRange01 = heightList[1]; - TerrainHeightRange10 = heightList[2]; - TerrainHeightRange11 = heightList[3]; - TerrainStartHeight00 = heightList[4]; - TerrainStartHeight01 = heightList[5]; - TerrainStartHeight10 = heightList[6]; - TerrainStartHeight11 = heightList[7]; - WaterHeight = heightList[8]; - - SimOwner = simOwner; - - TerrainBase0 = terrainImages[0]; - TerrainBase1 = terrainImages[1]; - TerrainBase2 = terrainImages[2]; - TerrainBase3 = terrainImages[3]; - TerrainDetail0 = terrainImages[4]; - TerrainDetail1 = terrainImages[5]; - TerrainDetail2 = terrainImages[6]; - TerrainDetail3 = terrainImages[7]; - - IsEstateManager = isEstateManager; - } - - /// - /// - /// - /// - /// - /// - /// - public void ParcelSubdivide(float west, float south, float east, float north) - { - ParcelDividePacket divide = new ParcelDividePacket(); - divide.AgentData.AgentID = Client.Network.AgentID; - divide.AgentData.SessionID = Client.Network.SessionID; - divide.ParcelData.East = east; - divide.ParcelData.North = north; - divide.ParcelData.South = south; - divide.ParcelData.West = west; - - // FIXME: Region needs a reference to it's parent Simulator - //Client.Network.SendPacket((Packet)divide, this.Simulator); - } - - /// - /// - /// - /// - /// - /// - /// - public void ParcelJoin(float west, float south, float east, float north) - { - ParcelJoinPacket join = new ParcelJoinPacket(); - join.AgentData.AgentID = Client.Network.AgentID; - join.AgentData.SessionID = Client.Network.SessionID; - join.ParcelData.East = east; - join.ParcelData.North = north; - join.ParcelData.South = south; - join.ParcelData.West = west; - - // FIXME: Region needs a reference to it's parent Simulator - //Client.Network.SendPacket((Packet)join, this.Simulator); - } - - /// - /// - /// - /// - public override int GetHashCode() - { - return ID.GetHashCode(); - } - } -} diff --git a/libsecondlife-cs/Simulator.cs b/libsecondlife-cs/Simulator.cs new file mode 100644 index 00000000..184daee4 --- /dev/null +++ b/libsecondlife-cs/Simulator.cs @@ -0,0 +1,680 @@ +/* + * Copyright (c) 2007, 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.Collections.Generic; +using System.Threading; +using System.Net; +using System.Net.Sockets; +using libsecondlife.Packets; + +namespace libsecondlife +{ + /// + /// Simulator is a wrapper for a network connection to a simulator and the + /// Region class representing the block of land in the metaverse + /// + public class Simulator + { + /// A public reference to the client that this Simulator object + /// is attached to + public SecondLife Client; + /// + public LLUUID ID = LLUUID.Zero; + /// + public ulong Handle; + /// + public string Name = String.Empty; + /// + public byte[] ParcelOverlay = new byte[4096]; + /// + public int ParcelOverlaysReceived; + /// + public float TerrainHeightRange00; + /// + public float TerrainHeightRange01; + /// + public float TerrainHeightRange10; + /// + public float TerrainHeightRange11; + /// + public float TerrainStartHeight00; + /// + public float TerrainStartHeight01; + /// + public float TerrainStartHeight10; + /// + public float TerrainStartHeight11; + /// + public float WaterHeight; + /// + public LLUUID SimOwner = LLUUID.Zero; + /// + public LLUUID TerrainBase0 = LLUUID.Zero; + /// + public LLUUID TerrainBase1 = LLUUID.Zero; + /// + public LLUUID TerrainBase2 = LLUUID.Zero; + /// + public LLUUID TerrainBase3 = LLUUID.Zero; + /// + public LLUUID TerrainDetail0 = LLUUID.Zero; + /// + public LLUUID TerrainDetail1 = LLUUID.Zero; + /// + public LLUUID TerrainDetail2 = LLUUID.Zero; + /// + public LLUUID TerrainDetail3 = LLUUID.Zero; + /// + public bool IsEstateManager; + /// + public EstateTools Estate; + /// Current time dilation of this simulator + public float Dilation; + + /// + /// The ID number associated with this particular connection to the + /// simulator, used to emulate TCP connections. This is used + /// internally for packets that have a CircuitCode field + /// + public uint CircuitCode + { + get { return circuitCode; } + set { circuitCode = value; } + } + /// The IP address and port of the server + public IPEndPoint IPEndPoint { get { return ipEndPoint; } } + /// Whether there is a working connection to the simulator or + /// not + public bool Connected { get { return connected; } } + + /// Used internally to track sim disconnections + internal bool DisconnectCandidate = false; + /// + internal ManualResetEvent ConnectedEvent = new ManualResetEvent(false); + /// + internal bool connected; + + private NetworkManager Network; + private uint Sequence = 0; + private object SequenceLock = new object(); + private byte[] RecvBuffer = new byte[4096]; + private byte[] ZeroBuffer = new byte[8192]; + private byte[] ZeroOutBuffer = new byte[4096]; + private Socket Connection = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + private AsyncCallback ReceivedData; + // Packets we sent out that need ACKs from the simulator + private Dictionary NeedAck = new Dictionary(); + // Sequence numbers of packets we've received from the simulator + private Queue Inbox; + // ACKs that are queued up to be sent to the simulator + private Dictionary PendingAcks = new Dictionary(); + private uint circuitCode; + private IPEndPoint ipEndPoint; + private EndPoint endPoint; + private System.Timers.Timer AckTimer; + + + /// + /// Default constructor + /// + /// Reference to the SecondLife client + /// Integer to uniquely identify the connection to this simulator + /// IP address of the simulator + /// Port on the simulator to connect to + /// Whether to move our agent in to this sim or not + public Simulator(SecondLife client, uint circuit, IPAddress ip, int port, bool moveToSim) + { + Client = client; + Estate = new EstateTools(Client); + Network = client.Network; + circuitCode = circuit; + Inbox = new Queue(Client.Settings.INBOX_SIZE); + AckTimer = new System.Timers.Timer(Client.Settings.NETWORK_TICK_LENGTH); + AckTimer.Elapsed += new System.Timers.ElapsedEventHandler(AckTimer_Elapsed); + + // Initialize the callback for receiving a new packet + ReceivedData = new AsyncCallback(OnReceivedData); + + Client.Log("Connecting to " + ip.ToString() + ":" + port, Helpers.LogLevel.Info); + + try + { + // Create an endpoint that we will be communicating with (need it in two + // types due to .NET weirdness) + ipEndPoint = new IPEndPoint(ip, port); + endPoint = (EndPoint)ipEndPoint; + + // Associate this simulator's socket with the given ip/port and start listening + Connection.Connect(endPoint); + Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); + + // Send the UseCircuitCode packet to initiate the connection + UseCircuitCodePacket use = new UseCircuitCodePacket(); + use.CircuitCode.Code = circuitCode; + use.CircuitCode.ID = Network.AgentID; + use.CircuitCode.SessionID = Network.SessionID; + + // Start the ACK timer + AckTimer.Start(); + + // Send the initial packet out + SendPacket(use, true); + + // Move our agent in to the sim to complete the connection + if (moveToSim) Client.Self.CompleteAgentMovement(this); + + ConnectedEvent.Reset(); + ConnectedEvent.WaitOne(Client.Settings.SIMULATOR_TIMEOUT, false); + } + catch (Exception e) + { + Client.Log(e.ToString(), Helpers.LogLevel.Error); + } + } + + /// + /// Disconnect a Simulator + /// + public void Disconnect() + { + if (connected) + { + connected = false; + AckTimer.Stop(); + + // Send the CloseCircuit notice + CloseCircuitPacket close = new CloseCircuitPacket(); + + if (Connection.Connected) + { + try + { + Connection.Send(close.ToBytes()); + } + catch (SocketException) + { + // There's a high probability of this failing if the network is + // disconnecting, so don't even bother logging the error + } + } + + try + { + // Shut the socket communication down + Connection.Shutdown(SocketShutdown.Both); + } + catch (SocketException) + { + } + } + } + + /// + /// Sends a packet + /// + /// Packet to be sent + /// Increment sequence number? + public void SendPacket(Packet packet, bool incrementSequence) + { + byte[] buffer; + int bytes; + + if (packet.Header.AckList.Length > 0) + { + // Scrub any appended ACKs since all of the ACK handling is done here + packet.Header.AckList = new uint[0]; + } + packet.Header.AppendedAcks = false; + + // Keep track of when this packet was sent out + packet.TickCount = Environment.TickCount; + + if (incrementSequence) + { + // Set the sequence number + lock (SequenceLock) + { + if (Sequence > Client.Settings.MAX_SEQUENCE) + Sequence = 1; + else + Sequence++; + packet.Header.Sequence = Sequence; + } + + if (packet.Header.Reliable) + { + lock (NeedAck) + { + if (!NeedAck.ContainsKey(packet.Header.Sequence)) + { + NeedAck.Add(packet.Header.Sequence, packet); + } + else + { + Client.Log("Attempted to add a duplicate sequence number (" + + packet.Header.Sequence + ") to the NeedAck dictionary for packet type " + + packet.Type.ToString(), Helpers.LogLevel.Warning); + } + } + + // Don't append ACKs to resent packets, in case that's what was causing the + // delivery to fail + if (!packet.Header.Resent) + { + // Append any ACKs that need to be sent out to this packet + lock (PendingAcks) + { + if (PendingAcks.Count > 0 && PendingAcks.Count < Client.Settings.MAX_APPENDED_ACKS && + packet.Type != PacketType.PacketAck && + packet.Type != PacketType.LogoutRequest) + { + packet.Header.AckList = new uint[PendingAcks.Count]; + + int i = 0; + + foreach (uint ack in PendingAcks.Values) + { + packet.Header.AckList[i] = ack; + i++; + } + + PendingAcks.Clear(); + packet.Header.AppendedAcks = true; + } + } + } + } + } + + // Serialize the packet + buffer = packet.ToBytes(); + bytes = buffer.Length; + + try + { + // Zerocode if needed + if (packet.Header.Zerocoded) + { + lock (ZeroOutBuffer) + { + bytes = Helpers.ZeroEncode(buffer, bytes, ZeroOutBuffer); + Connection.Send(ZeroOutBuffer, bytes, SocketFlags.None); + } + } + else + { + Connection.Send(buffer, bytes, SocketFlags.None); + } + } + catch (SocketException) + { + Client.Log("Tried to send a " + packet.Type.ToString() + " on a closed socket, shutting down " + + this.ToString(), Helpers.LogLevel.Info); + + Network.DisconnectSim(this); + return; + } + } + + /// + /// Send a raw byte array payload as a packet + /// + /// The packet payload + /// Whether the second, third, and fourth bytes + /// should be modified to the current stream sequence number + public void SendPacket(byte[] payload, bool setSequence) + { + try + { + if (setSequence && payload.Length > 3) + { + lock (SequenceLock) + { + payload[1] = (byte)(Sequence >> 16); + payload[2] = (byte)(Sequence >> 8); + payload[3] = (byte)(Sequence % 256); + Sequence++; + } + } + + Connection.Send(payload, payload.Length, SocketFlags.None); + } + catch (SocketException) + { + Client.Log("Tried to send a " + payload.Length + " byte payload on a closed socket, shutting down " + + this.ToString(), Helpers.LogLevel.Info); + + Network.DisconnectSim(this); + return; + } + } + + /// + /// + /// + /// + /// + /// + /// + public void ParcelSubdivide(float west, float south, float east, float north) + { + ParcelDividePacket divide = new ParcelDividePacket(); + divide.AgentData.AgentID = Client.Network.AgentID; + divide.AgentData.SessionID = Client.Network.SessionID; + divide.ParcelData.East = east; + divide.ParcelData.North = north; + divide.ParcelData.South = south; + divide.ParcelData.West = west; + + SendPacket(divide, true); + } + + /// + /// + /// + /// + /// + /// + /// + public void ParcelJoin(float west, float south, float east, float north) + { + ParcelJoinPacket join = new ParcelJoinPacket(); + join.AgentData.AgentID = Client.Network.AgentID; + join.AgentData.SessionID = Client.Network.SessionID; + join.ParcelData.East = east; + join.ParcelData.North = north; + join.ParcelData.South = south; + join.ParcelData.West = west; + + SendPacket(join, true); + } + + /// + /// Returns Simulator Name as a String + /// + /// + public override string ToString() + { + if (Name.Length > 0) + return Name + " (" + ipEndPoint.ToString() + ")"; + else + return "(" + ipEndPoint.ToString() + ")"; + } + + /// + /// + /// + /// + public override int GetHashCode() + { + return ID.GetHashCode(); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + Simulator sim = obj as Simulator; + if (sim == null) + return false; + return ID.Equals(sim.ID); + } + + /// + /// Sends out pending acknowledgements + /// + private void SendAcks() + { + lock (PendingAcks) + { + if (PendingAcks.Count > 0) + { + if (PendingAcks.Count > 250) + { + // FIXME: Handle the odd case where we have too many pending ACKs queued up + Client.Log("Too many ACKs queued up!", Helpers.LogLevel.Error); + return; + } + + int i = 0; + PacketAckPacket acks = new PacketAckPacket(); + acks.Packets = new PacketAckPacket.PacketsBlock[PendingAcks.Count]; + + foreach (uint ack in PendingAcks.Values) + { + acks.Packets[i] = new PacketAckPacket.PacketsBlock(); + acks.Packets[i].ID = ack; + i++; + } + + acks.Header.Reliable = false; + SendPacket(acks, true); + + PendingAcks.Clear(); + } + } + } + /// + /// Resend unacknowledged packets + /// + private void ResendUnacked() + { + if (connected) + { + int now = Environment.TickCount; + + lock (NeedAck) + { + foreach (Packet packet in NeedAck.Values) + { + if (now - packet.TickCount > Client.Settings.RESEND_TIMEOUT) + { + Client.Log("Resending " + packet.Type.ToString() + " packet (" + packet.Header.Sequence + + "), " + (now - packet.TickCount) + "ms have passed", Helpers.LogLevel.Info); + + packet.Header.Resent = true; + SendPacket(packet, false); + } + } + } + } + } + /// + /// Callback handler for incomming data + /// + /// + private void OnReceivedData(IAsyncResult result) + { + Packet packet = null; + int numBytes; + + // Update the disconnect flag so this sim doesn't time out + DisconnectCandidate = false; + + lock (RecvBuffer) + { + // Retrieve the incoming packet + try + { + numBytes = Connection.EndReceiveFrom(result, ref endPoint); + + int packetEnd = numBytes - 1; + packet = Packet.BuildPacket(RecvBuffer, ref packetEnd, ZeroBuffer); + + Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); + } + catch (SocketException) + { + Client.Log(endPoint.ToString() + " socket is closed, shutting down " + this.ToString(), + Helpers.LogLevel.Info); + Network.DisconnectSim(this); + return; + } + } + + // Fail-safe check + if (packet == null) + { + Client.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning); + return; + } + + // Track the sequence number for this packet if it's marked as reliable + if (packet.Header.Reliable) + { + if (PendingAcks.Count > Client.Settings.MAX_PENDING_ACKS) + { + SendAcks(); + } + + // Check if we already received this packet + if (Inbox.Contains(packet.Header.Sequence)) + { + Client.Log("Received a duplicate " + packet.Type.ToString() + ", sequence=" + + packet.Header.Sequence + ", resent=" + ((packet.Header.Resent) ? "Yes" : "No") + + ", Inbox.Count=" + Inbox.Count + ", NeedAck.Count=" + NeedAck.Count, + Helpers.LogLevel.Info); + + // Send an ACK for this packet immediately + //SendAck(packet.Header.Sequence); + + // TESTING: Try just queuing up ACKs for resent packets instead of immediately triggering an ACK + lock (PendingAcks) + { + uint sequence = (uint)packet.Header.Sequence; + if (!PendingAcks.ContainsKey(sequence)) { PendingAcks[sequence] = sequence; } + } + + // Avoid firing a callback twice for the same packet + return; + } + else + { + lock (PendingAcks) + { + uint sequence = (uint)packet.Header.Sequence; + if (!PendingAcks.ContainsKey(sequence)) { PendingAcks[sequence] = sequence; } + } + } + } + + // Add this packet to our inbox + lock (Inbox) + { + while (Inbox.Count >= Client.Settings.INBOX_SIZE) + { + Inbox.Dequeue(); + Inbox.Dequeue(); + } + Inbox.Enqueue(packet.Header.Sequence); + } + + // Handle appended ACKs + if (packet.Header.AppendedAcks) + { + lock (NeedAck) + { + foreach (uint ack in packet.Header.AckList) + { + NeedAck.Remove(ack); + } + } + } + + // Handle PacketAck packets + if (packet.Type == PacketType.PacketAck) + { + PacketAckPacket ackPacket = (PacketAckPacket)packet; + + lock (NeedAck) + { + foreach (PacketAckPacket.PacketsBlock block in ackPacket.Packets) + { + NeedAck.Remove(block.ID); + } + } + } + + + // Fire the registered packet events + #region FireCallbacks + if (Network.Callbacks.ContainsKey(packet.Type)) + { + List callbackArray = Network.Callbacks[packet.Type]; + + // Fire any registered callbacks + foreach (NetworkManager.PacketCallback callback in callbackArray) + { + if (callback != null) + { + try + { + callback(packet, this); + } + catch (Exception e) + { + Client.Log("Caught an exception in a packet callback: " + e.ToString(), + Helpers.LogLevel.Error); + } + } + } + } + + if (Network.Callbacks.ContainsKey(PacketType.Default)) + { + List callbackArray = Network.Callbacks[PacketType.Default]; + + // Fire any registered callbacks + foreach (NetworkManager.PacketCallback callback in callbackArray) + { + if (callback != null) + { + try + { + callback(packet, this); + } + catch (Exception e) + { + Client.Log("Caught an exception in a packet callback: " + e.ToString(), + Helpers.LogLevel.Error); + } + } + } + } + #endregion FireCallbacks + } + + private void AckTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs ea) + { + SendAcks(); + ResendUnacked(); + } + } +} diff --git a/libsecondlife-cs/examples/AnimationSample/AnimationSample.cs b/libsecondlife-cs/examples/AnimationSample/AnimationSample.cs index e512cf00..1340bf68 100644 --- a/libsecondlife-cs/examples/AnimationSample/AnimationSample.cs +++ b/libsecondlife-cs/examples/AnimationSample/AnimationSample.cs @@ -51,15 +51,18 @@ namespace AnimationSample private void btnLogin_Click(object sender, EventArgs e) { // Login - if (!client.Network.Login(txtFirst.Text, txtLast.Text, txtPassword.Text, "animationsample", "jessemalthus@gmail.com")) + if (!client.Network.Login(txtFirst.Text, txtLast.Text, txtPassword.Text, "animationsample", + "jessemalthus@gmail.com")) { // Login failed - MessageBox.Show("We're sorry, but login failed. Error: \n " + client.Network.LoginError); + MessageBox.Show("We're sorry, but login failed. Error: " + Environment.NewLine + + client.Network.LoginError); } else { - MessageBox.Show("Login succeded. You're at " + client.Self.Position + " on " + client.Network.CurrentSim.Region.Name); + MessageBox.Show("Login succeded. You're at " + client.Self.Position + " on " + + client.Network.CurrentSim.ToString()); } } } diff --git a/libsecondlife-cs/examples/Heightmap/frmHeightmap.cs b/libsecondlife-cs/examples/Heightmap/frmHeightmap.cs index 9d9ad03c..e68754ca 100644 --- a/libsecondlife-cs/examples/Heightmap/frmHeightmap.cs +++ b/libsecondlife-cs/examples/Heightmap/frmHeightmap.cs @@ -116,7 +116,7 @@ namespace Heightmap int lesserVal = (int)((float)colorVal * 0.75f); Color color; - if (height >= simulator.Region.WaterHeight) + if (height >= simulator.WaterHeight) color = Color.FromArgb(lesserVal, colorVal, lesserVal); else color = Color.FromArgb(lesserVal, lesserVal, colorVal); diff --git a/libsecondlife-cs/examples/Teleport/Teleport.cs b/libsecondlife-cs/examples/Teleport/Teleport.cs index ef79f26f..d79f91e6 100644 --- a/libsecondlife-cs/examples/Teleport/Teleport.cs +++ b/libsecondlife-cs/examples/Teleport/Teleport.cs @@ -40,15 +40,9 @@ namespace Teleport if (success) { - // Get the current sim name - while (app.Client.Network.CurrentSim.Region.Name == "") - { - System.Threading.Thread.Sleep(100); - } + Console.WriteLine("Starting in " + app.Client.Network.CurrentSim.ToString()); - Console.WriteLine("Starting in " + app.Client.Network.CurrentSim.Region.Name); - - if (sim.ToLower() == app.Client.Network.CurrentSim.Region.Name.ToLower()) + if (sim.ToLower() == app.Client.Network.CurrentSim.Name.ToLower()) { Console.WriteLine("TODO: Add the ability to teleport somewhere in the local region. " + "Exiting for now, please specify a region other than the current one"); diff --git a/libsecondlife-cs/examples/TestClient/ClientManager.cs b/libsecondlife-cs/examples/TestClient/ClientManager.cs index 454e5069..01aac4d7 100644 --- a/libsecondlife-cs/examples/TestClient/ClientManager.cs +++ b/libsecondlife-cs/examples/TestClient/ClientManager.cs @@ -86,22 +86,30 @@ namespace libsecondlife.TestClient client.SimPrims = SimPrims; client.Master = account.Master; - - bool check = false; - if ( this.startpos.sim != null ) { - if ( this.startpos.x == 0 || this.startpos.y == 0 || this.startpos.z == 0 ) { + + if (this.startpos.sim != null) + { + if (this.startpos.x == 0 || this.startpos.y == 0 || this.startpos.z == 0) + { this.startpos.x = 128; this.startpos.y = 128; this.startpos.z = 1; } - string startLoc = NetworkManager.StartLocation(this.startpos.sim, this.startpos.x, this.startpos.y, this.startpos.z); + + string startLoc = NetworkManager.StartLocation(this.startpos.sim, this.startpos.x, this.startpos.y, + this.startpos.z); Console.WriteLine(startLoc); - client.Network.Login(account.FirstName, account.LastName, account.Password, "TestClient", startLoc, contactPerson, false); - } else { - client.Network.Login(account.FirstName, account.LastName, account.Password, "TestClient", contactPerson); + client.Network.Login(account.FirstName, account.LastName, account.Password, "TestClient", startLoc, + contactPerson, false); } - if ( ! check ) { - Console.WriteLine("Failed to login " + account.FirstName + " " + account.LastName + ": " + client.Network.LoginError); + else + { + if (!client.Network.Login(account.FirstName, account.LastName, account.Password, "TestClient", + contactPerson)) + { + Console.WriteLine("Failed to login " + account.FirstName + " " + account.LastName + ": " + + client.Network.LoginError); + } } if (client.Network.Connected) diff --git a/libsecondlife-cs/examples/TestClient/Commands/Movement/JumpCommand.cs b/libsecondlife-cs/examples/TestClient/Commands/Movement/JumpCommand.cs index c56dd1ce..6cead83b 100644 --- a/libsecondlife-cs/examples/TestClient/Commands/Movement/JumpCommand.cs +++ b/libsecondlife-cs/examples/TestClient/Commands/Movement/JumpCommand.cs @@ -24,7 +24,7 @@ namespace libsecondlife.TestClient Client.Self.Teleport ( - Client.Network.CurrentSim.Region.Name, + Client.Network.CurrentSim.Name, new LLVector3(Client.Self.Position.X, Client.Self.Position.Y, Client.Self.Position.Z + height) ); diff --git a/libsecondlife-cs/examples/TestClient/Commands/Movement/LocationCommand.cs b/libsecondlife-cs/examples/TestClient/Commands/Movement/LocationCommand.cs index ac7d24f1..1758a482 100644 --- a/libsecondlife-cs/examples/TestClient/Commands/Movement/LocationCommand.cs +++ b/libsecondlife-cs/examples/TestClient/Commands/Movement/LocationCommand.cs @@ -16,7 +16,8 @@ namespace libsecondlife.TestClient public override string Execute(string[] args, LLUUID fromAgentID) { - return "CurrentSim: '" + Client.Network.CurrentSim.Region.Name + "' Position: " + Client.Self.Position.ToString(); + return "CurrentSim: '" + Client.Network.CurrentSim.ToString() + "' Position: " + + Client.Self.Position.ToString(); } } } diff --git a/libsecondlife-cs/examples/TestClient/Commands/ResearchCommand.cs b/libsecondlife-cs/examples/TestClient/Commands/ResearchCommand.cs index d5e03590..e7858bec 100644 --- a/libsecondlife-cs/examples/TestClient/Commands/ResearchCommand.cs +++ b/libsecondlife-cs/examples/TestClient/Commands/ResearchCommand.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Threading; +using System.Text; +using System.IO; using libsecondlife; -//using libsecondlife.Utilities.Assets; -//using libsecondlife.Utilities.Appearance; +using libsecondlife.Utilities.Assets; +using libsecondlife.Utilities.Appearance; using libsecondlife.Packets; namespace libsecondlife.TestClient @@ -11,20 +13,71 @@ namespace libsecondlife.TestClient public class ResearchCommand : Command { private ManualResetEvent AssetReceived = new ManualResetEvent(false); - //AssetManager manager; + AssetManager manager; //AppearanceManager appearance; + List Downloaded = new List(); public ResearchCommand(TestClient testClient) { Name = "research"; Description = "Does important research for the betterment of mankind"; - //manager = new AssetManager(testClient); + testClient.Objects.OnNewAvatar += new ObjectManager.NewAvatarCallback(Objects_OnNewAvatar); + + manager = new AssetManager(testClient); + manager.OnImageReceived += new AssetManager.ImageReceivedCallback(manager_OnImageReceived); //manager.OnAssetReceived += new AssetManager.AssetReceivedCallback(manager_OnAssetReceived); //appearance = new AppearanceManager(testClient, manager); //appearance.OnAgentWearables += new AppearanceManager.AgentWearablesCallback(appearance_OnAgentWearables); } + void manager_OnImageReceived(ImageDownload image) + { + if (image.Success) + { + File.WriteAllBytes(image.ID.ToStringHyphenated() + ".j2c", image.AssetData); + Console.WriteLine("Downloaded " + image.ID.ToStringHyphenated()); + } + else + { + Console.WriteLine("Failed to download " + image.ID.ToStringHyphenated() + ", NotFound=" + image.NotFound); + } + } + + void Objects_OnNewAvatar(Simulator simulator, Avatar avatar, ulong regionHandle, ushort timeDilation) + { + StringBuilder output = new StringBuilder("Avatar "); + output.Append(avatar.ID.ToStringHyphenated()); + output.Append(" is wearing: "); + + foreach (KeyValuePair texture in avatar.Textures.FaceTextures) + { + AppearanceManager.TextureIndex textureName = (AppearanceManager.TextureIndex)texture.Key; + + switch (textureName) + { + case AppearanceManager.TextureIndex.HeadBaked: + case AppearanceManager.TextureIndex.LowerBaked: + case AppearanceManager.TextureIndex.UpperBaked: + case AppearanceManager.TextureIndex.SkirtBaked: + if (!Downloaded.Contains(texture.Value.TextureID) && + texture.Value.TextureID != AppearanceManager.DEFAULT_AVATAR_TEXTURE) + { + Downloaded.Add(texture.Value.TextureID); + //manager.RequestImage(texture.Value.TextureID, ImageType.Baked, 120000.0f, 0); + } + break; + default: + break; + } + + output.Append(textureName.ToString()); + output.Append(" "); + } + + //Console.WriteLine(output.ToString()); + } + public override string Execute(string[] args, LLUUID fromAgentID) { //Dictionary wearables = new Dictionary(); diff --git a/libsecondlife-cs/examples/TestClient/Commands/WhoCommand.cs b/libsecondlife-cs/examples/TestClient/Commands/WhoCommand.cs index 21d0d036..3edb649a 100644 --- a/libsecondlife-cs/examples/TestClient/Commands/WhoCommand.cs +++ b/libsecondlife-cs/examples/TestClient/Commands/WhoCommand.cs @@ -19,7 +19,8 @@ namespace libsecondlife.TestClient StringBuilder result = new StringBuilder(); foreach (Avatar av in Client.AvatarList.Values) { - result.AppendFormat("\n{0} {1} {2}/{3} ID: {4}", av.Name, av.GroupName, av.CurrentRegion != null ? av.CurrentRegion.Name : String.Empty, av.Position, av.ID); + result.AppendFormat("\n{0} {1} {2}/{3} ID: {4}", av.Name, av.GroupName, + (av.CurrentSim != null ? av.CurrentSim.Name : String.Empty), av.Position, av.ID); } return result.ToString(); diff --git a/libsecondlife-cs/libsecondlife.Tests/NetworkTests.cs b/libsecondlife-cs/libsecondlife.Tests/NetworkTests.cs index f3445df8..423f2acd 100644 --- a/libsecondlife-cs/libsecondlife.Tests/NetworkTests.cs +++ b/libsecondlife-cs/libsecondlife.Tests/NetworkTests.cs @@ -17,23 +17,20 @@ namespace libsecondlife.Tests ulong AhernRegionHandle = 1096213093149184; ulong MorrisRegionHandle = 1096213093149183; bool DetectedObject = false; - bool DoneTeleporting = false; - MainAvatar.TeleportStatus tpStatus = MainAvatar.TeleportStatus.None; - string tpMessage = ""; LLUUID LookupKey1 = new LLUUID("25472683cb324516904a6cd0ecabf128"); - string LookupName1 = "Bot Ringo"; + //string LookupName1 = "Bot Ringo"; public NetworkTests() { Client = new SecondLife(); - string startLoc = NetworkManager.StartLocation("hooper", 128, 128, 32); - // Register callbacks Client.Network.RegisterCallback(PacketType.ObjectUpdate, new NetworkManager.PacketCallback(ObjectUpdateHandler)); - Client.Self.OnTeleport += new MainAvatar.TeleportCallback(OnTeleportHandler); + //Client.Self.OnTeleport += new MainAvatar.TeleportCallback(OnTeleportHandler); + // Connect to the grid + string startLoc = NetworkManager.StartLocation("Ahern", 128, 128, 32); Client.Network.Login("Testing", "Anvil", "testinganvil", "Unit Test Framework", startLoc, "contact@libsecondlife.org", false); } @@ -49,16 +46,9 @@ namespace libsecondlife.Tests Assert.IsTrue(Client.Network.Connected, "Client is not connected to the grid: " + Client.Network.LoginError); int start = Environment.TickCount; - while (Client.Network.CurrentSim.Region.Name == "") - { - if (Environment.TickCount - start > 5000) - { - Assert.Fail("Timeout waiting for a RegionHandshake packet"); - } - } - //Assert.AreEqual("ahern", Client.Network.CurrentSim.Region.Name.ToLower(), "Logged in to sim " + - // Client.Network.CurrentSim.Region.Name + " instead of Ahern"); + Assert.AreEqual("ahern", Client.Network.CurrentSim.Name.ToLower(), "Logged in to sim " + + Client.Network.CurrentSim.Name + " instead of Ahern"); } [Test] @@ -92,96 +82,27 @@ namespace libsecondlife.Tests CurrentRegionHandle + " when we were expecting " + AhernRegionHandle + ", possible endian issue"); } - [Test] - public void NameLookup() - { - AvatarTracker tracker = new AvatarTracker(Client); - - string name = tracker.GetAvatarName(LookupKey1); - - Assert.IsTrue(name == LookupName1, "AvatarTracker.GetAvatarName() returned " + name + - " instead of " + LookupName1); - } - [Test] public void Teleport() { - DoneTeleporting = false; - tpStatus = MainAvatar.TeleportStatus.None; - - Client.Self.Teleport(MorrisRegionHandle, new LLVector3(128, 128, 32)); - - int start = Environment.TickCount; - - while (!DoneTeleporting) - { - System.Threading.Thread.Sleep(100); - - if (Environment.TickCount - start > 10000) - { - Assert.Fail("Timeout waiting for the first teleport to finish"); - return; - } - } - - Assert.IsTrue(tpStatus == MainAvatar.TeleportStatus.Finished, - "Teleport status is " + tpStatus.ToString() + ", message=" + tpMessage); - - // Wait for the region information to come in - start = Environment.TickCount; - while (Client.Network.CurrentSim.Region.Name == "") - { - if (Environment.TickCount - start > 5000) - { - Assert.Fail("Timeout waiting for a RegionHandshake packet"); - } - } + Assert.IsTrue(Client.Self.Teleport(MorrisRegionHandle, new LLVector3(128, 128, 32)), + "Teleport to Morris failed"); // Assert that we really did make it to our scheduled destination - Assert.AreEqual("morris", Client.Network.CurrentSim.Region.Name.ToLower(), - "Expected to teleport to Morris, ended up in " + Client.Network.CurrentSim.Region.Name + + Assert.AreEqual("morris", Client.Network.CurrentSim.Name.ToLower(), + "Expected to teleport to Morris, ended up in " + Client.Network.CurrentSim.Name + ". Possibly region full or offline?"); /////////////////////////////////////////////////////////////////// - // TODO: Add a local region teleport - /////////////////////////////////////////////////////////////////// - DoneTeleporting = false; - tpStatus = MainAvatar.TeleportStatus.None; - - Client.Self.Teleport(AhernRegionHandle, new LLVector3(128, 128, 32)); - - start = Environment.TickCount; - - while (!DoneTeleporting) - { - System.Threading.Thread.Sleep(100); - - if (Environment.TickCount - start > 10000) - { - Assert.Fail("Timeout waiting for the second teleport to finish"); - return; - } - } - - Assert.IsTrue(tpStatus == MainAvatar.TeleportStatus.Finished, "Teleport status is " + - tpStatus.ToString() + ", message=" + tpMessage); - - // Wait for the region information to come in - start = Environment.TickCount; - while (Client.Network.CurrentSim.Region.Name == "") - { - if (Environment.TickCount - start > 5000) - { - Assert.Fail("Timeout waiting for a RegionHandshake packet"); - } - } + Assert.IsTrue(Client.Self.Teleport(AhernRegionHandle, new LLVector3(128, 128, 32)), + "Teleport to Ahern failed"); // Assert that we really did make it to our scheduled destination - Assert.AreEqual("ahern", Client.Network.CurrentSim.Region.Name.ToLower(), - "Expected to teleport to Ahern, ended up in " + Client.Network.CurrentSim.Region.Name + + Assert.AreEqual("ahern", Client.Network.CurrentSim.Name.ToLower(), + "Expected to teleport to Ahern, ended up in " + Client.Network.CurrentSim.Name + ". Possibly region full or offline?"); } @@ -193,33 +114,9 @@ namespace libsecondlife.Tests CurrentRegionHandle = update.RegionData.RegionHandle; } - private void OnTeleportHandler(string message, MainAvatar.TeleportStatus status) - { - switch (status) - { - case MainAvatar.TeleportStatus.None: - break; - case MainAvatar.TeleportStatus.Start: - break; - case MainAvatar.TeleportStatus.Progress: - break; - case MainAvatar.TeleportStatus.Failed: - DoneTeleporting = true; - break; - case MainAvatar.TeleportStatus.Finished: - DoneTeleporting = true; - break; - } - - tpMessage = message; - tpStatus = status; - } - [TearDown] public void Shutdown() { - //Client.Network.Logout(); - //Client = null; } } } diff --git a/libsecondlife-cs/libsecondlife.Utilities/Utilities.cs b/libsecondlife-cs/libsecondlife.Utilities/Utilities.cs index 11d47209..031d52ef 100644 --- a/libsecondlife-cs/libsecondlife.Utilities/Utilities.cs +++ b/libsecondlife-cs/libsecondlife.Utilities/Utilities.cs @@ -95,8 +95,8 @@ namespace libsecondlife.Utilities { if (SimHandle != 0) { - if (Client.Network.CurrentSim.Region.Handle != 0 && - Client.Network.CurrentSim.Region.Handle != SimHandle) + if (Client.Network.CurrentSim.Handle != 0 && + Client.Network.CurrentSim.Handle != SimHandle) { // Attempt to move to our target sim Client.Self.Teleport(SimHandle, Position); @@ -177,7 +177,7 @@ namespace libsecondlife.Utilities { foreach (Avatar avatar in avatars.Values) { - if (avatar.CurrentRegion == Client.Network.CurrentSim.Region) + if (avatar.CurrentSim == Client.Network.CurrentSim) local[avatar.ID] = avatar; } } diff --git a/libsecondlife-cs/libsecondlife.csproj b/libsecondlife-cs/libsecondlife.csproj index 8780d5b0..81bbf8c5 100644 --- a/libsecondlife-cs/libsecondlife.csproj +++ b/libsecondlife-cs/libsecondlife.csproj @@ -164,14 +164,12 @@ Code - - Code - Code + Code