2017-11-30 04:11:59 +00:00
import { Circuit } from './Circuit' ;
import { Packet } from './Packet' ;
import { Message } from '../enums/Message' ;
import { ObjectUpdateMessage } from './messages/ObjectUpdate' ;
import { ObjectUpdateCachedMessage } from './messages/ObjectUpdateCached' ;
import { ObjectUpdateCompressedMessage } from './messages/ObjectUpdateCompressed' ;
import { ImprovedTerseObjectUpdateMessage } from './messages/ImprovedTerseObjectUpdate' ;
import { MultipleObjectUpdateMessage } from './messages/MultipleObjectUpdate' ;
import { RequestMultipleObjectsMessage } from './messages/RequestMultipleObjects' ;
import { Agent } from './Agent' ;
import { UUID } from './UUID' ;
import { Quaternion } from './Quaternion' ;
import { Vector3 } from './Vector3' ;
import { ExtraParamType } from '../enums/ExtraParamType' ;
import { Utils } from './Utils' ;
import { PCode } from '../enums/PCode' ;
2017-12-14 01:21:18 +00:00
import { NameValue } from './NameValue' ;
import { ClientEvents } from './ClientEvents' ;
import { KillObjectMessage } from './messages/KillObject' ;
2017-12-14 02:06:28 +00:00
import { IObjectStore } from './interfaces/IObjectStore' ;
import { GameObjectFull } from './GameObjectFull' ;
import { IGameObject } from './interfaces/IGameObject' ;
2018-10-10 10:36:12 +01:00
import { BotOptionFlags , CompressedFlags } from '..' ;
2018-10-12 17:31:14 +01:00
import { BBox , RBush3D } from 'rbush-3d/dist' ;
import { ITreeBoundingBox } from './interfaces/ITreeBoundingBox' ;
import { GameObjectLite } from './GameObjectLite' ;
2017-11-30 04:11:59 +00:00
2017-12-14 02:06:28 +00:00
export class ObjectStoreFull implements IObjectStore
2017-11-30 04:11:59 +00:00
{
private circuit : Circuit ;
private agent : Agent ;
2017-12-14 02:06:28 +00:00
private objects : { [ key : number ] : GameObjectFull } = { } ;
2017-12-14 01:21:18 +00:00
private objectsByUUID : { [ key : string ] : number } = { } ;
private objectsByParent : { [ key : number ] : number [ ] } = { } ;
2017-11-30 04:11:59 +00:00
private clientEvents : ClientEvents ;
2017-12-14 02:18:50 +00:00
private options : BotOptionFlags ;
2018-10-12 17:31:14 +01:00
rtree : RBush3D ;
2017-11-30 04:11:59 +00:00
2017-12-14 02:18:50 +00:00
constructor ( circuit : Circuit , agent : Agent , clientEvents : ClientEvents , options : BotOptionFlags )
2017-11-30 04:11:59 +00:00
{
2017-12-14 02:37:47 +00:00
agent . localID = 0 ;
2018-10-12 17:31:14 +01:00
this . rtree = new RBush3D ( ) ;
2017-12-14 02:18:50 +00:00
this . options = options ;
2017-11-30 04:11:59 +00:00
this . clientEvents = clientEvents ;
this . circuit = circuit ;
this . agent = agent ;
this . circuit . subscribeToMessages ( [
2017-12-14 01:21:18 +00:00
Message . ObjectUpdate ,
Message . ObjectUpdateCached ,
Message . ObjectUpdateCompressed ,
Message . ImprovedTerseObjectUpdate ,
Message . MultipleObjectUpdate ,
Message . KillObject
2017-11-30 04:11:59 +00:00
] , ( packet : Packet ) = >
{
switch ( packet . message . id )
{
case Message . ObjectUpdate :
const objectUpdate = packet . message as ObjectUpdateMessage ;
2018-10-12 17:31:14 +01:00
for ( const objData of objectUpdate . ObjectData )
2017-11-30 04:11:59 +00:00
{
const localID = objData . ID ;
const parentID = objData . ParentID ;
let addToParentList = true ;
if ( this . objects [ localID ] )
{
if ( this . objects [ localID ] . ParentID !== parentID && this . objectsByParent [ parentID ] )
{
const ind = this . objectsByParent [ parentID ] . indexOf ( localID ) ;
if ( ind !== - 1 )
{
this . objectsByParent [ parentID ] . splice ( ind , 1 ) ;
}
}
else
{
addToParentList = false ;
}
}
else
{
2017-12-14 02:06:28 +00:00
this . objects [ localID ] = new GameObjectFull ( ) ;
2017-11-30 04:11:59 +00:00
}
const obj = this . objects [ localID ] ;
obj . ID = objData . ID ;
obj . State = objData . State ;
obj . FullID = objData . FullID ;
obj . CRC = objData . CRC ;
obj . PCode = objData . PCode ;
obj . Material = objData . Material ;
obj . ClickAction = objData . ClickAction ;
obj . Scale = objData . Scale ;
obj . ObjectData = objData . ObjectData ; // TODO: DECODE
obj . ParentID = objData . ParentID ;
obj . Flags = objData . UpdateFlags ;
obj . PathCurve = objData . PathCurve ;
obj . ProfileCurve = objData . ProfileCurve ;
obj . PathBegin = objData . PathBegin ;
obj . PathEnd = objData . PathEnd ;
obj . PathScaleX = objData . PathScaleX ;
obj . PathScaleY = objData . PathScaleY ;
obj . PathShearX = objData . PathShearX ;
obj . PathShearY = objData . PathShearY ;
obj . PathTwist = objData . PathTwist ;
obj . PathTwistBegin = objData . PathTwistBegin ;
obj . PathRadiusOffset = objData . PathRadiusOffset ;
obj . PathTaperX = objData . PathTaperX ;
obj . PathTaperY = objData . PathTaperY ;
obj . PathRevolutions = objData . PathRevolutions ;
obj . PathSkew = objData . PathSkew ;
obj . ProfileBegin = objData . ProfileBegin ;
obj . ProfileEnd = objData . ProfileEnd ;
obj . ProfileHollow = objData . ProfileHollow ;
obj . TextureEntry = objData . TextureEntry ; // TODO: DECODE
obj . TextureAnim = objData . TextureAnim ;
obj . Data = objData . Data ; // TODO: DECODE
obj . Text = Utils . BufferToStringSimple ( objData . Text ) ;
obj . TextColor = objData . TextColor ; // TODO: DECODE
obj . MediaURL = Utils . BufferToStringSimple ( objData . MediaURL ) ;
obj . PSBlock = objData . PSBlock ;
obj . Sound = objData . Sound ;
obj . OwnerID = objData . OwnerID ;
obj . SoundGain = objData . Gain ;
obj . SoundFlags = objData . Flags ;
obj . SoundRadius = objData . Radius ;
obj . JointType = objData . JointType ;
obj . JointPivot = objData . JointPivot ;
obj . JointAxisOrAnchor = objData . JointAxisOrAnchor ;
if ( this . objects [ localID ] . PCode === PCode . Avatar && this . objects [ localID ] . FullID . toString ( ) === this . agent . agentID . toString ( ) )
{
this . agent . localID = localID ;
2017-12-14 02:24:06 +00:00
if ( this . options & BotOptionFlags . StoreMyAttachmentsOnly )
{
Object . keys ( this . objectsByParent ) . forEach ( ( objParentID : string ) = >
{
const parent = parseInt ( objParentID , 10 ) ;
if ( parent !== this . agent . localID )
{
2017-12-14 02:37:47 +00:00
let foundAvatars = false ;
this . objectsByParent [ parent ] . forEach ( ( objID ) = >
{
if ( this . objects [ objID ] )
{
const o = this . objects [ objID ] ;
if ( o . PCode === PCode . Avatar )
{
foundAvatars = true ;
}
}
} ) ;
if ( this . objects [ parent ] )
{
const o = this . objects [ parent ] ;
if ( o . PCode === PCode . Avatar )
{
foundAvatars = true ;
}
}
if ( ! foundAvatars )
{
this . deleteObject ( parent ) ;
}
2017-12-14 02:24:06 +00:00
}
} ) ;
}
2017-11-30 04:11:59 +00:00
}
this . readExtraParams ( objData . ExtraParams , 0 , this . objects [ localID ] ) ;
this . objects [ localID ] . NameValue = this . parseNameValues ( Utils . BufferToStringSimple ( objData . NameValue ) ) ;
this . objectsByUUID [ objData . FullID . toString ( ) ] = localID ;
if ( ! this . objectsByParent [ parentID ] )
{
this . objectsByParent [ parentID ] = [ ] ;
}
if ( addToParentList )
{
this . objectsByParent [ parentID ] . push ( localID ) ;
}
2017-12-14 02:18:50 +00:00
2018-10-12 17:31:14 +01:00
if ( objData . PCode !== PCode . Avatar && this . options & BotOptionFlags . StoreMyAttachmentsOnly && ( this . agent . localID !== 0 && obj . ParentID !== this . agent . localID ) )
2017-12-14 02:18:50 +00:00
{
2018-10-12 17:31:14 +01:00
// Drop object
this . deleteObject ( localID ) ;
2017-12-14 02:18:50 +00:00
}
2018-10-12 17:31:14 +01:00
else
{
this . insertIntoRtree ( obj ) ;
}
}
2017-11-30 04:11:59 +00:00
break ;
case Message . ObjectUpdateCached :
const objectUpdateCached = packet . message as ObjectUpdateCachedMessage ;
const rmo = new RequestMultipleObjectsMessage ( ) ;
rmo . AgentData = {
AgentID : this.agent.agentID ,
SessionID : this.circuit.sessionID
} ;
rmo . ObjectData = [ ] ;
objectUpdateCached . ObjectData . forEach ( ( obj ) = >
{
rmo . ObjectData . push ( {
2017-12-14 01:21:18 +00:00
CacheMissType : 0 ,
ID : obj.ID
2017-11-30 04:11:59 +00:00
} ) ;
} ) ;
circuit . sendMessage ( rmo , 0 ) ;
break ;
case Message . ObjectUpdateCompressed :
{
const objectUpdateCompressed = packet . message as ObjectUpdateCompressedMessage ;
2018-10-12 17:31:14 +01:00
for ( const obj of objectUpdateCompressed . ObjectData )
2017-11-30 04:11:59 +00:00
{
const flags = obj . UpdateFlags ;
const buf = obj . Data ;
let pos = 0 ;
const fullID = new UUID ( buf , pos ) ;
pos += 16 ;
const localID = buf . readUInt32LE ( pos ) ;
pos += 4 ;
const pcode = buf . readUInt8 ( pos ++ ) ;
let newObj = false ;
if ( ! this . objects [ localID ] )
{
newObj = true ;
2017-12-14 02:06:28 +00:00
this . objects [ localID ] = new GameObjectFull ( ) ;
2017-11-30 04:11:59 +00:00
}
const o = this . objects [ localID ] ;
o . ID = localID ;
this . objectsByUUID [ fullID . toString ( ) ] = localID ;
o . FullID = fullID ;
o . Flags = flags ;
o . PCode = pcode ;
o . State = buf . readUInt8 ( pos ++ ) ;
o . CRC = buf . readUInt32LE ( pos ) ;
pos = pos + 4 ;
o . Material = buf . readUInt8 ( pos ++ ) ;
o . ClickAction = buf . readUInt8 ( pos ++ ) ;
o . Scale = new Vector3 ( buf , pos , false ) ;
pos = pos + 12 ;
o . Position = new Vector3 ( buf , pos , false ) ;
pos = pos + 12 ;
o . Rotation = new Quaternion ( buf , pos ) ;
pos = pos + 12 ;
const compressedflags : CompressedFlags = buf . readUInt32LE ( pos ) ;
pos = pos + 4 ;
o . OwnerID = new UUID ( buf , pos ) ;
pos += 16 ;
if ( compressedflags & CompressedFlags . HasAngularVelocity )
{
o . AngularVelocity = new Vector3 ( buf , pos , false ) ;
pos = pos + 12 ;
}
if ( compressedflags & CompressedFlags . HasParent )
{
const newParentID = buf . readUInt32LE ( pos ) ;
pos += 4 ;
let add = true ;
if ( ! newObj )
{
if ( newParentID !== o . ParentID )
{
const index = this . objectsByParent [ o . ParentID ] . indexOf ( localID ) ;
if ( index !== - 1 )
{
this . objectsByParent [ o . ParentID ] . splice ( index , 1 ) ;
}
}
else
{
add = false ;
}
}
if ( add )
{
if ( ! this . objectsByParent [ newParentID ] )
{
this . objectsByParent [ newParentID ] = [ ] ;
}
this . objectsByParent [ newParentID ] . push ( localID ) ;
}
o . ParentID = newParentID ;
}
2018-10-12 17:31:14 +01:00
if ( pcode !== PCode . Avatar && newObj && this . options & BotOptionFlags . StoreMyAttachmentsOnly && ( this . agent . localID !== 0 && o . ParentID !== this . agent . localID ) )
2017-11-30 04:11:59 +00:00
{
2018-10-12 17:31:14 +01:00
// Drop object
this . deleteObject ( localID ) ;
return ;
2017-11-30 04:11:59 +00:00
}
else
{
2018-10-12 17:31:14 +01:00
if ( compressedflags & CompressedFlags . Tree )
{
o . TreeSpecies = buf . readUInt8 ( pos ++ ) ;
}
else if ( compressedflags & CompressedFlags . ScratchPad )
{
o . TreeSpecies = 0 ;
const scratchPadSize = buf . readUInt8 ( pos ++ ) ;
// Ignore this data
pos = pos + scratchPadSize ;
}
if ( compressedflags & CompressedFlags . HasText )
{
// Read null terminated string
const result = Utils . BufferToString ( buf , pos ) ;
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
pos += result . readLength ;
o . Text = result . result ;
o . TextColor = buf . slice ( pos , pos + 4 ) ;
pos = pos + 4 ;
}
else
{
o . Text = '' ;
}
if ( compressedflags & CompressedFlags . MediaURL )
{
const result = Utils . BufferToString ( buf , pos ) ;
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
pos += result . readLength ;
o . MediaURL = result . result ;
}
if ( compressedflags & CompressedFlags . HasParticles )
{
// TODO: Particle system block
pos += 86 ;
}
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
// Extra params
pos = this . readExtraParams ( buf , pos , o ) ;
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
if ( compressedflags & CompressedFlags . HasSound )
{
o . Sound = new UUID ( buf , pos ) ;
pos = pos + 16 ;
o . SoundGain = buf . readFloatLE ( pos ) ;
pos += 4 ;
o . SoundFlags = buf . readUInt8 ( pos ++ ) ;
o . SoundRadius = buf . readFloatLE ( pos ) ;
pos = pos + 4 ;
}
if ( compressedflags & CompressedFlags . HasNameValues )
{
const result = Utils . BufferToString ( buf , pos ) ;
o . NameValue = this . parseNameValues ( result . result ) ;
pos += result . readLength ;
}
o . PathCurve = buf . readUInt8 ( pos ++ ) ;
o . PathBegin = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
o . PathEnd = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
o . PathScaleX = buf . readUInt8 ( pos ++ ) ;
o . PathScaleY = buf . readUInt8 ( pos ++ ) ;
o . PathShearX = buf . readUInt8 ( pos ++ ) ;
o . PathShearY = buf . readUInt8 ( pos ++ ) ;
o . PathTwist = buf . readUInt8 ( pos ++ ) ;
o . PathTwistBegin = buf . readUInt8 ( pos ++ ) ;
o . PathRadiusOffset = buf . readUInt8 ( pos ++ ) ;
o . PathTaperX = buf . readUInt8 ( pos ++ ) ;
o . PathTaperY = buf . readUInt8 ( pos ++ ) ;
o . PathRevolutions = buf . readUInt8 ( pos ++ ) ;
o . PathSkew = buf . readUInt8 ( pos ++ ) ;
o . ProfileCurve = buf . readUInt8 ( pos ++ ) ;
o . ProfileBegin = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
o . ProfileEnd = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
o . ProfileHollow = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
const textureEntryLength = buf . readUInt32LE ( pos ) ;
2017-11-30 04:11:59 +00:00
pos = pos + 4 ;
2018-10-12 17:31:14 +01:00
// TODO: Properly parse textureentry;
pos = pos + textureEntryLength ;
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
if ( compressedflags & CompressedFlags . TextureAnimation )
{
// TODO: Properly parse textureAnim
pos = pos + 4 ;
}
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
o . IsAttachment = ( compressedflags & CompressedFlags . HasNameValues ) !== 0 && o . ParentID !== 0 ;
2017-11-30 04:11:59 +00:00
2018-10-12 17:31:14 +01:00
this . insertIntoRtree ( o ) ;
}
}
2017-11-30 04:11:59 +00:00
break ;
}
case Message . ImprovedTerseObjectUpdate :
const objectUpdateTerse = packet . message as ImprovedTerseObjectUpdateMessage ;
2018-10-12 17:31:14 +01:00
// TODO: ImprovedTerseObjectUpdate
2017-11-30 04:11:59 +00:00
break ;
case Message . MultipleObjectUpdate :
const multipleObjectUpdate = packet . message as MultipleObjectUpdateMessage ;
// TODO: multipleObjectUpdate
console . error ( 'TODO: MultipleObjectUpdate' ) ;
break ;
2017-12-14 01:21:18 +00:00
case Message . KillObject :
const killObj = packet . message as KillObjectMessage ;
2018-10-12 17:31:14 +01:00
for ( const obj of killObj . ObjectData )
2017-12-14 01:21:18 +00:00
{
const objectID = obj . ID ;
this . deleteObject ( objectID ) ;
2018-10-12 17:31:14 +01:00
}
2017-12-14 01:21:18 +00:00
break ;
2017-11-30 04:11:59 +00:00
}
} ) ;
}
2017-12-14 01:21:18 +00:00
2018-10-12 17:31:14 +01:00
insertIntoRtree ( obj : GameObjectFull )
{
if ( obj . rtreeEntry !== undefined )
{
this . rtree . remove ( obj . rtreeEntry ) ;
}
const normalizedScale = obj . Scale . multiplyByQuat ( obj . Rotation ) ;
const bounds : ITreeBoundingBox = {
minX : obj.Position.x - ( normalizedScale . x / 2 ) ,
maxX : obj.Position.x + ( normalizedScale . x / 2 ) ,
minY : obj.Position.y - ( normalizedScale . y / 2 ) ,
maxY : obj.Position.y + ( normalizedScale . y / 2 ) ,
minZ : obj.Position.z - ( normalizedScale . z / 2 ) ,
maxZ : obj.Position.z + ( normalizedScale . z / 2 ) ,
gameObject : obj
} ;
obj . rtreeEntry = bounds ;
this . rtree . insert ( bounds ) ;
}
2017-12-14 01:21:18 +00:00
deleteObject ( objectID : number )
{
if ( this . objects [ objectID ] )
{
// First, kill all children
if ( this . objectsByParent [ objectID ] )
{
this . objectsByParent [ objectID ] . forEach ( ( childObjID ) = >
{
this . deleteObject ( childObjID ) ;
} ) ;
}
delete this . objectsByParent [ objectID ] ;
// Now delete this object
const objct = this . objects [ objectID ] ;
const uuid = objct . FullID . toString ( ) ;
if ( this . objectsByUUID [ uuid ] )
{
delete this . objectsByUUID [ uuid ] ;
}
const parentID = objct . ParentID ;
if ( this . objectsByParent [ parentID ] )
{
const ind = this . objectsByParent [ parentID ] . indexOf ( objectID ) ;
if ( ind !== - 1 )
{
this . objectsByParent [ parentID ] . splice ( ind , 1 ) ;
}
}
2018-10-12 17:31:14 +01:00
if ( this . objects [ objectID ] . rtreeEntry !== undefined )
{
this . rtree . remove ( this . objects [ objectID ] . rtreeEntry ) ;
}
2017-12-14 01:21:18 +00:00
delete this . objects [ objectID ] ;
}
}
2017-12-14 02:06:28 +00:00
readExtraParams ( buf : Buffer , pos : number , o : GameObjectFull ) : number
2017-11-30 04:11:59 +00:00
{
if ( pos >= buf . length )
{
return 0 ;
}
const extraParamCount = buf . readUInt8 ( pos ++ ) ;
for ( let k = 0 ; k < extraParamCount ; k ++ )
{
const type : ExtraParamType = buf . readUInt16LE ( pos ) ;
pos = pos + 2 ;
const paramLength = buf . readUInt32LE ( pos ) ;
pos = pos + 4 ;
// TODO: Read extra param data
pos += paramLength ;
}
return pos ;
}
2017-12-14 01:21:18 +00:00
2017-12-14 02:06:28 +00:00
getObjectsByParent ( parentID : number ) : IGameObject [ ]
2017-11-30 04:11:59 +00:00
{
const list = this . objectsByParent [ parentID ] ;
if ( list === undefined )
{
return [ ] ;
}
2017-12-14 02:06:28 +00:00
const result : IGameObject [ ] = [ ] ;
2017-11-30 04:11:59 +00:00
list . forEach ( ( localID ) = >
{
result . push ( this . objects [ localID ] ) ;
} ) ;
return result ;
}
2017-12-14 01:21:18 +00:00
parseNameValues ( str : string ) : { [ key : string ] : NameValue }
2017-11-30 04:11:59 +00:00
{
2017-12-14 01:21:18 +00:00
const nv : { [ key : string ] : NameValue } = { } ;
2017-11-30 04:11:59 +00:00
const lines = str . split ( '\n' ) ;
lines . forEach ( ( line ) = >
{
if ( line . length > 0 )
{
let kv = line . split ( /[\t ]/ ) ;
if ( kv . length > 5 )
{
2017-12-14 01:21:18 +00:00
for ( let x = 5 ; x < kv . length ; x ++ )
2017-11-30 04:11:59 +00:00
{
kv [ 4 ] += ' ' + kv [ x ] ;
}
kv = kv . slice ( 0 , 5 ) ;
}
if ( kv . length === 5 )
{
const namevalue = new NameValue ( ) ;
namevalue . type = kv [ 1 ] ;
namevalue . class = kv [ 2 ] ;
namevalue . sendTo = kv [ 3 ] ;
namevalue . value = kv [ 4 ] ;
nv [ kv [ 0 ] ] = namevalue ;
}
else
{
2017-12-14 02:06:28 +00:00
console . log ( 'namevalue unexpected length: ' + kv . length ) ;
2017-11-30 04:11:59 +00:00
console . log ( kv ) ;
}
}
} ) ;
return nv ;
}
2017-12-14 01:21:18 +00:00
2017-11-30 04:11:59 +00:00
shutdown ( )
{
this . objects = { } ;
2018-10-12 17:31:14 +01:00
this . rtree . clear ( ) ;
2017-11-30 04:11:59 +00:00
this . objectsByUUID = { } ;
this . objectsByParent = { } ;
}
2018-10-12 17:31:14 +01:00
private findParent ( go : GameObjectFull ) : GameObjectFull
{
if ( go . ParentID === 0 )
{
return go ;
}
else
{
return this . findParent ( this . objects [ go . ParentID ] ) ;
}
}
getObjectsInArea ( minX : number , maxX : number , minY : number , maxY : number , minZ : number , maxZ : number ) : GameObjectFull [ ]
{
const result = this . rtree . search ( {
minX : minX ,
maxX : maxX ,
minY : minY ,
maxY : maxY ,
minZ : minZ ,
maxZ : maxZ
} ) ;
const found : { [ key : string ] : GameObjectFull } = { } ;
const objs : GameObjectFull [ ] = [ ] ;
for ( const obj of result )
{
const o = obj as ITreeBoundingBox ;
const go = o . gameObject as GameObjectFull ;
try
{
const parent = this . findParent ( go ) ;
const uuid = parent . FullID . toString ( ) ;
if ( parent !== go )
{
console . log ( 'Resolved object ' + go . FullID . toString ( ) + ' to parent ' + parent . FullID . toString ( ) + ' which ' + ( ( found [ uuid ] === undefined ) ? 'does not exist' : 'already exists' ) ) ;
}
if ( found [ uuid ] === undefined )
{
found [ uuid ] = parent ;
objs . push ( parent ) ;
}
}
catch ( error )
{
console . log ( 'Failed to find parent for ' + go . FullID . toString ( ) ) ;
console . error ( error ) ;
// Unable to find parent, full object probably not fully loaded yet
}
}
return objs ;
}
2017-11-30 04:11:59 +00:00
}