From 5e235d2db1e4b998ebdccfae9d2030478366b2c0 Mon Sep 17 00:00:00 2001 From: Casper Warden <216465704+casperwardensl@users.noreply.github.com> Date: Tue, 7 Jan 2020 21:01:20 +0000 Subject: [PATCH] - Support specifying URL in loginParameters for connecting to OpenSim - Patch some miscellaneous OpenSim related glitches - Add waitForRegionHandshake function - Add a concurrent promise queue - Fix xml writing of Vector3s - Fix asset downloading on grids without HTTP assets - Fix buildObject to properly orientate prims - Wrangled with CreateSelected all day and it turned out to be an OpenSim bug - LinkFrom function for faster linking - Updated LLSD library to fix LLMesh decoding --- .gitignore | 3 +- example/loginParameters.example.json | 5 +- lib/LoginHandler.ts | 24 ++- lib/classes/Caps.ts | 5 +- lib/classes/EventQueueClient.ts | 15 +- lib/classes/Inventory.ts | 7 + lib/classes/InventoryFolder.ts | 5 + lib/classes/LoginParameters.ts | 1 + lib/classes/LoginResponse.ts | 31 +++- lib/classes/ObjectStoreLite.ts | 16 +- lib/classes/Region.ts | 5 + lib/classes/Utils.ts | 92 +++++++++++ lib/classes/Vector3.ts | 6 +- lib/classes/commands/AssetCommands.ts | 124 +++++++++----- lib/classes/commands/RegionCommands.ts | 219 ++++++++++++++++++------- lib/classes/public/GameObject.ts | 22 +++ lib/events/NewObjectEvent.ts | 1 + package-lock.json | 6 +- package.json | 2 +- 19 files changed, 467 insertions(+), 122 deletions(-) diff --git a/.gitignore b/.gitignore index f5a32ce..43309b4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /example/npm-debug.log /caspertech-node-metaverse-*.tgz /npm-debug.log -/dist \ No newline at end of file +/dist +/exampleMine diff --git a/example/loginParameters.example.json b/example/loginParameters.example.json index dad5bd1..236c2bc 100644 --- a/example/loginParameters.example.json +++ b/example/loginParameters.example.json @@ -2,5 +2,6 @@ "firstName": "Username", "lastName": "Resident", "password": "YourPassword", - "start": "last" //first, last, or login uri like uri:&&& -} \ No newline at end of file + "start": "last", //first, last, or login uri like uri:&&& + "url": "https://login.agni.lindenlab.com/cgi-bin/login.cgi" +} diff --git a/lib/LoginHandler.ts b/lib/LoginHandler.ts index bc01248..05f2b02 100644 --- a/lib/LoginHandler.ts +++ b/lib/LoginHandler.ts @@ -1,6 +1,7 @@ import * as xmlrpc from 'xmlrpc'; import * as crypto from 'crypto'; import * as uuid from 'uuid'; +import * as url from 'url'; import { LoginParameters } from './classes/LoginParameters'; import { LoginResponse } from './classes/LoginResponse'; import { ClientEvents } from './classes/ClientEvents'; @@ -38,13 +39,28 @@ export class LoginHandler { return new Promise((resolve, reject) => { + const loginURI = url.parse(params.url); + + let secure = false; + + if (loginURI.protocol !== undefined && loginURI.protocol.trim().toLowerCase() === 'https:') + { + secure = true; + } + + let port: string | undefined = loginURI.port; + if (port === undefined || port === null) + { + port = secure ? '443' : '80'; + } + const secureClientOptions = { - host: 'login.agni.lindenlab.com', - port: 443, - path: '/cgi-bin/login.cgi', + host: loginURI.hostname, + port: parseInt(port, 10), + path: loginURI.path, rejectUnauthorized: false }; - const client = xmlrpc.createSecureClient(secureClientOptions); + const client = (secure) ? xmlrpc.createSecureClient(secureClientOptions) : xmlrpc.createClient(secureClientOptions); client.methodCall('login_to_simulator', [ { diff --git a/lib/classes/Caps.ts b/lib/classes/Caps.ts index 2935ce3..f3129a9 100644 --- a/lib/classes/Caps.ts +++ b/lib/classes/Caps.ts @@ -172,6 +172,9 @@ export class Caps resolve(body); } }); + }).catch((err) => + { + reject(err); }); }); } @@ -261,7 +264,7 @@ export class Caps } else { - reject(new Error('Capability not available')); + reject(new Error('Capability ' + capability + ' not available')); } }); }); diff --git a/lib/classes/EventQueueClient.ts b/lib/classes/EventQueueClient.ts index e8b2df8..2e7d79a 100644 --- a/lib/classes/EventQueueClient.ts +++ b/lib/classes/EventQueueClient.ts @@ -118,7 +118,14 @@ export class EventQueueClient pprop.AABBMin = new Vector3([parseInt(body['ParcelData'][0]['AABBMin'][0], 10), parseInt(body['ParcelData'][0]['AABBMin'][1], 10), parseInt( body['ParcelData'][0]['AABBMin'][2], 10)]); pprop.AnyAVSounds = body['ParcelData'][0]['AnyAVSounds']; pprop.Area = body['ParcelData'][0]['Area']; - pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0); + try + { + pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0); + } + catch (ignore) + { + // TODO: Opensim glitch + } pprop.AuthBuyerID = new UUID(String(body['ParcelData'][0]['AuthBuyerID'])); pprop.Bitmap = Buffer.from(body['ParcelData'][0]['Bitmap'].toArray()); @@ -167,7 +174,11 @@ export class EventQueueClient pprop.TotalPrims = body['ParcelData'][0]['TotalPrims']; pprop.UserLocation = new Vector3([parseInt(body['ParcelData'][0]['UserLocation'][0], 10), parseInt(body['ParcelData'][0]['UserLocation'][1], 10), parseInt(body['ParcelData'][0]['UserLocation'][2], 10)]); pprop.UserLookAt = new Vector3([parseInt(body['ParcelData'][0]['UserLookAt'][0], 10), parseInt(body['ParcelData'][0]['UserLookAt'][1], 10), parseInt(body['ParcelData'][0]['UserLookAt'][2], 10)]); - pprop.RegionAllowAccessOverride = body['RegionAllowAccessBlock'][0]['RegionAllowAccessOverride']; + if (body['RegionAllowAccessBlock'] !== undefined && body['RegionAllowAccessBlock'].length > 0) + { + // TODO: OpenSim glitch + pprop.RegionAllowAccessOverride = body['RegionAllowAccessBlock'][0]['RegionAllowAccessOverride']; + } this.clientEvents.onParcelPropertiesEvent.next(pprop); break; } diff --git a/lib/classes/Inventory.ts b/lib/classes/Inventory.ts index 3585b45..bef8a17 100644 --- a/lib/classes/Inventory.ts +++ b/lib/classes/Inventory.ts @@ -97,6 +97,13 @@ export class Inventory invItem.inventoryType = parseInt(receivedItem['inv_type'], 10); invItem.type = parseInt(receivedItem['type'], 10); invItem.itemID = item; + + if (receivedItem['permissions']['last_owner_id'] === undefined) + { + // TODO: OpenSim glitch + receivedItem['permissions']['last_owner_id'] = receivedItem['permissions']['owner_id']; + } + invItem.permissions = { baseMask: parseInt(receivedItem['permissions']['base_mask'], 10), nextOwnerMask: parseInt(receivedItem['permissions']['next_owner_mask'], 10), diff --git a/lib/classes/InventoryFolder.ts b/lib/classes/InventoryFolder.ts index a2d5b17..5030508 100644 --- a/lib/classes/InventoryFolder.ts +++ b/lib/classes/InventoryFolder.ts @@ -206,6 +206,11 @@ export class InventoryFolder invItem.itemID = new UUID(item['item_id'].toString()); invItem.description = item['desc']; invItem.type = item['type']; + if (item['permissions']['last_owner_id'] === undefined) + { + // TODO: OpenSim Glitch; + item['permissions']['last_owner_id'] = item['permissions']['owner_id']; + } invItem.permissions = { baseMask: item['permissions']['base_mask'], groupMask: item['permissions']['group_mask'], diff --git a/lib/classes/LoginParameters.ts b/lib/classes/LoginParameters.ts index 481398f..b842773 100644 --- a/lib/classes/LoginParameters.ts +++ b/lib/classes/LoginParameters.ts @@ -4,4 +4,5 @@ export class LoginParameters lastName: string; password: string; start = 'last'; + url = 'https://login.agni.lindenlab.com/cgi-bin/login.cgi'; } diff --git a/lib/classes/LoginResponse.ts b/lib/classes/LoginResponse.ts index 962c475..fc4bd86 100644 --- a/lib/classes/LoginResponse.ts +++ b/lib/classes/LoginResponse.ts @@ -54,6 +54,10 @@ export class LoginResponse const x = parseFloat(num[0]); const y = parseFloat(num[1]); const z = parseFloat(num[2]); + if (isNaN(x) || isNaN(y) || isNaN(z)) + { + throw new Error('Invalid Vector'); + } return new Vector3([x, y, z]); } @@ -79,11 +83,25 @@ export class LoginResponse } if (parsed['position']) { - result['position'] = this.parseVector3('[' + parsed['position'] + ']'); + try + { + result['position'] = this.parseVector3('[' + parsed['position'] + ']'); + } + catch (error) + { + result['position'] = new Vector3([128.0, 128.0, 0.0]); + } } if (parsed['look_at']) { - result['lookAt'] = this.parseVector3('[' + parsed['lookAt'] + ']'); + try + { + result['lookAt'] = this.parseVector3('[' + parsed['lookAt'] + ']'); + } + catch (error) + { + result['lookAt'] = new Vector3([128.0, 128.0, 0.0]); + } } @@ -247,7 +265,14 @@ export class LoginResponse }); break; case 'look_at': - this.agent.cameraLookAt = LoginResponse.parseVector3(val); + try + { + this.agent.cameraLookAt = LoginResponse.parseVector3(val); + } + catch (error) + { + console.error('Invalid look_at from LoginResponse'); + } break; case 'openid_url': this.agent.openID.url = String(val); diff --git a/lib/classes/ObjectStoreLite.ts b/lib/classes/ObjectStoreLite.ts index 445a5ab..c36118d 100644 --- a/lib/classes/ObjectStoreLite.ts +++ b/lib/classes/ObjectStoreLite.ts @@ -71,6 +71,7 @@ export class ObjectStoreLite implements IObjectStore switch (packet.message.id) { case Message.ObjectProperties: + { const objProp = packet.message as ObjectPropertiesMessage; for (const obj of objProp.ObjectData) { @@ -86,14 +87,19 @@ export class ObjectStoreLite implements IObjectStore } } break; + } case Message.ObjectUpdate: + { const objectUpdate = packet.message as ObjectUpdateMessage; this.objectUpdate(objectUpdate); break; + } case Message.ObjectUpdateCached: + { const objectUpdateCached = packet.message as ObjectUpdateCachedMessage; this.objectUpdateCached(objectUpdateCached); break; + } case Message.ObjectUpdateCompressed: { const objectUpdateCompressed = packet.message as ObjectUpdateCompressedMessage; @@ -101,13 +107,17 @@ export class ObjectStoreLite implements IObjectStore break; } case Message.ImprovedTerseObjectUpdate: + { const objectUpdateTerse = packet.message as ImprovedTerseObjectUpdateMessage; this.objectUpdateTerse(objectUpdateTerse); break; + } case Message.KillObject: + { const killObj = packet.message as KillObjectMessage; this.killObject(killObj); break; + } } }); @@ -408,6 +418,7 @@ export class ObjectStoreLite implements IObjectStore newObj.localID = obj.ID; newObj.objectID = obj.FullID; newObj.object = obj; + newObj.createSelected = obj.Flags !== undefined && (obj.Flags & PrimFlags.CreateSelected) !== 0; if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()]) { this.selectedPrimsWithoutUpdate[obj.ID] = true; @@ -716,11 +727,6 @@ export class ObjectStoreLite implements IObjectStore namevalue.value = kv[4]; nv[kv[0]] = namevalue; } - else - { - console.log('namevalue unexpected length: ' + kv.length); - console.log(kv); - } } }); return nv; diff --git a/lib/classes/Region.ts b/lib/classes/Region.ts index 2a03c96..d065df5 100644 --- a/lib/classes/Region.ts +++ b/lib/classes/Region.ts @@ -107,6 +107,9 @@ export class Region terrainHeightRange10: number; terrainHeightRange11: number; + handshakeComplete = false; + handshakeCompleteEvent: Subject = new Subject(); + circuit: Circuit; objects: IObjectStore; caps: Caps; @@ -1170,6 +1173,8 @@ export class Region }; } } + this.handshakeComplete = true; + this.handshakeCompleteEvent.next(); } shutdown() { diff --git a/lib/classes/Utils.ts b/lib/classes/Utils.ts index a0fd4a3..3f626d2 100644 --- a/lib/classes/Utils.ts +++ b/lib/classes/Utils.ts @@ -3,6 +3,8 @@ import { Quaternion } from './Quaternion'; import { GlobalPosition } from './public/interfaces/GlobalPosition'; import { HTTPAssets } from '../enums/HTTPAssets'; import { Vector3 } from './Vector3'; +import { Subject, Subscription } from 'rxjs'; +import Timeout = NodeJS.Timeout; export class Utils { @@ -450,4 +452,94 @@ export class Utils return str.substr(0, index - 1); } } + + static promiseConcurrent(promises: (() => Promise)[], concurrency: number, timeout: number): Promise<{results: T[], errors: Error[]}> + { + return new Promise<{results: T[], errors: Error[]}>(async (resolve, reject) => + { + const originalConcurrency = concurrency; + const promiseQueue: (() => Promise)[] = []; + for (const promise of promises) + { + promiseQueue.push(promise); + } + const slotAvailable: Subject = new Subject(); + const errors: Error[] = []; + const results: T[] = []; + + function waitForAvailable() + { + return new Promise((resolve1, reject1) => + { + const subs = slotAvailable.subscribe(() => + { + subs.unsubscribe(); + resolve1(); + }); + }); + } + + function runPromise(promise: () => Promise) + { + concurrency--; + let timedOut = false; + let timeo: Timeout | undefined = undefined; + promise().then((result: T) => + { + if (timedOut) + { + return; + } + if (timeo !== undefined) + { + clearTimeout(timeo); + } + results.push(result); + concurrency++; + slotAvailable.next(); + }).catch((err) => + { + if (timedOut) + { + return; + } + if (timeo !== undefined) + { + clearTimeout(timeo); + } + errors.push(err); + concurrency++; + slotAvailable.next(); + }); + timeo = setTimeout(() => + { + timedOut = true; + errors.push(new Error('Promise timed out')); + concurrency++; + slotAvailable.next(); + }, timeout); + } + + while (promiseQueue.length > 0) + { + if (concurrency < 1) + { + await waitForAvailable(); + } + else + { + const thunk = promiseQueue.shift(); + if (thunk !== undefined) + { + runPromise(thunk); + } + } + } + while (concurrency < originalConcurrency) + { + await waitForAvailable(); + } + resolve({results: results, errors: errors}); + }); + } } diff --git a/lib/classes/Vector3.ts b/lib/classes/Vector3.ts index a304253..a684a15 100644 --- a/lib/classes/Vector3.ts +++ b/lib/classes/Vector3.ts @@ -14,9 +14,9 @@ export class Vector3 extends vec3 { v = Vector3.getZero(); } - //doc.ele('X', v.x); - //doc.ele('Y', v.y); - //doc.ele('Z', v.z); + doc.ele('X', v.x); + doc.ele('Y', v.y); + doc.ele('Z', v.z); } static fromXMLJS(obj: any, param: string): Vector3 | false diff --git a/lib/classes/commands/AssetCommands.ts b/lib/classes/commands/AssetCommands.ts index cd8cdf5..9a88cc9 100644 --- a/lib/classes/commands/AssetCommands.ts +++ b/lib/classes/commands/AssetCommands.ts @@ -24,47 +24,45 @@ import { HTTPAssets } from '../../enums/HTTPAssets'; export class AssetCommands extends CommandsBase { - async downloadAsset(type: HTTPAssets, uuid: UUID): Promise + async downloadAsset(type: HTTPAssets, uuid: UUID | string): Promise { - const result = await this.currentRegion.caps.downloadAsset(uuid, type); - if (result.toString('UTF-8').trim() === 'Not found!') + if (typeof uuid === 'string') { - throw new Error('Asset not found'); + uuid = new UUID(uuid); } - else if (result.toString('UTF-8').trim() === 'Incorrect Syntax') + try { - throw new Error('Invalid Syntax'); + const result = await this.currentRegion.caps.downloadAsset(uuid, type); + if (result.toString('UTF-8').trim() === 'Not found!') + { + throw new Error('Asset not found'); + } + else if (result.toString('UTF-8').trim() === 'Incorrect Syntax') + { + throw new Error('Invalid Syntax'); + } + return result; + } + catch (error) + { + // Fall back to old asset transfer + const transferParams = Buffer.allocUnsafe(20); + uuid.writeToBuffer(transferParams, 0); + transferParams.writeInt32LE(parseInt(type, 10), 16); + return this.transfer(TransferChannelType.Asset, TransferSourceType.Asset, false, transferParams); } - return result; } - downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise + transfer(channelType: TransferChannelType, sourceType: TransferSourceType, priority: boolean, transferParams: Buffer, outAssetID?: { assetID: UUID }): Promise { return new Promise((resolve, reject) => { - const transferParams = Buffer.allocUnsafe(100); - let pos = 0; - this.agent.agentID.writeToBuffer(transferParams, pos); - pos = pos + 16; - this.circuit.sessionID.writeToBuffer(transferParams, pos); - pos = pos + 16; - ownerID.writeToBuffer(transferParams, pos); - pos = pos + 16; - objectID.writeToBuffer(transferParams, pos); - pos = pos + 16; - itemID.writeToBuffer(transferParams, pos); - pos = pos + 16; - assetID.writeToBuffer(transferParams, pos); - pos = pos + 16; - transferParams.writeInt32LE(type, pos); - const transferID = UUID.random(); - const msg = new TransferRequestMessage(); msg.TransferInfo = { TransferID: transferID, - ChannelType: TransferChannelType.Asset, - SourceType: TransferSourceType.SimInventoryItem, + ChannelType: channelType, + SourceType: sourceType, Priority: 100.0 + (priority ? 1.0 : 0.0), Params: transferParams }; @@ -72,7 +70,7 @@ export class AssetCommands extends CommandsBase this.circuit.sendMessage(msg, PacketFlags.Reliable); let gotInfo = true; let expectedSize = 0; - const packets: {[key: number]: Buffer} = {}; + const packets: { [key: number]: Buffer } = {}; const subscription = this.circuit.subscribeToMessages([ Message.TransferInfo, Message.TransferAbort, @@ -90,16 +88,24 @@ export class AssetCommands extends CommandsBase switch (messg.TransferData.Status) { case TransferStatus.Abort: - throw new Error('Transfer Aborted'); + subscription.unsubscribe(); + reject(new Error('Transfer Aborted')); + break; case TransferStatus.Error: - throw new Error('Error'); + subscription.unsubscribe(); + reject(new Error('Error')); + break; case TransferStatus.Skip: console.error('TransferPacket: Skip! not sure what this means'); break; case TransferStatus.InsufficientPermissions: - throw new Error('Insufficient Permissions'); + subscription.unsubscribe(); + reject(new Error('Insufficient Permissions')); + break; case TransferStatus.NotFound: - throw new Error('Not Found'); + subscription.unsubscribe(); + reject(new Error('Not Found')); + break; } break; } @@ -122,18 +128,25 @@ export class AssetCommands extends CommandsBase } break; case TransferStatus.Abort: - throw new Error('Transfer Aborted'); + subscription.unsubscribe(); + reject(new Error('Transfer Aborted')); + break; case TransferStatus.Error: - throw new Error('Error'); + subscription.unsubscribe(); + reject(new Error('Error')); // See if we get anything else break; case TransferStatus.Skip: console.error('TransferInfo: Skip! not sure what this means'); break; case TransferStatus.InsufficientPermissions: - throw new Error('Insufficient Permissions'); + subscription.unsubscribe(); + reject(new Error('Insufficient Permissions')); + break; case TransferStatus.NotFound: - throw new Error('Not Found'); + subscription.unsubscribe(); + reject(new Error('Not Found')); + break; } break; @@ -145,7 +158,9 @@ export class AssetCommands extends CommandsBase { return; } - throw new Error('Transfer Aborted'); + subscription.unsubscribe(); + reject(new Error('Transfer Aborted')); + return; } } if (gotInfo) @@ -177,7 +192,38 @@ export class AssetCommands extends CommandsBase subscription.unsubscribe(); reject(error); } - }) + }); + }); + } + + downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise + { + return new Promise((resolve, reject) => + { + const transferParams = Buffer.allocUnsafe(100); + let pos = 0; + this.agent.agentID.writeToBuffer(transferParams, pos); + pos = pos + 16; + this.circuit.sessionID.writeToBuffer(transferParams, pos); + pos = pos + 16; + ownerID.writeToBuffer(transferParams, pos); + pos = pos + 16; + objectID.writeToBuffer(transferParams, pos); + pos = pos + 16; + itemID.writeToBuffer(transferParams, pos); + pos = pos + 16; + assetID.writeToBuffer(transferParams, pos); + pos = pos + 16; + transferParams.writeInt32LE(type, pos); + + this.transfer(TransferChannelType.Asset, TransferSourceType.SimInventoryItem, priority, transferParams, outAssetID).then((result) => + { + resolve(result); + }).catch((err) => + { + reject(err); + }); + }); } @@ -366,7 +412,7 @@ export class AssetCommands extends CommandsBase 'next_owner_mask': PermissionMask.All }; const result = await this.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap); - if (result['state'] === 'upload' && result['upload_price']) + if (result['state'] === 'upload' && result['upload_price'] !== undefined) { const cost = result['upload_price']; if (await confirmCostCallback(cost)) diff --git a/lib/classes/commands/RegionCommands.ts b/lib/classes/commands/RegionCommands.ts index 255a1f8..5800293 100644 --- a/lib/classes/commands/RegionCommands.ts +++ b/lib/classes/commands/RegionCommands.ts @@ -35,6 +35,9 @@ import { Parcel } from '../public/Parcel'; import * as Long from 'long'; import * as micromatch from 'micromatch'; import * as LLSD from '@caspertech/llsd'; +import { Subscription } from 'rxjs'; +import Timeout = NodeJS.Timeout; +import { ObjectUpdatedEvent } from '../..'; export class RegionCommands extends CommandsBase { @@ -60,6 +63,64 @@ export class RegionCommands extends CommandsBase return responseMsg.ReplyBlock.RegionHandle; } + waitForHandshake(timeout: number = 10000): Promise + { + return new Promise((resolve, reject) => + { + if (this.currentRegion.handshakeComplete) + { + resolve(); + } + else + { + let handshakeSubscription: Subscription | undefined; + let timeoutTimer: number | undefined; + handshakeSubscription = this.currentRegion.handshakeCompleteEvent.subscribe(() => + { + if (timeoutTimer !== undefined) + { + clearTimeout(timeoutTimer); + timeoutTimer = undefined; + } + if (handshakeSubscription !== undefined) + { + handshakeSubscription.unsubscribe(); + handshakeSubscription = undefined; + resolve(); + } + }); + timeoutTimer = setTimeout(() => + { + if (handshakeSubscription !== undefined) + { + handshakeSubscription.unsubscribe(); + handshakeSubscription = undefined; + } + if (timeoutTimer !== undefined) + { + clearTimeout(timeoutTimer); + timeoutTimer = undefined; + reject(new Error('Timeout')); + } + }, timeout) as any as number; + if (this.currentRegion.handshakeComplete) + { + if (handshakeSubscription !== undefined) + { + handshakeSubscription.unsubscribe(); + handshakeSubscription = undefined; + } + if (timeoutTimer !== undefined) + { + clearTimeout(timeoutTimer); + timeoutTimer = undefined; + } + resolve(); + } + } + }); + } + async deselectObjects(objects: GameObject[]) { // Limit to 255 objects at once @@ -230,8 +291,6 @@ export class RegionCommands extends CommandsBase obj.resolvedAt = new Date().getTime() / 1000; delete uuidMap[objDataUUID]; found = true; - - // console.log(obj.name + ' (' + resolved + ' of ' + objects.length + ')'); } } if (Object.keys(uuidMap).length === 0) @@ -827,7 +886,24 @@ export class RegionCommands extends CommandsBase }); } - private async buildPart(obj: GameObject, posOffset: Vector3, meshCallback: (object: GameObject, meshData: UUID) => UUID | null) + private async createPrimWithRetry(retries: number, obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID) + { + for (retries--; retries > -1; retries--) + { + try + { + const newObject = await this.createPrim(obj, new Vector3(posOffset), new Quaternion(rotOffset), inventoryID); + return newObject; + } + catch (ignore) + { + process.exit(1); + } + } + throw new Error('Failed to create prim with ' + retries + ' tries'); + } + + private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, meshCallback: (object: GameObject, meshData: UUID) => UUID | null) { // Rez a prim let newObject: GameObject; @@ -836,16 +912,16 @@ export class RegionCommands extends CommandsBase const inventoryID: UUID | null = await meshCallback(obj, obj.extraParams.meshData.meshData); if (inventoryID !== null) { - newObject = await this.createPrim(obj, posOffset, inventoryID); + newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset, inventoryID); } else { - newObject = await this.createPrim(obj, posOffset); + newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset); } } else { - newObject = await this.createPrim(obj, posOffset); + newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset); } await newObject.setExtraParams(obj.extraParams); if (obj.TextureEntry !== undefined) @@ -867,71 +943,92 @@ export class RegionCommands extends CommandsBase { return new Promise(async (resolve, reject) => { + const parts: (Promise)[] = []; + console.log('Rezzing prims'); + parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), meshCallback)); - const parts = []; - console.log('Rezzing root prim'); - parts.push(this.buildPart(obj, Vector3.getZero(), meshCallback)); - console.log('Building child prims'); - if (obj.children && obj.Position) + if (obj.children) { + if (obj.Position === undefined) + { + obj.Position = Vector3.getZero(); + } + if (obj.Rotation === undefined) + { + obj.Rotation = Quaternion.getIdentity(); + } for (const child of obj.children) { - parts.push(this.buildPart(child, obj.Position, meshCallback)); + if (child.Position !== undefined && child.Rotation !== undefined) + { + const objPos = new Vector3(obj.Position); + const objRot = new Quaternion(obj.Rotation); + parts.push(this.buildPart(child, objPos, objRot, meshCallback)); + } } } Promise.all(parts).then(async (results) => { console.log('Linking prims'); const rootObj = results[0]; + const childPrims: GameObject[] = []; for (const childObject of results) { if (childObject !== rootObj) { - await childObject.linkTo(rootObj); + childPrims.push(childObject); } } + await rootObj.linkFrom(childPrims); console.log('All done'); resolve(rootObj); }).catch((err) => { reject(err); }); + /* + Utils.promiseConcurrent(parts, 1000, 10000).then(async (results) => + { + console.log('Linking prims'); + const rootObj = results.results[0]; + await rootObj.linkFrom(results.results); + console.log('All done'); + resolve(rootObj); + }).catch((err) => + { + reject(err); + }); + */ }); } - createPrim(obj: GameObject, posOffset: Vector3, inventoryID?: UUID): Promise + createPrim(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID): Promise { - console.log('Create prim'); return new Promise(async (resolve, reject) => { const timeRequested = (new Date().getTime() / 1000) - this.currentRegion.timeOffset; - if (obj.Position === undefined) - { - obj.Position = Vector3.getZero(); - } - if (obj.Rotation === undefined) - { - obj.Rotation = Quaternion.getIdentity(); - } + const objectPosition = new Vector3(obj.Position); + const objectRotation = new Quaternion(obj.Rotation); + const objectScale = new Vector3(obj.Scale); + let finalPos = Vector3.getZero(); let finalRot = Quaternion.getIdentity(); - if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0) + if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0 && objectPosition !== undefined) { - finalPos = obj.Position; - finalRot = obj.Rotation; + finalPos = new Vector3(objectPosition); + finalRot = new Quaternion(objectRotation); } else { - const finalPosOffset: Vector3 = obj.Position; - finalPos = new Vector3(new Vector3(finalPosOffset).add(new Vector3(posOffset))); - finalRot = obj.Rotation; + const adjustedPos = new Vector3(new Vector3(objectPosition).multiplyByQuat(new Quaternion(rotOffset).inverse())); + finalPos = new Vector3(new Vector3(adjustedPos).add(new Vector3(posOffset))); + finalRot = new Quaternion(new Quaternion(objectRotation).add(new Quaternion(rotOffset))); } let msg: ObjectAddMessage | RezObjectMessage | null = null; let fromInventory = false; if (inventoryID === undefined || this.agent.inventory.itemsByID[inventoryID.toString()] === undefined) { - console.log('Regular prim'); // First, rez object in scene msg = new ObjectAddMessage(); msg.AgentData = { @@ -973,7 +1070,6 @@ export class RegionCommands extends CommandsBase } else { - console.log('Rezzing ' + this.agent.inventory.itemsByID[inventoryID.toString()].name); fromInventory = true; const invItem = this.agent.inventory.itemsByID[inventoryID.toString()]; const queryID = UUID.random(); @@ -1021,28 +1117,38 @@ export class RegionCommands extends CommandsBase CRC: 0, }; } - - const objSub = this.currentRegion.clientEvents.onSelectedObjectEvent.subscribe(async (evt: SelectedObjectEvent) => + let objSub: Subscription | undefined = undefined; + let timeout: Timeout | undefined = setTimeout(() => { - if (evt.object.creatorID !== undefined && - evt.object.creatorID.equals(this.agent.agentID) && - evt.object.creationDate !== undefined && - !evt.object.claimedForBuild) + if (objSub !== undefined) { - let claim = false; - const creationDate = evt.object.creationDate.toNumber() / 1000000; - if (fromInventory && inventoryID !== undefined && evt.object.itemID.equals(inventoryID)) - { - claim = true; - } - else if (!fromInventory && evt.object.itemID.equals(UUID.zero()) && creationDate > timeRequested) - { - claim = true; - } - if (claim) + objSub.unsubscribe(); + objSub = undefined; + } + if (timeout !== undefined) + { + clearTimeout(timeout); + timeout = undefined; + } + reject(new Error('Prim never arrived')); + }, 10000); + objSub = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) => + { + if (evt.createSelected && !evt.object.claimedForBuild) + { + if (!fromInventory || (inventoryID !== undefined && evt.object.itemID.equals(inventoryID))) { + if (objSub !== undefined) + { + objSub.unsubscribe(); + objSub = undefined; + } + if (timeout !== undefined) + { + clearTimeout(timeout); + timeout = undefined; + } evt.object.claimedForBuild = true; - objSub.unsubscribe(); if (!fromInventory) { @@ -1063,18 +1169,15 @@ export class RegionCommands extends CommandsBase } } }); - if (obj.Position !== undefined && obj.Scale !== undefined) - { - // Move the camera to look directly at prim for faster capture - const campos = new Vector3(finalPos); - campos.z += 5.0 + obj.Scale.z; - console.log('Moving camera to ' + campos.toString()); - await this.currentRegion.clientCommands.agent.setCamera(campos, finalPos, 4096, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); - } + + // Move the camera to look directly at prim for faster capture + const campos = new Vector3(finalPos); + campos.z += 128.0 + objectScale.z; + await this.currentRegion.clientCommands.agent.setCamera(campos, finalPos, 4096, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); + if (msg !== null) { this.circuit.sendMessage(msg, PacketFlags.Reliable); - console.log('Requested rez'); } }); } diff --git a/lib/classes/public/GameObject.ts b/lib/classes/public/GameObject.ts index f899834..0ea2683 100644 --- a/lib/classes/public/GameObject.ts +++ b/lib/classes/public/GameObject.ts @@ -853,6 +853,28 @@ export class GameObject implements IGameObjectData await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); } + async linkFrom(objects: GameObject[]) + { + const msg = new ObjectLinkMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + ObjectLocalID: this.ID + } + ]; + for (const obj of objects) + { + msg.ObjectData.push( + { + ObjectLocalID: obj.ID + }); + } + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); + } + async setDescription(desc: string) { this.description = desc; diff --git a/lib/events/NewObjectEvent.ts b/lib/events/NewObjectEvent.ts index 63b5daf..8a6b56c 100644 --- a/lib/events/NewObjectEvent.ts +++ b/lib/events/NewObjectEvent.ts @@ -6,4 +6,5 @@ export class NewObjectEvent objectID: UUID; localID: number; object: GameObject; + createSelected: boolean; } diff --git a/package-lock.json b/package-lock.json index 06ad98c..74a3b51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@caspertech/llsd": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@caspertech/llsd/-/llsd-1.0.2.tgz", - "integrity": "sha512-sVgsfk3x6cp/lXG9wdvQqIxKNYI2YqicQNw1TamfghRKwZV50w7pfZlG8pCJLKPKhYwSxSr0e32zn6H0L15k8g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@caspertech/llsd/-/llsd-1.0.3.tgz", + "integrity": "sha512-1GKgUfpBNhEQKwDrMqE6EJ6xc4tMIJX6wNf3ck8jqAYEvCkR3NO99ryLPUZagos6/PlA4TIzVThS6tTr/lHCUQ==", "requires": { "abab": "^1.0.4", "xmldom": "^0.1.27" diff --git a/package.json b/package.json index 1c611ce..5d3fc27 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "typescript": "^3.6.3" }, "dependencies": { - "@caspertech/llsd": "^1.0.2", + "@caspertech/llsd": "^1.0.3", "@types/long": "^4.0.0", "@types/micromatch": "^3.1.0", "@types/mocha": "^5.2.5",