Files
libremetaverse/LibreMetaverse/Simulator.cs
Robert Adams d862b737b0 Add RegionSizeX,RegionSizeY to Simulator class.
Add logic to copy region size info (if supplied) into Simulator class on instance creation.
Add region size fields to definitions of LoginResponse, EnableSimulator, TeleportFinishMessage, CrossedRegionMessage.
Add constants Simulator.DefaultRegionSizeX=256 and Simulator.DefaultRegionSizeY=256
Add display of region size in "RegionInfo" TestClient command
2023-06-16 13:17:13 -07:00

1509 lines
55 KiB
C#

/*
* Copyright (c) 2006-2016, openmetaverse.co
* Copyright (c) 2022, Sjofn LLC.
* All rights reserved.
*
* - Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Neither the name of the openmetaverse.co 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Net;
using OpenMetaverse.Packets;
namespace OpenMetaverse
{
#region Enums
/// <summary>
/// Simulator (region) properties
/// </summary>
[Flags]
public enum RegionFlags : ulong
{
/// <summary>No flags set</summary>
None = 0,
/// <summary>Agents can take damage and be killed</summary>
AllowDamage = 1 << 0,
/// <summary>Landmarks can be created here</summary>
AllowLandmark = 1 << 1,
/// <summary>Home position can be set in this sim</summary>
AllowSetHome = 1 << 2,
/// <summary>Home position is reset when an agent teleports away</summary>
ResetHomeOnTeleport = 1 << 3,
/// <summary>Sun does not move</summary>
SunFixed = 1 << 4,
/// <summary>Allows private parcels (ie. banlines)</summary>
AllowAccessOverride = 1 << 5,
/// <summary>Disable heightmap alterations (agents can still plant foliage)</summary>
BlockTerraform = 1 << 6,
/// <summary>Land cannot be released, sold, or purchased</summary>
BlockLandResell = 1 << 7,
/// <summary>All content is wiped nightly</summary>
Sandbox = 1 << 8,
/// <summary>Unknown: Related to the availability of an overview world map tile.(Think mainland images when zoomed out.)</summary>
NullLayer = 1 << 9,
/// <summary>Unknown: Related to region debug flags. Possibly to skip processing of agent interaction with world. </summary>
SkipAgentAction = 1 << 10,
/// <summary>Region does not update agent prim interest lists. Internal debugging option.</summary>
SkipUpdateInterestList = 1 << 11,
/// <summary>No collision detection for non-agent objects</summary>
SkipCollisions = 1 << 12,
/// <summary>No scripts are ran</summary>
SkipScripts = 1 << 13,
/// <summary>All physics processing is turned off</summary>
SkipPhysics = 1 << 14,
/// <summary>Region can be seen from other regions on world map. (Legacy world map option?) </summary>
ExternallyVisible = 1 << 15,
/// <summary>Region can be seen from mainland on world map. (Legacy world map option?) </summary>
MainlandVisible = 1 << 16,
/// <summary>Agents not explicitly on the access list can visit the region. </summary>
PublicAllowed = 1 << 17,
/// <summary>Traffic calculations are not run across entire region, overrides parcel settings. </summary>
BlockDwell = 1 << 18,
/// <summary>Flight is disabled (not currently enforced by the sim)</summary>
NoFly = 1 << 19,
/// <summary>Allow direct (p2p) teleporting</summary>
AllowDirectTeleport = 1 << 20,
/// <summary>Estate owner has temporarily disabled scripting</summary>
EstateSkipScripts = 1 << 21,
/// <summary>Restricts the usage of the LSL llPushObject function, applies to whole region.</summary>
RestrictPushObject = 1 << 22,
/// <summary>Deny agents with no payment info on file</summary>
DenyAnonymous = 1 << 23,
/// <summary>Deny agents with payment info on file</summary>
DenyIdentified = 1 << 24,
/// <summary>Deny agents who have made a monetary transaction</summary>
DenyTransacted = 1 << 25,
/// <summary>Parcels within the region may be joined or divided by anyone, not just estate owners/managers. </summary>
AllowParcelChanges = 1 << 26,
/// <summary>Abuse reports sent from within this region are sent to the estate owner defined email. </summary>
AbuseEmailToEstateOwner = 1 << 27,
/// <summary>Region is Voice Enabled</summary>
AllowVoice = 1 << 28,
/// <summary>Removes the ability from parcel owners to set their parcels to show in search.</summary>
BlockParcelSearch = 1 << 29,
/// <summary>Deny agents who have not been age verified from entering the region.</summary>
DenyAgeUnverified = 1 << 30
}
/// <summary>
/// Region protocol flags
/// </summary>
[Flags]
public enum RegionProtocols : ulong
{
/// <summary>Nothing special</summary>
None = 0,
/// <summary>Region supports Server side Appearance</summary>
AgentAppearanceService = 1 << 0,
/// <summary>Viewer supports Server side Appearance</summary>
SelfAppearanceSupport = 1 << 2
}
/// <summary>
/// Access level for a simulator
/// </summary>
[Flags]
public enum SimAccess : byte
{
/// <summary>Unknown or invalid access level</summary>
Unknown = 0,
/// <summary>Trial accounts allowed</summary>
Trial = 7,
/// <summary>PG rating</summary>
PG = 13,
/// <summary>Mature rating</summary>
Mature = 21,
/// <summary>Adult rating</summary>
Adult = 42,
/// <summary>Simulator is offline</summary>
Down = 254,
/// <summary>Simulator does not exist</summary>
NonExistent = 255
}
#endregion Enums
/// <summary>
/// Simulator class encapsulates the idea of what Linden Lab calls a "Region" not a "Simulator", per se.
/// </summary>
public class Simulator : UDPBase, IDisposable
{
#region Structs
/// <summary>
/// Simulator Statistics
/// </summary>
public struct SimStats
{
/// <summary>Total number of packets sent by this simulator to this agent</summary>
public long SentPackets;
/// <summary>Total number of packets received by this simulator to this agent</summary>
public long RecvPackets;
/// <summary>Total number of bytes sent by this simulator to this agent</summary>
public long SentBytes;
/// <summary>Total number of bytes received by this simulator to this agent</summary>
public long RecvBytes;
/// <summary>Time in seconds agent has been connected to simulator</summary>
public int ConnectTime;
/// <summary>Total number of packets that have been resent</summary>
public int ResentPackets;
/// <summary>Total number of resent packets received</summary>
public int ReceivedResends;
/// <summary>Total number of pings sent to this simulator by this agent</summary>
public int SentPings;
/// <summary>Total number of ping replies sent to this agent by this simulator</summary>
public int ReceivedPongs;
/// <summary>
/// Incoming bytes per second
/// </summary>
/// <remarks>It would be nice to have this calculated on the fly, but
/// this is far, far easier</remarks>
public int IncomingBPS;
/// <summary>
/// Outgoing bytes per second
/// </summary>
/// <remarks>It would be nice to have this claculated on the fly, but
/// this is far, far easier</remarks>
public int OutgoingBPS;
/// <summary>Time last ping was sent</summary>
public int LastPingSent;
/// <summary>ID of last Ping sent</summary>
public byte LastPingID;
/// <summary></summary>
public int LastLag;
/// <summary></summary>
public int MissedPings;
/// <summary>Current time dilation of this simulator</summary>
public float Dilation;
/// <summary>Current Frames per second of simulator</summary>
public int FPS;
/// <summary>Current Physics frames per second of simulator</summary>
public float PhysicsFPS;
/// <summary></summary>
public float AgentUpdates;
/// <summary></summary>
public float FrameTime;
/// <summary></summary>
public float NetTime;
/// <summary></summary>
public float PhysicsTime;
/// <summary></summary>
public float ImageTime;
/// <summary></summary>
public float ScriptTime;
/// <summary></summary>
public float AgentTime;
/// <summary></summary>
public float OtherTime;
/// <summary>Total number of objects Simulator is simulating</summary>
public int Objects;
/// <summary>Total number of Active (Scripted) objects running</summary>
public int ScriptedObjects;
/// <summary>Number of agents currently in this simulator</summary>
public int Agents;
/// <summary>Number of agents in neighbor simulators</summary>
public int ChildAgents;
/// <summary>Number of Active scripts running in this simulator</summary>
public int ActiveScripts;
/// <summary></summary>
public int LSLIPS;
/// <summary></summary>
public int INPPS;
/// <summary></summary>
public int OUTPPS;
/// <summary>Number of downloads pending</summary>
public int PendingDownloads;
/// <summary>Number of uploads pending</summary>
public int PendingUploads;
/// <summary></summary>
public int VirtualSize;
/// <summary></summary>
public int ResidentSize;
/// <summary>Number of local uploads pending</summary>
public int PendingLocalUploads;
/// <summary>Unacknowledged bytes in queue</summary>
public int UnackedBytes;
}
#endregion Structs
#region Public Members
// Default legacy simulator/region size
public const uint DefaultRegionSizeX = 256;
public const uint DefaultRegionSizeY = 256;
/// <summary>A public reference to the client that this Simulator object is attached to</summary>
public GridClient Client;
/// <summary>A Unique Cache identifier for this simulator</summary>
public UUID ID = UUID.Zero;
/// <summary>The capabilities for this simulator</summary>
public Caps Caps;
/// <summary>Unique identified for this region generated via it's coordinates on the world map</summary>
public ulong Handle;
/// <summary>Simulator land size in X direction in meters</summary>
public uint SizeX;
/// <summary>Simulator land size in Y direction in meters</summary>
public uint SizeY;
/// <summary>The current version of software this simulator is running</summary>
public string SimVersion = String.Empty;
/// <summary>Human readable name given to the simulator</summary>
public string Name = String.Empty;
/// <summary>A 64x64 grid of parcel coloring values. The values stored
/// in this array are of the <seealso cref="ParcelArrayType"/> type</summary>
public byte[] ParcelOverlay = new byte[4096];
/// <summary></summary>
public int ParcelOverlaysReceived;
/// <summary></summary>
public float TerrainHeightRange00;
/// <summary></summary>
public float TerrainHeightRange01;
/// <summary></summary>
public float TerrainHeightRange10;
/// <summary></summary>
public float TerrainHeightRange11;
/// <summary></summary>
public float TerrainStartHeight00;
/// <summary></summary>
public float TerrainStartHeight01;
/// <summary></summary>
public float TerrainStartHeight10;
/// <summary></summary>
public float TerrainStartHeight11;
/// <summary></summary>
public float WaterHeight;
/// <summary>UUID identifier of the owner of this Region</summary>
public UUID SimOwner = UUID.Zero;
/// <summary></summary>
public UUID TerrainBase0 = UUID.Zero;
/// <summary></summary>
public UUID TerrainBase1 = UUID.Zero;
/// <summary></summary>
public UUID TerrainBase2 = UUID.Zero;
/// <summary></summary>
public UUID TerrainBase3 = UUID.Zero;
/// <summary></summary>
public UUID TerrainDetail0 = UUID.Zero;
/// <summary></summary>
public UUID TerrainDetail1 = UUID.Zero;
/// <summary></summary>
public UUID TerrainDetail2 = UUID.Zero;
/// <summary></summary>
public UUID TerrainDetail3 = UUID.Zero;
/// <summary>true if your agent has Estate Manager rights on this region</summary>
public bool IsEstateManager;
/// <summary></summary>
public RegionFlags Flags;
/// <summary>Access level</summary>
public SimAccess Access;
/// <summary></summary>
public float BillableFactor;
/// <summary>Statistics information for this simulator and the
/// connection to the simulator, calculated by the simulator itself
/// and the library</summary>
public SimStats Stats;
/// <summary>The regions Unique ID</summary>
public UUID RegionID = UUID.Zero;
/// <summary>The physical data center the simulator is located</summary>
/// <remarks>Known values are:
/// <list type="table">
/// <item>Dallas</item>
/// <item>Chandler</item>
/// <item>SF</item>
/// </list>
/// </remarks>
public string ColoLocation;
/// <summary>The CPU Class of the simulator</summary>
/// <remarks>Most full mainland/estate sims appear to be 5,
/// Homesteads and Openspace appear to be 501</remarks>
public int CPUClass;
/// <summary>The number of regions sharing the same CPU as this one</summary>
/// <remarks>"Full Sims" appear to be 1, Homesteads appear to be 4</remarks>
public int CPURatio;
/// <summary>The billing product name</summary>
/// <remarks>Known values are:
/// <list type="table">
/// <item>Mainland / Full Region (Sku: 023)</item>
/// <item>Estate / Full Region (Sku: 024)</item>
/// <item>Estate / Openspace (Sku: 027)</item>
/// <item>Estate / Homestead (Sku: 029)</item>
/// <item>Mainland / Homestead (Sku: 129) (Linden Owned)</item>
/// <item>Mainland / Linden Homes (Sku: 131)</item>
/// </list>
/// </remarks>
public string ProductName;
/// <summary>The billing product SKU</summary>
/// <remarks>Known values are:
/// <list type="table">
/// <item>023 Mainland / Full Region</item>
/// <item>024 Estate / Full Region</item>
/// <item>027 Estate / Openspace</item>
/// <item>029 Estate / Homestead</item>
/// <item>129 Mainland / Homestead (Linden Owned)</item>
/// <item>131 Linden Homes / Full Region</item>
/// </list>
/// </remarks>
public string ProductSku;
/// <summary>
/// Flags indicating which protocols this region supports
/// </summary>
public RegionProtocols Protocols;
/// <summary>The current sequence number for packets sent to this
/// simulator. Must be Interlocked before modifying. Only
/// useful for applications manipulating sequence numbers</summary>
public int Sequence;
/// <summary>
/// A thread-safe dictionary containing avatars in a simulator
/// </summary>
public InternalDictionary<uint, Avatar> ObjectsAvatars = new InternalDictionary<uint, Avatar>();
/// <summary>
/// A thread-safe dictionary containing primitives in a simulator
/// </summary>
public InternalDictionary<uint, Primitive> ObjectsPrimitives = new InternalDictionary<uint, Primitive>();
public readonly TerrainPatch[] Terrain;
public readonly Vector2[] WindSpeeds;
/// <summary>
/// Provides access to an internal thread-safe dictionary containing parcel
/// information found in this simulator
/// </summary>
public InternalDictionary<int, Parcel> Parcels
{
get
{
if (Client.Settings.POOL_PARCEL_DATA)
{
return DataPool.Parcels;
}
return _Parcels ?? (_Parcels = new InternalDictionary<int, Parcel>());
}
}
private InternalDictionary<int, Parcel> _Parcels;
/// <summary>
/// Provides access to an internal thread-safe multidimensional array containing a x,y grid mapped
/// to each 64x64 parcel's LocalID.
/// </summary>
public int[,] ParcelMap
{
get
{
lock (this)
{
if (Client.Settings.POOL_PARCEL_DATA)
{
return DataPool.ParcelMap;
}
return _parcelMap ?? (_parcelMap = new int[64, 64]);
}
}
}
/// <summary>
/// Checks simulator parcel map to make sure it has downloaded all data successfully
/// </summary>
/// <returns>true if map is full (contains no 0's)</returns>
public bool IsParcelMapFull()
{
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
if (ParcelMap[y, x] == 0)
return false;
}
}
return true;
}
/// <summary>
/// Is it safe to send agent updates to this sim
/// AgentMovementComplete message received
/// </summary>
public bool AgentMovementComplete;
#endregion Public Members
#region Properties
/// <summary>The IP address and port of the server</summary>
public IPEndPoint IPEndPoint { get { return remoteEndPoint; } }
/// <summary>Whether there is a working connection to the simulator or
/// not</summary>
public bool Connected { get { return connected; } }
/// <summary>Coarse locations of avatars in this simulator</summary>
public InternalDictionary<UUID, Vector3> AvatarPositions { get { return avatarPositions; } }
/// <summary>AvatarPositions key representing TrackAgent target</summary>
public UUID PreyID { get { return preyID; } }
/// <summary>Indicates if UDP connection to the sim is fully established</summary>
public bool HandshakeComplete { get { return handshakeComplete; } }
#endregion Properties
#region Internal/Private Members
/// <summary>Used internally to track sim disconnections</summary>
internal bool DisconnectCandidate = false;
/// <summary>Event that is triggered when the simulator successfully
/// establishes a connection</summary>
internal ManualResetEvent ConnectedEvent = new ManualResetEvent(false);
/// <summary>Whether this sim is currently connected or not. Hooked up
/// to the property Connected</summary>
internal bool connected;
/// <summary>Coarse locations of avatars in this simulator</summary>
internal InternalDictionary<UUID, Vector3> avatarPositions = new InternalDictionary<UUID, Vector3>();
/// <summary>AvatarPositions key representing TrackAgent target</summary>
internal UUID preyID = UUID.Zero;
/// <summary>Sequence numbers of packets we've received
/// (for duplicate checking)</summary>
internal IncomingPacketIDCollection PacketArchive;
/// <summary>Packets we sent out that need ACKs from the simulator</summary>
internal SortedDictionary<uint, NetworkManager.OutgoingPacket> NeedAck = new SortedDictionary<uint, NetworkManager.OutgoingPacket>();
/// <summary>Sequence number for pause/resume</summary>
internal int pauseSerial;
/// <summary>Indicates if UDP connection to the sim is fully established</summary>
internal bool handshakeComplete;
private NetworkManager Network;
private Queue<long> InBytes, OutBytes;
// ACKs that are queued up to be sent to the simulator
private ConcurrentQueue<uint> PendingAcks = new ConcurrentQueue<uint>();
private Timer AckTimer;
private Timer PingTimer;
private Timer StatsTimer;
// simulator <> parcel LocalID Map
private int[,] _parcelMap;
public readonly SimulatorDataPool DataPool;
internal bool DownloadingParcelMap
{
get => Client.Settings.POOL_PARCEL_DATA ? DataPool.DownloadingParcelMap : _DownloadingParcelMap;
set
{
if (Client.Settings.POOL_PARCEL_DATA) DataPool.DownloadingParcelMap = value;
_DownloadingParcelMap = value;
}
}
internal bool _DownloadingParcelMap = false;
private ManualResetEvent GotUseCircuitCodeAck = new ManualResetEvent(false);
#endregion Internal/Private Members
/// <summary>
/// Constructor
/// </summary>
/// <param name="client">Reference to the <seealso cref="GridClient"/> object</param>
/// <param name="address">IPEndPoint of the simulator</param>
/// <param name="handle">Region handle for the simulator</param>
public Simulator(GridClient client, IPEndPoint address, ulong handle, uint sizeX = DefaultRegionSizeX, uint sizeY = DefaultRegionSizeY)
: base(address)
{
Client = client;
if (Client.Settings.POOL_PARCEL_DATA || Client.Settings.CACHE_PRIMITIVES)
{
SimulatorDataPool.SimulatorAdd(this);
DataPool = SimulatorDataPool.GetSimulatorData(Handle);
}
Handle = handle;
Network = Client.Network;
SizeX = sizeX;
SizeY = sizeY;
PacketArchive = new IncomingPacketIDCollection(Settings.PACKET_ARCHIVE_SIZE);
InBytes = new Queue<long>(Client.Settings.STATS_QUEUE_SIZE);
OutBytes = new Queue<long>(Client.Settings.STATS_QUEUE_SIZE);
if (client.Settings.STORE_LAND_PATCHES)
{
Terrain = new TerrainPatch[16 * 16];
WindSpeeds = new Vector2[16 * 16];
}
}
/// <summary>
/// Called when this Simulator object is being destroyed
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposing) return;
AckTimer?.Dispose();
PingTimer?.Dispose();
StatsTimer?.Dispose();
ConnectedEvent?.Close();
// Force all the CAPS connections closed for this simulator
Caps?.Disconnect(true);
}
/// <summary>
/// Attempt to connect to this simulator
/// </summary>
/// <param name="moveToSim">Whether to move our agent in to this sim or not</param>
/// <returns>True if the connection succeeded or unknown, false if there
/// was a failure</returns>
public bool Connect(bool moveToSim)
{
handshakeComplete = false;
if (connected)
{
UseCircuitCode(true);
if (moveToSim)
{
Client.Self.CompleteAgentMovement(this);
}
return true;
}
#region Start Timers
// Timer for sending out queued packet acknowledgements
if (AckTimer == null)
AckTimer = new Timer(AckTimer_Elapsed, null, Settings.NETWORK_TICK_INTERVAL, Timeout.Infinite);
// Timer for recording simulator connection statistics
if (StatsTimer == null)
StatsTimer = new Timer(StatsTimer_Elapsed, null, 1000, 1000);
// Timer for periodically pinging the simulator
if (PingTimer == null && Client.Settings.SEND_PINGS)
PingTimer = new Timer(PingTimer_Elapsed, null, Settings.PING_INTERVAL, Settings.PING_INTERVAL);
#endregion Start Timers
Logger.Log($"Connecting to {this}", Helpers.LogLevel.Info, Client);
try
{
// Create the UDP connection
Start();
// Mark ourselves as connected before firing everything else up
connected = true;
// Initiate connection
UseCircuitCode(true);
Stats.ConnectTime = Environment.TickCount;
// Move our agent in to the sim to complete the connection
if (moveToSim) Client.Self.CompleteAgentMovement(this);
if (!ConnectedEvent.WaitOne(Client.Settings.LOGIN_TIMEOUT, false))
{
Logger.Log($"Giving up waiting for RegionHandshake for {this}",
Helpers.LogLevel.Warning, Client);
//Remove the simulator from the list, not useful if we haven't received the RegionHandshake
lock (Client.Network.Simulators) {
Client.Network.Simulators.Remove(this);
}
}
if (Client.Settings.SEND_AGENT_THROTTLE)
Client.Throttle.Set(this);
if (Client.Settings.SEND_AGENT_UPDATES)
Client.Self.Movement.SendUpdate(true, this);
return true;
}
catch (Exception e)
{
Logger.Log(e.Message, Helpers.LogLevel.Error, Client, e);
}
return false;
}
/// <summary>
/// Initiates connection to the simulator
/// </summary>
/// <param name="waitForAck">Should we block until ack for this packet is received</param>
public void UseCircuitCode(bool waitForAck)
{
// Send the UseCircuitCode packet to initiate the connection
UseCircuitCodePacket use = new UseCircuitCodePacket
{
CircuitCode =
{
Code = Network.CircuitCode,
ID = Client.Self.AgentID,
SessionID = Client.Self.SessionID
}
};
if (waitForAck)
{
GotUseCircuitCodeAck.Reset();
}
// Send the initial packet out
SendPacket(use);
if (waitForAck)
{
if (!GotUseCircuitCodeAck.WaitOne(Client.Settings.LOGIN_TIMEOUT, false))
{
Logger.Log("Failed to get ACK for UseCircuitCode packet", Helpers.LogLevel.Error, Client);
}
}
}
public void SetSeedCaps(Uri seedcaps)
{
if (Caps != null)
{
if (Caps._SeedCapsURI == seedcaps) return;
Logger.Log("Unexpected change of seed capability", Helpers.LogLevel.Warning, Client);
Caps.Disconnect(true);
Caps = null;
}
// Connect to the CAPS system
if (seedcaps != null)
{
Caps = new Caps(this, seedcaps);
}
else
{
Logger.Log("Setting up a sim without valid http capabilities", Helpers.LogLevel.Error, Client);
}
}
/// <summary>
/// Disconnect from this simulator
/// </summary>
public void Disconnect(bool sendCloseCircuit)
{
DisconnectCandidate = false;
if (!connected) return;
connected = false;
// Destroy the timers
AckTimer?.Dispose();
StatsTimer?.Dispose();
PingTimer?.Dispose();
AckTimer = null;
StatsTimer = null;
PingTimer = null;
// 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(remoteEndPoint);
byte[] data = close.ToBytes();
buf.CopyFrom(data);
buf.DataLength = data.Length;
AsyncBeginSend(buf);
}
if (Client.Settings.POOL_PARCEL_DATA || Client.Settings.CACHE_PRIMITIVES)
{
SimulatorDataPool.SimulatorRelease(this);
}
// Shut the socket communication down
Stop();
}
/// <summary>
/// Instructs the simulator to stop sending update (and possibly other) packets
/// </summary>
public void Pause()
{
AgentPausePacket pause = new AgentPausePacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID,
SerialNum = (uint) Interlocked.Exchange(ref pauseSerial, pauseSerial + 1)
}
};
Client.Network.SendPacket(pause, this);
}
/// <summary>
/// Instructs the simulator to resume sending update packets (unpause)
/// </summary>
public void Resume()
{
AgentResumePacket resume = new AgentResumePacket
{
AgentData =
{
AgentID = Client.Self.AgentID,
SessionID = Client.Self.SessionID,
SerialNum = (uint) Interlocked.Exchange(ref pauseSerial, pauseSerial + 1)
}
};
Client.Network.SendPacket(resume, this);
}
/// <summary>
/// Retrieve the terrain height at a given coordinate
/// </summary>
/// <param name="x">Sim X coordinate, valid range is from 0 to 255</param>
/// <param name="y">Sim Y coordinate, valid range is from 0 to 255</param>
/// <param name="height">The terrain height at the given point if the
/// lookup was successful, otherwise 0.0f</param>
/// <returns>True if the lookup was successful, otherwise false</returns>
public bool TerrainHeightAtPoint(int x, int y, out float height)
{
if (Terrain != null && x >= 0 && x < SizeX && y >= 0 && y < SizeY)
{
int patchX = x / 16;
int patchY = y / 16;
x = x % 16;
y = y % 16;
TerrainPatch patch = Terrain[patchY * 16 + patchX];
if (patch != null)
{
height = patch.Data[y * 16 + x];
return true;
}
}
height = 0.0f;
return false;
}
#region Packet Sending
/// <summary>
/// Sends a packet
/// </summary>
/// <param name="packet">Packet to be sent</param>
public void SendPacket(Packet packet)
{
// DEBUG: This can go away after we are sure nothing in the library is trying to do this
if (packet.Header.AppendedAcks || (packet.Header.AckList != null && packet.Header.AckList.Length > 0))
Logger.Log("Attempting to send packet " + packet.Type + " with ACKs appended before serialization", Helpers.LogLevel.Error);
if (packet.HasVariableBlocks)
{
byte[][] datas;
try { datas = packet.ToBytesMultiple(); }
catch (NullReferenceException)
{
Logger.Log("Failed to serialize " + packet.Type + " packet to one or more payloads due to a missing block or field. StackTrace: " +
Environment.StackTrace, Helpers.LogLevel.Error);
return;
}
int packetCount = datas.Length;
if (packetCount > 1)
Logger.DebugLog("Split " + packet.Type + " packet into " + packetCount + " packets");
for (int i = 0; i < packetCount; i++)
{
byte[] data = datas[i];
SendPacketData(data, data.Length, packet.Type, packet.Header.Zerocoded);
}
}
else
{
byte[] data = packet.ToBytes();
SendPacketData(data, data.Length, packet.Type, packet.Header.Zerocoded);
}
}
public void SendPacketData(byte[] data, int dataLength, PacketType type, bool doZerocode)
{
UDPPacketBuffer buffer = new UDPPacketBuffer(remoteEndPoint, Packet.MTU);
// Zerocode if needed
if (doZerocode)
{
try
{
dataLength = Helpers.ZeroEncode(data, dataLength, buffer.Data);
}
catch (IndexOutOfRangeException)
{
// The packet grew larger than Packet.MTU bytes while zerocoding.
// Remove the MSG_ZEROCODED flag and send the unencoded data
// instead
data[0] = (byte)(data[0] & ~Helpers.MSG_ZEROCODED);
buffer.CopyFrom(data, dataLength);
}
}
else
{
buffer.CopyFrom(data, dataLength);
}
buffer.DataLength = dataLength;
#region Queue or Send
NetworkManager.OutgoingPacket outgoingPacket = new NetworkManager.OutgoingPacket(this, buffer, type);
// Send ACK and logout packets directly, everything else goes through the queue
if (Client.Settings.THROTTLE_OUTGOING_PACKETS == false ||
type == PacketType.PacketAck ||
type == PacketType.LogoutRequest)
{
SendPacketFinal(outgoingPacket);
}
else
{
Network.EnqueueOutgoing(outgoingPacket);
}
#endregion Queue or Send
#region Stats Tracking
if (Client.Settings.TRACK_UTILIZATION)
{
Client.Stats.Update(type.ToString(), OpenMetaverse.Stats.Type.Packet, dataLength, 0);
}
#endregion
}
internal void SendPacketFinal(NetworkManager.OutgoingPacket outgoingPacket)
{
UDPPacketBuffer buffer = outgoingPacket.Buffer;
byte flags = buffer.Data[0];
bool isResend = (flags & Helpers.MSG_RESENT) != 0;
bool isReliable = (flags & Helpers.MSG_RELIABLE) != 0;
// Keep track of when this packet was sent out (right now)
outgoingPacket.TickCount = Environment.TickCount;
#region ACK Appending
int dataLength = buffer.DataLength;
// Keep appending ACKs until there is no room left in the packet or there are
// no more ACKs to append
uint ackCount = 0;
uint ack;
while (dataLength + 5 < Packet.MTU && PendingAcks.TryDequeue(out ack))
{
Utils.UIntToBytesBig(ack, buffer.Data, dataLength);
dataLength += 4;
++ackCount;
}
if (ackCount > 0)
{
// Set the last byte of the packet equal to the number of appended ACKs
buffer.Data[dataLength++] = (byte)ackCount;
// Set the appended ACKs flag on this packet
buffer.Data[0] |= Helpers.MSG_APPENDED_ACKS;
}
buffer.DataLength = dataLength;
#endregion ACK Appending
if (!isResend)
{
// Not a resend, assign a new sequence number
uint sequenceNumber = (uint)Interlocked.Increment(ref Sequence);
Utils.UIntToBytesBig(sequenceNumber, buffer.Data, 1);
outgoingPacket.SequenceNumber = sequenceNumber;
if (isReliable)
{
// Add this packet to the list of ACK responses we are waiting on from the server
lock (NeedAck) NeedAck[sequenceNumber] = outgoingPacket;
}
}
// Put the UDP payload on the wire
AsyncBeginSend(buffer);
}
/// <summary>
///
/// </summary>
public void SendPing()
{
uint oldestUnacked = 0;
// Get the oldest NeedAck value, the first entry in the sorted dictionary
lock (NeedAck)
{
if (NeedAck.Count > 0)
{
using (var en = NeedAck.Keys.GetEnumerator())
{
en.MoveNext();
oldestUnacked = en.Current;
}
}
}
//if (oldestUnacked != 0)
// Logger.DebugLog("Sending ping with oldestUnacked=" + oldestUnacked);
StartPingCheckPacket ping = new StartPingCheckPacket
{
PingID =
{
PingID = Stats.LastPingID++,
OldestUnacked = oldestUnacked
},
Header = {Reliable = false}
};
SendPacket(ping);
Stats.LastPingSent = Environment.TickCount;
}
#endregion Packet Sending
/// <summary>
/// Returns Simulator Name as a String
/// </summary>
/// <returns>Simulator name as String</returns>
public override string ToString()
{
return !String.IsNullOrEmpty(Name)
? $"{Name} ({remoteEndPoint})"
: $"({remoteEndPoint})";
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return Handle.GetHashCode();
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
Simulator sim = obj as Simulator;
return sim != null && (remoteEndPoint.Equals(sim.remoteEndPoint));
}
public static bool operator ==(Simulator lhs, Simulator rhs)
{
// If both are null, or both are same instance, return true
if (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.remoteEndPoint.Equals(rhs.remoteEndPoint);
}
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 (!remoteEndPoint.Address.Equals(((IPEndPoint)buffer.RemoteEndPoint).Address))
{
Logger.Log($"Received {buffer.DataLength} bytes of data from unrecognized source {(IPEndPoint)buffer.RemoteEndPoint}",
Helpers.LogLevel.Warning, Client);
return;
}
// Update the disconnect flag so this sim doesn't time out
DisconnectCandidate = false;
#region Packet Decoding
int packetEnd = buffer.DataLength - 1;
try
{
packet = Packet.BuildPacket(buffer.Data, ref packetEnd,
// Only allocate a buffer for zerodecoding if the packet is zerocoded
((buffer.Data[0] & Helpers.MSG_ZEROCODED) != 0) ? new byte[8192] : null);
}
catch (MalformedDataException)
{
Logger.Log(String.Format("Malformed data, cannot parse packet:\n{0}",
Utils.BytesToHexString(buffer.Data, buffer.DataLength, null)), Helpers.LogLevel.Error);
}
// Fail-safe check
if (packet == null)
{
Logger.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning, Client);
return;
}
Interlocked.Add(ref Stats.RecvBytes, buffer.DataLength);
Interlocked.Increment(ref Stats.RecvPackets);
#endregion Packet Decoding
if (packet.Header.Resent)
Interlocked.Increment(ref Stats.ReceivedResends);
#region ACK Receiving
// Handle appended ACKs
if (packet.Header.AppendedAcks && packet.Header.AckList != null)
{
lock (NeedAck)
{
foreach (var t in packet.Header.AckList)
{
if (NeedAck.ContainsKey(t) && NeedAck[t].Type == PacketType.UseCircuitCode)
{
GotUseCircuitCodeAck.Set();
}
NeedAck.Remove(t);
}
}
}
// Handle PacketAck packets
if (packet.Type == PacketType.PacketAck)
{
PacketAckPacket ackPacket = (PacketAckPacket)packet;
lock (NeedAck)
{
foreach (var t in ackPacket.Packets)
{
if (NeedAck.ContainsKey(t.ID) && NeedAck[t.ID].Type == PacketType.UseCircuitCode)
{
GotUseCircuitCodeAck.Set();
}
NeedAck.Remove(t.ID);
}
}
}
#endregion ACK Receiving
if (packet.Header.Reliable)
{
#region ACK Sending
// Add this packet to the list of ACKs that need to be sent out
var sequence = packet.Header.Sequence;
PendingAcks.Enqueue(sequence);
// Send out ACKs if we have a lot of them
if (PendingAcks.Count >= Client.Settings.MAX_PENDING_ACKS)
SendAcks();
#endregion ACK Sending
// Check the archive of received packet IDs to see whether we already received this packet
if (!PacketArchive.TryEnqueue(packet.Header.Sequence))
{
if (packet.Header.Resent)
Logger.DebugLog(
string.Format(
"Received a resend of already processed packet #{0}, type: {1} from {2}",
packet.Header.Sequence, packet.Type, Name));
else
Logger.Log(
string.Format(
"Received a duplicate (not marked as resend) of packet #{0}, type: {1} for {2} from {3}",
packet.Header.Sequence, packet.Type, Client.Self.Name, Name),
Helpers.LogLevel.Warning);
// Avoid firing a callback twice for the same packet
return;
}
}
#region Inbox Insertion
var incomingPacket = new NetworkManager.IncomingPacket
{
Simulator = this,
Packet = packet
};
Network.EnqueueIncoming(incomingPacket);
#endregion Inbox Insertion
#region Stats Tracking
if (Client.Settings.TRACK_UTILIZATION)
{
Client.Stats.Update(packet.Type.ToString(), OpenMetaverse.Stats.Type.Packet, 0, packet.Length);
}
#endregion
}
protected override void PacketSent(UDPPacketBuffer buffer, int bytesSent)
{
// Stats tracking
Interlocked.Add(ref Stats.SentBytes, bytesSent);
Interlocked.Increment(ref Stats.SentPackets);
Client.Network.RaisePacketSentEvent(buffer.Data, bytesSent, this);
}
/// <summary>
/// Sends out pending acknowledgments
/// </summary>
/// <returns>Number of ACKs sent</returns>
private int SendAcks()
{
int ackCount = 0;
if (PendingAcks.TryDequeue(out var ack))
{
List<PacketAckPacket.PacketsBlock> blocks = new List<PacketAckPacket.PacketsBlock>();
PacketAckPacket.PacketsBlock block = new PacketAckPacket.PacketsBlock {ID = ack};
blocks.Add(block);
while (PendingAcks.TryDequeue(out ack))
{
block = new PacketAckPacket.PacketsBlock {ID = ack};
blocks.Add(block);
}
PacketAckPacket packet = new PacketAckPacket
{
Header = {Reliable = false},
Packets = blocks.ToArray()
};
ackCount = blocks.Count;
SendPacket(packet);
}
return ackCount;
}
/// <summary>
/// Resend unacknowledged packets
/// </summary>
private void ResendUnacked()
{
NetworkManager.OutgoingPacket[] array;
lock (NeedAck)
{
if (NeedAck.Count <= 0) return;
// Create a temporary copy of the outgoing packets array to iterate over
array = new NetworkManager.OutgoingPacket[NeedAck.Count];
NeedAck.Values.CopyTo(array, 0);
}
int now = Environment.TickCount;
// Resend packets
foreach (NetworkManager.OutgoingPacket outgoing in array)
{
if (outgoing.TickCount == 0 || now - outgoing.TickCount <= Client.Settings.RESEND_TIMEOUT) continue;
if (outgoing.ResendCount < Client.Settings.MAX_RESEND_COUNT)
{
if (Client.Settings.LOG_RESENDS)
{
Logger.DebugLog(String.Format("Resending {2} packet #{0}, {1}ms have passed",
outgoing.SequenceNumber, now - outgoing.TickCount, outgoing.Type), Client);
}
// The TickCount will be set to the current time when the packet
// is actually sent out again
outgoing.TickCount = 0;
// Set the resent flag
outgoing.Buffer.Data[0] = (byte)(outgoing.Buffer.Data[0] | Helpers.MSG_RESENT);
// Stats tracking
Interlocked.Increment(ref outgoing.ResendCount);
Interlocked.Increment(ref Stats.ResentPackets);
SendPacketFinal(outgoing);
}
else
{
Logger.DebugLog(String.Format("Dropping packet #{0} after {1} failed attempts",
outgoing.SequenceNumber, outgoing.ResendCount));
lock (NeedAck) NeedAck.Remove(outgoing.SequenceNumber);
}
}
}
private void AckTimer_Elapsed(object obj)
{
SendAcks();
ResendUnacked();
// Start the ACK handling functions again after NETWORK_TICK_INTERVAL milliseconds
if (null == AckTimer) return;
try
{
AckTimer.Change(Settings.NETWORK_TICK_INTERVAL, Timeout.Infinite);
}
catch (Exception)
{ } // *TODO: Review catch all code smell
}
private void StatsTimer_Elapsed(object obj)
{
long old_in = 0, old_out = 0;
var recv = Stats.RecvBytes;
var sent = Stats.SentBytes;
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(recv);
OutBytes.Enqueue(sent);
if (old_in > 0 && old_out > 0)
{
Stats.IncomingBPS = (int)(recv - old_in) / Client.Settings.STATS_QUEUE_SIZE;
Stats.OutgoingBPS = (int)(sent - 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();
Interlocked.Increment(ref Stats.SentPings);
}
}
public sealed class IncomingPacketIDCollection
{
readonly uint[] _items;
HashSet<uint> hashSet;
int first;
int next;
int capacity;
public IncomingPacketIDCollection(int capacity)
{
this.capacity = capacity;
_items = new uint[capacity];
hashSet = new HashSet<uint>();
}
public bool TryEnqueue(uint ack)
{
lock (hashSet)
{
if (hashSet.Add(ack))
{
_items[next] = ack;
next = (next + 1) % capacity;
if (next == first)
{
hashSet.Remove(_items[first]);
first = (first + 1) % capacity;
}
return true;
}
}
return false;
}
}
public class SimulatorDataPool
{
private static Timer InactiveSimReaper;
private static void RemoveOldSims(object state)
{
lock (SimulatorDataPools)
{
int simTimeout = Settings.SIMULATOR_POOL_TIMEOUT;
var reap = (from pool in SimulatorDataPools.Values
where pool.InactiveSince != DateTime.MaxValue
&& pool.InactiveSince.AddMilliseconds(simTimeout) < DateTime.Now
select pool.Handle).ToList();
foreach (var hndl in reap)
{
SimulatorDataPools.Remove(hndl);
}
}
}
public static void SimulatorAdd(Simulator sim)
{
lock (SimulatorDataPools)
{
if (InactiveSimReaper == null)
{
InactiveSimReaper = new Timer(RemoveOldSims, null, TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(3));
}
var pool = GetSimulatorData(sim.Handle);
if (pool.ActiveClients < 1) pool.ActiveClients = 1; else pool.ActiveClients++;
pool.InactiveSince = DateTime.MaxValue;
}
}
public static void SimulatorRelease(Simulator sim)
{
var hndl = sim.Handle;
lock (SimulatorDataPools)
{
SimulatorDataPool dataPool = GetSimulatorData(hndl);
dataPool.ActiveClients--;
if (dataPool.ActiveClients <= 0)
{
dataPool.InactiveSince = DateTime.Now;
}
}
}
public static Dictionary<ulong, SimulatorDataPool> SimulatorDataPools = new Dictionary<ulong, SimulatorDataPool>();
/// <summary>
/// Simulator handle
/// </summary>
public readonly ulong Handle;
/// <summary>
/// Number of GridClients using this datapool
/// </summary>
public int ActiveClients;
/// <summary>
/// Time that the last client disconnected from the simulator
/// </summary>
public DateTime InactiveSince = DateTime.MaxValue;
#region Pooled Items
/// <summary>
/// The cache of prims used and unused in this simulator
/// </summary>
public Dictionary<uint, Primitive> PrimCache = new Dictionary<uint, Primitive>();
/// <summary>
/// Shared parcel info only when POOL_PARCEL_DATA == true
/// </summary>
public InternalDictionary<int, Parcel> Parcels = new InternalDictionary<int, Parcel>();
public int[,] ParcelMap = new int[64, 64];
public bool DownloadingParcelMap = false;
#endregion Pooled Items
private SimulatorDataPool(ulong hndl)
{
this.Handle = hndl;
}
public static SimulatorDataPool GetSimulatorData(ulong hndl)
{
SimulatorDataPool dict;
lock (SimulatorDataPools)
{
if (!SimulatorDataPools.TryGetValue(hndl, out dict))
{
dict = SimulatorDataPools[hndl] = new SimulatorDataPool(hndl);
}
}
return dict;
}
#region Factories
internal Primitive MakePrimitive(uint localID)
{
var dict = PrimCache;
lock (dict)
{
Primitive prim;
if (!dict.TryGetValue(localID, out prim))
{
dict[localID] = prim = new Primitive { RegionHandle = Handle, LocalID = localID };
}
return prim;
}
}
internal bool NeedsRequest(uint localID)
{
var dict = PrimCache;
lock (dict) return !dict.ContainsKey(localID);
}
#endregion Factories
internal void ReleasePrims(List<uint> removePrims)
{
lock (PrimCache)
{
foreach (var u in removePrims)
{
Primitive prim;
if (PrimCache.TryGetValue(u, out prim)) prim.ActiveClients--;
}
}
}
}
}