From 001ea8daadb6a85f2151ab21b75fc1ebe994693d Mon Sep 17 00:00:00 2001 From: Casper Warden <216465704+casperwardensl@users.noreply.github.com> Date: Tue, 24 Nov 2020 17:04:53 +0000 Subject: [PATCH] * Add sim stats and add example to examples/Region/Region.ts - Resolves #35 * Add parcel stats (scripts / colliders) and add example to examples/Region/Parcels.ts --- examples/Region/Parcels.ts | 4 + examples/Region/Region.ts | 17 +++ lib/LoginHandler.ts | 12 +- lib/classes/ClientEvents.ts | 4 + lib/classes/EventQueueClient.ts | 47 ++++++++ lib/classes/Region.ts | 146 ++++++++++++++++++++++++- lib/classes/TarWriter.ts | 2 +- lib/classes/commands/AssetCommands.ts | 7 +- lib/classes/commands/ParcelCommands.ts | 54 ++++++++- lib/enums/LandStatFlags.ts | 7 ++ lib/enums/LandStatReportType.ts | 5 + lib/enums/StatID.ts | 44 ++++++++ lib/events/LandStatsEvent.ts | 26 +++++ lib/events/SimStatsEvent.ts | 44 ++++++++ package-lock.json | 15 ++- package.json | 2 +- 16 files changed, 417 insertions(+), 19 deletions(-) create mode 100644 examples/Region/Region.ts create mode 100644 lib/enums/LandStatFlags.ts create mode 100644 lib/enums/LandStatReportType.ts create mode 100644 lib/enums/StatID.ts create mode 100644 lib/events/LandStatsEvent.ts create mode 100644 lib/events/SimStatsEvent.ts diff --git a/examples/Region/Parcels.ts b/examples/Region/Parcels.ts index de5276c..47de6b4 100644 --- a/examples/Region/Parcels.ts +++ b/examples/Region/Parcels.ts @@ -1,4 +1,6 @@ import { ExampleBot } from '../ExampleBot'; +import { LandStatReportType } from '../../lib/enums/LandStatReportType'; +import { LandStatFlags } from '../../lib/enums/LandStatFlags'; class Parcels extends ExampleBot { @@ -15,6 +17,8 @@ class Parcels extends ExampleBot console.log(p.Name); } console.log('========================'); + const stats = await this.bot.clientCommands.parcel.getLandStats(parcels[0].ParcelID, LandStatReportType.Scripts, LandStatFlags.FilterByOwner); + console.log(JSON.stringify(stats, null, 4)); } } diff --git a/examples/Region/Region.ts b/examples/Region/Region.ts new file mode 100644 index 0000000..c149cc0 --- /dev/null +++ b/examples/Region/Region.ts @@ -0,0 +1,17 @@ +import { ExampleBot } from '../ExampleBot'; +import { SimStatsEvent } from '../../lib/events/SimStatsEvent'; + +class Region extends ExampleBot +{ + async onConnected() + { + this.bot.clientEvents.onSimStats.subscribe(this.onSimStats.bind(this)); + } + + onSimStats(stats: SimStatsEvent) + { + console.log(JSON.stringify(stats, null, 4)); + } +} + +new Region().run().then(() => {}).catch((err) => { console.error(err) }); diff --git a/lib/LoginHandler.ts b/lib/LoginHandler.ts index 27cebec..959389e 100644 --- a/lib/LoginHandler.ts +++ b/lib/LoginHandler.ts @@ -43,21 +43,21 @@ export class LoginHandler let secure = false; - if (loginURI.protocol !== undefined && loginURI.protocol.trim().toLowerCase() === 'https:') + if (loginURI.protocol !== null && loginURI.protocol.trim().toLowerCase() === 'https:') { secure = true; } - let port: string | undefined = loginURI.port; - if (port === undefined || port === null) + let port: string | null = loginURI.port; + if (port === null) { port = secure ? '443' : '80'; } const secureClientOptions = { - host: loginURI.hostname, + host: loginURI.hostname || undefined, port: parseInt(port, 10), - path: loginURI.path, + path: loginURI.path || undefined, rejectUnauthorized: false, timeout: 60000 }; @@ -94,7 +94,7 @@ export class LoginHandler 'global-textures' ] } - ], (error, value) => + ], (error: Object, value: any) => { if (error) { diff --git a/lib/classes/ClientEvents.ts b/lib/classes/ClientEvents.ts index 16dbd14..018a34b 100644 --- a/lib/classes/ClientEvents.ts +++ b/lib/classes/ClientEvents.ts @@ -28,6 +28,8 @@ import { GameObject } from './public/GameObject'; import { Avatar } from './public/Avatar'; import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent'; import { InventoryResponseEvent } from '../events/InventoryResponseEvent'; +import { LandStatsEvent } from '../events/LandStatsEvent'; +import { SimStatsEvent } from '../events/SimStatsEvent'; export class ClientEvents { @@ -63,4 +65,6 @@ export class ClientEvents onAvatarLeftRegion: Subject = new Subject(); onRegionTimeDilation: Subject = new Subject(); onBulkUpdateInventoryEvent: Subject = new Subject(); + onLandStatReplyEvent: Subject = new Subject(); + onSimStats: Subject = new Subject(); } diff --git a/lib/classes/EventQueueClient.ts b/lib/classes/EventQueueClient.ts index d2db5e1..88ab65e 100644 --- a/lib/classes/EventQueueClient.ts +++ b/lib/classes/EventQueueClient.ts @@ -20,6 +20,7 @@ import { InventoryFolder } from './InventoryFolder'; import { InventoryItem } from './InventoryItem'; import { Utils } from './Utils'; import { InventoryLibrary } from '../enums/InventoryLibrary'; +import { LandStatsEvent } from '../events/LandStatsEvent'; export class EventQueueClient { @@ -451,6 +452,52 @@ export class EventQueueClient break; } + case 'LandStatReply': + { + let requestData = event.body.RequestData; + if (requestData.length < 1) + { + console.error('LandStatReply invalid RequestData length'); + return; + } + requestData = requestData[0]; + + const evt = new LandStatsEvent(); + evt.totalObjects = Utils.OctetsToUInt32BE(requestData.TotalObjectCount.octets); + evt.reportType = Utils.OctetsToUInt32BE(requestData.ReportType.octets); + evt.requestFlags = Utils.OctetsToUInt32BE(requestData.RequestFlags.octets); + + if (event.body.ReportData.length !== evt.totalObjects || event.body.DataExtended.length !== evt.totalObjects) + { + console.error('LandStatReply: Invalid Reportdata or DataExtended block length'); + return; + } + + for (let x = 0; x < evt.totalObjects; x++) + { + const report = event.body.ReportData[x]; + const extended = event.body.DataExtended[x]; + + evt.objects.push({ + position: new Vector3([report.LocationX, report.LocationY, report.LocationZ]), + ownerName: report.OwnerName, + score: report.score, + objectID: new UUID(report.TaskID.toString()), + localID: Utils.OctetsToUInt32BE(report.TaskLocalID.octets), + objectName: report.TaskName, + monoScore: extended.MonoScore, + ownerID: new UUID(extended.OwnerID.toString()), + parcelName: extended.ParcelName, + publicURLs: extended.PublicURLs, + size: extended.Size, + timestamp: Utils.OctetsToUInt32BE(extended.TimeStamp.octets) + }); + + } + + this.clientEvents.onLandStatReplyEvent.next(evt); + break; + } default: console.log('Unhandled event:'); console.log(JSON.stringify(event, null, 4)); diff --git a/lib/classes/Region.ts b/lib/classes/Region.ts index f0c871f..17658e8 100644 --- a/lib/classes/Region.ts +++ b/lib/classes/Region.ts @@ -47,6 +47,9 @@ 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'; export class Region { @@ -339,11 +342,152 @@ export class Region this.messageSubscription = this.circuit.subscribeToMessages([ Message.ParcelOverlay, Message.LayerData, - Message.SimulatorViewerTimeMessage + Message.SimulatorViewerTimeMessage, + Message.SimStats, ], (packet: Packet) => { switch (packet.message.id) { + 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; diff --git a/lib/classes/TarWriter.ts b/lib/classes/TarWriter.ts index b859439..dc10628 100644 --- a/lib/classes/TarWriter.ts +++ b/lib/classes/TarWriter.ts @@ -117,7 +117,7 @@ export class TarWriter extends Transform this.fileActive = false; } - public _transform(chunk: any, encoding: string, callback: (error?: Error, data?: any) => void): void + public _transform(chunk: any, encoding: 'ascii' | 'utf-8' | 'utf16le' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex', callback: (error?: Error, data?: any) => void): void { this.push(chunk, encoding); callback(); diff --git a/lib/classes/commands/AssetCommands.ts b/lib/classes/commands/AssetCommands.ts index dd209a9..17f64e1 100644 --- a/lib/classes/commands/AssetCommands.ts +++ b/lib/classes/commands/AssetCommands.ts @@ -2,8 +2,6 @@ import { CommandsBase } from './CommandsBase'; import { UUID } from '../UUID'; import * as LLSD from '@caspertech/llsd'; import { Utils } from '../Utils'; -import * as zlib from 'zlib'; -import { ZlibOptions } from 'zlib'; import { TransferRequestMessage } from '../messages/TransferRequest'; import { TransferChannelType } from '../../enums/TransferChannelType'; import { TransferSourceType } from '../../enums/TransferSourceTypes'; @@ -22,7 +20,6 @@ import { InventoryItem } from '../InventoryItem'; import { BulkUpdateInventoryEvent } from '../../events/BulkUpdateInventoryEvent'; import { FilterResponse } from '../../enums/FilterResponse'; import { LLLindenText } from '../LLLindenText'; -import { Logger } from '../Logger'; import { Subscription } from 'rxjs'; export class AssetCommands extends CommandsBase @@ -38,11 +35,11 @@ export class AssetCommands extends CommandsBase try { const result = await this.currentRegion.caps.downloadAsset(uuid, type); - if (result.toString('UTF-8').trim() === 'Not found!') + if (result.toString('utf-8').trim() === 'Not found!') { throw new Error('Asset not found'); } - else if (result.toString('UTF-8').trim() === 'Incorrect Syntax') + else if (result.toString('utf-8').trim() === 'Incorrect Syntax') { throw new Error('Invalid Syntax'); } diff --git a/lib/classes/commands/ParcelCommands.ts b/lib/classes/commands/ParcelCommands.ts index 7a24d45..596f56f 100644 --- a/lib/classes/commands/ParcelCommands.ts +++ b/lib/classes/commands/ParcelCommands.ts @@ -8,6 +8,11 @@ import { Utils } from '../Utils'; import { ParcelInfoReplyEvent } from '../../events/ParcelInfoReplyEvent'; import { PacketFlags } from '../../enums/PacketFlags'; import { Vector3 } from '../Vector3'; +import { LandStatRequestMessage } from '../messages/LandStatRequest'; +import { LandStatReportType } from '../../enums/LandStatReportType'; +import { LandStatReplyMessage } from '../messages/LandStatReply'; +import { LandStatFlags } from '../../enums/LandStatFlags'; +import { LandStatsEvent } from '../../events/LandStatsEvent'; // This class was added to provide a new "Category" of commands, since we don't have any parcel specific functionality yet. @@ -39,7 +44,7 @@ export class ParcelCommands extends CommandsBase this.circuit.sendMessage(msg, PacketFlags.Reliable); // And wait for a reply. It's okay to do this after we send since we haven't yielded until this subscription is set up. - const parcelInfoReply = (await this.circuit.waitForMessage(Message.ParcelInfoReply, 10000, (replyMessage: ParcelInfoReplyMessage): FilterResponse => + const parcelInfoReply = (await this.circuit.waitForMessage(Message.ParcelInfoRequest, 10000, (replyMessage: ParcelInfoReplyMessage): FilterResponse => { // This function is here as a filter to ensure we get the correct message. // It compares every incoming ParcelInfoReplyMessage, checks the ParcelID and compares to the one we requested. @@ -76,4 +81,51 @@ export class ParcelCommands extends CommandsBase AuctionID = parcelInfoReply.Data.AuctionID; }; } + + async getLandStats(parcelID: string | UUID | number, reportType: LandStatReportType, flags: LandStatFlags, filter?: string): Promise + { + if (parcelID instanceof UUID) + { + parcelID = parcelID.toString(); + } + + if (typeof parcelID === 'string') + { + // Find the parcel localID + const parcels = await this.bot.clientCommands.region.getParcels(); + for (const parcel of parcels) + { + if (parcel.ParcelID.toString() === parcelID) + { + parcelID = parcel.LocalID; + break; + } + } + } + + if (typeof parcelID !== 'number') + { + throw new Error('Unable to locate parcel'); + } + + if (filter === undefined) + { + filter = ''; + } + + const msg = new LandStatRequestMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + msg.RequestData = { + ParcelLocalID: parcelID, + ReportType: reportType, + Filter: Utils.StringToBuffer(filter), + RequestFlags: flags + } + + this.circuit.sendMessage(msg, PacketFlags.Reliable); + return Utils.waitOrTimeOut(this.currentRegion.clientEvents.onLandStatReplyEvent, 10000); + } } diff --git a/lib/enums/LandStatFlags.ts b/lib/enums/LandStatFlags.ts new file mode 100644 index 0000000..e718665 --- /dev/null +++ b/lib/enums/LandStatFlags.ts @@ -0,0 +1,7 @@ +export enum LandStatFlags +{ + FilterByParcel = 1 << 0, + FilterByOwner = 1 << 1, + FilterByObject = 1 << 2, + FilterByParcelName = 1 << 3, +} diff --git a/lib/enums/LandStatReportType.ts b/lib/enums/LandStatReportType.ts new file mode 100644 index 0000000..b786521 --- /dev/null +++ b/lib/enums/LandStatReportType.ts @@ -0,0 +1,5 @@ +export enum LandStatReportType +{ + Scripts, + Colliders +} diff --git a/lib/enums/StatID.ts b/lib/enums/StatID.ts new file mode 100644 index 0000000..c13542f --- /dev/null +++ b/lib/enums/StatID.ts @@ -0,0 +1,44 @@ +export enum StatID +{ + TimeDilation = 0, + FPS = 1, + PhysFPS = 2, + AgentUPS = 3, + FrameMS = 4, + NetMS = 5, + SimOtherMS = 6, + SimPhysicsMS = 7, + AgentMS = 8, + ImagesMS = 9, + ScriptMS = 10, + NumTasks = 11, + NumTasksActive = 12, + NumAgentMain = 13, + NumAgentChild = 14, + NumScriptsActive = 15, + LSLIPS = 16, + InPPS = 17, + OutPPS = 18, + PendingDownloads = 19, + PendingUploads = 20, + VirtualSizeKB = 21, + ResidentSizeKB = 22, + PendingLocalUploads = 23, + TotalUnackedBytes = 24, + PhysicsPinnedTasks = 25, + PhysicsLODTasks = 26, + SimPhysicsStepMS = 27, + SimPhysicsShapeMS = 28, + SimPhysicsOtherMS = 29, + SimPhysicsMemory = 30, + ScriptEPS = 31, + SimSpareTime = 32, + SimSleepTime = 33, + IOPumpTime = 34, + PCTScriptsRun = 35, + RegionIdle = 36, + RegionIdlePossible = 37, + SimAIStepTimeMS = 38, + SkippedAISilStepsPS = 39, + PCTSteppedCharacters = 40 +} diff --git a/lib/events/LandStatsEvent.ts b/lib/events/LandStatsEvent.ts new file mode 100644 index 0000000..50bcc79 --- /dev/null +++ b/lib/events/LandStatsEvent.ts @@ -0,0 +1,26 @@ +import { LandStatReportType } from '../enums/LandStatReportType'; +import { LandStatFlags } from '../enums/LandStatFlags'; +import { Vector3 } from '../classes/Vector3'; +import { UUID } from '../classes/UUID'; + +export class LandStatsEvent +{ + totalObjects: number; + reportType: LandStatReportType; + requestFlags: LandStatFlags; + + objects: { + position: Vector3, + ownerName: string, + score: number, + objectID: UUID, + localID: number, + objectName: string, + monoScore: number, + ownerID: UUID, + parcelName: string, + publicURLs: number, + size: number, + timestamp: number, + }[] = []; +} diff --git a/lib/events/SimStatsEvent.ts b/lib/events/SimStatsEvent.ts new file mode 100644 index 0000000..9b6fc1f --- /dev/null +++ b/lib/events/SimStatsEvent.ts @@ -0,0 +1,44 @@ +export class SimStatsEvent +{ + timeDilation: number; + fps: number; + physFPS: number; + agentUPS: number; + frameMS: number; + netMS: number; + simOtherMS: number; + simPhysicsMS: number; + agentMS: number; + imagesMS: number; + scriptMS: number; + numTasks: number; + numTasksActive: number; + numAgentMain: number; + numAgentChild: number; + numScriptsActive: number; + lslIPS: number; + inPPS: number; + outPPS: number; + pendingDownloads: number; + pendingUploads: number; + virtualSizeKB: number; + residentSizeKB: number; + pendingLocalUploads: number; + totalUnackedBytes: number; + physicsPinnedTasks: number; + physicsLODTasks: number; + simPhysicsStepMS: number; + simPhysicsShapeMS: number; + simPhysicsOtherMS: number; + simPhysicsMemory: number; + scriptEPS: number; + simSpareTime: number; + simSleepTime: number; + ioPumpTime: number; + pctScriptsRun: number; + regionIdle: number; + regionIdlePossible: number; + simAIStepTimeMS: number; + skippedAISilStepsPS: number; + pctSteppedCharacters: number; +} diff --git a/package-lock.json b/package-lock.json index 12b7583..0032e12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@caspertech/node-metaverse", - "version": "0.5.13", + "version": "0.5.21", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -98,9 +98,9 @@ "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==" }, "@types/node": { - "version": "10.14.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.19.tgz", - "integrity": "sha512-j6Sqt38ssdMKutXBUuAcmWF8QtHW1Fwz/mz4Y+Wd9mzpBiVFirjpNQf363hG5itkG+yGaD+oiLyb50HxJ36l9Q==" + "version": "14.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz", + "integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw==" }, "@types/request": { "version": "2.48.3", @@ -1420,6 +1420,13 @@ "@types/node": "^10.5.1", "@types/tape": "^4.2.32", "quickselect": "^1.0.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.46.tgz", + "integrity": "sha512-Tice8a+sJtlP9C1EUo0DYyjq52T37b3LexVu3p871+kfIBIN+OQ7PKPei1oF3MgF39olEpUfxaLtD+QFc1k69Q==" + } } }, "readable-stream": { diff --git a/package.json b/package.json index fa08989..fa1f73d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@types/micromatch": "^4.0.1", + "@types/node": "^14.14.9", "mocha": "^8.2.1", "source-map-support": "^0.5.9", "ts-node": "^7.0.1", @@ -35,7 +36,6 @@ "@caspertech/llsd": "^1.0.3", "@types/long": "^4.0.0", "@types/mocha": "^5.2.5", - "@types/node": "^10.14.19", "@types/request": "^2.48.3", "@types/tiny-async-pool": "^1.0.0", "@types/uuid": "^3.4.4",