- Bump to 0.5.13

- Add building support for TaskInventory and nested objects (from XML)
- Add support for taking objects into inventory
- Add waitForAppearanceSet utility
- Add new event for when object is fully resolved (ObjectProperties received)
- Fixed InventoryItem CRC method
- Fixed quaternion bug
- Support for uploading Script, Notecard and Gesture assets
- Significantly improved build process
This commit is contained in:
Casper Warden
2020-01-09 17:53:22 +00:00
parent 5e235d2db1
commit 2145de775b
24 changed files with 1414 additions and 357 deletions

View File

@@ -5,6 +5,7 @@ lib/
.idea/
tools/
example/
exampleMine/
docs/
dist/tests/
testing/

View File

@@ -26,6 +26,7 @@ import { AgentFlags } from '../enums/AgentFlags';
import { ControlFlags } from '../enums/ControlFlags';
import { PacketFlags } from '../enums/PacketFlags';
import { FolderType } from '../enums/FolderType';
import { Subject, Subscription } from 'rxjs';
export class Agent
{
@@ -84,6 +85,9 @@ export class Agent
};
agentUpdateTimer: Timer | null = null;
estateManager = false;
appearanceSet = false;
appearanceSetEvent: Subject<void> = new Subject<void>();
private clientEvents: ClientEvents;
constructor(clientEvents: ClientEvents)
@@ -311,6 +315,9 @@ export class Agent
}
});
this.appearanceSet = true;
this.appearanceSetEvent.next();
});
}
}

21
lib/classes/AssetMap.ts Normal file
View File

@@ -0,0 +1,21 @@
export class AssetMap
{
mesh: {
[key: string]: {
objectName: string,
objectDescription: string,
assetID: string
}
} = {};
textures: { [key: string]: string } = {};
animations: { [key: string]: string } = {};
sounds: { [key: string]: string } = {};
gestures: { [key: string]: string } = {};
landmarks: { [key: string]: string } = {};
callingcards: { [key: string]: string } = {};
scripts: { [key: string]: string } = {};
clothing: { [key: string]: string } = {};
notecards: { [key: string]: string } = {};
bodyparts: { [key: string]: string } = {};
objects: { [key: string]: Buffer | null } = {};
}

15
lib/classes/BuildMap.ts Normal file
View File

@@ -0,0 +1,15 @@
import { AssetMap } from './AssetMap';
import { GameObject } from './public/GameObject';
import { Vector3 } from './Vector3';
export class BuildMap
{
public primsNeeded = 0;
public primReservoir: GameObject[] = [];
public rezLocation: Vector3 = Vector3.getZero();
constructor(public assetMap: AssetMap, public callback: (map: AssetMap) => void, public costOnly = false)
{
}
}

View File

@@ -484,7 +484,7 @@ export class Circuit
{
clearTimeout(this.receivedPackets[packet.sequenceNumber]);
this.receivedPackets[packet.sequenceNumber] = setTimeout(this.expireReceivedPacket.bind(this, packet.sequenceNumber), 10000);
console.log('Ignoring duplicate packet: ' + packet.message.name + ' sequenceID: ' + packet.sequenceNumber);
this.sendAck(packet.sequenceNumber);
return;
}
this.receivedPackets[packet.sequenceNumber] = setTimeout(this.expireReceivedPacket.bind(this, packet.sequenceNumber), 10000);

View File

@@ -23,6 +23,7 @@ import { FriendRightsEvent } from '../events/FriendRightsEvent';
import { FriendRemovedEvent } from '../events/FriendRemovedEvent';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
export class ClientEvents
@@ -52,4 +53,5 @@ export class ClientEvents
onObjectUpdatedEvent: Subject<ObjectUpdatedEvent> = new Subject<ObjectUpdatedEvent>();
onObjectKilledEvent: Subject<ObjectKilledEvent> = new Subject<ObjectKilledEvent>();
onSelectedObjectEvent: Subject<SelectedObjectEvent> = new Subject<SelectedObjectEvent>();
onObjectResolvedEvent: Subject<ObjectResolvedEvent> = new Subject<ObjectResolvedEvent>();
}

View File

@@ -141,7 +141,6 @@ export class InventoryFolder
delete this.agent.inventory.itemsByID[itemID.toString()];
this.items = this.items.filter((item) =>
{
console.log(item.itemID + ' vs ' + JSON.stringify(itemID));
return !item.itemID.equals(itemID);
})
}

View File

@@ -15,6 +15,9 @@ export class InventoryItem
parentID: UUID;
flags: InventoryItemFlags;
itemID: UUID;
oldItemID?: UUID;
parentPartID?: UUID;
permsGranter?: UUID;
description: string;
type: AssetType;
permissions: {
@@ -44,26 +47,24 @@ export class InventoryItem
getCRC(): number
{
let crc = 0;
crc += this.assetID.CRC();
crc += this.parentID.CRC();
crc += this.itemID.CRC();
crc += this.permissions.creator.CRC();
crc += this.permissions.owner.CRC();
crc += this.permissions.group.CRC();
crc += this.permissions.ownerMask;
crc += this.permissions.nextOwnerMask;
crc += this.permissions.everyoneMask;
crc += this.permissions.groupMask;
crc += this.flags;
crc += this.inventoryType;
crc += this.type;
crc += Math.round(this.created.getTime() / 1000);
crc += this.salePrice;
crc += this.saleType * 0x07073096;
while (crc > 4294967295)
{
crc -= 4294967295;
}
crc = crc + this.itemID.CRC() >>> 0;
crc = crc + this.parentID.CRC() >>> 0;
crc = crc + this.permissions.creator.CRC() >>> 0;
crc = crc + this.permissions.owner.CRC() >>> 0;
crc = crc + this.permissions.group.CRC() >>> 0;
crc = crc + this.permissions.baseMask >>> 0;
crc = crc + this.permissions.ownerMask >>> 0;
crc = crc + this.permissions.everyoneMask >>> 0;
crc = crc + this.permissions.groupMask >>> 0;
crc = crc + this.assetID.CRC() >>> 0;
crc = crc + this.type >>> 0;
crc = crc + this.inventoryType >>> 0;
crc = crc + this.flags >>> 0;
crc = crc + this.salePrice >>> 0;
crc = crc + (this.saleType * 0x07073096 >>> 0) >>> 0;
crc = crc + Math.round(this.created.getTime() / 1000) >>> 0;
return crc;
}
}

View File

@@ -33,6 +33,7 @@ import { ObjectUpdatedEvent } from '../events/ObjectUpdatedEvent';
import { CompressedFlags } from '../enums/CompressedFlags';
import { Vector3 } from './Vector3';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
export class ObjectStoreLite implements IObjectStore
{
@@ -207,8 +208,13 @@ export class ObjectStoreLite implements IObjectStore
o.touchName = Utils.BufferToStringSimple(obj.TouchName);
o.sitName = Utils.BufferToStringSimple(obj.SitName);
o.textureID = Utils.BufferToStringSimple(obj.TextureID);
o.resolvedAt = new Date().getTime() / 1000;
if (!o.resolvedAt)
{
o.resolvedAt = new Date().getTime() / 1000;
const evt = new ObjectResolvedEvent();
evt.object = o;
this.clientEvents.onObjectResolvedEvent.next(evt);
}
if (o.Flags !== undefined)
{
if (o.Flags & PrimFlags.CreateSelected)
@@ -419,6 +425,7 @@ export class ObjectStoreLite implements IObjectStore
newObj.objectID = obj.FullID;
newObj.object = obj;
newObj.createSelected = obj.Flags !== undefined && (obj.Flags & PrimFlags.CreateSelected) !== 0;
obj.createdSelected = newObj.createSelected;
if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()])
{
this.selectedPrimsWithoutUpdate[obj.ID] = true;

View File

@@ -72,7 +72,7 @@ export class Quaternion extends quat
this.x = buf.x;
this.y = buf.y;
this.z = buf.z;
this.w = buf.z;
this.w = buf.w;
}
else if (buf instanceof Quaternion)
{

View File

@@ -164,14 +164,11 @@ export class UUID
public CRC(): number
{
let retval = 0;
const bytes: Buffer = this.getBuffer();
retval += ((bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]);
retval += ((bytes[7] << 24) + (bytes[6] << 16) + (bytes[5] << 8) + bytes[4]);
retval += ((bytes[11] << 24) + (bytes[10] << 16) + (bytes[9] << 8) + bytes[8]);
retval += ((bytes[15] << 24) + (bytes[14] << 16) + (bytes[13] << 8) + bytes[12]);
return retval;
const crcOne = ((bytes[3] << 24 >>> 0) + (bytes[2] << 16 >>> 0) + (bytes[1] << 8 >>> 0) + bytes[0]);
const crcTwo = ((bytes[7] << 24 >>> 0) + (bytes[6] << 16 >>> 0) + (bytes[5] << 8 >>> 0) + bytes[4]);
const crcThree = ((bytes[11] << 24 >>> 0) + (bytes[10] << 16 >>> 0) + (bytes[9] << 8 >>> 0) + bytes[8]);
const crcFour = ((bytes[15] << 24 >>> 0) + (bytes[14] << 16 >>> 0) + (bytes[13] << 8 >>> 0) + bytes[12]);
return crcOne + crcTwo + crcThree + crcFour >>> 0;
}
}

View File

@@ -3,7 +3,9 @@ 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 { Subject } from 'rxjs';
import { AssetType } from '../enums/AssetType';
import { InventoryTypeLL } from '../enums/InventoryTypeLL';
import Timeout = NodeJS.Timeout;
export class Utils
@@ -132,7 +134,81 @@ export class Utils
};
}
static HTTPAssetTypeToInventoryType(HTTPAssetType: string)
static HTTPAssetTypeToAssetType(HTTPAssetType: string): AssetType
{
switch (HTTPAssetType)
{
case HTTPAssets.ASSET_TEXTURE:
return AssetType.Texture;
case HTTPAssets.ASSET_SOUND:
return AssetType.Sound;
case HTTPAssets.ASSET_ANIMATION:
return AssetType.Animation;
case HTTPAssets.ASSET_GESTURE:
return AssetType.Gesture;
case HTTPAssets.ASSET_LANDMARK:
return AssetType.Landmark;
case HTTPAssets.ASSET_CALLINGCARD:
return AssetType.CallingCard;
case HTTPAssets.ASSET_SCRIPT:
return AssetType.Script;
case HTTPAssets.ASSET_CLOTHING:
return AssetType.Clothing;
case HTTPAssets.ASSET_OBJECT:
return AssetType.Object;
case HTTPAssets.ASSET_NOTECARD:
return AssetType.Notecard;
case HTTPAssets.ASSET_LSL_TEXT:
return AssetType.LSLText;
case HTTPAssets.ASSET_LSL_BYTECODE:
return AssetType.LSLBytecode;
case HTTPAssets.ASSET_BODYPART:
return AssetType.Bodypart;
case HTTPAssets.ASSET_MESH:
return AssetType.Mesh;
default:
return 0;
}
}
static HTTPAssetTypeToInventoryType(HTTPAssetType: string): InventoryTypeLL
{
switch (HTTPAssetType)
{
case HTTPAssets.ASSET_TEXTURE:
return InventoryTypeLL.texture;
case HTTPAssets.ASSET_SOUND:
return InventoryTypeLL.sound;
case HTTPAssets.ASSET_ANIMATION:
return InventoryTypeLL.animation;
case HTTPAssets.ASSET_GESTURE:
return InventoryTypeLL.gesture;
case HTTPAssets.ASSET_LANDMARK:
return InventoryTypeLL.landmark;
case HTTPAssets.ASSET_CALLINGCARD:
return InventoryTypeLL.callcard;
case HTTPAssets.ASSET_SCRIPT:
return InventoryTypeLL.script;
case HTTPAssets.ASSET_CLOTHING:
return InventoryTypeLL.wearable;
case HTTPAssets.ASSET_OBJECT:
return InventoryTypeLL.object;
case HTTPAssets.ASSET_NOTECARD:
return InventoryTypeLL.notecard;
case HTTPAssets.ASSET_LSL_TEXT:
return InventoryTypeLL.script;
case HTTPAssets.ASSET_LSL_BYTECODE:
return InventoryTypeLL.script;
case HTTPAssets.ASSET_BODYPART:
return InventoryTypeLL.wearable;
case HTTPAssets.ASSET_MESH:
return InventoryTypeLL.mesh;
default:
return 0;
}
}
static HTTPAssetTypeToCapInventoryType(HTTPAssetType: string): String
{
switch (HTTPAssetType)
{

View File

@@ -9,6 +9,7 @@ import { FilterResponse } from '../../enums/FilterResponse';
import { AvatarPropertiesReplyMessage } from '../messages/AvatarPropertiesReply';
import { AvatarPropertiesRequestMessage } from '../messages/AvatarPropertiesRequest';
import { AvatarPropertiesReplyEvent } from '../../events/AvatarPropertiesReplyEvent';
import { Subscription } from 'rxjs';
export class AgentCommands extends CommandsBase
{
@@ -69,6 +70,64 @@ export class AgentCommands extends CommandsBase
this.agent.sendAgentUpdate();
}
waitForAppearanceSet(timeout: number = 10000): Promise<void>
{
return new Promise((resolve, reject) =>
{
if (this.agent.appearanceSet)
{
resolve();
}
else
{
let appearanceSubscription: Subscription | undefined;
let timeoutTimer: number | undefined;
appearanceSubscription = this.agent.appearanceSetEvent.subscribe(() =>
{
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
if (appearanceSubscription !== undefined)
{
appearanceSubscription.unsubscribe();
appearanceSubscription = undefined;
resolve();
}
});
timeoutTimer = setTimeout(() =>
{
if (appearanceSubscription !== undefined)
{
appearanceSubscription.unsubscribe();
appearanceSubscription = undefined;
}
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
reject(new Error('Timeout'));
}
}, timeout) as any as number;
if (this.agent.appearanceSet)
{
if (appearanceSubscription !== undefined)
{
appearanceSubscription.unsubscribe();
appearanceSubscription = undefined;
}
if (timeoutTimer !== undefined)
{
clearTimeout(timeoutTimer);
timeoutTimer = undefined;
}
resolve();
}
}
});
}
async getAvatarProperties(avatarID: UUID | string): Promise<AvatarPropertiesReplyEvent>
{
if (typeof avatarID === 'string')

View File

@@ -21,9 +21,15 @@ import { Material } from '../public/Material';
import { LLMesh } from '../public/LLMesh';
import { FolderType } from '../../enums/FolderType';
import { HTTPAssets } from '../../enums/HTTPAssets';
import { InventoryItem } from '../InventoryItem';
import { CreateInventoryItemMessage } from '../messages/CreateInventoryItem';
import { WearableType } from '../../enums/WearableType';
import { UpdateCreateInventoryItemMessage } from '../messages/UpdateCreateInventoryItem';
import { FilterResponse } from '../../enums/FilterResponse';
export class AssetCommands extends CommandsBase
{
private callbackID: number = 1;
async downloadAsset(type: HTTPAssets, uuid: UUID | string): Promise<Buffer>
{
if (typeof uuid === 'string')
@@ -400,8 +406,8 @@ export class AssetCommands extends CommandsBase
'metric': 'MUT_Unspecified'
};
const uploadMap = {
'name': name,
'description': description,
'name': String(name),
'description': String(description),
'asset_resources': assetResources,
'asset_type': 'mesh',
'inventory_type': 'object',
@@ -411,7 +417,15 @@ export class AssetCommands extends CommandsBase
'group_mask': PermissionMask.All,
'next_owner_mask': PermissionMask.All
};
const result = await this.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap);
let result;
try
{
result = await this.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap);
}
catch (error)
{
console.error(error);
}
if (result['state'] === 'upload' && result['upload_price'] !== undefined)
{
const cost = result['upload_price'];
@@ -445,16 +459,204 @@ export class AssetCommands extends CommandsBase
}
}
uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise<UUID>
uploadInventoryItem(type: HTTPAssets, data: Buffer, name: string, description: string): Promise<UUID>
{
return new Promise<UUID>((resolve, reject) =>
{
if (type === HTTPAssets.ASSET_SCRIPT)
{
type = HTTPAssets.ASSET_LSL_TEXT;
}
const transactionID = UUID.random();
const callbackID = ++this.callbackID;
const msg = new CreateInventoryItemMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID
};
msg.InventoryBlock = {
CallbackID: callbackID,
FolderID: this.agent.inventory.main.root || UUID.zero(),
TransactionID: transactionID,
NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
Type: Utils.HTTPAssetTypeToAssetType(type),
InvType: Utils.HTTPAssetTypeToInventoryType(type),
WearableType: WearableType.Shape,
Name: Utils.StringToBuffer(name),
Description: Utils.StringToBuffer(description)
};
this.currentRegion.circuit.waitForMessage<UpdateCreateInventoryItemMessage>(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) =>
{
if (message.InventoryData[0].CallbackID === callbackID)
{
return FilterResponse.Finish;
}
else
{
return FilterResponse.NoMatch;
}
}).then((createInventoryMsg: UpdateCreateInventoryItemMessage) =>
{
switch (type)
{
case HTTPAssets.ASSET_NOTECARD:
{
this.currentRegion.caps.capsPostXML('UpdateNotecardAgentInventory', {
'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()),
}).then((result: any) =>
{
if (result['uploader'])
{
const uploader = result['uploader'];
this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) =>
{
if (uploadResult['state'] && uploadResult['state'] === 'complete')
{
const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID;
resolve(itemID);
}
else
{
reject(new Error('Asset upload failed'))
}
}).catch((err) =>
{
reject(err);
});
}
else
{
reject(new Error('Invalid response when attempting to request upload URL for notecard'));
}
}).catch((err) =>
{
reject(err);
});
break;
}
case HTTPAssets.ASSET_GESTURE:
{
this.currentRegion.caps.capsPostXML('UpdateGestureAgentInventory', {
'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()),
}).then((result: any) =>
{
if (result['uploader'])
{
const uploader = result['uploader'];
this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) =>
{
if (uploadResult['state'] && uploadResult['state'] === 'complete')
{
const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID;
resolve(itemID);
}
else
{
reject(new Error('Asset upload failed'))
}
}).catch((err) =>
{
reject(err);
});
}
else
{
reject(new Error('Invalid response when attempting to request upload URL for notecard'));
}
}).catch((err) =>
{
reject(err);
});
break;
}
case HTTPAssets.ASSET_LSL_TEXT:
{
this.currentRegion.caps.capsPostXML('UpdateScriptAgent', {
'item_id': new LLSD.UUID(createInventoryMsg.InventoryData[0].ItemID.toString()),
'target': 'mono'
}).then((result: any) =>
{
if (result['uploader'])
{
const uploader = result['uploader'];
this.currentRegion.caps.capsRequestUpload(uploader, data).then((uploadResult: any) =>
{
if (uploadResult['state'] && uploadResult['state'] === 'complete')
{
const itemID: UUID = createInventoryMsg.InventoryData[0].ItemID;
resolve(itemID);
}
else
{
reject(new Error('Asset upload failed'))
}
}).catch((err) =>
{
reject(err);
});
}
else
{
reject(new Error('Invalid response when attempting to request upload URL for notecard'));
}
}).catch((err) =>
{
reject(err);
});
break;
}
default:
{
reject(new Error('Currently unsupported CreateInventoryType: ' + type));
}
}
}).catch(() =>
{
reject(new Error('Timed out waiting for UpdateCreateInventoryItem'));
});
this.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
uploadAsset(type: HTTPAssets, data: Buffer, name: string, description: string): Promise<InventoryItem>
{
return new Promise<InventoryItem>((resolve, reject) =>
{
switch (type)
{
case HTTPAssets.ASSET_LANDMARK:
case HTTPAssets.ASSET_NOTECARD:
case HTTPAssets.ASSET_GESTURE:
case HTTPAssets.ASSET_SCRIPT:
// These types of assets use an different process
const inventoryItem = this.uploadInventoryItem(type, data, name, description).then((invItemID: UUID) =>
{
this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) =>
{
if (item === null)
{
reject(new Error('Unable to get inventory item'));
}
else
{
resolve(item);
}
}).catch((err) =>
{
reject(err);
});
}).catch((err) =>
{
reject(err);
});
return ;
}
if (this.agent && this.agent.inventory && this.agent.inventory.main && this.agent.inventory.main.root)
{
this.currentRegion.caps.capsPostXML('NewFileAgentInventory', {
'folder_id': new LLSD.UUID(this.agent.inventory.main.root.toString()),
'asset_type': type,
'inventory_type': Utils.HTTPAssetTypeToInventoryType(type),
'inventory_type': Utils.HTTPAssetTypeToCapInventoryType(type),
'name': name,
'description': description,
'everyone_mask': PermissionMask.All,
@@ -468,7 +670,24 @@ export class AssetCommands extends CommandsBase
const uploadURL = response['uploader'];
this.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) =>
{
resolve(new UUID(responseUpload['new_asset'].toString()));
if (responseUpload['new_inventory_item'] !== undefined)
{
const invItemID = new UUID(responseUpload['new_inventory_item'].toString());
this.agent.inventory.fetchInventoryItem(invItemID).then((item: InventoryItem | null) =>
{
if (item === null)
{
reject(new Error('Unable to get inventory item'));
}
else
{
resolve(item);
}
}).catch((err) =>
{
reject(err);
});
}
}).catch((err) =>
{
reject(err);

View File

@@ -21,8 +21,6 @@ import { ObjectAddMessage } from '../messages/ObjectAdd';
import { Quaternion } from '../Quaternion';
import { RezObjectMessage } from '../messages/RezObject';
import { PermissionMask } from '../../enums/PermissionMask';
import { SelectedObjectEvent } from '../../events/SelectedObjectEvent';
import Timer = NodeJS.Timer;
import { PacketFlags } from '../../enums/PacketFlags';
import { GameObject } from '../public/GameObject';
import { PCode } from '../../enums/PCode';
@@ -35,12 +33,19 @@ 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 { Subject, Subscription } from 'rxjs';
import { SculptType } from '../..';
import { ObjectResolvedEvent } from '../../events/ObjectResolvedEvent';
import { AssetMap } from '../AssetMap';
import { InventoryType } from '../../enums/InventoryType';
import { BuildMap } from '../BuildMap';
import Timer = NodeJS.Timer;
import Timeout = NodeJS.Timeout;
import { ObjectUpdatedEvent } from '../..';
export class RegionCommands extends CommandsBase
{
private resolveQueue: {[key: number]: GameObject} = {};
async getRegionHandle(regionID: UUID): Promise<Long>
{
const circuit = this.currentRegion.circuit;
@@ -372,6 +377,102 @@ export class RegionCommands extends CommandsBase
return this.currentRegion.regionName;
}
private waitForObjectResolve(localID: number)
{
return new Promise((resolve, reject) =>
{
let timeout: Timeout | undefined = undefined;
let subs: Subscription | undefined = undefined;
try
{
const ourObject = this.currentRegion.objects.getObjectByLocalID(localID);
if (ourObject.resolvedAt)
{
resolve();
return;
}
}
catch (ignore)
{
}
subs = this.currentRegion.clientEvents.onObjectResolvedEvent.subscribe((evt: ObjectResolvedEvent) =>
{
if (evt.object.ID === localID)
{
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
resolve();
}
});
timeout = setTimeout(() =>
{
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
const object = this.currentRegion.objects.getObjectByLocalID(localID);
if (object.resolvedAt)
{
try
{
const ourObject = this.currentRegion.objects.getObjectByLocalID(localID);
if (ourObject.resolvedAt)
{
console.warn('Resolve timed out but object ' + localID + ' HAS been resolved!');
resolve();
return;
}
}
catch (ignore)
{
}
}
reject(new Error('Timeout'));
}, 10000);
});
}
private async queueResolveObject(object: GameObject, skipInventory = false)
{
if (object.resolvedAt)
{
return;
}
if (this.resolveQueue[object.ID] === undefined)
{
this.resolveQueue[object.ID] = object;
try
{
await this.resolveObjects([object], true, true);
}
catch (error)
{
console.error('Failed to resolve ' + object.ID);
}
delete this.resolveQueue[object.ID];
}
else
{
return this.waitForObjectResolve(object.ID);
}
}
private async resolveObjects(objects: GameObject[], onlyUnresolved: boolean = false, skipInventory = false)
{
// First, create a map of all object IDs
@@ -468,7 +569,6 @@ export class RegionCommands extends CommandsBase
const o = objs[ky];
if ((o.resolveAttempts === undefined || o.resolveAttempts < 3) && o.FullID !== undefined && o.name !== undefined && o.Flags !== undefined && !(o.Flags & PrimFlags.InventoryEmpty) && (!o.inventory || o.inventory.length === 0))
{
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,
@@ -886,237 +986,608 @@ export class RegionCommands extends CommandsBase
});
}
private async createPrimWithRetry(retries: number, obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID)
private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, buildMap: BuildMap)
{
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');
}
// Calculate geometry
const objectPosition = new Vector3(obj.Position);
const objectRotation = new Quaternion(obj.Rotation);
const objectScale = new Vector3(obj.Scale);
private async buildPart(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, meshCallback: (object: GameObject, meshData: UUID) => UUID | null)
{
// Rez a prim
let newObject: GameObject;
if (obj.extraParams !== undefined && obj.extraParams.meshData !== null)
let finalPos = Vector3.getZero();
let finalRot = Quaternion.getIdentity();
if (posOffset.x === 0.0 && posOffset.y === 0.0 && posOffset.z === 0.0 && objectPosition !== undefined)
{
const inventoryID: UUID | null = await meshCallback(obj, obj.extraParams.meshData.meshData);
if (inventoryID !== null)
{
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset, inventoryID);
}
else
{
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset);
}
finalPos = new Vector3(objectPosition);
finalRot = new Quaternion(objectRotation);
}
else
{
newObject = await this.createPrimWithRetry(3, obj, posOffset, rotOffset);
const adjustedPos = new Vector3(objectPosition).multiplyByQuat(new Quaternion(rotOffset));
finalPos = new Vector3(new Vector3(posOffset).add(adjustedPos));
const baseRot = new Quaternion(rotOffset);
finalRot = new Quaternion(baseRot.multiply(new Quaternion(objectRotation)));
}
await newObject.setExtraParams(obj.extraParams);
// Is this a mesh part?
let object: GameObject | null = null;
if (obj.extraParams !== undefined && obj.extraParams.meshData !== null)
{
if (buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] !== undefined)
{
const meshEntry = buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()];
const rezLocation = new Vector3(buildMap.rezLocation);
rezLocation.z += (objectScale.z / 2);
object = await this.rezFromInventory(obj, rezLocation, new UUID(meshEntry.assetID));
}
}
else if (buildMap.primReservoir.length > 0)
{
const newPrim = buildMap.primReservoir.shift();
if (newPrim !== undefined)
{
object = newPrim;
await object.setShape(
obj.PathCurve,
obj.ProfileCurve,
obj.PathBegin,
obj.PathEnd,
obj.PathScaleX,
obj.PathScaleY,
obj.PathShearX,
obj.PathShearY,
obj.PathTwist,
obj.PathTwistBegin,
obj.PathRadiusOffset,
obj.PathTaperX,
obj.PathTaperY,
obj.PathRevolutions,
obj.PathSkew,
obj.ProfileBegin,
obj.ProfileEnd,
obj.ProfileHollow
);
}
}
if (object === null)
{
throw new Error('Failed to acquire prim for build');
}
await object.setGeometry(finalPos, finalRot, objectScale);
if (obj.extraParams.sculptData !== null)
{
if (obj.extraParams.sculptData.type !== SculptType.Mesh)
{
const oldTextureID = obj.extraParams.sculptData.texture.toString();
if (buildMap.assetMap.textures[oldTextureID] !== undefined)
{
obj.extraParams.sculptData.texture = new UUID(buildMap.assetMap.textures[oldTextureID]);
}
}
}
await object.setExtraParams(obj.extraParams);
if (obj.TextureEntry !== undefined)
{
await newObject.setTextureEntry(obj.TextureEntry);
if (obj.TextureEntry.defaultTexture !== null)
{
const oldTextureID = obj.TextureEntry.defaultTexture.textureID.toString();
if (buildMap.assetMap.textures[oldTextureID] !== undefined)
{
obj.TextureEntry.defaultTexture.textureID = new UUID(buildMap.assetMap.textures[oldTextureID]);
}
}
for (const j of obj.TextureEntry.faces)
{
const oldTextureID = j.textureID.toString();
if (buildMap.assetMap.textures[oldTextureID] !== undefined)
{
j.textureID = new UUID(buildMap.assetMap.textures[oldTextureID]);
}
}
try
{
await object.setTextureEntry(obj.TextureEntry);
}
catch (error)
{
console.error(error);
}
}
if (obj.name !== undefined)
{
await newObject.setName(obj.name);
await object.setName(obj.name);
}
if (obj.description !== undefined)
{
await newObject.setDescription(obj.description);
await object.setDescription(obj.description);
}
return newObject;
}
buildObject(obj: GameObject, meshCallback: (object: GameObject, meshData: UUID) => UUID | null): Promise<GameObject>
{
return new Promise<GameObject>(async (resolve, reject) =>
for (const invItem of obj.inventory)
{
const parts: (Promise<GameObject>)[] = [];
console.log('Rezzing prims');
parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), meshCallback));
if (obj.children)
try
{
if (obj.Position === undefined)
switch (invItem.inventoryType)
{
obj.Position = Vector3.getZero();
}
if (obj.Rotation === undefined)
{
obj.Rotation = Quaternion.getIdentity();
}
for (const child of obj.children)
{
if (child.Position !== undefined && child.Rotation !== undefined)
case InventoryType.Clothing:
{
const objPos = new Vector3(obj.Position);
const objRot = new Quaternion(obj.Rotation);
parts.push(this.buildPart(child, objPos, objRot, meshCallback));
if (buildMap.assetMap.clothing[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.clothing[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Bodypart:
{
if (buildMap.assetMap.bodyparts[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.bodyparts[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Notecard:
{
if (buildMap.assetMap.notecards[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.notecards[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Sound:
{
if (buildMap.assetMap.sounds[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.sounds[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Gesture:
{
if (buildMap.assetMap.gestures[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.gestures[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Landmark:
{
if (buildMap.assetMap.landmarks[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.landmarks[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.LSL:
{
if (buildMap.assetMap.scripts[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.scripts[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
case InventoryType.Animation:
{
if (buildMap.assetMap.animations[invItem.assetID.toString()] !== undefined)
{
const invItemID = buildMap.assetMap.animations[invItem.assetID.toString()];
await object.dropInventoryIntoContents(new UUID(invItemID));
}
break;
}
}
}
Promise.all(parts).then(async (results) =>
catch (error)
{
console.log('Linking prims');
const rootObj = results[0];
const childPrims: GameObject[] = [];
for (const childObject of results)
console.error(error);
}
}
// Do nested objects last
for (const invItem of obj.inventory)
{
try
{
switch (invItem.inventoryType)
{
if (childObject !== rootObj)
case InventoryType.Object:
{
childPrims.push(childObject);
if (buildMap.assetMap.objects[invItem.assetID.toString()] !== undefined)
{
const objectXML = buildMap.assetMap.objects[invItem.assetID.toString()];
if (objectXML !== null)
{
const taskObjectXML = await GameObject.fromXML(objectXML.toString('utf-8'));
const taskObject = await this.buildObjectNew(taskObjectXML, buildMap.callback, buildMap.costOnly);
if (taskObject !== null)
{
const invItemUUID = await taskObject.takeToInventory();
await object.dropInventoryIntoContents(invItemUUID);
}
}
}
break;
}
}
await rootObj.linkFrom(childPrims);
console.log('All done');
resolve(rootObj);
}).catch((err) =>
}
catch (error)
{
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);
});
*/
});
console.error(error);
}
}
return object;
}
createPrim(obj: GameObject, posOffset: Vector3, rotOffset: Quaternion, inventoryID?: UUID): Promise<GameObject>
private gatherAssets(obj: GameObject, buildMap: BuildMap)
{
return new Promise(async (resolve, reject) =>
if (obj.extraParams !== undefined)
{
const timeRequested = (new Date().getTime() / 1000) - this.currentRegion.timeOffset;
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 && objectPosition !== undefined)
if (obj.extraParams.meshData !== null)
{
finalPos = new Vector3(objectPosition);
finalRot = new Quaternion(objectRotation);
buildMap.assetMap.mesh[obj.extraParams.meshData.meshData.toString()] = {
objectName: obj.name || 'Object',
objectDescription: obj.description || '(no description)',
assetID: obj.extraParams.meshData.meshData.toString()
};
}
else
{
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)));
buildMap.primsNeeded++;
}
let msg: ObjectAddMessage | RezObjectMessage | null = null;
let fromInventory = false;
if (inventoryID === undefined || this.agent.inventory.itemsByID[inventoryID.toString()] === undefined)
if (obj.extraParams.sculptData !== null)
{
// First, rez object in scene
msg = new ObjectAddMessage();
if (obj.extraParams.sculptData.type !== SculptType.Mesh)
{
buildMap.assetMap.textures[obj.extraParams.sculptData.texture.toString()] = obj.extraParams.sculptData.texture.toString();
}
}
if (obj.TextureEntry !== undefined)
{
for (const j of obj.TextureEntry.faces)
{
const textureID = j.textureID;
buildMap.assetMap.textures[textureID.toString()] = textureID.toString();
}
if (obj.TextureEntry.defaultTexture !== null)
{
const textureID = obj.TextureEntry.defaultTexture.textureID;
buildMap.assetMap.textures[textureID.toString()] = textureID.toString();
}
}
if (obj.inventory !== undefined)
{
for (const j of obj.inventory)
{
switch (j.inventoryType)
{
case InventoryType.Animation:
{
buildMap.assetMap.animations[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Bodypart:
{
buildMap.assetMap.bodyparts[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.CallingCard:
{
buildMap.assetMap.callingcards[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Clothing:
{
buildMap.assetMap.clothing[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Gesture:
{
buildMap.assetMap.gestures[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Landmark:
{
buildMap.assetMap.landmarks[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.LSL:
{
buildMap.assetMap.scripts[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Snapshot:
{
buildMap.assetMap.textures[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Notecard:
{
buildMap.assetMap.notecards[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Sound:
{
buildMap.assetMap.sounds[j.assetID.toString()] = j.assetID.toString();
break;
}
case InventoryType.Object:
{
buildMap.assetMap.objects[j.assetID.toString()] = null;
}
}
}
}
}
if (obj.children)
{
for (const child of obj.children)
{
this.gatherAssets(child, buildMap);
}
}
}
async buildObjectNew(obj: GameObject, callback: (map: AssetMap) => void, costOnly: boolean = false): Promise<GameObject | null>
{
const map: AssetMap = new AssetMap();
const buildMap = new BuildMap(map, callback, costOnly);
this.gatherAssets(obj, buildMap);
await callback(map);
if (costOnly)
{
return null;
}
let agentPos = new Vector3([128, 128, 2048]);
try
{
const agentLocalID = this.currentRegion.agent.localID;
const agentObject = this.currentRegion.objects.getObjectByLocalID(agentLocalID);
if (agentObject.Position !== undefined)
{
agentPos = new Vector3(agentObject.Position);
}
else
{
throw new Error('Agent position is undefined');
}
}
catch (error)
{
console.warn('Unable to find avatar location, rezzing at ' + agentPos.toString());
}
agentPos.z += 2.0;
buildMap.rezLocation = agentPos;
// Set camera above target location for fast acquisition
const campos = new Vector3(agentPos);
campos.z += 2.0;
await this.currentRegion.clientCommands.agent.setCamera(campos, agentPos, 10, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
if (buildMap.primsNeeded > 0)
{
buildMap.primReservoir = await this.createPrims(buildMap.primsNeeded, agentPos);
}
const parts = [];
parts.push(this.buildPart(obj, Vector3.getZero(), Quaternion.getIdentity(), buildMap));
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)
{
if (child.Position !== undefined && child.Rotation !== undefined)
{
const objPos = new Vector3(obj.Position);
const objRot = new Quaternion(obj.Rotation);
parts.push(this.buildPart(child, objPos, objRot, buildMap));
}
}
}
const results: GameObject[] = await Promise.all(parts);
const rootObj = results[0];
const childPrims: GameObject[] = [];
for (const childObject of results)
{
if (childObject !== rootObj)
{
childPrims.push(childObject);
}
}
await rootObj.linkFrom(childPrims);
return rootObj;
}
private createPrims(count: number, position: Vector3)
{
return new Promise<GameObject[]>((resolve, reject) =>
{
const gatheredPrims: GameObject[] = [];
let objSub: Subscription | undefined = undefined;
let timeout: Timeout | undefined = setTimeout(() =>
{
if (objSub !== undefined)
{
objSub.unsubscribe();
objSub = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
reject(new Error('Failed to gather ' + count + ' prims - only gathered ' + gatheredPrims.length));
}, 30000);
objSub = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) =>
{
if (!evt.object.resolvedAt)
{
// We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory
await this.queueResolveObject(evt.object, true);
}
if (evt.createSelected && !evt.object.claimedForBuild)
{
if (evt.object.itemID === undefined || evt.object.itemID.equals(UUID.zero()))
{
if (
evt.object.PCode === PCode.Prim &&
evt.object.Material === 3 &&
evt.object.PathCurve === 16 &&
evt.object.ProfileCurve === 1 &&
evt.object.PathBegin === 0 &&
evt.object.PathEnd === 1 &&
evt.object.PathScaleX === 1 &&
evt.object.PathScaleY === 1 &&
evt.object.PathShearX === 0 &&
evt.object.PathShearY === 0 &&
evt.object.PathTwist === 0 &&
evt.object.PathTwistBegin === 0 &&
evt.object.PathRadiusOffset === 0 &&
evt.object.PathTaperX === 0 &&
evt.object.PathTaperY === 0 &&
evt.object.PathRevolutions === 1 &&
evt.object.PathSkew === 0 &&
evt.object.ProfileBegin === 0 &&
evt.object.ProfileHollow === 0
)
{
evt.object.claimedForBuild = true;
gatheredPrims.push(evt.object);
if (gatheredPrims.length === count)
{
if (objSub !== undefined)
{
objSub.unsubscribe();
objSub = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
resolve(gatheredPrims);
}
}
}
}
});
for (let x = 0; x < count; x++)
{
const msg = new ObjectAddMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID,
GroupID: UUID.zero()
};
msg.ObjectData = {
PCode: Utils.numberOrZero(obj.PCode),
Material: Utils.numberOrZero(obj.Material),
PCode: PCode.Prim,
Material: 3,
AddFlags: PrimFlags.CreateSelected,
PathCurve: Utils.numberOrZero(obj.PathCurve),
ProfileCurve: Utils.numberOrZero(obj.ProfileCurve),
PathBegin: Utils.packBeginCut(Utils.numberOrZero(obj.PathBegin)),
PathEnd: Utils.packEndCut(Utils.numberOrZero(obj.PathEnd)),
PathScaleX: Utils.packPathScale(Utils.numberOrZero(obj.PathScaleX)),
PathScaleY: Utils.packPathScale(Utils.numberOrZero(obj.PathScaleY)),
PathShearX: Utils.packPathShear(Utils.numberOrZero(obj.PathShearX)),
PathShearY: Utils.packPathShear(Utils.numberOrZero(obj.PathShearY)),
PathTwist: Utils.packPathTwist(Utils.numberOrZero(obj.PathTwist)),
PathTwistBegin: Utils.packPathTwist(Utils.numberOrZero(obj.PathTwistBegin)),
PathRadiusOffset: Utils.packPathTwist(Utils.numberOrZero(obj.PathRadiusOffset)),
PathTaperX: Utils.packPathTaper(Utils.numberOrZero(obj.PathTaperX)),
PathTaperY: Utils.packPathTaper(Utils.numberOrZero(obj.PathTaperY)),
PathRevolutions: Utils.packPathRevolutions(Utils.numberOrZero(obj.PathRevolutions)),
PathSkew: Utils.packPathTwist(Utils.numberOrZero(obj.PathSkew)),
ProfileBegin: Utils.packBeginCut(Utils.numberOrZero(obj.ProfileBegin)),
ProfileEnd: Utils.packEndCut(Utils.numberOrZero(obj.ProfileEnd)),
ProfileHollow: Utils.packProfileHollow(Utils.numberOrZero(obj.ProfileHollow)),
PathCurve: 16,
ProfileCurve: 1,
PathBegin: 0,
PathEnd: 0,
PathScaleX: 100,
PathScaleY: 100,
PathShearX: 0,
PathShearY: 0,
PathTwist: 0,
PathTwistBegin: 0,
PathRadiusOffset: 0,
PathTaperX: 0,
PathTaperY: 0,
PathRevolutions: 0,
PathSkew: 0,
ProfileBegin: 0,
ProfileEnd: 0,
ProfileHollow: 0,
BypassRaycast: 1,
RayStart: finalPos,
RayEnd: finalPos,
RayStart: position,
RayEnd: position,
RayTargetID: UUID.zero(),
RayEndIsIntersection: 0,
Scale: Utils.vector3OrZero(obj.Scale),
Rotation: finalRot,
State: Utils.numberOrZero(obj.State)
};
}
else
{
fromInventory = true;
const invItem = this.agent.inventory.itemsByID[inventoryID.toString()];
const queryID = UUID.random();
msg = new RezObjectMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID,
GroupID: UUID.zero()
};
msg.RezData = {
FromTaskID: UUID.zero(),
BypassRaycast: 1,
RayStart: finalPos,
RayEnd: finalPos,
RayTargetID: UUID.zero(),
RayEndIsIntersection: false,
RezSelected: true,
RemoveItem: false,
ItemFlags: invItem.flags,
GroupMask: PermissionMask.All,
EveryoneMask: PermissionMask.All,
NextOwnerMask: PermissionMask.All,
};
msg.InventoryData = {
ItemID: invItem.itemID,
FolderID: invItem.parentID,
CreatorID: invItem.permissions.creator,
OwnerID: invItem.permissions.owner,
GroupID: invItem.permissions.group,
BaseMask: invItem.permissions.baseMask,
OwnerMask: invItem.permissions.ownerMask,
GroupMask: invItem.permissions.groupMask,
EveryoneMask: invItem.permissions.everyoneMask,
NextOwnerMask: invItem.permissions.nextOwnerMask,
GroupOwned: false,
TransactionID: queryID,
Type: invItem.type,
InvType: invItem.inventoryType,
Flags: invItem.flags,
SaleType: invItem.saleType,
SalePrice: invItem.salePrice,
Name: Utils.StringToBuffer(invItem.name),
Description: Utils.StringToBuffer(invItem.description),
CreationDate: Math.round(invItem.created.getTime() / 1000),
CRC: 0,
Scale: new Vector3([0.5, 0.5, 0.5]),
Rotation: Quaternion.getIdentity(),
State: 0
};
this.circuit.sendMessage(msg, PacketFlags.Reliable);
}
});
}
rezFromInventory(obj: GameObject, position: Vector3, inventoryID: UUID): Promise<GameObject>
{
return new Promise(async (resolve, reject) =>
{
const invItem = this.agent.inventory.itemsByID[inventoryID.toString()];
const queryID = UUID.random();
const msg = new RezObjectMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID,
GroupID: UUID.zero()
};
msg.RezData = {
FromTaskID: UUID.zero(),
BypassRaycast: 1,
RayStart: position,
RayEnd: position,
RayTargetID: UUID.zero(),
RayEndIsIntersection: false,
RezSelected: true,
RemoveItem: false,
ItemFlags: invItem.flags,
GroupMask: PermissionMask.All,
EveryoneMask: PermissionMask.All,
NextOwnerMask: PermissionMask.All,
};
msg.InventoryData = {
ItemID: invItem.itemID,
FolderID: invItem.parentID,
CreatorID: invItem.permissions.creator,
OwnerID: invItem.permissions.owner,
GroupID: invItem.permissions.group,
BaseMask: invItem.permissions.baseMask,
OwnerMask: invItem.permissions.ownerMask,
GroupMask: invItem.permissions.groupMask,
EveryoneMask: invItem.permissions.everyoneMask,
NextOwnerMask: invItem.permissions.nextOwnerMask,
GroupOwned: false,
TransactionID: queryID,
Type: invItem.type,
InvType: invItem.inventoryType,
Flags: invItem.flags,
SaleType: invItem.saleType,
SalePrice: invItem.salePrice,
Name: Utils.StringToBuffer(invItem.name),
Description: Utils.StringToBuffer(invItem.description),
CreationDate: Math.round(invItem.created.getTime() / 1000),
CRC: 0,
};
let objSub: Subscription | undefined = undefined;
let timeout: Timeout | undefined = setTimeout(() =>
{
@@ -1132,11 +1603,17 @@ export class RegionCommands extends CommandsBase
}
reject(new Error('Prim never arrived'));
}, 10000);
let claimedPrim = false;
objSub = this.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) =>
{
if (evt.createSelected && !evt.object.claimedForBuild)
if (evt.createSelected && !evt.object.resolvedAt)
{
if (!fromInventory || (inventoryID !== undefined && evt.object.itemID.equals(inventoryID)))
// We need to get the full ObjectProperties so we can be sure this is or isn't a rez from inventory
await this.queueResolveObject(evt.object, true);
}
if (evt.createSelected && !evt.object.claimedForBuild && !claimedPrim)
{
if (inventoryID !== undefined && evt.object.itemID !== undefined && evt.object.itemID.equals(inventoryID))
{
if (objSub !== undefined)
{
@@ -1149,36 +1626,20 @@ export class RegionCommands extends CommandsBase
timeout = undefined;
}
evt.object.claimedForBuild = true;
if (!fromInventory)
{
await evt.object.setShape(obj.PathCurve, obj.ProfileCurve, obj.PathBegin, obj.PathEnd, obj.PathScaleX, obj.PathScaleY, obj.PathShearX, obj.PathShearY, obj.PathTwist, obj.PathTwistBegin, obj.PathRadiusOffset, obj.PathTaperX, obj.PathTaperY, obj.PathRevolutions, obj.PathSkew, obj.ProfileBegin, obj.ProfileEnd, obj.ProfileHollow);
}
// Set the object's position properly
try
{
await evt.object.setGeometry(finalPos, finalRot, obj.Scale);
}
catch (error)
{
console.log('Failed to set object position :/');
}
claimedPrim = true;
resolve(evt.object);
}
}
});
// 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)
if (obj.Scale !== undefined)
{
this.circuit.sendMessage(msg, PacketFlags.Reliable);
const camLocation = new Vector3(position);
camLocation.z += (obj.Scale.z / 2) + 1;
await this.currentRegion.clientCommands.agent.setCamera(camLocation, position, obj.Scale.z, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
}
this.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
@@ -1357,7 +1818,6 @@ export class RegionCommands extends CommandsBase
{
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)
{

View File

@@ -25,14 +25,10 @@ export class MapBlockReplyMessage implements MessageBase
Agents: number;
MapImageID: UUID;
}[];
Size: {
SizeX: number;
SizeY: number;
}[];
getSize(): number
{
return this.calculateVarVarSize(this.Data, 'Name', 1) + ((27) * this.Data.length) + ((4) * this.Size.length) + 22;
return this.calculateVarVarSize(this.Data, 'Name', 1) + ((27) * this.Data.length) + 21;
}
calculateVarVarSize(block: object[], paramName: string, extraPerVar: number): number
@@ -52,7 +48,7 @@ export class MapBlockReplyMessage implements MessageBase
pos += 16;
buf.writeUInt32LE(this.AgentData['Flags'], pos);
pos += 4;
let count = this.Data.length;
const count = this.Data.length;
buf.writeUInt8(this.Data.length, pos++);
for (let i = 0; i < count; i++)
{
@@ -71,15 +67,6 @@ export class MapBlockReplyMessage implements MessageBase
this.Data[i]['MapImageID'].writeToBuffer(buf, pos);
pos += 16;
}
count = this.Size.length;
buf.writeUInt8(this.Size.length, pos++);
for (let i = 0; i < count; i++)
{
buf.writeUInt16LE(this.Size[i]['SizeX'], pos);
pos += 2;
buf.writeUInt16LE(this.Size[i]['SizeY'], pos);
pos += 2;
}
return pos - startPos;
}
@@ -103,7 +90,7 @@ export class MapBlockReplyMessage implements MessageBase
{
return pos - startPos;
}
let count = buf.readUInt8(pos++);
const count = buf.readUInt8(pos++);
this.Data = [];
for (let i = 0; i < count; i++)
{
@@ -142,27 +129,6 @@ export class MapBlockReplyMessage implements MessageBase
pos += 16;
this.Data.push(newObjData);
}
if (pos >= buf.length)
{
return pos - startPos;
}
count = buf.readUInt8(pos++);
this.Size = [];
for (let i = 0; i < count; i++)
{
const newObjSize: {
SizeX: number,
SizeY: number
} = {
SizeX: 0,
SizeY: 0
};
newObjSize['SizeX'] = buf.readUInt16LE(pos);
pos += 2;
newObjSize['SizeY'] = buf.readUInt16LE(pos);
pos += 2;
this.Size.push(newObjSize);
}
return pos - startPos;
}
}

View File

@@ -38,6 +38,15 @@ import { HTTPAssets } from '../../enums/HTTPAssets';
import { PhysicsShapeType } from '../../enums/PhysicsShapeType';
import { PCode } from '../../enums/PCode';
import { SoundFlags } from '../../enums/SoundFlags';
import { DeRezObjectMessage } from '../messages/DeRezObject';
import { DeRezDestination } from '../../enums/DeRezDestination';
import { Message } from '../../enums/Message';
import { UpdateCreateInventoryItemMessage } from '../messages/UpdateCreateInventoryItem';
import { FilterResponse } from '../../enums/FilterResponse';
import { UpdateTaskInventoryMessage } from '../messages/UpdateTaskInventory';
import { ObjectPropertiesMessage } from '../messages/ObjectProperties';
import { ObjectSelectMessage } from '../messages/ObjectSelect';
import { ObjectDeselectMessage } from '../messages/ObjectDeselect';
export class GameObject implements IGameObjectData
{
@@ -149,6 +158,7 @@ export class GameObject implements IGameObjectData
resolveAttempts = 0;
claimedForBuild = false;
createdSelected = false;
private static getFromXMLJS(obj: any, param: string): any
{
@@ -569,7 +579,98 @@ export class GameObject implements IGameObjectData
go.extraParams = ExtraParams.from(buf);
}
}
// TODO: TaskInventory
if ((prop = this.getFromXMLJS(obj, 'TaskInventory')) !== undefined)
{
if (prop.TaskInventoryItem)
{
for (const invItemXML of prop.TaskInventoryItem)
{
const invItem = new InventoryItem();
let subProp: any;
if ((subProp = UUID.fromXMLJS(invItemXML, 'AssetID')) !== undefined)
{
invItem.assetID = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'BasePermissions')) !== undefined)
{
invItem.permissions.baseMask = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'EveryonePermissions')) !== undefined)
{
invItem.permissions.everyoneMask = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'GroupPermissions')) !== undefined)
{
invItem.permissions.groupMask = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'NextPermissions')) !== undefined)
{
invItem.permissions.nextOwnerMask = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'CurrentPermissions')) !== undefined)
{
invItem.permissions.ownerMask = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'CreationDate')) !== undefined)
{
invItem.created = new Date(parseInt(subProp, 10) * 1000);
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'CreatorID')) !== undefined)
{
invItem.permissions.creator = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'Description')) !== undefined)
{
invItem.description = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'Flags')) !== undefined)
{
invItem.flags = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'GroupID')) !== undefined)
{
invItem.permissions.group = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'InvType')) !== undefined)
{
invItem.inventoryType = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'ItemID')) !== undefined)
{
invItem.itemID = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'OldItemID')) !== undefined)
{
invItem.oldItemID = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'LastOwnerID')) !== undefined)
{
invItem.permissions.lastOwner = subProp;
}
if ((subProp = this.getFromXMLJS(invItemXML, 'Name')) !== undefined)
{
invItem.name = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'OwnerID')) !== undefined)
{
invItem.permissions.owner = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'ParentID')) !== undefined)
{
invItem.parentID = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'ParentPartID')) !== undefined)
{
invItem.parentPartID = subProp;
}
if ((subProp = UUID.fromXMLJS(invItemXML, 'PermsGranter')) !== undefined)
{
invItem.permsGranter = subProp;
}
go.inventory.push(invItem);
}
}
}
return go;
}
@@ -590,11 +691,22 @@ export class GameObject implements IGameObjectData
throw new Error('SceneObjectGroup not found');
}
result = result['SceneObjectGroup'];
if (!result['SceneObjectPart'])
let rootPartXML;
if (result['SceneObjectPart'])
{
rootPartXML = result['SceneObjectPart'];
}
else if (result['RootPart'] && result['RootPart'][0] && result['RootPart'][0]['SceneObjectPart'])
{
rootPartXML = result['RootPart'][0]['SceneObjectPart'];
}
else
{
throw new Error('Root part not found');
}
const rootPart = GameObject.partFromXMLJS(result['SceneObjectPart'][0], true);
const rootPart = GameObject.partFromXMLJS(rootPartXML[0], true);
rootPart.children = [];
rootPart.totalChildren = 0;
if (result['OtherParts'] && Array.isArray(result['OtherParts']) && result['OtherParts'].length > 0)
@@ -749,49 +861,6 @@ export class GameObject implements IGameObjectData
await this.region.circuit.waitForAck(this.region.circuit.sendMessage(msg, PacketFlags.Reliable), 10000);
}
private compareParam(name: string, param1: number | undefined, param2: number | undefined): boolean
{
if (param1 === undefined)
{
param1 = 0;
}
if (param2 === undefined)
{
param2 = 0;
}
if (Math.abs(param1 - param2) < 0.0001)
{
return true;
}
else
{
console.log('Failed ' + name + ' - ' + param1 + ' vs ' + param2);
return false;
}
}
compareShape(obj: GameObject): boolean
{
return this.compareParam('PathCurve', this.PathCurve, obj.PathCurve) &&
this.compareParam('ProfileCurve', this.ProfileCurve, obj.ProfileCurve) &&
this.compareParam('PathBegin', this.PathBegin, obj.PathBegin) &&
this.compareParam('PathEnd', this.PathEnd, obj.PathEnd) &&
this.compareParam('PathScaleX', this.PathScaleX, obj.PathScaleX) &&
this.compareParam('PathScaleY', this.PathScaleY, obj.PathScaleY) &&
this.compareParam('PathShearX', this.PathShearX, obj.PathShearX) &&
this.compareParam('PathShearY', this.PathShearY, obj.PathShearY) &&
this.compareParam('PathTwist', this.PathTwist, obj.PathTwist) &&
this.compareParam('PathTwistBegin', this.PathTwistBegin, obj.PathTwistBegin) &&
this.compareParam('PathRadiusOffset', this.PathRadiusOffset, obj.PathRadiusOffset) &&
this.compareParam('PathTaperX', this.PathTaperX, obj.PathTaperX) &&
this.compareParam('PathTaperY', this.PathTaperY, obj.PathTaperY) &&
this.compareParam('PathRevolutions', this.PathRevolutions, obj.PathRevolutions) &&
this.compareParam('PathSkew', this.PathSkew, obj.PathSkew) &&
this.compareParam('ProfileBegin', this.ProfileBegin, obj.ProfileBegin) &&
this.compareParam('ProfileEnd', this.ProfileEnd, obj.ProfileEnd) &&
this.compareParam('PRofileHollow', this.ProfileHollow, obj.ProfileHollow);
}
async setGeometry(pos?: Vector3, rot?: Quaternion, scale?: Vector3)
{
const data = [];
@@ -1455,4 +1524,162 @@ export class GameObject implements IGameObjectData
break;
}
}
private async deRezObject(destination: DeRezDestination, transactionID: UUID, destFolder: UUID): Promise<void>
{
const msg = new DeRezObjectMessage();
msg.AgentData = {
AgentID: this.region.agent.agentID,
SessionID: this.region.circuit.sessionID
};
msg.AgentBlock = {
GroupID: UUID.zero(),
Destination: destination,
DestinationID: destFolder,
TransactionID: transactionID,
PacketCount: 1,
PacketNumber: 1
};
msg.ObjectData = [{
ObjectLocalID: this.ID
}];
const ack = this.region.circuit.sendMessage(msg, PacketFlags.Reliable);
return this.region.circuit.waitForAck(ack, 10000);
}
takeToInventory(): Promise<UUID>
{
const transactionID = UUID.random();
const rootFolder = this.region.agent.inventory.getRootFolderMain();
return new Promise<UUID>((resolve, reject) =>
{
this.region.circuit.waitForMessage<UpdateCreateInventoryItemMessage>(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) =>
{
if (Utils.BufferToStringSimple(message.InventoryData[0].Name, 0) === this.name)
{
return FilterResponse.Finish;
}
else
{
return FilterResponse.NoMatch;
}
}).then((createInventoryMsg: UpdateCreateInventoryItemMessage) =>
{
resolve(createInventoryMsg.InventoryData[0].ItemID);
}).catch(() =>
{
reject(new Error('Timed out waiting for UpdateCreateInventoryItem'));
});
this.deRezObject(DeRezDestination.AgentInventoryTake, transactionID, rootFolder.folderID).then(() => {}).catch((err) =>
{
console.error(err);
});
});
}
dropInventoryIntoContents(inventoryID: UUID): Promise<void>
{
return new Promise<void>(async (resolve, reject) =>
{
const transactionID = UUID.random();
const item: InventoryItem | null = await this.region.agent.inventory.fetchInventoryItem(inventoryID);
if (item === null)
{
reject(new Error('Failed to drop inventory into object contents - Inventory item ' + inventoryID.toString() + ' not found'));
return;
}
const msg = new UpdateTaskInventoryMessage();
msg.AgentData = {
AgentID: this.region.agent.agentID,
SessionID: this.region.circuit.sessionID
};
msg.UpdateData = {
Key: 0,
LocalID: this.ID
};
msg.InventoryData = {
ItemID: item.itemID,
FolderID: item.parentID,
CreatorID: item.permissions.creator,
OwnerID: item.permissions.owner,
GroupID: item.permissions.group,
BaseMask: item.permissions.baseMask,
OwnerMask: item.permissions.ownerMask,
GroupMask: item.permissions.groupMask,
EveryoneMask: item.permissions.everyoneMask,
NextOwnerMask: item.permissions.nextOwnerMask,
GroupOwned: item.permissions.groupOwned || false,
TransactionID: transactionID,
Type: item.type,
InvType: item.inventoryType,
Flags: item.flags,
SaleType: item.saleType,
SalePrice: item.salePrice,
Name: Utils.StringToBuffer(item.name),
Description: Utils.StringToBuffer(item.description),
CreationDate: item.created.getTime() / 1000,
CRC: item.getCRC()
};
const inventorySerial = this.inventorySerial;
this.region.circuit.waitForMessage<ObjectPropertiesMessage>(Message.ObjectProperties, 10000, (message: ObjectPropertiesMessage) =>
{
const n = 5;
for (const obj of message.ObjectData)
{
if (obj.ObjectID.equals(this.FullID))
{
if (obj.InventorySerial > inventorySerial)
{
return FilterResponse.Finish;
}
}
}
return FilterResponse.NoMatch;
}).then((message: ObjectPropertiesMessage) =>
{
this.deselect().then(() => {}).catch(() => {});
resolve();
}).catch(() =>
{
reject(new Error('Timed out waiting for task inventory drop'));
});
// We need to select the object or we won't get the objectProperties message
await this.select();
this.region.circuit.sendMessage(msg, PacketFlags.Reliable)
});
}
async select()
{
const selectObject = new ObjectSelectMessage();
selectObject.AgentData = {
AgentID: this.region.agent.agentID,
SessionID: this.region.circuit.sessionID
};
selectObject.ObjectData = [{
ObjectLocalID: this.ID
}];
const ack = this.region.circuit.sendMessage(selectObject, PacketFlags.Reliable);
await this.region.circuit.waitForAck(ack, 10000);
}
async deselect()
{
const deselectObject = new ObjectDeselectMessage();
deselectObject.AgentData = {
AgentID: this.region.agent.agentID,
SessionID: this.region.circuit.sessionID
};
deselectObject.ObjectData = [{
ObjectLocalID: this.ID
}];
const ack = this.region.circuit.sendMessage(deselectObject, PacketFlags.Reliable);
await this.region.circuit.waitForAck(ack, 10000);
}
}

View File

@@ -0,0 +1,14 @@
export enum DeRezDestination
{
AgentInventorySave = 0,
AgentInventoryCopy = 1,
TaskInventory = 2,
Attachment = 3,
AgentInventoryTake = 4,
ForceToGodInventory = 5,
TrashFolder = 6,
AttachmentToInventory = 7,
AttachmentExists = 8,
ReturnToOwner = 9,
ReturnToLastOwner = 10
}

View File

@@ -0,0 +1,6 @@
import { GameObject } from '../classes/public/GameObject';
export class ObjectResolvedEvent
{
object: GameObject
}

View File

@@ -94,6 +94,7 @@ import { ParticleSystem } from './classes/ParticleSystem';
import { ExtraParams } from './classes/public/ExtraParams';
import { LLMesh } from './classes/public/LLMesh';
import { FolderType } from './enums/FolderType';
import { InventoryItem } from './classes/InventoryItem';
export {
Bot,
@@ -193,6 +194,7 @@ export {
SculptData,
MeshData,
LLMesh,
InventoryItem,
// Public Interfaces
GlobalPosition,

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@caspertech/node-metaverse",
"version": "0.5.12",
"version": "0.5.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@caspertech/node-metaverse",
"version": "0.5.12",
"version": "0.5.13",
"description": "A node.js interface for Second Life.",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -25039,23 +25039,6 @@
"size": 1
}
]
},
{
"name": "Size",
"type": "Variable",
"count": 1,
"params": [
{
"name": "SizeX",
"type": "U16",
"size": 1
},
{
"name": "SizeY",
"type": "U16",
"size": 1
}
]
}
]
},

View File

@@ -8766,11 +8766,6 @@ version 2.0
{ Agents U8 }
{ MapImageID LLUUID }
}
{
Size Variable
{ SizeX U16 }
{ SizeY U16 }
}
}
// viewer -> sim