From 76b080757ba7bfe4d495455d3290fc65cecd21f5 Mon Sep 17 00:00:00 2001 From: Casper Warden <216465704+casperwardensl@users.noreply.github.com> Date: Thu, 15 Nov 2018 03:10:14 +0000 Subject: [PATCH] - Mesh upload support - LLMesh asset decoding and encoding (inc. LLPhysicsConvex, LLSkin, LLSubMesh) - Query inventory folder by type - onSelectedObject event - fetchInventoryItem command - Fix packing/unpacking of object shape - Time sync with SimulatorViewerTimeMessage - Changed several classes to a .from style rather than setting up in the constructor (exception friendly) - Whole bunch of other improvements - Object building --- example/testBot.js | 96 +-- lib/classes/Agent.ts | 4 +- lib/classes/Caps.ts | 75 +-- lib/classes/ClientEvents.ts | 2 + lib/classes/Color4.ts | 30 +- lib/classes/Inventory.ts | 80 ++- lib/classes/InventoryFolder.ts | 40 +- lib/classes/InventoryItem.ts | 30 +- lib/classes/ObjectStoreFull.ts | 70 +- lib/classes/ObjectStoreLite.ts | 117 ++++ lib/classes/Quaternion.ts | 40 +- lib/classes/Region.ts | 15 +- lib/classes/TextureEntry.ts | 351 ++++++++-- lib/classes/TextureEntryFace.ts | 227 ++++++- lib/classes/UUID.ts | 13 + lib/classes/Utils.ts | 247 ++++++- lib/classes/Vector2.ts | 4 + lib/classes/Vector3.ts | 27 +- lib/classes/commands/AssetCommands.ts | 123 +++- lib/classes/commands/FriendCommands.ts | 16 +- lib/classes/commands/InventoryCommands.ts | 8 +- lib/classes/commands/RegionCommands.ts | 291 +++++++- lib/classes/public/FlexibleData.ts | 6 + lib/classes/public/GameObject.ts | 613 ++++++++++++++--- lib/classes/public/LLMesh.ts | 630 ++++++++++++++++++ lib/classes/public/LightData.ts | 6 + lib/classes/public/LightImageData.ts | 6 + lib/classes/public/MeshData.ts | 6 + lib/classes/public/SculptData.ts | 6 + .../public/interfaces/LLPhysicsConvex.ts | 12 + lib/classes/public/interfaces/LLSkin.ts | 10 + lib/classes/public/interfaces/LLSubMesh.ts | 21 + lib/enums/FolderType.ts | 32 + lib/enums/UpdateType.ts | 9 + lib/events/SelectedObjectEvent.ts | 6 + lib/index.ts | 6 +- lib/tsm/mat4.ts | 4 + 37 files changed, 2864 insertions(+), 415 deletions(-) create mode 100644 lib/classes/public/LLMesh.ts create mode 100644 lib/classes/public/interfaces/LLPhysicsConvex.ts create mode 100644 lib/classes/public/interfaces/LLSkin.ts create mode 100644 lib/classes/public/interfaces/LLSubMesh.ts create mode 100644 lib/enums/FolderType.ts create mode 100644 lib/enums/UpdateType.ts create mode 100644 lib/events/SelectedObjectEvent.ts diff --git a/example/testBot.js b/example/testBot.js index 047251d..6491222 100644 --- a/example/testBot.js +++ b/example/testBot.js @@ -299,101 +299,7 @@ async function connect() new nmv.Vector3([-1.0, 0, 0]), new nmv.Vector3([0.0, 1.0, 0])); - let foundObjects = {}; - - const getAllChildren = function(obj) - { - let children = obj.children; - let newChildren = [obj] - ; - for (const child of children) - { - newChildren = newChildren.concat(getAllChildren(child)); - } - return children.concat(newChildren); - }; - - const scan = setInterval(async () => { - const count = bot.clientCommands.region.countObjects(); - if (count > lastObjects) - { - console.log(count + ' objects found.'); - lastObjects = count; - } - else - { - if (height > 4096) - { - if (Object.keys(foundObjects).length > lastObjects) - { - const index = {}; - console.log('Indexing found objects'); - for(const o of foundObjects) - { - index[o.FullID.toString()] = true; - } - console.log('Searching for missing objects'); - for(const k of Object.keys(foundObjects)) - { - if (index[k] === undefined) - { - console.log('Object missing: ' + k); - } - } - } - console.log('Finished scanning region! ' + lastObjects + ' objects found. Resolving all objects..'); - clearInterval(scan); - // Query all objects in the region - var tmr = new Date().getTime(); - const objs = await bot.clientCommands.region.getAllObjects(true); - var tmr2 = new Date().getTime(); - - let totalObjects = 0; - let totalLandImpact = 0; - - for (const obj of objs) - { - totalObjects += (1 + obj.totalChildren); - if (obj.landImpact) - { - totalLandImpact += obj.landImpact; - } - } - - console.log('Found ' + objs.length + ' linksets with ' + totalObjects + ' objects in ' + (tmr2 - tmr) + 'ms. Land impact: ' + totalLandImpact); - - let searchResults = await bot.clientCommands.region.findObjectsByName('FINDME-*'); - console.log('Found ' + searchResults.length + ' objects containing the string FINDME-*'); - for (const obj of searchResults) - { - console.log('Object: ' + obj.name + ', ' + obj.FullID.toString() + ', position: ' + obj.Position.toString() + ', land impact: ' + obj.landImpact) - } - - searchResults = await bot.clientCommands.region.findObjectsByName('rezcubes'); - console.log('Found ' + searchResults.length + ' objects containing the string rezcubes'); - for (const obj of searchResults) - { - console.log('Object: ' + obj.name + ', ' + obj.FullID.toString() + ', position: ' + obj.Position.toString() + ', land impact: ' + obj.landImpact) - for (const k of Object.keys(obj.NameValue)) - { - console.log(k + ': ' + obj.NameValue[k]) - } - } - } - else - { - console.log('Moving to ' + height); - height += 128; - bot.clientCommands.agent.setCamera( - new nmv.Vector3([128, 128, height]), - new nmv.Vector3([128, 128, 0]), - 5000, - new nmv.Vector3([-1.0, 0, 0]), - new nmv.Vector3([0.0, 1.0, 0])); - } - } - }, 5000); - + //await bot.clientCommands.friends.grantFriendRights('d1cd5b71-6209-4595-9bf0-771bf689ce00', nmv.RightsFlags.CanModifyObjects | nmv.RightsFlags.CanSeeOnline | nmv.RightsFlags.CanSeeOnMap ); } diff --git a/lib/classes/Agent.ts b/lib/classes/Agent.ts index d215344..08218e6 100644 --- a/lib/classes/Agent.ts +++ b/lib/classes/Agent.ts @@ -20,7 +20,7 @@ import {AttachmentPoint} from '../enums/AttachmentPoint'; import {Utils} from './Utils'; import {ClientEvents} from './ClientEvents'; import Timer = NodeJS.Timer; -import {ControlFlags, GroupChatSessionAgentListEvent, AgentFlags, PacketFlags, AssetType} from '..'; +import {ControlFlags, GroupChatSessionAgentListEvent, AgentFlags, PacketFlags, AssetType, FolderType} from '..'; import {GameObject} from './public/GameObject'; export class Agent @@ -245,7 +245,7 @@ export class Agent Object.keys(this.inventory.main.skeleton).forEach((uuid) => { const folder = this.inventory.main.skeleton[uuid]; - if (folder.typeDefault === AssetType.CurrentOutfitFolder) + if (folder.typeDefault === FolderType.CurrentOutfit) { const folderID = folder.folderID; diff --git a/lib/classes/Caps.ts b/lib/classes/Caps.ts index 394eac3..cbf51e5 100644 --- a/lib/classes/Caps.ts +++ b/lib/classes/Caps.ts @@ -322,51 +322,48 @@ export class Caps }); } - capsRequestXML(capability: string, data: any, debug = false): Promise + capsPerformXMLRequest(url: string, data: any): Promise + { + return new Promise(async (resolve, reject) => + { + const xml = LLSD.LLSD.formatXML(data); + this.request(url, xml, 'application/llsd+xml').then((body: string) => + { + let result: any = null; + try + { + result = LLSD.LLSD.parseXML(body); + } + catch (err) + { + console.error('Error parsing LLSD'); + console.error(body); + reject(err); + } + resolve(result); + }).catch((err) => + { + console.error(err); + reject(err); + }); + }); + } + + async capsRequestXML(capability: string, data: any, debug = false): Promise { if (debug) { console.log(data); } - return new Promise(async (resolve, reject) => + + const t = new Date().getTime(); + if (this.capRateLimitTimers[capability] && (this.capRateLimitTimers[capability] + Caps.CAP_INVOCATION_INTERVAL_MS) > t) { - const t = new Date().getTime(); - if (this.capRateLimitTimers[capability] && (this.capRateLimitTimers[capability] + Caps.CAP_INVOCATION_INTERVAL_MS) > t) - { - await this.waitForCapTimeout(capability); - } - this.capRateLimitTimers[capability] = t; - this.getCapability(capability).then((url) => - { - const xml = LLSD.LLSD.formatXML(data); - if (debug) - { - console.log(xml); - } - this.request(url, xml, 'application/llsd+xml').then((body: string) => - { - let result: any = null; - try - { - result = LLSD.LLSD.parseXML(body); - } - catch (err) - { - console.error('Error parsing LLSD'); - console.error(body); - reject(err); - } - resolve(result); - }).catch((err) => - { - console.error(err); - reject(err); - }); - }).catch((err) => - { - reject(err); - }); - }); + await this.waitForCapTimeout(capability); + } + this.capRateLimitTimers[capability] = t; + const url = await this.getCapability(capability); + return await this.capsPerformXMLRequest(url, data); } shutdown() diff --git a/lib/classes/ClientEvents.ts b/lib/classes/ClientEvents.ts index bb670ae..53c66f3 100644 --- a/lib/classes/ClientEvents.ts +++ b/lib/classes/ClientEvents.ts @@ -23,6 +23,7 @@ import {Subject} from 'rxjs/internal/Subject'; import {NewObjectEvent} from '../events/NewObjectEvent'; import {ObjectUpdatedEvent} from '../events/ObjectUpdatedEvent'; import {ObjectKilledEvent} from '../events/ObjectKilledEvent'; +import {SelectedObjectEvent} from '../events/SelectedObjectEvent'; export class ClientEvents @@ -50,4 +51,5 @@ export class ClientEvents onNewObjectEvent: Subject = new Subject(); onObjectUpdatedEvent: Subject = new Subject(); onObjectKilledEvent: Subject = new Subject(); + onSelectedObjectEvent: Subject = new Subject(); } diff --git a/lib/classes/Color4.ts b/lib/classes/Color4.ts index 8b244e8..2030167 100644 --- a/lib/classes/Color4.ts +++ b/lib/classes/Color4.ts @@ -139,11 +139,31 @@ export class Color4 return 0; } - writeToBuffer(buf: Buffer, pos: number) + writeToBuffer(buf: Buffer, pos: number, inverted: boolean = false) { - buf.writeUInt8(Utils.FloatToByte(this.getRed(), 0, 1.0), pos++); - buf.writeUInt8(Utils.FloatToByte(this.getGreen(), 0, 1.0), pos++); - buf.writeUInt8(Utils.FloatToByte(this.getBlue(), 0, 1.0), pos++); - buf.writeUInt8(Utils.FloatToByte(this.getAlpha(), 0, 1.0), pos); + buf.writeUInt8(Utils.FloatToByte(this.getRed(), 0, 1.0), pos); + buf.writeUInt8(Utils.FloatToByte(this.getGreen(), 0, 1.0), pos + 1); + buf.writeUInt8(Utils.FloatToByte(this.getBlue(), 0, 1.0), pos + 2); + buf.writeUInt8(Utils.FloatToByte(this.getAlpha(), 0, 1.0), pos + 3); + + if (inverted) + { + buf[pos] = (255 - buf[pos]); + buf[pos + 1] = (255 - buf[pos + 1]); + buf[pos + 2] = (255 - buf[pos + 2]); + buf[pos + 3] = (255 - buf[pos + 3]); + } + } + + getBuffer(inverted: boolean = false): Buffer + { + const buf = Buffer.allocUnsafe(4); + this.writeToBuffer(buf, 0, inverted); + return buf; + } + + equals(other: Color4): boolean + { + return (this.red === other.red && this.green === other.green && this.blue === other.blue && this.alpha === other.alpha); } } diff --git a/lib/classes/Inventory.ts b/lib/classes/Inventory.ts index d18e967..cb51d04 100644 --- a/lib/classes/Inventory.ts +++ b/lib/classes/Inventory.ts @@ -2,7 +2,9 @@ import {UUID} from './UUID'; import {ClientEvents} from './ClientEvents'; import {InventoryFolder} from './InventoryFolder'; import {Agent} from './Agent'; -import {AssetType} from '..'; +import {AssetType, FolderType} from '..'; +import * as LLSD from '@caspertech/llsd'; +import {InventoryItem} from './InventoryItem'; export class Inventory { @@ -19,6 +21,9 @@ export class Inventory } = { skeleton: {} }; + + itemsByID: {[key: string]: InventoryItem} = {}; + private clientEvents: ClientEvents; private agent: Agent; @@ -59,25 +64,66 @@ export class Inventory return new InventoryFolder(this.main, this.agent); } } - findFolderForType(type: AssetType): UUID + findFolderForType(type: FolderType): UUID { - if (this.main.root === undefined) + const root = this.main.skeleton; + for (const key of Object.keys(root)) { - return UUID.zero(); - } - if (type === AssetType.Folder) - { - return this.main.root; - } - let found = UUID.zero(); - Object.keys(this.main.skeleton).forEach((fUUID) => - { - const folder = this.main.skeleton[fUUID]; - if (folder.typeDefault === type) + const f = root[key]; + if (f.typeDefault === type) { - found = folder.folderID; + return f.folderID; } - }); - return found; + } + return this.getRootFolderMain().folderID; + } + async fetchInventoryItem(item: UUID): Promise + { + const params = { + 'agent_id': new LLSD.UUID(this.agent.agentID), + 'items': [ + { + 'item_id': new LLSD.UUID(item), + 'owner_id': new LLSD.UUID(this.agent.agentID) + } + ] + }; + const response = await this.agent.currentRegion.caps.capsRequestXML('FetchInventory2', params); + for (const receivedItem of response['items']) + { + const invItem = new InventoryItem(); + invItem.assetID = new UUID(receivedItem['asset_id'].toString()); + invItem.inventoryType = parseInt(receivedItem['inv_type'], 10); + invItem.type = parseInt(receivedItem['type'], 10); + invItem.itemID = item; + invItem.permissions = { + baseMask: parseInt(receivedItem['permissions']['base_mask'], 10), + nextOwnerMask: parseInt(receivedItem['permissions']['next_owner_mask'], 10), + groupMask: parseInt(receivedItem['permissions']['group_mask'], 10), + lastOwner: new UUID(receivedItem['permissions']['last_owner_id'].toString()), + owner: new UUID(receivedItem['permissions']['owner_id'].toString()), + creator: new UUID(receivedItem['permissions']['creator_id'].toString()), + group: new UUID(receivedItem['permissions']['group_id'].toString()), + ownerMask: parseInt(receivedItem['permissions']['owner_mask'], 10), + everyoneMask: parseInt(receivedItem['permissions']['everyone_mask'], 10), + }; + invItem.flags = parseInt(receivedItem['flags'], 10); + invItem.description = receivedItem['desc']; + invItem.name = receivedItem['name']; + invItem.created = new Date(receivedItem['created_at'] * 1000); + invItem.parentID = new UUID(receivedItem['parent_id'].toString()); + invItem.saleType = parseInt(receivedItem['sale_info']['sale_type'], 10); + invItem.salePrice = parseInt(receivedItem['sale_info']['sale_price'], 10); + if (this.main.skeleton[invItem.parentID.toString()]) + { + await this.main.skeleton[invItem.parentID.toString()].addItem(invItem); + } + else + { + throw new Error('FolderID of ' + invItem.parentID.toString() + ' not found!'); + } + return invItem; + } + return null; } } diff --git a/lib/classes/InventoryFolder.ts b/lib/classes/InventoryFolder.ts index 1d81d93..c500a2a 100644 --- a/lib/classes/InventoryFolder.ts +++ b/lib/classes/InventoryFolder.ts @@ -5,11 +5,12 @@ import * as path from 'path'; import * as LLSD from '@caspertech/llsd'; import {InventorySortOrder} from '../enums/InventorySortOrder'; import {Agent} from './Agent'; -import {AssetType} from '..'; +import {FolderType} from '..'; +import {Inventory} from './Inventory'; export class InventoryFolder { - typeDefault: AssetType; + typeDefault: FolderType; version: number; name: string; folderID: UUID; @@ -111,7 +112,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.items.push(item); + this.addItem(item); }); resolve(); } @@ -134,6 +135,37 @@ export class InventoryFolder }); } + async removeItem(itemID: UUID, save: boolean = false) + { + if (this.agent.inventory.itemsByID[itemID.toString()]) + { + delete this.agent.inventory.itemsByID[itemID.toString()]; + this.items = this.items.filter((item) => + { + console.log(item.itemID + ' vs ' + JSON.stringify(itemID)); + return !item.itemID.equals(itemID); + }) + } + if (save) + { + await this.saveCache(); + } + } + + async addItem(item: InventoryItem, save: boolean = false) + { + if (this.agent.inventory.itemsByID[item.itemID.toString()]) + { + await this.removeItem(item.itemID, false); + } + this.items.push(item); + this.agent.inventory.itemsByID[item.itemID.toString()] = item; + if (save) + { + await this.saveCache(); + } + } + populate() { return new Promise((resolve, reject) => @@ -186,7 +218,7 @@ export class InventoryFolder creator: new UUID(item['permissions']['creator_id'].toString()), group: new UUID(item['permissions']['group_id'].toString()) }; - this.items.push(invItem); + this.addItem(invItem); }); this.saveCache().then(() => { diff --git a/lib/classes/InventoryItem.ts b/lib/classes/InventoryItem.ts index e0a0a5c..5b47b47 100644 --- a/lib/classes/InventoryItem.ts +++ b/lib/classes/InventoryItem.ts @@ -5,7 +5,7 @@ import {AssetType, InventoryItemFlags} from '..'; export class InventoryItem { - assetID: UUID = UUID.zero();; + assetID: UUID = UUID.zero(); inventoryType: InventoryType; name: string; salePrice: number; @@ -39,4 +39,30 @@ export class InventoryItem group: UUID.zero(), groupOwned: false }; -} \ No newline at end of file + + getCRC(): number + { + let crc = 0; + crc += this.assetID.CRC(); + crc += this.parentID.CRC(); + crc += this.itemID.CRC(); + crc += this.permissions.creator.CRC(); + crc += this.permissions.owner.CRC(); + crc += this.permissions.group.CRC(); + crc += this.permissions.ownerMask; + crc += this.permissions.nextOwnerMask; + crc += this.permissions.everyoneMask; + crc += this.permissions.groupMask; + crc += this.flags; + crc += this.inventoryType; + crc += this.type; + crc += Math.round(this.created.getTime() / 1000); + crc += this.salePrice; + crc += this.saleType * 0x07073096; + while (crc > 4294967295) + { + crc -= 4294967295; + } + return crc; + } +} diff --git a/lib/classes/ObjectStoreFull.ts b/lib/classes/ObjectStoreFull.ts index e8525c1..d7659e1 100644 --- a/lib/classes/ObjectStoreFull.ts +++ b/lib/classes/ObjectStoreFull.ts @@ -80,23 +80,23 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore obj.Flags = objData.UpdateFlags; obj.PathCurve = objData.PathCurve; obj.ProfileCurve = objData.ProfileCurve; - obj.PathBegin = objData.PathBegin; - obj.PathEnd = objData.PathEnd; - obj.PathScaleX = objData.PathScaleX; - obj.PathScaleY = objData.PathScaleY; - obj.PathShearX = objData.PathShearX; - obj.PathShearY = objData.PathShearY; - obj.PathTwist = objData.PathTwist; - obj.PathTwistBegin = objData.PathTwistBegin; - obj.PathRadiusOffset = objData.PathRadiusOffset; - obj.PathTaperX = objData.PathTaperX; - obj.PathTaperY = objData.PathTaperY; - obj.PathRevolutions = objData.PathRevolutions; - obj.PathSkew = objData.PathSkew; - obj.ProfileBegin = objData.ProfileBegin; - obj.ProfileEnd = objData.ProfileEnd; - obj.ProfileHollow = objData.ProfileHollow; - obj.TextureEntry = new TextureEntry(objData.TextureEntry); + obj.PathBegin = Utils.unpackBeginCut(objData.PathBegin); + obj.PathEnd = Utils.unpackEndCut(objData.PathEnd); + obj.PathScaleX = Utils.unpackPathScale(objData.PathScaleX); + obj.PathScaleY = Utils.unpackPathScale(objData.PathScaleY); + obj.PathShearX = Utils.unpackPathShear(objData.PathShearX); + obj.PathShearY = Utils.unpackPathShear(objData.PathShearY); + obj.PathTwist = Utils.unpackPathTwist(objData.PathTwist); + obj.PathTwistBegin = Utils.unpackPathTwist(objData.PathTwistBegin); + obj.PathRadiusOffset = Utils.unpackPathTwist(objData.PathRadiusOffset); + obj.PathTaperX = Utils.unpackPathTaper(objData.PathTaperX); + obj.PathTaperY = Utils.unpackPathTaper(objData.PathTaperY); + obj.PathRevolutions = Utils.unpackPathRevolutions(objData.PathRevolutions); + obj.PathSkew = Utils.unpackPathTwist(objData.PathSkew); + obj.ProfileBegin = Utils.unpackBeginCut(objData.ProfileBegin); + obj.ProfileEnd = Utils.unpackEndCut(objData.ProfileEnd); + obj.ProfileHollow = Utils.unpackProfileHollow(objData.ProfileHollow); + obj.TextureEntry = TextureEntry.from(objData.TextureEntry); obj.textureAnim = TextureAnim.from(objData.TextureAnim); const pcodeData = objData.Data; @@ -378,31 +378,31 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore pos += result.readLength; } o.PathCurve = buf.readUInt8(pos++); - o.PathBegin = buf.readUInt16LE(pos); + o.PathBegin = Utils.unpackBeginCut(buf.readUInt16LE(pos)); pos = pos + 2; - o.PathEnd = buf.readUInt16LE(pos); + o.PathEnd = Utils.unpackEndCut(buf.readUInt16LE(pos)); pos = pos + 2; - o.PathScaleX = buf.readUInt8(pos++); - o.PathScaleY = buf.readUInt8(pos++); - o.PathShearX = buf.readUInt8(pos++); - o.PathShearY = buf.readUInt8(pos++); - o.PathTwist = buf.readUInt8(pos++); - o.PathTwistBegin = buf.readUInt8(pos++); - o.PathRadiusOffset = buf.readUInt8(pos++); - o.PathTaperX = buf.readUInt8(pos++); - o.PathTaperY = buf.readUInt8(pos++); - o.PathRevolutions = buf.readUInt8(pos++); - o.PathSkew = buf.readUInt8(pos++); + o.PathScaleX = Utils.unpackPathScale(buf.readUInt8(pos++)); + o.PathScaleY = Utils.unpackPathScale(buf.readUInt8(pos++)); + o.PathShearX = Utils.unpackPathShear(buf.readUInt8(pos++)); + o.PathShearY = Utils.unpackPathShear(buf.readUInt8(pos++)); + o.PathTwist = Utils.unpackPathTwist(buf.readUInt8(pos++)); + o.PathTwistBegin = Utils.unpackPathTwist(buf.readUInt8(pos++)); + o.PathRadiusOffset = Utils.unpackPathTwist(buf.readUInt8(pos++)); + o.PathTaperX = Utils.unpackPathTaper(buf.readUInt8(pos++)); + o.PathTaperY = Utils.unpackPathTaper(buf.readUInt8(pos++)); + o.PathRevolutions = Utils.unpackPathRevolutions(buf.readUInt8(pos++)); + o.PathSkew = Utils.unpackPathTwist(buf.readUInt8(pos++)); o.ProfileCurve = buf.readUInt8(pos++); - o.ProfileBegin = buf.readUInt16LE(pos); + o.ProfileBegin = Utils.unpackBeginCut(buf.readUInt16LE(pos)); pos = pos + 2; - o.ProfileEnd = buf.readUInt16LE(pos); + o.ProfileEnd = Utils.unpackEndCut(buf.readUInt16LE(pos)); pos = pos + 2; - o.ProfileHollow = buf.readUInt16LE(pos); + o.ProfileHollow = Utils.unpackProfileHollow(buf.readUInt16LE(pos)); pos = pos + 2; const textureEntryLength = buf.readUInt32LE(pos); pos = pos + 4; - o.TextureEntry = new TextureEntry(buf.slice(pos, pos + textureEntryLength)); + o.TextureEntry = TextureEntry.from(buf.slice(pos, pos + textureEntryLength)); pos = pos + textureEntryLength; if (compressedflags & CompressedFlags.TextureAnimation) @@ -476,7 +476,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore if (objectData.TextureEntry.length > 0) { // No idea why the first four bytes are skipped here. - this.objects[localID].TextureEntry = new TextureEntry(objectData.TextureEntry.slice(4)); + this.objects[localID].TextureEntry = TextureEntry.from(objectData.TextureEntry.slice(4)); } this.insertIntoRtree(this.objects[localID]); } diff --git a/lib/classes/ObjectStoreLite.ts b/lib/classes/ObjectStoreLite.ts index b835751..f3d34de 100644 --- a/lib/classes/ObjectStoreLite.ts +++ b/lib/classes/ObjectStoreLite.ts @@ -21,6 +21,7 @@ import { ObjectUpdatedEvent, PacketFlags, PCode, + PrimFlags, Vector3 } from '..'; import {GameObject} from './public/GameObject'; @@ -32,6 +33,8 @@ import {ObjectDeselectMessage} from './messages/ObjectDeselect'; import {Quaternion} from './Quaternion'; import {Subscription} from 'rxjs/internal/Subscription'; import {ExtraParams} from './public/ExtraParams'; +import {ObjectPropertiesMessage} from './messages/ObjectProperties'; +import {SelectedObjectEvent} from '../events/SelectedObjectEvent'; export class ObjectStoreLite implements IObjectStore { @@ -45,7 +48,9 @@ export class ObjectStoreLite implements IObjectStore protected requestedObjects: {[key: number]: boolean} = {}; protected deadObjects: number[] = []; protected persist = false; + protected pendingObjectProperties: {[key: string]: any} = {}; private physicsSubscription: Subscription; + private selectedPrimsWithoutUpdate: {[key: number]: boolean} = {}; rtree?: RBush3D; @@ -61,11 +66,28 @@ export class ObjectStoreLite implements IObjectStore Message.ObjectUpdateCached, Message.ObjectUpdateCompressed, Message.ImprovedTerseObjectUpdate, + Message.ObjectProperties, Message.KillObject ], async (packet: Packet) => { switch (packet.message.id) { + case Message.ObjectProperties: + const objProp = packet.message as ObjectPropertiesMessage; + for (const obj of objProp.ObjectData) + { + const obje = this.objectsByUUID[obj.ObjectID.toString()]; + if (obje !== undefined && this.objects[obje] !== undefined) + { + const o = this.objects[obje]; + this.applyObjectProperties(o, obj); + } + else + { + this.pendingObjectProperties[obj.ObjectID.toString()] = obj; + } + } + break; case Message.ObjectUpdate: const objectUpdate = packet.message as ObjectUpdateMessage; this.objectUpdate(objectUpdate); @@ -102,6 +124,92 @@ export class ObjectStoreLite implements IObjectStore this.objects[evt.localID].friction = evt.friction; } }); + + setInterval(() => + { + let selectObjects = []; + for (const key of Object.keys(this.selectedPrimsWithoutUpdate)) + { + selectObjects.push(key); + } + function shuffle(a: any) + { + let j, x, i; + for (i = a.length - 1; i > 0; i--) + { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } + return a; + } + selectObjects = shuffle(selectObjects); + if (selectObjects.length > 10) + { + selectObjects = selectObjects.slice(0, 20); + } + if (selectObjects.length > 0) + { + const selectObject = new ObjectSelectMessage(); + selectObject.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + selectObject.ObjectData = []; + for (const id of selectObjects) + { + selectObject.ObjectData.push({ + ObjectLocalID: id + }); + } + this.circuit.sendMessage(selectObject, PacketFlags.Reliable); + } + }, 1000) + } + + private applyObjectProperties(o: GameObject, obj: any) + { + if (this.selectedPrimsWithoutUpdate[o.ID]) + { + delete this.selectedPrimsWithoutUpdate[o.ID]; + } + o.creatorID = obj.CreatorID; + o.creationDate = obj.CreationDate; + o.baseMask = obj.BaseMask; + o.ownerMask = obj.OwnerMask; + o.groupMask = obj.GroupMask; + o.everyoneMask = obj.EveryoneMask; + o.nextOwnerMask = obj.NextOwnerMask; + o.ownershipCost = obj.OwnershipCost; + o.saleType = obj.SaleType; + o.salePrice = obj.SalePrice; + o.aggregatePerms = obj.AggregatePerms; + o.aggregatePermTextures = obj.AggregatePermTextures; + o.aggregatePermTexturesOwner = obj.AggregatePermTexturesOwner; + o.category = obj.Category; + o.inventorySerial = obj.InventorySerial; + o.itemID = obj.ItemID; + o.folderID = obj.FolderID; + o.fromTaskID = obj.FromTaskID; + o.groupID = obj.GroupID; + o.lastOwnerID = obj.LastOwnerID; + o.name = Utils.BufferToStringSimple(obj.Name); + o.description = Utils.BufferToStringSimple(obj.Description); + o.touchName = Utils.BufferToStringSimple(obj.TouchName); + o.sitName = Utils.BufferToStringSimple(obj.SitName); + o.textureID = Utils.BufferToStringSimple(obj.TextureID); + o.resolvedAt = new Date().getTime() / 1000; + + if (o.Flags !== undefined) + { + if (o.Flags & PrimFlags.CreateSelected) + { + const evt = new SelectedObjectEvent(); + evt.object = o; + this.clientEvents.onSelectedObjectEvent.next(evt); + } + } } protected async requestMissingObject(localID: number, attempt = 0) @@ -302,6 +410,10 @@ export class ObjectStoreLite implements IObjectStore newObj.localID = obj.ID; newObj.objectID = obj.FullID; newObj.object = obj; + if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()]) + { + this.selectedPrimsWithoutUpdate[obj.ID] = true; + } this.clientEvents.onNewObjectEvent.next(newObj); } else @@ -312,6 +424,11 @@ export class ObjectStoreLite implements IObjectStore 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()]; + } } protected objectUpdateCached(objectUpdateCached: ObjectUpdateCachedMessage) diff --git a/lib/classes/Quaternion.ts b/lib/classes/Quaternion.ts index 30b4b3f..1ba6385 100644 --- a/lib/classes/Quaternion.ts +++ b/lib/classes/Quaternion.ts @@ -64,9 +64,17 @@ export class Quaternion extends quat return false; } - constructor(buf?: Buffer | number[] | Quaternion, pos?: number) + constructor(buf?: Buffer | number[] | Quaternion | quat, pos?: number) { - if (buf instanceof Quaternion) + if (buf instanceof quat) + { + super(); + this.x = buf.x; + this.y = buf.y; + this.z = buf.z; + this.w = buf.z; + } + else if (buf instanceof Quaternion) { super(); this.x = buf.x; @@ -106,4 +114,32 @@ export class Quaternion extends quat { return '<' + this.x + ', ' + this.y + ', ' + this.z + ', ' + this.w + '>'; } + getBuffer(): Buffer + { + const j = Buffer.allocUnsafe(12); + this.writeToBuffer(j, 0); + return j; + } + compareApprox(rot: Quaternion): boolean + { + return this.angleBetween(rot) < 0.0001 || rot.equals(this, 0.0001); + } + angleBetween(b: Quaternion): number + { + const a = this; + const aa = (a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w); + const bb = (b.x * b.x + b.y * b.y + b.z * b.z + b.w * b.w); + const aa_bb = aa * bb; + if (aa_bb === 0) + { + return 0.0; + } + const ab = (a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w); + const quotient = (ab * ab) / aa_bb; + if (quotient >= 1.0) + { + return 0.0; + } + return Math.acos(2 * quotient - 1); + } } diff --git a/lib/classes/Region.ts b/lib/classes/Region.ts index 8485661..b7cb9fe 100644 --- a/lib/classes/Region.ts +++ b/lib/classes/Region.ts @@ -35,6 +35,7 @@ import {SkyPreset} from './public/interfaces/SkyPreset'; import {Vector4} from './Vector4'; import {WaterPreset} from './public/interfaces/WaterPreset'; import {ClientCommands} from './ClientCommands'; +import {SimulatorViewerTimeMessageMessage} from './messages/SimulatorViewerTimeMessage'; export class Region { @@ -119,6 +120,8 @@ export class Region environment: RegionEnvironment; + timeOffset = 0; + static IDCTColumn16(linein: number[], lineout: number[], column: number) { let total: number; @@ -421,12 +424,14 @@ export class Region }); this.messageSubscription = this.circuit.subscribeToMessages([ - Message.LayerData + Message.LayerData, + Message.SimulatorViewerTimeMessage ], (packet: Packet) => { switch (packet.message.id) { case Message.LayerData: + { const layerData: LayerDataMessage = packet.message as LayerDataMessage; const type: LayerType = layerData.LayerID.Type; @@ -576,6 +581,14 @@ export class Region break; } break; + } + case Message.SimulatorViewerTimeMessage: + { + const msg = packet.message as SimulatorViewerTimeMessageMessage; + const timeStamp = msg.TimeInfo.UsecSinceStart.toNumber() / 1000000; + this.timeOffset = (new Date().getTime() / 1000) - timeStamp; + break; + } } }) } diff --git a/lib/classes/TextureEntry.ts b/lib/classes/TextureEntry.ts index 4d5554c..33b0ddc 100644 --- a/lib/classes/TextureEntry.ts +++ b/lib/classes/TextureEntry.ts @@ -5,9 +5,9 @@ import {Utils} from './Utils'; export class TextureEntry { + static MAX_UINT32 = 4294967295; defaultTexture: TextureEntryFace | null; faces: TextureEntryFace[] = []; - binary: Buffer; static readFaceBitfield(buf: Buffer, pos: number): { result: boolean, @@ -27,9 +27,19 @@ export class TextureEntry return result; } let b = 0; + let outputValue = false; + let str = '0x'; do { b = buf.readUInt8(result.pos); + if (b === 0x81) + { + outputValue = true; + } + if (outputValue) + { + str += b.toString(16); + } result.faceBits = (result.faceBits << 7) | (b & 0x7F); result.bitfieldSize += 7; result.pos++; @@ -39,22 +49,51 @@ export class TextureEntry return result; } - constructor(buf: Buffer) + static getFaceBitfieldBuffer(bitfield: number): Buffer { - this.binary = buf; + let byteLength = 0; + let tmpBitfield = bitfield; + while (tmpBitfield !== 0) + { + tmpBitfield >>= 7; + byteLength++; + } + + + if (byteLength === 0) + { + const buf = Buffer.allocUnsafe(1); + buf[0] = 0; + return buf; + } + const bytes = Buffer.allocUnsafe(byteLength); + for (let i = 0; i < byteLength; i++) + { + bytes[i] = ((bitfield >> (7 * (byteLength - i - 1))) & 0x7F); + if (i < byteLength - 1) + { + bytes[i] |= 0x80; + } + } + return bytes; + } + + static from(buf: Buffer) + { + const te = new TextureEntry(); if (buf.length < 16) { - this.defaultTexture = null; + te.defaultTexture = null; } else { - this.defaultTexture = new TextureEntryFace(null); + te.defaultTexture = new TextureEntryFace(null); const pos = 0; let i = pos; // Texture { - this.defaultTexture.textureID = new UUID(buf, i); + te.defaultTexture.textureID = new UUID(buf, i); i += 16; let done = false; @@ -71,8 +110,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].textureID = uuid; + te.createFace(face); + te.faces[face].textureID = uuid; } } } @@ -81,7 +120,7 @@ export class TextureEntry // Colour { - this.defaultTexture.rgba = new Color4(buf, i, true); + te.defaultTexture.rgba = new Color4(buf, i, true); i += 4; let done = false; @@ -98,8 +137,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].rgba = tmpColor; + te.createFace(face); + te.faces[face].rgba = tmpColor; } } } @@ -108,7 +147,7 @@ export class TextureEntry // RepeatU { - this.defaultTexture.repeatU = buf.readFloatLE(i); + te.defaultTexture.repeatU = buf.readFloatLE(i); i += 4; let done = false; @@ -125,8 +164,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].repeatU = tmpFloat; + te.createFace(face); + te.faces[face].repeatU = tmpFloat; } } } @@ -135,7 +174,7 @@ export class TextureEntry // RepeatV { - this.defaultTexture.repeatV = buf.readFloatLE(i); + te.defaultTexture.repeatV = buf.readFloatLE(i); i += 4; let done = false; @@ -152,8 +191,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].repeatV = tmpFloat; + te.createFace(face); + te.faces[face].repeatV = tmpFloat; } } } @@ -162,7 +201,7 @@ export class TextureEntry // OffsetU { - this.defaultTexture.offsetU = Utils.ReadOffsetFloat(buf, i); + te.defaultTexture.offsetU = Utils.ReadOffsetFloat(buf, i); i += 2; let done = false; @@ -179,8 +218,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].offsetU = tmpFloat; + te.createFace(face); + te.faces[face].offsetU = tmpFloat; } } } @@ -189,7 +228,7 @@ export class TextureEntry // OffsetV { - this.defaultTexture.offsetV = Utils.ReadOffsetFloat(buf, i); + te.defaultTexture.offsetV = Utils.ReadOffsetFloat(buf, i); i += 2; let done = false; @@ -206,8 +245,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].offsetV = tmpFloat; + te.createFace(face); + te.faces[face].offsetV = tmpFloat; } } } @@ -216,7 +255,7 @@ export class TextureEntry // Rotation { - this.defaultTexture.rotation = Utils.ReadRotationFloat(buf, i); + te.defaultTexture.rotation = Utils.ReadRotationFloat(buf, i); i += 2; let done = false; @@ -233,8 +272,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].rotation = tmpFloat; + te.createFace(face); + te.faces[face].rotation = tmpFloat; } } } @@ -243,7 +282,7 @@ export class TextureEntry // Material { - this.defaultTexture.material = buf[i++]; + te.defaultTexture.material = buf[i++]; let done = false; while (!done) @@ -258,8 +297,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].material = tmpByte; + te.createFace(face); + te.faces[face].material = tmpByte; } } } @@ -268,7 +307,7 @@ export class TextureEntry // Media { - this.defaultTexture.media = buf[i++]; + te.defaultTexture.media = buf[i++]; let done = false; while (i - pos < buf.length && !done) @@ -283,8 +322,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].media = tmpByte; + te.createFace(face); + te.faces[face].media = tmpByte; } } } @@ -293,7 +332,7 @@ export class TextureEntry // Glow { - this.defaultTexture.glow = Utils.ReadGlowFloat(buf, i++); + te.defaultTexture.glow = Utils.ReadGlowFloat(buf, i++); let done = false; while (!done) @@ -308,8 +347,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].glow = tmpFloat; + te.createFace(face); + te.faces[face].glow = tmpFloat; } } } @@ -321,7 +360,7 @@ export class TextureEntry const len = i - pos + 16; if (i - pos + 16 <= buf.length) { - this.defaultTexture.materialID = new UUID(buf, i); + te.defaultTexture.materialID = new UUID(buf, i); i += 16; let done = false; @@ -338,8 +377,8 @@ export class TextureEntry { if ((result.faceBits & bit) !== 0) { - this.createFace(face); - this.faces[face].materialID = uuid; + te.createFace(face); + te.faces[face].materialID = uuid; } } } @@ -347,6 +386,11 @@ export class TextureEntry } } } + return te; + } + + constructor() + { } @@ -361,4 +405,235 @@ export class TextureEntry this.faces.push(new TextureEntryFace(this.defaultTexture)); } } + + toBuffer(): Buffer + { + if (this.defaultTexture === null) + { + return Buffer.allocUnsafe(0); + } + const textures: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const rgbas: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const repeatus: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const repeatvs: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const offsetus: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const offsetvs: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const rotations: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const materials: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const medias: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const glows: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + const materialIDs: number[] = Utils.fillArray(TextureEntry.MAX_UINT32, this.faces.length); + + for (let i = 0; i < this.faces.length; i++) + { + if (this.faces[i] == null) + { + continue; + } + + if (!this.faces[i].textureID.equals(this.defaultTexture.textureID)) + { + if (textures[i] === TextureEntry.MAX_UINT32) + { + textures[i] = 0; + } + textures[i] |= (1 << i); + } + if (!this.faces[i].rgba.equals(this.defaultTexture.rgba)) + { + if (rgbas[i] === TextureEntry.MAX_UINT32) + { + rgbas[i] = 0; + } + rgbas[i] |= (1 << i); + } + if (this.faces[i].repeatU !== this.defaultTexture.repeatU) + { + if (repeatus[i] === TextureEntry.MAX_UINT32) + { + repeatus[i] = 0; + } + repeatus[i] |= (1 << i); + } + if (this.faces[i].repeatV !== this.defaultTexture.repeatV) + { + if (repeatvs[i] === TextureEntry.MAX_UINT32) + { + repeatvs[i] = 0; + } + repeatvs[i] |= (1 << i); + } + if (Utils.TEOffsetShort(this.faces[i].offsetU) !== Utils.TEOffsetShort(this.defaultTexture.offsetU)) + { + if (offsetus[i] === TextureEntry.MAX_UINT32) + { + offsetus[i] = 0; + } + offsetus[i] |= (1 << i); + } + if (Utils.TEOffsetShort(this.faces[i].offsetV) !== Utils.TEOffsetShort(this.defaultTexture.offsetV)) + { + if (offsetvs[i] === TextureEntry.MAX_UINT32) + { + offsetvs[i] = 0; + } + offsetvs[i] |= (1 << i); + } + if (Utils.TERotationShort(this.faces[i].rotation) !== Utils.TERotationShort(this.defaultTexture.rotation)) + { + if (rotations[i] === TextureEntry.MAX_UINT32) + { + rotations[i] = 0; + } + rotations[i] |= (1 << i); + } + if (this.faces[i].material !== this.defaultTexture.material) + { + if (materials[i] === TextureEntry.MAX_UINT32) + { + materials[i] = 0; + } + materials[i] |= (1 << i); + } + if (this.faces[i].media !== this.defaultTexture.media) + { + if (medias[i] === TextureEntry.MAX_UINT32) + { + medias[i] = 0; + } + medias[i] |= (1 << i); + } + if (Utils.TEGlowByte(this.faces[i].glow) !== Utils.TEGlowByte(this.defaultTexture.glow)) + { + if (glows[i] === TextureEntry.MAX_UINT32) + { + glows[i] = 0; + } + glows[i] |= (1 << i); + } + if (!this.faces[i].materialID.equals(this.defaultTexture.materialID)) + { + if (materialIDs[i] === TextureEntry.MAX_UINT32) + { + materialIDs[i] = 0; + } + materialIDs[i] |= (1 << i); + } + } + + const chunks: Buffer[] = []; + + // Textures + this.getChunks( chunks, textures, (face: TextureEntryFace): Buffer => + { + return face.textureID.getBuffer(); + }); + // Colour + this.getChunks(chunks, rgbas, (face: TextureEntryFace): Buffer => + { + return face.rgba.getBuffer(true); + }); + // RepeatU + this.getChunks( chunks, repeatus, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToFloatBuffer(face.repeatU); + }); + // RepeatV + this.getChunks(chunks, repeatvs, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToFloatBuffer(face.repeatV); + }); + // OffsetU + this.getChunks( chunks, offsetus, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToShortBuffer(Utils.TEOffsetShort(face.offsetU)); + }); + // OffsetV + this.getChunks( chunks, offsetvs, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToShortBuffer(Utils.TEOffsetShort(face.offsetV)); + }); + // Rotation + this.getChunks( chunks, rotations, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToShortBuffer(Utils.TERotationShort(face.rotation)); + }); + // Material + this.getChunks( chunks, materials, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToByteBuffer(face.material); + }); + // Media + this.getChunks( chunks, medias, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToByteBuffer(face.media); + }); + // Glows + this.getChunks( chunks, glows, (face: TextureEntryFace): Buffer => + { + return Utils.NumberToByteBuffer(Utils.TEGlowByte(face.glow)); + }); + // MaterialID + this.getChunks(chunks, materialIDs, (face: TextureEntryFace): Buffer => + { + return face.materialID.getBuffer(); + }); + + return Buffer.concat(chunks); + } + + getChunks(chunks: Buffer[], items: number[], func: (item: TextureEntryFace) => Buffer) + { + if (this.defaultTexture !== null) + { + if (chunks.length > 0) + { + // Finish off the last chunk + const zero = Buffer.allocUnsafe(1); + zero[0] = 0; + chunks.push(zero); + } + chunks.push(func(this.defaultTexture)); + const existingChunks: { + buf: Buffer, + bitfield: number + }[] = []; + for (let i = items.length - 1; i > -1; i--) + { + if (items[i] !== TextureEntry.MAX_UINT32) + { + const bitField = items[i]; + const buf = func(this.faces[i]); + + let found = false; + for (const ch of existingChunks) + { + if (ch.buf.compare(buf) === 0) + { + ch.bitfield = ch.bitfield | bitField; + found = true; + break; + } + } + if (!found) + { + existingChunks.push({ + bitfield: bitField, + buf: buf + }); + } + } + } + for (const chunk of existingChunks) + { + chunks.push(TextureEntry.getFaceBitfieldBuffer(chunk.bitfield)); + chunks.push(chunk.buf); + } + } + } + + toBase64(): string + { + return this.toBuffer().toString('base64'); + } } diff --git a/lib/classes/TextureEntryFace.ts b/lib/classes/TextureEntryFace.ts index 506ca65..0f478db 100644 --- a/lib/classes/TextureEntryFace.ts +++ b/lib/classes/TextureEntryFace.ts @@ -13,31 +13,28 @@ export class TextureEntryFace static MEDIA_MASK = 0x01; static TEX_MAP_MASK = 0x06; - textureID: UUID; - rgba: Color4; - repeatU: number; - repeatV: number; - offsetU: number; - offsetV: number; - rotation: number; - glow: number; - materialID: UUID; - bumpiness: Bumpiness = Bumpiness.None; - shininess: Shininess = Shininess.None; - mappingType: MappingType = MappingType.Default; - fullBright = false; - mediaFlags = false; + private _textureID: UUID; + private _rgba: Color4; + private _repeatU: number; + private _repeatV: number; + private _offsetU: number; + private _offsetV: number; + private _rotation: number; + private _glow: number; + private _materialID: UUID; + private _bumpiness: Bumpiness = Bumpiness.None; + private _shininess: Shininess = Shininess.None; + private _mappingType: MappingType = MappingType.Default; + private _fullBright = false; + private _mediaFlags = false; - private materialb: number; - private mediab: number; + private _material: number; + private _media: number; private hasAttribute: TextureFlags; private defaultTexture: TextureEntryFace | null; constructor(def: TextureEntryFace | null) { - this.rgba = Color4.white; - this.repeatU = 1.0; - this.repeatV = 1.0; this.defaultTexture = def; if (this.defaultTexture == null) { @@ -49,49 +46,213 @@ export class TextureEntryFace } } + get rgba(): Color4 + { + if (this._rgba === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.rgba; + } + return this._rgba; + } + + set rgba(value: Color4) + { + this._rgba = value; + } + + get repeatU(): number + { + if (this._repeatU === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.repeatU; + } + return this._repeatU; + } + + set repeatU(value: number) + { + this._repeatU = value; + } + + get repeatV(): number + { + if (this._repeatV === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.repeatV; + } + return this._repeatV; + } + + set repeatV(value: number) + { + this._repeatV = value; + } + + get offsetU(): number + { + if (this._offsetU === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.offsetU; + } + return this._offsetU; + } + + set offsetU(value: number) + { + this._offsetU = value; + } + + get offsetV(): number + { + if (this._offsetV === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.offsetV; + } + return this._offsetV; + } + + set offsetV(value: number) + { + this._offsetV = value; + } + + get rotation(): number + { + if (this._rotation === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.rotation; + } + return this._rotation; + } + + set rotation(value: number) + { + this._rotation = value; + } + + get glow(): number + { + if (this._glow === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.glow + } + return this._glow; + } + + set glow(value: number) + { + this._glow = value; + } + + get textureID(): UUID + { + if (this._textureID === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.textureID; + } + return this._textureID; + } + + set textureID(value: UUID) + { + this._textureID = value; + } + + get materialID(): UUID + { + if (this._materialID === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.materialID; + } + return this._materialID; + } + + set materialID(value: UUID) + { + this._materialID = value; + } + get material(): number { - return this.materialb; + if (this._material === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.material; + } + return this._material; } set material(material: number) { - this.materialb = material; + this._material = material; if ((this.hasAttribute & TextureFlags.Material) !== 0) { - this.bumpiness = this.materialb & TextureEntryFace.BUMP_MASK; - this.shininess = this.materialb & TextureEntryFace.SHINY_MASK; - this.fullBright = ((this.materialb & TextureEntryFace.FULLBRIGHT_MASK) !== 0); + this._bumpiness = this._material & TextureEntryFace.BUMP_MASK; + this._shininess = this._material & TextureEntryFace.SHINY_MASK; + this._fullBright = ((this._material & TextureEntryFace.FULLBRIGHT_MASK) !== 0); } else if (this.defaultTexture !== null) { - this.bumpiness = this.defaultTexture.bumpiness; - this.shininess = this.defaultTexture.shininess; - this.fullBright = this.defaultTexture.fullBright; + this._bumpiness = this.defaultTexture._bumpiness; + this._shininess = this.defaultTexture._shininess; + this._fullBright = this.defaultTexture._fullBright; } } get media(): number { - return this.mediab; + if (this._media === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.media; + } + return this._media; } set media(media: number) { - this.mediab = media; + this._media = media; if ((this.hasAttribute & TextureFlags.Media) !== 0) { - this.mappingType = media & TextureEntryFace.TEX_MAP_MASK; - this.mediaFlags = ((media & TextureEntryFace.MEDIA_MASK) !== 0); + this._mappingType = media & TextureEntryFace.TEX_MAP_MASK; + this._mediaFlags = ((media & TextureEntryFace.MEDIA_MASK) !== 0); } else if (this.defaultTexture !== null) { - this.mappingType = this.defaultTexture.mappingType; - this.mediaFlags = this.defaultTexture.mediaFlags; + this._mappingType = this.defaultTexture.mappingType; + this._mediaFlags = this.defaultTexture.mediaFlags; } else { throw new Error('No media attribute and default texture is null'); } } + + get mappingType(): number + { + if (this._mappingType === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.mappingType; + } + return this._mappingType; + } + + set mappingType(value: number) + { + this._mappingType = value; + } + + get mediaFlags(): boolean + { + if (this._mediaFlags === undefined && this.defaultTexture !== null) + { + return this.defaultTexture.mediaFlags; + } + return this._mediaFlags; + } + + set mediaFlags(value: boolean) + { + this._mediaFlags = value; + } + + } diff --git a/lib/classes/UUID.ts b/lib/classes/UUID.ts index e62a4fb..0364378 100644 --- a/lib/classes/UUID.ts +++ b/lib/classes/UUID.ts @@ -162,4 +162,17 @@ export class UUID } return new UUID(buf3, 0); } + + public CRC(): number + { + let retval = 0; + const bytes: Buffer = this.getBuffer(); + + retval += ((bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]); + retval += ((bytes[7] << 24) + (bytes[6] << 16) + (bytes[5] << 8) + bytes[4]); + retval += ((bytes[11] << 24) + (bytes[10] << 16) + (bytes[9] << 8) + bytes[8]); + retval += ((bytes[15] << 24) + (bytes[14] << 16) + (bytes[13] << 8) + bytes[12]); + + return retval; + } } diff --git a/lib/classes/Utils.ts b/lib/classes/Utils.ts index ddb9b22..cab10bb 100644 --- a/lib/classes/Utils.ts +++ b/lib/classes/Utils.ts @@ -1,12 +1,22 @@ import * as Long from 'long'; -import {GlobalPosition, HTTPAssets} from '..'; +import {GlobalPosition, HTTPAssets, Vector3} from '..'; +import {Quaternion} from './Quaternion'; export class Utils { + static TWO_PI = 6.283185307179586476925286766559; + static CUT_QUANTA = 0.00002; + static SCALE_QUANTA = 0.01; + static SHEAR_QUANTA = 0.01; + static TAPER_QUANTA = 0.01; + static REV_QUANTA = 0.015; + static HOLLOW_QUANTA = 0.00002; + static StringToBuffer(str: string): Buffer { return Buffer.from(str + '\0', 'utf8'); } + static BufferToStringSimple(buf: Buffer, startPos?: number): string { if (buf.length === 0) @@ -22,12 +32,24 @@ export class Utils return buf.toString('utf8'); } } + static Clamp(value: number, min: number, max: number) { value = (value > max) ? max : value; value = (value < min) ? min : value; return value; } + + static fillArray(value: T, count: number) + { + const arr: T[] = new Array(count); + while (count--) + { + arr[count] = value; + } + return arr; + } + static JSONStringify(obj: object, space: number) { const cache: any[] = []; @@ -51,11 +73,12 @@ export class Utils return value; }, space); } + static BufferToString(buf: Buffer, startPos?: number): - { - readLength: number, - result: string - } + { + readLength: number, + result: string + } { if (buf.length === 0) { @@ -143,6 +166,7 @@ export class Utils return ''; } } + static FloatToByte(val: number, lower: number, upper: number) { val = Utils.Clamp(val, lower, upper); @@ -150,6 +174,7 @@ export class Utils val /= (upper - lower); return Math.round(val * 255); } + static ByteToFloat(byte: number, lower: number, upper: number) { const ONE_OVER_BYTEMAX: number = 1.0 / 255; @@ -182,16 +207,19 @@ export class Utils } return fval; } + static Base64EncodeString(str: string): string { const buff = new Buffer(str, 'utf8'); return buff.toString('base64'); } + static Base64DecodeString(str: string): string { const buff = new Buffer(str, 'base64'); return buff.toString('utf8'); } + static HexToLong(hex: string) { while (hex.length < 16) @@ -200,17 +228,224 @@ export class Utils } return new Long(parseInt(hex.substr(8), 16), parseInt(hex.substr(0, 8), 16)); } + static ReadRotationFloat(buf: Buffer, pos: number): number { - return ((buf[pos] | (buf[pos + 1] << 8)) / 32768.0) * (2 * Math.PI); + return ((buf[pos] | (buf[pos + 1] << 8)) / 32768.0) * Utils.TWO_PI; } + static ReadGlowFloat(buf: Buffer, pos: number): number { return buf[pos] / 255; } + static ReadOffsetFloat(buf: Buffer, pos: number): number { const offset = buf.readInt16LE(pos); return offset / 32767.0; } + + static TEOffsetShort(num: number) + { + num = Utils.Clamp(num, -1.0, 1.0); + num *= 32767.0; + return Math.round(num); + } + + static IEEERemainder(x: number, y: number) + { + if (isNaN(x)) + { + return x; // IEEE 754-2008: NaN payload must be preserved + } + if (isNaN(y)) + { + return y; // IEEE 754-2008: NaN payload must be preserved + } + const regularMod = x % y; + if (isNaN(regularMod)) + { + return NaN; + } + if (regularMod === 0) + { + if (Math.sign(x) < 0) + { + return -0; + } + } + const alternativeResult = regularMod - (Math.abs(y) * Math.sign(x)); + if (Math.abs(alternativeResult) === Math.abs(regularMod)) + { + const divisionResult = x / y; + const roundedResult = Math.round(divisionResult); + if (Math.abs(roundedResult) > Math.abs(divisionResult)) + { + return alternativeResult; + } + else + { + return regularMod; + } + } + if (Math.abs(alternativeResult) < Math.abs(regularMod)) + { + return alternativeResult; + } + else + { + return regularMod; + } + } + + static TERotationShort(rotation: number) + { + return Math.floor(((Utils.IEEERemainder(rotation, Utils.TWO_PI) / Utils.TWO_PI) * 32768.0) + 0.5); + } + + static TEGlowByte(glow: number) + { + return (glow * 255.0); + } + + static NumberToByteBuffer(num: number): Buffer + { + const buf = Buffer.allocUnsafe(1); + buf.writeUInt8(num, 0); + return buf; + } + + static NumberToShortBuffer(num: number): Buffer + { + const buf = Buffer.allocUnsafe(2); + buf.writeInt16LE(num, 0); + return buf; + } + + static NumberToFloatBuffer(num: number): Buffer + { + const buf = Buffer.allocUnsafe(4); + buf.writeFloatLE(num, 0); + return buf; + } + + static numberOrZero(num: number | undefined): number + { + if (num === undefined) + { + return 0; + } + return num; + } + + static vector3OrZero(vec: Vector3 | undefined): Vector3 + { + if (vec === undefined) + { + return Vector3.getZero(); + } + return vec; + } + + static quaternionOrZero(quat: Quaternion | undefined): Quaternion + { + if (quat === undefined) + { + return Quaternion.getIdentity(); + } + return quat; + } + + static packBeginCut(beginCut: number): number + { + return Math.round(beginCut / Utils.CUT_QUANTA); + } + + static packEndCut(endCut: number): number + { + return (50000 - Math.round(endCut / Utils.CUT_QUANTA)); + } + + static packPathScale(pathScale: number): number + { + return (200 - Math.round(pathScale / Utils.SCALE_QUANTA)); + } + + static packPathShear(pathShear: number): number + { + return Math.round(pathShear / Utils.SHEAR_QUANTA); + } + + static packPathTwist(pathTwist: number): number + { + return Math.round(pathTwist / Utils.SCALE_QUANTA); + } + + static packPathTaper(pathTaper: number): number + { + return Math.round(pathTaper / Utils.TAPER_QUANTA); + } + + static packPathRevolutions(pathRevolutions: number): number + { + return Math.round((pathRevolutions - 1) / Utils.REV_QUANTA); + } + + static packProfileHollow(profileHollow: number): number + { + return Math.round(profileHollow / Utils.HOLLOW_QUANTA); + } + + static unpackBeginCut(beginCut: number): number + { + return beginCut * Utils.CUT_QUANTA; + } + + static unpackEndCut(endCut: number): number + { + return (50000 - endCut) * Utils.CUT_QUANTA; + } + + static unpackPathScale(pathScale: number): number + { + return (200 - pathScale) * Utils.SCALE_QUANTA; + } + + static unpackPathShear(pathShear: number): number + { + return pathShear * Utils.SHEAR_QUANTA; + } + + static unpackPathTwist(pathTwist: number): number + { + return pathTwist * Utils.SCALE_QUANTA; + } + + static unpackPathTaper(pathTaper: number): number + { + return pathTaper * Utils.TAPER_QUANTA; + } + + static unpackPathRevolutions(pathRevolutions: number): number + { + return pathRevolutions * Utils.REV_QUANTA + 1; + } + + static unpackProfileHollow(profileHollow: number): number + { + return profileHollow * Utils.HOLLOW_QUANTA; + } + + static nullTerminatedString(str: string) + { + const index = str.indexOf('\0'); + if (index === -1) + { + return str; + } + else + { + return str.substr(0, index - 1); + } + } } diff --git a/lib/classes/Vector2.ts b/lib/classes/Vector2.ts index 2c954e5..d9334f1 100644 --- a/lib/classes/Vector2.ts +++ b/lib/classes/Vector2.ts @@ -65,4 +65,8 @@ export class Vector2 extends vec2 { return '<' + this.x + ', ' + this.y + '>'; } + toArray() + { + return [this.x, this.y]; + } } diff --git a/lib/classes/Vector3.ts b/lib/classes/Vector3.ts index 5bdf9a9..ef84238 100644 --- a/lib/classes/Vector3.ts +++ b/lib/classes/Vector3.ts @@ -56,9 +56,16 @@ export class Vector3 extends vec3 return false; } - constructor(buf?: Buffer | number[] | Vector3, pos?: number, double?: boolean) + constructor(buf?: Buffer | number[] | Vector3 | vec3, pos?: number, double?: boolean) { - if (buf instanceof Vector3) + if (buf instanceof vec3) + { + super(); + this.x = buf.x; + this.y = buf.y; + this.z = buf.z; + } + else if (buf instanceof Vector3) { super(); this.x = buf.x; @@ -117,6 +124,18 @@ export class Vector3 extends vec3 { return '<' + this.x + ', ' + this.y + ', ' + this.z + '>'; } - - + getBuffer(double: boolean = false): Buffer + { + const buf = Buffer.allocUnsafe((double) ? 24 : 12); + this.writeToBuffer(buf, 0, double); + return buf; + } + compareApprox(vec: Vector3) + { + return vec.equals(this, 0.00001); + } + toArray() + { + return [this.x, this.y, this.z]; + } } diff --git a/lib/classes/commands/AssetCommands.ts b/lib/classes/commands/AssetCommands.ts index e654147..be70240 100644 --- a/lib/classes/commands/AssetCommands.ts +++ b/lib/classes/commands/AssetCommands.ts @@ -2,11 +2,10 @@ import {CommandsBase} from './CommandsBase'; import {UUID} from '../UUID'; import * as LLSD from '@caspertech/llsd'; import {Utils} from '../Utils'; -import {AssetType, HTTPAssets, PacketFlags} from '../..'; +import {AssetType, FolderType, HTTPAssets, LLMesh, Material, PacketFlags, TransferStatus} from '../..'; import {PermissionMask} from '../../enums/PermissionMask'; import * as zlib from 'zlib'; import {ZlibOptions} from 'zlib'; -import {Material} from '../public/Material'; import {Color4} from '../Color4'; import {TransferRequestMessage} from '../messages/TransferRequest'; import {TransferChannelType} from '../../enums/TransferChannelType'; @@ -16,7 +15,6 @@ import {Message} from '../../enums/Message'; import {Packet} from '../Packet'; import {TransferPacketMessage} from '../messages/TransferPacket'; import {TransferAbortMessage} from '../messages/TransferAbort'; -import {TransferStatus} from '../../enums/TransferStatus'; export class AssetCommands extends CommandsBase { @@ -205,7 +203,12 @@ export class AssetCommands extends CommandsBase return; } const binData = new LLSD.Binary(Array.from(reslt), 'BASE64'); - const obj = LLSD.LLSD.parseBinary(binData); + const llsdResult = LLSD.LLSD.parseBinary(binData); + let obj = []; + if (llsdResult.result) + { + obj = llsdResult.result; + } if (obj.length > 0) { for (const mat of obj) @@ -313,6 +316,83 @@ export class AssetCommands extends CommandsBase } } + 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': name, + 'description': 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 + }; + const result = await this.currentRegion.caps.capsRequestXML('NewFileAgentInventory', uploadMap); + if (result['state'] === 'upload' && result['upload_price']) + { + const cost = result['upload_price']; + if (await confirmCostCallback(cost)) + { + const uploader = result['uploader']; + const uploadResult = await this.currentRegion.caps.capsPerformXMLRequest(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'); + } + } + uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise { return new Promise((resolve, reject) => @@ -325,9 +405,9 @@ export class AssetCommands extends CommandsBase 'inventory_type': Utils.HTTPAssetTypeToInventoryType(type), 'name': name, 'description': description, - 'everyone_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), - 'group_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), - 'next_owner_mask': (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19), + 'everyone_mask': PermissionMask.All, + 'group_mask': PermissionMask.All, + 'next_owner_mask': PermissionMask.All, 'expected_upload_cost': 0 }).then((response: any) => { @@ -342,11 +422,40 @@ export class AssetCommands extends CommandsBase 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/FriendCommands.ts b/lib/classes/commands/FriendCommands.ts index 913b216..548a78c 100644 --- a/lib/classes/commands/FriendCommands.ts +++ b/lib/classes/commands/FriendCommands.ts @@ -8,7 +8,19 @@ import {Packet} from '../Packet'; import {OnlineNotificationMessage} from '../messages/OnlineNotification'; import {OfflineNotificationMessage} from '../messages/OfflineNotification'; import {TerminateFriendshipMessage} from '../messages/TerminateFriendship'; -import {AssetType, Friend, FriendOnlineEvent, FriendRemovedEvent, FriendRequestEvent, FriendRightsEvent, MapInfoReplyEvent, MapLocation, PacketFlags, RightsFlags, UUID, Vector3} from '../..'; +import { + FolderType, + Friend, + FriendOnlineEvent, + FriendRemovedEvent, + FriendRequestEvent, + FriendRightsEvent, + MapLocation, + PacketFlags, + RightsFlags, + UUID, + Vector3 +} from '../..'; import {AcceptFriendshipMessage} from '../messages/AcceptFriendship'; import {ImprovedInstantMessageMessage} from '../messages/ImprovedInstantMessage'; import {InstantMessageDialog} from '../../enums/InstantMessageDialog'; @@ -249,7 +261,7 @@ export class FriendCommands extends CommandsBase accept.FolderData = []; accept.FolderData.push( { - 'FolderID': this.agent.inventory.findFolderForType(AssetType.CallingCard) + 'FolderID': this.agent.inventory.findFolderForType(FolderType.CallingCard) } ); const sequenceNo = this.circuit.sendMessage(accept, PacketFlags.Reliable); diff --git a/lib/classes/commands/InventoryCommands.ts b/lib/classes/commands/InventoryCommands.ts index 0be8793..1fe9ff8 100644 --- a/lib/classes/commands/InventoryCommands.ts +++ b/lib/classes/commands/InventoryCommands.ts @@ -1,9 +1,11 @@ import {CommandsBase} from './CommandsBase'; import {InventoryFolder} from '../InventoryFolder'; -import {ChatSourceType, InventoryOfferedEvent, PacketFlags, UUID, Vector3} from '../..'; +import {AssetType, ChatSourceType, InventoryOfferedEvent, PacketFlags, UUID, Vector3} from '../..'; import {InstantMessageDialog} from '../../enums/InstantMessageDialog'; import {ImprovedInstantMessageMessage} from '../messages/ImprovedInstantMessage'; import {Utils} from '../Utils'; +import {InventoryType} from '../../enums/InventoryType'; +import {FolderType} from '../../enums/FolderType'; export class InventoryCommands extends CommandsBase { @@ -19,8 +21,8 @@ export class InventoryCommands extends CommandsBase { const agentName = this.agent.firstName + ' ' + this.agent.lastName; const im: ImprovedInstantMessageMessage = new ImprovedInstantMessageMessage(); - - const folder = this.agent.inventory.findFolderForType(event.type); + const folderType = (event.type as unknown) as FolderType; + const folder = this.agent.inventory.findFolderForType(folderType); const binary = Buffer.allocUnsafe(16); folder.writeToBuffer(binary, 0); diff --git a/lib/classes/commands/RegionCommands.ts b/lib/classes/commands/RegionCommands.ts index 40ef90d..4175b71 100644 --- a/lib/classes/commands/RegionCommands.ts +++ b/lib/classes/commands/RegionCommands.ts @@ -5,19 +5,26 @@ import {RegionHandleRequestMessage} from '../messages/RegionHandleRequest'; import {Message} from '../../enums/Message'; import {FilterResponse} from '../../enums/FilterResponse'; import {RegionIDAndHandleReplyMessage} from '../messages/RegionIDAndHandleReply'; -import {AssetType, PacketFlags, PCode, Vector3} from '../..'; +import { + AssetType, + GameObject, + InventoryItemFlags, + NewObjectEvent, + PacketFlags, + Parcel, + PCode, + PrimFlags, + Vector3 +} from '../..'; import {ObjectGrabMessage} from '../messages/ObjectGrab'; import {ObjectDeGrabMessage} from '../messages/ObjectDeGrab'; import {ObjectGrabUpdateMessage} from '../messages/ObjectGrabUpdate'; -import {GameObject} from '../public/GameObject'; import {ObjectSelectMessage} from '../messages/ObjectSelect'; import {ObjectPropertiesMessage} from '../messages/ObjectProperties'; import {Utils} from '../Utils'; import {ObjectDeselectMessage} from '../messages/ObjectDeselect'; import * as micromatch from 'micromatch'; import * as LLSD from '@caspertech/llsd'; -import {PrimFlags} from '../../enums/PrimFlags'; -import {Parcel} from '../public/Parcel'; import {ParcelPropertiesRequestMessage} from '../messages/ParcelPropertiesRequest'; import {RequestTaskInventoryMessage} from '../messages/RequestTaskInventory'; import {ReplyTaskInventoryMessage} from '../messages/ReplyTaskInventory'; @@ -25,8 +32,14 @@ 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 Timer = NodeJS.Timer; -import {NewObjectEvent} from '../../events/NewObjectEvent'; +import {RezObjectMessage} from '../messages/RezObject'; +import {PermissionMask} from '../../enums/PermissionMask'; +import {from} from 'rxjs'; +import {SelectedObjectEvent} from '../../events/SelectedObjectEvent'; +import uuid = require('uuid'); export class RegionCommands extends CommandsBase { @@ -305,7 +318,7 @@ export class RegionCommands extends CommandsBase return this.currentRegion.regionName; } - private async resolveObjects(objects: GameObject[], onlyUnresolved: boolean = false) + private async resolveObjects(objects: GameObject[], onlyUnresolved: boolean = false, skipInventory = false) { // First, create a map of all object IDs const objs: {[key: number]: GameObject} = {}; @@ -396,7 +409,7 @@ export class RegionCommands extends CommandsBase { count++; const ky = parseInt(k, 10); - if (objs[ky] !== undefined) + 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)) @@ -786,7 +799,8 @@ export class RegionCommands extends CommandsBase resolve(event.object); } }); - tmr = setTimeout(() => { + tmr = setTimeout(() => + { subscription.unsubscribe(); reject(new Error('Timeout')); }, timeout) @@ -810,13 +824,272 @@ export class RegionCommands extends CommandsBase resolve(event.object); } }); - tmr = setTimeout(() => { + tmr = setTimeout(() => + { subscription.unsubscribe(); reject(new Error('Timeout')); }, timeout) }); } + private async buildPart(obj: GameObject, posOffset: Vector3, meshCallback: (object: GameObject, meshData: UUID) => UUID | null) + { + // Rez a prim + let newObject: GameObject; + if (obj.extraParams !== undefined && obj.extraParams.meshData !== null) + { + const inventoryID: UUID | null = await meshCallback(obj, obj.extraParams.meshData.meshData); + if (inventoryID !== null) + { + newObject = await this.createPrim(obj, posOffset, inventoryID); + } + else + { + newObject = await this.createPrim(obj, posOffset); + } + } + else + { + newObject = await this.createPrim(obj, posOffset); + } + await newObject.setExtraParams(obj.extraParams); + if (obj.TextureEntry !== undefined) + { + await newObject.setTextureEntry(obj.TextureEntry); + } + if (obj.name !== undefined) + { + await newObject.setName(obj.name); + } + if (obj.description !== undefined) + { + await newObject.setDescription(obj.description); + } + return newObject; + } + + buildObject(obj: GameObject, meshCallback: (object: GameObject, meshData: UUID) => UUID | null): Promise + { + return new Promise(async (resolve, reject) => + { + + const parts = []; + console.log('Rezzing root prim'); + parts.push(this.buildPart(obj, Vector3.getZero(), meshCallback)); + console.log('Building child prims'); + if (obj.children && obj.Position) + { + for (const child of obj.children) + { + parts.push(this.buildPart(child, obj.Position, meshCallback)); + } + } + Promise.all(parts).then(async (results) => + { + console.log('Linking prims'); + const rootObj = results[0]; + for (const childObject of results) + { + if (childObject !== rootObj) + { + await childObject.linkTo(rootObj); + } + } + console.log('All done'); + resolve(rootObj); + }).catch((err) => + { + reject(err); + }); + }); + } + + private echo(st: string): boolean + { + //console.log(st); + return true; + } + + createPrim(obj: GameObject, posOffset: Vector3, inventoryID?: UUID): Promise + { + console.log('Create prim'); + return new Promise(async (resolve, reject) => + { + const timeRequested = (new Date().getTime() / 1000) - this.currentRegion.timeOffset; + + if (obj.Position === undefined) + { + obj.Position = Vector3.getZero(); + } + if (obj.Rotation === undefined) + { + obj.Rotation = Quaternion.getIdentity(); + } + let finalPos = Vector3.getZero(); + let finalRot = Quaternion.getIdentity(); + if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0) + { + finalPos = obj.Position; + finalRot = obj.Rotation; + } + else + { + const finalPosOffset: Vector3 = obj.Position; + finalPos = new Vector3(new Vector3(finalPosOffset).add(new Vector3(posOffset))); + finalRot = obj.Rotation; + } + let msg: ObjectAddMessage | RezObjectMessage | null = null; + let fromInventory = false; + if (inventoryID === undefined || this.agent.inventory.itemsByID[inventoryID.toString()] === undefined) + { + console.log('Regular prim'); + // First, rez object in scene + msg = new ObjectAddMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID, + GroupID: UUID.zero() + }; + msg.ObjectData = { + PCode: Utils.numberOrZero(obj.PCode), + Material: Utils.numberOrZero(obj.Material), + AddFlags: PrimFlags.CreateSelected, + PathCurve: Utils.numberOrZero(obj.PathCurve), + ProfileCurve: Utils.numberOrZero(obj.ProfileCurve), + PathBegin: Utils.packBeginCut(Utils.numberOrZero(obj.PathBegin)), + PathEnd: Utils.packEndCut(Utils.numberOrZero(obj.PathEnd)), + PathScaleX: Utils.packPathScale(Utils.numberOrZero(obj.PathScaleX)), + PathScaleY: Utils.packPathScale(Utils.numberOrZero(obj.PathScaleY)), + PathShearX: Utils.packPathShear(Utils.numberOrZero(obj.PathShearX)), + PathShearY: Utils.packPathShear(Utils.numberOrZero(obj.PathShearY)), + PathTwist: Utils.packPathTwist(Utils.numberOrZero(obj.PathTwist)), + PathTwistBegin: Utils.packPathTwist(Utils.numberOrZero(obj.PathTwistBegin)), + PathRadiusOffset: Utils.packPathTwist(Utils.numberOrZero(obj.PathRadiusOffset)), + PathTaperX: Utils.packPathTaper(Utils.numberOrZero(obj.PathTaperX)), + PathTaperY: Utils.packPathTaper(Utils.numberOrZero(obj.PathTaperY)), + PathRevolutions: Utils.packPathRevolutions(Utils.numberOrZero(obj.PathRevolutions)), + PathSkew: Utils.packPathTwist(Utils.numberOrZero(obj.PathSkew)), + ProfileBegin: Utils.packBeginCut(Utils.numberOrZero(obj.ProfileBegin)), + ProfileEnd: Utils.packEndCut(Utils.numberOrZero(obj.ProfileEnd)), + ProfileHollow: Utils.packProfileHollow(Utils.numberOrZero(obj.ProfileHollow)), + BypassRaycast: 1, + RayStart: finalPos, + RayEnd: finalPos, + RayTargetID: UUID.zero(), + RayEndIsIntersection: 0, + Scale: Utils.vector3OrZero(obj.Scale), + Rotation: finalRot, + State: Utils.numberOrZero(obj.State) + }; + } + else + { + console.log('Rezzing ' + this.agent.inventory.itemsByID[inventoryID.toString()].name); + fromInventory = true; + const invItem = this.agent.inventory.itemsByID[inventoryID.toString()]; + const queryID = UUID.random(); + msg = new RezObjectMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID, + GroupID: UUID.zero() + }; + msg.RezData = { + FromTaskID: UUID.zero(), + BypassRaycast: 1, + RayStart: finalPos, + RayEnd: finalPos, + 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, + }; + } + + const objSub = this.currentRegion.clientEvents.onSelectedObjectEvent.subscribe(async (evt: SelectedObjectEvent) => + { + if (evt.object.creatorID !== undefined && + evt.object.creatorID.equals(this.agent.agentID) && + evt.object.creationDate !== undefined && + !evt.object.claimedForBuild) + { + let claim = false; + const creationDate = evt.object.creationDate.toNumber() / 1000000; + if (fromInventory && inventoryID !== undefined && evt.object.itemID.equals(inventoryID)) + { + claim = true; + } + else if (!fromInventory && evt.object.itemID.equals(UUID.zero()) && creationDate > timeRequested) + { + claim = true; + } + if (claim) + { + evt.object.claimedForBuild = true; + objSub.unsubscribe(); + + if (!fromInventory) + { + await evt.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); + } + + + // Set the object's position properly + try + { + await evt.object.setGeometry(finalPos, finalRot, obj.Scale); + } + catch (error) + { + console.log('Failed to set object position :/'); + } + resolve(evt.object); + } + } + }); + if (obj.Position !== undefined && obj.Scale !== undefined) + { + // Move the camera to look directly at prim for faster capture + const campos = new Vector3(finalPos); + campos.z += 5.0 + obj.Scale.z; + console.log('Moving camera to ' + campos.toString()); + await this.currentRegion.clientCommands.agent.setCamera(campos, finalPos, 4096, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0])); + } + if (msg !== null) + { + this.circuit.sendMessage(msg, PacketFlags.Reliable); + console.log('Requested rez'); + } + }); + } + async getObjectByLocalID(id: number, resolve: boolean, waitFor: number = 0) { let obj = null; diff --git a/lib/classes/public/FlexibleData.ts b/lib/classes/public/FlexibleData.ts index e6ab66c..29af23c 100644 --- a/lib/classes/public/FlexibleData.ts +++ b/lib/classes/public/FlexibleData.ts @@ -34,4 +34,10 @@ export class FlexibleData buf[pos++] = (this.Wind) * 10; this.Force.writeToBuffer(buf, pos, false); } + getBuffer(): Buffer + { + const buf = Buffer.allocUnsafe(16); + this.writeToBuffer(buf, 0); + return buf; + } } diff --git a/lib/classes/public/GameObject.ts b/lib/classes/public/GameObject.ts index 3444aa1..4e534b3 100644 --- a/lib/classes/public/GameObject.ts +++ b/lib/classes/public/GameObject.ts @@ -12,11 +12,12 @@ import * as Long from 'long'; import {IGameObjectData} from '../interfaces/IGameObjectData'; import { HoleType, - HTTPAssets, + HTTPAssets, PacketFlags, PCode, PhysicsShapeType, PrimFlags, - ProfileShape, SculptType, + ProfileShape, + SculptType, SoundFlags, Utils } from '../..'; @@ -29,6 +30,16 @@ import {InventoryType} from '../../enums/InventoryType'; import {LLWearable} from '../LLWearable'; import {TextureAnim} from './TextureAnim'; import {ExtraParams} from './ExtraParams'; +import {ObjectExtraParamsMessage} from '../messages/ObjectExtraParams'; +import {ExtraParamType} from '../../enums/ExtraParamType'; +import {ObjectImageMessage} from '../messages/ObjectImage'; +import {ObjectNameMessage} from '../messages/ObjectName'; +import {ObjectDescriptionMessage} from '../messages/ObjectDescription'; +import {ObjectPositionMessage} from '../messages/ObjectPosition'; +import {MultipleObjectUpdateMessage} from '../messages/MultipleObjectUpdate'; +import {UpdateType} from '../../enums/UpdateType'; +import {ObjectLinkMessage} from '../messages/ObjectLink'; +import {ObjectShapeMessage} from '../messages/ObjectShape'; export class GameObject implements IGameObjectData { @@ -139,11 +150,13 @@ export class GameObject implements IGameObjectData resolveAttempts = 0; + claimedForBuild = false; + private static getFromXMLJS(obj: any, param: string): any { if (obj[param] === undefined) { - return false; + return undefined; } let retParam; if (Array.isArray(obj[param])) @@ -178,149 +191,149 @@ export class GameObject implements IGameObjectData const go = new GameObject(); go.Flags = 0; let prop: any; - if (this.getFromXMLJS(obj, 'AllowedDrop')) + if (this.getFromXMLJS(obj, 'AllowedDrop') !== undefined) { go.Flags = go.Flags | PrimFlags.AllowInventoryDrop; } - if (prop = UUID.fromXMLJS(obj, 'CreatorID')) + if ((prop = UUID.fromXMLJS(obj, 'CreatorID')) !== undefined) { go.creatorID = prop; } - if (prop = UUID.fromXMLJS(obj, 'FolderID')) + if ((prop = UUID.fromXMLJS(obj, 'FolderID')) !== undefined) { go.folderID = prop; } - if (prop = this.getFromXMLJS(obj, 'InventorySerial')) + if ((prop = this.getFromXMLJS(obj, 'InventorySerial')) !== undefined) { go.inventorySerial = prop; } - if (prop = UUID.fromXMLJS(obj, 'UUID')) + if ((prop = UUID.fromXMLJS(obj, 'UUID')) !== undefined) { go.FullID = prop; } - if (prop = this.getFromXMLJS(obj, 'LocalId')) + if ((prop = this.getFromXMLJS(obj, 'LocalId')) !== undefined) { go.ID = prop; } - if (prop = this.getFromXMLJS(obj, 'Name')) + if ((prop = this.getFromXMLJS(obj, 'Name')) !== undefined) { go.name = prop; } - if (prop = this.getFromXMLJS(obj, 'Material')) + if ((prop = this.getFromXMLJS(obj, 'Material')) !== undefined) { go.Material = prop; } - if (prop = Vector3.fromXMLJS(obj, 'GroupPosition')) + if ((prop = Vector3.fromXMLJS(obj, 'GroupPosition')) !== undefined) { if (root) { go.Position = prop; } } - if (prop = Vector3.fromXMLJS(obj, 'OffsetPosition')) + if ((prop = Vector3.fromXMLJS(obj, 'OffsetPosition')) !== undefined) { if (!root) { go.Position = prop; } } - if (prop = Quaternion.fromXMLJS(obj, 'RotationOffset')) + if ((prop = Quaternion.fromXMLJS(obj, 'RotationOffset')) !== undefined) { go.Rotation = prop; } - if (prop = Vector3.fromXMLJS(obj, 'Velocity')) + if ((prop = Vector3.fromXMLJS(obj, 'Velocity')) !== undefined) { go.Velocity = prop; } - if (prop = Vector3.fromXMLJS(obj, 'AngularVelocity')) + if ((prop = Vector3.fromXMLJS(obj, 'AngularVelocity')) !== undefined) { go.AngularVelocity = prop; } - if (prop = Vector3.fromXMLJS(obj, 'Acceleration')) + if ((prop = Vector3.fromXMLJS(obj, 'Acceleration')) !== undefined) { go.Acceleration = prop; } - if (prop = this.getFromXMLJS(obj, 'Description')) + if ((prop = this.getFromXMLJS(obj, 'Description')) !== undefined) { go.description = prop; } - if (prop = this.getFromXMLJS(obj, 'Text')) + if ((prop = this.getFromXMLJS(obj, 'Text')) !== undefined) { go.Text = prop; } - if (prop = Color4.fromXMLJS(obj, 'Color')) + if ((prop = Color4.fromXMLJS(obj, 'Color')) !== undefined) { go.TextColor = prop; } - if (prop = this.getFromXMLJS(obj, 'SitName')) + if ((prop = this.getFromXMLJS(obj, 'SitName')) !== undefined) { go.sitName = prop; } - if (prop = this.getFromXMLJS(obj, 'TouchName')) + if ((prop = this.getFromXMLJS(obj, 'TouchName')) !== undefined) { go.touchName = prop; } - if (prop = this.getFromXMLJS(obj, 'ClickAction')) + if ((prop = this.getFromXMLJS(obj, 'ClickAction')) !== undefined) { go.ClickAction = prop; } - if (prop = Vector3.fromXMLJS(obj, 'Scale')) + if ((prop = Vector3.fromXMLJS(obj, 'Scale')) !== undefined) { go.Scale = prop; } - if (prop = this.getFromXMLJS(obj, 'ParentID')) + if ((prop = this.getFromXMLJS(obj, 'ParentID')) !== undefined) { go.ParentID = prop; } - if (prop = this.getFromXMLJS(obj, 'Category')) + if ((prop = this.getFromXMLJS(obj, 'Category')) !== undefined) { go.category = prop; } - if (prop = this.getFromXMLJS(obj, 'SalePrice')) + if ((prop = this.getFromXMLJS(obj, 'SalePrice')) !== undefined) { go.salePrice = prop; } - if (prop = this.getFromXMLJS(obj, 'ObjectSaleType')) + if ((prop = this.getFromXMLJS(obj, 'ObjectSaleType')) !== undefined) { go.saleType = prop; } - if (prop = this.getFromXMLJS(obj, 'OwnershipCost')) + if ((prop = this.getFromXMLJS(obj, 'OwnershipCost')) !== undefined) { go.ownershipCost = prop; } - if (prop = UUID.fromXMLJS(obj, 'GroupID')) + if ((prop = UUID.fromXMLJS(obj, 'GroupID')) !== undefined) { go.groupID = prop; } - if (prop = UUID.fromXMLJS(obj, 'OwnerID')) + if ((prop = UUID.fromXMLJS(obj, 'OwnerID')) !== undefined) { go.OwnerID = prop; } - if (prop = UUID.fromXMLJS(obj, 'LastOwnerID')) + if ((prop = UUID.fromXMLJS(obj, 'LastOwnerID')) !== undefined) { go.lastOwnerID = prop; } - if (prop = this.getFromXMLJS(obj, 'BaseMask')) + if ((prop = this.getFromXMLJS(obj, 'BaseMask')) !== undefined) { go.baseMask = prop; } - if (prop = this.getFromXMLJS(obj, 'OwnerMask')) + if ((prop = this.getFromXMLJS(obj, 'OwnerMask')) !== undefined) { go.ownerMask = prop; } - if (prop = this.getFromXMLJS(obj, 'GroupMask')) + if ((prop = this.getFromXMLJS(obj, 'GroupMask')) !== undefined) { go.groupMask = prop; } - if (prop = this.getFromXMLJS(obj, 'EveryoneMask')) + if ((prop = this.getFromXMLJS(obj, 'EveryoneMask')) !== undefined) { go.everyoneMask = prop; } - if (prop = this.getFromXMLJS(obj, 'NextOwnerMask')) + if ((prop = this.getFromXMLJS(obj, 'NextOwnerMask')) !== undefined) { go.nextOwnerMask = prop; } - if (prop = this.getFromXMLJS(obj, 'Flags')) + if ((prop = this.getFromXMLJS(obj, 'Flags')) !== undefined) { let flags = 0; if (typeof prop === 'string') @@ -337,121 +350,125 @@ export class GameObject implements IGameObjectData } go.Flags = flags; } - if (prop = this.getFromXMLJS(obj, 'TextureAnimation')) + if ((prop = this.getFromXMLJS(obj, 'TextureAnimation')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.textureAnim = TextureAnim.from(buf); } - if (prop = this.getFromXMLJS(obj, 'ParticleSystem')) + if ((prop = this.getFromXMLJS(obj, 'ParticleSystem')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.Particles = ParticleSystem.from(buf); } - if (prop = this.getFromXMLJS(obj, 'PhysicsShapeType')) + if ((prop = this.getFromXMLJS(obj, 'PhysicsShapeType')) !== undefined) { go.physicsShapeType = prop; } - if (prop = UUID.fromXMLJS(obj, 'SoundID')) + if ((prop = UUID.fromXMLJS(obj, 'SoundID')) !== undefined) { go.Sound = prop; } - if (prop = UUID.fromXMLJS(obj, 'SoundGain')) + if ((prop = UUID.fromXMLJS(obj, 'SoundGain')) !== undefined) { go.SoundGain = prop; } - if (prop = UUID.fromXMLJS(obj, 'SoundFlags')) + if ((prop = UUID.fromXMLJS(obj, 'SoundFlags')) !== undefined) { go.SoundFlags = prop; } - if (prop = UUID.fromXMLJS(obj, 'SoundRadius')) + if ((prop = UUID.fromXMLJS(obj, 'SoundRadius')) !== undefined) { go.SoundRadius = prop; } - if (prop = this.getFromXMLJS(obj, 'Shape')) + if ((prop = this.getFromXMLJS(obj, 'Shape')) !== undefined) { const shape = prop; - if (prop = this.getFromXMLJS(shape, 'ProfileCurve')) + if ((prop = this.getFromXMLJS(shape, 'ProfileCurve')) !== undefined) { go.ProfileCurve = prop; } - if (prop = this.getFromXMLJS(shape, 'TextureEntry')) + if ((prop = this.getFromXMLJS(shape, 'TextureEntry')) !== undefined) { const buf = Buffer.from(prop, 'base64'); - go.TextureEntry = new TextureEntry(buf); + go.TextureEntry = TextureEntry.from(buf); } - if (prop = this.getFromXMLJS(shape, 'PathBegin')) + if ((prop = this.getFromXMLJS(shape, 'PathBegin')) !== undefined) { - go.PathBegin = prop; + go.PathBegin = Utils.unpackBeginCut(prop); } - if (prop = this.getFromXMLJS(shape, 'PathCurve')) + if ((prop = this.getFromXMLJS(shape, 'PathCurve')) !== undefined) { go.PathCurve = prop; } - if (prop = this.getFromXMLJS(shape, 'PathEnd')) + if ((prop = this.getFromXMLJS(shape, 'PathEnd')) !== undefined) { - go.PathEnd = prop; + go.PathEnd = Utils.unpackEndCut(prop); } - if (prop = this.getFromXMLJS(shape, 'PathRadiusOffset')) + if ((prop = this.getFromXMLJS(shape, 'PathRadiusOffset')) !== undefined) { - go.PathRadiusOffset = prop; + go.PathRadiusOffset = Utils.unpackPathTwist(prop); } - if (prop = this.getFromXMLJS(shape, 'PathRevolutions')) + if ((prop = this.getFromXMLJS(shape, 'PathRevolutions')) !== undefined) { - go.PathRevolutions = prop; + go.PathRevolutions = Utils.unpackPathRevolutions(prop); } - if (prop = this.getFromXMLJS(shape, 'PathScaleX')) + if ((prop = this.getFromXMLJS(shape, 'PathScaleX')) !== undefined) { - go.PathScaleX = prop; + go.PathScaleX = Utils.unpackPathScale(prop); } - if (prop = this.getFromXMLJS(shape, 'PathScaleY')) + if ((prop = this.getFromXMLJS(shape, 'PathScaleY')) !== undefined) { - go.PathScaleY = prop; + go.PathScaleY = Utils.unpackPathScale(prop); } - if (prop = this.getFromXMLJS(shape, 'PathShearX')) + if ((prop = this.getFromXMLJS(shape, 'PathShearX')) !== undefined) { - go.PathShearX = prop; + go.PathShearX = Utils.unpackPathShear(prop); } - if (prop = this.getFromXMLJS(shape, 'PathSkew')) + if ((prop = this.getFromXMLJS(shape, 'PathShearY')) !== undefined) { - go.PathSkew = prop; + go.PathShearY = Utils.unpackPathShear(prop); } - if (prop = this.getFromXMLJS(shape, 'PathTaperX')) + if ((prop = this.getFromXMLJS(shape, 'PathSkew')) !== undefined) { - go.PathTaperX = prop; + go.PathSkew = Utils.unpackPathShear(prop); } - if (prop = this.getFromXMLJS(shape, 'PathTaperY')) + if ((prop = this.getFromXMLJS(shape, 'PathTaperX')) !== undefined) { - go.PathTaperY = prop; + go.PathTaperX = Utils.unpackPathTaper(prop); } - if (prop = this.getFromXMLJS(shape, 'PathTwist')) + if ((prop = this.getFromXMLJS(shape, 'PathTaperY')) !== undefined) { - go.PathTwist = prop; + go.PathTaperY = Utils.unpackPathTaper(prop); } - if (prop = this.getFromXMLJS(shape, 'PathTwistBegin')) + if ((prop = this.getFromXMLJS(shape, 'PathTwist')) !== undefined) { - go.PathTwistBegin = prop; + go.PathTwist = Utils.unpackPathTwist(prop); } - if (prop = this.getFromXMLJS(shape, 'PCode')) + if ((prop = this.getFromXMLJS(shape, 'PathTwistBegin')) !== undefined) + { + go.PathTwistBegin = Utils.unpackPathTwist(prop); + } + if ((prop = this.getFromXMLJS(shape, 'PCode')) !== undefined) { go.PCode = prop; } - if (prop = this.getFromXMLJS(shape, 'ProfileBegin')) + if ((prop = this.getFromXMLJS(shape, 'ProfileBegin')) !== undefined) { - go.ProfileBegin = prop; + go.ProfileBegin = Utils.unpackBeginCut(prop); } - if (prop = this.getFromXMLJS(shape, 'ProfileEnd')) + if ((prop = this.getFromXMLJS(shape, 'ProfileEnd')) !== undefined) { - go.ProfileEnd = prop; + go.ProfileEnd = Utils.unpackEndCut(prop); } - if (prop = this.getFromXMLJS(shape, 'ProfileHollow')) + if ((prop = this.getFromXMLJS(shape, 'ProfileHollow')) !== undefined) { - go.ProfileHollow = prop; + go.ProfileHollow = Utils.unpackProfileHollow(prop); } - if (prop = this.getFromXMLJS(shape, 'State')) + if ((prop = this.getFromXMLJS(shape, 'State')) !== undefined) { go.State = prop; } - if (prop = this.getFromXMLJS(shape, 'ProfileShape')) + if ((prop = this.getFromXMLJS(shape, 'ProfileShape')) !== undefined) { if (!go.ProfileCurve) { @@ -459,7 +476,7 @@ export class GameObject implements IGameObjectData } go.ProfileCurve = go.ProfileCurve | parseInt(ProfileShape[prop], 10); } - if (prop = this.getFromXMLJS(shape, 'HollowShape')) + if ((prop = this.getFromXMLJS(shape, 'HollowShape')) !== undefined) { if (!go.ProfileCurve) { @@ -467,7 +484,7 @@ export class GameObject implements IGameObjectData } go.ProfileCurve = go.ProfileCurve | parseInt(HoleType[prop], 10); } - if (this.getFromXMLJS(shape, 'SculptEntry')) + if (this.getFromXMLJS(shape, 'SculptEntry') !== undefined) { const type = this.getFromXMLJS(shape, 'SculptType'); if (type !== false && type !== undefined) @@ -475,6 +492,10 @@ export class GameObject implements IGameObjectData const id = UUID.fromXMLJS(shape, 'SculptTexture'); if (id instanceof UUID) { + if (!go.extraParams) + { + go.extraParams = new ExtraParams(); + } if (type & SculptType.Mesh) { go.extraParams.setMeshData(type, id); @@ -486,7 +507,7 @@ export class GameObject implements IGameObjectData } } } - if (this.getFromXMLJS(shape, 'FlexiEntry')) + if (this.getFromXMLJS(shape, 'FlexiEntry') !== undefined) { const flexiSoftness = this.getFromXMLJS(shape, 'FlexiSoftness'); const flexiTension = this.getFromXMLJS(shape, 'FlexiTension'); @@ -505,10 +526,14 @@ export class GameObject implements IGameObjectData flexiForceY !== false && flexiForceZ !== false) { + if (!go.extraParams) + { + go.extraParams = new ExtraParams(); + } go.extraParams.setFlexiData(flexiSoftness, flexiTension, flexiDrag, flexiGravity, flexiWind, new Vector3([flexiForceX, flexiForceY, flexiForceZ])); } } - if (this.getFromXMLJS(shape, 'LightEntry')) + if (this.getFromXMLJS(shape, 'LightEntry') !== undefined) { const lightColorR = this.getFromXMLJS(shape, 'LightColorR'); const lightColorG = this.getFromXMLJS(shape, 'LightColorG'); @@ -527,6 +552,10 @@ export class GameObject implements IGameObjectData lightFalloff !== false && lightIntensity !== false) { + if (!go.extraParams) + { + go.extraParams = new ExtraParams(); + } go.extraParams.setLightData( new Color4(lightColorR, lightColorG, lightColorB, lightColorA), lightRadius, @@ -536,18 +565,14 @@ export class GameObject implements IGameObjectData ); } } - if (prop = this.getFromXMLJS(shape, 'ExtraParams')) + if ((prop = this.getFromXMLJS(shape, 'ExtraParams')) !== undefined) { const buf = Buffer.from(prop, 'base64'); go.extraParams = ExtraParams.from(buf); } } // TODO: TaskInventory - - - - console.log('BURP'); - process.exit(0); + return go; } static fromXML(xml: string) @@ -572,7 +597,21 @@ export class GameObject implements IGameObjectData throw new Error('Root part not found'); } const rootPart = GameObject.partFromXMLJS(result['SceneObjectPart'][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); } }); }); @@ -603,6 +642,359 @@ export class GameObject implements IGameObjectData return ''; } + setIfDefined(def?: number, v?: number) + { + if (def === undefined) + { + def = 0; + } + if (v === undefined) + { + return def; + } + else + { + return v; + } + } + + async setShape(PathCurve?: number, + ProfileCurve?: number, + PathBegin?: number, + PathEnd?: number, + PathScaleX?: number, + PathScaleY?: number, + PathShearX?: number, + PathShearY?: number, + PathTwist?: number, + PathTwistBegin?: number, + PathRadiusOffset?: number, + PathTaperX?: number, + PathTaperY?: number, + PathRevolutions?: number, + PathSkew?: number, + ProfileBegin?: number, + ProfileEnd?: number, + ProfileHollow?: number) + { + this.PathCurve = this.setIfDefined(this.PathCurve, PathCurve); + this.ProfileCurve = this.setIfDefined(this.ProfileCurve, ProfileCurve); + this.PathBegin = this.setIfDefined(this.PathBegin, PathBegin); + this.PathEnd = this.setIfDefined(this.PathEnd, PathEnd); + this.PathScaleX = this.setIfDefined(this.PathScaleX, PathScaleX); + this.PathScaleY = this.setIfDefined(this.PathScaleY, PathScaleY); + this.PathShearX = this.setIfDefined(this.PathShearX, PathShearX); + this.PathShearY = this.setIfDefined(this.PathShearY, PathShearY); + this.PathTwist = this.setIfDefined(this.PathTwist, PathTwist); + this.PathTwistBegin = this.setIfDefined(this.PathTwistBegin, PathTwistBegin); + this.PathRadiusOffset = this.setIfDefined(this.PathRadiusOffset, PathRadiusOffset); + this.PathTaperX = this.setIfDefined(this.PathTaperX, PathTaperX); + this.PathTaperY = this.setIfDefined(this.PathTaperY, PathTaperY); + this.PathRevolutions = this.setIfDefined(this.PathRevolutions, PathRevolutions); + this.PathSkew = this.setIfDefined(this.PathSkew, PathSkew); + this.ProfileBegin = this.setIfDefined(this.ProfileBegin, ProfileBegin); + this.ProfileEnd = this.setIfDefined(this.ProfileEnd, ProfileEnd); + this.ProfileHollow = this.setIfDefined(this.ProfileHollow, ProfileHollow); + if (!this.region) + { + return; + } + const msg = new ObjectShapeMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + ObjectLocalID: this.ID, + PathCurve: this.PathCurve, + ProfileCurve: this.ProfileCurve, + PathBegin: Utils.packBeginCut(this.PathBegin), + PathEnd: Utils.packEndCut(this.PathEnd), + PathScaleX: Utils.packPathScale(this.PathScaleX), + PathScaleY: Utils.packPathScale(this.PathScaleY), + PathShearX: Utils.packPathShear(this.PathShearX), + PathShearY: Utils.packPathShear(this.PathShearY), + PathTwist: Utils.packPathTwist(this.PathTwist), + PathTwistBegin: Utils.packPathTwist(this.PathTwistBegin), + PathRadiusOffset: Utils.packPathTwist(this.PathRadiusOffset), + PathTaperX: Utils.packPathTaper(this.PathTaperX), + PathTaperY: Utils.packPathTaper(this.PathTaperY), + PathRevolutions: Utils.packPathRevolutions(this.PathRevolutions), + PathSkew: Utils.packPathTwist(this.PathSkew), + ProfileBegin: Utils.packBeginCut(this.ProfileBegin), + ProfileEnd: Utils.packEndCut(this.ProfileEnd), + ProfileHollow: Utils.packProfileHollow(this.ProfileHollow) + } + ]; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); + } + + async setName(name: string) + { + this.name = name; + if (!this.region) + { + return; + } + const msg = new ObjectNameMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + LocalID: this.ID, + Name: Utils.StringToBuffer(name) + } + ]; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); + } + + private compareParam(name: string, param1: number | undefined, param2: number | undefined): boolean + { + if (param1 === undefined) + { + param1 = 0; + } + if (param2 === undefined) + { + param2 = 0; + } + if (Math.abs(param1 - param2) < 0.0001) + { + return true; + } + else + { + console.log('Failed ' + name + ' - ' + param1 + ' vs ' + param2); + return false; + } + } + + compareShape(obj: GameObject): boolean + { + return this.compareParam('PathCurve', this.PathCurve, obj.PathCurve) && + this.compareParam('ProfileCurve', this.ProfileCurve, obj.ProfileCurve) && + this.compareParam('PathBegin', this.PathBegin, obj.PathBegin) && + this.compareParam('PathEnd', this.PathEnd, obj.PathEnd) && + this.compareParam('PathScaleX', this.PathScaleX, obj.PathScaleX) && + this.compareParam('PathScaleY', this.PathScaleY, obj.PathScaleY) && + this.compareParam('PathShearX', this.PathShearX, obj.PathShearX) && + this.compareParam('PathShearY', this.PathShearY, obj.PathShearY) && + this.compareParam('PathTwist', this.PathTwist, obj.PathTwist) && + this.compareParam('PathTwistBegin', this.PathTwistBegin, obj.PathTwistBegin) && + this.compareParam('PathRadiusOffset', this.PathRadiusOffset, obj.PathRadiusOffset) && + this.compareParam('PathTaperX', this.PathTaperX, obj.PathTaperX) && + this.compareParam('PathTaperY', this.PathTaperY, obj.PathTaperY) && + this.compareParam('PathRevolutions', this.PathRevolutions, obj.PathRevolutions) && + this.compareParam('PathSkew', this.PathSkew, obj.PathSkew) && + this.compareParam('ProfileBegin', this.ProfileBegin, obj.ProfileBegin) && + this.compareParam('ProfileEnd', this.ProfileEnd, obj.ProfileEnd) && + this.compareParam('PRofileHollow', this.ProfileHollow, obj.ProfileHollow); + } + + async setGeometry(pos?: Vector3, rot?: Quaternion, scale?: Vector3) + { + const data = []; + if (pos !== undefined) + { + this.Position = pos; + data.push({ + ObjectLocalID: this.ID, + Type: UpdateType.Position, + Data: pos.getBuffer() + }); + } + if (rot !== undefined) + { + this.Rotation = rot; + data.push({ + ObjectLocalID: this.ID, + Type: UpdateType.Rotation, + Data: rot.getBuffer() + }) + } + if (scale !== undefined) + { + this.Scale = scale; + data.push({ + ObjectLocalID: this.ID, + Type: UpdateType.Scale, + Data: scale.getBuffer() + }) + } + if (!this.region || data.length === 0) + { + return; + } + const msg = new MultipleObjectUpdateMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = data; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); + } + + async linkTo(root: GameObject) + { + const msg = new ObjectLinkMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + ObjectLocalID: root.ID + }, + { + ObjectLocalID: this.ID + } + ]; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000); + } + + async setDescription(desc: string) + { + this.description = desc; + if (!this.region) + { + return; + } + const msg = new ObjectDescriptionMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = [ + { + LocalID: this.ID, + Description: Utils.StringToBuffer(desc) + } + ]; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); + } + + async setTextureEntry(e: TextureEntry) + { + this.TextureEntry = e; + if (!this.region) + { + return; + } + + await this.setTextureAndMediaURL(); + } + + private async setTextureAndMediaURL() + { + const msg = new ObjectImageMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + if (this.MediaURL === undefined) + { + this.MediaURL = ''; + } + if (this.TextureEntry === undefined) + { + this.TextureEntry = new TextureEntry(); + } + msg.ObjectData = [ + { + ObjectLocalID: this.ID, + TextureEntry: this.TextureEntry.toBuffer(), + MediaURL: Utils.StringToBuffer(this.MediaURL) + } + ]; + await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000); + } + + async setExtraParams(ex: ExtraParams) + { + this.extraParams = ex; + if (!this.region) + { + return; + } + + // Set ExtraParams + const msg = new ObjectExtraParamsMessage(); + msg.AgentData = { + AgentID: this.region.agent.agentID, + SessionID: this.region.circuit.sessionID + }; + msg.ObjectData = []; + let params = 0; + if (ex.lightData !== null) + { + params++; + const data = ex.lightData.getBuffer(); + msg.ObjectData.push({ + ObjectLocalID: this.ID, + ParamType: ExtraParamType.Light, + ParamInUse: (ex.lightData.Intensity !== 0.0), + ParamData: data, + ParamSize: data.length + }); + } + if (ex.flexibleData !== null) + { + params++; + const data = ex.flexibleData.getBuffer(); + msg.ObjectData.push({ + ObjectLocalID: this.ID, + ParamType: ExtraParamType.Flexible, + ParamInUse: true, + ParamData: data, + ParamSize: data.length + }); + } + if (ex.lightImageData !== null) + { + params++; + const data = ex.lightImageData.getBuffer(); + msg.ObjectData.push({ + ObjectLocalID: this.ID, + ParamType: ExtraParamType.LightImage, + ParamInUse: true, + ParamData: data, + ParamSize: data.length + }); + } + if (ex.sculptData !== null) + { + params++; + const data = ex.sculptData.getBuffer(); + msg.ObjectData.push({ + ObjectLocalID: this.ID, + ParamType: ExtraParamType.Sculpt, + ParamInUse: true, + ParamData: data, + ParamSize: data.length + }); + } + if (ex.meshData !== null) + { + params++; + const data = ex.meshData.getBuffer(); + msg.ObjectData.push({ + ObjectLocalID: this.ID, + ParamType: ExtraParamType.Mesh, + ParamInUse: true, + ParamData: data, + ParamSize: data.length + }); + } + if (params > 0) + { + const ack = this.region.circuit.sendMessage(msg, PacketFlags.Reliable); + await this.region.circuit.waitForAck(ack, 10000); + } + } + private async getInventoryXML(xml: XMLElementOrXMLNode, inv: InventoryItem) { if (!inv.assetID.equals(UUID.zero())) @@ -695,29 +1087,30 @@ export class GameObject implements IGameObjectData shape.ele('ProfileCurve', this.ProfileCurve); if (this.TextureEntry) { - shape.ele('TextureEntry', this.TextureEntry.binary.toString('base64')); + shape.ele('TextureEntry', this.TextureEntry.toBase64()); } if (this.extraParams) { shape.ele('ExtraParams', this.extraParams.toBase64()); } - shape.ele('PathBegin', this.PathBegin); + shape.ele('PathBegin', Utils.packBeginCut(Utils.numberOrZero(this.PathBegin))); shape.ele('PathCurve', this.PathCurve); - shape.ele('PathEnd', this.PathEnd); - shape.ele('PathRadiusOffset', this.PathRadiusOffset); - shape.ele('PathRevolutions', this.PathRevolutions); - shape.ele('PathScaleX', this.PathScaleX); - shape.ele('PathScaleY', this.PathScaleY); - shape.ele('PathShearX', this.PathShearX); - shape.ele('PathSkew', this.PathSkew); - shape.ele('PathTaperX', this.PathTaperX); - shape.ele('PathTaperY', this.PathTaperY); - shape.ele('PathTwist', this.PathTwist); - shape.ele('PathTwistBegin', this.PathTwistBegin); + shape.ele('PathEnd', Utils.packEndCut(Utils.numberOrZero(this.PathEnd))); + shape.ele('PathRadiusOffset', Utils.packPathTwist(Utils.numberOrZero(this.PathRadiusOffset))); + shape.ele('PathRevolutions', Utils.packPathRevolutions(Utils.numberOrZero(this.PathRevolutions))); + shape.ele('PathScaleX', Utils.packPathScale(Utils.numberOrZero(this.PathScaleX))); + shape.ele('PathScaleY', Utils.packPathScale(Utils.numberOrZero(this.PathScaleY))); + shape.ele('PathShearX', Utils.packPathShear(Utils.numberOrZero(this.PathShearX))); + shape.ele('PathShearY', Utils.packPathShear(Utils.numberOrZero(this.PathShearY))); + shape.ele('PathSkew', Utils.packPathTwist(Utils.numberOrZero(this.PathSkew))); + shape.ele('PathTaperX', Utils.packPathTaper(Utils.numberOrZero(this.PathTaperX))); + shape.ele('PathTaperY', Utils.packPathTaper(Utils.numberOrZero(this.PathTaperY))); + shape.ele('PathTwist', Utils.packPathTwist(Utils.numberOrZero(this.PathTwist))); + shape.ele('PathTwistBegin', Utils.packPathTwist(Utils.numberOrZero(this.PathTwistBegin))); shape.ele('PCode', this.PCode); - shape.ele('ProfileBegin', this.ProfileBegin); - shape.ele('ProfileEnd', this.ProfileEnd); - shape.ele('ProfileHollow', this.ProfileHollow); + 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); if (this.ProfileCurve) diff --git a/lib/classes/public/LLMesh.ts b/lib/classes/public/LLMesh.ts new file mode 100644 index 0000000..f0e12fd --- /dev/null +++ b/lib/classes/public/LLMesh.ts @@ -0,0 +1,630 @@ +import {Utils} from '../Utils'; +import * as zlib from 'zlib'; +import * as LLSD from '@caspertech/llsd'; +import {UUID} from '../UUID'; +import {LLSubMesh} from './interfaces/LLSubMesh'; +import {Vector3} from '../Vector3'; +import {Vector2} from '../Vector2'; +import {LLSkin} from './interfaces/LLSkin'; +import {mat4} from '../../tsm/mat4'; +import {LLPhysicsConvex} from './interfaces/LLPhysicsConvex'; + +export class LLMesh +{ + version: number; + lodLevels: {[key: string]: LLSubMesh[]} = {}; + physicsConvex: LLPhysicsConvex; + skin?: LLSkin; + creatorID: UUID; + date: Date; + + static async from(buf: Buffer): Promise + { + const llmesh = new LLMesh(); + const binData = new LLSD.Binary(Array.from(buf), 'BASE64'); + let obj = LLSD.LLSD.parseBinary(binData); + if (obj['result'] === undefined) + { + throw new Error('Failed to decode header'); + } + if (obj['position'] === undefined) + { + throw new Error('Position not reported'); + } + const startPos = parseInt(obj['position'], 10); + obj = obj['result']; + if (!obj['version']) + { + throw new Error('No version found'); + } + if (!obj['creator']) + { + throw new Error('Creator UUID not found'); + } + if (obj['date'] === undefined) + { + throw new Error('Date not found'); + } + llmesh.creatorID = new UUID(obj['creator'].toString()); + llmesh.date = obj['date']; + llmesh.version = parseInt(obj['version'], 10); + for (const key of Object.keys(obj)) + { + const o = obj[key]; + if (typeof o === 'object' && o !== null && o['offset'] !== undefined) + { + 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 mesh = LLSD.LLSD.parseBinary(new LLSD.Binary(Array.from(deflated), 'BASE64')); + if (mesh['result'] === undefined) + { + throw new Error('Failed to parse compressed submesh data'); + } + if (key === 'physics_convex') + { + llmesh.physicsConvex = this.parsePhysicsConvex(mesh['result']); + } + else if (key === 'skin') + { + llmesh.skin = this.parseSkin(mesh['result']); + } + else if (key === 'physics_havok' || key === 'physics_cost_data') + { + // Used by the simulator + } + else + { + llmesh.lodLevels[key] = this.parseLODLevel(mesh['result']); + } + + } + } + return llmesh; + } + static parseSkin(mesh: any): LLSkin + { + if (!mesh['joint_names']) + { + throw new Error('Joint names missing from skin'); + } + if (!mesh['bind_shape_matrix']) + { + throw new Error('Bind shape matrix missing from skin'); + } + if (!mesh['inverse_bind_matrix']) + { + throw new Error('Inverse bind matrix missing from skin'); + } + const skin: LLSkin = { + jointNames: mesh['joint_names'], + bindShapeMatrix: new mat4(mesh['bind_shape_matrix']), + inverseBindMatrix: [] + }; + if (mesh['inverse_bind_matrix']) + { + skin.inverseBindMatrix = []; + for (const inv of mesh['inverse_bind_matrix']) + { + skin.inverseBindMatrix.push(new mat4(inv)); + } + } + if (mesh['alt_inverse_bind_matrix']) + { + skin.altInverseBindMatrix = []; + for (const inv of mesh['alt_inverse_bind_matrix']) + { + skin.altInverseBindMatrix.push(new mat4(inv)); + } + } + if (mesh['pelvis_offset']) + { + skin.pelvisOffset = new mat4(mesh['pelvis_offset']); + } + return skin; + } + + static fixReal(arr: number[]): number[] + { + const newArr = []; + for (let num of arr) + { + if ((num >> 0) === num && !((num === 0) && ((1 / num) === -Infinity))) + { + num += 0.0000000001; + } + newArr.push(num); + } + return newArr; + } + static parsePhysicsConvex(mesh: any): LLPhysicsConvex + { + const conv: LLPhysicsConvex = { + boundingVerts: [], + domain: { + min: new Vector3([-0.5, -0.5, -0.5]), + max: new Vector3([0.5, 0.5, 0.5]) + } + }; + if (mesh['Min']) + { + conv.domain.min.x = mesh['Min'][0]; + conv.domain.min.y = mesh['Min'][1]; + conv.domain.min.z = mesh['Min'][2]; + } + if (mesh['Max']) + { + conv.domain.max.x = mesh['Max'][0]; + conv.domain.max.y = mesh['Max'][1]; + conv.domain.max.z = mesh['Max'][2]; + } + if (mesh['HullList']) + { + if (!mesh['Positions']) + { + throw new Error('Positions must be supplied if hull list is present'); + } + conv.positions = this.decodeByteDomain3(mesh['Positions'].toArray(), conv.domain.min, conv.domain.max); + conv.hullList = mesh['HullList'].toArray(); + if (conv.hullList === undefined) + { + throw new Error('HullList undefined'); + } + else + { + let totalPoints = 0; + for (const hull of conv.hullList) + { + totalPoints += hull; + } + if (conv.positions.length !== totalPoints) + { + throw new Error('Hull list expected number of points does not match number of positions: ' + totalPoints + ' vs ' + conv.positions.length); + } + } + } + if (!mesh['BoundingVerts']) + { + throw new Error('BoundingVerts is required'); + } + conv.boundingVerts = this.decodeByteDomain3(mesh['BoundingVerts'].toArray(), conv.domain.min, conv.domain.max); + return conv; + } + static parseLODLevel(mesh: any): LLSubMesh[] + { + const list: LLSubMesh[] = []; + for (const submesh of mesh) + { + const decoded: LLSubMesh = { + positionDomain: { + min: new Vector3([-0.5, -0.5, -0.5]), + max: new Vector3([0.5, 0.5, 0.5]) + } + }; + if (submesh['NoGeometry']) + { + decoded.noGeometry = true; + list.push(decoded); + } + else + { + decoded.position = []; + if (!submesh['Position']) + { + throw new Error('Submesh does not contain position data'); + } + if (decoded.positionDomain !== undefined) + { + if (submesh['PositionDomain']) + { + if (submesh['PositionDomain']['Max'] !== undefined) + { + const dom = submesh['PositionDomain']['Max']; + decoded.positionDomain.max.x = dom[0]; + decoded.positionDomain.max.y = dom[1]; + decoded.positionDomain.max.z = dom[2]; + } + if (submesh['PositionDomain']['Min'] !== undefined) + { + const dom = submesh['PositionDomain']['Min']; + decoded.positionDomain.min.x = dom[0]; + decoded.positionDomain.min.y = dom[1]; + decoded.positionDomain.min.z = dom[2]; + } + } + decoded.position = this.decodeByteDomain3(submesh['Position'].toArray(), decoded.positionDomain.min, decoded.positionDomain.max); + } + if (submesh['Normal']) + { + decoded.normal = this.decodeByteDomain3(submesh['Normal'].toArray(), new Vector3([-1.0, -1.0, -1.0]), new Vector3([1.0, 1.0, 1.0])); + if (decoded.normal.length !== decoded.position.length) + { + throw new Error('Normal length does not match vertex position length'); + } + } + if (submesh['TexCoord0']) + { + decoded.texCoord0Domain = { + min: new Vector2([-0.5, -0.5]), + max: new Vector2([0.5, 0.5]) + }; + if (submesh['TexCoord0Domain']) + { + if (submesh['TexCoord0Domain']['Max'] !== undefined) + { + const dom = submesh['TexCoord0Domain']['Max']; + decoded.texCoord0Domain.max.x = dom[0]; + decoded.texCoord0Domain.max.y = dom[1]; + } + if (submesh['TexCoord0Domain']['Min'] !== undefined) + { + const dom = submesh['TexCoord0Domain']['Min']; + decoded.texCoord0Domain.min.x = dom[0]; + decoded.texCoord0Domain.min.y = dom[1]; + } + } + else + { + throw new Error('TexCoord0Domain is required if Texcoord0 is present'); + } + decoded.texCoord0 = this.decodeByteDomain2(submesh['TexCoord0'].toArray(), decoded.texCoord0Domain.min, decoded.texCoord0Domain.max); + } + if (!submesh['TriangleList']) + { + throw new Error('TriangleList is required'); + } + const indexBuf = new Buffer(submesh['TriangleList'].toArray()); + decoded.triangleList = []; + for (let pos = 0; pos < indexBuf.length; pos = pos + 2) + { + const vertIndex = indexBuf.readUInt16LE(pos); + if (vertIndex >= decoded.position.length) + { + throw new Error('Vertex index out of range: ' + vertIndex) + } + decoded.triangleList.push(vertIndex); + } + if (submesh['Weights']) + { + const skinBuf = new Buffer(submesh['Weights'].toArray()); + decoded.weights = []; + let pos = 0; + while (pos < skinBuf.length) + { + const entry: {[key: number]: number} = {}; + for (let x = 0; x < 4; x++) + { + const jointNum = skinBuf.readUInt8(pos++); + if (jointNum === 0xFF) + { + break; + } + const value = skinBuf.readUInt16LE(pos); pos = pos + 2; + entry[jointNum] = value; + } + decoded.weights.push(entry); + } + if (decoded.weights.length !== decoded.position.length) + { + throw new Error('Weight list differs in length from position list'); + } + } + list.push(decoded); + } + } + return list; + } + static decodeByteDomain3(posArray: number[], minDomain: Vector3, maxDomain: Vector3): Vector3[] + { + const result: Vector3[] = []; + const buf = new Buffer(posArray); + for (let idx = 0; idx < posArray.length; idx = idx + 6) + { + const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x); + const posY = this.normalizeDomain(buf.readUInt16LE(idx + 2), minDomain.y, maxDomain.y); + const posZ = this.normalizeDomain(buf.readUInt16LE(idx + 4), minDomain.z, maxDomain.z); + result.push(new Vector3([posX, posY, posZ])); + } + return result; + } + static decodeByteDomain2(posArray: number[], minDomain: Vector2, maxDomain: Vector2): Vector2[] + { + const result: Vector2[] = []; + const buf = new Buffer(posArray); + for (let idx = 0; idx < posArray.length; idx = idx + 4) + { + const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x); + const posY = this.normalizeDomain(buf.readUInt16LE(idx + 2), minDomain.y, maxDomain.y); + result.push(new Vector2([posX, posY])); + } + return result; + } + static normalizeDomain(value: number, min: number, max: number) + { + 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: { + NoGeometry?: true, + Position?: any, // LLSD.Binary + PositionDomain?: { + Min: number[], + Max: number[] + }, + Normal?: any, // LLSD.Binary + TexCoord0?: any, // LLSD.Binary + TexCoord0Domain?: { + Min: number[], + Max: number[] + }, + TriangleList?: any, // LLSD.Binary + Weights?: any // LLSD.Binary + } = {}; + if (mesh.noGeometry === true) + { + data.NoGeometry = true; + return data; + } + if (!mesh.position) + { + throw new Error('No position data when encoding submesh'); + } + if (mesh.positionDomain !== undefined) + { + data.Position = new LLSD.Binary(Array.from(this.expandFromDomain(mesh.position, mesh.positionDomain.min, mesh.positionDomain.max))); + data.PositionDomain = { + Min: LLMesh.fixReal(mesh.positionDomain.min.toArray()), + Max: LLMesh.fixReal(mesh.positionDomain.max.toArray()) + }; + } + if (mesh.texCoord0 && mesh.texCoord0Domain !== undefined) + { + data.TexCoord0 = new LLSD.Binary(Array.from(this.expandFromDomain(mesh.texCoord0, mesh.texCoord0Domain.min, mesh.texCoord0Domain.max))); + data.TexCoord0Domain = { + Min: LLMesh.fixReal(mesh.texCoord0Domain.min.toArray()), + Max: LLMesh.fixReal(mesh.texCoord0Domain.max.toArray()) + }; + } + if (mesh.normal) + { + data.Normal = new LLSD.Binary(Array.from(this.expandFromDomain(mesh.normal, new Vector3([-1.0, -1.0, -1.0]), new Vector3([1.0, 1.0, 1.0])))); + } + if (mesh.triangleList) + { + const triangles = Buffer.allocUnsafe(mesh.triangleList.length * 2); + let pos = 0; + for (let x = 0; x < mesh.triangleList.length; x++) + { + triangles.writeUInt16LE(mesh.triangleList[x], pos); pos = pos + 2; + } + data.TriangleList = new LLSD.Binary(Array.from(triangles)); + } + else + { + throw new Error('Triangle list is required'); + } + if (mesh.weights) + { + // Calculate how much space we need + let spaceNeeded = 0; + for (const weight of mesh.weights) + { + const keys = Object.keys(weight); + spaceNeeded = spaceNeeded + keys.length * 3; + if (keys.length < 4) + { + spaceNeeded = spaceNeeded + 1; + } + } + const weightBuff = Buffer.allocUnsafe(spaceNeeded); + let pos = 0; + for (const weight of mesh.weights) + { + const keys = Object.keys(weight); + for (const jointID of keys) + { + weightBuff.writeUInt8(parseInt(jointID, 10), pos++); + weightBuff.writeUInt16LE(weight[parseInt(jointID, 10)], pos); pos = pos + 2; + } + if (keys.length < 4) + { + weightBuff.writeUInt8(0xFF, pos++); + } + } + data.Weights = new LLSD.Binary(Array.from(weightBuff)); + } + return data; + } + private expandFromDomain(data: Vector3[] | Vector2[], domainMin: Vector3 | Vector2, domainMax: Vector3 | Vector2) + { + let length = 4; + if (data.length > 0 && data[0] instanceof Vector3) + { + length = 6; + } + const buf = Buffer.allocUnsafe(data.length * length); + let pos = 0; + for (const c of data) + { + const coord: Vector3 | Vector2 = c; + const sizeX = domainMax.x - domainMin.x; + const newX = Math.round(((coord.x - domainMin.x) / sizeX) * 65535); + + const sizeY = domainMax.y - domainMin.y; + const newY = Math.round(((coord.y - domainMin.y) / sizeY) * 65535); + buf.writeUInt16LE(newX, pos); pos = pos + 2; + buf.writeUInt16LE(newY, pos); pos = pos + 2; + if (coord instanceof Vector3 && domainMin instanceof Vector3 && domainMax instanceof Vector3) + { + const sizeZ = domainMax.z - domainMin.z; + const newZ = Math.round(((coord.z - domainMin.z) / sizeZ) * 65535); + buf.writeUInt16LE(newZ, pos); pos = pos + 2; + } + } + return buf; + } + private async encodeLODLevel(name: string, submeshes: LLSubMesh[]): Promise + { + const smList = []; + for (const sub of submeshes) + { + smList.push(this.encodeSubMesh(sub)) + } + const mesh = LLSD.LLSD.formatBinary(smList); + return await LLMesh.deflate(Buffer.from(mesh.toArray())); + } + private async encodePhysicsConvex(conv: LLPhysicsConvex): Promise + { + const llsd: { + 'HullList'?: any, + 'Positions'?: any, + 'BoundingVerts'?: any, + 'Min': number[], + 'Max': number[]; + } = { + Min: LLMesh.fixReal(conv.domain.min.toArray()), + Max: LLMesh.fixReal(conv.domain.max.toArray()) + }; + const sizeX = conv.domain.max.x - conv.domain.min.x; + const sizeY = conv.domain.max.y - conv.domain.min.y; + const sizeZ = conv.domain.max.z - conv.domain.min.z; + if (conv.hullList) + { + if (!conv.positions) + { + throw new Error('Positions must be present if hullList is set.') + } + llsd.HullList = new LLSD.Binary(conv.hullList); + const buf = Buffer.allocUnsafe(conv.positions.length * 6); + let pos = 0; + for (const vec of conv.positions) + { + buf.writeUInt16LE(Math.round(((vec.x - conv.domain.min.x) / sizeX) * 65535), pos); pos = pos + 2; + buf.writeUInt16LE(Math.round(((vec.y - conv.domain.min.y) / sizeY) * 65535), pos); pos = pos + 2; + buf.writeUInt16LE(Math.round(((vec.z - conv.domain.min.z) / sizeZ) * 65535), pos); pos = pos + 2; + } + llsd.Positions = new LLSD.Binary(Array.from(buf)); + } + { + const buf = Buffer.allocUnsafe(conv.boundingVerts.length * 6); + let pos = 0; + for (const vec of conv.boundingVerts) + { + buf.writeUInt16LE(Math.round(((vec.x - conv.domain.min.x) / sizeX)) * 65535, pos); + pos = pos + 2; + buf.writeUInt16LE(Math.round(((vec.y - conv.domain.min.y) / sizeY)) * 65535, pos); + pos = pos + 2; + buf.writeUInt16LE(Math.round(((vec.z - conv.domain.min.z) / sizeZ)) * 65535, pos); + pos = pos + 2; + } + llsd.BoundingVerts = new LLSD.Binary(Array.from(buf)); + } + const mesh = LLSD.LLSD.formatBinary(llsd); + return await LLMesh.deflate(Buffer.from(mesh.toArray())); + } + private async encodeSkin(skin: LLSkin): Promise + { + const llsd: {[key: string]: any} = {}; + llsd['joint_names'] = skin.jointNames; + llsd['bind_shape_matrix'] = skin.bindShapeMatrix.toArray(); + llsd['inverse_bind_matrix'] = []; + for (const matrix of skin.inverseBindMatrix) + { + llsd['inverse_bind_matrix'].push(matrix.toArray()) + } + if (skin.altInverseBindMatrix) + { + llsd['alt_inverse_bind_matrix'] = []; + for (const matrix of skin.altInverseBindMatrix) + { + llsd['alt_inverse_bind_matrix'].push(matrix.toArray()) + } + } + if (skin.pelvisOffset) + { + llsd['pelvis_offset'] = skin.pelvisOffset.toArray(); + } + const mesh = LLSD.LLSD.formatBinary(llsd); + return await LLMesh.deflate(Buffer.from(mesh.toArray())); + } + async toAsset(): Promise + { + const llsd: {[key: string]: any} = { + 'creator': new LLSD.UUID(this.creatorID.toString()), + 'version': this.version, + 'date': null + }; + let offset = 0; + const bufs = []; + for (const lod of Object.keys(this.lodLevels)) + { + const lodBlob = await this.encodeLODLevel(lod, this.lodLevels[lod]); + llsd[lod] = { + 'offset': offset, + 'size': lodBlob.length + }; + offset += lodBlob.length; + bufs.push(lodBlob); + } + if (this.physicsConvex) + { + const physBlob = await this.encodePhysicsConvex(this.physicsConvex); + llsd['physics_convex'] = { + 'offset': offset, + 'size': physBlob.length + }; + offset += physBlob.length; + bufs.push(physBlob); + + } + if (this.skin) + { + const skinBlob = await this.encodeSkin(this.skin); + llsd['skin'] = { + 'offset': offset, + 'size': skinBlob.length + }; + offset += skinBlob.length; + bufs.push(skinBlob); + } + bufs.unshift(Buffer.from(LLSD.LLSD.formatBinary(llsd).toArray())); + return Buffer.concat(bufs); + } +} diff --git a/lib/classes/public/LightData.ts b/lib/classes/public/LightData.ts index 6ad9682..85431b9 100644 --- a/lib/classes/public/LightData.ts +++ b/lib/classes/public/LightData.ts @@ -39,4 +39,10 @@ export class LightData buf.writeFloatLE(this.Cutoff, pos); pos = pos + 4; buf.writeFloatLE(this.Falloff, pos); } + getBuffer(): Buffer + { + const buf = Buffer.allocUnsafe(16); + this.writeToBuffer(buf, 0); + return buf; + } } diff --git a/lib/classes/public/LightImageData.ts b/lib/classes/public/LightImageData.ts index 22e0bab..f5ed34c 100644 --- a/lib/classes/public/LightImageData.ts +++ b/lib/classes/public/LightImageData.ts @@ -20,4 +20,10 @@ export class LightImageData this.texture.writeToBuffer(buf, pos); pos = pos + 16; this.params.writeToBuffer(buf, pos, false); } + getBuffer(): Buffer + { + const buf = Buffer.allocUnsafe(28); + this.writeToBuffer(buf, 0); + return buf; + } } diff --git a/lib/classes/public/MeshData.ts b/lib/classes/public/MeshData.ts index b32eae5..44744d2 100644 --- a/lib/classes/public/MeshData.ts +++ b/lib/classes/public/MeshData.ts @@ -23,4 +23,10 @@ export class MeshData this.meshData.writeToBuffer(buf, pos); pos = pos + 16; buf.writeUInt8(this.type, pos); } + getBuffer(): Buffer + { + const buf = Buffer.allocUnsafe(17); + this.writeToBuffer(buf, 0); + return buf; + } } diff --git a/lib/classes/public/SculptData.ts b/lib/classes/public/SculptData.ts index fadc70b..84ce3eb 100644 --- a/lib/classes/public/SculptData.ts +++ b/lib/classes/public/SculptData.ts @@ -23,4 +23,10 @@ export class SculptData this.texture.writeToBuffer(buf, pos); pos = pos + 16; buf.writeUInt8(this.type, pos); } + getBuffer(): Buffer + { + const buf = Buffer.allocUnsafe(17); + this.writeToBuffer(buf, 0); + return buf; + } } diff --git a/lib/classes/public/interfaces/LLPhysicsConvex.ts b/lib/classes/public/interfaces/LLPhysicsConvex.ts new file mode 100644 index 0000000..81af03f --- /dev/null +++ b/lib/classes/public/interfaces/LLPhysicsConvex.ts @@ -0,0 +1,12 @@ +import {Vector3} from '../../Vector3'; + +export interface LLPhysicsConvex +{ + hullList?: number[]; + positions?: Vector3[]; + boundingVerts: Vector3[]; + domain: { + min: Vector3, + max: Vector3 + } +} diff --git a/lib/classes/public/interfaces/LLSkin.ts b/lib/classes/public/interfaces/LLSkin.ts new file mode 100644 index 0000000..20a8c3f --- /dev/null +++ b/lib/classes/public/interfaces/LLSkin.ts @@ -0,0 +1,10 @@ +import {mat4} from '../../../tsm/mat4'; + +export interface LLSkin +{ + jointNames: string[]; + bindShapeMatrix: mat4; + inverseBindMatrix: mat4[]; + altInverseBindMatrix?: mat4[]; + pelvisOffset?: mat4; +} diff --git a/lib/classes/public/interfaces/LLSubMesh.ts b/lib/classes/public/interfaces/LLSubMesh.ts new file mode 100644 index 0000000..baf8394 --- /dev/null +++ b/lib/classes/public/interfaces/LLSubMesh.ts @@ -0,0 +1,21 @@ +import {Vector3} from '../../Vector3'; +import {Vector2} from '../../Vector2'; + +export interface LLSubMesh +{ + noGeometry?: true, + position?: Vector3[], + positionDomain?: { + min: Vector3, + max: Vector3 + }, + normal?: Vector3[], + texCoord0?: Vector2[], + texCoord0Domain?: { + min: Vector2, + max: Vector2 + } + triangleList?: number[], + weights?: {[key: number]: number}[], + +} diff --git a/lib/enums/FolderType.ts b/lib/enums/FolderType.ts new file mode 100644 index 0000000..41f8b5f --- /dev/null +++ b/lib/enums/FolderType.ts @@ -0,0 +1,32 @@ +export enum FolderType +{ + None = -1, + Texture = 0, + Sound = 1, + CallingCard = 2, + Landmark = 3, + Clothing = 5, + Object = 6, + Notecard = 7, + Root = 8, + LSLText = 10, + BodyPart = 13, + Trash = 14, + Snapshot = 15, + LostAndFound = 16, + Animation = 20, + Gesture = 21, + Favorites = 23, + EnsembleStart = 26, + EnsembleEnd = 45, + CurrentOutfit = 46, + Outfit = 47, + MyOutfits = 48, + Mesh = 49, + Inbox = 50, + Outbox = 51, + BasicRoot = 52, + MarketplaceListings = 53, + MarkplaceStock = 54, + Suitcase = 100 +} diff --git a/lib/enums/UpdateType.ts b/lib/enums/UpdateType.ts new file mode 100644 index 0000000..02f0976 --- /dev/null +++ b/lib/enums/UpdateType.ts @@ -0,0 +1,9 @@ +export enum UpdateType +{ + None = 0x00, + Position = 0x01, + Rotation = 0x02, + Scale = 0x04, + Linked = 0x08, + Uniform = 0x10 +} \ No newline at end of file diff --git a/lib/events/SelectedObjectEvent.ts b/lib/events/SelectedObjectEvent.ts new file mode 100644 index 0000000..6780b69 --- /dev/null +++ b/lib/events/SelectedObjectEvent.ts @@ -0,0 +1,6 @@ +import {GameObject} from '..'; + +export class SelectedObjectEvent +{ + object: GameObject +} diff --git a/lib/index.ts b/lib/index.ts index 3334b6f..295ebe9 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -89,6 +89,8 @@ import {TransferStatus} from './enums/TransferStatus'; import {LLWearable} from './classes/LLWearable'; import {ParticleSystem} from './classes/ParticleSystem'; import {ExtraParams} from './classes/public/ExtraParams'; +import {LLMesh} from './classes/public/LLMesh'; +import {FolderType} from './enums/FolderType'; export { Bot, @@ -98,7 +100,6 @@ export { ClientEvents, BVH, ChatSourceType, - BotOptionFlags, UUID, Vector3, Vector2, @@ -107,9 +108,11 @@ export { LLWearable, ParticleSystem, ExtraParams, + FolderType, // Flags AgentFlags, + BotOptionFlags, CompressedFlags, ControlFlags, DecodeFlags, @@ -183,6 +186,7 @@ export { RegionEnvironment, SculptData, MeshData, + LLMesh, // Public Interfaces GlobalPosition, diff --git a/lib/tsm/mat4.ts b/lib/tsm/mat4.ts index af962c4..db3cc5b 100644 --- a/lib/tsm/mat4.ts +++ b/lib/tsm/mat4.ts @@ -632,6 +632,10 @@ export class mat4 return this; } + toArray() + { + return Array.from(this.values); + } }