From 2ff00a30f8a62aecadf4944dbe4659871539f6a8 Mon Sep 17 00:00:00 2001 From: Casper Warden <216465704+casperwardensl@users.noreply.github.com> Date: Thu, 19 Nov 2020 16:51:14 +0000 Subject: [PATCH] Extensive work on building, wearables, assets, inventory, attachments, serialization, etc. Resolves #36 --- lib/Bot.ts | 5 + lib/LoginHandler.ts | 7 +- lib/classes/Agent.ts | 226 ++-- lib/classes/AssetMap.ts | 96 +- lib/classes/Caps.ts | 117 +- lib/classes/Circuit.ts | 117 +- lib/classes/ClientEvents.ts | 9 +- lib/classes/CoalescedGameObject.ts | 66 + lib/classes/EventQueueClient.ts | 70 +- lib/classes/Inventory.ts | 32 +- lib/classes/InventoryFolder.ts | 719 ++++++++++- lib/classes/InventoryItem.ts | 933 ++++++++++++++ lib/classes/LLGesture.ts | 159 +++ lib/classes/LLGestureAnimationStep.ts | 12 + lib/classes/LLGestureChatStep.ts | 10 + lib/classes/LLGestureSoundStep.ts | 12 + lib/classes/LLGestureStep.ts | 6 + lib/classes/LLGestureWaitStep.ts | 11 + lib/classes/LLLindenText.ts | 182 +++ lib/classes/LLWearable.ts | 257 ++-- lib/classes/Logger.ts | 137 ++ lib/classes/ObjectResolver.ts | 441 +++++++ lib/classes/ObjectStoreFull.ts | 26 +- lib/classes/ObjectStoreLite.ts | 207 ++- lib/classes/Region.ts | 17 +- lib/classes/TarArchive.ts | 6 + lib/classes/TarFile.ts | 44 + lib/classes/TarReader.ts | 201 +++ lib/classes/TarWriter.ts | 143 +++ lib/classes/UUID.ts | 13 + lib/classes/Utils.ts | 374 +++++- lib/classes/commands/AgentCommands.ts | 27 +- lib/classes/commands/AssetCommands.ts | 614 +++------ lib/classes/commands/GridCommands.ts | 12 +- lib/classes/commands/InventoryCommands.ts | 18 + lib/classes/commands/RegionCommands.ts | 1390 ++++++++------------- lib/classes/interfaces/IGameObjectData.ts | 1 + lib/classes/interfaces/IObjectStore.ts | 3 + lib/classes/interfaces/IResolveJob.ts | 8 + lib/classes/public/Avatar.ts | 225 +++- lib/classes/public/AvatarQueryResult.ts | 29 + lib/classes/public/GameObject.ts | 963 +++++++++----- lib/classes/public/LLMesh.ts | 53 +- lib/classes/public/Material.ts | 130 +- lib/classes/public/Parcel.ts | 23 + lib/enums/AttachmentPoint.ts | 100 +- lib/enums/InventoryType.ts | 2 +- lib/enums/InventoryTypeLL.ts | 18 - lib/enums/LLGestureAnimationFlags.ts | 5 + lib/enums/LLGestureChatFlags.ts | 4 + lib/enums/LLGestureSoundFlags.ts | 4 + lib/enums/LLGestureStepType.ts | 7 + lib/enums/LLGestureWaitFlags.ts | 6 + lib/enums/ParcelFlags.ts | 4 +- lib/events/BulkUpdateInventoryEvent.ts | 8 + lib/index.ts | 39 +- package-lock.json | 525 ++++++-- package.json | 14 +- 58 files changed, 6659 insertions(+), 2228 deletions(-) create mode 100644 lib/classes/CoalescedGameObject.ts create mode 100644 lib/classes/LLGesture.ts create mode 100644 lib/classes/LLGestureAnimationStep.ts create mode 100644 lib/classes/LLGestureChatStep.ts create mode 100644 lib/classes/LLGestureSoundStep.ts create mode 100644 lib/classes/LLGestureStep.ts create mode 100644 lib/classes/LLGestureWaitStep.ts create mode 100644 lib/classes/LLLindenText.ts create mode 100644 lib/classes/Logger.ts create mode 100644 lib/classes/ObjectResolver.ts create mode 100644 lib/classes/TarArchive.ts create mode 100644 lib/classes/TarFile.ts create mode 100644 lib/classes/TarReader.ts create mode 100644 lib/classes/TarWriter.ts create mode 100644 lib/classes/interfaces/IResolveJob.ts create mode 100644 lib/classes/public/AvatarQueryResult.ts delete mode 100644 lib/enums/InventoryTypeLL.ts create mode 100644 lib/enums/LLGestureAnimationFlags.ts create mode 100644 lib/enums/LLGestureChatFlags.ts create mode 100644 lib/enums/LLGestureSoundFlags.ts create mode 100644 lib/enums/LLGestureStepType.ts create mode 100644 lib/enums/LLGestureWaitFlags.ts create mode 100644 lib/events/BulkUpdateInventoryEvent.ts diff --git a/lib/Bot.ts b/lib/Bot.ts index a226898..570d1e7 100644 --- a/lib/Bot.ts +++ b/lib/Bot.ts @@ -87,6 +87,11 @@ export class Bot } } + getCurrentRegion(): Region + { + return this.currentRegion; + } + async login() { const loginHandler = new LoginHandler(this.clientEvents, this.options); diff --git a/lib/LoginHandler.ts b/lib/LoginHandler.ts index 05f2b02..27cebec 100644 --- a/lib/LoginHandler.ts +++ b/lib/LoginHandler.ts @@ -58,7 +58,8 @@ export class LoginHandler host: loginURI.hostname, port: parseInt(port, 10), path: loginURI.path, - rejectUnauthorized: false + rejectUnauthorized: false, + timeout: 60000 }; const client = (secure) ? xmlrpc.createSecureClient(secureClientOptions) : xmlrpc.createClient(secureClientOptions); client.methodCall('login_to_simulator', @@ -75,8 +76,8 @@ export class LoginHandler 'platform': 'win', 'mac': LoginHandler.GenerateMAC(), 'viewer_digest': uuid.v4(), - 'user_agent': 'nmv', - 'author': 'tom@caspertech.co.uk', + 'user_agent': 'node-metaverse', + 'author': 'nmv@caspertech.co.uk', 'options': [ 'inventory-root', 'inventory-skeleton', diff --git a/lib/classes/Agent.ts b/lib/classes/Agent.ts index d0ca09b..e78a83d 100644 --- a/lib/classes/Agent.ts +++ b/lib/classes/Agent.ts @@ -10,10 +10,8 @@ import { AgentUpdateMessage } from './messages/AgentUpdate'; import { Quaternion } from './Quaternion'; import { AgentState } from '../enums/AgentState'; import { BuiltInAnimations } from '../enums/BuiltInAnimations'; -import * as LLSD from '@caspertech/llsd'; import { AgentWearablesRequestMessage } from './messages/AgentWearablesRequest'; import { AgentWearablesUpdateMessage } from './messages/AgentWearablesUpdate'; -import { InventorySortOrder } from '../enums/InventorySortOrder'; import { RezSingleAttachmentFromInvMessage } from './messages/RezSingleAttachmentFromInv'; import { AttachmentPoint } from '../enums/AttachmentPoint'; import { Utils } from './Utils'; @@ -27,6 +25,11 @@ import { ControlFlags } from '../enums/ControlFlags'; import { PacketFlags } from '../enums/PacketFlags'; import { FolderType } from '../enums/FolderType'; import { Subject, Subscription } from 'rxjs'; +import { InventoryFolder } from './InventoryFolder'; +import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent'; +import { BulkUpdateInventoryMessage } from './messages/BulkUpdateInventory'; +import { InventoryItem } from './InventoryItem'; +import { AgentDataUpdateMessage } from './messages/AgentDataUpdate'; export class Agent { @@ -34,6 +37,7 @@ export class Agent lastName: string; localID = 0; agentID: UUID; + activeGroupID: UUID = UUID.zero(); accessMax: string; regionAccess: string; agentAccess: string; @@ -85,10 +89,12 @@ export class Agent }; agentUpdateTimer: Timer | null = null; estateManager = false; - appearanceSet = false; - appearanceSetEvent: Subject = new Subject(); + + appearanceComplete = false; + appearanceCompleteEvent: Subject = new Subject(); private clientEvents: ClientEvents; + private animSubscription?: Subscription; constructor(clientEvents: ClientEvents) { @@ -153,10 +159,16 @@ export class Agent setCurrentRegion(region: Region) { + if (this.animSubscription !== undefined) + { + this.animSubscription.unsubscribe(); + } this.currentRegion = region; - this.currentRegion.circuit.subscribeToMessages([ - Message.AvatarAnimation - ], this.onAnimState.bind(this)); + this.animSubscription = this.currentRegion.circuit.subscribeToMessages([ + Message.AvatarAnimation, + Message.AgentDataUpdate, + Message.BulkUpdateInventory + ], this.onMessage.bind(this)); } circuitActive() { @@ -194,9 +206,57 @@ export class Agent this.agentUpdateTimer = null; } } - onAnimState(packet: Packet) + onMessage(packet: Packet) { - if (packet.message.id === Message.AvatarAnimation) + if (packet.message.id === Message.AgentDataUpdate) + { + const msg = packet.message as AgentDataUpdateMessage; + this.activeGroupID = msg.AgentData.ActiveGroupID; + } + else if (packet.message.id === Message.BulkUpdateInventory) + { + const msg = packet.message as BulkUpdateInventoryMessage; + const evt = new BulkUpdateInventoryEvent(); + + for (const newItem of msg.ItemData) + { + const folder = this.inventory.findFolder(newItem.FolderID); + const item = new InventoryItem(folder || undefined, this); + item.assetID = newItem.AssetID; + item.inventoryType = newItem.InvType; + item.name = Utils.BufferToStringSimple(newItem.Name); + item.salePrice = newItem.SalePrice; + item.saleType = newItem.SaleType; + item.created = new Date(newItem.CreationDate * 1000); + item.parentID = newItem.FolderID; + item.flags = newItem.Flags; + item.itemID = newItem.ItemID; + item.description = Utils.BufferToStringSimple(newItem.Description); + item.type = newItem.Type; + item.callbackID = newItem.CallbackID; + item.permissions.baseMask = newItem.BaseMask; + item.permissions.groupMask = newItem.GroupMask; + item.permissions.nextOwnerMask = newItem.NextOwnerMask; + item.permissions.ownerMask = newItem.OwnerMask; + item.permissions.everyoneMask = newItem.EveryoneMask; + item.permissions.owner = newItem.OwnerID; + item.permissions.creator = newItem.CreatorID; + item.permissions.group = newItem.GroupID; + item.permissions.groupOwned = newItem.GroupOwned; + evt.itemData.push(item); + } + for (const newFolder of msg.FolderData) + { + const fld = new InventoryFolder(this.inventory.main, this); + fld.typeDefault = newFolder.Type; + fld.name = Utils.BufferToStringSimple(newFolder.Name); + fld.folderID = newFolder.FolderID; + fld.parentID = newFolder.ParentID; + evt.folderData.push(fld); + } + this.clientEvents.onBulkUpdateInventoryEvent.next(evt); + } + else if (packet.message.id === Message.AvatarAnimation) { const animMsg = packet.message as AvatarAnimationMessage; if (animMsg.Sender.ID.toString() === this.agentID.toString()) @@ -220,7 +280,22 @@ export class Agent } } } - setInitialAppearance() + + async getWearables(): Promise + { + for (const uuid of Object.keys(this.inventory.main.skeleton)) + { + const folder = this.inventory.main.skeleton[uuid]; + if (folder.typeDefault === FolderType.CurrentOutfit) + { + await folder.populate(false); + return folder; + } + } + throw new Error('Unable to get wearables from inventory') + } + + async setInitialAppearance() { const circuit = this.currentRegion.circuit; const wearablesRequest: AgentWearablesRequestMessage = new AgentWearablesRequestMessage(); @@ -229,95 +304,70 @@ export class Agent SessionID: circuit.sessionID }; circuit.sendMessage(wearablesRequest, PacketFlags.Reliable); - circuit.waitForMessage(Message.AgentWearablesUpdate, 10000).then((wearables: AgentWearablesUpdateMessage) => + + const wearables: AgentWearablesUpdateMessage = await circuit.waitForMessage(Message.AgentWearablesUpdate, 10000); + + if (!this.wearables || wearables.AgentData.SerialNum > this.wearables.serialNumber) { - if (!this.wearables || wearables.AgentData.SerialNum > this.wearables.serialNumber) + this.wearables = { + serialNumber: wearables.AgentData.SerialNum, + attachments: [] + }; + for (const wearable of wearables.WearableData) { - this.wearables = { - serialNumber: wearables.AgentData.SerialNum, - attachments: [] - }; - for (const wearable of wearables.WearableData) + if (this.wearables && this.wearables.attachments) { - if (this.wearables && this.wearables.attachments) + this.wearables.attachments.push({ + itemID: wearable.ItemID, + assetID: wearable.AssetID, + wearableType: wearable.WearableType + }); + } + } + } + + + const currentOutfitFolder = await this.getWearables(); + const wornObjects = this.currentRegion.objects.getObjectsByParent(this.localID); + for (const item of currentOutfitFolder.items) + { + if (item.type === 6) + { + let found = false; + for (const obj of wornObjects) + { + if (obj.hasNameValueEntry('AttachItemID')) { - this.wearables.attachments.push({ - itemID: wearable.ItemID, - assetID: wearable.AssetID, - wearableType: wearable.WearableType - }); + if (item.itemID.toString() === obj.getNameValueEntry('AttachItemID')) + { + found = true; + } } } - } - for (const uuid of Object.keys(this.inventory.main.skeleton)) - { - const folder = this.inventory.main.skeleton[uuid]; - if (folder.typeDefault === FolderType.CurrentOutfit) + if (!found) { - const folderID = folder.folderID; - - const requestFolder = { - folder_id: new LLSD.UUID(folderID), - owner_id: new LLSD.UUID(this.agentID), - fetch_folders: true, - fetch_items: true, - sort_order: InventorySortOrder.ByName + const rsafi = new RezSingleAttachmentFromInvMessage(); + rsafi.AgentData = { + AgentID: this.agentID, + SessionID: circuit.sessionID }; - const requestedFolders = { - 'folders': [ - requestFolder - ] + rsafi.ObjectData = { + ItemID: new UUID(item.itemID.toString()), + OwnerID: this.agentID, + AttachmentPt: 0x80 | AttachmentPoint.Default, + ItemFlags: item.flags, + GroupMask: item.permissions.groupMask, + EveryoneMask: item.permissions.everyoneMask, + NextOwnerMask: item.permissions.nextOwnerMask, + Name: Utils.StringToBuffer(item.name), + Description: Utils.StringToBuffer(item.description) }; - this.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) => - { - const currentOutfitFolderContents = folderContents['folders'][0]['items']; - const wornObjects = this.currentRegion.objects.getObjectsByParent(this.localID); - for (const item of currentOutfitFolderContents) - { - if (item.type === 6) - { - let found = false; - for (const obj of wornObjects) - { - if (obj.hasNameValueEntry('AttachItemID')) - { - if (item['item_id'].toString() === obj.getNameValueEntry('AttachItemID')) - { - found = true; - } - } - } - - if (!found) - { - const rsafi = new RezSingleAttachmentFromInvMessage(); - rsafi.AgentData = { - AgentID: this.agentID, - SessionID: circuit.sessionID - }; - rsafi.ObjectData = { - ItemID: new UUID(item['item_id'].toString()), - OwnerID: this.agentID, - AttachmentPt: 0x80 | AttachmentPoint.Default, - ItemFlags: item['flags'], - GroupMask: item['permissions']['group_mask'], - EveryoneMask: item['permissions']['everyone_mask'], - NextOwnerMask: item['permissions']['next_owner_mask'], - Name: Utils.StringToBuffer(item['name']), - Description: Utils.StringToBuffer(item['desc']) - }; - circuit.sendMessage(rsafi, PacketFlags.Reliable); - } - } - } - }); - + circuit.sendMessage(rsafi, PacketFlags.Reliable); } } - - this.appearanceSet = true; - this.appearanceSetEvent.next(); - }); + } + this.appearanceComplete = true; + this.appearanceCompleteEvent.next(); } } diff --git a/lib/classes/AssetMap.ts b/lib/classes/AssetMap.ts index af6beb7..151b541 100644 --- a/lib/classes/AssetMap.ts +++ b/lib/classes/AssetMap.ts @@ -1,21 +1,89 @@ +import { InventoryItem } from './InventoryItem'; +import { Material } from './public/Material'; + export class AssetMap { mesh: { [key: string]: { - objectName: string, - objectDescription: string, - assetID: string + name: string, + description: string, + item: InventoryItem | null } } = {}; - textures: { [key: string]: string } = {}; - animations: { [key: string]: string } = {}; - sounds: { [key: string]: string } = {}; - gestures: { [key: string]: string } = {}; - landmarks: { [key: string]: string } = {}; - callingcards: { [key: string]: string } = {}; - scripts: { [key: string]: string } = {}; - clothing: { [key: string]: string } = {}; - notecards: { [key: string]: string } = {}; - bodyparts: { [key: string]: string } = {}; - objects: { [key: string]: Buffer | null } = {}; + textures: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + materials: { + [key: string]: Material | null + } = {}; + animations: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + sounds: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + gestures: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + callingcards: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + scripts: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + clothing: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + notecards: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + bodyparts: { + [key: string]: { + name?: string, + description?: string, + item: InventoryItem | null + } + } = {}; + objects: { + [key: string]: InventoryItem | null + } = {}; + temporaryInventory: { + [key: string]: InventoryItem + } = {}; + byUUID: { + [key: string]: InventoryItem + } = {}; + pending: {[key: string]: boolean} = {}; } diff --git a/lib/classes/Caps.ts b/lib/classes/Caps.ts index f3129a9..716c2cf 100644 --- a/lib/classes/Caps.ts +++ b/lib/classes/Caps.ts @@ -153,6 +153,10 @@ export class Caps { return new Promise((resolve, reject) => { + if (type === HTTPAssets.ASSET_LSL_TEXT || type === HTTPAssets.ASSET_NOTECARD) + { + throw new Error('Invalid Syntax'); + } this.getCapability('ViewerAsset').then((capURL) => { const assetURL = capURL + '/?' + type + '_id=' + uuid.toString(); @@ -206,6 +210,33 @@ export class Caps }); } + requestPut(capURL: string, data: string | Buffer, contentType: string): Promise + { + return new Promise((resolve, reject) => + { + request({ + 'headers': { + 'Content-Length': data.length, + 'Content-Type': contentType + }, + 'uri': capURL, + 'body': data, + 'rejectUnauthorized': false, + 'method': 'PUT' + }, (err, res, body) => + { + if (err) + { + reject(err); + } + else + { + resolve({status: res.statusCode, body: body}); + } + }); + }); + } + requestGet(requestURL: string): Promise { return new Promise((resolve, reject) => @@ -247,6 +278,12 @@ export class Caps }); } + async isCapAvailable(capability: string): Promise + { + await this.waitForSeedCapability(); + return (this.capabilities[capability] !== undefined); + } + getCapability(capability: string): Promise { return new Promise((resolve, reject) => @@ -342,7 +379,47 @@ export class Caps return new Promise(async (resolve, reject) => { const xml = LLSD.LLSD.formatXML(data); - this.request(capURL, xml, 'application/llsd+xml').then((resp: ICapResponse) => + this.request(capURL, xml, 'application/llsd+xml').then(async (resp: ICapResponse) => + { + let result: any = null; + try + { + result = LLSD.LLSD.parseXML(resp.body); + resolve(result); + } + catch (err) + { + if (resp.status === 201) + { + resolve({}); + } + else if (resp.status === 403) + { + reject(new Error('Access Denied')); + } + else if (resp.status === 404) + { + reject(new Error('Not found')); + } + else + { + reject(resp.body); + } + } + }).catch((err) => + { + console.error(err); + reject(err); + }); + }); + } + + capsPerformXMLPut(capURL: string, data: any): Promise + { + return new Promise(async (resolve, reject) => + { + const xml = LLSD.LLSD.formatXML(data); + this.requestPut(capURL, xml, 'application/llsd+xml').then((resp: ICapResponse) => { let result: any = null; try @@ -484,6 +561,44 @@ export class Caps } } + async capsPutXML(capability: string | [string, {[key: string]: string}], data: any): Promise + { + let capName = ''; + let queryParams: {[key: string]: string} = {}; + if (typeof capability === 'string') + { + capName = capability; + } + else + { + capName = capability[0]; + queryParams = capability[1]; + } + + await this.waitForCapTimeout(capName); + + let capURL = await this.getCapability(capName); + if (Object.keys(queryParams).length > 0) + { + const parsedURL = url.parse(capURL, true); + for (const key of Object.keys(queryParams)) + { + parsedURL.query[key] = queryParams[key]; + } + capURL = url.format(parsedURL); + } + try + { + return await this.capsPerformXMLPut(capURL, data); + } + catch (error) + { + console.log('Error with cap ' + capName); + console.log(error); + throw error; + } + } + shutdown() { this.onGotSeedCap.complete(); diff --git a/lib/classes/Circuit.ts b/lib/classes/Circuit.ts index a7a7b92..ec1aa16 100644 --- a/lib/classes/Circuit.ts +++ b/lib/classes/Circuit.ts @@ -21,6 +21,8 @@ import Timer = NodeJS.Timer; import { PacketFlags } from '../enums/PacketFlags'; import { AssetType } from '../enums/AssetType'; import { Utils } from './Utils'; +import * as Long from 'long'; +import { AssetUploadCompleteMessage } from './messages/AssetUploadComplete'; export class Circuit { @@ -85,7 +87,99 @@ export class Circuit return packet.sequenceNumber; } - XferFile(fileName: string, deleteOnCompletion: boolean, useBigPackets: boolean, vFileID: UUID, vFileType: AssetType, fromCache: boolean): Promise + private sendXferPacket(xferID: Long, packetID: number, data: Buffer, pos: {position: number}) + { + const sendXfer = new SendXferPacketMessage(); + let final = false; + sendXfer.XferID = { + ID: xferID, + Packet: packetID + }; + const packetLength = Math.min(data.length - pos.position, 1000); + if (packetLength < 1000) + { + sendXfer.XferID.Packet = (sendXfer.XferID.Packet | 0x80000000) >>> 0; + final = true; + } + if (packetID === 0) + { + const packet = Buffer.allocUnsafe(packetLength + 4); + packet.writeUInt32LE(data.length, 0); + data.copy(packet, 4, 0, packetLength); + sendXfer.DataPacket = { + Data: packet + }; + pos.position += packetLength; + } + else + { + const packet = data.slice(pos.position, pos.position + packetLength); + sendXfer.DataPacket = { + Data: packet + }; + pos.position += packetLength; + } + console.log('Sent packet ' + packetID + ', ' + packetLength + ' bytes'); + this.sendMessage(sendXfer, PacketFlags.Reliable); + if (final) + { + pos.position = -1; + } + } + + XferFileUp(xferID: Long, data: Buffer) + { + return new Promise((resolve, reject) => + { + let packetID = 0; + const pos = { + position: 0 + }; + + const subs = this.subscribeToMessages([ + Message.AbortXfer, + Message.ConfirmXferPacket + ], (packet: Packet) => + { + switch (packet.message.id) + { + case Message.ConfirmXferPacket: + { + const msg = packet.message as ConfirmXferPacketMessage; + if (msg.XferID.ID.equals(xferID)) + { + if (pos.position > -1) + { + console.log('Packet confirmed, sending next. Position: ' + pos.position); + packetID++; + this.sendXferPacket(xferID, packetID, data, pos); + } + } + break; + } + case Message.AbortXfer: + { + const msg = packet.message as AbortXferMessage; + if (msg.XferID.ID.equals(xferID)) + { + console.log('Transfer aborted'); + subs.unsubscribe(); + reject(new Error('Transfer aborted')); + } + } + } + }); + + this.sendXferPacket(xferID, packetID, data, pos); + if (pos.position === -1) + { + subs.unsubscribe(); + resolve(); + } + }); + } + + XferFileDown(fileName: string, deleteOnCompletion: boolean, useBigPackets: boolean, vFileID: UUID, vFileType: AssetType, fromCache: boolean): Promise { return new Promise((resolve, reject) => { @@ -127,7 +221,8 @@ export class Circuit let finished = false; let finishID = 0; const receivedChunks: { [key: number]: Buffer } = {}; - + let firstPacket = true; + let dataSize = 0; subscription = this.subscribeToMessages([ Message.SendXferPacket, Message.AbortXfer @@ -161,7 +256,16 @@ export class Circuit resetTimeout(); const packetNum = message.XferID.Packet & 0x7FFFFFFF; const finishedNow = message.XferID.Packet & 0x80000000; - receivedChunks[packetNum] = message.DataPacket.Data; + if (firstPacket) + { + dataSize = message.DataPacket.Data.readUInt32LE(0); + receivedChunks[packetNum] = message.DataPacket.Data.slice(4); + firstPacket = false; + } + else + { + receivedChunks[packetNum] = message.DataPacket.Data; + } const confirm = new ConfirmXferPacketMessage(); confirm.XferID = { ID: transferID, @@ -199,7 +303,12 @@ export class Circuit subscription.unsubscribe(); } clearInterval(progress); - resolve(Buffer.concat(conc)); + const buf = Buffer.concat(conc); + if (buf.length !== dataSize) + { + console.warn('Warning: Received data size does not match expected'); + } + resolve(buf); } } break; diff --git a/lib/classes/ClientEvents.ts b/lib/classes/ClientEvents.ts index e1eaa64..d3007e0 100644 --- a/lib/classes/ClientEvents.ts +++ b/lib/classes/ClientEvents.ts @@ -24,7 +24,9 @@ import { FriendRemovedEvent } from '../events/FriendRemovedEvent'; import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent'; import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent'; import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent'; - +import { GameObject } from './public/GameObject'; +import { Avatar } from './public/Avatar'; +import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent'; export class ClientEvents { @@ -51,7 +53,12 @@ export class ClientEvents onParcelPropertiesEvent: Subject = new Subject(); onNewObjectEvent: Subject = new Subject(); onObjectUpdatedEvent: Subject = new Subject(); + onObjectUpdatedTerseEvent: Subject = new Subject(); onObjectKilledEvent: Subject = new Subject(); onSelectedObjectEvent: Subject = new Subject(); onObjectResolvedEvent: Subject = new Subject(); + onAvatarEnteredRegion: Subject = new Subject(); + onAvatarLeftRegion: Subject = new Subject(); + onRegionTimeDilation: Subject = new Subject(); + onBulkUpdateInventoryEvent: Subject = new Subject(); } diff --git a/lib/classes/CoalescedGameObject.ts b/lib/classes/CoalescedGameObject.ts new file mode 100644 index 0000000..f9a895a --- /dev/null +++ b/lib/classes/CoalescedGameObject.ts @@ -0,0 +1,66 @@ +import { Vector3 } from './Vector3'; +import { GameObject } from './public/GameObject'; +import { UUID } from './UUID'; +import * as builder from 'xmlbuilder'; +import { XMLElement, XMLNode } from 'xmlbuilder'; +import { Utils } from './Utils'; + +export class CoalescedGameObject +{ + itemID: UUID; + size: Vector3; + objects: { + offset: Vector3, + object: GameObject + }[]; + + static async fromXML(xml: string) + { + const obj = new CoalescedGameObject(); + + const parsed = await Utils.parseXML(xml); + + if (!parsed['CoalescedObject']) + { + throw new Error('CoalescedObject not found'); + } + const result = parsed['CoalescedObject']; + obj.size = new Vector3([parseFloat(result.$.x), parseFloat(result.$.y), parseFloat(result.$.z)]); + const sog = result['SceneObjectGroup']; + obj.objects = []; + for (const object of sog) + { + const toProcess = object['SceneObjectGroup'][0]; + const go = await GameObject.fromXML(toProcess); + obj.objects.push({ + offset: new Vector3([parseFloat(object.$.offsetx), parseFloat(object.$.offsety), parseFloat(object.$.offsetz)]), + object: go + }); + } + return obj; + } + + async exportXMLElement(rootNode?: string): Promise + { + const document = builder.create('CoalescedObject'); + document.att('x', this.size.x); + document.att('y', this.size.y); + document.att('z', this.size.z); + + for (const obj of this.objects) + { + const ele = document.ele('SceneObjectGroup'); + ele.att('offsetx', obj.offset.x); + ele.att('offsety', obj.offset.y); + ele.att('offsetz', obj.offset.z); + const child = await obj.object.exportXMLElement(rootNode); + ele.children.push(child); + } + return document; + } + + async exportXML(rootNode?: string): Promise + { + return (await this.exportXMLElement(rootNode)).end({pretty: true, allowEmpty: true}); + } +} diff --git a/lib/classes/EventQueueClient.ts b/lib/classes/EventQueueClient.ts index 1b33f3b..2447603 100644 --- a/lib/classes/EventQueueClient.ts +++ b/lib/classes/EventQueueClient.ts @@ -15,6 +15,10 @@ import { GroupChatEvent } from '../events/GroupChatEvent'; import { GroupChatSessionAgentListEvent } from '../events/GroupChatSessionAgentListEvent'; import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent'; import { IPAddress } from './IPAddress'; +import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent'; +import { InventoryFolder } from './InventoryFolder'; +import { InventoryItem } from './InventoryItem'; +import { Utils } from './Utils'; export class EventQueueClient { @@ -102,6 +106,64 @@ export class EventQueueClient */ break; + case 'BulkUpdateInventory': + { + const body = event['body']; + const buie = new BulkUpdateInventoryEvent(); + if (body['FolderData']) + { + for (const f of body['FolderData']) + { + const folderID = new UUID(f['FolderID']); + if (!folderID.isZero()) + { + const folder = new InventoryFolder(this.agent.inventory.main, this.agent); + folder.folderID = folderID; + folder.name = f['Name']; + folder.parentID = new UUID(f['ParentID']); + folder.typeDefault = parseInt(f['Type'], 10); + buie.folderData.push(folder); + } + } + } + if (body['ItemData']) + { + for (const i of body['ItemData']) + { + const itemID = new UUID(i['ItemID']); + if (!itemID.isZero()) + { + const folder = this.agent.inventory.findFolder(new UUID(i['FolderID'])); + const item = new InventoryItem(folder || undefined, this.agent); + + item.assetID = new UUID(i['AssetID']); + item.permissions.baseMask = Utils.OctetsToUInt32BE(i['BaseMask'].octets); + item.permissions.everyoneMask = Utils.OctetsToUInt32BE(i['EveryoneMask'].octets); + item.permissions.groupMask = Utils.OctetsToUInt32BE(i['GroupMask'].octets); + item.permissions.nextOwnerMask = Utils.OctetsToUInt32BE(i['NextOwnerMask'].octets); + item.permissions.ownerMask = Utils.OctetsToUInt32BE(i['OwnerMask'].octets); + item.permissions.groupOwned = i['GroupOwned']; + item.permissions.creator = new UUID(i['CreatorID']); + item.permissions.group = new UUID(i['GroupID']); + item.permissions.owner = new UUID(i['OwnerID']); + item.flags = Utils.OctetsToUInt32BE(i['Flags'].octets); + item.callbackID = Utils.OctetsToUInt32BE(i['CallbackID'].octets); + item.created = new Date(parseInt(i['CreationDate'], 10) * 1000); + item.description = i['Description']; + item.parentID = new UUID(i['FolderID']); + item.inventoryType = parseInt(i['InvType'], 10); + item.salePrice = parseInt(i['SalePrice'], 10); + item.saleType = parseInt(i['SaleType'], 10); + item.type = parseInt(i['Type'], 10); + item.itemID = itemID; + item.name = i['Name']; + buie.itemData.push(item); + } + } + } + this.clientEvents.onBulkUpdateInventoryEvent.next(buie); + break; + } case 'ParcelProperties': { const body = event['body']; @@ -120,7 +182,7 @@ export class EventQueueClient pprop.Area = body['ParcelData'][0]['Area']; try { - pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0); + pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32BE(0); } catch (ignore) { @@ -150,7 +212,7 @@ export class EventQueueClient pprop.OtherPrims = body['ParcelData'][0]['OtherPrims']; pprop.OwnerID = body['ParcelData'][0]['OwnerID']; pprop.OwnerPrims = body['ParcelData'][0]['OwnerPrims']; - pprop.ParcelFlags = Buffer.from(body['ParcelData'][0]['ParcelFlags'].toArray()).readUInt32LE(0); + pprop.ParcelFlags = Buffer.from(body['ParcelData'][0]['ParcelFlags'].toArray()).readUInt32BE(0); pprop.ParcelPrimBonus = body['ParcelData'][0]['ParcelPrimBonus']; pprop.PassHours = body['ParcelData'][0]['PassHours']; pprop.PassPrice = body['ParcelData'][0]['PassPrice']; @@ -367,7 +429,7 @@ export class EventQueueClient const info = event['body']['Info'][0]; if (info['LocationID']) { - info['LocationID'] = Buffer.from(info['LocationID'].toArray()).readUInt32LE(0); + info['LocationID'] = Buffer.from(info['LocationID'].toArray()).readUInt32BE(0); const regionHandleBuf = Buffer.from(info['RegionHandle'].toArray()); info['RegionHandle'] = new Long(regionHandleBuf.readUInt32LE(0), regionHandleBuf.readUInt32LE(4), true); @@ -375,7 +437,7 @@ export class EventQueueClient info['SimIP'] = new IPAddress(Buffer.from(info['SimIP'].toArray()), 0).toString(); - info['TeleportFlags'] = Buffer.from(info['TeleportFlags'].toArray()).readUInt32LE(0); + info['TeleportFlags'] = Buffer.from(info['TeleportFlags'].toArray()).readUInt32BE(0); const tpEvent = new TeleportEvent(); tpEvent.message = ''; diff --git a/lib/classes/Inventory.ts b/lib/classes/Inventory.ts index bef8a17..61d2110 100644 --- a/lib/classes/Inventory.ts +++ b/lib/classes/Inventory.ts @@ -77,6 +77,27 @@ export class Inventory } return this.getRootFolderMain().folderID; } + + findFolder(folderID: UUID): InventoryFolder | null + { + for (const id of Object.keys(this.main.skeleton)) + { + if (folderID.equals(id)) + { + return this.main.skeleton[id]; + } + else + { + const result = this.main.skeleton[id].findFolder(folderID); + if (result !== null) + { + return result; + } + } + } + return null; + } + async fetchInventoryItem(item: UUID): Promise { const params = { @@ -92,7 +113,12 @@ export class Inventory if (response['items'].length > 0) { const receivedItem = response['items'][0]; - const invItem = new InventoryItem(); + let folder = await this.findFolder(new UUID(receivedItem['parent_id'].toString())); + if (folder === null) + { + folder = this.getRootFolderMain(); + } + const invItem = new InventoryItem(folder, this.agent); invItem.assetID = new UUID(receivedItem['asset_id'].toString()); invItem.inventoryType = parseInt(receivedItem['inv_type'], 10); invItem.type = parseInt(receivedItem['type'], 10); @@ -126,10 +152,6 @@ export class Inventory { await this.main.skeleton[invItem.parentID.toString()].addItem(invItem); } - else - { - throw new Error('FolderID of ' + invItem.parentID.toString() + ' not found!'); - } return invItem; } else diff --git a/lib/classes/InventoryFolder.ts b/lib/classes/InventoryFolder.ts index f814662..4dfa2cb 100644 --- a/lib/classes/InventoryFolder.ts +++ b/lib/classes/InventoryFolder.ts @@ -6,6 +6,22 @@ import * as LLSD from '@caspertech/llsd'; import { InventorySortOrder } from '../enums/InventorySortOrder'; import { Agent } from './Agent'; import { FolderType } from '../enums/FolderType'; +import { CreateInventoryFolderMessage } from './messages/CreateInventoryFolder'; +import { Utils } from './Utils'; +import { PacketFlags } from '../enums/PacketFlags'; +import { Message } from '../enums/Message'; +import { FilterResponse } from '../enums/FilterResponse'; +import { UpdateCreateInventoryItemMessage } from './messages/UpdateCreateInventoryItem'; +import { LLMesh } from '..'; +import { CreateInventoryItemMessage } from './messages/CreateInventoryItem'; +import { WearableType } from '../enums/WearableType'; +import { PermissionMask } from '../enums/PermissionMask'; +import { AssetType } from '../enums/AssetType'; +import { LLWearable } from './LLWearable'; +import { InventoryType } from '../enums/InventoryType'; +import { AssetUploadRequestMessage } from './messages/AssetUploadRequest'; +import { RequestXferMessage } from './messages/RequestXfer'; +import { Logger } from './Logger'; export class InventoryFolder { @@ -15,9 +31,12 @@ export class InventoryFolder folderID: UUID; parentID: UUID; items: InventoryItem[] = []; + folders: InventoryFolder[] = []; cacheDir: string; agent: Agent; + private callbackID = 1; + private inventoryBase: { skeleton: {[key: string]: InventoryFolder}, root?: UUID @@ -57,6 +76,57 @@ export class InventoryFolder return children; } + async createFolder(name: string, type: FolderType) + { + const msg = new CreateInventoryFolderMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + msg.FolderData = { + FolderID: UUID.random(), + ParentID: this.folderID, + Type: type, + Name: Utils.StringToBuffer(name), + }; + const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + await this.agent.currentRegion.circuit.waitForAck(ack, 10000); + + const requestFolder = { + folder_id: new LLSD.UUID(this.folderID), + owner_id: new LLSD.UUID(this.agent.agentID), + fetch_folders: true, + fetch_items: false, + sort_order: InventorySortOrder.ByName + }; + const requestedFolders = { + 'folders': [ + requestFolder + ] + }; + + const folderContents: any = await this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders); + if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['categories'] && folderContents['folders'][0]['categories'].length > 0) + { + for (const folder of folderContents['folders'][0]['categories']) + { + const foundFolderID = new UUID(folder['folder_id'].toString()); + if (foundFolderID.equals(msg.FolderData.FolderID)) + { + const newFolder = new InventoryFolder(this.agent.inventory.main, this.agent); + newFolder.typeDefault = parseInt(folder['type_default'], 10); + newFolder.version = parseInt(folder['version'], 10); + newFolder.name = String(folder['name']); + newFolder.folderID = new UUID(folder['folder_id']); + newFolder.parentID = new UUID(folder['parent_id']); + this.folders.push(newFolder); + return newFolder; + } + } + } + throw new Error('Failed to create inventory folder'); + } + private saveCache(): Promise { return new Promise((resolve, reject) => @@ -111,10 +181,7 @@ export class InventoryFolder item.permissions.owner = new UUID(item.permissions.owner.mUUID); item.permissions.creator = new UUID(item.permissions.creator.mUUID); item.permissions.group = new UUID(item.permissions.group.mUUID); - this.addItem(item).catch((error) => - { - console.error(error); - }); + this.addItem(item, false); } resolve(); } @@ -167,8 +234,83 @@ export class InventoryFolder } } - populate() + private populateInternal(): Promise { + return new Promise((resolve, reject) => + { + const requestFolder = { + folder_id: new LLSD.UUID(this.folderID), + owner_id: new LLSD.UUID(this.agent.agentID), + fetch_folders: true, + fetch_items: true, + sort_order: InventorySortOrder.ByName + }; + const requestedFolders = { + 'folders': [ + requestFolder + ] + }; + this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) => + { + if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['items']) + { + this.version = folderContents['folders'][0]['version']; + this.items = []; + for (const item of folderContents['folders'][0]['items']) + { + const invItem = new InventoryItem(this, this.agent); + invItem.assetID = new UUID(item['asset_id'].toString()); + invItem.inventoryType = item['inv_type']; + invItem.name = item['name']; + invItem.salePrice = item['sale_info']['sale_price']; + invItem.saleType = item['sale_info']['sale_type']; + invItem.created = new Date(item['created_at'] * 1000); + invItem.parentID = new UUID(item['parent_id'].toString()); + invItem.flags = item['flags']; + 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'], + nextOwnerMask: item['permissions']['next_owner_mask'], + ownerMask: item['permissions']['owner_mask'], + everyoneMask: item['permissions']['everyone_mask'], + lastOwner: new UUID(item['permissions']['last_owner_id'].toString()), + owner: new UUID(item['permissions']['owner_id'].toString()), + creator: new UUID(item['permissions']['creator_id'].toString()), + group: new UUID(item['permissions']['group_id'].toString()) + }; + this.addItem(invItem, false); + } + this.saveCache().then(() => + { + resolve(); + }).catch(() => + { + // Resolve anyway + resolve(); + }); + } + else + { + resolve(); + } + }); + }); + } + + populate(useCached = true) + { + if (!useCached) + { + return this.populateInternal(); + } return new Promise((resolve, reject) => { this.loadCache().then(() => @@ -176,71 +318,514 @@ export class InventoryFolder resolve(); }).catch((err) => { - const requestFolder = { - folder_id: new LLSD.UUID(this.folderID), - owner_id: new LLSD.UUID(this.agent.agentID), - fetch_folders: true, - fetch_items: true, - sort_order: InventorySortOrder.ByName - }; - const requestedFolders = { - 'folders': [ - requestFolder - ] - }; - this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) => + this.populateInternal().then(() => { - if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['items']) - { - this.version = folderContents['folders'][0]['version']; - this.items = []; - for (const item of folderContents['folders'][0]['items']) - { - const invItem = new InventoryItem(); - invItem.assetID = new UUID(item['asset_id'].toString()); - invItem.inventoryType = item['inv_type']; - invItem.name = item['name']; - invItem.salePrice = item['sale_info']['sale_price']; - invItem.saleType = item['sale_info']['sale_type']; - invItem.created = new Date(item['created_at'] * 1000); - invItem.parentID = new UUID(item['parent_id'].toString()); - invItem.flags = item['flags']; - 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'], - nextOwnerMask: item['permissions']['next_owner_mask'], - ownerMask: item['permissions']['owner_mask'], - everyoneMask: item['permissions']['everyone_mask'], - lastOwner: new UUID(item['permissions']['last_owner_id'].toString()), - owner: new UUID(item['permissions']['owner_id'].toString()), - creator: new UUID(item['permissions']['creator_id'].toString()), - group: new UUID(item['permissions']['group_id'].toString()) - }; - this.addItem(invItem); - } - this.saveCache().then(() => - { - resolve(); - }).catch(() => - { - // Resolve anyway - resolve(); - }); - } - else - { - resolve(); - } + resolve(); + }).catch((erro: Error) => + { + reject(erro); }); }); }); } + + private uploadInventoryAssetLegacy(assetType: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise + { + return new Promise(async (resolve, reject) => + { + // Send an AssetUploadRequest and a CreateInventoryRequest simultaneously + const msg = new AssetUploadRequestMessage(); + const transactionID = UUID.random(); + msg.AssetBlock = { + StoreLocal: false, + Type: assetType, + Tempfile: false, + TransactionID: transactionID, + AssetData: Buffer.allocUnsafe(0) + }; + + const callbackID = ++this.callbackID; + + + const createMsg = new CreateInventoryItemMessage(); + + let wearableType = WearableType.Shape; + if (inventoryType === InventoryType.Wearable) + { + const wearable = new LLWearable(data.toString('utf-8')); + wearableType = wearable.type; + } + + createMsg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + createMsg.InventoryBlock = { + CallbackID: callbackID, + FolderID: this.folderID, + TransactionID: transactionID, + NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), + Type: assetType, + InvType: inventoryType, + WearableType: wearableType, + Name: Utils.StringToBuffer(name), + Description: Utils.StringToBuffer(description) + }; + + + if (data.length + 100 < 1200) + { + msg.AssetBlock.AssetData = data; + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + this.agent.currentRegion.circuit.sendMessage(createMsg, PacketFlags.Reliable); + } + else + { + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + this.agent.currentRegion.circuit.sendMessage(createMsg, PacketFlags.Reliable); + this.agent.currentRegion.circuit.waitForMessage(Message.RequestXfer, 10000).then((result: RequestXferMessage) => + { + this.agent.currentRegion.circuit.XferFileUp(result.XferID.ID, data).then(() => + { + console.log('Xfer finished'); + resolve(); + }).catch((err: Error) => + { + console.error('Error with transfer'); + console.error(err); + reject(err); + }); + }); + } + this.agent.currentRegion.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => + { + if (message.InventoryData[0].CallbackID === callbackID) + { + return FilterResponse.Finish; + } + else + { + return FilterResponse.NoMatch; + } + }).then((result: UpdateCreateInventoryItemMessage) => + { + if (!result.InventoryData || result.InventoryData.length < 1) + { + reject('Failed to create inventory item for wearable'); + } + resolve(result.InventoryData[0].ItemID); + }); + }); + } + + private uploadInventoryItem(assetType: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise + { + return new Promise((resolve, reject) => + { + const wearableType = WearableType.Shape; + + const transactionID = UUID.zero(); + const callbackID = ++this.callbackID; + const msg = new CreateInventoryItemMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + msg.InventoryBlock = { + CallbackID: callbackID, + FolderID: this.folderID, + TransactionID: transactionID, + NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), + Type: assetType, + InvType: inventoryType, + WearableType: wearableType, + Name: Utils.StringToBuffer(name), + Description: Utils.StringToBuffer(description) + }; + this.agent.currentRegion.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => + { + if (message.InventoryData[0].CallbackID === callbackID) + { + return FilterResponse.Finish; + } + else + { + return FilterResponse.NoMatch; + } + }).then((createInventoryMsg: UpdateCreateInventoryItemMessage) => + { + switch (inventoryType) + { + case InventoryType.Notecard: + { + this.agent.currentRegion.caps.capsPostXML('UpdateNotecardAgentInventory', { + 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), + }).then((result: any) => + { + if (result['uploader']) + { + const uploader = result['uploader']; + this.agent.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => + { + if (uploadResult['state'] && uploadResult['state'] === 'complete') + { + const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; + resolve(itemID); + } + else + { + reject(new Error('Asset upload failed')) + } + }).catch((err) => + { + reject(err); + }); + } + else + { + reject(new Error('Invalid response when attempting to request upload URL for notecard')); + } + }).catch((err) => + { + reject(err); + }); + break; + } + case InventoryType.Gesture: + { + this.agent.currentRegion.caps.isCapAvailable('UpdateGestureAgentInventory').then((available) => + { + if (available) + { + this.agent.currentRegion.caps.capsPostXML('UpdateGestureAgentInventory', { + 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), + }).then((result: any) => + { + if (result['uploader']) + { + const uploader = result['uploader']; + this.agent.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => + { + if (uploadResult['state'] && uploadResult['state'] === 'complete') + { + const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; + resolve(itemID); + } + else + { + reject(new Error('Asset upload failed')) + } + }).catch((err) => + { + reject(err); + }); + } + else + { + reject(new Error('Invalid response when attempting to request upload URL for notecard')); + } + }).catch((err) => + { + reject(err); + }); + } + else + { + this.uploadInventoryAssetLegacy(assetType, inventoryType, data, name, description).then((invItemID: UUID) => + { + resolve(invItemID); + }).catch((err: Error) => + { + reject(err); + }); + } + }); + break; + } + case InventoryType.Script: + { + this.agent.currentRegion.caps.capsPostXML('UpdateScriptAgent', { + 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), + 'target': 'mono' + }).then((result: any) => + { + if (result['uploader']) + { + const uploader = result['uploader']; + this.agent.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => + { + if (uploadResult['state'] && uploadResult['state'] === 'complete') + { + const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; + resolve(itemID); + } + else + { + reject(new Error('Asset upload failed')) + } + }).catch((err) => + { + reject(err); + }); + } + else + { + reject(new Error('Invalid response when attempting to request upload URL for notecard')); + } + }).catch((err) => + { + reject(err); + }); + break; + } + default: + { + reject(new Error('Currently unsupported CreateInventoryType: ' + inventoryType)); + } + } + }).catch(() => + { + reject(new Error('Timed out waiting for UpdateCreateInventoryItem')); + }); + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + }); + } + + uploadAsset(type: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise + { + return new Promise((resolve, reject) => + { + switch (inventoryType) + { + case InventoryType.Wearable: + // Wearables have to be uploaded using the legacy method and then created + this.uploadInventoryAssetLegacy(type, inventoryType, data, name, description).then((invItemID: UUID) => + { + this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) => + { + if (item === null) + { + reject(new Error('Unable to get inventory item')); + } + else + { + this.addItem(item, false).then(() => + { + resolve(item); + }); + } + }).catch((err) => + { + reject(err); + }); + }).catch((err) => + { + reject(err); + }); + return; + case InventoryType.Landmark: + case InventoryType.Notecard: + case InventoryType.Gesture: + case InventoryType.Script: + // These types must be created first and then modified + this.uploadInventoryItem(type, inventoryType, data, name, description).then((invItemID: UUID) => + { + this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) => + { + if (item === null) + { + reject(new Error('Unable to get inventory item')); + } + else + { + this.addItem(item, false).then(() => + { + resolve(item); + }); + } + }).catch((err) => + { + reject(err); + }); + }).catch((err) => + { + reject(err); + }); + return; + } + Logger.Info('[' + name + ']'); + const httpType = Utils.AssetTypeToHTTPAssetType(type); + this.agent.currentRegion.caps.capsPostXML('NewFileAgentInventory', { + 'folder_id': new LLSD.UUID(this.folderID.toString()), + 'asset_type': httpType, + 'inventory_type': Utils.HTTPAssetTypeToCapInventoryType(httpType), + 'name': name, + 'description': description, + 'everyone_mask': PermissionMask.All, + 'group_mask': PermissionMask.All, + 'next_owner_mask': PermissionMask.All, + 'expected_upload_cost': 0 + }).then((response: any) => + { + if (response['state'] === 'upload') + { + const uploadURL = response['uploader']; + this.agent.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) => + { + if (responseUpload['new_inventory_item'] !== undefined) + { + const invItemID = new UUID(responseUpload['new_inventory_item'].toString()); + this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) => + { + if (item === null) + { + reject(new Error('Unable to get inventory item')); + } + else + { + this.addItem(item, false).then(() => + { + resolve(item); + }); + } + }).catch((err) => + { + reject(err); + }); + } + }).catch((err) => + { + reject(err); + }); + } + else if (response['error']) + { + reject(response['error']['message']); + } + else + { + reject('Unable to upload asset'); + } + }).catch((err) => + { + console.log('Got err'); + console.log(err); + reject(err); + }) + }); + } + + checkCopyright(creatorID: UUID) + { + if (!creatorID.equals(this.agent.agentID) && !creatorID.isZero()) + { + throw new Error('Unable to upload - copyright violation'); + } + } + + findFolder(id: UUID): InventoryFolder | null + { + for (const folder of this.folders) + { + if (folder.folderID.equals(id)) + { + return folder; + } + const result = folder.findFolder(id); + if (result !== null) + { + return result; + } + } + return null; + } + + async uploadMesh(name: string, description: string, mesh: Buffer, confirmCostCallback: (cost: number) => Promise): Promise + { + const decodedMesh = await LLMesh.from(mesh); + + this.checkCopyright(decodedMesh.creatorID); + + const faces = []; + const faceCount = decodedMesh.lodLevels['high_lod'].length; + for (let x = 0; x < faceCount; x++) + { + faces.push({ + 'diffuse_color': [1.000000000000001, 1.000000000000001, 1.000000000000001, 1.000000000000001], + 'fullbright': false + }); + } + const prim = { + 'face_list': faces, + 'position': [0.000000000000001, 0.000000000000001, 0.000000000000001], + 'rotation': [0.000000000000001, 0.000000000000001, 0.000000000000001, 1.000000000000001], + 'scale': [2.000000000000001, 2.000000000000001, 2.000000000000001], + 'material': 3, + 'physics_shape_type': 2, + 'mesh': 0 + }; + const assetResources = { + 'instance_list': [prim], + 'mesh_list': [new LLSD.Binary(Array.from(mesh))], + 'texture_list': [], + 'metric': 'MUT_Unspecified' + }; + const uploadMap = { + 'name': String(name), + 'description': String(description), + 'asset_resources': assetResources, + 'asset_type': 'mesh', + 'inventory_type': 'object', + 'folder_id': new LLSD.UUID(this.folderID.toString()), + 'texture_folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Texture)), + 'everyone_mask': PermissionMask.All, + 'group_mask': PermissionMask.All, + 'next_owner_mask': PermissionMask.All + }; + let result; + try + { + result = await this.agent.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap); + } + catch (error) + { + console.error(error); + } + if (result['state'] === 'upload' && result['upload_price'] !== undefined) + { + const cost = result['upload_price']; + if (await confirmCostCallback(cost)) + { + const uploader = result['uploader']; + const uploadResult = await this.agent.currentRegion.caps.capsPerformXMLPost(uploader, assetResources); + if (uploadResult['new_inventory_item'] && uploadResult['new_asset']) + { + const inventoryItem = new UUID(uploadResult['new_inventory_item'].toString()); + const item = await this.agent.inventory.fetchInventoryItem(inventoryItem); + if (item !== null) + { + item.assetID = new UUID(uploadResult['new_asset'].toString()); + await this.addItem(item, false); + return item; + } + else + { + throw new Error('Unable to locate inventory item following mesh upload'); + } + } + else + { + + throw new Error('Upload failed - no new inventory item returned'); + } + } + throw new Error('Upload cost declined') + } + else + { + console.log(result); + console.log(JSON.stringify(result.error)); + throw new Error('Upload failed'); + } + } } diff --git a/lib/classes/InventoryItem.ts b/lib/classes/InventoryItem.ts index 1a286c9..5a3e5b1 100644 --- a/lib/classes/InventoryItem.ts +++ b/lib/classes/InventoryItem.ts @@ -3,6 +3,31 @@ import { InventoryType } from '../enums/InventoryType'; import { PermissionMask } from '../enums/PermissionMask'; import { InventoryItemFlags } from '../enums/InventoryItemFlags'; import { AssetType } from '../enums/AssetType'; +import * as builder from 'xmlbuilder'; +import * as xml2js from 'xml2js'; +import { Utils } from './Utils'; +import { AttachmentPoint } from '../enums/AttachmentPoint'; +import { RezSingleAttachmentFromInvMessage } from './messages/RezSingleAttachmentFromInv'; +import { GameObject } from '..'; +import { Agent } from './Agent'; +import { Subscription } from 'rxjs'; +import { DetachAttachmentIntoInvMessage } from './messages/DetachAttachmentIntoInv'; +import { Vector3 } from './Vector3'; +import { RezObjectMessage } from './messages/RezObject'; +import { NewObjectEvent } from '../events/NewObjectEvent'; +import { InventoryFolder } from './InventoryFolder'; +import { MoveInventoryItemMessage } from './messages/MoveInventoryItem'; +import { RemoveInventoryItemMessage } from './messages/RemoveInventoryItem'; +import { SaleTypeLL } from '../enums/SaleTypeLL'; +import { AssetTypeLL } from '../enums/AssetTypeLL'; +import { UpdateTaskInventoryMessage } from './messages/UpdateTaskInventory'; +import { PacketFlags } from '../enums/PacketFlags'; +import Timeout = NodeJS.Timeout; +import * as LLSD from '@caspertech/llsd'; +import { MoveTaskInventoryMessage } from './messages/MoveTaskInventory'; +import { UpdateCreateInventoryItemMessage } from './messages/UpdateCreateInventoryItem'; +import { Message } from '../enums/Message'; +import { FilterResponse } from '../enums/FilterResponse'; export class InventoryItem { @@ -20,6 +45,7 @@ export class InventoryItem permsGranter?: UUID; description: string; type: AssetType; + callbackID: number; permissions: { baseMask: PermissionMask; groupMask: PermissionMask; @@ -44,6 +70,414 @@ export class InventoryItem groupOwned: false }; + static fromAsset(lineObj: {lines: string[], lineNum: number}, container?: GameObject | InventoryFolder, agent?: Agent): InventoryItem + { + const item: InventoryItem = new InventoryItem(container, agent); + while (lineObj.lineNum < lineObj.lines.length) + { + const line = lineObj.lines[lineObj.lineNum++]; + let result = Utils.parseLine(line); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'item_id') + { + item.itemID = new UUID(result.value); + } + else if (result.key === 'parent_id') + { + item.parentID = new UUID(result.value); + } + else if (result.key === 'permissions') + { + while (lineObj.lineNum < lineObj.lines.length) + { + result = Utils.parseLine(lineObj.lines[lineObj.lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'creator_mask') + { + item.permissions.baseMask = parseInt(result.value, 16); + } + else if (result.key === 'base_mask') + { + item.permissions.baseMask = parseInt(result.value, 16); + } + else if (result.key === 'owner_mask') + { + item.permissions.ownerMask = parseInt(result.value, 16); + } + else if (result.key === 'group_mask') + { + item.permissions.groupMask = parseInt(result.value, 16); + } + else if (result.key === 'everyone_mask') + { + item.permissions.everyoneMask = parseInt(result.value, 16); + } + else if (result.key === 'next_owner_mask') + { + item.permissions.nextOwnerMask = parseInt(result.value, 16); + } + else if (result.key === 'creator_id') + { + item.permissions.creator = new UUID(result.value); + } + else if (result.key === 'owner_id') + { + item.permissions.owner = new UUID(result.value); + } + else if (result.key === 'last_owner_id') + { + item.permissions.lastOwner = new UUID(result.value); + } + else if (result.key === 'group_id') + { + item.permissions.group = new UUID(result.value); + } + else if (result.key === 'group_owned') + { + const val = parseInt(result.value, 10); + item.permissions.groupOwned = (val !== 0); + } + else + { + console.log('Unrecognised key (4): ' + result.key); + } + } + } + } + else if (result.key === 'sale_info') + { + while (lineObj.lineNum < lineObj.lines.length) + { + result = Utils.parseLine(lineObj.lines[lineObj.lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'sale_type') + { + const typeString = result.value as any; + item.saleType = parseInt(SaleTypeLL[typeString], 10); + } + else if (result.key === 'sale_price') + { + item.salePrice = parseInt(result.value, 10); + } + else + { + console.log('Unrecognised key (3): ' + result.key); + } + } + } + } + else if (result.key === 'shadow_id') + { + item.assetID = new UUID(result.value).bitwiseOr(new UUID('3c115e51-04f4-523c-9fa6-98aff1034730')); + } + else if (result.key === 'asset_id') + { + item.assetID = new UUID(result.value); + } + else if (result.key === 'type') + { + const typeString = result.value as any; + item.type = parseInt(AssetTypeLL[typeString], 10); + } + else if (result.key === 'inv_type') + { + const typeString = String(result.value); + switch (typeString) + { + case 'texture': + item.inventoryType = InventoryType.Texture; + break; + case 'sound': + item.inventoryType = InventoryType.Sound; + break; + case 'callcard': + item.inventoryType = InventoryType.CallingCard; + break; + case 'landmark': + item.inventoryType = InventoryType.Landmark; + break; + case 'object': + item.inventoryType = InventoryType.Object; + break; + case 'notecard': + item.inventoryType = InventoryType.Notecard; + break; + case 'category': + item.inventoryType = InventoryType.Category; + break; + case 'root': + item.inventoryType = InventoryType.RootCategory; + break; + case 'script': + item.inventoryType = InventoryType.Script; + break; + case 'snapshot': + item.inventoryType = InventoryType.Snapshot; + break; + case 'attach': + item.inventoryType = InventoryType.Attachment; + break; + case 'wearable': + item.inventoryType = InventoryType.Wearable; + break; + case 'animation': + item.inventoryType = InventoryType.Animation; + break; + case 'gesture': + item.inventoryType = InventoryType.Gesture; + break; + case 'mesh': + item.inventoryType = InventoryType.Mesh; + break; + default: + console.error('Unknown inventory type: ' + typeString); + } + } + else if (result.key === 'flags') + { + item.flags = parseInt(result.value, 16); + } + else if (result.key === 'name') + { + item.name = result.value.substr(0, result.value.indexOf('|')); + } + else if (result.key === 'desc') + { + item.description = result.value.substr(0, result.value.indexOf('|')); + } + else if (result.key === 'creation_date') + { + item.created = new Date(parseInt(result.value, 10) * 1000); + } + else + { + console.log('Unrecognised key (2): ' + result.key); + } + } + } + return item; + } + + static async fromXML(xml: string): Promise + { + const parsed = await Utils.parseXML(xml); + + if (!parsed['InventoryItem']) + { + throw new Error('InventoryItem not found'); + } + const inventoryItem = new InventoryItem(); + const result = parsed['InventoryItem']; + let prop: any; + if ((prop = Utils.getFromXMLJS(result, 'Name')) !== undefined) + { + inventoryItem.name = prop.toString(); + } + if ((prop = Utils.getFromXMLJS(result, 'ID')) !== undefined) + { + try + { + inventoryItem.itemID = new UUID(prop.toString()); + } + catch (error) + { + console.error(error); + } + } + if ((prop = Utils.getFromXMLJS(result, 'InvType')) !== undefined) + { + inventoryItem.inventoryType = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'CreatorUUID')) !== undefined) + { + try + { + inventoryItem.permissions.creator = new UUID(prop.toString()); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'CreationDate')) !== undefined) + { + try + { + inventoryItem.created = new Date(parseInt(prop, 10) * 1000); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'Owner')) !== undefined) + { + try + { + inventoryItem.permissions.owner = new UUID(prop.toString()); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'Description')) !== undefined) + { + inventoryItem.description = prop.toString(); + } + if ((prop = Utils.getFromXMLJS(result, 'AssetType')) !== undefined) + { + inventoryItem.type = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'AssetID')) !== undefined) + { + try + { + inventoryItem.assetID = new UUID(prop.toString()); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'SaleType')) !== undefined) + { + inventoryItem.saleType = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'SalePrice')) !== undefined) + { + inventoryItem.salePrice = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'BasePermissions')) !== undefined) + { + inventoryItem.permissions.baseMask = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'CurrentPermissions')) !== undefined) + { + inventoryItem.permissions.ownerMask = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'EveryonePermissions')) !== undefined) + { + inventoryItem.permissions.everyoneMask = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'NextPermissions')) !== undefined) + { + inventoryItem.permissions.nextOwnerMask = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'Flags')) !== undefined) + { + inventoryItem.flags = parseInt(prop, 10); + } + if ((prop = Utils.getFromXMLJS(result, 'GroupID')) !== undefined) + { + try + { + inventoryItem.permissions.group = new UUID(prop.toString()); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'LastOwner')) !== undefined) + { + try + { + inventoryItem.permissions.lastOwner = new UUID(prop.toString()); + } + catch (err) + { + console.error(err); + } + } + if ((prop = Utils.getFromXMLJS(result, 'GroupOwned')) !== undefined) + { + inventoryItem.permissions.groupOwned = parseInt(prop, 10) > 0 + } + return inventoryItem; + } + + constructor(private container?: GameObject | InventoryFolder, private agent?: Agent) + { + + } + + toAsset(indent: string = '') + { + const lines: string[] = []; + lines.push('{'); + lines.push('\titem_id\t' + this.itemID.toString()); + lines.push('\tparent_id\t' + this.parentID.toString()); + lines.push('permissions 0'); + lines.push('{'); + lines.push('\tbase_mask\t' + Utils.numberToFixedHex(this.permissions.baseMask)); + lines.push('\towner_mask\t' + Utils.numberToFixedHex(this.permissions.ownerMask)); + lines.push('\tgroup_mask\t' + Utils.numberToFixedHex(this.permissions.groupMask)); + lines.push('\teveryone_mask\t' + Utils.numberToFixedHex(this.permissions.everyoneMask)); + lines.push('\tnext_owner_mask\t' + Utils.numberToFixedHex(this.permissions.nextOwnerMask)); + lines.push('\tcreator_id\t' + this.permissions.creator.toString()); + lines.push('\towner_id\t' + this.permissions.owner.toString()); + lines.push('\tlast_owner_id\t' + this.permissions.lastOwner.toString()); + lines.push('\tgroup_id\t' + this.permissions.group.toString()); + lines.push('}'); + lines.push('\tasset_id\t' + this.assetID.toString()); + lines.push('\ttype\t' + Utils.AssetTypeToHTTPAssetType(this.type)); + lines.push('\tinv_type\t' + Utils.InventoryTypeToLLInventoryType(this.inventoryType)); + lines.push('\tflags\t' + Utils.numberToFixedHex(this.flags)); + lines.push('sale_info\t0'); + lines.push('{'); + switch (this.saleType) + { + case 0: + lines.push('\tsale_type\tnot'); + break; + case 1: + lines.push('\tsale_type\torig'); + break; + case 2: + lines.push('\tsale_type\tcopy'); + break; + case 3: + lines.push('\tsale_type\tcntn'); + break; + } + lines.push('\tsale_price\t' + this.salePrice); + lines.push('}'); + lines.push('\tname\t' + this.name + '|'); + lines.push('\tdesc\t' + this.description + '|'); + lines.push('\tcreation_date\t' + Math.floor(this.created.getTime() / 1000)); + lines.push('}'); + + return indent + lines.join('\n' + indent); + } + getCRC(): number { let crc = 0; @@ -67,4 +501,503 @@ export class InventoryItem crc = crc + Math.round(this.created.getTime() / 1000) >>> 0; return crc; } + + async moveToFolder(targetFolder: InventoryFolder): Promise + { + if (this.agent !== undefined) + { + if (this.container instanceof GameObject) + { + const msg = new MoveTaskInventoryMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID, + FolderID: targetFolder.folderID + }; + msg.InventoryData = { + LocalID: this.container.ID, + ItemID: this.itemID + }; + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + const response: UpdateCreateInventoryItemMessage = await this.agent.currentRegion.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => + { + for (const inv of message.InventoryData) + { + if (Utils.BufferToStringSimple(inv.Name) === this.name) + { + return FilterResponse.Finish; + } + } + return FilterResponse.NoMatch; + }); + for (const inv of response.InventoryData) + { + if (Utils.BufferToStringSimple(inv.Name) === this.name) + { + const item = await this.agent.inventory.fetchInventoryItem(inv.ItemID); + if (item === null) + { + throw new Error('Unable to get inventory item after move'); + } + if (!item.parentID.equals(targetFolder.folderID)) + { + await item.moveToFolder(targetFolder); + } + return item; + } + } + throw new Error('Unable to get inventory item after move'); + } + else + { + const msg = new MoveInventoryItemMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID, + Stamp: false + }; + msg.InventoryData = [ + { + ItemID: this.itemID, + FolderID: targetFolder.folderID, + NewName: Buffer.alloc(0) + } + ]; + const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + await this.agent.currentRegion.circuit.waitForAck(ack, 10000); + const item = await this.agent.inventory.fetchInventoryItem(this.itemID); + if (item === null) + { + throw new Error('Unable to find inventory item after move') + } + return item; + } + } + else + { + throw new Error('This inventoryItem is local only and cannot be moved to a folder') + } + } + + async delete() + { + if (this.agent !== undefined) + { + const msg = new RemoveInventoryItemMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + msg.InventoryData = [ + { + ItemID: this.itemID + } + ]; + const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + return this.agent.currentRegion.circuit.waitForAck(ack, 10000); + } + else + { + throw new Error('This inventoryItem is local only and cannot be deleted') + } + } + + async exportXML(): Promise + { + const document = builder.create('InventoryItem'); + document.ele('Name', this.name); + document.ele('ID', this.itemID.toString()); + document.ele('InvType', this.inventoryType); + document.ele('CreatorUUID', this.permissions.creator.toString()); + document.ele('CreationDate', this.created.getTime() / 1000); + document.ele('Owner', this.permissions.owner.toString()); + document.ele('LastOwner', this.permissions.lastOwner.toString()); + document.ele('Description', this.description); + document.ele('AssetType', this.type); + document.ele('AssetID', this.assetID.toString()); + document.ele('SaleType', this.saleType); + document.ele('SalePrice', this.salePrice); + document.ele('BasePermissions', this.permissions.baseMask); + document.ele('CurrentPermissions', this.permissions.ownerMask); + document.ele('EveryonePermissions', this.permissions.everyoneMask); + document.ele('NextPermissions', this.permissions.nextOwnerMask); + document.ele('Flags', this.flags); + document.ele('GroupID', this.permissions.group.toString()); + document.ele('GroupOwned', this.permissions.groupOwned); + return document.end({pretty: true, allowEmpty: true}); + } + + detachFromAvatar() + { + if (this.agent === undefined) + { + throw new Error('This inventory item was created locally. Please import to the grid.'); + } + const msg = new DetachAttachmentIntoInvMessage(); + msg.ObjectData = { + AgentID: this.agent.agentID, + ItemID: this.itemID + }; + const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + return this.agent.currentRegion.circuit.waitForAck(ack, 10000); + } + + attachToAvatar(attachPoint: AttachmentPoint, timeout: number = 10000): Promise + { + return new Promise((resolve, reject) => + { + if (this.agent === undefined) + { + throw new Error('This inventory item was created locally. Please import to the grid.'); + } + const rsafi = new RezSingleAttachmentFromInvMessage(); + rsafi.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + + rsafi.ObjectData = { + ItemID: this.itemID, + OwnerID: this.permissions.owner, + AttachmentPt: 0x80 | attachPoint, + ItemFlags: this.flags, + GroupMask: this.permissions.groupMask, + EveryoneMask: this.permissions.everyoneMask, + NextOwnerMask: this.permissions.nextOwnerMask, + Name: Utils.StringToBuffer(this.name), + Description: Utils.StringToBuffer(this.description) + }; + const avatar = this.agent.currentRegion.clientCommands.agent.getAvatar(); + let subs: Subscription | undefined = undefined; + let tmout: Timeout | undefined = undefined; + subs = avatar.onAttachmentAdded.subscribe((obj: GameObject) => + { + if (obj.name === this.name) + { + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + if (tmout !== undefined) + { + clearTimeout(tmout); + tmout = undefined; + } + resolve(obj); + } + }); + setTimeout(() => + { + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + if (tmout !== undefined) + { + clearTimeout(tmout); + tmout = undefined; + } + reject(new Error('Attach to avatar timed out')); + }, timeout); + this.agent.currentRegion.circuit.sendMessage(rsafi, PacketFlags.Reliable); + }); + } + + rezGroupInWorld(position: Vector3): Promise + { + return new Promise(async (resolve, reject) => + { + if (this.agent === undefined) + { + reject(new Error('This InventoryItem is local only, so cant rez')); + return; + } + const queryID = UUID.random(); + const msg = new RezObjectMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID, + GroupID: UUID.zero() + }; + msg.RezData = { + FromTaskID: (this.container instanceof GameObject) ? this.container.FullID : UUID.zero(), + BypassRaycast: 1, + RayStart: position, + RayEnd: position, + RayTargetID: UUID.zero(), + RayEndIsIntersection: false, + RezSelected: true, + RemoveItem: false, + ItemFlags: this.flags, + GroupMask: PermissionMask.All, + EveryoneMask: PermissionMask.All, + NextOwnerMask: PermissionMask.All, + }; + msg.InventoryData = { + ItemID: this.itemID, + FolderID: this.parentID, + CreatorID: this.permissions.creator, + OwnerID: this.permissions.owner, + GroupID: this.permissions.group, + BaseMask: this.permissions.baseMask, + OwnerMask: this.permissions.ownerMask, + GroupMask: this.permissions.groupMask, + EveryoneMask: this.permissions.everyoneMask, + NextOwnerMask: this.permissions.nextOwnerMask, + GroupOwned: false, + TransactionID: queryID, + Type: this.type, + InvType: this.inventoryType, + Flags: this.flags, + SaleType: this.saleType, + SalePrice: this.salePrice, + Name: Utils.StringToBuffer(this.name), + Description: Utils.StringToBuffer(this.description), + CreationDate: Math.round(this.created.getTime() / 1000), + CRC: 0, + }; + + let objSub: Subscription | undefined = undefined; + + const agent = this.agent; + + const gotObjects: GameObject[] = []; + + objSub = this.agent.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) => + { + if (evt.createSelected && !evt.object.resolvedAt) + { + // We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory + await agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true); + } + if (evt.createSelected && !evt.object.claimedForBuild) + { + if (evt.object.itemID !== undefined && evt.object.itemID.equals(this.itemID)) + { + evt.object.claimedForBuild = true; + gotObjects.push(evt.object); + } + } + }); + + // We have no way of knowing when the cluster is finished rezzing, so we just wait for 30 seconds + setTimeout(() => + { + if (objSub !== undefined) + { + objSub.unsubscribe(); + objSub = undefined; + } + if (gotObjects.length > 0) + { + resolve(gotObjects); + } + else + { + reject(new Error('No objects arrived')); + } + }, 30000); + + // Move the camera to look directly at prim for faster capture + const camLocation = new Vector3(position); + camLocation.z += (5) + 1; + await this.agent.currentRegion.clientCommands.agent.setCamera(camLocation, position, 256, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + }); + } + + rezInWorld(position: Vector3, objectScale?: Vector3): Promise + { + return new Promise(async (resolve, reject) => + { + if (this.agent === undefined) + { + reject(new Error('This InventoryItem is local only, so cant rez')); + return; + } + const queryID = UUID.random(); + const msg = new RezObjectMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID, + GroupID: UUID.zero() + }; + msg.RezData = { + FromTaskID: (this.container instanceof GameObject) ? this.container.FullID : UUID.zero(), + BypassRaycast: 1, + RayStart: position, + RayEnd: position, + RayTargetID: UUID.zero(), + RayEndIsIntersection: false, + RezSelected: true, + RemoveItem: false, + ItemFlags: this.flags, + GroupMask: PermissionMask.All, + EveryoneMask: PermissionMask.All, + NextOwnerMask: PermissionMask.All, + }; + msg.InventoryData = { + ItemID: this.itemID, + FolderID: this.parentID, + CreatorID: this.permissions.creator, + OwnerID: this.permissions.owner, + GroupID: this.permissions.group, + BaseMask: this.permissions.baseMask, + OwnerMask: this.permissions.ownerMask, + GroupMask: this.permissions.groupMask, + EveryoneMask: this.permissions.everyoneMask, + NextOwnerMask: this.permissions.nextOwnerMask, + GroupOwned: false, + TransactionID: queryID, + Type: this.type, + InvType: this.inventoryType, + Flags: this.flags, + SaleType: this.saleType, + SalePrice: this.salePrice, + Name: Utils.StringToBuffer(this.name), + Description: Utils.StringToBuffer(this.description), + CreationDate: Math.round(this.created.getTime() / 1000), + CRC: 0, + }; + + let objSub: Subscription | undefined = undefined; + let timeout: Timeout | undefined = setTimeout(() => + { + if (objSub !== undefined) + { + objSub.unsubscribe(); + objSub = undefined; + } + if (timeout !== undefined) + { + clearTimeout(timeout); + timeout = undefined; + } + reject(new Error('Prim never arrived')); + }, 10000); + let claimedPrim = false; + const agent = this.agent; + objSub = this.agent.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) => + { + if (evt.createSelected && !evt.object.resolvedAt) + { + // We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory + await agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true); + } + if (evt.createSelected && !evt.object.claimedForBuild && !claimedPrim) + { + if (evt.object.itemID !== undefined && evt.object.itemID.equals(this.itemID)) + { + if (objSub !== undefined) + { + objSub.unsubscribe(); + objSub = undefined; + } + if (timeout !== undefined) + { + clearTimeout(timeout); + timeout = undefined; + } + evt.object.claimedForBuild = true; + claimedPrim = true; + resolve(evt.object); + } + } + }); + + // Move the camera to look directly at prim for faster capture + let height = 10; + if (objectScale !== undefined) + { + height = objectScale.z; + } + const camLocation = new Vector3(position); + camLocation.z += (height / 2) + 1; + await this.agent.currentRegion.clientCommands.agent.setCamera(camLocation, position, height, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); + this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable); + }); + } + + async renameInTask(task: GameObject, newName: string) + { + this.name = newName; + if (this.agent === undefined) + { + return; + } + const msg = new UpdateTaskInventoryMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.agent.currentRegion.circuit.sessionID + }; + msg.UpdateData = { + Key: 0, + LocalID: task.ID + }; + msg.InventoryData = { + ItemID: this.itemID, + FolderID: this.parentID, + CreatorID: this.permissions.creator, + OwnerID: this.permissions.owner, + GroupID: this.permissions.group, + BaseMask: this.permissions.baseMask, + OwnerMask: this.permissions.ownerMask, + GroupMask: this.permissions.groupMask, + EveryoneMask: this.permissions.everyoneMask, + NextOwnerMask: this.permissions.nextOwnerMask, + GroupOwned: this.permissions.groupOwned || false, + TransactionID: UUID.zero(), + Type: this.type, + InvType: this.inventoryType, + Flags: this.flags, + SaleType: this.saleType, + SalePrice: this.salePrice, + Name: Utils.StringToBuffer(this.name), + Description: Utils.StringToBuffer(this.description), + CreationDate: this.created.getTime() / 1000, + CRC: this.getCRC() + }; + return this.agent.currentRegion.circuit.waitForAck(this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); + } + + async updateScript(scriptAsset: Buffer): Promise + { + if (this.agent === undefined) + { + throw new Error('This item was created locally and can\'t be updated'); + } + if (this.container instanceof GameObject) + { + try + { + const result: any = await this.agent.currentRegion.caps.capsPostXML('UpdateScriptTask', { + 'item_id': new LLSD.UUID(this.itemID.toString()), + 'task_id': new LLSD.UUID(this.container.FullID.toString()), + 'is_script_running': true, + 'target': 'mono' + }); + if (result['uploader']) + { + const uploader = result['uploader']; + const uploadResult: any = await this.agent.currentRegion.caps.capsRequestUpload(uploader, scriptAsset); + if (uploadResult['state'] && uploadResult['state'] === 'complete') + { + return new UUID(uploadResult['new_asset'].toString()); + } + } + throw new Error('Asset upload failed'); + } + catch (err) + { + console.error(err); + throw err; + } + } + else + { + throw new Error('Agent inventory not supported just yet') + } + } } diff --git a/lib/classes/LLGesture.ts b/lib/classes/LLGesture.ts new file mode 100644 index 0000000..d1cdce8 --- /dev/null +++ b/lib/classes/LLGesture.ts @@ -0,0 +1,159 @@ +import { LLGestureStep } from './LLGestureStep'; +import { LLGestureStepType } from '../enums/LLGestureStepType'; +import { LLGestureAnimationStep } from './LLGestureAnimationStep'; +import { UUID } from './UUID'; +import { LLGestureSoundStep } from './LLGestureSoundStep'; +import { LLGestureWaitStep } from './LLGestureWaitStep'; +import { LLGestureChatStep } from './LLGestureChatStep'; + +export class LLGesture +{ + version: number; + key: number; + mask: number; + trigger: string; + replace: string; + steps: LLGestureStep[] = []; + + constructor(data?: string) + { + if (data !== undefined) + { + const lines: string[] = data.replace(/\r\n/g, '\n').split('\n'); + if (lines.length > 5) + { + this.version = parseInt(lines[0].trim(), 10); + this.key = parseInt(lines[1].trim(), 10); + this.mask = parseInt(lines[2].trim(), 10); + this.trigger = lines[3].trim(); + this.replace = lines[4].trim(); + + const stepCount = parseInt(lines[5].trim(), 10); + let lineNumber = 6; + for (let step = 0; step < stepCount; step++) + { + if (lineNumber >= lines.length) + { + throw new Error('Invalid gesture step - unexpected end of file'); + } + const stepType: LLGestureStepType = parseInt(lines[lineNumber++].trim(), 10); + let gestureStep: LLGestureStep | undefined = undefined; + switch (stepType) + { + case LLGestureStepType.Animation: + { + if (lineNumber + 2 >= lines.length) + { + throw new Error('Invalid animation gesture step - unexpected end of file'); + } + const animStep = new LLGestureAnimationStep(); + animStep.animationName = lines[lineNumber++].trim(); + animStep.assetID = new UUID(lines[lineNumber++].trim()); + animStep.flags = parseInt(lines[lineNumber++].trim(), 10); + gestureStep = animStep; + break; + } + case LLGestureStepType.Sound: + { + if (lineNumber + 2 >= lines.length) + { + throw new Error('Invalid sound gesture step - unexpected end of file'); + } + const soundStep = new LLGestureSoundStep(); + soundStep.soundName = lines[lineNumber++].trim(); + soundStep.assetID = new UUID(lines[lineNumber++].trim()); + soundStep.flags = parseInt(lines[lineNumber++].trim(), 10); + gestureStep = soundStep; + break; + } + case LLGestureStepType.Chat: + { + if (lineNumber + 1 >= lines.length) + { + throw new Error('Invalid chat gesture step - unexpected end of file'); + } + const chatStep = new LLGestureChatStep(); + chatStep.chatText = lines[lineNumber++].trim(); + chatStep.flags = parseInt(lines[lineNumber++].trim(), 10); + gestureStep = chatStep; + break; + } + case LLGestureStepType.Wait: + { + if (lineNumber + 1 >= lines.length) + { + throw new Error('Invalid wait gesture step - unexpected end of file'); + } + const waitStep = new LLGestureWaitStep(); + waitStep.waitTime = parseFloat(lines[lineNumber++].trim()); + waitStep.flags = parseInt(lines[lineNumber++].trim(), 10); + gestureStep = waitStep; + break; + } + default: + throw new Error('Unknown gesture step type: ' + stepType); + } + if (gestureStep !== undefined) + { + this.steps.push(gestureStep); + } + } + } + else + { + throw new Error('Invalid gesture asset - unexpected end of file'); + } + } + } + + toAsset(): string + { + const lines: string[] = [ + String(this.version), + String(this.key), + String(this.mask), + this.trigger, + this.replace, + String(this.steps.length) + ]; + for (const step of this.steps) + { + lines.push(String(step.stepType)); + switch (step.stepType) + { + case LLGestureStepType.Animation: + { + const gStep = step as LLGestureAnimationStep; + lines.push(gStep.animationName); + lines.push(gStep.assetID.toString()); + lines.push(String(gStep.flags)); + break; + } + case LLGestureStepType.Sound: + { + const gStep = step as LLGestureSoundStep; + lines.push(gStep.soundName); + lines.push(gStep.assetID.toString()); + lines.push(String(gStep.flags)); + break; + } + case LLGestureStepType.Chat: + { + const gStep = step as LLGestureChatStep; + lines.push(gStep.chatText); + lines.push(String(gStep.flags)); + break; + } + case LLGestureStepType.Wait: + { + const gStep = step as LLGestureWaitStep; + lines.push(gStep.waitTime.toFixed(6)); + lines.push(String(gStep.flags)); + break; + } + } + } + lines.push('\n'); + return lines.join('\n'); + } +} diff --git a/lib/classes/LLGestureAnimationStep.ts b/lib/classes/LLGestureAnimationStep.ts new file mode 100644 index 0000000..6849fd2 --- /dev/null +++ b/lib/classes/LLGestureAnimationStep.ts @@ -0,0 +1,12 @@ +import { LLGestureStep } from './LLGestureStep'; +import { LLGestureStepType } from '../enums/LLGestureStepType'; +import { UUID } from './UUID'; +import { LLGestureAnimationFlags } from '../enums/LLGestureAnimationFlags'; + +export class LLGestureAnimationStep extends LLGestureStep +{ + stepType: LLGestureStepType = LLGestureStepType.Animation; + animationName: string; + assetID: UUID; + flags: LLGestureAnimationFlags = LLGestureAnimationFlags.None; +} diff --git a/lib/classes/LLGestureChatStep.ts b/lib/classes/LLGestureChatStep.ts new file mode 100644 index 0000000..f4720d1 --- /dev/null +++ b/lib/classes/LLGestureChatStep.ts @@ -0,0 +1,10 @@ +import { LLGestureStep } from './LLGestureStep'; +import { LLGestureStepType } from '../enums/LLGestureStepType'; +import { LLGestureChatFlags } from '../enums/LLGestureChatFlags'; + +export class LLGestureChatStep extends LLGestureStep +{ + stepType: LLGestureStepType = LLGestureStepType.Chat; + chatText: string; + flags: LLGestureChatFlags = LLGestureChatFlags.None; +} diff --git a/lib/classes/LLGestureSoundStep.ts b/lib/classes/LLGestureSoundStep.ts new file mode 100644 index 0000000..401cfb9 --- /dev/null +++ b/lib/classes/LLGestureSoundStep.ts @@ -0,0 +1,12 @@ +import { LLGestureStep } from './LLGestureStep'; +import { LLGestureStepType } from '../enums/LLGestureStepType'; +import { UUID } from './UUID'; +import { LLGestureSoundFlags } from '../enums/LLGestureSoundFlags'; + +export class LLGestureSoundStep extends LLGestureStep +{ + stepType: LLGestureStepType = LLGestureStepType.Sound; + soundName: string; + assetID: UUID; + flags: LLGestureSoundFlags = LLGestureSoundFlags.None; +} diff --git a/lib/classes/LLGestureStep.ts b/lib/classes/LLGestureStep.ts new file mode 100644 index 0000000..0931bcb --- /dev/null +++ b/lib/classes/LLGestureStep.ts @@ -0,0 +1,6 @@ +import { LLGestureStepType } from '../enums/LLGestureStepType'; + +export class LLGestureStep +{ + stepType: LLGestureStepType +} diff --git a/lib/classes/LLGestureWaitStep.ts b/lib/classes/LLGestureWaitStep.ts new file mode 100644 index 0000000..90c6081 --- /dev/null +++ b/lib/classes/LLGestureWaitStep.ts @@ -0,0 +1,11 @@ +import { LLGestureStep } from './LLGestureStep'; +import { LLGestureStepType } from '../enums/LLGestureStepType'; +import { UUID } from './UUID'; +import { LLGestureWaitFlags } from '../enums/LLGestureWaitFlags'; + +export class LLGestureWaitStep extends LLGestureStep +{ + stepType: LLGestureStepType = LLGestureStepType.Wait; + waitTime: number; + flags: LLGestureWaitFlags = LLGestureWaitFlags.None; +} diff --git a/lib/classes/LLLindenText.ts b/lib/classes/LLLindenText.ts new file mode 100644 index 0000000..fda541f --- /dev/null +++ b/lib/classes/LLLindenText.ts @@ -0,0 +1,182 @@ +import { InventoryItem } from './InventoryItem'; + +export class LLLindenText +{ + version: number = 2; + + private lineObj: { + lines: string[], + lineNum: number + } = { + lines: [], + lineNum: 0 + }; + + body = ''; + embeddedItems: {[key: number]: InventoryItem} = {}; + + constructor(data?: Buffer) + { + if (data !== undefined) + { + const initial = data.toString('ascii'); + this.lineObj.lines = initial.replace(/\r\n/g, '\n').split('\n'); + + let line = this.getLine(); + if (!line.startsWith('Linden text version')) + { + throw new Error('Invalid Linden Text header'); + } + this.version = parseInt(this.getLastToken(line), 10); + if (this.version < 1 || this.version > 2) + { + throw new Error('Unsupported Linden Text version'); + } + if (this.version === 2) + { + const v2 = data.toString('utf-8'); + this.lineObj.lines = v2.replace(/\r\n/g, '\n').split('\n'); + } + line = this.getLine(); + if (line !== '{') + { + throw new Error('Error parsing Linden Text file'); + } + line = this.getLine(); + if (line.startsWith('LLEmbeddedItems')) + { + this.parseEmbeddedItems(); + line = this.getLine(); + } + if (!line.startsWith('Text length')) + { + throw new Error('Error parsing Linden Text file: ' + line); + } + let textLength = parseInt(this.getLastToken(line), 10); + do + { + line = this.getLine(); + textLength -= Buffer.byteLength(line); + if (textLength < 0) + { + const extraChars = 0 - textLength; + const rest = line.substr(line.length - extraChars); + line = line.substr(0, line.length - extraChars); + this.lineObj.lines.splice(this.lineObj.lineNum, 0, rest); + textLength = 0; + this.body += line; + } + else + { + this.body += line; + if (textLength > 0) + { + this.body += '\n'; + textLength--; + } + } + } + while (textLength > 0); + line = this.getLine(); + if (line !== '}') + { + throw new Error('Error parsing Linden Text file'); + } + } + } + + toAsset(): Buffer + { + const lines: string[] = []; + lines.push('Linden text version ' + this.version); + lines.push('{'); + const count = Object.keys(this.embeddedItems).length; + if (count > 0) + { + lines.push('LLEmbeddedItems version 1'); + lines.push('{'); + lines.push('count ' + String(count)); + for (const key of Object.keys(this.embeddedItems)) + { + lines.push('{'); + lines.push('ext char index ' + key); + lines.push('\tinv_item\t0'); + lines.push(this.embeddedItems[parseInt(key, 10)].toAsset('\t')); + lines.push('}'); + } + lines.push('}'); + } + lines.push('Text length ' + String(Buffer.byteLength(this.body))); + lines.push(this.body + '}\n\0'); + if (this.version === 1) + { + return Buffer.from(lines.join('\n'), 'ascii'); + } + return Buffer.from(lines.join('\n'), 'utf-8'); + } + + private parseEmbeddedItems() + { + let line = this.getLine(); + if (line !== '{') + { + throw new Error('Invalid LLEmbeddedItems format (no opening brace)'); + } + line = this.getLine(); + if (!line.startsWith('count')) + { + throw new Error('Invalid LLEmbeddedItems format (no count)'); + } + const itemCount = parseInt(this.getLastToken(line), 10); + for (let x = 0; x < itemCount; x++) + { + line = this.getLine(); + if (line !== '{') + { + throw new Error('Invalid LLEmbeddedItems format (no item opening brace)'); + } + line = this.getLine(); + if (!line.startsWith('ext char index')) + { + throw new Error('Invalid LLEmbeddedItems format (no ext char index)'); + } + const index = parseInt(this.getLastToken(line), 10); + line = this.getLine(); + if (!line.startsWith('inv_item')) + { + throw new Error('Invalid LLEmbeddedItems format (no inv_item)'); + } + const item = InventoryItem.fromAsset(this.lineObj); + this.embeddedItems[index] = item; + line = this.getLine(); + if (line !== '}') + { + throw new Error('Invalid LLEmbeddedItems format (no closing brace)'); + } + } + line = this.getLine(); + if (line !== '}') + { + throw new Error('Error parsing Linden Text file'); + } + } + + private getLastToken(input: string): string + { + const index = input.lastIndexOf(' '); + if (index === -1) + { + return input; + } + else + { + return input.substr(index + 1); + } + } + + private getLine(): string + { + return this.lineObj.lines[this.lineObj.lineNum++].trim().replace(/[\t ]+/g, ' '); + } + +} diff --git a/lib/classes/LLWearable.ts b/lib/classes/LLWearable.ts index e5507cd..dd74dee 100644 --- a/lib/classes/LLWearable.ts +++ b/lib/classes/LLWearable.ts @@ -1,6 +1,8 @@ import { UUID } from './UUID'; import { WearableType } from '../enums/WearableType'; import { SaleType } from '../enums/SaleType'; +import { SaleTypeLL } from '../enums/SaleTypeLL'; +import { Utils } from './Utils'; export class LLWearable { @@ -31,148 +33,165 @@ export class LLWearable }; saleType: SaleType; salePrice: number; - constructor(data: string) + constructor(data?: string) { - const lines: string[] = data.replace(/\r\n/g, '\n').split('\n'); - for (let index = 0; index < lines.length; index++) + if (data !== undefined) { - if (index === 0) + const lines: string[] = data.replace(/\r\n/g, '\n').split('\n'); + for (let index = 0; index < lines.length; index++) { - const header = lines[index].split(' '); - if (header[0] !== 'LLWearable') + if (index === 0) { - return; - } - } - else if (index === 1) - { - this.name = lines[index]; - } - else - { - const parsedLine = this.parseLine(lines[index]); - if (parsedLine.key !== null) - { - switch (parsedLine.key) + const header = lines[index].split(' '); + if (header[0] !== 'LLWearable') { - case 'base_mask': - this.permission.baseMask = parseInt(parsedLine.value, 16); - break; - case 'owner_mask': - this.permission.ownerMask = parseInt(parsedLine.value, 16); - break; - case 'group_mask': - this.permission.groupMask = parseInt(parsedLine.value, 16); - break; - case 'everyone_mask': - this.permission.everyoneMask = parseInt(parsedLine.value, 16); - break; - case 'next_owner_mask': - this.permission.nextOwnerMask = parseInt(parsedLine.value, 16); - break; - case 'creator_id': - this.permission.creatorID = new UUID(parsedLine.value); - break; - case 'owner_id': - this.permission.ownerID = new UUID(parsedLine.value); - break; - case 'last_owner_id': - this.permission.lastOwnerID = new UUID(parsedLine.value); - break; - case 'group_id': - this.permission.groupID = new UUID(parsedLine.value); - break; - case 'sale_type': - this.saleType = parseInt(parsedLine.value, 10); - break; - case 'sale_price': - this.salePrice = parseInt(parsedLine.value, 10); - break; - case 'type': - this.type = parseInt(parsedLine.value, 10); - break; - case 'parameters': + return; + } + } + else if (index === 1) + { + this.name = lines[index]; + } + else + { + const parsedLine = Utils.parseLine(lines[index]); + if (parsedLine.key !== null) + { + switch (parsedLine.key) { - const num = parseInt(parsedLine.value, 10); - const max = index + num; - for (index; index < max; index++) - { - const paramLine = this.parseLine(lines[index++]); - if (paramLine.key !== null) + case 'base_mask': + this.permission.baseMask = parseInt(parsedLine.value, 16); + break; + case 'owner_mask': + this.permission.ownerMask = parseInt(parsedLine.value, 16); + break; + case 'group_mask': + this.permission.groupMask = parseInt(parsedLine.value, 16); + break; + case 'everyone_mask': + this.permission.everyoneMask = parseInt(parsedLine.value, 16); + break; + case 'next_owner_mask': + this.permission.nextOwnerMask = parseInt(parsedLine.value, 16); + break; + case 'creator_id': + this.permission.creatorID = new UUID(parsedLine.value); + break; + case 'owner_id': + this.permission.ownerID = new UUID(parsedLine.value); + break; + case 'last_owner_id': + this.permission.lastOwnerID = new UUID(parsedLine.value); + break; + case 'group_id': + this.permission.groupID = new UUID(parsedLine.value); + break; + case 'sale_type': + switch (parsedLine.value.trim().toLowerCase()) { - this.parameters[parseInt(paramLine.key, 10)] = parseInt(paramLine.value, 10); + case 'not': + this.saleType = 0; + break; + case 'orig': + this.saleType = 1; + break; + case 'copy': + this.saleType = 2; + break; + case 'cntn': + this.saleType = 3; + break; + default: + console.log('Unrecognised saleType: ' + parsedLine.value.trim().toLowerCase()); } - } - break; - } - case 'textures': - { - const num = parseInt(parsedLine.value, 10); - const max = index + num ; - for (index; index < max; index++) + break; + case 'sale_price': + this.salePrice = parseInt(parsedLine.value, 10); + break; + case 'type': + this.type = parseInt(parsedLine.value, 10); + break; + case 'parameters': { - const texLine = this.parseLine(lines[index + 1]); - if (texLine.key !== null) + const num = parseInt(parsedLine.value, 10); + const max = index + num; + for (index; index < max; index++) { - this.textures[parseInt(texLine.key, 10)] = new UUID(texLine.value); + const paramLine = Utils.parseLine(lines[index + 1]); + if (paramLine.key !== null) + { + this.parameters[parseInt(paramLine.key, 10)] = parseFloat(paramLine.value.replace('-.', '-0.')); + } } + break; } - break; + case 'textures': + { + const num = parseInt(parsedLine.value, 10); + const max = index + num; + for (index; index < max; index++) + { + const texLine = Utils.parseLine(lines[index + 1]); + if (texLine.key !== null) + { + this.textures[parseInt(texLine.key, 10)] = new UUID(texLine.value); + } + } + break; + } + case 'permissions': + case 'sale_info': + case '{': + case '}': + // ignore + break; + default: + console.log('skipping: ' + lines[index]); + break; } - case 'permissions': - case 'sale_info': - case '{': - case '}': - // ignore - break; - default: - console.log('skipping: ' + lines[index]); - break; } } } } } - private parseLine(line: string): { - 'key': string | null, - 'value': string - } + toAsset(): string { - line = line.trim().replace(/[\t]/gu, ' ').trim(); - while (line.indexOf('\u0020\u0020') > 0) + const lines: string[] = [ + 'LLWearable version 22' + ]; + lines.push(this.name); + lines.push(''); + lines.push('\tpermissions 0'); + lines.push('\t{'); + lines.push('\t\tbase_mask\t' + Utils.numberToFixedHex(this.permission.baseMask)); + lines.push('\t\towner_mask\t' + Utils.numberToFixedHex(this.permission.ownerMask)); + lines.push('\t\tgroup_mask\t' + Utils.numberToFixedHex(this.permission.groupMask)); + lines.push('\t\teveryone_mask\t' + Utils.numberToFixedHex(this.permission.everyoneMask)); + lines.push('\t\tnext_owner_mask\t' + Utils.numberToFixedHex(this.permission.nextOwnerMask)); + lines.push('\t\tcreator_id\t' + this.permission.creatorID.toString()); + lines.push('\t\towner_id\t' + this.permission.ownerID.toString()); + lines.push('\t\tlast_owner_id\t' + this.permission.lastOwnerID.toString()); + lines.push('\t\tgroup_id\t' + this.permission.groupID.toString()); + lines.push('\t}'); + lines.push('\tsale_info\t0'); + lines.push('\t{'); + lines.push('\t\tsale_type\t' + SaleTypeLL[this.saleType]); + lines.push('\t\tsale_price\t' + this.salePrice); + lines.push('\t}'); + lines.push('type ' + this.type); + lines.push('parameters ' + Object.keys(this.parameters).length); + for (const num of Object.keys(this.parameters)) { - line = line.replace(/\u0020\u0020/gu, '\u0020'); + const val = this.parameters[parseInt(num, 10)]; + lines.push(num + (' ' + String(val).replace('-0.', '-.')).replace(' 0.', ' .')); } - let key: string | null = null; - let value = ''; - if (line.length > 2) + lines.push('textures ' + Object.keys(this.textures).length); + for (const num of Object.keys(this.textures)) { - const sep = line.indexOf(' '); - if (sep > 0) - { - key = line.substr(0, sep); - value = line.substr(sep + 1); - } - } - else if (line.length === 1) - { - key = line; - } - else if (line.length > 0) - { - return { - 'key': line, - 'value': '' - } - } - if (key !== null) - { - key = key.trim(); - } - return { - 'key': key, - 'value': value + const val = this.textures[parseInt(num, 10)]; + lines.push(num + ' ' + val); } + return lines.join('\n') + '\n'; } } diff --git a/lib/classes/Logger.ts b/lib/classes/Logger.ts new file mode 100644 index 0000000..71304ef --- /dev/null +++ b/lib/classes/Logger.ts @@ -0,0 +1,137 @@ +import * as logger from 'winston'; +import * as winston from 'winston'; +import * as moment from 'moment'; +import * as chalk from 'chalk'; +import {TransformableInfo} from 'logform'; + +const formatLevel = function(text: string, level: string) +{ + switch (level) + { + case 'warn': + return chalk.yellowBright(text); + case 'error': + return chalk.redBright(text); + case 'debug': + return chalk.green(text); + case 'info': + return chalk.magentaBright(text); + default: + return text; + } +}; + +const formatMessage = function(text: string, level: string) +{ + switch (level) + { + case 'warn': + return chalk.yellowBright(text); + case 'error': + return chalk.redBright(text); + default: + return text; + } +}; + +const logFormat = winston.format.printf(function(info: TransformableInfo) +{ + const logComponents = [ + moment().format('YYYY-MM-DD HH:mm:ss'), + '-', + '[' + formatLevel(info.level.toUpperCase(), info.level) + ']', + formatMessage(info.message, info.level) + ]; + return logComponents.join(' '); +}); + +logger.configure({ + format: logFormat, + silent: false, + transports: [ + new winston.transports.Console({ + 'level': 'debug', + handleExceptions: true + }) + ], +}); + +export class Logger +{ + private static prefixLevel = 0; + static prefix = ''; + + static increasePrefixLevel() + { + this.prefixLevel++; + this.generatePrefix(); + } + + static decreasePrefixLevel() + { + this.prefixLevel--; + this.generatePrefix(); + } + + static generatePrefix() + { + this.prefix = ''; + for (let x = 0; x < this.prefixLevel; x++) + { + this.prefix += ' '; + } + if (this.prefix.length > 0) + { + this.prefix += '... '; + } + } + + static Debug(message: string | object) + { + if (typeof message === 'string') + { + message = this.prefix + message; + } + this.Log('debug', message); + } + static Info(message: string | object) + { + if (typeof message === 'string') + { + message = this.prefix + message; + } + this.Log('info', message); + } + static Warn(message: string | object) + { + if (typeof message === 'string') + { + message = this.prefix + message; + } + this.Log('warn', message); + } + static Error(message: string | object) + { + if (typeof message === 'string') + { + message = this.prefix + message; + } + this.Log('error', message); + } + + static Log(type: string, message: string | object) + { + if (typeof message === 'object') + { + if (message instanceof Error) + { + message = message.message + '\n\n' + message.stack; + } + else + { + message = JSON.stringify(message); + } + } + logger.log(type, message); + } +} diff --git a/lib/classes/ObjectResolver.ts b/lib/classes/ObjectResolver.ts new file mode 100644 index 0000000..17f43c1 --- /dev/null +++ b/lib/classes/ObjectResolver.ts @@ -0,0 +1,441 @@ +import { GameObject } from './public/GameObject'; +import { PCode, PrimFlags, UUID } from '..'; +import * as LLSD from '@caspertech/llsd'; +import { Region } from './Region'; +import { skip } from 'rxjs/operators'; +import { on } from 'cluster'; +import { IResolveJob } from './interfaces/IResolveJob'; +import { Subject, Subscription } from 'rxjs'; +import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent'; + +export class ObjectResolver +{ + private objectsInQueue: {[key: number]: IResolveJob} = {}; + + private queue: number[] = []; + + private maxConcurrency = 128; + private currentlyRunning = false; + + private onObjectResolveRan: Subject = new Subject(); + + constructor(private region: Region) + { + + } + + resolveObjects(objects: GameObject[], forceResolve: boolean = false, skipInventory = false, log = false): Promise + { + return new Promise((resolve, reject) => + { + if (log) + { + // console.log('[RESOLVER] Scanning ' + objects.length + ' objects, skipInventory: ' + skipInventory); + } + + // First, create a map of all object IDs + const objs: {[key: number]: GameObject} = {}; + const failed: GameObject[] = []; + for (const obj of objects) + { + this.region.objects.populateChildren(obj); + this.scanObject(obj, objs); + } + + let amountLeft = Object.keys(objs).length; + if (log) + { + // console.log('[RESOLVER] ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)'); + } + + const queueObject = (id: number) => + { + if (this.objectsInQueue[id] === undefined) + { + this.objectsInQueue[id] = { + object: objs[id], + skipInventory: skipInventory, + log + }; + this.queue.push(id); + } + else if (this.objectsInQueue[id].skipInventory && !skipInventory) + { + this.objectsInQueue[id].skipInventory = true + } + }; + + const skipped: number[] = []; + for (const obj of Object.keys(objs)) + { + const id = parseInt(obj, 10); + const gameObject = objs[id]; + if (log) + { + // console.log('ResolvedInventory: ' + gameObject.resolvedInventory + ', skip: ' + skipInventory); + } + if (forceResolve || gameObject.resolvedAt === undefined || gameObject.resolvedAt === 0 || (!skipInventory && !gameObject.resolvedInventory)) + { + if (forceResolve) + { + gameObject.resolvedAt = 0; + gameObject.resolveAttempts = 0; + } + queueObject(id); + } + else + { + skipped.push(id); + } + } + for (const id of skipped) + { + delete objs[id]; + amountLeft--; + if (log) + { + // console.log('[RESOLVER] Skipping already resolved object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)'); + } + } + + if (Object.keys(objs).length === 0) + { + resolve(failed); + return; + } + + let objResolve: Subscription | undefined = undefined; + let objProps: Subscription | undefined = undefined; + + const checkObject = (obj: GameObject): boolean => + { + let done = false; + if (obj.resolvedAt !== undefined && obj.resolvedAt > 0) + { + if (skipInventory || obj.resolvedInventory) + { + amountLeft--; + if (log) + { + // console.log('[RESOLVER] Resolved an object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)'); + } + done = true; + } + } + if (obj.resolveAttempts > 2) + { + // Give up + amountLeft--; + if (log) + { + // console.log('[RESOLVER] Failed to resolve an object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)'); + } + failed.push(obj); + done = true; + } + if (done) + { + delete objs[obj.ID]; + if (Object.keys(objs).length === 0) + { + if (objResolve !== undefined) + { + objResolve.unsubscribe(); + objResolve = undefined; + } + if (objProps !== undefined) + { + objProps.unsubscribe(); + objProps = undefined; + } + resolve(failed); + } + } + return done; + }; + + objResolve = this.onObjectResolveRan.subscribe((obj: GameObject) => + { + if (objs[obj.ID] !== undefined) + { + if (log) + { + // console.log('Got onObjectResolveRan for 1 object ...'); + } + if (!checkObject(obj)) + { + if (log) + { + // console.log(' .. Not resolved yet'); + } + setTimeout(() => + { + if (!checkObject(obj)) + { + // Requeue + if (log) + { + // console.log(' .. ' + obj.ID + ' still not resolved yet, requeuing'); + } + queueObject(obj.ID); + this.run().then(() => + { + + }).catch((err) => + { + console.error(err); + }); + } + }, 10000); + } + } + }); + + objProps = this.region.clientEvents.onObjectResolvedEvent.subscribe((obj: ObjectResolvedEvent) => + { + if (objs[obj.object.ID] !== undefined) + { + if (log) + { + // console.log('Got object resolved event for ' + obj.object.ID); + } + if (!checkObject(obj.object)) + { + // console.log(' ... Still not resolved yet'); + } + + } + }); + + this.run().then(() => + { + + }).catch((err) => + { + console.error(err); + }); + }); + } + + private scanObject(obj: GameObject, map: {[key: number]: GameObject}) + { + const localID = obj.ID; + if (!map[localID]) + { + map[localID] = obj; + if (obj.children) + { + for (const child of obj.children) + { + this.scanObject(child, map); + } + } + } + } + + private async run() + { + if (this.currentlyRunning) + { + // console.log('Prodded but already running'); + return; + } + try + { + // console.log('Running. Queue length: ' + this.queue.length); + while (this.queue.length > 0) + { + const jobs = []; + for (let x = 0; x < this.maxConcurrency && this.queue.length > 0; x++) + { + const objectID = this.queue.shift(); + if (objectID !== undefined) + { + jobs.push(this.objectsInQueue[objectID]); + delete this.objectsInQueue[objectID]; + } + } + await this.doResolve(jobs); + } + } + catch (error) + { + console.error(error); + } + finally + { + this.currentlyRunning = false; + } + if (this.queue.length > 0) + { + this.run().then(() => {}, (err) => + { + console.error(err); + }); + } + } + + private async doResolve(jobs: IResolveJob[]) + { + const resolveTime = new Date().getTime() / 1000; + const objectList = []; + let totalRemaining = 0; + try + { + for (const job of jobs) + { + if (job.object.resolvedAt === undefined || job.object.resolvedAt < resolveTime) + { + objectList.push(job.object); + totalRemaining++; + } + } + + if (objectList.length > 0) + { + // console.log('Selecting ' + objectList.length + ' objects'); + await this.region.clientCommands.region.selectObjects(objectList); + // console.log('Deselecting ' + objectList.length + ' objects'); + await this.region.clientCommands.region.deselectObjects(objectList); + for (const chk of objectList) + { + if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime) + { + totalRemaining --; + } + } + } + + for (const job of jobs) + { + if (!job.skipInventory) + { + const o = job.object; + if ((o.resolveAttempts === undefined || o.resolveAttempts < 3) && o.FullID !== undefined && o.name !== undefined && o.Flags !== undefined && !(o.Flags & PrimFlags.InventoryEmpty) && (!o.inventory || o.inventory.length === 0)) + { + if (job.log) + { + // console.log('Processing inventory for ' + job.object.ID); + } + try + { + await o.updateInventory(); + } + catch (error) + { + if (o.resolveAttempts === undefined) + { + o.resolveAttempts = 0; + } + o.resolveAttempts++; + if (o.FullID !== undefined) + { + console.error('Error downloading task inventory of ' + o.FullID.toString() + ':'); + console.error(error); + } + else + { + console.error('Error downloading task inventory of ' + o.ID + ':'); + console.error(error); + } + } + } + else + { + if (job.log) + { + // console.log('Skipping inventory for ' + job.object.ID); + } + } + o.resolvedInventory = true; + } + } + } + catch (ignore) + { + console.error(ignore); + } + finally + { + if (totalRemaining < 1) + { + totalRemaining = 0; + for (const obj of objectList) + { + if (obj.resolvedAt === undefined || obj.resolvedAt < resolveTime) + { + totalRemaining++; + } + } + if (totalRemaining > 0) + { + console.error(totalRemaining + ' objects could not be resolved'); + } + } + const that = this; + const getCosts = async function(objIDs: UUID[]) + { + const result = await that.region.caps.capsPostXML('GetObjectCost', { + 'object_ids': objIDs + }); + const uuids = Object.keys(result); + for (const key of uuids) + { + const costs = result[key]; + try + { + const obj: GameObject = that.region.objects.getObjectByUUID(new UUID(key)); + obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']); + obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']); + obj.physicaImpact = parseFloat(costs['physics_cost']); + obj.resourceImpact = parseFloat(costs['resource_cost']); + obj.limitingType = costs['resource_limiting_type']; + + + obj.landImpact = Math.round(obj.linkPhysicsImpact); + if (obj.linkResourceImpact > obj.linkPhysicsImpact) + { + obj.landImpact = Math.round(obj.linkResourceImpact); + } + obj.calculatedLandImpact = obj.landImpact; + if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy') + { + obj.calculatedLandImpact = 0; + } + } + catch (error) + {} + } + }; + + let ids: UUID[] = []; + const promises: Promise[] = []; + for (const job of jobs) + { + if (job.object.landImpact === undefined) + { + ids.push(new LLSD.UUID(job.object.FullID)); + } + if (ids.length > 255) + { + promises.push(getCosts(ids)); + ids = []; + } + } + if (ids.length > 0) + { + promises.push(getCosts(ids)); + } + // console.log('Waiting for all'); + await Promise.all(promises); + for (const job of jobs) + { + if (job.log) + { + // console.log('Signalling resolve OK for ' + job.object.ID); + } + this.onObjectResolveRan.next(job.object); + } + } + } +} diff --git a/lib/classes/ObjectStoreFull.ts b/lib/classes/ObjectStoreFull.ts index 80a07f7..258f7ca 100644 --- a/lib/classes/ObjectStoreFull.ts +++ b/lib/classes/ObjectStoreFull.ts @@ -173,6 +173,10 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue)); this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined; + if (obj.IsAttachment && obj.State !== undefined) + { + this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State); + } this.objectsByUUID[objData.FullID.toString()] = localID; if (!this.objectsByParent[parentID]) @@ -194,12 +198,10 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore this.insertIntoRtree(obj); if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID]) { - this.requestMissingObject(objData.ParentID); - } - if (obj.ParentID === 0) - { - this.notifyObjectUpdate(newObject, obj); + this.requestMissingObject(objData.ParentID).then(() => {}).catch(() => {}); } + this.notifyObjectUpdate(newObject, obj); + obj.onTextureUpdate.next(); } } } @@ -415,13 +417,15 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore } o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0; + if (o.IsAttachment && o.State !== undefined) + { + this.objects[localID].attachmentPoint = this.decodeAttachPoint(o.State); + } this.insertIntoRtree(o); - if (o.ParentID === 0) - { - this.notifyObjectUpdate(newObj, o); - } + this.notifyObjectUpdate(newObj, o); + o.onTextureUpdate.next(); } } } @@ -429,6 +433,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore protected objectUpdateTerse(objectUpdateTerse: ImprovedTerseObjectUpdateMessage) { const dilation = objectUpdateTerse.RegionData.TimeDilation / 65535.0; + this.clientEvents.onRegionTimeDilation.next(dilation); for (let i = 0; i < objectUpdateTerse.ObjectData.length; i++) { @@ -479,8 +484,11 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore { // No idea why the first four bytes are skipped here. this.objects[localID].TextureEntry = TextureEntry.from(objectData.TextureEntry.slice(4)); + this.objects[localID].onTextureUpdate.next(); } this.insertIntoRtree(this.objects[localID]); + this.notifyTerseUpdate(this.objects[localID]); + } else { diff --git a/lib/classes/ObjectStoreLite.ts b/lib/classes/ObjectStoreLite.ts index de7b42a..e0566bc 100644 --- a/lib/classes/ObjectStoreLite.ts +++ b/lib/classes/ObjectStoreLite.ts @@ -34,6 +34,8 @@ import { CompressedFlags } from '../enums/CompressedFlags'; import { Vector3 } from './Vector3'; import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent'; import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent'; +import { Avatar } from './public/Avatar'; +import { AttachmentPoint } from '../enums/AttachmentPoint'; export class ObjectStoreLite implements IObjectStore { @@ -42,6 +44,7 @@ export class ObjectStoreLite implements IObjectStore protected objects: { [key: number]: GameObject } = {}; protected objectsByUUID: { [key: string]: number } = {}; protected objectsByParent: { [key: number]: number[] } = {}; + protected avatars: {[key: number]: Avatar} = {}; protected clientEvents: ClientEvents; protected options: BotOptionFlags; protected requestedObjects: {[key: number]: boolean} = {}; @@ -183,6 +186,11 @@ export class ObjectStoreLite implements IObjectStore { delete this.selectedPrimsWithoutUpdate[o.ID]; } + const n = Utils.BufferToStringSimple(obj.Name); + if (n === 'FullPerm') + { + const h = 5; + } o.creatorID = obj.CreatorID; o.creationDate = obj.CreationDate; o.baseMask = obj.BaseMask; @@ -211,6 +219,8 @@ export class ObjectStoreLite implements IObjectStore if (!o.resolvedAt) { o.resolvedAt = new Date().getTime() / 1000; + } + { const evt = new ObjectResolvedEvent(); evt.object = o; this.clientEvents.onObjectResolvedEvent.next(evt); @@ -343,6 +353,10 @@ export class ObjectStoreLite implements IObjectStore this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue)); this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined; + if (obj.IsAttachment && obj.State !== undefined) + { + this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State); + } if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString()) { @@ -404,10 +418,7 @@ export class ObjectStoreLite implements IObjectStore } } - if (obj.ParentID === 0) - { - this.notifyObjectUpdate(newObject, obj); - } + this.notifyObjectUpdate(newObject, obj); if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID]) { @@ -416,34 +427,118 @@ export class ObjectStoreLite implements IObjectStore } } - protected notifyObjectUpdate(newObject: boolean, obj: GameObject) + protected notifyTerseUpdate(obj: GameObject) { - if (newObject) + if (this.objects[obj.ID]) { - const newObj = new NewObjectEvent(); - newObj.localID = obj.ID; - newObj.objectID = obj.FullID; - newObj.object = obj; - newObj.createSelected = obj.Flags !== undefined && (obj.Flags & PrimFlags.CreateSelected) !== 0; - obj.createdSelected = newObj.createSelected; - if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()]) + if (obj.PCode === PCode.Avatar) { - this.selectedPrimsWithoutUpdate[obj.ID] = true; + if (this.avatars[obj.ID] !== undefined) + { + this.avatars[obj.ID].processObjectUpdate(obj); + } + else + { + console.warn('Received update for unknown avatar, but not a new object?!'); + } } - this.clientEvents.onNewObjectEvent.next(newObj); - } - else - { const updObj = new ObjectUpdatedEvent(); updObj.localID = obj.ID; updObj.objectID = obj.FullID; updObj.object = obj; - this.clientEvents.onObjectUpdatedEvent.next(updObj); + this.clientEvents.onObjectUpdatedTerseEvent.next(updObj); } - if (this.pendingObjectProperties[obj.FullID.toString()]) + } + + protected notifyObjectUpdate(newObject: boolean, obj: GameObject) + { + if (obj.ParentID === 0 || (obj.ParentID !== undefined && this.avatars[obj.ParentID] !== undefined)) { - this.applyObjectProperties(obj, this.pendingObjectProperties[obj.FullID.toString()]); - delete this.pendingObjectProperties[obj.FullID.toString()]; + if (newObject) + { + if (obj.PCode === PCode.Avatar) + { + if (this.avatars[obj.ID] === undefined) + { + this.avatars[obj.ID] = Avatar.fromGameObject(obj); + this.clientEvents.onAvatarEnteredRegion.next(this.avatars[obj.ID]) + } + } + if (obj.IsAttachment && obj.ParentID !== undefined) + { + if (this.avatars[obj.ParentID] !== undefined) + { + const avatar = this.avatars[obj.ParentID]; + + let invItemID = UUID.zero(); + if (obj.NameValue['AttachItemID']) + { + invItemID = new UUID(obj.NameValue['AttachItemID'].value); + } + + this.agent.currentRegion.clientCommands.region.resolveObject(obj, true, false).then(() => + { + try + { + if (obj.itemID === undefined) + { + obj.itemID = UUID.zero(); + } + obj.itemID = invItemID; + if (avatar !== undefined) + { + avatar.addAttachment(obj); + } + } + catch (err) + { + console.error(err); + } + }).catch((err) => + { + console.error('Failed to resolve new avatar attachment'); + }); + + } + } + + const newObj = new NewObjectEvent(); + newObj.localID = obj.ID; + newObj.objectID = obj.FullID; + newObj.object = obj; + newObj.createSelected = obj.Flags !== undefined && (obj.Flags & PrimFlags.CreateSelected) !== 0; + obj.createdSelected = newObj.createSelected; + if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()]) + { + this.selectedPrimsWithoutUpdate[obj.ID] = true; + } + this.clientEvents.onNewObjectEvent.next(newObj); + } + else + { + if (obj.PCode === PCode.Avatar) + { + if (this.avatars[obj.ID] !== undefined) + { + this.avatars[obj.ID].processObjectUpdate(obj); + } + else + { + console.warn('Received update for unknown avatar, but not a new object?!'); + } + } + + const updObj = new ObjectUpdatedEvent(); + updObj.localID = obj.ID; + updObj.objectID = obj.FullID; + updObj.object = obj; + this.clientEvents.onObjectUpdatedEvent.next(updObj); + } + if (this.pendingObjectProperties[obj.FullID.toString()]) + { + this.applyObjectProperties(obj, this.pendingObjectProperties[obj.FullID.toString()]); + delete this.pendingObjectProperties[obj.FullID.toString()]; + } } } @@ -605,14 +700,21 @@ export class ObjectStoreLite implements IObjectStore } o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0; - - if (o.ParentID === 0) + if (o.IsAttachment && o.State !== undefined) { - this.notifyObjectUpdate(newObj, o); + o.attachmentPoint = this.decodeAttachPoint(o.State); } + + this.notifyObjectUpdate(newObj, o); } } + protected decodeAttachPoint(state: number) + { + const mask = 0xf << 4 >>> 0; + return (((state & mask) >>> 4) | ((state & ~mask) << 4)) >>> 0; + } + protected objectUpdateTerse(objectUpdateTerse: ImprovedTerseObjectUpdateMessage) { } @@ -641,11 +743,32 @@ export class ObjectStoreLite implements IObjectStore } } + getAvatar(avatarID: UUID) + { + const obj = this.objectsByUUID[avatarID.toString()]; + if (obj !== undefined) + { + if (this.avatars[obj] !== undefined) + { + return this.avatars[obj]; + } + else + { + throw new Error('Found the UUID in the region, but it doesn\'t appear to be an avatar'); + } + } + else + { + throw new Error('Avatar does not exist in the region at the moment'); + } + } + deleteObject(objectID: number) { if (this.objects[objectID]) { - this.objects[objectID].deleted = true; + const obj = this.objects[objectID]; + obj.deleted = true; if (this.persist) { @@ -653,7 +776,22 @@ export class ObjectStoreLite implements IObjectStore return; } - // First, kill all children + if (obj.IsAttachment && obj.ParentID !== undefined) + { + if (this.avatars[obj.ParentID] !== undefined) + { + this.avatars[obj.ParentID].removeAttachment(obj); + } + } + + if (this.avatars[objectID] !== undefined) + { + this.clientEvents.onAvatarLeftRegion.next(this.avatars[objectID]); + this.avatars[objectID].leftRegion(); + delete this.avatars[objectID]; + } + + // First, kill all children (not the people kind) if (this.objectsByParent[objectID]) { for (const childObjID of this.objectsByParent[objectID]) @@ -664,16 +802,15 @@ export class ObjectStoreLite implements IObjectStore delete this.objectsByParent[objectID]; // Now delete this object - const objct = this.objects[objectID]; - const uuid = objct.FullID.toString(); + const uuid = obj.FullID.toString(); if (this.objectsByUUID[uuid]) { delete this.objectsByUUID[uuid]; } - if (objct.ParentID !== undefined) + if (obj.ParentID !== undefined) { - const parentID = objct.ParentID; + const parentID = obj.ParentID; if (this.objectsByParent[parentID]) { const ind = this.objectsByParent[parentID].indexOf(objectID); @@ -683,9 +820,9 @@ export class ObjectStoreLite implements IObjectStore } } } - if (this.rtree && this.objects[objectID].rtreeEntry !== undefined) + if (this.rtree && obj.rtreeEntry !== undefined) { - this.rtree.remove(this.objects[objectID].rtreeEntry); + this.rtree.remove(obj.rtreeEntry); } delete this.objects[objectID]; } @@ -767,7 +904,7 @@ export class ObjectStoreLite implements IObjectStore } } - private populateChildren(obj: GameObject) + populateChildren(obj: GameObject, resolve = false) { if (obj !== undefined) { @@ -801,7 +938,7 @@ export class ObjectStoreLite implements IObjectStore try { const parent = this.findParent(go); - if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false) && parent.ParentID === 0) + if (parent.ParentID === 0) { const uuid = parent.FullID.toString(); diff --git a/lib/classes/Region.ts b/lib/classes/Region.ts index cafcacf..acad48b 100644 --- a/lib/classes/Region.ts +++ b/lib/classes/Region.ts @@ -46,6 +46,7 @@ import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent'; import { PacketFlags } from '../enums/PacketFlags'; import { Vector3 } from './Vector3'; import { Vector2 } from './Vector2'; +import { ObjectResolver } from './ObjectResolver'; export class Region { @@ -144,6 +145,8 @@ export class Region timeOffset = 0; + resolver: ObjectResolver = new ObjectResolver(this); + private parcelOverlayReceived: {[key: number]: Buffer} = {}; static IDCTColumn16(linein: number[], lineout: number[], column: number) @@ -604,13 +607,13 @@ export class Region } }); const parcelID: string = dwellReply.Data.ParcelID.toString(); - let parcel = new Parcel(); + let parcel = new Parcel(this); if (this.parcelsByUUID[parcelID]) { parcel = this.parcelsByUUID[parcelID]; } parcel.LocalID = parcelProperties.LocalID; - parcel.ParcelID = dwellReply.Data.ParcelID; + parcel.ParcelID = new UUID(dwellReply.Data.ParcelID.toString()); parcel.RegionDenyAgeUnverified = parcelProperties.RegionDenyTransacted; parcel.MediaDesc = parcelProperties.MediaDesc; parcel.MediaHeight = parcelProperties.MediaHeight; @@ -624,7 +627,7 @@ export class Region parcel.AnyAVSounds = parcelProperties.AnyAVSounds; parcel.Area = parcelProperties.Area; parcel.AuctionID = parcelProperties.AuctionID; - parcel.AuthBuyerID = parcelProperties.AuthBuyerID; + parcel.AuthBuyerID = new UUID(parcelProperties.AuthBuyerID.toString()); parcel.Bitmap = parcelProperties.Bitmap; parcel.Category = parcelProperties.Category; parcel.ClaimDate = parcelProperties.ClaimDate; @@ -632,20 +635,20 @@ export class Region parcel.Desc = parcelProperties.Desc; parcel.Dwell = dwellReply.Data.Dwell; parcel.GroupAVSounds = parcelProperties.GroupAVSounds; - parcel.GroupID = parcelProperties.GroupID; + 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 = parcelProperties.MediaID; + 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 = parcelProperties.OwnerID; + parcel.OwnerID = new UUID(parcelProperties.OwnerID.toString()); parcel.OwnerPrims = parcelProperties.OwnerPrims; parcel.ParcelFlags = parcelProperties.ParcelFlags; parcel.ParcelPrimBonus = parcelProperties.ParcelPrimBonus; @@ -666,7 +669,7 @@ export class Region parcel.SimWideMaxPrims = parcelProperties.SimWideMaxPrims; parcel.SimWideTotalPrims = parcelProperties.SimWideTotalPrims; parcel.SnapSelection = parcelProperties.SnapSelection; - parcel.SnapshotID = parcelProperties.SnapshotID; + parcel.SnapshotID = new UUID(parcelProperties.SnapshotID.toString()); parcel.Status = parcelProperties.Status; parcel.TotalPrims = parcelProperties.TotalPrims; parcel.UserLocation = parcelProperties.UserLocation; diff --git a/lib/classes/TarArchive.ts b/lib/classes/TarArchive.ts new file mode 100644 index 0000000..1baca2c --- /dev/null +++ b/lib/classes/TarArchive.ts @@ -0,0 +1,6 @@ +import { TarFile } from './TarFile'; + +export class TarArchive +{ + files: TarFile[] = []; +} diff --git a/lib/classes/TarFile.ts b/lib/classes/TarFile.ts new file mode 100644 index 0000000..00d3148 --- /dev/null +++ b/lib/classes/TarFile.ts @@ -0,0 +1,44 @@ +import * as fs from 'fs'; + +export class TarFile +{ + fileName: string; + fileMode: number; + userID: number; + groupID: number; + modifyTime: Date; + linkIndicator: number; + linkedFile: string; + offset: number; + fileSize: number; + archiveFile: string; + + read(): Promise + { + return new Promise((resolve, reject) => + { + fs.open(this.archiveFile, 'r', (err: Error | null, fd: number) => + { + if (err) + { + reject(err); + } + else + { + const buf = Buffer.alloc(this.fileSize); + fs.read(fd, buf, 0, this.fileSize, this.offset, (err2: Error | null, bytesRead: number, buffer: Buffer) => + { + if (err2) + { + reject(err2); + } + else + { + resolve(buffer); + } + }) + } + }); + }); + } +} diff --git a/lib/classes/TarReader.ts b/lib/classes/TarReader.ts new file mode 100644 index 0000000..5fe60fe --- /dev/null +++ b/lib/classes/TarReader.ts @@ -0,0 +1,201 @@ +import { TarFile } from './TarFile'; +import { TarArchive } from './TarArchive'; +import { Readable } from 'stream'; + +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import * as uuid from 'uuid'; + +export class TarReader +{ + private outFile: string; + + constructor(private fileName: string) + { + + } + + parse(stream: Readable): Promise + { + return new Promise((resolve, reject) => + { + let longName = false; + let readState = 0; // 0 = waiting for header, 1 = reading file, 2 = padding, 3 = end of file + let queuedChunks: Buffer[] = []; + let fileChunks: Buffer[] = []; + let queuedBytes = 0; + let remainingBytes = 0; + let longNameStr: string | undefined = undefined; + let fileSize = 0; + let paddingSize = 0; + let pos = 0; + let fileCount = 0; + const archive = new TarArchive(); + this.outFile = path.resolve(os.tmpdir() + '/' + uuid.v4() + '.tar'); + const outStream = fs.openSync(this.outFile, 'w'); + stream.on('data', (chunk: Buffer) => + { + fs.writeSync(outStream, chunk); + let goAgain = false; + do + { + goAgain = false; + if (readState === 1) + { + if (chunk.length > remainingBytes) + { + const wantedBytes = chunk.length - remainingBytes; + if (longName) + { + fileChunks.push(chunk.slice(0, chunk.length - wantedBytes)); + } + queuedChunks = [chunk.slice(chunk.length - wantedBytes)]; + queuedBytes = queuedChunks[0].length; + remainingBytes = 0; + } + else + { + remainingBytes -= chunk.length; + if (longName) + { + fileChunks.push(chunk); + } + } + } + else + { + queuedChunks.push(chunk); + queuedBytes += chunk.length; + } + + if (readState === 0) + { + if (queuedBytes >= 512) + { + const buf = Buffer.concat(queuedChunks); + const header = buf.slice(0, 512); + queuedChunks = [buf.slice(512)]; + queuedBytes = queuedChunks[0].length; + + let hdrFileName = this.trimEntry(header.slice(0, 100)); + console.log('Filename: ' + hdrFileName); + const hdrFileMode = this.decodeOctal(header.slice(100, 100 + 8)); + const hdrUserID = this.decodeOctal(header.slice(108, 108 + 8)); + const hdrGroupID = this.decodeOctal(header.slice(116, 116 + 8)); + fileSize = this.decodeOctal(header.slice(124, 124 + 12)); + const hdrModifyTime = this.decodeOctal(header.slice(136, 136 + 12)); + const checksum = this.decodeOctal(header.slice(148, 148 + 8)); + const linkIndicator = header[156]; + const linkedFile = this.trimEntry(header.slice(157, 157 + 100)); + paddingSize = (Math.ceil(fileSize / 512) * 512) - fileSize; + + // Check CRC + let sum = 8 * 32; + for (let x = 0; x < 512; x++) + { + if (x < 148 || x > 155) + { + sum += header[x]; + } + } + fileCount++; + if (sum !== checksum) + { + readState = 3; + continue; + } + if (linkIndicator === 76) + { + longName = true; + } + else + { + if (longNameStr !== undefined) + { + hdrFileName = longNameStr; + longNameStr = undefined; + longName = false; + } + const file = new TarFile(); + file.archiveFile = this.outFile; + file.fileName = hdrFileName; + file.fileMode = hdrFileMode; + file.userID = hdrUserID; + file.groupID = hdrGroupID; + file.modifyTime = new Date(hdrModifyTime * 1000); + file.linkIndicator = linkIndicator; + file.linkedFile = linkedFile; + file.offset = pos + 512; + file.fileSize = fileSize; + archive.files.push(file); + } + remainingBytes = fileSize; + readState = 1; + goAgain = true; + chunk = queuedChunks[0]; + queuedBytes = 0; + queuedChunks = []; + pos += 512; + continue; + } + } + if (readState === 1 && remainingBytes === 0) + { + if (longName) + { + longNameStr = Buffer.concat(fileChunks).toString('ascii'); + fileChunks = []; + } + pos += fileSize; + readState = 2; + } + if (readState === 2 && queuedBytes >= paddingSize) + { + const buf = Buffer.concat(queuedChunks); + queuedChunks = [buf.slice(paddingSize)]; + queuedBytes = queuedChunks[0].length; + readState = 0; + chunk = Buffer.alloc(0); + goAgain = true; + pos += paddingSize; + } + } + while (goAgain); + }).on('end', () => + { + if ((readState !== 0 && readState !== 3) || queuedBytes > 0) + { + console.warn('Warning: Garbage at end of file'); + } + fs.closeSync(outStream); + resolve(archive); + }).on('error', (err) => + { + reject(err); + }); + }); + } + + close() + { + fs.unlinkSync(this.outFile); + this.outFile = ''; + } + + private trimEntry(buf: Buffer) + { + let end = buf.indexOf('\0'); + if (end === -1) + { + end = buf.length - 1; + } + return buf.slice(0, end).toString('ascii'); + } + + private decodeOctal(buf: Buffer) + { + const str = this.trimEntry(buf); + return parseInt(str, 8); + } +} diff --git a/lib/classes/TarWriter.ts b/lib/classes/TarWriter.ts new file mode 100644 index 0000000..b859439 --- /dev/null +++ b/lib/classes/TarWriter.ts @@ -0,0 +1,143 @@ +import * as fs from 'fs'; +import { Readable, Transform } from 'stream'; + +export class TarWriter extends Transform +{ + private thisFileSize = 0; + + private realPath: string; + private fileActive = false; + + async newFile(archivePath: string, realPath: string) + { + if (this.fileActive) + { + this.endFile(); + } + const stat = fs.statSync(realPath); + + //if (archivePath.length > 100) + //{ + const buf = Buffer.from(archivePath, 'ascii'); + this.writeHeader( + this.chopString('././@LongName', 100), + stat.mode, + stat.uid, + stat.gid, + buf.length, + stat.mtime, + 'L' + ); + this.thisFileSize = buf.length; + await this.pipeFromBuffer(buf); + this.endFile(); + //} + + this.writeHeader( + this.chopString(archivePath, 100), + stat.mode, + stat.uid, + stat.gid, + stat.size, + stat.mtime, + '0' + ); + + this.thisFileSize = stat.size; + this.fileActive = true; + } + + async pipeFromBuffer(buf: Buffer): Promise + { + const readableInstanceStream = new Readable({ + read() + { + this.push(buf); + this.push(null); + } + }); + return this.pipeFrom(readableInstanceStream); + } + + pipeFrom(str: Readable): Promise + { + return new Promise((resolve, reject) => + { + str.on('error', (err) => + { + reject(err); + }); + str.on('end', () => + { + resolve(); + }); + str.pipe(this, {end: false}); + }); + } + + private async writeHeader(fileName: string, mode: number, uid: number, gid: number, fileSize: number, mTime: Date, fileType: string) + { + const header = Buffer.alloc(512); + + const name = this.chopString(fileName, 100); + header.write(name, 0, (name.length <= 100 ? name.length : 100)); + + this.octalBuf(mode, 8).copy(header, 100); + this.octalBuf(uid, 8).copy(header, 108); + this.octalBuf(gid, 8).copy(header, 116); + this.octalBuf(fileSize, 12).copy(header, 124); + this.octalBuf(Math.floor(mTime.getTime() / 1000), 12).copy(header, 136); + + header.write(fileType, 156, 1); + + let sum = 8 * 32; + for (let x = 0; x < 512; x++) + { + if (x < 148 || x > 155) + { + sum += header.readUInt8(x); + } + } + let sumStr = this.octalString(sum, 6); + while (sumStr.length < 6) + { + sumStr = '0' + sumStr; + } + sumStr += '\0 '; + header.write(sumStr, 148, sumStr.length); + return this.pipeFromBuffer(header); + } + + async endFile() + { + const finalSize = Math.ceil(this.thisFileSize / 512) * 512; + const remainingSize = finalSize - this.thisFileSize; + const buf = Buffer.alloc(remainingSize); + await this.pipeFromBuffer(buf); + this.fileActive = false; + } + + public _transform(chunk: any, encoding: string, callback: (error?: Error, data?: any) => void): void + { + this.push(chunk, encoding); + callback(); + } + + private chopString(str: string, maxLength: number): string + { + return str.substr(0, maxLength - 1); + } + + private octalBuf(num: number, length: number): Buffer + { + const buf = Buffer.alloc(length - 1, '0'); + const result = this.chopString(Math.floor(num).toString(8), length); + buf.write(result, length - (result.length + 1), result.length); + return buf; + } + + private octalString(num: number, length: number): string + { + return this.octalBuf(num, length).toString('ascii'); + } +} diff --git a/lib/classes/UUID.ts b/lib/classes/UUID.ts index 81c4040..9f74f8d 100644 --- a/lib/classes/UUID.ts +++ b/lib/classes/UUID.ts @@ -91,6 +91,10 @@ export class UUID + hexString.substr(16, 4) + '-' + hexString.substr(20, 12)); } + else if (typeof buf === 'object' && buf.toString !== undefined) + { + this.setUUID(buf.toString()); + } else { console.error('Can\'t accept UUIDs of type ' + typeof buf); @@ -125,6 +129,11 @@ export class UUID binary.copy(buf, pos, 0); } + public isZero(): boolean + { + return (this.mUUID === '00000000-0000-0000-0000-000000000000'); + } + public equals(cmp: UUID | string): boolean { if (typeof cmp === 'string') @@ -133,6 +142,10 @@ export class UUID } else { + if (cmp.equals === undefined) + { + throw new Error(cmp.constructor.name + ' is not a UUID'); + } return cmp.equals(this.mUUID); } } diff --git a/lib/classes/Utils.ts b/lib/classes/Utils.ts index f40f3b0..59b71f4 100644 --- a/lib/classes/Utils.ts +++ b/lib/classes/Utils.ts @@ -3,10 +3,15 @@ import { Quaternion } from './Quaternion'; import { GlobalPosition } from './public/interfaces/GlobalPosition'; import { HTTPAssets } from '../enums/HTTPAssets'; import { Vector3 } from './Vector3'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { AssetType } from '../enums/AssetType'; -import { InventoryTypeLL } from '../enums/InventoryTypeLL'; +import { InventoryType } from '../enums/InventoryType'; +import * as zlib from 'zlib'; +import { FilterResponse } from '../enums/FilterResponse'; import Timeout = NodeJS.Timeout; +import * as xml2js from 'xml2js'; +import { XMLElement } from 'xmlbuilder'; +import { Logger } from './Logger'; export class Utils { @@ -134,6 +139,46 @@ export class Utils }; } + static InventoryTypeToLLInventoryType(type: InventoryType): string + { + switch (type) + { + case InventoryType.Texture: + return 'texture'; + case InventoryType.Sound: + return 'sound'; + case InventoryType.CallingCard: + return 'callcard'; + case InventoryType.Landmark: + return 'landmark'; + case InventoryType.Object: + return 'object'; + case InventoryType.Notecard: + return 'notecard'; + case InventoryType.Category: + return 'category'; + case InventoryType.RootCategory: + return 'root'; + case InventoryType.Script: + return 'script'; + case InventoryType.Snapshot: + return 'snapshot'; + case InventoryType.Attachment: + return 'attach'; + case InventoryType.Wearable: + return 'wearable'; + case InventoryType.Animation: + return 'animation'; + case InventoryType.Gesture: + return 'gesture'; + case InventoryType.Mesh: + return 'mesh'; + default: + console.error('Unknown inventory type: ' + InventoryType[type]); + return 'texture'; + } + } + static HTTPAssetTypeToAssetType(HTTPAssetType: string): AssetType { switch (HTTPAssetType) @@ -171,38 +216,76 @@ export class Utils } } - static HTTPAssetTypeToInventoryType(HTTPAssetType: string): InventoryTypeLL + static AssetTypeToHTTPAssetType(assetType: AssetType): HTTPAssets + { + switch (assetType) + { + case AssetType.Texture: + return HTTPAssets.ASSET_TEXTURE; + case AssetType.Sound: + return HTTPAssets.ASSET_SOUND; + case AssetType.Animation: + return HTTPAssets.ASSET_ANIMATION; + case AssetType.Gesture: + return HTTPAssets.ASSET_GESTURE; + case AssetType.Landmark: + return HTTPAssets.ASSET_LANDMARK; + case AssetType.CallingCard: + return HTTPAssets.ASSET_CALLINGCARD; + case AssetType.Script: + return HTTPAssets.ASSET_SCRIPT; + case AssetType.Clothing: + return HTTPAssets.ASSET_CLOTHING; + case AssetType.Object: + return HTTPAssets.ASSET_OBJECT; + case AssetType.Notecard: + return HTTPAssets.ASSET_NOTECARD; + case AssetType.LSLText: + return HTTPAssets.ASSET_LSL_TEXT; + case AssetType.LSLBytecode: + return HTTPAssets.ASSET_LSL_BYTECODE; + case AssetType.Bodypart: + return HTTPAssets.ASSET_BODYPART; + case AssetType.Mesh: + return HTTPAssets.ASSET_MESH; + default: + return HTTPAssets.ASSET_TEXTURE; + } + + } + + static HTTPAssetTypeToInventoryType(HTTPAssetType: string): InventoryType { switch (HTTPAssetType) { case HTTPAssets.ASSET_TEXTURE: - return InventoryTypeLL.texture; + return InventoryType.Texture; case HTTPAssets.ASSET_SOUND: - return InventoryTypeLL.sound; + return InventoryType.Sound; case HTTPAssets.ASSET_ANIMATION: - return InventoryTypeLL.animation; + return InventoryType.Animation; case HTTPAssets.ASSET_GESTURE: - return InventoryTypeLL.gesture; + return InventoryType.Gesture; case HTTPAssets.ASSET_LANDMARK: - return InventoryTypeLL.landmark; + return InventoryType.Landmark; case HTTPAssets.ASSET_CALLINGCARD: - return InventoryTypeLL.callcard; + return InventoryType.CallingCard; case HTTPAssets.ASSET_SCRIPT: - return InventoryTypeLL.script; + return InventoryType.Script; case HTTPAssets.ASSET_CLOTHING: - return InventoryTypeLL.wearable; + return InventoryType.Wearable; case HTTPAssets.ASSET_OBJECT: - return InventoryTypeLL.object; + return InventoryType.Object; case HTTPAssets.ASSET_NOTECARD: - return InventoryTypeLL.notecard; + return InventoryType.Notecard; case HTTPAssets.ASSET_LSL_TEXT: - return InventoryTypeLL.script; + return InventoryType.Script; case HTTPAssets.ASSET_LSL_BYTECODE: - return InventoryTypeLL.script; + return InventoryType.Script; case HTTPAssets.ASSET_BODYPART: - return InventoryTypeLL.wearable; + return InventoryType.Wearable; case HTTPAssets.ASSET_MESH: - return InventoryTypeLL.mesh; + return InventoryType.Mesh; default: return 0; } @@ -290,13 +373,13 @@ export class Utils static Base64EncodeString(str: string): string { - const buff = new Buffer(str, 'utf8'); + const buff = Buffer.from(str, 'utf8'); return buff.toString('base64'); } static Base64DecodeString(str: string): string { - const buff = new Buffer(str, 'base64'); + const buff = Buffer.from(str, 'base64'); return buff.toString('utf8'); } @@ -383,6 +466,52 @@ export class Utils return Math.floor(((Utils.IEEERemainder(rotation, Utils.TWO_PI) / Utils.TWO_PI) * 32768.0) + 0.5); } + static OctetsToUInt32BE(octets: number[]) + { + const buf = Buffer.allocUnsafe(4); + let pos = 0; + for (let x = octets.length - 4; x < octets.length; x++) + { + if (x >= 0) + { + buf.writeUInt8(octets[x], pos++); + } + else + { + pos++; + } + } + return buf.readUInt32BE(0); + } + + static OctetsToUInt32LE(octets: number[]) + { + const buf = Buffer.allocUnsafe(4); + let pos = 0; + for (let x = octets.length - 4; x < octets.length; x++) + { + if (x >= 0) + { + buf.writeUInt8(octets[x], pos++); + } + else + { + pos++; + } + } + return buf.readUInt32LE(0); + } + + static numberToFixedHex(num: number) + { + let str = num.toString(16); + while (str.length < 8) + { + str = '0' + str; + } + return str; + } + static TEGlowByte(glow: number) { return (glow * 255.0); @@ -535,6 +664,7 @@ export class Utils { const originalConcurrency = concurrency; const promiseQueue: (() => Promise)[] = []; + Logger.Info('PromiseConcurrent: ' + promiseQueue.length + ' in queue. Concurrency: ' + concurrency); for (const promise of promises) { promiseQueue.push(promise); @@ -587,13 +717,16 @@ export class Utils concurrency++; slotAvailable.next(); }); - timeo = setTimeout(() => + if (timeout > 0) { - timedOut = true; - errors.push(new Error('Promise timed out')); - concurrency++; - slotAvailable.next(); - }, timeout); + timeo = setTimeout(() => + { + timedOut = true; + errors.push(new Error('Promise timed out')); + concurrency++; + slotAvailable.next(); + }, timeout); + } } while (promiseQueue.length > 0) @@ -618,4 +751,195 @@ export class Utils resolve({results: results, errors: errors}); }); } + + static waitFor(timeout: number): Promise + { + return new Promise((resolve, reject) => + { + setTimeout(() => + { + resolve(); + }, timeout); + }) + } + + static getFromXMLJS(obj: any, param: string): any + { + if (obj[param] === undefined) + { + return undefined; + } + let retParam; + if (Array.isArray(obj[param])) + { + retParam = obj[param][0]; + } + else + { + retParam = obj[param]; + } + if (typeof retParam === 'string') + { + if (retParam.toLowerCase() === 'false') + { + return false; + } + if (retParam.toLowerCase() === 'true') + { + return true; + } + const numVar = parseInt(retParam, 10); + if (numVar >= Number.MIN_SAFE_INTEGER && numVar <= Number.MAX_SAFE_INTEGER && String(numVar) === retParam) + { + return numVar + } + } + return retParam; + } + static inflate(buf: Buffer): Promise + { + return new Promise((resolve, reject) => + { + zlib.inflate(buf, (error: (Error| null), result: Buffer) => + { + if (error) + { + reject(error) + } + else + { + resolve(result); + } + }) + }); + } + static deflate(buf: Buffer): Promise + { + return new Promise((resolve, reject) => + { + zlib.deflate(buf, { level: 9}, (error: (Error| null), result: Buffer) => + { + if (error) + { + reject(error) + } + else + { + resolve(result); + } + }) + }); + } + static waitOrTimeOut(subject: Subject, timeout?: number, callback?: (msg: T) => FilterResponse): Promise + { + return new Promise((resolve, reject) => + { + let timer: Timeout | undefined = undefined; + let subs: Subscription | undefined = undefined; + subs = subject.subscribe((result: T) => + { + if (callback !== undefined) + { + const accepted = callback(result); + if (accepted !== FilterResponse.Finish) + { + return; + } + } + if (timer !== undefined) + { + clearTimeout(timer); + timer = undefined; + } + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + resolve(result); + }); + if (timeout !== undefined) + { + timer = setTimeout(() => + { + if (timer !== undefined) + { + clearTimeout(timer); + timer = undefined; + } + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + reject(new Error('Timeout')); + }, timeout); + } + }) + } + + static parseLine(line: string): { + 'key': string | null, + 'value': string + } + { + line = line.trim().replace(/[\t]/gu, ' ').trim(); + while (line.indexOf('\u0020\u0020') > 0) + { + line = line.replace(/\u0020\u0020/gu, '\u0020'); + } + let key: string | null = null; + let value = ''; + if (line.length > 2) + { + const sep = line.indexOf(' '); + if (sep > 0) + { + key = line.substr(0, sep); + value = line.substr(sep + 1); + } + } + else if (line.length === 1) + { + key = line; + } + else if (line.length > 0) + { + return { + 'key': line, + 'value': '' + } + } + if (key !== null) + { + key = key.trim(); + } + return { + 'key': key, + 'value': value + } + } + + static sanitizePath(input: string) + { + return input.replace(/[^a-z0-9]/gi, '').replace(/ /gi, '_'); + } + + static parseXML(input: string): Promise + { + return new Promise((resolve, reject) => + { + xml2js.parseString(input, (err: Error, result: any) => + { + if (err) + { + reject(err); + } + else + { + resolve(result); + } + }); + }); + } } diff --git a/lib/classes/commands/AgentCommands.ts b/lib/classes/commands/AgentCommands.ts index 6ab1e58..f669c9f 100644 --- a/lib/classes/commands/AgentCommands.ts +++ b/lib/classes/commands/AgentCommands.ts @@ -10,6 +10,7 @@ import { AvatarPropertiesReplyMessage } from '../messages/AvatarPropertiesReply' import { AvatarPropertiesRequestMessage } from '../messages/AvatarPropertiesRequest'; import { AvatarPropertiesReplyEvent } from '../../events/AvatarPropertiesReplyEvent'; import { Subscription } from 'rxjs'; +import { Avatar } from '../public/Avatar'; export class AgentCommands extends CommandsBase { @@ -70,11 +71,16 @@ export class AgentCommands extends CommandsBase this.agent.sendAgentUpdate(); } - waitForAppearanceSet(timeout: number = 10000): Promise + async getWearables() + { + return this.agent.getWearables(); + } + + waitForAppearanceComplete(timeout: number = 30000): Promise { return new Promise((resolve, reject) => { - if (this.agent.appearanceSet) + if (this.agent.appearanceComplete) { resolve(); } @@ -82,7 +88,7 @@ export class AgentCommands extends CommandsBase { let appearanceSubscription: Subscription | undefined; let timeoutTimer: number | undefined; - appearanceSubscription = this.agent.appearanceSetEvent.subscribe(() => + appearanceSubscription = this.agent.appearanceCompleteEvent.subscribe(() => { if (timeoutTimer !== undefined) { @@ -110,7 +116,7 @@ export class AgentCommands extends CommandsBase reject(new Error('Timeout')); } }, timeout) as any as number; - if (this.agent.appearanceSet) + if (this.agent.appearanceComplete) { if (appearanceSubscription !== undefined) { @@ -128,6 +134,19 @@ export class AgentCommands extends CommandsBase }); } + getAvatar(avatarID: UUID | string = UUID.zero()): Avatar + { + if (typeof avatarID === 'string') + { + avatarID = new UUID(avatarID); + } + else if (avatarID.isZero()) + { + avatarID = this.agent.agentID; + } + return this.currentRegion.objects.getAvatar(avatarID); + } + async getAvatarProperties(avatarID: UUID | string): Promise { if (typeof avatarID === 'string') diff --git a/lib/classes/commands/AssetCommands.ts b/lib/classes/commands/AssetCommands.ts index deed003..dd209a9 100644 --- a/lib/classes/commands/AssetCommands.ts +++ b/lib/classes/commands/AssetCommands.ts @@ -2,10 +2,8 @@ import { CommandsBase } from './CommandsBase'; import { UUID } from '../UUID'; import * as LLSD from '@caspertech/llsd'; import { Utils } from '../Utils'; -import { PermissionMask } from '../../enums/PermissionMask'; import * as zlib from 'zlib'; import { ZlibOptions } from 'zlib'; -import { Color4 } from '../Color4'; import { TransferRequestMessage } from '../messages/TransferRequest'; import { TransferChannelType } from '../../enums/TransferChannelType'; import { TransferSourceType } from '../../enums/TransferSourceTypes'; @@ -18,18 +16,19 @@ import { AssetType } from '../../enums/AssetType'; import { PacketFlags } from '../../enums/PacketFlags'; import { TransferStatus } from '../../enums/TransferStatus'; import { Material } from '../public/Material'; -import { LLMesh } from '../public/LLMesh'; -import { FolderType } from '../../enums/FolderType'; import { HTTPAssets } from '../../enums/HTTPAssets'; +import { InventoryFolder } from '../InventoryFolder'; import { InventoryItem } from '../InventoryItem'; -import { CreateInventoryItemMessage } from '../messages/CreateInventoryItem'; -import { WearableType } from '../../enums/WearableType'; -import { UpdateCreateInventoryItemMessage } from '../messages/UpdateCreateInventoryItem'; +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 { - private callbackID: number = 1; + private callbackID = 0; + async downloadAsset(type: HTTPAssets, uuid: UUID | string): Promise { if (typeof uuid === 'string') @@ -59,6 +58,49 @@ export class AssetCommands extends CommandsBase } } + async copyInventoryFromNotecard(notecardID: UUID, folder: InventoryFolder, itemID: UUID, objectID: UUID = UUID.zero()): Promise + { + const gotCap = await this.currentRegion.caps.isCapAvailable('CopyInventoryFromNotecard'); + if (gotCap) + { + const callbackID = Math.floor(Math.random() * 2147483647); + const request = { + 'callback-id': callbackID, + 'folder-id': new LLSD.UUID(folder.folderID.toString()), + 'item-id': new LLSD.UUID(itemID.toString()), + 'notecard-id': new LLSD.UUID(notecardID.toString()), + 'object-id': new LLSD.UUID(objectID.toString()) + }; + this.currentRegion.caps.capsPostXML('CopyInventoryFromNotecard', request).then(() => {}).catch((err) => + { + throw err; + }); + const evt: BulkUpdateInventoryEvent = await Utils.waitOrTimeOut(this.currentRegion.clientEvents.onBulkUpdateInventoryEvent, 10000, (event: BulkUpdateInventoryEvent) => + { + for (const item of event.itemData) + { + if (item.callbackID === callbackID) + { + return FilterResponse.Finish; + } + } + return FilterResponse.NoMatch; + }); + for (const item of evt.itemData) + { + if (item.callbackID === callbackID) + { + return item; + } + } + throw new Error('No match'); + } + else + { + throw new Error('CopyInventoryFromNotecard cap not available'); + } + } + transfer(channelType: TransferChannelType, sourceType: TransferSourceType, priority: boolean, transferParams: Buffer, outAssetID?: { assetID: UUID }): Promise { return new Promise((resolve, reject) => @@ -72,12 +114,45 @@ export class AssetCommands extends CommandsBase Priority: 100.0 + (priority ? 1.0 : 0.0), Params: transferParams }; - - this.circuit.sendMessage(msg, PacketFlags.Reliable); let gotInfo = true; let expectedSize = 0; const packets: { [key: number]: Buffer } = {}; - const subscription = this.circuit.subscribeToMessages([ + let subscription: Subscription | undefined = undefined; + let timeout: number | undefined; + + function cleanup() + { + if (subscription !== undefined) + { + subscription.unsubscribe(); + subscription = undefined; + } + if (timeout !== undefined) + { + clearTimeout(timeout); + timeout = undefined; + } + } + + function placeTimeout() + { + timeout = setTimeout(() => + { + cleanup(); + reject(new Error('Timeout')); + }, 10000) as any as number; + } + + function resetTimeout() + { + if (timeout !== undefined) + { + clearTimeout(timeout); + } + placeTimeout(); + } + + subscription = this.circuit.subscribeToMessages([ Message.TransferInfo, Message.TransferAbort, Message.TransferPacket @@ -90,26 +165,31 @@ export class AssetCommands extends CommandsBase case Message.TransferPacket: { const messg = packet.message as TransferPacketMessage; + if (!messg.TransferData.TransferID.equals(transferID)) + { + return; + } + resetTimeout(); packets[messg.TransferData.Packet] = messg.TransferData.Data; switch (messg.TransferData.Status) { case TransferStatus.Abort: - subscription.unsubscribe(); + cleanup(); reject(new Error('Transfer Aborted')); break; case TransferStatus.Error: - subscription.unsubscribe(); + cleanup(); reject(new Error('Error')); break; case TransferStatus.Skip: console.error('TransferPacket: Skip! not sure what this means'); break; case TransferStatus.InsufficientPermissions: - subscription.unsubscribe(); + cleanup(); reject(new Error('Insufficient Permissions')); break; case TransferStatus.NotFound: - subscription.unsubscribe(); + cleanup(); reject(new Error('Not Found')); break; } @@ -122,6 +202,7 @@ export class AssetCommands extends CommandsBase { return; } + resetTimeout(); const status = messg.TransferInfo.Status as TransferStatus; switch (status) { @@ -134,23 +215,23 @@ export class AssetCommands extends CommandsBase } break; case TransferStatus.Abort: - subscription.unsubscribe(); + cleanup(); reject(new Error('Transfer Aborted')); break; case TransferStatus.Error: - subscription.unsubscribe(); - reject(new Error('Error')); + cleanup(); + reject(new Error('Error downloading asset')); // See if we get anything else break; case TransferStatus.Skip: console.error('TransferInfo: Skip! not sure what this means'); break; case TransferStatus.InsufficientPermissions: - subscription.unsubscribe(); + cleanup(); reject(new Error('Insufficient Permissions')); break; case TransferStatus.NotFound: - subscription.unsubscribe(); + cleanup(); reject(new Error('Not Found')); break; } @@ -164,7 +245,8 @@ export class AssetCommands extends CommandsBase { return; } - subscription.unsubscribe(); + resetTimeout(); + cleanup(); reject(new Error('Transfer Aborted')); return; } @@ -188,24 +270,33 @@ export class AssetCommands extends CommandsBase { buffers.push(packets[parseInt(pn, 10)]); } - subscription.unsubscribe(); + cleanup(); resolve(Buffer.concat(buffers)); } } } catch (error) { - subscription.unsubscribe(); + cleanup(); reject(error); } }); + placeTimeout(); + this.circuit.sendMessage(msg, PacketFlags.Reliable); }); } - downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise + downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }, sourceType: TransferSourceType = TransferSourceType.SimInventoryItem, channelType: TransferChannelType = TransferChannelType.Asset): Promise { return new Promise((resolve, reject) => { + if (type === AssetType.Notecard && assetID.isZero()) + { + // Empty notecard + const note = new LLLindenText(); + resolve(note.toAsset()); + } + const transferParams = Buffer.allocUnsafe(100); let pos = 0; this.agent.agentID.writeToBuffer(transferParams, pos); @@ -222,7 +313,7 @@ export class AssetCommands extends CommandsBase pos = pos + 16; transferParams.writeInt32LE(type, pos); - this.transfer(TransferChannelType.Asset, TransferSourceType.SimInventoryItem, priority, transferParams, outAssetID).then((result) => + this.transfer(channelType, sourceType, priority, transferParams, outAssetID).then((result) => { resolve(result); }).catch((err) => @@ -233,85 +324,45 @@ export class AssetCommands extends CommandsBase }); } - private getMaterialsLimited(uuidArray: any[], uuids: {[key: string]: Material | null}): Promise + private async getMaterialsLimited(uuidArray: any[], uuids: {[key: string]: Material | null}) { - return new Promise((resolve, reject) => - { - const binary = LLSD.LLSD.formatBinary(uuidArray); - const options: ZlibOptions = { - level: 9 - }; - zlib.deflate(Buffer.from(binary.toArray()), options, async (error: Error | null, res: Buffer) => - { - if (error) - { - reject(error); - return; - } - const result = await this.currentRegion.caps.capsPostXML('RenderMaterials', { - 'Zipped': new LLSD.LLSD.asBinary(res.toString('base64')) - }); - - const resultZipped = Buffer.from(result['Zipped'].octets); - zlib.inflate(resultZipped, async (err: Error | null, reslt: Buffer) => - { - if (err) - { - reject(error); - return; - } - const binData = new LLSD.Binary(Array.from(reslt), 'BASE64'); - const llsdResult = LLSD.LLSD.parseBinary(binData); - let obj = []; - if (llsdResult.result) - { - obj = llsdResult.result; - } - if (obj.length > 0) - { - for (const mat of obj) - { - if (mat['ID']) - { - const nbuf = Buffer.from(mat['ID'].toArray()); - const nuuid = new UUID(nbuf, 0).toString(); - if (uuids[nuuid] !== undefined) - { - if (mat['Material']) - { - const material = new Material(); - material.alphaMaskCutoff = mat['Material']['AlphaMaskCutoff']; - material.diffuseAlphaMode = mat['Material']['DiffuseAlphaMode']; - material.envIntensity = mat['Material']['EnvIntensity']; - material.normMap = new UUID(mat['Material']['NormMap'].toString()); - material.normOffsetX = mat['Material']['NormOffsetX']; - material.normOffsetY = mat['Material']['NormOffsetY']; - material.normRepeatX = mat['Material']['NormRepeatX']; - material.normRepeatY = mat['Material']['NormRepeatY']; - material.normRotation = mat['Material']['NormRotation']; - material.specColor = new Color4(mat['Material']['SpecColor'][0], mat['Material']['SpecColor'][1], mat['Material']['SpecColor'][2], mat['Material']['SpecColor'][3]); - material.specExp = mat['Material']['SpecExp']; - material.specMap = new UUID(mat['Material']['SpecMap'].toString()); - material.specOffsetX = mat['Material']['SpecOffsetX']; - material.specOffsetY = mat['Material']['SpecOffsetY']; - material.specRepeatX = mat['Material']['SpecRepeatX']; - material.specRepeatY = mat['Material']['SpecRepeatY']; - material.specRotation = mat['Material']['SpecRotation']; - material.llsd = LLSD.LLSD.formatXML(mat['Material']); - uuids[nuuid] = material; - } - } - } - } - resolve(); - } - else - { - reject(new Error('Material data not found')); - } - }); - }); + const binary = LLSD.LLSD.formatBinary(uuidArray); + const res: Buffer = await Utils.deflate(Buffer.from(binary.toArray())); + const result = await this.currentRegion.caps.capsPostXML('RenderMaterials', { + 'Zipped': LLSD.LLSD.asBinary(res.toString('base64')) }); + + const resultZipped = Buffer.from(result['Zipped'].octets); + const reslt: Buffer = await Utils.inflate(resultZipped); + const binData = new LLSD.Binary(Array.from(reslt), 'BASE64'); + const llsdResult = LLSD.LLSD.parseBinary(binData); + let obj = []; + if (llsdResult.result) + { + obj = llsdResult.result; + } + if (obj.length > 0) + { + for (const mat of obj) + { + if (mat['ID']) + { + const nbuf = Buffer.from(mat['ID'].toArray()); + const nuuid = new UUID(nbuf, 0).toString(); + if (uuids[nuuid] !== undefined) + { + if (mat['Material']) + { + uuids[nuuid] = Material.fromLLSDObject(mat['Material']); + } + } + } + } + } + else + { + throw new Error('Material data not found'); + } } async getMaterials(uuids: {[key: string]: Material | null}): Promise @@ -336,8 +387,6 @@ export class AssetCommands extends CommandsBase } totalCount++; } - console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials'); - } catch (error) { @@ -366,367 +415,10 @@ export class AssetCommands extends CommandsBase } totalCount++; } - console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials (end)'); } catch (error) { console.error(error); } } - - async uploadMesh(name: string, description: string, mesh: Buffer, confirmCostCallback: (cost: number) => boolean): Promise - { - const decodedMesh = await LLMesh.from(mesh); - if (!decodedMesh.creatorID.equals(this.agent.agentID) && !decodedMesh.creatorID.equals(UUID.zero())) - { - throw new Error('Unable to upload - copyright violation'); - } - const faces = []; - const faceCount = decodedMesh.lodLevels['high_lod'].length; - for (let x = 0; x < faceCount; x++) - { - faces.push({ - 'diffuse_color': [1.000000000000001, 1.000000000000001, 1.000000000000001, 1.000000000000001], - 'fullbright': false - }); - } - const prim = { - 'face_list': faces, - 'position': [0.000000000000001, 0.000000000000001, 0.000000000000001], - 'rotation': [0.000000000000001, 0.000000000000001, 0.000000000000001, 1.000000000000001], - 'scale': [2.000000000000001, 2.000000000000001, 2.000000000000001], - 'material': 3, - 'physics_shape_type': 2, - 'mesh': 0 - }; - const assetResources = { - 'instance_list': [prim], - 'mesh_list': [new LLSD.Binary(Array.from(mesh))], - 'texture_list': [], - 'metric': 'MUT_Unspecified' - }; - const uploadMap = { - 'name': String(name), - 'description': String(description), - 'asset_resources': assetResources, - 'asset_type': 'mesh', - 'inventory_type': 'object', - 'folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Object)), - 'texture_folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Texture)), - 'everyone_mask': PermissionMask.All, - 'group_mask': PermissionMask.All, - 'next_owner_mask': PermissionMask.All - }; - let result; - try - { - result = await this.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap); - } - catch (error) - { - console.error(error); - } - if (result['state'] === 'upload' && result['upload_price'] !== undefined) - { - const cost = result['upload_price']; - if (await confirmCostCallback(cost)) - { - const uploader = result['uploader']; - const uploadResult = await this.currentRegion.caps.capsPerformXMLPost(uploader, assetResources); - if (uploadResult['new_inventory_item'] && uploadResult['new_asset']) - { - const inventoryItem = new UUID(uploadResult['new_inventory_item'].toString()); - const item = await this.agent.inventory.fetchInventoryItem(inventoryItem); - if (item !== null) - { - item.assetID = new UUID(uploadResult['new_asset'].toString()); - } - return inventoryItem; - } - else - { - - throw new Error('Upload failed - no new inventory item returned'); - } - } - throw new Error('Upload cost declined') - } - else - { - console.log(result); - console.log(JSON.stringify(result.error)); - throw new Error('Upload failed'); - } - } - - uploadInventoryItem(type: HTTPAssets, data: Buffer, name: string, description: string): Promise - { - return new Promise((resolve, reject) => - { - if (type === HTTPAssets.ASSET_SCRIPT) - { - type = HTTPAssets.ASSET_LSL_TEXT; - } - const transactionID = UUID.random(); - const callbackID = ++this.callbackID; - const msg = new CreateInventoryItemMessage(); - msg.AgentData = { - AgentID: this.agent.agentID, - SessionID: this.circuit.sessionID - }; - msg.InventoryBlock = { - CallbackID: callbackID, - FolderID: this.agent.inventory.main.root || UUID.zero(), - TransactionID: transactionID, - NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), - Type: Utils.HTTPAssetTypeToAssetType(type), - InvType: Utils.HTTPAssetTypeToInventoryType(type), - WearableType: WearableType.Shape, - Name: Utils.StringToBuffer(name), - Description: Utils.StringToBuffer(description) - }; - this.currentRegion.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => - { - if (message.InventoryData[0].CallbackID === callbackID) - { - return FilterResponse.Finish; - } - else - { - return FilterResponse.NoMatch; - } - }).then((createInventoryMsg: UpdateCreateInventoryItemMessage) => - { - switch (type) - { - case HTTPAssets.ASSET_NOTECARD: - { - this.currentRegion.caps.capsPostXML('UpdateNotecardAgentInventory', { - 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), - }).then((result: any) => - { - if (result['uploader']) - { - const uploader = result['uploader']; - this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => - { - if (uploadResult['state'] && uploadResult['state'] === 'complete') - { - const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; - resolve(itemID); - } - else - { - reject(new Error('Asset upload failed')) - } - }).catch((err) => - { - reject(err); - }); - } - else - { - reject(new Error('Invalid response when attempting to request upload URL for notecard')); - } - }).catch((err) => - { - reject(err); - }); - break; - } - case HTTPAssets.ASSET_GESTURE: - { - this.currentRegion.caps.capsPostXML('UpdateGestureAgentInventory', { - 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), - }).then((result: any) => - { - if (result['uploader']) - { - const uploader = result['uploader']; - this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => - { - if (uploadResult['state'] && uploadResult['state'] === 'complete') - { - const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; - resolve(itemID); - } - else - { - reject(new Error('Asset upload failed')) - } - }).catch((err) => - { - reject(err); - }); - } - else - { - reject(new Error('Invalid response when attempting to request upload URL for notecard')); - } - }).catch((err) => - { - reject(err); - }); - break; - } - case HTTPAssets.ASSET_LSL_TEXT: - { - this.currentRegion.caps.capsPostXML('UpdateScriptAgent', { - 'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()), - 'target': 'mono' - }).then((result: any) => - { - if (result['uploader']) - { - const uploader = result['uploader']; - this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) => - { - if (uploadResult['state'] && uploadResult['state'] === 'complete') - { - const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID; - resolve(itemID); - } - else - { - reject(new Error('Asset upload failed')) - } - }).catch((err) => - { - reject(err); - }); - } - else - { - reject(new Error('Invalid response when attempting to request upload URL for notecard')); - } - }).catch((err) => - { - reject(err); - }); - break; - } - default: - { - reject(new Error('Currently unsupported CreateInventoryType: ' + type)); - } - } - }).catch(() => - { - reject(new Error('Timed out waiting for UpdateCreateInventoryItem')); - }); - this.circuit.sendMessage(msg, PacketFlags.Reliable); - }); - } - - uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise - { - return new Promise((resolve, reject) => - { - switch (type) - { - case HTTPAssets.ASSET_LANDMARK: - case HTTPAssets.ASSET_NOTECARD: - case HTTPAssets.ASSET_GESTURE: - case HTTPAssets.ASSET_SCRIPT: - // These types of assets use an different process - const inventoryItem = this.uploadInventoryItem(type, data, name, description).then((invItemID: UUID) => - { - this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) => - { - if (item === null) - { - reject(new Error('Unable to get inventory item')); - } - else - { - resolve(item); - } - }).catch((err) => - { - reject(err); - }); - }).catch((err) => - { - reject(err); - }); - return ; - } - if (this.agent && this.agent.inventory && this.agent.inventory.main && this.agent.inventory.main.root) - { - this.currentRegion.caps.capsPostXML('NewFileAgentInventory', { - 'folder_id': new LLSD.UUID(this.agent.inventory.main.root.toString()), - 'asset_type': type, - 'inventory_type': Utils.HTTPAssetTypeToCapInventoryType(type), - 'name': name, - 'description': description, - 'everyone_mask': PermissionMask.All, - 'group_mask': PermissionMask.All, - 'next_owner_mask': PermissionMask.All, - 'expected_upload_cost': 0 - }).then((response: any) => - { - if (response['state'] === 'upload') - { - const uploadURL = response['uploader']; - this.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) => - { - if (responseUpload['new_inventory_item'] !== undefined) - { - const invItemID = new UUID(responseUpload['new_inventory_item'].toString()); - this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) => - { - if (item === null) - { - reject(new Error('Unable to get inventory item')); - } - else - { - resolve(item); - } - }).catch((err) => - { - reject(err); - }); - } - }).catch((err) => - { - reject(err); - }); - } - else if (response['error']) - { - reject(response['error']['message']); - } - else - { - reject('Unable to upload asset'); - } - }).catch((err) => - { - console.log('Got err'); - console.log(err); - reject(err); - }) - } - else - { - if (!this.agent) - { - throw(new Error('Missing agent')); - } - else if (!this.agent.inventory) - { - throw(new Error('Missing agent inventory')); - } - else if (!this.agent.inventory.main) - { - throw new Error('Missing agent inventory main skeleton'); - } - else if (!this.agent.inventory.main.root) - { - throw new Error('Missing agent inventory main skeleton root'); - } - } - }); - } } diff --git a/lib/classes/commands/GridCommands.ts b/lib/classes/commands/GridCommands.ts index 011771a..df10ed0 100644 --- a/lib/classes/commands/GridCommands.ts +++ b/lib/classes/commands/GridCommands.ts @@ -22,7 +22,7 @@ import { MapInfoReplyEvent } from '../../events/MapInfoReplyEvent'; import { PacketFlags } from '../../enums/PacketFlags'; import { Vector2 } from '../Vector2'; import { MapInfoRangeReplyEvent } from '../../events/MapInfoRangeReplyEvent'; -import { Avatar } from '../public/Avatar'; +import { AvatarQueryResult } from '../public/AvatarQueryResult'; export class GridCommands extends CommandsBase { @@ -389,9 +389,9 @@ export class GridCommands extends CommandsBase }); } - avatarKey2Name(uuid: UUID | UUID[]): Promise + avatarKey2Name(uuid: UUID | UUID[]): Promise { - return new Promise(async (resolve, reject) => + return new Promise(async (resolve, reject) => { const req = new UUIDNameRequestMessage(); req.UUIDNameBlock = []; @@ -446,16 +446,16 @@ export class GridCommands extends CommandsBase if (!arr) { const result = waitingFor[uuid[0].toString()]; - const av = new Avatar(uuid[0], result.firstName, result.lastName); + const av = new AvatarQueryResult(uuid[0], result.firstName, result.lastName); resolve(av); } else { - const response: Avatar[] = []; + const response: AvatarQueryResult[] = []; for (const k of uuid) { const result = waitingFor[k.toString()]; - const av = new Avatar(k, result.firstName, result.lastName); + const av = new AvatarQueryResult(k, result.firstName, result.lastName); response.push(av); } resolve(response); diff --git a/lib/classes/commands/InventoryCommands.ts b/lib/classes/commands/InventoryCommands.ts index cafd7e6..3cd401b 100644 --- a/lib/classes/commands/InventoryCommands.ts +++ b/lib/classes/commands/InventoryCommands.ts @@ -9,6 +9,7 @@ import { UUID } from '../UUID'; import { Vector3 } from '../Vector3'; import { PacketFlags } from '../../enums/PacketFlags'; import { ChatSourceType } from '../../enums/ChatSourceType'; +import { InventoryItem } from '../InventoryItem'; export class InventoryCommands extends CommandsBase { @@ -54,6 +55,23 @@ export class InventoryCommands extends CommandsBase return await this.circuit.waitForAck(sequenceNo, 10000); } + async getInventoryItem(item: UUID | string): Promise + { + if (typeof item === 'string') + { + item = new UUID(item); + } + const result = await this.currentRegion.agent.inventory.fetchInventoryItem(item); + if (result === null) + { + throw new Error('Unable to get inventory item'); + } + else + { + return result; + } + } + async acceptInventoryOffer(event: InventoryOfferedEvent): Promise { if (event.source === ChatSourceType.Object) diff --git a/lib/classes/commands/RegionCommands.ts b/lib/classes/commands/RegionCommands.ts index 0443c5c..bd8c42f 100644 --- a/lib/classes/commands/RegionCommands.ts +++ b/lib/classes/commands/RegionCommands.ts @@ -11,21 +11,12 @@ import { ObjectSelectMessage } from '../messages/ObjectSelect'; import { ObjectPropertiesMessage } from '../messages/ObjectProperties'; import { Utils } from '../Utils'; import { ObjectDeselectMessage } from '../messages/ObjectDeselect'; -import { RequestTaskInventoryMessage } from '../messages/RequestTaskInventory'; -import { ReplyTaskInventoryMessage } from '../messages/ReplyTaskInventory'; -import { InventoryItem } from '../InventoryItem'; -import { AssetTypeLL } from '../../enums/AssetTypeLL'; -import { SaleTypeLL } from '../../enums/SaleTypeLL'; -import { InventoryTypeLL } from '../../enums/InventoryTypeLL'; import { ObjectAddMessage } from '../messages/ObjectAdd'; import { Quaternion } from '../Quaternion'; -import { RezObjectMessage } from '../messages/RezObject'; -import { PermissionMask } from '../../enums/PermissionMask'; import { PacketFlags } from '../../enums/PacketFlags'; import { GameObject } from '../public/GameObject'; import { PCode } from '../../enums/PCode'; import { PrimFlags } from '../../enums/PrimFlags'; -import { AssetType } from '../../enums/AssetType'; import { NewObjectEvent } from '../../events/NewObjectEvent'; import { Vector3 } from '../Vector3'; import { Parcel } from '../public/Parcel'; @@ -33,19 +24,18 @@ import { Parcel } from '../public/Parcel'; import * as Long from 'long'; import * as micromatch from 'micromatch'; import * as LLSD from '@caspertech/llsd'; -import { Subject, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { SculptType } from '../..'; -import { ObjectResolvedEvent } from '../../events/ObjectResolvedEvent'; import { AssetMap } from '../AssetMap'; import { InventoryType } from '../../enums/InventoryType'; import { BuildMap } from '../BuildMap'; +import { ObjectResolver } from '../ObjectResolver'; +import { Logger } from '../Logger'; import Timer = NodeJS.Timer; import Timeout = NodeJS.Timeout; export class RegionCommands extends CommandsBase { - private resolveQueue: {[key: number]: GameObject} = {}; - async getRegionHandle(regionID: UUID): Promise { const circuit = this.currentRegion.circuit; @@ -330,610 +320,19 @@ export class RegionCommands extends CommandsBase } } - private parseLine(line: string): { - 'key': string | null, - 'value': string - } - { - line = line.trim().replace(/[\t]/gu, ' ').trim(); - while (line.indexOf('\u0020\u0020') > 0) - { - line = line.replace(/\u0020\u0020/gu, '\u0020'); - } - let key: string | null = null; - let value = ''; - if (line.length > 2) - { - const sep = line.indexOf(' '); - if (sep > 0) - { - key = line.substr(0, sep); - value = line.substr(sep + 1); - } - } - else if (line.length === 1) - { - key = line; - } - else if (line.length > 0) - { - return { - 'key': line, - 'value': '' - } - } - if (key !== null) - { - key = key.trim(); - } - return { - 'key': key, - 'value': value - } - } - getName(): string { return this.currentRegion.regionName; } - private waitForObjectResolve(localID: number) + async resolveObject(object: GameObject, forceResolve = false, skipInventory = false) { - return new Promise((resolve, reject) => - { - let timeout: Timeout | undefined = undefined; - let subs: Subscription | undefined = undefined; - try - { - const ourObject = this.currentRegion.objects.getObjectByLocalID(localID); - if (ourObject.resolvedAt) - { - resolve(); - return; - } - } - catch (ignore) - { - - } - subs = this.currentRegion.clientEvents.onObjectResolvedEvent.subscribe((evt: ObjectResolvedEvent) => - { - if (evt.object.ID === localID) - { - if (timeout !== undefined) - { - clearTimeout(timeout); - timeout = undefined; - } - if (subs !== undefined) - { - subs.unsubscribe(); - subs = undefined; - } - resolve(); - } - }); - timeout = setTimeout(() => - { - if (timeout !== undefined) - { - clearTimeout(timeout); - timeout = undefined; - } - if (subs !== undefined) - { - subs.unsubscribe(); - subs = undefined; - } - const object = this.currentRegion.objects.getObjectByLocalID(localID); - if (object.resolvedAt) - { - try - { - const ourObject = this.currentRegion.objects.getObjectByLocalID(localID); - if (ourObject.resolvedAt) - { - console.warn('Resolve timed out but object ' + localID + ' HAS been resolved!'); - resolve(); - return; - } - } - catch (ignore) - { - - } - } - reject(new Error('Timeout')); - }, 10000); - }); + return this.currentRegion.resolver.resolveObjects([object], forceResolve, skipInventory); } - private async queueResolveObject(object: GameObject, skipInventory = false) + async resolveObjects(objects: GameObject[], forceResolve = false, skipInventory = false, log = false) { - if (object.resolvedAt) - { - return; - } - if (this.resolveQueue[object.ID] === undefined) - { - this.resolveQueue[object.ID] = object; - try - { - await this.resolveObjects([object], true, true); - } - catch (error) - { - console.error('Failed to resolve ' + object.ID); - } - delete this.resolveQueue[object.ID]; - } - else - { - return this.waitForObjectResolve(object.ID); - } - } - - private async resolveObjects(objects: GameObject[], onlyUnresolved: boolean = false, skipInventory = false) - { - // First, create a map of all object IDs - const objs: {[key: number]: GameObject} = {}; - const scanObject = function(obj: GameObject) - { - const localID = obj.ID; - if (!objs[localID]) - { - objs[localID] = obj; - if (obj.children) - { - for (const child of obj.children) - { - scanObject(child); - } - } - } - }; - for (const obj of objects) - { - scanObject(obj); - } - - const resolveTime = new Date().getTime() / 1000; - let objectList = []; - let totalRemaining = 0; - try - { - for (const k of Object.keys(objs)) - { - const ky = parseInt(k, 10); - if (objs[ky] !== undefined) - { - const o = objs[ky]; - if (o.resolvedAt === undefined) - { - o.resolvedAt = 0; - } - if (o.resolvedAt !== undefined && o.resolvedAt < resolveTime && o.PCode !== PCode.Avatar && o.resolveAttempts < 3 && (o.Flags === undefined || !(o.Flags & PrimFlags.TemporaryOnRez))) - { - totalRemaining++; - if (!onlyUnresolved || objs[ky].name === undefined) - { - objs[ky].name = undefined; - objectList.push(objs[ky]); - } - if (objectList.length > 254) - { - try - { - await this.selectObjects(objectList); - await this.deselectObjects(objectList); - for (const chk of objectList) - { - if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime) - { - totalRemaining--; - } - } - } - catch (ignore) - { - - } - finally - { - objectList = []; - } - } - } - } - } - if (objectList.length > 0) - { - await this.selectObjects(objectList); - await this.deselectObjects(objectList); - for (const chk of objectList) - { - if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime) - { - totalRemaining --; - } - } - } - const objectSet = Object.keys(objs); - let count = 0; - for (const k of objectSet) - { - count++; - const ky = parseInt(k, 10); - if (objs[ky] !== undefined && !skipInventory) - { - const o = objs[ky]; - if ((o.resolveAttempts === undefined || o.resolveAttempts < 3) && o.FullID !== undefined && o.name !== undefined && o.Flags !== undefined && !(o.Flags & PrimFlags.InventoryEmpty) && (!o.inventory || o.inventory.length === 0)) - { - const req = new RequestTaskInventoryMessage(); - req.AgentData = { - AgentID: this.agent.agentID, - SessionID: this.circuit.sessionID - }; - req.InventoryData = { - LocalID: o.ID - }; - this.circuit.sendMessage(req, PacketFlags.Reliable); - try - { - const inventory = await this.circuit.waitForMessage(Message.ReplyTaskInventory, 10000, (message: ReplyTaskInventoryMessage): FilterResponse => - { - if (message.InventoryData.TaskID.equals(o.FullID)) - { - return FilterResponse.Finish; - } - else - { - return FilterResponse.Match; - } - }); - const fileName = Utils.BufferToStringSimple(inventory.InventoryData.Filename); - - const file = await this.circuit.XferFile(fileName, true, false, UUID.zero(), AssetType.Unknown, true); - if (file.length === 0) - { - o.Flags = o.Flags | PrimFlags.InventoryEmpty; - } - else - { - let str = file.toString('utf-8'); - let nl = str.indexOf('\0'); - while (nl !== -1) - { - str = str.substr(nl + 1); - nl = str.indexOf('\0') - } - const lines: string[] = str.replace(/\r\n/g, '\n').split('\n'); - let lineNum = 0; - while (lineNum < lines.length) - { - let line = lines[lineNum++]; - let result = this.parseLine(line); - if (result.key !== null) - { - switch (result.key) - { - case 'inv_object': - let itemID = UUID.zero(); - let parentID = UUID.zero(); - let name = ''; - let assetType: AssetType = AssetType.Unknown; - - while (lineNum < lines.length) - { - result = this.parseLine(lines[lineNum++]); - if (result.key !== null) - { - if (result.key === '{') - { - // do nothing - } - else if (result.key === '}') - { - break; - } - else if (result.key === 'obj_id') - { - itemID = new UUID(result.value); - } - else if (result.key === 'parent_id') - { - parentID = new UUID(result.value); - } - else if (result.key === 'type') - { - const typeString = result.value as any; - assetType = parseInt(AssetTypeLL[typeString], 10); - } - else if (result.key === 'name') - { - name = result.value.substr(0, result.value.indexOf('|')); - } - } - } - - if (name !== 'Contents') - { - console.log('TODO: Do something useful with inv_objects') - } - - break; - case 'inv_item': - const item: InventoryItem = new InventoryItem(); - while (lineNum < lines.length) - { - line = lines[lineNum++]; - result = this.parseLine(line); - if (result.key !== null) - { - if (result.key === '{') - { - // do nothing - } - else if (result.key === '}') - { - break; - } - else if (result.key === 'item_id') - { - item.itemID = new UUID(result.value); - } - else if (result.key === 'parent_id') - { - item.parentID = new UUID(result.value); - } - else if (result.key === 'permissions') - { - while (lineNum < lines.length) - { - result = this.parseLine(lines[lineNum++]); - if (result.key !== null) - { - if (result.key === '{') - { - // do nothing - } - else if (result.key === '}') - { - break; - } - else if (result.key === 'creator_mask') - { - item.permissions.baseMask = parseInt(result.value, 16); - } - else if (result.key === 'base_mask') - { - item.permissions.baseMask = parseInt(result.value, 16); - } - else if (result.key === 'owner_mask') - { - item.permissions.ownerMask = parseInt(result.value, 16); - } - else if (result.key === 'group_mask') - { - item.permissions.groupMask = parseInt(result.value, 16); - } - else if (result.key === 'everyone_mask') - { - item.permissions.everyoneMask = parseInt(result.value, 16); - } - else if (result.key === 'next_owner_mask') - { - item.permissions.nextOwnerMask = parseInt(result.value, 16); - } - else if (result.key === 'creator_id') - { - item.permissions.creator = new UUID(result.value); - } - else if (result.key === 'owner_id') - { - item.permissions.owner = new UUID(result.value); - } - else if (result.key === 'last_owner_id') - { - item.permissions.lastOwner = new UUID(result.value); - } - else if (result.key === 'group_id') - { - item.permissions.group = new UUID(result.value); - } - else if (result.key === 'group_owned') - { - const val = parseInt(result.value, 10); - item.permissions.groupOwned = (val !== 0); - } - else - { - console.log('Unrecognised key (4): ' + result.key); - } - } - } - } - else if (result.key === 'sale_info') - { - while (lineNum < lines.length) - { - result = this.parseLine(lines[lineNum++]); - if (result.key !== null) - { - if (result.key === '{') - { - // do nothing - } - else if (result.key === '}') - { - break; - } - else if (result.key === 'sale_type') - { - const typeString = result.value as any; - item.saleType = parseInt(SaleTypeLL[typeString], 10); - } - else if (result.key === 'sale_price') - { - item.salePrice = parseInt(result.value, 10); - } - else - { - console.log('Unrecognised key (3): ' + result.key); - } - } - } - } - else if (result.key === 'shadow_id') - { - item.assetID = new UUID(result.value).bitwiseOr(new UUID('3c115e51-04f4-523c-9fa6-98aff1034730')); - } - else if (result.key === 'asset_id') - { - item.assetID = new UUID(result.value); - } - else if (result.key === 'type') - { - const typeString = result.value as any; - item.type = parseInt(AssetTypeLL[typeString], 10); - } - else if (result.key === 'inv_type') - { - const typeString = result.value as any; - item.inventoryType = parseInt(InventoryTypeLL[typeString], 10); - } - else if (result.key === 'flags') - { - item.flags = parseInt(result.value, 10); - } - else if (result.key === 'name') - { - item.name = result.value.substr(0, result.value.indexOf('|')); - } - else if (result.key === 'desc') - { - item.description = result.value.substr(0, result.value.indexOf('|')); - } - else if (result.key === 'creation_date') - { - item.created = new Date(parseInt(result.value, 10) * 1000); - } - else - { - console.log('Unrecognised key (2): ' + result.key); - } - } - } - o.inventory.push(item); - break; - default: - { - console.log('Unrecognised task inventory token: [' + result.key + ']'); - } - } - } - } - } - } - catch (error) - { - if (o.resolveAttempts === undefined) - { - o.resolveAttempts = 0; - } - o.resolveAttempts++; - if (o.FullID !== undefined) - { - console.error('Error downloading task inventory of ' + o.FullID.toString() + ':'); - console.error(error); - } - else - { - console.error('Error downloading task inventory of ' + o.ID + ':'); - console.error(error); - } - } - } - } - } - } - catch (ignore) - { - console.error(ignore); - } - finally - { - if (totalRemaining < 1) - { - totalRemaining = 0; - for (const obj of objectList) - { - if (obj.resolvedAt === undefined || obj.resolvedAt < resolveTime) - { - totalRemaining++; - } - } - if (totalRemaining > 0) - { - console.error(totalRemaining + ' objects could not be resolved'); - } - } - const that = this; - const getCosts = async function(objIDs: UUID[]) - { - const result = await that.currentRegion.caps.capsPostXML('GetObjectCost', { - 'object_ids': objIDs - }); - const uuids = Object.keys(result); - for (const key of uuids) - { - const costs = result[key]; - try - { - const obj: GameObject = that.currentRegion.objects.getObjectByUUID(new UUID(key)); - obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']); - obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']); - obj.physicaImpact = parseFloat(costs['physics_cost']); - obj.resourceImpact = parseFloat(costs['resource_cost']); - obj.limitingType = costs['resource_limiting_type']; - - - obj.landImpact = Math.round(obj.linkPhysicsImpact); - if (obj.linkResourceImpact > obj.linkPhysicsImpact) - { - obj.landImpact = Math.round(obj.linkResourceImpact); - } - obj.calculatedLandImpact = obj.landImpact; - if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy') - { - obj.calculatedLandImpact = 0; - } - } - catch (error) - {} - } - }; - - let ids: UUID[] = []; - const promises: Promise[] = []; - for (const obj of objects) - { - if (!onlyUnresolved || obj.landImpact === undefined) - { - ids.push(new LLSD.UUID(obj.FullID)); - } - if (ids.length > 255) - { - promises.push(getCosts(ids)); - ids = []; - } - } - if (ids.length > 0) - { - promises.push(getCosts(ids)); - } - await Promise.all(promises); - } + return this.currentRegion.resolver.resolveObjects(objects, forceResolve, skipInventory, log); } private waitForObjectByLocalID(localID: number, timeout: number): Promise @@ -986,9 +385,10 @@ export class RegionCommands extends CommandsBase }); } - private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, buildMap: BuildMap) + private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, buildMap: BuildMap, markRoot = false) { // Calculate geometry + Logger.Info('Calculating geometry'); const objectPosition = new Vector3(obj.Position); const objectRotation = new Quaternion(obj.Rotation); const objectScale = new Vector3(obj.Scale); @@ -1011,6 +411,7 @@ export class RegionCommands extends CommandsBase // Is this a mesh part? let object: GameObject | null = null; + let rezzedMesh = false; if (obj.extraParams !== undefined && obj.extraParams.meshData !== null) { if (buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] !== undefined) @@ -1019,7 +420,24 @@ export class RegionCommands extends CommandsBase const rezLocation = new Vector3(buildMap.rezLocation); rezLocation.z += (objectScale.z / 2); - object = await this.rezFromInventory(obj, rezLocation, new UUID(meshEntry.assetID)); + if (meshEntry.item !== null) + { + Logger.Info('Rezzing mesh part'); + try + { + object = await meshEntry.item.rezInWorld(rezLocation); + } + catch (err) + { + console.error('Failed to rez object ' + obj.name + ' in-world'); + console.error(err); + } + rezzedMesh = true; + } + else + { + console.error('Unable to rez mesh item from inventory - item is null'); + } } } else if (buildMap.primReservoir.length > 0) @@ -1028,100 +446,255 @@ export class RegionCommands extends CommandsBase if (newPrim !== undefined) { object = newPrim; - await object.setShape( - obj.PathCurve, - obj.ProfileCurve, - obj.PathBegin, - obj.PathEnd, - obj.PathScaleX, - obj.PathScaleY, - obj.PathShearX, - obj.PathShearY, - obj.PathTwist, - obj.PathTwistBegin, - obj.PathRadiusOffset, - obj.PathTaperX, - obj.PathTaperY, - obj.PathRevolutions, - obj.PathSkew, - obj.ProfileBegin, - obj.ProfileEnd, - obj.ProfileHollow - ); + Logger.Info('Setting shape'); + try + { + await object.setShape( + obj.PathCurve, + obj.ProfileCurve, + obj.PathBegin, + obj.PathEnd, + obj.PathScaleX, + obj.PathScaleY, + obj.PathShearX, + obj.PathShearY, + obj.PathTwist, + obj.PathTwistBegin, + obj.PathRadiusOffset, + obj.PathTaperX, + obj.PathTaperY, + obj.PathRevolutions, + obj.PathSkew, + obj.ProfileBegin, + obj.ProfileEnd, + obj.ProfileHollow + ); + } + catch (err) + { + console.error('Error setting shape on ' + obj.name); + console.error(err); + } } } + else + { + console.error('Exhausted prim reservoir!!'); + } if (object === null) { + console.error('Failed to acquire prim for build'); throw new Error('Failed to acquire prim for build'); } - await object.setGeometry(finalPos, finalRot, objectScale); + if (markRoot) + { + object.isMarkedRoot = true; + } + Logger.Info('Setting geometry'); + try + { + await object.setGeometry(finalPos, finalRot, objectScale); + } + catch (err) + { + console.error('Error setting geometry on ' + obj.name); + console.error(err); + } + + Logger.Info('Setting ExtraParams'); if (obj.extraParams.sculptData !== null) { if (obj.extraParams.sculptData.type !== SculptType.Mesh) { const oldTextureID = obj.extraParams.sculptData.texture.toString(); - if (buildMap.assetMap.textures[oldTextureID] !== undefined) + const item = buildMap.assetMap.textures[oldTextureID]; + if (item !== null && item !== undefined && item.item !== null) { - obj.extraParams.sculptData.texture = new UUID(buildMap.assetMap.textures[oldTextureID]); + obj.extraParams.sculptData.texture = item.item.assetID; } } } - await object.setExtraParams(obj.extraParams); + + if (rezzedMesh) + { + obj.extraParams.meshData = object.extraParams.meshData; + obj.extraParams.sculptData = object.extraParams.sculptData; + } + + try + { + await object.setExtraParams(obj.extraParams); + } + catch (err) + { + console.error('Error setting ExtraParams on ' + obj.name); + console.error(err); + throw new Error(err); + } if (obj.TextureEntry !== undefined) { + // Handle materials + const materialUpload: { + 'FullMaterialsPerFace': any[] + } = { + 'FullMaterialsPerFace': [] + }; + + let gotSomeActualMaterials = false; + for (let face = 0; face < obj.TextureEntry.faces.length; face++) + { + const materialID = obj.TextureEntry.faces[face].materialID; + if (!materialID.isZero()) + { + const storedMat = buildMap.assetMap.materials[materialID.toString()]; + if (storedMat !== null && storedMat !== undefined) + { + materialUpload.FullMaterialsPerFace.push({ + Face: face, + ID: object.ID, + Material: storedMat.toLLSDObject() + }); + gotSomeActualMaterials = true; + } + } + } + + if (gotSomeActualMaterials) + { + Logger.Info('Setting materials'); + const zipped = await Utils.deflate(Buffer.from(LLSD.LLSD.formatBinary(materialUpload).octets)); + const newMat = { + 'Zipped': new LLSD.Binary(Array.from(zipped), 'BASE64') + }; + this.currentRegion.caps.capsPutXML('RenderMaterials', newMat).then(() => {}).catch((err) => + { + console.error(err); + }); + try + { + await object.waitForTextureUpdate(1000); + } + catch (error) + { + console.error('Timed out while waiting for RenderMaterials update'); + } + if (object.TextureEntry !== undefined) + { + for (let face = 0; face < object.TextureEntry.faces.length; face++) + { + const oldFace = obj.TextureEntry.faces[face]; + if (!oldFace.materialID.isZero()) + { + const newFace = object.TextureEntry.faces[face]; + if (newFace.materialID.isZero()) + { + const h = 5; + } + obj.TextureEntry.faces[face].materialID = object.TextureEntry.faces[face].materialID; + if (obj.TextureEntry.defaultTexture !== null) + { + if (oldFace.materialID.equals(obj.TextureEntry.defaultTexture.materialID)) + { + obj.TextureEntry.defaultTexture.materialID = obj.TextureEntry.faces[face].materialID; + } + } + } + } + } + } + + // We're zero-ing out the materialID here because we'll apply materials immediately after if (obj.TextureEntry.defaultTexture !== null) { const oldTextureID = obj.TextureEntry.defaultTexture.textureID.toString(); - if (buildMap.assetMap.textures[oldTextureID] !== undefined) + + const item = buildMap.assetMap.textures[oldTextureID]; + if (item !== null && item !== undefined && item.item !== null) { - obj.TextureEntry.defaultTexture.textureID = new UUID(buildMap.assetMap.textures[oldTextureID]); + obj.TextureEntry.defaultTexture.textureID = item.item.assetID; } } for (const j of obj.TextureEntry.faces) { const oldTextureID = j.textureID.toString(); - if (buildMap.assetMap.textures[oldTextureID] !== undefined) + + const item = buildMap.assetMap.textures[oldTextureID]; + if (item !== null && item !== undefined && item.item !== null) { - j.textureID = new UUID(buildMap.assetMap.textures[oldTextureID]); + j.textureID = item.item.assetID; } } try { - await object.setTextureEntry(obj.TextureEntry); + Logger.Info('Setting texture entry'); + await object.setTextureEntry(obj.TextureEntry).then(() => {}).catch((err) => { + console.error(err); + }); } catch (error) { + console.error('Error setting TextureEntry on ' + obj.name); console.error(error); } + + } if (obj.name !== undefined) { - await object.setName(obj.name); + Logger.Info('Setting name'); + try + { + await object.setName(obj.name); + } + catch (error) + { + console.error('Error setting name on ' + obj.name); + console.error(error); + throw new Error(error); + } } if (obj.description !== undefined) { - await object.setDescription(obj.description); + Logger.Info('Setting description'); + try + { + await object.setDescription(obj.description); + } + catch (error) + { + console.error('Error setting name on ' + obj.name); + console.error(error); + throw new Error(error); + } } for (const invItem of obj.inventory) { try { + Logger.Info('Processing inventory item ' + invItem.name); + if (invItem.inventoryType === InventoryType.Object && invItem.assetID.isZero()) + { + invItem.assetID = invItem.itemID; + } switch (invItem.inventoryType) { case InventoryType.Clothing: { if (buildMap.assetMap.clothing[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.clothing[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.clothing[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } @@ -1129,8 +702,11 @@ export class RegionCommands extends CommandsBase { if (buildMap.assetMap.bodyparts[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.bodyparts[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.bodyparts[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } @@ -1138,8 +714,11 @@ export class RegionCommands extends CommandsBase { if (buildMap.assetMap.notecards[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.notecards[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.notecards[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } @@ -1147,8 +726,11 @@ export class RegionCommands extends CommandsBase { if (buildMap.assetMap.sounds[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.sounds[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.sounds[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } @@ -1156,26 +738,24 @@ export class RegionCommands extends CommandsBase { if (buildMap.assetMap.gestures[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.gestures[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); - } - break; - } - case InventoryType.Landmark: - { - if (buildMap.assetMap.landmarks[invItem.assetID.toString()] !== undefined) - { - const invItemID = buildMap.assetMap.landmarks[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.gestures[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } + case InventoryType.Script: case InventoryType.LSL: { if (buildMap.assetMap.scripts[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.scripts[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); + const item = buildMap.assetMap.scripts[invItem.assetID.toString()].item; + if (item !== null) + { + await object.dropInventoryIntoContents(item); + } } break; } @@ -1183,44 +763,52 @@ export class RegionCommands extends CommandsBase { if (buildMap.assetMap.animations[invItem.assetID.toString()] !== undefined) { - const invItemID = buildMap.assetMap.animations[invItem.assetID.toString()]; - await object.dropInventoryIntoContents(new UUID(invItemID)); - } - break; - } - } - } - catch (error) - { - console.error(error); - } - } - - // Do nested objects last - for (const invItem of obj.inventory) - { - try - { - switch (invItem.inventoryType) - { - case InventoryType.Object: - { - if (buildMap.assetMap.objects[invItem.assetID.toString()] !== undefined) - { - const objectXML = buildMap.assetMap.objects[invItem.assetID.toString()]; - if (objectXML !== null) + const item = buildMap.assetMap.animations[invItem.assetID.toString()].item; + if (item !== null) { - const taskObjectXML = await GameObject.fromXML(objectXML.toString('utf-8')); - const taskObject = await this.buildObjectNew(taskObjectXML, buildMap.callback, buildMap.costOnly); - if (taskObject !== null) - { - const invItemUUID = await taskObject.takeToInventory(); - await object.dropInventoryIntoContents(invItemUUID); - } + await object.dropInventoryIntoContents(item); } } break; } + case InventoryType.Object: + { + if (buildMap.assetMap.objects[invItem.itemID.toString()] !== undefined) + { + const inventoryItem = buildMap.assetMap.objects[invItem.itemID.toString()]; + if (inventoryItem !== null) + { + Logger.Info('Dropping inventory into contents..'); + await object.dropInventoryIntoContents(inventoryItem); + Logger.Info('.. Done'); + } + else + { + console.error('Unable to drop object: item is null'); + } + } + break; + } + case InventoryType.Texture: + case InventoryType.Snapshot: + { + if (buildMap.assetMap.textures[invItem.assetID.toString()] !== undefined) + { + const texItem = buildMap.assetMap.textures[invItem.assetID.toString()]; + if (texItem.item !== null) + { + await object.dropInventoryIntoContents(texItem.item); + } + else + { + console.error('Unable to drop object: item is null'); + } + } + break; + } + default: // TODO: 3 - landmark + console.error('Unsupported inventory type: ' + invItem.inventoryType); + break; } } catch (error) @@ -1228,6 +816,7 @@ export class RegionCommands extends CommandsBase console.error(error); } } + Logger.Info('Part build done'); return object; } @@ -1237,11 +826,14 @@ export class RegionCommands extends CommandsBase { if (obj.extraParams.meshData !== null) { - buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] = { - objectName: obj.name || 'Object', - objectDescription: obj.description || '(no description)', - assetID: obj.extraParams.meshData.meshData.toString() - }; + if (buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] === undefined) + { + buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] = { + name: obj.name || 'Object', + description: obj.description || '(no description)', + item: null + }; + } } else { @@ -1251,7 +843,12 @@ export class RegionCommands extends CommandsBase { if (obj.extraParams.sculptData.type !== SculptType.Mesh) { - buildMap.assetMap.textures[obj.extraParams.sculptData.texture.toString()] = obj.extraParams.sculptData.texture.toString(); + if (buildMap.assetMap.textures[obj.extraParams.sculptData.texture.toString()] === undefined) + { + buildMap.assetMap.textures[obj.extraParams.sculptData.texture.toString()] = { + item: null + }; + } } } if (obj.TextureEntry !== undefined) @@ -1259,73 +856,161 @@ export class RegionCommands extends CommandsBase for (const j of obj.TextureEntry.faces) { const textureID = j.textureID; - buildMap.assetMap.textures[textureID.toString()] = textureID.toString(); + if (buildMap.assetMap.textures[textureID.toString()] === undefined) + { + buildMap.assetMap.textures[textureID.toString()] = { + item: null + } + } + const materialID = j.materialID; + if (!materialID.isZero()) + { + if (buildMap.assetMap.materials[materialID.toString()] === undefined) + { + buildMap.assetMap.materials[materialID.toString()] = null + } + } } if (obj.TextureEntry.defaultTexture !== null) { const textureID = obj.TextureEntry.defaultTexture.textureID; - buildMap.assetMap.textures[textureID.toString()] = textureID.toString(); + if (buildMap.assetMap.textures[textureID.toString()] === undefined) + { + buildMap.assetMap.textures[textureID.toString()] = { + item: null + } + } + const materialID = obj.TextureEntry.defaultTexture.materialID; + if (!materialID.isZero()) + { + if (buildMap.assetMap.materials[materialID.toString()] === undefined) + { + buildMap.assetMap.materials[materialID.toString()] = null + } + } } } if (obj.inventory !== undefined) { for (const j of obj.inventory) { + const assetID = j.assetID; switch (j.inventoryType) { case InventoryType.Animation: { - buildMap.assetMap.animations[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.animations[assetID.toString()] === undefined) + { + buildMap.assetMap.animations[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Bodypart: { - buildMap.assetMap.bodyparts[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.bodyparts[assetID.toString()] === undefined) + { + buildMap.assetMap.bodyparts[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.CallingCard: { - buildMap.assetMap.callingcards[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.callingcards[assetID.toString()] === undefined) + { + buildMap.assetMap.callingcards[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Clothing: { - buildMap.assetMap.clothing[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.clothing[assetID.toString()] === undefined) + { + buildMap.assetMap.clothing[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Gesture: { - buildMap.assetMap.gestures[j.assetID.toString()] = j.assetID.toString(); - break; - } - case InventoryType.Landmark: - { - buildMap.assetMap.landmarks[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.clothing[assetID.toString()] === undefined) + { + buildMap.assetMap.gestures[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.LSL: { - buildMap.assetMap.scripts[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.scripts[assetID.toString()] === undefined) + { + buildMap.assetMap.scripts[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Snapshot: { - buildMap.assetMap.textures[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.textures[assetID.toString()] === undefined) + { + buildMap.assetMap.textures[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Notecard: { - buildMap.assetMap.notecards[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.notecards[assetID.toString()] === undefined) + { + buildMap.assetMap.notecards[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Sound: { - buildMap.assetMap.sounds[j.assetID.toString()] = j.assetID.toString(); + if (buildMap.assetMap.sounds[assetID.toString()] === undefined) + { + buildMap.assetMap.sounds[assetID.toString()] = { + name: j.name, + description: j.description, + item: null + }; + } break; } case InventoryType.Object: { - buildMap.assetMap.objects[j.assetID.toString()] = null; + if (buildMap.assetMap.objects[assetID.toString()] === undefined) + { + buildMap.assetMap.objects[assetID.toString()] = null; + } } } } @@ -1340,15 +1025,40 @@ export class RegionCommands extends CommandsBase } } - async buildObjectNew(obj: GameObject, callback: (map: AssetMap) => void, costOnly: boolean = false): Promise + async buildObjectNew(obj: GameObject, map: AssetMap, callback: (map: AssetMap) => void, costOnly: boolean = false, skipMove = false): Promise { - const map: AssetMap = new AssetMap(); const buildMap = new BuildMap(map, callback, costOnly); + Logger.Info('Building object: ' + obj.name); + Logger.Info('Gathering immediate assets'); this.gatherAssets(obj, buildMap); + Logger.Info('Excluding BOM assets'); + const bomTextures = [ + '5a9f4a74-30f2-821c-b88d-70499d3e7183', + 'ae2de45c-d252-50b8-5c6e-19f39ce79317', + '24daea5f-0539-cfcf-047f-fbc40b2786ba', + '52cc6bb6-2ee5-e632-d3ad-50197b1dcb8a', + '43529ce8-7faa-ad92-165a-bc4078371687', + '09aac1fb-6bce-0bee-7d44-caac6dbb6c63', + 'ff62763f-d60a-9855-890b-0c96f8f8cd98', + '8e915e25-31d1-cc95-ae08-d58a47488251', + '9742065b-19b5-297c-858a-29711d539043', + '03642e83-2bd1-4eb9-34b4-4c47ed586d2d', + 'edd51b77-fc10-ce7a-4b3d-011dfc349e4f' + ]; + for (const bomTexture of bomTextures) + { + if (buildMap.assetMap.textures[bomTexture] !== undefined) + { + Logger.Info('Not replacing BOM texture ' + bomTexture); + delete buildMap.assetMap.textures[bomTexture]; + } + } + Logger.Info('Mapping assets'); await callback(map); if (costOnly) { + Logger.Info('Cost only, not proceeding with build'); return null; } @@ -1379,11 +1089,13 @@ export class RegionCommands extends CommandsBase if (buildMap.primsNeeded > 0) { + Logger.Info('Pre-rezzing ' + buildMap.primsNeeded + ' prims'); buildMap.primReservoir = await this.createPrims(buildMap.primsNeeded, agentPos); } + /* const parts = []; - parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), buildMap)); + parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), buildMap, skipMove)); if (obj.children) { @@ -1395,28 +1107,138 @@ export class RegionCommands extends CommandsBase { obj.Rotation = Quaternion.getIdentity(); } + let childNumber = 0; for (const child of obj.children) { 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, buildMap)); + parts.push(this.buildPart(child, objPos, objRot, buildMap, skipMove)); + console.log(' ... Building child ' + String(++childNumber)); } } } const results: GameObject[] = await Promise.all(parts); + */ + let storedPosition: Vector3 | undefined = undefined; + if (skipMove) + { + storedPosition = obj.Position; + obj.Position = new Vector3(buildMap.rezLocation); + } + + const parts = []; + parts.push(async () => + { + Logger.Info('Building root prim'); + Logger.increasePrefixLevel(); + try + { + return await this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), buildMap, true); + } + finally + { + Logger.decreasePrefixLevel(); + } + }); + + if (obj.children) + { + if (obj.Position === undefined) + { + obj.Position = Vector3.getZero(); + } + if (obj.Rotation === undefined) + { + obj.Rotation = Quaternion.getIdentity(); + } + let childNumber = 0; + for (const child of obj.children) + { + if (child.Position !== undefined && child.Rotation !== undefined) + { + parts.push(async () => + { + Logger.Info('Building child ' + String(++childNumber)); + try + { + Logger.increasePrefixLevel(); + return await this.buildPart(child, new Vector3(obj.Position), new Quaternion(obj.Rotation), buildMap, false); + } + finally + { + Logger.decreasePrefixLevel(); + } + }); + + } + } + } + Logger.Info('Running build'); + let results: { + results: GameObject[], + errors: Error[] + } = { + results: [], + errors: [] + }; + + try + { + Logger.increasePrefixLevel(); + results = await Utils.promiseConcurrent(parts, 5, 0); + } + finally + { + Logger.decreasePrefixLevel(); + } + Logger.Info('Done'); + if (results.errors.length > 0) + { + for (const err of results.errors) + { + console.error(err); + } + } + + let rootObj: GameObject | null = null; + Logger.Info('Linking ' + results.results.length + ' prims together'); + for (const childObject of results.results) + { + if (childObject.isMarkedRoot) + { + rootObj = childObject; + break; + } + } + if (rootObj === null) + { + throw new Error('Failed to find root prim..'); + } - const rootObj = results[0]; const childPrims: GameObject[] = []; - for (const childObject of results) + for (const childObject of results.results) { if (childObject !== rootObj) { childPrims.push(childObject); } } - await rootObj.linkFrom(childPrims); + try + { + await rootObj.linkFrom(childPrims); + } + catch (err) + { + console.error('Link failed:'); + console.error(err); + } + if (storedPosition !== undefined) + { + obj.Position = storedPosition; + } + Logger.Info('Build done'); return rootObj; } @@ -1445,11 +1267,11 @@ export class RegionCommands extends CommandsBase if (!evt.object.resolvedAt) { // We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory - await this.queueResolveObject(evt.object, true); + await this.resolveObject(evt.object, false, true); } if (evt.createSelected && !evt.object.claimedForBuild) { - if (evt.object.itemID === undefined || evt.object.itemID.equals(UUID.zero())) + if (evt.object.itemID === undefined || evt.object.itemID.isZero()) { if ( evt.object.PCode === PCode.Prim && @@ -1538,111 +1360,6 @@ export class RegionCommands extends CommandsBase }); } - rezFromInventory(obj: GameObject, position: Vector3, inventoryID: UUID): Promise - { - return new Promise(async (resolve, reject) => - { - const invItem = this.agent.inventory.itemsByID[inventoryID.toString()]; - const queryID = UUID.random(); - const msg = new RezObjectMessage(); - msg.AgentData = { - AgentID: this.agent.agentID, - SessionID: this.circuit.sessionID, - GroupID: UUID.zero() - }; - msg.RezData = { - FromTaskID: UUID.zero(), - BypassRaycast: 1, - RayStart: position, - RayEnd: position, - RayTargetID: UUID.zero(), - RayEndIsIntersection: false, - RezSelected: true, - RemoveItem: false, - ItemFlags: invItem.flags, - GroupMask: PermissionMask.All, - EveryoneMask: PermissionMask.All, - NextOwnerMask: PermissionMask.All, - }; - msg.InventoryData = { - ItemID: invItem.itemID, - FolderID: invItem.parentID, - CreatorID: invItem.permissions.creator, - OwnerID: invItem.permissions.owner, - GroupID: invItem.permissions.group, - BaseMask: invItem.permissions.baseMask, - OwnerMask: invItem.permissions.ownerMask, - GroupMask: invItem.permissions.groupMask, - EveryoneMask: invItem.permissions.everyoneMask, - NextOwnerMask: invItem.permissions.nextOwnerMask, - GroupOwned: false, - TransactionID: queryID, - Type: invItem.type, - InvType: invItem.inventoryType, - Flags: invItem.flags, - SaleType: invItem.saleType, - SalePrice: invItem.salePrice, - Name: Utils.StringToBuffer(invItem.name), - Description: Utils.StringToBuffer(invItem.description), - CreationDate: Math.round(invItem.created.getTime() / 1000), - CRC: 0, - }; - - let objSub: Subscription | undefined = undefined; - let timeout: Timeout | undefined = setTimeout(() => - { - if (objSub !== undefined) - { - objSub.unsubscribe(); - objSub = undefined; - } - if (timeout !== undefined) - { - clearTimeout(timeout); - timeout = undefined; - } - reject(new Error('Prim never arrived')); - }, 10000); - let claimedPrim = false; - objSub = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) => - { - if (evt.createSelected && !evt.object.resolvedAt) - { - // We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory - await this.queueResolveObject(evt.object, true); - } - if (evt.createSelected && !evt.object.claimedForBuild && !claimedPrim) - { - if (inventoryID !== undefined && evt.object.itemID !== undefined && evt.object.itemID.equals(inventoryID)) - { - if (objSub !== undefined) - { - objSub.unsubscribe(); - objSub = undefined; - } - if (timeout !== undefined) - { - clearTimeout(timeout); - timeout = undefined; - } - evt.object.claimedForBuild = true; - claimedPrim = true; - resolve(evt.object); - } - } - }); - - // Move the camera to look directly at prim for faster capture - if (obj.Scale !== undefined) - { - const camLocation = new Vector3(position); - camLocation.z += (obj.Scale.z / 2) + 1; - await this.currentRegion.clientCommands.agent.setCamera(camLocation, position, obj.Scale.z, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); - } - this.circuit.sendMessage(msg, PacketFlags.Reliable); - }); - } - async getObjectByLocalID(id: number, resolve: boolean, waitFor: number = 0) { let obj = null; @@ -1663,7 +1380,7 @@ export class RegionCommands extends CommandsBase } if (resolve) { - await this.resolveObjects([obj]); + await this.currentRegion.resolver.resolveObjects([obj]); } return obj; } @@ -1688,7 +1405,7 @@ export class RegionCommands extends CommandsBase } if (resolve) { - await this.resolveObjects([obj]); + await this.currentRegion.resolver.resolveObjects([obj]); } return obj; } @@ -1763,7 +1480,8 @@ export class RegionCommands extends CommandsBase const objs = await this.currentRegion.objects.getAllObjects(); if (resolve) { - await this.resolveObjects(objs, onlyUnresolved); + const resolver = new ObjectResolver(this.currentRegion); + await resolver.resolveObjects(objs, onlyUnresolved); } return objs; } @@ -1773,7 +1491,7 @@ export class RegionCommands extends CommandsBase const objs = await this.currentRegion.objects.getObjectsInArea(minX, maxX, minY, maxY, minZ, maxZ); if (resolve) { - await this.resolveObjects(objs); + await this.currentRegion.resolver.resolveObjects(objs); } return objs; } diff --git a/lib/classes/interfaces/IGameObjectData.ts b/lib/classes/interfaces/IGameObjectData.ts index 144bb2e..f401386 100644 --- a/lib/classes/interfaces/IGameObjectData.ts +++ b/lib/classes/interfaces/IGameObjectData.ts @@ -44,6 +44,7 @@ export interface IGameObjectData sitName?: string; textureID?: string; resolvedAt?: number; + resolvedInventory: boolean; totalChildren?: number; landImpact?: number; diff --git a/lib/classes/interfaces/IObjectStore.ts b/lib/classes/interfaces/IObjectStore.ts index 07bf431..548bf43 100644 --- a/lib/classes/interfaces/IObjectStore.ts +++ b/lib/classes/interfaces/IObjectStore.ts @@ -1,10 +1,12 @@ import { RBush3D } from 'rbush-3d/dist'; import { UUID } from '../UUID'; import { GameObject } from '../public/GameObject'; +import { Avatar } from '../public/Avatar'; export interface IObjectStore { rtree?: RBush3D; + populateChildren(obj: GameObject): void; getObjectsByParent(parentID: number): GameObject[]; shutdown(): void; getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): Promise; @@ -13,4 +15,5 @@ export interface IObjectStore getNumberOfObjects(): number; getAllObjects(): Promise; setPersist(persist: boolean): void; + getAvatar(avatarID: UUID): Avatar; } diff --git a/lib/classes/interfaces/IResolveJob.ts b/lib/classes/interfaces/IResolveJob.ts new file mode 100644 index 0000000..51a45e0 --- /dev/null +++ b/lib/classes/interfaces/IResolveJob.ts @@ -0,0 +1,8 @@ +import { GameObject } from '../..'; + +export interface IResolveJob +{ + object: GameObject, + skipInventory: boolean, + log: boolean +} diff --git a/lib/classes/public/Avatar.ts b/lib/classes/public/Avatar.ts index 8df0e91..b61b231 100644 --- a/lib/classes/public/Avatar.ts +++ b/lib/classes/public/Avatar.ts @@ -1,25 +1,200 @@ -import { UUID } from '../UUID'; - -export class Avatar -{ - constructor(private avatarKey: UUID, private firstName: string, private lastName: string) - { - - } - getName(): string - { - return this.firstName + ' ' + this.lastName; - } - getFirstName(): string - { - return this.firstName; - } - getLastName(): string - { - return this.lastName; - } - getKey(): UUID - { - return this.avatarKey; - } -} +import { AvatarQueryResult } from './AvatarQueryResult'; +import { GameObject } from './GameObject'; +import { Vector3 } from '../Vector3'; +import { Quaternion } from '../Quaternion'; +import { Subject, Subscription } from 'rxjs'; +import { UUID } from '../UUID'; +import Timer = NodeJS.Timer; + +export class Avatar extends AvatarQueryResult +{ + private position: Vector3 = Vector3.getZero(); + private rotation: Quaternion = Quaternion.getIdentity(); + private title = ''; + + public onAvatarMoved: Subject = new Subject(); + public onTitleChanged: Subject = new Subject(); + public onLeftRegion: Subject = new Subject(); + public onAttachmentAdded: Subject = new Subject(); + public onAttachmentRemoved: Subject = new Subject(); + + private attachments: {[key: string]: GameObject} = {}; + + static fromGameObject(obj: GameObject): Avatar + { + let firstName = 'Unknown'; + let lastName = 'Avatar'; + + if (obj.NameValue['FirstName'] !== undefined) + { + firstName = obj.NameValue['FirstName'].value; + } + if (obj.NameValue['LastName'] !== undefined) + { + lastName = obj.NameValue['LastName'].value; + } + + const av = new Avatar(obj, firstName , lastName); + if (obj.NameValue['Title'] !== undefined) + { + av.setTitle(obj.NameValue['Title'].value); + } + av.processObjectUpdate(obj); + return av; + } + + constructor(private gameObject: GameObject, firstName: string, lastName: string) + { + super(gameObject.FullID, firstName, lastName); + const objs: GameObject[] = this.gameObject.region.objects.getObjectsByParent(gameObject.ID); + for (const attachment of objs) + { + this.gameObject.region.clientCommands.region.resolveObject(attachment, true, false).then(() => + { + this.addAttachment(attachment); + }).catch((err) => + { + console.error('Failed to resolve attachment for avatar'); + }); + } + } + + setTitle(newTitle: string) + { + if (newTitle !== this.title) + { + this.title = newTitle; + this.onTitleChanged.next(this); + } + } + + getTitle(): string + { + return this.title; + } + + getPosition(): Vector3 + { + return new Vector3(this.position); + } + + getRotation(): Quaternion + { + return new Quaternion(this.rotation); + } + + processObjectUpdate(obj: GameObject) + { + if (obj.Position !== undefined && obj.Rotation !== undefined) + { + this.setGeometry(obj.Position, obj.Rotation); + } + if (obj.NameValue['Title'] !== undefined) + { + this.setTitle(obj.NameValue['Title'].value); + } + } + + setGeometry(position: Vector3, rotation: Quaternion) + { + const oldPosition = this.position; + const oldRotation = this.rotation; + + this.position = new Vector3(position); + this.rotation = new Quaternion(rotation); + + const rotDist = new Quaternion(this.rotation).angleBetween(oldRotation); + if (Vector3.distance(position, oldPosition) > 0.0001 || rotDist > 0.0001) + { + this.onAvatarMoved.next(this); + } + } + + leftRegion() + { + this.onLeftRegion.next(this); + } + + getAttachment(itemID: UUID) + { + if (this.attachments[itemID.toString()] !== undefined) + { + return this.attachments[itemID.toString()]; + } + throw new Error('Attachment not found'); + } + + waitForAttachment(itemID: UUID | string, timeout: number = 30000) + { + return new Promise((resolve, reject) => + { + if (typeof itemID === 'string') + { + itemID = new UUID(itemID); + } + try + { + const attach = this.getAttachment(itemID); + resolve(attach); + } + catch (ignore) + { + let subs: Subscription | undefined = undefined; + let timr: Timer | undefined = undefined; + subs = this.onAttachmentAdded.subscribe((obj: GameObject) => + { + if (obj.itemID.equals(itemID)) + { + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + if (timr !== undefined) + { + clearTimeout(timr); + timr = undefined; + } + resolve(obj); + } + }); + timr = setTimeout(() => + { + if (subs !== undefined) + { + subs.unsubscribe(); + subs = undefined; + } + if (timr !== undefined) + { + clearTimeout(timr); + timr = undefined; + } + reject(new Error('WaitForAttachment timed out')); + }, timeout); + } + }); + } + + addAttachment(obj: GameObject) + { + if (obj.itemID !== undefined) + { + this.attachments[obj.itemID.toString()] = obj; + this.onAttachmentAdded.next(obj); + } + } + + removeAttachment(obj: GameObject) + { + if (obj.NameValue['AttachItemID']) + { + const itemID = new UUID(obj.NameValue['AttachItemID'].value); + if (this.attachments[itemID.toString()] !== undefined) + { + this.onAttachmentRemoved.next(obj); + delete this.attachments[itemID.toString()]; + } + } + } +} diff --git a/lib/classes/public/AvatarQueryResult.ts b/lib/classes/public/AvatarQueryResult.ts new file mode 100644 index 0000000..909e2a6 --- /dev/null +++ b/lib/classes/public/AvatarQueryResult.ts @@ -0,0 +1,29 @@ +import { UUID } from '../UUID'; + +export class AvatarQueryResult +{ + constructor(private avatarKey: UUID, private firstName: string, private lastName: string) + { + + } + + getName(): string + { + return this.firstName + ' ' + this.lastName; + } + + getFirstName(): string + { + return this.firstName; + } + + getLastName(): string + { + return this.lastName; + } + + getKey(): UUID + { + return this.avatarKey; + } +} diff --git a/lib/classes/public/GameObject.ts b/lib/classes/public/GameObject.ts index 327dc42..fd1e995 100644 --- a/lib/classes/public/GameObject.ts +++ b/lib/classes/public/GameObject.ts @@ -11,11 +11,9 @@ import { NameValue } from '../NameValue'; import * as Long from 'long'; import { IGameObjectData } from '../interfaces/IGameObjectData'; import * as builder from 'xmlbuilder'; -import { XMLNode } from 'xmlbuilder'; -import * as xml2js from 'xml2js'; +import { XMLElement, XMLNode } from 'xmlbuilder'; import { Region } from '../Region'; import { InventoryItem } from '../InventoryItem'; -import { InventoryType } from '../../enums/InventoryType'; import { LLWearable } from '../LLWearable'; import { TextureAnim } from './TextureAnim'; import { ExtraParams } from './ExtraParams'; @@ -47,6 +45,19 @@ import { UpdateTaskInventoryMessage } from '../messages/UpdateTaskInventory'; import { ObjectPropertiesMessage } from '../messages/ObjectProperties'; import { ObjectSelectMessage } from '../messages/ObjectSelect'; import { ObjectDeselectMessage } from '../messages/ObjectDeselect'; +import { AttachmentPoint } from '../../enums/AttachmentPoint'; +import { RequestTaskInventoryMessage } from '../messages/RequestTaskInventory'; +import { ReplyTaskInventoryMessage } from '../messages/ReplyTaskInventory'; +import { AssetTypeLL } from '../../enums/AssetTypeLL'; +import { InventoryType } from '../../enums/InventoryType'; +import { InventoryFolder } from '../InventoryFolder'; +import { ObjectUpdateMessage } from '../messages/ObjectUpdate'; +import { Subject } from 'rxjs'; +import { RezScriptMessage } from '../messages/RezScript'; +import { PermissionMask } from '../../enums/PermissionMask'; +import { AssetType } from '../../enums/AssetType'; + +import * as uuid from 'uuid'; export class GameObject implements IGameObjectData { @@ -82,6 +93,7 @@ export class GameObject implements IGameObjectData sitName?: string; textureID?: string; resolvedAt?: number; + resolvedInventory = false; totalChildren?: number; landImpact?: number; @@ -150,6 +162,7 @@ export class GameObject implements IGameObjectData gravityMultiplier?: number; physicsShapeType?: PhysicsShapeType; restitution?: number; + attachmentPoint: AttachmentPoint = AttachmentPoint.Default; region: Region; @@ -159,47 +172,15 @@ export class GameObject implements IGameObjectData claimedForBuild = false; createdSelected = false; + isMarkedRoot = false; + onTextureUpdate: Subject = new Subject(); - private static getFromXMLJS(obj: any, param: string): any - { - if (obj[param] === undefined) - { - return undefined; - } - let retParam; - if (Array.isArray(obj[param])) - { - retParam = obj[param][0]; - } - else - { - retParam = obj[param]; - } - if (typeof retParam === 'string') - { - if (retParam.toLowerCase() === 'false') - { - return false; - } - if (retParam.toLowerCase() === 'true') - { - return true; - } - const numVar = parseInt(retParam, 10); - if (numVar >= Number.MIN_SAFE_INTEGER && numVar <= Number.MAX_SAFE_INTEGER && String(numVar) === retParam) - { - return numVar - } - } - return retParam; - } - - private static partFromXMLJS(obj: any, root: boolean) + private static partFromXMLJS(obj: any, isRoot: boolean) { const go = new GameObject(); go.Flags = 0; let prop: any; - if (this.getFromXMLJS(obj, 'AllowedDrop') !== undefined) + if (Utils.getFromXMLJS(obj, 'AllowedDrop') !== undefined) { go.Flags = go.Flags | PrimFlags.AllowInventoryDrop; } @@ -211,7 +192,7 @@ export class GameObject implements IGameObjectData { go.folderID = prop; } - if ((prop = this.getFromXMLJS(obj, 'InventorySerial')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'InventorySerial')) !== undefined) { go.inventorySerial = prop; } @@ -219,28 +200,28 @@ export class GameObject implements IGameObjectData { go.FullID = prop; } - if ((prop = this.getFromXMLJS(obj, 'LocalId')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'LocalId')) !== undefined) { go.ID = prop; } - if ((prop = this.getFromXMLJS(obj, 'Name')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Name')) !== undefined) { - go.name = prop; + go.name = String(prop); } - if ((prop = this.getFromXMLJS(obj, 'Material')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Material')) !== undefined) { go.Material = prop; } if ((prop = Vector3.fromXMLJS(obj, 'GroupPosition')) !== undefined) { - if (root) + if (isRoot) { go.Position = prop; } } if ((prop = Vector3.fromXMLJS(obj, 'OffsetPosition')) !== undefined) { - if (!root) + if (!isRoot) { go.Position = prop; } @@ -261,27 +242,27 @@ export class GameObject implements IGameObjectData { go.Acceleration = prop; } - if ((prop = this.getFromXMLJS(obj, 'Description')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Description')) !== undefined) { - go.description = prop; + go.description = String(prop); } - if ((prop = this.getFromXMLJS(obj, 'Text')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Text')) !== undefined) { - go.Text = prop; + go.Text = String(prop); } if ((prop = Color4.fromXMLJS(obj, 'Color')) !== undefined) { go.TextColor = prop; } - if ((prop = this.getFromXMLJS(obj, 'SitName')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'SitName')) !== undefined) { - go.sitName = prop; + go.sitName = String(prop); } - if ((prop = this.getFromXMLJS(obj, 'TouchName')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'TouchName')) !== undefined) { - go.touchName = prop; + go.touchName = String(prop); } - if ((prop = this.getFromXMLJS(obj, 'ClickAction')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'ClickAction')) !== undefined) { go.ClickAction = prop; } @@ -289,23 +270,23 @@ export class GameObject implements IGameObjectData { go.Scale = prop; } - if ((prop = this.getFromXMLJS(obj, 'ParentID')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'ParentID')) !== undefined) { go.ParentID = prop; } - if ((prop = this.getFromXMLJS(obj, 'Category')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Category')) !== undefined) { go.category = prop; } - if ((prop = this.getFromXMLJS(obj, 'SalePrice')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'SalePrice')) !== undefined) { go.salePrice = prop; } - if ((prop = this.getFromXMLJS(obj, 'ObjectSaleType')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'ObjectSaleType')) !== undefined) { go.saleType = prop; } - if ((prop = this.getFromXMLJS(obj, 'OwnershipCost')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'OwnershipCost')) !== undefined) { go.ownershipCost = prop; } @@ -321,27 +302,27 @@ export class GameObject implements IGameObjectData { go.lastOwnerID = prop; } - if ((prop = this.getFromXMLJS(obj, 'BaseMask')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'BaseMask')) !== undefined) { go.baseMask = prop; } - if ((prop = this.getFromXMLJS(obj, 'OwnerMask')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'OwnerMask')) !== undefined) { go.ownerMask = prop; } - if ((prop = this.getFromXMLJS(obj, 'GroupMask')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'GroupMask')) !== undefined) { go.groupMask = prop; } - if ((prop = this.getFromXMLJS(obj, 'EveryoneMask')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'EveryoneMask')) !== undefined) { go.everyoneMask = prop; } - if ((prop = this.getFromXMLJS(obj, 'NextOwnerMask')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'NextOwnerMask')) !== undefined) { go.nextOwnerMask = prop; } - if ((prop = this.getFromXMLJS(obj, 'Flags')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Flags')) !== undefined) { let flags = 0; if (typeof prop === 'string') @@ -358,17 +339,17 @@ export class GameObject implements IGameObjectData } go.Flags = flags; } - if ((prop = this.getFromXMLJS(obj, 'TextureAnimation')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'TextureAnimation')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.textureAnim = TextureAnim.from(buf); } - if ((prop = this.getFromXMLJS(obj, 'ParticleSystem')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'ParticleSystem')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.Particles = ParticleSystem.from(buf); } - if ((prop = this.getFromXMLJS(obj, 'PhysicsShapeType')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'PhysicsShapeType')) !== undefined) { go.physicsShapeType = prop; } @@ -388,95 +369,97 @@ export class GameObject implements IGameObjectData { go.SoundRadius = prop; } - if ((prop = this.getFromXMLJS(obj, 'Shape')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'Shape')) !== undefined) { const shape = prop; - if ((prop = this.getFromXMLJS(shape, 'ProfileCurve')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ProfileCurve')) !== undefined) { go.ProfileCurve = prop; } - if ((prop = this.getFromXMLJS(shape, 'TextureEntry')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'TextureEntry')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.TextureEntry = TextureEntry.from(buf); } - if ((prop = this.getFromXMLJS(shape, 'PathBegin')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathBegin')) !== undefined) { go.PathBegin = Utils.unpackBeginCut(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathCurve')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathCurve')) !== undefined) { go.PathCurve = prop; } - if ((prop = this.getFromXMLJS(shape, 'PathEnd')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathEnd')) !== undefined) { go.PathEnd = Utils.unpackEndCut(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathRadiusOffset')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathRadiusOffset')) !== undefined) { go.PathRadiusOffset = Utils.unpackPathTwist(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathRevolutions')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathRevolutions')) !== undefined) { go.PathRevolutions = Utils.unpackPathRevolutions(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathScaleX')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathScaleX')) !== undefined) { go.PathScaleX = Utils.unpackPathScale(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathScaleY')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathScaleY')) !== undefined) { go.PathScaleY = Utils.unpackPathScale(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathShearX')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathShearX')) !== undefined) { go.PathShearX = Utils.unpackPathShear(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathShearY')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathShearY')) !== undefined) { go.PathShearY = Utils.unpackPathShear(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathSkew')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathSkew')) !== undefined) { go.PathSkew = Utils.unpackPathShear(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathTaperX')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathTaperX')) !== undefined) { go.PathTaperX = Utils.unpackPathTaper(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathTaperY')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathTaperY')) !== undefined) { go.PathTaperY = Utils.unpackPathTaper(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathTwist')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathTwist')) !== undefined) { go.PathTwist = Utils.unpackPathTwist(prop); } - if ((prop = this.getFromXMLJS(shape, 'PathTwistBegin')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PathTwistBegin')) !== undefined) { go.PathTwistBegin = Utils.unpackPathTwist(prop); } - if ((prop = this.getFromXMLJS(shape, 'PCode')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'PCode')) !== undefined) { go.PCode = prop; } - if ((prop = this.getFromXMLJS(shape, 'ProfileBegin')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ProfileBegin')) !== undefined) { go.ProfileBegin = Utils.unpackBeginCut(prop); } - if ((prop = this.getFromXMLJS(shape, 'ProfileEnd')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ProfileEnd')) !== undefined) { go.ProfileEnd = Utils.unpackEndCut(prop); } - if ((prop = this.getFromXMLJS(shape, 'ProfileHollow')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ProfileHollow')) !== undefined) { go.ProfileHollow = Utils.unpackProfileHollow(prop); } - if ((prop = this.getFromXMLJS(shape, 'State')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'State')) !== undefined) { - go.State = prop; + go.attachmentPoint = parseInt(prop, 10); + const mask = 0xf << 4 >>> 0; + go.State = (((prop & mask) >>> 4) | ((prop & ~mask) << 4)) >>> 0; } - if ((prop = this.getFromXMLJS(shape, 'ProfileShape')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ProfileShape')) !== undefined) { if (!go.ProfileCurve) { @@ -484,7 +467,7 @@ export class GameObject implements IGameObjectData } go.ProfileCurve = go.ProfileCurve | parseInt(ProfileShape[prop], 10); } - if ((prop = this.getFromXMLJS(shape, 'HollowShape')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'HollowShape')) !== undefined) { if (!go.ProfileCurve) { @@ -492,9 +475,9 @@ export class GameObject implements IGameObjectData } go.ProfileCurve = go.ProfileCurve | parseInt(HoleType[prop], 10); } - if (this.getFromXMLJS(shape, 'SculptEntry') !== undefined) + if (Utils.getFromXMLJS(shape, 'SculptEntry') !== undefined) { - const type = this.getFromXMLJS(shape, 'SculptType'); + const type = Utils.getFromXMLJS(shape, 'SculptType'); if (type !== false && type !== undefined) { const id = UUID.fromXMLJS(shape, 'SculptTexture'); @@ -515,16 +498,16 @@ export class GameObject implements IGameObjectData } } } - if (this.getFromXMLJS(shape, 'FlexiEntry') !== undefined) + if (Utils.getFromXMLJS(shape, 'FlexiEntry') !== undefined) { - const flexiSoftness = this.getFromXMLJS(shape, 'FlexiSoftness'); - const flexiTension = this.getFromXMLJS(shape, 'FlexiTension'); - const flexiDrag = this.getFromXMLJS(shape, 'FlexiDrag'); - const flexiGravity = this.getFromXMLJS(shape, 'FlexiGravity'); - const flexiWind = this.getFromXMLJS(shape, 'FlexiWind'); - const flexiForceX = this.getFromXMLJS(shape, 'FlexiForceX'); - const flexiForceY = this.getFromXMLJS(shape, 'FlexiForceY'); - const flexiForceZ = this.getFromXMLJS(shape, 'FlexiForceZ'); + const flexiSoftness = Utils.getFromXMLJS(shape, 'FlexiSoftness'); + const flexiTension = Utils.getFromXMLJS(shape, 'FlexiTension'); + const flexiDrag = Utils.getFromXMLJS(shape, 'FlexiDrag'); + const flexiGravity = Utils.getFromXMLJS(shape, 'FlexiGravity'); + const flexiWind = Utils.getFromXMLJS(shape, 'FlexiWind'); + const flexiForceX = Utils.getFromXMLJS(shape, 'FlexiForceX'); + const flexiForceY = Utils.getFromXMLJS(shape, 'FlexiForceY'); + const flexiForceZ = Utils.getFromXMLJS(shape, 'FlexiForceZ'); if (flexiSoftness !== false && flexiTension !== false && flexiDrag && false && @@ -541,16 +524,16 @@ export class GameObject implements IGameObjectData go.extraParams.setFlexiData(flexiSoftness, flexiTension, flexiDrag, flexiGravity, flexiWind, new Vector3([flexiForceX, flexiForceY, flexiForceZ])); } } - if (this.getFromXMLJS(shape, 'LightEntry') !== undefined) + if (Utils.getFromXMLJS(shape, 'LightEntry') !== undefined) { - const lightColorR = this.getFromXMLJS(shape, 'LightColorR'); - const lightColorG = this.getFromXMLJS(shape, 'LightColorG'); - const lightColorB = this.getFromXMLJS(shape, 'LightColorB'); - const lightColorA = this.getFromXMLJS(shape, 'LightColorA'); - const lightRadius = this.getFromXMLJS(shape, 'LightRadius'); - const lightCutoff = this.getFromXMLJS(shape, 'LightCutoff'); - const lightFalloff = this.getFromXMLJS(shape, 'LightFalloff'); - const lightIntensity = this.getFromXMLJS(shape, 'LightIntensity'); + const lightColorR = Utils.getFromXMLJS(shape, 'LightColorR'); + const lightColorG = Utils.getFromXMLJS(shape, 'LightColorG'); + const lightColorB = Utils.getFromXMLJS(shape, 'LightColorB'); + const lightColorA = Utils.getFromXMLJS(shape, 'LightColorA'); + const lightRadius = Utils.getFromXMLJS(shape, 'LightRadius'); + const lightCutoff = Utils.getFromXMLJS(shape, 'LightCutoff'); + const lightFalloff = Utils.getFromXMLJS(shape, 'LightFalloff'); + const lightIntensity = Utils.getFromXMLJS(shape, 'LightIntensity'); if (lightColorR !== false && lightColorG !== false && lightColorB !== false && @@ -573,45 +556,45 @@ export class GameObject implements IGameObjectData ); } } - if ((prop = this.getFromXMLJS(shape, 'ExtraParams')) !== undefined) + if ((prop = Utils.getFromXMLJS(shape, 'ExtraParams')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.extraParams = ExtraParams.from(buf); } } - if ((prop = this.getFromXMLJS(obj, 'TaskInventory')) !== undefined) + if ((prop = Utils.getFromXMLJS(obj, 'TaskInventory')) !== undefined) { if (prop.TaskInventoryItem) { for (const invItemXML of prop.TaskInventoryItem) { - const invItem = new InventoryItem(); + const invItem = new InventoryItem(go); let subProp: any; if ((subProp = UUID.fromXMLJS(invItemXML, 'AssetID')) !== undefined) { invItem.assetID = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'BasePermissions')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'BasePermissions')) !== undefined) { invItem.permissions.baseMask = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'EveryonePermissions')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'EveryonePermissions')) !== undefined) { invItem.permissions.everyoneMask = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'GroupPermissions')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'GroupPermissions')) !== undefined) { invItem.permissions.groupMask = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'NextPermissions')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'NextPermissions')) !== undefined) { invItem.permissions.nextOwnerMask = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'CurrentPermissions')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'CurrentPermissions')) !== undefined) { invItem.permissions.ownerMask = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'CreationDate')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'CreationDate')) !== undefined) { invItem.created = new Date(parseInt(subProp, 10) * 1000); } @@ -619,11 +602,11 @@ export class GameObject implements IGameObjectData { invItem.permissions.creator = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'Description')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'Description')) !== undefined) { - invItem.description = subProp; + invItem.description = String(subProp); } - if ((subProp = this.getFromXMLJS(invItemXML, 'Flags')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'Flags')) !== undefined) { invItem.flags = subProp; } @@ -631,7 +614,7 @@ export class GameObject implements IGameObjectData { invItem.permissions.group = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'InvType')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'InvType')) !== undefined) { invItem.inventoryType = subProp; } @@ -647,9 +630,9 @@ export class GameObject implements IGameObjectData { invItem.permissions.lastOwner = subProp; } - if ((subProp = this.getFromXMLJS(invItemXML, 'Name')) !== undefined) + if ((subProp = Utils.getFromXMLJS(invItemXML, 'Name')) !== undefined) { - invItem.name = subProp; + invItem.name = String(subProp); } if ((subProp = UUID.fromXMLJS(invItemXML, 'OwnerID')) !== undefined) { @@ -674,56 +657,169 @@ export class GameObject implements IGameObjectData return go; } - static fromXML(xml: string) + static async fromXML(xml: string | any) { - return new Promise((resolve, reject) => + let result; + if (typeof xml === 'string') { - xml2js.parseString(xml, (err, result) => + const parsed = await Utils.parseXML(xml); + if (!parsed['SceneObjectGroup']) { - if (err) + throw new Error('SceneObjectGroup not found'); + } + result = parsed['SceneObjectGroup']; + } + else + { + result = xml; + } + + let rootPartXML; + if (result['SceneObjectPart']) + { + rootPartXML = result['SceneObjectPart']; + } + else if (result['RootPart'] && result['RootPart'][0] && result['RootPart'][0]['SceneObjectPart']) + { + rootPartXML = result['RootPart'][0]['SceneObjectPart']; + } + else + { + throw new Error('Root part not found'); + } + + const rootPart = GameObject.partFromXMLJS(rootPartXML[0], true); + rootPart.children = []; + rootPart.totalChildren = 0; + if (result['OtherParts'] && Array.isArray(result['OtherParts']) && result['OtherParts'].length > 0) + { + const obj = result['OtherParts'][0]; + if (obj['SceneObjectPart'] || obj['Part']) + { + if (obj['Part']) { - reject(err); + for (const part of obj['Part']) + { + rootPart.children.push(GameObject.partFromXMLJS(part['SceneObjectPart'][0], false)); + rootPart.totalChildren++; + } } else { - if (!result['SceneObjectGroup']) + for (const part of obj['SceneObjectPart']) { - throw new Error('SceneObjectGroup not found'); + rootPart.children.push(GameObject.partFromXMLJS(part, false)); + rootPart.totalChildren++; } - result = result['SceneObjectGroup']; - - let rootPartXML; - if (result['SceneObjectPart']) - { - rootPartXML = result['SceneObjectPart']; - } - else if (result['RootPart'] && result['RootPart'][0] && result['RootPart'][0]['SceneObjectPart']) - { - rootPartXML = result['RootPart'][0]['SceneObjectPart']; - } - else - { - throw new Error('Root part not found'); - } - - const rootPart = GameObject.partFromXMLJS(rootPartXML[0], true); - rootPart.children = []; - rootPart.totalChildren = 0; - if (result['OtherParts'] && Array.isArray(result['OtherParts']) && result['OtherParts'].length > 0) - { - const obj = result['OtherParts'][0]; - if (obj['SceneObjectPart']) - { - for (const part of obj['SceneObjectPart']) - { - rootPart.children.push(GameObject.partFromXMLJS(part, false)); - rootPart.totalChildren++; - } - } - } - resolve(rootPart); } + } + } + return rootPart; + } + + static async deRezObjects(region: Region, objects: GameObject[], destination: DeRezDestination, transactionID: UUID, destFolder: UUID): Promise + { + const msg = new DeRezObjectMessage(); + + msg.AgentData = { + AgentID: region.agent.agentID, + SessionID: region.circuit.sessionID + }; + msg.AgentBlock = { + GroupID: UUID.zero(), + Destination: destination, + DestinationID: destFolder, + TransactionID: transactionID, + PacketCount: 1, + PacketNumber: 1 + }; + msg.ObjectData = []; + for (const obj of objects) + { + msg.ObjectData.push({ + ObjectLocalID: obj.ID }); + } + const ack = region.circuit.sendMessage(msg, PacketFlags.Reliable); + return region.circuit.waitForAck(ack, 10000); + } + + static takeManyToInventory(region: Region, objects: GameObject[], folder?: InventoryFolder): Promise + { + const transactionID = UUID.random(); + let enforceFolder = true; + if (folder === undefined) + { + enforceFolder = false; + folder = region.agent.inventory.getRootFolderMain(); + } + return new Promise((resolve, reject) => + { + + region.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => + { + for (const inv of message.InventoryData) + { + const name = Utils.BufferToStringSimple(inv.Name, 0); + if (name === objects[0].name) + { + return FilterResponse.Finish; + } + } + return FilterResponse.NoMatch; + }).then((createInventoryMsg: UpdateCreateInventoryItemMessage) => + { + for (const inv of createInventoryMsg.InventoryData) + { + const name = Utils.BufferToStringSimple(inv.Name, 0); + if (name === objects[0].name) + { + const itemID = inv.ItemID; + region.agent.inventory.fetchInventoryItem(itemID).then((item: InventoryItem | null) => + { + if (item === null) + { + reject(new Error('Inventory item was unable to be retrieved after take to inventory')); + } + else + { + if (enforceFolder && folder !== undefined && !item.parentID.equals(folder.folderID)) + { + item.moveToFolder(folder).then(() => + { + resolve(item); + }).catch((err: Error) => + { + console.error('Error moving item to correct folder'); + console.error(err); + resolve(item); + }); + } + else + { + resolve(item); + } + } + }).catch((err) => + { + reject(err); + }); + return; + } + } + }).catch(() => + { + reject(new Error('Timed out waiting for UpdateCreateInventoryItem')); + }); + if (folder !== undefined) + { + GameObject.deRezObjects(region, objects, DeRezDestination.AgentInventoryTake, transactionID, folder.folderID).then(() => + { + }).catch((err) => + { + console.error(err); + }); + } }); } @@ -738,6 +834,203 @@ export class GameObject implements IGameObjectData this.SoundGain = 1.0; } + async waitForTextureUpdate(timeout?: number) + { + return Utils.waitOrTimeOut(this.onTextureUpdate, timeout); + } + + async rezScript(name: string, description: string, perms: PermissionMask = 532480): Promise + { + const rezScriptMsg = new RezScriptMessage(); + rezScriptMsg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID, + GroupID: this.region.agent.activeGroupID + }; + rezScriptMsg.UpdateBlock = { + ObjectLocalID: this.ID, + Enabled: true + }; + const tmpName = uuid.v4(); + const invItem = new InventoryItem(this); + invItem.itemID = UUID.zero(); + invItem.parentID = this.FullID; + invItem.permissions.creator = this.region.agent.agentID; + invItem.permissions.group = UUID.zero(); + invItem.permissions.baseMask = PermissionMask.All; + invItem.permissions.ownerMask = PermissionMask.All; + invItem.permissions.groupMask = 0; + invItem.permissions.everyoneMask = 0; + invItem.permissions.nextOwnerMask = perms; + invItem.permissions.groupOwned = false; + invItem.type = AssetType.LSLText; + invItem.inventoryType = InventoryType.LSL; + invItem.flags = 0; + invItem.salePrice = this.salePrice || 10; + invItem.saleType = this.saleType || 0; + invItem.name = tmpName; + invItem.description = description; + invItem.created = new Date(); + + rezScriptMsg.InventoryBlock = { + ItemID: UUID.zero(), + FolderID: this.FullID, + CreatorID: this.region.agent.agentID, + OwnerID: this.region.agent.agentID, + GroupID: UUID.zero(), + BaseMask: PermissionMask.All, + OwnerMask: PermissionMask.All, + GroupMask: 0, + EveryoneMask: 0, + NextOwnerMask: perms, + GroupOwned: false, + TransactionID: UUID.zero(), + Type: AssetType.LSLText, + InvType: InventoryType.LSL, + Flags: 0, + SaleType: this.saleType || 0, + SalePrice: this.salePrice || 10, + Name: Utils.StringToBuffer(tmpName), + Description: Utils.StringToBuffer(description), + CreationDate: Math.floor(invItem.created.getTime() / 1000), + CRC: invItem.getCRC() + }; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(rezScriptMsg, PacketFlags.Reliable), 10000); + await this.updateInventory(); + for (const item of this.inventory) + { + if (item.name === tmpName) + { + item.renameInTask(this, name).then(() => {}).catch((err) => {}); + await this.waitForInventoryUpdate(); + await this.updateInventory(); + for (const newItem of this.inventory) + { + if (newItem.itemID.equals(item.itemID)) + { + return newItem; + } + } + return item; + } + } + throw new Error('Failed to add script to object'); + } + + async updateInventory() + { + const req = new RequestTaskInventoryMessage(); + req.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + req.InventoryData = { + LocalID: this.ID + }; + this.region.circuit.sendMessage(req, PacketFlags.Reliable); + await this.waitForTaskInventory(); + } + + private async waitForTaskInventory() + { + let serial = 0; + const inventory = await this.region.circuit.waitForMessage(Message.ReplyTaskInventory, 10000, (message: ReplyTaskInventoryMessage): FilterResponse => + { + serial = message.InventoryData.Serial; + if (message.InventoryData.TaskID.equals(this.FullID)) + { + return FilterResponse.Finish; + } + else + { + return FilterResponse.Match; + } + }); + const fileName = Utils.BufferToStringSimple(inventory.InventoryData.Filename); + + const file = await this.region.circuit.XferFileDown(fileName, true, false, UUID.zero(), AssetType.Unknown, true); + this.inventory = []; + if (file.length === 0) + { + if (this.Flags === undefined) + { + this.Flags = 0; + } + this.Flags = this.Flags | PrimFlags.InventoryEmpty; + } + else + { + const str = file.toString('utf-8'); + const lineObj = { + lines: str.replace(/\r\n/g, '\n').split('\n'), + lineNum: 0 + }; + while (lineObj.lineNum < lineObj.lines.length) + { + const line = lineObj.lines[lineObj.lineNum++]; + let result = Utils.parseLine(line); + if (result.key !== null) + { + switch (result.key) + { + case 'inv_object': + let itemID = UUID.zero(); + let parentID = UUID.zero(); + let name = ''; + let assetType: AssetType = AssetType.Unknown; + + while (lineObj.lineNum < lineObj.lines.length) + { + result = Utils.parseLine(lineObj.lines[lineObj.lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'obj_id') + { + itemID = new UUID(result.value); + } + else if (result.key === 'parent_id') + { + parentID = new UUID(result.value); + } + else if (result.key === 'type') + { + const typeString = result.value as any; + assetType = parseInt(AssetTypeLL[typeString], 10); + } + else if (result.key === 'name') + { + name = result.value.substr(0, result.value.indexOf('|')); + } + } + } + + if (name !== 'Contents') + { + console.log('TODO: Do something useful with inv_objects') + } + + break; + case 'inv_item': + this.inventory.push(InventoryItem.fromAsset(lineObj, this, this.region.agent)); + break; + default: + { + console.log('Unrecognised task inventory token: [' + result.key + ']'); + } + } + } + } + } + } + hasNameValueEntry(key: string): boolean { return this.NameValue[key] !== undefined; @@ -861,15 +1154,16 @@ export class GameObject implements IGameObjectData await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); } - async setGeometry(pos?: Vector3, rot?: Quaternion, scale?: Vector3) + async setGeometry(pos?: Vector3, rot?: Quaternion, scale?: Vector3, wholeLinkset: boolean = false) { const data = []; + const linked = (wholeLinkset) ? UpdateType.Linked : 0; if (pos !== undefined) { this.Position = pos; data.push({ ObjectLocalID: this.ID, - Type: UpdateType.Position, + Type: UpdateType.Position | linked, Data: pos.getBuffer() }); } @@ -878,7 +1172,7 @@ export class GameObject implements IGameObjectData this.Rotation = rot; data.push({ ObjectLocalID: this.ID, - Type: UpdateType.Rotation, + Type: UpdateType.Rotation | linked, Data: rot.getBuffer() }) } @@ -887,7 +1181,7 @@ export class GameObject implements IGameObjectData this.Scale = scale; data.push({ ObjectLocalID: this.ID, - Type: UpdateType.Scale, + Type: UpdateType.Scale | linked, Data: scale.getBuffer() }) } @@ -904,7 +1198,7 @@ export class GameObject implements IGameObjectData await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); } - async linkTo(root: GameObject) + async linkTo(rootObj: GameObject) { const msg = new ObjectLinkMessage(); msg.AgentData = { @@ -913,7 +1207,7 @@ export class GameObject implements IGameObjectData }; msg.ObjectData = [ { - ObjectLocalID: root.ID + ObjectLocalID: rootObj.ID }, { ObjectLocalID: this.ID @@ -922,26 +1216,66 @@ export class GameObject implements IGameObjectData await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); } - async linkFrom(objects: GameObject[]) + async linkFrom(objects: GameObject[]): Promise { - 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) + return new Promise((resolve, reject) => { - msg.ObjectData.push( + if (objects.length === 0) { - ObjectLocalID: obj.ID + resolve(); + return; + } + const primsExpectingUpdate: {[key: number]: GameObject} = {}; + const msg = new ObjectLinkMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + ObjectLocalID: this.ID + } + ]; + primsExpectingUpdate[this.ID] = this; + for (const obj of objects) + { + msg.ObjectData.push( + { + ObjectLocalID: obj.ID + }); + primsExpectingUpdate[obj.ID] = obj; + } + this.region.circuit.waitForMessage(Message.ObjectUpdate, 10000, (message: ObjectUpdateMessage) => + { + let match = false; + for (const obj of message.ObjectData) + { + const num = obj.ID; + if (primsExpectingUpdate[num] !== undefined) + { + delete primsExpectingUpdate[num]; + match = true; + } + } + if (match) + { + if (Object.keys(primsExpectingUpdate).length === 0) + { + return FilterResponse.Finish; + } + return FilterResponse.Match; + } + return FilterResponse.NoMatch; + }).then((message: ObjectUpdateMessage) => + { + resolve(); + }).catch((err) => + { + reject(err); }); - } - await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); + this.region.circuit.sendMessage(msg, PacketFlags.Reliable); + }); + } async setDescription(desc: string) @@ -1086,11 +1420,18 @@ export class GameObject implements IGameObjectData private async getInventoryXML(xml: XMLNode, inv: InventoryItem) { - if (!inv.assetID.equals(UUID.zero())) + if (!inv.assetID.isZero() || !inv.itemID.isZero()) { const item = xml.ele('TaskInventoryItem'); + + if (inv.inventoryType === InventoryType.Object && inv.assetID.isZero()) + { + inv.assetID = inv.itemID; + } + UUID.getXML(item.ele('AssetID'), inv.assetID); UUID.getXML(item.ele('ItemID'), inv.itemID); + if (inv.permissions) { item.ele('BasePermissions', inv.permissions.baseMask); @@ -1110,7 +1451,7 @@ export class GameObject implements IGameObjectData item.ele('InvType', inv.inventoryType); // For wearables, OpenSim expects flags to include the wearable type - if (inv.inventoryType === InventoryType.Wearable && !inv.assetID.equals(UUID.zero())) + if (inv.inventoryType === InventoryType.Wearable && !inv.assetID.isZero()) { try { @@ -1133,9 +1474,24 @@ export class GameObject implements IGameObjectData } } - private async getXML(xml: XMLNode, rootPrim: GameObject, linkNum: number) + private async resolve(objectsToResolve: GameObject[]) { - const sceneObjectPart = xml.ele('SceneObjectPart').att('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance').att('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); + this.populateChildren(); + return this.region.resolver.resolveObjects(objectsToResolve, true, false); + } + + private async getXML(xml: XMLNode, rootPrim: GameObject, linkNum: number, rootNode?: string) + { + if (this.resolvedAt === undefined || this.resolvedAt === 0 || this.resolvedInventory === false) + { + await this.region.resolver.resolveObjects([this], true, false, false); + } + let root = xml; + if (rootNode) + { + root = xml.ele(rootNode); + } + const sceneObjectPart = root.ele('SceneObjectPart').att('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance').att('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); sceneObjectPart.ele('AllowedDrop', (this.Flags !== undefined && (this.Flags & PrimFlags.AllowInventoryDrop) !== 0) ? 'true' : 'false'); UUID.getXML(sceneObjectPart.ele('CreatorID'), this.creatorID); UUID.getXML(sceneObjectPart.ele('FolderID'), this.folderID); @@ -1200,7 +1556,17 @@ export class GameObject implements IGameObjectData shape.ele('ProfileBegin', Utils.packBeginCut(Utils.numberOrZero(this.ProfileBegin))); shape.ele('ProfileEnd', Utils.packEndCut(Utils.numberOrZero(this.ProfileEnd))); shape.ele('ProfileHollow', Utils.packProfileHollow(Utils.numberOrZero(this.ProfileHollow))); - shape.ele('State', this.State); + + // This is wrong, but opensim expects it + const mask = 0xf << 4 >>> 0; + if (this.State === undefined) + { + this.State = 0; + } + let state = (((this.State & mask) >>> 4) | ((this.State & ~mask) << 4)) >>> 0; + state = state | this.attachmentPoint; + shape.ele('State', state); + shape.ele('LastAttachPoint', 0); if (this.ProfileCurve) { @@ -1265,6 +1631,10 @@ export class GameObject implements IGameObjectData sceneObjectPart.ele('ParentID', this.ParentID); sceneObjectPart.ele('CreationDate', Math.round((new Date()).getTime() / 1000)); sceneObjectPart.ele('Category', this.category); + if (this.IsAttachment) + { + Vector3.getXML(sceneObjectPart.ele('AttachPos'), this.Position); + } sceneObjectPart.ele('SalePrice', this.salePrice); sceneObjectPart.ele('ObjectSaleType', this.saleType); sceneObjectPart.ele('OwnershipCost', this.ownershipCost); @@ -1306,7 +1676,7 @@ export class GameObject implements IGameObjectData { sceneObjectPart.ele('PhysicsShapeType', this.physicsShapeType); } - if (this.Sound && !this.Sound.equals(UUID.zero())) + if (this.Sound && !this.Sound.isZero()) { UUID.getXML(sceneObjectPart.ele('SoundID'), this.Sound); sceneObjectPart.ele('SoundGain', this.SoundGain); @@ -1324,20 +1694,30 @@ export class GameObject implements IGameObjectData } } - async exportXML(): Promise + async populateChildren() + { + this.region.objects.populateChildren(this); + } + + async exportXMLElement(rootNode?: string): Promise { const document = builder.create('SceneObjectGroup'); let linkNum = 1; - await this.getXML(document, this, linkNum); + await this.getXML(document, this, linkNum, rootNode); if (this.children && this.children.length > 0) { const otherParts = document.ele('OtherParts'); for (const child of this.children) { - await child.getXML(otherParts, this, ++linkNum); + await child.getXML(otherParts, this, ++linkNum, (rootNode !== undefined) ? 'Part' : undefined); } } - return document.end({pretty: true, allowEmpty: true}); + return document; + } + + async exportXML(rootNode?: string): Promise + { + return (await this.exportXMLElement(rootNode)).end({pretty: true, allowEmpty: true}); } public toJSON(): IGameObjectData @@ -1368,6 +1748,7 @@ export class GameObject implements IGameObjectData touchName: this.touchName, sitName: this.sitName, resolvedAt: this.resolvedAt, + resolvedInventory: this.resolvedInventory, totalChildren: this.totalChildren, landImpact: this.landImpact, calculatedLandImpact: this.calculatedLandImpact, @@ -1525,134 +1906,92 @@ export class GameObject implements IGameObjectData } } - private async deRezObject(destination: DeRezDestination, transactionID: UUID, destFolder: UUID): Promise + async deRezObject(destination: DeRezDestination, transactionID: UUID, destFolder: UUID): Promise { - const msg = new DeRezObjectMessage(); + return GameObject.deRezObjects(this.region, [this], destination, transactionID, destFolder); + } + async takeToInventory(folder?: InventoryFolder): Promise + { + return GameObject.takeManyToInventory(this.region, [this], folder); + } + + async dropInventoryIntoContents(inventoryItem: InventoryItem | UUID): Promise + { + const transactionID = UUID.zero(); + + if (inventoryItem instanceof UUID) + { + + const item: InventoryItem | null = await this.region.agent.inventory.fetchInventoryItem(inventoryItem); + if (item === null) + { + throw new Error('Failed to drop inventory into object contents - Inventory item ' + inventoryItem.toString() + ' not found'); + return; + } + inventoryItem = item; + } + + const msg = new UpdateTaskInventoryMessage(); msg.AgentData = { AgentID: this.region.agent.agentID, SessionID: this.region.circuit.sessionID }; - msg.AgentBlock = { - GroupID: UUID.zero(), - Destination: destination, - DestinationID: destFolder, - TransactionID: transactionID, - PacketCount: 1, - PacketNumber: 1 + msg.UpdateData = { + Key: 0, + LocalID: this.ID }; - msg.ObjectData = [{ - ObjectLocalID: this.ID - }]; - const ack = this.region.circuit.sendMessage(msg, PacketFlags.Reliable); - return this.region.circuit.waitForAck(ack, 10000); + msg.InventoryData = { + ItemID: inventoryItem.itemID, + FolderID: inventoryItem.parentID, + CreatorID: inventoryItem.permissions.creator, + OwnerID: inventoryItem.permissions.owner, + GroupID: inventoryItem.permissions.group, + BaseMask: inventoryItem.permissions.baseMask, + OwnerMask: inventoryItem.permissions.ownerMask, + GroupMask: inventoryItem.permissions.groupMask, + EveryoneMask: inventoryItem.permissions.everyoneMask, + NextOwnerMask: inventoryItem.permissions.nextOwnerMask, + GroupOwned: inventoryItem.permissions.groupOwned || false, + TransactionID: transactionID, + Type: inventoryItem.type, + InvType: inventoryItem.inventoryType, + Flags: inventoryItem.flags, + SaleType: inventoryItem.saleType, + SalePrice: inventoryItem.salePrice, + Name: Utils.StringToBuffer(inventoryItem.name), + Description: Utils.StringToBuffer(inventoryItem.description), + CreationDate: inventoryItem.created.getTime() / 1000, + CRC: inventoryItem.getCRC() + }; + const serial = this.inventorySerial; + this.region.circuit.sendMessage(msg, PacketFlags.Reliable); + return this.waitForInventoryUpdate(serial); } - takeToInventory(): Promise + async waitForInventoryUpdate(inventorySerial?: number): Promise { - const transactionID = UUID.random(); - const rootFolder = this.region.agent.inventory.getRootFolderMain(); - return new Promise((resolve, reject) => + // We need to select the object or we won't get the objectProperties message + this.select(); + await this.region.circuit.waitForMessage(Message.ObjectProperties, 10000, (message: ObjectPropertiesMessage) => { - - this.region.circuit.waitForMessage(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) => + for (const obj of message.ObjectData) { - if (Utils.BufferToStringSimple(message.InventoryData[0].Name, 0) === this.name) + if (obj.ObjectID.equals(this.FullID)) { - return FilterResponse.Finish; - } - else - { - return FilterResponse.NoMatch; - } - }).then((createInventoryMsg: UpdateCreateInventoryItemMessage) => - { - resolve(createInventoryMsg.InventoryData[0].ItemID); - }).catch(() => - { - reject(new Error('Timed out waiting for UpdateCreateInventoryItem')); - }); - this.deRezObject(DeRezDestination.AgentInventoryTake, transactionID, rootFolder.folderID).then(() => {}).catch((err) => - { - console.error(err); - }); - }); - } - - dropInventoryIntoContents(inventoryID: UUID): Promise - { - return new Promise(async (resolve, reject) => - { - const transactionID = UUID.random(); - const item: InventoryItem | null = await this.region.agent.inventory.fetchInventoryItem(inventoryID); - if (item === null) - { - reject(new Error('Failed to drop inventory into object contents - Inventory item ' + inventoryID.toString() + ' not found')); - return; - } - - const msg = new UpdateTaskInventoryMessage(); - msg.AgentData = { - AgentID: this.region.agent.agentID, - SessionID: this.region.circuit.sessionID - }; - msg.UpdateData = { - Key: 0, - LocalID: this.ID - }; - msg.InventoryData = { - ItemID: item.itemID, - FolderID: item.parentID, - CreatorID: item.permissions.creator, - OwnerID: item.permissions.owner, - GroupID: item.permissions.group, - BaseMask: item.permissions.baseMask, - OwnerMask: item.permissions.ownerMask, - GroupMask: item.permissions.groupMask, - EveryoneMask: item.permissions.everyoneMask, - NextOwnerMask: item.permissions.nextOwnerMask, - GroupOwned: item.permissions.groupOwned || false, - TransactionID: transactionID, - Type: item.type, - InvType: item.inventoryType, - Flags: item.flags, - SaleType: item.saleType, - SalePrice: item.salePrice, - Name: Utils.StringToBuffer(item.name), - Description: Utils.StringToBuffer(item.description), - CreationDate: item.created.getTime() / 1000, - CRC: item.getCRC() - }; - - const inventorySerial = this.inventorySerial; - this.region.circuit.waitForMessage(Message.ObjectProperties, 10000, (message: ObjectPropertiesMessage) => - { - const n = 5; - for (const obj of message.ObjectData) - { - if (obj.ObjectID.equals(this.FullID)) + if (inventorySerial === undefined) { - if (obj.InventorySerial > inventorySerial) - { - return FilterResponse.Finish; - } + inventorySerial = this.inventorySerial; + } + if (obj.InventorySerial > inventorySerial) + { + return FilterResponse.Finish; } } - return FilterResponse.NoMatch; - }).then((message: ObjectPropertiesMessage) => - { - this.deselect().then(() => {}).catch(() => {}); - resolve(); - }).catch(() => - { - reject(new Error('Timed out waiting for task inventory drop')); - }); - - // We need to select the object or we won't get the objectProperties message - await this.select(); - - this.region.circuit.sendMessage(msg, PacketFlags.Reliable) + } + return FilterResponse.NoMatch; }); + await this.deselect(); } async select() @@ -1680,6 +2019,6 @@ export class GameObject implements IGameObjectData ObjectLocalID: this.ID }]; const ack = this.region.circuit.sendMessage(deselectObject, PacketFlags.Reliable); - await this.region.circuit.waitForAck(ack, 10000); + return this.region.circuit.waitForAck(ack, 10000); } } diff --git a/lib/classes/public/LLMesh.ts b/lib/classes/public/LLMesh.ts index 67e2c4a..b3fb51b 100644 --- a/lib/classes/public/LLMesh.ts +++ b/lib/classes/public/LLMesh.ts @@ -1,4 +1,3 @@ -import * as zlib from 'zlib'; import * as LLSD from '@caspertech/llsd'; import { UUID } from '../UUID'; import { LLSubMesh } from './interfaces/LLSubMesh'; @@ -7,6 +6,7 @@ import { Vector2 } from '../Vector2'; import { LLSkin } from './interfaces/LLSkin'; import { mat4 } from '../../tsm/mat4'; import { LLPhysicsConvex } from './interfaces/LLPhysicsConvex'; +import { Utils } from '../Utils'; export class LLMesh { @@ -55,7 +55,8 @@ export class LLMesh const bufFrom = startPos + parseInt(o['offset'], 10); const bufTo = startPos + parseInt(o['offset'], 10) + parseInt(o['size'], 10); const partBuf = buf.slice(bufFrom, bufTo); - const deflated = await this.inflate(partBuf); + + const deflated = await Utils.inflate(partBuf); const mesh = LLSD.LLSD.parseBinary(new LLSD.Binary(Array.from(deflated), 'BASE64')); if (mesh['result'] === undefined) @@ -274,7 +275,7 @@ export class LLMesh { throw new Error('TriangleList is required'); } - const indexBuf = new Buffer(submesh['TriangleList'].toArray()); + const indexBuf = Buffer.from(submesh['TriangleList'].toArray()); decoded.triangleList = []; for (let pos = 0; pos < indexBuf.length; pos = pos + 2) { @@ -287,7 +288,7 @@ export class LLMesh } if (submesh['Weights']) { - const skinBuf = new Buffer(submesh['Weights'].toArray()); + const skinBuf = Buffer.from(submesh['Weights'].toArray()); decoded.weights = []; let pos = 0; while (pos < skinBuf.length) @@ -318,7 +319,7 @@ export class LLMesh static decodeByteDomain3(posArray: number[], minDomain: Vector3, maxDomain: Vector3): Vector3[] { const result: Vector3[] = []; - const buf = new Buffer(posArray); + const buf = Buffer.from(posArray); for (let idx = 0; idx < posArray.length; idx = idx + 6) { const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x); @@ -331,7 +332,7 @@ export class LLMesh static decodeByteDomain2(posArray: number[], minDomain: Vector2, maxDomain: Vector2): Vector2[] { const result: Vector2[] = []; - const buf = new Buffer(posArray); + const buf = Buffer.from(posArray); for (let idx = 0; idx < posArray.length; idx = idx + 4) { const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x); @@ -344,40 +345,6 @@ export class LLMesh { return ((value / 65535) * (max - min)) + min; } - static inflate(buf: Buffer): Promise - { - return new Promise((resolve, reject) => - { - zlib.inflate(buf, (error: (Error| null), result: Buffer) => - { - if (error) - { - reject(error) - } - else - { - resolve(result); - } - }) - }); - } - static deflate(buf: Buffer): Promise - { - return new Promise((resolve, reject) => - { - zlib.deflate(buf, { level: 9}, (error: (Error| null), result: Buffer) => - { - if (error) - { - reject(error) - } - else - { - resolve(result); - } - }) - }); - } private encodeSubMesh(mesh: LLSubMesh) { const data: { @@ -507,7 +474,7 @@ export class LLMesh smList.push(this.encodeSubMesh(sub)) } const mesh = LLSD.LLSD.formatBinary(smList); - return await LLMesh.deflate(Buffer.from(mesh.toArray())); + return await Utils.deflate(Buffer.from(mesh.toArray())); } private async encodePhysicsConvex(conv: LLPhysicsConvex): Promise { @@ -556,7 +523,7 @@ export class LLMesh llsd.BoundingVerts = new LLSD.Binary(Array.from(buf)); } const mesh = LLSD.LLSD.formatBinary(llsd); - return await LLMesh.deflate(Buffer.from(mesh.toArray())); + return await Utils.deflate(Buffer.from(mesh.toArray())); } private async encodeSkin(skin: LLSkin): Promise { @@ -581,7 +548,7 @@ export class LLMesh llsd['pelvis_offset'] = skin.pelvisOffset.toArray(); } const mesh = LLSD.LLSD.formatBinary(llsd); - return await LLMesh.deflate(Buffer.from(mesh.toArray())); + return await Utils.deflate(Buffer.from(mesh.toArray())); } async toAsset(): Promise { diff --git a/lib/classes/public/Material.ts b/lib/classes/public/Material.ts index 1e58114..6963aee 100644 --- a/lib/classes/public/Material.ts +++ b/lib/classes/public/Material.ts @@ -1,5 +1,7 @@ import { UUID } from '../UUID'; import { Color4 } from '../Color4'; +import * as LLSD from '@caspertech/llsd'; +import { Utils } from '../Utils'; export class Material { @@ -20,5 +22,131 @@ export class Material specRepeatX: number; specRepeatY: number; specRotation: number; - llsd: string; + + static fromLLSD(llsd: string): Material + { + const parsed = LLSD.LLSD.parseXML(llsd); + return this.fromLLSDObject(parsed); + } + static fromLLSDObject(parsed: any): Material + { + const material = new Material(); + if (parsed['AlphaMaskCutoff'] !== undefined) + { + material.alphaMaskCutoff = parsed['AlphaMaskCutoff']; + } + if (parsed['DiffuseAlphaMode'] !== undefined) + { + material.diffuseAlphaMode = parsed['DiffuseAlphaMode']; + } + if (parsed['EnvIntensity'] !== undefined) + { + material.envIntensity = parsed['EnvIntensity']; + } + if (parsed['NormMap'] !== undefined) + { + material.normMap = new UUID(parsed['NormMap'].toString()) + } + if (parsed['NormOffsetX'] !== undefined) + { + material.normOffsetX = parsed['NormOffsetX']; + } + if (parsed['NormOffsetY'] !== undefined) + { + material.normOffsetY = parsed['NormOffsetY']; + } + if (parsed['NormRepeatX'] !== undefined) + { + material.normRepeatX = parsed['NormRepeatX']; + } + if (parsed['NormRepeatY'] !== undefined) + { + material.normRepeatY = parsed['NormRepeatY']; + } + if (parsed['NormRotation'] !== undefined) + { + material.normRotation = parsed['NormRotation']; + } + if (parsed['SpecColor'] !== undefined && Array.isArray(parsed['SpecColor']) && parsed['SpecColor'].length > 3) + { + material.specColor = new Color4([ + parsed['SpecColor'][0], + parsed['SpecColor'][1], + parsed['SpecColor'][2], + parsed['SpecColor'][3] + ]); + } + if (parsed['SpecExp'] !== undefined) + { + material.specExp = parsed['SpecExp']; + } + if (parsed['SpecMap'] !== undefined) + { + material.specMap = new UUID(parsed['SpecMap'].toString()) + } + if (parsed['SpecOffsetX'] !== undefined) + { + material.specOffsetX = parsed['SpecOffsetX']; + } + if (parsed['SpecOffsetY'] !== undefined) + { + material.specOffsetY = parsed['SpecOffsetY']; + } + if (parsed['SpecRepeatX'] !== undefined) + { + material.specRepeatX = parsed['SpecRepeatX']; + } + if (parsed['SpecRepeatY'] !== undefined) + { + material.specRepeatY = parsed['SpecRepeatY']; + } + if (parsed['SpecRotation'] !== undefined) + { + material.specRotation = parsed['SpecRotation']; + } + return material; + } + + toLLSDObject(): any + { + return { + 'AlphaMaskCutoff': this.alphaMaskCutoff, + 'DiffuseAlphaMode': this.diffuseAlphaMode, + 'EnvIntensity': this.envIntensity, + 'NormMap': new LLSD.UUID(this.normMap.toString()), + 'NormOffsetX': this.normOffsetX, + 'NormOffsetY': this.normOffsetY, + 'NormRepeatX': this.normRepeatX, + 'NormRepeatY': this.normRepeatY, + 'NormRotation': this.normRotation, + 'SpecColor': [ + this.specColor.getRed(), + this.specColor.getGreen(), + this.specColor.getBlue(), + this.specColor.getAlpha() + ], + 'SpecExp': this.specExp, + 'SpecMap': new LLSD.UUID(this.specMap.toString()), + 'SpecOffsetX': this.specOffsetX, + 'SpecOffsetY': this.specOffsetY, + 'SpecRepeatX': this.specRepeatX, + 'SpecRepeatY': this.specRepeatY, + 'SpecRotation': this.specRotation, + }; + } + + toLLSD(): string + { + return LLSD.LLSD.formatXML(this.toLLSDObject()); + } + + async toAsset(uuid: UUID): Promise + { + const asset = { + 'ID': new LLSD.UUID(uuid.toString()), + 'Material': this.toLLSD() + }; + const binary = LLSD.LLSD.formatBinary(asset); + return await Utils.deflate(Buffer.from(binary.toArray())); + } } diff --git a/lib/classes/public/Parcel.ts b/lib/classes/public/Parcel.ts index eb08fbe..ff1cf3e 100644 --- a/lib/classes/public/Parcel.ts +++ b/lib/classes/public/Parcel.ts @@ -2,6 +2,7 @@ import { Vector3 } from '../Vector3'; import { UUID } from '../UUID'; import * as builder from 'xmlbuilder'; import { ParcelFlags } from '../../enums/ParcelFlags'; +import { Region } from '../Region'; export class Parcel { @@ -73,6 +74,28 @@ export class Parcel RegionAllowAccessOverride: boolean; + constructor(private region: Region) + { + + } + + canIRez(): boolean + { + if (this.ParcelFlags & ParcelFlags.CreateObjects) + { + return true; + } + if (this.region.agent.activeGroupID.equals(this.OwnerID) && this.ParcelFlags & ParcelFlags.CreateGroupObjects) + { + return true; + } + if (this.OwnerID.equals(this.region.agent.agentID)) + { + return true; + } + return false; + } + exportXML(): string { const document = builder.create('LandData'); diff --git a/lib/enums/AttachmentPoint.ts b/lib/enums/AttachmentPoint.ts index 3341129..dee09a0 100644 --- a/lib/enums/AttachmentPoint.ts +++ b/lib/enums/AttachmentPoint.ts @@ -1,44 +1,64 @@ export enum AttachmentPoint { Default = 0, - Chest = 1, - Skull, - LeftShoulder, - RightShoulder, - LeftHand, - RightHand, - LeftFoot, - RightFoot, - Spine, - Pelvis, - Mouth, - Chin, - LeftEar, - RightEar, - LeftEyeball, - RightEyeball, - Nose, - RightUpperArm, - RightForearm, - LeftUpperArm, - LeftForearm, - RightHip, - RightUpperLeg, - RightLowerLeg, - LeftHip, - LeftUpperLeg, - LeftLowerLeg, - Stomach, - LeftPec, - RightPec, - HUDCenter2, - HUDTopRight, - HUDTop, - HUDTopLeft, - HUDCenter, - HUDBottomLeft, - HUDBottom, - HUDBottomRight, - Neck, - Root + Chest = 1, // FS: Chest + Skull, // FS: Skull + LeftShoulder, // FS: Left Shoulder + RightShoulder, // FS: Right Shoulder + LeftHand, // FS: Left Hand + RightHand, // FS: Right Hand + LeftFoot, // FS: Left Foot + RightFoot, // FS: Right Root + Spine, // FS: Spine + Pelvis, // FS: Pelvis + Mouth, // FS: Mouth + Chin, // FS: Chin + LeftEar, // FS: Left Ear + RightEar, // FS: Right Ear + LeftEyeball, // FS: Left Eyeball + RightEyeball, // FS: Right Eyeball + Nose, // FS: Nose + RightUpperArm, // FS: R Upper Arm + RightForearm, // FS: R Forearm + LeftUpperArm, // FS: L Upper Arm + LeftForearm, // FS: L Forearm + RightHip, // FS: Right Hip + RightUpperLeg, // FS: R Upper Leg + RightLowerLeg, // FS: R Lower Leg + LeftHip, // FS: Left Hip + LeftUpperLeg, // FS: L Upper Leg + LeftLowerLeg, // FS: L Lower Leg + Stomach, // FS: Stomach + LeftPec, // FS: Left Pec + RightPec, // FS: Right Pec + + // Private/HUD attachments + HUDCenter2, // FS: Center 2 + HUDTopRight, // FS: Top Right + HUDTop, // FS: Top + HUDTopLeft, // FS: Top Left + HUDCenter, // FS: Center + HUDBottomLeft, // FS: Bottom Left + HUDBottom, // FS: Bottom + HUDBottomRight, // FS: Bottom Right + + // Extras + Neck, // FS: Neck + Root = 40, // FS: Avatar Center + Center = 40, + LefHandRing1 = 41, // FS: Left Ring Finger + RightHandRing1 = 42, // FS: Right Ring Finger + TailBase = 43, // FS: Tail Base + TailTip = 44, // FS: Tail Tip + LeftWing = 45, // FS: Left Wing + RightWing = 46, // FS: Right Wing + FaceJaw = 47, // FS: Jaw + FaceLeftEar = 48, // FS: Alt Left Ear + FaceRightEar = 49, // FS: Alt Right Ear + FaceLeftEye = 50, // FS: Alt Left Eye + FaceRightEye = 51, // FS: Alt Right Eye + FaceTongue = 52, // FS: Tongue + Groin = 53, // FS: Groin + HindLeftFoot = 54, // FS: Left Hind Foot + HindRightFoot = 55 // FS: Right Hind Foot } diff --git a/lib/enums/InventoryType.ts b/lib/enums/InventoryType.ts index 9e37f4f..23bd30b 100644 --- a/lib/enums/InventoryType.ts +++ b/lib/enums/InventoryType.ts @@ -23,5 +23,5 @@ export enum InventoryType Wearable = 18, Animation = 19, Gesture = 20, - Mesh = 22, + Mesh = 22 } diff --git a/lib/enums/InventoryTypeLL.ts b/lib/enums/InventoryTypeLL.ts deleted file mode 100644 index 353b066..0000000 --- a/lib/enums/InventoryTypeLL.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum InventoryTypeLL -{ - texture = 0, - sound = 1, - callcard = 2, - landmark = 3, - object = 6, - notecard = 7, - category = 8, - root = 9, - script = 10, - snapshot = 15, - attach = 17, - wearable = 18, - animation = 19, - gesture = 20, - mesh = 22 -} \ No newline at end of file diff --git a/lib/enums/LLGestureAnimationFlags.ts b/lib/enums/LLGestureAnimationFlags.ts new file mode 100644 index 0000000..127e03a --- /dev/null +++ b/lib/enums/LLGestureAnimationFlags.ts @@ -0,0 +1,5 @@ +export enum LLGestureAnimationFlags +{ + None = 0, + Stop = 1 +} diff --git a/lib/enums/LLGestureChatFlags.ts b/lib/enums/LLGestureChatFlags.ts new file mode 100644 index 0000000..6c5e072 --- /dev/null +++ b/lib/enums/LLGestureChatFlags.ts @@ -0,0 +1,4 @@ +export enum LLGestureChatFlags +{ + None = 0 +} diff --git a/lib/enums/LLGestureSoundFlags.ts b/lib/enums/LLGestureSoundFlags.ts new file mode 100644 index 0000000..5bfd1a3 --- /dev/null +++ b/lib/enums/LLGestureSoundFlags.ts @@ -0,0 +1,4 @@ +export enum LLGestureSoundFlags +{ + None = 0 +} diff --git a/lib/enums/LLGestureStepType.ts b/lib/enums/LLGestureStepType.ts new file mode 100644 index 0000000..f8505ff --- /dev/null +++ b/lib/enums/LLGestureStepType.ts @@ -0,0 +1,7 @@ +export enum LLGestureStepType +{ + Animation = 0, + Sound = 1, + Chat = 2, + Wait = 3 +} diff --git a/lib/enums/LLGestureWaitFlags.ts b/lib/enums/LLGestureWaitFlags.ts new file mode 100644 index 0000000..83dbc76 --- /dev/null +++ b/lib/enums/LLGestureWaitFlags.ts @@ -0,0 +1,6 @@ +export enum LLGestureWaitFlags +{ + None = 0, + Time = 1, + AllAnim = 2 +} diff --git a/lib/enums/ParcelFlags.ts b/lib/enums/ParcelFlags.ts index 7fc9d5c..c6fda5c 100644 --- a/lib/enums/ParcelFlags.ts +++ b/lib/enums/ParcelFlags.ts @@ -32,5 +32,5 @@ export enum ParcelFlags AllowGroupObjectEntry = 1 << 28, AllowVoiceChat = 1 << 29, UseEstateVoiceChan = 1 << 30, - DenyAgeUnverified = 1 << 31 -} \ No newline at end of file + DenyAgeUnverified = 2147483648 +}; diff --git a/lib/events/BulkUpdateInventoryEvent.ts b/lib/events/BulkUpdateInventoryEvent.ts new file mode 100644 index 0000000..62dabe2 --- /dev/null +++ b/lib/events/BulkUpdateInventoryEvent.ts @@ -0,0 +1,8 @@ +import { InventoryFolder } from '../classes/InventoryFolder'; +import { InventoryItem } from '../classes/InventoryItem'; + +export class BulkUpdateInventoryEvent +{ + folderData: InventoryFolder[] = []; + itemData: InventoryItem[] = []; +} diff --git a/lib/index.ts b/lib/index.ts index 30bc12d..b23abac 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -95,12 +95,20 @@ import { ExtraParams } from './classes/public/ExtraParams'; import { LLMesh } from './classes/public/LLMesh'; import { FolderType } from './enums/FolderType'; import { InventoryItem } from './classes/InventoryItem'; +import { InventoryType } from './enums/InventoryType'; +import { TarWriter } from './classes/TarWriter'; +import { TarReader } from './classes/TarReader'; +import { LLGesture } from './classes/LLGesture'; +import { LLGestureAnimationStep } from './classes/LLGestureAnimationStep'; +import { LLGestureSoundStep } from './classes/LLGestureSoundStep'; +import { LLGestureWaitStep } from './classes/LLGestureWaitStep'; +import { LLGestureChatStep } from './classes/LLGestureChatStep'; +import { LLGestureStepType } from './enums/LLGestureStepType'; +import { LLLindenText } from './classes/LLLindenText'; export { Bot, LoginParameters, - AssetType, - HTTPAssets, ClientEvents, BVH, ChatSourceType, @@ -110,9 +118,14 @@ export { Utils, TextureEntry, LLWearable, + LLLindenText, + LLGesture, + LLGestureAnimationStep, + LLGestureSoundStep, + LLGestureChatStep, + LLGestureWaitStep, ParticleSystem, ExtraParams, - FolderType, // Flags AgentFlags, @@ -133,22 +146,30 @@ export { RightsFlags, ParticleDataFlags, TextureFlags, + PrimFlags, + ParcelFlags, + SimAccessFlags, + TextureAnimFlags, + + // Enums + InventoryType, + AssetType, + HTTPAssets, + FolderType, + TransferStatus, SourcePattern, BlendFunc, PCode, - PrimFlags, Bumpiness, HoleType, LayerType, MappingType, - ParcelFlags, PhysicsShapeType, ProfileShape, SculptType, Shininess, - SimAccessFlags, - TextureAnimFlags, - TransferStatus, + LLGestureStepType, + // Events ChatEvent, @@ -195,6 +216,8 @@ export { MeshData, LLMesh, InventoryItem, + TarReader, + TarWriter, // Public Interfaces GlobalPosition, diff --git a/package-lock.json b/package-lock.json index 3d6253d..d636786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,21 @@ "xmldom": "^0.1.27" } }, + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "@types/braces": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@types/braces/-/braces-2.3.0.tgz", - "integrity": "sha512-A3MV5EsLHgShHoJ/XES/fQAnwNISKLrFuH9eNBZY5OkTQB7JPIwbRoExvRpDsNABvkMojnKqKWS8x0m2rLYi+A==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.0.tgz", + "integrity": "sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==", + "dev": true }, "@types/caseless": { "version": "0.12.2", @@ -34,9 +45,10 @@ "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" }, "@types/micromatch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-3.1.0.tgz", - "integrity": "sha512-06uA9V7v68RTOzA3ky1Oi0HmCPa+YJ050vM+sTECwkxnHUQnO17TAcNCGX400QT6bldUiPb7ux5oKy0j8ccEDw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.1.tgz", + "integrity": "sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==", + "dev": true, "requires": { "@types/braces": "*" } @@ -191,6 +203,11 @@ "version": "github:rxaviers/assertion#c4e07ce9a5cbda262df4cf14657fc0df94da13e6", "from": "github:rxaviers/assertion" }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -244,6 +261,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -253,6 +275,23 @@ "tweetnacl": "^0.14.3" } }, + "bl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -277,6 +316,15 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -295,51 +343,106 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "dependencies": { "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" } } } }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", "requires": { - "color-name": "1.1.3" + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } }, "combined-stream": { "version": "1.0.7", @@ -422,6 +525,19 @@ "safer-buffer": "^2.1.0" } }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -460,6 +576,16 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -468,6 +594,16 @@ "to-regex-range": "^5.0.1" } }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -493,12 +629,17 @@ } } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "requires": { - "minipass": "^2.2.1" + "minipass": "^3.0.0" } }, "fs.realpath": { @@ -580,6 +721,11 @@ "sshpk": "^1.7.0" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -593,19 +739,28 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ipaddr.js": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.1.tgz", "integrity": "sha1-+kt5+kf9Pe9eOxWYJRYcClGclCc=" }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -670,6 +825,30 @@ "verror": "1.10.0" } }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -682,12 +861,12 @@ "dev": true }, "micromatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.0.tgz", - "integrity": "sha512-THzpRAtp/NcyqnAzYwvP9V1bMAM4zFs2AR02wwxNLzEbi6Mn2suaQ6lhiD8Ug+X3L3g9grohOe1NGb2m+72eeA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.3" + "picomatch": "^2.0.5" } }, "mime-db": { @@ -718,27 +897,20 @@ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "yallist": "^4.0.0" } }, "minizlib": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", - "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "requires": { - "minipass": "^2.2.1" + "minipass": "^3.0.0", + "yallist": "^4.0.0" } }, "mkdirp": { @@ -749,6 +921,11 @@ "minimist": "0.0.8" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mocha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", @@ -788,11 +965,18 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -811,15 +995,29 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "picomatch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", - "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "psl": { "version": "1.1.29", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -845,6 +1043,16 @@ "quickselect": "^1.0.0" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -889,9 +1097,9 @@ } }, "rxjs": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", - "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "requires": { "tslib": "^1.9.0" } @@ -916,6 +1124,14 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -954,6 +1170,26 @@ "tweetnacl": "~0.14.0" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -973,26 +1209,45 @@ } }, "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "version": "git+https://github.com/CasperTech/node-tar.git#a483361a64c42c4fb83cce6c198aa895ee18e4b6", + "from": "git+https://github.com/CasperTech/node-tar.git", "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", + "chownr": "^1.1.3", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "yallist": "^4.0.0" } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz", + "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "tiny-async-pool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.0.1.tgz", @@ -1019,6 +1274,11 @@ "punycode": "^1.4.1" } }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "ts-node": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", @@ -1066,6 +1326,43 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.27.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } } }, "tslint-eslint-rules": { @@ -1125,6 +1422,11 @@ "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", "dev": true }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -1145,11 +1447,64 @@ "extsprintf": "^1.2.0" } }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "xml": { "version": "1.0.1", @@ -1199,9 +1554,9 @@ } }, "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yn": { "version": "2.0.0", diff --git a/package.json b/package.json index 0127f80..911e483 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "url": "git+https://github.com/CasperTech/node-metaverse.git" }, "devDependencies": { + "@types/micromatch": "^4.0.1", "mocha": "^5.2.0", "source-map-support": "^0.5.9", "ts-node": "^7.0.1", @@ -33,7 +34,6 @@ "dependencies": { "@caspertech/llsd": "^1.0.3", "@types/long": "^4.0.0", - "@types/micromatch": "^3.1.0", "@types/mocha": "^5.2.5", "@types/node": "^10.14.19", "@types/request": "^2.48.3", @@ -43,17 +43,23 @@ "@types/xml": "^1.0.2", "@types/xml2js": "^0.4.3", "@types/xmlrpc": "^1.3.5", + "chalk": "^3.0.0", + "flatted": "^2.0.1", "ipaddr.js": "^1.8.1", + "logform": "^2.1.2", "long": "^4.0.0", - "micromatch": "^4.0.0", + "micromatch": "^4.0.2", "moment": "^2.22.2", "rbush-3d": "0.0.4", "request": "^2.88.0", - "rxjs": "^6.3.3", - "tar": "^4.4.6", + "rxjs": "^6.6.3", + "tar": "git+https://github.com/CasperTech/node-tar.git", + "tar-fs": "^2.0.0", + "tar-stream": "^2.1.0", "tiny-async-pool": "^1.0.1", "uuid": "^3.3.2", "validator": "^10.8.0", + "winston": "^3.2.1", "xml": "^1.0.1", "xml2js": "^0.4.19", "xmlbuilder": "^13.0.2",