Implement GLTF overrides

This commit is contained in:
Casper Warden
2023-11-10 23:57:26 +00:00
parent 3c69b8f05e
commit cb77865537
10 changed files with 1034 additions and 354 deletions

View File

@@ -13,25 +13,65 @@ describe('LLGLTFMaterial', () =>
assert.equal(mat.version, '1.1');
assert.equal(mat.type, 'GLTF 2.0');
assert.ok(mat.data);
assert.equal(mat.data.asset.version, '2.0');
assert.equal(mat.data.images.length, 4);
assert.equal(mat.data.images[0].uri, '2c7e7332-3717-5f4e-8f22-6ee9da5275fb');
assert.equal(mat.data.images[1].uri, '1078f5ec-1c56-e73f-8f8f-be36c40e5121');
assert.equal(mat.data.images[2].uri, '230d2d2d-b092-9bf5-ba7e-b319566322a6');
assert.equal(mat.data.images[3].uri, '230d2d2d-b092-9bf5-ba7e-b319566322a6');
assert.equal(mat.data.materials.length, 1);
assert.equal(mat.data.asset?.version, '2.0');
assert.equal(mat.data.images?.length, 4);
let image = mat.data?.images?.[0];
if (image && 'uri' in image)
{
assert.equal(image.uri, '2c7e7332-3717-5f4e-8f22-6ee9da5275fb');
}
else
{
assert(false, 'Image missing');
}
image = mat.data?.images?.[1];
if (image && 'uri' in image)
{
assert.equal(image.uri, '1078f5ec-1c56-e73f-8f8f-be36c40e5121');
}
else
{
assert(false, 'Image missing');
}
image = mat.data?.images?.[2];
if (image && 'uri' in image)
{
assert.equal(image.uri, '230d2d2d-b092-9bf5-ba7e-b319566322a6');
}
else
{
assert(false, 'Image missing');
}
image = mat.data?.images?.[3];
if (image && 'uri' in image)
{
assert.equal(image.uri, '230d2d2d-b092-9bf5-ba7e-b319566322a6');
}
else
{
assert(false, 'Image missing');
}
const mat0 = mat.data.materials[0];
assert.equal(mat0.normalTexture.index, 1);
assert.equal(mat0.occlusionTexture.index, 3);
assert.equal(mat0.pbrMetallicRoughness.baseColorTexture.index, 0);
assert.equal(mat0.pbrMetallicRoughness.metallicRoughnessTexture.index, 2);
assert.equal(mat.data.materials?.length, 1);
assert.equal(mat.data.textures.length, 4);
assert.equal(mat.data.textures[0].source, 0);
assert.equal(mat.data.textures[1].source, 1);
assert.equal(mat.data.textures[2].source, 2);
assert.equal(mat.data.textures[3].source, 3);
const mat0 = mat.data.materials?.[0];
if (mat0)
{
assert.equal(mat0.normalTexture?.index, 1);
assert.equal(mat0.occlusionTexture?.index, 3);
assert.equal(mat0.pbrMetallicRoughness?.baseColorTexture?.index, 0);
assert.equal(mat0.pbrMetallicRoughness?.metallicRoughnessTexture?.index, 2);
}
else
{
assert(false, 'Material missing');
}
assert.equal(mat.data.textures?.length, 4);
assert.equal(mat.data.textures?.[0].source, 0);
assert.equal(mat.data.textures?.[1].source, 1);
assert.equal(mat.data.textures?.[2].source, 2);
assert.equal(mat.data.textures?.[3].source, 3);
});
});
});

View File

@@ -1,28 +1,196 @@
export interface LLGLTFMaterialData
export interface LLGLTFExtensionsAndExtras
{
asset: {
version: string;
};
images: {
uri: string;
}[];
materials: {
normalTexture: {
index: number
},
occlusionTexture: {
index: number;
},
pbrMetallicRoughness: {
baseColorTexture: {
index: number
},
metallicRoughnessTexture: {
index: number
}
}
}[];
textures: {
source: number
}[];
extensions?: Record<string, unknown>
extras?: Record<string, unknown>
}
export interface LLGLTFTexture
{
index: number,
extensions?: Record<string, unknown>,
extras?: Record<string, unknown>,
texCoord?: number,
}
export type LLGLTFTextureInfo = LLGLTFTexture & LLGLTFExtensionsAndExtras;
export interface LLGLTFMaterialDataPart
{
asset?: {
version: string;
generator?: string;
minVersion?: string;
copyright?: string;
} & LLGLTFExtensionsAndExtras;
extensionsUsed?: string[];
extensionsRequired?: string[];
buffers?: ({
byteLength: number;
uri?: string;
type?: string;
name?: string;
} & LLGLTFExtensionsAndExtras)[],
bufferViews?: ({
buffer: number;
byteOffset?: number;
byteLength: number;
byteStride?: number;
target: number;
name?: string;
} & LLGLTFExtensionsAndExtras)[],
accessors?: ({
bufferView?: number;
byteOffset?: number;
normalized?: number;
componentType: number;
count: number;
type: string;
name?: string;
minValues?: number[];
maxValues?: number[];
sparse?: {
count: number;
bufferView: number;
indices: {
bufferView: number;
byteOffset?: number;
componentType?: number;
} & LLGLTFExtensionsAndExtras,
values: {
bufferView: number;
byteOffset?: number;
} & LLGLTFExtensionsAndExtras
} & LLGLTFExtensionsAndExtras
} & LLGLTFExtensionsAndExtras)[];
meshes?: ({
name?: string;
primitives?: ({
material?: number;
mode?: number;
indices?: number;
targets?: Record<string, number>[];
} & LLGLTFExtensionsAndExtras)[];
weights?: number[];
} & LLGLTFExtensionsAndExtras)[],
nodes?: ({
name?: string;
skin?: number;
camera?: number;
mesh?: number;
children?: number[];
weights?: number[];
emitter?: number;
light?: number;
} & (
{
rotation?: number[];
scale?: number[];
translaction?: number[]
} | {
matrix?: number[]
}
)) & LLGLTFExtensionsAndExtras[],
scenes?: ({
name?: string;
nodes?: number[];
} & LLGLTFExtensionsAndExtras)[];
scene?: number;
materials?: ({
name?: string;
emissiveFactor?: number[];
alphaMode?: string;
alphaCutoff?: number;
doubleSided?: boolean;
pbrMetallicRoughness?: {
baseColorFactor: number[];
baseColorTexture?: LLGLTFTextureInfo;
metallicRoughnessTexture?: LLGLTFTextureInfo;
metallicFactor: number;
roughnessFactor: number;
} & LLGLTFExtensionsAndExtras,
normalTexture?: {
index: number;
texCoord?: number;
scale?: number;
} & LLGLTFExtensionsAndExtras,
occlusionTexture?: {
index: number;
texCoord?: number;
strength?: number;
} & LLGLTFExtensionsAndExtras,
emissiveTexture?: {
extensions?: {
KHR_texture_transform?: {
offset: number[],
rotation: number,
scale: number[]
}
},
index: number
texCoord?: number;
}
} & LLGLTFExtensionsAndExtras)[];
images?: (({
bufferView: number;
mimeType: string;
width: number;
height: number;
} | {
uri: string;
}) & LLGLTFExtensionsAndExtras)[];
textures?: ({
sampler?: number;
source?: number;
name?: string;
} & LLGLTFExtensionsAndExtras)[];
animations?: ({
name?: string;
channels?: ({
sampler: number;
target?: {
node?: number;
path: string;
} & LLGLTFExtensionsAndExtras
} & LLGLTFExtensionsAndExtras)[]
samplers?: ({
input: number;
output: number;
interpolation?: string;
} & LLGLTFExtensionsAndExtras)[]
} & LLGLTFExtensionsAndExtras)[];
skins?: ({
name?: string;
joints?: number[];
skeleton?: number;
inverseBindMatrices: number;
} & LLGLTFExtensionsAndExtras)[];
samplers?: ({
name?: string;
minFilter?: number;
magFilter: number;
wrapS?: number;
wrapT?: number;
} & LLGLTFExtensionsAndExtras)[];
cameras?: (({
name?: string;
type: 'orthographic';
orthographic: {
xmag: number;
ymag: number;
zfar: number;
znear: number;
} & LLGLTFExtensionsAndExtras
} | {
name?: string;
type: 'perspective';
perspective: {
yfov: number;
znear: number;
aspectRatio?: number;
zfar?: number;
} & LLGLTFExtensionsAndExtras
}) & LLGLTFExtensionsAndExtras)[];
}
export type LLGLTFMaterialData = LLGLTFMaterialDataPart & LLGLTFExtensionsAndExtras;

View File

@@ -0,0 +1,19 @@
export interface LLGLTFTextureTransformOverride
{
offset: number[];
scale: number[];
rotation: number
}
export class LLGLTFMaterialOverride
{
public textures?: string[];
public baseColor?: number[];
public emissiveColor?: number[];
public metallicFactor?: number;
public roughnessFactor?: number;
public alphaMode?: number;
public alphaCutoff?: number;
public doubleSided?: boolean;
public textureTransforms?: LLGLTFTextureTransformOverride[];
}

View File

@@ -33,6 +33,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
{
super(circuit, agent, clientEvents, options);
this.rtree = new RBush3D();
this.fullStore = true;
}
protected objectUpdate(objectUpdate: ObjectUpdateMessage): void
@@ -43,17 +44,19 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
const parentID = objData.ParentID;
let addToParentList = true;
let newObject = false;
if (this.objects[localID])
let obj = this.objects.get(localID);
if (obj)
{
if (this.objects[localID].ParentID !== parentID && this.objectsByParent[parentID])
const objsByParent = this.objectsByParent.get(parentID ?? 0);
if (obj.ParentID !== parentID && objsByParent)
{
const ind = this.objectsByParent[parentID].indexOf(localID);
const ind = objsByParent.indexOf(localID);
if (ind !== -1)
{
this.objectsByParent[parentID].splice(ind, 1);
objsByParent.splice(ind, 1);
}
}
else if (this.objectsByParent[parentID])
else if (objsByParent)
{
addToParentList = false;
}
@@ -61,12 +64,12 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
else
{
newObject = true;
this.objects[localID] = new GameObject();
this.objects[localID].region = this.agent.currentRegion;
obj = new GameObject();
obj.region = this.agent.currentRegion;
this.objects.set(localID, obj);
}
this.objects[localID].deleted = false;
obj.deleted = false;
const obj = this.objects[localID];
obj.ID = objData.ID;
obj.State = objData.State;
obj.FullID = objData.FullID;
@@ -100,6 +103,12 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
obj.ProfileEnd = Utils.unpackEndCut(objData.ProfileEnd);
obj.ProfileHollow = Utils.unpackProfileHollow(objData.ProfileHollow);
obj.TextureEntry = TextureEntry.from(objData.TextureEntry);
const override = this.cachedMaterialOverrides.get(obj.ID);
if (override)
{
obj.TextureEntry.gltfMaterialOverrides = override;
this.cachedMaterialOverrides.delete(obj.ID);
}
obj.textureAnim = TextureAnim.from(objData.TextureAnim);
const pcodeData = objData.Data;
@@ -131,7 +140,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
break;
}
if (this.objects[localID].PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString())
if (obj.PCode === PCode.Avatar && obj.FullID.toString() === this.agent.agentID.toString())
{
this.agent.localID = localID;
@@ -143,50 +152,53 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
if (parent !== this.agent.localID)
{
let foundAvatars = false;
for (const objID of this.objectsByParent[parent])
const objsByParent = this.objectsByParent.get(parent);
if (objsByParent)
{
if (this.objects[objID])
for (const objID of objsByParent)
{
const o = this.objects[objID];
if (o.PCode === PCode.Avatar)
const ob = this.objects.get(objID);
if (ob && ob.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
}
if (this.objects[parent])
{
const o = this.objects[parent];
if (o.PCode === PCode.Avatar)
const o = this.objects.get(parent);
if (o && o.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
if (!foundAvatars)
{
this.deleteObject(parent);
if (!foundAvatars)
{
this.deleteObject(parent);
}
}
}
}
}
}
this.objects[localID].extraParams = ExtraParams.from(objData.ExtraParams);
this.objects[localID].NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined;
obj.extraParams = ExtraParams.from(objData.ExtraParams);
obj.NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
obj.IsAttachment = obj.NameValue['AttachItemID'] !== undefined;
if (obj.IsAttachment && obj.State !== undefined)
{
this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State);
obj.attachmentPoint = this.decodeAttachPoint(obj.State);
}
this.objectsByUUID[objData.FullID.toString()] = localID;
if (!this.objectsByParent[parentID])
this.objectsByUUID.set(objData.FullID.toString(), localID);
let objByParent = this.objectsByParent.get(parentID);
if (!objByParent)
{
this.objectsByParent[parentID] = [];
objByParent = [];
this.objectsByParent.set(parentID, objByParent);
}
if (addToParentList)
{
this.objectsByParent[parentID].push(localID);
objByParent.push(localID);
}
if (objData.PCode !== PCode.Avatar && this.options & BotOptionFlags.StoreMyAttachmentsOnly && (this.agent.localID !== 0 && obj.ParentID !== this.agent.localID))
@@ -197,7 +209,8 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
else
{
this.insertIntoRtree(obj);
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID])
const parentObj = this.objects.get(objData.ParentID ?? 0);
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !parentObj)
{
this.requestMissingObject(objData.ParentID).then(() =>
{
@@ -225,7 +238,7 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
rmo.ObjectData = [];
for (const obj of objectUpdateCached.ObjectData)
{
if (!this.objects[obj.ID])
if (!this.objects.has(obj.ID))
{
rmo.ObjectData.push({
CacheMissType: 0,
@@ -257,15 +270,16 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
pos += 4;
const pcode = buf.readUInt8(pos++);
let newObj = false;
if (!this.objects[localID])
let o = this.objects.get(localID);
if (!o)
{
newObj = true;
this.objects[localID] = new GameObject();
this.objects[localID].region = this.agent.currentRegion;
o = new GameObject();
o.region = this.agent.currentRegion;
this.objects.set(localID, o);
}
const o = this.objects[localID];
o.ID = localID;
this.objectsByUUID[fullID.toString()] = localID;
this.objectsByUUID.set(fullID.toString(), localID);
o.FullID = fullID;
o.Flags = flags;
o.PCode = pcode;
@@ -301,26 +315,29 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
let add = true;
if (!newObj && o.ParentID !== undefined)
{
if (newParentID !== o.ParentID)
const obsByParent = this.objectsByParent.get(o.ParentID);
if (newParentID !== o.ParentID && obsByParent)
{
const index = this.objectsByParent[o.ParentID].indexOf(localID);
const index = obsByParent.indexOf(localID);
if (index !== -1)
{
this.objectsByParent[o.ParentID].splice(index, 1);
obsByParent.splice(index, 1);
}
}
else if (this.objectsByParent[o.ParentID])
else if (obsByParent)
{
add = false;
}
}
if (add)
{
if (!this.objectsByParent[newParentID])
let objsByNewParent = this.objectsByParent.get(newParentID);
if (!objsByNewParent)
{
this.objectsByParent[newParentID] = [];
objsByNewParent = [];
this.objectsByParent.set(newParentID, objsByNewParent);
}
this.objectsByParent[newParentID].push(localID);
objsByNewParent.push(localID);
}
if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly && (this.agent.localID !== 0 && o.ParentID !== this.agent.localID))
@@ -331,9 +348,12 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
}
else
{
if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects[o.ParentID])
if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects.has(o.ParentID))
{
this.requestMissingObject(o.ParentID);
this.requestMissingObject(o.ParentID).catch((e) =>
{
console.error(e);
});
}
if (compressedflags & CompressedFlags.Tree)
{
@@ -420,6 +440,12 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
const textureEntryLength = buf.readUInt32LE(pos);
pos = pos + 4;
o.TextureEntry = TextureEntry.from(buf.slice(pos, pos + textureEntryLength));
const override = this.cachedMaterialOverrides.get(o.ID);
if (override)
{
o.TextureEntry.gltfMaterialOverrides = override;
this.cachedMaterialOverrides.delete(o.ID);
}
pos = pos + textureEntryLength;
if (compressedflags & CompressedFlags.TextureAnimation)
@@ -432,7 +458,7 @@ 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);
o.attachmentPoint = this.decodeAttachPoint(o.State);
}
this.insertIntoRtree(o);
@@ -456,37 +482,38 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
let pos = 0;
const localID = objectData.Data.readUInt32LE(pos);
pos = pos + 4;
if (this.objects[localID])
const o = this.objects.get(localID);
if (o)
{
this.objects[localID].State = objectData.Data.readUInt8(pos++);
o.State = objectData.Data.readUInt8(pos++);
const avatar: boolean = (objectData.Data.readUInt8(pos++) !== 0);
if (avatar)
{
this.objects[localID].CollisionPlane = new Vector4(objectData.Data, pos);
o.CollisionPlane = new Vector4(objectData.Data, pos);
pos += 16;
}
this.objects[localID].Position = new Vector3(objectData.Data, pos);
o.Position = new Vector3(objectData.Data, pos);
pos += 12;
this.objects[localID].Velocity = new Vector3([
o.Velocity = new Vector3([
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos), -128.0, 128.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 2), -128.0, 128.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 4), -128.0, 128.0)
]);
pos += 6;
this.objects[localID].Acceleration = new Vector3([
o.Acceleration = new Vector3([
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos), -64.0, 64.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 2), -64.0, 64.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 4), -64.0, 64.0)
]);
pos += 6;
this.objects[localID].Rotation = new Quaternion([
o.Rotation = new Quaternion([
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos), -1.0, 1.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 2), -1.0, 1.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 4), -1.0, 1.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 6), -1.0, 1.0)
]);
pos += 8;
this.objects[localID].AngularVelocity = new Vector3([
o.AngularVelocity = new Vector3([
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos), -64.0, 64.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 2), -64.0, 64.0),
Utils.UInt16ToFloat(objectData.Data.readUInt16LE(pos + 4), -64.0, 64.0)
@@ -496,11 +523,17 @@ export class ObjectStoreFull extends ObjectStoreLite implements IObjectStore
if (objectData.TextureEntry.length > 0)
{
// 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();
o.TextureEntry = TextureEntry.from(objectData.TextureEntry.slice(4));
const override = this.cachedMaterialOverrides.get(o.ID);
if (override)
{
o.TextureEntry.gltfMaterialOverrides = override;
this.cachedMaterialOverrides.delete(o.ID);
}
o.onTextureUpdate.next();
}
this.insertIntoRtree(this.objects[localID]);
this.notifyTerseUpdate(this.objects[localID]);
this.insertIntoRtree(o);
this.notifyTerseUpdate(o);
}
else

View File

@@ -40,22 +40,54 @@ import Timer = NodeJS.Timer;
import { GenericStreamingMessageMessage } from './messages/GenericStreamingMessage';
import { LLSDNotationParser } from './llsd/LLSDNotationParser';
import { LLSDMap } from './llsd/LLSDMap';
import { LLGLTFMaterialOverride, LLGLTFTextureTransformOverride } from './LLGLTFMaterialOverride';
import * as Long from 'long';
export class ObjectStoreLite implements IObjectStore
{
protected circuit?: Circuit;
protected agent: Agent;
protected objects: { [key: number]: GameObject } = {};
protected objectsByUUID: { [key: string]: number } = {};
protected objectsByParent: { [key: number]: number[] } = {};
protected objects = new Map<number, GameObject>();
protected objectsByUUID = new Map<string, number>();
protected objectsByParent = new Map<number, number[]>();
protected clientEvents: ClientEvents;
protected options: BotOptionFlags;
protected requestedObjects: { [key: number]: boolean } = {};
protected fullStore = false;
protected requestedObjects = new Set<number>();
protected deadObjects: number[] = [];
protected persist = false;
protected pendingObjectProperties: { [key: string]: any } = {};
protected cachedMaterialOverrides = new Map<number, Map<number, LLGLTFMaterialOverride>>();
protected pendingObjectProperties = new Map<string, {
ObjectID: UUID;
CreatorID: UUID;
OwnerID: UUID;
GroupID: 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: Buffer;
Description: Buffer;
TouchName: Buffer;
SitName: Buffer;
TextureID: Buffer;
}>;
private physicsSubscription: Subscription;
private selectedPrimsWithoutUpdate: { [key: number]: boolean } = {};
private selectedPrimsWithoutUpdate = new Map<number, boolean>();
private selectedChecker?: Timer;
rtree?: RBush3D;
@@ -64,6 +96,7 @@ export class ObjectStoreLite implements IObjectStore
{
agent.localID = 0;
this.options = options;
this.fullStore = false;
this.clientEvents = clientEvents;
this.circuit = circuit;
this.agent = agent;
@@ -81,6 +114,10 @@ export class ObjectStoreLite implements IObjectStore
{
case Message.GenericStreamingMessage:
{
if (!this.fullStore)
{
return;
}
const genMsg = packet.message as GenericStreamingMessageMessage;
if (genMsg.MethodData.Method === 0x4175)
{
@@ -88,16 +125,141 @@ export class ObjectStoreLite implements IObjectStore
const result = LLSDNotationParser.parse(genMsg.DataBlock.Data.toString('utf-8'));
if (result instanceof LLSDMap)
{
const arr = result.get('te');
if (Array.isArray(arr))
const localID = result.get('id');
if (typeof localID !== 'number')
{
if (arr.length === 0)
{
return;
}
return;
}
const tes = result.get('te');
const ods = result.get('od');
// TODO: figure out what to do with this..
console.log(JSON.stringify(result, null, 4));
const overrides = new Map<number, LLGLTFMaterialOverride>();
if (Array.isArray(tes) && Array.isArray(ods) && tes.length === ods.length)
{
for (let x = 0; x < tes.length; x++)
{
const te = tes[x];
if (typeof te !== 'number')
{
continue;
}
const params = ods[x];
if (!(params instanceof LLSDMap))
{
continue;
}
const textureIDs = params.get('tex');
const baseColor = params.get('bc');
const emissiveColor = params.get('ec');
const metallicFactor = params.get('mf');
const roughnessFactor = params.get('rf');
const alphaMode = params.get('am');
const alphaCutoff = params.get('ac');
const doubleSided = params.get('ds');
const textureTransforms = params.get('ti');
const override = new LLGLTFMaterialOverride();
overrides.set(te, override);
if (textureIDs !== undefined && Array.isArray(textureIDs) && textureIDs.length === 4)
{
override.textures = [];
for (const tex of textureIDs)
{
if (typeof tex === 'string')
{
override.textures.push(tex);
}
else if (tex instanceof UUID)
{
override.textures.push(tex.toString());
}
}
}
function isNumberArray(array: unknown[]): array is number[]
{
return array.every(element => typeof element === 'number');
}
if (baseColor !== undefined && Array.isArray(baseColor) && baseColor.length === 4 && isNumberArray(baseColor))
{
override.baseColor = baseColor;
}
if (emissiveColor !== undefined && Array.isArray(emissiveColor) && emissiveColor.length === 3 && isNumberArray(emissiveColor))
{
override.emissiveColor = emissiveColor;
}
if (metallicFactor !== undefined && typeof metallicFactor === 'number')
{
override.metallicFactor = metallicFactor;
}
if (roughnessFactor !== undefined && typeof roughnessFactor === 'number')
{
override.roughnessFactor = roughnessFactor;
}
if (alphaMode !== undefined && typeof alphaMode === 'number')
{
override.alphaMode = alphaMode;
}
if (alphaCutoff !== undefined && typeof alphaCutoff === 'number')
{
override.alphaCutoff = alphaCutoff;
}
if (doubleSided !== undefined && typeof doubleSided === 'boolean')
{
override.doubleSided = doubleSided;
}
function isLLGLTFTextureTransformOverride(objToCheck: unknown): objToCheck is LLGLTFTextureTransformOverride
{
const isArrayOfNumbers = (value: unknown): value is number[] =>
{
return Array.isArray(value) && value.every(item => typeof item === 'number');
};
// Validate the object structure and types
return (
typeof objToCheck === 'object' &&
objToCheck !== null &&
'offset' in objToCheck && isArrayOfNumbers((objToCheck as LLGLTFTextureTransformOverride).offset) &&
'scale' in objToCheck && isArrayOfNumbers((objToCheck as LLGLTFTextureTransformOverride).scale) &&
'rotation' in objToCheck && typeof (objToCheck as LLGLTFTextureTransformOverride).rotation === 'number'
);
}
if (textureTransforms !== undefined && Array.isArray(textureTransforms) && textureTransforms.length === 4)
{
override.textureTransforms = [];
for (const transform of textureTransforms)
{
if (isLLGLTFTextureTransformOverride(transform))
{
override.textureTransforms.push(transform);
}
}
}
}
const obj = this.objects.get(localID);
const textureEntry = obj?.TextureEntry;
if (textureEntry)
{
textureEntry.gltfMaterialOverrides = overrides;
}
else
{
this.cachedMaterialOverrides.set(localID, overrides);
}
}
}
}
@@ -108,15 +270,15 @@ export class ObjectStoreLite implements IObjectStore
const objProp = packet.message as ObjectPropertiesMessage;
for (const obj of objProp.ObjectData)
{
const obje = this.objectsByUUID[obj.ObjectID.toString()];
if (obje !== undefined && this.objects[obje] !== undefined)
const obje = this.objectsByUUID.get(obj.ObjectID.toString());
const o = this.objects.get(obje ?? 0);
if (obje !== undefined && o !== undefined)
{
const o = this.objects[obje];
this.applyObjectProperties(o, obj);
}
else
{
this.pendingObjectProperties[obj.ObjectID.toString()] = obj;
this.pendingObjectProperties.set(obj.ObjectID.toString(), obj);
}
}
break;
@@ -156,13 +318,14 @@ export class ObjectStoreLite implements IObjectStore
this.physicsSubscription = this.clientEvents.onPhysicsDataEvent.subscribe((evt: ObjectPhysicsDataEvent) =>
{
if (this.objects[evt.localID])
const obj = this.objects.get(evt.localID);
if (obj)
{
this.objects[evt.localID].physicsShapeType = evt.physicsShapeType;
this.objects[evt.localID].density = evt.density;
this.objects[evt.localID].restitution = evt.restitution;
this.objects[evt.localID].gravityMultiplier = evt.gravityMultiplier;
this.objects[evt.localID].friction = evt.friction;
obj.physicsShapeType = evt.physicsShapeType;
obj.density = evt.density;
obj.restitution = evt.restitution;
obj.gravityMultiplier = evt.gravityMultiplier;
obj.friction = evt.friction;
}
});
@@ -224,10 +387,7 @@ export class ObjectStoreLite implements IObjectStore
private applyObjectProperties(o: GameObject, obj: any): void
{
if (this.selectedPrimsWithoutUpdate[o.ID])
{
delete this.selectedPrimsWithoutUpdate[o.ID];
}
this.selectedPrimsWithoutUpdate.delete(o.ID);
// const n = Utils.BufferToStringSimple(obj.Name); // Currently unused
o.creatorID = obj.CreatorID;
o.creationDate = obj.CreationDate;
@@ -278,7 +438,7 @@ export class ObjectStoreLite implements IObjectStore
protected async requestMissingObject(localID: number, attempt = 0): Promise<void>
{
if (this.requestedObjects[localID])
if (this.requestedObjects.has(localID))
{
return;
}
@@ -286,7 +446,7 @@ export class ObjectStoreLite implements IObjectStore
{
return;
}
this.requestedObjects[localID] = true;
this.requestedObjects.add(localID);
const rmo = new RequestMultipleObjectsMessage();
rmo.AgentData = {
AgentID: this.agent.agentID,
@@ -324,11 +484,11 @@ export class ObjectStoreLite implements IObjectStore
}
return FilterResponse.NoMatch;
});
delete this.requestedObjects[localID];
this.requestedObjects.delete(localID);
}
catch (error)
{
delete this.requestedObjects[localID];
this.requestedObjects.delete(localID);
if (attempt < 5)
{
await this.requestMissingObject(localID, ++attempt);
@@ -371,17 +531,19 @@ export class ObjectStoreLite implements IObjectStore
let addToParentList = true;
let newObject = false;
if (this.objects[localID])
let obj = this.objects.get(localID);
if (obj)
{
if (this.objects[localID].ParentID !== parentID && this.objectsByParent[parentID])
const p = this.objectsByParent.get(parentID);
if (obj.ParentID !== parentID && p !== undefined)
{
const ind = this.objectsByParent[parentID].indexOf(localID);
const ind = p.indexOf(localID);
if (ind !== -1)
{
this.objectsByParent[parentID].splice(ind, 1);
p.splice(ind, 1);
}
}
else if (this.objectsByParent[parentID])
else if (p)
{
addToParentList = false;
}
@@ -389,27 +551,28 @@ export class ObjectStoreLite implements IObjectStore
else
{
newObject = true;
this.objects[localID] = new GameObject();
this.objects[localID].region = this.agent.currentRegion;
const newObj = new GameObject();
newObj.region = this.agent.currentRegion;
this.objects.set(localID, newObj);
}
const obj = this.objects[localID];
obj.deleted = false;
obj.ID = objData.ID;
obj.FullID = objData.FullID;
obj.ParentID = objData.ParentID;
obj.OwnerID = objData.OwnerID;
obj.PCode = objData.PCode;
obj = this.objects.get(localID);
obj!.deleted = false;
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));
obj!.NameValue = this.parseNameValues(Utils.BufferToStringSimple(objData.NameValue));
this.objects[localID].IsAttachment = this.objects[localID].NameValue['AttachItemID'] !== undefined;
if (obj.IsAttachment && obj.State !== undefined)
obj!.IsAttachment = obj!.NameValue.AttachItemID !== undefined;
if (obj!.IsAttachment && obj!.State !== undefined)
{
this.objects[localID].attachmentPoint = this.decodeAttachPoint(obj.State);
obj!.attachmentPoint = this.decodeAttachPoint(obj!.State);
}
if (objData.PCode === PCode.Avatar && this.objects[localID].FullID.toString() === this.agent.agentID.toString())
if (objData.PCode === PCode.Avatar && obj!.FullID.toString() === this.agent.agentID.toString())
{
this.agent.localID = localID;
@@ -421,21 +584,25 @@ export class ObjectStoreLite implements IObjectStore
if (parent !== this.agent.localID)
{
let foundAvatars = false;
for (const objID of this.objectsByParent[parent])
const p = this.objectsByParent.get(parent);
if (p !== undefined)
{
if (this.objects[objID])
for (const objID of p)
{
const o = this.objects[objID];
if (o.PCode === PCode.Avatar)
const childObj = this.objects.get(objID);
if (childObj)
{
foundAvatars = true;
if (childObj.PCode === PCode.Avatar)
{
foundAvatars = true;
}
}
}
}
if (this.objects[parent])
const parentObj = this.objects.get(parent);
if (parentObj)
{
const o = this.objects[parent];
if (o.PCode === PCode.Avatar)
if (parentObj.PCode === PCode.Avatar)
{
foundAvatars = true;
}
@@ -449,19 +616,21 @@ export class ObjectStoreLite implements IObjectStore
}
}
this.objectsByUUID[objData.FullID.toString()] = localID;
if (!this.objectsByParent[parentID])
this.objectsByUUID.set(objData.FullID.toString(), localID);
let objByParent = this.objectsByParent.get(parentID);
if (!objByParent)
{
this.objectsByParent[parentID] = [];
objByParent = [];
this.objectsByParent.set(parentID, objByParent);
}
if (addToParentList)
{
this.objectsByParent[parentID].push(localID);
objByParent.push(localID);
}
if (objData.PCode !== PCode.Avatar && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
if (this.agent.localID !== 0 && obj.ParentID !== this.agent.localID)
if (this.agent.localID !== 0 && obj!.ParentID !== this.agent.localID)
{
// Drop object
this.deleteObject(localID);
@@ -469,9 +638,9 @@ export class ObjectStoreLite implements IObjectStore
}
}
this.notifyObjectUpdate(newObject, obj);
this.notifyObjectUpdate(newObject, obj!);
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects[objData.ParentID])
if (objData.ParentID !== undefined && objData.ParentID !== 0 && !this.objects.get(objData.ParentID))
{
this.requestMissingObject(objData.ParentID);
}
@@ -480,7 +649,7 @@ export class ObjectStoreLite implements IObjectStore
protected notifyTerseUpdate(obj: GameObject): void
{
if (this.objects[obj.ID])
if (this.objects.get(obj.ID))
{
if (obj.PCode === PCode.Avatar)
{
@@ -531,15 +700,16 @@ export class ObjectStoreLite implements IObjectStore
}
}
}
if (obj.ParentID === 0 || (obj.ParentID !== undefined && this.objects[obj.ParentID] !== undefined && this.objects[obj.ParentID].PCode === PCode.Avatar))
const parentObj = this.objects.get(obj.ParentID ?? 0);
if (obj.ParentID === 0 || (obj.ParentID !== undefined && parentObj !== undefined && parentObj.PCode === PCode.Avatar))
{
if (newObject)
{
if (obj.IsAttachment && obj.ParentID !== undefined)
{
if (this.objects[obj.ParentID] !== undefined && this.objects[obj.ParentID].PCode === PCode.Avatar)
if (parentObj !== undefined && parentObj.PCode === PCode.Avatar)
{
const avatar = this.agent.currentRegion.agents[this.objects[obj.ParentID].FullID.toString()];
const avatar = this.agent.currentRegion.agents[parentObj.FullID.toString()];
let invItemID = UUID.zero();
if (obj.NameValue['AttachItemID'])
@@ -581,9 +751,9 @@ export class ObjectStoreLite implements IObjectStore
obj.createdSelected = newObj.createSelected;
// tslint:disable-next-line:no-bitwise
// noinspection JSBitwiseOperatorUsage
if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties[obj.FullID.toString()])
if (obj.Flags !== undefined && obj.Flags & PrimFlags.CreateSelected && !this.pendingObjectProperties.get(obj.FullID.toString()))
{
this.selectedPrimsWithoutUpdate[obj.ID] = true;
this.selectedPrimsWithoutUpdate.set(obj.ID, true);
}
this.clientEvents.onNewObjectEvent.next(newObj);
}
@@ -595,10 +765,11 @@ export class ObjectStoreLite implements IObjectStore
updObj.object = obj;
this.clientEvents.onObjectUpdatedEvent.next(updObj);
}
if (this.pendingObjectProperties[obj.FullID.toString()])
const pendingProp = this.pendingObjectProperties.get(obj.FullID.toString());
if (pendingProp)
{
this.applyObjectProperties(obj, this.pendingObjectProperties[obj.FullID.toString()]);
delete this.pendingObjectProperties[obj.FullID.toString()];
this.applyObjectProperties(obj, pendingProp);
this.pendingObjectProperties.delete(obj.FullID.toString());
}
}
}
@@ -637,18 +808,18 @@ export class ObjectStoreLite implements IObjectStore
const localID = buf.readUInt32LE(pos);
pos += 4;
const pcode = buf.readUInt8(pos++);
let newObj = false;
if (!this.objects[localID])
const newObj = false;
let o = this.objects.get(localID);
if (!o)
{
newObj = true;
this.objects[localID] = new GameObject();
this.objects[localID].region = this.agent.currentRegion;
o = new GameObject();
o.region = this.agent.currentRegion;
this.objects.set(localID, o);
}
const o = this.objects[localID];
o.deleted = false;
o.ID = localID;
o.PCode = pcode;
this.objectsByUUID[fullID.toString()] = localID;
this.objectsByUUID.set(fullID.toString(), localID);
o.FullID = fullID;
@@ -675,26 +846,29 @@ export class ObjectStoreLite implements IObjectStore
let add = true;
if (!newObj && o.ParentID !== undefined)
{
if (newParentID !== o.ParentID)
const p = this.objectsByParent.get(o.ParentID);
if (newParentID !== o.ParentID && p)
{
const index = this.objectsByParent[o.ParentID].indexOf(localID);
if (index !== -1)
const ind = p.indexOf(localID);
if (ind !== -1)
{
this.objectsByParent[o.ParentID].splice(index, 1);
p.splice(ind, 1);
}
}
else if (this.objectsByParent[o.ParentID])
else if (p)
{
add = false;
}
}
if (add)
{
if (!this.objectsByParent[newParentID])
let objByParent = this.objectsByParent.get(newParentID);
if (!objByParent)
{
this.objectsByParent[newParentID] = [];
objByParent = [];
this.objectsByParent.set(newParentID, objByParent);
}
this.objectsByParent[newParentID].push(localID);
objByParent.push(localID);
}
if (pcode !== PCode.Avatar && newObj && this.options & BotOptionFlags.StoreMyAttachmentsOnly)
{
@@ -705,9 +879,12 @@ export class ObjectStoreLite implements IObjectStore
return;
}
}
if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects[o.ParentID])
if (o.ParentID !== undefined && o.ParentID !== 0 && !this.objects.has(o.ParentID))
{
this.requestMissingObject(o.ParentID);
this.requestMissingObject(o.ParentID).catch((e) =>
{
console.error(e);
});
}
if (compressedflags & CompressedFlags.Tree)
{
@@ -789,7 +966,7 @@ export class ObjectStoreLite implements IObjectStore
for (const obj of killObj.ObjectData)
{
const objectID = obj.ID;
if (this.objects[objectID])
if (this.objects.has(objectID))
{
this.deleteObject(objectID);
}
@@ -811,10 +988,10 @@ export class ObjectStoreLite implements IObjectStore
deleteObject(objectID: number): void
{
if (this.objects[objectID])
const obj = this.objects.get(objectID);
if (obj)
{
const objectUUID = this.objects[objectID].FullID;
const obj = this.objects[objectID];
const objectUUID = obj.FullID;
obj.deleted = true;
if (this.persist)
@@ -825,9 +1002,10 @@ export class ObjectStoreLite implements IObjectStore
if (obj.IsAttachment && obj.ParentID !== undefined)
{
if (this.objects[obj.ParentID] !== undefined && this.objects[obj.ParentID].PCode === PCode.Avatar)
const parent = this.objects.get(obj.ParentID);
if (parent !== undefined && parent.PCode === PCode.Avatar)
{
this.agent.currentRegion.agents[this.objects[obj.ParentID].FullID.toString()]?.removeAttachment(obj);
this.agent.currentRegion.agents[parent.FullID.toString()]?.removeAttachment(obj);
}
}
@@ -837,31 +1015,31 @@ export class ObjectStoreLite implements IObjectStore
}
// First, kill all children (not the people kind)
if (this.objectsByParent[objectID])
const objsByParent = this.objectsByParent.get(objectID);
if (objsByParent)
{
for (const childObjID of this.objectsByParent[objectID])
for (const childObjID of objsByParent)
{
this.deleteObject(childObjID);
}
}
delete this.objectsByParent[objectID];
this.objectsByParent.delete(objectID);
// Now delete this object
const uuid = obj.FullID.toString();
if (this.objectsByUUID[uuid])
{
delete this.objectsByUUID[uuid];
}
this.objectsByUUID.delete(uuid);
if (obj.ParentID !== undefined)
{
const parentID = obj.ParentID;
if (this.objectsByParent[parentID])
const objsByParentParent = this.objectsByParent.get(parentID);
if (objsByParentParent)
{
const ind = this.objectsByParent[parentID].indexOf(objectID);
const ind = objsByParentParent.indexOf(objectID);
if (ind !== -1)
{
this.objectsByParent[parentID].splice(ind, 1);
objsByParentParent.splice(ind, 1);
}
}
}
@@ -869,12 +1047,13 @@ export class ObjectStoreLite implements IObjectStore
{
this.rtree.remove(obj.rtreeEntry);
}
delete this.objects[objectID];
this.objects.delete(objectID);
this.cachedMaterialOverrides.delete(objectID);
}
}
getObjectsByParent(parentID: number): GameObject[]
{
const list = this.objectsByParent[parentID];
const list = this.objectsByParent.get(parentID);
if (list === undefined)
{
return [];
@@ -882,9 +1061,10 @@ export class ObjectStoreLite implements IObjectStore
const result: GameObject[] = [];
for (const localID of list)
{
if (this.objects[localID])
const obj = this.objects.get(localID);
if (obj)
{
result.push(this.objects[localID]);
result.push(obj);
}
}
result.sort((a: GameObject, b: GameObject) =>
@@ -933,25 +1113,26 @@ export class ObjectStoreLite implements IObjectStore
delete this.selectedChecker;
}
this.physicsSubscription.unsubscribe();
this.objects = {};
this.objects.clear();
if (this.rtree)
{
this.rtree.clear();
}
this.objectsByUUID = {};
this.objectsByParent = {};
this.objectsByUUID.clear();
this.objectsByParent.clear()
delete this.circuit;
}
protected findParent(go: GameObject): GameObject
{
if (go.ParentID !== undefined && go.ParentID !== 0 && this.objects[go.ParentID])
const parentObj = this.objects.get(go.ParentID ?? 0);
if (go.ParentID !== undefined && go.ParentID !== 0 && parentObj)
{
return this.findParent(this.objects[go.ParentID]);
return this.findParent(parentObj);
}
else
{
if (go.ParentID !== undefined && go.ParentID !== 0 && !this.objects[go.ParentID])
if (go.ParentID !== undefined && go.ParentID !== 0 && !parentObj)
{
this.requestMissingObject(go.ParentID).catch((e: unknown) =>
{
@@ -988,10 +1169,10 @@ export class ObjectStoreLite implements IObjectStore
{
const results = [];
const found: { [key: string]: GameObject } = {};
for (const k of Object.keys(this.objects))
for (const localID of this.objects.keys())
{
const go = this.objects[parseInt(k, 10)];
if (go.PCode !== PCode.Avatar && (go.IsAttachment === undefined || !go.IsAttachment))
const go = this.objects.get(localID);
if (go && go.PCode !== PCode.Avatar && (go.IsAttachment === undefined || !go.IsAttachment))
{
try
{
@@ -1091,21 +1272,23 @@ export class ObjectStoreLite implements IObjectStore
{
fullID = fullID.toString();
}
if (!this.objectsByUUID[fullID])
const localID = this.objectsByUUID.get(fullID);
const go = this.objects.get(localID ?? 0);
if (localID === undefined || go === undefined)
{
throw new Error('No object found with that UUID');
}
const localID: number = this.objectsByUUID[fullID];
return this.objects[localID];
return go;
}
getObjectByLocalID(localID: number): GameObject
{
if (!this.objects[localID])
const go = this.objects.get(localID);
if (!go)
{
throw new Error('No object found with that UUID');
}
return this.objects[localID];
return go;
}
insertIntoRtree(obj: GameObject): void

View File

@@ -2,12 +2,14 @@ import { TextureEntryFace } from './TextureEntryFace';
import { UUID } from './UUID';
import { Color4 } from './Color4';
import { Utils } from './Utils';
import { LLGLTFMaterialOverride } from './LLGLTFMaterialOverride';
export class TextureEntry
{
static MAX_UINT32 = 4294967295;
defaultTexture: TextureEntryFace | null;
faces: TextureEntryFace[] = [];
public defaultTexture: TextureEntryFace | null;
public faces: TextureEntryFace[] = [];
public gltfMaterialOverrides = new Map<number, LLGLTFMaterialOverride>()
static readFaceBitfield(buf: Buffer, pos: number): {
result: boolean,

View File

@@ -196,125 +196,6 @@ export class LLSDNotationParser
{
return generator.next().value;
}
const data = this.parseValueToken(getToken);
do
{
const t = getToken();
if (t === undefined)
{
return data;
}
if (t.type !== LLSDTokenType.WHITESPACE)
{
throw new Error('Unexpected token at end of document: ' + t.value);
}
}
while (true);
return this.parseValueToken(getToken);
}
/*
private static readObject(cont: LLSDParserContainer): LLSDType
{
let tokenType: LLSDTokenType | null = null;
const stack: LLSDObject[] = [];
while (cont.pos < cont.str.length)
{
const subString = cont.str.slice(cont.pos);
let value: string | null = null;
for (const { regex, type } of this.tokenSpecs)
{
const tokenMatch = subString.match(regex);
if (tokenMatch)
{
value = tokenMatch[0];
tokenType = type;
if (tokenMatch.length > 1)
{
value = tokenMatch[tokenMatch.length - 1];
}
cont.pos += tokenMatch[0].length; // Move past this token
break;
}
}
if (tokenType === null)
{
tokenType = LLSDTokenType.UNKNOWN;
value = cont.str[cont.pos++];
}
if (stack.length > 0)
{
if (stack[stack.length - 1].acceptToken(tokenType, value))
{
// stack object completed
}
}
switch (tokenType)
{
case LLSDTokenType.WHITESPACE:
{
continue;
}
case LLSDTokenType.UNKNOWN:
{
throw new Error('Unexpected token: ' + value);
}
case LLSDTokenType.NULL:
{
return null;
}
case LLSDTokenType.MAP_START:
{
break;
}
case LLSDTokenType.MAP_END:
break;
case LLSDTokenType.COLON:
break;
case LLSDTokenType.COMMA:
break;
case LLSDTokenType.ARRAY_START:
break;
case LLSDTokenType.ARRAY_END:
break;
case LLSDTokenType.BOOLEAN:
break;
case LLSDTokenType.INTEGER:
break;
case LLSDTokenType.REAL:
break;
case LLSDTokenType.UUID:
break;
case LLSDTokenType.STRING_FIXED:
break;
case LLSDTokenType.STRING_DYNAMIC_START:
break;
case LLSDTokenType.URI:
break;
case LLSDTokenType.DATE:
break;
case LLSDTokenType.BINARY_STATIC:
break;
case LLSDTokenType.BINARY_DYNAMIC_START:
break;
}
}
}
public static parse(input: string): LLSDType
{
const cont: LLSDParserContainer = {
str: input,
pos: 0
};
const token = this.readObject(cont);
if (cont.pos < input.length)
{
throw new Error('Only one root object expected');
}
return token;
}
*/
}

View File

@@ -0,0 +1,157 @@
import { GameObject } from './GameObject';
import { TextureEntry } from '../TextureEntry';
import { LLGLTFMaterialOverride } from '../LLGLTFMaterialOverride';
import { TextureEntryFace } from '../TextureEntryFace';
import { UUID } from '../UUID';
import { Color4 } from '../Color4';
import * as assert from 'assert';
describe('GameObject', () =>
{
it('Can serialize and unserialize GLTF override data to xml', async() =>
{
const go = new GameObject();
const te = new TextureEntry();
te.defaultTexture = new TextureEntryFace(null);
te.defaultTexture.textureID = new UUID('3f268e01-7368-7002-e5fb-cd7af0a3931c');
te.defaultTexture.rgba = new Color4(1.0, 1.0, 1.0, 1.0);
te.defaultTexture.glow = 1.0;
te.defaultTexture.rotation = 0.5;
te.defaultTexture.media = 0;
te.defaultTexture.mappingType = 0;
te.defaultTexture.materialID = new UUID('00000000-0000-0000-0000-000000000000');
go.TextureEntry = te;
te.gltfMaterialOverrides = new Map<number, LLGLTFMaterialOverride>();
const override = new LLGLTFMaterialOverride();
te.gltfMaterialOverrides.set(0, override)
override.doubleSided = true;
override.alphaCutoff = 0.5;
override.alphaMode = 2;
override.roughnessFactor = 0.2;
override.metallicFactor = 0.6;
override.textures = [
'c9eca1db-8f2f-4d53-930e-77e6f06d9f37',
'ec3cfa77-bc40-4fc3-9e81-ce9d51e86b77',
'1e079cce-eeca-4e05-9a4f-e58d8398ecf1',
'03573ae5-4c1a-44f7-9cd3-8b49028f9e48'
];
override.emissiveColor = [0.2, 0.5, 0.6];
override.baseColor = [0.1, 0.2, 0.3, 0.4];
override.textureTransforms = [
{
offset: [0.1, 0.2],
scale: [0.5, 0.5],
rotation: 1
},
{
offset: [0.1, 0.2],
scale: [0.5, 0.5],
rotation: 2
},
{
offset: [0.1, 0.2],
scale: [0.5, 0.5],
rotation: 3
},
{
offset: [0.1, 0.2],
scale: [0.5, 0.5],
rotation: 4
}
];
te.gltfMaterialOverrides.set(1, override)
const xml = await go.exportXML();
const obj = await GameObject.fromXML(xml);
assert.notEqual(obj, undefined);
assert.notEqual(obj.TextureEntry, undefined);
assert.notEqual(obj.TextureEntry?.gltfMaterialOverrides, undefined);
const overrides = obj.TextureEntry?.gltfMaterialOverrides;
if (overrides)
{
const entry = overrides.get(0);
if (entry === undefined)
{
assert(false, 'Failed to get material override');
}
else
{
assert.equal(entry.doubleSided, true);
assert.equal(entry.alphaCutoff, 0.5);
assert.equal(entry.alphaMode, 2);
assert.equal(entry.roughnessFactor, 0.2);
assert.equal(entry.metallicFactor, 0.6);
if (Array.isArray(entry.textures))
{
assert.equal(entry.textures.length, 4);
assert.equal(entry.textures[0], 'c9eca1db-8f2f-4d53-930e-77e6f06d9f37');
assert.equal(entry.textures[1], 'ec3cfa77-bc40-4fc3-9e81-ce9d51e86b77');
assert.equal(entry.textures[2], '1e079cce-eeca-4e05-9a4f-e58d8398ecf1');
assert.equal(entry.textures[3], '03573ae5-4c1a-44f7-9cd3-8b49028f9e48');
}
else
{
assert(false, 'Textures is not an array');
}
if (Array.isArray(entry.emissiveColor))
{
assert.equal(entry.emissiveColor.length, 3);
assert.equal(entry.emissiveColor[0], 0.2);
assert.equal(entry.emissiveColor[1], 0.5);
assert.equal(entry.emissiveColor[2], 0.6);
}
else
{
assert(false, 'EmissiveColor is not an array');
}
if (Array.isArray(entry.baseColor))
{
assert.equal(entry.baseColor.length, 4);
assert.equal(entry.baseColor[0], 0.1);
assert.equal(entry.baseColor[1], 0.2);
assert.equal(entry.baseColor[2], 0.3);
assert.equal(entry.baseColor[3], 0.4);
}
else
{
assert(false, 'BaseColor is not an array');
}
if (Array.isArray(entry.textureTransforms))
{
assert.equal(entry.textureTransforms.length, 4);
for (let x = 0; x < 4; x++)
{
const transform = entry.textureTransforms[x];
if (Array.isArray(transform.scale))
{
assert.equal(transform.scale.length, 2);
assert.equal(transform.scale[0], 0.5);
assert.equal(transform.scale[1], 0.5);
}
else
{
assert(false, 'Scale is not an array');
}
if (Array.isArray(transform.offset))
{
assert.equal(transform.offset.length, 2);
assert.equal(transform.offset[0], 0.1);
assert.equal(transform.offset[1], 0.2);
}
else
{
assert(false, 'Offset is not an array');
}
assert.equal(transform.rotation, x + 1);
}
}
else
{
assert(false, 'BaseColor is not an array');
}
}
}
});
});

View File

@@ -55,6 +55,7 @@ import { Subject } from 'rxjs';
import { RezScriptMessage } from '../messages/RezScript';
import { PermissionMask } from '../../enums/PermissionMask';
import { AssetType } from '../../enums/AssetType';
import { LLGLTFMaterialOverride, LLGLTFTextureTransformOverride } from '../LLGLTFMaterialOverride';
import * as uuid from 'uuid';
@@ -380,6 +381,125 @@ export class GameObject implements IGameObjectData
const buf = Buffer.from(prop, 'base64');
go.TextureEntry = TextureEntry.from(buf);
}
if (go.TextureEntry && shape['GLTFMaterialOverrides'] && Array.isArray(shape['GLTFMaterialOverrides']) && shape['GLTFMaterialOverrides'].length > 0)
{
const te = go.TextureEntry;
te.gltfMaterialOverrides = new Map<number, LLGLTFMaterialOverride>();
const children = shape['GLTFMaterialOverrides'][0];
const childObj = children['GLTFMaterialOverride'];
if (childObj)
{
for (const entry of childObj)
{
const override = new LLGLTFMaterialOverride();
let textureEntry = 0;
if ((prop = Utils.getFromXMLJS(entry, 'TextureEntry')) !== undefined)
{
textureEntry = prop;
}
else
{
continue;
}
if ((prop = Utils.getFromXMLJS(entry, 'DoubleSided')) !== undefined)
{
override.doubleSided = prop;
}
if ((prop = Utils.getFromXMLJS(entry, 'AlphaCutoff')) !== undefined)
{
override.alphaCutoff = parseFloat(prop);
}
if ((prop = Utils.getFromXMLJS(entry, 'RoughnessFactor')) !== undefined)
{
override.roughnessFactor = parseFloat(prop);
}
if ((prop = Utils.getFromXMLJS(entry, 'MetallicFactor')) !== undefined)
{
override.metallicFactor = parseFloat(prop);
}
if ((prop = Utils.getFromXMLJS(entry, 'AlphaMode')) !== undefined)
{
override.alphaMode = prop;
}
if (entry['Textures'] && Array.isArray(entry['Textures']) && entry['Textures'].length > 0)
{
override.textures = [];
const childArr = entry['Textures'][0];
for (const tex of childArr['UUID'])
{
override.textures.push(tex);
}
}
if (entry['EmissiveColor'] && Array.isArray(entry['EmissiveColor']) && entry['EmissiveColor'].length > 0)
{
const childArr = entry['EmissiveColor'][0];
const red = parseFloat(childArr['R'][0]);
const green = parseFloat(childArr['G'][0]);
const blue = parseFloat(childArr['B'][0]);
override.emissiveColor = [
red,
green,
blue
];
}
if (entry['BaseColor'] && Array.isArray(entry['BaseColor']) && entry['BaseColor'].length > 0)
{
const childArr = entry['BaseColor'][0];
const red = parseFloat(childArr['R'][0]);
const green = parseFloat(childArr['G'][0]);
const blue = parseFloat(childArr['B'][0]);
const alpha = parseFloat(childArr['A'][0]);
override.baseColor = [
red,
green,
blue,
alpha
];
}
if (entry['TextureTransforms'] && Array.isArray(entry['TextureTransforms']) && entry['TextureTransforms'].length > 0)
{
const childArr = entry['TextureTransforms'][0];
override.textureTransforms = [];
for (const tex of childArr['Transform'])
{
const t: LLGLTFTextureTransformOverride = {
offset: [],
scale: [],
rotation: 0,
};
if ((prop = Utils.getFromXMLJS(tex, 'Rotation')) !== undefined)
{
t.rotation = parseFloat(prop);
}
if (tex['Offsets'] && Array.isArray(tex['Offsets']) && tex['Offsets'].length > 0)
{
const offsetArr = tex['Offsets'][0];
const xOffset = parseFloat(offsetArr['X'][0]);
const yOffset = parseFloat(offsetArr['Y'][0]);
t.offset = [
xOffset,
yOffset
];
}
if (tex['Scale'] && Array.isArray(tex['Scale']) && tex['Scale'].length > 0)
{
const offsetArr = tex['Scale'][0];
const xOffset = parseFloat(offsetArr['X'][0]);
const yOffset = parseFloat(offsetArr['Y'][0]);
t.scale = [
xOffset,
yOffset
];
}
override.textureTransforms.push(t);
}
}
te.gltfMaterialOverrides.set(textureEntry, override);
}
}
}
if ((prop = Utils.getFromXMLJS(shape, 'PathBegin')) !== undefined)
{
go.PathBegin = Utils.unpackBeginCut(prop);
@@ -1484,7 +1604,7 @@ export class GameObject implements IGameObjectData
private async getXML(xml: XMLNode, rootPrim: GameObject, linkNum: number, rootNode?: string): Promise<void>
{
if (this.resolvedAt === undefined || this.resolvedAt === 0 || this.resolvedInventory === false)
if ((this.resolvedAt === undefined || this.resolvedAt === 0 || !this.resolvedInventory) && this.region?.resolver)
{
await this.region.resolver.resolveObjects([this], true, false, false);
}
@@ -1502,7 +1622,10 @@ export class GameObject implements IGameObjectData
sceneObjectPart.ele('LocalId', this.ID);
sceneObjectPart.ele('Name', this.name);
sceneObjectPart.ele('Material', this.Material);
sceneObjectPart.ele('RegionHandle', this.region.regionHandle.toString());
if (this.region)
{
sceneObjectPart.ele('RegionHandle', this.region.regionHandle.toString());
}
Vector3.getXML(sceneObjectPart.ele('GroupPosition'), rootPrim.Position);
if (rootPrim === this)
{
@@ -1535,6 +1658,80 @@ export class GameObject implements IGameObjectData
if (this.TextureEntry)
{
shape.ele('TextureEntry', this.TextureEntry.toBase64());
if (this.TextureEntry.gltfMaterialOverrides)
{
const overrides = shape.ele('GLTFMaterialOverrides');
for (const te of this.TextureEntry.gltfMaterialOverrides.keys())
{
const entry = overrides.ele('GLTFMaterialOverride');
entry.ele('TextureEntry', te);
const override = this.TextureEntry.gltfMaterialOverrides.get(te);
if (override)
{
if (override.doubleSided !== undefined)
{
entry.ele('DoubleSided', override.doubleSided);
}
if (override.alphaCutoff !== undefined)
{
entry.ele('AlphaCutoff', override.alphaCutoff);
}
if (override.roughnessFactor !== undefined)
{
entry.ele('RoughnessFactor', override.roughnessFactor);
}
if (override.metallicFactor !== undefined)
{
entry.ele('MetallicFactor', override.metallicFactor);
}
if (override.alphaMode !== undefined)
{
entry.ele('AlphaMode', override.alphaMode);
}
if (override.textures !== undefined)
{
const texs = entry.ele('Textures');
for (const tex of override.textures)
{
texs.ele('UUID', tex);
}
}
if (override.emissiveColor !== undefined && override.emissiveColor.length === 3)
{
const emissive = entry.ele('EmissiveColor');
emissive.ele('R', override.emissiveColor[0]);
emissive.ele('G', override.emissiveColor[1]);
emissive.ele('B', override.emissiveColor[2]);
}
if (override.baseColor !== undefined && override.baseColor.length === 4)
{
const base = entry.ele('BaseColor');
base.ele('R', override.baseColor[0]);
base.ele('G', override.baseColor[1]);
base.ele('B', override.baseColor[2]);
base.ele('A', override.baseColor[3]);
}
if (override.textureTransforms && override.textureTransforms.length === 4)
{
const transforms = entry.ele('TextureTransforms');
for (const trans of override.textureTransforms)
{
const tfm = transforms.ele('Transform');
tfm.ele('Rotation', trans.rotation);
const offsets = tfm.ele('Offsets');
offsets.ele('X', trans.offset[0]);
offsets.ele('Y', trans.offset[1]);
const scale = tfm.ele('Scale');
scale.ele('X', trans.scale[0]);
scale.ele('Y', trans.scale[1]);
}
}
}
}
}
}
if (this.extraParams)
{

View File

@@ -1,6 +1,6 @@
{
"name": "@caspertech/node-metaverse",
"version": "0.6.5",
"version": "0.6.6",
"description": "A node.js interface for Second Life.",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",