/* * 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 { /// /// /// public class Simulator : UDPBase, IDisposable { #region Enums /// /// Simulator (region) properties /// [Flags] public enum RegionFlags { /// No flags set None = 0, /// Agents can take damage and be killed AllowDamage = 1 << 0, /// Landmarks can be created here AllowLandmark = 1 << 1, /// Home position can be set in this sim AllowSetHome = 1 << 2, /// Home position is reset when an agent teleports away ResetHomeOnTeleport = 1 << 3, /// Sun does not move SunFixed = 1 << 4, /// No object, land, etc. taxes TaxFree = 1 << 5, /// Disable heightmap alterations (agents can still plant /// foliage) BlockTerraform = 1 << 6, /// Land cannot be released, sold, or purchased BlockLandResell = 1 << 7, /// All content is wiped nightly Sandbox = 1 << 8, /// NullLayer = 1 << 9, /// SkipAgentAction = 1 << 10, /// SkipUpdateInterestList = 1 << 11, /// No collision detection for non-agent objects SkipCollisions = 1 << 12, /// No scripts are ran SkipScripts = 1 << 13, /// All physics processing is turned off SkipPhysics = 1 << 14, /// ExternallyVisible = 1 << 15, /// MainlandVisible = 1 << 16, /// PublicAllowed = 1 << 17, /// BlockDwell = 1 << 18, /// Flight is disabled (not currently enforced by the sim) NoFly = 1 << 19, /// Allow direct (p2p) teleporting AllowDirectTeleport = 1 << 20, /// Estate owner has temporarily disabled scripting EstateSkipScripts = 1 << 21, /// RestrictPushObject = 1 << 22, /// Deny agents with no payment info on file DenyAnonymous = 1 << 23, /// Deny agents with payment info on file DenyIdentified = 1 << 24, /// Deny agents who have made a monetary transaction DenyTransacted = 1 << 25, /// AllowParcelChanges = 1 << 26, /// AbuseEmailToEstateOwner = 1 << 27, /// Region is Voice Enabled AllowVoice = 1 << 28 } /// /// /// public enum SimAccess : byte { /// Min = 0, /// Trial = 7, /// PG = 13, /// Mature = 21, /// Down = 254, /// NonExistent = 255 } #endregion Enums #region Structs /// /// Simulator Statistics /// public struct SimStats { /// Total number of packets sent by this simulator to this agent public ulong SentPackets; /// Total number of packets received by this simulator to this agent public ulong RecvPackets; /// Total number of bytes sent by this simulator to this agent public ulong SentBytes; /// Total number of bytes received by this simulator to this agent public ulong RecvBytes; /// Time in seconds agent has been connected to simulator public int ConnectTime; /// Total number of packets that have been resent public int ResentPackets; /// Total number of resent packets recieved public int ReceivedResends; /// Total number of pings sent to this simulator by this agent public int SentPings; /// Total number of ping replies sent to this agent by this simulator public int ReceivedPongs; /// /// Incoming bytes per second /// /// It would be nice to have this claculated on the fly, but /// this is far, far easier public int IncomingBPS; /// /// Outgoing bytes per second /// /// It would be nice to have this claculated on the fly, but /// this is far, far easier public int OutgoingBPS; /// Time last ping was sent public int LastPingSent; /// ID of last Ping sent public byte LastPingID; /// public int LastLag; /// public int MissedPings; /// Current time dilation of this simulator public float Dilation; /// Current Frames per second of simulator public int FPS; /// Current Physics frames per second of simulator public float PhysicsFPS; /// public float AgentUpdates; /// public float FrameTime; /// public float NetTime; /// public float PhysicsTime; /// public float ImageTime; /// public float ScriptTime; /// public float OtherTime; /// Total number of objects Simulator is simulating public int Objects; /// Total number of Active (Scripted) objects running public int ScriptedObjects; /// Number of agents currently in this simulator public int Agents; /// Number of agents in neighbor simulators public int ChildAgents; /// Number of Active scripts running in this simulator public int ActiveScripts; /// public int LSLIPS; /// public int INPPS; /// public int OUTPPS; /// Number of downloads pending public int PendingDownloads; /// Number of uploads pending public int PendingUploads; /// public int VirtualSize; /// public int ResidentSize; /// Number of local uploads pending public int PendingLocalUploads; /// Unacknowledged bytes in queue public int UnackedBytes; } #endregion Structs #region Public Members /// A public reference to the client that this Simulator object /// is attached to public SecondLife Client; /// public LLUUID ID = LLUUID.Zero; /// The capabilities for this simulator public Caps Caps = null; /// 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; /// public RegionFlags Flags; /// public SimAccess Access; /// public float BillableFactor; /// Statistics information for this simulator and the /// connection to the simulator, calculated by the simulator itself /// and libsecondlife public SimStats Stats; /// Provides access to two thread-safe dictionaries containing /// avatars and primitives found in this simulator //public ObjectTracker Objects = new ObjectTracker(); public InternalDictionary ObjectsAvatars = new InternalDictionary(); public InternalDictionary ObjectsPrimitives = new InternalDictionary(); /// Used to obtain a lock on the sequence number for packets /// sent to this simulator. Only useful for applications manipulating /// sequence numbers public object SequenceLock = new object(); /// The current sequence number for packets sent to this /// simulator. Must be locked with SequenceLock before modifying. Only /// useful for applications manipulating sequence numbers public volatile uint Sequence; /// /// Provides access to an internal thread-safe dictionary containing parcel /// information found in this simulator /// public InternalDictionary Parcels = new InternalDictionary(); /// /// Provides access to an internal thread-safe multidimensional array containing a x,y grid mapped /// each 64x64 parcel's LocalID. /// public int[,] ParcelMap { get { lock (this) return _ParcelMap; } set { lock (this) _ParcelMap = value; } } /// /// Checks simulator parcel map to make sure it has downloaded all data successfully /// /// true if map is full (contains no 0's) public bool IsParcelMapFull() { int ny = this.ParcelMap.GetLength(0); int nx = this.ParcelMap.GetLength(1); for (int y=0; y<64; y++) { for (int x=0; x<64; x++) { if (this.ParcelMap[y, x] == 0) return false; } } return true; } #endregion Public Members #region Properties /// 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; } } /// Coarse locations of avatars in this simulator public List AvatarPositions { get { return avatarPositions; } } /// AvatarPositions index representing your avatar public int PositionIndexYou { get { return positionIndexYou; } } /// AvatarPositions index representing TrackAgent target public int PositionIndexPrey { get { return positionIndexPrey; } } #endregion Properties #region Internal/Private Members /// Used internally to track sim disconnections internal bool DisconnectCandidate = false; /// Event that is triggered when the simulator successfully /// establishes a connection internal AutoResetEvent ConnectedEvent = new AutoResetEvent(false); /// Whether this sim is currently connected or not. Hooked up /// to the property Connected internal bool connected; /// Coarse locations of avatars in this simulator internal List avatarPositions = new List(); /// AvatarPositions index representing your avatar internal int positionIndexYou = -1; /// AvatarPositions index representing TrackAgent target internal int positionIndexPrey = -1; /// Sequence numbers of packets we've received /// (for duplicate checking) internal Queue PacketArchive; /// Packets we sent out that need ACKs from the simulator internal Dictionary NeedAck = new Dictionary(); private NetworkManager Network; private Queue InBytes, OutBytes; // ACKs that are queued up to be sent to the simulator private SortedList PendingAcks = new SortedList(); private IPEndPoint ipEndPoint; private Timer AckTimer; private Timer PingTimer; private Timer StatsTimer; // simulator <> parcel LocalID Map private int[,] _ParcelMap = new int[64, 64]; #endregion Internal/Private Members /// /// /// /// Reference to the SecondLife client /// IPEndPoint of the simulator /// handle of the simulator public Simulator(SecondLife client, IPEndPoint address, ulong handle) : base(address) { Client = client; ipEndPoint = address; Handle = handle; Estate = new EstateTools(Client); Network = Client.Network; PacketArchive = new Queue(Settings.PACKET_ARCHIVE_SIZE); InBytes = new Queue(Client.Settings.STATS_QUEUE_SIZE); OutBytes = new Queue(Client.Settings.STATS_QUEUE_SIZE); } /// /// Called when this Simulator object is being destroyed /// public void Dispose() { // Force all the CAPS connections closed for this simulator if (Caps != null) { Caps.Disconnect(true); } } /// /// Attempt to connect to this simulator /// /// Whether to move our agent in to this sim or not /// True if the connection succeeded or connection status is /// unknown, false if there was a failure public bool Connect(bool moveToSim) { if (connected) { Client.Self.CompleteAgentMovement(this); return true; } #region Start Timers // Destroy the timers if (AckTimer != null) AckTimer.Dispose(); if (StatsTimer != null) StatsTimer.Dispose(); if (PingTimer != null) PingTimer.Dispose(); // Timer for sending out queued packet acknowledgements AckTimer = new Timer(new TimerCallback(AckTimer_Elapsed), null, Settings.NETWORK_TICK_INTERVAL, Settings.NETWORK_TICK_INTERVAL); // Timer for recording simulator connection statistics StatsTimer = new Timer(new TimerCallback(StatsTimer_Elapsed), null, 1000, 1000); // Timer for periodically pinging the simulator if (Client.Settings.SEND_PINGS) PingTimer = new Timer(new TimerCallback(PingTimer_Elapsed), null, Settings.PING_INTERVAL, Settings.PING_INTERVAL); #endregion Start Timers Client.Log("Connecting to " + this.ToString(), Helpers.LogLevel.Info); try { // Create the UDP connection Start(); // Mark ourselves as connected before firing everything else up connected = true; // Send the UseCircuitCode packet to initiate the connection UseCircuitCodePacket use = new UseCircuitCodePacket(); use.CircuitCode.Code = Network.CircuitCode; use.CircuitCode.ID = Client.Self.AgentID; use.CircuitCode.SessionID = Client.Self.SessionID; // Send the initial packet out SendPacket(use, true); Stats.ConnectTime = Environment.TickCount; // Move our agent in to the sim to complete the connection if (moveToSim) Client.Self.CompleteAgentMovement(this); if (Client.Settings.SEND_AGENT_UPDATES) Client.Self.Movement.SendUpdate(true, this); if (!ConnectedEvent.WaitOne(Client.Settings.SIMULATOR_TIMEOUT, false)) { Client.Log("Giving up on waiting for RegionHandshake for " + this.ToString(), Helpers.LogLevel.Warning); } return true; } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } return false; } public void SetSeedCaps(string seedcaps) { if (Caps != null) { if (Caps._SeedCapsURI == seedcaps) return; Client.Log("Unexpected change of seed capability", Helpers.LogLevel.Warning); Caps.Disconnect(true); Caps = null; } if (Client.Settings.ENABLE_CAPS) { // Connect to the new CAPS system if (!String.IsNullOrEmpty(seedcaps)) Caps = new Caps(this, seedcaps); else Client.Log("Setting up a sim without a valid capabilities server!", Helpers.LogLevel.Error); } } /// /// Disconnect from this simulator /// public void Disconnect(bool sendCloseCircuit) { if (connected) { connected = false; // Destroy the timers if (AckTimer != null) AckTimer.Dispose(); if (StatsTimer != null) StatsTimer.Dispose(); if (PingTimer != null) PingTimer.Dispose(); // Kill the current CAPS system if (Caps != null) { Caps.Disconnect(true); Caps = null; } if (sendCloseCircuit) { // Try to send the CloseCircuit notice CloseCircuitPacket close = new CloseCircuitPacket(); UDPPacketBuffer buf = new UDPPacketBuffer(ipEndPoint, false); buf.Data = close.ToBytes(); buf.DataLength = buf.Data.Length; AsyncBeginSend(buf); } // Shut the socket communication down Stop(); } } /// /// Sends a packet /// /// Packet to be sent /// Increment sequence number? public void SendPacket(Packet packet, bool incrementSequence) { byte[] buffer; int bytes; // Keep track of when this packet was sent out packet.TickCount = Environment.TickCount; if (incrementSequence) { // Set the sequence number lock (SequenceLock) { if (Sequence > Settings.MAX_SEQUENCE) Sequence = 1; else Sequence++; packet.Header.Sequence = Sequence; } // Scrub any appended ACKs since all of the ACK handling is done here if (packet.Header.AckList.Length > 0) packet.Header.AckList = new uint[0]; packet.Header.AppendedAcks = false; 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]; for (int i = 0; i < PendingAcks.Count; i++) packet.Header.AckList[i] = PendingAcks.Values[i]; PendingAcks.Clear(); packet.Header.AppendedAcks = true; } } } } } // Serialize the packet buffer = packet.ToBytes(); bytes = buffer.Length; Stats.SentBytes += (ulong)bytes; Stats.SentPackets++; UDPPacketBuffer buf; // Zerocode if needed if (packet.Header.Zerocoded) { buf = new UDPPacketBuffer(ipEndPoint, true, false); bytes = Helpers.ZeroEncode(buffer, bytes, buf.Data); buf.DataLength = bytes; } else { buf = new UDPPacketBuffer(ipEndPoint, false, false); buf.Data = buffer; buf.DataLength = bytes; } AsyncBeginSend(buf); } /// /// 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++; } } Stats.SentBytes += (ulong)payload.Length; Stats.SentPackets++; UDPPacketBuffer buf = new UDPPacketBuffer(ipEndPoint, false); buf.Data = payload; buf.DataLength = payload.Length; AsyncBeginSend(buf); } 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, false); return; } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } /// /// Send a prepared UDPPacketBuffer object as a packet /// /// The prepared packet structure to be sent out public void SendPacket(UDPPacketBuffer buffer) { try { AsyncBeginSend(buffer); } catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); } } /// /// /// public void SendPing() { StartPingCheckPacket ping = new StartPingCheckPacket(); ping.PingID.PingID = Stats.LastPingID++; ping.PingID.OldestUnacked = 0; // FIXME ping.Header.Reliable = false; SendPacket(ping, true); Stats.LastPingSent = Environment.TickCount; } /// /// Returns Simulator Name as a String /// /// public override string ToString() { if (!String.IsNullOrEmpty(Name)) return String.Format("{0} ({1})", Name, ipEndPoint); else return String.Format("({0})", ipEndPoint); } /// /// /// /// public override int GetHashCode() { return Handle.GetHashCode(); } /// /// /// /// /// public override bool Equals(object obj) { Simulator sim = obj as Simulator; if (sim == null) return false; return (ipEndPoint.Equals(sim.ipEndPoint)); } public static bool operator ==(Simulator lhs, Simulator rhs) { // If both are null, or both are same instance, return true if (System.Object.ReferenceEquals(lhs, rhs)) { return true; } // If one is null, but not both, return false. if (((object)lhs == null) || ((object)rhs == null)) { return false; } return lhs.ipEndPoint.Equals(rhs.ipEndPoint); } public static bool operator !=(Simulator lhs, Simulator rhs) { return !(lhs == rhs); } protected override void PacketReceived(UDPPacketBuffer buffer) { Packet packet = null; // Check if this packet came from the server we expected it to come from if (!ipEndPoint.Address.Equals(((IPEndPoint)buffer.RemoteEndPoint).Address)) { Client.Log("Received " + buffer.DataLength + " bytes of data from unrecognized source " + ((IPEndPoint)buffer.RemoteEndPoint).ToString(), Helpers.LogLevel.Warning); return; } // Update the disconnect flag so this sim doesn't time out DisconnectCandidate = false; #region Packet Decoding int packetEnd = buffer.DataLength - 1; packet = Packet.BuildPacket(buffer.Data, ref packetEnd, buffer.ZeroData); // Fail-safe check if (packet == null) { Client.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning); return; } Stats.RecvBytes += (ulong)buffer.DataLength; Stats.RecvPackets++; #endregion Packet Decoding #region Reliable Handling if (packet.Header.Reliable) { // Add this packet to the list of ACKs that need to be sent out lock (PendingAcks) { uint sequence = (uint)packet.Header.Sequence; if (!PendingAcks.ContainsKey(sequence)) PendingAcks[sequence] = sequence; } // Send out ACKs if we have a lot of them if (PendingAcks.Count >= Client.Settings.MAX_PENDING_ACKS) SendAcks(); if (packet.Header.Resent) ++Stats.ReceivedResends; } #endregion Reliable Handling #region Inbox Insertion NetworkManager.IncomingPacket incomingPacket; incomingPacket.Simulator = this; incomingPacket.Packet = packet; // TODO: Prioritize the queue Network.PacketInbox.Enqueue(incomingPacket); #endregion Inbox Insertion } protected override void PacketSent(UDPPacketBuffer buffer, int bytesSent) { } /// /// Sends out pending acknowledgements /// private void SendAcks() { lock (PendingAcks) { if (PendingAcks.Count > 0) { if (PendingAcks.Count > 250) { Client.Log("Too many ACKs queued up!", Helpers.LogLevel.Error); return; } PacketAckPacket acks = new PacketAckPacket(); acks.Header.Reliable = false; acks.Packets = new PacketAckPacket.PacketsBlock[PendingAcks.Count]; for (int i = 0; i < PendingAcks.Count; i++) { acks.Packets[i] = new PacketAckPacket.PacketsBlock(); acks.Packets[i].ID = PendingAcks.Values[i]; } SendPacket(acks, true); PendingAcks.Clear(); } } } /// /// Resend unacknowledged packets /// private void ResendUnacked() { int now = Environment.TickCount; lock (NeedAck) { foreach (Packet packet in NeedAck.Values) { if (now - packet.TickCount > Client.Settings.RESEND_TIMEOUT) { try { if (Client.Settings.LOG_RESENDS) Client.DebugLog(String.Format("Resending packet #{0} ({1}), {2}ms have passed", packet.Header.Sequence, packet.GetType(), now - packet.TickCount)); packet.Header.Resent = true; packet.TickCount = now; ++Stats.ResentPackets; SendPacket(packet, false); } catch (Exception ex) { Client.DebugLog("Exception trying to resend packet: " + ex.ToString()); } } } } } private void AckTimer_Elapsed(object obj) { SendAcks(); ResendUnacked(); } private void StatsTimer_Elapsed(object obj) { ulong old_in = 0, old_out = 0; if (InBytes.Count >= Client.Settings.STATS_QUEUE_SIZE) old_in = InBytes.Dequeue(); if (OutBytes.Count >= Client.Settings.STATS_QUEUE_SIZE) old_out = OutBytes.Dequeue(); InBytes.Enqueue(Stats.RecvBytes); OutBytes.Enqueue(Stats.SentBytes); if (old_in > 0 && old_out > 0) { Stats.IncomingBPS = (int)(Stats.RecvBytes - old_in) / Client.Settings.STATS_QUEUE_SIZE; Stats.OutgoingBPS = (int)(Stats.SentBytes - old_out) / Client.Settings.STATS_QUEUE_SIZE; //Client.Log("Incoming: " + IncomingBPS + " Out: " + OutgoingBPS + // " Lag: " + LastLag + " Pings: " + ReceivedPongs + // "/" + SentPings, Helpers.LogLevel.Debug); } } private void PingTimer_Elapsed(object obj) { SendPing(); Stats.SentPings++; } } }