- Implement camera controls

- Option to resolve object properties when fetching from object store (names, descriptions etc). Can be more efficient - TODO: use FamilyProperties for child prims.
- Refactored objectstore to reduce code duplication
This commit is contained in:
Casper Warden
2018-10-19 16:30:09 +01:00
parent ff0a5fa58b
commit 2852c76cb0
42 changed files with 2651 additions and 2121 deletions

View File

@@ -19,9 +19,9 @@ import {RezSingleAttachmentFromInvMessage} from './messages/RezSingleAttachmentF
import {AttachmentPoint} from '../enums/AttachmentPoint';
import {Utils} from './Utils';
import {ClientEvents} from './ClientEvents';
import {IGameObject} from './interfaces/IGameObject';
import Timer = NodeJS.Timer;
import {ControlFlags, GroupChatSessionAgentListEvent, AgentFlags, PacketFlags, AssetType} from '..';
import {GameObject} from './GameObject';
export class Agent
{
@@ -53,7 +53,11 @@ export class Agent
uiFlags: {
'allowFirstLife'?: boolean
} = {};
lookAt: Vector3;
cameraLookAt: Vector3 = new Vector3([0.979546, 0.105575, -0.171303]);
cameraCenter: Vector3 = new Vector3([199.58, 203.95, 24.304]);
cameraLeftAxis: Vector3 = new Vector3([-1.0, 0.0, 0]);
cameraUpAxis: Vector3 = new Vector3([0.0, 0.0, 1.0]);
cameraFar = 1;
maxGroups: number;
agentFlags: number;
startLocation: string;
@@ -164,11 +168,11 @@ export class Agent
HeadRotation: Quaternion.getIdentity(),
BodyRotation: Quaternion.getIdentity(),
State: AgentState.None,
CameraCenter: new Vector3([199.58, 203.95, 24.304]),
CameraAtAxis: new Vector3([0.979546, 0.105575, -0.171303]),
CameraLeftAxis: new Vector3([-0.107158, 0.994242, 0]),
CameraUpAxis: new Vector3([0.170316, 0.018357, 0.985218]),
Far: 128,
CameraCenter: this.cameraCenter,
CameraAtAxis: this.cameraLookAt,
CameraLeftAxis: this.cameraLeftAxis,
CameraUpAxis: this.cameraUpAxis,
Far: this.cameraFar,
ControlFlags: this.controlFlags,
Flags: AgentFlags.None
};
@@ -266,7 +270,7 @@ export class Agent
if (item.type === 6)
{
let found = false;
wornObjects.forEach((obj: IGameObject) =>
wornObjects.forEach((obj: GameObject) =>
{
if (obj.hasNameValueEntry('AttachItemID'))
{

View File

@@ -366,6 +366,10 @@ export class EventQueueClient
}
break;
}
case 'ObjectPhysicsProperties':
{
break;
}
case 'TeleportFinish':
{
const info = event['body']['Info'][0];

130
lib/classes/GameObject.ts Normal file
View File

@@ -0,0 +1,130 @@
import {Vector3} from './Vector3';
import {UUID} from './UUID';
import {Quaternion} from './Quaternion';
import {Tree} from '../enums/Tree';
import {SoundFlags} from '..';
import {Vector4} from './Vector4';
import {TextureEntry} from './TextureEntry';
import {Color4} from './Color4';
import {ParticleSystem} from './ParticleSystem';
import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox';
import {NameValue} from './NameValue';
import {PCode} from '../enums/PCode';
import {Utils} from './Utils';
import * as Long from 'long';
export class GameObject
{
creatorID?: UUID;
creationDate?: Long;
baseMask?: number;
ownerMask?: number;
groupMask?: number;
everyoneMask?: number;
nextOwnerMask?: number;
ownershipCost?: number;
saleType?: number;
salePrice?: number;
aggregatePerms?: number;
aggregatePermTextures?: number;
aggregatePermTexturesOwner?: number;
category: number;
inventorySerial: number;
itemID: UUID;
folderID: UUID;
fromTaskID: UUID;
lastOwnerID: UUID;
name?: string;
description?: string;
touchName?: string;
sitName?: string;
textureID?: string;
resolvedAt?: number;
totalChildren?: number;
children?: GameObject[];
rtreeEntry?: ITreeBoundingBox;
ID = 0;
FullID = UUID.random();
ParentID = 0;
OwnerID = UUID.zero();
IsAttachment = false;
NameValue: {[key: string]: NameValue} = {};
PCode: PCode = PCode.None;
State?: number;
CRC?: number;
Material?: number;
ClickAction?: number;
Scale?: Vector3;
ObjectData?: Buffer;
UpdateFlags?: number;
Flags?: number;
PathCurve?: number;
ProfileCurve?: number;
PathBegin?: number;
PathEnd?: number;
PathScaleX?: number;
PathScaleY?: number;
PathShearX?: number;
PathShearY?: number;
PathTwist?: number;
PathTwistBegin?: number;
PathRadiusOffset?: number;
PathTaperX?: number;
PathTaperY?: number;
PathRevolutions?: number;
PathSkew?: number;
ProfileBegin?: number;
ProfileEnd?: number;
ProfileHollow?: number;
TextureEntry?: TextureEntry;
TextureAnim?: Buffer;
Data?: Buffer;
Text?: string;
TextColor?: Color4;
MediaURL?: string;
PSBlock?: Buffer;
JointType?: number;
JointPivot?: Vector3;
JointAxisOrAnchor?: Vector3;
Position?: Vector3;
Rotation?: Quaternion;
CollisionPlane?: Vector4;
Velocity?: Vector3;
Acceleration?: Vector3;
AngularVelocity?: Vector3;
TreeSpecies?: Tree;
Sound?: UUID;
SoundGain?: number;
SoundFlags?: SoundFlags;
SoundRadius?: number;
Particles?: ParticleSystem;
constructor()
{
this.Position = Vector3.getZero();
this.Rotation = Quaternion.getIdentity();
this.AngularVelocity = Vector3.getZero();
this.TreeSpecies = 0;
this.SoundFlags = 0;
this.SoundRadius = 1.0;
this.SoundGain = 1.0;
this.ParentID = 0;
}
hasNameValueEntry(key: string): boolean
{
return this.NameValue[key] !== undefined;
}
getNameValueEntry(key: string): string
{
if (this.NameValue[key])
{
return this.NameValue[key].value;
}
return '';
}
}

View File

@@ -1,104 +0,0 @@
import {Vector3} from './Vector3';
import {UUID} from './UUID';
import {PCode} from '../enums/PCode';
import {Quaternion} from './Quaternion';
import {Tree} from '../enums/Tree';
import {NameValue} from './NameValue';
import {IGameObject} from './interfaces/IGameObject';
import {SoundFlags} from '..';
import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox';
import {Vector4} from './Vector4';
import {TextureEntry} from './TextureEntry';
import {Color4} from './Color4';
import {ParticleSystem} from './ParticleSystem';
export class GameObjectFull implements IGameObject
{
rtreeEntry?: ITreeBoundingBox;
ID: number;
State: number;
FullID: UUID;
CRC: number;
PCode: PCode;
Material: number;
ClickAction: number;
Scale: Vector3;
ObjectData: Buffer;
ParentID: number;
UpdateFlags: number;
Flags: number;
PathCurve: number;
ProfileCurve: number;
PathBegin: number;
PathEnd: number;
PathScaleX: number;
PathScaleY: number;
PathShearX: number;
PathShearY: number;
PathTwist: number;
PathTwistBegin: number;
PathRadiusOffset: number;
PathTaperX: number;
PathTaperY: number;
PathRevolutions: number;
PathSkew: number;
ProfileBegin: number;
ProfileEnd: number;
ProfileHollow: number;
TextureEntry: TextureEntry;
TextureAnim: Buffer;
Data: Buffer;
Text: string;
TextColor: Color4;
MediaURL: string;
PSBlock: Buffer;
OwnerID: UUID;
JointType: number;
JointPivot: Vector3;
JointAxisOrAnchor: Vector3;
Position: Vector3;
Rotation: Quaternion;
CollisionPlane: Vector4;
Velocity: Vector3;
Acceleration: Vector3;
AngularVelocity: Vector3;
TreeSpecies: Tree;
Sound: UUID;
SoundGain: number;
SoundFlags: SoundFlags;
SoundRadius: number;
IsAttachment: boolean;
NameValue: {[key: string]: NameValue};
Particles: ParticleSystem;
constructor()
{
this.Position = Vector3.getZero();
this.Rotation = Quaternion.getIdentity();
this.IsAttachment = false;
this.NameValue = {};
this.AngularVelocity = Vector3.getZero();
this.TreeSpecies = 0;
this.SoundFlags = 0;
this.SoundRadius = 1.0;
this.SoundGain = 1.0;
this.ParentID = 0;
}
hasNameValueEntry(key: string): boolean
{
if (this.NameValue['AttachItemID'])
{
return true;
}
return false;
}
getNameValueEntry(key: string): string
{
if (this.NameValue['AttachItemID'])
{
return this.NameValue['AttachItemID'].value;
}
return '';
}
}

View File

@@ -1,35 +0,0 @@
import {UUID} from './UUID';
import {IGameObject} from './interfaces/IGameObject';
import {NameValue} from './NameValue';
import {PCode} from '../enums/PCode';
import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox';
export class GameObjectLite implements IGameObject
{
rtreeEntry?: ITreeBoundingBox;
ID: number;
FullID: UUID;
ParentID: number;
OwnerID: UUID;
IsAttachment: boolean;
NameValue: {[key: string]: NameValue};
PCode: PCode;
constructor()
{
this.IsAttachment = false;
}
hasNameValueEntry(key: string): boolean
{
return this.NameValue['AttachItemID'] !== undefined;
}
getNameValueEntry(key: string): string
{
if (this.NameValue['AttachItemID'])
{
return this.NameValue['AttachItemID'].value;
}
return '';
}
}

View File

@@ -246,7 +246,7 @@ export class LoginResponse
});
break;
case 'look_at':
this.agent.lookAt = LoginResponse.parseVector3(val);
this.agent.cameraLookAt = LoginResponse.parseVector3(val);
break;
case 'openid_url':
this.agent.openID.url = String(val);

File diff suppressed because it is too large Load Diff

View File

@@ -15,21 +15,23 @@ import {PCode} from '../enums/PCode';
import {ClientEvents} from './ClientEvents';
import {KillObjectMessage} from './messages/KillObject';
import {IObjectStore} from './interfaces/IObjectStore';
import {GameObjectLite} from './GameObjectLite';
import {NameValue} from './NameValue';
import {BotOptionFlags, CompressedFlags} from '..';
import {IGameObject} from './interfaces/IGameObject';
import {GameObjectFull} from './GameObjectFull';
import {GameObject} from './GameObject';
import {RBush3D} from 'rbush-3d/dist';
import {ITreeBoundingBox} from './interfaces/ITreeBoundingBox';
export class ObjectStoreLite implements IObjectStore
{
private circuit: Circuit;
private agent: Agent;
private objects: { [key: number]: GameObjectLite } = {};
private objectsByUUID: { [key: string]: number } = {};
private objectsByParent: { [key: number]: number[] } = {};
private clientEvents: ClientEvents;
private options: BotOptionFlags;
protected circuit: Circuit;
protected agent: Agent;
protected objects: { [key: number]: GameObject } = {};
protected objectsByUUID: { [key: string]: number } = {};
protected objectsByParent: { [key: number]: number[] } = {};
protected clientEvents: ClientEvents;
protected options: BotOptionFlags;
rtree?: RBush3D;
constructor(circuit: Circuit, agent: Agent, clientEvents: ClientEvents, options: BotOptionFlags)
{
@@ -51,295 +53,319 @@ export class ObjectStoreLite implements IObjectStore
{
case Message.ObjectUpdate:
const objectUpdate = packet.message as ObjectUpdateMessage;
objectUpdate.ObjectData.forEach((objData) =>
{
const localID = objData.ID;
const parentID = objData.ParentID;
let addToParentList = true;
if (this.objects[localID])
{
if (this.objects[localID].ParentID !== parentID && this.objectsByParent[parentID])
{
const ind = this.objectsByParent[parentID].indexOf(localID);
if (ind !== -1)
{
this.objectsByParent[parentID].splice(ind, 1);
}
}
else
{
addToParentList = false;
}
}
else
{
this.objects[localID] = new GameObjectLite();
}
const obj = this.objects[localID];
obj.ID = objData.ID;
obj.FullID = objData.FullID;
obj.ParentID = objData.ParentID;
obj.OwnerID = objData.OwnerID;
obj.PCode = objData.PCode;
this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString())
{
this.agent.localID = localID;
if (this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
Object.keys(this.objectsByParent).forEach((objParentID: string) =>
{
const parent = parseInt(objParentID, 10);
if (parent !== this.agent.localID)
{
let foundAvatars = false;
this.objectsByParent[parent].forEach((objID) =>
{
if (this.objects[objID])
{
const o = this.objects[objID];
if (o.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
});
if (this.objects[parent])
{
const o = this.objects[parent];
if (o.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
if (!foundAvatars)
{
this.deleteObject(parent);
}
}
});
}
}
this.objectsByUUID[objData.FullID.toString()] = localID;
if (!this.objectsByParent[parentID])
{
this.objectsByParent[parentID] = [];
}
if (addToParentList)
{
this.objectsByParent[parentID].push(localID);
}
if (objData.PCode !== PCode.Avatar && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
if (this.agent.localID !== 0 && obj.ParentID !== this.agent.localID)
{
// Drop object
this.deleteObject(localID);
return;
}
}
});
this.objectUpdate(objectUpdate);
break;
case Message.ObjectUpdateCached:
const objectUpdateCached = packet.message as ObjectUpdateCachedMessage;
const rmo = new RequestMultipleObjectsMessage();
rmo.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID
};
rmo.ObjectData = [];
objectUpdateCached.ObjectData.forEach((obj) =>
{
rmo.ObjectData.push({
CacheMissType: 0,
ID: obj.ID
});
});
circuit.sendMessage(rmo, 0);
this.objectUpdateCached(objectUpdateCached);
break;
case Message.ObjectUpdateCompressed:
{
const objectUpdateCompressed = packet.message as ObjectUpdateCompressedMessage;
for (const obj of objectUpdateCompressed.ObjectData)
{
const flags = obj.UpdateFlags;
const buf = obj.Data;
let pos = 0;
const fullID = new UUID(buf, pos);
pos += 16;
const localID = buf.readUInt32LE(pos);
pos += 4;
const pcode = buf.readUInt8(pos++);
let newObj = false;
if (!this.objects[localID])
{
newObj = true;
this.objects[localID] = new GameObjectLite();
}
const o = this.objects[localID];
o.ID = localID;
o.PCode = pcode;
this.objectsByUUID[fullID.toString()] = localID;
o.FullID = fullID;
pos++;
pos = pos + 4;
pos++;
pos++;
pos = pos + 12;
pos = pos + 12;
pos = pos + 12;
const compressedflags: CompressedFlags = buf.readUInt32LE(pos);
pos = pos + 4;
o.OwnerID = new UUID(buf, pos);
pos += 16;
if (compressedflags & CompressedFlags.HasAngularVelocity)
{
pos = pos + 12;
}
if (compressedflags & CompressedFlags.HasParent)
{
const newParentID = buf.readUInt32LE(pos);
pos += 4;
let add = true;
if (!newObj)
{
if (newParentID !== o.ParentID)
{
const index = this.objectsByParent[o.ParentID].indexOf(localID);
if (index !== -1)
{
this.objectsByParent[o.ParentID].splice(index, 1);
}
}
else
{
add = false;
}
}
if (add)
{
if (!this.objectsByParent[newParentID])
{
this.objectsByParent[newParentID] = [];
}
this.objectsByParent[newParentID].push(localID);
}
o.ParentID = newParentID;
}
if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
if (this.agent.localID !== 0 && o.ParentID !== this.agent.localID)
{
// Drop object
this.deleteObject(localID);
return;
}
}
if (compressedflags & CompressedFlags.Tree)
{
pos++;
}
else if (compressedflags & CompressedFlags.ScratchPad)
{
const scratchPadSize = buf.readUInt8(pos++);
// Ignore this data
pos = pos + scratchPadSize;
}
if (compressedflags & CompressedFlags.HasText)
{
// Read null terminated string
const result = Utils.BufferToString(buf, pos);
pos += result.readLength;
pos = pos + 4;
}
if (compressedflags & CompressedFlags.MediaURL)
{
const result = Utils.BufferToString(buf, pos);
pos += result.readLength;
}
if (compressedflags & CompressedFlags.HasParticles)
{
// TODO: Particle system block
pos += 86;
}
// Extra params
pos = this.readExtraParams(buf, pos, o);
if (compressedflags & CompressedFlags.HasSound)
{
pos = pos + 16;
pos += 4;
pos++;
pos = pos + 4;
}
if (compressedflags & CompressedFlags.HasNameValues)
{
const result = Utils.BufferToString(buf, pos);
o.NameValue = this.parseNameValues(result.result);
pos += result.readLength;
}
pos++;
pos = pos + 2;
pos = pos + 2;
pos = pos + 12;
pos = pos + 2;
pos = pos + 2;
pos = pos + 2;
const textureEntryLength = buf.readUInt32LE(pos);
pos = pos + 4;
// TODO: Properly parse textureentry;
pos = pos + textureEntryLength;
if (compressedflags & CompressedFlags.TextureAnimation)
{
// TODO: Properly parse textureAnim
pos = pos + 4;
}
o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0;
};
this.objectUpdateCompressed(objectUpdateCompressed);
break;
}
case Message.ImprovedTerseObjectUpdate:
const objectUpdateTerse = packet.message as ImprovedTerseObjectUpdateMessage;
// TODO: ImprovedTerseObjectUPdate
this.objectUpdateTerse(objectUpdateTerse);
break;
case Message.MultipleObjectUpdate:
const multipleObjectUpdate = packet.message as MultipleObjectUpdateMessage;
// TODO: multipleObjectUpdate
console.error('TODO: MultipleObjectUpdate');
this.objectUpdateMultiple(multipleObjectUpdate);
break;
case Message.KillObject:
const killObj = packet.message as KillObjectMessage;
killObj.ObjectData.forEach((obj) =>
{
const objectID = obj.ID;
this.deleteObject(objectID);
});
this.killObject(killObj);
break;
}
});
}
protected objectUpdate(objectUpdate: ObjectUpdateMessage)
{
objectUpdate.ObjectData.forEach((objData) =>
{
const localID = objData.ID;
const parentID = objData.ParentID;
let addToParentList = true;
if (this.objects[localID])
{
if (this.objects[localID].ParentID !== parentID && this.objectsByParent[parentID])
{
const ind = this.objectsByParent[parentID].indexOf(localID);
if (ind !== -1)
{
this.objectsByParent[parentID].splice(ind, 1);
}
}
else
{
addToParentList = false;
}
}
else
{
this.objects[localID] = new GameObject();
}
const obj = this.objects[localID];
obj.ID = objData.ID;
obj.FullID = objData.FullID;
obj.ParentID = objData.ParentID;
obj.OwnerID = objData.OwnerID;
obj.PCode = objData.PCode;
this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString())
{
this.agent.localID = localID;
if (this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
Object.keys(this.objectsByParent).forEach((objParentID: string) =>
{
const parent = parseInt(objParentID, 10);
if (parent !== this.agent.localID)
{
let foundAvatars = false;
this.objectsByParent[parent].forEach((objID) =>
{
if (this.objects[objID])
{
const o = this.objects[objID];
if (o.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
});
if (this.objects[parent])
{
const o = this.objects[parent];
if (o.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
if (!foundAvatars)
{
this.deleteObject(parent);
}
}
});
}
}
this.objectsByUUID[objData.FullID.toString()] = localID;
if (!this.objectsByParent[parentID])
{
this.objectsByParent[parentID] = [];
}
if (addToParentList)
{
this.objectsByParent[parentID].push(localID);
}
if (objData.PCode !== PCode.Avatar && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
if (this.agent.localID !== 0 && obj.ParentID !== this.agent.localID)
{
// Drop object
this.deleteObject(localID);
return;
}
}
});
}
protected objectUpdateCached(objectUpdateCached: ObjectUpdateCachedMessage)
{
const rmo = new RequestMultipleObjectsMessage();
rmo.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID
};
rmo.ObjectData = [];
objectUpdateCached.ObjectData.forEach((obj) =>
{
rmo.ObjectData.push({
CacheMissType: 0,
ID: obj.ID
});
});
this.circuit.sendMessage(rmo, 0);
}
protected objectUpdateCompressed(objectUpdateCompressed: ObjectUpdateCompressedMessage)
{
for (const obj of objectUpdateCompressed.ObjectData)
{
const flags = obj.UpdateFlags;
const buf = obj.Data;
let pos = 0;
const fullID = new UUID(buf, pos);
pos += 16;
const localID = buf.readUInt32LE(pos);
pos += 4;
const pcode = buf.readUInt8(pos++);
let newObj = false;
if (!this.objects[localID])
{
newObj = true;
this.objects[localID] = new GameObject();
}
const o = this.objects[localID];
o.ID = localID;
o.PCode = pcode;
this.objectsByUUID[fullID.toString()] = localID;
o.FullID = fullID;
pos++;
pos = pos + 4;
pos++;
pos++;
pos = pos + 12;
pos = pos + 12;
pos = pos + 12;
const compressedflags: CompressedFlags = buf.readUInt32LE(pos);
pos = pos + 4;
o.OwnerID = new UUID(buf, pos);
pos += 16;
if (compressedflags & CompressedFlags.HasAngularVelocity)
{
pos = pos + 12;
}
if (compressedflags & CompressedFlags.HasParent)
{
const newParentID = buf.readUInt32LE(pos);
pos += 4;
let add = true;
if (!newObj)
{
if (newParentID !== o.ParentID)
{
const index = this.objectsByParent[o.ParentID].indexOf(localID);
if (index !== -1)
{
this.objectsByParent[o.ParentID].splice(index, 1);
}
}
else
{
add = false;
}
}
if (add)
{
if (!this.objectsByParent[newParentID])
{
this.objectsByParent[newParentID] = [];
}
this.objectsByParent[newParentID].push(localID);
}
o.ParentID = newParentID;
}
if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
if (this.agent.localID !== 0 && o.ParentID !== this.agent.localID)
{
// Drop object
this.deleteObject(localID);
return;
}
}
if (compressedflags & CompressedFlags.Tree)
{
pos++;
}
else if (compressedflags & CompressedFlags.ScratchPad)
{
const scratchPadSize = buf.readUInt8(pos++);
// Ignore this data
pos = pos + scratchPadSize;
}
if (compressedflags & CompressedFlags.HasText)
{
// Read null terminated string
const result = Utils.BufferToString(buf, pos);
pos += result.readLength;
pos = pos + 4;
}
if (compressedflags & CompressedFlags.MediaURL)
{
const result = Utils.BufferToString(buf, pos);
pos += result.readLength;
}
if (compressedflags & CompressedFlags.HasParticles)
{
// TODO: Particle system block
pos += 86;
}
// Extra params
pos = this.readExtraParams(buf, pos, o);
if (compressedflags & CompressedFlags.HasSound)
{
pos = pos + 16;
pos += 4;
pos++;
pos = pos + 4;
}
if (compressedflags & CompressedFlags.HasNameValues)
{
const result = Utils.BufferToString(buf, pos);
o.NameValue = this.parseNameValues(result.result);
pos += result.readLength;
}
pos++;
pos = pos + 2;
pos = pos + 2;
pos = pos + 12;
pos = pos + 2;
pos = pos + 2;
pos = pos + 2;
const textureEntryLength = buf.readUInt32LE(pos);
pos = pos + 4;
// TODO: Properly parse textureentry;
pos = pos + textureEntryLength;
if (compressedflags & CompressedFlags.TextureAnimation)
{
// TODO: Properly parse textureAnim
pos = pos + 4;
}
o.IsAttachment = (compressedflags & CompressedFlags.HasNameValues) !== 0 && o.ParentID !== 0;
}
}
protected objectUpdateTerse(objectUpdateTerse: ImprovedTerseObjectUpdateMessage)
{ }
protected objectUpdateMultiple(objectUpdateMultiple: MultipleObjectUpdateMessage)
{ }
protected killObject(killObj: KillObjectMessage)
{
killObj.ObjectData.forEach((obj) =>
{
const objectID = obj.ID;
this.deleteObject(objectID);
});
}
deleteObject(objectID: number)
{
if (this.objects[objectID])
@@ -371,11 +397,15 @@ export class ObjectStoreLite implements IObjectStore
this.objectsByParent[parentID].splice(ind, 1);
}
}
if (this.rtree && this.objects[objectID].rtreeEntry !== undefined)
{
this.rtree.remove(this.objects[objectID].rtreeEntry);
}
delete this.objects[objectID];
}
}
readExtraParams(buf: Buffer, pos: number, o: GameObjectLite): number
readExtraParams(buf: Buffer, pos: number, o: GameObject): number
{
if (pos >= buf.length)
{
@@ -395,14 +425,14 @@ export class ObjectStoreLite implements IObjectStore
return pos;
}
getObjectsByParent(parentID: number): GameObjectLite[]
getObjectsByParent(parentID: number): GameObject[]
{
const list = this.objectsByParent[parentID];
if (list === undefined)
{
return [];
}
const result: GameObjectLite[] = [];
const result: GameObject[] = [];
list.forEach((localID) =>
{
result.push(this.objects[localID]);
@@ -449,16 +479,102 @@ export class ObjectStoreLite implements IObjectStore
shutdown()
{
this.objects = {};
if (this.rtree)
{
this.rtree.clear();
}
this.objectsByUUID = {};
this.objectsByParent = {};
}
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObjectFull[]
protected findParent(go: GameObject): GameObject
{
throw new Error('GetObjectsInArea not available with the Lite object store.');
if (go.ParentID !== 0 && this.objects[go.ParentID])
{
return this.findParent(this.objects[go.ParentID]);
}
else
{
return go;
}
}
getObjectByUUID(fullID: UUID | string): IGameObject
private populateChildren(obj: GameObject)
{
obj.children = [];
obj.totalChildren = 0;
for (const child of this.getObjectsByParent(obj.ID))
{
obj.totalChildren++;
this.populateChildren(child);
if (child.totalChildren !== undefined)
{
obj.totalChildren += child.totalChildren;
}
obj.children.push(child);
}
}
getNumberOfObjects()
{
return Object.keys(this.objects).length;
}
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObject[]
{
if (!this.rtree)
{
throw new Error('GetObjectsInArea not available with the Lite object store');
}
const result = this.rtree.search({
minX: minX,
maxX: maxX,
minY: minY,
maxY: maxY,
minZ: minZ,
maxZ: maxZ
});
const found: {[key: string]: GameObject} = {};
const objs: GameObject[] = [];
for (const obj of result)
{
const o = obj as ITreeBoundingBox;
const go = o.gameObject as GameObject;
if (go.PCode !== PCode.Avatar && (go.IsAttachment === undefined || go.IsAttachment === false))
{
try
{
const parent = this.findParent(go);
if (parent.PCode !== PCode.Avatar && (parent.IsAttachment === undefined || parent.IsAttachment === false))
{
const uuid = parent.FullID.toString();
if (found[uuid] === undefined)
{
found[uuid] = parent;
objs.push(parent);
}
}
}
catch (error)
{
console.log('Failed to find parent for ' + go.FullID.toString());
console.error(error);
// Unable to find parent, full object probably not fully loaded yet
}
}
}
// Now populate children of each found object
for (const obj of objs)
{
this.populateChildren(obj);
}
return objs;
}
getObjectByUUID(fullID: UUID | string): GameObject
{
if (fullID instanceof UUID)
{
@@ -472,7 +588,7 @@ export class ObjectStoreLite implements IObjectStore
return this.objects[localID];
}
getObjectByLocalID(localID: number): IGameObject
getObjectByLocalID(localID: number): GameObject
{
if (!this.objects[localID])
{
@@ -480,4 +596,33 @@ export class ObjectStoreLite implements IObjectStore
}
return this.objects[localID];
}
insertIntoRtree(obj: GameObject)
{
if (!this.rtree)
{
return;
}
if (obj.rtreeEntry !== undefined)
{
this.rtree.remove(obj.rtreeEntry);
}
if (!obj.Scale || !obj.Position || !obj.Rotation)
{
return;
}
const normalizedScale = obj.Scale.multiplyByQuat(obj.Rotation);
const bounds: ITreeBoundingBox = {
minX: obj.Position.x - (normalizedScale.x / 2),
maxX: obj.Position.x + (normalizedScale.x / 2),
minY: obj.Position.y - (normalizedScale.y / 2),
maxY: obj.Position.y + (normalizedScale.y / 2),
minZ: obj.Position.z - (normalizedScale.z / 2),
maxZ: obj.Position.z + (normalizedScale.z / 2),
gameObject: obj
};
obj.rtreeEntry = bounds;
this.rtree.insert(bounds);
}
}

View File

@@ -2,6 +2,7 @@ import {UUID} from '../UUID';
import {AgentAnimationMessage} from '../messages/AgentAnimation';
import {PacketFlags} from '../../enums/PacketFlags';
import {CommandsBase} from './CommandsBase';
import {Vector3} from '../Vector3';
export class AgentCommands extends CommandsBase
{
@@ -36,4 +37,27 @@ export class AgentCommands extends CommandsBase
{
return await this.animate(anim, false);
}
setCamera(position: Vector3, lookAt: Vector3, viewDistance?: number, leftAxis?: Vector3, upAxis?: Vector3)
{
this.agent.cameraCenter = position;
this.agent.cameraLookAt = lookAt;
if (viewDistance !== undefined)
{
this.agent.cameraFar = viewDistance;
}
if (leftAxis !== undefined)
{
this.agent.cameraLeftAxis = leftAxis;
}
if (upAxis !== undefined)
{
this.agent.cameraUpAxis = upAxis;
}
}
setViewDistance(viewDistance: number)
{
this.agent.cameraFar = viewDistance;
}
}

View File

@@ -6,10 +6,15 @@ import {Message} from '../../enums/Message';
import {FilterResponse} from '../../enums/FilterResponse';
import {RegionIDAndHandleReplyMessage} from '../messages/RegionIDAndHandleReply';
import {PacketFlags, Vector3} from '../..';
import {IGameObject} from '../interfaces/IGameObject';
import {ObjectGrabMessage} from '../messages/ObjectGrab';
import {ObjectDeGrabMessage} from '../messages/ObjectDeGrab';
import {ObjectGrabUpdateMessage} from '../messages/ObjectGrabUpdate';
import {GameObject} from '../GameObject';
import {ObjectSelectMessage} from '../messages/ObjectSelect';
import {ObjectPropertiesMessage} from '../messages/ObjectProperties';
import {Utils} from '../Utils';
import {ObjectDeselectMessage} from '../messages/ObjectDeselect';
import {PCode} from '../../enums/PCode';
export class RegionCommands extends CommandsBase
{
@@ -35,9 +40,277 @@ export class RegionCommands extends CommandsBase
return responseMsg.ReplyBlock.RegionHandle;
}
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): IGameObject[]
async deselectObjects(objects: GameObject[])
{
return this.currentRegion.objects.getObjectsInArea(minX, maxX, minY, maxY, minZ, maxZ);
// Limit to 255 objects at once
const selectLimit = 255;
if (objects.length > selectLimit)
{
for (let x = 0; x < objects.length; x += selectLimit)
{
const selectList: GameObject[] = [];
for (let y = 0; y < selectLimit; y++)
{
if (y < objects.length)
{
selectList.push(objects[x + y]);
}
}
await this.deselectObjects(selectList);
}
return;
}
else
{
const deselectObject = new ObjectDeselectMessage();
deselectObject.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID
};
deselectObject.ObjectData = [];
const uuidMap: {[key: string]: GameObject} = {};
for (const obj of objects)
{
const uuidStr = obj.FullID.toString();
if (!uuidMap[uuidStr])
{
uuidMap[uuidStr] = obj;
deselectObject.ObjectData.push({
ObjectLocalID: obj.ID
});
}
}
// Create a map of our expected UUIDs
const sequenceID = this.circuit.sendMessage(deselectObject, PacketFlags.Reliable);
return await this.circuit.waitForAck(sequenceID, 10000);
}
}
countObjects(): number
{
return this.currentRegion.objects.getNumberOfObjects();
}
async selectObjects(objects: GameObject[])
{
// Limit to 255 objects at once
const selectLimit = 255;
if (objects.length > selectLimit)
{
for (let x = 0; x < objects.length; x += selectLimit)
{
const selectList: GameObject[] = [];
for (let y = 0; y < selectLimit; y++)
{
if (y < objects.length)
{
selectList.push(objects[x + y]);
}
}
await this.selectObjects(selectList);
}
return;
}
else
{
const selectObject = new ObjectSelectMessage();
selectObject.AgentData = {
AgentID: this.agent.agentID,
SessionID: this.circuit.sessionID
};
selectObject.ObjectData = [];
const uuidMap: {[key: string]: GameObject} = {};
for (const obj of objects)
{
const uuidStr = obj.FullID.toString();
if (!uuidMap[uuidStr])
{
uuidMap[uuidStr] = obj;
selectObject.ObjectData.push({
ObjectLocalID: obj.ID
});
}
}
// Create a map of our expected UUIDs
let resolved = 0;
this.circuit.sendMessage(selectObject, PacketFlags.Reliable);
return await this.circuit.waitForMessage<ObjectPropertiesMessage>(Message.ObjectProperties, 10000, (propertiesMessage: ObjectPropertiesMessage): FilterResponse =>
{
let found = false;
for (const objData of propertiesMessage.ObjectData)
{
const objDataUUID = objData.ObjectID.toString();
if (uuidMap[objDataUUID] !== undefined)
{
resolved++;
const obj = uuidMap[objDataUUID];
obj.creatorID = objData.CreatorID;
obj.creationDate = objData.CreationDate;
obj.baseMask = objData.BaseMask;
obj.ownerMask = objData.OwnerMask;
obj.groupMask = objData.GroupMask;
obj.everyoneMask = objData.EveryoneMask;
obj.nextOwnerMask = objData.NextOwnerMask;
obj.ownershipCost = objData.OwnershipCost;
obj.saleType = objData.SaleType;
obj.salePrice = objData.SalePrice;
obj.aggregatePerms = objData.AggregatePerms;
obj.aggregatePermTextures = objData.AggregatePermTextures;
obj.aggregatePermTexturesOwner = objData.AggregatePermTexturesOwner;
obj.category = objData.Category;
obj.inventorySerial = objData.InventorySerial;
obj.itemID = objData.ItemID;
obj.folderID = objData.FolderID;
obj.fromTaskID = objData.FromTaskID;
obj.lastOwnerID = objData.LastOwnerID;
obj.name = Utils.BufferToStringSimple(objData.Name);
obj.description = Utils.BufferToStringSimple(objData.Description);
obj.touchName = Utils.BufferToStringSimple(objData.TouchName);
obj.sitName = Utils.BufferToStringSimple(objData.SitName);
obj.textureID = Utils.BufferToStringSimple(objData.TextureID);
obj.resolvedAt = new Date().getTime() / 1000;
delete uuidMap[objDataUUID];
found = true;
// console.log(obj.name + ' (' + resolved + ' of ' + objects.length + ')');
}
}
if (Object.keys(uuidMap).length === 0)
{
return FilterResponse.Finish;
}
if (!found)
{
return FilterResponse.NoMatch;
}
else
{
return FilterResponse.Match;
}
});
}
}
private async resolveObjects(objects: GameObject[])
{
// First, create a map of all object IDs
const objs: {[key: number]: GameObject} = {};
const scanObject = function(obj: GameObject)
{
const localID = obj.ID;
if (!objs[localID])
{
objs[localID] = obj;
if (obj.children)
{
for (const child of obj.children)
{
scanObject(child);
}
}
}
};
for (const obj of objects)
{
scanObject(obj);
}
const resolveTime = new Date().getTime() / 1000;
let objectList = [];
let totalRemaining = 0;
try
{
for (const k of Object.keys(objs))
{
const ky = parseInt(k, 10);
if (objs[ky] !== undefined)
{
const o = objs[ky];
if (o.resolvedAt === undefined)
{
o.resolvedAt = 0;
}
if (o.resolvedAt !== undefined && o.resolvedAt < resolveTime && o.PCode !== PCode.Avatar)
{
objs[ky].name = undefined;
totalRemaining++;
objectList.push(objs[ky]);
if (objectList.length > 254)
{
try
{
await this.selectObjects(objectList);
await this.deselectObjects(objectList);
for (const chk of objectList)
{
if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime)
{
totalRemaining--;
}
}
}
catch (ignore)
{
}
finally
{
objectList = [];
}
}
}
}
}
if (objectList.length > 0)
{
await this.selectObjects(objectList);
await this.deselectObjects(objectList);
for (const chk of objectList)
{
if (chk.resolvedAt !== undefined && chk.resolvedAt >= resolveTime)
{
totalRemaining --;
}
}
}
}
catch (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');
}
}
}
}
async getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number, resolve: boolean = false): Promise<GameObject[]>
{
const objs = this.currentRegion.objects.getObjectsInArea(minX, maxX, minY, maxY, minZ, maxZ);
if (resolve)
{
console.log('Resolving ' + objs.length + ' objects');
await this.resolveObjects(objs);
}
return objs;
}
async grabObject(localID: number | UUID,
@@ -51,7 +324,7 @@ export class RegionCommands extends CommandsBase
{
if (localID instanceof UUID)
{
const obj: IGameObject = this.currentRegion.objects.getObjectByUUID(localID);
const obj: GameObject = this.currentRegion.objects.getObjectByUUID(localID);
localID = obj.ID;
}
const msg = new ObjectGrabMessage();
@@ -88,7 +361,7 @@ export class RegionCommands extends CommandsBase
{
if (localID instanceof UUID)
{
const obj: IGameObject = this.currentRegion.objects.getObjectByUUID(localID);
const obj: GameObject = this.currentRegion.objects.getObjectByUUID(localID);
localID = obj.ID;
}
const msg = new ObjectDeGrabMessage();
@@ -126,7 +399,7 @@ export class RegionCommands extends CommandsBase
// For some reason this message takes a UUID when the others take a LocalID - wtf?
if (!(localID instanceof UUID))
{
const obj: IGameObject = this.currentRegion.objects.getObjectByLocalID(localID);
const obj: GameObject = this.currentRegion.objects.getObjectByLocalID(localID);
localID = obj.FullID;
}
const msg = new ObjectGrabUpdateMessage();
@@ -165,7 +438,7 @@ export class RegionCommands extends CommandsBase
{
if (localID instanceof UUID)
{
const obj: IGameObject = this.currentRegion.objects.getObjectByUUID(localID);
const obj: GameObject = this.currentRegion.objects.getObjectByUUID(localID);
localID = obj.ID;
}
await this.grabObject(localID, grabOffset, uvCoordinate, stCoordinate, faceIndex, position, normal, binormal);

View File

@@ -1,16 +0,0 @@
import {ITreeBoundingBox} from './ITreeBoundingBox';
import {UUID} from '../UUID';
import {PCode} from '../../enums/PCode';
export interface IGameObject
{
ID: number;
FullID: UUID;
ParentID: number;
OwnerID: UUID;
IsAttachment: boolean;
PCode: PCode;
rtreeEntry?: ITreeBoundingBox;
hasNameValueEntry(key: string): boolean;
getNameValueEntry(key: string): string;
}

View File

@@ -1,14 +1,14 @@
import {IGameObject} from './IGameObject';
import {RBush3D} from 'rbush-3d/dist';
import {GameObjectFull} from '../GameObjectFull';
import {UUID} from '../UUID';
import {GameObject} from '../GameObject';
export interface IObjectStore
{
rtree?: RBush3D;
getObjectsByParent(parentID: number): IGameObject[];
getObjectsByParent(parentID: number): GameObject[];
shutdown(): void;
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObjectFull[];
getObjectByUUID(fullID: UUID): IGameObject;
getObjectByLocalID(ID: number): IGameObject;
getObjectsInArea(minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number): GameObject[];
getObjectByUUID(fullID: UUID): GameObject;
getObjectByLocalID(ID: number): GameObject;
getNumberOfObjects(): number;
}

View File

@@ -1,7 +1,7 @@
import {BBox} from 'rbush-3d/dist';
import {IGameObject} from './IGameObject';
import {GameObject} from '../GameObject';
export interface ITreeBoundingBox extends BBox
{
gameObject: IGameObject;
gameObject: GameObject;
}