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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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