Torque3D/Engine/source/T3D/SubScene.cpp
JeffR 10d1aeca1f Shift tracking of dynamicObjects from strict child objects for Scenes, to tracking the objects but keeping them in the Cleanup Group to fix prefab loading behavior
Shifted to utilizing SimGroupInterator and persistenceManager to fix saving issues with SubScenes
Shifted to utilizing SimGroupIterator for Scene saving to standardize and minimize object misses
Changed prefab load file logic to add loaded child simgroup to rootscene for consistent tracking and avoiding conflicts with subScene's hijacking the ImmediateGroup var
Reduced duplication of build<type>field callback for inspectors
Added more standard field type lookups to inspector group when creating a field to avoid misses for types like 'F32' or similar.
Folded the added-on MinSize/MaxSize/SimgroupSelect fields for SimGroup editing into a single compound field and fixed bugs with it's behavior so it works as expected now
2024-10-27 20:04:13 -05:00

518 lines
13 KiB
C++

#include "SubScene.h"
#include "gameMode.h"
#include "console/persistenceManager.h"
#include "console/script.h"
#include "scene/sceneRenderState.h"
#include "renderInstance/renderPassManager.h"
#include "gfx/gfxDrawUtil.h"
#include "gfx/gfxTransformSaver.h"
#include "gui/editor/inspector/group.h"
#include "T3D/gameBase/gameBase.h"
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() :
mLevelAssetId(StringTable->EmptyString()),
mGameModesNames(StringTable->EmptyString()),
mScopeDistance(-1),
mLoaded(false),
mFreezeLoading(false),
mTickPeriodMS(1000),
mCurrTick(0),
mGlobalLayer(false)
{
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_LEVELASSET(Level, SubScene, "The level asset to load.");
addField("gameModes", TypeGameModeList, Offset(mGameModesNames, SubScene), "The game modes that this subscene is associated with.");
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");
}
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);
return retMask;
}
void SubScene::unpackUpdate(NetConnection* conn, BitStream* stream)
{
Parent::unpackUpdate(conn, stream);
mGlobalLayer = stream->readFlag();
}
void SubScene::onInspect(GuiInspector* inspector)
{
Parent::onInspect(inspector);
//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<GuiControl*>(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);
}
void SubScene::inspectPostApply()
{
Parent::inspectPostApply();
setMaskBits(-1);
}
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)
{
if (mGlobalLayer)
return true;
bool 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(mLevelAsset.isNull() || Torque::Path(mLevelAsset->getLevelPath()) != path)
return;
AssertFatal(path == mLevelAsset->getLevelPath(), "Prefab::_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<SimGroup*>(*itr);
if (child)
{
_removeContents(SimGroupIterator(child));
GameBase* asGameBase = dynamic_cast<GameBase*>(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 && mLevelAsset.notNull() && mLevelAsset->getLevelPath() != StringTable->EmptyString())
{
Torque::FS::RemoveChangeNotification(mLevelAsset->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(mLevelAsset.isNull() || mLevelAsset->getLevelPath() == StringTable->EmptyString())
return;
String evalCmd = String::ToString("exec(\"%s\");", mLevelAsset->getLevelPath());
String instantGroup = Con::getVariable("InstantGroup");
Con::setIntVariable("InstantGroup", this->getId());
Con::evaluate((const char*)evalCmd.c_str(), false, mLevelAsset->getLevelPath());
Con::setVariable("InstantGroup", instantGroup.c_str());
if (addFileNotify)
Torque::FS::AddChangeNotification(mLevelAsset->getLevelPath(), this, &SubScene::_onFileChanged);
}
void SubScene::load()
{
mStartUnloadTimerMS = -1; //reset unload timers
//no need to load multiple times
if (mLoaded)
return;
if (mFreezeLoading)
return;
_loadFile(true);
mLoaded = true;
GameMode::findGameModes(mGameModesNames, &mGameModesList);
onLoaded_callback();
for (U32 i = 0; i < mGameModesList.size(); i++)
{
mGameModesList[i]->onSubsceneLoaded_callback(this);
}
if (!mOnLoadCommand.isEmpty())
{
String command = "%this = " + String(getIdString()) + "; " + mLoadIf + ";";
Con::evaluatef(command.c_str());
}
}
void SubScene::unload()
{
if (!mLoaded)
return;
if (mFreezeLoading)
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<SimGroup*>(*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<SimObject*>(*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()
{
//if there's nothing TO save, don't bother
if (size() == 0 || !isLoaded())
return false;
if (mLevelAsset.isNull())
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();
PersistenceManager prMger;
StringTableEntry levelPath = mLevelAsset->getLevelPath();
FileStream fs;
fs.open(levelPath, Torque::FS::File::Write);
fs.close();
for (SimGroupIterator itr(this); *itr; ++itr)
{
if ((*itr)->isMethod("onSaving"))
{
ConsoleValue vars[3];
vars[2].setString(mLevelAssetId);
Con::execute((*itr), 3, vars);
}
prMger.setDirty((*itr), levelPath);
}
prMger.saveDirty();
//process our gameModeList and write it out to the levelAsset for metadata stashing
bool saveSuccess = false;
//Get the level asset
if (mLevelAsset.isNull())
return saveSuccess;
//update the gamemode list as well
mLevelAsset->setDataField(StringTable->insert("gameModesNames"), NULL, StringTable->insert(mGameModesNames));
//Finally, save
saveSuccess = mLevelAsset->saveAsset();
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<ObjectRenderInst>();
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)
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, (),,
"Save out the subScene.\n")
{
return object->save();
}
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();
}