- Support specifying URL in loginParameters for connecting to OpenSim

- Patch some miscellaneous OpenSim related glitches
- Add waitForRegionHandshake function
- Add a concurrent promise queue
- Fix xml writing of Vector3s
- Fix asset downloading on grids without HTTP assets
- Fix buildObject to properly orientate prims
- Wrangled with CreateSelected all day and it turned out to be an OpenSim bug
- LinkFrom function for faster linking
- Updated LLSD library to fix LLMesh decoding
This commit is contained in:
Casper Warden
2020-01-07 21:01:20 +00:00
parent b248fa17ed
commit 5e235d2db1
19 changed files with 467 additions and 122 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@
/example/npm-debug.log
/caspertech-node-metaverse-*.tgz
/npm-debug.log
/dist
/dist
/exampleMine

View File

@@ -2,5 +2,6 @@
"firstName": "Username",
"lastName": "Resident",
"password": "YourPassword",
"start": "last" //first, last, or login uri like uri:<existing region name>&<x>&<y>&<z>
}
"start": "last", //first, last, or login uri like uri:<existing region name>&<x>&<y>&<z>
"url": "https://login.agni.lindenlab.com/cgi-bin/login.cgi"
}

View File

@@ -1,6 +1,7 @@
import * as xmlrpc from 'xmlrpc';
import * as crypto from 'crypto';
import * as uuid from 'uuid';
import * as url from 'url';
import { LoginParameters } from './classes/LoginParameters';
import { LoginResponse } from './classes/LoginResponse';
import { ClientEvents } from './classes/ClientEvents';
@@ -38,13 +39,28 @@ export class LoginHandler
{
return new Promise<LoginResponse>((resolve, reject) =>
{
const loginURI = url.parse(params.url);
let secure = false;
if (loginURI.protocol !== undefined && loginURI.protocol.trim().toLowerCase() === 'https:')
{
secure = true;
}
let port: string | undefined = loginURI.port;
if (port === undefined || port === null)
{
port = secure ? '443' : '80';
}
const secureClientOptions = {
host: 'login.agni.lindenlab.com',
port: 443,
path: '/cgi-bin/login.cgi',
host: loginURI.hostname,
port: parseInt(port, 10),
path: loginURI.path,
rejectUnauthorized: false
};
const client = xmlrpc.createSecureClient(secureClientOptions);
const client = (secure) ? xmlrpc.createSecureClient(secureClientOptions) : xmlrpc.createClient(secureClientOptions);
client.methodCall('login_to_simulator',
[
{

View File

@@ -172,6 +172,9 @@ export class Caps
resolve(body);
}
});
}).catch((err) =>
{
reject(err);
});
});
}
@@ -261,7 +264,7 @@ export class Caps
}
else
{
reject(new Error('Capability not available'));
reject(new Error('Capability ' + capability + ' not available'));
}
});
});

View File

@@ -118,7 +118,14 @@ export class EventQueueClient
pprop.AABBMin = new Vector3([parseInt(body['ParcelData'][0]['AABBMin'][0], 10), parseInt(body['ParcelData'][0]['AABBMin'][1], 10), parseInt( body['ParcelData'][0]['AABBMin'][2], 10)]);
pprop.AnyAVSounds = body['ParcelData'][0]['AnyAVSounds'];
pprop.Area = body['ParcelData'][0]['Area'];
pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0);
try
{
pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0);
}
catch (ignore)
{
// TODO: Opensim glitch
}
pprop.AuthBuyerID = new UUID(String(body['ParcelData'][0]['AuthBuyerID']));
pprop.Bitmap = Buffer.from(body['ParcelData'][0]['Bitmap'].toArray());
@@ -167,7 +174,11 @@ export class EventQueueClient
pprop.TotalPrims = body['ParcelData'][0]['TotalPrims'];
pprop.UserLocation = new Vector3([parseInt(body['ParcelData'][0]['UserLocation'][0], 10), parseInt(body['ParcelData'][0]['UserLocation'][1], 10), parseInt(body['ParcelData'][0]['UserLocation'][2], 10)]);
pprop.UserLookAt = new Vector3([parseInt(body['ParcelData'][0]['UserLookAt'][0], 10), parseInt(body['ParcelData'][0]['UserLookAt'][1], 10), parseInt(body['ParcelData'][0]['UserLookAt'][2], 10)]);
pprop.RegionAllowAccessOverride = body['RegionAllowAccessBlock'][0]['RegionAllowAccessOverride'];
if (body['RegionAllowAccessBlock'] !== undefined && body['RegionAllowAccessBlock'].length > 0)
{
// TODO: OpenSim glitch
pprop.RegionAllowAccessOverride = body['RegionAllowAccessBlock'][0]['RegionAllowAccessOverride'];
}
this.clientEvents.onParcelPropertiesEvent.next(pprop);
break;
}

View File

@@ -97,6 +97,13 @@ export class Inventory
invItem.inventoryType = parseInt(receivedItem['inv_type'], 10);
invItem.type = parseInt(receivedItem['type'], 10);
invItem.itemID = item;
if (receivedItem['permissions']['last_owner_id'] === undefined)
{
// TODO: OpenSim glitch
receivedItem['permissions']['last_owner_id'] = receivedItem['permissions']['owner_id'];
}
invItem.permissions = {
baseMask: parseInt(receivedItem['permissions']['base_mask'], 10),
nextOwnerMask: parseInt(receivedItem['permissions']['next_owner_mask'], 10),

View File

@@ -206,6 +206,11 @@ export class InventoryFolder
invItem.itemID = new UUID(item['item_id'].toString());
invItem.description = item['desc'];
invItem.type = item['type'];
if (item['permissions']['last_owner_id'] === undefined)
{
// TODO: OpenSim Glitch;
item['permissions']['last_owner_id'] = item['permissions']['owner_id'];
}
invItem.permissions = {
baseMask: item['permissions']['base_mask'],
groupMask: item['permissions']['group_mask'],

View File

@@ -4,4 +4,5 @@ export class LoginParameters
lastName: string;
password: string;
start = 'last';
url = 'https://login.agni.lindenlab.com/cgi-bin/login.cgi';
}

View File

@@ -54,6 +54,10 @@ export class LoginResponse
const x = parseFloat(num[0]);
const y = parseFloat(num[1]);
const z = parseFloat(num[2]);
if (isNaN(x) || isNaN(y) || isNaN(z))
{
throw new Error('Invalid Vector');
}
return new Vector3([x, y, z]);
}
@@ -79,11 +83,25 @@ export class LoginResponse
}
if (parsed['position'])
{
result['position'] = this.parseVector3('[' + parsed['position'] + ']');
try
{
result['position'] = this.parseVector3('[' + parsed['position'] + ']');
}
catch (error)
{
result['position'] = new Vector3([128.0, 128.0, 0.0]);
}
}
if (parsed['look_at'])
{
result['lookAt'] = this.parseVector3('[' + parsed['lookAt'] + ']');
try
{
result['lookAt'] = this.parseVector3('[' + parsed['lookAt'] + ']');
}
catch (error)
{
result['lookAt'] = new Vector3([128.0, 128.0, 0.0]);
}
}
@@ -247,7 +265,14 @@ export class LoginResponse
});
break;
case 'look_at':
this.agent.cameraLookAt = LoginResponse.parseVector3(val);
try
{
this.agent.cameraLookAt = LoginResponse.parseVector3(val);
}
catch (error)
{
console.error('Invalid look_at from LoginResponse');
}
break;
case 'openid_url':
this.agent.openID.url = String(val);

View File

@@ -71,6 +71,7 @@ export class ObjectStoreLite implements IObjectStore
switch (packet.message.id)
{
case Message.ObjectProperties:
{
const objProp = packet.message as ObjectPropertiesMessage;
for (const obj of objProp.ObjectData)
{
@@ -86,14 +87,19 @@ export class ObjectStoreLite implements IObjectStore
}
}
break;
}
case Message.ObjectUpdate:
{
const objectUpdate = packet.message as ObjectUpdateMessage;
this.objectUpdate(objectUpdate);
break;
}
case Message.ObjectUpdateCached:
{
const objectUpdateCached = packet.message as ObjectUpdateCachedMessage;
this.objectUpdateCached(objectUpdateCached);
break;
}
case Message.ObjectUpdateCompressed:
{
const objectUpdateCompressed = packet.message as ObjectUpdateCompressedMessage;
@@ -101,13 +107,17 @@ export class ObjectStoreLite implements IObjectStore
break;
}
case Message.ImprovedTerseObjectUpdate:
{
const objectUpdateTerse = packet.message as ImprovedTerseObjectUpdateMessage;
this.objectUpdateTerse(objectUpdateTerse);
break;
}
case Message.KillObject:
{
const killObj = packet.message as KillObjectMessage;
this.killObject(killObj);
break;
}
}
});
@@ -408,6 +418,7 @@ export class ObjectStoreLite implements IObjectStore
newObj.localID = obj.ID;
newObj.objectID = obj.FullID;
newObj.object = obj;
newObj.createSelected = obj.Flags !== undefined && (obj.Flags & PrimFlags.CreateSelected) !== 0;
if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()])
{
this.selectedPrimsWithoutUpdate[obj.ID] = true;
@@ -716,11 +727,6 @@ export class ObjectStoreLite implements IObjectStore
namevalue.value = kv[4];
nv[kv[0]] = namevalue;
}
else
{
console.log('namevalue unexpected length: ' + kv.length);
console.log(kv);
}
}
});
return nv;

View File

@@ -107,6 +107,9 @@ export class Region
terrainHeightRange10: number;
terrainHeightRange11: number;
handshakeComplete = false;
handshakeCompleteEvent: Subject<void> = new Subject<void>();
circuit: Circuit;
objects: IObjectStore;
caps: Caps;
@@ -1170,6 +1173,8 @@ export class Region
};
}
}
this.handshakeComplete = true;
this.handshakeCompleteEvent.next();
}
shutdown()
{

View File

@@ -3,6 +3,8 @@ import { Quaternion } from './Quaternion';
import { GlobalPosition } from './public/interfaces/GlobalPosition';
import { HTTPAssets } from '../enums/HTTPAssets';
import { Vector3 } from './Vector3';
import { Subject, Subscription } from 'rxjs';
import Timeout = NodeJS.Timeout;
export class Utils
{
@@ -450,4 +452,94 @@ export class Utils
return str.substr(0, index - 1);
}
}
static promiseConcurrent<T>(promises: (() => Promise<T>)[], concurrency: number, timeout: number): Promise<{results: T[], errors: Error[]}>
{
return new Promise<{results: T[], errors: Error[]}>(async (resolve, reject) =>
{
const originalConcurrency = concurrency;
const promiseQueue: (() => Promise<T>)[] = [];
for (const promise of promises)
{
promiseQueue.push(promise);
}
const slotAvailable: Subject<void> = new Subject<void>();
const errors: Error[] = [];
const results: T[] = [];
function waitForAvailable()
{
return new Promise<void>((resolve1, reject1) =>
{
const subs = slotAvailable.subscribe(() =>
{
subs.unsubscribe();
resolve1();
});
});
}
function runPromise(promise: () => Promise<T>)
{
concurrency--;
let timedOut = false;
let timeo: Timeout | undefined = undefined;
promise().then((result: T) =>
{
if (timedOut)
{
return;
}
if (timeo !== undefined)
{
clearTimeout(timeo);
}
results.push(result);
concurrency++;
slotAvailable.next();
}).catch((err) =>
{
if (timedOut)
{
return;
}
if (timeo !== undefined)
{
clearTimeout(timeo);
}
errors.push(err);
concurrency++;
slotAvailable.next();
});
timeo = setTimeout(() =>
{
timedOut = true;
errors.push(new Error('Promise timed out'));
concurrency++;
slotAvailable.next();
}, timeout);
}
while (promiseQueue.length > 0)
{
if (concurrency < 1)
{
await waitForAvailable();
}
else
{
const thunk = promiseQueue.shift();
if (thunk !== undefined)
{
runPromise(thunk);
}
}
}
while (concurrency < originalConcurrency)
{
await waitForAvailable();
}
resolve({results: results, errors: errors});
});
}
}

View File

@@ -14,9 +14,9 @@ export class Vector3 extends vec3
{
v = Vector3.getZero();
}
//doc.ele('X', v.x);
//doc.ele('Y', v.y);
//doc.ele('Z', v.z);
doc.ele('X', v.x);
doc.ele('Y', v.y);
doc.ele('Z', v.z);
}
static fromXMLJS(obj: any, param: string): Vector3 | false

View File

@@ -24,47 +24,45 @@ import { HTTPAssets } from '../../enums/HTTPAssets';
export class AssetCommands extends CommandsBase
{
async downloadAsset(type: HTTPAssets, uuid: UUID): Promise<Buffer>
async downloadAsset(type: HTTPAssets, uuid: UUID | string): Promise<Buffer>
{
const result = await this.currentRegion.caps.downloadAsset(uuid, type);
if (result.toString('UTF-8').trim() === 'Not found!')
if (typeof uuid === 'string')
{
throw new Error('Asset not found');
uuid = new UUID(uuid);
}
else if (result.toString('UTF-8').trim() === 'Incorrect Syntax')
try
{
throw new Error('Invalid Syntax');
const result = await this.currentRegion.caps.downloadAsset(uuid, type);
if (result.toString('UTF-8').trim() === 'Not found!')
{
throw new Error('Asset not found');
}
else if (result.toString('UTF-8').trim() === 'Incorrect Syntax')
{
throw new Error('Invalid Syntax');
}
return result;
}
catch (error)
{
// Fall back to old asset transfer
const transferParams = Buffer.allocUnsafe(20);
uuid.writeToBuffer(transferParams, 0);
transferParams.writeInt32LE(parseInt(type, 10), 16);
return this.transfer(TransferChannelType.Asset, TransferSourceType.Asset, false, transferParams);
}
return result;
}
downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise<Buffer>
transfer(channelType: TransferChannelType, sourceType: TransferSourceType, priority: boolean, transferParams: Buffer, outAssetID?: { assetID: UUID }): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
const transferParams = Buffer.allocUnsafe(100);
let pos = 0;
this.agent.agentID.writeToBuffer(transferParams, pos);
pos = pos + 16;
this.circuit.sessionID.writeToBuffer(transferParams, pos);
pos = pos + 16;
ownerID.writeToBuffer(transferParams, pos);
pos = pos + 16;
objectID.writeToBuffer(transferParams, pos);
pos = pos + 16;
itemID.writeToBuffer(transferParams, pos);
pos = pos + 16;
assetID.writeToBuffer(transferParams, pos);
pos = pos + 16;
transferParams.writeInt32LE(type, pos);
const transferID = UUID.random();
const msg = new TransferRequestMessage();
msg.TransferInfo = {
TransferID: transferID,
ChannelType: TransferChannelType.Asset,
SourceType: TransferSourceType.SimInventoryItem,
ChannelType: channelType,
SourceType: sourceType,
Priority: 100.0 + (priority ? 1.0 : 0.0),
Params: transferParams
};
@@ -72,7 +70,7 @@ export class AssetCommands extends CommandsBase
this.circuit.sendMessage(msg, PacketFlags.Reliable);
let gotInfo = true;
let expectedSize = 0;
const packets: {[key: number]: Buffer} = {};
const packets: { [key: number]: Buffer } = {};
const subscription = this.circuit.subscribeToMessages([
Message.TransferInfo,
Message.TransferAbort,
@@ -90,16 +88,24 @@ export class AssetCommands extends CommandsBase
switch (messg.TransferData.Status)
{
case TransferStatus.Abort:
throw new Error('Transfer Aborted');
subscription.unsubscribe();
reject(new Error('Transfer Aborted'));
break;
case TransferStatus.Error:
throw new Error('Error');
subscription.unsubscribe();
reject(new Error('Error'));
break;
case TransferStatus.Skip:
console.error('TransferPacket: Skip! not sure what this means');
break;
case TransferStatus.InsufficientPermissions:
throw new Error('Insufficient Permissions');
subscription.unsubscribe();
reject(new Error('Insufficient Permissions'));
break;
case TransferStatus.NotFound:
throw new Error('Not Found');
subscription.unsubscribe();
reject(new Error('Not Found'));
break;
}
break;
}
@@ -122,18 +128,25 @@ export class AssetCommands extends CommandsBase
}
break;
case TransferStatus.Abort:
throw new Error('Transfer Aborted');
subscription.unsubscribe();
reject(new Error('Transfer Aborted'));
break;
case TransferStatus.Error:
throw new Error('Error');
subscription.unsubscribe();
reject(new Error('Error'));
// See if we get anything else
break;
case TransferStatus.Skip:
console.error('TransferInfo: Skip! not sure what this means');
break;
case TransferStatus.InsufficientPermissions:
throw new Error('Insufficient Permissions');
subscription.unsubscribe();
reject(new Error('Insufficient Permissions'));
break;
case TransferStatus.NotFound:
throw new Error('Not Found');
subscription.unsubscribe();
reject(new Error('Not Found'));
break;
}
break;
@@ -145,7 +158,9 @@ export class AssetCommands extends CommandsBase
{
return;
}
throw new Error('Transfer Aborted');
subscription.unsubscribe();
reject(new Error('Transfer Aborted'));
return;
}
}
if (gotInfo)
@@ -177,7 +192,38 @@ export class AssetCommands extends CommandsBase
subscription.unsubscribe();
reject(error);
}
})
});
});
}
downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
const transferParams = Buffer.allocUnsafe(100);
let pos = 0;
this.agent.agentID.writeToBuffer(transferParams, pos);
pos = pos + 16;
this.circuit.sessionID.writeToBuffer(transferParams, pos);
pos = pos + 16;
ownerID.writeToBuffer(transferParams, pos);
pos = pos + 16;
objectID.writeToBuffer(transferParams, pos);
pos = pos + 16;
itemID.writeToBuffer(transferParams, pos);
pos = pos + 16;
assetID.writeToBuffer(transferParams, pos);
pos = pos + 16;
transferParams.writeInt32LE(type, pos);
this.transfer(TransferChannelType.Asset, TransferSourceType.SimInventoryItem, priority, transferParams, outAssetID).then((result) =>
{
resolve(result);
}).catch((err) =>
{
reject(err);
});
});
}
@@ -366,7 +412,7 @@ export class AssetCommands extends CommandsBase
'next_owner_mask': PermissionMask.All
};
const result = await this.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap);
if (result['state'] === 'upload' && result['upload_price'])
if (result['state'] === 'upload' && result['upload_price'] !== undefined)
{
const cost = result['upload_price'];
if (await confirmCostCallback(cost))

View File

@@ -35,6 +35,9 @@ import { Parcel } from '../public/Parcel';
import * as Long from 'long';
import * as micromatch from 'micromatch';
import * as LLSD from '@caspertech/llsd';
import { Subscription } from 'rxjs';
import Timeout = NodeJS.Timeout;
import { ObjectUpdatedEvent } from '../..';
export class RegionCommands extends CommandsBase
{
@@ -60,6 +63,64 @@ export class RegionCommands extends CommandsBase
return responseMsg.ReplyBlock.RegionHandle;
}
waitForHandshake(timeout: number = 10000): Promise<void>
{
return new Promise((resolve, reject) =>
{
if (this.currentRegion.handshakeComplete)
{
resolve();
}
else
{
let handshakeSubscription: Subscription | undefined;
let timeoutTimer: number | undefined;
handshakeSubscription = this.currentRegion.handshakeCompleteEvent.subscribe(() =>
{
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
if (handshakeSubscription !== undefined)
{
handshakeSubscription.unsubscribe();
handshakeSubscription = undefined;
resolve();
}
});
timeoutTimer = setTimeout(() =>
{
if (handshakeSubscription !== undefined)
{
handshakeSubscription.unsubscribe();
handshakeSubscription = undefined;
}
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
reject(new Error('Timeout'));
}
}, timeout) as any as number;
if (this.currentRegion.handshakeComplete)
{
if (handshakeSubscription !== undefined)
{
handshakeSubscription.unsubscribe();
handshakeSubscription = undefined;
}
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
resolve();
}
}
});
}
async deselectObjects(objects: GameObject[])
{
// Limit to 255 objects at once
@@ -230,8 +291,6 @@ export class RegionCommands extends CommandsBase
obj.resolvedAt = new Date().getTime() / 1000;
delete uuidMap[objDataUUID];
found = true;
// console.log(obj.name + ' (' + resolved + ' of ' + objects.length + ')');
}
}
if (Object.keys(uuidMap).length === 0)
@@ -827,7 +886,24 @@ export class RegionCommands extends CommandsBase
});
}
private async buildPart(obj: GameObject, posOffset: Vector3, meshCallback: (object: GameObject, meshData: UUID) => UUID | null)
private async createPrimWithRetry(retries: number, obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID)
{
for (retries--; retries > -1; retries--)
{
try
{
const newObject = await this.createPrim(obj, new Vector3(posOffset), new Quaternion(rotOffset), inventoryID);
return newObject;
}
catch (ignore)
{
process.exit(1);
}
}
throw new Error('Failed to create prim with ' + retries + ' tries');
}
private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, meshCallback: (object: GameObject, meshData: UUID) => UUID | null)
{
// Rez a prim
let newObject: GameObject;
@@ -836,16 +912,16 @@ export class RegionCommands extends CommandsBase
const inventoryID: UUID | null = await meshCallback(obj, obj.extraParams.meshData.meshData);
if (inventoryID !== null)
{
newObject = await this.createPrim(obj, posOffset, inventoryID);
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset, inventoryID);
}
else
{
newObject = await this.createPrim(obj, posOffset);
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset);
}
}
else
{
newObject = await this.createPrim(obj, posOffset);
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset);
}
await newObject.setExtraParams(obj.extraParams);
if (obj.TextureEntry !== undefined)
@@ -867,71 +943,92 @@ export class RegionCommands extends CommandsBase
{
return new Promise<GameObject>(async (resolve, reject) =>
{
const parts: (Promise<GameObject>)[] = [];
console.log('Rezzing prims');
parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), meshCallback));
const parts = [];
console.log('Rezzing root prim');
parts.push(this.buildPart(obj, Vector3.getZero(), meshCallback));
console.log('Building child prims');
if (obj.children && obj.Position)
if (obj.children)
{
if (obj.Position === undefined)
{
obj.Position = Vector3.getZero();
}
if (obj.Rotation === undefined)
{
obj.Rotation = Quaternion.getIdentity();
}
for (const child of obj.children)
{
parts.push(this.buildPart(child, obj.Position, meshCallback));
if (child.Position !== undefined && child.Rotation !== undefined)
{
const objPos = new Vector3(obj.Position);
const objRot = new Quaternion(obj.Rotation);
parts.push(this.buildPart(child, objPos, objRot, meshCallback));
}
}
}
Promise.all(parts).then(async (results) =>
{
console.log('Linking prims');
const rootObj = results[0];
const childPrims: GameObject[] = [];
for (const childObject of results)
{
if (childObject !== rootObj)
{
await childObject.linkTo(rootObj);
childPrims.push(childObject);
}
}
await rootObj.linkFrom(childPrims);
console.log('All done');
resolve(rootObj);
}).catch((err) =>
{
reject(err);
});
/*
Utils.promiseConcurrent<GameObject>(parts, 1000, 10000).then(async (results) =>
{
console.log('Linking prims');
const rootObj = results.results[0];
await rootObj.linkFrom(results.results);
console.log('All done');
resolve(rootObj);
}).catch((err) =>
{
reject(err);
});
*/
});
}
createPrim(obj: GameObject, posOffset: Vector3, inventoryID?: UUID): Promise<GameObject>
createPrim(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID): Promise<GameObject>
{
console.log('Create prim');
return new Promise(async (resolve, reject) =>
{
const timeRequested = (new Date().getTime() / 1000) - this.currentRegion.timeOffset;
if (obj.Position === undefined)
{
obj.Position = Vector3.getZero();
}
if (obj.Rotation === undefined)
{
obj.Rotation = Quaternion.getIdentity();
}
const objectPosition = new Vector3(obj.Position);
const objectRotation = new Quaternion(obj.Rotation);
const objectScale = new Vector3(obj.Scale);
let finalPos = Vector3.getZero();
let finalRot = Quaternion.getIdentity();
if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0)
if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0 && objectPosition !== undefined)
{
finalPos = obj.Position;
finalRot = obj.Rotation;
finalPos = new Vector3(objectPosition);
finalRot = new Quaternion(objectRotation);
}
else
{
const finalPosOffset: Vector3 = obj.Position;
finalPos = new Vector3(new Vector3(finalPosOffset).add(new Vector3(posOffset)));
finalRot = obj.Rotation;
const adjustedPos = new Vector3(new Vector3(objectPosition).multiplyByQuat(new Quaternion(rotOffset).inverse()));
finalPos = new Vector3(new Vector3(adjustedPos).add(new Vector3(posOffset)));
finalRot = new Quaternion(new Quaternion(objectRotation).add(new Quaternion(rotOffset)));
}
let msg: ObjectAddMessage | RezObjectMessage | null = null;
let fromInventory = false;
if (inventoryID === undefined || this.agent.inventory.itemsByID[inventoryID.toString()] === undefined)
{
console.log('Regular prim');
// First, rez object in scene
msg = new ObjectAddMessage();
msg.AgentData = {
@@ -973,7 +1070,6 @@ export class RegionCommands extends CommandsBase
}
else
{
console.log('Rezzing ' + this.agent.inventory.itemsByID[inventoryID.toString()].name);
fromInventory = true;
const invItem = this.agent.inventory.itemsByID[inventoryID.toString()];
const queryID = UUID.random();
@@ -1021,28 +1117,38 @@ export class RegionCommands extends CommandsBase
CRC: 0,
};
}
const objSub = this.currentRegion.clientEvents.onSelectedObjectEvent.subscribe(async (evt: SelectedObjectEvent) =>
let objSub: Subscription | undefined = undefined;
let timeout: Timeout | undefined = setTimeout(() =>
{
if (evt.object.creatorID !== undefined &&
evt.object.creatorID.equals(this.agent.agentID) &&
evt.object.creationDate !== undefined &&
!evt.object.claimedForBuild)
if (objSub !== undefined)
{
let claim = false;
const creationDate = evt.object.creationDate.toNumber() / 1000000;
if (fromInventory && inventoryID !== undefined && evt.object.itemID.equals(inventoryID))
{
claim = true;
}
else if (!fromInventory && evt.object.itemID.equals(UUID.zero()) && creationDate > timeRequested)
{
claim = true;
}
if (claim)
objSub.unsubscribe();
objSub = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
reject(new Error('Prim never arrived'));
}, 10000);
objSub = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) =>
{
if (evt.createSelected && !evt.object.claimedForBuild)
{
if (!fromInventory || (inventoryID !== undefined && evt.object.itemID.equals(inventoryID)))
{
if (objSub !== undefined)
{
objSub.unsubscribe();
objSub = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
evt.object.claimedForBuild = true;
objSub.unsubscribe();
if (!fromInventory)
{
@@ -1063,18 +1169,15 @@ export class RegionCommands extends CommandsBase
}
}
});
if (obj.Position !== undefined && obj.Scale !== undefined)
{
// Move the camera to look directly at prim for faster capture
const campos = new Vector3(finalPos);
campos.z += 5.0 + obj.Scale.z;
console.log('Moving camera to ' + campos.toString());
await this.currentRegion.clientCommands.agent.setCamera(campos, finalPos, 4096, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
}
// Move the camera to look directly at prim for faster capture
const campos = new Vector3(finalPos);
campos.z += 128.0 + objectScale.z;
await this.currentRegion.clientCommands.agent.setCamera(campos, finalPos, 4096, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
if (msg !== null)
{
this.circuit.sendMessage(msg, PacketFlags.Reliable);
console.log('Requested rez');
}
});
}

View File

@@ -853,6 +853,28 @@ export class GameObject implements IGameObjectData
await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000);
}
async linkFrom(objects: GameObject[])
{
const msg = new ObjectLinkMessage();
msg.AgentData = {
AgentID: this.region.agent.agentID,
SessionID: this.region.circuit.sessionID
};
msg.ObjectData = [
{
ObjectLocalID: this.ID
}
];
for (const obj of objects)
{
msg.ObjectData.push(
{
ObjectLocalID: obj.ID
});
}
await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 30000);
}
async setDescription(desc: string)
{
this.description = desc;

View File

@@ -6,4 +6,5 @@ export class NewObjectEvent
objectID: UUID;
localID: number;
object: GameObject;
createSelected: boolean;
}

6
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@caspertech/llsd": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@caspertech/llsd/-/llsd-1.0.2.tgz",
"integrity": "sha512-sVgsfk3x6cp/lXG9wdvQqIxKNYI2YqicQNw1TamfghRKwZV50w7pfZlG8pCJLKPKhYwSxSr0e32zn6H0L15k8g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@caspertech/llsd/-/llsd-1.0.3.tgz",
"integrity": "sha512-1GKgUfpBNhEQKwDrMqE6EJ6xc4tMIJX6wNf3ck8jqAYEvCkR3NO99ryLPUZagos6/PlA4TIzVThS6tTr/lHCUQ==",
"requires": {
"abab": "^1.0.4",
"xmldom": "^0.1.27"

View File

@@ -31,7 +31,7 @@
"typescript": "^3.6.3"
},
"dependencies": {
"@caspertech/llsd": "^1.0.2",
"@caspertech/llsd": "^1.0.3",
"@types/long": "^4.0.0",
"@types/micromatch": "^3.1.0",
"@types/mocha": "^5.2.5",