//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "T3D/prefab.h" #include "math/mathIO.h" #include "core/stream/bitStream.h" #include "scene/sceneRenderState.h" #include "gfx/gfxTransformSaver.h" #include "renderInstance/renderPassManager.h" #include "console/consoleTypes.h" #include "core/volume.h" #include "console/engineAPI.h" #include "T3D/physics/physicsShape.h" #include "core/util/path.h" #include "T3D/Scene.h" // We use this locally ( within this file ) to prevent infinite recursion // while loading prefab files that contain other prefabs. static Vector sPrefabFileStack; Map Prefab::smChildToPrefabMap; IMPLEMENT_CO_NETOBJECT_V1(Prefab); ConsoleDocClass( Prefab, "@brief A collection of arbitrary objects which can be allocated and manipulated as a group.\n\n" "%Prefab always points to a (.prefab) file which defines its objects. In " "fact more than one %Prefab can reference this file and both will update " "if the file is modified.\n\n" "%Prefab is a very simple object and only exists on the server. When it is " "created it allocates children objects by reading the (.prefab) file like " "a list of instructions. It then sets their transform relative to the %Prefab " "and Torque networking handles the rest by ghosting the new objects to clients. " "%Prefab itself is not ghosted.\n\n" "@ingroup enviroMisc" ); IMPLEMENT_CALLBACK( Prefab, onLoad, void, ( SimGroup *children ), ( children ), "Called when the prefab file is loaded and children objects are created.\n" "@param children SimGroup containing all children objects.\n" ); Prefab::Prefab() { // Not ghosted unless we're editing mNetFlags.clear(Ghostable); mTypeMask |= StaticObjectType; mFilename = StringTable->EmptyString(); } Prefab::~Prefab() { } void Prefab::initPersistFields() { docsURL; addGroup( "Prefab" ); addProtectedField( "filename", TypePrefabFilename, Offset( mFilename, Prefab ), &protectedSetFile, &defaultProtectedGetFn, "(.prefab) File describing objects within this prefab." ); endGroup( "Prefab" ); Parent::initPersistFields(); } extern bool gEditingMission; bool Prefab::onAdd() { if ( !Parent::onAdd() ) return false; mObjBox.set( Point3F( -0.5f, -0.5f, -0.5f ), Point3F( 0.5f, 0.5f, 0.5f ) ); resetWorldBox(); // Not added to the scene unless we are editing. if ( gEditingMission ) onEditorEnable(); // Only the server-side prefab needs to create/update child objects. // We rely on regular Torque ghosting of the individual child objects // to take care of the rest. if ( isServerObject() ) { _loadFile( true ); _updateChildren(); } return true; } void Prefab::onRemove() { if ( isServerObject() ) _closeFile( true ); removeFromScene(); Parent::onRemove(); } void Prefab::onEditorEnable() { if ( isClientObject() ) return; // Just in case we are already in the scene, lets not cause an assert. if ( mContainer != NULL ) return; // Enable ghosting so we can see this on the client. mNetFlags.set(Ghostable); setScopeAlways(); addToScene(); Parent::onEditorEnable(); } void Prefab::onEditorDisable() { if ( isClientObject() ) return; // Just in case we are not in the scene, lets not cause an assert. if ( mContainer == NULL ) return; // Do not need this on the client if we are not editing. removeFromScene(); mNetFlags.clear(Ghostable); clearScopeAlways(); Parent::onEditorDisable(); } void Prefab::inspectPostApply() { Parent::inspectPostApply(); } void Prefab::setTransform(const MatrixF & mat) { Parent::setTransform( mat ); if ( isServerObject() ) { setMaskBits( TransformMask ); _updateChildren(); } } void Prefab::setScale(const VectorF & scale) { Parent::setScale( scale ); if ( isServerObject() ) { setMaskBits( TransformMask ); _updateChildren(); } } U32 Prefab::packUpdate( NetConnection *conn, U32 mask, BitStream *stream ) { U32 retMask = Parent::packUpdate( conn, mask, stream ); mathWrite(*stream,mObjBox); if ( stream->writeFlag( mask & FileMask ) ) { stream->writeString( mFilename ); } if ( stream->writeFlag( mask & TransformMask ) ) { mathWrite(*stream, getTransform()); mathWrite(*stream, getScale()); } return retMask; } void Prefab::unpackUpdate(NetConnection *conn, BitStream *stream) { Parent::unpackUpdate(conn, stream); mathRead(*stream, &mObjBox); resetWorldBox(); // FileMask if ( stream->readFlag() ) { mFilename = stream->readSTString(); } // TransformMask if ( stream->readFlag() ) { mathRead(*stream, &mObjToWorld); mathRead(*stream, &mObjScale); setTransform( mObjToWorld ); } } bool Prefab::protectedSetFile( void *object, const char *index, const char *data ) { Prefab *prefab = static_cast(object); prefab->setFile( StringTable->insert(Platform::makeRelativePathName(data, Platform::getMainDotCsDir()))); return false; } void Prefab::setFile( StringTableEntry file ) { AssertFatal( isServerObject(), "Prefab-bad" ); if ( !isProperlyAdded() ) { mFilename = file; return; } // Client-side Prefab(s) do not create/update/reference children, everything // is handled on the server-side. In normal usage this will never actually // be called for the client-side prefab but maybe the user did so accidentally. if ( isClientObject() ) { Con::errorf( "Prefab::setFile( %s ) - Should not be called on a client-side Prefab.", file ); return; } _closeFile( true ); mFilename = file; if ( isProperlyAdded() ) _loadFile( true ); } SimGroup* Prefab::explode() { Scene* scene = Scene::getRootScene(); if ( !scene) { Con::errorf( "Prefab::explode, Scene was not found." ); return NULL; } if ( !mChildGroup ) return NULL; SimGroup *group = mChildGroup; Vector foundObjects; group->findObjectByType( foundObjects ); if ( foundObjects.empty() ) return NULL; for ( S32 i = 0; i < foundObjects.size(); i++ ) { SceneObject *child = foundObjects[i]; _updateChildTransform( child ); smChildToPrefabMap.erase( child->getId() ); } scene->addObject(group); mChildGroup = NULL; mChildMap.clear(); return group; } void Prefab::_closeFile( bool removeFileNotify ) { AssertFatal( isServerObject(), "Prefab-bad" ); mChildMap.clear(); if ( mChildGroup ) { // Get a flat vector of all our children. Vector foundObjects; mChildGroup->findObjectByType( foundObjects ); // Remove them all from the ChildToPrefabMap. for ( S32 i = 0; i < foundObjects.size(); i++ ) smChildToPrefabMap.erase( foundObjects[i]->getId() ); mChildGroup->deleteObject(); mChildGroup = NULL; } if ( removeFileNotify ) Torque::FS::RemoveChangeNotification( mFilename, this, &Prefab::_onFileChanged ); // Back to a default bounding box size. mObjBox.set( Point3F( -0.5f, -0.5f, -0.5f ), Point3F( 0.5f, 0.5f, 0.5f ) ); resetWorldBox(); } void Prefab::_loadFile( bool addFileNotify ) { AssertFatal( isServerObject(), "Prefab-bad" ); if ( mFilename == StringTable->EmptyString()) return; if ( !Torque::FS::IsScriptFile( mFilename ) ) { Con::errorf( "Prefab::_loadFile() - file %s was not found.", mFilename ); return; } if ( sPrefabFileStack.contains(mFilename) ) { Con::errorf( "Prefab::_loadFile - failed loading prefab file (%s). \n" "File was referenced recursively by both a Parent and Child prefab.", mFilename ); return; } sPrefabFileStack.push_back(mFilename); String command = String::ToString( "exec( \"%s\" );", mFilename ); Con::evaluate( command ); SimGroup *group; if ( !Sim::findObject( Con::getVariable( "$ThisPrefab" ), group ) ) { Con::errorf( "Prefab::_loadFile() - file %s did not create $ThisPrefab.", mFilename ); return; } if ( addFileNotify ) Torque::FS::AddChangeNotification( mFilename, this, &Prefab::_onFileChanged ); mChildGroup = group; Vector foundObjects; mChildGroup->findObjectByType( foundObjects ); if ( !foundObjects.empty() ) { mWorldBox = Box3F::Invalid; for ( S32 i = 0; i < foundObjects.size(); i++ ) { SceneObject *child = foundObjects[i]; mChildMap.insert( child->getId(), Transform( child->getTransform(), child->getScale() ) ); smChildToPrefabMap.insert( child->getId(), getId() ); _updateChildTransform( child ); mWorldBox.intersect( child->getWorldBox() ); } resetObjectBox(); } sPrefabFileStack.pop_back(); onLoad_callback( mChildGroup ); } void Prefab::_updateChildTransform( SceneObject* child ) { ChildToMatMap::Iterator itr = mChildMap.find(child->getId()); AssertFatal( itr != mChildMap.end(), "Prefab, mChildMap out of synch with mChildGroup." ); MatrixF mat( itr->value.mat ); Point3F pos = mat.getPosition(); pos.convolve( mObjScale ); mat.setPosition( pos ); mat.mulL( mObjToWorld ); child->setTransform( mat ); child->setScale( itr->value.scale * mObjScale ); // Hack for PhysicsShape... need to store the "editor" position to return to // when a physics reset event occurs. Normally this would be where it is // during onAdd, but in this case it is not because the prefab stores its // child objects in object space... PhysicsShape *childPS = dynamic_cast( child ); if ( childPS ) childPS->storeRestorePos(); } void Prefab::_updateChildren() { if ( !mChildGroup ) return; Vector foundObjects; mChildGroup->findObjectByType( foundObjects ); for ( S32 i = 0; i < foundObjects.size(); i++ ) { SceneObject *child = foundObjects[i]; _updateChildTransform( child ); if ( child->getClientObject() ) { ((SceneObject*)child->getClientObject())->setTransform( child->getTransform() ); ((SceneObject*)child->getClientObject())->setScale( child->getScale() ); } } } void Prefab::_onFileChanged( const Torque::Path &path ) { AssertFatal( path == mFilename, "Prefab::_onFileChanged - path does not match filename." ); _closeFile(false); _loadFile(false); setMaskBits(U32_MAX); } Prefab* Prefab::getPrefabByChild( SimObject *child ) { ChildToPrefabMap::Iterator itr = smChildToPrefabMap.find( child->getId() ); if ( itr == smChildToPrefabMap.end() ) return NULL; Prefab *prefab; if ( !Sim::findObject( itr->value, prefab ) ) { Con::errorf( "Prefab::getPrefabByChild - child object mapped to a prefab that no longer exists." ); return NULL; } return prefab; } bool Prefab::isValidChild( SimObject *simobj, bool logWarnings ) { if ( simobj->getName() && simobj == Scene::getRootScene() ) { if ( logWarnings ) Con::warnf( "root Scene is not valid within a Prefab." ); return false; } if ( simobj->getClassRep()->isClass( AbstractClassRep::findClassRep("LevelInfo") ) ) { if ( logWarnings ) Con::warnf( "LevelInfo objects are not valid within a Prefab" ); return false; } if ( simobj->getClassRep()->isClass( AbstractClassRep::findClassRep("TimeOfDay") ) ) { if ( logWarnings ) Con::warnf( "TimeOfDay objects are not valid within a Prefab" ); return false; } SceneObject *sceneobj = dynamic_cast(simobj); if ( !sceneobj ) return false; if ( sceneobj->isGlobalBounds() ) { if ( logWarnings ) Con::warnf( "SceneObject's with global bounds are not valid within a Prefab." ); return false; } if ( sceneobj->getClassRep()->isClass( AbstractClassRep::findClassRep("TerrainBlock") ) ) { if ( logWarnings ) Con::warnf( "TerrainBlock objects are not valid within a Prefab" ); return false; } if ( sceneobj->getClassRep()->isClass( AbstractClassRep::findClassRep("Player") ) ) { if ( logWarnings ) Con::warnf( "Player objects are not valid within a Prefab" ); return false; } if ( sceneobj->getClassRep()->isClass( AbstractClassRep::findClassRep("DecalRoad") ) ) { if ( logWarnings ) Con::warnf( "DecalRoad objects are not valid within a Prefab" ); return false; } return true; } bool Prefab::buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F &box, const SphereF& sphere) { Vector foundObjects; if (mChildGroup.isNull() || mChildGroup->empty()) { Con::warnf("Bad Prefab Config! %s has no valid entries!", getName()); return false; } mChildGroup->findObjectByType(foundObjects); for (S32 i = 0; i < foundObjects.size(); i++) { foundObjects[i]->buildPolyList(context, polyList, box, sphere); } return true; } bool Prefab::buildExportPolyList(ColladaUtils::ExportData* exportData, const Box3F &box, const SphereF &sphere) { Vector foundObjects; mChildGroup->findObjectByType(foundObjects); for (S32 i = 0; i < foundObjects.size(); i++) { foundObjects[i]->buildExportPolyList(exportData, box, sphere); } return true; } void Prefab::getUtilizedAssets(Vector* usedAssetsList) { if (!mChildGroup) return; Vector foundObjects; mChildGroup->findObjectByType(foundObjects); for (S32 i = 0; i < foundObjects.size(); i++) { SceneObject* child = foundObjects[i]; child->getUtilizedAssets(usedAssetsList); } } ExplodePrefabUndoAction::ExplodePrefabUndoAction( Prefab *prefab ) : UndoAction( "Explode Prefab" ) { mPrefabId = prefab->getId(); mGroup = NULL; // Do the action. redo(); } void ExplodePrefabUndoAction::undo() { if ( !mGroup ) { Con::errorf( "ExplodePrefabUndoAction::undo - NULL Group" ); return; } mGroup->deleteObject(); mGroup = NULL; } void ExplodePrefabUndoAction::redo() { Prefab *prefab; if ( !Sim::findObject( mPrefabId, prefab ) ) { Con::errorf( "ExplodePrefabUndoAction::redo - Prefab (%i) not found.", mPrefabId ); return; } mGroup = prefab->explode(); String name; if ( prefab->getName() && prefab->getName()[0] != '\0' ) name = prefab->getName(); else name = "prefab"; name += "_exploded"; name = Sim::getUniqueName( name ); mGroup->assignName( name ); } DefineEngineMethod(Prefab, getChildGroup, S32, (),, "") { return object->getChildGroup(); }