Extensive work on building, wearables, assets, inventory, attachments, serialization, etc.

Resolves #36
This commit is contained in:
Casper Warden
2020-11-19 16:51:14 +00:00
parent 7b41239a39
commit 2ff00a30f8
58 changed files with 6659 additions and 2228 deletions

View File

@@ -10,10 +10,8 @@ import { AgentUpdateMessage } from './messages/AgentUpdate';
import { Quaternion } from './Quaternion';
import { AgentState } from '../enums/AgentState';
import { BuiltInAnimations } from '../enums/BuiltInAnimations';
import * as LLSD from '@caspertech/llsd';
import { AgentWearablesRequestMessage } from './messages/AgentWearablesRequest';
import { AgentWearablesUpdateMessage } from './messages/AgentWearablesUpdate';
import { InventorySortOrder } from '../enums/InventorySortOrder';
import { RezSingleAttachmentFromInvMessage } from './messages/RezSingleAttachmentFromInv';
import { AttachmentPoint } from '../enums/AttachmentPoint';
import { Utils } from './Utils';
@@ -27,6 +25,11 @@ import { ControlFlags } from '../enums/ControlFlags';
import { PacketFlags } from '../enums/PacketFlags';
import { FolderType } from '../enums/FolderType';
import { Subject, Subscription } from 'rxjs';
import { InventoryFolder } from './InventoryFolder';
import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent';
import { BulkUpdateInventoryMessage } from './messages/BulkUpdateInventory';
import { InventoryItem } from './InventoryItem';
import { AgentDataUpdateMessage } from './messages/AgentDataUpdate';
export class Agent
{
@@ -34,6 +37,7 @@ export class Agent
lastName: string;
localID = 0;
agentID: UUID;
activeGroupID: UUID = UUID.zero();
accessMax: string;
regionAccess: string;
agentAccess: string;
@@ -85,10 +89,12 @@ export class Agent
};
agentUpdateTimer: Timer | null = null;
estateManager = false;
appearanceSet = false;
appearanceSetEvent: Subject<void> = new Subject<void>();
appearanceComplete = false;
appearanceCompleteEvent: Subject<void> = new Subject<void>();
private clientEvents: ClientEvents;
private animSubscription?: Subscription;
constructor(clientEvents: ClientEvents)
{
@@ -153,10 +159,16 @@ export class Agent
setCurrentRegion(region: Region)
{
if (this.animSubscription !== undefined)
{
this.animSubscription.unsubscribe();
}
this.currentRegion = region;
this.currentRegion.circuit.subscribeToMessages([
Message.AvatarAnimation
], this.onAnimState.bind(this));
this.animSubscription = this.currentRegion.circuit.subscribeToMessages([
Message.AvatarAnimation,
Message.AgentDataUpdate,
Message.BulkUpdateInventory
], this.onMessage.bind(this));
}
circuitActive()
{
@@ -194,9 +206,57 @@ export class Agent
this.agentUpdateTimer = null;
}
}
onAnimState(packet: Packet)
onMessage(packet: Packet)
{
if (packet.message.id === Message.AvatarAnimation)
if (packet.message.id === Message.AgentDataUpdate)
{
const msg = packet.message as AgentDataUpdateMessage;
this.activeGroupID = msg.AgentData.ActiveGroupID;
}
else if (packet.message.id === Message.BulkUpdateInventory)
{
const msg = packet.message as BulkUpdateInventoryMessage;
const evt = new BulkUpdateInventoryEvent();
for (const newItem of msg.ItemData)
{
const folder = this.inventory.findFolder(newItem.FolderID);
const item = new InventoryItem(folder || undefined, this);
item.assetID = newItem.AssetID;
item.inventoryType = newItem.InvType;
item.name = Utils.BufferToStringSimple(newItem.Name);
item.salePrice = newItem.SalePrice;
item.saleType = newItem.SaleType;
item.created = new Date(newItem.CreationDate * 1000);
item.parentID = newItem.FolderID;
item.flags = newItem.Flags;
item.itemID = newItem.ItemID;
item.description = Utils.BufferToStringSimple(newItem.Description);
item.type = newItem.Type;
item.callbackID = newItem.CallbackID;
item.permissions.baseMask = newItem.BaseMask;
item.permissions.groupMask = newItem.GroupMask;
item.permissions.nextOwnerMask = newItem.NextOwnerMask;
item.permissions.ownerMask = newItem.OwnerMask;
item.permissions.everyoneMask = newItem.EveryoneMask;
item.permissions.owner = newItem.OwnerID;
item.permissions.creator = newItem.CreatorID;
item.permissions.group = newItem.GroupID;
item.permissions.groupOwned = newItem.GroupOwned;
evt.itemData.push(item);
}
for (const newFolder of msg.FolderData)
{
const fld = new InventoryFolder(this.inventory.main, this);
fld.typeDefault = newFolder.Type;
fld.name = Utils.BufferToStringSimple(newFolder.Name);
fld.folderID = newFolder.FolderID;
fld.parentID = newFolder.ParentID;
evt.folderData.push(fld);
}
this.clientEvents.onBulkUpdateInventoryEvent.next(evt);
}
else if (packet.message.id === Message.AvatarAnimation)
{
const animMsg = packet.message as AvatarAnimationMessage;
if (animMsg.Sender.ID.toString() === this.agentID.toString())
@@ -220,7 +280,22 @@ export class Agent
}
}
}
setInitialAppearance()
async getWearables(): Promise<InventoryFolder>
{
for (const uuid of Object.keys(this.inventory.main.skeleton))
{
const folder = this.inventory.main.skeleton[uuid];
if (folder.typeDefault === FolderType.CurrentOutfit)
{
await folder.populate(false);
return folder;
}
}
throw new Error('Unable to get wearables from inventory')
}
async setInitialAppearance()
{
const circuit = this.currentRegion.circuit;
const wearablesRequest: AgentWearablesRequestMessage = new AgentWearablesRequestMessage();
@@ -229,95 +304,70 @@ export class Agent
SessionID: circuit.sessionID
};
circuit.sendMessage(wearablesRequest, PacketFlags.Reliable);
circuit.waitForMessage<AgentWearablesUpdateMessage>(Message.AgentWearablesUpdate, 10000).then((wearables: AgentWearablesUpdateMessage) =>
const wearables: AgentWearablesUpdateMessage = await circuit.waitForMessage<AgentWearablesUpdateMessage>(Message.AgentWearablesUpdate, 10000);
if (!this.wearables || wearables.AgentData.SerialNum > this.wearables.serialNumber)
{
if (!this.wearables || wearables.AgentData.SerialNum > this.wearables.serialNumber)
this.wearables = {
serialNumber: wearables.AgentData.SerialNum,
attachments: []
};
for (const wearable of wearables.WearableData)
{
this.wearables = {
serialNumber: wearables.AgentData.SerialNum,
attachments: []
};
for (const wearable of wearables.WearableData)
if (this.wearables && this.wearables.attachments)
{
if (this.wearables && this.wearables.attachments)
this.wearables.attachments.push({
itemID: wearable.ItemID,
assetID: wearable.AssetID,
wearableType: wearable.WearableType
});
}
}
}
const currentOutfitFolder = await this.getWearables();
const wornObjects = this.currentRegion.objects.getObjectsByParent(this.localID);
for (const item of currentOutfitFolder.items)
{
if (item.type === 6)
{
let found = false;
for (const obj of wornObjects)
{
if (obj.hasNameValueEntry('AttachItemID'))
{
this.wearables.attachments.push({
itemID: wearable.ItemID,
assetID: wearable.AssetID,
wearableType: wearable.WearableType
});
if (item.itemID.toString() === obj.getNameValueEntry('AttachItemID'))
{
found = true;
}
}
}
}
for (const uuid of Object.keys(this.inventory.main.skeleton))
{
const folder = this.inventory.main.skeleton[uuid];
if (folder.typeDefault === FolderType.CurrentOutfit)
if (!found)
{
const folderID = folder.folderID;
const requestFolder = {
folder_id: new LLSD.UUID(folderID),
owner_id: new LLSD.UUID(this.agentID),
fetch_folders: true,
fetch_items: true,
sort_order: InventorySortOrder.ByName
const rsafi = new RezSingleAttachmentFromInvMessage();
rsafi.AgentData = {
AgentID: this.agentID,
SessionID: circuit.sessionID
};
const requestedFolders = {
'folders': [
requestFolder
]
rsafi.ObjectData = {
ItemID: new UUID(item.itemID.toString()),
OwnerID: this.agentID,
AttachmentPt: 0x80 | AttachmentPoint.Default,
ItemFlags: item.flags,
GroupMask: item.permissions.groupMask,
EveryoneMask: item.permissions.everyoneMask,
NextOwnerMask: item.permissions.nextOwnerMask,
Name: Utils.StringToBuffer(item.name),
Description: Utils.StringToBuffer(item.description)
};
this.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) =>
{
const currentOutfitFolderContents = folderContents['folders'][0]['items'];
const wornObjects = this.currentRegion.objects.getObjectsByParent(this.localID);
for (const item of currentOutfitFolderContents)
{
if (item.type === 6)
{
let found = false;
for (const obj of wornObjects)
{
if (obj.hasNameValueEntry('AttachItemID'))
{
if (item['item_id'].toString() === obj.getNameValueEntry('AttachItemID'))
{
found = true;
}
}
}
if (!found)
{
const rsafi = new RezSingleAttachmentFromInvMessage();
rsafi.AgentData = {
AgentID: this.agentID,
SessionID: circuit.sessionID
};
rsafi.ObjectData = {
ItemID: new UUID(item['item_id'].toString()),
OwnerID: this.agentID,
AttachmentPt: 0x80 | AttachmentPoint.Default,
ItemFlags: item['flags'],
GroupMask: item['permissions']['group_mask'],
EveryoneMask: item['permissions']['everyone_mask'],
NextOwnerMask: item['permissions']['next_owner_mask'],
Name: Utils.StringToBuffer(item['name']),
Description: Utils.StringToBuffer(item['desc'])
};
circuit.sendMessage(rsafi, PacketFlags.Reliable);
}
}
}
});
circuit.sendMessage(rsafi, PacketFlags.Reliable);
}
}
this.appearanceSet = true;
this.appearanceSetEvent.next();
});
}
this.appearanceComplete = true;
this.appearanceCompleteEvent.next();
}
}

View File

@@ -1,21 +1,89 @@
import { InventoryItem } from './InventoryItem';
import { Material } from './public/Material';
export class AssetMap
{
mesh: {
[key: string]: {
objectName: string,
objectDescription: string,
assetID: string
name: string,
description: string,
item: InventoryItem | null
}
} = {};
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 } = {};
textures: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
materials: {
[key: string]: Material | null
} = {};
animations: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
sounds: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
gestures: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
callingcards: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
scripts: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
clothing: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
notecards: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
bodyparts: {
[key: string]: {
name?: string,
description?: string,
item: InventoryItem | null
}
} = {};
objects: {
[key: string]: InventoryItem | null
} = {};
temporaryInventory: {
[key: string]: InventoryItem
} = {};
byUUID: {
[key: string]: InventoryItem
} = {};
pending: {[key: string]: boolean} = {};
}

View File

@@ -153,6 +153,10 @@ export class Caps
{
return new Promise<Buffer>((resolve, reject) =>
{
if (type === HTTPAssets.ASSET_LSL_TEXT || type === HTTPAssets.ASSET_NOTECARD)
{
throw new Error('Invalid Syntax');
}
this.getCapability('ViewerAsset').then((capURL) =>
{
const assetURL = capURL + '/?' + type + '_id=' + uuid.toString();
@@ -206,6 +210,33 @@ export class Caps
});
}
requestPut(capURL: string, data: string | Buffer, contentType: string): Promise<ICapResponse>
{
return new Promise<ICapResponse>((resolve, reject) =>
{
request({
'headers': {
'Content-Length': data.length,
'Content-Type': contentType
},
'uri': capURL,
'body': data,
'rejectUnauthorized': false,
'method': 'PUT'
}, (err, res, body) =>
{
if (err)
{
reject(err);
}
else
{
resolve({status: res.statusCode, body: body});
}
});
});
}
requestGet(requestURL: string): Promise<ICapResponse>
{
return new Promise<ICapResponse>((resolve, reject) =>
@@ -247,6 +278,12 @@ export class Caps
});
}
async isCapAvailable(capability: string): Promise<boolean>
{
await this.waitForSeedCapability();
return (this.capabilities[capability] !== undefined);
}
getCapability(capability: string): Promise<string>
{
return new Promise<string>((resolve, reject) =>
@@ -342,7 +379,47 @@ export class Caps
return new Promise<any>(async (resolve, reject) =>
{
const xml = LLSD.LLSD.formatXML(data);
this.request(capURL, xml, 'application/llsd+xml').then((resp: ICapResponse) =>
this.request(capURL, xml, 'application/llsd+xml').then(async (resp: ICapResponse) =>
{
let result: any = null;
try
{
result = LLSD.LLSD.parseXML(resp.body);
resolve(result);
}
catch (err)
{
if (resp.status === 201)
{
resolve({});
}
else if (resp.status === 403)
{
reject(new Error('Access Denied'));
}
else if (resp.status === 404)
{
reject(new Error('Not found'));
}
else
{
reject(resp.body);
}
}
}).catch((err) =>
{
console.error(err);
reject(err);
});
});
}
capsPerformXMLPut(capURL: string, data: any): Promise<any>
{
return new Promise<any>(async (resolve, reject) =>
{
const xml = LLSD.LLSD.formatXML(data);
this.requestPut(capURL, xml, 'application/llsd+xml').then((resp: ICapResponse) =>
{
let result: any = null;
try
@@ -484,6 +561,44 @@ export class Caps
}
}
async capsPutXML(capability: string | [string, {[key: string]: string}], data: any): Promise<any>
{
let capName = '';
let queryParams: {[key: string]: string} = {};
if (typeof capability === 'string')
{
capName = capability;
}
else
{
capName = capability[0];
queryParams = capability[1];
}
await this.waitForCapTimeout(capName);
let capURL = await this.getCapability(capName);
if (Object.keys(queryParams).length > 0)
{
const parsedURL = url.parse(capURL, true);
for (const key of Object.keys(queryParams))
{
parsedURL.query[key] = queryParams[key];
}
capURL = url.format(parsedURL);
}
try
{
return await this.capsPerformXMLPut(capURL, data);
}
catch (error)
{
console.log('Error with cap ' + capName);
console.log(error);
throw error;
}
}
shutdown()
{
this.onGotSeedCap.complete();

View File

@@ -21,6 +21,8 @@ import Timer = NodeJS.Timer;
import { PacketFlags } from '../enums/PacketFlags';
import { AssetType } from '../enums/AssetType';
import { Utils } from './Utils';
import * as Long from 'long';
import { AssetUploadCompleteMessage } from './messages/AssetUploadComplete';
export class Circuit
{
@@ -85,7 +87,99 @@ export class Circuit
return packet.sequenceNumber;
}
XferFile(fileName: string, deleteOnCompletion: boolean, useBigPackets: boolean, vFileID: UUID, vFileType: AssetType, fromCache: boolean): Promise<Buffer>
private sendXferPacket(xferID: Long, packetID: number, data: Buffer, pos: {position: number})
{
const sendXfer = new SendXferPacketMessage();
let final = false;
sendXfer.XferID = {
ID: xferID,
Packet: packetID
};
const packetLength = Math.min(data.length - pos.position, 1000);
if (packetLength < 1000)
{
sendXfer.XferID.Packet = (sendXfer.XferID.Packet | 0x80000000) >>> 0;
final = true;
}
if (packetID === 0)
{
const packet = Buffer.allocUnsafe(packetLength + 4);
packet.writeUInt32LE(data.length, 0);
data.copy(packet, 4, 0, packetLength);
sendXfer.DataPacket = {
Data: packet
};
pos.position += packetLength;
}
else
{
const packet = data.slice(pos.position, pos.position + packetLength);
sendXfer.DataPacket = {
Data: packet
};
pos.position += packetLength;
}
console.log('Sent packet ' + packetID + ', ' + packetLength + ' bytes');
this.sendMessage(sendXfer, PacketFlags.Reliable);
if (final)
{
pos.position = -1;
}
}
XferFileUp(xferID: Long, data: Buffer)
{
return new Promise<void>((resolve, reject) =>
{
let packetID = 0;
const pos = {
position: 0
};
const subs = this.subscribeToMessages([
Message.AbortXfer,
Message.ConfirmXferPacket
], (packet: Packet) =>
{
switch (packet.message.id)
{
case Message.ConfirmXferPacket:
{
const msg = packet.message as ConfirmXferPacketMessage;
if (msg.XferID.ID.equals(xferID))
{
if (pos.position > -1)
{
console.log('Packet confirmed, sending next. Position: ' + pos.position);
packetID++;
this.sendXferPacket(xferID, packetID, data, pos);
}
}
break;
}
case Message.AbortXfer:
{
const msg = packet.message as AbortXferMessage;
if (msg.XferID.ID.equals(xferID))
{
console.log('Transfer aborted');
subs.unsubscribe();
reject(new Error('Transfer aborted'));
}
}
}
});
this.sendXferPacket(xferID, packetID, data, pos);
if (pos.position === -1)
{
subs.unsubscribe();
resolve();
}
});
}
XferFileDown(fileName: string, deleteOnCompletion: boolean, useBigPackets: boolean, vFileID: UUID, vFileType: AssetType, fromCache: boolean): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
@@ -127,7 +221,8 @@ export class Circuit
let finished = false;
let finishID = 0;
const receivedChunks: { [key: number]: Buffer } = {};
let firstPacket = true;
let dataSize = 0;
subscription = this.subscribeToMessages([
Message.SendXferPacket,
Message.AbortXfer
@@ -161,7 +256,16 @@ export class Circuit
resetTimeout();
const packetNum = message.XferID.Packet & 0x7FFFFFFF;
const finishedNow = message.XferID.Packet & 0x80000000;
receivedChunks[packetNum] = message.DataPacket.Data;
if (firstPacket)
{
dataSize = message.DataPacket.Data.readUInt32LE(0);
receivedChunks[packetNum] = message.DataPacket.Data.slice(4);
firstPacket = false;
}
else
{
receivedChunks[packetNum] = message.DataPacket.Data;
}
const confirm = new ConfirmXferPacketMessage();
confirm.XferID = {
ID: transferID,
@@ -199,7 +303,12 @@ export class Circuit
subscription.unsubscribe();
}
clearInterval(progress);
resolve(Buffer.concat(conc));
const buf = Buffer.concat(conc);
if (buf.length !== dataSize)
{
console.warn('Warning: Received data size does not match expected');
}
resolve(buf);
}
}
break;

View File

@@ -24,7 +24,9 @@ import { FriendRemovedEvent } from '../events/FriendRemovedEvent';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import { GameObject } from './public/GameObject';
import { Avatar } from './public/Avatar';
import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent';
export class ClientEvents
{
@@ -51,7 +53,12 @@ export class ClientEvents
onParcelPropertiesEvent: Subject<ParcelPropertiesEvent> = new Subject<ParcelPropertiesEvent>();
onNewObjectEvent: Subject<NewObjectEvent> = new Subject<NewObjectEvent>();
onObjectUpdatedEvent: Subject<ObjectUpdatedEvent> = new Subject<ObjectUpdatedEvent>();
onObjectUpdatedTerseEvent: Subject<ObjectUpdatedEvent> = new Subject<ObjectUpdatedEvent>();
onObjectKilledEvent: Subject<ObjectKilledEvent> = new Subject<ObjectKilledEvent>();
onSelectedObjectEvent: Subject<SelectedObjectEvent> = new Subject<SelectedObjectEvent>();
onObjectResolvedEvent: Subject<ObjectResolvedEvent> = new Subject<ObjectResolvedEvent>();
onAvatarEnteredRegion: Subject<Avatar> = new Subject<Avatar>();
onAvatarLeftRegion: Subject<Avatar> = new Subject<Avatar>();
onRegionTimeDilation: Subject<number> = new Subject<number>();
onBulkUpdateInventoryEvent: Subject<BulkUpdateInventoryEvent> = new Subject<BulkUpdateInventoryEvent>();
}

View File

@@ -0,0 +1,66 @@
import { Vector3 } from './Vector3';
import { GameObject } from './public/GameObject';
import { UUID } from './UUID';
import * as builder from 'xmlbuilder';
import { XMLElement, XMLNode } from 'xmlbuilder';
import { Utils } from './Utils';
export class CoalescedGameObject
{
itemID: UUID;
size: Vector3;
objects: {
offset: Vector3,
object: GameObject
}[];
static async fromXML(xml: string)
{
const obj = new CoalescedGameObject();
const parsed = await Utils.parseXML(xml);
if (!parsed['CoalescedObject'])
{
throw new Error('CoalescedObject not found');
}
const result = parsed['CoalescedObject'];
obj.size = new Vector3([parseFloat(result.$.x), parseFloat(result.$.y), parseFloat(result.$.z)]);
const sog = result['SceneObjectGroup'];
obj.objects = [];
for (const object of sog)
{
const toProcess = object['SceneObjectGroup'][0];
const go = await GameObject.fromXML(toProcess);
obj.objects.push({
offset: new Vector3([parseFloat(object.$.offsetx), parseFloat(object.$.offsety), parseFloat(object.$.offsetz)]),
object: go
});
}
return obj;
}
async exportXMLElement(rootNode?: string): Promise<XMLElement>
{
const document = builder.create('CoalescedObject');
document.att('x', this.size.x);
document.att('y', this.size.y);
document.att('z', this.size.z);
for (const obj of this.objects)
{
const ele = document.ele('SceneObjectGroup');
ele.att('offsetx', obj.offset.x);
ele.att('offsety', obj.offset.y);
ele.att('offsetz', obj.offset.z);
const child = await obj.object.exportXMLElement(rootNode);
ele.children.push(child);
}
return document;
}
async exportXML(rootNode?: string): Promise<string>
{
return (await this.exportXMLElement(rootNode)).end({pretty: true, allowEmpty: true});
}
}

View File

@@ -15,6 +15,10 @@ import { GroupChatEvent } from '../events/GroupChatEvent';
import { GroupChatSessionAgentListEvent } from '../events/GroupChatSessionAgentListEvent';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { IPAddress } from './IPAddress';
import { BulkUpdateInventoryEvent } from '../events/BulkUpdateInventoryEvent';
import { InventoryFolder } from './InventoryFolder';
import { InventoryItem } from './InventoryItem';
import { Utils } from './Utils';
export class EventQueueClient
{
@@ -102,6 +106,64 @@ export class EventQueueClient
*/
break;
case 'BulkUpdateInventory':
{
const body = event['body'];
const buie = new BulkUpdateInventoryEvent();
if (body['FolderData'])
{
for (const f of body['FolderData'])
{
const folderID = new UUID(f['FolderID']);
if (!folderID.isZero())
{
const folder = new InventoryFolder(this.agent.inventory.main, this.agent);
folder.folderID = folderID;
folder.name = f['Name'];
folder.parentID = new UUID(f['ParentID']);
folder.typeDefault = parseInt(f['Type'], 10);
buie.folderData.push(folder);
}
}
}
if (body['ItemData'])
{
for (const i of body['ItemData'])
{
const itemID = new UUID(i['ItemID']);
if (!itemID.isZero())
{
const folder = this.agent.inventory.findFolder(new UUID(i['FolderID']));
const item = new InventoryItem(folder || undefined, this.agent);
item.assetID = new UUID(i['AssetID']);
item.permissions.baseMask = Utils.OctetsToUInt32BE(i['BaseMask'].octets);
item.permissions.everyoneMask = Utils.OctetsToUInt32BE(i['EveryoneMask'].octets);
item.permissions.groupMask = Utils.OctetsToUInt32BE(i['GroupMask'].octets);
item.permissions.nextOwnerMask = Utils.OctetsToUInt32BE(i['NextOwnerMask'].octets);
item.permissions.ownerMask = Utils.OctetsToUInt32BE(i['OwnerMask'].octets);
item.permissions.groupOwned = i['GroupOwned'];
item.permissions.creator = new UUID(i['CreatorID']);
item.permissions.group = new UUID(i['GroupID']);
item.permissions.owner = new UUID(i['OwnerID']);
item.flags = Utils.OctetsToUInt32BE(i['Flags'].octets);
item.callbackID = Utils.OctetsToUInt32BE(i['CallbackID'].octets);
item.created = new Date(parseInt(i['CreationDate'], 10) * 1000);
item.description = i['Description'];
item.parentID = new UUID(i['FolderID']);
item.inventoryType = parseInt(i['InvType'], 10);
item.salePrice = parseInt(i['SalePrice'], 10);
item.saleType = parseInt(i['SaleType'], 10);
item.type = parseInt(i['Type'], 10);
item.itemID = itemID;
item.name = i['Name'];
buie.itemData.push(item);
}
}
}
this.clientEvents.onBulkUpdateInventoryEvent.next(buie);
break;
}
case 'ParcelProperties':
{
const body = event['body'];
@@ -120,7 +182,7 @@ export class EventQueueClient
pprop.Area = body['ParcelData'][0]['Area'];
try
{
pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32LE(0);
pprop.AuctionID = Buffer.from(body['ParcelData'][0]['AuctionID'].toArray()).readUInt32BE(0);
}
catch (ignore)
{
@@ -150,7 +212,7 @@ export class EventQueueClient
pprop.OtherPrims = body['ParcelData'][0]['OtherPrims'];
pprop.OwnerID = body['ParcelData'][0]['OwnerID'];
pprop.OwnerPrims = body['ParcelData'][0]['OwnerPrims'];
pprop.ParcelFlags = Buffer.from(body['ParcelData'][0]['ParcelFlags'].toArray()).readUInt32LE(0);
pprop.ParcelFlags = Buffer.from(body['ParcelData'][0]['ParcelFlags'].toArray()).readUInt32BE(0);
pprop.ParcelPrimBonus = body['ParcelData'][0]['ParcelPrimBonus'];
pprop.PassHours = body['ParcelData'][0]['PassHours'];
pprop.PassPrice = body['ParcelData'][0]['PassPrice'];
@@ -367,7 +429,7 @@ export class EventQueueClient
const info = event['body']['Info'][0];
if (info['LocationID'])
{
info['LocationID'] = Buffer.from(info['LocationID'].toArray()).readUInt32LE(0);
info['LocationID'] = Buffer.from(info['LocationID'].toArray()).readUInt32BE(0);
const regionHandleBuf = Buffer.from(info['RegionHandle'].toArray());
info['RegionHandle'] = new Long(regionHandleBuf.readUInt32LE(0), regionHandleBuf.readUInt32LE(4), true);
@@ -375,7 +437,7 @@ export class EventQueueClient
info['SimIP'] = new IPAddress(Buffer.from(info['SimIP'].toArray()), 0).toString();
info['TeleportFlags'] = Buffer.from(info['TeleportFlags'].toArray()).readUInt32LE(0);
info['TeleportFlags'] = Buffer.from(info['TeleportFlags'].toArray()).readUInt32BE(0);
const tpEvent = new TeleportEvent();
tpEvent.message = '';

View File

@@ -77,6 +77,27 @@ export class Inventory
}
return this.getRootFolderMain().folderID;
}
findFolder(folderID: UUID): InventoryFolder | null
{
for (const id of Object.keys(this.main.skeleton))
{
if (folderID.equals(id))
{
return this.main.skeleton[id];
}
else
{
const result = this.main.skeleton[id].findFolder(folderID);
if (result !== null)
{
return result;
}
}
}
return null;
}
async fetchInventoryItem(item: UUID): Promise<InventoryItem | null>
{
const params = {
@@ -92,7 +113,12 @@ export class Inventory
if (response['items'].length > 0)
{
const receivedItem = response['items'][0];
const invItem = new InventoryItem();
let folder = await this.findFolder(new UUID(receivedItem['parent_id'].toString()));
if (folder === null)
{
folder = this.getRootFolderMain();
}
const invItem = new InventoryItem(folder, this.agent);
invItem.assetID = new UUID(receivedItem['asset_id'].toString());
invItem.inventoryType = parseInt(receivedItem['inv_type'], 10);
invItem.type = parseInt(receivedItem['type'], 10);
@@ -126,10 +152,6 @@ export class Inventory
{
await this.main.skeleton[invItem.parentID.toString()].addItem(invItem);
}
else
{
throw new Error('FolderID of ' + invItem.parentID.toString() + ' not found!');
}
return invItem;
}
else

View File

@@ -6,6 +6,22 @@ import * as LLSD from '@caspertech/llsd';
import { InventorySortOrder } from '../enums/InventorySortOrder';
import { Agent } from './Agent';
import { FolderType } from '../enums/FolderType';
import { CreateInventoryFolderMessage } from './messages/CreateInventoryFolder';
import { Utils } from './Utils';
import { PacketFlags } from '../enums/PacketFlags';
import { Message } from '../enums/Message';
import { FilterResponse } from '../enums/FilterResponse';
import { UpdateCreateInventoryItemMessage } from './messages/UpdateCreateInventoryItem';
import { LLMesh } from '..';
import { CreateInventoryItemMessage } from './messages/CreateInventoryItem';
import { WearableType } from '../enums/WearableType';
import { PermissionMask } from '../enums/PermissionMask';
import { AssetType } from '../enums/AssetType';
import { LLWearable } from './LLWearable';
import { InventoryType } from '../enums/InventoryType';
import { AssetUploadRequestMessage } from './messages/AssetUploadRequest';
import { RequestXferMessage } from './messages/RequestXfer';
import { Logger } from './Logger';
export class InventoryFolder
{
@@ -15,9 +31,12 @@ export class InventoryFolder
folderID: UUID;
parentID: UUID;
items: InventoryItem[] = [];
folders: InventoryFolder[] = [];
cacheDir: string;
agent: Agent;
private callbackID = 1;
private inventoryBase: {
skeleton: {[key: string]: InventoryFolder},
root?: UUID
@@ -57,6 +76,57 @@ export class InventoryFolder
return children;
}
async createFolder(name: string, type: FolderType)
{
const msg = new CreateInventoryFolderMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
msg.FolderData = {
FolderID: UUID.random(),
ParentID: this.folderID,
Type: type,
Name: Utils.StringToBuffer(name),
};
const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
await this.agent.currentRegion.circuit.waitForAck(ack, 10000);
const requestFolder = {
folder_id: new LLSD.UUID(this.folderID),
owner_id: new LLSD.UUID(this.agent.agentID),
fetch_folders: true,
fetch_items: false,
sort_order: InventorySortOrder.ByName
};
const requestedFolders = {
'folders': [
requestFolder
]
};
const folderContents: any = await this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders);
if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['categories'] && folderContents['folders'][0]['categories'].length > 0)
{
for (const folder of folderContents['folders'][0]['categories'])
{
const foundFolderID = new UUID(folder['folder_id'].toString());
if (foundFolderID.equals(msg.FolderData.FolderID))
{
const newFolder = new InventoryFolder(this.agent.inventory.main, this.agent);
newFolder.typeDefault = parseInt(folder['type_default'], 10);
newFolder.version = parseInt(folder['version'], 10);
newFolder.name = String(folder['name']);
newFolder.folderID = new UUID(folder['folder_id']);
newFolder.parentID = new UUID(folder['parent_id']);
this.folders.push(newFolder);
return newFolder;
}
}
}
throw new Error('Failed to create inventory folder');
}
private saveCache(): Promise<void>
{
return new Promise((resolve, reject) =>
@@ -111,10 +181,7 @@ export class InventoryFolder
item.permissions.owner = new UUID(item.permissions.owner.mUUID);
item.permissions.creator = new UUID(item.permissions.creator.mUUID);
item.permissions.group = new UUID(item.permissions.group.mUUID);
this.addItem(item).catch((error) =>
{
console.error(error);
});
this.addItem(item, false);
}
resolve();
}
@@ -167,8 +234,83 @@ export class InventoryFolder
}
}
populate()
private populateInternal(): Promise<void>
{
return new Promise<void>((resolve, reject) =>
{
const requestFolder = {
folder_id: new LLSD.UUID(this.folderID),
owner_id: new LLSD.UUID(this.agent.agentID),
fetch_folders: true,
fetch_items: true,
sort_order: InventorySortOrder.ByName
};
const requestedFolders = {
'folders': [
requestFolder
]
};
this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) =>
{
if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['items'])
{
this.version = folderContents['folders'][0]['version'];
this.items = [];
for (const item of folderContents['folders'][0]['items'])
{
const invItem = new InventoryItem(this, this.agent);
invItem.assetID = new UUID(item['asset_id'].toString());
invItem.inventoryType = item['inv_type'];
invItem.name = item['name'];
invItem.salePrice = item['sale_info']['sale_price'];
invItem.saleType = item['sale_info']['sale_type'];
invItem.created = new Date(item['created_at'] * 1000);
invItem.parentID = new UUID(item['parent_id'].toString());
invItem.flags = item['flags'];
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'],
nextOwnerMask: item['permissions']['next_owner_mask'],
ownerMask: item['permissions']['owner_mask'],
everyoneMask: item['permissions']['everyone_mask'],
lastOwner: new UUID(item['permissions']['last_owner_id'].toString()),
owner: new UUID(item['permissions']['owner_id'].toString()),
creator: new UUID(item['permissions']['creator_id'].toString()),
group: new UUID(item['permissions']['group_id'].toString())
};
this.addItem(invItem, false);
}
this.saveCache().then(() =>
{
resolve();
}).catch(() =>
{
// Resolve anyway
resolve();
});
}
else
{
resolve();
}
});
});
}
populate(useCached = true)
{
if (!useCached)
{
return this.populateInternal();
}
return new Promise((resolve, reject) =>
{
this.loadCache().then(() =>
@@ -176,71 +318,514 @@ export class InventoryFolder
resolve();
}).catch((err) =>
{
const requestFolder = {
folder_id: new LLSD.UUID(this.folderID),
owner_id: new LLSD.UUID(this.agent.agentID),
fetch_folders: true,
fetch_items: true,
sort_order: InventorySortOrder.ByName
};
const requestedFolders = {
'folders': [
requestFolder
]
};
this.agent.currentRegion.caps.capsPostXML('FetchInventoryDescendents2', requestedFolders).then((folderContents: any) =>
this.populateInternal().then(() =>
{
if (folderContents['folders'] && folderContents['folders'][0] && folderContents['folders'][0]['items'])
{
this.version = folderContents['folders'][0]['version'];
this.items = [];
for (const item of folderContents['folders'][0]['items'])
{
const invItem = new InventoryItem();
invItem.assetID = new UUID(item['asset_id'].toString());
invItem.inventoryType = item['inv_type'];
invItem.name = item['name'];
invItem.salePrice = item['sale_info']['sale_price'];
invItem.saleType = item['sale_info']['sale_type'];
invItem.created = new Date(item['created_at'] * 1000);
invItem.parentID = new UUID(item['parent_id'].toString());
invItem.flags = item['flags'];
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'],
nextOwnerMask: item['permissions']['next_owner_mask'],
ownerMask: item['permissions']['owner_mask'],
everyoneMask: item['permissions']['everyone_mask'],
lastOwner: new UUID(item['permissions']['last_owner_id'].toString()),
owner: new UUID(item['permissions']['owner_id'].toString()),
creator: new UUID(item['permissions']['creator_id'].toString()),
group: new UUID(item['permissions']['group_id'].toString())
};
this.addItem(invItem);
}
this.saveCache().then(() =>
{
resolve();
}).catch(() =>
{
// Resolve anyway
resolve();
});
}
else
{
resolve();
}
resolve();
}).catch((erro: Error) =>
{
reject(erro);
});
});
});
}
private uploadInventoryAssetLegacy(assetType: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise<UUID>
{
return new Promise<UUID>(async (resolve, reject) =>
{
// Send an AssetUploadRequest and a CreateInventoryRequest simultaneously
const msg = new AssetUploadRequestMessage();
const transactionID = UUID.random();
msg.AssetBlock = {
StoreLocal: false,
Type: assetType,
Tempfile: false,
TransactionID: transactionID,
AssetData: Buffer.allocUnsafe(0)
};
const callbackID = ++this.callbackID;
const createMsg = new CreateInventoryItemMessage();
let wearableType = WearableType.Shape;
if (inventoryType === InventoryType.Wearable)
{
const wearable = new LLWearable(data.toString('utf-8'));
wearableType = wearable.type;
}
createMsg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
createMsg.InventoryBlock = {
CallbackID: callbackID,
FolderID: this.folderID,
TransactionID: transactionID,
NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
Type: assetType,
InvType: inventoryType,
WearableType: wearableType,
Name: Utils.StringToBuffer(name),
Description: Utils.StringToBuffer(description)
};
if (data.length + 100 < 1200)
{
msg.AssetBlock.AssetData = data;
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
this.agent.currentRegion.circuit.sendMessage(createMsg, PacketFlags.Reliable);
}
else
{
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
this.agent.currentRegion.circuit.sendMessage(createMsg, PacketFlags.Reliable);
this.agent.currentRegion.circuit.waitForMessage<RequestXferMessage>(Message.RequestXfer, 10000).then((result: RequestXferMessage) =>
{
this.agent.currentRegion.circuit.XferFileUp(result.XferID.ID, data).then(() =>
{
console.log('Xfer finished');
resolve();
}).catch((err: Error) =>
{
console.error('Error with transfer');
console.error(err);
reject(err);
});
});
}
this.agent.currentRegion.circuit.waitForMessage<UpdateCreateInventoryItemMessage>(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) =>
{
if (message.InventoryData[0].CallbackID === callbackID)
{
return FilterResponse.Finish;
}
else
{
return FilterResponse.NoMatch;
}
}).then((result: UpdateCreateInventoryItemMessage) =>
{
if (!result.InventoryData || result.InventoryData.length < 1)
{
reject('Failed to create inventory item for wearable');
}
resolve(result.InventoryData[0].ItemID);
});
});
}
private uploadInventoryItem(assetType: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise<UUID>
{
return new Promise<UUID>((resolve, reject) =>
{
const wearableType = WearableType.Shape;
const transactionID = UUID.zero();
const callbackID = ++this.callbackID;
const msg = new CreateInventoryItemMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
msg.InventoryBlock = {
CallbackID: callbackID,
FolderID: this.folderID,
TransactionID: transactionID,
NextOwnerMask: (1 << 13) | (1 << 14) | (1 << 15) | (1 << 19),
Type: assetType,
InvType: inventoryType,
WearableType: wearableType,
Name: Utils.StringToBuffer(name),
Description: Utils.StringToBuffer(description)
};
this.agent.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 (inventoryType)
{
case InventoryType.Notecard:
{
this.agent.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.agent.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 InventoryType.Gesture:
{
this.agent.currentRegion.caps.isCapAvailable('UpdateGestureAgentInventory').then((available) =>
{
if (available)
{
this.agent.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.agent.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);
});
}
else
{
this.uploadInventoryAssetLegacy(assetType, inventoryType, data, name, description).then((invItemID: UUID) =>
{
resolve(invItemID);
}).catch((err: Error) =>
{
reject(err);
});
}
});
break;
}
case InventoryType.Script:
{
this.agent.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.agent.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: ' + inventoryType));
}
}
}).catch(() =>
{
reject(new Error('Timed out waiting for UpdateCreateInventoryItem'));
});
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
uploadAsset(type: AssetType, inventoryType: InventoryType, data: Buffer, name: string, description: string): Promise<InventoryItem>
{
return new Promise<InventoryItem>((resolve, reject) =>
{
switch (inventoryType)
{
case InventoryType.Wearable:
// Wearables have to be uploaded using the legacy method and then created
this.uploadInventoryAssetLegacy(type, inventoryType, 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
{
this.addItem(item, false).then(() =>
{
resolve(item);
});
}
}).catch((err) =>
{
reject(err);
});
}).catch((err) =>
{
reject(err);
});
return;
case InventoryType.Landmark:
case InventoryType.Notecard:
case InventoryType.Gesture:
case InventoryType.Script:
// These types must be created first and then modified
this.uploadInventoryItem(type, inventoryType, 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
{
this.addItem(item, false).then(() =>
{
resolve(item);
});
}
}).catch((err) =>
{
reject(err);
});
}).catch((err) =>
{
reject(err);
});
return;
}
Logger.Info('[' + name + ']');
const httpType = Utils.AssetTypeToHTTPAssetType(type);
this.agent.currentRegion.caps.capsPostXML('NewFileAgentInventory', {
'folder_id': new LLSD.UUID(this.folderID.toString()),
'asset_type': httpType,
'inventory_type': Utils.HTTPAssetTypeToCapInventoryType(httpType),
'name': name,
'description': description,
'everyone_mask': PermissionMask.All,
'group_mask': PermissionMask.All,
'next_owner_mask': PermissionMask.All,
'expected_upload_cost': 0
}).then((response: any) =>
{
if (response['state'] === 'upload')
{
const uploadURL = response['uploader'];
this.agent.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) =>
{
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
{
this.addItem(item, false).then(() =>
{
resolve(item);
});
}
}).catch((err) =>
{
reject(err);
});
}
}).catch((err) =>
{
reject(err);
});
}
else if (response['error'])
{
reject(response['error']['message']);
}
else
{
reject('Unable to upload asset');
}
}).catch((err) =>
{
console.log('Got err');
console.log(err);
reject(err);
})
});
}
checkCopyright(creatorID: UUID)
{
if (!creatorID.equals(this.agent.agentID) && !creatorID.isZero())
{
throw new Error('Unable to upload - copyright violation');
}
}
findFolder(id: UUID): InventoryFolder | null
{
for (const folder of this.folders)
{
if (folder.folderID.equals(id))
{
return folder;
}
const result = folder.findFolder(id);
if (result !== null)
{
return result;
}
}
return null;
}
async uploadMesh(name: string, description: string, mesh: Buffer, confirmCostCallback: (cost: number) => Promise<boolean>): Promise<InventoryItem>
{
const decodedMesh = await LLMesh.from(mesh);
this.checkCopyright(decodedMesh.creatorID);
const faces = [];
const faceCount = decodedMesh.lodLevels['high_lod'].length;
for (let x = 0; x < faceCount; x++)
{
faces.push({
'diffuse_color': [1.000000000000001, 1.000000000000001, 1.000000000000001, 1.000000000000001],
'fullbright': false
});
}
const prim = {
'face_list': faces,
'position': [0.000000000000001, 0.000000000000001, 0.000000000000001],
'rotation': [0.000000000000001, 0.000000000000001, 0.000000000000001, 1.000000000000001],
'scale': [2.000000000000001, 2.000000000000001, 2.000000000000001],
'material': 3,
'physics_shape_type': 2,
'mesh': 0
};
const assetResources = {
'instance_list': [prim],
'mesh_list': [new LLSD.Binary(Array.from(mesh))],
'texture_list': [],
'metric': 'MUT_Unspecified'
};
const uploadMap = {
'name': String(name),
'description': String(description),
'asset_resources': assetResources,
'asset_type': 'mesh',
'inventory_type': 'object',
'folder_id': new LLSD.UUID(this.folderID.toString()),
'texture_folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Texture)),
'everyone_mask': PermissionMask.All,
'group_mask': PermissionMask.All,
'next_owner_mask': PermissionMask.All
};
let result;
try
{
result = await this.agent.currentRegion.caps.capsPostXML('NewFileAgentInventory', uploadMap);
}
catch (error)
{
console.error(error);
}
if (result['state'] === 'upload' && result['upload_price'] !== undefined)
{
const cost = result['upload_price'];
if (await confirmCostCallback(cost))
{
const uploader = result['uploader'];
const uploadResult = await this.agent.currentRegion.caps.capsPerformXMLPost(uploader, assetResources);
if (uploadResult['new_inventory_item'] && uploadResult['new_asset'])
{
const inventoryItem = new UUID(uploadResult['new_inventory_item'].toString());
const item = await this.agent.inventory.fetchInventoryItem(inventoryItem);
if (item !== null)
{
item.assetID = new UUID(uploadResult['new_asset'].toString());
await this.addItem(item, false);
return item;
}
else
{
throw new Error('Unable to locate inventory item following mesh upload');
}
}
else
{
throw new Error('Upload failed - no new inventory item returned');
}
}
throw new Error('Upload cost declined')
}
else
{
console.log(result);
console.log(JSON.stringify(result.error));
throw new Error('Upload failed');
}
}
}

View File

@@ -3,6 +3,31 @@ import { InventoryType } from '../enums/InventoryType';
import { PermissionMask } from '../enums/PermissionMask';
import { InventoryItemFlags } from '../enums/InventoryItemFlags';
import { AssetType } from '../enums/AssetType';
import * as builder from 'xmlbuilder';
import * as xml2js from 'xml2js';
import { Utils } from './Utils';
import { AttachmentPoint } from '../enums/AttachmentPoint';
import { RezSingleAttachmentFromInvMessage } from './messages/RezSingleAttachmentFromInv';
import { GameObject } from '..';
import { Agent } from './Agent';
import { Subscription } from 'rxjs';
import { DetachAttachmentIntoInvMessage } from './messages/DetachAttachmentIntoInv';
import { Vector3 } from './Vector3';
import { RezObjectMessage } from './messages/RezObject';
import { NewObjectEvent } from '../events/NewObjectEvent';
import { InventoryFolder } from './InventoryFolder';
import { MoveInventoryItemMessage } from './messages/MoveInventoryItem';
import { RemoveInventoryItemMessage } from './messages/RemoveInventoryItem';
import { SaleTypeLL } from '../enums/SaleTypeLL';
import { AssetTypeLL } from '../enums/AssetTypeLL';
import { UpdateTaskInventoryMessage } from './messages/UpdateTaskInventory';
import { PacketFlags } from '../enums/PacketFlags';
import Timeout = NodeJS.Timeout;
import * as LLSD from '@caspertech/llsd';
import { MoveTaskInventoryMessage } from './messages/MoveTaskInventory';
import { UpdateCreateInventoryItemMessage } from './messages/UpdateCreateInventoryItem';
import { Message } from '../enums/Message';
import { FilterResponse } from '../enums/FilterResponse';
export class InventoryItem
{
@@ -20,6 +45,7 @@ export class InventoryItem
permsGranter?: UUID;
description: string;
type: AssetType;
callbackID: number;
permissions: {
baseMask: PermissionMask;
groupMask: PermissionMask;
@@ -44,6 +70,414 @@ export class InventoryItem
groupOwned: false
};
static fromAsset(lineObj: {lines: string[], lineNum: number}, container?: GameObject | InventoryFolder, agent?: Agent): InventoryItem
{
const item: InventoryItem = new InventoryItem(container, agent);
while (lineObj.lineNum < lineObj.lines.length)
{
const line = lineObj.lines[lineObj.lineNum++];
let result = Utils.parseLine(line);
if (result.key !== null)
{
if (result.key === '{')
{
// do nothing
}
else if (result.key === '}')
{
break;
}
else if (result.key === 'item_id')
{
item.itemID = new UUID(result.value);
}
else if (result.key === 'parent_id')
{
item.parentID = new UUID(result.value);
}
else if (result.key === 'permissions')
{
while (lineObj.lineNum < lineObj.lines.length)
{
result = Utils.parseLine(lineObj.lines[lineObj.lineNum++]);
if (result.key !== null)
{
if (result.key === '{')
{
// do nothing
}
else if (result.key === '}')
{
break;
}
else if (result.key === 'creator_mask')
{
item.permissions.baseMask = parseInt(result.value, 16);
}
else if (result.key === 'base_mask')
{
item.permissions.baseMask = parseInt(result.value, 16);
}
else if (result.key === 'owner_mask')
{
item.permissions.ownerMask = parseInt(result.value, 16);
}
else if (result.key === 'group_mask')
{
item.permissions.groupMask = parseInt(result.value, 16);
}
else if (result.key === 'everyone_mask')
{
item.permissions.everyoneMask = parseInt(result.value, 16);
}
else if (result.key === 'next_owner_mask')
{
item.permissions.nextOwnerMask = parseInt(result.value, 16);
}
else if (result.key === 'creator_id')
{
item.permissions.creator = new UUID(result.value);
}
else if (result.key === 'owner_id')
{
item.permissions.owner = new UUID(result.value);
}
else if (result.key === 'last_owner_id')
{
item.permissions.lastOwner = new UUID(result.value);
}
else if (result.key === 'group_id')
{
item.permissions.group = new UUID(result.value);
}
else if (result.key === 'group_owned')
{
const val = parseInt(result.value, 10);
item.permissions.groupOwned = (val !== 0);
}
else
{
console.log('Unrecognised key (4): ' + result.key);
}
}
}
}
else if (result.key === 'sale_info')
{
while (lineObj.lineNum < lineObj.lines.length)
{
result = Utils.parseLine(lineObj.lines[lineObj.lineNum++]);
if (result.key !== null)
{
if (result.key === '{')
{
// do nothing
}
else if (result.key === '}')
{
break;
}
else if (result.key === 'sale_type')
{
const typeString = result.value as any;
item.saleType = parseInt(SaleTypeLL[typeString], 10);
}
else if (result.key === 'sale_price')
{
item.salePrice = parseInt(result.value, 10);
}
else
{
console.log('Unrecognised key (3): ' + result.key);
}
}
}
}
else if (result.key === 'shadow_id')
{
item.assetID = new UUID(result.value).bitwiseOr(new UUID('3c115e51-04f4-523c-9fa6-98aff1034730'));
}
else if (result.key === 'asset_id')
{
item.assetID = new UUID(result.value);
}
else if (result.key === 'type')
{
const typeString = result.value as any;
item.type = parseInt(AssetTypeLL[typeString], 10);
}
else if (result.key === 'inv_type')
{
const typeString = String(result.value);
switch (typeString)
{
case 'texture':
item.inventoryType = InventoryType.Texture;
break;
case 'sound':
item.inventoryType = InventoryType.Sound;
break;
case 'callcard':
item.inventoryType = InventoryType.CallingCard;
break;
case 'landmark':
item.inventoryType = InventoryType.Landmark;
break;
case 'object':
item.inventoryType = InventoryType.Object;
break;
case 'notecard':
item.inventoryType = InventoryType.Notecard;
break;
case 'category':
item.inventoryType = InventoryType.Category;
break;
case 'root':
item.inventoryType = InventoryType.RootCategory;
break;
case 'script':
item.inventoryType = InventoryType.Script;
break;
case 'snapshot':
item.inventoryType = InventoryType.Snapshot;
break;
case 'attach':
item.inventoryType = InventoryType.Attachment;
break;
case 'wearable':
item.inventoryType = InventoryType.Wearable;
break;
case 'animation':
item.inventoryType = InventoryType.Animation;
break;
case 'gesture':
item.inventoryType = InventoryType.Gesture;
break;
case 'mesh':
item.inventoryType = InventoryType.Mesh;
break;
default:
console.error('Unknown inventory type: ' + typeString);
}
}
else if (result.key === 'flags')
{
item.flags = parseInt(result.value, 16);
}
else if (result.key === 'name')
{
item.name = result.value.substr(0, result.value.indexOf('|'));
}
else if (result.key === 'desc')
{
item.description = result.value.substr(0, result.value.indexOf('|'));
}
else if (result.key === 'creation_date')
{
item.created = new Date(parseInt(result.value, 10) * 1000);
}
else
{
console.log('Unrecognised key (2): ' + result.key);
}
}
}
return item;
}
static async fromXML(xml: string): Promise<InventoryItem>
{
const parsed = await Utils.parseXML(xml);
if (!parsed['InventoryItem'])
{
throw new Error('InventoryItem not found');
}
const inventoryItem = new InventoryItem();
const result = parsed['InventoryItem'];
let prop: any;
if ((prop = Utils.getFromXMLJS(result, 'Name')) !== undefined)
{
inventoryItem.name = prop.toString();
}
if ((prop = Utils.getFromXMLJS(result, 'ID')) !== undefined)
{
try
{
inventoryItem.itemID = new UUID(prop.toString());
}
catch (error)
{
console.error(error);
}
}
if ((prop = Utils.getFromXMLJS(result, 'InvType')) !== undefined)
{
inventoryItem.inventoryType = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'CreatorUUID')) !== undefined)
{
try
{
inventoryItem.permissions.creator = new UUID(prop.toString());
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'CreationDate')) !== undefined)
{
try
{
inventoryItem.created = new Date(parseInt(prop, 10) * 1000);
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'Owner')) !== undefined)
{
try
{
inventoryItem.permissions.owner = new UUID(prop.toString());
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'Description')) !== undefined)
{
inventoryItem.description = prop.toString();
}
if ((prop = Utils.getFromXMLJS(result, 'AssetType')) !== undefined)
{
inventoryItem.type = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'AssetID')) !== undefined)
{
try
{
inventoryItem.assetID = new UUID(prop.toString());
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'SaleType')) !== undefined)
{
inventoryItem.saleType = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'SalePrice')) !== undefined)
{
inventoryItem.salePrice = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'BasePermissions')) !== undefined)
{
inventoryItem.permissions.baseMask = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'CurrentPermissions')) !== undefined)
{
inventoryItem.permissions.ownerMask = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'EveryonePermissions')) !== undefined)
{
inventoryItem.permissions.everyoneMask = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'NextPermissions')) !== undefined)
{
inventoryItem.permissions.nextOwnerMask = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'Flags')) !== undefined)
{
inventoryItem.flags = parseInt(prop, 10);
}
if ((prop = Utils.getFromXMLJS(result, 'GroupID')) !== undefined)
{
try
{
inventoryItem.permissions.group = new UUID(prop.toString());
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'LastOwner')) !== undefined)
{
try
{
inventoryItem.permissions.lastOwner = new UUID(prop.toString());
}
catch (err)
{
console.error(err);
}
}
if ((prop = Utils.getFromXMLJS(result, 'GroupOwned')) !== undefined)
{
inventoryItem.permissions.groupOwned = parseInt(prop, 10) > 0
}
return inventoryItem;
}
constructor(private container?: GameObject | InventoryFolder, private agent?: Agent)
{
}
toAsset(indent: string = '')
{
const lines: string[] = [];
lines.push('{');
lines.push('\titem_id\t' + this.itemID.toString());
lines.push('\tparent_id\t' + this.parentID.toString());
lines.push('permissions 0');
lines.push('{');
lines.push('\tbase_mask\t' + Utils.numberToFixedHex(this.permissions.baseMask));
lines.push('\towner_mask\t' + Utils.numberToFixedHex(this.permissions.ownerMask));
lines.push('\tgroup_mask\t' + Utils.numberToFixedHex(this.permissions.groupMask));
lines.push('\teveryone_mask\t' + Utils.numberToFixedHex(this.permissions.everyoneMask));
lines.push('\tnext_owner_mask\t' + Utils.numberToFixedHex(this.permissions.nextOwnerMask));
lines.push('\tcreator_id\t' + this.permissions.creator.toString());
lines.push('\towner_id\t' + this.permissions.owner.toString());
lines.push('\tlast_owner_id\t' + this.permissions.lastOwner.toString());
lines.push('\tgroup_id\t' + this.permissions.group.toString());
lines.push('}');
lines.push('\tasset_id\t' + this.assetID.toString());
lines.push('\ttype\t' + Utils.AssetTypeToHTTPAssetType(this.type));
lines.push('\tinv_type\t' + Utils.InventoryTypeToLLInventoryType(this.inventoryType));
lines.push('\tflags\t' + Utils.numberToFixedHex(this.flags));
lines.push('sale_info\t0');
lines.push('{');
switch (this.saleType)
{
case 0:
lines.push('\tsale_type\tnot');
break;
case 1:
lines.push('\tsale_type\torig');
break;
case 2:
lines.push('\tsale_type\tcopy');
break;
case 3:
lines.push('\tsale_type\tcntn');
break;
}
lines.push('\tsale_price\t' + this.salePrice);
lines.push('}');
lines.push('\tname\t' + this.name + '|');
lines.push('\tdesc\t' + this.description + '|');
lines.push('\tcreation_date\t' + Math.floor(this.created.getTime() / 1000));
lines.push('}');
return indent + lines.join('\n' + indent);
}
getCRC(): number
{
let crc = 0;
@@ -67,4 +501,503 @@ export class InventoryItem
crc = crc + Math.round(this.created.getTime() / 1000) >>> 0;
return crc;
}
async moveToFolder(targetFolder: InventoryFolder): Promise<InventoryItem>
{
if (this.agent !== undefined)
{
if (this.container instanceof GameObject)
{
const msg = new MoveTaskInventoryMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID,
FolderID: targetFolder.folderID
};
msg.InventoryData = {
LocalID: this.container.ID,
ItemID: this.itemID
};
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
const response: UpdateCreateInventoryItemMessage = await this.agent.currentRegion.circuit.waitForMessage<UpdateCreateInventoryItemMessage>(Message.UpdateCreateInventoryItem, 10000, (message: UpdateCreateInventoryItemMessage) =>
{
for (const inv of message.InventoryData)
{
if (Utils.BufferToStringSimple(inv.Name) === this.name)
{
return FilterResponse.Finish;
}
}
return FilterResponse.NoMatch;
});
for (const inv of response.InventoryData)
{
if (Utils.BufferToStringSimple(inv.Name) === this.name)
{
const item = await this.agent.inventory.fetchInventoryItem(inv.ItemID);
if (item === null)
{
throw new Error('Unable to get inventory item after move');
}
if (!item.parentID.equals(targetFolder.folderID))
{
await item.moveToFolder(targetFolder);
}
return item;
}
}
throw new Error('Unable to get inventory item after move');
}
else
{
const msg = new MoveInventoryItemMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID,
Stamp: false
};
msg.InventoryData = [
{
ItemID: this.itemID,
FolderID: targetFolder.folderID,
NewName: Buffer.alloc(0)
}
];
const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
await this.agent.currentRegion.circuit.waitForAck(ack, 10000);
const item = await this.agent.inventory.fetchInventoryItem(this.itemID);
if (item === null)
{
throw new Error('Unable to find inventory item after move')
}
return item;
}
}
else
{
throw new Error('This inventoryItem is local only and cannot be moved to a folder')
}
}
async delete()
{
if (this.agent !== undefined)
{
const msg = new RemoveInventoryItemMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
msg.InventoryData = [
{
ItemID: this.itemID
}
];
const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
return this.agent.currentRegion.circuit.waitForAck(ack, 10000);
}
else
{
throw new Error('This inventoryItem is local only and cannot be deleted')
}
}
async exportXML(): Promise<string>
{
const document = builder.create('InventoryItem');
document.ele('Name', this.name);
document.ele('ID', this.itemID.toString());
document.ele('InvType', this.inventoryType);
document.ele('CreatorUUID', this.permissions.creator.toString());
document.ele('CreationDate', this.created.getTime() / 1000);
document.ele('Owner', this.permissions.owner.toString());
document.ele('LastOwner', this.permissions.lastOwner.toString());
document.ele('Description', this.description);
document.ele('AssetType', this.type);
document.ele('AssetID', this.assetID.toString());
document.ele('SaleType', this.saleType);
document.ele('SalePrice', this.salePrice);
document.ele('BasePermissions', this.permissions.baseMask);
document.ele('CurrentPermissions', this.permissions.ownerMask);
document.ele('EveryonePermissions', this.permissions.everyoneMask);
document.ele('NextPermissions', this.permissions.nextOwnerMask);
document.ele('Flags', this.flags);
document.ele('GroupID', this.permissions.group.toString());
document.ele('GroupOwned', this.permissions.groupOwned);
return document.end({pretty: true, allowEmpty: true});
}
detachFromAvatar()
{
if (this.agent === undefined)
{
throw new Error('This inventory item was created locally. Please import to the grid.');
}
const msg = new DetachAttachmentIntoInvMessage();
msg.ObjectData = {
AgentID: this.agent.agentID,
ItemID: this.itemID
};
const ack = this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
return this.agent.currentRegion.circuit.waitForAck(ack, 10000);
}
attachToAvatar(attachPoint: AttachmentPoint, timeout: number = 10000): Promise<GameObject>
{
return new Promise<GameObject>((resolve, reject) =>
{
if (this.agent === undefined)
{
throw new Error('This inventory item was created locally. Please import to the grid.');
}
const rsafi = new RezSingleAttachmentFromInvMessage();
rsafi.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
rsafi.ObjectData = {
ItemID: this.itemID,
OwnerID: this.permissions.owner,
AttachmentPt: 0x80 | attachPoint,
ItemFlags: this.flags,
GroupMask: this.permissions.groupMask,
EveryoneMask: this.permissions.everyoneMask,
NextOwnerMask: this.permissions.nextOwnerMask,
Name: Utils.StringToBuffer(this.name),
Description: Utils.StringToBuffer(this.description)
};
const avatar = this.agent.currentRegion.clientCommands.agent.getAvatar();
let subs: Subscription | undefined = undefined;
let tmout: Timeout | undefined = undefined;
subs = avatar.onAttachmentAdded.subscribe((obj: GameObject) =>
{
if (obj.name === this.name)
{
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
if (tmout !== undefined)
{
clearTimeout(tmout);
tmout = undefined;
}
resolve(obj);
}
});
setTimeout(() =>
{
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
if (tmout !== undefined)
{
clearTimeout(tmout);
tmout = undefined;
}
reject(new Error('Attach to avatar timed out'));
}, timeout);
this.agent.currentRegion.circuit.sendMessage(rsafi, PacketFlags.Reliable);
});
}
rezGroupInWorld(position: Vector3): Promise<GameObject[]>
{
return new Promise<GameObject[]>(async (resolve, reject) =>
{
if (this.agent === undefined)
{
reject(new Error('This InventoryItem is local only, so cant rez'));
return;
}
const queryID = UUID.random();
const msg = new RezObjectMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID,
GroupID: UUID.zero()
};
msg.RezData = {
FromTaskID: (this.container instanceof GameObject) ? this.container.FullID : UUID.zero(),
BypassRaycast: 1,
RayStart: position,
RayEnd: position,
RayTargetID: UUID.zero(),
RayEndIsIntersection: false,
RezSelected: true,
RemoveItem: false,
ItemFlags: this.flags,
GroupMask: PermissionMask.All,
EveryoneMask: PermissionMask.All,
NextOwnerMask: PermissionMask.All,
};
msg.InventoryData = {
ItemID: this.itemID,
FolderID: this.parentID,
CreatorID: this.permissions.creator,
OwnerID: this.permissions.owner,
GroupID: this.permissions.group,
BaseMask: this.permissions.baseMask,
OwnerMask: this.permissions.ownerMask,
GroupMask: this.permissions.groupMask,
EveryoneMask: this.permissions.everyoneMask,
NextOwnerMask: this.permissions.nextOwnerMask,
GroupOwned: false,
TransactionID: queryID,
Type: this.type,
InvType: this.inventoryType,
Flags: this.flags,
SaleType: this.saleType,
SalePrice: this.salePrice,
Name: Utils.StringToBuffer(this.name),
Description: Utils.StringToBuffer(this.description),
CreationDate: Math.round(this.created.getTime() / 1000),
CRC: 0,
};
let objSub: Subscription | undefined = undefined;
const agent = this.agent;
const gotObjects: GameObject[] = [];
objSub = this.agent.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) =>
{
if (evt.createSelected && !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 agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true);
}
if (evt.createSelected && !evt.object.claimedForBuild)
{
if (evt.object.itemID !== undefined && evt.object.itemID.equals(this.itemID))
{
evt.object.claimedForBuild = true;
gotObjects.push(evt.object);
}
}
});
// We have no way of knowing when the cluster is finished rezzing, so we just wait for 30 seconds
setTimeout(() =>
{
if (objSub !== undefined)
{
objSub.unsubscribe();
objSub = undefined;
}
if (gotObjects.length > 0)
{
resolve(gotObjects);
}
else
{
reject(new Error('No objects arrived'));
}
}, 30000);
// Move the camera to look directly at prim for faster capture
const camLocation = new Vector3(position);
camLocation.z += (5) + 1;
await this.agent.currentRegion.clientCommands.agent.setCamera(camLocation, position, 256, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
rezInWorld(position: Vector3, objectScale?: Vector3): Promise<GameObject>
{
return new Promise<GameObject>(async (resolve, reject) =>
{
if (this.agent === undefined)
{
reject(new Error('This InventoryItem is local only, so cant rez'));
return;
}
const queryID = UUID.random();
const msg = new RezObjectMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID,
GroupID: UUID.zero()
};
msg.RezData = {
FromTaskID: (this.container instanceof GameObject) ? this.container.FullID : UUID.zero(),
BypassRaycast: 1,
RayStart: position,
RayEnd: position,
RayTargetID: UUID.zero(),
RayEndIsIntersection: false,
RezSelected: true,
RemoveItem: false,
ItemFlags: this.flags,
GroupMask: PermissionMask.All,
EveryoneMask: PermissionMask.All,
NextOwnerMask: PermissionMask.All,
};
msg.InventoryData = {
ItemID: this.itemID,
FolderID: this.parentID,
CreatorID: this.permissions.creator,
OwnerID: this.permissions.owner,
GroupID: this.permissions.group,
BaseMask: this.permissions.baseMask,
OwnerMask: this.permissions.ownerMask,
GroupMask: this.permissions.groupMask,
EveryoneMask: this.permissions.everyoneMask,
NextOwnerMask: this.permissions.nextOwnerMask,
GroupOwned: false,
TransactionID: queryID,
Type: this.type,
InvType: this.inventoryType,
Flags: this.flags,
SaleType: this.saleType,
SalePrice: this.salePrice,
Name: Utils.StringToBuffer(this.name),
Description: Utils.StringToBuffer(this.description),
CreationDate: Math.round(this.created.getTime() / 1000),
CRC: 0,
};
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('Prim never arrived'));
}, 10000);
let claimedPrim = false;
const agent = this.agent;
objSub = this.agent.currentRegion.clientEvents.onNewObjectEvent.subscribe(async (evt: NewObjectEvent) =>
{
if (evt.createSelected && !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 agent.currentRegion.clientCommands.region.resolveObject(evt.object, false, true);
}
if (evt.createSelected && !evt.object.claimedForBuild && !claimedPrim)
{
if (evt.object.itemID !== undefined && evt.object.itemID.equals(this.itemID))
{
if (objSub !== undefined)
{
objSub.unsubscribe();
objSub = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
evt.object.claimedForBuild = true;
claimedPrim = true;
resolve(evt.object);
}
}
});
// Move the camera to look directly at prim for faster capture
let height = 10;
if (objectScale !== undefined)
{
height = objectScale.z;
}
const camLocation = new Vector3(position);
camLocation.z += (height / 2) + 1;
await this.agent.currentRegion.clientCommands.agent.setCamera(camLocation, position, height, new Vector3([-1.0, 0, 0]), new Vector3([0.0, 1.0, 0]));
this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
async renameInTask(task: GameObject, newName: string)
{
this.name = newName;
if (this.agent === undefined)
{
return;
}
const msg = new UpdateTaskInventoryMessage();
msg.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.agent.currentRegion.circuit.sessionID
};
msg.UpdateData = {
Key: 0,
LocalID: task.ID
};
msg.InventoryData = {
ItemID: this.itemID,
FolderID: this.parentID,
CreatorID: this.permissions.creator,
OwnerID: this.permissions.owner,
GroupID: this.permissions.group,
BaseMask: this.permissions.baseMask,
OwnerMask: this.permissions.ownerMask,
GroupMask: this.permissions.groupMask,
EveryoneMask: this.permissions.everyoneMask,
NextOwnerMask: this.permissions.nextOwnerMask,
GroupOwned: this.permissions.groupOwned || false,
TransactionID: UUID.zero(),
Type: this.type,
InvType: this.inventoryType,
Flags: this.flags,
SaleType: this.saleType,
SalePrice: this.salePrice,
Name: Utils.StringToBuffer(this.name),
Description: Utils.StringToBuffer(this.description),
CreationDate: this.created.getTime() / 1000,
CRC: this.getCRC()
};
return this.agent.currentRegion.circuit.waitForAck(this.agent.currentRegion.circuit.sendMessage(msg, PacketFlags.Reliable), 10000);
}
async updateScript(scriptAsset: Buffer): Promise<UUID>
{
if (this.agent === undefined)
{
throw new Error('This item was created locally and can\'t be updated');
}
if (this.container instanceof GameObject)
{
try
{
const result: any = await this.agent.currentRegion.caps.capsPostXML('UpdateScriptTask', {
'item_id': new LLSD.UUID(this.itemID.toString()),
'task_id': new LLSD.UUID(this.container.FullID.toString()),
'is_script_running': true,
'target': 'mono'
});
if (result['uploader'])
{
const uploader = result['uploader'];
const uploadResult: any = await this.agent.currentRegion.caps.capsRequestUpload(uploader, scriptAsset);
if (uploadResult['state'] && uploadResult['state'] === 'complete')
{
return new UUID(uploadResult['new_asset'].toString());
}
}
throw new Error('Asset upload failed');
}
catch (err)
{
console.error(err);
throw err;
}
}
else
{
throw new Error('Agent inventory not supported just yet')
}
}
}

159
lib/classes/LLGesture.ts Normal file
View File

@@ -0,0 +1,159 @@
import { LLGestureStep } from './LLGestureStep';
import { LLGestureStepType } from '../enums/LLGestureStepType';
import { LLGestureAnimationStep } from './LLGestureAnimationStep';
import { UUID } from './UUID';
import { LLGestureSoundStep } from './LLGestureSoundStep';
import { LLGestureWaitStep } from './LLGestureWaitStep';
import { LLGestureChatStep } from './LLGestureChatStep';
export class LLGesture
{
version: number;
key: number;
mask: number;
trigger: string;
replace: string;
steps: LLGestureStep[] = [];
constructor(data?: string)
{
if (data !== undefined)
{
const lines: string[] = data.replace(/\r\n/g, '\n').split('\n');
if (lines.length > 5)
{
this.version = parseInt(lines[0].trim(), 10);
this.key = parseInt(lines[1].trim(), 10);
this.mask = parseInt(lines[2].trim(), 10);
this.trigger = lines[3].trim();
this.replace = lines[4].trim();
const stepCount = parseInt(lines[5].trim(), 10);
let lineNumber = 6;
for (let step = 0; step < stepCount; step++)
{
if (lineNumber >= lines.length)
{
throw new Error('Invalid gesture step - unexpected end of file');
}
const stepType: LLGestureStepType = parseInt(lines[lineNumber++].trim(), 10);
let gestureStep: LLGestureStep | undefined = undefined;
switch (stepType)
{
case LLGestureStepType.Animation:
{
if (lineNumber + 2 >= lines.length)
{
throw new Error('Invalid animation gesture step - unexpected end of file');
}
const animStep = new LLGestureAnimationStep();
animStep.animationName = lines[lineNumber++].trim();
animStep.assetID = new UUID(lines[lineNumber++].trim());
animStep.flags = parseInt(lines[lineNumber++].trim(), 10);
gestureStep = animStep;
break;
}
case LLGestureStepType.Sound:
{
if (lineNumber + 2 >= lines.length)
{
throw new Error('Invalid sound gesture step - unexpected end of file');
}
const soundStep = new LLGestureSoundStep();
soundStep.soundName = lines[lineNumber++].trim();
soundStep.assetID = new UUID(lines[lineNumber++].trim());
soundStep.flags = parseInt(lines[lineNumber++].trim(), 10);
gestureStep = soundStep;
break;
}
case LLGestureStepType.Chat:
{
if (lineNumber + 1 >= lines.length)
{
throw new Error('Invalid chat gesture step - unexpected end of file');
}
const chatStep = new LLGestureChatStep();
chatStep.chatText = lines[lineNumber++].trim();
chatStep.flags = parseInt(lines[lineNumber++].trim(), 10);
gestureStep = chatStep;
break;
}
case LLGestureStepType.Wait:
{
if (lineNumber + 1 >= lines.length)
{
throw new Error('Invalid wait gesture step - unexpected end of file');
}
const waitStep = new LLGestureWaitStep();
waitStep.waitTime = parseFloat(lines[lineNumber++].trim());
waitStep.flags = parseInt(lines[lineNumber++].trim(), 10);
gestureStep = waitStep;
break;
}
default:
throw new Error('Unknown gesture step type: ' + stepType);
}
if (gestureStep !== undefined)
{
this.steps.push(gestureStep);
}
}
}
else
{
throw new Error('Invalid gesture asset - unexpected end of file');
}
}
}
toAsset(): string
{
const lines: string[] = [
String(this.version),
String(this.key),
String(this.mask),
this.trigger,
this.replace,
String(this.steps.length)
];
for (const step of this.steps)
{
lines.push(String(step.stepType));
switch (step.stepType)
{
case LLGestureStepType.Animation:
{
const gStep = step as LLGestureAnimationStep;
lines.push(gStep.animationName);
lines.push(gStep.assetID.toString());
lines.push(String(gStep.flags));
break;
}
case LLGestureStepType.Sound:
{
const gStep = step as LLGestureSoundStep;
lines.push(gStep.soundName);
lines.push(gStep.assetID.toString());
lines.push(String(gStep.flags));
break;
}
case LLGestureStepType.Chat:
{
const gStep = step as LLGestureChatStep;
lines.push(gStep.chatText);
lines.push(String(gStep.flags));
break;
}
case LLGestureStepType.Wait:
{
const gStep = step as LLGestureWaitStep;
lines.push(gStep.waitTime.toFixed(6));
lines.push(String(gStep.flags));
break;
}
}
}
lines.push('\n');
return lines.join('\n');
}
}

View File

@@ -0,0 +1,12 @@
import { LLGestureStep } from './LLGestureStep';
import { LLGestureStepType } from '../enums/LLGestureStepType';
import { UUID } from './UUID';
import { LLGestureAnimationFlags } from '../enums/LLGestureAnimationFlags';
export class LLGestureAnimationStep extends LLGestureStep
{
stepType: LLGestureStepType = LLGestureStepType.Animation;
animationName: string;
assetID: UUID;
flags: LLGestureAnimationFlags = LLGestureAnimationFlags.None;
}

View File

@@ -0,0 +1,10 @@
import { LLGestureStep } from './LLGestureStep';
import { LLGestureStepType } from '../enums/LLGestureStepType';
import { LLGestureChatFlags } from '../enums/LLGestureChatFlags';
export class LLGestureChatStep extends LLGestureStep
{
stepType: LLGestureStepType = LLGestureStepType.Chat;
chatText: string;
flags: LLGestureChatFlags = LLGestureChatFlags.None;
}

View File

@@ -0,0 +1,12 @@
import { LLGestureStep } from './LLGestureStep';
import { LLGestureStepType } from '../enums/LLGestureStepType';
import { UUID } from './UUID';
import { LLGestureSoundFlags } from '../enums/LLGestureSoundFlags';
export class LLGestureSoundStep extends LLGestureStep
{
stepType: LLGestureStepType = LLGestureStepType.Sound;
soundName: string;
assetID: UUID;
flags: LLGestureSoundFlags = LLGestureSoundFlags.None;
}

View File

@@ -0,0 +1,6 @@
import { LLGestureStepType } from '../enums/LLGestureStepType';
export class LLGestureStep
{
stepType: LLGestureStepType
}

View File

@@ -0,0 +1,11 @@
import { LLGestureStep } from './LLGestureStep';
import { LLGestureStepType } from '../enums/LLGestureStepType';
import { UUID } from './UUID';
import { LLGestureWaitFlags } from '../enums/LLGestureWaitFlags';
export class LLGestureWaitStep extends LLGestureStep
{
stepType: LLGestureStepType = LLGestureStepType.Wait;
waitTime: number;
flags: LLGestureWaitFlags = LLGestureWaitFlags.None;
}

182
lib/classes/LLLindenText.ts Normal file
View File

@@ -0,0 +1,182 @@
import { InventoryItem } from './InventoryItem';
export class LLLindenText
{
version: number = 2;
private lineObj: {
lines: string[],
lineNum: number
} = {
lines: [],
lineNum: 0
};
body = '';
embeddedItems: {[key: number]: InventoryItem} = {};
constructor(data?: Buffer)
{
if (data !== undefined)
{
const initial = data.toString('ascii');
this.lineObj.lines = initial.replace(/\r\n/g, '\n').split('\n');
let line = this.getLine();
if (!line.startsWith('Linden text version'))
{
throw new Error('Invalid Linden Text header');
}
this.version = parseInt(this.getLastToken(line), 10);
if (this.version < 1 || this.version > 2)
{
throw new Error('Unsupported Linden Text version');
}
if (this.version === 2)
{
const v2 = data.toString('utf-8');
this.lineObj.lines = v2.replace(/\r\n/g, '\n').split('\n');
}
line = this.getLine();
if (line !== '{')
{
throw new Error('Error parsing Linden Text file');
}
line = this.getLine();
if (line.startsWith('LLEmbeddedItems'))
{
this.parseEmbeddedItems();
line = this.getLine();
}
if (!line.startsWith('Text length'))
{
throw new Error('Error parsing Linden Text file: ' + line);
}
let textLength = parseInt(this.getLastToken(line), 10);
do
{
line = this.getLine();
textLength -= Buffer.byteLength(line);
if (textLength < 0)
{
const extraChars = 0 - textLength;
const rest = line.substr(line.length - extraChars);
line = line.substr(0, line.length - extraChars);
this.lineObj.lines.splice(this.lineObj.lineNum, 0, rest);
textLength = 0;
this.body += line;
}
else
{
this.body += line;
if (textLength > 0)
{
this.body += '\n';
textLength--;
}
}
}
while (textLength > 0);
line = this.getLine();
if (line !== '}')
{
throw new Error('Error parsing Linden Text file');
}
}
}
toAsset(): Buffer
{
const lines: string[] = [];
lines.push('Linden text version ' + this.version);
lines.push('{');
const count = Object.keys(this.embeddedItems).length;
if (count > 0)
{
lines.push('LLEmbeddedItems version 1');
lines.push('{');
lines.push('count ' + String(count));
for (const key of Object.keys(this.embeddedItems))
{
lines.push('{');
lines.push('ext char index ' + key);
lines.push('\tinv_item\t0');
lines.push(this.embeddedItems[parseInt(key, 10)].toAsset('\t'));
lines.push('}');
}
lines.push('}');
}
lines.push('Text length ' + String(Buffer.byteLength(this.body)));
lines.push(this.body + '}\n\0');
if (this.version === 1)
{
return Buffer.from(lines.join('\n'), 'ascii');
}
return Buffer.from(lines.join('\n'), 'utf-8');
}
private parseEmbeddedItems()
{
let line = this.getLine();
if (line !== '{')
{
throw new Error('Invalid LLEmbeddedItems format (no opening brace)');
}
line = this.getLine();
if (!line.startsWith('count'))
{
throw new Error('Invalid LLEmbeddedItems format (no count)');
}
const itemCount = parseInt(this.getLastToken(line), 10);
for (let x = 0; x < itemCount; x++)
{
line = this.getLine();
if (line !== '{')
{
throw new Error('Invalid LLEmbeddedItems format (no item opening brace)');
}
line = this.getLine();
if (!line.startsWith('ext char index'))
{
throw new Error('Invalid LLEmbeddedItems format (no ext char index)');
}
const index = parseInt(this.getLastToken(line), 10);
line = this.getLine();
if (!line.startsWith('inv_item'))
{
throw new Error('Invalid LLEmbeddedItems format (no inv_item)');
}
const item = InventoryItem.fromAsset(this.lineObj);
this.embeddedItems[index] = item;
line = this.getLine();
if (line !== '}')
{
throw new Error('Invalid LLEmbeddedItems format (no closing brace)');
}
}
line = this.getLine();
if (line !== '}')
{
throw new Error('Error parsing Linden Text file');
}
}
private getLastToken(input: string): string
{
const index = input.lastIndexOf(' ');
if (index === -1)
{
return input;
}
else
{
return input.substr(index + 1);
}
}
private getLine(): string
{
return this.lineObj.lines[this.lineObj.lineNum++].trim().replace(/[\t ]+/g, ' ');
}
}

View File

@@ -1,6 +1,8 @@
import { UUID } from './UUID';
import { WearableType } from '../enums/WearableType';
import { SaleType } from '../enums/SaleType';
import { SaleTypeLL } from '../enums/SaleTypeLL';
import { Utils } from './Utils';
export class LLWearable
{
@@ -31,148 +33,165 @@ export class LLWearable
};
saleType: SaleType;
salePrice: number;
constructor(data: string)
constructor(data?: string)
{
const lines: string[] = data.replace(/\r\n/g, '\n').split('\n');
for (let index = 0; index < lines.length; index++)
if (data !== undefined)
{
if (index === 0)
const lines: string[] = data.replace(/\r\n/g, '\n').split('\n');
for (let index = 0; index < lines.length; index++)
{
const header = lines[index].split(' ');
if (header[0] !== 'LLWearable')
if (index === 0)
{
return;
}
}
else if (index === 1)
{
this.name = lines[index];
}
else
{
const parsedLine = this.parseLine(lines[index]);
if (parsedLine.key !== null)
{
switch (parsedLine.key)
const header = lines[index].split(' ');
if (header[0] !== 'LLWearable')
{
case 'base_mask':
this.permission.baseMask = parseInt(parsedLine.value, 16);
break;
case 'owner_mask':
this.permission.ownerMask = parseInt(parsedLine.value, 16);
break;
case 'group_mask':
this.permission.groupMask = parseInt(parsedLine.value, 16);
break;
case 'everyone_mask':
this.permission.everyoneMask = parseInt(parsedLine.value, 16);
break;
case 'next_owner_mask':
this.permission.nextOwnerMask = parseInt(parsedLine.value, 16);
break;
case 'creator_id':
this.permission.creatorID = new UUID(parsedLine.value);
break;
case 'owner_id':
this.permission.ownerID = new UUID(parsedLine.value);
break;
case 'last_owner_id':
this.permission.lastOwnerID = new UUID(parsedLine.value);
break;
case 'group_id':
this.permission.groupID = new UUID(parsedLine.value);
break;
case 'sale_type':
this.saleType = parseInt(parsedLine.value, 10);
break;
case 'sale_price':
this.salePrice = parseInt(parsedLine.value, 10);
break;
case 'type':
this.type = parseInt(parsedLine.value, 10);
break;
case 'parameters':
return;
}
}
else if (index === 1)
{
this.name = lines[index];
}
else
{
const parsedLine = Utils.parseLine(lines[index]);
if (parsedLine.key !== null)
{
switch (parsedLine.key)
{
const num = parseInt(parsedLine.value, 10);
const max = index + num;
for (index; index < max; index++)
{
const paramLine = this.parseLine(lines[index++]);
if (paramLine.key !== null)
case 'base_mask':
this.permission.baseMask = parseInt(parsedLine.value, 16);
break;
case 'owner_mask':
this.permission.ownerMask = parseInt(parsedLine.value, 16);
break;
case 'group_mask':
this.permission.groupMask = parseInt(parsedLine.value, 16);
break;
case 'everyone_mask':
this.permission.everyoneMask = parseInt(parsedLine.value, 16);
break;
case 'next_owner_mask':
this.permission.nextOwnerMask = parseInt(parsedLine.value, 16);
break;
case 'creator_id':
this.permission.creatorID = new UUID(parsedLine.value);
break;
case 'owner_id':
this.permission.ownerID = new UUID(parsedLine.value);
break;
case 'last_owner_id':
this.permission.lastOwnerID = new UUID(parsedLine.value);
break;
case 'group_id':
this.permission.groupID = new UUID(parsedLine.value);
break;
case 'sale_type':
switch (parsedLine.value.trim().toLowerCase())
{
this.parameters[parseInt(paramLine.key, 10)] = parseInt(paramLine.value, 10);
case 'not':
this.saleType = 0;
break;
case 'orig':
this.saleType = 1;
break;
case 'copy':
this.saleType = 2;
break;
case 'cntn':
this.saleType = 3;
break;
default:
console.log('Unrecognised saleType: ' + parsedLine.value.trim().toLowerCase());
}
}
break;
}
case 'textures':
{
const num = parseInt(parsedLine.value, 10);
const max = index + num ;
for (index; index < max; index++)
break;
case 'sale_price':
this.salePrice = parseInt(parsedLine.value, 10);
break;
case 'type':
this.type = parseInt(parsedLine.value, 10);
break;
case 'parameters':
{
const texLine = this.parseLine(lines[index + 1]);
if (texLine.key !== null)
const num = parseInt(parsedLine.value, 10);
const max = index + num;
for (index; index < max; index++)
{
this.textures[parseInt(texLine.key, 10)] = new UUID(texLine.value);
const paramLine = Utils.parseLine(lines[index + 1]);
if (paramLine.key !== null)
{
this.parameters[parseInt(paramLine.key, 10)] = parseFloat(paramLine.value.replace('-.', '-0.'));
}
}
break;
}
break;
case 'textures':
{
const num = parseInt(parsedLine.value, 10);
const max = index + num;
for (index; index < max; index++)
{
const texLine = Utils.parseLine(lines[index + 1]);
if (texLine.key !== null)
{
this.textures[parseInt(texLine.key, 10)] = new UUID(texLine.value);
}
}
break;
}
case 'permissions':
case 'sale_info':
case '{':
case '}':
// ignore
break;
default:
console.log('skipping: ' + lines[index]);
break;
}
case 'permissions':
case 'sale_info':
case '{':
case '}':
// ignore
break;
default:
console.log('skipping: ' + lines[index]);
break;
}
}
}
}
}
private parseLine(line: string): {
'key': string | null,
'value': string
}
toAsset(): string
{
line = line.trim().replace(/[\t]/gu, ' ').trim();
while (line.indexOf('\u0020\u0020') > 0)
const lines: string[] = [
'LLWearable version 22'
];
lines.push(this.name);
lines.push('');
lines.push('\tpermissions 0');
lines.push('\t{');
lines.push('\t\tbase_mask\t' + Utils.numberToFixedHex(this.permission.baseMask));
lines.push('\t\towner_mask\t' + Utils.numberToFixedHex(this.permission.ownerMask));
lines.push('\t\tgroup_mask\t' + Utils.numberToFixedHex(this.permission.groupMask));
lines.push('\t\teveryone_mask\t' + Utils.numberToFixedHex(this.permission.everyoneMask));
lines.push('\t\tnext_owner_mask\t' + Utils.numberToFixedHex(this.permission.nextOwnerMask));
lines.push('\t\tcreator_id\t' + this.permission.creatorID.toString());
lines.push('\t\towner_id\t' + this.permission.ownerID.toString());
lines.push('\t\tlast_owner_id\t' + this.permission.lastOwnerID.toString());
lines.push('\t\tgroup_id\t' + this.permission.groupID.toString());
lines.push('\t}');
lines.push('\tsale_info\t0');
lines.push('\t{');
lines.push('\t\tsale_type\t' + SaleTypeLL[this.saleType]);
lines.push('\t\tsale_price\t' + this.salePrice);
lines.push('\t}');
lines.push('type ' + this.type);
lines.push('parameters ' + Object.keys(this.parameters).length);
for (const num of Object.keys(this.parameters))
{
line = line.replace(/\u0020\u0020/gu, '\u0020');
const val = this.parameters[parseInt(num, 10)];
lines.push(num + (' ' + String(val).replace('-0.', '-.')).replace(' 0.', ' .'));
}
let key: string | null = null;
let value = '';
if (line.length > 2)
lines.push('textures ' + Object.keys(this.textures).length);
for (const num of Object.keys(this.textures))
{
const sep = line.indexOf(' ');
if (sep > 0)
{
key = line.substr(0, sep);
value = line.substr(sep + 1);
}
}
else if (line.length === 1)
{
key = line;
}
else if (line.length > 0)
{
return {
'key': line,
'value': ''
}
}
if (key !== null)
{
key = key.trim();
}
return {
'key': key,
'value': value
const val = this.textures[parseInt(num, 10)];
lines.push(num + ' ' + val);
}
return lines.join('\n') + '\n';
}
}

137
lib/classes/Logger.ts Normal file
View File

@@ -0,0 +1,137 @@
import * as logger from 'winston';
import * as winston from 'winston';
import * as moment from 'moment';
import * as chalk from 'chalk';
import {TransformableInfo} from 'logform';
const formatLevel = function(text: string, level: string)
{
switch (level)
{
case 'warn':
return chalk.yellowBright(text);
case 'error':
return chalk.redBright(text);
case 'debug':
return chalk.green(text);
case 'info':
return chalk.magentaBright(text);
default:
return text;
}
};
const formatMessage = function(text: string, level: string)
{
switch (level)
{
case 'warn':
return chalk.yellowBright(text);
case 'error':
return chalk.redBright(text);
default:
return text;
}
};
const logFormat = winston.format.printf(function(info: TransformableInfo)
{
const logComponents = [
moment().format('YYYY-MM-DD HH:mm:ss'),
'-',
'[' + formatLevel(info.level.toUpperCase(), info.level) + ']',
formatMessage(info.message, info.level)
];
return logComponents.join(' ');
});
logger.configure({
format: logFormat,
silent: false,
transports: [
new winston.transports.Console({
'level': 'debug',
handleExceptions: true
})
],
});
export class Logger
{
private static prefixLevel = 0;
static prefix = '';
static increasePrefixLevel()
{
this.prefixLevel++;
this.generatePrefix();
}
static decreasePrefixLevel()
{
this.prefixLevel--;
this.generatePrefix();
}
static generatePrefix()
{
this.prefix = '';
for (let x = 0; x < this.prefixLevel; x++)
{
this.prefix += ' ';
}
if (this.prefix.length > 0)
{
this.prefix += '... ';
}
}
static Debug(message: string | object)
{
if (typeof message === 'string')
{
message = this.prefix + message;
}
this.Log('debug', message);
}
static Info(message: string | object)
{
if (typeof message === 'string')
{
message = this.prefix + message;
}
this.Log('info', message);
}
static Warn(message: string | object)
{
if (typeof message === 'string')
{
message = this.prefix + message;
}
this.Log('warn', message);
}
static Error(message: string | object)
{
if (typeof message === 'string')
{
message = this.prefix + message;
}
this.Log('error', message);
}
static Log(type: string, message: string | object)
{
if (typeof message === 'object')
{
if (message instanceof Error)
{
message = message.message + '\n\n' + message.stack;
}
else
{
message = JSON.stringify(message);
}
}
logger.log(type, message);
}
}

View File

@@ -0,0 +1,441 @@
import { GameObject } from './public/GameObject';
import { PCode, PrimFlags, UUID } from '..';
import * as LLSD from '@caspertech/llsd';
import { Region } from './Region';
import { skip } from 'rxjs/operators';
import { on } from 'cluster';
import { IResolveJob } from './interfaces/IResolveJob';
import { Subject, Subscription } from 'rxjs';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
export class ObjectResolver
{
private objectsInQueue: {[key: number]: IResolveJob} = {};
private queue: number[] = [];
private maxConcurrency = 128;
private currentlyRunning = false;
private onObjectResolveRan: Subject<GameObject> = new Subject<GameObject>();
constructor(private region: Region)
{
}
resolveObjects(objects: GameObject[], forceResolve: boolean = false, skipInventory = false, log = false): Promise<GameObject[]>
{
return new Promise<GameObject[]>((resolve, reject) =>
{
if (log)
{
// console.log('[RESOLVER] Scanning ' + objects.length + ' objects, skipInventory: ' + skipInventory);
}
// First, create a map of all object IDs
const objs: {[key: number]: GameObject} = {};
const failed: GameObject[] = [];
for (const obj of objects)
{
this.region.objects.populateChildren(obj);
this.scanObject(obj, objs);
}
let amountLeft = Object.keys(objs).length;
if (log)
{
// console.log('[RESOLVER] ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
const queueObject = (id: number) =>
{
if (this.objectsInQueue[id] === undefined)
{
this.objectsInQueue[id] = {
object: objs[id],
skipInventory: skipInventory,
log
};
this.queue.push(id);
}
else if (this.objectsInQueue[id].skipInventory && !skipInventory)
{
this.objectsInQueue[id].skipInventory = true
}
};
const skipped: number[] = [];
for (const obj of Object.keys(objs))
{
const id = parseInt(obj, 10);
const gameObject = objs[id];
if (log)
{
// console.log('ResolvedInventory: ' + gameObject.resolvedInventory + ', skip: ' + skipInventory);
}
if (forceResolve || gameObject.resolvedAt === undefined || gameObject.resolvedAt === 0 || (!skipInventory && !gameObject.resolvedInventory))
{
if (forceResolve)
{
gameObject.resolvedAt = 0;
gameObject.resolveAttempts = 0;
}
queueObject(id);
}
else
{
skipped.push(id);
}
}
for (const id of skipped)
{
delete objs[id];
amountLeft--;
if (log)
{
// console.log('[RESOLVER] Skipping already resolved object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
}
if (Object.keys(objs).length === 0)
{
resolve(failed);
return;
}
let objResolve: Subscription | undefined = undefined;
let objProps: Subscription | undefined = undefined;
const checkObject = (obj: GameObject): boolean =>
{
let done = false;
if (obj.resolvedAt !== undefined && obj.resolvedAt > 0)
{
if (skipInventory || obj.resolvedInventory)
{
amountLeft--;
if (log)
{
// console.log('[RESOLVER] Resolved an object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
done = true;
}
}
if (obj.resolveAttempts > 2)
{
// Give up
amountLeft--;
if (log)
{
// console.log('[RESOLVER] Failed to resolve an object. ' + amountLeft + ' objects remaining to resolve (' + this.queue.length + ' in queue)');
}
failed.push(obj);
done = true;
}
if (done)
{
delete objs[obj.ID];
if (Object.keys(objs).length === 0)
{
if (objResolve !== undefined)
{
objResolve.unsubscribe();
objResolve = undefined;
}
if (objProps !== undefined)
{
objProps.unsubscribe();
objProps = undefined;
}
resolve(failed);
}
}
return done;
};
objResolve = this.onObjectResolveRan.subscribe((obj: GameObject) =>
{
if (objs[obj.ID] !== undefined)
{
if (log)
{
// console.log('Got onObjectResolveRan for 1 object ...');
}
if (!checkObject(obj))
{
if (log)
{
// console.log(' .. Not resolved yet');
}
setTimeout(() =>
{
if (!checkObject(obj))
{
// Requeue
if (log)
{
// console.log(' .. ' + obj.ID + ' still not resolved yet, requeuing');
}
queueObject(obj.ID);
this.run().then(() =>
{
}).catch((err) =>
{
console.error(err);
});
}
}, 10000);
}
}
});
objProps = this.region.clientEvents.onObjectResolvedEvent.subscribe((obj: ObjectResolvedEvent) =>
{
if (objs[obj.object.ID] !== undefined)
{
if (log)
{
// console.log('Got object resolved event for ' + obj.object.ID);
}
if (!checkObject(obj.object))
{
// console.log(' ... Still not resolved yet');
}
}
});
this.run().then(() =>
{
}).catch((err) =>
{
console.error(err);
});
});
}
private scanObject(obj: GameObject, map: {[key: number]: GameObject})
{
const localID = obj.ID;
if (!map[localID])
{
map[localID] = obj;
if (obj.children)
{
for (const child of obj.children)
{
this.scanObject(child, map);
}
}
}
}
private async run()
{
if (this.currentlyRunning)
{
// console.log('Prodded but already running');
return;
}
try
{
// console.log('Running. Queue length: ' + this.queue.length);
while (this.queue.length > 0)
{
const jobs = [];
for (let x = 0; x < this.maxConcurrency && this.queue.length > 0; x++)
{
const objectID = this.queue.shift();
if (objectID !== undefined)
{
jobs.push(this.objectsInQueue[objectID]);
delete this.objectsInQueue[objectID];
}
}
await this.doResolve(jobs);
}
}
catch (error)
{
console.error(error);
}
finally
{
this.currentlyRunning = false;
}
if (this.queue.length > 0)
{
this.run().then(() => {}, (err) =>
{
console.error(err);
});
}
}
private async doResolve(jobs: IResolveJob[])
{
const resolveTime = new Date().getTime() / 1000;
const objectList = [];
let totalRemaining = 0;
try
{
for (const job of jobs)
{
if (job.object.resolvedAt === undefined || job.object.resolvedAt < resolveTime)
{
objectList.push(job.object);
totalRemaining++;
}
}
if (objectList.length > 0)
{
// console.log('Selecting ' + objectList.length + ' objects');
await this.region.clientCommands.region.selectObjects(objectList);
// console.log('Deselecting ' + objectList.length + ' objects');
await this.region.clientCommands.region.deselectObjects(objectList);
for (const chk of objectList)
{
if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime)
{
totalRemaining --;
}
}
}
for (const job of jobs)
{
if (!job.skipInventory)
{
const o = job.object;
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))
{
if (job.log)
{
// console.log('Processing inventory for ' + job.object.ID);
}
try
{
await o.updateInventory();
}
catch (error)
{
if (o.resolveAttempts === undefined)
{
o.resolveAttempts = 0;
}
o.resolveAttempts++;
if (o.FullID !== undefined)
{
console.error('Error downloading task inventory of ' + o.FullID.toString() + ':');
console.error(error);
}
else
{
console.error('Error downloading task inventory of ' + o.ID + ':');
console.error(error);
}
}
}
else
{
if (job.log)
{
// console.log('Skipping inventory for ' + job.object.ID);
}
}
o.resolvedInventory = true;
}
}
}
catch (ignore)
{
console.error(ignore);
}
finally
{
if (totalRemaining < 1)
{
totalRemaining = 0;
for (const obj of objectList)
{
if (obj.resolvedAt === undefined || obj.resolvedAt < resolveTime)
{
totalRemaining++;
}
}
if (totalRemaining > 0)
{
console.error(totalRemaining + ' objects could not be resolved');
}
}
const that = this;
const getCosts = async function(objIDs: UUID[])
{
const result = await that.region.caps.capsPostXML('GetObjectCost', {
'object_ids': objIDs
});
const uuids = Object.keys(result);
for (const key of uuids)
{
const costs = result[key];
try
{
const obj: GameObject = that.region.objects.getObjectByUUID(new UUID(key));
obj.linkPhysicsImpact = parseFloat(costs['linked_set_physics_cost']);
obj.linkResourceImpact = parseFloat(costs['linked_set_resource_cost']);
obj.physicaImpact = parseFloat(costs['physics_cost']);
obj.resourceImpact = parseFloat(costs['resource_cost']);
obj.limitingType = costs['resource_limiting_type'];
obj.landImpact = Math.round(obj.linkPhysicsImpact);
if (obj.linkResourceImpact > obj.linkPhysicsImpact)
{
obj.landImpact = Math.round(obj.linkResourceImpact);
}
obj.calculatedLandImpact = obj.landImpact;
if (obj.Flags !== undefined && obj.Flags & PrimFlags.TemporaryOnRez && obj.limitingType === 'legacy')
{
obj.calculatedLandImpact = 0;
}
}
catch (error)
{}
}
};
let ids: UUID[] = [];
const promises: Promise<void>[] = [];
for (const job of jobs)
{
if (job.object.landImpact === undefined)
{
ids.push(new LLSD.UUID(job.object.FullID));
}
if (ids.length > 255)
{
promises.push(getCosts(ids));
ids = [];
}
}
if (ids.length > 0)
{
promises.push(getCosts(ids));
}
// console.log('Waiting for all');
await Promise.all(promises);
for (const job of jobs)
{
if (job.log)
{
// console.log('Signalling resolve OK for ' + job.object.ID);
}
this.onObjectResolveRan.next(job.object);
}
}
}
}

View File

@@ -173,6 +173,10 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined;
if (obj.IsAttachment && obj.State !== undefined)
{
this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State);
}
this.objectsByUUID[objData.FullID.toString()] = localID;
if (!this.objectsByParent[parentID])
@@ -194,12 +198,10 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
this.insertIntoRtree(obj);
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID])
{
this.requestMissingObject(objData.ParentID);
}
if (obj.ParentID === 0)
{
this.notifyObjectUpdate(newObject, obj);
this.requestMissingObject(objData.ParentID).then(() => {}).catch(() => {});
}
this.notifyObjectUpdate(newObject, obj);
obj.onTextureUpdate.next();
}
}
}
@@ -415,13 +417,15 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
}
o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0;
if (o.IsAttachment && o.State !== undefined)
{
this.objects[localID].attachmentPoint = this.decodeAttachPoint(o.State);
}
this.insertIntoRtree(o);
if (o.ParentID === 0)
{
this.notifyObjectUpdate(newObj, o);
}
this.notifyObjectUpdate(newObj, o);
o.onTextureUpdate.next();
}
}
}
@@ -429,6 +433,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
protected objectUpdateTerse(objectUpdateTerse: ImprovedTerseObjectUpdateMessage)
{
const dilation = objectUpdateTerse.RegionData.TimeDilation / 65535.0;
this.clientEvents.onRegionTimeDilation.next(dilation);
for (let i = 0; i < objectUpdateTerse.ObjectData.length; i++)
{
@@ -479,8 +484,11 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
{
// No idea why the first four bytes are skipped here.
this.objects[localID].TextureEntry = TextureEntry.from(objectData.TextureEntry.slice(4));
this.objects[localID].onTextureUpdate.next();
}
this.insertIntoRtree(this.objects[localID]);
this.notifyTerseUpdate(this.objects[localID]);
}
else
{

View File

@@ -34,6 +34,8 @@ import { CompressedFlags } from '../enums/CompressedFlags';
import { Vector3 } from './Vector3';
import { ObjectPhysicsDataEvent } from '../events/ObjectPhysicsDataEvent';
import { ObjectResolvedEvent } from '../events/ObjectResolvedEvent';
import { Avatar } from './public/Avatar';
import { AttachmentPoint } from '../enums/AttachmentPoint';
export class ObjectStoreLite implements IObjectStore
{
@@ -42,6 +44,7 @@ export class ObjectStoreLite implements IObjectStore
protected objects: { [key: number]: GameObject } = {};
protected objectsByUUID: { [key: string]: number } = {};
protected objectsByParent: { [key: number]: number[] } = {};
protected avatars: {[key: number]: Avatar} = {};
protected clientEvents: ClientEvents;
protected options: BotOptionFlags;
protected requestedObjects: {[key: number]: boolean} = {};
@@ -183,6 +186,11 @@ export class ObjectStoreLite implements IObjectStore
{
delete this.selectedPrimsWithoutUpdate[o.ID];
}
const n = Utils.BufferToStringSimple(obj.Name);
if (n === 'FullPerm')
{
const h = 5;
}
o.creatorID = obj.CreatorID;
o.creationDate = obj.CreationDate;
o.baseMask = obj.BaseMask;
@@ -211,6 +219,8 @@ export class ObjectStoreLite implements IObjectStore
if (!o.resolvedAt)
{
o.resolvedAt = new Date().getTime() / 1000;
}
{
const evt = new ObjectResolvedEvent();
evt.object = o;
this.clientEvents.onObjectResolvedEvent.next(evt);
@@ -343,6 +353,10 @@ export class ObjectStoreLite implements IObjectStore
this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined;
if (obj.IsAttachment && obj.State !== undefined)
{
this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State);
}
if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString())
{
@@ -404,10 +418,7 @@ export class ObjectStoreLite implements IObjectStore
}
}
if (obj.ParentID === 0)
{
this.notifyObjectUpdate(newObject, obj);
}
this.notifyObjectUpdate(newObject, obj);
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID])
{
@@ -416,34 +427,118 @@ export class ObjectStoreLite implements IObjectStore
}
}
protected notifyObjectUpdate(newObject: boolean, obj: GameObject)
protected notifyTerseUpdate(obj: GameObject)
{
if (newObject)
if (this.objects[obj.ID])
{
const newObj = new NewObjectEvent();
newObj.localID = obj.ID;
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()])
if (obj.PCode === PCode.Avatar)
{
this.selectedPrimsWithoutUpdate[obj.ID] = true;
if (this.avatars[obj.ID] !== undefined)
{
this.avatars[obj.ID].processObjectUpdate(obj);
}
else
{
console.warn('Received update for unknown avatar, but not a new object?!');
}
}
this.clientEvents.onNewObjectEvent.next(newObj);
}
else
{
const updObj = new ObjectUpdatedEvent();
updObj.localID = obj.ID;
updObj.objectID = obj.FullID;
updObj.object = obj;
this.clientEvents.onObjectUpdatedEvent.next(updObj);
this.clientEvents.onObjectUpdatedTerseEvent.next(updObj);
}
if (this.pendingObjectProperties[obj.FullID.toString()])
}
protected notifyObjectUpdate(newObject: boolean, obj: GameObject)
{
if (obj.ParentID === 0 || (obj.ParentID !== undefined && this.avatars[obj.ParentID] !== undefined))
{
this.applyObjectProperties(obj, this.pendingObjectProperties[obj.FullID.toString()]);
delete this.pendingObjectProperties[obj.FullID.toString()];
if (newObject)
{
if (obj.PCode === PCode.Avatar)
{
if (this.avatars[obj.ID] === undefined)
{
this.avatars[obj.ID] = Avatar.fromGameObject(obj);
this.clientEvents.onAvatarEnteredRegion.next(this.avatars[obj.ID])
}
}
if (obj.IsAttachment && obj.ParentID !== undefined)
{
if (this.avatars[obj.ParentID] !== undefined)
{
const avatar = this.avatars[obj.ParentID];
let invItemID = UUID.zero();
if (obj.NameValue['AttachItemID'])
{
invItemID = new UUID(obj.NameValue['AttachItemID'].value);
}
this.agent.currentRegion.clientCommands.region.resolveObject(obj, true, false).then(() =>
{
try
{
if (obj.itemID === undefined)
{
obj.itemID = UUID.zero();
}
obj.itemID = invItemID;
if (avatar !== undefined)
{
avatar.addAttachment(obj);
}
}
catch (err)
{
console.error(err);
}
}).catch((err) =>
{
console.error('Failed to resolve new avatar attachment');
});
}
}
const newObj = new NewObjectEvent();
newObj.localID = obj.ID;
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;
}
this.clientEvents.onNewObjectEvent.next(newObj);
}
else
{
if (obj.PCode === PCode.Avatar)
{
if (this.avatars[obj.ID] !== undefined)
{
this.avatars[obj.ID].processObjectUpdate(obj);
}
else
{
console.warn('Received update for unknown avatar, but not a new object?!');
}
}
const updObj = new ObjectUpdatedEvent();
updObj.localID = obj.ID;
updObj.objectID = obj.FullID;
updObj.object = obj;
this.clientEvents.onObjectUpdatedEvent.next(updObj);
}
if (this.pendingObjectProperties[obj.FullID.toString()])
{
this.applyObjectProperties(obj, this.pendingObjectProperties[obj.FullID.toString()]);
delete this.pendingObjectProperties[obj.FullID.toString()];
}
}
}
@@ -605,14 +700,21 @@ export class ObjectStoreLite implements IObjectStore
}
o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0;
if (o.ParentID === 0)
if (o.IsAttachment && o.State !== undefined)
{
this.notifyObjectUpdate(newObj, o);
o.attachmentPoint = this.decodeAttachPoint(o.State);
}
this.notifyObjectUpdate(newObj, o);
}
}
protected decodeAttachPoint(state: number)
{
const mask = 0xf << 4 >>> 0;
return (((state & mask) >>> 4) | ((state & ~mask) << 4)) >>> 0;
}
protected objectUpdateTerse(objectUpdateTerse: ImprovedTerseObjectUpdateMessage)
{ }
@@ -641,11 +743,32 @@ export class ObjectStoreLite implements IObjectStore
}
}
getAvatar(avatarID: UUID)
{
const obj = this.objectsByUUID[avatarID.toString()];
if (obj !== undefined)
{
if (this.avatars[obj] !== undefined)
{
return this.avatars[obj];
}
else
{
throw new Error('Found the UUID in the region, but it doesn\'t appear to be an avatar');
}
}
else
{
throw new Error('Avatar does not exist in the region at the moment');
}
}
deleteObject(objectID: number)
{
if (this.objects[objectID])
{
this.objects[objectID].deleted = true;
const obj = this.objects[objectID];
obj.deleted = true;
if (this.persist)
{
@@ -653,7 +776,22 @@ export class ObjectStoreLite implements IObjectStore
return;
}
// First, kill all children
if (obj.IsAttachment && obj.ParentID !== undefined)
{
if (this.avatars[obj.ParentID] !== undefined)
{
this.avatars[obj.ParentID].removeAttachment(obj);
}
}
if (this.avatars[objectID] !== undefined)
{
this.clientEvents.onAvatarLeftRegion.next(this.avatars[objectID]);
this.avatars[objectID].leftRegion();
delete this.avatars[objectID];
}
// First, kill all children (not the people kind)
if (this.objectsByParent[objectID])
{
for (const childObjID of this.objectsByParent[objectID])
@@ -664,16 +802,15 @@ export class ObjectStoreLite implements IObjectStore
delete this.objectsByParent[objectID];
// Now delete this object
const objct = this.objects[objectID];
const uuid = objct.FullID.toString();
const uuid = obj.FullID.toString();
if (this.objectsByUUID[uuid])
{
delete this.objectsByUUID[uuid];
}
if (objct.ParentID !== undefined)
if (obj.ParentID !== undefined)
{
const parentID = objct.ParentID;
const parentID = obj.ParentID;
if (this.objectsByParent[parentID])
{
const ind = this.objectsByParent[parentID].indexOf(objectID);
@@ -683,9 +820,9 @@ export class ObjectStoreLite implements IObjectStore
}
}
}
if (this.rtree && this.objects[objectID].rtreeEntry !== undefined)
if (this.rtree && obj.rtreeEntry !== undefined)
{
this.rtree.remove(this.objects[objectID].rtreeEntry);
this.rtree.remove(obj.rtreeEntry);
}
delete this.objects[objectID];
}
@@ -767,7 +904,7 @@ export class ObjectStoreLite implements IObjectStore
}
}
private populateChildren(obj: GameObject)
populateChildren(obj: GameObject, resolve = false)
{
if (obj !== undefined)
{
@@ -801,7 +938,7 @@ export class ObjectStoreLite implements IObjectStore
try
{
const parent = this.findParent(go);
if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false) && parent.ParentID === 0)
if (parent.ParentID === 0)
{
const uuid = parent.FullID.toString();

View File

@@ -46,6 +46,7 @@ import { ParcelPropertiesEvent } from '../events/ParcelPropertiesEvent';
import { PacketFlags } from '../enums/PacketFlags';
import { Vector3 } from './Vector3';
import { Vector2 } from './Vector2';
import { ObjectResolver } from './ObjectResolver';
export class Region
{
@@ -144,6 +145,8 @@ export class Region
timeOffset = 0;
resolver: ObjectResolver = new ObjectResolver(this);
private parcelOverlayReceived: {[key: number]: Buffer} = {};
static IDCTColumn16(linein: number[], lineout: number[], column: number)
@@ -604,13 +607,13 @@ export class Region
}
});
const parcelID: string = dwellReply.Data.ParcelID.toString();
let parcel = new Parcel();
let parcel = new Parcel(this);
if (this.parcelsByUUID[parcelID])
{
parcel = this.parcelsByUUID[parcelID];
}
parcel.LocalID = parcelProperties.LocalID;
parcel.ParcelID = dwellReply.Data.ParcelID;
parcel.ParcelID = new UUID(dwellReply.Data.ParcelID.toString());
parcel.RegionDenyAgeUnverified = parcelProperties.RegionDenyTransacted;
parcel.MediaDesc = parcelProperties.MediaDesc;
parcel.MediaHeight = parcelProperties.MediaHeight;
@@ -624,7 +627,7 @@ export class Region
parcel.AnyAVSounds = parcelProperties.AnyAVSounds;
parcel.Area = parcelProperties.Area;
parcel.AuctionID = parcelProperties.AuctionID;
parcel.AuthBuyerID = parcelProperties.AuthBuyerID;
parcel.AuthBuyerID = new UUID(parcelProperties.AuthBuyerID.toString());
parcel.Bitmap = parcelProperties.Bitmap;
parcel.Category = parcelProperties.Category;
parcel.ClaimDate = parcelProperties.ClaimDate;
@@ -632,20 +635,20 @@ export class Region
parcel.Desc = parcelProperties.Desc;
parcel.Dwell = dwellReply.Data.Dwell;
parcel.GroupAVSounds = parcelProperties.GroupAVSounds;
parcel.GroupID = parcelProperties.GroupID;
parcel.GroupID = new UUID(parcelProperties.GroupID.toString());
parcel.GroupPrims = parcelProperties.GroupPrims;
parcel.IsGroupOwned = parcelProperties.IsGroupOwned;
parcel.LandingType = parcelProperties.LandingType;
parcel.MaxPrims = parcelProperties.MaxPrims;
parcel.MediaAutoScale = parcelProperties.MediaAutoScale;
parcel.MediaID = parcelProperties.MediaID;
parcel.MediaID = new UUID(parcelProperties.MediaID.toString());
parcel.MediaURL = parcelProperties.MediaURL;
parcel.MusicURL = parcelProperties.MusicURL;
parcel.Name = parcelProperties.Name;
parcel.OtherCleanTime = parcelProperties.OtherCleanTime;
parcel.OtherCount = parcelProperties.OtherCount;
parcel.OtherPrims = parcelProperties.OtherPrims;
parcel.OwnerID = parcelProperties.OwnerID;
parcel.OwnerID = new UUID(parcelProperties.OwnerID.toString());
parcel.OwnerPrims = parcelProperties.OwnerPrims;
parcel.ParcelFlags = parcelProperties.ParcelFlags;
parcel.ParcelPrimBonus = parcelProperties.ParcelPrimBonus;
@@ -666,7 +669,7 @@ export class Region
parcel.SimWideMaxPrims = parcelProperties.SimWideMaxPrims;
parcel.SimWideTotalPrims = parcelProperties.SimWideTotalPrims;
parcel.SnapSelection = parcelProperties.SnapSelection;
parcel.SnapshotID = parcelProperties.SnapshotID;
parcel.SnapshotID = new UUID(parcelProperties.SnapshotID.toString());
parcel.Status = parcelProperties.Status;
parcel.TotalPrims = parcelProperties.TotalPrims;
parcel.UserLocation = parcelProperties.UserLocation;

View File

@@ -0,0 +1,6 @@
import { TarFile } from './TarFile';
export class TarArchive
{
files: TarFile[] = [];
}

44
lib/classes/TarFile.ts Normal file
View File

@@ -0,0 +1,44 @@
import * as fs from 'fs';
export class TarFile
{
fileName: string;
fileMode: number;
userID: number;
groupID: number;
modifyTime: Date;
linkIndicator: number;
linkedFile: string;
offset: number;
fileSize: number;
archiveFile: string;
read(): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
fs.open(this.archiveFile, 'r', (err: Error | null, fd: number) =>
{
if (err)
{
reject(err);
}
else
{
const buf = Buffer.alloc(this.fileSize);
fs.read(fd, buf, 0, this.fileSize, this.offset, (err2: Error | null, bytesRead: number, buffer: Buffer) =>
{
if (err2)
{
reject(err2);
}
else
{
resolve(buffer);
}
})
}
});
});
}
}

201
lib/classes/TarReader.ts Normal file
View File

@@ -0,0 +1,201 @@
import { TarFile } from './TarFile';
import { TarArchive } from './TarArchive';
import { Readable } from 'stream';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as uuid from 'uuid';
export class TarReader
{
private outFile: string;
constructor(private fileName: string)
{
}
parse(stream: Readable): Promise<TarArchive>
{
return new Promise<TarArchive>((resolve, reject) =>
{
let longName = false;
let readState = 0; // 0 = waiting for header, 1 = reading file, 2 = padding, 3 = end of file
let queuedChunks: Buffer[] = [];
let fileChunks: Buffer[] = [];
let queuedBytes = 0;
let remainingBytes = 0;
let longNameStr: string | undefined = undefined;
let fileSize = 0;
let paddingSize = 0;
let pos = 0;
let fileCount = 0;
const archive = new TarArchive();
this.outFile = path.resolve(os.tmpdir() + '/' + uuid.v4() + '.tar');
const outStream = fs.openSync(this.outFile, 'w');
stream.on('data', (chunk: Buffer) =>
{
fs.writeSync(outStream, chunk);
let goAgain = false;
do
{
goAgain = false;
if (readState === 1)
{
if (chunk.length > remainingBytes)
{
const wantedBytes = chunk.length - remainingBytes;
if (longName)
{
fileChunks.push(chunk.slice(0, chunk.length - wantedBytes));
}
queuedChunks = [chunk.slice(chunk.length - wantedBytes)];
queuedBytes = queuedChunks[0].length;
remainingBytes = 0;
}
else
{
remainingBytes -= chunk.length;
if (longName)
{
fileChunks.push(chunk);
}
}
}
else
{
queuedChunks.push(chunk);
queuedBytes += chunk.length;
}
if (readState === 0)
{
if (queuedBytes >= 512)
{
const buf = Buffer.concat(queuedChunks);
const header = buf.slice(0, 512);
queuedChunks = [buf.slice(512)];
queuedBytes = queuedChunks[0].length;
let hdrFileName = this.trimEntry(header.slice(0, 100));
console.log('Filename: ' + hdrFileName);
const hdrFileMode = this.decodeOctal(header.slice(100, 100 + 8));
const hdrUserID = this.decodeOctal(header.slice(108, 108 + 8));
const hdrGroupID = this.decodeOctal(header.slice(116, 116 + 8));
fileSize = this.decodeOctal(header.slice(124, 124 + 12));
const hdrModifyTime = this.decodeOctal(header.slice(136, 136 + 12));
const checksum = this.decodeOctal(header.slice(148, 148 + 8));
const linkIndicator = header[156];
const linkedFile = this.trimEntry(header.slice(157, 157 + 100));
paddingSize = (Math.ceil(fileSize / 512) * 512) - fileSize;
// Check CRC
let sum = 8 * 32;
for (let x = 0; x < 512; x++)
{
if (x < 148 || x > 155)
{
sum += header[x];
}
}
fileCount++;
if (sum !== checksum)
{
readState = 3;
continue;
}
if (linkIndicator === 76)
{
longName = true;
}
else
{
if (longNameStr !== undefined)
{
hdrFileName = longNameStr;
longNameStr = undefined;
longName = false;
}
const file = new TarFile();
file.archiveFile = this.outFile;
file.fileName = hdrFileName;
file.fileMode = hdrFileMode;
file.userID = hdrUserID;
file.groupID = hdrGroupID;
file.modifyTime = new Date(hdrModifyTime * 1000);
file.linkIndicator = linkIndicator;
file.linkedFile = linkedFile;
file.offset = pos + 512;
file.fileSize = fileSize;
archive.files.push(file);
}
remainingBytes = fileSize;
readState = 1;
goAgain = true;
chunk = queuedChunks[0];
queuedBytes = 0;
queuedChunks = [];
pos += 512;
continue;
}
}
if (readState === 1 && remainingBytes === 0)
{
if (longName)
{
longNameStr = Buffer.concat(fileChunks).toString('ascii');
fileChunks = [];
}
pos += fileSize;
readState = 2;
}
if (readState === 2 && queuedBytes >= paddingSize)
{
const buf = Buffer.concat(queuedChunks);
queuedChunks = [buf.slice(paddingSize)];
queuedBytes = queuedChunks[0].length;
readState = 0;
chunk = Buffer.alloc(0);
goAgain = true;
pos += paddingSize;
}
}
while (goAgain);
}).on('end', () =>
{
if ((readState !== 0 && readState !== 3) || queuedBytes > 0)
{
console.warn('Warning: Garbage at end of file');
}
fs.closeSync(outStream);
resolve(archive);
}).on('error', (err) =>
{
reject(err);
});
});
}
close()
{
fs.unlinkSync(this.outFile);
this.outFile = '';
}
private trimEntry(buf: Buffer)
{
let end = buf.indexOf('\0');
if (end === -1)
{
end = buf.length - 1;
}
return buf.slice(0, end).toString('ascii');
}
private decodeOctal(buf: Buffer)
{
const str = this.trimEntry(buf);
return parseInt(str, 8);
}
}

143
lib/classes/TarWriter.ts Normal file
View File

@@ -0,0 +1,143 @@
import * as fs from 'fs';
import { Readable, Transform } from 'stream';
export class TarWriter extends Transform
{
private thisFileSize = 0;
private realPath: string;
private fileActive = false;
async newFile(archivePath: string, realPath: string)
{
if (this.fileActive)
{
this.endFile();
}
const stat = fs.statSync(realPath);
//if (archivePath.length > 100)
//{
const buf = Buffer.from(archivePath, 'ascii');
this.writeHeader(
this.chopString('././@LongName', 100),
stat.mode,
stat.uid,
stat.gid,
buf.length,
stat.mtime,
'L'
);
this.thisFileSize = buf.length;
await this.pipeFromBuffer(buf);
this.endFile();
//}
this.writeHeader(
this.chopString(archivePath, 100),
stat.mode,
stat.uid,
stat.gid,
stat.size,
stat.mtime,
'0'
);
this.thisFileSize = stat.size;
this.fileActive = true;
}
async pipeFromBuffer(buf: Buffer): Promise<void>
{
const readableInstanceStream = new Readable({
read()
{
this.push(buf);
this.push(null);
}
});
return this.pipeFrom(readableInstanceStream);
}
pipeFrom(str: Readable): Promise<void>
{
return new Promise((resolve, reject) =>
{
str.on('error', (err) =>
{
reject(err);
});
str.on('end', () =>
{
resolve();
});
str.pipe(this, {end: false});
});
}
private async writeHeader(fileName: string, mode: number, uid: number, gid: number, fileSize: number, mTime: Date, fileType: string)
{
const header = Buffer.alloc(512);
const name = this.chopString(fileName, 100);
header.write(name, 0, (name.length <= 100 ? name.length : 100));
this.octalBuf(mode, 8).copy(header, 100);
this.octalBuf(uid, 8).copy(header, 108);
this.octalBuf(gid, 8).copy(header, 116);
this.octalBuf(fileSize, 12).copy(header, 124);
this.octalBuf(Math.floor(mTime.getTime() / 1000), 12).copy(header, 136);
header.write(fileType, 156, 1);
let sum = 8 * 32;
for (let x = 0; x < 512; x++)
{
if (x < 148 || x > 155)
{
sum += header.readUInt8(x);
}
}
let sumStr = this.octalString(sum, 6);
while (sumStr.length < 6)
{
sumStr = '0' + sumStr;
}
sumStr += '\0 ';
header.write(sumStr, 148, sumStr.length);
return this.pipeFromBuffer(header);
}
async endFile()
{
const finalSize = Math.ceil(this.thisFileSize / 512) * 512;
const remainingSize = finalSize - this.thisFileSize;
const buf = Buffer.alloc(remainingSize);
await this.pipeFromBuffer(buf);
this.fileActive = false;
}
public _transform(chunk: any, encoding: string, callback: (error?: Error, data?: any) => void): void
{
this.push(chunk, encoding);
callback();
}
private chopString(str: string, maxLength: number): string
{
return str.substr(0, maxLength - 1);
}
private octalBuf(num: number, length: number): Buffer
{
const buf = Buffer.alloc(length - 1, '0');
const result = this.chopString(Math.floor(num).toString(8), length);
buf.write(result, length - (result.length + 1), result.length);
return buf;
}
private octalString(num: number, length: number): string
{
return this.octalBuf(num, length).toString('ascii');
}
}

View File

@@ -91,6 +91,10 @@ export class UUID
+ hexString.substr(16, 4) + '-'
+ hexString.substr(20, 12));
}
else if (typeof buf === 'object' && buf.toString !== undefined)
{
this.setUUID(buf.toString());
}
else
{
console.error('Can\'t accept UUIDs of type ' + typeof buf);
@@ -125,6 +129,11 @@ export class UUID
binary.copy(buf, pos, 0);
}
public isZero(): boolean
{
return (this.mUUID === '00000000-0000-0000-0000-000000000000');
}
public equals(cmp: UUID | string): boolean
{
if (typeof cmp === 'string')
@@ -133,6 +142,10 @@ export class UUID
}
else
{
if (cmp.equals === undefined)
{
throw new Error(cmp.constructor.name + ' is not a UUID');
}
return cmp.equals(this.mUUID);
}
}

View File

@@ -3,10 +3,15 @@ import { Quaternion } from './Quaternion';
import { GlobalPosition } from './public/interfaces/GlobalPosition';
import { HTTPAssets } from '../enums/HTTPAssets';
import { Vector3 } from './Vector3';
import { Subject } from 'rxjs';
import { Subject, Subscription } from 'rxjs';
import { AssetType } from '../enums/AssetType';
import { InventoryTypeLL } from '../enums/InventoryTypeLL';
import { InventoryType } from '../enums/InventoryType';
import * as zlib from 'zlib';
import { FilterResponse } from '../enums/FilterResponse';
import Timeout = NodeJS.Timeout;
import * as xml2js from 'xml2js';
import { XMLElement } from 'xmlbuilder';
import { Logger } from './Logger';
export class Utils
{
@@ -134,6 +139,46 @@ export class Utils
};
}
static InventoryTypeToLLInventoryType(type: InventoryType): string
{
switch (type)
{
case InventoryType.Texture:
return 'texture';
case InventoryType.Sound:
return 'sound';
case InventoryType.CallingCard:
return 'callcard';
case InventoryType.Landmark:
return 'landmark';
case InventoryType.Object:
return 'object';
case InventoryType.Notecard:
return 'notecard';
case InventoryType.Category:
return 'category';
case InventoryType.RootCategory:
return 'root';
case InventoryType.Script:
return 'script';
case InventoryType.Snapshot:
return 'snapshot';
case InventoryType.Attachment:
return 'attach';
case InventoryType.Wearable:
return 'wearable';
case InventoryType.Animation:
return 'animation';
case InventoryType.Gesture:
return 'gesture';
case InventoryType.Mesh:
return 'mesh';
default:
console.error('Unknown inventory type: ' + InventoryType[type]);
return 'texture';
}
}
static HTTPAssetTypeToAssetType(HTTPAssetType: string): AssetType
{
switch (HTTPAssetType)
@@ -171,38 +216,76 @@ export class Utils
}
}
static HTTPAssetTypeToInventoryType(HTTPAssetType: string): InventoryTypeLL
static AssetTypeToHTTPAssetType(assetType: AssetType): HTTPAssets
{
switch (assetType)
{
case AssetType.Texture:
return HTTPAssets.ASSET_TEXTURE;
case AssetType.Sound:
return HTTPAssets.ASSET_SOUND;
case AssetType.Animation:
return HTTPAssets.ASSET_ANIMATION;
case AssetType.Gesture:
return HTTPAssets.ASSET_GESTURE;
case AssetType.Landmark:
return HTTPAssets.ASSET_LANDMARK;
case AssetType.CallingCard:
return HTTPAssets.ASSET_CALLINGCARD;
case AssetType.Script:
return HTTPAssets.ASSET_SCRIPT;
case AssetType.Clothing:
return HTTPAssets.ASSET_CLOTHING;
case AssetType.Object:
return HTTPAssets.ASSET_OBJECT;
case AssetType.Notecard:
return HTTPAssets.ASSET_NOTECARD;
case AssetType.LSLText:
return HTTPAssets.ASSET_LSL_TEXT;
case AssetType.LSLBytecode:
return HTTPAssets.ASSET_LSL_BYTECODE;
case AssetType.Bodypart:
return HTTPAssets.ASSET_BODYPART;
case AssetType.Mesh:
return HTTPAssets.ASSET_MESH;
default:
return HTTPAssets.ASSET_TEXTURE;
}
}
static HTTPAssetTypeToInventoryType(HTTPAssetType: string): InventoryType
{
switch (HTTPAssetType)
{
case HTTPAssets.ASSET_TEXTURE:
return InventoryTypeLL.texture;
return InventoryType.Texture;
case HTTPAssets.ASSET_SOUND:
return InventoryTypeLL.sound;
return InventoryType.Sound;
case HTTPAssets.ASSET_ANIMATION:
return InventoryTypeLL.animation;
return InventoryType.Animation;
case HTTPAssets.ASSET_GESTURE:
return InventoryTypeLL.gesture;
return InventoryType.Gesture;
case HTTPAssets.ASSET_LANDMARK:
return InventoryTypeLL.landmark;
return InventoryType.Landmark;
case HTTPAssets.ASSET_CALLINGCARD:
return InventoryTypeLL.callcard;
return InventoryType.CallingCard;
case HTTPAssets.ASSET_SCRIPT:
return InventoryTypeLL.script;
return InventoryType.Script;
case HTTPAssets.ASSET_CLOTHING:
return InventoryTypeLL.wearable;
return InventoryType.Wearable;
case HTTPAssets.ASSET_OBJECT:
return InventoryTypeLL.object;
return InventoryType.Object;
case HTTPAssets.ASSET_NOTECARD:
return InventoryTypeLL.notecard;
return InventoryType.Notecard;
case HTTPAssets.ASSET_LSL_TEXT:
return InventoryTypeLL.script;
return InventoryType.Script;
case HTTPAssets.ASSET_LSL_BYTECODE:
return InventoryTypeLL.script;
return InventoryType.Script;
case HTTPAssets.ASSET_BODYPART:
return InventoryTypeLL.wearable;
return InventoryType.Wearable;
case HTTPAssets.ASSET_MESH:
return InventoryTypeLL.mesh;
return InventoryType.Mesh;
default:
return 0;
}
@@ -290,13 +373,13 @@ export class Utils
static Base64EncodeString(str: string): string
{
const buff = new Buffer(str, 'utf8');
const buff = Buffer.from(str, 'utf8');
return buff.toString('base64');
}
static Base64DecodeString(str: string): string
{
const buff = new Buffer(str, 'base64');
const buff = Buffer.from(str, 'base64');
return buff.toString('utf8');
}
@@ -383,6 +466,52 @@ export class Utils
return Math.floor(((Utils.IEEERemainder(rotation, Utils.TWO_PI) / Utils.TWO_PI) * 32768.0) + 0.5);
}
static OctetsToUInt32BE(octets: number[])
{
const buf = Buffer.allocUnsafe(4);
let pos = 0;
for (let x = octets.length - 4; x < octets.length; x++)
{
if (x >= 0)
{
buf.writeUInt8(octets[x], pos++);
}
else
{
pos++;
}
}
return buf.readUInt32BE(0);
}
static OctetsToUInt32LE(octets: number[])
{
const buf = Buffer.allocUnsafe(4);
let pos = 0;
for (let x = octets.length - 4; x < octets.length; x++)
{
if (x >= 0)
{
buf.writeUInt8(octets[x], pos++);
}
else
{
pos++;
}
}
return buf.readUInt32LE(0);
}
static numberToFixedHex(num: number)
{
let str = num.toString(16);
while (str.length < 8)
{
str = '0' + str;
}
return str;
}
static TEGlowByte(glow: number)
{
return (glow * 255.0);
@@ -535,6 +664,7 @@ export class Utils
{
const originalConcurrency = concurrency;
const promiseQueue: (() => Promise<T>)[] = [];
Logger.Info('PromiseConcurrent: ' + promiseQueue.length + ' in queue. Concurrency: ' + concurrency);
for (const promise of promises)
{
promiseQueue.push(promise);
@@ -587,13 +717,16 @@ export class Utils
concurrency++;
slotAvailable.next();
});
timeo = setTimeout(() =>
if (timeout > 0)
{
timedOut = true;
errors.push(new Error('Promise timed out'));
concurrency++;
slotAvailable.next();
}, timeout);
timeo = setTimeout(() =>
{
timedOut = true;
errors.push(new Error('Promise timed out'));
concurrency++;
slotAvailable.next();
}, timeout);
}
}
while (promiseQueue.length > 0)
@@ -618,4 +751,195 @@ export class Utils
resolve({results: results, errors: errors});
});
}
static waitFor(timeout: number): Promise<void>
{
return new Promise<void>((resolve, reject) =>
{
setTimeout(() =>
{
resolve();
}, timeout);
})
}
static getFromXMLJS(obj: any, param: string): any
{
if (obj[param] === undefined)
{
return undefined;
}
let retParam;
if (Array.isArray(obj[param]))
{
retParam = obj[param][0];
}
else
{
retParam = obj[param];
}
if (typeof retParam === 'string')
{
if (retParam.toLowerCase() === 'false')
{
return false;
}
if (retParam.toLowerCase() === 'true')
{
return true;
}
const numVar = parseInt(retParam, 10);
if (numVar >= Number.MIN_SAFE_INTEGER && numVar <= Number.MAX_SAFE_INTEGER && String(numVar) === retParam)
{
return numVar
}
}
return retParam;
}
static inflate(buf: Buffer): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
zlib.inflate(buf, (error: (Error| null), result: Buffer) =>
{
if (error)
{
reject(error)
}
else
{
resolve(result);
}
})
});
}
static deflate(buf: Buffer): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
zlib.deflate(buf, { level: 9}, (error: (Error| null), result: Buffer) =>
{
if (error)
{
reject(error)
}
else
{
resolve(result);
}
})
});
}
static waitOrTimeOut<T>(subject: Subject<T>, timeout?: number, callback?: (msg: T) => FilterResponse): Promise<T>
{
return new Promise<T>((resolve, reject) =>
{
let timer: Timeout | undefined = undefined;
let subs: Subscription | undefined = undefined;
subs = subject.subscribe((result: T) =>
{
if (callback !== undefined)
{
const accepted = callback(result);
if (accepted !== FilterResponse.Finish)
{
return;
}
}
if (timer !== undefined)
{
clearTimeout(timer);
timer = undefined;
}
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
resolve(result);
});
if (timeout !== undefined)
{
timer = setTimeout(() =>
{
if (timer !== undefined)
{
clearTimeout(timer);
timer = undefined;
}
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
reject(new Error('Timeout'));
}, timeout);
}
})
}
static parseLine(line: string): {
'key': string | null,
'value': string
}
{
line = line.trim().replace(/[\t]/gu, ' ').trim();
while (line.indexOf('\u0020\u0020') > 0)
{
line = line.replace(/\u0020\u0020/gu, '\u0020');
}
let key: string | null = null;
let value = '';
if (line.length > 2)
{
const sep = line.indexOf(' ');
if (sep > 0)
{
key = line.substr(0, sep);
value = line.substr(sep + 1);
}
}
else if (line.length === 1)
{
key = line;
}
else if (line.length > 0)
{
return {
'key': line,
'value': ''
}
}
if (key !== null)
{
key = key.trim();
}
return {
'key': key,
'value': value
}
}
static sanitizePath(input: string)
{
return input.replace(/[^a-z0-9]/gi, '').replace(/ /gi, '_');
}
static parseXML(input: string): Promise<any>
{
return new Promise<any>((resolve, reject) =>
{
xml2js.parseString(input, (err: Error, result: any) =>
{
if (err)
{
reject(err);
}
else
{
resolve(result);
}
});
});
}
}

View File

@@ -10,6 +10,7 @@ import { AvatarPropertiesReplyMessage } from '../messages/AvatarPropertiesReply'
import { AvatarPropertiesRequestMessage } from '../messages/AvatarPropertiesRequest';
import { AvatarPropertiesReplyEvent } from '../../events/AvatarPropertiesReplyEvent';
import { Subscription } from 'rxjs';
import { Avatar } from '../public/Avatar';
export class AgentCommands extends CommandsBase
{
@@ -70,11 +71,16 @@ export class AgentCommands extends CommandsBase
this.agent.sendAgentUpdate();
}
waitForAppearanceSet(timeout: number = 10000): Promise<void>
async getWearables()
{
return this.agent.getWearables();
}
waitForAppearanceComplete(timeout: number = 30000): Promise<void>
{
return new Promise((resolve, reject) =>
{
if (this.agent.appearanceSet)
if (this.agent.appearanceComplete)
{
resolve();
}
@@ -82,7 +88,7 @@ export class AgentCommands extends CommandsBase
{
let appearanceSubscription: Subscription | undefined;
let timeoutTimer: number | undefined;
appearanceSubscription = this.agent.appearanceSetEvent.subscribe(() =>
appearanceSubscription = this.agent.appearanceCompleteEvent.subscribe(() =>
{
if (timeoutTimer !== undefined)
{
@@ -110,7 +116,7 @@ export class AgentCommands extends CommandsBase
reject(new Error('Timeout'));
}
}, timeout) as any as number;
if (this.agent.appearanceSet)
if (this.agent.appearanceComplete)
{
if (appearanceSubscription !== undefined)
{
@@ -128,6 +134,19 @@ export class AgentCommands extends CommandsBase
});
}
getAvatar(avatarID: UUID | string = UUID.zero()): Avatar
{
if (typeof avatarID === 'string')
{
avatarID = new UUID(avatarID);
}
else if (avatarID.isZero())
{
avatarID = this.agent.agentID;
}
return this.currentRegion.objects.getAvatar(avatarID);
}
async getAvatarProperties(avatarID: UUID | string): Promise<AvatarPropertiesReplyEvent>
{
if (typeof avatarID === 'string')

View File

@@ -2,10 +2,8 @@ import { CommandsBase } from './CommandsBase';
import { UUID } from '../UUID';
import * as LLSD from '@caspertech/llsd';
import { Utils } from '../Utils';
import { PermissionMask } from '../../enums/PermissionMask';
import * as zlib from 'zlib';
import { ZlibOptions } from 'zlib';
import { Color4 } from '../Color4';
import { TransferRequestMessage } from '../messages/TransferRequest';
import { TransferChannelType } from '../../enums/TransferChannelType';
import { TransferSourceType } from '../../enums/TransferSourceTypes';
@@ -18,18 +16,19 @@ import { AssetType } from '../../enums/AssetType';
import { PacketFlags } from '../../enums/PacketFlags';
import { TransferStatus } from '../../enums/TransferStatus';
import { Material } from '../public/Material';
import { LLMesh } from '../public/LLMesh';
import { FolderType } from '../../enums/FolderType';
import { HTTPAssets } from '../../enums/HTTPAssets';
import { InventoryFolder } from '../InventoryFolder';
import { InventoryItem } from '../InventoryItem';
import { CreateInventoryItemMessage } from '../messages/CreateInventoryItem';
import { WearableType } from '../../enums/WearableType';
import { UpdateCreateInventoryItemMessage } from '../messages/UpdateCreateInventoryItem';
import { BulkUpdateInventoryEvent } from '../../events/BulkUpdateInventoryEvent';
import { FilterResponse } from '../../enums/FilterResponse';
import { LLLindenText } from '../LLLindenText';
import { Logger } from '../Logger';
import { Subscription } from 'rxjs';
export class AssetCommands extends CommandsBase
{
private callbackID: number = 1;
private callbackID = 0;
async downloadAsset(type: HTTPAssets, uuid: UUID | string): Promise<Buffer>
{
if (typeof uuid === 'string')
@@ -59,6 +58,49 @@ export class AssetCommands extends CommandsBase
}
}
async copyInventoryFromNotecard(notecardID: UUID, folder: InventoryFolder, itemID: UUID, objectID: UUID = UUID.zero()): Promise<InventoryItem>
{
const gotCap = await this.currentRegion.caps.isCapAvailable('CopyInventoryFromNotecard');
if (gotCap)
{
const callbackID = Math.floor(Math.random() * 2147483647);
const request = {
'callback-id': callbackID,
'folder-id': new LLSD.UUID(folder.folderID.toString()),
'item-id': new LLSD.UUID(itemID.toString()),
'notecard-id': new LLSD.UUID(notecardID.toString()),
'object-id': new LLSD.UUID(objectID.toString())
};
this.currentRegion.caps.capsPostXML('CopyInventoryFromNotecard', request).then(() => {}).catch((err) =>
{
throw err;
});
const evt: BulkUpdateInventoryEvent = await Utils.waitOrTimeOut<BulkUpdateInventoryEvent>(this.currentRegion.clientEvents.onBulkUpdateInventoryEvent, 10000, (event: BulkUpdateInventoryEvent) =>
{
for (const item of event.itemData)
{
if (item.callbackID === callbackID)
{
return FilterResponse.Finish;
}
}
return FilterResponse.NoMatch;
});
for (const item of evt.itemData)
{
if (item.callbackID === callbackID)
{
return item;
}
}
throw new Error('No match');
}
else
{
throw new Error('CopyInventoryFromNotecard cap not available');
}
}
transfer(channelType: TransferChannelType, sourceType: TransferSourceType, priority: boolean, transferParams: Buffer, outAssetID?: { assetID: UUID }): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
@@ -72,12 +114,45 @@ export class AssetCommands extends CommandsBase
Priority: 100.0 + (priority ? 1.0 : 0.0),
Params: transferParams
};
this.circuit.sendMessage(msg, PacketFlags.Reliable);
let gotInfo = true;
let expectedSize = 0;
const packets: { [key: number]: Buffer } = {};
const subscription = this.circuit.subscribeToMessages([
let subscription: Subscription | undefined = undefined;
let timeout: number | undefined;
function cleanup()
{
if (subscription !== undefined)
{
subscription.unsubscribe();
subscription = undefined;
}
if (timeout !== undefined)
{
clearTimeout(timeout);
timeout = undefined;
}
}
function placeTimeout()
{
timeout = setTimeout(() =>
{
cleanup();
reject(new Error('Timeout'));
}, 10000) as any as number;
}
function resetTimeout()
{
if (timeout !== undefined)
{
clearTimeout(timeout);
}
placeTimeout();
}
subscription = this.circuit.subscribeToMessages([
Message.TransferInfo,
Message.TransferAbort,
Message.TransferPacket
@@ -90,26 +165,31 @@ export class AssetCommands extends CommandsBase
case Message.TransferPacket:
{
const messg = packet.message as TransferPacketMessage;
if (!messg.TransferData.TransferID.equals(transferID))
{
return;
}
resetTimeout();
packets[messg.TransferData.Packet] = messg.TransferData.Data;
switch (messg.TransferData.Status)
{
case TransferStatus.Abort:
subscription.unsubscribe();
cleanup();
reject(new Error('Transfer Aborted'));
break;
case TransferStatus.Error:
subscription.unsubscribe();
cleanup();
reject(new Error('Error'));
break;
case TransferStatus.Skip:
console.error('TransferPacket: Skip! not sure what this means');
break;
case TransferStatus.InsufficientPermissions:
subscription.unsubscribe();
cleanup();
reject(new Error('Insufficient Permissions'));
break;
case TransferStatus.NotFound:
subscription.unsubscribe();
cleanup();
reject(new Error('Not Found'));
break;
}
@@ -122,6 +202,7 @@ export class AssetCommands extends CommandsBase
{
return;
}
resetTimeout();
const status = messg.TransferInfo.Status as TransferStatus;
switch (status)
{
@@ -134,23 +215,23 @@ export class AssetCommands extends CommandsBase
}
break;
case TransferStatus.Abort:
subscription.unsubscribe();
cleanup();
reject(new Error('Transfer Aborted'));
break;
case TransferStatus.Error:
subscription.unsubscribe();
reject(new Error('Error'));
cleanup();
reject(new Error('Error downloading asset'));
// See if we get anything else
break;
case TransferStatus.Skip:
console.error('TransferInfo: Skip! not sure what this means');
break;
case TransferStatus.InsufficientPermissions:
subscription.unsubscribe();
cleanup();
reject(new Error('Insufficient Permissions'));
break;
case TransferStatus.NotFound:
subscription.unsubscribe();
cleanup();
reject(new Error('Not Found'));
break;
}
@@ -164,7 +245,8 @@ export class AssetCommands extends CommandsBase
{
return;
}
subscription.unsubscribe();
resetTimeout();
cleanup();
reject(new Error('Transfer Aborted'));
return;
}
@@ -188,24 +270,33 @@ export class AssetCommands extends CommandsBase
{
buffers.push(packets[parseInt(pn, 10)]);
}
subscription.unsubscribe();
cleanup();
resolve(Buffer.concat(buffers));
}
}
}
catch (error)
{
subscription.unsubscribe();
cleanup();
reject(error);
}
});
placeTimeout();
this.circuit.sendMessage(msg, PacketFlags.Reliable);
});
}
downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }): Promise<Buffer>
downloadInventoryAsset(itemID: UUID, ownerID: UUID, type: AssetType, priority: boolean, objectID: UUID = UUID.zero(), assetID: UUID = UUID.zero(), outAssetID?: { assetID: UUID }, sourceType: TransferSourceType = TransferSourceType.SimInventoryItem, channelType: TransferChannelType = TransferChannelType.Asset): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
if (type === AssetType.Notecard && assetID.isZero())
{
// Empty notecard
const note = new LLLindenText();
resolve(note.toAsset());
}
const transferParams = Buffer.allocUnsafe(100);
let pos = 0;
this.agent.agentID.writeToBuffer(transferParams, pos);
@@ -222,7 +313,7 @@ export class AssetCommands extends CommandsBase
pos = pos + 16;
transferParams.writeInt32LE(type, pos);
this.transfer(TransferChannelType.Asset, TransferSourceType.SimInventoryItem, priority, transferParams, outAssetID).then((result) =>
this.transfer(channelType, sourceType, priority, transferParams, outAssetID).then((result) =>
{
resolve(result);
}).catch((err) =>
@@ -233,85 +324,45 @@ export class AssetCommands extends CommandsBase
});
}
private getMaterialsLimited(uuidArray: any[], uuids: {[key: string]: Material | null}): Promise<void>
private async getMaterialsLimited(uuidArray: any[], uuids: {[key: string]: Material | null})
{
return new Promise<void>((resolve, reject) =>
{
const binary = LLSD.LLSD.formatBinary(uuidArray);
const options: ZlibOptions = {
level: 9
};
zlib.deflate(Buffer.from(binary.toArray()), options, async (error: Error | null, res: Buffer) =>
{
if (error)
{
reject(error);
return;
}
const result = await this.currentRegion.caps.capsPostXML('RenderMaterials', {
'Zipped': new LLSD.LLSD.asBinary(res.toString('base64'))
});
const resultZipped = Buffer.from(result['Zipped'].octets);
zlib.inflate(resultZipped, async (err: Error | null, reslt: Buffer) =>
{
if (err)
{
reject(error);
return;
}
const binData = new LLSD.Binary(Array.from(reslt), 'BASE64');
const llsdResult = LLSD.LLSD.parseBinary(binData);
let obj = [];
if (llsdResult.result)
{
obj = llsdResult.result;
}
if (obj.length > 0)
{
for (const mat of obj)
{
if (mat['ID'])
{
const nbuf = Buffer.from(mat['ID'].toArray());
const nuuid = new UUID(nbuf, 0).toString();
if (uuids[nuuid] !== undefined)
{
if (mat['Material'])
{
const material = new Material();
material.alphaMaskCutoff = mat['Material']['AlphaMaskCutoff'];
material.diffuseAlphaMode = mat['Material']['DiffuseAlphaMode'];
material.envIntensity = mat['Material']['EnvIntensity'];
material.normMap = new UUID(mat['Material']['NormMap'].toString());
material.normOffsetX = mat['Material']['NormOffsetX'];
material.normOffsetY = mat['Material']['NormOffsetY'];
material.normRepeatX = mat['Material']['NormRepeatX'];
material.normRepeatY = mat['Material']['NormRepeatY'];
material.normRotation = mat['Material']['NormRotation'];
material.specColor = new Color4(mat['Material']['SpecColor'][0], mat['Material']['SpecColor'][1], mat['Material']['SpecColor'][2], mat['Material']['SpecColor'][3]);
material.specExp = mat['Material']['SpecExp'];
material.specMap = new UUID(mat['Material']['SpecMap'].toString());
material.specOffsetX = mat['Material']['SpecOffsetX'];
material.specOffsetY = mat['Material']['SpecOffsetY'];
material.specRepeatX = mat['Material']['SpecRepeatX'];
material.specRepeatY = mat['Material']['SpecRepeatY'];
material.specRotation = mat['Material']['SpecRotation'];
material.llsd = LLSD.LLSD.formatXML(mat['Material']);
uuids[nuuid] = material;
}
}
}
}
resolve();
}
else
{
reject(new Error('Material data not found'));
}
});
});
const binary = LLSD.LLSD.formatBinary(uuidArray);
const res: Buffer = await Utils.deflate(Buffer.from(binary.toArray()));
const result = await this.currentRegion.caps.capsPostXML('RenderMaterials', {
'Zipped': LLSD.LLSD.asBinary(res.toString('base64'))
});
const resultZipped = Buffer.from(result['Zipped'].octets);
const reslt: Buffer = await Utils.inflate(resultZipped);
const binData = new LLSD.Binary(Array.from(reslt), 'BASE64');
const llsdResult = LLSD.LLSD.parseBinary(binData);
let obj = [];
if (llsdResult.result)
{
obj = llsdResult.result;
}
if (obj.length > 0)
{
for (const mat of obj)
{
if (mat['ID'])
{
const nbuf = Buffer.from(mat['ID'].toArray());
const nuuid = new UUID(nbuf, 0).toString();
if (uuids[nuuid] !== undefined)
{
if (mat['Material'])
{
uuids[nuuid] = Material.fromLLSDObject(mat['Material']);
}
}
}
}
}
else
{
throw new Error('Material data not found');
}
}
async getMaterials(uuids: {[key: string]: Material | null}): Promise<void>
@@ -336,8 +387,6 @@ export class AssetCommands extends CommandsBase
}
totalCount++;
}
console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials');
}
catch (error)
{
@@ -366,367 +415,10 @@ export class AssetCommands extends CommandsBase
}
totalCount++;
}
console.log('Resolved ' + resolvedCount + ' of ' + totalCount + ' materials (end)');
}
catch (error)
{
console.error(error);
}
}
async uploadMesh(name: string, description: string, mesh: Buffer, confirmCostCallback: (cost: number) => boolean): Promise<UUID>
{
const decodedMesh = await LLMesh.from(mesh);
if (!decodedMesh.creatorID.equals(this.agent.agentID) && !decodedMesh.creatorID.equals(UUID.zero()))
{
throw new Error('Unable to upload - copyright violation');
}
const faces = [];
const faceCount = decodedMesh.lodLevels['high_lod'].length;
for (let x = 0; x < faceCount; x++)
{
faces.push({
'diffuse_color': [1.000000000000001, 1.000000000000001, 1.000000000000001, 1.000000000000001],
'fullbright': false
});
}
const prim = {
'face_list': faces,
'position': [0.000000000000001, 0.000000000000001, 0.000000000000001],
'rotation': [0.000000000000001, 0.000000000000001, 0.000000000000001, 1.000000000000001],
'scale': [2.000000000000001, 2.000000000000001, 2.000000000000001],
'material': 3,
'physics_shape_type': 2,
'mesh': 0
};
const assetResources = {
'instance_list': [prim],
'mesh_list': [new LLSD.Binary(Array.from(mesh))],
'texture_list': [],
'metric': 'MUT_Unspecified'
};
const uploadMap = {
'name': String(name),
'description': String(description),
'asset_resources': assetResources,
'asset_type': 'mesh',
'inventory_type': 'object',
'folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Object)),
'texture_folder_id': new LLSD.UUID(await this.agent.inventory.findFolderForType(FolderType.Texture)),
'everyone_mask': PermissionMask.All,
'group_mask': PermissionMask.All,
'next_owner_mask': PermissionMask.All
};
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'];
if (await confirmCostCallback(cost))
{
const uploader = result['uploader'];
const uploadResult = await this.currentRegion.caps.capsPerformXMLPost(uploader, assetResources);
if (uploadResult['new_inventory_item'] && uploadResult['new_asset'])
{
const inventoryItem = new UUID(uploadResult['new_inventory_item'].toString());
const item = await this.agent.inventory.fetchInventoryItem(inventoryItem);
if (item !== null)
{
item.assetID = new UUID(uploadResult['new_asset'].toString());
}
return inventoryItem;
}
else
{
throw new Error('Upload failed - no new inventory item returned');
}
}
throw new Error('Upload cost declined')
}
else
{
console.log(result);
console.log(JSON.stringify(result.error));
throw new Error('Upload failed');
}
}
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.HTTPAssetTypeToCapInventoryType(type),
'name': name,
'description': description,
'everyone_mask': PermissionMask.All,
'group_mask': PermissionMask.All,
'next_owner_mask': PermissionMask.All,
'expected_upload_cost': 0
}).then((response: any) =>
{
if (response['state'] === 'upload')
{
const uploadURL = response['uploader'];
this.currentRegion.caps.capsRequestUpload(uploadURL, data).then((responseUpload: any) =>
{
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);
});
}
else if (response['error'])
{
reject(response['error']['message']);
}
else
{
reject('Unable to upload asset');
}
}).catch((err) =>
{
console.log('Got err');
console.log(err);
reject(err);
})
}
else
{
if (!this.agent)
{
throw(new Error('Missing agent'));
}
else if (!this.agent.inventory)
{
throw(new Error('Missing agent inventory'));
}
else if (!this.agent.inventory.main)
{
throw new Error('Missing agent inventory main skeleton');
}
else if (!this.agent.inventory.main.root)
{
throw new Error('Missing agent inventory main skeleton root');
}
}
});
}
}

View File

@@ -22,7 +22,7 @@ import { MapInfoReplyEvent } from '../../events/MapInfoReplyEvent';
import { PacketFlags } from '../../enums/PacketFlags';
import { Vector2 } from '../Vector2';
import { MapInfoRangeReplyEvent } from '../../events/MapInfoRangeReplyEvent';
import { Avatar } from '../public/Avatar';
import { AvatarQueryResult } from '../public/AvatarQueryResult';
export class GridCommands extends CommandsBase
{
@@ -389,9 +389,9 @@ export class GridCommands extends CommandsBase
});
}
avatarKey2Name(uuid: UUID | UUID[]): Promise<Avatar | Avatar[]>
avatarKey2Name(uuid: UUID | UUID[]): Promise<AvatarQueryResult | AvatarQueryResult[]>
{
return new Promise<Avatar | Avatar[]>(async (resolve, reject) =>
return new Promise<AvatarQueryResult | AvatarQueryResult[]>(async (resolve, reject) =>
{
const req = new UUIDNameRequestMessage();
req.UUIDNameBlock = [];
@@ -446,16 +446,16 @@ export class GridCommands extends CommandsBase
if (!arr)
{
const result = waitingFor[uuid[0].toString()];
const av = new Avatar(uuid[0], result.firstName, result.lastName);
const av = new AvatarQueryResult(uuid[0], result.firstName, result.lastName);
resolve(av);
}
else
{
const response: Avatar[] = [];
const response: AvatarQueryResult[] = [];
for (const k of uuid)
{
const result = waitingFor[k.toString()];
const av = new Avatar(k, result.firstName, result.lastName);
const av = new AvatarQueryResult(k, result.firstName, result.lastName);
response.push(av);
}
resolve(response);

View File

@@ -9,6 +9,7 @@ import { UUID } from '../UUID';
import { Vector3 } from '../Vector3';
import { PacketFlags } from '../../enums/PacketFlags';
import { ChatSourceType } from '../../enums/ChatSourceType';
import { InventoryItem } from '../InventoryItem';
export class InventoryCommands extends CommandsBase
{
@@ -54,6 +55,23 @@ export class InventoryCommands extends CommandsBase
return await this.circuit.waitForAck(sequenceNo, 10000);
}
async getInventoryItem(item: UUID | string): Promise<InventoryItem>
{
if (typeof item === 'string')
{
item = new UUID(item);
}
const result = await this.currentRegion.agent.inventory.fetchInventoryItem(item);
if (result === null)
{
throw new Error('Unable to get inventory item');
}
else
{
return result;
}
}
async acceptInventoryOffer(event: InventoryOfferedEvent): Promise<void>
{
if (event.source === ChatSourceType.Object)

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ export interface IGameObjectData
sitName?: string;
textureID?: string;
resolvedAt?: number;
resolvedInventory: boolean;
totalChildren?: number;
landImpact?: number;

View File

@@ -1,10 +1,12 @@
import { RBush3D } from 'rbush-3d/dist';
import { UUID } from '../UUID';
import { GameObject } from '../public/GameObject';
import { Avatar } from '../public/Avatar';
export interface IObjectStore
{
rtree?: RBush3D;
populateChildren(obj: GameObject): void;
getObjectsByParent(parentID: number): GameObject[];
shutdown(): void;
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): Promise<GameObject[]>;
@@ -13,4 +15,5 @@ export interface IObjectStore
getNumberOfObjects(): number;
getAllObjects(): Promise<GameObject[]>;
setPersist(persist: boolean): void;
getAvatar(avatarID: UUID): Avatar;
}

View File

@@ -0,0 +1,8 @@
import { GameObject } from '../..';
export interface IResolveJob
{
object: GameObject,
skipInventory: boolean,
log: boolean
}

View File

@@ -1,25 +1,200 @@
import { UUID } from '../UUID';
export class Avatar
{
constructor(private avatarKey: UUID, private firstName: string, private lastName: string)
{
}
getName(): string
{
return this.firstName + ' ' + this.lastName;
}
getFirstName(): string
{
return this.firstName;
}
getLastName(): string
{
return this.lastName;
}
getKey(): UUID
{
return this.avatarKey;
}
}
import { AvatarQueryResult } from './AvatarQueryResult';
import { GameObject } from './GameObject';
import { Vector3 } from '../Vector3';
import { Quaternion } from '../Quaternion';
import { Subject, Subscription } from 'rxjs';
import { UUID } from '../UUID';
import Timer = NodeJS.Timer;
export class Avatar extends AvatarQueryResult
{
private position: Vector3 = Vector3.getZero();
private rotation: Quaternion = Quaternion.getIdentity();
private title = '';
public onAvatarMoved: Subject<Avatar> = new Subject<Avatar>();
public onTitleChanged: Subject<Avatar> = new Subject<Avatar>();
public onLeftRegion: Subject<Avatar> = new Subject<Avatar>();
public onAttachmentAdded: Subject<GameObject> = new Subject<GameObject>();
public onAttachmentRemoved: Subject<GameObject> = new Subject<GameObject>();
private attachments: {[key: string]: GameObject} = {};
static fromGameObject(obj: GameObject): Avatar
{
let firstName = 'Unknown';
let lastName = 'Avatar';
if (obj.NameValue['FirstName'] !== undefined)
{
firstName = obj.NameValue['FirstName'].value;
}
if (obj.NameValue['LastName'] !== undefined)
{
lastName = obj.NameValue['LastName'].value;
}
const av = new Avatar(obj, firstName , lastName);
if (obj.NameValue['Title'] !== undefined)
{
av.setTitle(obj.NameValue['Title'].value);
}
av.processObjectUpdate(obj);
return av;
}
constructor(private gameObject: GameObject, firstName: string, lastName: string)
{
super(gameObject.FullID, firstName, lastName);
const objs: GameObject[] = this.gameObject.region.objects.getObjectsByParent(gameObject.ID);
for (const attachment of objs)
{
this.gameObject.region.clientCommands.region.resolveObject(attachment, true, false).then(() =>
{
this.addAttachment(attachment);
}).catch((err) =>
{
console.error('Failed to resolve attachment for avatar');
});
}
}
setTitle(newTitle: string)
{
if (newTitle !== this.title)
{
this.title = newTitle;
this.onTitleChanged.next(this);
}
}
getTitle(): string
{
return this.title;
}
getPosition(): Vector3
{
return new Vector3(this.position);
}
getRotation(): Quaternion
{
return new Quaternion(this.rotation);
}
processObjectUpdate(obj: GameObject)
{
if (obj.Position !== undefined && obj.Rotation !== undefined)
{
this.setGeometry(obj.Position, obj.Rotation);
}
if (obj.NameValue['Title'] !== undefined)
{
this.setTitle(obj.NameValue['Title'].value);
}
}
setGeometry(position: Vector3, rotation: Quaternion)
{
const oldPosition = this.position;
const oldRotation = this.rotation;
this.position = new Vector3(position);
this.rotation = new Quaternion(rotation);
const rotDist = new Quaternion(this.rotation).angleBetween(oldRotation);
if (Vector3.distance(position, oldPosition) > 0.0001 || rotDist > 0.0001)
{
this.onAvatarMoved.next(this);
}
}
leftRegion()
{
this.onLeftRegion.next(this);
}
getAttachment(itemID: UUID)
{
if (this.attachments[itemID.toString()] !== undefined)
{
return this.attachments[itemID.toString()];
}
throw new Error('Attachment not found');
}
waitForAttachment(itemID: UUID | string, timeout: number = 30000)
{
return new Promise<GameObject>((resolve, reject) =>
{
if (typeof itemID === 'string')
{
itemID = new UUID(itemID);
}
try
{
const attach = this.getAttachment(itemID);
resolve(attach);
}
catch (ignore)
{
let subs: Subscription | undefined = undefined;
let timr: Timer | undefined = undefined;
subs = this.onAttachmentAdded.subscribe((obj: GameObject) =>
{
if (obj.itemID.equals(itemID))
{
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
if (timr !== undefined)
{
clearTimeout(timr);
timr = undefined;
}
resolve(obj);
}
});
timr = setTimeout(() =>
{
if (subs !== undefined)
{
subs.unsubscribe();
subs = undefined;
}
if (timr !== undefined)
{
clearTimeout(timr);
timr = undefined;
}
reject(new Error('WaitForAttachment timed out'));
}, timeout);
}
});
}
addAttachment(obj: GameObject)
{
if (obj.itemID !== undefined)
{
this.attachments[obj.itemID.toString()] = obj;
this.onAttachmentAdded.next(obj);
}
}
removeAttachment(obj: GameObject)
{
if (obj.NameValue['AttachItemID'])
{
const itemID = new UUID(obj.NameValue['AttachItemID'].value);
if (this.attachments[itemID.toString()] !== undefined)
{
this.onAttachmentRemoved.next(obj);
delete this.attachments[itemID.toString()];
}
}
}
}

View File

@@ -0,0 +1,29 @@
import { UUID } from '../UUID';
export class AvatarQueryResult
{
constructor(private avatarKey: UUID, private firstName: string, private lastName: string)
{
}
getName(): string
{
return this.firstName + ' ' + this.lastName;
}
getFirstName(): string
{
return this.firstName;
}
getLastName(): string
{
return this.lastName;
}
getKey(): UUID
{
return this.avatarKey;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
import * as zlib from 'zlib';
import * as LLSD from '@caspertech/llsd';
import { UUID } from '../UUID';
import { LLSubMesh } from './interfaces/LLSubMesh';
@@ -7,6 +6,7 @@ import { Vector2 } from '../Vector2';
import { LLSkin } from './interfaces/LLSkin';
import { mat4 } from '../../tsm/mat4';
import { LLPhysicsConvex } from './interfaces/LLPhysicsConvex';
import { Utils } from '../Utils';
export class LLMesh
{
@@ -55,7 +55,8 @@ export class LLMesh
const bufFrom = startPos + parseInt(o['offset'], 10);
const bufTo = startPos + parseInt(o['offset'], 10) + parseInt(o['size'], 10);
const partBuf = buf.slice(bufFrom, bufTo);
const deflated = await this.inflate(partBuf);
const deflated = await Utils.inflate(partBuf);
const mesh = LLSD.LLSD.parseBinary(new LLSD.Binary(Array.from(deflated), 'BASE64'));
if (mesh['result'] === undefined)
@@ -274,7 +275,7 @@ export class LLMesh
{
throw new Error('TriangleList is required');
}
const indexBuf = new Buffer(submesh['TriangleList'].toArray());
const indexBuf = Buffer.from(submesh['TriangleList'].toArray());
decoded.triangleList = [];
for (let pos = 0; pos < indexBuf.length; pos = pos + 2)
{
@@ -287,7 +288,7 @@ export class LLMesh
}
if (submesh['Weights'])
{
const skinBuf = new Buffer(submesh['Weights'].toArray());
const skinBuf = Buffer.from(submesh['Weights'].toArray());
decoded.weights = [];
let pos = 0;
while (pos < skinBuf.length)
@@ -318,7 +319,7 @@ export class LLMesh
static decodeByteDomain3(posArray: number[], minDomain: Vector3, maxDomain: Vector3): Vector3[]
{
const result: Vector3[] = [];
const buf = new Buffer(posArray);
const buf = Buffer.from(posArray);
for (let idx = 0; idx < posArray.length; idx = idx + 6)
{
const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x);
@@ -331,7 +332,7 @@ export class LLMesh
static decodeByteDomain2(posArray: number[], minDomain: Vector2, maxDomain: Vector2): Vector2[]
{
const result: Vector2[] = [];
const buf = new Buffer(posArray);
const buf = Buffer.from(posArray);
for (let idx = 0; idx < posArray.length; idx = idx + 4)
{
const posX = this.normalizeDomain(buf.readUInt16LE(idx), minDomain.x, maxDomain.x);
@@ -344,40 +345,6 @@ export class LLMesh
{
return ((value / 65535) * (max - min)) + min;
}
static inflate(buf: Buffer): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
zlib.inflate(buf, (error: (Error| null), result: Buffer) =>
{
if (error)
{
reject(error)
}
else
{
resolve(result);
}
})
});
}
static deflate(buf: Buffer): Promise<Buffer>
{
return new Promise<Buffer>((resolve, reject) =>
{
zlib.deflate(buf, { level: 9}, (error: (Error| null), result: Buffer) =>
{
if (error)
{
reject(error)
}
else
{
resolve(result);
}
})
});
}
private encodeSubMesh(mesh: LLSubMesh)
{
const data: {
@@ -507,7 +474,7 @@ export class LLMesh
smList.push(this.encodeSubMesh(sub))
}
const mesh = LLSD.LLSD.formatBinary(smList);
return await LLMesh.deflate(Buffer.from(mesh.toArray()));
return await Utils.deflate(Buffer.from(mesh.toArray()));
}
private async encodePhysicsConvex(conv: LLPhysicsConvex): Promise<Buffer>
{
@@ -556,7 +523,7 @@ export class LLMesh
llsd.BoundingVerts = new LLSD.Binary(Array.from(buf));
}
const mesh = LLSD.LLSD.formatBinary(llsd);
return await LLMesh.deflate(Buffer.from(mesh.toArray()));
return await Utils.deflate(Buffer.from(mesh.toArray()));
}
private async encodeSkin(skin: LLSkin): Promise<Buffer>
{
@@ -581,7 +548,7 @@ export class LLMesh
llsd['pelvis_offset'] = skin.pelvisOffset.toArray();
}
const mesh = LLSD.LLSD.formatBinary(llsd);
return await LLMesh.deflate(Buffer.from(mesh.toArray()));
return await Utils.deflate(Buffer.from(mesh.toArray()));
}
async toAsset(): Promise<Buffer>
{

View File

@@ -1,5 +1,7 @@
import { UUID } from '../UUID';
import { Color4 } from '../Color4';
import * as LLSD from '@caspertech/llsd';
import { Utils } from '../Utils';
export class Material
{
@@ -20,5 +22,131 @@ export class Material
specRepeatX: number;
specRepeatY: number;
specRotation: number;
llsd: string;
static fromLLSD(llsd: string): Material
{
const parsed = LLSD.LLSD.parseXML(llsd);
return this.fromLLSDObject(parsed);
}
static fromLLSDObject(parsed: any): Material
{
const material = new Material();
if (parsed['AlphaMaskCutoff'] !== undefined)
{
material.alphaMaskCutoff = parsed['AlphaMaskCutoff'];
}
if (parsed['DiffuseAlphaMode'] !== undefined)
{
material.diffuseAlphaMode = parsed['DiffuseAlphaMode'];
}
if (parsed['EnvIntensity'] !== undefined)
{
material.envIntensity = parsed['EnvIntensity'];
}
if (parsed['NormMap'] !== undefined)
{
material.normMap = new UUID(parsed['NormMap'].toString())
}
if (parsed['NormOffsetX'] !== undefined)
{
material.normOffsetX = parsed['NormOffsetX'];
}
if (parsed['NormOffsetY'] !== undefined)
{
material.normOffsetY = parsed['NormOffsetY'];
}
if (parsed['NormRepeatX'] !== undefined)
{
material.normRepeatX = parsed['NormRepeatX'];
}
if (parsed['NormRepeatY'] !== undefined)
{
material.normRepeatY = parsed['NormRepeatY'];
}
if (parsed['NormRotation'] !== undefined)
{
material.normRotation = parsed['NormRotation'];
}
if (parsed['SpecColor'] !== undefined && Array.isArray(parsed['SpecColor']) && parsed['SpecColor'].length > 3)
{
material.specColor = new Color4([
parsed['SpecColor'][0],
parsed['SpecColor'][1],
parsed['SpecColor'][2],
parsed['SpecColor'][3]
]);
}
if (parsed['SpecExp'] !== undefined)
{
material.specExp = parsed['SpecExp'];
}
if (parsed['SpecMap'] !== undefined)
{
material.specMap = new UUID(parsed['SpecMap'].toString())
}
if (parsed['SpecOffsetX'] !== undefined)
{
material.specOffsetX = parsed['SpecOffsetX'];
}
if (parsed['SpecOffsetY'] !== undefined)
{
material.specOffsetY = parsed['SpecOffsetY'];
}
if (parsed['SpecRepeatX'] !== undefined)
{
material.specRepeatX = parsed['SpecRepeatX'];
}
if (parsed['SpecRepeatY'] !== undefined)
{
material.specRepeatY = parsed['SpecRepeatY'];
}
if (parsed['SpecRotation'] !== undefined)
{
material.specRotation = parsed['SpecRotation'];
}
return material;
}
toLLSDObject(): any
{
return {
'AlphaMaskCutoff': this.alphaMaskCutoff,
'DiffuseAlphaMode': this.diffuseAlphaMode,
'EnvIntensity': this.envIntensity,
'NormMap': new LLSD.UUID(this.normMap.toString()),
'NormOffsetX': this.normOffsetX,
'NormOffsetY': this.normOffsetY,
'NormRepeatX': this.normRepeatX,
'NormRepeatY': this.normRepeatY,
'NormRotation': this.normRotation,
'SpecColor': [
this.specColor.getRed(),
this.specColor.getGreen(),
this.specColor.getBlue(),
this.specColor.getAlpha()
],
'SpecExp': this.specExp,
'SpecMap': new LLSD.UUID(this.specMap.toString()),
'SpecOffsetX': this.specOffsetX,
'SpecOffsetY': this.specOffsetY,
'SpecRepeatX': this.specRepeatX,
'SpecRepeatY': this.specRepeatY,
'SpecRotation': this.specRotation,
};
}
toLLSD(): string
{
return LLSD.LLSD.formatXML(this.toLLSDObject());
}
async toAsset(uuid: UUID): Promise<Buffer>
{
const asset = {
'ID': new LLSD.UUID(uuid.toString()),
'Material': this.toLLSD()
};
const binary = LLSD.LLSD.formatBinary(asset);
return await Utils.deflate(Buffer.from(binary.toArray()));
}
}

View File

@@ -2,6 +2,7 @@ import { Vector3 } from '../Vector3';
import { UUID } from '../UUID';
import * as builder from 'xmlbuilder';
import { ParcelFlags } from '../../enums/ParcelFlags';
import { Region } from '../Region';
export class Parcel
{
@@ -73,6 +74,28 @@ export class Parcel
RegionAllowAccessOverride: boolean;
constructor(private region: Region)
{
}
canIRez(): boolean
{
if (this.ParcelFlags & ParcelFlags.CreateObjects)
{
return true;
}
if (this.region.agent.activeGroupID.equals(this.OwnerID) && this.ParcelFlags & ParcelFlags.CreateGroupObjects)
{
return true;
}
if (this.OwnerID.equals(this.region.agent.agentID))
{
return true;
}
return false;
}
exportXML(): string
{
const document = builder.create('LandData');