From cb778655379be8a6534f6053d408b62abf53a30d Mon Sep 17 00:00:00 2001 From: Casper Warden <216465704+casperwardensl@users.noreply.github.com> Date: Fri, 10 Nov 2023 23:57:26 +0000 Subject: [PATCH] Implement GLTF overrides --- lib/classes/LLGLTFMaterial.spec.ts | 74 ++++- lib/classes/LLGLTFMaterialData.ts | 220 +++++++++++-- lib/classes/LLGLTFMaterialOverride.ts | 19 ++ lib/classes/ObjectStoreFull.ts | 153 +++++---- lib/classes/ObjectStoreLite.ts | 435 ++++++++++++++++++------- lib/classes/TextureEntry.ts | 6 +- lib/classes/llsd/LLSDNotationParser.ts | 121 +------ lib/classes/public/GameObject.spec.ts | 157 +++++++++ lib/classes/public/GameObject.ts | 201 +++++++++++- package.json | 2 +- 10 files changed, 1034 insertions(+), 354 deletions(-) create mode 100644 lib/classes/LLGLTFMaterialOverride.ts create mode 100644 lib/classes/public/GameObject.spec.ts diff --git a/lib/classes/LLGLTFMaterial.spec.ts b/lib/classes/LLGLTFMaterial.spec.ts index 5c1f7c8..bf7ad3d 100644 --- a/lib/classes/LLGLTFMaterial.spec.ts +++ b/lib/classes/LLGLTFMaterial.spec.ts @@ -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); }); }); }); diff --git a/lib/classes/LLGLTFMaterialData.ts b/lib/classes/LLGLTFMaterialData.ts index 20da51c..843bb2c 100644 --- a/lib/classes/LLGLTFMaterialData.ts +++ b/lib/classes/LLGLTFMaterialData.ts @@ -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 + extras?: Record } + +export interface LLGLTFTexture +{ + index: number, + extensions?: Record, + extras?: Record, + 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[]; + } & 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; diff --git a/lib/classes/LLGLTFMaterialOverride.ts b/lib/classes/LLGLTFMaterialOverride.ts new file mode 100644 index 0000000..a010714 --- /dev/null +++ b/lib/classes/LLGLTFMaterialOverride.ts @@ -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[]; +} diff --git a/lib/classes/ObjectStoreFull.ts b/lib/classes/ObjectStoreFull.ts index 796941a..e2fb3fd 100644 --- a/lib/classes/ObjectStoreFull.ts +++ b/lib/classes/ObjectStoreFull.ts @@ -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 diff --git a/lib/classes/ObjectStoreLite.ts b/lib/classes/ObjectStoreLite.ts index e09263a..292f8b2 100644 --- a/lib/classes/ObjectStoreLite.ts +++ b/lib/classes/ObjectStoreLite.ts @@ -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(); + protected objectsByUUID = new Map(); + protected objectsByParent = new Map(); protected clientEvents: ClientEvents; protected options: BotOptionFlags; - protected requestedObjects: { [key: number]: boolean } = {}; + protected fullStore = false; + protected requestedObjects = new Set(); protected deadObjects: number[] = []; protected persist = false; - protected pendingObjectProperties: { [key: string]: any } = {}; + protected cachedMaterialOverrides = new Map>(); + protected pendingObjectProperties = new Map; private physicsSubscription: Subscription; - private selectedPrimsWithoutUpdate: { [key: number]: boolean } = {}; + private selectedPrimsWithoutUpdate = new Map(); 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(); + + 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 { - 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 diff --git a/lib/classes/TextureEntry.ts b/lib/classes/TextureEntry.ts index 1875671..2a65dcf 100644 --- a/lib/classes/TextureEntry.ts +++ b/lib/classes/TextureEntry.ts @@ -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() static readFaceBitfield(buf: Buffer, pos: number): { result: boolean, diff --git a/lib/classes/llsd/LLSDNotationParser.ts b/lib/classes/llsd/LLSDNotationParser.ts index 3d9225a..0104780 100644 --- a/lib/classes/llsd/LLSDNotationParser.ts +++ b/lib/classes/llsd/LLSDNotationParser.ts @@ -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; - } - */ } diff --git a/lib/classes/public/GameObject.spec.ts b/lib/classes/public/GameObject.spec.ts new file mode 100644 index 0000000..d1fede3 --- /dev/null +++ b/lib/classes/public/GameObject.spec.ts @@ -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(); + 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'); + } + } + } + }); +}); + diff --git a/lib/classes/public/GameObject.ts b/lib/classes/public/GameObject.ts index af17f49..874af17 100644 --- a/lib/classes/public/GameObject.ts +++ b/lib/classes/public/GameObject.ts @@ -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(); + 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 { - 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) { diff --git a/package.json b/package.json index a925993..1b97720 100644 --- a/package.json +++ b/package.json @@ -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",