#include "SubScene.h" #include "gameMode.h" #include "console/persistenceManager.h" #include "console/script.h" #include "environment/meshRoad.h" #include "environment/river.h" #include "scene/sceneRenderState.h" #include "renderInstance/renderPassManager.h" #include "gfx/gfxDrawUtil.h" #include "gfx/gfxTransformSaver.h" #include "gui/editor/inspector/group.h" #include "gui/worldEditor/editor.h" #include "math/mathIO.h" #include "T3D/gameBase/gameBase.h" bool SubScene::smTransformChildren = false; IMPLEMENT_CO_NETOBJECT_V1(SubScene); S32 SubScene::mUnloadTimeoutMs = 5000; IMPLEMENT_CALLBACK(SubScene, onLoaded, void, (), (), "@brief Called when a subScene has been loaded and has game mode implications.\n\n"); IMPLEMENT_CALLBACK(SubScene, onUnloaded, void, (), (), "@brief Called when a subScene has been unloaded and has game mode implications.\n\n"); SubScene::SubScene() : mSubSceneAssetId(StringTable->EmptyString()), mGameModesNames(StringTable->EmptyString()), mScopeDistance(-1), mLoaded(false), mFreezeLoading(false), mTickPeriodMS(1000), mCurrTick(0), mGlobalLayer(false), mSaving(false), mUseSeparateLoadBounds(false), mLoadBounds(Point3F::One) { mNetFlags.set(Ghostable | ScopeAlways); mTypeMask |= StaticObjectType; } SubScene::~SubScene() { } bool SubScene::onAdd() { if (!Parent::onAdd()) return false; setProcessTick(true); return true; } void SubScene::onRemove() { if (isClientObject()) removeFromScene(); unload(); Parent::onRemove(); } void SubScene::initPersistFields() { addGroup("SubScene"); addField("isGlobalLayer", TypeBool, Offset(mGlobalLayer, SubScene), ""); INITPERSISTFIELD_SUBSCENEASSET(SubScene, SubScene, "The subscene asset to load."); addField("tickPeriodMS", TypeS32, Offset(mTickPeriodMS, SubScene), "evaluation rate (ms)"); addField("gameModes", TypeGameModeList, Offset(mGameModesNames, SubScene), "The game modes that this subscene is associated with."); addField("UseSeparateLoadBounds", TypeBool, Offset(mUseSeparateLoadBounds, SubScene), "If true, this subscene will utilize a separate bounds for triggering loading/unloading than it's object bounds"); addField("LoadBounds", TypePoint3F, Offset(mLoadBounds, SubScene), "If UseSeparateLoadBounds is true, this subscene will use this value to set up the load/unload bounds"); endGroup("SubScene"); addGroup("LoadingManagement"); addField("freezeLoading", TypeBool, Offset(mFreezeLoading, SubScene), "If true, will prevent the zone from being changed from it's current loading state."); addField("loadIf", TypeCommand, Offset(mLoadIf, SubScene), "evaluation condition (true/false)"); addField("tickPeriodMS", TypeS32, Offset(mTickPeriodMS, SubScene), "evaluation rate (ms)"); addField("onLoadCommand", TypeCommand, Offset(mOnLoadCommand, SubScene), "The command to execute when the subscene is loaded. Maximum 1023 characters."); addField("onUnloadCommand", TypeCommand, Offset(mOnUnloadCommand, SubScene), "The command to execute when subscene is unloaded. Maximum 1023 characters."); endGroup("LoadingManagement"); Parent::initPersistFields(); } void SubScene::consoleInit() { Parent::consoleInit(); Con::addVariable("$SubScene::UnloadTimeoutMS", TypeBool, &SubScene::mUnloadTimeoutMs, "The amount of time in milliseconds it takes for a SubScene to be unloaded if it's inactive.\n" "@ingroup Editors\n"); Con::addVariable("$SubScene::transformChildren", TypeBool, &SubScene::smTransformChildren, "@brief If true, then transform manipulations modify child objects. If false, only triggering bounds is manipulated\n\n" "@ingroup Editors"); } void SubScene::addObject(SimObject* object) { SceneObject::addObject(object); } void SubScene::removeObject(SimObject* object) { SceneObject::removeObject(object); } U32 SubScene::packUpdate(NetConnection* conn, U32 mask, BitStream* stream) { U32 retMask = Parent::packUpdate(conn, mask, stream); stream->writeFlag(mGlobalLayer); if(stream->writeFlag(mUseSeparateLoadBounds)) { mathWrite(*stream, mLoadBounds); } return retMask; } void SubScene::unpackUpdate(NetConnection* conn, BitStream* stream) { Parent::unpackUpdate(conn, stream); mGlobalLayer = stream->readFlag(); mUseSeparateLoadBounds = stream->readFlag(); if(mUseSeparateLoadBounds) { mathRead(*stream, &mLoadBounds); } } void SubScene::onInspect(GuiInspector* inspector) { Parent::onInspect(inspector); #ifdef TORQUE_TOOLS //Put the SubScene group before everything that'd be SubScene-effecting, for orginazational purposes GuiInspectorGroup* subsceneGrp = inspector->findExistentGroup(StringTable->insert("SubScene")); if (!subsceneGrp) return; GuiControl* stack = dynamic_cast(subsceneGrp->findObjectByInternalName(StringTable->insert("Stack"))); //Save button GuiInspectorField* saveFieldGui = subsceneGrp->createInspectorField(); saveFieldGui->init(inspector, subsceneGrp); saveFieldGui->setSpecialEditField(true); saveFieldGui->setTargetObject(this); StringTableEntry fldnm = StringTable->insert("SaveSubScene"); saveFieldGui->setSpecialEditVariableName(fldnm); saveFieldGui->setInspectorField(NULL, fldnm); saveFieldGui->setDocs(""); stack->addObject(saveFieldGui); GuiButtonCtrl* saveButton = new GuiButtonCtrl(); saveButton->registerObject(); saveButton->setDataField(StringTable->insert("profile"), NULL, "ToolsGuiButtonProfile"); saveButton->setText("Save SubScene"); saveButton->resize(Point2I::Zero, saveFieldGui->getExtent()); saveButton->setHorizSizing(GuiControl::horizResizeWidth); saveButton->setVertSizing(GuiControl::vertResizeHeight); char szBuffer[512]; dSprintf(szBuffer, 512, "%d.save();", this->getId()); saveButton->setConsoleCommand(szBuffer); saveFieldGui->addObject(saveButton); #endif } void SubScene::inspectPostApply() { Parent::inspectPostApply(); setMaskBits(-1); } void SubScene::setTransform(const MatrixF& mat) { if(SubScene::smTransformChildren) { Parent::setTransform(mat); } else { SceneObject::setTransform(mat); } } void SubScene::setRenderTransform(const MatrixF& mat) { if (SubScene::smTransformChildren) { Parent::setRenderTransform(mat); } else { SceneObject::setRenderTransform(mat); } } bool SubScene::evaluateCondition() { if (!mLoadIf.isEmpty()) { //test the mapper plugged in condition line String resVar = getIdString() + String(".result"); Con::setBoolVariable(resVar.c_str(), false); String command = resVar + "=" + mLoadIf + ";"; Con::evaluatef(command.c_str()); return Con::getBoolVariable(resVar.c_str()); } return true; } bool SubScene::testBox(const Box3F& testBox) { bool passes = mGlobalLayer; if (!passes) { if(mUseSeparateLoadBounds) { Box3F loadBox = Box3F(-mLoadBounds.x, -mLoadBounds.y, -mLoadBounds.z, mLoadBounds.x, mLoadBounds.y, mLoadBounds.z); loadBox.setCenter(getPosition()); passes = loadBox.isOverlapped(testBox); } else { passes = getWorldBox().isOverlapped(testBox); } } if (passes) passes = evaluateCondition(); return passes; } void SubScene::write(Stream& stream, U32 tabStop, U32 flags) { MutexHandle handle; handle.lock(mMutex); // export selected only? if ((flags & SelectedOnly) && !isSelected()) { for (U32 i = 0; i < size(); i++) (*this)[i]->write(stream, tabStop, flags); return; } stream.writeTabs(tabStop); char buffer[2048]; const U32 bufferWriteLen = dSprintf(buffer, sizeof(buffer), "new %s(%s) {\r\n", getClassName(), getName() && !(flags & NoName) ? getName() : ""); stream.write(bufferWriteLen, buffer); writeFields(stream, tabStop + 1); //The only meaningful difference between this and simSet for writing is we skip the children, since they're just the levelAsset contents stream.writeTabs(tabStop); stream.write(4, "};\r\n"); } void SubScene::processTick(const Move* move) { mCurrTick += TickMs; if (mCurrTick > mTickPeriodMS) { mCurrTick = 0; //re-evaluate if (!evaluateCondition()) unload(); } } void SubScene::_onFileChanged(const Torque::Path& path) { if (gEditingMission) return; if(mSubSceneAsset.isNull() || Torque::Path(mSubSceneAsset->getLevelPath()) != path) return; if (mSaving) return; AssertFatal(path == mSubSceneAsset->getLevelPath(), "SubScene::_onFileChanged - path does not match filename."); _closeFile(false); _loadFile(false); setMaskBits(U32_MAX); } void SubScene::_removeContents(SimGroupIterator set) { for (SimGroupIterator itr(set); *itr; ++itr) { SimGroup* child = dynamic_cast(*itr); if (child) { _removeContents(SimGroupIterator(child)); GameBase* asGameBase = dynamic_cast(child); if (asGameBase) { asGameBase->scriptOnRemove(); } Sim::cancelPendingEvents(child); child->safeDeleteObject(); } } } void SubScene::_closeFile(bool removeFileNotify) { AssertFatal(isServerObject(), "Trying to close out a subscene file on the client is bad!"); _removeContents(SimGroupIterator(this)); if (removeFileNotify && mSubSceneAsset.notNull() && mSubSceneAsset->getLevelPath() != StringTable->EmptyString()) { Torque::FS::RemoveChangeNotification(mSubSceneAsset->getLevelPath(), this, &SubScene::_onFileChanged); } mGameModesList.clear(); } void SubScene::_loadFile(bool addFileNotify) { AssertFatal(isServerObject(), "Trying to load a SubScene file on the client is bad!"); if(mSubSceneAsset.isNull() || mSubSceneAsset->getLevelPath() == StringTable->EmptyString()) return; String evalCmd = String::ToString("exec(\"%s\");", mSubSceneAsset->getLevelPath()); String instantGroup = Con::getVariable("InstantGroup"); Con::setIntVariable("InstantGroup", this->getId()); Con::evaluate((const char*)evalCmd.c_str(), false, mSubSceneAsset->getLevelPath()); Con::setVariable("InstantGroup", instantGroup.c_str()); if (addFileNotify) Torque::FS::AddChangeNotification(mSubSceneAsset->getLevelPath(), this, &SubScene::_onFileChanged); } void SubScene::load() { mStartUnloadTimerMS = -1; //reset unload timers //no need to load multiple times if (mLoaded) return; if (mFreezeLoading) return; if (mSaving) return; GameMode::findGameModes(mGameModesNames, &mGameModesList); if (!isSelected() && (String(mGameModesNames).isNotEmpty() && mGameModesList.size() == 0) || !evaluateCondition()) { mLoaded = false; return; } _loadFile(true); mLoaded = true; onLoaded_callback(); for (U32 i = 0; i < mGameModesList.size(); i++) { mGameModesList[i]->onSubsceneLoaded_callback(this); } } void SubScene::unload() { if (!mLoaded) return; if (mFreezeLoading) return; if (mSaving) return; if (isSelected()) { mStartUnloadTimerMS = Sim::getCurrentTime(); return; //if a child is selected, then we don't want to unload } //scan down through our child objects, see if any are marked as selected, //if so, skip unloading and reset the timer for (SimGroupIterator itr(this); *itr; ++itr) { SimGroup* childGrp = dynamic_cast(*itr); if (childGrp) { if (childGrp->isSelected()) { mStartUnloadTimerMS = Sim::getCurrentTime(); return; //if a child is selected, then we don't want to unload } for (SimGroupIterator cldItr(childGrp); *cldItr; ++cldItr) { SimObject* chldChld = dynamic_cast(*cldItr); if (chldChld && chldChld->isSelected()) { mStartUnloadTimerMS = Sim::getCurrentTime(); return; //if a child is selected, then we don't want to unload } } } } onUnloaded_callback(); for (U32 i = 0; i < mGameModesList.size(); i++) { mGameModesList[i]->onSubsceneUnloaded_callback(this); } if (!mOnUnloadCommand.isEmpty()) { String command = "%this = " + String(getIdString()) + "; " + mOnUnloadCommand + ";"; Con::evaluatef(command.c_str()); } _closeFile(true); mLoaded = false; } bool SubScene::save(const String& filename) { if (!isServerObject()) return false; //if there's nothing TO save, don't bother if (size() == 0 || !isLoaded()) return false; if (mSubSceneAsset.isNull()) return false; if (mSaving) return false; //If we're flagged for unload, push back the unload timer so we can't accidentally trip be saving partway through an unload if (mStartUnloadTimerMS != -1) mStartUnloadTimerMS = Sim::getCurrentTime(); mSaving = true; PersistenceManager prMger; StringTableEntry levelPath = mSubSceneAsset->getLevelPath(); if (filename.isNotEmpty()) levelPath = StringTable->insert(filename.c_str()); FileStream fs; fs.open(levelPath, Torque::FS::File::Write); fs.close(); for (SimGroupIterator itr(this); *itr; ++itr) { SimObject* childObj = (*itr); if (!prMger.isDirty(childObj)) { if ((*itr)->isMethod("onSaving")) { Con::executef((*itr), "onSaving", mSubSceneAssetId); } if (childObj->getGroup() == this) { prMger.setDirty((*itr), levelPath); } } if(dynamic_cast(childObj) || dynamic_cast(childObj)) { prMger.addRemoveField(childObj, "position"); } } prMger.saveDirty(); //process our gameModeList and write it out to the levelAsset for metadata stashing bool saveSuccess = false; //Get the level asset if (mSubSceneAsset.isNull()) return saveSuccess; //update the gamemode list as well mSubSceneAsset->setDataField(StringTable->insert("gameModesNames"), NULL, StringTable->insert(mGameModesNames)); //Finally, save saveSuccess = mSubSceneAsset->saveAsset(); mSaving = false; return saveSuccess; } void SubScene::_onSelected() { if (!isLoaded() && isServerObject()) load(); } void SubScene::_onUnselected() { } void SubScene::prepRenderImage(SceneRenderState* state) { // only render if selected or render flag is set if (/*!smRenderTriggers && */!isSelected()) return; ObjectRenderInst* ri = state->getRenderPass()->allocInst(); ri->renderDelegate.bind(this, &SubScene::renderObject); ri->type = RenderPassManager::RIT_Editor; ri->translucentSort = true; ri->defaultKey = 1; state->getRenderPass()->addInst(ri); } void SubScene::renderObject(ObjectRenderInst* ri, SceneRenderState* state, BaseMatInstance* overrideMat) { if (overrideMat) return; GFXStateBlockDesc desc; desc.setZReadWrite(true, false); desc.setBlend(true); // Trigger polyhedrons are set up with outward facing normals and CCW ordering // so can't enable backface culling. desc.setCullMode(GFXCullNone); GFXTransformSaver saver; MatrixF mat = getRenderTransform(); GFX->multWorld(mat); GFXDrawUtil* drawer = GFX->getDrawUtil(); //Box3F scale = getScale() //Box3F bounds = Box3F(-m) if(mUseSeparateLoadBounds && !mGlobalLayer) { Box3F loadBounds = Box3F(-mLoadBounds.x, -mLoadBounds.y, -mLoadBounds.z, mLoadBounds.x, mLoadBounds.y, mLoadBounds.z); //bounds.setCenter(getPosition()); ColorI loadBoundsColor = ColorI(200, 200, 100, 50); drawer->drawCube(desc, loadBounds, loadBoundsColor); // Render wireframe. desc.setFillModeWireframe(); drawer->drawCube(desc, loadBounds, ColorI::BLACK); desc.setFillModeSolid(); } Point3F scale = getScale(); Box3F bounds = Box3F(-scale / 2, scale / 2); ColorI boundsColor = ColorI(135, 206, 235, 50); if (mGlobalLayer) boundsColor = ColorI(200, 100, 100, 25); else if (mLoaded) boundsColor = ColorI(50, 200, 50, 50); drawer->drawCube(desc, bounds, boundsColor); // Render wireframe. desc.setFillModeWireframe(); drawer->drawCube(desc, bounds, ColorI::BLACK); } DefineEngineMethod(SubScene, save, bool, (const char* filename), (""), "Save out the subScene.\n" "@param filename (optional) If empty, the subScene will save to it's regular asset path. If defined, it will save out to the filename provided") { return object->save(filename); } DefineEngineMethod(SubScene, load, void, (), , "Loads the SubScene's level file.\n") { object->load(); } DefineEngineMethod(SubScene, unload, void, (), , "Unloads the SubScene's level file.\n") { object->unload(); }