Extensive work on building, wearables, assets, inventory, attachments, serialization, etc.
Resolves #36
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} = {};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
66
lib/classes/CoalescedGameObject.ts
Normal file
66
lib/classes/CoalescedGameObject.ts
Normal 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});
|
||||
}
|
||||
}
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
159
lib/classes/LLGesture.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
12
lib/classes/LLGestureAnimationStep.ts
Normal file
12
lib/classes/LLGestureAnimationStep.ts
Normal 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;
|
||||
}
|
||||
10
lib/classes/LLGestureChatStep.ts
Normal file
10
lib/classes/LLGestureChatStep.ts
Normal 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;
|
||||
}
|
||||
12
lib/classes/LLGestureSoundStep.ts
Normal file
12
lib/classes/LLGestureSoundStep.ts
Normal 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;
|
||||
}
|
||||
6
lib/classes/LLGestureStep.ts
Normal file
6
lib/classes/LLGestureStep.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { LLGestureStepType } from '../enums/LLGestureStepType';
|
||||
|
||||
export class LLGestureStep
|
||||
{
|
||||
stepType: LLGestureStepType
|
||||
}
|
||||
11
lib/classes/LLGestureWaitStep.ts
Normal file
11
lib/classes/LLGestureWaitStep.ts
Normal 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
182
lib/classes/LLLindenText.ts
Normal 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, ' ');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
137
lib/classes/Logger.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
441
lib/classes/ObjectResolver.ts
Normal file
441
lib/classes/ObjectResolver.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
6
lib/classes/TarArchive.ts
Normal file
6
lib/classes/TarArchive.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { TarFile } from './TarFile';
|
||||
|
||||
export class TarArchive
|
||||
{
|
||||
files: TarFile[] = [];
|
||||
}
|
||||
44
lib/classes/TarFile.ts
Normal file
44
lib/classes/TarFile.ts
Normal 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
201
lib/classes/TarReader.ts
Normal 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
143
lib/classes/TarWriter.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -44,6 +44,7 @@ export interface IGameObjectData
|
||||
sitName?: string;
|
||||
textureID?: string;
|
||||
resolvedAt?: number;
|
||||
resolvedInventory: boolean;
|
||||
totalChildren?: number;
|
||||
|
||||
landImpact?: number;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
8
lib/classes/interfaces/IResolveJob.ts
Normal file
8
lib/classes/interfaces/IResolveJob.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { GameObject } from '../..';
|
||||
|
||||
export interface IResolveJob
|
||||
{
|
||||
object: GameObject,
|
||||
skipInventory: boolean,
|
||||
log: boolean
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
lib/classes/public/AvatarQueryResult.ts
Normal file
29
lib/classes/public/AvatarQueryResult.ts
Normal 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
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user