Extensive work on building, wearables, assets, inventory, attachments, serialization, etc.
Resolves #36
This commit is contained in:
@@ -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