diff --git a/lib/Bot.ts b/lib/Bot.ts index ae48980..ca210d3 100644 --- a/lib/Bot.ts +++ b/lib/Bot.ts @@ -203,7 +203,7 @@ export class Bot return this.agent.agentID; } - async connectToSim(requested: boolean) + async connectToSim(requested: boolean = true) { this.agent.setCurrentRegion(this.currentRegion); const circuit = this.currentRegion.circuit; diff --git a/lib/classes/Agent.ts b/lib/classes/Agent.ts index d44e3f2..d215344 100644 --- a/lib/classes/Agent.ts +++ b/lib/classes/Agent.ts @@ -21,7 +21,7 @@ import {Utils} from './Utils'; import {ClientEvents} from './ClientEvents'; import Timer = NodeJS.Timer; import {ControlFlags, GroupChatSessionAgentListEvent, AgentFlags, PacketFlags, AssetType} from '..'; -import {GameObject} from './GameObject'; +import {GameObject} from './public/GameObject'; export class Agent { diff --git a/lib/classes/BitPack.ts b/lib/classes/BitPack.ts new file mode 100644 index 0000000..1b72861 --- /dev/null +++ b/lib/classes/BitPack.ts @@ -0,0 +1,153 @@ +export class BitPack +{ + static MAX_BITS = 8; + static ON = [1]; + static OFF = [0]; + + private bitPos = 0; + + constructor(private Data: Buffer, private bytePos: number) + { + + } + + get BytePos(): number + { + if (this.bytePos !== 0 && this.bitPos === 0) + { + return this.bytePos - 1; + } + else + { + return this.bytePos; + } + } + + get BitPos(): number + { + return this.bitPos; + } + + UnpackFloat(): number + { + const output = this.UnpackBitsBuffer(32); + return output.readFloatLE(0); + } + + UnpackBits(count: number): number + { + const output = this.UnpackBitsBuffer(count); + return output.readInt32LE(0); + } + + UnpackUBits(count: number) + { + const output = this.UnpackBitsBuffer(count); + return output.readUInt32LE(0); + } + + UnpsckShort(): number + { + return this.UnpackBits(16); + } + + UnpackUShort(): number + { + return this.UnpackUBits(16); + } + + UnpackInt(): number + { + return this.UnpackBits(32); + } + + UnpackUInt(): number + { + return this.UnpackUBits(32); + } + + UnpackByte(): number + { + const output = this.UnpackBitsBuffer(8); + return output[0]; + } + + UnpackFixed(signed: boolean, intBits: number, fracBits: number): number + { + let maxVal; + let totalBits = intBits + fracBits; + + if (signed) + { + totalBits++; + } + maxVal = 1 << intBits; + let fixedVal = 0; + if (totalBits <= 8) + { + fixedVal = this.UnpackByte(); + } + else if (totalBits <= 16) + { + fixedVal = this.UnpackUBits(16); + } + else if (totalBits <= 31) + { + fixedVal = this.UnpackUBits(32); + } + else + { + return 0.0; + } + + fixedVal /= (1 << fracBits); + if (signed) + { + fixedVal -= maxVal; + } + return fixedVal; + } + + UnpackBitsBuffer(totalCount: number): Buffer + { + const newBuf = Buffer.alloc(4, 0); + let count = 0; + let curBytePos = 0; + let curBitPos = 0; + + while (totalCount > 0) + { + if (totalCount > BitPack.MAX_BITS) + { + count = BitPack.MAX_BITS; + totalCount -= BitPack.MAX_BITS; + } + else + { + count = totalCount; + totalCount = 0; + } + while (count > 0) + { + newBuf[curBytePos] <<= 1; + if ((this.Data[this.bytePos] & (0x80 >> this.bitPos++)) !== 0) + { + ++newBuf[curBytePos]; + } + --count; + ++curBitPos; + if (this.bitPos >= BitPack.MAX_BITS) + { + this.bitPos = 0; + ++this.bytePos; + } + if (curBitPos >= BitPack.MAX_BITS) + { + curBitPos = 0; + ++curBytePos; + } + } + } + return newBuf; + } +} diff --git a/lib/classes/Caps.ts b/lib/classes/Caps.ts index f79f138..e56f947 100644 --- a/lib/classes/Caps.ts +++ b/lib/classes/Caps.ts @@ -189,6 +189,28 @@ export class Caps }); } + requestGet(url: string): Promise + { + return new Promise((resolve, reject) => + { + request({ + 'uri': url, + 'rejectUnauthorized': false, + 'method': 'GET' + }, (err, res, body) => + { + if (err) + { + reject(err); + } + else + { + resolve(body); + } + }); + }); + } + waitForSeedCapability(): Promise { return new Promise((resolve, reject) => @@ -246,13 +268,54 @@ export class Caps }); } - capsRequestXML(capability: string, data: any): Promise + capsGetXML(capability: string): Promise { return new Promise((resolve, reject) => { this.getCapability(capability).then((url) => { - this.request(url, LLSD.LLSD.formatXML(data), 'application/llsd+xml').then((body: string) => + this.requestGet(url).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); + }); + }); + } + + capsRequestXML(capability: string, data: any, debug = false): Promise + { + if (debug) + { + console.log(data); + } + return new Promise((resolve, reject) => + { + 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 diff --git a/lib/classes/Circuit.ts b/lib/classes/Circuit.ts index 382f3ba..74f6ce5 100644 --- a/lib/classes/Circuit.ts +++ b/lib/classes/Circuit.ts @@ -1,6 +1,6 @@ import {UUID} from './UUID'; -import {Socket} from 'dgram'; import * as dgram from 'dgram'; +import {Socket} from 'dgram'; import {Packet} from './Packet'; import {MessageBase} from './MessageBase'; import {PacketAckMessage} from './messages/PacketAck'; @@ -8,13 +8,17 @@ import {Message} from '../enums/Message'; import {StartPingCheckMessage} from './messages/StartPingCheck'; import {CompletePingCheckMessage} from './messages/CompletePingCheck'; import {Subscription} from 'rxjs/internal/Subscription'; -import { filter } from 'rxjs/operators'; -import Timer = NodeJS.Timer; +import {filter} from 'rxjs/operators'; import {ClientEvents} from './ClientEvents'; import {FilterResponse} from '../enums/FilterResponse'; import {Subject} from 'rxjs/internal/Subject'; -import {PacketFlags} from '..'; +import {AssetType, PacketFlags, Utils} from '..'; import {TimeoutError} from './TimeoutError'; +import {RequestXferMessage} from './messages/RequestXfer'; +import {SendXferPacketMessage} from './messages/SendXferPacket'; +import {ConfirmXferPacketMessage} from './messages/ConfirmXferPacket'; +import Timer = NodeJS.Timer; +import {AbortXferMessage} from './messages/AbortXfer'; export class Circuit { @@ -79,6 +83,130 @@ export class Circuit return packet.sequenceNumber; } + XferFile(fileName: string, deleteOnCompletion: boolean, useBigPackets: boolean, vFileID: UUID, vFileType: AssetType, fromCache: boolean): Promise + { + return new Promise((resolve, reject) => + { + let subscription: null | Subscription = null; + let timeout: Timer | null = null; + const progress = setInterval(() => + { + console.log( ' ... Got ' + Object.keys(receivedChunks).length + ' packets'); + }, 5000); + const resetTimeout = function () + { + if (timeout !== null) + { + clearTimeout(timeout); + } + timeout = setTimeout(() => + { + if (subscription !== null) + { + subscription.unsubscribe(); + } + clearInterval(progress); + reject(new Error('Xfer Timeout')); + }, 10000); + }; + resetTimeout(); + const xferRequest = new RequestXferMessage(); + const transferID = UUID.random().getLong(); + xferRequest.XferID = { + ID: transferID, + Filename: Utils.StringToBuffer(fileName), + FilePath: (fromCache) ? 4 : 0, + DeleteOnCompletion: deleteOnCompletion, + UseBigPackets: useBigPackets, + VFileID: vFileID, + VFileType: vFileType + }; + this.sendMessage(xferRequest, PacketFlags.Reliable); + let finished = false; + let finishID = 0; + const receivedChunks: { [key: number]: Buffer } = {}; + + subscription = this.subscribeToMessages([ + Message.SendXferPacket, + Message.AbortXfer + ], (packet: Packet) => + { + switch (packet.message.id) + { + case Message.AbortXfer: + { + const message = packet.message as AbortXferMessage; + if (message.XferID.ID.compare(transferID) === 0) + { + if (timeout !== null) + { + clearTimeout(timeout); + } + if (subscription !== null) + { + subscription.unsubscribe(); + } + clearInterval(progress); + reject(new Error('Xfer Aborted')); + } + break; + } + case Message.SendXferPacket: + { + const message = packet.message as SendXferPacketMessage; + if (message.XferID.ID.compare(transferID) === 0) + { + resetTimeout(); + const packetNum = message.XferID.Packet & 0x7FFFFFFF; + const finishedNow = message.XferID.Packet & 0x80000000; + receivedChunks[packetNum] = message.DataPacket.Data; + const confirm = new ConfirmXferPacketMessage(); + confirm.XferID = { + ID: transferID, + Packet: packetNum + }; + this.sendMessage(confirm, PacketFlags.Reliable); + + if (finishedNow) + { + finished = true; + finishID = packetNum; + } + + if (finished) + { + // Check if we have all the pieces + for (let x = 0; x <= finishID; x++) + { + if (!receivedChunks[x]) + { + return; + } + } + const conc: Buffer[] = []; + for (let x = 0; x <= finishID; x++) + { + conc.push(receivedChunks[x]); + } + if (timeout !== null) + { + clearTimeout(timeout); + } + if (subscription !== null) + { + subscription.unsubscribe(); + } + clearInterval(progress); + resolve(Buffer.concat(conc)); + } + } + break; + } + } + }); + }); + } + resend(sequenceNumber: number) { if (!this.active) diff --git a/lib/classes/ClientEvents.ts b/lib/classes/ClientEvents.ts index 716d63e..bb670ae 100644 --- a/lib/classes/ClientEvents.ts +++ b/lib/classes/ClientEvents.ts @@ -14,9 +14,15 @@ import { ScriptDialogEvent, EventQueueStateChangeEvent, FriendOnlineEvent, - FriendRightsEvent, FriendRemovedEvent + FriendRightsEvent, + FriendRemovedEvent, + ObjectPhysicsDataEvent, + ParcelPropertiesEvent } from '..'; import {Subject} from 'rxjs/internal/Subject'; +import {NewObjectEvent} from '../events/NewObjectEvent'; +import {ObjectUpdatedEvent} from '../events/ObjectUpdatedEvent'; +import {ObjectKilledEvent} from '../events/ObjectKilledEvent'; export class ClientEvents @@ -39,4 +45,9 @@ export class ClientEvents onFriendOnline: Subject = new Subject(); onFriendRights: Subject = new Subject(); onFriendRemoved: Subject = new Subject(); + onPhysicsDataEvent: Subject = new Subject(); + onParcelPropertiesEvent: Subject = new Subject(); + onNewObjectEvent: Subject = new Subject(); + onObjectUpdatedEvent: Subject = new Subject(); + onObjectKilledEvent: Subject = new Subject(); } diff --git a/lib/classes/Color4.ts b/lib/classes/Color4.ts index f9277b9..5618591 100644 --- a/lib/classes/Color4.ts +++ b/lib/classes/Color4.ts @@ -1,9 +1,23 @@ +import {XMLElementOrXMLNode} from 'xmlbuilder'; + export class Color4 { static black: Color4 = new Color4(0.0, 0.0, 0.0, 1.0); static white: Color4 = new Color4(1.0, 1.0, 1.0, 1.0); - constructor(public red: number | Buffer, public green: number, public blue: number | boolean, public alpha: number | boolean = 0) + static getXML(doc: XMLElementOrXMLNode, c?: Color4) + { + if (c === undefined) + { + c = Color4.white; + } + doc.ele('R', c.red); + doc.ele('G', c.green); + doc.ele('B', c.blue); + doc.ele('A', c.alpha); + } + + constructor(public red: number | Buffer | number[], public green: number = 0, public blue: number | boolean = 0, public alpha: number | boolean = 0) { if (red instanceof Buffer && typeof blue === 'boolean') { @@ -41,5 +55,12 @@ export class Color4 this.alpha = 1.0 - this.alpha; } } + if (Array.isArray(red)) + { + this.green = red[1]; + this.blue = red[2]; + this.alpha = red[3]; + this.red = red[0]; + } } } diff --git a/lib/classes/EventQueueClient.ts b/lib/classes/EventQueueClient.ts index 9d52ff5..89aed61 100644 --- a/lib/classes/EventQueueClient.ts +++ b/lib/classes/EventQueueClient.ts @@ -11,8 +11,8 @@ import { EventQueueStateChangeEvent, GroupChatEvent, GroupChatSessionAgentListEvent, - GroupChatSessionJoinEvent, - TeleportEvent + GroupChatSessionJoinEvent, ObjectPhysicsDataEvent, ParcelPropertiesEvent, + TeleportEvent, Vector3 } from '..'; export class EventQueueClient @@ -102,112 +102,74 @@ export class EventQueueClient break; case 'ParcelProperties': - /* - { - "body": { - "AgeVerificationBlock": [ - { + { + const body = event['body']; + const pprop = new ParcelPropertiesEvent(); + pprop.RegionDenyAgeUnverified = body['AgeVerificationBlock'][0]['RegionDenyAgeUnverified']; + pprop.MediaDesc = body['MediaData'][0]['MediaDesc']; + pprop.MediaHeight = body['MediaData'][0]['MediaHeight']; + pprop.MediaLoop = body['MediaData'][0]['MediaLoop']; + pprop.MediaType = body['MediaData'][0]['MediaType']; + pprop.MediaWidth = body['MediaData'][0]['MediaWidth']; + pprop.ObscureMedia = body['MediaData'][0]['ObscureMedia']; + pprop.ObscureMusic = body['MediaData'][0]['ObscureMusic']; + pprop.AABBMax = new Vector3([parseInt(body['ParcelData'][0]['AABBMax'][0], 10), parseInt( body['ParcelData'][0]['AABBMax'][1], 10), parseInt(body['ParcelData'][0]['AABBMax'][2], 10)]); + pprop.AABBMin = new Vector3([parseInt(body['ParcelData'][0]['AABBMin'][0], 10), parseInt(body['ParcelData'][0]['AABBMin'][1], 10), parseInt( body['ParcelData'][0]['AABBMin'][2], 10)]); + pprop.AnyAVSounds = body['ParcelData'][0]['AnyAVSounds']; + pprop.Area = body['ParcelData'][0]['Area']; + pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0); + pprop.AuthBuyerID = new UUID(String(body['ParcelData'][0]['AuthBuyerID'])); - "RegionDenyAgeUnverified": true - } - ], - "MediaData": [ - { - "MediaDesc": "", - "MediaHeight": 0, - "MediaLoop": 0, - "MediaType": "text/html", - "MediaWidth": 0, - "ObscureMedia": 0, - "ObscureMusic": 0 - } - ], - "ParcelData": [ - { - "AABBMax": [ - 256, - 256, - 50 - ], - "AABBMin": [ - 0, - 0, - 0 - ], - "AnyAVSounds": true, - "Area": 65536, - "AuctionID": "AAAAAA==", - "AuthBuyerID": "00000000-0000-0000-0000-000000000000", - "Bitmap": "///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8=", - "Category": 0, - "ClaimDate": 1333505995, - "ClaimPrice": 0, - "Desc": "adoption parent furry parent teen twin cub neko pets adult elf vamp toddleedoo baby child panel brother sister numbers meshmerized gacha adoptions adopt family mesh skin shape camp ngi youthspot foster kids mall zoo train kid primbay\ndupli - city onlinker", - "GroupAVSounds": true, - "GroupID": "f2b75b49-8ebc-2a9c-f345-aa2f91adc908", - "GroupPrims": 18677, - "IsGroupOwned": true, - "LandingType": 2, - "LocalID": 15, - "MaxPrims": 30000, - "MediaAutoScale": 1, - "MediaID": "6bd35c06-2b24-a83e-03f6-f547c65c8556", - "MediaURL": "", - "MusicURL": "http://142.4.209.63:8071", - "Name": "Next Gen Inc. Adoption Agency on the :::: KiD GRiD :::", - "OtherCleanTime": 0, - "OtherCount": 4096, - "OtherPrims": 312, - "OwnerID": "f2b75b49-8ebc-2a9c-f345-aa2f91adc908", - "OwnerPrims": 3, - "ParcelFlags": "NiAUSw==", - "ParcelPrimBonus": 1, - "PassHours": 10, - "PassPrice": 10, - "PublicCount": 0, - "RegionDenyAnonymous": true, - "RegionDenyIdentified": true, - "RegionDenyTransacted": true, - "RegionPushOverride": true, - "RentPrice": 0, - "RequestResult": 0, - "SalePrice": 1, - "SeeAVs": true, - "SelectedPrims": 1, - "SelfCount": 0, - "SequenceID": 0, - "SimWideMaxPrims": 30000, - "SimWideTotalPrims": 18993, - "SnapSelection": true, - "SnapshotID": "09c4101a-9406-2501-b9b7-dbb60260fd7a", - "Status": 0, - "TotalPrims": 18993, - "UserLocation": [ - 131.48399353027344, - 171.41600036621094, - 21.544700622558594 - ], - "UserLookAt": [ - 0.0325143001973629, - -0.9994710087776184, - 0 - ] - } - ], - "RegionAllowAccessBlock": [ - { - "RegionAllowAccessOverride": true - } - ] - }, - "message": "ParcelProperties" - } - - */ + pprop.Bitmap = Buffer.from(body['ParcelData'][0]['Bitmap'].toArray()); + pprop.Category = body['ParcelData'][0]['Category']; + pprop.ClaimDate = body['ParcelData'][0]['ClaimDate']; + pprop.ClaimPrice = body['ParcelData'][0]['ClaimPrice']; + pprop.Desc = body['ParcelData'][0]['Desc']; + pprop.GroupAVSounds = body['ParcelData'][0]['GroupAVSounds']; + pprop.GroupID = new UUID(String(body['ParcelData'][0]['GroupID'])); + pprop.GroupPrims = body['ParcelData'][0]['GroupPrims']; + pprop.IsGroupOwned = body['ParcelData'][0]['IsGroupOwned']; + pprop.LandingType = body['ParcelData'][0]['LandingType']; + pprop.LocalID = body['ParcelData'][0]['LocalID']; + pprop.MaxPrims = body['ParcelData'][0]['MaxPrims']; + pprop.MediaAutoScale = body['ParcelData'][0]['MediaAutoScale']; + pprop.MediaID = new UUID(String(body['ParcelData'][0]['MediaID'])); + pprop.MediaURL = body['ParcelData'][0]['MediaURL']; + pprop.MusicURL = body['ParcelData'][0]['MusicURL']; + pprop.Name = body['ParcelData'][0]['Name']; + pprop.OtherCleanTime = body['ParcelData'][0]['OtherCleanTime']; + pprop.OtherCount = body['ParcelData'][0]['OtherCount']; + pprop.OtherPrims = body['ParcelData'][0]['OtherPrims']; + pprop.OwnerID = body['ParcelData'][0]['OwnerID']; + pprop.OwnerPrims = body['ParcelData'][0]['OwnerPrims']; + pprop.ParcelFlags = Buffer.from(body['ParcelData'][0]['ParcelFlags'].toArray()).readUInt32LE(0); + pprop.ParcelPrimBonus = body['ParcelData'][0]['ParcelPrimBonus']; + pprop.PassHours = body['ParcelData'][0]['PassHours']; + pprop.PassPrice = body['ParcelData'][0]['PassPrice']; + pprop.PublicCount = body['ParcelData'][0]['PublicCount']; + pprop.RegionDenyAnonymous = body['ParcelData'][0]['RegionDenyAnonymous']; + pprop.RegionDenyIdentified = body['ParcelData'][0]['RegionDenyIdentified']; + pprop.RegionPushOverride = body['ParcelData'][0]['RegionPushOverride']; + pprop.RegionDenyTransacted = body['ParcelData'][0]['RegionDenyTransacted']; + pprop.RentPrice = body['ParcelData'][0]['RentPrice']; + pprop.RequestResult = body['ParcelData'][0]['RequestResult']; + pprop.SalePrice = body['ParcelData'][0]['SalePrice']; + pprop.SeeAvs = body['ParcelData'][0]['SeeAVs']; + pprop.SelectedPrims = body['ParcelData'][0]['SelectedPrims']; + pprop.SelfCount = body['ParcelData'][0]['SelfCount']; + pprop.SequenceID = body['ParcelData'][0]['SequenceID']; + pprop.SimWideMaxPrims = body['ParcelData'][0]['SimWideMaxPrims']; + pprop.SimWideTotalPrims = body['ParcelData'][0]['SimWideTotalPrims']; + pprop.SnapSelection = body['ParcelData'][0]['SnapSelection']; + pprop.SnapshotID = new UUID(body['ParcelData'][0]['SnapshotID'].toString()); + pprop.Status = body['ParcelData'][0]['Status']; + pprop.TotalPrims = body['ParcelData'][0]['TotalPrims']; + pprop.UserLocation = new Vector3([parseInt(body['ParcelData'][0]['UserLocation'][0], 10), parseInt(body['ParcelData'][0]['UserLocation'][1], 10), parseInt(body['ParcelData'][0]['UserLocation'][2], 10)]); + pprop.UserLookAt = new Vector3([parseInt(body['ParcelData'][0]['UserLookAt'][0], 10), parseInt(body['ParcelData'][0]['UserLookAt'][1], 10), parseInt(body['ParcelData'][0]['UserLookAt'][2], 10)]); + pprop.RegionAllowAccessOverride = body['RegionAllowAccessBlock'][0]['RegionAllowAccessOverride']; + this.clientEvents.onParcelPropertiesEvent.next(pprop); break; + } case 'AgentGroupDataUpdate': /* { @@ -368,6 +330,20 @@ export class EventQueueClient } case 'ObjectPhysicsProperties': { + const objData = event['body']['ObjectData']; + for (const obj of objData) + { + const objPhysEvent = new ObjectPhysicsDataEvent(); + objPhysEvent.localID = obj.LocalID; + objPhysEvent.density = obj.Density; + objPhysEvent.friction = obj.Friction; + objPhysEvent.gravityMultiplier = obj.GravityMultiplier; + objPhysEvent.physicsShapeType = obj.PhysicsShapeType; + objPhysEvent.restitution = obj.Restitution; + + this.clientEvents.onPhysicsDataEvent.next(objPhysEvent); + } + break; } case 'TeleportFinish': diff --git a/lib/classes/InventoryItem.ts b/lib/classes/InventoryItem.ts index 4311d4f..e0a0a5c 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; @@ -26,5 +26,17 @@ export class InventoryItem owner: UUID; creator: UUID; group: UUID; + groupOwned?: boolean + } = { + baseMask: 0, + groupMask: 0, + nextOwnerMask: 0, + ownerMask: 0, + everyoneMask: 0, + lastOwner: UUID.zero(), + owner: UUID.zero(), + creator: UUID.zero(), + group: UUID.zero(), + groupOwned: false }; } \ No newline at end of file diff --git a/lib/classes/ObjectStoreFull.ts b/lib/classes/ObjectStoreFull.ts index 169ffa8..2c1be36 100644 --- a/lib/classes/ObjectStoreFull.ts +++ b/lib/classes/ObjectStoreFull.ts @@ -9,16 +9,15 @@ import {UUID} from './UUID'; import {Quaternion} from './Quaternion'; import {Vector3} from './Vector3'; import {Utils} from './Utils'; -import {PCode} from '../enums/PCode'; import {ClientEvents} from './ClientEvents'; import {IObjectStore} from './interfaces/IObjectStore'; -import {BotOptionFlags, CompressedFlags} from '..'; +import {BotOptionFlags, CompressedFlags, PacketFlags, PCode} from '..'; import {RBush3D} from 'rbush-3d/dist'; import {Vector4} from './Vector4'; import {TextureEntry} from './TextureEntry'; import {Color4} from './Color4'; import {ParticleSystem} from './ParticleSystem'; -import {GameObject} from './GameObject'; +import {GameObject} from './public/GameObject'; import {ObjectStoreLite} from './ObjectStoreLite'; export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore @@ -38,7 +37,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore const localID = objData.ID; const parentID = objData.ParentID; let addToParentList = true; - + let newObject = false; if (this.objects[localID]) { if (this.objects[localID].ParentID !== parentID && this.objectsByParent[parentID]) @@ -49,15 +48,18 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore this.objectsByParent[parentID].splice(ind, 1); } } - else + else if (this.objectsByParent[parentID]) { addToParentList = false; } } else { + newObject = true; this.objects[localID] = new GameObject(); + this.objects[localID].region = this.agent.currentRegion; } + this.objects[localID].deleted = false; const obj = this.objects[localID]; obj.ID = objData.ID; @@ -67,6 +69,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore obj.PCode = objData.PCode; obj.Material = objData.Material; obj.ClickAction = objData.ClickAction; + obj.Scale = objData.Scale; obj.ObjectData = objData.ObjectData; const data: Buffer = objData.ObjectData; @@ -158,6 +161,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore break; } obj.ParentID = objData.ParentID; + obj.Flags = objData.UpdateFlags; obj.PathCurve = objData.PathCurve; obj.ProfileCurve = objData.ProfileCurve; @@ -179,11 +183,21 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore obj.ProfileHollow = objData.ProfileHollow; obj.TextureEntry = new TextureEntry(objData.TextureEntry); obj.TextureAnim = objData.TextureAnim; + + if (obj.TextureAnim.length >= 16) + { + this.readTextureAnim(obj); + } + const pcodeData = objData.Data; obj.Text = Utils.BufferToStringSimple(objData.Text); obj.TextColor = new Color4(objData.TextColor, 0, false, true); obj.MediaURL = Utils.BufferToStringSimple(objData.MediaURL); obj.PSBlock = objData.PSBlock; + if (obj.PSBlock.length > 0) + { + obj.Particles = new ParticleSystem(obj.PSBlock, 0); + } obj.Sound = objData.Sound; obj.OwnerID = objData.OwnerID; obj.SoundGain = objData.Gain; @@ -202,6 +216,9 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore { obj.TreeSpecies = pcodeData[0]; } + break; + case PCode.Prim: + break; } @@ -248,14 +265,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore this.readExtraParams(objData.ExtraParams, 0, this.objects[localID]); this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue)); - if (this.objects[localID].NameValue['AttachItemID']) - { - this.objects[localID].IsAttachment = true; - } - else - { - this.objects[localID].IsAttachment = false; - } + this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined; this.objectsByUUID[objData.FullID.toString()] = localID; if (!this.objectsByParent[parentID]) @@ -275,6 +285,14 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore else { this.insertIntoRtree(obj); + if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID]) + { + this.requestMissingObject(objData.ParentID); + } + if (obj.ParentID === 0) + { + this.notifyObjectUpdate(newObject, obj); + } } } } @@ -303,7 +321,24 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore } } - protected objectUpdateCompressed(objectUpdateCompressed: ObjectUpdateCompressedMessage) + private readTextureAnim(obj: GameObject) + { + let animPos = 0; + if (obj.TextureAnim !== undefined) + { + obj.TextureAnimFlags = obj.TextureAnim.readUInt8(animPos++); + obj.TextureAnimFace = obj.TextureAnim.readUInt8(animPos++); + obj.TextureAnimSizeX = obj.TextureAnim.readUInt8(animPos++); + obj.TextureAnimSizeY = obj.TextureAnim.readUInt8(animPos++); + obj.TextureAnimStart = obj.TextureAnim.readFloatLE(animPos); + animPos = animPos + 4; + obj.TextureAnimLength = obj.TextureAnim.readFloatLE(animPos); + animPos = animPos + 4; + obj.TextureAnimRate = obj.TextureAnim.readFloatLE(animPos); + } + } + + protected async objectUpdateCompressed(objectUpdateCompressed: ObjectUpdateCompressedMessage) { for (const obj of objectUpdateCompressed.ObjectData) { @@ -321,6 +356,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore { newObj = true; this.objects[localID] = new GameObject(); + this.objects[localID].region = this.agent.currentRegion; } const o = this.objects[localID]; o.ID = localID; @@ -328,6 +364,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore o.FullID = fullID; o.Flags = flags; o.PCode = pcode; + o.deleted = false; o.State = buf.readUInt8(pos++); o.CRC = buf.readUInt32LE(pos); pos = pos + 4; @@ -349,36 +386,38 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore o.AngularVelocity = new Vector3(buf, pos, false); pos = pos + 12; } + let newParentID = 0; if (compressedflags & CompressedFlags.HasParent) { - const newParentID = buf.readUInt32LE(pos); + newParentID = buf.readUInt32LE(pos); pos += 4; - let add = true; - if (!newObj) - { - if (newParentID !== o.ParentID) - { - const index = this.objectsByParent[o.ParentID].indexOf(localID); - if (index !== -1) - { - this.objectsByParent[o.ParentID].splice(index, 1); - } - } - else - { - add = false; - } - } - if (add) - { - if (!this.objectsByParent[newParentID]) - { - this.objectsByParent[newParentID] = []; - } - this.objectsByParent[newParentID].push(localID); - } - o.ParentID = newParentID; } + o.ParentID = newParentID; + let add = true; + if (!newObj && o.ParentID !== undefined) + { + if (newParentID !== o.ParentID) + { + const index = this.objectsByParent[o.ParentID].indexOf(localID); + if (index !== -1) + { + this.objectsByParent[o.ParentID].splice(index, 1); + } + } + else if (this.objectsByParent[o.ParentID]) + { + add = false; + } + } + if (add) + { + if (!this.objectsByParent[newParentID]) + { + this.objectsByParent[newParentID] = []; + } + this.objectsByParent[newParentID].push(localID); + } + if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly && (this.agent.localID !== 0 && o.ParentID !== this.agent.localID)) { // Drop object @@ -387,6 +426,10 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore } else { + if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects[o.ParentID]) + { + this.requestMissingObject(o.ParentID); + } if (compressedflags & CompressedFlags.Tree) { o.TreeSpecies = buf.readUInt8(pos++); @@ -421,7 +464,8 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore } if (compressedflags & CompressedFlags.HasParticles) { - o.Particles = new ParticleSystem(buf.slice(pos, pos + 86), 0); + o.PSBlock = buf.slice(pos, pos + 86); + o.Particles = new ParticleSystem(o.PSBlock, 0); pos += 86; } @@ -474,13 +518,23 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore if (compressedflags & CompressedFlags.TextureAnimation) { - // TODO: Properly parse textureAnim + const textureAnimLength = buf.readUInt32LE(pos); pos = pos + 4; + o.TextureAnim = buf.slice(pos, pos + textureAnimLength); + if (o.TextureAnim.length >= 16) + { + this.readTextureAnim(o); + } } o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0; this.insertIntoRtree(o); + + if (o.ParentID === 0) + { + this.notifyObjectUpdate(newObj, o); + } } } } @@ -543,19 +597,8 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore } else { - console.log('Received terse update for object ' + localID + ' which is not in the store, so requesting the object'); // We don't know about this object, so request it - const rmo = new RequestMultipleObjectsMessage(); - rmo.AgentData = { - AgentID: this.agent.agentID, - SessionID: this.circuit.sessionID - }; - rmo.ObjectData = []; - rmo.ObjectData.push({ - CacheMissType: 0, - ID: localID - }); - this.circuit.sendMessage(rmo, 0); + this.requestMissingObject(localID); } } } diff --git a/lib/classes/ObjectStoreLite.ts b/lib/classes/ObjectStoreLite.ts index 848d44e..ed99c1d 100644 --- a/lib/classes/ObjectStoreLite.ts +++ b/lib/classes/ObjectStoreLite.ts @@ -10,15 +10,26 @@ import {Agent} from './Agent'; import {UUID} from './UUID'; import {ExtraParamType} from '../enums/ExtraParamType'; import {Utils} from './Utils'; -import {PCode} from '../enums/PCode'; import {ClientEvents} from './ClientEvents'; import {KillObjectMessage} from './messages/KillObject'; import {IObjectStore} from './interfaces/IObjectStore'; import {NameValue} from './NameValue'; -import {BotOptionFlags, CompressedFlags} from '..'; -import {GameObject} from './GameObject'; +import {BotOptionFlags, CompressedFlags, ObjectPhysicsDataEvent, PacketFlags, PCode, Vector3} from '..'; +import {GameObject} from './public/GameObject'; import {RBush3D} from 'rbush-3d/dist'; import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox'; +import {FilterResponse} from '../enums/FilterResponse'; +import {ObjectSelectMessage} from './messages/ObjectSelect'; +import {ObjectDeselectMessage} from './messages/ObjectDeselect'; +import {FlexibleData} from './public/FlexibleData'; +import {LightImageData} from './public/LightImageData'; +import {LightData} from './public/LightData'; +import {MeshData} from './public/MeshData'; +import {SculptData} from './public/SculptData'; +import {Quaternion} from './Quaternion'; +import {Subscription} from 'rxjs/internal/Subscription'; +import {NewObjectEvent} from '../events/NewObjectEvent'; +import {ObjectUpdatedEvent} from '../events/ObjectUpdatedEvent'; export class ObjectStoreLite implements IObjectStore { @@ -29,6 +40,10 @@ export class ObjectStoreLite implements IObjectStore protected objectsByParent: { [key: number]: number[] } = {}; protected clientEvents: ClientEvents; protected options: BotOptionFlags; + protected requestedObjects: {[key: number]: boolean} = {}; + protected deadObjects: number[] = []; + protected persist = false; + private physicsSubscription: Subscription; rtree?: RBush3D; @@ -45,7 +60,7 @@ export class ObjectStoreLite implements IObjectStore Message.ObjectUpdateCompressed, Message.ImprovedTerseObjectUpdate, Message.KillObject - ], (packet: Packet) => + ], async (packet: Packet) => { switch (packet.message.id) { @@ -60,7 +75,7 @@ export class ObjectStoreLite implements IObjectStore case Message.ObjectUpdateCompressed: { const objectUpdateCompressed = packet.message as ObjectUpdateCompressedMessage; - this.objectUpdateCompressed(objectUpdateCompressed); + await this.objectUpdateCompressed(objectUpdateCompressed); break; } case Message.ImprovedTerseObjectUpdate: @@ -73,6 +88,93 @@ export class ObjectStoreLite implements IObjectStore break; } }); + + this.physicsSubscription = this.clientEvents.onPhysicsDataEvent.subscribe((evt: ObjectPhysicsDataEvent) => + { + if (this.objects[evt.localID]) + { + this.objects[evt.localID].physicsShapeType = evt.physicsShapeType; + this.objects[evt.localID].density = evt.density; + this.objects[evt.localID].restitution = evt.restitution; + this.objects[evt.localID].gravityMultiplier = evt.gravityMultiplier; + this.objects[evt.localID].friction = evt.friction; + } + }); + } + + protected async requestMissingObject(localID: number, attempt = 0) + { + if (this.requestedObjects[localID]) + { + return; + } + this.requestedObjects[localID] = true; + const rmo = new RequestMultipleObjectsMessage(); + rmo.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + rmo.ObjectData = []; + rmo.ObjectData.push({ + CacheMissType: 0, + ID: localID + }); + this.circuit.sendMessage(rmo, PacketFlags.Reliable); + + const selectObject = new ObjectSelectMessage(); + selectObject.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + selectObject.ObjectData = [ + { + 'ObjectLocalID': localID + } + ]; + this.circuit.sendMessage(selectObject, PacketFlags.Reliable); + + try + { + await this.circuit.waitForMessage(Message.ObjectUpdate, 10000, (message: ObjectUpdateMessage): FilterResponse => + { + for (const obj of message.ObjectData) + { + if (obj.ID === localID) + { + return FilterResponse.Finish; + } + } + return FilterResponse.NoMatch; + }); + delete this.requestedObjects[localID]; + } + catch (error) + { + delete this.requestedObjects[localID]; + if (attempt < 5) + { + await this.requestMissingObject(localID, ++attempt); + } + else + { + console.error('Error retrieving missing object after 5 attempts: ' + localID); + console.error(error); + } + } + finally + { + const deselectObject = new ObjectDeselectMessage(); + deselectObject.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + deselectObject.ObjectData = [ + { + 'ObjectLocalID': localID + } + ]; + this.circuit.sendMessage(selectObject, PacketFlags.Reliable); + } } protected objectUpdate(objectUpdate: ObjectUpdateMessage) @@ -82,6 +184,7 @@ export class ObjectStoreLite implements IObjectStore const localID = objData.ID; const parentID = objData.ParentID; let addToParentList = true; + let newObject = false; if (this.objects[localID]) { @@ -93,17 +196,20 @@ export class ObjectStoreLite implements IObjectStore this.objectsByParent[parentID].splice(ind, 1); } } - else + else if (this.objectsByParent[parentID]) { addToParentList = false; } } else { + newObject = true; this.objects[localID] = new GameObject(); + this.objects[localID].region = this.agent.currentRegion; } const obj = this.objects[localID]; + obj.deleted = false; obj.ID = objData.ID; obj.FullID = objData.FullID; obj.ParentID = objData.ParentID; @@ -112,14 +218,7 @@ export class ObjectStoreLite implements IObjectStore this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue)); - if (this.objects[localID].NameValue['AttachItemID']) - { - this.objects[localID].IsAttachment = true; - } - else - { - this.objects[localID].IsAttachment = false; - } + this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined; if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString()) { @@ -180,9 +279,39 @@ export class ObjectStoreLite implements IObjectStore return; } } + + if (obj.ParentID === 0) + { + this.notifyObjectUpdate(newObject, obj); + } + + if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID]) + { + this.requestMissingObject(objData.ParentID); + } }); } + protected notifyObjectUpdate(newObject: boolean, obj: GameObject) + { + if (newObject) + { + const newObj = new NewObjectEvent(); + newObj.localID = obj.ID; + newObj.objectID = obj.FullID; + newObj.object = obj; + this.clientEvents.onNewObjectEvent.next(newObj); + } + else + { + const updObj = new ObjectUpdatedEvent(); + updObj.localID = obj.ID; + updObj.objectID = obj.FullID; + updObj.object = obj; + this.clientEvents.onObjectUpdatedEvent.next(updObj); + } + } + protected objectUpdateCached(objectUpdateCached: ObjectUpdateCachedMessage) { const rmo = new RequestMultipleObjectsMessage(); @@ -201,7 +330,7 @@ export class ObjectStoreLite implements IObjectStore this.circuit.sendMessage(rmo, 0); } - protected objectUpdateCompressed(objectUpdateCompressed: ObjectUpdateCompressedMessage) + protected async objectUpdateCompressed(objectUpdateCompressed: ObjectUpdateCompressedMessage) { for (const obj of objectUpdateCompressed.ObjectData) { @@ -219,8 +348,10 @@ export class ObjectStoreLite implements IObjectStore { newObj = true; this.objects[localID] = new GameObject(); + this.objects[localID].region = this.agent.currentRegion; } const o = this.objects[localID]; + o.deleted = false; o.ID = localID; o.PCode = pcode; this.objectsByUUID[fullID.toString()] = localID; @@ -229,15 +360,7 @@ export class ObjectStoreLite implements IObjectStore pos++; - pos = pos + 4; - pos++; - pos++; - - pos = pos + 12; - - pos = pos + 12; - - pos = pos + 12; + pos = pos + 42; const compressedflags: CompressedFlags = buf.readUInt32LE(pos); pos = pos + 4; o.OwnerID = new UUID(buf, pos); @@ -247,35 +370,37 @@ export class ObjectStoreLite implements IObjectStore { pos = pos + 12; } + let newParentID = 0; if (compressedflags & CompressedFlags.HasParent) { - const newParentID = buf.readUInt32LE(pos); + newParentID = buf.readUInt32LE(pos); pos += 4; - let add = true; - if (!newObj) + } + + o.ParentID = newParentID; + let add = true; + if (!newObj && o.ParentID !== undefined) + { + if (newParentID !== o.ParentID) { - if (newParentID !== o.ParentID) + const index = this.objectsByParent[o.ParentID].indexOf(localID); + if (index !== -1) { - const index = this.objectsByParent[o.ParentID].indexOf(localID); - if (index !== -1) - { - this.objectsByParent[o.ParentID].splice(index, 1); - } - } - else - { - add = false; + this.objectsByParent[o.ParentID].splice(index, 1); } } - if (add) + else if (this.objectsByParent[o.ParentID]) { - if (!this.objectsByParent[newParentID]) - { - this.objectsByParent[newParentID] = []; - } - this.objectsByParent[newParentID].push(localID); + add = false; } - o.ParentID = newParentID; + } + if (add) + { + if (!this.objectsByParent[newParentID]) + { + this.objectsByParent[newParentID] = []; + } + this.objectsByParent[newParentID].push(localID); } if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly) { @@ -286,6 +411,10 @@ export class ObjectStoreLite implements IObjectStore return; } } + if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects[o.ParentID]) + { + this.requestMissingObject(o.ParentID); + } if (compressedflags & CompressedFlags.Tree) { pos++; @@ -312,7 +441,6 @@ export class ObjectStoreLite implements IObjectStore } if (compressedflags & CompressedFlags.HasParticles) { - // TODO: Particle system block pos += 86; } @@ -321,10 +449,7 @@ export class ObjectStoreLite implements IObjectStore if (compressedflags & CompressedFlags.HasSound) { - pos = pos + 16; - pos += 4; - pos++; - pos = pos + 4; + pos = pos + 25 } if (compressedflags & CompressedFlags.HasNameValues) { @@ -333,24 +458,21 @@ export class ObjectStoreLite implements IObjectStore pos += result.readLength; } pos++; - pos = pos + 2; - pos = pos + 2; - pos = pos + 12; - pos = pos + 2; - pos = pos + 2; - pos = pos + 2; + pos = pos + 22; const textureEntryLength = buf.readUInt32LE(pos); pos = pos + 4; - // TODO: Properly parse textureentry; pos = pos + textureEntryLength; - if (compressedflags & CompressedFlags.TextureAnimation) { - // TODO: Properly parse textureAnim pos = pos + 4; } o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0; + + if (o.ParentID === 0) + { + this.notifyObjectUpdate(newObj, o); + } } } @@ -369,17 +491,38 @@ export class ObjectStoreLite implements IObjectStore }); } + setPersist(persist: boolean): void + { + this.persist = persist; + if (!this.persist) + { + for (const d of this.deadObjects) + { + this.deleteObject(d); + } + this.deadObjects = []; + } + } + deleteObject(objectID: number) { if (this.objects[objectID]) { + this.objects[objectID].deleted = true; + + if (this.persist) + { + this.deadObjects.push(objectID); + return; + } + // First, kill all children if (this.objectsByParent[objectID]) { - this.objectsByParent[objectID].forEach((childObjID) => + for (const childObjID of this.objectsByParent[objectID]) { this.deleteObject(childObjID); - }); + } } delete this.objectsByParent[objectID]; @@ -391,13 +534,16 @@ export class ObjectStoreLite implements IObjectStore { delete this.objectsByUUID[uuid]; } - const parentID = objct.ParentID; - if (this.objectsByParent[parentID]) + if (objct.ParentID !== undefined) { - const ind = this.objectsByParent[parentID].indexOf(objectID); - if (ind !== -1) + const parentID = objct.ParentID; + if (this.objectsByParent[parentID]) { - this.objectsByParent[parentID].splice(ind, 1); + const ind = this.objectsByParent[parentID].indexOf(objectID); + if (ind !== -1) + { + this.objectsByParent[parentID].splice(ind, 1); + } } } if (this.rtree && this.objects[objectID].rtreeEntry !== undefined) @@ -410,6 +556,7 @@ export class ObjectStoreLite implements IObjectStore readExtraParams(buf: Buffer, pos: number, o: GameObject): number { + const startPos = pos; if (pos >= buf.length) { return 0; @@ -422,9 +569,28 @@ export class ObjectStoreLite implements IObjectStore const paramLength = buf.readUInt32LE(pos); pos = pos + 4; - // TODO: Read extra param data + switch (type) + { + case ExtraParamType.Flexible: + o.FlexibleData = new FlexibleData(buf, pos, paramLength); + break; + case ExtraParamType.Light: + o.LightData = new LightData(buf, pos, paramLength); + break; + case ExtraParamType.LightImage: + o.LightImageData = new LightImageData(buf, pos, paramLength); + break; + case ExtraParamType.Mesh: + o.MeshData = new MeshData(buf, pos, paramLength); + break; + case ExtraParamType.Sculpt: + o.SculptData = new SculptData(buf, pos, paramLength); + break; + } + pos += paramLength; } + o.ExtraParams = buf.slice(startPos, pos); return pos; } @@ -438,7 +604,10 @@ export class ObjectStoreLite implements IObjectStore const result: GameObject[] = []; list.forEach((localID) => { - result.push(this.objects[localID]); + if (this.objects[localID]) + { + result.push(this.objects[localID]); + } }); return result; } @@ -481,6 +650,7 @@ export class ObjectStoreLite implements IObjectStore shutdown() { + this.physicsSubscription.unsubscribe(); this.objects = {}; if (this.rtree) { @@ -492,33 +662,43 @@ export class ObjectStoreLite implements IObjectStore protected findParent(go: GameObject): GameObject { - if (go.ParentID !== 0 && this.objects[go.ParentID]) + if (go.ParentID !== undefined && go.ParentID !== 0 && this.objects[go.ParentID]) { return this.findParent(this.objects[go.ParentID]); } else { + if (go.ParentID !== undefined && go.ParentID !== 0 && !this.objects[go.ParentID]) + { + this.requestMissingObject(go.ParentID); + } return go; } } private populateChildren(obj: GameObject) { - obj.children = []; - obj.totalChildren = 0; - for (const child of this.getObjectsByParent(obj.ID)) + if (obj !== undefined) { - obj.totalChildren++; - this.populateChildren(child); - if (child.totalChildren !== undefined) + obj.children = []; + obj.totalChildren = 0; + for (const child of this.getObjectsByParent(obj.ID)) { - obj.totalChildren += child.totalChildren; + if (child.PCode !== PCode.Avatar) + { + obj.totalChildren++; + this.populateChildren(child); + if (child.totalChildren !== undefined) + { + obj.totalChildren += child.totalChildren; + } + obj.children.push(child); + } } - obj.children.push(child); } } - getAllObjects(): GameObject[] + async getAllObjects(): Promise { const results = []; const found: {[key: string]: GameObject} = {}; @@ -530,7 +710,7 @@ export class ObjectStoreLite implements IObjectStore try { const parent = this.findParent(go); - if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false)) + if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false) && parent.ParentID === 0) { const uuid = parent.FullID.toString(); @@ -565,7 +745,7 @@ export class ObjectStoreLite implements IObjectStore return Object.keys(this.objects).length; } - getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObject[] + async getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): Promise { if (!this.rtree) { @@ -590,7 +770,7 @@ export class ObjectStoreLite implements IObjectStore try { const parent = this.findParent(go); - if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false)) + if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false) && parent.ParentID === 0) { const uuid = parent.FullID.toString(); @@ -656,7 +836,8 @@ export class ObjectStoreLite implements IObjectStore { return; } - const normalizedScale = obj.Scale.multiplyByQuat(obj.Rotation); + const normalizedScale = new Vector3(obj.Scale).multiplyByQuat(new Quaternion(obj.Rotation)); + const bounds: ITreeBoundingBox = { minX: obj.Position.x - (normalizedScale.x / 2), maxX: obj.Position.x + (normalizedScale.x / 2), diff --git a/lib/classes/Quaternion.ts b/lib/classes/Quaternion.ts index 64bc222..704da45 100644 --- a/lib/classes/Quaternion.ts +++ b/lib/classes/Quaternion.ts @@ -1,4 +1,5 @@ import {quat} from '../tsm/quat'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; export class Quaternion extends quat { @@ -9,24 +10,47 @@ export class Quaternion extends quat return q; } - constructor(buf?: Buffer | number[], pos?: number) + static getXML(doc: XMLElementOrXMLNode, v?: Quaternion) { - if (buf !== undefined && pos !== undefined && buf instanceof Buffer) + if (v === undefined) { - const x = buf.readFloatLE(pos); - const y = buf.readFloatLE(pos + 4); - const z = buf.readFloatLE(pos + 8); - const xyzsum = 1.0 - x * x - y * y - z * z; - const w = (xyzsum > 0.0) ? Math.sqrt(xyzsum) : 0; - super([x, y, z, w]); + v = Quaternion.getIdentity(); } - else if (buf !== undefined && Array.isArray(buf)) + doc.ele('X', v.x); + doc.ele('Y', v.y); + doc.ele('Z', v.z); + doc.ele('W', v.w); + } + + constructor(buf?: Buffer | number[] | Quaternion, pos?: number) + { + if (buf instanceof Quaternion) { - super(buf); + super(); + this.x = buf.x; + this.y = buf.y; + this.z = buf.z; + this.w = buf.w; } else { - super(); + if (buf !== undefined && pos !== undefined && buf instanceof Buffer) + { + const x = buf.readFloatLE(pos); + const y = buf.readFloatLE(pos + 4); + const z = buf.readFloatLE(pos + 8); + const xyzsum = 1.0 - x * x - y * y - z * z; + const w = (xyzsum > 0.0) ? Math.sqrt(xyzsum) : 0; + super([x, y, z, w]); + } + else if (buf !== undefined && Array.isArray(buf)) + { + super(buf); + } + else + { + super(); + } } } writeToBuffer(buf: Buffer, pos: number) diff --git a/lib/classes/Region.ts b/lib/classes/Region.ts index d4a7b95..712cf20 100644 --- a/lib/classes/Region.ts +++ b/lib/classes/Region.ts @@ -6,7 +6,7 @@ import {ClientEvents} from './ClientEvents'; import {IObjectStore} from './interfaces/IObjectStore'; import {ObjectStoreFull} from './ObjectStoreFull'; import {ObjectStoreLite} from './ObjectStoreLite'; -import {BotOptionFlags, PacketFlags, RegionFlags, UUID} from '..'; +import {BotOptionFlags, PacketFlags, ParcelPropertiesEvent, RegionFlags, UUID, Vector2, Vector3} from '..'; import {RequestRegionInfoMessage} from './messages/RequestRegionInfo'; import {RegionInfoMessage} from './messages/RegionInfo'; import {Message} from '../enums/Message'; @@ -17,9 +17,32 @@ import {GridLayerType} from '../enums/GridLayerType'; import {MapBlockReplyMessage} from './messages/MapBlockReply'; import {FilterResponse} from '../enums/FilterResponse'; import * as Long from 'long'; +import {Packet} from './Packet'; +import {LayerDataMessage} from './messages/LayerData'; +import {LayerType} from '../enums/LayerType'; +import {Subscription} from 'rxjs/internal/Subscription'; +import {BitPack} from './BitPack'; +import * as builder from 'xmlbuilder'; +import {SimAccessFlags} from '../enums/SimAccessFlags'; +import {Subject} from 'rxjs/internal/Subject'; +import {ParcelDwellRequestMessage} from './messages/ParcelDwellRequest'; +import {ParcelDwellReplyMessage} from './messages/ParcelDwellReply'; +import Timer = NodeJS.Timer; +import {Parcel} from './public/Parcel'; +import {RegionEnvironment} from './public/RegionEnvironment'; +import {Color4} from './Color4'; +import {SkyPreset} from './public/interfaces/SkyPreset'; +import {Vector4} from './Vector4'; +import {WaterPreset} from './public/interfaces/WaterPreset'; export class Region { + static CopyMatrix16: number[] = []; + static CosineTable16: number[] = []; + static DequantizeTable16: number[] = []; + static setup = false; + static OO_SQRT_2 = 0.7071067811865475244008443621049; + regionName: string; regionOwner: UUID; regionID: UUID; @@ -77,9 +100,180 @@ export class Region clientEvents: ClientEvents; options: BotOptionFlags; agent: Agent; + messageSubscription: Subscription; + parcelPropertiesSubscription: Subscription; + + terrain: number[][] = []; + tilesReceived = 0; + terrainComplete = false; + terrainCompleteEvent: Subject = new Subject(); + + parcelsComplete = false; + parcelsCompleteEvent: Subject = new Subject(); + + parcels: {[key: number]: Parcel} = {}; + parcelsByUUID: {[key: string]: Parcel} = {}; + parcelMap: number[][] = []; + + environment: RegionEnvironment; + + static IDCTColumn16(linein: number[], lineout: number[], column: number) + { + let total: number; + let usize: number; + + for (let n = 0; n < 16; n++) + { + total = this.OO_SQRT_2 * linein[column]; + + for (let u = 1; u < 16; u++) + { + usize = u * 16; + total += linein[usize + column] * this.CosineTable16[usize + n]; + } + + lineout[16 * n + column] = total; + } + } + + static IDCTLine16(linein: number[], lineout: number[], line: number) + { + const oosob: number = 2.0 / 16.0; + const lineSize: number = line * 16; + let total = 0; + + for (let n = 0; n < 16; n++) + { + total = this.OO_SQRT_2 * linein[lineSize]; + + for (let u = 1; u < 16; u++) + { + total += linein[lineSize + u] * this.CosineTable16[u * 16 + n]; + } + + lineout[lineSize + n] = total * oosob; + } + } + + static InitialSetup() + { + // Build copy matrix 16 + { + let diag = false; + let right = true; + let i = 0; + let j = 0; + let count = 0; + + for (let x = 0; x < 16 * 16; x++) + { + this.CopyMatrix16.push(0); + this.DequantizeTable16.push(0); + this.CosineTable16.push(0); + } + while (i < 16 && j < 16) + { + this.CopyMatrix16[j * 16 + i] = count++; + + if (!diag) + { + if (right) + { + if (i < 16 - 1) + { + i++; + } + else + { + j++; + } + + right = false; + diag = true; + } + else + { + if (j < 16 - 1) + { + j++; + } + else + { + i++; + } + + right = true; + diag = true; + } + } + else + { + if (right) + { + i++; + j--; + if (i === 16 - 1 || j === 0) + { + diag = false; + } + } + else + { + i--; + j++; + if (j === 16 - 1 || i === 0) + { + diag = false; + } + } + } + } + } + { + for (let j = 0; j < 16; j++) + { + for (let i = 0; i < 16; i++) + { + this.DequantizeTable16[j * 16 + i] = 1.0 + 2.0 * (i + j); + } + } + } + { + const hposz: number = Math.PI * 0.5 / 16.0; + + for (let u = 0; u < 16; u++) + { + for (let n = 0; n < 16; n++) + { + this.CosineTable16[u * 16 + n] = Math.cos((2.0 * n + 1.0) * u * hposz); + } + } + } + this.setup = true; + } constructor(agent: Agent, clientEvents: ClientEvents, options: BotOptionFlags) { + if (!Region.setup) + { + Region.InitialSetup(); + } + for (let x = 0; x < 256; x++) + { + this.terrain.push([]); + for (let y = 0; y < 256; y++) + { + this.terrain[x].push(-1); + } + } + for (let x = 0; x < 64; x++) + { + this.parcelMap.push([]); + for (let y = 0; y < 64; y++) + { + this.parcelMap[x].push(0); + } + } this.agent = agent; this.options = options; this.clientEvents = clientEvents; @@ -93,7 +287,454 @@ export class Region this.objects = new ObjectStoreFull(this.circuit, agent, clientEvents, options); } this.comms = new Comms(this.circuit, agent, clientEvents); + + this.parcelPropertiesSubscription = this.clientEvents.onParcelPropertiesEvent.subscribe(async (parcelProperties: ParcelPropertiesEvent) => + { + // Get the parcel UUID + const msg = new ParcelDwellRequestMessage(); + msg.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + msg.Data = { + LocalID: parcelProperties.LocalID, + ParcelID: UUID.zero() + }; + + this.circuit.sendMessage(msg, PacketFlags.Reliable); + const dwellReply = await this.circuit.waitForMessage(Message.ParcelDwellReply, 1000, (message: ParcelDwellReplyMessage): FilterResponse => + { + if (message.Data.LocalID === parcelProperties.LocalID) + { + return FilterResponse.Finish; + } + else + { + return FilterResponse.NoMatch; + } + }); + const parcelID: string = dwellReply.Data.ParcelID.toString(); + let parcel = new Parcel(); + if (this.parcelsByUUID[parcelID]) + { + parcel = this.parcelsByUUID[parcelID]; + } + parcel.LocalID = parcelProperties.LocalID; + parcel.ParcelID = dwellReply.Data.ParcelID; + parcel.RegionDenyAgeUnverified = parcelProperties.RegionDenyTransacted; + parcel.MediaDesc = parcelProperties.MediaDesc; + parcel.MediaHeight = parcelProperties.MediaHeight; + parcel.MediaLoop = parcelProperties.MediaLoop; + parcel.MediaType = parcelProperties.MediaType; + parcel.MediaWidth = parcelProperties.MediaWidth; + parcel.ObscureMedia = parcelProperties.ObscureMedia; + parcel.ObscureMusic = parcelProperties.ObscureMusic; + parcel.AABBMax = parcelProperties.AABBMax; + parcel.AABBMin = parcelProperties.AABBMin; + parcel.AnyAVSounds = parcelProperties.AnyAVSounds; + parcel.Area = parcelProperties.Area; + parcel.AuctionID = parcelProperties.AuctionID; + parcel.AuthBuyerID = parcelProperties.AuthBuyerID; + parcel.Bitmap = parcelProperties.Bitmap; + parcel.Category = parcelProperties.Category; + parcel.ClaimDate = parcelProperties.ClaimDate; + parcel.ClaimPrice = parcelProperties.ClaimPrice; + parcel.Desc = parcelProperties.Desc; + parcel.GroupAVSounds = parcelProperties.GroupAVSounds; + parcel.GroupID = parcelProperties.GroupID; + parcel.GroupPrims = parcelProperties.GroupPrims; + parcel.IsGroupOwned = parcelProperties.IsGroupOwned; + parcel.LandingType = parcelProperties.LandingType; + parcel.MaxPrims = parcelProperties.MaxPrims; + parcel.MediaAutoScale = parcelProperties.MediaAutoScale; + parcel.MediaID = parcelProperties.MediaID; + parcel.MediaURL = parcelProperties.MediaURL; + parcel.MusicURL = parcelProperties.MusicURL; + parcel.Name = parcelProperties.Name; + parcel.OtherCleanTime = parcelProperties.OtherCleanTime; + parcel.OtherCount = parcelProperties.OtherCount; + parcel.OtherPrims = parcelProperties.OtherPrims; + parcel.OwnerID = parcelProperties.OwnerID; + parcel.OwnerPrims = parcelProperties.OwnerPrims; + parcel.ParcelFlags = parcelProperties.ParcelFlags; + parcel.ParcelPrimBonus = parcelProperties.ParcelPrimBonus; + parcel.PassHours = parcelProperties.PassHours; + parcel.PassPrice = parcelProperties.PassPrice; + parcel.PublicCount = parcelProperties.PublicCount; + parcel.RegionDenyAnonymous = parcelProperties.RegionDenyAnonymous; + parcel.RegionDenyIdentified = parcelProperties.RegionDenyIdentified; + parcel.RegionPushOverride = parcelProperties.RegionPushOverride; + parcel.RegionDenyTransacted = parcelProperties.RegionDenyTransacted; + parcel.RentPrice = parcelProperties.RentPrice; + parcel.RequestResult = parcelProperties.RequestResult; + parcel.SalePrice = parcelProperties.SalePrice; + parcel.SeeAvs = parcelProperties.SeeAvs; + parcel.SelectedPrims = parcelProperties.SelectedPrims; + parcel.SelfCount = parcelProperties.SelfCount; + parcel.SequenceID = parcelProperties.SequenceID; + parcel.SimWideMaxPrims = parcelProperties.SimWideMaxPrims; + parcel.SimWideTotalPrims = parcelProperties.SimWideTotalPrims; + parcel.SnapSelection = parcelProperties.SnapSelection; + parcel.SnapshotID = parcelProperties.SnapshotID; + parcel.Status = parcelProperties.Status; + parcel.TotalPrims = parcelProperties.TotalPrims; + parcel.UserLocation = parcelProperties.UserLocation; + parcel.UserLookAt = parcelProperties.UserLookAt; + parcel.RegionAllowAccessOverride = parcelProperties.RegionAllowAccessOverride; + this.parcels[parcelProperties.LocalID] = parcel; + + let foundEmpty = false; + for (let y = 0; y < 64; y++) + { + for (let x = 0; x < 64; x++) + { + let index = (y * 64) + x; + const bit = index % 8; + index >>= 3; + if ((parcel.Bitmap[index] & (1 << bit)) !== 0) + { + this.parcelMap[y][x] = parcel.LocalID; + } + else + { + if (this.parcelMap[y][x] === 0) + { + foundEmpty = true; + } + } + } + } + if (foundEmpty === false) + { + if (this.parcelsComplete === false) + { + this.parcelsComplete = true; + this.parcelsCompleteEvent.next(); + } + } + else if (this.parcelsComplete === true) + { + this.parcelsComplete = false; + } + }); + + this.messageSubscription = this.circuit.subscribeToMessages([ + Message.LayerData + ], (packet: Packet) => + { + switch (packet.message.id) + { + case Message.LayerData: + const layerData: LayerDataMessage = packet.message as LayerDataMessage; + const type: LayerType = layerData.LayerID.Type; + + const nibbler = new BitPack(layerData.LayerData.Data, 0); + + const stride = nibbler.UnpackBits(16); + const patchSize = nibbler.UnpackBits(8); + const headerLayerType = nibbler.UnpackBits(8); + + switch (type) + { + case LayerType.Land: + if (headerLayerType === type) // Quick sanity check + { + let x = 0; + let y = 0; + const patches: number[] = []; + for (let xi = 0; xi < 32 * 32; xi++) + { + patches.push(0); + } + + while (true) + { + // DecodePatchHeader + const quantWBits = nibbler.UnpackBits(8); + if (quantWBits === 97) + { + break; + } + const dcOffset = nibbler.UnpackFloat(); + const range = nibbler.UnpackBits(16); + const patchIDs = nibbler.UnpackBits(10); + const wordBits = (quantWBits & 0x0f) + 2; + + x = patchIDs >> 5; + y = patchIDs & 0x1F; + if (x >= 16 || y >= 16) + { + console.error('Invalid land packet. x: ' + x + ', y: ' + y + ', patchSize: ' + patchSize); + return; + } + else + { + // Decode patch + let temp = 0; + for (let n = 0; n < patchSize * patchSize; n++) + { + temp = nibbler.UnpackBits(1); + if (temp !== 0) + { + temp = nibbler.UnpackBits(1); + if (temp !== 0) + { + temp = nibbler.UnpackBits(1); + if (temp !== 0) + { + // negative + temp = nibbler.UnpackBits(wordBits); + patches[n] = temp * -1; + + } + else + { + // positive + temp = nibbler.UnpackBits(wordBits); + patches[n] = temp; + } + } + else + { + for (let o = n; o < patchSize * patchSize; o++) + { + patches[o] = 0; + } + break; + } + } + else + { + patches[n] = 0; + } + } + + // Decompress this patch + const block: number[] = []; + const output: number[] = []; + + const prequant = (quantWBits >> 4) + 2; + const quantize = 1 << prequant; + const ooq = 1.0 / quantize; + const mult = ooq * range; + const addVal = mult * (1 << (prequant - 1)) + dcOffset; + + if (patchSize === 16) + { + for (let n = 0; n < 16 * 16; n++) + { + block.push(patches[Region.CopyMatrix16[n]] * Region.DequantizeTable16[n]) + } + + const ftemp: number[] = []; + for (let o = 0; o < 16 * 16; o++) + { + ftemp.push(o); + } + for (let o = 0; o < 16; o++) + { + Region.IDCTColumn16(block, ftemp, o); + } + for (let o = 0; o < 16; o++) + { + Region.IDCTLine16(ftemp, block, o); + } + } + else + { + throw new Error('IDCTPatchLarge not implemented'); + } + + for (let j = 0; j < block.length; j++) + { + output.push(block[j] * mult + addVal); + } + + let outputIndex = 0; + for (let yPoint = y * 16; yPoint < (y + 1) * 16; yPoint++) + { + for (let xPoint = x * 16; xPoint < (x + 1) * 16; xPoint++) + { + if (this.terrain[yPoint][xPoint] === -1) + { + this.tilesReceived++; + } + this.terrain[yPoint][xPoint] = output[outputIndex++]; + } + } + + if (this.tilesReceived === 65536) + { + this.terrainComplete = true; + this.terrainCompleteEvent.next(); + } + } + } + } + break; + } + break; + } + }) } + getParcels(): Parcel[] + { + const found: {[key: number]: Parcel} = {}; + for (let y = 0; y < 64; y++) + { + for (let x = 0; x < 64; x++) + { + if (this.parcelMap[y][x] !== 0) + { + const localID = this.parcelMap[y][x]; + if (!found[localID]) + { + found[localID] = this.parcels[localID]; + } + } + } + } + const result: Parcel[] = []; + for (const key of Object.keys(found)) + { + result.push(found[parseInt(key, 10)]); + } + return result; + } + + resetParcels(): void + { + this.parcelMap = []; + for (let x = 0; x < 64; x++) + { + this.parcelMap.push([]); + for (let y = 0; y < 64; y++) + { + this.parcelMap[x].push(0); + } + } + this.parcels = {}; + this.parcelsByUUID = {}; + this.parcelsComplete = false; + } + + waitForParcels(): Promise + { + return new Promise((resolve, reject) => + { + if (this.parcelsComplete) + { + resolve(); + } + else + { + let timeout: Timer | null = null; + const subscription = this.parcelsCompleteEvent.subscribe(() => + { + if (timeout !== null) + { + clearTimeout(timeout); + } + subscription.unsubscribe(); + resolve(); + }); + timeout = setTimeout(() => + { + subscription.unsubscribe(); + reject(new Error('Timeout waiting for parcels')); + }, 10000); + } + }); + } + + waitForTerrain(): Promise + { + return new Promise((resolve, reject) => + { + if (this.terrainComplete) + { + resolve(); + } + else + { + let timeout: Timer | null = null; + const subscription = this.terrainCompleteEvent.subscribe(() => + { + if (timeout !== null) + { + clearTimeout(timeout); + } + subscription.unsubscribe(); + resolve(); + }); + timeout = setTimeout(() => + { + subscription.unsubscribe(); + reject(new Error('Timeout waiting for terrain')); + }, 10000); + } + }); + } + + getTerrainHeightAtPoint(x: number, y: number): number + { + const patchX = Math.floor(x / 16); + const patchY = Math.floor(y / 16); + x = x % 16; + y = y % 16; + + const p = this.terrain[patchY * 16 + patchX]; + if (p === null) + { + return 0; + } + return p[y * 16 + x]; + } + + exportXML(): string + { + const document = builder.create('RegionSettings'); + const general = document.ele('General'); + general.ele('AllowDamage', (this.regionFlags & RegionFlags.AllowDamage) ? 'True' : 'False'); + general.ele('AllowLandResell', !(this.regionFlags & RegionFlags.BlockLandResell) ? 'True' : 'False'); + general.ele('AllowLandJoinDivide', (this.regionFlags & RegionFlags.AllowParcelChanges) ? 'True' : 'False'); + general.ele('BlockFly', (this.regionFlags & RegionFlags.NoFly) ? 'True' : 'False'); + general.ele('BlockLandShowInSearch', (this.regionFlags & RegionFlags.BlockParcelSearch) ? 'True' : 'False'); + general.ele('BlockTerraform', (this.regionFlags & RegionFlags.BlockTerraform) ? 'True' : 'False'); + general.ele('DisableCollisions', (this.regionFlags & RegionFlags.SkipCollisions) ? 'True' : 'False'); + general.ele('DisablePhysics', (this.regionFlags & RegionFlags.SkipPhysics) ? 'True' : 'False'); + general.ele('DisableScripts', (this.regionFlags & RegionFlags.EstateSkipScripts) ? 'True' : 'False'); + general.ele('MaturityRating', (this.simAccess & SimAccessFlags.Mature & SimAccessFlags.Adult & SimAccessFlags.PG)); + general.ele('RestrictPushing', (this.regionFlags & RegionFlags.RestrictPushObject) ? 'True' : 'False'); + general.ele('AgentLimit', this.maxAgents); + general.ele('ObjectBonus', this.objectBonusFactor); + const groundTextures = document.ele('GroundTextures'); + groundTextures.ele('Texture1', this.terrainDetail0.toString()); + groundTextures.ele('Texture2', this.terrainDetail1.toString()); + groundTextures.ele('Texture3', this.terrainDetail2.toString()); + groundTextures.ele('Texture4', this.terrainDetail3.toString()); + + groundTextures.ele('ElevationLowSW', this.terrainStartHeight00); + groundTextures.ele('ElevationLowNW', this.terrainStartHeight01); + groundTextures.ele('ElevationLowSE', this.terrainStartHeight10); + groundTextures.ele('ElevationLowNE', this.terrainStartHeight11); + + groundTextures.ele('ElevationHighSW', this.terrainHeightRange00); + groundTextures.ele('ElevationHighNW', this.terrainHeightRange01); + groundTextures.ele('ElevationHighSE', this.terrainHeightRange10); + groundTextures.ele('ElevationHighNE', this.terrainHeightRange11); + + const terrain = document.ele('Terrain'); + terrain.ele('WaterHeight', this.waterHeight); + terrain.ele('TerrainRaiseLimit', this.terrainRaiseLimit); + terrain.ele('TerrainLowerLimit', this.terrainLowerLimit); + terrain.ele('UseEstateSun', (this.useEstateSun) ? 'True' : 'False'); + terrain.ele('FixedSun', (this.regionFlags & RegionFlags.SunFixed) ? 'True' : 'False'); + terrain.ele('SunPosition', this.sunHour); + this.environment.getXML(document); + return document.end({pretty: true, allowEmpty: true}); + } + activateCaps(seedURL: string) { this.caps = new Caps(this.agent, this, seedURL, this.clientEvents); @@ -185,9 +826,93 @@ export class Region } return FilterResponse.NoMatch; }); + + this.environment = new RegionEnvironment(); + this.environment.dayCycleKeyframes = []; + this.environment.skyPresets = {}; + this.environment.water = { + blurMultiplier: 0, + fresnelOffset: 0, + fresnelScale: 0, + normalScale: Vector3.getZero(), + normalMap: UUID.zero(), + scaleAbove: 0, + scaleBelow: 0, + underWaterFogMod: 0, + waterFogColor: Color4.white, + waterFogDensity: 0, + wave1Dir: Vector2.getZero(), + wave2Dir: Vector2.getZero() + }; + + await this.caps.waitForSeedCapability(); + const response = await this.caps.capsGetXML('EnvironmentSettings'); + if (response.length >= 4) + { + if (Array.isArray(response[1]) && typeof response[2] === 'object' && typeof response[3] === 'object') + { + for (const kf of response[1]) + { + this.environment.dayCycleKeyframes.push({ + time: kf[0], + preset: kf[1] + }); + } + for (const presetKey of Object.keys(response[2])) + { + const preset = response[2][presetKey]; + this.environment.skyPresets[presetKey] = new class implements SkyPreset + { + ambient = new Vector4(preset['ambient']); + blueDensity = new Vector4(preset['blue_density']); + blueHorizon = new Vector4(preset['blue_horizon']); + cloudColor = new Color4(preset['cloud_color']); + cloudPosDensity1 = new Vector4(preset['cloud_pos_density1']); + cloudPosDensity2 = new Vector4(preset['cloud_pos_density2']); + cloudScale = new Vector4(preset['cloud_scale']); + cloudScrollRate = new Vector2(preset['cloud_scroll_rate']); + cloudShadow = new Vector4(preset['cloud_shadow']); + densityMultiplier = new Vector4(preset['density_multiplier']); + distanceMultiplier = new Vector4(preset['distance_multiplier']); + eastAngle = preset['east_angle']; + enableCloudScroll = { + x: preset['enable_cloud_scroll'][0], + y: preset['enable_cloud_scroll'][1] + }; + gamma = new Vector4(preset['gamma']); + glow = new Vector4(preset['glow']); + hazeDensity = new Vector4(preset['haze_density']); + hazeHorizon = new Vector4(preset['haze_horizon']); + lightNormal = new Vector4(preset['lightnorm']); + maxY = new Vector4(preset['max_y']); + starBrightness = preset['start_brightness']; + sunAngle = preset['sun_angle']; + sunlightColor = new Color4(preset['sunlight_color']); + }; + } + const wat = response[3]; + this.environment.water = new class implements WaterPreset + { + blurMultiplier = wat['blurMultiplier']; + fresnelOffset = wat['fresnelOffset']; + fresnelScale = wat['fresnelScale']; + normalScale = new Vector3(wat['normScale']); + normalMap = new UUID(wat['normalMap'].toString()); + scaleAbove = wat['scaleAbove']; + scaleBelow = wat['scaleBelow']; + underWaterFogMod = wat['underWaterFogMod']; + waterFogColor = new Color4(wat['waterFogColor']); + waterFogDensity = wat['waterFogDensity']; + wave1Dir = new Vector2(wat['wave1Dir']); + wave2Dir = new Vector2(wat['wave2Dir']); + }; + } + } } shutdown() { + this.parcelPropertiesSubscription.unsubscribe(); + this.messageSubscription.unsubscribe(); this.comms.shutdown(); this.caps.shutdown(); this.objects.shutdown(); diff --git a/lib/classes/TextureEntry.ts b/lib/classes/TextureEntry.ts index 66fad2a..4d5554c 100644 --- a/lib/classes/TextureEntry.ts +++ b/lib/classes/TextureEntry.ts @@ -7,6 +7,7 @@ export class TextureEntry { defaultTexture: TextureEntryFace | null; faces: TextureEntryFace[] = []; + binary: Buffer; static readFaceBitfield(buf: Buffer, pos: number): { result: boolean, @@ -40,6 +41,7 @@ export class TextureEntry constructor(buf: Buffer) { + this.binary = buf; if (buf.length < 16) { this.defaultTexture = null; @@ -47,7 +49,7 @@ export class TextureEntry else { this.defaultTexture = new TextureEntryFace(null); - let pos = 0; + const pos = 0; let i = pos; // Texture @@ -241,7 +243,7 @@ export class TextureEntry // Material { - this.defaultTexture.materialb = buf[i++]; + this.defaultTexture.material = buf[i++]; let done = false; while (!done) @@ -257,7 +259,7 @@ export class TextureEntry if ((result.faceBits & bit) !== 0) { this.createFace(face); - this.faces[face].materialb = tmpByte; + this.faces[face].material = tmpByte; } } } @@ -266,7 +268,7 @@ export class TextureEntry // Media { - this.defaultTexture.mediab = buf[i++]; + this.defaultTexture.media = buf[i++]; let done = false; while (i - pos < buf.length && !done) @@ -282,7 +284,7 @@ export class TextureEntry if ((result.faceBits & bit) !== 0) { this.createFace(face); - this.faces[face].mediab = tmpByte; + this.faces[face].media = tmpByte; } } } diff --git a/lib/classes/TextureEntryFace.ts b/lib/classes/TextureEntryFace.ts index ce3f4a1..506ca65 100644 --- a/lib/classes/TextureEntryFace.ts +++ b/lib/classes/TextureEntryFace.ts @@ -1,14 +1,17 @@ import {UUID} from './UUID'; import {Color4} from './Color4'; import {TextureFlags} from '../enums/TextureFlags'; +import {Bumpiness} from '../enums/Bumpiness'; +import {Shininess} from '../enums/Shininess'; +import {MappingType} from '../enums/MappingType'; export class TextureEntryFace { - private BUMP_MASK = 0x1F; - private FULLBRIGHT_MASK = 0x20; - private SHINY_MASK = 0xC0; - private MEDIA_MASK = 0x01; - private TEX_MAP_MASK = 0x06; + static BUMP_MASK = 0x1F; + static FULLBRIGHT_MASK = 0x20; + static SHINY_MASK = 0xC0; + static MEDIA_MASK = 0x01; + static TEX_MAP_MASK = 0x06; textureID: UUID; rgba: Color4; @@ -17,11 +20,16 @@ export class TextureEntryFace offsetU: number; offsetV: number; rotation: number; - materialb: number; - mediab: number; glow: number; materialID: UUID; + bumpiness: Bumpiness = Bumpiness.None; + shininess: Shininess = Shininess.None; + mappingType: MappingType = MappingType.Default; + fullBright = false; + mediaFlags = false; + private materialb: number; + private mediab: number; private hasAttribute: TextureFlags; private defaultTexture: TextureEntryFace | null; @@ -40,4 +48,50 @@ export class TextureEntryFace this.hasAttribute = TextureFlags.None; } } + + get material(): number + { + return this.materialb; + } + + set material(material: number) + { + this.materialb = 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); + } + else if (this.defaultTexture !== null) + { + this.bumpiness = this.defaultTexture.bumpiness; + this.shininess = this.defaultTexture.shininess; + this.fullBright = this.defaultTexture.fullBright; + } + } + + get media(): number + { + return this.mediab; + } + + set media(media: number) + { + this.mediab = media; + if ((this.hasAttribute & TextureFlags.Media) !== 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; + } + else + { + throw new Error('No media attribute and default texture is null'); + } + } } diff --git a/lib/classes/UUID.ts b/lib/classes/UUID.ts index ba1c55d..09f63b5 100644 --- a/lib/classes/UUID.ts +++ b/lib/classes/UUID.ts @@ -1,4 +1,7 @@ import * as validator from 'validator'; +import * as builder from 'xmlbuilder'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; +import * as Long from 'long'; const uuid = require('uuid'); export class UUID @@ -15,6 +18,24 @@ export class UUID return new UUID(newUUID); } + static getString(u?: UUID): string + { + if (u === undefined) + { + return UUID.zero().toString(); + } + else + { + return u.toString(); + } + } + + static getXML(doc: XMLElementOrXMLNode, u?: UUID) + { + const str = UUID.getString(u); + doc.ele('UUID', str); + } + constructor(buf?: Buffer | string, pos?: number) { if (buf !== undefined) @@ -78,4 +99,29 @@ export class UUID return cmp.equals(this.mUUID); } } + + public getBuffer() + { + const buf = Buffer.allocUnsafe(16); + this.writeToBuffer(buf, 0); + return buf; + } + + public getLong() + { + const buf = this.getBuffer(); + return new Long(buf.readUInt32LE(7), buf.readUInt32LE(12)); + } + + public bitwiseOr(w: UUID): UUID + { + const buf1 = this.getBuffer(); + const buf2 = w.getBuffer(); + const buf3 = Buffer.allocUnsafe(16); + for (let x = 0; x < 16; x++) + { + buf3[x] = buf1[x] ^ buf2[x]; + } + return new UUID(buf3, 0); + } } diff --git a/lib/classes/Utils.ts b/lib/classes/Utils.ts index 25cd3c9..9dc60fc 100644 --- a/lib/classes/Utils.ts +++ b/lib/classes/Utils.ts @@ -22,6 +22,29 @@ export class Utils return buf.toString('utf8'); } } + static JSONStringify(obj: object, space: number) + { + const cache: any[] = []; + return JSON.stringify(obj, function (key, value) + { + if (typeof value === 'object' && value !== null) + { + if (cache.indexOf(value) !== -1) + { + try + { + return JSON.parse(JSON.stringify(value)); + } + catch (error) + { + return 'Circular Reference'; + } + } + cache.push(value); + } + return value; + }, space); + } static BufferToString(buf: Buffer, startPos?: number): { readLength: number, diff --git a/lib/classes/Vector2.ts b/lib/classes/Vector2.ts index b346e33..2c954e5 100644 --- a/lib/classes/Vector2.ts +++ b/lib/classes/Vector2.ts @@ -1,4 +1,5 @@ import {vec2} from '../tsm/vec2'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; export class Vector2 extends vec2 { @@ -7,6 +8,16 @@ export class Vector2 extends vec2 return new Vector2(); } + static getXML(doc: XMLElementOrXMLNode, v?: Vector2) + { + if (v === undefined) + { + v = Vector2.getZero(); + } + doc.ele('X', v.x); + doc.ele('Y', v.y); + } + constructor(buf?: Buffer | number[], pos?: number, double?: boolean) { if (double === undefined) diff --git a/lib/classes/Vector3.ts b/lib/classes/Vector3.ts index 059c65b..c467dfc 100644 --- a/lib/classes/Vector3.ts +++ b/lib/classes/Vector3.ts @@ -1,4 +1,5 @@ import {vec3} from '../tsm/vec3'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; export class Vector3 extends vec3 { @@ -7,36 +8,57 @@ export class Vector3 extends vec3 return new Vector3(); } - constructor(buf?: Buffer | number[], pos?: number, double?: boolean) + static getXML(doc: XMLElementOrXMLNode, v?: Vector3) { - if (double === undefined) + if (v === undefined) { - double = false; + v = Vector3.getZero(); } - if (buf !== undefined && pos !== undefined && buf instanceof Buffer) + doc.ele('X', v.x); + doc.ele('Y', v.y); + doc.ele('Z', v.z); + } + + constructor(buf?: Buffer | number[] | Vector3, pos?: number, double?: boolean) + { + if (buf instanceof Vector3) { - if (!double) - { - const x = buf.readFloatLE(pos); - const y = buf.readFloatLE(pos + 4); - const z = buf.readFloatLE(pos + 8); - super([x, y, z]); - } - else - { - const x = buf.readDoubleLE(pos); - const y = buf.readDoubleLE(pos + 8); - const z = buf.readDoubleLE(pos + 16); - super([x, y, z]); - } - } - else if (buf !== undefined && Array.isArray(buf)) - { - super(buf); + super(); + this.x = buf.x; + this.y = buf.y; + this.z = buf.z; } else { - super(); + if (double === undefined) + { + double = false; + } + if (buf !== undefined && pos !== undefined && buf instanceof Buffer) + { + if (!double) + { + const x = buf.readFloatLE(pos); + const y = buf.readFloatLE(pos + 4); + const z = buf.readFloatLE(pos + 8); + super([x, y, z]); + } + else + { + const x = buf.readDoubleLE(pos); + const y = buf.readDoubleLE(pos + 8); + const z = buf.readDoubleLE(pos + 16); + super([x, y, z]); + } + } + else if (buf !== undefined && Array.isArray(buf)) + { + super(buf); + } + else + { + super(); + } } } writeToBuffer(buf: Buffer, pos: number, double: boolean) @@ -58,4 +80,6 @@ export class Vector3 extends vec3 { return '<' + this.x + ', ' + this.y + ', ' + this.z + '>'; } + + } diff --git a/lib/classes/Vector4.ts b/lib/classes/Vector4.ts index 0f7ac72..ee3f01b 100644 --- a/lib/classes/Vector4.ts +++ b/lib/classes/Vector4.ts @@ -1,4 +1,5 @@ import {vec4} from '../tsm/vec4'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; export class Vector4 extends vec4 { @@ -7,6 +8,18 @@ export class Vector4 extends vec4 return new Vector4(); } + static getXML(doc: XMLElementOrXMLNode, v?: Vector4) + { + if (v === undefined) + { + v = Vector4.getZero(); + } + doc.ele('X', v.x); + doc.ele('Y', v.y); + doc.ele('Z', v.z); + doc.ele('W', v.w); + } + constructor(buf?: Buffer | number[], pos?: number) { if (buf !== undefined && pos !== undefined && buf instanceof Buffer) diff --git a/lib/classes/commands/AssetCommands.ts b/lib/classes/commands/AssetCommands.ts index e779d01..2338e10 100644 --- a/lib/classes/commands/AssetCommands.ts +++ b/lib/classes/commands/AssetCommands.ts @@ -2,13 +2,313 @@ import {CommandsBase} from './CommandsBase'; import {UUID} from '../UUID'; import * as LLSD from '@caspertech/llsd'; import {Utils} from '../Utils'; -import {HTTPAssets} from '../..'; +import {AssetType, HTTPAssets, PacketFlags} 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'; +import {TransferSourceType} from '../../enums/TransferSourceTypes'; +import {TransferInfoMessage} from '../messages/TransferInfo'; +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 { async downloadAsset(type: HTTPAssets, uuid: UUID): Promise { - return await this.currentRegion.caps.downloadAsset(uuid, type); + const result = await this.currentRegion.caps.downloadAsset(uuid, type); + if (result.toString('UTF-8').trim() === 'Not found!') + { + throw new Error('Asset not found'); + } + else if (result.toString('UTF-8').trim() === 'Incorrect Syntax') + { + throw new Error('Invalid Syntax'); + } + return result; + } + + downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise + { + return new Promise((resolve, reject) => + { + const transferParams = Buffer.allocUnsafe(100); + let pos = 0; + this.agent.agentID.writeToBuffer(transferParams, pos); + pos = pos + 16; + this.circuit.sessionID.writeToBuffer(transferParams, pos); + pos = pos + 16; + ownerID.writeToBuffer(transferParams, pos); + pos = pos + 16; + objectID.writeToBuffer(transferParams, pos); + pos = pos + 16; + itemID.writeToBuffer(transferParams, pos); + pos = pos + 16; + assetID.writeToBuffer(transferParams, pos); + pos = pos + 16; + transferParams.writeInt32LE(type, pos); + + const transferID = UUID.random(); + + const msg = new TransferRequestMessage(); + msg.TransferInfo = { + TransferID: transferID, + ChannelType: TransferChannelType.Asset, + SourceType: TransferSourceType.SimInventoryItem, + Priority: 100.0 + (priority ? 1.0 : 0.0), + Params: transferParams + }; + + this.circuit.sendMessage(msg, PacketFlags.Reliable); + let gotInfo = true; + let expectedSize = 0; + const packets: {[key: number]: Buffer} = {}; + const subscription = this.circuit.subscribeToMessages([ + Message.TransferInfo, + Message.TransferAbort, + Message.TransferPacket + ], (packet: Packet) => + { + try + { + switch (packet.message.id) + { + case Message.TransferPacket: + { + const messg = packet.message as TransferPacketMessage; + packets[messg.TransferData.Packet] = messg.TransferData.Data; + switch (messg.TransferData.Status) + { + case TransferStatus.Abort: + throw new Error('Transfer Aborted'); + case TransferStatus.Error: + throw new Error('Error'); + case TransferStatus.Skip: + console.error('TransferPacket: Skip! not sure what this means'); + break; + case TransferStatus.InsufficientPermissions: + throw new Error('Insufficient Permissions'); + case TransferStatus.NotFound: + throw new Error('Not Found'); + } + break; + } + case Message.TransferInfo: + { + const messg = packet.message as TransferInfoMessage; + if (!messg.TransferInfo.TransferID.equals(transferID)) + { + return; + } + const status = messg.TransferInfo.Status as TransferStatus; + switch (status) + { + case TransferStatus.OK: + expectedSize = messg.TransferInfo.Size; + gotInfo = true; + if (outAssetID !== undefined) + { + outAssetID.assetID = new UUID(messg.TransferInfo.Params, 80); + } + break; + case TransferStatus.Abort: + throw new Error('Transfer Aborted'); + case TransferStatus.Error: + throw new Error('Error'); + // See if we get anything else + break; + case TransferStatus.Skip: + console.error('TransferInfo: Skip! not sure what this means'); + break; + case TransferStatus.InsufficientPermissions: + throw new Error('Insufficient Permissions'); + case TransferStatus.NotFound: + throw new Error('Not Found'); + } + + break; + } + case Message.TransferAbort: + { + console.log('GOT TRANSFERABORT'); + const messg = packet.message as TransferAbortMessage; + if (!messg.TransferInfo.TransferID.equals(transferID)) + { + return; + } + throw new Error('Transfer Aborted'); + } + } + if (gotInfo) + { + let gotSize = 0; + for (const packetNum of Object.keys(packets)) + { + const pn: number = parseInt(packetNum, 10); + gotSize += packets[pn].length; + } + if (gotSize >= expectedSize) + { + const packetNumbers = Object.keys(packets).sort(); + const buffers = []; + for (const pn of packetNumbers) + { + buffers.push(packets[parseInt(pn, 10)]); + } + subscription.unsubscribe(); + resolve(Buffer.concat(buffers)); + } + } + } + catch (error) + { + subscription.unsubscribe(); + reject(error); + } + }) + }); + } + + private getMaterialsLimited(uuidArray: any[], uuids: {[key: string]: Material | null}): Promise + { + return new Promise((resolve, reject) => + { + const binary = LLSD.LLSD.formatBinary(uuidArray); + const options: ZlibOptions = { + level: 9 + }; + zlib.deflate(Buffer.from(binary.toArray()), options, async (error: Error | null, res: Buffer) => + { + if (error) + { + reject(error); + return; + } + const result = await this.currentRegion.caps.capsRequestXML('RenderMaterials', { + 'Zipped': new LLSD.LLSD.asBinary(res.toString('base64')) + }, false); + + const resultZipped = Buffer.from(result['Zipped'].octets); + zlib.inflate(resultZipped, async (err: Error | null, reslt: Buffer) => + { + if (err) + { + reject(error); + return; + } + const binData = new LLSD.Binary(Array.from(reslt), 'BASE64'); + const obj = LLSD.LLSD.parseBinary(binData); + if (obj.length > 0) + { + for (const mat of obj) + { + if (mat['ID']) + { + const nbuf = Buffer.from(mat['ID'].toArray()); + const nuuid = new UUID(nbuf, 0).toString(); + if (uuids[nuuid] !== undefined) + { + if (mat['Material']) + { + const material = new Material(); + material.alphaMaskCutoff = mat['Material']['AlphaMaskCutoff']; + material.diffuseAlphaMode = mat['Material']['DiffuseAlphaMode']; + material.envIntensity = mat['Material']['EnvIntensity']; + material.normMap = new UUID(mat['Material']['NormMap'].toString()); + material.normOffsetX = mat['Material']['NormOffsetX']; + material.normOffsetY = mat['Material']['NormOffsetY']; + material.normRepeatX = mat['Material']['NormRepeatX']; + material.normRepeatY = mat['Material']['NormRepeatY']; + material.normRotation = mat['Material']['NormRotation']; + material.specColor = new Color4(mat['Material']['SpecColor'][0], mat['Material']['SpecColor'][1], mat['Material']['SpecColor'][2], mat['Material']['SpecColor'][3]); + material.specExp = mat['Material']['SpecExp']; + material.specMap = new UUID(mat['Material']['SpecMap'].toString()); + material.specOffsetX = mat['Material']['SpecOffsetX']; + material.specOffsetY = mat['Material']['SpecOffsetY']; + material.specRepeatX = mat['Material']['SpecRepeatX']; + material.specRepeatY = mat['Material']['SpecRepeatY']; + material.specRotation = mat['Material']['SpecRotation']; + material.llsd = LLSD.LLSD.formatXML(mat['Material']); + uuids[nuuid] = material; + } + } + } + } + resolve(); + } + else + { + reject(new Error('Material data not found')); + } + }); + }); + }); + } + + async getMaterials(uuids: {[key: string]: Material | null}): Promise + { + let uuidArray: any[] = []; + let submittedUUIDS: {[key: string]: Material | null} = {}; + for (const uuid of Object.keys(uuids)) + { + if (uuidArray.length > 32) + { + try + { + await this.getMaterialsLimited(uuidArray, submittedUUIDS); + let resolvedCount = 0; + let totalCount = 0; + for (const uu of Object.keys(submittedUUIDS)) + { + if (submittedUUIDS[uu] !== null) + { + resolvedCount++; + uuids[uu] = submittedUUIDS[uu]; + } + totalCount++; + } + console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials'); + + } + catch (error) + { + console.error(error); + } + uuidArray = []; + submittedUUIDS = {}; + } + if (!submittedUUIDS[uuid]) + { + submittedUUIDS[uuid] = uuids[uuid]; + uuidArray.push(new LLSD.Binary(Array.from(new UUID(uuid).getBuffer()))) + } + } + try + { + await this.getMaterialsLimited(uuidArray, submittedUUIDS); + let resolvedCount = 0; + let totalCount = 0; + for (const uu of Object.keys(submittedUUIDS)) + { + if (submittedUUIDS[uu] !== null) + { + resolvedCount++; + uuids[uu] = submittedUUIDS[uu]; + } + totalCount++; + } + console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials (end)'); + } + catch (error) + { + console.error(error); + } } uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise diff --git a/lib/classes/commands/RegionCommands.ts b/lib/classes/commands/RegionCommands.ts index d120c6d..47a7ef4 100644 --- a/lib/classes/commands/RegionCommands.ts +++ b/lib/classes/commands/RegionCommands.ts @@ -5,17 +5,28 @@ import {RegionHandleRequestMessage} from '../messages/RegionHandleRequest'; import {Message} from '../../enums/Message'; import {FilterResponse} from '../../enums/FilterResponse'; import {RegionIDAndHandleReplyMessage} from '../messages/RegionIDAndHandleReply'; -import {PacketFlags, PCode, Vector3} from '../..'; +import {AssetType, PacketFlags, PCode, Vector3} from '../..'; import {ObjectGrabMessage} from '../messages/ObjectGrab'; import {ObjectDeGrabMessage} from '../messages/ObjectDeGrab'; import {ObjectGrabUpdateMessage} from '../messages/ObjectGrabUpdate'; -import {GameObject} from '../GameObject'; +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 * 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'; +import {InventoryItem} from '../InventoryItem'; +import {AssetTypeLL} from '../../enums/AssetTypeLL'; +import {SaleTypeLL} from '../../enums/SaleTypeLL'; +import {InventoryTypeLL} from '../../enums/InventoryTypeLL'; +import Timer = NodeJS.Timer; +import {NewObjectEvent} from '../../events/NewObjectEvent'; export class RegionCommands extends CommandsBase { @@ -94,6 +105,37 @@ export class RegionCommands extends CommandsBase return this.currentRegion.objects.getNumberOfObjects(); } + getTerrainTextures(): UUID[] + { + const textures: UUID[] = []; + textures.push(this.currentRegion.terrainDetail0); + textures.push(this.currentRegion.terrainDetail1); + textures.push(this.currentRegion.terrainDetail2); + textures.push(this.currentRegion.terrainDetail3); + return textures; + } + + exportSettings(): string + { + return this.currentRegion.exportXML(); + } + + async getTerrain() + { + await this.currentRegion.waitForTerrain(); + const buf = Buffer.allocUnsafe(262144); + let pos = 0; + for (let x = 0; x < 256; x++) + { + for (let y = 0; y < 256; y++) + { + buf.writeFloatLE(this.currentRegion.terrain[x][y], pos); + pos = pos + 4; + } + } + return buf; + } + async selectObjects(objects: GameObject[]) { // Limit to 255 objects at once @@ -139,64 +181,131 @@ export class RegionCommands extends CommandsBase let resolved = 0; this.circuit.sendMessage(selectObject, PacketFlags.Reliable); - return await this.circuit.waitForMessage(Message.ObjectProperties, 10000, (propertiesMessage: ObjectPropertiesMessage): FilterResponse => + const unresolved = []; + try { - let found = false; - for (const objData of propertiesMessage.ObjectData) + const results = await this.circuit.waitForMessage(Message.ObjectProperties, 10000, (propertiesMessage: ObjectPropertiesMessage): FilterResponse => { - const objDataUUID = objData.ObjectID.toString(); - if (uuidMap[objDataUUID] !== undefined) + let found = false; + for (const objData of propertiesMessage.ObjectData) { - resolved++; - const obj = uuidMap[objDataUUID]; - obj.creatorID = objData.CreatorID; - obj.creationDate = objData.CreationDate; - obj.baseMask = objData.BaseMask; - obj.ownerMask = objData.OwnerMask; - obj.groupMask = objData.GroupMask; - obj.everyoneMask = objData.EveryoneMask; - obj.nextOwnerMask = objData.NextOwnerMask; - obj.ownershipCost = objData.OwnershipCost; - obj.saleType = objData.SaleType; - obj.salePrice = objData.SalePrice; - obj.aggregatePerms = objData.AggregatePerms; - obj.aggregatePermTextures = objData.AggregatePermTextures; - obj.aggregatePermTexturesOwner = objData.AggregatePermTexturesOwner; - obj.category = objData.Category; - obj.inventorySerial = objData.InventorySerial; - obj.itemID = objData.ItemID; - obj.folderID = objData.FolderID; - obj.fromTaskID = objData.FromTaskID; - obj.lastOwnerID = objData.LastOwnerID; - obj.name = Utils.BufferToStringSimple(objData.Name); - obj.description = Utils.BufferToStringSimple(objData.Description); - obj.touchName = Utils.BufferToStringSimple(objData.TouchName); - obj.sitName = Utils.BufferToStringSimple(objData.SitName); - obj.textureID = Utils.BufferToStringSimple(objData.TextureID); - obj.resolvedAt = new Date().getTime() / 1000; - delete uuidMap[objDataUUID]; - found = true; + const objDataUUID = objData.ObjectID.toString(); + if (uuidMap[objDataUUID] !== undefined) + { + resolved++; + const obj = uuidMap[objDataUUID]; + obj.creatorID = objData.CreatorID; + obj.creationDate = objData.CreationDate; + obj.baseMask = objData.BaseMask; + obj.ownerMask = objData.OwnerMask; + obj.groupMask = objData.GroupMask; + obj.everyoneMask = objData.EveryoneMask; + obj.nextOwnerMask = objData.NextOwnerMask; + obj.ownershipCost = objData.OwnershipCost; + obj.saleType = objData.SaleType; + obj.salePrice = objData.SalePrice; + obj.aggregatePerms = objData.AggregatePerms; + obj.aggregatePermTextures = objData.AggregatePermTextures; + obj.aggregatePermTexturesOwner = objData.AggregatePermTexturesOwner; + obj.category = objData.Category; + obj.inventorySerial = objData.InventorySerial; + obj.itemID = objData.ItemID; + obj.folderID = objData.FolderID; + obj.fromTaskID = objData.FromTaskID; + obj.groupID = objData.GroupID; + obj.lastOwnerID = objData.LastOwnerID; + obj.name = Utils.BufferToStringSimple(objData.Name); + obj.description = Utils.BufferToStringSimple(objData.Description); + obj.touchName = Utils.BufferToStringSimple(objData.TouchName); + obj.sitName = Utils.BufferToStringSimple(objData.SitName); + obj.textureID = Utils.BufferToStringSimple(objData.TextureID); + obj.resolvedAt = new Date().getTime() / 1000; + delete uuidMap[objDataUUID]; + found = true; - // console.log(obj.name + ' (' + resolved + ' of ' + objects.length + ')'); + // console.log(obj.name + ' (' + resolved + ' of ' + objects.length + ')'); + } } - } - if (Object.keys(uuidMap).length === 0) + if (Object.keys(uuidMap).length === 0) + { + return FilterResponse.Finish; + } + if (!found) + { + return FilterResponse.NoMatch; + } + else + { + return FilterResponse.Match; + } + }); + } + catch (error) + { + + } + finally + { + + for (const obj of objects) { - return FilterResponse.Finish; + if (obj.resolvedAt === undefined || obj.name === undefined) + { + obj.resolveAttempts++; + } } - if (!found) - { - return FilterResponse.NoMatch; - } - else - { - return FilterResponse.Match; - } - }); + } } } - private async resolveObjects(objects: GameObject[]) + private parseLine(line: string): { + 'key': string | null, + 'value': string + } + { + line = line.trim().replace(/[\t]/gu, ' ').trim(); + while (line.indexOf('\u0020\u0020') > 0) + { + line = line.replace(/\u0020\u0020/gu, '\u0020'); + } + let key: string | null = null; + let value = ''; + if (line.length > 2) + { + const sep = line.indexOf(' '); + if (sep > 0) + { + key = line.substr(0, sep); + value = line.substr(sep + 1); + } + } + else if (line.length === 1) + { + key = line; + } + else if (line.length > 0) + { + return { + 'key': line, + 'value': '' + } + } + if (key !== null) + { + key = key.trim(); + } + return { + 'key': key, + 'value': value + } + } + + getName(): string + { + return this.currentRegion.regionName; + } + + private async resolveObjects(objects: GameObject[], onlyUnresolved: boolean = false) { // First, create a map of all object IDs const objs: {[key: number]: GameObject} = {}; @@ -235,11 +344,14 @@ export class RegionCommands extends CommandsBase { o.resolvedAt = 0; } - if (o.resolvedAt !== undefined && o.resolvedAt < resolveTime && o.PCode !== PCode.Avatar) + if (o.resolvedAt !== undefined && o.resolvedAt < resolveTime && o.PCode !== PCode.Avatar && o.resolveAttempts < 3 && (o.Flags === undefined || !(o.Flags & PrimFlags.TemporaryOnRez))) { - objs[ky].name = undefined; totalRemaining++; - objectList.push(objs[ky]); + if (!onlyUnresolved || objs[ky].name === undefined) + { + objs[ky].name = undefined; + objectList.push(objs[ky]); + } if (objectList.length > 254) { try @@ -278,10 +390,304 @@ export class RegionCommands extends CommandsBase } } } + const objectSet = Object.keys(objs); + let count = 0; + for (const k of objectSet) + { + count++; + const ky = parseInt(k, 10); + if (objs[ky] !== undefined) + { + const o = objs[ky]; + if (o.FullID !== undefined && o.name !== undefined && o.Flags !== undefined && !(o.Flags & PrimFlags.InventoryEmpty) && (!o.inventory || o.inventory.length === 0)) + { + console.log(' ... Downloading task inventory for object ' + o.FullID.toString() + ' (' + o.name + '), done ' + count + ' of ' + objectSet.length); + const req = new RequestTaskInventoryMessage(); + req.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + req.InventoryData = { + LocalID: o.ID + }; + this.circuit.sendMessage(req, PacketFlags.Reliable); + try + { + const inventory = await this.circuit.waitForMessage(Message.ReplyTaskInventory, 10000, (message: ReplyTaskInventoryMessage): FilterResponse => + { + if (message.InventoryData.TaskID.equals(o.FullID)) + { + return FilterResponse.Finish; + } + else + { + return FilterResponse.Match; + } + }); + const fileName = Utils.BufferToStringSimple(inventory.InventoryData.Filename); + + const file = await this.circuit.XferFile(fileName, true, false, UUID.zero(), AssetType.Unknown, true); + if (file.length === 0) + { + o.Flags = o.Flags | PrimFlags.InventoryEmpty; + } + else + { + let str = file.toString('utf-8'); + let nl = str.indexOf('\0'); + while (nl !== -1) + { + str = str.substr(nl + 1); + nl = str.indexOf('\0') + } + const lines: string[] = str.replace(/\r\n/g, '\n').split('\n'); + let lineNum = 0; + while (lineNum < lines.length) + { + let line = lines[lineNum++]; + let result = this.parseLine(line); + if (result.key !== null) + { + switch (result.key) + { + case 'inv_object': + let itemID = UUID.zero(); + let parentID = UUID.zero(); + let name = ''; + let assetType: AssetType = AssetType.Unknown; + + while (lineNum < lines.length) + { + result = this.parseLine(lines[lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'obj_id') + { + itemID = new UUID(result.value); + } + else if (result.key === 'parent_id') + { + parentID = new UUID(result.value); + } + else if (result.key === 'type') + { + const typeString = result.value as any; + assetType = parseInt(AssetTypeLL[typeString], 10); + } + else if (result.key === 'name') + { + name = result.value.substr(0, result.value.indexOf('|')); + } + } + } + + if (name !== 'Contents') + { + console.log('TODO: Do something useful with inv_objects') + } + + break; + case 'inv_item': + const item: InventoryItem = new InventoryItem(); + while (lineNum < lines.length) + { + line = lines[lineNum++]; + result = this.parseLine(line); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'item_id') + { + item.itemID = new UUID(result.value); + } + else if (result.key === 'parent_id') + { + item.parentID = new UUID(result.value); + } + else if (result.key === 'permissions') + { + while (lineNum < lines.length) + { + result = this.parseLine(lines[lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'creator_mask') + { + item.permissions.baseMask = parseInt(result.value, 16); + } + else if (result.key === 'base_mask') + { + item.permissions.baseMask = parseInt(result.value, 16); + } + else if (result.key === 'owner_mask') + { + item.permissions.ownerMask = parseInt(result.value, 16); + } + else if (result.key === 'group_mask') + { + item.permissions.groupMask = parseInt(result.value, 16); + } + else if (result.key === 'everyone_mask') + { + item.permissions.everyoneMask = parseInt(result.value, 16); + } + else if (result.key === 'next_owner_mask') + { + item.permissions.nextOwnerMask = parseInt(result.value, 16); + } + else if (result.key === 'creator_id') + { + item.permissions.creator = new UUID(result.value); + } + else if (result.key === 'owner_id') + { + item.permissions.owner = new UUID(result.value); + } + else if (result.key === 'last_owner_id') + { + item.permissions.lastOwner = new UUID(result.value); + } + else if (result.key === 'group_id') + { + item.permissions.group = new UUID(result.value); + } + else if (result.key === 'group_owned') + { + const val = parseInt(result.value, 10); + item.permissions.groupOwned = (val !== 0); + } + else + { + console.log('Unrecognised key (4): ' + result.key); + } + } + } + } + else if (result.key === 'sale_info') + { + while (lineNum < lines.length) + { + result = this.parseLine(lines[lineNum++]); + if (result.key !== null) + { + if (result.key === '{') + { + // do nothing + } + else if (result.key === '}') + { + break; + } + else if (result.key === 'sale_type') + { + const typeString = result.value as any; + item.saleType = parseInt(SaleTypeLL[typeString], 10); + } + else if (result.key === 'sale_price') + { + item.salePrice = parseInt(result.value, 10); + } + else + { + console.log('Unrecognised key (3): ' + result.key); + } + } + } + } + else if (result.key === 'shadow_id') + { + item.assetID = new UUID(result.value).bitwiseOr(new UUID('3c115e51-04f4-523c-9fa6-98aff1034730')); + } + else if (result.key === 'asset_id') + { + item.assetID = new UUID(result.value); + } + else if (result.key === 'type') + { + const typeString = result.value as any; + item.type = parseInt(AssetTypeLL[typeString], 10); + } + else if (result.key === 'inv_type') + { + const typeString = result.value as any; + item.inventoryType = parseInt(InventoryTypeLL[typeString], 10); + } + else if (result.key === 'flags') + { + item.flags = parseInt(result.value, 10); + } + else if (result.key === 'name') + { + item.name = result.value.substr(0, result.value.indexOf('|')); + } + else if (result.key === 'desc') + { + item.description = result.value.substr(0, result.value.indexOf('|')); + } + else if (result.key === 'creation_date') + { + item.created = new Date(parseInt(result.value, 10) * 1000); + } + else + { + console.log('Unrecognised key (2): ' + result.key); + } + } + } + o.inventory.push(item); + break; + default: + { + console.log('Unrecognised task inventory token: [' + result.key + ']'); + } + } + } + } + } + } + catch (error) + { + if (o.FullID !== undefined) + { + console.error('Error downloading task inventory of ' + o.FullID.toString() + ':'); + console.error(error); + } + else + { + console.error('Error downloading task inventory of ' + o.ID + ':'); + console.error(error); + } + } + } + } + } } catch (ignore) { - + console.error(ignore); } finally { @@ -310,17 +716,29 @@ export class RegionCommands extends CommandsBase for (const key of uuids) { const costs = result[key]; - const obj: GameObject = that.currentRegion.objects.getObjectByUUID(new UUID(key)); - obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']); - obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']); - obj.physicaImpact = parseFloat(costs['physics_cost']); - obj.resourceImpact = parseFloat(costs['resource_cost']); - - obj.landImpact = Math.ceil(obj.linkPhysicsImpact); - if (obj.linkResourceImpact > obj.linkPhysicsImpact) + try { - obj.landImpact = Math.ceil(obj.linkResourceImpact); + const obj: GameObject = that.currentRegion.objects.getObjectByUUID(new UUID(key)); + obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']); + obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']); + obj.physicaImpact = parseFloat(costs['physics_cost']); + obj.resourceImpact = parseFloat(costs['resource_cost']); + obj.limitingType = costs['resource_limiting_type']; + + + obj.landImpact = Math.round(obj.linkPhysicsImpact); + if (obj.linkResourceImpact > obj.linkPhysicsImpact) + { + obj.landImpact = Math.round(obj.linkResourceImpact); + } + obj.calculatedLandImpact = obj.landImpact; + if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy') + { + obj.calculatedLandImpact = 0; + } } + catch (error) + {} } }; @@ -328,7 +746,10 @@ export class RegionCommands extends CommandsBase const promises: Promise[] = []; for (const obj of objects) { - ids.push(new LLSD.UUID(obj.FullID)); + if (!onlyUnresolved || obj.landImpact === undefined) + { + ids.push(new LLSD.UUID(obj.FullID)); + } if (ids.length > 255) { promises.push(getCosts(ids)); @@ -343,6 +764,104 @@ export class RegionCommands extends CommandsBase } } + private waitForObjectByLocalID(localID: number, timeout: number): Promise + { + return new Promise((resolve, reject) => + { + let tmr: Timer | null = null; + const subscription = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (event: NewObjectEvent) => + { + if (event.localID === localID) + { + if (tmr !== null) + { + clearTimeout(tmr); + } + subscription.unsubscribe(); + resolve(event.object); + } + }); + tmr = setTimeout(() => { + subscription.unsubscribe(); + reject(new Error('Timeout')); + }, timeout) + }); + } + + private waitForObjectByUUID(uuid: UUID, timeout: number): Promise + { + return new Promise((resolve, reject) => + { + let tmr: Timer | null = null; + const subscription = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (event: NewObjectEvent) => + { + if (event.objectID.equals(uuid)) + { + if (tmr !== null) + { + clearTimeout(tmr); + } + subscription.unsubscribe(); + resolve(event.object); + } + }); + tmr = setTimeout(() => { + subscription.unsubscribe(); + reject(new Error('Timeout')); + }, timeout) + }); + } + + async getObjectByLocalID(id: number, resolve: boolean, waitFor: number = 0) + { + let obj = null; + try + { + obj = this.currentRegion.objects.getObjectByLocalID(id); + } + catch (error) + { + if (waitFor > 0) + { + obj = await this.waitForObjectByLocalID(id, waitFor); + } + else + { + throw(error); + } + } + if (resolve) + { + await this.resolveObjects([obj]); + } + return obj; + } + + async getObjectByUUID(id: UUID, resolve: boolean, waitFor: number = 0) + { + let obj = null; + try + { + obj = this.currentRegion.objects.getObjectByUUID(id); + } + catch (error) + { + if (waitFor > 0) + { + obj = await this.waitForObjectByUUID(id, waitFor); + } + else + { + throw(error); + } + } + if (resolve) + { + await this.resolveObjects([obj]); + } + return obj; + } + async findObjectsByName(pattern: string | RegExp, minX?: number, maxX?: number, minY?: number, maxY?: number, minZ?: number, maxZ?: number): Promise { let objects: GameObject[] = []; @@ -398,9 +917,61 @@ export class RegionCommands extends CommandsBase return matches; } - async getAllObjects(resolve: boolean = false): Promise + async getParcels(): Promise { - const objs = this.currentRegion.objects.getAllObjects(); + this.currentRegion.resetParcels(); + for (let y = 0; y < 64; y++) + { + for (let x = 0; x < 64; x++) + { + if (this.currentRegion.parcelMap[y][x] === 0) + { + const request = new ParcelPropertiesRequestMessage(); + request.AgentData = { + AgentID: this.agent.agentID, + SessionID: this.circuit.sessionID + }; + request.ParcelData = { + North: (y + 1) * 4.0, + East: (x + 1) * 4.0, + South: y * 4.0, + West: x * 4.0, + SequenceID: 2147483647, + SnapSelection: false + }; + const seqNo = this.circuit.sendMessage(request, PacketFlags.Reliable); + await this.circuit.waitForAck(seqNo, 10000); + // Wait a second until we request the next one + await function() + { + return new Promise((resolve, reject) => + { + setTimeout(() => + { + resolve(); + }, 1000); + }) + }(); + } + } + } + await this.currentRegion.waitForParcels(); + return this.currentRegion.getParcels(); + } + + async getAllObjects(resolve: boolean = false, onlyUnresolved: boolean = false): Promise + { + const objs = await this.currentRegion.objects.getAllObjects(); + if (resolve) + { + await this.resolveObjects(objs, onlyUnresolved); + } + return objs; + } + + async getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number, resolve: boolean = false): Promise + { + const objs = await this.currentRegion.objects.getObjectsInArea(minX, maxX, minY, maxY, minZ, maxZ); if (resolve) { await this.resolveObjects(objs); @@ -408,14 +979,70 @@ export class RegionCommands extends CommandsBase return objs; } - async getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number, resolve: boolean = false): Promise + async pruneObjects(checkList: GameObject[]): Promise { - const objs = this.currentRegion.objects.getObjectsInArea(minX, maxX, minY, maxY, minZ, maxZ); - if (resolve) + let uuids = []; + let objects = []; + const stillAlive: {[key: string]: GameObject} = {}; + const checkObjects = async (uuidList: any[], objectList: GameObject[]) => { - await this.resolveObjects(objs); + + const objRef: {[key: string]: GameObject} = {}; + for (const obj of objectList) + { + objRef[obj.FullID.toString()] = obj; + } + const result = await this.currentRegion.caps.capsRequestXML('GetObjectCost', { + 'object_ids': uuidList + }); + for (const u of Object.keys(result)) + { + stillAlive[u] = objRef[u]; + } + }; + + for (const o of checkList) + { + if (o.FullID) + { + uuids.push(new LLSD.UUID(o.FullID)); + objects.push(o); + if (uuids.length > 256) + { + await checkObjects(uuids, objects); + uuids = []; + objects = []; + } + } } - return objs; + if (uuids.length > 0) + { + await checkObjects(uuids, objects); + } + console.log('Found ' + Object.keys(stillAlive).length + ' objects still present out of ' + checkList.length + ' objects'); + const deadObjects: GameObject[] = []; + for (const o of checkList) + { + let found = false; + if (o.FullID) + { + const uuid = o.FullID.toString(); + if (stillAlive[uuid]) + { + found = true; + } + } + if (!found) + { + deadObjects.push(o); + } + } + return deadObjects; + } + + setPersist(persist: boolean) + { + this.currentRegion.objects.setPersist(persist); } async grabObject(localID: number | UUID, diff --git a/lib/classes/GameObject.ts b/lib/classes/interfaces/IGameObjectData.ts similarity index 55% rename from lib/classes/GameObject.ts rename to lib/classes/interfaces/IGameObjectData.ts index 69fbaeb..2b5a9ec 100644 --- a/lib/classes/GameObject.ts +++ b/lib/classes/interfaces/IGameObjectData.ts @@ -1,20 +1,24 @@ -import {Vector3} from './Vector3'; -import {UUID} from './UUID'; -import {Quaternion} from './Quaternion'; -import {Tree} from '../enums/Tree'; -import {SoundFlags} from '..'; -import {Vector4} from './Vector4'; -import {TextureEntry} from './TextureEntry'; -import {Color4} from './Color4'; -import {ParticleSystem} from './ParticleSystem'; -import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox'; -import {NameValue} from './NameValue'; -import {PCode} from '../enums/PCode'; -import {Utils} from './Utils'; +import {UUID} from '../UUID'; import * as Long from 'long'; +import {NameValue} from '../NameValue'; +import {Vector3} from '../Vector3'; +import {TextureEntry} from '../TextureEntry'; +import {Color4} from '../Color4'; +import {Quaternion} from '../Quaternion'; +import {Vector4} from '../Vector4'; +import {Tree} from '../../enums/Tree'; +import {PCode, SoundFlags} from '../..'; +import {ParticleSystem} from '../ParticleSystem'; +import {GameObject} from '../public/GameObject'; +import {FlexibleData} from '../public/FlexibleData'; +import {LightData} from '../public/LightData'; +import {LightImageData} from '../public/LightImageData'; +import {SculptData} from '../public/SculptData'; +import {MeshData} from '../public/MeshData'; -export class GameObject +export interface IGameObjectData { + deleted: boolean; creatorID?: UUID; creationDate?: Long; baseMask?: number; @@ -43,6 +47,7 @@ export class GameObject totalChildren?: number; landImpact?: number; + calculatedLandImpact?: number; physicaImpact?: number; resourceImpact?: number; linkResourceImpact?: number; @@ -50,22 +55,18 @@ export class GameObject limitingType?: string; children?: GameObject[]; - rtreeEntry?: ITreeBoundingBox; - ID = 0; - FullID = UUID.random(); - ParentID = 0; - OwnerID = UUID.zero(); - IsAttachment = false; - NameValue: {[key: string]: NameValue} = {}; - PCode: PCode = PCode.None; - + ID: number; + FullID: UUID; + ParentID?: number; + OwnerID: UUID; + IsAttachment: boolean; + NameValue: {[key: string]: NameValue}; + PCode: PCode; State?: number; CRC?: number; Material?: number; ClickAction?: number; Scale?: Vector3; - ObjectData?: Buffer; - UpdateFlags?: number; Flags?: number; PathCurve?: number; ProfileCurve?: number; @@ -86,12 +87,9 @@ export class GameObject ProfileEnd?: number; ProfileHollow?: number; TextureEntry?: TextureEntry; - TextureAnim?: Buffer; - Data?: Buffer; Text?: string; TextColor?: Color4; MediaURL?: string; - PSBlock?: Buffer; JointType?: number; JointPivot?: Vector3; JointAxisOrAnchor?: Vector3; @@ -107,30 +105,14 @@ export class GameObject SoundFlags?: SoundFlags; SoundRadius?: number; Particles?: ParticleSystem; - - constructor() - { - this.Position = Vector3.getZero(); - this.Rotation = Quaternion.getIdentity(); - this.AngularVelocity = Vector3.getZero(); - this.TreeSpecies = 0; - this.SoundFlags = 0; - this.SoundRadius = 1.0; - this.SoundGain = 1.0; - this.ParentID = 0; - } - - hasNameValueEntry(key: string): boolean - { - return this.NameValue[key] !== undefined; - } - - getNameValueEntry(key: string): string - { - if (this.NameValue[key]) - { - return this.NameValue[key].value; - } - return ''; - } -} + FlexibleData?: FlexibleData; + LightData?: LightData; + LightImageData?: LightImageData; + SculptData?: SculptData; + MeshData?: MeshData; + density?: number; + friction?: number; + gravityMultiplier?: number; + physicsShapeType?: number; + restitution?: number; +} \ No newline at end of file diff --git a/lib/classes/interfaces/IObjectStore.ts b/lib/classes/interfaces/IObjectStore.ts index bc2c7e6..84e1c9f 100644 --- a/lib/classes/interfaces/IObjectStore.ts +++ b/lib/classes/interfaces/IObjectStore.ts @@ -1,15 +1,16 @@ import {RBush3D} from 'rbush-3d/dist'; import {UUID} from '../UUID'; -import {GameObject} from '../GameObject'; +import {GameObject} from '../public/GameObject'; export interface IObjectStore { rtree?: RBush3D; getObjectsByParent(parentID: number): GameObject[]; shutdown(): void; - getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObject[]; + getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): Promise; getObjectByUUID(fullID: UUID): GameObject; getObjectByLocalID(ID: number): GameObject; getNumberOfObjects(): number; - getAllObjects(): GameObject[]; + getAllObjects(): Promise; + setPersist(persist: boolean): void; } diff --git a/lib/classes/interfaces/ITreeBoundingBox.ts b/lib/classes/interfaces/ITreeBoundingBox.ts index 17aca0e..2db70fd 100644 --- a/lib/classes/interfaces/ITreeBoundingBox.ts +++ b/lib/classes/interfaces/ITreeBoundingBox.ts @@ -1,5 +1,5 @@ import {BBox} from 'rbush-3d/dist'; -import {GameObject} from '../GameObject'; +import {GameObject} from '../public/GameObject'; export interface ITreeBoundingBox extends BBox { diff --git a/lib/classes/public/FlexibleData.ts b/lib/classes/public/FlexibleData.ts new file mode 100644 index 0000000..231111f --- /dev/null +++ b/lib/classes/public/FlexibleData.ts @@ -0,0 +1,24 @@ +import {Vector3} from '../Vector3'; + +export class FlexibleData +{ + Softness = 0; + Tension = 0.0; + Drag = 0.0; + Gravity = 0.0; + Wind = 0.0; + Force = Vector3.getZero(); + + constructor(buf: Buffer, pos: number, length: number) + { + if (length >= 5) + { + this.Softness = ((buf.readUInt8(pos) & 0x80) >> 6) | ((buf.readUInt8(pos + 1) & 0x80) >> 7); + this.Tension = (buf.readUInt8(pos++) & 0x7F) / 10.0; + this.Drag = (buf.readUInt8(pos++) & 0x7F) / 10.0; + this.Gravity = (buf.readUInt8(pos++) / 10.0) - 10.0; + this.Wind = (buf.readUInt8(pos++) / 10.0); + this.Force = new Vector3(buf, pos); + } + } +} \ No newline at end of file diff --git a/lib/classes/public/GameObject.ts b/lib/classes/public/GameObject.ts new file mode 100644 index 0000000..24500e2 --- /dev/null +++ b/lib/classes/public/GameObject.ts @@ -0,0 +1,514 @@ +import {Vector3} from '../Vector3'; +import {UUID} from '../UUID'; +import {Quaternion} from '../Quaternion'; +import {Tree} from '../../enums/Tree'; +import {Vector4} from '../Vector4'; +import {TextureEntry} from '../TextureEntry'; +import {Color4} from '../Color4'; +import {ParticleSystem} from '../ParticleSystem'; +import {ITreeBoundingBox} from '../interfaces/ITreeBoundingBox'; +import {NameValue} from '../NameValue'; +import * as Long from 'long'; +import {IGameObjectData} from '../interfaces/IGameObjectData'; +import {FlexibleData} from './FlexibleData'; +import {LightData} from './LightData'; +import {LightImageData} from './LightImageData'; +import {SculptData} from './SculptData'; +import {MeshData} from './MeshData'; +import {PCode, PrimFlags, SoundFlags} from '../..'; +import * as builder from 'xmlbuilder'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; +import {Region} from '../Region'; +import {TextureAnimFlags} from '../../enums/TextureAnimFlags'; +import {ProfileShape} from '../../enums/ProfileShape'; +import {HoleType} from '../../enums/HoleType'; +import {PhysicsShapeType} from '../../enums/PhysicsShapeType'; +import {InventoryItem} from '../InventoryItem'; + +export class GameObject implements IGameObjectData +{ + rtreeEntry?: ITreeBoundingBox; + TextureAnim?: Buffer; + Data?: Buffer; + ObjectData?: Buffer; + PSBlock?: Buffer; + + deleted = false; + creatorID?: UUID; + creationDate?: Long; + baseMask?: number; + ownerMask?: number; + groupMask?: number; + groupID?: UUID; + everyoneMask?: number; + nextOwnerMask?: number; + ownershipCost?: number; + saleType?: number; + salePrice?: number; + aggregatePerms?: number; + aggregatePermTextures?: number; + aggregatePermTexturesOwner?: number; + category: number; + inventorySerial: number; + itemID: UUID; + folderID: UUID; + fromTaskID: UUID; + lastOwnerID: UUID; + name?: string; + description?: string; + touchName?: string; + sitName?: string; + textureID?: string; + resolvedAt?: number; + totalChildren?: number; + + landImpact?: number; + calculatedLandImpact?: number; + physicaImpact?: number; + resourceImpact?: number; + linkResourceImpact?: number; + linkPhysicsImpact?: number; + limitingType?: string; + + children?: GameObject[]; + ID = 0; + FullID = UUID.random(); + ParentID?: number; + OwnerID = UUID.zero(); + IsAttachment = false; + NameValue: {[key: string]: NameValue} = {}; + PCode: PCode = PCode.None; + + State?: number; + CRC?: number; + Material?: number; + ClickAction?: number; + Scale?: Vector3; + Flags?: PrimFlags; + PathCurve?: number; + ProfileCurve?: number; + PathBegin?: number; + PathEnd?: number; + PathScaleX?: number; + PathScaleY?: number; + ExtraParams?: Buffer; + PathShearX?: number; + PathShearY?: number; + PathTwist?: number; + PathTwistBegin?: number; + PathRadiusOffset?: number; + PathTaperX?: number; + PathTaperY?: number; + PathRevolutions?: number; + PathSkew?: number; + ProfileBegin?: number; + ProfileEnd?: number; + ProfileHollow?: number; + TextureEntry?: TextureEntry; + Text?: string; + TextColor?: Color4; + MediaURL?: string; + JointType?: number; + JointPivot?: Vector3; + JointAxisOrAnchor?: Vector3; + Position?: Vector3; + Rotation?: Quaternion; + CollisionPlane?: Vector4; + Velocity?: Vector3; + Acceleration?: Vector3; + AngularVelocity?: Vector3; + TreeSpecies?: Tree; + Sound?: UUID; + SoundGain?: number; + SoundFlags?: SoundFlags; + SoundRadius?: number; + Particles?: ParticleSystem; + FlexibleData?: FlexibleData; + LightData?: LightData; + LightImageData?: LightImageData; + SculptData?: SculptData; + MeshData?: MeshData; + TextureAnimFlags?: TextureAnimFlags; + TextureAnimFace?: number; + TextureAnimSizeX?: number; + TextureAnimSizeY?: number; + TextureAnimStart?: number; + TextureAnimLength?: number; + TextureAnimRate?: number; + + density?: number; + friction?: number; + gravityMultiplier?: number; + physicsShapeType?: PhysicsShapeType; + restitution?: number; + + region: Region; + + inventory: InventoryItem[] = []; + + resolveAttempts = 0; + + constructor() + { + this.Position = Vector3.getZero(); + this.Rotation = Quaternion.getIdentity(); + this.AngularVelocity = Vector3.getZero(); + this.TreeSpecies = 0; + this.SoundFlags = 0; + this.SoundRadius = 1.0; + this.SoundGain = 1.0; + } + + hasNameValueEntry(key: string): boolean + { + return this.NameValue[key] !== undefined; + } + + getNameValueEntry(key: string): string + { + if (this.NameValue[key]) + { + return this.NameValue[key].value; + } + return ''; + } + + private getInventoryXML(xml: XMLElementOrXMLNode, inv: InventoryItem) + { + if (!inv.assetID.equals(UUID.zero())) + { + const item = xml.ele('TaskInventoryItem'); + UUID.getXML(item.ele('AssetID'), inv.assetID); + UUID.getXML(item.ele('ItemID'), inv.itemID); + if (inv.permissions) + { + item.ele('BasePermissions', inv.permissions.baseMask); + item.ele('EveryonePermissions', inv.permissions.everyoneMask); + item.ele('GroupPermissions', inv.permissions.groupMask); + item.ele('NextPermissions', inv.permissions.nextOwnerMask); + item.ele('CurrentPermissions', inv.permissions.ownerMask); + item.ele('PermsMask', 0); + UUID.getXML(item.ele('CreatorID'), inv.permissions.creator); + UUID.getXML(item.ele('LastOwnerID'), inv.permissions.lastOwner); + UUID.getXML(item.ele('OwnerID'), inv.permissions.owner); + UUID.getXML(item.ele('GroupID'), inv.permissions.group); + + } + item.ele('CreationDate', inv.created.getTime() / 1000); + item.ele('Description', inv.description); + item.ele('Flags', inv.flags); + item.ele('InvType', inv.inventoryType); + UUID.getXML(item.ele('ParentID'), this.FullID); + UUID.getXML(item.ele('ParentPartID'), this.FullID); + item.ele('Type', inv.type); + item.ele('Name', inv.name); + } + } + + private getXML(xml: XMLElementOrXMLNode, rootPrim: GameObject, linkNum: number) + { + const sceneObjectPart = xml.ele('SceneObjectPart').att('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance').att('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); + sceneObjectPart.ele('AllowedDrop', (this.Flags !== undefined && (this.Flags & PrimFlags.AllowInventoryDrop) !== 0) ? 'true' : 'false'); + UUID.getXML(sceneObjectPart.ele('CreatorID'), this.creatorID); + sceneObjectPart.ele('CreatorData', 'node-metaverse'); + UUID.getXML(sceneObjectPart.ele('CreatorID'), this.folderID); + sceneObjectPart.ele('InventorySerial', this.inventorySerial); + UUID.getXML(sceneObjectPart.ele('UUID'), this.FullID); + sceneObjectPart.ele('LocalId', this.ID); + sceneObjectPart.ele('Name', this.name); + sceneObjectPart.ele('Material', this.Material); + sceneObjectPart.ele('RegionHandle', this.region.regionHandle.toString()); + Vector3.getXML(sceneObjectPart.ele('GroupPosition'), rootPrim.Position); + if (rootPrim === this) + { + Vector3.getXML(sceneObjectPart.ele('OffsetPosition'), Vector3.getZero()); + } + else + { + Vector3.getXML(sceneObjectPart.ele('OffsetPosition'), this.Position); + } + Quaternion.getXML(sceneObjectPart.ele('RotationOffset'), this.Rotation); + Vector3.getXML(sceneObjectPart.ele('Velocity'), this.Velocity); + Vector3.getXML(sceneObjectPart.ele('AngularVelocity'), this.AngularVelocity); + Vector3.getXML(sceneObjectPart.ele('Acceleration'), this.Acceleration); + sceneObjectPart.ele('Description', this.description); + if (this.Text !== undefined && this.Text !== '') + { + sceneObjectPart.ele('Text', this.Text); + } + if (this.TextColor !== undefined) + { + Color4.getXML(sceneObjectPart.ele('Color'), this.TextColor); + } + sceneObjectPart.ele('SitName', this.sitName); + sceneObjectPart.ele('TouchName', this.touchName); + sceneObjectPart.ele('LinkNum', linkNum); + sceneObjectPart.ele('ClickAction', this.ClickAction); + const shape = sceneObjectPart.ele('Shape'); + { + shape.ele('ProfileCurve', this.ProfileCurve); + if (this.TextureEntry) + { + shape.ele('TextureEntry', this.TextureEntry.binary.toString('base64')); + } + if (this.ExtraParams) + { + shape.ele('ExtraParams', this.ExtraParams.toString('base64')); + } + shape.ele('PathBegin', 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('PCode', this.PCode); + shape.ele('ProfileBegin', this.ProfileBegin); + shape.ele('ProfileEnd', this.ProfileEnd); + shape.ele('ProfileHollow', this.ProfileHollow); + shape.ele('State', this.State); + + if (this.ProfileCurve) + { + + const profileShape: ProfileShape = this.ProfileCurve & 0x0F; + const holeType: HoleType = this.ProfileCurve & 0xF0; + + shape.ele('ProfileShape', ProfileShape[profileShape]); + shape.ele('HollowShape', HoleType[holeType]); + } + if (this.MeshData !== undefined) + { + shape.ele('SculptType', this.MeshData.type); + UUID.getXML(shape.ele('SculptTexture'), this.MeshData.meshData); + shape.ele('SculptEntry', true); + } + else if (this.SculptData !== undefined) + { + shape.ele('SculptType', this.SculptData.type); + UUID.getXML(shape.ele('SculptTexture'), this.SculptData.texture); + shape.ele('SculptEntry', true); + } + else + { + shape.ele('SculptEntry', false); + } + if (this.FlexibleData !== undefined) + { + shape.ele('FlexiSoftness', this.FlexibleData.Softness); + shape.ele('FlexiTension', this.FlexibleData.Tension); + shape.ele('FlexiDrag', this.FlexibleData.Drag); + shape.ele('FlexiGravity', this.FlexibleData.Gravity); + shape.ele('FlexiWind', this.FlexibleData.Wind); + shape.ele('FlexiForceX', this.FlexibleData.Force.x); + shape.ele('FlexiForceY', this.FlexibleData.Force.y); + shape.ele('FlexiForceZ', this.FlexibleData.Force.z); + shape.ele('FlexiEntry', true); + } + else + { + shape.ele('FlexiEntry', false); + } + if (this.LightData !== undefined) + { + shape.ele('LightColorR', this.LightData.Color.red); + shape.ele('LightColorG', this.LightData.Color.green); + shape.ele('LightColorB', this.LightData.Color.blue); + shape.ele('LightColorA', this.LightData.Color.alpha); + shape.ele('LightRadius', this.LightData.Radius); + shape.ele('LightCutoff', this.LightData.Cutoff); + shape.ele('LightFalloff', this.LightData.Falloff); + shape.ele('LightIntensity', this.LightData.Intensity); + shape.ele('LightEntry', true); + } + else + { + shape.ele('LightEntry', false); + } + + } + Vector3.getXML(sceneObjectPart.ele('Scale'), this.Scale); + sceneObjectPart.ele('ParentID', this.ParentID); + sceneObjectPart.ele('CreationDate', Math.round((new Date()).getTime() / 1000)); + sceneObjectPart.ele('Category', this.category); + sceneObjectPart.ele('SalePrice', this.salePrice); + sceneObjectPart.ele('ObjectSaleType', this.saleType); + sceneObjectPart.ele('OwnershipCost', this.ownershipCost); + UUID.getXML(sceneObjectPart.ele('GroupID'), this.groupID); + UUID.getXML(sceneObjectPart.ele('OwnerID'), this.OwnerID); + UUID.getXML(sceneObjectPart.ele('LastOwnerID'), this.lastOwnerID); + sceneObjectPart.ele('BaseMask', this.baseMask); + sceneObjectPart.ele('OwnerMask', this.ownerMask); + sceneObjectPart.ele('GroupMask', this.groupMask); + sceneObjectPart.ele('EveryoneMask', this.everyoneMask); + sceneObjectPart.ele('NextOwnerMask', this.nextOwnerMask); + const flags = []; + if (this.Flags !== undefined) + { + for (const flag of Object.keys(PrimFlags)) + { + if (typeof flag === 'string') + { + const fl: any = PrimFlags; + const flagName: string = flag; + const flagValue: number = fl[flagName]; + if (this.Flags & flagValue) + { + flags.push(flagName); + } + } + } + } + sceneObjectPart.ele('Flags', flags.join(' ')); + if (this.TextureAnim) + { + sceneObjectPart.ele('TextureAnimation', this.TextureAnim.toString('base64')); + } + if (this.Particles && this.PSBlock) + { + sceneObjectPart.ele('ParticleSystem', this.PSBlock.toString('base64')); + } + if (this.physicsShapeType) + { + sceneObjectPart.ele('PhysicsShapeType', this.physicsShapeType); + } + if (this.Sound && !this.Sound.equals(UUID.zero())) + { + UUID.getXML(sceneObjectPart.ele('SoundID'), this.Sound); + sceneObjectPart.ele('SoundGain', this.SoundGain); + sceneObjectPart.ele('SoundFlags', this.SoundFlags); + sceneObjectPart.ele('SoundRadius', this.SoundRadius); + sceneObjectPart.ele('SoundQueueing', false); + } + if (this.inventory && this.inventory.length > 0) + { + const inventory = sceneObjectPart.ele('TaskInventory'); + for (const inv of this.inventory) + { + this.getInventoryXML(inventory, inv); + } + } + } + + exportXML(): string + { + const document = builder.create('SceneObjectGroup'); + let linkNum = 1; + this.getXML(document, this, linkNum); + if (this.children && this.children.length > 0) + { + const otherParts = document.ele('OtherParts'); + for (const child of this.children) + { + child.getXML(otherParts, this, ++linkNum); + } + } + return document.end({pretty: true, allowEmpty: true}); + } + + public toJSON(): IGameObjectData + { + return { + deleted: this.deleted, + creatorID: this.creatorID, + creationDate: this.creationDate, + baseMask: this.baseMask, + ownerMask: this.ownerMask, + groupMask: this.groupMask, + everyoneMask: this.everyoneMask, + nextOwnerMask: this.nextOwnerMask, + ownershipCost: this.ownershipCost, + saleType: this.saleType, + salePrice: this.salePrice, + aggregatePerms: this.aggregatePerms, + aggregatePermTextures: this.aggregatePermTextures, + aggregatePermTexturesOwner: this.aggregatePermTexturesOwner, + category: this.category, + inventorySerial: this.inventorySerial, + itemID: this.itemID, + folderID: this.folderID, + fromTaskID: this.fromTaskID, + lastOwnerID: this.lastOwnerID, + name: this.name, + description: this.description, + touchName: this.touchName, + sitName: this.sitName, + resolvedAt: this.resolvedAt, + totalChildren: this.totalChildren, + landImpact: this.landImpact, + calculatedLandImpact: this.calculatedLandImpact, + physicaImpact: this.physicaImpact, + resourceImpact: this.resourceImpact, + linkResourceImpact: this.linkResourceImpact, + linkPhysicsImpact: this.linkPhysicsImpact, + limitingType: this.limitingType, + children: this.children, + ID: this.ID, + FullID: this.FullID, + ParentID: this.ParentID, + OwnerID: this.OwnerID, + IsAttachment: this.IsAttachment, + NameValue: this.NameValue, + PCode: this.PCode, + State: this.State, + CRC: this.CRC, + Material: this.Material, + ClickAction: this.ClickAction, + Scale: this.Scale, + Flags: this.Flags, + PathCurve: this.PathCurve, + ProfileCurve: this.ProfileCurve, + PathBegin: this.PathBegin, + PathEnd: this.PathEnd, + PathScaleX: this.PathScaleX, + PathScaleY: this.PathScaleY, + PathShearX: this.PathShearX, + PathShearY: this.PathShearY, + PathTwist: this.PathTwist, + PathTwistBegin: this.PathTwistBegin, + PathRadiusOffset: this.PathRadiusOffset, + PathTaperX: this.PathTaperX, + PathTaperY: this.PathTaperY, + PathRevolutions: this.PathRevolutions, + PathSkew: this.PathSkew, + ProfileBegin: this.ProfileBegin, + ProfileEnd: this.ProfileEnd, + ProfileHollow: this.ProfileHollow, + TextureEntry: this.TextureEntry, + Text: this.Text, + TextColor: this.TextColor, + MediaURL: this.MediaURL, + JointType: this.JointType, + JointPivot: this.JointPivot, + JointAxisOrAnchor: this.JointAxisOrAnchor, + Position: this.Position, + Rotation: this.Rotation, + CollisionPlane: this.CollisionPlane, + Velocity: this.Velocity, + Acceleration: this.Acceleration, + AngularVelocity: this.AngularVelocity, + TreeSpecies: this.TreeSpecies, + Sound: this.Sound, + SoundGain: this.SoundGain, + SoundFlags: this.SoundFlags, + SoundRadius: this.SoundRadius, + Particles: this.Particles, + FlexibleData: this.FlexibleData, + LightData: this.LightData, + LightImageData: this.LightImageData, + SculptData: this.SculptData, + MeshData: this.MeshData, + density: this.density, + friction: this.friction, + gravityMultiplier: this.gravityMultiplier, + physicsShapeType: this.physicsShapeType, + restitution: this.restitution + } + } +} diff --git a/lib/classes/public/LightData.ts b/lib/classes/public/LightData.ts new file mode 100644 index 0000000..04c79eb --- /dev/null +++ b/lib/classes/public/LightData.ts @@ -0,0 +1,30 @@ +import {Color4} from '../Color4'; +import {Utils} from '../Utils'; + +export class LightData +{ + Color: Color4 = Color4.black; + Radius = 0.0; + Cutoff = 0.0; + Falloff = 0.0; + Intensity = 0.0; + + constructor(buf: Buffer, pos: number, length: number) + { + if (length >= 16) + { + this.Color = new Color4(buf, pos, false); + pos += 4; + this.Radius = buf.readFloatLE(pos); + pos += 4; + this.Cutoff = buf.readFloatLE(pos); + pos += 4; + this.Falloff = buf.readFloatLE(pos); + if (typeof this.Color.alpha === 'number') + { + this.Intensity = this.Color.alpha; + } + this.Color.alpha = 1.0; + } + } +} diff --git a/lib/classes/public/LightImageData.ts b/lib/classes/public/LightImageData.ts new file mode 100644 index 0000000..288c06b --- /dev/null +++ b/lib/classes/public/LightImageData.ts @@ -0,0 +1,18 @@ +import {UUID} from '../UUID'; +import {Vector3} from '../Vector3'; + +export class LightImageData +{ + texture: UUID = UUID.zero(); + params: Vector3 = Vector3.getZero(); + + constructor(buf: Buffer, pos: number, length: number) + { + if (length >= 28) + { + this.texture = new UUID(buf, pos); + pos += 16; + this.params = new Vector3(buf, pos); + } + } +} \ No newline at end of file diff --git a/lib/classes/public/Material.ts b/lib/classes/public/Material.ts new file mode 100644 index 0000000..d68020c --- /dev/null +++ b/lib/classes/public/Material.ts @@ -0,0 +1,24 @@ +import {UUID} from '../UUID'; +import {Color4} from '../Color4'; + +export class Material +{ + alphaMaskCutoff: number; + diffuseAlphaMode: number; + envIntensity: number; + normMap: UUID; + normOffsetX: number; + normOffsetY: number; + normRepeatX: number; + normRepeatY: number; + normRotation: number; + specColor: Color4; + specExp: number; + specMap: UUID; + specOffsetX: number; + specOffsetY: number; + specRepeatX: number; + specRepeatY: number; + specRotation: number; + llsd: string; +} diff --git a/lib/classes/public/MeshData.ts b/lib/classes/public/MeshData.ts new file mode 100644 index 0000000..fdea06f --- /dev/null +++ b/lib/classes/public/MeshData.ts @@ -0,0 +1,18 @@ +import {UUID} from '../UUID'; +import {SculptType} from '../../enums/SculptType'; + +export class MeshData +{ + meshData: UUID = UUID.zero(); + type: SculptType = SculptType.None; + + constructor(buf: Buffer, pos: number, length: number) + { + if (length >= 17) + { + this.meshData = new UUID(buf, pos); + pos += 16; + this.type = buf.readUInt8(pos); + } + } +} diff --git a/lib/classes/public/Parcel.ts b/lib/classes/public/Parcel.ts new file mode 100644 index 0000000..c884ded --- /dev/null +++ b/lib/classes/public/Parcel.ts @@ -0,0 +1,111 @@ +import {Vector3} from '../Vector3'; +import {UUID} from '../UUID'; +import * as builder from 'xmlbuilder'; +import * as LLSD from '@caspertech/llsd'; +import {ParcelFlags} from '../../enums/ParcelFlags'; + +export class Parcel +{ + LocalID: number; + ParcelID: UUID; + + RegionDenyAgeUnverified: boolean; + + MediaDesc: string; + MediaWidth: number; + MediaHeight: number; + MediaLoop: number; + MediaType: string; + ObscureMedia: number; + ObscureMusic: number; + + AABBMax: Vector3; + AABBMin: Vector3; + AnyAVSounds: boolean; + Area: number; + AuctionID: number; + AuthBuyerID: UUID; + Bitmap: Buffer; + Category: number; + ClaimDate: number; + ClaimPrice: number; + Desc: string; + GroupAVSounds: boolean; + GroupID: UUID; + GroupPrims: number; + IsGroupOwned: boolean; + LandingType: number; + MaxPrims: number; + MediaAutoScale: number; + MediaID: UUID; + MediaURL: string; + MusicURL: string; + Name: string; + OtherCleanTime: number; + OtherCount: number; + OtherPrims: number; + OwnerID: UUID; + OwnerPrims: number; + ParcelFlags: ParcelFlags; + ParcelPrimBonus: number; + PassHours: number; + PassPrice: number; + PublicCount: number; + RegionDenyAnonymous: boolean; + RegionDenyIdentified: boolean; + RegionDenyTransacted: boolean; + RegionPushOverride: boolean; + RentPrice: number; + RequestResult: number; + SalePrice: number; + SeeAvs: boolean; + SelectedPrims: number; + SelfCount: number; + SequenceID: number; + SimWideMaxPrims: number; + SimWideTotalPrims: number; + SnapSelection: boolean; + SnapshotID: UUID; + Status: number; + TotalPrims: number; + UserLocation: Vector3; + UserLookAt: Vector3; + + RegionAllowAccessOverride: boolean; + + exportXML(): string + { + const document = builder.create('LandData'); + document.ele('Area', this.Area); + document.ele('AuctionID', this.AuctionID); + document.ele('AuthBuyerID', this.AuthBuyerID.toString()); + document.ele('Category', this.Category); + document.ele('ClaimDate', this.ClaimDate); + document.ele('ClaimPrice', this.ClaimPrice); + document.ele('GlobalID', this.ParcelID.toString()); + document.ele('GroupID', this.GroupID.toString()); + document.ele('IsGroupOwned', this.IsGroupOwned); + document.ele('Bitmap', this.Bitmap.toString('base64')); + document.ele('Description', this.Desc); + document.ele('Flags', this.ParcelFlags); + document.ele('LandingType', this.LandingType); + document.ele('Name', this.Name); + document.ele('Status', this.Status); + document.ele('LocalID', this.LocalID); + document.ele('MediaAutoScale', this.MediaAutoScale); + document.ele('MediaID', this.MediaID.toString()); + document.ele('MediaURL', this.MediaURL); + document.ele('MusicURL', this.MusicURL); + document.ele('OwnerID', this.OwnerID.toString()); + document.ele('ParcelAccessList'); + document.ele('PassHours', this.PassHours); + document.ele('PassPrice', this.PassPrice); + document.ele('SalePrice', this.SalePrice); + document.ele('SnapshotID', this.SnapshotID.toString()); + document.ele('UserLocation', this.UserLocation.toString()); + document.ele('UserLookAt', this.UserLookAt.toString()); + document.ele('Dwell', 0); + document.ele('OtherCleanTime', this.OtherCleanTime); + return document.end({pretty: true, allowEmpty: true}); + } +} diff --git a/lib/classes/public/RegionEnvironment.ts b/lib/classes/public/RegionEnvironment.ts new file mode 100644 index 0000000..f405fb8 --- /dev/null +++ b/lib/classes/public/RegionEnvironment.ts @@ -0,0 +1,76 @@ +import {UUID} from '../UUID'; +import {Vector4} from '../Vector4'; +import {Color4} from '../Color4'; +import {Vector2} from '../Vector2'; +import {Vector3} from '../Vector3'; +import {XMLElementOrXMLNode} from 'xmlbuilder'; +import {SkyPreset} from './interfaces/SkyPreset'; +import {WaterPreset} from './interfaces/WaterPreset'; + +export class RegionEnvironment +{ + regionID: UUID; + dayCycleKeyframes: { + time: number, + preset: string + }[]; + skyPresets: { + [key: string]: SkyPreset + } = {}; + water: WaterPreset; + + getXML(xml: XMLElementOrXMLNode) + { + const env = xml.ele('Environment'); + const dayCycle = env.ele('DayCycle'); + for (const keyFrame of this.dayCycleKeyframes) + { + const kf = dayCycle.ele('KeyFrame'); + kf.ele('Time', keyFrame.time); + kf.ele('Preset', keyFrame.preset); + } + const skyPresets = env.ele('SkyPresets'); + for (const presetKey of Object.keys(this.skyPresets)) + { + const preset = this.skyPresets[presetKey]; + const pre = skyPresets.ele('Preset'); + pre.att('name', presetKey); + Vector4.getXML(pre.ele('Ambient'), preset.ambient); + Vector4.getXML(pre.ele('BlueDensity'), preset.blueDensity); + Vector4.getXML(pre.ele('BlueHorizon'), preset.blueHorizon); + Color4.getXML(pre.ele('CloudColor'), preset.cloudColor); + Vector4.getXML(pre.ele('CloudPosDensity1'), preset.cloudPosDensity1); + Vector4.getXML(pre.ele('CloudPosDensity2'), preset.cloudPosDensity2); + Vector4.getXML(pre.ele('CloudScale'), preset.cloudScale); + Vector2.getXML(pre.ele('CloudScrollRate'), preset.cloudScrollRate); + Vector4.getXML(pre.ele('CloudShadow'), preset.cloudScale); + Vector4.getXML(pre.ele('DensityMultiplier'), preset.cloudScale); + Vector4.getXML(pre.ele('DistanceMultiplier'), preset.cloudScale); + pre.ele('EastAngle', preset.eastAngle); + const cloudScroll = pre.ele('EnableCloudScroll'); + cloudScroll.ele('X', preset.enableCloudScroll.x); + cloudScroll.ele('Y', preset.enableCloudScroll.y); + Vector4.getXML(pre.ele('Gamma'), preset.gamma); + Vector4.getXML(pre.ele('Glow'), preset.glow); + Vector4.getXML(pre.ele('HazeDensity'), preset.hazeDensity); + Vector4.getXML(pre.ele('HazeHorizon'), preset.hazeHorizon); + Vector4.getXML(pre.ele('LightNormal'), preset.lightNormal); + Vector4.getXML(pre.ele('MaxY'), preset.maxY); + pre.ele('StarBrightness', preset.starBrightness); + pre.ele('SunAngle', preset.sunAngle); + Color4.getXML(pre.ele('SunLightColor'), preset.sunlightColor); + } + const water = env.ele('Water'); + water.ele('BlurMultiplier', this.water.blurMultiplier); + water.ele('FresnelOffset', this.water.fresnelOffset); + water.ele('FresnelScale', this.water.fresnelScale); + UUID.getXML(water.ele('NormalMap'), this.water.normalMap); + water.ele('ScaleAbove', this.water.scaleAbove); + water.ele('ScaleBelow', this.water.scaleBelow); + water.ele('UnderWaterFogMod', this.water.underWaterFogMod); + Color4.getXML(water.ele('WaterFogColor'), this.water.waterFogColor); + water.ele('WaterFogDensity', this.water.waterFogDensity); + Vector2.getXML(water.ele('Wave1Dir'), this.water.wave1Dir); + Vector2.getXML(water.ele('Wave2Dir'), this.water.wave2Dir); + } +} diff --git a/lib/classes/public/SculptData.ts b/lib/classes/public/SculptData.ts new file mode 100644 index 0000000..fb76ad2 --- /dev/null +++ b/lib/classes/public/SculptData.ts @@ -0,0 +1,18 @@ +import {UUID} from '../UUID'; +import {SculptType} from '../../enums/SculptType'; + +export class SculptData +{ + texture: UUID = UUID.zero(); + type: SculptType = SculptType.None; + + constructor(buf: Buffer, pos: number, length: number) + { + if (length >= 17) + { + this.texture = new UUID(buf, pos); + pos += 16; + this.type = buf.readUInt8(pos); + } + } +} diff --git a/lib/classes/public/interfaces/SkyPreset.ts b/lib/classes/public/interfaces/SkyPreset.ts new file mode 100644 index 0000000..60cf4e2 --- /dev/null +++ b/lib/classes/public/interfaces/SkyPreset.ts @@ -0,0 +1,32 @@ +import {Vector4} from '../../Vector4'; +import {Color4} from '../../Color4'; +import {Vector2} from '../../Vector2'; + +export interface SkyPreset +{ + ambient: Vector4, + blueDensity: Vector4, + blueHorizon: Vector4, + cloudColor: Color4, + cloudPosDensity1: Vector4, + cloudPosDensity2: Vector4, + cloudScale: Vector4, + cloudScrollRate: Vector2, + cloudShadow: Vector4, + densityMultiplier: Vector4, + distanceMultiplier: Vector4, + eastAngle: number, + enableCloudScroll: { + x: boolean, + y: boolean + }, + gamma: Vector4, + glow: Vector4, + hazeDensity: Vector4, + hazeHorizon: Vector4, + lightNormal: Vector4, + maxY: Vector4, + starBrightness: number, + sunAngle: number, + sunlightColor: Color4 +} \ No newline at end of file diff --git a/lib/classes/public/interfaces/WaterPreset.ts b/lib/classes/public/interfaces/WaterPreset.ts new file mode 100644 index 0000000..07fba7b --- /dev/null +++ b/lib/classes/public/interfaces/WaterPreset.ts @@ -0,0 +1,20 @@ +import {Vector3} from '../../Vector3'; +import {UUID} from '../../UUID'; +import {Color4} from '../../Color4'; +import {Vector2} from '../../Vector2'; + +export interface WaterPreset +{ + blurMultiplier: number, + fresnelOffset: number, + fresnelScale: number, + normalScale: Vector3, + normalMap: UUID, + scaleAbove: number, + scaleBelow: number, + underWaterFogMod: number, + waterFogColor: Color4, + waterFogDensity: number, + wave1Dir: Vector2, + wave2Dir: Vector2 +} \ No newline at end of file diff --git a/lib/enums/AssetTypeLL.ts b/lib/enums/AssetTypeLL.ts new file mode 100644 index 0000000..0d3f38d --- /dev/null +++ b/lib/enums/AssetTypeLL.ts @@ -0,0 +1,25 @@ +export enum AssetTypeLL +{ + texture = 0, + sound = 1, + callcard = 2, + landmark = 3, + script = 4, + clothing = 5, + object = 6, + notecard = 7, + category = 8, + lsltext = 10, + lslbyte = 11, + txtr_tga = 12, + bodypart = 13, + snd_wav = 17, + img_tga = 18, + jpeg = 19, + animatn = 20, + gesture = 21, + simstate = 22, + link = 24, + link_f = 25, + mesh = 49 +} \ No newline at end of file diff --git a/lib/enums/Bumpiness.ts b/lib/enums/Bumpiness.ts new file mode 100644 index 0000000..79fed47 --- /dev/null +++ b/lib/enums/Bumpiness.ts @@ -0,0 +1,21 @@ +export enum Bumpiness +{ + None = 0, + Brightness = 1, + Darkness = 2, + Woodgrain = 3, + Bark = 4, + Bricks = 5, + Checker = 6, + Concrete = 7, + Crustytile = 8, + Cutstone = 9, + Discs = 10, + Gravel = 11, + Petridish = 12, + Siding = 13, + Stonetile = 14, + Stucco = 15, + Suction = 16, + Weave = 17 +} diff --git a/lib/enums/HTTPAssets.ts b/lib/enums/HTTPAssets.ts index 8870a77..6b26b14 100644 --- a/lib/enums/HTTPAssets.ts +++ b/lib/enums/HTTPAssets.ts @@ -19,5 +19,6 @@ export enum HTTPAssets ASSET_LINK_FOLDER = 'link_f', ASSET_MESH = 'mesh', ASSET_WIDGET = 'widget', - ASSET_PERSON = 'person' + ASSET_PERSON = 'person', + ASSET_MATERIAL = 'material' } diff --git a/lib/enums/HoleType.ts b/lib/enums/HoleType.ts new file mode 100644 index 0000000..b42130d --- /dev/null +++ b/lib/enums/HoleType.ts @@ -0,0 +1,7 @@ +export enum HoleType +{ + Same = 0x00, + Circle = 0x10, + Square = 0x20, + Triangle = 0x30 +} diff --git a/lib/enums/InventoryTypeLL.ts b/lib/enums/InventoryTypeLL.ts new file mode 100644 index 0000000..353b066 --- /dev/null +++ b/lib/enums/InventoryTypeLL.ts @@ -0,0 +1,18 @@ +export enum InventoryTypeLL +{ + texture = 0, + sound = 1, + callcard = 2, + landmark = 3, + object = 6, + notecard = 7, + category = 8, + root = 9, + script = 10, + snapshot = 15, + attach = 17, + wearable = 18, + animation = 19, + gesture = 20, + mesh = 22 +} \ No newline at end of file diff --git a/lib/enums/LayerType.ts b/lib/enums/LayerType.ts new file mode 100644 index 0000000..ec5abcd --- /dev/null +++ b/lib/enums/LayerType.ts @@ -0,0 +1,11 @@ +export enum LayerType +{ + Land = 0x4C, + LandExtended = 0x4D, + Water = 0x57, + WaterExtended = 0x57, + Wind = 0x37, + WindExtended = 0x39, + Cloud = 0x38, + CloudExtended = 0x3A +} \ No newline at end of file diff --git a/lib/enums/MappingType.ts b/lib/enums/MappingType.ts new file mode 100644 index 0000000..c00c3d3 --- /dev/null +++ b/lib/enums/MappingType.ts @@ -0,0 +1,6 @@ +export enum MappingType +{ + Default = 0, + Planar = 2, + Spherical = 4 +} diff --git a/lib/enums/ParcelFlags.ts b/lib/enums/ParcelFlags.ts new file mode 100644 index 0000000..7fc9d5c --- /dev/null +++ b/lib/enums/ParcelFlags.ts @@ -0,0 +1,36 @@ +export enum ParcelFlags +{ + None = 0, + AllowFly = 1 << 0, + AllowOtherScripts = 1 << 1, + ForSale = 1 << 2, + AllowLandmark = 1 << 3, + AllowTerraform = 1 << 4, + AllowDamage = 1 << 5, + CreateObjects = 1 << 6, + ForSaleObjects = 1 << 7, + UseAccessGroup = 1 << 8, + UseAccessList = 1 << 9, + UseBanList = 1 << 10, + UsePassList = 1 << 11, + ShowDirectory = 1 << 12, + AllowDeedToGroup = 1 << 13, + ContributeWithDeed = 1 << 14, + SoundLocal = 1 << 15, + SellParcelObjects = 1 << 16, + AllowPublish = 1 << 17, + MaturePublish = 1 << 18, + UrlWebPage = 1 << 19, + UrlRawHtml = 1 << 20, + RestrictPushObject = 1 << 21, + DenyAnonymous = 1 << 22, + LindenHome = 1 << 23, + DenyTransacted = 1 << 24, + AllowGroupScripts = 1 << 25, + CreateGroupObjects = 1 << 26, + AllowAPrimitiveEntry = 1 << 27, + AllowGroupObjectEntry = 1 << 28, + AllowVoiceChat = 1 << 29, + UseEstateVoiceChan = 1 << 30, + DenyAgeUnverified = 1 << 31 +} \ No newline at end of file diff --git a/lib/enums/PhysicsShapeType.ts b/lib/enums/PhysicsShapeType.ts new file mode 100644 index 0000000..2389acc --- /dev/null +++ b/lib/enums/PhysicsShapeType.ts @@ -0,0 +1,6 @@ +export enum PhysicsShapeType +{ + Prim = 0, + None = 1, + ConvexHull = 2 +} diff --git a/lib/enums/PrimFlags.ts b/lib/enums/PrimFlags.ts new file mode 100644 index 0000000..0043e64 --- /dev/null +++ b/lib/enums/PrimFlags.ts @@ -0,0 +1,36 @@ +export enum PrimFlags +{ + None = 0, + Physics = 0x00000001, + CreateSelected = 0x00000002, + ObjectModify = 0x00000004, + ObjectCopy = 0x00000008, + ObjectAnyOwner = 0x00000010, + ObjectYouOwner = 0x00000020, + Scripted = 0x00000040, + Touch = 0x00000080, + ObjectMove = 0x00000100, + Money = 0x00000200, + Phantom = 0x00000400, + InventoryEmpty = 0x00000800, + JointHinge = 0x00001000, + JointP2P = 0x00002000, + JointLP2P = 0x00004000, + JointWheel = 0x00008000, + AllowInventoryDrop = 0x00010000, + ObjectTransfer = 0x00020000, + ObjectGroupOwned = 0x00040000, + ObjectYouOfficer = 0x00080000, + CameraDecoupled = 0x00100000, + AnimSource = 0x00200000, + CameraSource = 0x00400000, + CastShadows = 0x00800000, + DieAtEdge = 0x01000000, + ReturnAtEdge = 0x02000000, + Sandbox = 0x04000000, + Flying = 0x08000000, + ObjectOwnerModify = 0x10000000, + TemporaryOnRez = 0x20000000, + Temporary = 0x40000000, + ZlibCompressed = 0x80000000 +} diff --git a/lib/enums/ProfileShape.ts b/lib/enums/ProfileShape.ts new file mode 100644 index 0000000..f952d2a --- /dev/null +++ b/lib/enums/ProfileShape.ts @@ -0,0 +1,9 @@ +export enum ProfileShape +{ + Circle = 0, + Square = 1, + IsometricTriangle = 2, + EquilateralTriangle = 3, + RightTriangle = 4, + HalfCircle = 5 +} diff --git a/lib/enums/SaleTypeLL.ts b/lib/enums/SaleTypeLL.ts new file mode 100644 index 0000000..d6c152d --- /dev/null +++ b/lib/enums/SaleTypeLL.ts @@ -0,0 +1,7 @@ +export enum SaleTypeLL +{ + not = 0, + orig = 1, + copy = 2, + cntn = 3 +} diff --git a/lib/enums/SculptType.ts b/lib/enums/SculptType.ts new file mode 100644 index 0000000..f833226 --- /dev/null +++ b/lib/enums/SculptType.ts @@ -0,0 +1,11 @@ +export enum SculptType +{ + None = 0, + Sphere = 1, + Torus = 2, + Plane = 3, + Cylinder = 4, + Mesh = 5, + Invert = 64, + Mirror = 128 +} \ No newline at end of file diff --git a/lib/enums/Shininess.ts b/lib/enums/Shininess.ts new file mode 100644 index 0000000..2392bb2 --- /dev/null +++ b/lib/enums/Shininess.ts @@ -0,0 +1,7 @@ +export enum Shininess +{ + None = 0, + Low = 0x40, + Medium = 0x80, + High = 0xC0 +} \ No newline at end of file diff --git a/lib/enums/SimAccessFlags.ts b/lib/enums/SimAccessFlags.ts new file mode 100644 index 0000000..3032441 --- /dev/null +++ b/lib/enums/SimAccessFlags.ts @@ -0,0 +1,10 @@ +export enum SimAccessFlags +{ + Unknown = 0, + Trial = 7, + PG = 13, + Mature = 21, + Adult = 42, + Down = 254, + NonExistent = 255 +} diff --git a/lib/enums/TextureAnimFlags.ts b/lib/enums/TextureAnimFlags.ts new file mode 100644 index 0000000..351035f --- /dev/null +++ b/lib/enums/TextureAnimFlags.ts @@ -0,0 +1,11 @@ +export enum TextureAnimFlags +{ + ANIM_OFF = 0x00, + ANIM_ON = 0x01, + LOOP = 0x02, + REVERSE = 0x04, + PING_PONG = 0x08, + SMOOTH = 0x10, + ROTATE = 0x20, + SCALE = 0x40 +} diff --git a/lib/enums/TransferStatus.ts b/lib/enums/TransferStatus.ts new file mode 100644 index 0000000..452b30b --- /dev/null +++ b/lib/enums/TransferStatus.ts @@ -0,0 +1,10 @@ +export enum TransferStatus +{ + InsufficientPermissions = -3, + NotFound = -2, + Error = -1, + OK = 0, + Done = 1, + Skip = 2, + Abort = 3, +} diff --git a/lib/events/NewObjectEvent.ts b/lib/events/NewObjectEvent.ts new file mode 100644 index 0000000..2b89940 --- /dev/null +++ b/lib/events/NewObjectEvent.ts @@ -0,0 +1,9 @@ +import {UUID} from '..'; +import {GameObject} from '../classes/public/GameObject'; + +export class NewObjectEvent +{ + objectID: UUID; + localID: number; + object: GameObject; +} diff --git a/lib/events/ObjectKilledEvent.ts b/lib/events/ObjectKilledEvent.ts new file mode 100644 index 0000000..7cbd547 --- /dev/null +++ b/lib/events/ObjectKilledEvent.ts @@ -0,0 +1,9 @@ +import {UUID} from '..'; +import {GameObject} from '../classes/public/GameObject'; + +export class ObjectKilledEvent +{ + objectID: UUID; + localID: number; + object: GameObject; +} diff --git a/lib/events/ObjectPhysicsDataEvent.ts b/lib/events/ObjectPhysicsDataEvent.ts new file mode 100644 index 0000000..0886e43 --- /dev/null +++ b/lib/events/ObjectPhysicsDataEvent.ts @@ -0,0 +1,12 @@ +import {PhysicsShapeType} from '../enums/PhysicsShapeType'; + +export class ObjectPhysicsDataEvent +{ + localID: number; + + density: number; + friction: number; + gravityMultiplier: number; + physicsShapeType: PhysicsShapeType; + restitution: number; +} diff --git a/lib/events/ObjectUpdatedEvent.ts b/lib/events/ObjectUpdatedEvent.ts new file mode 100644 index 0000000..2c0a8f9 --- /dev/null +++ b/lib/events/ObjectUpdatedEvent.ts @@ -0,0 +1,9 @@ +import {UUID} from '..'; +import {GameObject} from '../classes/public/GameObject'; + +export class ObjectUpdatedEvent +{ + objectID: UUID; + localID: number; + object: GameObject; +} diff --git a/lib/events/ParcelPropertiesEvent.ts b/lib/events/ParcelPropertiesEvent.ts new file mode 100644 index 0000000..ddc2a34 --- /dev/null +++ b/lib/events/ParcelPropertiesEvent.ts @@ -0,0 +1,71 @@ +import {UUID, Vector3} from '..'; +import {ParcelFlags} from '../enums/ParcelFlags'; + +export class ParcelPropertiesEvent +{ + LocalID: number; + + RegionDenyAgeUnverified: boolean; + + MediaDesc: string; + MediaWidth: number; + MediaHeight: number; + MediaLoop: number; + MediaType: string; + ObscureMedia: number; + ObscureMusic: number; + + AABBMax: Vector3; + AABBMin: Vector3; + AnyAVSounds: boolean; + Area: number; + AuctionID: number; + AuthBuyerID: UUID; + Bitmap: Buffer; + Category: number; + ClaimDate: number; + ClaimPrice: number; + Desc: string; + GroupAVSounds: boolean; + GroupID: UUID; + GroupPrims: number; + IsGroupOwned: boolean; + LandingType: number; + MaxPrims: number; + MediaAutoScale: number; + MediaID: UUID; + MediaURL: string; + MusicURL: string; + Name: string; + OtherCleanTime: number; + OtherCount: number; + OtherPrims: number; + OwnerID: UUID; + OwnerPrims: number; + ParcelFlags: ParcelFlags; + ParcelPrimBonus: number; + PassHours: number; + PassPrice: number; + PublicCount: number; + RegionDenyAnonymous: boolean; + RegionDenyIdentified: boolean; + RegionDenyTransacted: boolean; + RegionPushOverride: boolean; + RentPrice: number; + RequestResult: number; + SalePrice: number; + SeeAvs: boolean; + SelectedPrims: number; + SelfCount: number; + SequenceID: number; + SimWideMaxPrims: number; + SimWideTotalPrims: number; + SnapSelection: boolean; + SnapshotID: UUID; + Status: number; + TotalPrims: number; + UserLocation: Vector3; + UserLookAt: Vector3; + + RegionAllowAccessOverride: boolean; +} diff --git a/lib/index.ts b/lib/index.ts index 3d62b01..8e863d9 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -55,6 +55,37 @@ import {TextureFlags} from './enums/TextureFlags'; import {SourcePattern} from './enums/SourcePattern'; import {BlendFunc} from './enums/BlendFunc'; import {PCode} from './enums/PCode'; +import {Utils} from './classes/Utils'; +import {ObjectPhysicsDataEvent} from './events/ObjectPhysicsDataEvent'; +import {ParcelPropertiesEvent} from './events/ParcelPropertiesEvent'; +import {PrimFlags} from './enums/PrimFlags'; +import {TextureEntry} from './classes/TextureEntry'; +import {RegionEnvironment} from './classes/public/RegionEnvironment'; +import {Parcel} from './classes/public/Parcel'; +import {Material} from './classes/public/Material'; +import {GameObject} from './classes/public/GameObject'; +import {LightImageData} from './classes/public/LightImageData'; +import {LightData} from './classes/public/LightData'; +import {FlexibleData} from './classes/public/FlexibleData'; +import {MeshData} from './classes/public/MeshData'; +import {SculptData} from './classes/public/SculptData'; +import {SkyPreset} from './classes/public/interfaces/SkyPreset'; +import {WaterPreset} from './classes/public/interfaces/WaterPreset'; +import {NewObjectEvent} from './events/NewObjectEvent'; +import {ObjectKilledEvent} from './events/ObjectKilledEvent'; +import {ObjectUpdatedEvent} from './events/ObjectUpdatedEvent'; +import {Bumpiness} from './enums/Bumpiness'; +import {HoleType} from './enums/HoleType'; +import {LayerType} from './enums/LayerType'; +import {MappingType} from './enums/MappingType'; +import {PhysicsShapeType} from './enums/PhysicsShapeType'; +import {ParcelFlags} from './enums/ParcelFlags'; +import {ProfileShape} from './enums/ProfileShape'; +import {SculptType} from './enums/SculptType'; +import {Shininess} from './enums/Shininess'; +import {SimAccessFlags} from './enums/SimAccessFlags'; +import {TextureAnimFlags} from './enums/TextureAnimFlags'; +import {TransferStatus} from './enums/TransferStatus'; export { Bot, @@ -68,6 +99,8 @@ export { UUID, Vector3, Vector2, + Utils, + TextureEntry, // Flags AgentFlags, @@ -90,6 +123,19 @@ export { SourcePattern, BlendFunc, PCode, + PrimFlags, + Bumpiness, + HoleType, + LayerType, + MappingType, + ParcelFlags, + PhysicsShapeType, + ProfileShape, + SculptType, + Shininess, + SimAccessFlags, + TextureAnimFlags, + TransferStatus, // Events ChatEvent, @@ -113,12 +159,28 @@ export { FriendOnlineEvent, FriendRightsEvent, FriendRemovedEvent, + ObjectPhysicsDataEvent, + ParcelPropertiesEvent, + NewObjectEvent, + ObjectKilledEvent, + ObjectUpdatedEvent, // Public Classes Avatar, Friend, + FlexibleData, + LightData, + LightImageData, + GameObject, + Material, + Parcel, + RegionEnvironment, + SculptData, + MeshData, // Public Interfaces GlobalPosition, - MapLocation + MapLocation, + SkyPreset, + WaterPreset }; diff --git a/package-lock.json b/package-lock.json index fb87425..36121fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,6 +122,12 @@ "@types/node": "*" } }, + "@types/xmlbuilder": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/xmlbuilder/-/xmlbuilder-0.0.34.tgz", + "integrity": "sha512-yVsHfYqJblSEg3DvUhGndpCZBZz2GiGVmqMa04fbGro2xzxRj85Q7MQ4os+MaXmKcpCDD42MXuxUWfoUKTuVdQ==", + "dev": true + }, "@types/xmlrpc": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/xmlrpc/-/xmlrpc-1.3.5.tgz", @@ -432,6 +438,11 @@ } } }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -830,6 +841,14 @@ "map-cache": "^0.2.2" } }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1201,8 +1220,31 @@ "minimist": { "version": "0.0.8", "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "minizlib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", + "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "requires": { + "minipass": "^2.2.1" + } }, "mixin-deep": { "version": "1.3.1", @@ -1227,7 +1269,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -1251,6 +1292,11 @@ "supports-color": "5.4.0" } }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1728,6 +1774,27 @@ "has-flag": "^3.0.0" } }, + "tar": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", + "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.3", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "tiny-async-pool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.0.1.tgz", @@ -2005,9 +2072,9 @@ "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" }, "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.0.tgz", + "integrity": "sha512-In21jFWiaulS7Cmw1fPT1Lm7g7L6ml/uwZNAaKlDZc78szm3pn5oH9gizH7sh1h2GGRb3OkL5kLCeMEENEnZwA==" }, "xmldom": { "version": "0.1.27", @@ -2021,8 +2088,20 @@ "requires": { "sax": "1.2.x", "xmlbuilder": "8.2.x" + }, + "dependencies": { + "xmlbuilder": { + "version": "8.2.2", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=" + } } }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + }, "yn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", diff --git a/package.json b/package.json index 4ba99bf..aa3dc27 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/uuid": "^3.4.4", "@types/validator": "^9.4.2", "@types/xml": "^1.0.2", + "@types/xmlbuilder": "0.0.34", "@types/xmlrpc": "^1.3.5", "mocha": "^5.2.0", "source-map-support": "^0.5.9", @@ -45,13 +46,16 @@ "ipaddr.js": "^1.8.1", "long": "^4.0.0", "micromatch": "^3.1.10", + "moment": "^2.22.2", "rbush-3d": "0.0.4", "request": "^2.88.0", "rxjs": "^6.3.3", + "tar": "^4.4.6", "tiny-async-pool": "^1.0.1", "uuid": "^3.3.2", "validator": "^10.8.0", "xml": "^1.0.1", + "xmlbuilder": "^10.1.0", "xmlrpc": "^1.3.2" } }