- 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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
||||
/example/npm-debug.log
|
||||
/caspertech-node-metaverse-*.tgz
|
||||
/npm-debug.log
|
||||
/dist
|
||||
/dist
|
||||
/exampleMine
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
[
|
||||
{
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -4,4 +4,5 @@ export class LoginParameters
|
||||
lastName: string;
|
||||
password: string;
|
||||
start = 'last';
|
||||
url = 'https://login.agni.lindenlab.com/cgi-bin/login.cgi';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,4 +6,5 @@ export class NewObjectEvent
|
||||
objectID: UUID;
|
||||
localID: number;
|
||||
object: GameObject;
|
||||
createSelected: boolean;
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user