import { Circuit } from './Circuit'; import { Agent } from './Agent'; import { Caps } from './Caps'; import { Comms } from './Comms'; import { ClientEvents } from './ClientEvents'; import { IObjectStore } from './interfaces/IObjectStore'; import { ObjectStoreFull } from './ObjectStoreFull'; import { ObjectStoreLite } from './ObjectStoreLite'; import { RequestRegionInfoMessage } from './messages/RequestRegionInfo'; import { RegionInfoMessage } from './messages/RegionInfo'; import { Message } from '../enums/Message'; import { Utils } from './Utils'; import { RegionHandshakeMessage } from './messages/RegionHandshake'; import { MapNameRequestMessage } from './messages/MapNameRequest'; import { GridLayerType } from '../enums/GridLayerType'; import { MapBlockReplyMessage } from './messages/MapBlockReply'; import { FilterResponse } from '../enums/FilterResponse'; import * as Long from 'long'; import { Packet } from './Packet'; import { LayerDataMessage } from './messages/LayerData'; import { LayerType } from '../enums/LayerType'; import { Subject, Subscription } from 'rxjs'; import { BitPack } from './BitPack'; import * as builder from 'xmlbuilder'; import { SimAccessFlags } from '../enums/SimAccessFlags'; import { ParcelDwellRequestMessage } from './messages/ParcelDwellRequest'; import { ParcelDwellReplyMessage } from './messages/ParcelDwellReply'; import { Parcel } from './public/Parcel'; import { RegionEnvironment } from './public/RegionEnvironment'; import { Color4 } from './Color4'; import { SkyPreset } from './public/interfaces/SkyPreset'; import { Vector4 } from './Vector4'; import { WaterPreset } from './public/interfaces/WaterPreset'; import { ClientCommands } from './ClientCommands'; import { SimulatorViewerTimeMessageMessage } from './messages/SimulatorViewerTimeMessage'; import { ParcelOverlayMessage } from './messages/ParcelOverlay'; import { ILandBlock } from './interfaces/ILandBlock'; import { LandFlags } from '../enums/LandFlags'; import { ParcelPropertiesRequestMessage } from './messages/ParcelPropertiesRequest'; import { UUID } from './UUID'; import { RegionFlags } from '../enums/RegionFlags'; import { BotOptionFlags } from '../enums/BotOptionFlags'; import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent'; import { PacketFlags } from '../enums/PacketFlags'; import { Vector3 } from './Vector3'; import { Vector2 } from './Vector2'; import { ObjectResolver } from './ObjectResolver'; import { SimStatsMessage } from './messages/SimStats'; import { SimStatsEvent } from '../events/SimStatsEvent'; import { StatID } from '../enums/StatID'; import { CoarseLocationUpdateMessage } from './messages/CoarseLocationUpdate'; import { Avatar } from './public/Avatar'; import { MoneyBalanceReplyMessage } from './messages/MoneyBalanceReply'; import { BalanceUpdatedEvent } from '../events/BalanceUpdatedEvent'; import { Logger } from './Logger'; import { EconomyDataRequestMessage } from './messages/EconomyDataRequest'; import { EconomyDataMessage } from './messages/EconomyData'; export class Region { static CopyMatrix16: number[] = []; static CosineTable16: number[] = []; static DequantizeTable16: number[] = []; static setup = false; static OO_SQRT_2 = 0.7071067811865475244008443621049; regionName: string; regionOwner: UUID; regionID: UUID; regionSizeX = 256; regionSizeY = 256; regionHandle: Long; xCoordinate: number; yCoordinate: number; estateID: number; parentEstateID: number; regionFlags: RegionFlags; mapImage: UUID; simAccess: number; maxAgents: number; billableFactor: number; objectBonusFactor: number; waterHeight: number; terrainRaiseLimit: number; terrainLowerLimit: number; pricePerMeter: number; redirectGridX: number; redirectGridY: number; useEstateSun: boolean; sunHour: number; productSKU: string; productName: string; maxAgents32: number; hardMaxAgents: number; hardMaxObjects: number; cacheID: UUID; cpuClassID: number; cpuRatio: number; coloName: string; terrainBase0: UUID; terrainBase1: UUID; terrainBase2: UUID; terrainBase3: UUID; terrainDetail0: UUID; terrainDetail1: UUID; terrainDetail2: UUID; terrainDetail3: UUID; terrainStartHeight00: number; terrainStartHeight01: number; terrainStartHeight10: number; terrainStartHeight11: number; terrainHeightRange00: number; terrainHeightRange01: number; terrainHeightRange10: number; terrainHeightRange11: number; handshakeComplete = false; handshakeCompleteEvent: Subject = new Subject(); circuit: Circuit; objects: IObjectStore; caps: Caps; comms: Comms; clientEvents: ClientEvents; clientCommands: ClientCommands; options: BotOptionFlags; agent: Agent; messageSubscription: Subscription; parcelPropertiesSubscription: Subscription; terrain: number[][] = []; tilesReceived = 0; terrainComplete = false; terrainCompleteEvent: Subject = new Subject(); parcelsComplete = false; parcelsCompleteEvent: Subject = new Subject(); parcelOverlayComplete = false; parcelOverlayCompleteEvent: Subject = new Subject(); parcelOverlay: ILandBlock[] = []; parcels: { [key: number]: Parcel } = {}; parcelsByUUID: { [key: string]: Parcel } = {}; parcelMap: number[][] = []; parcelCoordinates: { x: number, y: number }[] = []; environment: RegionEnvironment; timeOffset = 0; resolver: ObjectResolver = new ObjectResolver(this); agents: { [key: string]: Avatar } = {}; private uploadCost: number; private parcelOverlayReceived: { [key: number]: Buffer } = {}; static IDCTColumn16(linein: number[], lineout: number[], column: number): void { let total: number; let usize: number; for (let n = 0; n < 16; n++) { total = this.OO_SQRT_2 * linein[column]; for (let u = 1; u < 16; u++) { usize = u * 16; total += linein[usize + column] * this.CosineTable16[usize + n]; } lineout[16 * n + column] = total; } } static IDCTLine16(linein: number[], lineout: number[], line: number): void { const oosob: number = 2.0 / 16.0; const lineSize: number = line * 16; let total = 0; for (let n = 0; n < 16; n++) { total = this.OO_SQRT_2 * linein[lineSize]; for (let u = 1; u < 16; u++) { total += linein[lineSize + u] * this.CosineTable16[u * 16 + n]; } lineout[lineSize + n] = total * oosob; } } static InitialSetup(): void { // Build copy matrix 16 { let diag = false; let right = true; let i = 0; let j = 0; let count = 0; for (let x = 0; x < 16 * 16; x++) { this.CopyMatrix16.push(0); this.DequantizeTable16.push(0); this.CosineTable16.push(0); } while (i < 16 && j < 16) { this.CopyMatrix16[j * 16 + i] = count++; if (!diag) { if (right) { if (i < 16 - 1) { i++; } else { j++; } right = false; diag = true; } else { if (j < 16 - 1) { j++; } else { i++; } right = true; diag = true; } } else { if (right) { i++; j--; if (i === 16 - 1 || j === 0) { diag = false; } } else { i--; j++; if (j === 16 - 1 || i === 0) { diag = false; } } } } } { for (let j = 0; j < 16; j++) { for (let i = 0; i < 16; i++) { this.DequantizeTable16[j * 16 + i] = 1.0 + 2.0 * (i + j); } } } { const hposz: number = Math.PI * 0.5 / 16.0; for (let u = 0; u < 16; u++) { for (let n = 0; n < 16; n++) { this.CosineTable16[u * 16 + n] = Math.cos((2.0 * n + 1.0) * u * hposz); } } } this.setup = true; } private static doesBitmapContainCoordinate(bitmap: Buffer, x: number, y: number): boolean { const mapBlockX = Math.floor(x / 4); const mapBlockY = Math.floor(y / 4); let index = (mapBlockY * 64) + mapBlockX; const bit = index % 8; index >>= 3; return ((bitmap[index] & (1 << bit)) !== 0); } constructor(agent: Agent, clientEvents: ClientEvents, options: BotOptionFlags) { if (!Region.setup) { Region.InitialSetup(); } for (let x = 0; x < 256; x++) { this.terrain.push([]); for (let y = 0; y < 256; y++) { this.terrain[x].push(-1); } } for (let x = 0; x < 64; x++) { this.parcelMap.push([]); for (let y = 0; y < 64; y++) { this.parcelMap[x].push(0); } } this.agent = agent; this.options = options; this.clientEvents = clientEvents; this.circuit = new Circuit(); if (options & BotOptionFlags.LiteObjectStore) { this.objects = new ObjectStoreLite(this.circuit, agent, clientEvents, options); } else { this.objects = new ObjectStoreFull(this.circuit, agent, clientEvents, options); } this.comms = new Comms(this.circuit, agent, clientEvents); this.parcelPropertiesSubscription = this.clientEvents.onParcelPropertiesEvent.subscribe(async(parcelProperties: ParcelPropertiesEvent) => { await this.resolveParcel(parcelProperties); }); this.messageSubscription = this.circuit.subscribeToMessages([ Message.ParcelOverlay, Message.LayerData, Message.SimulatorViewerTimeMessage, Message.SimStats, Message.CoarseLocationUpdate, Message.MoneyBalanceReply ], async(packet: Packet) => { switch (packet.message.id) { case Message.MoneyBalanceReply: { const msg = packet.message as MoneyBalanceReplyMessage; const evt = new BalanceUpdatedEvent(); if (msg.TransactionInfo.Amount === -1) { // This is a requested balance update, so don't sent an event return; } evt.balance = msg.MoneyData.MoneyBalance; evt.transaction = { type: msg.TransactionInfo.TransactionType, amount: msg.TransactionInfo.Amount, from: msg.TransactionInfo.SourceID, to: msg.TransactionInfo.DestID, success: msg.MoneyData.TransactionSuccess, fromGroup: msg.TransactionInfo.IsSourceGroup, toGroup: msg.TransactionInfo.IsDestGroup, description: Utils.BufferToStringSimple(msg.TransactionInfo.ItemDescription) } this.clientEvents.onBalanceUpdated.next(evt); break; } case Message.CoarseLocationUpdate: { const locations: CoarseLocationUpdateMessage = packet.message as CoarseLocationUpdateMessage; const foundAgents: { [key: string]: Vector3 } = {}; for (let x = 0; x < locations.AgentData.length; x++) { const agentData = locations.AgentData[x]; const location = locations.Location[x]; const newPosition = new Vector3([location.X, location.Y, location.Z * 4]); foundAgents[agentData.AgentID.toString()] = newPosition; if (this.agents[agentData.AgentID.toString()] === undefined) { let resolved = await this.clientCommands.grid.avatarKey2Name(agentData.AgentID); if (Array.isArray(resolved)) { resolved = resolved[0]; } if (this.agents[agentData.AgentID.toString()] === undefined) { this.agents[agentData.AgentID.toString()] = new Avatar(agentData.AgentID, resolved.getFirstName(), resolved.getLastName()); this.clientEvents.onAvatarEnteredRegion.next(this.agents[agentData.AgentID.toString()]); } else { this.agents[agentData.AgentID.toString()].coarsePosition = newPosition; } } else { this.agents[agentData.AgentID.toString()].coarsePosition = newPosition; } } const keys = Object.keys(this.agents) for (const agentID of keys) { if (foundAgents[agentID] === undefined) { this.agents[agentID].coarseLeftRegion(); delete this.agents[agentID]; } } break; } case Message.SimStats: { const stats: SimStatsMessage = packet.message as SimStatsMessage; if (stats.Stat.length > 0) { const evt = new SimStatsEvent(); for (const pair of stats.Stat) { const value = pair.StatValue; switch (pair.StatID) { case StatID.TimeDilation: evt.timeDilation = value; break; case StatID.FPS: evt.fps = value; break; case StatID.PhysFPS: evt.physFPS = value; break; case StatID.AgentUPS: evt.agentUPS = value; break; case StatID.FrameMS: evt.frameMS = value; break; case StatID.NetMS: evt.netMS = value; break; case StatID.SimOtherMS: evt.simOtherMS = value; break; case StatID.SimPhysicsMS: evt.simPhysicsMS = value; break; case StatID.AgentMS: evt.agentMS = value; break; case StatID.ImagesMS: evt.imagesMS = value; break; case StatID.ScriptMS: evt.scriptMS = value; break; case StatID.NumTasks: evt.numTasks = value; break; case StatID.NumTasksActive: evt.numTasksActive = value; break; case StatID.NumAgentMain: evt.numAgentMain = value; break; case StatID.NumAgentChild: evt.numAgentChild = value; break; case StatID.NumScriptsActive: evt.numScriptsActive = value; break; case StatID.LSLIPS: evt.lslIPS = value; break; case StatID.InPPS: evt.inPPS = value; break; case StatID.OutPPS: evt.outPPS = value; break; case StatID.PendingDownloads: evt.pendingDownloads = value; break; case StatID.PendingUploads: evt.pendingUploads = value; break; case StatID.VirtualSizeKB: evt.virtualSizeKB = value; break; case StatID.ResidentSizeKB: evt.residentSizeKB = value; break; case StatID.PendingLocalUploads: evt.pendingLocalUploads = value; break; case StatID.TotalUnackedBytes: evt.totalUnackedBytes = value; break; case StatID.PhysicsPinnedTasks: evt.physicsPinnedTasks = value; break; case StatID.PhysicsLODTasks: evt.physicsLODTasks = value; break; case StatID.SimPhysicsStepMS: evt.simPhysicsStepMS = value; break; case StatID.SimPhysicsShapeMS: evt.simPhysicsShapeMS = value; break; case StatID.SimPhysicsOtherMS: evt.simPhysicsOtherMS = value; break; case StatID.SimPhysicsMemory: evt.simPhysicsMemory = value; break; case StatID.ScriptEPS: evt.scriptEPS = value; break; case StatID.SimSpareTime: evt.simSpareTime = value; break; case StatID.SimSleepTime: evt.simSleepTime = value; break; case StatID.IOPumpTime: evt.ioPumpTime = value; break; case StatID.PCTScriptsRun: evt.pctScriptsRun = value; break; case StatID.RegionIdle: evt.regionIdle = value; break; case StatID.RegionIdlePossible: evt.regionIdlePossible = value; break; case StatID.SimAIStepTimeMS: evt.simAIStepTimeMS = value; break; case StatID.SkippedAISilStepsPS: evt.skippedAISilStepsPS = value; break; case StatID.PCTSteppedCharacters: evt.pctSteppedCharacters = value; break; } this.clientEvents.onSimStats.next(evt); } } break; } case Message.ParcelOverlay: { const parcelData: ParcelOverlayMessage = packet.message as ParcelOverlayMessage; const sequence = parcelData.ParcelData.SequenceID; if (this.parcelOverlayReceived[sequence] !== undefined) { this.parcelOverlayReceived = {}; } this.parcelOverlayReceived[sequence] = parcelData.ParcelData.Data; let totalLength = 0; let highestSeq = 0; for (const seq of Object.keys(this.parcelOverlayReceived)) { const sequenceID: number = parseInt(seq, 10); totalLength += this.parcelOverlayReceived[sequenceID].length; if (sequenceID > highestSeq) { highestSeq = sequenceID; } } if (totalLength !== (this.regionSizeX / 4) * (this.regionSizeY / 4)) { // Overlay is incomplete return; } for (let x = 0; x <= highestSeq; x++) { if (this.parcelOverlayReceived[x] === undefined) { // Overlay is incomplete return; } } this.parcelOverlay = []; for (let seq = 0; seq <= highestSeq; seq++) { const data = this.parcelOverlayReceived[seq]; for (let x = 0; x < data.length; x++) { const block = data.readUInt8(x); this.parcelOverlay.push({ landType: block & 0xF, landFlags: block & ~0xF, parcelID: -1 }); } } this.parcelCoordinates = []; let currentParcelID = 0; for (let y = 63; y > -1; y--) { for (let x = 63; x > -1; x--) { if (this.parcelOverlay[(y * 64) + x].parcelID === -1) { this.parcelCoordinates.push({ x, y }); currentParcelID++; this.fillParcel(currentParcelID, x, y); } } } if (!this.parcelOverlayComplete) { this.parcelOverlayComplete = true; this.parcelOverlayCompleteEvent.next(); } this.parcelOverlayReceived = {}; break; } case Message.LayerData: { const layerData: LayerDataMessage = packet.message as LayerDataMessage; const type: LayerType = layerData.LayerID.Type; const nibbler = new BitPack(layerData.LayerData.Data, 0); // stride - unused for now nibbler.UnpackBits(16); const patchSize = nibbler.UnpackBits(8); const headerLayerType = nibbler.UnpackBits(8); switch (type) { case LayerType.Land: if (headerLayerType === type) // Quick sanity check { let x = 0; let y = 0; const patches: number[] = []; for (let xi = 0; xi < 32 * 32; xi++) { patches.push(0); } while (true) { // DecodePatchHeader const quantWBits = nibbler.UnpackBits(8); if (quantWBits === 97) { break; } const dcOffset = nibbler.UnpackFloat(); const range = nibbler.UnpackBits(16); const patchIDs = nibbler.UnpackBits(10); const wordBits = (quantWBits & 0x0f) + 2; x = patchIDs >> 5; y = patchIDs & 0x1F; if (x >= 16 || y >= 16) { console.error('Invalid land packet. x: ' + x + ', y: ' + y + ', patchSize: ' + patchSize); return; } else { // Decode patch let temp = 0; for (let n = 0; n < patchSize * patchSize; n++) { temp = nibbler.UnpackBits(1); if (temp !== 0) { temp = nibbler.UnpackBits(1); if (temp !== 0) { temp = nibbler.UnpackBits(1); if (temp !== 0) { // negative temp = nibbler.UnpackBits(wordBits); patches[n] = temp * -1; } else { // positive temp = nibbler.UnpackBits(wordBits); patches[n] = temp; } } else { for (let o = n; o < patchSize * patchSize; o++) { patches[o] = 0; } break; } } else { patches[n] = 0; } } // Decompress this patch const block: number[] = []; const output: number[] = []; const prequant = (quantWBits >> 4) + 2; const quantize = 1 << prequant; const ooq = 1.0 / quantize; const mult = ooq * range; const addVal = mult * (1 << (prequant - 1)) + dcOffset; if (patchSize === 16) { for (let n = 0; n < 16 * 16; n++) { block.push(patches[Region.CopyMatrix16[n]] * Region.DequantizeTable16[n]) } const ftemp: number[] = []; for (let o = 0; o < 16 * 16; o++) { ftemp.push(o); } for (let o = 0; o < 16; o++) { Region.IDCTColumn16(block, ftemp, o); } for (let o = 0; o < 16; o++) { Region.IDCTLine16(ftemp, block, o); } } else { throw new Error('IDCTPatchLarge not implemented'); } for (let j = 0; j < block.length; j++) { output.push(block[j] * mult + addVal); } let outputIndex = 0; for (let yPoint = y * 16; yPoint < (y + 1) * 16; yPoint++) { for (let xPoint = x * 16; xPoint < (x + 1) * 16; xPoint++) { if (this.terrain[yPoint][xPoint] === -1) { this.tilesReceived++; } this.terrain[yPoint][xPoint] = output[outputIndex++]; } } if (this.tilesReceived === 65536) { this.terrainComplete = true; this.terrainCompleteEvent.next(); } } } } break; } break; } case Message.SimulatorViewerTimeMessage: { const msg = packet.message as SimulatorViewerTimeMessageMessage; const timeStamp = msg.TimeInfo.UsecSinceStart.toNumber() / 1000000; this.timeOffset = (new Date().getTime() / 1000) - timeStamp; break; } } }) } private async resolveParcel(parcelProperties: ParcelPropertiesEvent): Promise { // Get the parcel UUID const msg = new ParcelDwellRequestMessage(); msg.AgentData = { AgentID: this.agent.agentID, SessionID: this.circuit.sessionID }; msg.Data = { LocalID: parcelProperties.LocalID, ParcelID: UUID.zero() }; this.circuit.sendMessage(msg, PacketFlags.Reliable); const dwellReply = await this.circuit.waitForMessage(Message.ParcelDwellReply, 10000, (message: ParcelDwellReplyMessage): FilterResponse => { if (message.Data.LocalID === parcelProperties.LocalID) { return FilterResponse.Finish; } else { return FilterResponse.NoMatch; } }); const parcelID: string = dwellReply.Data.ParcelID.toString(); let parcel = new Parcel(this); if (this.parcelsByUUID[parcelID]) { parcel = this.parcelsByUUID[parcelID]; } parcel.LocalID = parcelProperties.LocalID; parcel.ParcelID = new UUID(dwellReply.Data.ParcelID.toString()); parcel.RegionDenyAgeUnverified = parcelProperties.RegionDenyTransacted; parcel.MediaDesc = parcelProperties.MediaDesc; parcel.MediaHeight = parcelProperties.MediaHeight; parcel.MediaLoop = parcelProperties.MediaLoop; parcel.MediaType = parcelProperties.MediaType; parcel.MediaWidth = parcelProperties.MediaWidth; parcel.ObscureMedia = parcelProperties.ObscureMedia; parcel.ObscureMusic = parcelProperties.ObscureMusic; parcel.AABBMax = parcelProperties.AABBMax; parcel.AABBMin = parcelProperties.AABBMin; parcel.AnyAVSounds = parcelProperties.AnyAVSounds; parcel.Area = parcelProperties.Area; parcel.AuctionID = parcelProperties.AuctionID; parcel.AuthBuyerID = new UUID(parcelProperties.AuthBuyerID.toString()); parcel.Bitmap = parcelProperties.Bitmap; parcel.Category = parcelProperties.Category; parcel.ClaimDate = parcelProperties.ClaimDate; parcel.ClaimPrice = parcelProperties.ClaimPrice; parcel.Desc = parcelProperties.Desc; parcel.Dwell = dwellReply.Data.Dwell; parcel.GroupAVSounds = parcelProperties.GroupAVSounds; parcel.GroupID = new UUID(parcelProperties.GroupID.toString()); parcel.GroupPrims = parcelProperties.GroupPrims; parcel.IsGroupOwned = parcelProperties.IsGroupOwned; parcel.LandingType = parcelProperties.LandingType; parcel.MaxPrims = parcelProperties.MaxPrims; parcel.MediaAutoScale = parcelProperties.MediaAutoScale; parcel.MediaID = new UUID(parcelProperties.MediaID.toString()); parcel.MediaURL = parcelProperties.MediaURL; parcel.MusicURL = parcelProperties.MusicURL; parcel.Name = parcelProperties.Name; parcel.OtherCleanTime = parcelProperties.OtherCleanTime; parcel.OtherCount = parcelProperties.OtherCount; parcel.OtherPrims = parcelProperties.OtherPrims; parcel.OwnerID = new UUID(parcelProperties.OwnerID.toString()); parcel.OwnerPrims = parcelProperties.OwnerPrims; parcel.ParcelFlags = parcelProperties.ParcelFlags; parcel.ParcelPrimBonus = parcelProperties.ParcelPrimBonus; parcel.PassHours = parcelProperties.PassHours; parcel.PassPrice = parcelProperties.PassPrice; parcel.PublicCount = parcelProperties.PublicCount; parcel.RegionDenyAnonymous = parcelProperties.RegionDenyAnonymous; parcel.RegionDenyIdentified = parcelProperties.RegionDenyIdentified; parcel.RegionPushOverride = parcelProperties.RegionPushOverride; parcel.RegionDenyTransacted = parcelProperties.RegionDenyTransacted; parcel.RentPrice = parcelProperties.RentPrice; parcel.RequestResult = parcelProperties.RequestResult; parcel.SalePrice = parcelProperties.SalePrice; parcel.SeeAvs = parcelProperties.SeeAvs; parcel.SelectedPrims = parcelProperties.SelectedPrims; parcel.SelfCount = parcelProperties.SelfCount; parcel.SequenceID = parcelProperties.SequenceID; parcel.SimWideMaxPrims = parcelProperties.SimWideMaxPrims; parcel.SimWideTotalPrims = parcelProperties.SimWideTotalPrims; parcel.SnapSelection = parcelProperties.SnapSelection; parcel.SnapshotID = new UUID(parcelProperties.SnapshotID.toString()); parcel.Status = parcelProperties.Status; parcel.TotalPrims = parcelProperties.TotalPrims; parcel.UserLocation = parcelProperties.UserLocation; parcel.UserLookAt = parcelProperties.UserLookAt; parcel.RegionAllowAccessOverride = parcelProperties.RegionAllowAccessOverride; this.parcels[parcelProperties.LocalID] = parcel; let foundEmpty = false; for (let y = 0; y < 64; y++) { for (let x = 0; x < 64; x++) { if (Region.doesBitmapContainCoordinate(parcel.Bitmap, x * 4, y * 4)) { this.parcelMap[y][x] = parcel.LocalID; } else { if (this.parcelMap[y][x] === 0) { foundEmpty = true; } } } } if (!foundEmpty) { if (!this.parcelsComplete) { this.parcelsComplete = true; this.parcelsCompleteEvent.next(); } } else if (this.parcelsComplete) { this.parcelsComplete = false; } return parcel; } /* // This was useful for debugging, so leaving it here for the future private drawParcelMap() { console.log('===================================================='); for (let y2 = 63; y2 > -1; y2--) { let row = ''; for (let x2 = 0; x2 < 64; x2++) { const parcelID = this.parcelOverlay[(y2 * 64) + x2].parcelID; if (parcelID === -1) { row += 'X'; } else if (parcelID < 10) { row += String(parcelID); } else { row += '#'; } } console.log(row); } } */ private fillParcel(parcelID: number, x: number, y: number): void { if (x < 0 || y < 0 || x > 63 || y > 63) { return; } if (this.parcelOverlay[(y * 64) + x].parcelID !== -1) { return; } this.parcelOverlay[(y * 64) + x].parcelID = parcelID; const flags = this.parcelOverlay[(y * 64) + x].landFlags; if (!(flags & LandFlags.BorderSouth)) { this.fillParcel(parcelID, x, y - 1); } if (!(flags & LandFlags.BorderWest)) { this.fillParcel(parcelID, x - 1, y); } if (x < 63 && !(this.parcelOverlay[(y * 64) + (x + 1)].landFlags & LandFlags.BorderWest)) { this.fillParcel(parcelID, x + 1, y); } if (y < 63 && !(this.parcelOverlay[((y + 1) * 64) + x].landFlags & LandFlags.BorderSouth)) { this.fillParcel(parcelID, x, y + 1); } } public async getUploadCost(): Promise { if (this.uploadCost !== undefined) { return this.uploadCost; } const msg = new EconomyDataRequestMessage(); this.circuit.sendMessage(msg, PacketFlags.Reliable); const economyReply = await this.circuit.waitForMessage(Message.EconomyData, 10000, (_message: EconomyDataMessage): FilterResponse => { return FilterResponse.Finish; }); this.uploadCost = economyReply.Info.PriceUpload; return this.uploadCost; } public getParcelProperties(x: number, y: number): Promise { return new Promise((resolve, reject) => { const request = new ParcelPropertiesRequestMessage(); request.AgentData = { AgentID: this.agent.agentID, SessionID: this.circuit.sessionID }; request.ParcelData = { North: y + 1, East: x + 1, South: y, West: x, SequenceID: -10000, SnapSelection: false }; this.circuit.sendMessage(request, PacketFlags.Reliable); let messageAwait: Subscription | undefined = undefined; let messageWaitTimer: number | undefined = undefined; messageAwait = this.clientEvents.onParcelPropertiesEvent.subscribe(async(parcelProperties: ParcelPropertiesEvent) => { if (Region.doesBitmapContainCoordinate(parcelProperties.Bitmap, x, y)) { if (messageAwait !== undefined) { messageAwait.unsubscribe(); messageAwait = undefined; } if (messageWaitTimer !== undefined) { clearTimeout(messageWaitTimer); messageWaitTimer = undefined; } resolve(await this.resolveParcel(parcelProperties)); } }); messageWaitTimer = setTimeout(() => { if (messageAwait !== undefined) { messageAwait.unsubscribe(); messageAwait = undefined; } if (messageWaitTimer !== undefined) { clearTimeout(messageWaitTimer); messageWaitTimer = undefined; } reject(new Error('Timed out')); }, 10000) as any as number; }); } async getParcels(): Promise { await this.waitForParcelOverlay(); const parcels: Parcel[] = []; for (const parcel of this.parcelCoordinates) { try { parcels.push(await this.getParcelProperties(parcel.x * 4.0, parcel.y * 4.0)); } catch (error) { console.error(error); } } return parcels; } resetParcels(): void { this.parcelMap = []; for (let x = 0; x < 64; x++) { this.parcelMap.push([]); for (let y = 0; y < 64; y++) { this.parcelMap[x].push(0); } } this.parcels = {}; this.parcelsByUUID = {}; this.parcelsComplete = false; } waitForParcelOverlay(): Promise { return new Promise((resolve, reject) => { if (this.parcelOverlayComplete) { resolve(); } else { let timeout: NodeJS.Timeout | null = null; const subscription = this.parcelOverlayCompleteEvent.subscribe(() => { if (timeout !== null) { clearTimeout(timeout); } subscription.unsubscribe(); resolve(); }); timeout = setTimeout(() => { subscription.unsubscribe(); reject(new Error('Timeout waiting for parcel overlay')); }, 10000); } }); } waitForParcels(): Promise { return new Promise((resolve, reject) => { if (this.parcelsComplete) { resolve(); } else { let timeout: NodeJS.Timeout | null = null; const subscription = this.parcelsCompleteEvent.subscribe(() => { if (timeout !== null) { clearTimeout(timeout); } subscription.unsubscribe(); resolve(); }); timeout = setTimeout(() => { subscription.unsubscribe(); reject(new Error('Timeout waiting for parcels')); }, 10000); } }); } waitForTerrain(): Promise { return new Promise((resolve, reject) => { if (this.terrainComplete) { resolve(); } else { let timeout: NodeJS.Timeout | null = null; const subscription = this.terrainCompleteEvent.subscribe(() => { if (timeout !== null) { clearTimeout(timeout); } subscription.unsubscribe(); resolve(); }); timeout = setTimeout(() => { subscription.unsubscribe(); reject(new Error('Timeout waiting for terrain')); }, 10000); } }); } getTerrainHeightAtPoint(x: number, y: number): number { const patchX = Math.floor(x / 16); const patchY = Math.floor(y / 16); x = x % 16; y = y % 16; const p = this.terrain[patchY * 16 + patchX]; if (p === null) { return 0; } return p[y * 16 + x]; } exportXML(): string { const document = builder.create('RegionSettings'); const general = document.ele('General'); general.ele('AllowDamage', (this.regionFlags & RegionFlags.AllowDamage) ? 'True' : 'False'); general.ele('AllowLandResell', !(this.regionFlags & RegionFlags.BlockLandResell) ? 'True' : 'False'); general.ele('AllowLandJoinDivide', (this.regionFlags & RegionFlags.AllowParcelChanges) ? 'True' : 'False'); general.ele('BlockFly', (this.regionFlags & RegionFlags.NoFly) ? 'True' : 'False'); general.ele('BlockLandShowInSearch', (this.regionFlags & RegionFlags.BlockParcelSearch) ? 'True' : 'False'); general.ele('BlockTerraform', (this.regionFlags & RegionFlags.BlockTerraform) ? 'True' : 'False'); general.ele('DisableCollisions', (this.regionFlags & RegionFlags.SkipCollisions) ? 'True' : 'False'); general.ele('DisablePhysics', (this.regionFlags & RegionFlags.SkipPhysics) ? 'True' : 'False'); general.ele('DisableScripts', (this.regionFlags & RegionFlags.EstateSkipScripts) ? 'True' : 'False'); general.ele('MaturityRating', (this.simAccess & SimAccessFlags.Mature & SimAccessFlags.Adult & SimAccessFlags.PG)); general.ele('RestrictPushing', (this.regionFlags & RegionFlags.RestrictPushObject) ? 'True' : 'False'); general.ele('AgentLimit', this.maxAgents); general.ele('ObjectBonus', this.objectBonusFactor); const groundTextures = document.ele('GroundTextures'); groundTextures.ele('Texture1', this.terrainDetail0.toString()); groundTextures.ele('Texture2', this.terrainDetail1.toString()); groundTextures.ele('Texture3', this.terrainDetail2.toString()); groundTextures.ele('Texture4', this.terrainDetail3.toString()); groundTextures.ele('ElevationLowSW', this.terrainStartHeight00); groundTextures.ele('ElevationLowNW', this.terrainStartHeight01); groundTextures.ele('ElevationLowSE', this.terrainStartHeight10); groundTextures.ele('ElevationLowNE', this.terrainStartHeight11); groundTextures.ele('ElevationHighSW', this.terrainHeightRange00); groundTextures.ele('ElevationHighNW', this.terrainHeightRange01); groundTextures.ele('ElevationHighSE', this.terrainHeightRange10); groundTextures.ele('ElevationHighNE', this.terrainHeightRange11); const terrain = document.ele('Terrain'); terrain.ele('WaterHeight', this.waterHeight); terrain.ele('TerrainRaiseLimit', this.terrainRaiseLimit); terrain.ele('TerrainLowerLimit', this.terrainLowerLimit); terrain.ele('UseEstateSun', (this.useEstateSun) ? 'True' : 'False'); terrain.ele('FixedSun', (this.regionFlags & RegionFlags.SunFixed) ? 'True' : 'False'); terrain.ele('SunPosition', this.sunHour); this.environment.getXML(document); return document.end({ pretty: true, allowEmpty: true }); } activateCaps(seedURL: string): void { if (this.caps !== undefined) { this.caps.shutdown(); } this.caps = new Caps(this.agent, seedURL, this.clientEvents); } async handshake(handshake: RegionHandshakeMessage): Promise { this.regionName = Utils.BufferToStringSimple(handshake.RegionInfo.SimName); this.simAccess = handshake.RegionInfo.SimAccess; this.regionFlags = handshake.RegionInfo.RegionFlags; this.regionOwner = handshake.RegionInfo.SimOwner; this.agent.setIsEstateManager(handshake.RegionInfo.IsEstateManager); this.waterHeight = handshake.RegionInfo.WaterHeight; this.billableFactor = handshake.RegionInfo.BillableFactor; this.cacheID = handshake.RegionInfo.CacheID; this.terrainBase0 = handshake.RegionInfo.TerrainBase0; this.terrainBase1 = handshake.RegionInfo.TerrainBase1; this.terrainBase2 = handshake.RegionInfo.TerrainBase2; this.terrainBase3 = handshake.RegionInfo.TerrainBase3; this.terrainDetail0 = handshake.RegionInfo.TerrainDetail0; this.terrainDetail1 = handshake.RegionInfo.TerrainDetail1; this.terrainDetail2 = handshake.RegionInfo.TerrainDetail2; this.terrainDetail3 = handshake.RegionInfo.TerrainDetail3; this.terrainStartHeight00 = handshake.RegionInfo.TerrainStartHeight00; this.terrainStartHeight01 = handshake.RegionInfo.TerrainStartHeight01; this.terrainStartHeight10 = handshake.RegionInfo.TerrainStartHeight10; this.terrainStartHeight11 = handshake.RegionInfo.TerrainStartHeight11; this.terrainHeightRange00 = handshake.RegionInfo.TerrainHeightRange00; this.terrainHeightRange01 = handshake.RegionInfo.TerrainHeightRange01; this.terrainHeightRange10 = handshake.RegionInfo.TerrainHeightRange10; this.terrainHeightRange11 = handshake.RegionInfo.TerrainHeightRange11; this.regionID = handshake.RegionInfo2.RegionID; this.cpuClassID = handshake.RegionInfo3.CPUClassID; this.cpuRatio = handshake.RegionInfo3.CPURatio; this.coloName = Utils.BufferToStringSimple(handshake.RegionInfo3.ColoName); this.productSKU = Utils.BufferToStringSimple(handshake.RegionInfo3.ProductSKU); this.productName = Utils.BufferToStringSimple(handshake.RegionInfo3.ProductName); const request: RequestRegionInfoMessage = new RequestRegionInfoMessage(); request.AgentData = { AgentID: this.agent.agentID, SessionID: this.circuit.sessionID }; this.circuit.sendMessage(request, PacketFlags.Reliable); const regionInfo: RegionInfoMessage = await this.circuit.waitForMessage(Message.RegionInfo, 10000); this.estateID = regionInfo.RegionInfo.EstateID; this.parentEstateID = regionInfo.RegionInfo.ParentEstateID; this.maxAgents = regionInfo.RegionInfo.MaxAgents; this.objectBonusFactor = regionInfo.RegionInfo.ObjectBonusFactor; this.terrainRaiseLimit = regionInfo.RegionInfo.TerrainRaiseLimit; this.terrainLowerLimit = regionInfo.RegionInfo.TerrainLowerLimit; this.pricePerMeter = regionInfo.RegionInfo.PricePerMeter; this.redirectGridX = regionInfo.RegionInfo.RedirectGridX; this.redirectGridY = regionInfo.RegionInfo.RedirectGridY; this.useEstateSun = regionInfo.RegionInfo.UseEstateSun; this.sunHour = regionInfo.RegionInfo.SunHour; this.maxAgents32 = regionInfo.RegionInfo2.MaxAgents32; this.hardMaxAgents = regionInfo.RegionInfo2.HardMaxAgents; this.hardMaxObjects = regionInfo.RegionInfo2.HardMaxObjects; const msg: MapNameRequestMessage = new MapNameRequestMessage(); msg.AgentData = { AgentID: this.agent.agentID, SessionID: this.circuit.sessionID, Flags: GridLayerType.Objects, EstateID: 0, Godlike: false }; msg.NameData = { Name: handshake.RegionInfo.SimName }; this.circuit.sendMessage(msg, PacketFlags.Reliable); await this.circuit.waitForMessage(Message.MapBlockReply, 10000, (filterMsg: MapBlockReplyMessage): FilterResponse => { for (const region of filterMsg.Data) { const name = Utils.BufferToStringSimple(region.Name); if (name.trim().toLowerCase() === this.regionName.trim().toLowerCase()) { this.xCoordinate = region.X; this.yCoordinate = region.Y; this.mapImage = region.MapImageID; const globalPos = Utils.RegionCoordinatesToHandle(this.xCoordinate, this.yCoordinate); this.regionHandle = globalPos.regionHandle; return FilterResponse.Finish; } } return FilterResponse.NoMatch; }); this.environment = new RegionEnvironment(); this.environment.dayCycleKeyframes = []; this.environment.skyPresets = {}; this.environment.water = { blurMultiplier: 0, fresnelOffset: 0, fresnelScale: 0, normalScale: Vector3.getZero(), normalMap: UUID.zero(), scaleAbove: 0, scaleBelow: 0, underWaterFogMod: 0, waterFogColor: Color4.white, waterFogDensity: 0, wave1Dir: Vector2.getZero(), wave2Dir: Vector2.getZero() }; await this.caps.waitForSeedCapability(); try { const response = await this.caps.capsGetXML('EnvironmentSettings'); if (response.length >= 4) { if (Array.isArray(response[1]) && typeof response[2] === 'object' && typeof response[3] === 'object') { for (const kf of response[1]) { this.environment.dayCycleKeyframes.push({ time: kf[0], preset: kf[1] }); } for (const presetKey of Object.keys(response[2])) { const preset = response[2][presetKey]; this.environment.skyPresets[presetKey] = new class implements SkyPreset { ambient = new Vector4(preset['ambient']); blueDensity = new Vector4(preset['blue_density']); blueHorizon = new Vector4(preset['blue_horizon']); cloudColor = new Color4(preset['cloud_color']); cloudPosDensity1 = new Vector4(preset['cloud_pos_density1']); cloudPosDensity2 = new Vector4(preset['cloud_pos_density2']); cloudScale = new Vector4(preset['cloud_scale']); cloudScrollRate = new Vector2(preset['cloud_scroll_rate']); cloudShadow = new Vector4(preset['cloud_shadow']); densityMultiplier = new Vector4(preset['density_multiplier']); distanceMultiplier = new Vector4(preset['distance_multiplier']); eastAngle = preset['east_angle']; enableCloudScroll = { x: preset['enable_cloud_scroll'][0], y: preset['enable_cloud_scroll'][1] }; gamma = new Vector4(preset['gamma']); glow = new Vector4(preset['glow']); hazeDensity = new Vector4(preset['haze_density']); hazeHorizon = new Vector4(preset['haze_horizon']); lightNormal = new Vector4(preset['lightnorm']); maxY = new Vector4(preset['max_y']); starBrightness = preset['start_brightness']; sunAngle = preset['sun_angle']; sunlightColor = new Color4(preset['sunlight_color']); }; } const wat = response[3]; this.environment.water = new class implements WaterPreset { blurMultiplier = wat['blurMultiplier']; fresnelOffset = wat['fresnelOffset']; fresnelScale = wat['fresnelScale']; normalScale = new Vector3(wat['normScale']); normalMap = new UUID(wat['normalMap'].toString()); scaleAbove = wat['scaleAbove']; scaleBelow = wat['scaleBelow']; underWaterFogMod = wat['underWaterFogMod']; waterFogColor = new Color4(wat['waterFogColor']); waterFogDensity = wat['waterFogDensity']; wave1Dir = new Vector2(wat['wave1Dir']); wave2Dir = new Vector2(wat['wave2Dir']); }; } } } catch (e) { Logger.Warn('Unable to get environment settings from region'); } this.handshakeComplete = true; this.handshakeCompleteEvent.next(); } shutdown(): void { this.parcelPropertiesSubscription.unsubscribe(); this.messageSubscription.unsubscribe(); this.comms.shutdown(); this.caps.shutdown(); this.objects.shutdown(); this.resolver.shutdown(); this.circuit.shutdown(); } }