/* * 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.Generic; using System.Threading; using OpenMetaverse.StructuredData; using OpenMetaverse.Packets; using System.Net.Http; using System.Threading.Tasks; namespace OpenMetaverse { #region Enums /// /// Map layer request type /// public enum GridLayerType : uint { /// Objects and terrain are shown Objects = 0, /// Only the terrain is shown, no objects Terrain = 1, /// Overlay showing land for sale and for auction LandForSale = 2 } /// /// Type of grid item, such as telehub, event, popular location, etc. /// public enum GridItemType : uint { /// Telehub Telehub = 1, /// PG rated event PgEvent = 2, /// Mature rated event MatureEvent = 3, /// Popular location Popular = 4, /// Locations of avatar groups in a region AgentLocations = 6, /// Land for sale LandForSale = 7, /// Classified ad Classified = 8, /// Adult rated event AdultEvent = 9, /// Adult land for sale AdultLandForSale = 10 } #endregion Enums #region Structs /// /// Information about a region on the grid map /// public struct GridRegion { /// Sim X position on World Map public int X; /// Sim Y position on World Map public int Y; /// Sim Name (NOTE: In lowercase!) public string Name; /// Access level public SimAccess Access; /// Appears to always be zero (None) public RegionFlags RegionFlags; /// Water Height public byte WaterHeight; /// public byte Agents; /// UUID of the World Map image public UUID MapImageID; /// Unique identifier for this region, a combination of the X /// and Y position public ulong RegionHandle; public override string ToString() { return $"{Name} ({X}/{Y}), Handle: {RegionHandle}, MapImage: {MapImageID}, Access: {Access}"; } public override int GetHashCode() { return X.GetHashCode() ^ Y.GetHashCode(); } public override bool Equals(object obj) { return (obj is GridRegion region) && Equals(region); } private bool Equals(GridRegion region) { return this.X == region.X && this.Y == region.Y; } } /// /// Visual chunk of the grid map /// public struct GridLayer { public int Bottom; public int Left; public int Top; public int Right; public UUID ImageID; public bool ContainsRegion(int x, int y) { return x >= Left && x <= Right && y >= Bottom && y <= Top; } } #endregion Structs #region Map Item Classes /// /// Base class for Map Items /// public abstract class MapItem { /// The Global X position of the item public uint GlobalX; /// The Global Y position of the item public uint GlobalY; /// Get the Local X position of the item public uint LocalX { get { return GlobalX % 256; } } /// Get the Local Y position of the item public uint LocalY { get { return GlobalY % 256; } } /// Get the Handle of the region public ulong RegionHandle { get { return Utils.UIntsToLong((uint)(GlobalX - (GlobalX % 256)), (uint)(GlobalY - (GlobalY % 256))); } } } /// /// Represents an agent or group of agents location /// public class MapAgentLocation : MapItem { public int AvatarCount; public string Identifier; } /// /// Represents a Telehub location /// public class MapTelehub : MapItem { } /// /// Represents a non-adult parcel of land for sale /// public class MapLandForSale : MapItem { public int Size; public int Price; public string Name; public UUID ID; } /// /// Represents an Adult parcel of land for sale /// public class MapAdultLandForSale : MapItem { public int Size; public int Price; public string Name; public UUID ID; } /// /// Represents a PG Event /// public class MapPGEvent : MapItem { public DirectoryManager.EventFlags Flags; // Extra public DirectoryManager.EventCategories Category; // Extra2 public string Description; } /// /// Represents a Mature event /// public class MapMatureEvent : MapItem { public DirectoryManager.EventFlags Flags; // Extra public DirectoryManager.EventCategories Category; // Extra2 public string Description; } /// /// Represents an Adult event /// public class MapAdultEvent : MapItem { public DirectoryManager.EventFlags Flags; // Extra public DirectoryManager.EventCategories Category; // Extra2 public string Description; } #endregion Grid Item Classes /// /// Manages grid-wide tasks such as the world map /// public class GridManager { #region Delegates /// The event subscribers. null if no subscribers private EventHandler m_CoarseLocationUpdate; /// Raises the CoarseLocationUpdate event /// A CoarseLocationUpdateEventArgs object containing the /// data sent by simulator protected virtual void OnCoarseLocationUpdate(CoarseLocationUpdateEventArgs e) { EventHandler handler = m_CoarseLocationUpdate; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_CoarseLocationUpdateLock = new object(); /// Raised when the simulator sends a /// containing the location of agents in the simulator public event EventHandler CoarseLocationUpdate { add { lock (m_CoarseLocationUpdateLock) { m_CoarseLocationUpdate += value; } } remove { lock (m_CoarseLocationUpdateLock) { m_CoarseLocationUpdate -= value; } } } /// The event subscribers. null if no subscribers private EventHandler m_GridRegion; /// Raises the GridRegion event /// A GridRegionEventArgs object containing the /// data sent by simulator protected virtual void OnGridRegion(GridRegionEventArgs e) { EventHandler handler = m_GridRegion; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_GridRegionLock = new object(); /// Raised when the simulator sends a Region Data in response to /// a Map request public event EventHandler GridRegion { add { lock (m_GridRegionLock) { m_GridRegion += value; } } remove { lock (m_GridRegionLock) { m_GridRegion -= value; } } } /// The event subscribers. null if no subscribers private EventHandler m_GridLayer; /// Raises the GridLayer event /// A GridLayerEventArgs object containing the /// data sent by simulator protected virtual void OnGridLayer(GridLayerEventArgs e) { EventHandler handler = m_GridLayer; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_GridLayerLock = new object(); /// Raised when the simulator sends GridLayer object containing /// a map tile coordinates and texture information public event EventHandler GridLayer { add { lock (m_GridLayerLock) { m_GridLayer += value; } } remove { lock (m_GridLayerLock) { m_GridLayer -= value; } } } /// The event subscribers. null if no subscribers private EventHandler m_GridItems; /// Raises the GridItems event /// A GridItemEventArgs object containing the /// data sent by simulator protected virtual void OnGridItems(GridItemsEventArgs e) { EventHandler handler = m_GridItems; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_GridItemsLock = new object(); /// Raised when the simulator sends GridItems object containing /// details on events, land sales at a specific location public event EventHandler GridItems { add { lock (m_GridItemsLock) { m_GridItems += value; } } remove { lock (m_GridItemsLock) { m_GridItems -= value; } } } /// The event subscribers. null if no subscribers private EventHandler m_RegionHandleReply; /// Raises the RegionHandleReply event /// A RegionHandleReplyEventArgs object containing the /// data sent by simulator protected virtual void OnRegionHandleReply(RegionHandleReplyEventArgs e) { EventHandler handler = m_RegionHandleReply; handler?.Invoke(this, e); } /// Thread sync lock object private readonly object m_RegionHandleReplyLock = new object(); /// Raised in response to a Region lookup public event EventHandler RegionHandleReply { add { lock (m_RegionHandleReplyLock) { m_RegionHandleReply += value; } } remove { lock (m_RegionHandleReplyLock) { m_RegionHandleReply -= value; } } } #endregion Delegates /// Unknown public float SunPhase { get; private set; } /// Current direction of the sun public Vector3 SunDirection { get; private set; } /// Current angular velocity of the sun public Vector3 SunAngVelocity { get; private set; } /// Microseconds since the start of SL 4-hour day public ulong TimeOfDay { get; private set; } /// A dictionary of all the regions, indexed by region name internal Dictionary Regions = new Dictionary(); /// A dictionary of all the regions, indexed by region handle internal Dictionary RegionsByHandle = new Dictionary(); private readonly GridClient Client; /// /// Constructor /// /// Instance of GridClient object to associate with this GridManager instance public GridManager(GridClient client) { Client = client; //Client.Network.RegisterCallback(PacketType.MapLayerReply, MapLayerReplyHandler); Client.Network.RegisterCallback(PacketType.MapBlockReply, MapBlockReplyHandler); Client.Network.RegisterCallback(PacketType.MapItemReply, MapItemReplyHandler); Client.Network.RegisterCallback(PacketType.SimulatorViewerTimeMessage, SimulatorViewerTimeMessageHandler); Client.Network.RegisterCallback(PacketType.CoarseLocationUpdate, CoarseLocationHandler, false); Client.Network.RegisterCallback(PacketType.RegionIDAndHandleReply, RegionHandleReplyHandler); } /// /// Request a map layer from simulator capability /// /// Requested public void RequestMapLayer(GridLayerType layer) { Uri cap = Client.Network.CurrentSim.Caps.CapabilityURI("MapLayer"); if (cap != null) { OSDMap payload = new OSDMap {["Flags"] = OSD.FromInteger((int) layer)}; Task req = Client.HttpCapsClient.PostRequestAsync(cap, OSDFormat.Xml, payload, CancellationToken.None, MapLayerResponseHandler); } } /// /// Request a map layer through the simulator /// /// The name of the region /// Requested public void RequestMapRegion(string regionName, GridLayerType layer) { MapNameRequestPacket request = new MapNameRequestPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Flags = (uint)layer, EstateID = 0, // Filled in on the sim Godlike = false // Filled in on the sim }, NameData = { Name = Utils.StringToBytes(regionName) } }; Client.Network.SendPacket(request); } /// /// Return map blocks for a given segment of the world map /// /// /// /// /// /// /// public void RequestMapBlocks(GridLayerType layer, ushort minX, ushort minY, ushort maxX, ushort maxY, bool returnNonExistent) { MapBlockRequestPacket request = new MapBlockRequestPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Flags = (uint)layer } }; request.AgentData.Flags |= (uint)(returnNonExistent ? 0x10000 : 0); request.AgentData.EstateID = 0; // Filled in at the simulator request.AgentData.Godlike = false; // Filled in at the simulator request.PositionData.MinX = minX; request.PositionData.MinY = minY; request.PositionData.MaxX = maxX; request.PositionData.MaxY = maxY; Client.Network.SendPacket(request); } /// /// /// /// /// /// /// /// public List MapItems(ulong regionHandle, GridItemType item, GridLayerType layer, int timeoutMS) { List itemList = null; AutoResetEvent itemsEvent = new AutoResetEvent(false); void Callback(object sender, GridItemsEventArgs e) { if (e.Type == GridItemType.AgentLocations) { itemList = e.Items; itemsEvent.Set(); } } GridItems += Callback; RequestMapItems(regionHandle, item, layer); itemsEvent.WaitOne(timeoutMS, false); GridItems -= Callback; return itemList; } /// /// Request for a given region /// /// Requested region handle /// being requested /// being requested public void RequestMapItems(ulong regionHandle, GridItemType item, GridLayerType layer) { MapItemRequestPacket request = new MapItemRequestPacket { AgentData = { AgentID = Client.Self.AgentID, SessionID = Client.Self.SessionID, Flags = (uint)layer, Godlike = false, // Filled in on the sim EstateID = 0 // Filled in on the sim }, RequestData = { ItemType = (uint)item, RegionHandle = regionHandle } }; Client.Network.SendPacket(request); } /// /// Request data for all mainland (Linden managed) simulators /// public void RequestMainlandSims(GridLayerType layer) { RequestMapBlocks(layer, 0, 0, 65535, 65535, false); } /// /// Request the region handle for the specified region UUID /// /// UUID of the region to look up public void RequestRegionHandle(UUID regionID) { RegionHandleRequestPacket request = new RegionHandleRequestPacket { RequestBlock = new RegionHandleRequestPacket.RequestBlockBlock { RegionID = regionID } }; Client.Network.SendPacket(request); } /// /// Retrieves information using the region name /// /// This function will block until it can find the region or gives up /// Name of requested /// for the /// being requested /// Output for the fetched , /// or empty struct if failure /// True if the was fetched, otherwise false public bool GetGridRegion(string name, GridLayerType layer, out GridRegion region) { if (string.IsNullOrEmpty(name)) { Logger.Log("GetGridRegion called with a null or empty region name", Helpers.LogLevel.Error, Client); region = new GridRegion(); return false; } if (Regions.ContainsKey(name)) { // We already have this GridRegion structure region = Regions[name]; return true; } AutoResetEvent regionEvent = new AutoResetEvent(false); void Callback(object sender, GridRegionEventArgs e) { if (e.Region.Name == name) { regionEvent.Set(); } } GridRegion += Callback; RequestMapRegion(name, layer); regionEvent.WaitOne(Client.Settings.MAP_REQUEST_TIMEOUT, false); GridRegion -= Callback; if (Regions.ContainsKey(name)) { // The region was found after our request region = Regions[name]; return true; } else { Logger.Log($"Could not find region named {name}", Helpers.LogLevel.Warning, Client); region = new GridRegion(); return false; } } protected void MapLayerResponseHandler(HttpResponseMessage response, byte[] responseData, Exception error) { if (error != null) { Logger.Log($"MapLayerResponseHandler error: {error.Message}", Helpers.LogLevel.Error, Client, error); return; } OSD result = OSDParser.Deserialize(responseData); OSDMap body = (OSDMap)result; OSDArray layerData = (OSDArray)body["LayerData"]; if (m_GridLayer != null) { foreach (var data in layerData) { OSDMap thisLayerData = (OSDMap)data; GridLayer layer; layer.Bottom = thisLayerData["Bottom"].AsInteger(); layer.Left = thisLayerData["Left"].AsInteger(); layer.Top = thisLayerData["Top"].AsInteger(); layer.Right = thisLayerData["Right"].AsInteger(); layer.ImageID = thisLayerData["ImageID"].AsUUID(); OnGridLayer(new GridLayerEventArgs(layer)); } } if (body.ContainsKey("MapBlocks")) { // TODO: At one point this will become activated Logger.Log("Got MapBlocks through CAPS, please finish this function!", Helpers.LogLevel.Error, Client); } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void MapBlockReplyHandler(object sender, PacketReceivedEventArgs e) { MapBlockReplyPacket map = (MapBlockReplyPacket)e.Packet; foreach (MapBlockReplyPacket.DataBlock block in map.Data) { if (block.X != 0 || block.Y != 0) { GridRegion region; region.X = block.X; region.Y = block.Y; region.Name = Utils.BytesToString(block.Name); // RegionFlags seems to always be zero here? region.RegionFlags = (RegionFlags)block.RegionFlags; region.WaterHeight = block.WaterHeight; region.Agents = block.Agents; region.Access = (SimAccess)block.Access; region.MapImageID = block.MapImageID; region.RegionHandle = Utils.UIntsToLong((uint)(region.X * 256), (uint)(region.Y * 256)); lock (Regions) { Regions[region.Name] = region; RegionsByHandle[region.RegionHandle] = region; } if (m_GridRegion != null) { OnGridRegion(new GridRegionEventArgs(region)); } } } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void MapItemReplyHandler(object sender, PacketReceivedEventArgs e) { if (m_GridItems != null) { MapItemReplyPacket reply = (MapItemReplyPacket)e.Packet; GridItemType type = (GridItemType)reply.RequestData.ItemType; List items = new List(); foreach (var data in reply.Data) { string name = Utils.BytesToString(data.Name); switch (type) { case GridItemType.AgentLocations: MapAgentLocation location = new MapAgentLocation { GlobalX = data.X, GlobalY = data.Y, Identifier = name, AvatarCount = data.Extra }; items.Add(location); break; case GridItemType.Classified: //FIXME: Logger.Log("FIXME", Helpers.LogLevel.Error, Client); break; case GridItemType.LandForSale: MapLandForSale landsale = new MapLandForSale { GlobalX = data.X, GlobalY = data.Y, ID = data.ID, Name = name, Size = data.Extra, Price = data.Extra2 }; items.Add(landsale); break; case GridItemType.MatureEvent: MapMatureEvent matureEvent = new MapMatureEvent { GlobalX = data.X, GlobalY = data.Y, Description = name, Flags = (DirectoryManager.EventFlags)data.Extra2 }; items.Add(matureEvent); break; case GridItemType.PgEvent: MapPGEvent PGEvent = new MapPGEvent { GlobalX = data.X, GlobalY = data.Y, Description = name, Flags = (DirectoryManager.EventFlags)data.Extra2 }; items.Add(PGEvent); break; case GridItemType.Popular: //FIXME: Logger.Log("FIXME", Helpers.LogLevel.Error, Client); break; case GridItemType.Telehub: MapTelehub teleHubItem = new MapTelehub { GlobalX = data.X, GlobalY = data.Y }; items.Add(teleHubItem); break; case GridItemType.AdultLandForSale: MapAdultLandForSale adultLandsale = new MapAdultLandForSale { GlobalX = data.X, GlobalY = data.Y, ID = data.ID, Name = name, Size = data.Extra, Price = data.Extra2 }; items.Add(adultLandsale); break; case GridItemType.AdultEvent: MapAdultEvent adultEvent = new MapAdultEvent { GlobalX = data.X, GlobalY = data.Y, Description = Utils.BytesToString(data.Name), Flags = (DirectoryManager.EventFlags)data.Extra2 }; items.Add(adultEvent); break; default: Logger.Log($"Unknown map item type: {type}", Helpers.LogLevel.Warning, Client); break; } } OnGridItems(new GridItemsEventArgs(type, items)); } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void SimulatorViewerTimeMessageHandler(object sender, PacketReceivedEventArgs e) { SimulatorViewerTimeMessagePacket time = (SimulatorViewerTimeMessagePacket)e.Packet; SunPhase = time.TimeInfo.SunPhase; SunDirection = time.TimeInfo.SunDirection; SunAngVelocity = time.TimeInfo.SunAngVelocity; TimeOfDay = time.TimeInfo.UsecSinceStart; // TODO: Does anyone have a use for the time stuff? } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void CoarseLocationHandler(object sender, PacketReceivedEventArgs e) { CoarseLocationUpdatePacket coarse = (CoarseLocationUpdatePacket)e.Packet; // populate a dictionary from the packet, for local use Dictionary coarseEntries = new Dictionary(); for (int i = 0; i < coarse.AgentData.Length; i++) { if(coarse.Location.Length > 0) coarseEntries[coarse.AgentData[i].AgentID] = new Vector3((int)coarse.Location[i].X, (int)coarse.Location[i].Y, (int)coarse.Location[i].Z * 4); // the friend we are tracking on radar if (i == coarse.Index.Prey) e.Simulator.preyID = coarse.AgentData[i].AgentID; } // find stale entries (people who left the sim) List removedEntries = e.Simulator.avatarPositions.FindAll( findID => !coarseEntries.ContainsKey(findID)); // anyone who was not listed in the previous update List newEntries = new List(); lock (e.Simulator.avatarPositions.Dictionary) { // remove stale entries foreach(UUID trackedID in removedEntries) e.Simulator.avatarPositions.Dictionary.Remove(trackedID); // add or update tracked info, and record who is new foreach (KeyValuePair entry in coarseEntries) { if (!e.Simulator.avatarPositions.Dictionary.ContainsKey(entry.Key)) newEntries.Add(entry.Key); e.Simulator.avatarPositions.Dictionary[entry.Key] = entry.Value; } } if (m_CoarseLocationUpdate != null) { ThreadPool.QueueUserWorkItem((object o) => { OnCoarseLocationUpdate(new CoarseLocationUpdateEventArgs(e.Simulator, newEntries, removedEntries)); }); } } /// Process an incoming packet and raise the appropriate events /// The sender /// The EventArgs object containing the packet data protected void RegionHandleReplyHandler(object sender, PacketReceivedEventArgs e) { if (m_RegionHandleReply != null) { RegionIDAndHandleReplyPacket reply = (RegionIDAndHandleReplyPacket)e.Packet; OnRegionHandleReply(new RegionHandleReplyEventArgs(reply.ReplyBlock.RegionID, reply.ReplyBlock.RegionHandle)); } } } #region EventArgs classes public class CoarseLocationUpdateEventArgs : EventArgs { public Simulator Simulator { get; } public List NewEntries { get; } public List RemovedEntries { get; } public CoarseLocationUpdateEventArgs(Simulator simulator, List newEntries, List removedEntries) { this.Simulator = simulator; this.NewEntries = newEntries; this.RemovedEntries = removedEntries; } } public class GridRegionEventArgs : EventArgs { public GridRegion Region { get; } public GridRegionEventArgs(GridRegion region) { this.Region = region; } } public class GridLayerEventArgs : EventArgs { public GridLayer Layer { get; } public GridLayerEventArgs(GridLayer layer) { this.Layer = layer; } } public class GridItemsEventArgs : EventArgs { public GridItemType Type { get; } public List Items { get; } public GridItemsEventArgs(GridItemType type, List items) { this.Type = type; this.Items = items; } } public class RegionHandleReplyEventArgs : EventArgs { public UUID RegionID { get; } public ulong RegionHandle { get; } public RegionHandleReplyEventArgs(UUID regionID, ulong regionHandle) { this.RegionID = regionID; this.RegionHandle = regionHandle; } } #endregion }