Просмотр исходного кода

Implementation of Subscenes, SceneGroups and Gamemodes
Standardizes Gamemodes to be an actual class with data and utility functions that can be parsed
Adds inspector field handling for selecting gamemodes
Updated Scene class to work with Gamemodes for the gamemode field
Updates editor suite elements to be able to create SubScenes, SceneGroups and Gamemodes
Adds ability to convert SimGroup to SubScene
Updates BaseUI's chooselevel menu to have gamemode selection and filters shown levels based on selected gamemode

Areloch 1 год назад
Родитель
Сommit
ae8eca48e1
36 измененных файлов с 2962 добавлено и 172 удалено
  1. 89 36
      Engine/source/T3D/Scene.cpp
  2. 16 7
      Engine/source/T3D/Scene.h
  3. 350 0
      Engine/source/T3D/SceneGroup.cpp
  4. 55 0
      Engine/source/T3D/SceneGroup.h
  5. 427 0
      Engine/source/T3D/SubScene.cpp
  6. 108 0
      Engine/source/T3D/SubScene.h
  7. 2 2
      Engine/source/T3D/assets/ImageAsset.cpp
  8. 124 4
      Engine/source/T3D/assets/LevelAsset.cpp
  9. 94 3
      Engine/source/T3D/assets/LevelAsset.h
  10. 436 0
      Engine/source/T3D/gameMode.cpp
  11. 110 0
      Engine/source/T3D/gameMode.h
  12. 7 0
      Engine/source/gui/core/guiControl.h
  13. 5 0
      Engine/source/scene/sceneObject.cpp
  14. 2 0
      Engine/source/scene/sceneObject.h
  15. 15 5
      Templates/BaseGame/game/core/utility/scripts/helperFunctions.tscript
  16. 2 13
      Templates/BaseGame/game/data/ExampleModule/ExampleModule.tscript
  17. 1 0
      Templates/BaseGame/game/data/ExampleModule/levels/ExampleLevel.asset.taml
  18. 26 8
      Templates/BaseGame/game/data/ExampleModule/scripts/server/ExampleGameMode.tscript
  19. 5 0
      Templates/BaseGame/game/data/UI/UI.tscript
  20. 76 15
      Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.gui
  21. 193 51
      Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.tscript
  22. 2 0
      Templates/BaseGame/game/tools/assetBrowser/main.tscript
  23. 0 1
      Templates/BaseGame/game/tools/assetBrowser/scripts/addModuleWindow.tscript
  24. 96 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/gameMode.tscript
  25. 121 3
      Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/level.tscript
  26. 25 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/editAsset.tscript
  27. 2 1
      Templates/BaseGame/game/tools/assetBrowser/scripts/popupMenus.tscript
  28. 176 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/gameMode.tscript.template
  29. 12 10
      Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/module.tscript.template
  30. 253 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/utils.tscript
  31. 4 0
      Templates/BaseGame/game/tools/gui/editorSettingsWindow.ed.tscript
  32. 2 0
      Templates/BaseGame/game/tools/settings.xml
  33. 26 2
      Templates/BaseGame/game/tools/worldEditor/gui/WorldEditorTreeWindow.ed.gui
  34. 91 2
      Templates/BaseGame/game/tools/worldEditor/scripts/EditorGui.ed.tscript
  35. 1 0
      Templates/BaseGame/game/tools/worldEditor/scripts/editorPrefs.ed.tscript
  36. 8 9
      Templates/BaseGame/game/tools/worldEditor/scripts/menuHandlers.ed.tscript

+ 89 - 36
Engine/source/T3D/Scene.cpp

@@ -1,20 +1,26 @@
 #include "Scene.h"
 #include "T3D/assets/LevelAsset.h"
+#include "T3D/gameBase/gameConnection.h"
+#include "T3D/gameMode.h"
 
 Scene * Scene::smRootScene = nullptr;
 Vector<Scene*> Scene::smSceneList;
 
+IMPLEMENT_CALLBACK(Scene, onSaving, void, (const char* fileName), (fileName),
+   "@brief Called when a scene is saved to allow scenes to special-handle prepwork for saving if required.\n\n"
+
+   "@param fileName The level file being saved\n");
+
 IMPLEMENT_CO_NETOBJECT_V1(Scene);
 
 Scene::Scene() : 
-   mIsSubScene(false),
    mParentScene(nullptr),
    mSceneId(-1),
    mIsEditing(false),
    mIsDirty(false),
    mEditPostFX(0)
 {
-   mGameModeName = StringTable->EmptyString();
+   mGameModesNames = StringTable->EmptyString();
 }
 
 Scene::~Scene()
@@ -28,13 +34,12 @@ void Scene::initPersistFields()
    Parent::initPersistFields();
 
    addGroup("Internal");
-   addField("isSubscene", TypeBool, Offset(mIsSubScene, Scene), "", AbstractClassRep::FIELD_HideInInspectors);
    addField("isEditing", TypeBool, Offset(mIsEditing, Scene), "", AbstractClassRep::FIELD_HideInInspectors);
    addField("isDirty", TypeBool, Offset(mIsDirty, Scene), "", AbstractClassRep::FIELD_HideInInspectors);
    endGroup("Internal");
 
    addGroup("Gameplay");
-   addField("gameModeName", TypeString, Offset(mGameModeName, Scene), "The name of the gamemode that this scene utilizes");
+   addField("gameModes", TypeGameModeList, Offset(mGameModesNames, Scene), "The game modes that this Scene is associated with.");
    endGroup("Gameplay");
 
    addGroup("PostFX");
@@ -51,48 +56,33 @@ bool Scene::onAdd()
    smSceneList.push_back(this);
    mSceneId = smSceneList.size() - 1;
 
-   /*if (smRootScene == nullptr)
-   {
-      //we're the first scene, so we're the root. woo!
-      smRootScene = this;
-   }
-   else
-   {
-      mIsSubScene = true;
-      smRootScene->mSubScenes.push_back(this);
-   }*/
+   GameMode::findGameModes(mGameModesNames, &mGameModesList);
 
    return true;
 }
 
 void Scene::onRemove()
 {
+   for (U32 i = 0; i < mGameModesList.size(); i++)
+   {
+      mGameModesList[i]->onSceneUnloaded_callback();
+   }
+
    Parent::onRemove();
 
    smSceneList.remove(this);
    mSceneId = -1;
-
-   /*if (smRootScene == this)
-   {
-      for (U32 i = 0; i < mSubScenes.size(); i++)
-      {
-         mSubScenes[i]->deleteObject();
-      }
-   }
-   else if (smRootScene != nullptr)
-   {
-      for (U32 i = 0; i < mSubScenes.size(); i++)
-      {
-         if(mSubScenes[i]->getId() == getId())
-            smRootScene->mSubScenes.erase(i);
-      }
-   }*/
 }
 
 void Scene::onPostAdd()
 {
    if (isMethod("onPostAdd"))
       Con::executef(this, "onPostAdd");
+
+   for (U32 i = 0; i < mGameModesList.size(); i++)
+   {
+      mGameModesList[i]->onSceneLoaded_callback();
+   }
 }
 
 bool Scene::_editPostEffects(void* object, const char* index, const char* data)
@@ -110,11 +100,12 @@ bool Scene::_editPostEffects(void* object, const char* index, const char* data)
 void Scene::addObject(SimObject* object)
 {
    //Child scene
-   Scene* scene = dynamic_cast<Scene*>(object);
+   SubScene* scene = dynamic_cast<SubScene*>(object);
    if (scene)
    {
       //We'll keep these principly separate so they don't get saved into each other
       mSubScenes.push_back(scene);
+      Parent::addObject(object);
       return;
    }
 
@@ -135,7 +126,7 @@ void Scene::addObject(SimObject* object)
 void Scene::removeObject(SimObject* object)
 {
    //Child scene
-   Scene* scene = dynamic_cast<Scene*>(object);
+   SubScene* scene = dynamic_cast<SubScene*>(object);
    if (scene)
    {
       //We'll keep these principly separate so they don't get saved into each other
@@ -175,12 +166,58 @@ void Scene::removeDynamicObject(SceneObject* object)
 
 void Scene::interpolateTick(F32 delta)
 {
-
 }
 
 void Scene::processTick()
 {
+   if (!isServerObject())
+      return;
+
+   //iterate over our subscenes to update their status of loaded or unloaded based on if any control objects intersect their bounds
+   for (U32 i = 0; i < mSubScenes.size(); i++)
+   {
+      bool hasClients = false;
+
+      SimGroup* pClientGroup = Sim::getClientGroup();
+      for (SimGroup::iterator itr = pClientGroup->begin(); itr != pClientGroup->end(); itr++)
+      {
+         GameConnection* gc = dynamic_cast<GameConnection*>(*itr);
+         if (gc)
+         {
+            GameBase* controlObj = gc->getControlObject();
+            if (controlObj == nullptr)
+            {
+               controlObj = gc->getCameraObject();
+            }
+
+            if (controlObj != nullptr)
+            {
+               if (mSubScenes[i]->testBox(controlObj->getWorldBox()))
+               {
+                  //we have a client controlling object in the bounds, so we ensure the contents are loaded
+                  hasClients = true;
+                  break;
+               }
+            }
+         }
+      }
 
+      if (hasClients)
+      {
+         mSubScenes[i]->setUnloadTimeMS(-1);
+         mSubScenes[i]->load();
+      }
+      else
+      {
+         if (mSubScenes[i]->isLoaded() && mSubScenes[i]->getUnloadTimsMS() == -1)
+         {
+            mSubScenes[i]->setUnloadTimeMS(Sim::getCurrentTime());
+         }
+
+         if (Sim::getCurrentTime() - mSubScenes[i]->getUnloadTimsMS() > 5000)
+            mSubScenes[i]->unload();
+      }
+   }
 }
 
 void Scene::advanceTime(F32 timeDelta)
@@ -257,6 +294,21 @@ bool Scene::saveScene(StringTableEntry fileName)
       fileName = getOriginatingFile();
    }
 
+   //Inform our objects we're saving, so if they do any special stuff
+   //they can do it before the actual write-out
+   for (U32 i = 0; i < mPermanentObjects.size(); i++)
+   {
+      SceneObject* obj = mPermanentObjects[i];
+      obj->onSaving_callback(fileName);
+   }
+
+   //Inform our subscenes we're saving so they can do any
+   //special work required as well
+   for (U32 i = 0; i < mSubScenes.size(); i++)
+   {
+      mSubScenes[i]->save();
+   }
+
    bool saveSuccess = save(fileName);
 
    if (!saveSuccess)
@@ -286,9 +338,12 @@ bool Scene::saveScene(StringTableEntry fileName)
       dSprintf(depValue, sizeof(depValue), "%s=%s", ASSET_ID_SIGNATURE, utilizedAssetsList[i]);
 
       levelAssetDef->setDataField(StringTable->insert(depSlotName), NULL, StringTable->insert(depValue));
-
    }
 
+   //update the gamemode list as well
+   levelAssetDef->setDataField(StringTable->insert("gameModesNames"), NULL, StringTable->insert(mGameModesNames));
+
+   //Finally, save
    saveSuccess = levelAssetDef->saveAsset();
 
    return saveSuccess;
@@ -413,8 +468,6 @@ DefineEngineMethod(Scene, getLevelAsset, const char*, (), ,
 DefineEngineMethod(Scene, save, bool, (const char* fileName), (""),
    "Save out the object to the given file.\n"
    "@param fileName The name of the file to save to."
-   "@param selectedOnly If true, only objects marked as selected will be saved out.\n"
-   "@param preAppendString Text which will be preprended directly to the object serialization.\n"
    "@param True on success, false on failure.")
 {
    return object->saveScene(StringTable->insert(fileName));

+ 16 - 7
Engine/source/T3D/Scene.h

@@ -1,5 +1,5 @@
 #pragma once
-
+#ifndef SCENE_H
 #include "console/engineAPI.h"
 
 #ifndef _NETOBJECT_H_
@@ -9,8 +9,16 @@
 #ifndef _ITICKABLE_H_
 #include "core/iTickable.h"
 #endif
-
+#ifndef _SCENEOBJECT_H_
 #include "scene/sceneObject.h"
+#endif
+
+#ifndef GAME_MODE_H
+#include "gameMode.h"
+#endif
+#ifndef SUB_SCENE_H
+#include "SubScene.h"
+#endif
 
 /// Scene
 /// This object is effectively a smart container to hold and manage any relevent scene objects and data
@@ -19,14 +27,11 @@ class Scene : public NetObject, public virtual ITickable
 {
    typedef NetObject Parent;
 
-   bool mIsSubScene;
-
    Scene* mParentScene;
 
-   Vector<Scene*> mSubScenes;
+   Vector<SubScene*> mSubScenes;
 
    Vector<SceneObject*> mPermanentObjects;
-
    Vector<SceneObject*> mDynamicObjects;
 
    S32 mSceneId;
@@ -37,7 +42,8 @@ class Scene : public NetObject, public virtual ITickable
 
    bool mEditPostFX;
 
-   StringTableEntry mGameModeName;
+   StringTableEntry mGameModesNames;
+   Vector<GameMode*> mGameModesList;
 
 protected:
    static Scene * smRootScene;
@@ -96,6 +102,8 @@ public:
    }
 
    static Vector<Scene*> smSceneList;
+
+   DECLARE_CALLBACK(void, onSaving, (const char* fileName));
 };
 
 
@@ -136,3 +144,4 @@ Vector<T*> Scene::getObjectsByClass(bool checkSubscenes)
 
    return foundObjects;
 }
+#endif

+ 350 - 0
Engine/source/T3D/SceneGroup.cpp

@@ -0,0 +1,350 @@
+#include "SceneGroup.h"
+
+#include "gameBase/gameConnection.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 "physics/physicsShape.h"
+#include "renderInstance/renderPassManager.h"
+#include "scene/sceneRenderState.h"
+
+IMPLEMENT_CO_NETOBJECT_V1(SceneGroup);
+
+ConsoleDocClass(SceneGroup,
+   "@brief A collection of arbitrary objects which can be allocated and manipulated as a group.\n\n"
+
+   "%SceneGroup always points to a (.SceneGroup) file which defines its objects. In "
+   "fact more than one %SceneGroup can reference this file and both will update "
+   "if the file is modified.\n\n"
+
+   "%SceneGroup is a very simple object and only exists on the server. When it is "
+   "created it allocates children objects by reading the (.SceneGroup) file like "
+   "a list of instructions.  It then sets their transform relative to the %SceneGroup "
+   "and Torque networking handles the rest by ghosting the new objects to clients. "
+   "%SceneGroup itself is not ghosted.\n\n"
+
+   "@ingroup enviroMisc"
+);
+
+SceneGroup::SceneGroup()
+{
+   // Not ghosted unless we're editing
+   mNetFlags.clear(Ghostable | ScopeAlways);
+
+   mTypeMask |= StaticObjectType;
+}
+
+SceneGroup::~SceneGroup()
+{
+}
+
+void SceneGroup::initPersistFields()
+{
+   docsURL;
+
+   addGroup("SceneGroup");
+   endGroup("SceneGroup");
+
+   Parent::initPersistFields();
+}
+
+bool SceneGroup::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();
+
+   addToScene();
+
+   return true;
+}
+
+void SceneGroup::onRemove()
+{
+   removeFromScene();
+   Parent::onRemove();
+}
+
+void SceneGroup::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 SceneGroup::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 SceneGroup::inspectPostApply()
+{
+   Parent::inspectPostApply();
+}
+
+void SceneGroup::onInspect(GuiInspector* inspector)
+{
+   //Put the SubScene group before everything that'd be SubScene-effecting, for orginazational purposes
+   GuiInspectorGroup* sceneGroupGrp = inspector->findExistentGroup(StringTable->insert("SceneGroup"));
+   if (!sceneGroupGrp)
+      return;
+
+   GuiControl* stack = dynamic_cast<GuiControl*>(sceneGroupGrp->findObjectByInternalName(StringTable->insert("Stack")));
+
+   //Regen bounds button
+   GuiInspectorField* regenFieldGui = sceneGroupGrp->createInspectorField();
+   regenFieldGui->init(inspector, sceneGroupGrp);
+
+   regenFieldGui->setSpecialEditField(true);
+   regenFieldGui->setTargetObject(this);
+
+   StringTableEntry fldnm = StringTable->insert("RegenerateBounds");
+
+   regenFieldGui->setSpecialEditVariableName(fldnm);
+
+   regenFieldGui->setInspectorField(NULL, fldnm);
+   regenFieldGui->setDocs("");
+
+   stack->addObject(regenFieldGui);
+
+   GuiButtonCtrl* regenButton = new GuiButtonCtrl();
+   regenButton->registerObject();
+   regenButton->setDataField(StringTable->insert("profile"), NULL, "ToolsGuiButtonProfile");
+   regenButton->setText("Regenerate Bounds");
+   regenButton->resize(Point2I::Zero, regenFieldGui->getExtent());
+   regenButton->setHorizSizing(GuiControl::horizResizeWidth);
+   regenButton->setVertSizing(GuiControl::vertResizeHeight);
+
+   char rgBuffer[512];
+   dSprintf(rgBuffer, 512, "%d.recalculateBounds();", this->getId());
+   regenButton->setConsoleCommand(rgBuffer);
+
+   regenFieldGui->addObject(regenButton);
+}
+
+void SceneGroup::setTransform(const MatrixF& mat)
+{
+   //transform difference
+   MatrixF oldTransform = getTransform();
+
+   Parent::setTransform(mat);
+
+   // Calculate the delta transformation
+   MatrixF deltaTransform;
+   oldTransform.inverse();
+   deltaTransform.mul(oldTransform, getTransform());
+
+   if (isServerObject())
+   {
+      setMaskBits(TransformMask);
+
+      // Update all child transforms
+      for (SimSetIterator itr(this); *itr; ++itr)
+      {
+         SceneObject* child = dynamic_cast<SceneObject*>(*itr);
+         if (child)
+         {
+            MatrixF childTransform = child->getTransform();
+            MatrixF relativeTransform;
+            relativeTransform.mul(deltaTransform, childTransform);
+            child->setTransform(relativeTransform);
+            child->setScale(childTransform.getScale()); //we don't modify scale
+         }
+      }
+   }
+}
+
+void SceneGroup::setRenderTransform(const MatrixF& mat)
+{
+   //transform difference
+   MatrixF oldTransform = getRenderTransform();
+
+   Parent::setRenderTransform(mat);
+
+   // Calculate the delta transformation
+   MatrixF deltaTransform;
+   oldTransform.inverse();
+   deltaTransform.mul(oldTransform, getRenderTransform());
+
+   // Update all child transforms
+   for (SimSetIterator itr(this); *itr; ++itr)
+   {
+      SceneObject* child = dynamic_cast<SceneObject*>(*itr);
+      if (child)
+      {
+         MatrixF childTransform = child->getRenderTransform();
+         MatrixF relativeTransform;
+         relativeTransform.mul(deltaTransform, childTransform);
+         child->setRenderTransform(relativeTransform);
+         child->setScale(childTransform.getScale()); //we don't modify scale
+      }
+   }
+}
+
+void SceneGroup::addObject(SimObject* object)
+{
+   Parent::addObject(object);
+
+   // Recalculate the bounding box from scratch (simpler but potentially costly)
+   recalculateBoundingBox();
+}
+
+void SceneGroup::removeObject(SimObject* object)
+{
+   Parent::removeObject(object);
+
+   // Recalculate the bounding box from scratch (simpler but potentially costly)
+   recalculateBoundingBox();
+}
+
+void SceneGroup::recalculateBoundingBox()
+{
+   if (empty())
+      return;
+
+   // Reset the bounding box
+   Box3F bounds;
+
+   bounds.minExtents.set(1e10, 1e10, 1e10);
+   bounds.maxExtents.set(-1e10, -1e10, -1e10);
+
+   // Extend the bounding box to include each child's bounding box
+   for (SimSetIterator itr(this); *itr; ++itr)
+   {
+      SceneObject* child = dynamic_cast<SceneObject*>(*itr);
+      if (child)
+      {
+         const Box3F& childBox = child->getWorldBox();
+
+         bounds.minExtents.setMin(childBox.minExtents);
+         bounds.maxExtents.setMax(childBox.maxExtents);
+      }
+   }
+
+   MatrixF newTrans = mObjToWorld;
+   newTrans.setPosition(bounds.getCenter());
+   Parent::setTransform(newTrans);
+
+   mObjScale = Point3F(bounds.len_x(), bounds.len_y(), bounds.len_z());
+   mWorldBox = bounds;
+   resetObjectBox();
+
+   setMaskBits(TransformMask);
+}
+
+U32 SceneGroup::packUpdate(NetConnection* conn, U32 mask, BitStream* stream)
+{
+   U32 retMask = Parent::packUpdate(conn, mask, stream);
+
+   mathWrite(*stream, mObjBox);
+
+   if (stream->writeFlag(mask & TransformMask))
+   {
+      mathWrite(*stream, getTransform());
+      mathWrite(*stream, getScale());
+   }
+
+   return retMask;
+}
+
+void SceneGroup::unpackUpdate(NetConnection* conn, BitStream* stream)
+{
+   Parent::unpackUpdate(conn, stream);
+
+   mathRead(*stream, &mObjBox);
+   resetWorldBox();
+
+   // TransformMask
+   if (stream->readFlag())
+   {
+      mathRead(*stream, &mObjToWorld);
+      mathRead(*stream, &mObjScale);
+
+      setTransform(mObjToWorld);
+   }
+}
+
+bool SceneGroup::buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere)
+{
+   Vector<SceneObject*> foundObjects;
+   if (empty())
+   {
+      Con::warnf("SceneGroup::buildPolyList() - SceneGroup %s is empty!", getName());
+      return false;
+   }
+   findObjectByType(foundObjects);
+
+   for (S32 i = 0; i < foundObjects.size(); i++)
+   {
+      foundObjects[i]->buildPolyList(context, polyList, box, sphere);
+   }
+
+   return true;
+}
+
+bool SceneGroup::buildExportPolyList(ColladaUtils::ExportData* exportData, const Box3F& box, const SphereF& sphere)
+{
+   Vector<SceneObject*> foundObjects;
+   findObjectByType(foundObjects);
+
+   for (S32 i = 0; i < foundObjects.size(); i++)
+   {
+      foundObjects[i]->buildExportPolyList(exportData, box, sphere);
+   }
+
+   return true;
+}
+
+void SceneGroup::getUtilizedAssets(Vector<StringTableEntry>* usedAssetsList)
+{
+   //if (empty())
+      return;
+
+   Vector<SceneObject*> foundObjects;
+   findObjectByType(foundObjects);
+
+   for (S32 i = 0; i < foundObjects.size(); i++)
+   {
+      SceneObject* child = foundObjects[i];
+
+      child->getUtilizedAssets(usedAssetsList);
+   }
+}
+
+DefineEngineMethod(SceneGroup, recalculateBounds, void, (), ,
+   "Recalculates the SceneGroups' bounds and centerpoint.\n")
+{
+   object->recalculateBoundingBox();
+}

+ 55 - 0
Engine/source/T3D/SceneGroup.h

@@ -0,0 +1,55 @@
+#pragma once
+#ifndef SCENE_GROUP_H
+#define SCENE_GROUP_H
+
+#ifndef _SCENEOBJECT_H_
+#include "scene/sceneObject.h"
+#endif
+
+class SceneGroup : public SceneObject
+{
+   typedef SceneObject Parent;
+
+public:
+   enum MaskBits
+   {
+      TransformMask = Parent::NextFreeMask << 0,
+      NextFreeMask = Parent::NextFreeMask << 1
+   };
+
+public:
+   SceneGroup();
+   virtual ~SceneGroup();
+
+   DECLARE_CONOBJECT(SceneGroup);
+   DECLARE_CATEGORY("Object \t Collection");
+
+   static void initPersistFields();
+
+   // SimObject
+   bool onAdd() override;
+   void onRemove() override;
+   void onEditorEnable() override;
+   void onEditorDisable() override;
+   void inspectPostApply() override;
+   void onInspect(GuiInspector* inspector) override;
+
+   // NetObject
+   U32 packUpdate(NetConnection* conn, U32 mask, BitStream* stream) override;
+   void unpackUpdate(NetConnection* conn, BitStream* stream) override;
+
+   // SceneObject
+   void setTransform(const MatrixF& mat) override;
+   void setRenderTransform(const MatrixF& mat) override;
+
+   // Object management
+   void addObject(SimObject* object) override;
+   void removeObject(SimObject* object) override;
+   void recalculateBoundingBox();
+
+   ///
+   bool buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere) override;
+   bool buildExportPolyList(ColladaUtils::ExportData* exportData, const Box3F& box, const SphereF&) override;
+   void getUtilizedAssets(Vector<StringTableEntry>* usedAssetsList) override;
+};
+#endif

+ 427 - 0
Engine/source/T3D/SubScene.cpp

@@ -0,0 +1,427 @@
+#include "SubScene.h"
+
+#include "gameMode.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"
+
+IMPLEMENT_CO_NETOBJECT_V1(SubScene);
+
+S32 SubScene::mUnloadTimeoutMs = 5000;
+
+SubScene::SubScene() :
+   mLevelAssetId(StringTable->EmptyString()),
+   mGameModesNames(StringTable->EmptyString()),
+   mScopeDistance(-1),
+   mLoaded(false),
+   mGlobalLayer(false)
+{
+   mNetFlags.set(Ghostable | ScopeAlways);
+
+   mTypeMask |= StaticObjectType;
+}
+
+SubScene::~SubScene()
+{
+}
+
+bool SubScene::onAdd()
+{
+   if (!Parent::onAdd())
+     return false;
+
+    return true;
+}
+
+void SubScene::onRemove()
+{
+    if (isClientObject())
+      removeFromScene();
+
+    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");
+
+   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::testBox(const Box3F& testBox)
+{
+   if (mGlobalLayer)
+      return true;
+
+   bool isOverlapped = getWorldBox().isOverlapped(testBox);
+      
+   return getWorldBox().isOverlapped(testBox);
+}
+
+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)
+{
+}
+
+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::_closeFile(bool removeFileNotify)
+{
+   AssertFatal(isServerObject(), "Trying to close out a subscene file on the client is bad!");
+
+   U32 count = size();
+
+   for (SimSetIterator itr(this); *itr; ++itr)
+   {
+      SimObject* child = dynamic_cast<SimObject*>(*itr);
+
+      if (child)
+         child->safeDeleteObject();
+   }
+
+   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;
+
+   _loadFile(true);
+   mLoaded = true;
+
+   GameMode::findGameModes(mGameModesNames, &mGameModesList);
+
+   for (U32 i = 0; i < mGameModesList.size(); i++)
+   {
+      mGameModesList[i]->onSubsceneLoaded_callback();
+   }
+}
+
+void SubScene::unload()
+{
+   if (!mLoaded)
+      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 (SimSetIterator itr(this); *itr; ++itr)
+   {
+      SimGroup* childGrp = dynamic_cast<SimGroup*>(*itr);
+      if (childGrp && childGrp->isSelected())
+      {
+         mStartUnloadTimerMS = Sim::getCurrentTime();
+         return; //if a child is selected, then we don't want to unload
+      }
+
+      for (SimSetIterator 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
+         }
+      }
+   }
+
+   _closeFile(true);
+   mLoaded = false;
+
+   for (U32 i = 0; i < mGameModesList.size(); i++)
+   {
+      mGameModesList[i]->onSubsceneUnloaded_callback();
+   }
+}
+
+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();
+
+   StringTableEntry levelPath = mLevelAsset->getLevelPath();
+
+   for (SimGroup::iterator itr = begin(); itr != end(); itr++)
+   {
+      //Just in case there's a valid callback the scene object would like to invoke for saving
+      SceneObject* gc = dynamic_cast<SceneObject*>(*itr);
+      if (gc)
+      {
+         gc->onSaving_callback(mLevelAssetId);
+      }
+
+      SimObject* sO = static_cast<SimObject*>(*itr);
+
+      if (!sO->save(levelPath))
+      {
+         Con::errorf("SubScene::save() - error, failed to write object %s to file: %s", sO->getIdString(), levelPath);
+         return false;
+      }
+   }
+
+   //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();
+
+}
+
+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();
+}

+ 108 - 0
Engine/source/T3D/SubScene.h

@@ -0,0 +1,108 @@
+#pragma once
+#ifndef SUB_SCENE_H
+#define SUB_SCENE_H
+
+#ifndef SCENE_GROUP_H
+#include "SceneGroup.h"
+#endif
+#ifndef LEVEL_ASSET_H
+#include "assets/LevelAsset.h"
+#endif
+#ifndef GAME_MODE_H
+#include "gameMode.h"
+#endif
+
+class SubScene : public SceneGroup
+{
+   typedef SceneGroup Parent;
+
+public:
+   enum MaskBits
+   {
+      NextFreeMask = Parent::NextFreeMask << 0
+   };
+
+   void onLevelChanged() {}
+
+private:
+   DECLARE_LEVELASSET(SubScene, Level, onLevelChanged);
+
+   StringTableEntry mGameModesNames;
+   Vector<GameMode*> mGameModesList;
+
+   F32 mScopeDistance;
+
+   /// <summary>
+   /// How long we wait once every control object has left the SubScene's load boundary for us to unload the levelAsset.
+   /// </summary>
+   S32 mStartUnloadTimerMS;
+
+   bool mLoaded;
+
+   bool mGlobalLayer;
+public:
+   SubScene();
+   virtual ~SubScene();
+
+   DECLARE_CONOBJECT(SubScene);
+   DECLARE_CATEGORY("Object \t Collection");
+
+   static void initPersistFields();
+   static void consoleInit();
+
+   // SimObject
+   bool onAdd() override;
+   void onRemove() override;
+
+   U32 packUpdate(NetConnection* conn, U32 mask, BitStream* stream) override;
+   void unpackUpdate(NetConnection* conn, BitStream* stream) override;
+
+   void addObject(SimObject* object);
+   void removeObject(SimObject* object);
+   //void onEditorEnable() override;
+   //void onEditorDisable() override;
+   void inspectPostApply() override;
+
+   bool testBox(const Box3F& testBox);
+
+   void _onSelected() override;
+   void _onUnselected() override;
+
+   static S32 mUnloadTimeoutMs;
+
+protected:
+   void write(Stream& stream, U32 tabStop, U32 flags = 0) override;
+
+   //
+   void _onFileChanged(const Torque::Path& path);
+   void _closeFile(bool removeFileNotify);
+   void _loadFile(bool addFileNotify);
+
+   //
+public:
+   void processTick(const Move* move) override;
+
+   //
+   void onInspect(GuiInspector* inspector) override;
+
+   void load();
+   void unload();
+
+   void prepRenderImage(SceneRenderState* state) override;
+   void renderObject(ObjectRenderInst* ri,
+      SceneRenderState* state,
+      BaseMatInstance* overrideMat);
+
+   bool isLoaded() { return mLoaded; }
+   void setUnloadTimeMS(S32 unloadTimeMS) {
+      mStartUnloadTimerMS = unloadTimeMS;
+   }
+   inline S32 getUnloadTimsMS() {
+      return mStartUnloadTimerMS;
+   }
+
+   bool save();
+
+   DECLARE_ASSET_SETGET(SubScene, Level);
+};
+#endif

+ 2 - 2
Engine/source/T3D/assets/ImageAsset.cpp

@@ -104,7 +104,7 @@ ConsoleSetType(TypeImageAssetId)
    }
 
    // Warn.
-   Con::warnf("(TypeAssetId) - Cannot set multiple args to a single asset.");
+   Con::warnf("(TypeImageAssetId) - Cannot set multiple args to a single asset.");
 }
 //-----------------------------------------------------------------------------
 
@@ -748,7 +748,7 @@ void GuiInspectorTypeImageAssetPtr::setPreviewImage(StringTableEntry assetId)
 IMPLEMENT_CONOBJECT(GuiInspectorTypeImageAssetId);
 
 ConsoleDocClass(GuiInspectorTypeImageAssetId,
-   "@brief Inspector field type for Shapes\n\n"
+   "@brief Inspector field type for Images\n\n"
    "Editor use only.\n\n"
    "@internal"
 );

+ 124 - 4
Engine/source/T3D/assets/LevelAsset.cpp

@@ -42,12 +42,14 @@
 
 // Debug Profiling.
 #include "platform/profiler.h"
+#include "gfx/gfxDrawUtil.h"
+
 
 //-----------------------------------------------------------------------------
 
 IMPLEMENT_CONOBJECT(LevelAsset);
 
-ConsoleType(LevelAssetPtr, TypeLevelAssetPtr, const char*, ASSET_ID_FIELD_PREFIX)
+ConsoleType(LevelAssetPtr, TypeLevelAssetPtr, const char*, "")
 
 //-----------------------------------------------------------------------------
 
@@ -74,6 +76,28 @@ ConsoleSetType(TypeLevelAssetPtr)
    Con::warnf("(TypeLevelAssetPtr) - Cannot set multiple args to a single asset.");
 }
 
+//-----------------------------------------------------------------------------
+ConsoleType(assetIdString, TypeLevelAssetId, const char*, "")
+
+ConsoleGetType(TypeLevelAssetId)
+{
+   // Fetch asset Id.
+   return *((const char**)(dptr));
+}
+
+ConsoleSetType(TypeLevelAssetId)
+{
+   // Was a single argument specified?
+   if (argc == 1)
+   {
+      *((const char**)dptr) = StringTable->insert(argv[0]);
+
+      return;
+   }
+
+   // Warn.
+   Con::warnf("(TypeLevelAssetId) - Cannot set multiple args to a single asset.");
+}
 //-----------------------------------------------------------------------------
 
 LevelAsset::LevelAsset() : AssetBase(), mIsSubLevel(false)
@@ -91,7 +115,7 @@ LevelAsset::LevelAsset() : AssetBase(), mIsSubLevel(false)
    mForestPath = StringTable->EmptyString();
    mNavmeshPath = StringTable->EmptyString();
 
-   mGamemodeName = StringTable->EmptyString();
+   mGameModesNames = StringTable->EmptyString();
    mMainLevelAsset = StringTable->EmptyString();
 
    mEditorFile = StringTable->EmptyString();
@@ -134,7 +158,7 @@ void LevelAsset::initPersistFields()
       &setBakedSceneFile, &getBakedSceneFile, "Path to the level file with the objects generated as part of the baking process");
 
    addField("isSubScene", TypeBool, Offset(mIsSubLevel, LevelAsset), "Is this a sublevel to another Scene");
-   addField("gameModeName", TypeString, Offset(mGamemodeName, LevelAsset), "Name of the Game Mode to be used with this level");
+   addField("gameModesNames", TypeString, Offset(mGameModesNames, LevelAsset), "Name of the Game Mode to be used with this level");
 }
 
 //------------------------------------------------------------------------------
@@ -357,7 +381,7 @@ void LevelAsset::unloadDependencies()
    }
 }
 
-DefineEngineMethod(LevelAsset, getLevelPath, const char*, (),,
+DefineEngineMethod(LevelAsset, getLevelPath, const char*, (), ,
    "Gets the full path of the asset's defined level file.\n"
    "@return The string result of the level path")
 {
@@ -417,3 +441,99 @@ DefineEngineMethod(LevelAsset, unloadDependencies, void, (), ,
 {
    return object->unloadDependencies();
 }
+
+//-----------------------------------------------------------------------------
+// GuiInspectorTypeAssetId
+//-----------------------------------------------------------------------------
+
+IMPLEMENT_CONOBJECT(GuiInspectorTypeLevelAssetPtr);
+
+ConsoleDocClass(GuiInspectorTypeLevelAssetPtr,
+   "@brief Inspector field type for Shapes\n\n"
+   "Editor use only.\n\n"
+   "@internal"
+);
+
+void GuiInspectorTypeLevelAssetPtr::consoleInit()
+{
+   Parent::consoleInit();
+
+   ConsoleBaseType::getType(TypeLevelAssetPtr)->setInspectorFieldType("GuiInspectorTypeLevelAssetPtr");
+}
+
+GuiControl* GuiInspectorTypeLevelAssetPtr::constructEditControl()
+{
+   // Create base filename edit controls
+   GuiControl* retCtrl = Parent::constructEditControl();
+   if (retCtrl == NULL)
+      return retCtrl;
+
+   // Change filespec
+   char szBuffer[512];
+   dSprintf(szBuffer, sizeof(szBuffer), "AssetBrowser.showDialog(\"LevelAsset\", \"AssetBrowser.changeAsset\", %s, \"\");",
+      getIdString());
+   mBrowseButton->setField("Command", szBuffer);
+
+   setDataField(StringTable->insert("targetObject"), NULL, mInspector->getInspectObject()->getIdString());
+
+   // Create "Open in Editor" button
+   mEditButton = new GuiBitmapButtonCtrl();
+
+   dSprintf(szBuffer, sizeof(szBuffer), "$createAndAssignField = %s; AssetBrowser.setupCreateNewAsset(\"LevelAsset\", AssetBrowser.selectedModule, \"createAndAssignLevelAsset\");",
+      getIdString());
+   mEditButton->setField("Command", szBuffer);
+
+   char bitmapName[512] = "ToolsModule:iconAdd_image";
+   mEditButton->setBitmap(StringTable->insert(bitmapName));
+
+   mEditButton->setDataField(StringTable->insert("Profile"), NULL, "GuiButtonProfile");
+   mEditButton->setDataField(StringTable->insert("tooltipprofile"), NULL, "GuiToolTipProfile");
+   mEditButton->setDataField(StringTable->insert("hovertime"), NULL, "1000");
+   mEditButton->setDataField(StringTable->insert("tooltip"), NULL, "Test play this sound");
+
+   mEditButton->registerObject();
+   addObject(mEditButton);
+
+   return retCtrl;
+}
+
+bool GuiInspectorTypeLevelAssetPtr::updateRects()
+{
+   S32 dividerPos, dividerMargin;
+   mInspector->getDivider(dividerPos, dividerMargin);
+   Point2I fieldExtent = getExtent();
+   Point2I fieldPos = getPosition();
+
+   mCaptionRect.set(0, 0, fieldExtent.x - dividerPos - dividerMargin, fieldExtent.y);
+   mEditCtrlRect.set(fieldExtent.x - dividerPos + dividerMargin, 1, dividerPos - dividerMargin - 34, fieldExtent.y);
+
+   bool resized = mEdit->resize(mEditCtrlRect.point, mEditCtrlRect.extent);
+   if (mBrowseButton != NULL)
+   {
+      mBrowseRect.set(fieldExtent.x - 32, 2, 14, fieldExtent.y - 4);
+      resized |= mBrowseButton->resize(mBrowseRect.point, mBrowseRect.extent);
+   }
+
+   if (mEditButton != NULL)
+   {
+      RectI shapeEdRect(fieldExtent.x - 16, 2, 14, fieldExtent.y - 4);
+      resized |= mEditButton->resize(shapeEdRect.point, shapeEdRect.extent);
+   }
+
+   return resized;
+}
+
+IMPLEMENT_CONOBJECT(GuiInspectorTypeLevelAssetId);
+
+ConsoleDocClass(GuiInspectorTypeLevelAssetId,
+   "@brief Inspector field type for Levels\n\n"
+   "Editor use only.\n\n"
+   "@internal"
+);
+
+void GuiInspectorTypeLevelAssetId::consoleInit()
+{
+   Parent::consoleInit();
+
+   ConsoleBaseType::getType(TypeLevelAssetId)->setInspectorFieldType("GuiInspectorTypeLevelAssetId");
+}

+ 94 - 3
Engine/source/T3D/assets/LevelAsset.h

@@ -40,6 +40,11 @@
 #endif
 #include "T3D/assets/ImageAsset.h"
 
+#ifndef _GUI_INSPECTOR_TYPES_H_
+#include "gui/editor/guiInspectorTypes.h"
+#endif
+#include <gui/controls/guiBitmapCtrl.h>
+
 //-----------------------------------------------------------------------------
 class LevelAsset : public AssetBase
 {
@@ -64,7 +69,7 @@ class LevelAsset : public AssetBase
    bool                    mIsSubLevel;
    StringTableEntry        mMainLevelAsset;
 
-   StringTableEntry        mGamemodeName;
+   StringTableEntry        mGameModesNames;
 
    Vector<AssetBase*>      mAssetDependencies;
 
@@ -114,7 +119,7 @@ public:
    U32 load() override { return Ok; };
 
 protected:
-   static bool setLevelFile(void *obj, const char *index, const char *data) { static_cast<LevelAsset*>(obj)->setLevelFile(data); return false; }
+   static bool setLevelFile(void* obj, const char* index, const char* data) { static_cast<LevelAsset*>(obj)->setLevelFile(data); return false; }
    static const char* getLevelFile(void* obj, const char* data) { return static_cast<LevelAsset*>(obj)->getLevelFile(); }
 
    static bool setEditorFile(void* obj, const char* index, const char* data) { static_cast<LevelAsset*>(obj)->setEditorFile(data); return false; }
@@ -135,10 +140,96 @@ protected:
 
    void            initializeAsset(void) override;
    void            onAssetRefresh(void) override;
-   void                    loadAsset();
+   void            loadAsset();
+
+   typedef Signal<void()> LevelAssetChanged;
+   LevelAssetChanged mChangeSignal;
+
+public:
+   LevelAssetChanged& getChangedSignal() { return mChangeSignal; }
 };
 
+#ifdef TORQUE_TOOLS
+class GuiInspectorTypeLevelAssetPtr : public GuiInspectorTypeFileName
+{
+   typedef GuiInspectorTypeFileName Parent;
+public:
+
+   GuiBitmapButtonCtrl* mEditButton;
+
+   DECLARE_CONOBJECT(GuiInspectorTypeLevelAssetPtr);
+   static void consoleInit();
+
+   GuiControl* constructEditControl() override;
+   bool updateRects() override;
+};
+
+class GuiInspectorTypeLevelAssetId : public GuiInspectorTypeLevelAssetPtr
+{
+   typedef GuiInspectorTypeLevelAssetPtr Parent;
+public:
+
+   DECLARE_CONOBJECT(GuiInspectorTypeLevelAssetId);
+   static void consoleInit();
+};
+#endif
+
+
 DefineConsoleType(TypeLevelAssetPtr, LevelAsset)
+DefineConsoleType(TypeLevelAssetId, String)
+
+#pragma region Singular Asset Macros
+
+//Singular assets
+/// <Summary>
+/// Declares an level asset
+/// This establishes the assetId, asset and legacy filepath fields, along with supplemental getter and setter functions
+/// </Summary>
+#define DECLARE_LEVELASSET(className, name, changeFunc) public: \
+   StringTableEntry m##name##AssetId;\
+   AssetPtr<LevelAsset>  m##name##Asset;\
+public: \
+   const AssetPtr<LevelAsset> & get##name##Asset() const { return m##name##Asset; }\
+   void set##name##Asset(const AssetPtr<LevelAsset> &_in) { m##name##Asset = _in;}\
+   \
+   bool _set##name(StringTableEntry _in)\
+   {\
+      if(m##name##AssetId != _in)\
+      {\
+         if (m##name##Asset.notNull())\
+         {\
+            m##name##Asset->getChangedSignal().remove(this, &className::changeFunc);\
+         }\
+         if (_in == NULL || _in == StringTable->EmptyString())\
+         {\
+            m##name##AssetId = StringTable->EmptyString();\
+            m##name##Asset = NULL;\
+            return true;\
+         }\
+         if (AssetDatabase.isDeclaredAsset(_in))\
+         {\
+            m##name##AssetId = _in;\
+            m##name##Asset = _in;\
+            return true;\
+         }\
+      }\
+      \
+      if(get##name() == StringTable->EmptyString())\
+         return true;\
+      \
+      return false;\
+   }\
+   \
+   const StringTableEntry get##name() const\
+   {\
+      return m##name##AssetId;\
+   }\
+   bool name##Valid() {return (get##name() != StringTable->EmptyString() && m##name##Asset->getStatus() == AssetBase::Ok); }
+
+#define INITPERSISTFIELD_LEVELASSET(name, consoleClass, docs) \
+   addProtectedField(assetText(name, Asset), TypeLevelAssetId, Offset(m##name##AssetId, consoleClass), _set##name##Data, &defaultProtectedGetFn, assetDoc(name, asset docs.));
+
+#pragma endregion
 
 #endif // _ASSET_BASE_H_
 

+ 436 - 0
Engine/source/T3D/gameMode.cpp

@@ -0,0 +1,436 @@
+#include "gameMode.h"
+
+#ifdef TORQUE_TOOLS
+#include "gui/containers/guiDynamicCtrlArrayCtrl.h"
+#endif
+
+IMPLEMENT_CONOBJECT(GameMode);
+
+IMPLEMENT_CALLBACK(GameMode, onActivated, void, (), (),
+   "@brief Called when a gamemode is activated.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onDeactivated, void, (), (),
+   "@brief Called when a gamemode is deactivated.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onSceneLoaded, void, (), (),
+   "@brief Called when a scene has been loaded and has game mode implications.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onSceneUnloaded, void, (), (),
+   "@brief Called when a scene has been unloaded and has game mode implications.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onSubsceneLoaded, void, (), (),
+   "@brief Called when a subScene has been loaded and has game mode implications.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onSubsceneUnloaded, void, (), (),
+   "@brief Called when a subScene has been unloaded and has game mode implications.\n\n");
+
+
+ConsoleType(GameModeList, TypeGameModeList, const char*, "")
+
+ConsoleGetType(TypeGameModeList)
+{
+   // Fetch asset Id.
+   return *((const char**)(dptr));
+}
+
+//-----------------------------------------------------------------------------
+
+ConsoleSetType(TypeGameModeList)
+{
+   // Was a single argument specified?
+   if (argc == 1)
+   {
+      // Yes, so fetch field value.
+      *((const char**)dptr) = StringTable->insert(argv[0]);
+
+      return;
+   }
+
+   // Warn.
+   //Con::warnf("(TypeGameModeList) - Cannot set multiple args to a single asset.");
+}
+
+GameMode::GameMode() :
+   mGameModeName(StringTable->EmptyString()),
+   mGameModeDesc(StringTable->EmptyString())
+{
+   INIT_ASSET(PreviewImage);
+}
+
+void GameMode::initPersistFields()
+{
+     Parent::initPersistFields();
+
+     addField("gameModeName", TypeString, Offset(mGameModeName, GameMode), "Human-readable name of the gamemode");
+     addField("description", TypeString, Offset(mGameModeDesc, GameMode), "Description of the gamemode");
+
+     INITPERSISTFIELD_IMAGEASSET(PreviewImage, GameMode, "Preview Image");
+
+     addField("active", TypeBool, Offset(mIsActive, GameMode), "Is the gamemode active");
+}
+
+bool GameMode::onAdd()
+{
+   if (!Parent::onAdd())
+      return false;
+
+   return true;
+}
+
+void GameMode::onRemove()
+{
+   Parent::onRemove();
+}
+
+void GameMode::findGameModes(const char* gameModeList, Vector<GameMode*> *outGameModes)
+{
+   if (outGameModes == nullptr)
+      return;
+
+   Vector<String> gameModeNames;
+   U32 uCount = StringUnit::getUnitCount(gameModeList, ";");
+   for (U32 i = 0; i < uCount; i++)
+   {
+      String name = StringUnit::getUnit(gameModeList, i, ";");
+      if (!name.isEmpty())
+         gameModeNames.push_back(name);
+   }
+
+   for (U32 i = 0; i < gameModeNames.size(); i++)
+   {
+      GameMode* gm;
+      if (Sim::findObject(gameModeNames[i].c_str(), gm))
+      {
+         outGameModes->push_back(gm);
+      }
+   }
+}
+
+void GameMode::setActive(const bool& active)
+{
+   mIsActive = active;
+   if (mIsActive)
+      onActivated_callback();
+   else
+      onDeactivated_callback();
+}
+
+DefineEngineMethod(GameMode, isActive, bool, (), ,
+   "Returns if the GameMode is currently active.\n"
+   "@return The active status of the GameMode")
+{
+   return object->isActive();
+}
+
+DefineEngineMethod(GameMode, setActive, void, (bool active), (true),
+   "Sets the active state of the GameMode.\n"
+   "@param active A bool of the state the GameMode should be set to")
+{
+   object->setActive(active);
+}
+
+DefineEngineFunction(getGameModesList, const char*, (), , "")
+{
+   char* returnBuffer = Con::getReturnBuffer(1024);
+
+   String formattedList;
+
+   for (SimGroup::iterator itr = Sim::getRootGroup()->begin(); itr != Sim::getRootGroup()->end(); itr++)
+   {
+      GameMode* gm = dynamic_cast<GameMode*>(*itr);
+      if (gm)
+      {
+         formattedList += String(gm->getName()) + ";";
+      }
+   }
+
+   dSprintf(returnBuffer, 1024, "%s", formattedList.c_str());
+
+   return returnBuffer;
+}
+
+//-----------------------------------------------------------------------------
+// GuiInspectorTypeAssetId
+//-----------------------------------------------------------------------------
+#ifdef TORQUE_TOOLS
+
+GuiInspectorTypeGameModeList::GuiInspectorTypeGameModeList()
+   : mHelper(NULL),
+   mRollout(NULL),
+   mArrayCtrl(NULL)
+{
+
+}
+IMPLEMENT_CONOBJECT(GuiInspectorTypeGameModeList);
+
+ConsoleDocClass(GuiInspectorTypeGameModeList,
+   "@brief Inspector field type for selecting GameModes\n\n"
+   "Editor use only.\n\n"
+   "@internal"
+);
+
+bool GuiInspectorTypeGameModeList::onAdd()
+{
+   // Skip our parent because we aren't using mEditCtrl
+   // and according to our parent that would be cause to fail onAdd.
+   if (!Parent::Parent::onAdd())
+      return false;
+
+   if (!mInspector)
+      return false;
+
+   //build out our list of gamemodes
+   Vector<GameMode*> gameModesList;
+
+   for (SimGroup::iterator itr = Sim::getRootGroup()->begin(); itr != Sim::getRootGroup()->end(); itr++)
+   {
+      GameMode* gm = dynamic_cast<GameMode*>(*itr);
+      if (gm)
+         gameModesList.push_back(gm);
+   }
+   
+   static StringTableEntry sProfile = StringTable->insert("profile");
+   setDataField(sProfile, NULL, "GuiInspectorFieldProfile");
+   setBounds(0, 0, 100, 18);
+
+   // Allocate our children controls...
+
+   mRollout = new GuiRolloutCtrl();
+   mRollout->setMargin(14, 0, 0, 0);
+   mRollout->setCanCollapse(false);
+   mRollout->registerObject();
+   addObject(mRollout);
+
+   mArrayCtrl = new GuiDynamicCtrlArrayControl();
+   mArrayCtrl->setDataField(sProfile, NULL, "GuiInspectorBitMaskArrayProfile");
+   mArrayCtrl->setField("autoCellSize", "true");
+   mArrayCtrl->setField("fillRowFirst", "true");
+   mArrayCtrl->setField("dynamicSize", "true");
+   mArrayCtrl->setField("rowSpacing", "4");
+   mArrayCtrl->setField("colSpacing", "1");
+   mArrayCtrl->setField("frozen", "true");
+   mArrayCtrl->registerObject();
+
+   mRollout->addObject(mArrayCtrl);
+
+   GuiCheckBoxCtrl* pCheckBox = NULL;
+
+   for (S32 i = 0; i < gameModesList.size(); i++)
+   {
+      pCheckBox = new GuiCheckBoxCtrl();
+      pCheckBox->setText(gameModesList[i]->getName());
+      pCheckBox->registerObject();
+      mArrayCtrl->addObject(pCheckBox);
+
+      pCheckBox->autoSize();
+
+      // Override the normal script callbacks for GuiInspectorTypeCheckBox
+      char szBuffer[512];
+      dSprintf(szBuffer, 512, "%d.applyValue();", getId());
+      pCheckBox->setField("Command", szBuffer);
+   }
+
+   mArrayCtrl->setField("frozen", "false");
+   mArrayCtrl->refresh();
+
+   mHelper = new GuiInspectorTypeGameModeListHelper();
+   mHelper->init(mInspector, mParent);
+   mHelper->mParentRollout = mRollout;
+   mHelper->mParentField = this;
+   mHelper->setInspectorField(mField, mCaption, mFieldArrayIndex);
+   mHelper->registerObject();
+   mHelper->setExtent(pCheckBox->getExtent());
+   mHelper->setPosition(0, 0);
+   mRollout->addObject(mHelper);
+
+   mRollout->sizeToContents();
+   mRollout->instantCollapse();
+
+   updateValue();
+
+   return true;
+}
+
+void GuiInspectorTypeGameModeList::consoleInit()
+{
+   Parent::consoleInit();
+
+   ConsoleBaseType::getType(TypeGameModeList)->setInspectorFieldType("GuiInspectorTypeGameModeList");
+}
+
+void GuiInspectorTypeGameModeList::childResized(GuiControl* child)
+{
+   setExtent(mRollout->getExtent());
+}
+
+bool GuiInspectorTypeGameModeList::resize(const Point2I& newPosition, const Point2I& newExtent)
+{
+   if (!Parent::resize(newPosition, newExtent))
+      return false;
+
+   // Hack... height of 18 is hardcoded
+   return mHelper->resize(Point2I(0, 0), Point2I(newExtent.x, 18));
+}
+
+bool GuiInspectorTypeGameModeList::updateRects()
+{
+   if (!mRollout)
+      return false;
+
+   bool result = mRollout->setExtent(getExtent());
+
+   for (U32 i = 0; i < mArrayCtrl->size(); i++)
+   {
+      GuiInspectorField* pField = dynamic_cast<GuiInspectorField*>(mArrayCtrl->at(i));
+      if (pField)
+         if (pField->updateRects())
+            result = true;
+   }
+
+   if (mHelper && mHelper->updateRects())
+      result = true;
+
+   return result;
+}
+
+StringTableEntry GuiInspectorTypeGameModeList::getValue()
+{
+   if (!mRollout)
+      return StringTable->insert("");
+
+   String results = "";
+
+   for (U32 i = 0; i < mArrayCtrl->size(); i++)
+   {
+      GuiCheckBoxCtrl* pCheckBox = dynamic_cast<GuiCheckBoxCtrl*>(mArrayCtrl->at(i));
+
+      if (pCheckBox->getStateOn())
+         results += pCheckBox->getText() + String(";");
+   }
+
+   if (!results.isEmpty())
+      return StringTable->insert(results.c_str());
+   else
+      return StringTable->EmptyString();
+}
+
+void GuiInspectorTypeGameModeList::setValue(StringTableEntry value)
+{
+   Vector<String> gameModeNames;
+   U32 uCount = StringUnit::getUnitCount(value, ";");
+   for (U32 i = 0; i < uCount; i++)
+   {
+      String name = StringUnit::getUnit(value, i, ";");
+      if (!name.isEmpty())
+         gameModeNames.push_back(name);
+   }
+
+   for (U32 i = 0; i < mArrayCtrl->size(); i++)
+   {
+      GuiCheckBoxCtrl* pCheckBox = dynamic_cast<GuiCheckBoxCtrl*>(mArrayCtrl->at(i));
+
+      for (U32 m = 0; m < gameModeNames.size(); m++)
+      {
+         if (gameModeNames[m].equal(pCheckBox->getText()))
+         {
+            pCheckBox->setStateOn(true);
+         }
+      }
+   }
+
+   mHelper->setValue(value);
+}
+
+void GuiInspectorTypeGameModeList::updateData()
+{
+   StringTableEntry data = getValue();
+   setData(data);
+}
+
+DefineEngineMethod(GuiInspectorTypeGameModeList, applyValue, void, (), , "")
+{
+   object->updateData();
+}
+
+GuiInspectorTypeGameModeListHelper::GuiInspectorTypeGameModeListHelper()
+   : mButton(NULL),
+   mParentRollout(NULL),
+   mParentField(NULL)
+{
+}
+
+IMPLEMENT_CONOBJECT(GuiInspectorTypeGameModeListHelper);
+
+ConsoleDocClass(GuiInspectorTypeGameModeListHelper,
+   "@brief Inspector field type support for GameModes lists.\n\n"
+   "Editor use only.\n\n"
+   "@internal"
+);
+
+GuiControl* GuiInspectorTypeGameModeListHelper::constructEditControl()
+{
+   GuiControl* retCtrl = new GuiTextEditCtrl();
+   retCtrl->setDataField(StringTable->insert("profile"), NULL, "GuiInspectorTextEditProfile");
+   retCtrl->setField("hexDisplay", "true");
+
+   _registerEditControl(retCtrl);
+
+   char szBuffer[512];
+   dSprintf(szBuffer, 512, "%d.apply(%d.getText());", mParentField->getId(), retCtrl->getId());
+   retCtrl->setField("AltCommand", szBuffer);
+   retCtrl->setField("Validate", szBuffer);
+
+   mButton = new GuiBitmapButtonCtrl();
+
+   RectI browseRect(Point2I((getLeft() + getWidth()) - 26, getTop() + 2), Point2I(20, getHeight() - 4));
+   dSprintf(szBuffer, 512, "%d.toggleExpanded(false);", mParentRollout->getId());
+   mButton->setField("Command", szBuffer);
+   mButton->setField("buttonType", "ToggleButton");
+   mButton->setDataField(StringTable->insert("Profile"), NULL, "GuiInspectorButtonProfile");
+   mButton->setBitmap(StringTable->insert("ToolsModule:arrowBtn_N_image"));
+   mButton->setStateOn(true);
+   mButton->setExtent(16, 16);
+   mButton->registerObject();
+   addObject(mButton);
+
+   mButton->resize(browseRect.point, browseRect.extent);
+
+   return retCtrl;
+}
+
+bool GuiInspectorTypeGameModeListHelper::resize(const Point2I& newPosition, const Point2I& newExtent)
+{
+   if (!Parent::resize(newPosition, newExtent))
+      return false;
+
+   if (mEdit != NULL)
+   {
+      return updateRects();
+   }
+
+   return false;
+}
+
+bool GuiInspectorTypeGameModeListHelper::updateRects()
+{
+   S32 dividerPos, dividerMargin;
+   mInspector->getDivider(dividerPos, dividerMargin);
+   Point2I fieldExtent = getExtent();
+   Point2I fieldPos = getPosition();
+
+   mCaptionRect.set(0, 0, fieldExtent.x - dividerPos - dividerMargin, fieldExtent.y);
+   mEditCtrlRect.set(fieldExtent.x - dividerPos + dividerMargin, 1, dividerPos - dividerMargin - 32, fieldExtent.y);
+
+   bool editResize = mEdit->resize(mEditCtrlRect.point, mEditCtrlRect.extent);
+   bool buttonResize = false;
+
+   if (mButton != NULL)
+   {
+      mButtonRect.set(fieldExtent.x - 26, 2, 16, 16);
+      buttonResize = mButton->resize(mButtonRect.point, mButtonRect.extent);
+   }
+
+   return (editResize || buttonResize);
+}
+
+void GuiInspectorTypeGameModeListHelper::setValue(StringTableEntry newValue)
+{
+   GuiTextEditCtrl* edit = dynamic_cast<GuiTextEditCtrl*>(mEdit);
+   edit->setText(newValue);
+}
+#endif

+ 110 - 0
Engine/source/T3D/gameMode.h

@@ -0,0 +1,110 @@
+#pragma once
+#ifndef GAME_MODE_H
+#define GAME_MODE_H
+
+#ifdef TORQUE_TOOLS
+#ifndef _GUI_INSPECTOR_TYPES_H_
+#include "gui/editor/guiInspectorTypes.h"
+#endif
+#endif
+
+
+#include "T3D/assets/ImageAsset.h"
+
+class GameMode : public SimObject
+{
+   typedef SimObject Parent;
+private:
+   StringTableEntry mGameModeName;
+   StringTableEntry mGameModeDesc;
+
+   DECLARE_IMAGEASSET(GameMode, PreviewImage, previewChange, GFXStaticTextureSRGBProfile);
+   DECLARE_ASSET_SETGET(GameMode, PreviewImage);
+
+   bool mIsActive;
+
+public:
+
+   GameMode();
+   ~GameMode() = default;
+   static void initPersistFields();
+   bool onAdd() override;
+   void onRemove() override;
+
+   bool isActive() { return mIsActive; }
+   void setActive(const bool& active);
+
+   DECLARE_CONOBJECT(GameMode);
+
+   static void findGameModes(const char* gameModeList, Vector<GameMode*>* outGameModes);
+
+   void previewChange() {}
+
+   DECLARE_CALLBACK(void, onActivated, ());
+   DECLARE_CALLBACK(void, onDeactivated, ());
+   DECLARE_CALLBACK(void, onSceneLoaded, ());
+   DECLARE_CALLBACK(void, onSceneUnloaded, ());
+   DECLARE_CALLBACK(void, onSubsceneLoaded, ());
+   DECLARE_CALLBACK(void, onSubsceneUnloaded, ());
+};
+
+DefineConsoleType(TypeGameModeList, String)
+
+#ifdef TORQUE_TOOLS
+class GuiInspectorTypeGameModeListHelper;
+
+class GuiInspectorTypeGameModeList : public GuiInspectorField
+{
+   typedef GuiInspectorField Parent;
+public:
+
+   GuiInspectorTypeGameModeListHelper* mHelper;
+   GuiRolloutCtrl* mRollout;
+   GuiDynamicCtrlArrayControl* mArrayCtrl;
+   Vector<GuiInspectorField*> mChildren;
+
+   GuiBitmapButtonCtrl* mButton;
+   RectI mButtonRect;
+
+   DECLARE_CONOBJECT(GuiInspectorTypeGameModeList);
+
+   GuiInspectorTypeGameModeList();
+
+   // ConsoleObject
+   bool onAdd() override;
+   static void consoleInit();
+
+   // GuiInspectorField
+   bool resize(const Point2I& newPosition, const Point2I& newExtent) override;
+   void childResized(GuiControl* child) override;
+   bool updateRects() override;
+   void updateData() override;
+   StringTableEntry getValue() override;
+   void setValue(StringTableEntry value) override;
+};
+
+class GuiInspectorTypeGameModeListHelper : public GuiInspectorField
+{
+   typedef GuiInspectorField Parent;
+
+public:
+
+   GuiInspectorTypeGameModeListHelper();
+
+   DECLARE_CONOBJECT(GuiInspectorTypeGameModeListHelper);
+
+   GuiBitmapButtonCtrl* mButton;
+   GuiRolloutCtrl* mParentRollout;
+   GuiInspectorTypeGameModeList* mParentField;
+   RectI mButtonRect;
+
+   //-----------------------------------------------------------------------------
+   // Override able methods for custom edit fields
+   //-----------------------------------------------------------------------------
+   GuiControl* constructEditControl() override;
+   bool               resize(const Point2I& newPosition, const Point2I& newExtent) override;
+   bool               updateRects() override;
+   void               setValue(StringTableEntry value) override;
+};
+#endif
+#endif

+ 7 - 0
Engine/source/gui/core/guiControl.h

@@ -372,6 +372,13 @@ class GuiControl : public SimGroup
       
       inline const S32        getHorizSizing() const { return mHorizSizing; }
       inline const S32        getVertSizing() const { return mVertSizing; }
+
+      void        setHorizSizing(horizSizingOptions horizSizing) {
+         mHorizSizing = horizSizing;
+      }
+      void        setVertSizing(vertSizingOptions vertSizing) {
+         mVertSizing = vertSizing;
+      }
       
       /// @}
       

+ 5 - 0
Engine/source/scene/sceneObject.cpp

@@ -2013,3 +2013,8 @@ void SceneObject::onNewParent(SceneObject *newParent) { if (isServerObject()) on
 void SceneObject::onLostParent(SceneObject *oldParent) { if (isServerObject()) onLostParent_callback(oldParent); }
 void SceneObject::onNewChild(SceneObject *newKid) { if (isServerObject()) onNewChild_callback(newKid); }
 void SceneObject::onLostChild(SceneObject *lostKid) { if (isServerObject()) onLostChild_callback(lostKid); }
+
+IMPLEMENT_CALLBACK(SceneObject, onSaving, void, (const char* fileName), (fileName),
+   "@brief Called when a saving is occuring to allow objects to special-handle prepwork for saving if required.\n\n"
+
+   "@param fileName The level file being saved\n");

+ 2 - 0
Engine/source/scene/sceneObject.h

@@ -913,6 +913,8 @@ class SceneObject : public NetObject, public ProcessObject
    DECLARE_CALLBACK(void, onLostChild, (SceneObject *subObject));
 // PATHSHAPE END
 
+   DECLARE_CALLBACK(void, onSaving, (const char* fileName));
+
    virtual void getUtilizedAssets(Vector<StringTableEntry>* usedAssetsList) {}
 };
 

+ 15 - 5
Templates/BaseGame/game/core/utility/scripts/helperFunctions.tscript

@@ -325,6 +325,8 @@ function replaceInFile(%fileName, %fromWord, %toWord)
    //Go through our scriptfile and replace the old namespace with the new
    %editedFileContents = "";
    
+   %lineArray = new ArrayObject(){};
+   
    %file = new FileObject();
    if ( %file.openForRead( %fileName ) ) 
    {
@@ -334,6 +336,8 @@ function replaceInFile(%fileName, %fromWord, %toWord)
          %line = trim( %line );
          
          %editedFileContents = %editedFileContents @ strreplace(%line, %fromWord, %toWord) @ "\n";
+         
+         %lineArray.add(strreplace(%line, %fromWord, %toWord));
       }
       
       %file.close();
@@ -341,12 +345,18 @@ function replaceInFile(%fileName, %fromWord, %toWord)
    
    if(%editedFileContents !$= "")
    {
-      %file.openForWrite(%fileName);
-      
-      %file.writeline(%editedFileContents);
-      
-      %file.close();
+      if( %file.openForWrite(%fileName) )
+      {
+         for(%i=0; %i < %lineArray.getCount(); %i++)
+         {
+            %file.writeline(%lineArray.getKey(%i));
+         }
+         
+         %file.close();
+      }
    }
+   
+   %lineArray.delete();
 }
 
 //------------------------------------------------------------------------------

+ 2 - 13
Templates/BaseGame/game/data/ExampleModule/ExampleModule.tscript

@@ -15,19 +15,6 @@ function ExampleModule::initServer(%this)
 //This is called when the server is created for an actual game/map to be played
 function ExampleModule::onCreateGameServer(%this)
 {
-    //These are common managed data files. For any datablock-based stuff that gets generated by the editors
-    //(that doesn't have a specific associated file, like data for a player class) will go into these.
-    //So we'll register them now if they exist.
-    if(isFile("./scripts/managedData/managedDatablocks." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedDatablocks");
-    if(isFile("./scripts/managedData/managedForestItemData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedForestItemData");
-    if(isFile("./scripts/managedData/managedForestBrushData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedForestBrushData");
-    if(isFile("./scripts/managedData/managedParticleData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedParticleData");
-    if(isFile("./scripts/managedData/managedParticleEmitterData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedParticleEmitterData");
 }
 
 //This is called when the server is shut down due to the game/map being exited
@@ -46,6 +33,8 @@ function ExampleModule::initClient(%this)
    %prefPath = getPrefpath();
    if(isScriptFile(%prefPath @ "/keybinds"))
       exec(%prefPath @ "/keybinds");
+      
+   %this.queueExec("./scripts/server/ExampleGameMode");
 }
 
 //This is called when a client connects to a server

+ 1 - 0
Templates/BaseGame/game/data/ExampleModule/levels/ExampleLevel.asset.taml

@@ -8,4 +8,5 @@
     ForestFile="@assetFile=ExampleLevel.forest"
     NavmeshFile="@assetFile=ExampleLevel.nav"
     staticObjectAssetDependency0="@asset=Prototyping:FloorGray"
+    gameModesNames="ExampleGameMode;"
     VersionId="1"/>

+ 26 - 8
Templates/BaseGame/game/data/ExampleModule/scripts/server/ExampleGameMode.tscript

@@ -1,11 +1,5 @@
-function ExampleGameMode::onCreateGame()
-{
-   // Note: The Game object will be cleaned up by MissionCleanup.  Therefore its lifetime is
-   // limited to that of the mission.
-   new ScriptObject(ExampleGameMode){};
-
-   return ExampleGameMode;
-}
+if(!isObject(ExampleGameMode))
+   new GameMode(ExampleGameMode){};
 
 //-----------------------------------------------------------------------------
 // The server has started up so do some game start up
@@ -159,6 +153,30 @@ function ExampleGameMode::onInitialControlSet(%this)
    
 }
 
+function ExampleGameMode::onSceneLoaded(%this)
+{
+   echo("===================================");
+   echo("ExampleGameMode - Scene is loaded");  
+}
+
+function ExampleGameMode::onSceneUnloaded(%this)
+{
+   echo("===================================");
+   echo("ExampleGameMode - Scene is unloaded");  
+}
+
+function ExampleGameMode::onSubsceneLoaded(%this)
+{
+   echo("===================================");
+   echo("ExampleGameMode - Subscene is loaded");  
+}
+
+function ExampleGameMode::onSubsceneUnloaded(%this)
+{
+   echo("===================================");
+   echo("ExampleGameMode - Subscene is unloaded");  
+}
+
 function ExampleGameMode::spawnCamera(%this, %client, %spawnPoint)
 {
    // Set the control object to the default camera

+ 5 - 0
Templates/BaseGame/game/data/UI/UI.tscript

@@ -99,4 +99,9 @@ function UI::onDestroyClientConnection(%this)
 function UI::registerGameMenus(%this, %menusArrayObj)
 {
    %menusArrayObj.add("System", SystemMenu);
+}
+
+function listLevelsAndGameModes()
+{
+   
 }

+ 76 - 15
Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.gui

@@ -9,6 +9,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
    tooltipProfile = "GuiToolTipProfile";
    isContainer = "1";
    canSaveDynamicFields = "1";
+      currentMenuIdx = "0";
       launchInEditor = "0";
       previewButtonSize = "445 120";
 
@@ -39,31 +40,42 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
    new GuiStackControl(ChooseLevelMenuTabList) {
       stackingType = "Horizontal";
       padding = "10";
-      position = "485 61";
-      extent = "310 41";
+      position = "405 61";
+      extent = "470 41";
       horizSizing = "center";
       profile = "GuiDefaultProfile";
-      visible = "0";
       tooltipProfile = "GuiToolTipProfile";
-      hidden = "1";
 
       new GuiButtonCtrl() {
-         text = "Level";
+         text = "Game Mode";
          groupNum = "1";
          extent = "150 41";
          profile = "GuiMenuButtonProfile";
          command = "ChooseLevelMenu.openMenu(0);";
          tooltipProfile = "GuiToolTipProfile";
+         internalName = "GameModeBtn";
          class = "ChooseLevelMenuButton";
       };
       new GuiButtonCtrl() {
-         text = "Server Config";
+         text = "Level";
          groupNum = "1";
          position = "160 0";
          extent = "150 41";
          profile = "GuiMenuButtonProfile";
          command = "ChooseLevelMenu.openMenu(1);";
          tooltipProfile = "GuiToolTipProfile";
+         internalName = "LevelBtn";
+         class = "ChooseLevelMenuButton";
+      };
+      new GuiButtonCtrl() {
+         text = "Server Config";
+         groupNum = "1";
+         position = "320 0";
+         extent = "150 41";
+         profile = "GuiMenuButtonProfile";
+         command = "ChooseLevelMenu.openMenu(2);";
+         tooltipProfile = "GuiToolTipProfile";
+         internalName = "ConfigBtn";
          class = "ChooseLevelMenuButton";
       };
    };
@@ -72,14 +84,12 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
       extent = "1281 60";
       horizSizing = "width";
       profile = "GuiNonModalDefaultProfile";
-      visible = "0";
       tooltipProfile = "GuiToolTipProfile";
       isContainer = "1";
-      hidden = "1";
 
       new GuiBitmapCtrl(ChooseLevelMenuPrevNavIcon) {
          BitmapAsset = "UI:Keyboard_Black_Q_image";
-         position = "485 24";
+         position = "405 24";
          extent = "40 40";
          vertSizing = "top";
          profile = "GuiNonModalDefaultProfile";
@@ -87,19 +97,72 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
       };
       new GuiBitmapCtrl(ChooseLevelMenuNextNavIcon) {
          BitmapAsset = "UI:Keyboard_Black_E_image";
-         position = "595 24";
+         position = "515 24";
          extent = "40 40";
          vertSizing = "top";
          profile = "GuiNonModalDefaultProfile";
          tooltipProfile = "GuiToolTipProfile";
       };
    };
+   new GuiContainer(GameModeSelectContainer) {
+      position = "196 119";
+      extent = "888 566";
+      horizSizing = "center";
+      profile = "GuiNonModalDefaultProfile";
+      tooltipProfile = "GuiToolTipProfile";
+
+      new GuiScrollCtrl(GameModePreviewScroll) {
+         hScrollBar = "alwaysOff";
+         vScrollBar = "dynamic";
+         extent = "445 562";
+         vertSizing = "height";
+         profile = "GuiMenuScrollProfile";
+         tooltipProfile = "GuiToolTipProfile";
+
+         new GuiStackControl(GameModePreviewArray) {
+            padding = "5";
+            position = "0 1";
+            extent = "445 120";
+            horizSizing = "width";
+            vertSizing = "height";
+            profile = "GuiMenuDefaultProfile";
+            tooltipProfile = "GuiToolTipProfile";
+         };
+      };
+      new GuiBitmapCtrl(GameModePreviewBitmap) {
+         BitmapAsset = "UI:no_preview_image";
+         position = "448 0";
+         extent = "440 440";
+         horizSizing = "left";
+         profile = "GuiNonModalDefaultProfile";
+         tooltipProfile = "GuiToolTipProfile";
+      };
+      new GuiTextCtrl(GameModeNameText) {
+         text = "Example Level";
+         position = "448 445";
+         extent = "440 20";
+         horizSizing = "left";
+         profile = "MenuSubHeaderText";
+         tooltipProfile = "GuiToolTipProfile";
+         internalName = "GameModeNameTxt";
+      };
+      new GuiMLTextCtrl(GameModeDescriptionText) {
+         position = "448 473";
+         extent = "440 19";
+         horizSizing = "left";
+         profile = "GuiMLTextProfile";
+         tooltipProfile = "GuiToolTipProfile";
+         internalName = "GameModeDescTxt";
+      };
+   };
    new GuiContainer(LevelSelectContainer) {
       position = "196 119";
       extent = "888 566";
       horizSizing = "center";
       profile = "GuiNonModalDefaultProfile";
+      visible = "0";
       tooltipProfile = "GuiToolTipProfile";
+      hidden = "1";
 
       new GuiScrollCtrl(LevelPreviewScroll) {
          hScrollBar = "alwaysOff";
@@ -120,7 +183,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
          };
       };
       new GuiBitmapCtrl(LevelPreviewBitmap) {
-         BitmapAsset = "";
+         BitmapAsset = "UI:no_preview_image";
          position = "448 0";
          extent = "440 440";
          horizSizing = "left";
@@ -128,7 +191,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
          tooltipProfile = "GuiToolTipProfile";
       };
       new GuiTextCtrl(LevelNameText) {
-         text = "";
+         text = "Example Level";
          position = "448 445";
          extent = "440 20";
          horizSizing = "left";
@@ -214,7 +277,6 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
                   tooltipProfile = "GuiToolTipProfile";
                };
                new GuiTextEditCtrl(serverNameCTRL) {
-                  text = "";
                   position = "606 4";
                   extent = "295 22";
                   horizSizing = "left";
@@ -240,7 +302,6 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
                   tooltipProfile = "GuiToolTipProfile";
                };
                new GuiTextEditCtrl(serverPassCTRL) {
-                  text = "";
                   position = "606 4";
                   extent = "295 22";
                   horizSizing = "left";
@@ -314,7 +375,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
       tooltipProfile = "GuiToolTipProfile";
 
       new GuiIconButtonCtrl(ChooseLevelStartBtn) {
-         BitmapAsset = "UI:Keyboard_Black_Return_image";
+         BitmapAsset = "UI:Keyboard_Black_Space_image";
          sizeIconToButton = "1";
          makeIconSquare = "1";
          textLocation = "Center";

+ 193 - 51
Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.tscript

@@ -42,6 +42,70 @@ function ChooseLevelMenu::onWake(%this)
    %this.fillPrefEntries();
    LevelPreviewArray.clear();
    
+   refreshGameModesList();
+
+   if(!$pref::HostMultiPlayer)
+      ChooseLevelTitleText.setText("SINGLE PLAYER");
+   else
+      ChooseLevelTitleText.setText("CREATE SERVER");
+      
+   ChooseLevelMenuTabList-->ConfigBtn.visible = $pref::HostMultiPlayer;
+      
+   ChooseLevelMenuTabList.visible = true;//$pref::HostMultiPlayer;
+   ChooseLevelMenuNavButtonOverlay.visible = true;//$pref::HostMultiPlayer;
+      
+   %this.schedule(32, openMenu, 0);
+}
+
+function refreshGameModesList()
+{
+   GameModePreviewArray.clear();
+   
+   $pref::Server::GameMode = "";
+   ChooseLevelMenuTabList-->LevelBtn.active = false;
+   
+   //popilate the gamemodes list first
+   %gamemodeList = getGameModesList();
+   for(%i=0; %i < getTokenCount(%gamemodeList, ";"); %i++)
+   {
+      %gameModeObj = getToken(%gamemodeList, ";", %i);
+      
+      if(isObject(%gameModeObj))
+      {
+         %gameModeName = %gameModeObj.gameModeName;
+         if(%gameModeName $= "")
+            %gameModeName = %gameModeObj.getName();
+            
+         %preview = new GuiButtonCtrl() {
+            position = "0 0";
+            extent = ChooseLevelMenu.previewButtonSize;
+            buttonType = "PushButton";
+            profile = GuiMenuButtonLeftJustProfile;
+            horizSizing = "right";
+            vertSizing = "bottom";
+            internalName = "button";
+            class = "GameModePreviewButton";
+            //command = "ChooseGameModeBegin(" @ %i @ ");";
+            altCommand = "ChooseGameModeBegin(" @ %i @ ");"; //allow doubleclick to quick action it
+            text = %gameModeName;
+            gameModeObj = %gameModeObj;
+            gameModeDesc = %gameModeObj.description;
+            previewImage = %gameModeObj.previewImage;
+            textMargin = 120;
+            groupNum = 2;
+            cansave = false;
+         };
+         
+         GameModePreviewArray.add(%preview);
+      }
+   }
+}
+
+function refreshLevelsList()
+{
+   LevelPreviewArray.clear();
+   
+   //fetch the levelAssets   
    ChooseLevelAssetQuery.clear();
    AssetDatabase.findAssetType(ChooseLevelAssetQuery, "LevelAsset");
       
@@ -57,6 +121,7 @@ function ChooseLevelMenu::onWake(%this)
       return;
    }
    
+   //filter the levelAssets by the gamemode selected
    for(%i=0; %i < %count; %i++)
 	{
 	   %assetId = ChooseLevelAssetQuery.getAsset(%i);
@@ -71,6 +136,32 @@ function ChooseLevelMenu::onWake(%this)
       if ( !isFile(%file) )
          continue;
          
+      if( %levelAsset.isSubScene )
+         continue;
+         
+      //filter for selected gamemode
+      %levelGameModes = %levelAsset.gameModesNames;
+      
+      echo("LevelAsset gamemodes: " @ %levelGameModes);
+      
+      %foundGameModeMatch = false;
+      for(%gm = 0; %gm < getTokenCount(%levelGameModes, ";"); %gm++)
+      {
+         %gameModeName = getToken(%levelGameModes, ";", %gm);
+         
+         echo("testing gamemode: " @ %gameModeName);
+         echo("selected gamemode: " @ $pref::Server::GameMode.getName());
+         
+         if(%gameModeName $= $pref::Server::GameMode.getName())
+         {
+            %foundGameModeMatch = true;
+            break;
+         }
+      }
+      
+      if(!%foundGameModeMatch)
+         continue;
+         
       %levelPreviewImg = %levelAsset.getPreviewImagePath();
    
       if (!isFile(%levelPreviewImg))
@@ -78,7 +169,7 @@ function ChooseLevelMenu::onWake(%this)
          
       %preview = new GuiIconButtonCtrl() {
          position = "0 0";
-         extent = %this.previewButtonSize;
+         extent = ChooseLevelMenu.previewButtonSize;
          buttonType = "PushButton";
          profile = GuiMenuButtonLeftJustProfile;
          horizSizing = "right";
@@ -86,7 +177,7 @@ function ChooseLevelMenu::onWake(%this)
          internalName = "button";
          class = "LevelPreviewButton";
          //command = "$selectedLevelAsset = " @ %assetId @ ";";
-         altCommand = "ChooseLevelBegin(1);"; //allow doubleclick to quick action it
+         altCommand = "ChooseLevelBegin(" @ %i @ ");"; //allow doubleclick to quick action it
          text = %levelAsset.levelName;
          bitmapAsset = %levelPreviewImg;
          levelAsset = %levelAsset;
@@ -107,20 +198,10 @@ function ChooseLevelMenu::onWake(%this)
    
    // Also add the new level mission as defined in the world editor settings
    // if we are choosing a level to launch in the editor.
-   if ( %this.launchInEditor )
+   if ( ChooseLevelMenu.launchInEditor )
    {
-      %this.addMissionFile( "tools/levels/DefaultEditorLevel.mis" );
+      ChooseLevelMenu.addMissionFile( "tools/levels/DefaultEditorLevel.mis" );
    }
-
-   if(!$pref::HostMultiPlayer)
-      ChooseLevelTitleText.setText("SINGLE PLAYER");
-   else
-      ChooseLevelTitleText.setText("CREATE SERVER");
-      
-   ChooseLevelMenuTabList.visible = $pref::HostMultiPlayer;
-   ChooseLevelMenuNavButtonOverlay.visible = $pref::HostMultiPlayer;
-      
-   %this.schedule(32, openMenu, 0);
 }
 
 if(!isObject( ChooseLevelActionMap ) )
@@ -133,8 +214,8 @@ if(!isObject( ChooseLevelActionMap ) )
    ChooseLevelActionMap.bind( keyboard, e, ChooseLevelMenuNextMenu);
    ChooseLevelActionMap.bind( gamepad, btn_r, ChooseLevelMenuNextMenu);
    
-   ChooseLevelActionMap.bind( keyboard, Space, ChooseLevelBegin );
-   ChooseLevelActionMap.bind( gamepad, btn_a, ChooseLevelBegin );
+   ChooseLevelActionMap.bind( keyboard, Space, ChooseLevelMenuOption );
+   ChooseLevelActionMap.bind( gamepad, btn_a, ChooseLevelMenuOption );
 }
 
 function ChooseLevelMenu::syncGUI(%this)
@@ -169,14 +250,26 @@ function LevelPreviewArray::syncGUI(%this)
    $selectedLevelAsset = %btn.levelAssetId;
 }
 
+function GameModePreviewArray::syncGUI(%this)
+{
+   %btn = %this.getObject(%this.listPosition);
+   %btn.setHighlighted(true);
+   
+   $pref::Server::GameMode = %btn.gameModeObject;
+}
+
 function ChooseLevelMenuPrevMenu(%val)
 {
-   if(%val && $pref::HostMultiPlayer)
+   if(%val)
    {
       %currentIdx = ChooseLevelMenu.currentMenuIdx;
       ChooseLevelMenu.currentMenuIdx -= 1;
       
-      ChooseLevelMenu.currentMenuIdx = mClamp(ChooseLevelMenu.currentMenuIdx, 0, 1);
+      %endIndex = 1;
+      if($pref::HostMultiPlayer)
+         %endIndex = 2;
+      
+      ChooseLevelMenu.currentMenuIdx = mClamp(ChooseLevelMenu.currentMenuIdx, 0, %endIndex);
     
       if(%currentIdx == ChooseLevelMenu.currentMenuIdx)
          return;
@@ -187,12 +280,16 @@ function ChooseLevelMenuPrevMenu(%val)
 
 function ChooseLevelMenuNextMenu(%val)
 {
-   if(%val && $pref::HostMultiPlayer)
+   if(%val)
    {
       %currentIdx = ChooseLevelMenu.currentMenuIdx;
       ChooseLevelMenu.currentMenuIdx += 1;
       
-      ChooseLevelMenu.currentMenuIdx = mClamp(ChooseLevelMenu.currentMenuIdx, 0, 1);
+      %endIndex = 1;
+      if($pref::HostMultiPlayer)
+         %endIndex = 2;
+      
+      ChooseLevelMenu.currentMenuIdx = mClamp(ChooseLevelMenu.currentMenuIdx, 0, %endIndex);
     
       if(%currentIdx == ChooseLevelMenu.currentMenuIdx)
          return;
@@ -201,15 +298,17 @@ function ChooseLevelMenuNextMenu(%val)
    }
 }
 
-
 function ChooseLevelMenu::openMenu(%this, %menuIdx)
 {
-   LevelSelectContainer.setVisible(%menuIdx == 0);
-   ServerConfigContainer.setVisible(%menuIdx == 1);
-   
+   GameModeSelectContainer.setVisible(%menuIdx == 0);
+   LevelSelectContainer.setVisible(%menuIdx == 1);
+   ServerConfigContainer.setVisible(%menuIdx == 2);
+
    if(%menuIdx == 0)
-      $MenuList = LevelPreviewArray;
+      $MenuList = GameModePreviewArray;
    else if(%menuIdx == 1)
+      $MenuList = LevelPreviewArray;
+   else if(%menuIdx == 2)
       $MenuList = ServerConfigList;
    
     %this.currentMenuIdx = %menuIdx;
@@ -220,37 +319,63 @@ function ChooseLevelMenu::openMenu(%this, %menuIdx)
    %this.syncGui();
 }
 
-function ChooseLevelBegin(%val)
+function ChooseLevelMenuOption(%val)
 {
    if(%val)
    {
-      // So we can't fire the button when loading is in progress.
-      if ( isObject( ServerGroup ) )
-         return;
-         
-      Canvas.popDialog();
-      
-      %entry = LevelPreviewArray.getObject(LevelPreviewArray.listPosition);
-      
-      if(!AssetDatabase.isDeclaredAsset(%entry.levelAssetId))
-      {
-         MessageBoxOK("Error", "Selected level preview does not have a valid level asset!");
-         return;  
-      }
+      if(ChooseLevelMenu.currentMenuIdx == 0)
+         ChooseGameModeBegin(ChooseLevelMenu.listPosition);
+      else if(ChooseLevelMenu.currentMenuIdx == 1)
+         ChooseLevelBegin(ChooseLevelMenu.listPosition);
+   }
+}
+
+function ChooseGameModeBegin(%index)
+{
+   %entry = GameModePreviewArray.getObject(%index);
+   if(!isObject(%entry) || !isObject(%entry.gameModeObj))
+   {
+      MessageBoxOK("Error", "Selected game mode does not have a valid mode");
+      return; 
+   }
+
+   $pref::Server::GameMode = %entry.gameModeObj;
+
+   ChooseLevelMenuTabList-->LevelBtn.active = true;
+   
+   refreshLevelsList();
+   
+   ChooseLevelMenu.openMenu(1);
+}
+
+function ChooseLevelBegin(%index)
+{
+   // So we can't fire the button when loading is in progress.
+   if ( isObject( ServerGroup ) )
+      return;
       
-      $selectedLevelAsset = %entry.levelAssetId;
+   Canvas.popDialog();
+   
+   %entry = LevelPreviewArray.getObject(%index);
+   
+   if(!AssetDatabase.isDeclaredAsset(%entry.levelAssetId))
+   {
+      MessageBoxOK("Error", "Selected level preview does not have a valid level asset!");
+      return;  
+   }
+   
+   $selectedLevelAsset = %entry.levelAssetId;
 
-      // Launch the chosen level with the editor open?
-      if ( ChooseLevelMenu.launchInEditor )
-      {
-         activatePackage( "BootEditor" );
-         ChooseLevelMenu.launchInEditor = false; 
-         StartGame(%entry.levelAssetId, "SinglePlayer");
-      }
-      else
-      {
-         StartGame(%entry.levelAssetId); 
-      }
+   // Launch the chosen level with the editor open?
+   if ( ChooseLevelMenu.launchInEditor )
+   {
+      activatePackage( "BootEditor" );
+      ChooseLevelMenu.launchInEditor = false; 
+      StartGame(%entry.levelAssetId, "SinglePlayer");
+   }
+   else
+   {
+      StartGame(%entry.levelAssetId); 
    }
 }
 
@@ -270,6 +395,23 @@ function ChooseLevelMenu::onSleep( %this )
    }
 }
 
+function GameModePreviewButton::onHighlighted(%this, %highlighted)
+{
+   if(%highlighted)
+   {
+      $MenuList.listPosition = $MenuList.getObjectIndex(%this);
+      
+      GameModePreviewBitmap.bitmapAsset = %this.previewImage;
+      
+      GameModePreviewBitmap.visible = (%this.previewImage !$= "");
+      
+      GameModeNameText.text = %this.text;
+      GameModeDescriptionText.setText(%this.gameModeDesc);
+      
+      GameModePreviewScroll.scrollToObject(%this);
+   }
+}
+
 function LevelPreviewButton::onHighlighted(%this, %highlighted)
 {
    if(%highlighted)

+ 2 - 0
Templates/BaseGame/game/tools/assetBrowser/main.tscript

@@ -87,6 +87,7 @@ function initializeAssetBrowser()
    exec("./scripts/looseFileAudit." @ $TorqueScriptFileExtension);
    exec("./scripts/creator." @ $TorqueScriptFileExtension);
    exec("./scripts/setAssetTarget." @ $TorqueScriptFileExtension);
+   exec("./scripts/utils." @ $TorqueScriptFileExtension);
    
    //Processing for the different asset types
    exec("./scripts/assetTypes/component." @ $TorqueScriptFileExtension); 
@@ -110,6 +111,7 @@ function initializeAssetBrowser()
    exec("./scripts/assetTypes/looseFiles." @ $TorqueScriptFileExtension); 
    exec("./scripts/assetTypes/prefab." @ $TorqueScriptFileExtension); 
    exec("./scripts/assetTypes/creatorObj." @ $TorqueScriptFileExtension); 
+   exec("./scripts/assetTypes/gameMode." @ $TorqueScriptFileExtension); 
    
    new ScriptObject( AssetBrowserPlugin )
    {

+ 0 - 1
Templates/BaseGame/game/tools/assetBrowser/scripts/addModuleWindow.tscript

@@ -79,7 +79,6 @@ function AssetBrowser_addModuleWindow::CreateNewModule(%this)
          %line = strreplace( %line, "@@", %newModuleName );
          
          %file.writeline(%line);
-         echo(%line);
       }
       
       %file.close();

+ 96 - 0
Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/gameMode.tscript

@@ -0,0 +1,96 @@
+function AssetBrowser::createGameMode(%this)
+{
+   %moduleName = AssetBrowser.newAssetSettings.moduleName;
+   %moduleDef = ModuleDatabase.findModule(%moduleName, 1);
+      
+   %assetName = AssetBrowser.newAssetSettings.assetName;   
+   %assetPath = NewAssetTargetAddress.getText() @ "/";
+   
+   %scriptPath = %assetPath @ %assetName @ "." @ $TorqueScriptFileExtension;
+   %fullScriptPath = makeFullPath(%scriptPath);
+
+   %file = new FileObject();
+	%templateFile = new FileObject();
+	
+   %postFXTemplateCodeFilePath = %this.templateFilesPath @ "gameMode." @ $TorqueScriptFileExtension @ ".template";
+   
+   if(%file.openForWrite(%fullScriptPath) && %templateFile.openForRead(%postFXTemplateCodeFilePath))
+   {
+      while( !%templateFile.isEOF() )
+      {
+         %line = %templateFile.readline();
+         %line = strreplace( %line, "@@", %assetName );
+         
+         %file.writeline(%line);
+      }
+      
+      %file.close();
+      %templateFile.close();
+   }
+   else
+   {
+      %file.close();
+      %templateFile.close();
+      
+      warnf("createGameMode - Something went wrong and we couldn't write the gameMode script file!");
+   }
+
+   %localScriptPath = strReplace(%scriptPath, "data/" @ %moduleName @ "/", "./");
+   %execLine = "   %this.queueExec(\"" @ %localScriptPath @ "\");";
+   
+   %moduleScriptPath = makeFullPath(%moduleDef.ModuleScriptFilePath @ "." @ $TorqueScriptFileExtension);
+   
+   echo("Attempting exec insert for file: " @ %moduleScriptPath);
+   
+   %lineIdx = Tools::findInFile(%moduleScriptPath, "*function*" @ %moduleName @ "::initClient*");
+   if(%lineIdx != -1)
+   {
+      echo("INIT CLIENT FUNCTION LINE FOUND ON: " @ %lineIdx);
+      
+      %insertLineIdx = Tools::findInFunction(%moduleScriptPath, %moduleName, "initClient", "*//--FILE EXEC END--*");
+      echo("FILE EXEC END LINE FOUND ON: " @ %insertLineIdx);
+      
+      if(%insertLineIdx == -1)
+      {
+         //If there are not 'blocking' comments, then just slap the exec on the end of the function def
+         //as it doesn't really matter now
+         Tools::appendLineToFunction(%moduleScriptPath, %moduleName, "initClient", %execLine);
+      }
+      else
+      {
+         Tools::insertInFile(%moduleScriptPath, %insertLineIdx, %execLine, true);
+      }
+   }
+   
+   %lineIdx = Tools::findInFile(%moduleScriptPath, "*function*" @ %moduleName @ "::initServer*");
+   if(%lineIdx != -1)
+   {
+      echo("INIT SERVER FUNCTION LINE FOUND ON: " @ %lineIdx);
+      
+      %insertLineIdx = Tools::findInFunction(%moduleScriptPath, %moduleName, "initServer", "*//--FILE EXEC END--*");
+      echo("FILE EXEC END LINE FOUND ON: " @ %insertLineIdx);
+      
+      if(%insertLineIdx == -1)
+      {
+         //If there are not 'blocking' comments, then just slap the exec on the end of the function def
+         //as it doesn't really matter now
+         Tools::appendLineToFunction(%moduleScriptPath, %moduleName, "initServer", %execLine);
+      }
+      else
+      {
+         Tools::insertInFile(%moduleScriptPath, %insertLineIdx, %execLine, true);
+      }
+   }
+   
+   //and we'll go ahead and force execute the script file so the gamemode is 'available' for use immediately
+   exec(%scriptPath);
+   
+   if(isObject(%assetName))
+   {
+      //it's possible it got moved to an instant group upon execution, so we'll just 
+      //shove it back to the RootGroup by force to be 100% sure
+      RootGroup.add(%assetName); 
+   }
+   
+	return %scriptPath;
+}

+ 121 - 3
Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/level.tscript

@@ -3,6 +3,8 @@ function AssetBrowser::setupCreateNewLevelAsset(%this)
    NewAssetPropertiesInspector.startGroup("Level");
    NewAssetPropertiesInspector.addField("LevelName", "Level Name", "String",  "Human-readable name of new level", "", "", %this.newAssetSettings);
    NewAssetPropertiesInspector.addField("levelPreviewImage", "Level Preview Image", "Image",  "Preview Image for the level", "", "", %this.newAssetSettings);
+   
+   NewAssetPropertiesInspector.addField("isSubScene", "Is SubScene", "bool",  "Is this levelAsset intended as a subScene", "", "", %this.newAssetSettings);
 
    NewAssetPropertiesInspector.endGroup();
 }
@@ -20,14 +22,16 @@ function AssetBrowser::createLevelAsset(%this)
    
    %assetPath = NewAssetTargetAddress.getText() @ "/";
    
+   %misExtension = AssetBrowser.newAssetSettings.isSubScene ? ".subMis" : ".mis";
+   
    %tamlpath = %assetPath @ %assetName @ ".asset.taml";
-   %levelPath = %assetPath @ %assetName @ ".mis";
+   %levelPath = %assetPath @ %assetName @ %misExtension;
    
    %asset = new LevelAsset()
    {
       AssetName = %assetName;
       versionId = 1;
-      LevelFile = %assetName @ ".mis";
+      LevelFile = %assetName @ %misExtension;
       DecalsFile = %assetName @ ".mis.decals";
       PostFXPresetFile = %assetName @ ".postfxpreset." @ $TorqueScriptFileExtension;
       ForestFile = %assetName @ ".forest";
@@ -35,6 +39,7 @@ function AssetBrowser::createLevelAsset(%this)
       LevelName = AssetBrowser.newAssetSettings.levelName;
       AssetDescription = AssetBrowser.newAssetSettings.description;
       PreviewImage = AssetBrowser.newAssetSettings.levelPreviewImage;
+      isSubScene = AssetBrowser.newAssetSettings.isSubScene;
    };
    
    TamlWrite(%asset, %tamlpath);
@@ -58,15 +63,32 @@ function AssetBrowser::createLevelAsset(%this)
 
 	%moduleDef = ModuleDatabase.findModule(%moduleName, 1);
 	%addSuccess = AssetDatabase.addDeclaredAsset(%moduleDef, %tamlpath);
+	
+	if(!%addSuccess)
+	{
+	   error("AssetBrowser::createLevelAsset() - failed to add declared asset: " @ %tamlpath @ " for module: " @ %moduleDef);
+	}
 
 	AssetBrowser.refresh();
 	
 	return %tamlpath;  
 }
 
+//==============================================================================
+//SubScene derivative
+//==============================================================================
+function AssetBrowser::setupCreateNewSubScene(%this)
+{
+   %this.newAssetSettings.isSubScene = true;
+   %this.newAssetSettings.assetType = "LevelAsset";
+}
+
+//==============================================================================
+
 function AssetBrowser::editLevelAsset(%this, %assetDef)
 {
-   schedule( 1, 0, "EditorOpenMission", %assetDef);
+   if(!%assetDef.isSubScene)
+      schedule( 1, 0, "EditorOpenMission", %assetDef);
 }
 
 //Renames the asset
@@ -150,4 +172,100 @@ function AssetBrowser::buildLevelAssetPreview(%this, %assetDef, %previewData)
       "Asset Type: Level Asset\n" @ 
       "Asset Definition ID: " @ %assetDef @ "\n" @ 
       "Level File path: " @ %assetDef.getLevelPath(); 
+}
+
+function createAndAssignLevelAsset(%moduleName, %assetName)
+{
+   $createAndAssignField.apply(%moduleName @ ":" @ %assetName);
+}
+
+function EWorldEditor::createSelectedAsSubScene( %this, %object )
+{
+   //create new level asset here
+   AssetBrowser.setupCreateNewAsset("SubScene", AssetBrowser.selectedModule, "finishCreateSelectedAsSubScene");
+   %this.contextActionObject = %object;
+}
+
+function finishCreateSelectedAsSubScene(%assetId)
+{
+   echo("finishCreateSelectedAsSubScene");
+   
+   %subScene = new SubScene() {
+      levelAsset = %assetId;
+   };
+   
+   %levelAssetDef = AssetDatabase.acquireAsset(%assetId);
+   %levelFilePath = %levelAssetDef.getLevelPath();
+   
+   //write out to file
+   EWorldEditor.contextActionObject.save(%levelFilePath);
+   
+   AssetDatabase.releaseAsset(%assetId);
+   
+   getRootScene().add(%subScene);
+   
+   EWorldEditor.contextActionObject.delete();
+   
+   %subScene.load();
+   
+   %subScene.recalculateBounds();
+   
+   %scalar = $SubScene::createScalar;
+   if(%scalar $= "")
+      %scalar = 1.5;
+   
+   //pad for loading boundary
+   %subScene.scale = VectorScale(%subScene.scale, %scalar);
+}
+
+function GuiInspectorTypeLevelAssetPtr::onControlDropped( %this, %payload, %position )
+{
+   Canvas.popDialog(EditorDragAndDropLayer);
+   
+   // Make sure this is a color swatch drag operation.
+   if( !%payload.parentGroup.isInNamespaceHierarchy( "AssetPreviewControlType_AssetDrop" ) )
+      return;
+
+   %assetType = %payload.assetType;
+   %module = %payload.moduleName;
+   %assetName = %payload.assetName;
+   
+   if(%assetType $= "LevelAsset")
+   {
+      %cmd = %this @ ".apply(\""@ %module @ ":" @ %assetName @ "\");";
+      eval(%cmd);
+   }
+   
+   EWorldEditor.isDirty = true;
+}
+
+function AssetBrowser::onLevelAssetEditorDropped(%this, %assetDef, %position)
+{
+   %assetId = %assetDef.getAssetId();
+   
+   if(!%assetDef.isSubScene)
+   {
+      errorf("Cannot drag-and-drop LevelAsset into WorldEditor");
+      return;  
+   }
+   
+   %newSubScene = new SubScene()
+   {
+      position = %position;
+      levelAsset = %assetId;
+   };
+   
+   getScene(0).add(%newSubScene);
+   
+   %newSubScene.load();
+   %newSubScene.recalculateBounds();
+   
+   EWorldEditor.clearSelection();
+   EWorldEditor.selectObject(%newSubScene);
+   
+   EWorldEditor.dropSelection();
+      
+   EWorldEditor.isDirty = true;
+   
+   MECreateUndoAction::submit(%newSubScene );
 }

+ 25 - 0
Templates/BaseGame/game/tools/assetBrowser/scripts/editAsset.tscript

@@ -176,6 +176,31 @@ function AssetBrowser::performRenameAsset(%this, %originalAssetName, %newName)
             %buildCommand = %this @ ".rename" @ EditAssetPopup.assetType @ "(" @ %assetDef @ "," @ %newName @ ");";
             eval(%buildCommand);
          }
+         else
+         {
+            //do generic action here
+            %oldAssetId = %moduleName @ ":" @ %originalAssetName;
+            %assetDef = AssetDatabase.acquireAsset(%oldAssetId);
+            
+            %assetFileName = fileName(%assetDef.getFileName());
+            %assetFilePath = filePath(%assetDef.getFileName());
+            
+            %pattern = %assetFilePath @ "/" @ %assetFileName @ ".*";
+            
+            echo("Searching for matches of asset files following pattern: " @ %pattern);
+            
+            //get list of files that match the name in the same dir
+            //do a rename on them
+            for (%file = findFirstFileMultiExpr(%pattern); %file !$= ""; %file = findNextFileMultiExpr(%pattern))
+            {
+               if(%file !$= %assetFileName)
+                  renameAssetLooseFile(%file, %newName);
+            }
+            
+            //update the assetDef
+            replaceInFile(%assetDef.getFileName(), %originalAssetName, %newName);
+            renameAssetFile(%assetDef, %newName);
+         }
       }
       else
       {

+ 2 - 1
Templates/BaseGame/game/tools/assetBrowser/scripts/popupMenus.tscript

@@ -117,7 +117,8 @@ function AssetBrowser::buildPopupMenus(%this)
          //item[ 0 ] = "Create Component" TAB AddNewComponentAssetPopup;
          item[ 0 ] = "Create Script" TAB "" TAB "AssetBrowser.setupCreateNewAsset(\"ScriptAsset\", AssetBrowser.selectedModule);";
          item[ 1 ] = "Create State Machine" TAB "" TAB "AssetBrowser.setupCreateNewAsset(\"StateMachineAsset\", AssetBrowser.selectedModule);";
-         //item[ 3 ] = "-";
+         item[ 2 ] = "-";
+         item[ 3 ] = "Create Game Mode" TAB "" TAB "AssetBrowser.setupCreateNewAsset(\"GameMode\", AssetBrowser.selectedModule);";
          //item[ 3 ] = "Create Game Object" TAB "" TAB "AssetBrowser.createNewGameObjectAsset(\"NewGameObject\", AssetBrowser.selectedModule);";
       };
       //%this.AddNewScriptAssetPopup.insertSubMenu(0, "Create Component", AddNewComponentAssetPopup);

+ 176 - 0
Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/gameMode.tscript.template

@@ -0,0 +1,176 @@
+//This file implements game mode logic for an Example gamemode. The primary functions:
+//@@::onMissionStart
+//@@::onMissionReset
+//@@::onMissionEnd
+//Are the primary hooks for the server to start, restart and end any active gamemodes
+//onMissionStart, for example is called from core/clientServer/scripts/server/levelLoad.cs
+//It's called once the server has successfully loaded the level, and has parsed
+//through any active scenes to get GameModeNames defined by them. It then iterates
+//over them and calls these callbacks to envoke gamemode behaviors. This allows multiple
+//gamemodes to be in effect at one time. Modules can implement as many gamemodes as you want.
+//
+//For levels that can be reused for multiple gammodes, the general setup would be a primary level file
+//with the Scene in it having the main geometry, weapons, terrain, etc. You would then have subScenes that
+//each contain what's necessary for the given gamemode, such as a subScene that just adds the flags and capture
+//triggers for a CTF mode. The subscene would then have it's GameModeName defined to run the CTF gamemode logic
+//and the levelLoad code will execute it.
+
+if(!isObject(@@))
+{
+    new GameMode(@@){};
+}
+
+//This function is called when the level finishes loading. It sets up the initial configuration, variables and
+//spawning and dynamic objects, timers or rules needed for the gamemode to run
+function @@::onMissionStart(%this)
+{
+   //set up the game and game variables
+   %this.initGameVars();
+
+   if (%this.isActive())
+   {
+      error("@@::onMissionStart: End the game first!");
+      return;
+   }
+      
+   %this.setActive(true);
+}
+
+//This function is called when the level ends. It can be envoked due to the gamemode ending
+//but is also kicked off when the game server is shut down as a form of cleanup for anything the gamemode
+//created or is managing like the above mentioned dynamic objects or timers
+function @@::onMissionEnded(%this)
+{
+   if (!%this.isActive())
+   {
+      error("@@::onMissionEnded: No game running!");
+      return;
+   }
+
+   %this.setActive(false);
+}
+
+//This function is called in the event the server resets and is used to re-initialize the gamemode
+function @@::onMissionReset(%this)
+{
+   // Called by resetMission(), after all the temporary mission objects
+   // have been deleted.
+   %this.initGameVars();
+}
+
+//This sets up our gamemode's duration time
+function @@::initGameVars(%this)
+{
+   // Set the gameplay parameters
+   %this.duration = 30 * 60;
+}
+
+//This is called when the timer runs out, allowing the gamemode to end
+function @@::onGameDurationEnd(%this)
+{
+   //we don't end if we're currently editing the level
+   if (%this.duration && !(EditorIsActive() && GuiEditorIsActive()))
+      %this.onMissionEnded();
+}
+
+//This is called to actually spawn a control object for the player to utilize
+//A player character, spectator camera, etc.
+function @@::spawnControlObject(%this, %client)
+{
+   //In this example, we just spawn a camera
+   /*if (!isObject(%client.camera))
+   {
+      if(!isObject(Observer))
+      {
+         datablock CameraData(Observer)
+         {
+            mode = "Observer";
+         };  
+      }
+      
+      %client.camera = spawnObject("Camera", Observer);
+   }
+
+   // If we have a camera then set up some properties
+   if (isObject(%client.camera))
+   {
+      MissionCleanup.add( %this.camera );
+      %client.camera.scopeToClient(%client);
+
+      %client.setControlObject(%client.camera);
+
+      %client.camera.setTransform("0 0 1 0 0 0 0");
+   }*/
+   
+}
+
+//This is called when the client has finished loading the mission, but aren't "in" it yet
+//allowing for some last-second prepwork
+function @@::onClientMissionLoaded(%this)
+{
+    $clientLoaded++;
+    if ($clientLoaded == $clientConneted)
+        $gameReady = true;
+}
+
+//This is called when the client has initially established a connection to the game server
+//It's used for setting up anything ahead of time for the client, such as loading in client-passed
+//config stuffs, saved data or the like that should be handled BEFORE the client has actually entered
+//the game itself
+function @@::onClientConnect(%this, %client)
+{
+    $clientConneted++;
+    getPlayer(%client);
+}
+
+   
+//This is called when a client enters the game server. It's used to spawn a player object
+//set up any client-specific properties such as saved configs, values, their name, etc
+//These callbacks are activated in core/clientServer/scripts/server/levelDownload.cs
+function @@::onClientEnterGame(%this, %client)
+{
+   $Game::DefaultPlayerClass        = "Player";
+   $Game::DefaultPlayerDataBlock    = "DefaultPlayerData";
+   $Game::DefaultPlayerSpawnGroups = "spawn_player CameraSpawnPoints PlayerSpawnPoints PlayerDropPoints";
+
+   $Game::DefaultCameraClass = "Camera";
+   $Game::DefaultCameraDataBlock = "Observer";
+   $Game::DefaultCameraSpawnGroups = "spawn_player CameraSpawnPoints PlayerSpawnPoints PlayerDropPoints";
+
+
+   //Spawn NPCs for the map and stuff here?
+
+   // This function currently relies on some helper functions defined in
+   // core/scripts/spawn.cs. For custom spawn behaviors one can either
+   // override the properties on the SpawnSphere's or directly override the
+   // functions themselves.
+}
+
+function @@::onSceneLoaded(%this)
+{
+}
+
+function @@::onSceneUnloaded(%this)
+{
+}
+
+function @@::onSubsceneLoaded(%this)
+{
+}
+
+function @@::onSubsceneUnloaded(%this)
+{
+}
+
+//This is called when the player leaves the game server. It's used to clean up anything that
+//was spawned or setup for the client when it connected, in onClientEnterGame
+//These callbacks are activated in core/clientServer/scripts/server/levelDownload.cs
+function @@::onClientLeaveGame(%this, %client)
+{
+    // Cleanup the camera
+   if (isObject(%this.camera))
+      %this.camera.delete();
+   // Cleanup the player
+   if (isObject(%this.player))
+      %this.player.delete();
+}

+ 12 - 10
Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/module.tscript.template

@@ -9,6 +9,8 @@ function @@::onDestroy(%this)
 //This is called when the server is initially set up by the game application
 function @@::initServer(%this)
 {
+    //--FILE EXEC BEGIN--
+    //--FILE EXEC END--
 }
 
 //This is called when the server is created for an actual game/map to be played
@@ -17,16 +19,14 @@ function @@::onCreateGameServer(%this)
     //These are common managed data files. For any datablock-based stuff that gets generated by the editors
     //(that doesn't have a specific associated file, like data for a player class) will go into these.
     //So we'll register them now if they exist.
-    if(isFile("./scripts/managedData/managedDatablocks." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedDatablocks");
-    if(isFile("./scripts/managedData/managedForestItemData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedForestItemData");
-    if(isFile("./scripts/managedData/managedForestBrushData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedForestBrushData");
-    if(isFile("./scripts/managedData/managedParticleEmitterData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedParticleEmitterData");
-    if(isFile("./scripts/managedData/managedParticleData." @ $TorqueScriptFileExtension))
-        %this.registerDatablock("./scripts/managedData/managedParticleData");
+    %this.registerDatablock("./scripts/managedData/managedDatablocks");
+    %this.registerDatablock("./scripts/managedData/managedForestItemData");
+    %this.registerDatablock("./scripts/managedData/managedForestBrushData");
+    %this.registerDatablock("./scripts/managedData/managedParticleEmitterData");
+    %this.registerDatablock("./scripts/managedData/managedParticleData");
+
+    //--DATABLOCK EXEC BEGIN--
+    //--DATABLOCK EXEC END--
 }
 
 //This is called when the server is shut down due to the game/map being exited
@@ -37,6 +37,8 @@ function @@::onDestroyGameServer(%this)
 //This is called when the client is initially set up by the game application
 function @@::initClient(%this)
 {
+    //--FILE EXEC BEGIN--
+    //--FILE EXEC END--
 }
 
 //This is called when a client connects to a server

+ 253 - 0
Templates/BaseGame/game/tools/assetBrowser/scripts/utils.tscript

@@ -0,0 +1,253 @@
+function testRpl()
+{
+   ToolUtilityScripts::appendLineToFunction("data/prototyping/prototyping.tscript", "Prototyping", "onDestroyGameServer", "//AAAAAAAAAAAAAAAAAAAAA");
+}
+
+function Tools::appendLineToFunction(%file, %nameSpace, %functionName, %appendLine)
+{
+   %fileOutput = new ArrayObject(){};
+   %fileObj = new FileObject(){};
+   
+   %insideFunction = false;
+   %scopeDepth = 0;
+   
+   if ( %fileObj.openForRead( %file ) ) 
+   {
+      while ( !%fileObj.isEOF() ) 
+      {
+         %line = %fileObj.readLine();
+         
+         if(strIsMatchExpr("*function *(*)*", %line))
+         {
+            %fileOutput.add(%line);
+            
+            %start = strpos(%line, "function ");
+            %end = strpos(%line, "(", %start);
+            
+            %scannedFunctionName = "";
+
+            if(%start != -1 && %end != -1)
+            {
+               %scannedFunctionName = getSubStr(%line, %start + 9, %end-%start-9);
+            }
+            
+            %matchesFunctionSig = false;
+            if(%nameSpace !$= "")
+            {
+               if(%scannedFunctionName $= (%nameSpace @ "::" @ %functionName))
+                  %matchesFunctionSig = true;
+            }  
+            else
+            {
+               if(%scannedFunctionName $= %functionName)
+                  %matchesFunctionSig = true;
+            }
+            
+            if(!%matchesFunctionSig)
+               continue;
+               
+            %insideFunction = true;
+               
+            if(strIsMatchExpr("*{*", %line))
+            {
+               %scopeDepth++;
+            }
+            if(strIsMatchExpr("*}*", %line))
+            {
+               %scopeDepth--;
+            }
+         }
+         else
+         {
+            if(%insideFunction && strIsMatchExpr("*{*", %line))
+            {
+               %scopeDepth++;
+            }
+            if(%insideFunction && strIsMatchExpr("*}*", %line))
+            {
+               %scopeDepth--;
+               
+               if(%scopeDepth == 0) //we've fully backed out of the function scope, so resolve back to the parent
+               {
+                  %insideFunction = false;
+
+                  %fileOutput.add(%appendLine);
+               }
+            }
+            
+            %fileOutput.add(%line);
+         }
+      }
+      %fileObj.close();
+   }
+   
+   if ( %fileObj.openForWrite( %file ) )
+   {
+      for(%i=0; %i < %fileOutput.count(); %i++)
+      {
+         %line = %fileOutput.getKey(%i);
+         
+         %fileObj.writeLine(%line);  
+      }
+      %fileObj.close();
+   }
+}
+
+function Tools::findInFile(%file, %findText, %startingLineId)
+{
+   if(%startingLineId $= "")
+      %startingLineId = 0;
+      
+   %fileObj = new FileObject(){};
+      
+   if ( %fileObj.openForRead( %file ) ) 
+   {
+      %lineId = 0;
+      while ( !%fileObj.isEOF() ) 
+      {
+         if(%lineId >= %startingLineId)
+         {
+            %line = %fileObj.readLine();
+            
+            if(strIsMatchExpr(%findText, %line))
+            {
+               return %lineId;
+            }
+         }
+         
+         %lineId++;
+      }
+      %fileObj.close();
+   }
+   
+   return -1;
+}
+
+function Tools::findInFunction(%file, %nameSpace, %functionName, %findText)
+{
+   %fileObj = new FileObject(){};
+   
+   %insideFunction = false;
+   %scopeDepth = 0;
+   
+   if ( %fileObj.openForRead( %file ) ) 
+   {
+      %lineId = -1;
+      while ( !%fileObj.isEOF() ) 
+      {
+         %line = %fileObj.readLine();
+         %lineId++;
+         
+         if(strIsMatchExpr("*function *(*)*", %line))
+         {
+            %start = strpos(%line, "function ");
+            %end = strpos(%line, "(", %start);
+            
+            %scannedFunctionName = "";
+
+            if(%start != -1 && %end != -1)
+            {
+               %scannedFunctionName = getSubStr(%line, %start + 9, %end-%start-9);
+            }
+            
+            %matchesFunctionSig = false;
+            if(%nameSpace !$= "")
+            {
+               if(%scannedFunctionName $= (%nameSpace @ "::" @ %functionName))
+                  %matchesFunctionSig = true;
+            }  
+            else
+            {
+               if(%scannedFunctionName $= %functionName)
+                  %matchesFunctionSig = true;
+            }
+            
+            if(!%matchesFunctionSig)
+               continue;
+               
+            %insideFunction = true;
+               
+            if(strIsMatchExpr("*{*", %line))
+            {
+               %scopeDepth++;
+            }
+            if(strIsMatchExpr("*}*", %line))
+            {
+               %scopeDepth--;
+            }
+         }
+         else
+         {
+            if(%insideFunction && strIsMatchExpr("*{*", %line))
+            {
+               %scopeDepth++;
+            }
+            if(%insideFunction && strIsMatchExpr("*}*", %line))
+            {
+               %scopeDepth--;
+               
+               if(%scopeDepth == 0) //we've fully backed out of the function scope, so resolve back to the parent
+               {
+                  %insideFunction = false;
+                  break;
+               }
+            }
+            
+            if(%insideFunction && strIsMatchExpr(%findText, %line))
+            {
+               break;
+            }
+         }
+      }
+      %fileObj.close();
+   }
+   
+   return %lineId;
+}
+
+function Tools::insertInFile(%file, %insertLineId, %insertText, %insertBefore)
+{
+   %fileOutput = new ArrayObject(){};
+   %fileObj = new FileObject(){};
+
+   if ( %fileObj.openForRead( %file ) ) 
+   {
+      %lineId = 0;
+      while ( !%fileObj.isEOF() ) 
+      {
+         %line = %fileObj.readLine();
+         
+         if(%insertLineId == %lineId)
+         {
+            if(!%insertBefore || %insertBefore $= "")
+            {
+               %fileOutput.add(%line);
+               %fileOutput.add(%insertText);
+            }
+            else
+            {
+               %fileOutput.add(%insertText);
+               %fileOutput.add(%line);
+            }
+         }
+         else
+         {
+            %fileOutput.add(%line);
+         }
+         
+         %lineId++;
+      }
+      %fileObj.close();
+   }
+   
+   if ( %fileObj.openForWrite( %file ) )
+   {
+      for(%i=0; %i < %fileOutput.count(); %i++)
+      {
+         %line = %fileOutput.getKey(%i);
+         
+         %fileObj.writeLine(%line);  
+      }
+      %fileObj.close();
+   }
+}

+ 4 - 0
Templates/BaseGame/game/tools/gui/editorSettingsWindow.ed.tscript

@@ -301,6 +301,10 @@ function ESettingsWindow::getGeneralSettings(%this)
    SettingsInspector.addSettingsField("WorldEditor/AutosaveInterval", "Autosave Interval(in minutes)", "int", "");
    SettingsInspector.endGroup();
    
+   SettingsInspector.startGroup("SubScenes");
+   SettingsInspector.addSettingsField("WorldEditor/subSceneCreateScalar", "SubScene Creation Scalar", "float", "Amount to scale SubScene's bounds by when creating from scene objects.", "1.5");
+   SettingsInspector.endGroup();
+   
    SettingsInspector.startGroup("Paths");
    //SettingsInspector.addSettingsField("WorldEditor/torsionPath", "Torsion Path", "filename", "");
    SettingsInspector.endGroup();

+ 2 - 0
Templates/BaseGame/game/tools/settings.xml

@@ -375,6 +375,8 @@
             name="orthoShowGrid">1</Setting>
         <Setting
             name="startupMode">Blank Level</Setting>
+        <Setting
+            name="subSceneCreateScalar">2</Setting>
         <Setting
             name="torsionPath">AssetWork_Debug.exe</Setting>
         <Setting

+ 26 - 2
Templates/BaseGame/game/tools/worldEditor/gui/WorldEditorTreeWindow.ed.gui

@@ -472,7 +472,7 @@ $guiContent = new GuiControl() {
          Profile = "ToolsGuiButtonProfile";
          HorizSizing = "left";
          VertSizing = "bottom";
-         Position = "290 22";
+         Position = "269 22";
          Extent = "16 16";
          MinExtent = "8 2";
          canSave = "1";
@@ -496,7 +496,7 @@ $guiContent = new GuiControl() {
          Profile = "ToolsGuiButtonProfile";
          HorizSizing = "left";
          VertSizing = "bottom";
-         Position = "311 22";
+         Position = "290 22";
          Extent = "16 16";
          MinExtent = "8 2";
          canSave = "1";
@@ -512,6 +512,30 @@ $guiContent = new GuiControl() {
          useModifiers = "1";
       };
       
+      new GuiBitmapButtonCtrl(EWAddSceneGroupButton) {
+         canSaveDynamicFields = "0";
+         internalName = "AddSceneGroup";
+         Enabled = "1";
+         isContainer = "0";
+         Profile = "ToolsGuiButtonProfile";
+         HorizSizing = "left";
+         VertSizing = "bottom";
+         Position = "311 22";
+         Extent = "16 16";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = "Add Scene Group";
+         hovertime = "1000";
+         bitmapAsset = "ToolsModule:add_simgroup_btn_n_image";
+         buttonType = "PushButton";
+         groupNum = "-1";
+         text = "";
+         useMouseEvents = "0";
+         useModifiers = "1";
+      };
+      
       new GuiBitmapButtonCtrl() {
          canSaveDynamicFields = "0";
          internalName = "DeleteSelection";

+ 91 - 2
Templates/BaseGame/game/tools/worldEditor/scripts/EditorGui.ed.tscript

@@ -2007,7 +2007,7 @@ function EditorTree::onRightMouseUp( %this, %itemId, %mouse, %obj )
       %popup.item[ 0 ] = "Delete" TAB "" TAB "EditorMenuEditDelete();";
       %popup.item[ 1 ] = "Group" TAB "" TAB "EWorldEditor.addSimGroup( true );";
       %popup.item[ 2 ] = "-";
-      %popup.item[ 3 ] = "Make select a Sub-Level" TAB "" TAB "MakeSelectionASublevel();";
+      %popup.item[ 3 ] = "Make selected a Sub-Level" TAB "" TAB "MakeSelectionASublevel();";
    }
    else
    {
@@ -2103,6 +2103,14 @@ function EditorTree::onRightMouseUp( %this, %itemId, %mouse, %obj )
                   %popup.enableItem(15, true);
                }
             }
+            else if(%obj.getClassName() $= "SimGroup" || 
+                    %obj.getClassName() $= "SceneGroup")
+            {
+               //if it's ACTUALLY a SimGroup or SceneGroup, have the ability to
+               //upconvert to SubScene  
+               %popup.item[ 12 ] = "-";
+               %popup.item[ 13 ] = "Convert to SubScene" TAB "" TAB "EWorldEditor.createSelectedAsSubScene( " @ %popup.object @ " );";
+            }
          }  
       }
    }
@@ -2142,7 +2150,7 @@ function EditorTree::isValidDragTarget( %this, %id, %obj )
    if( %obj.name $= "CameraBookmarks" )
       return EWorldEditor.areAllSelectedObjectsOfType( "CameraBookmark" );
    else
-      return ( %obj.getClassName() $= "SimGroup" || %obj.isMemberOfClass("Scene"));
+      return ( %obj.getClassName() $= "SimGroup" || %obj.isMemberOfClass("Scene") || %obj.isMemberOfClass("SceneGroup"));
 }
 
 function EditorTree::onBeginReparenting( %this )
@@ -2582,6 +2590,77 @@ function EWorldEditor::addSimGroup( %this, %groupCurrentSelection )
    %this.syncGui();
 }
 
+function EWorldEditor::addSceneGroup( %this, %groupCurrentSelection )
+{
+   %activeSelection = %this.getActiveSelection();
+   if ( %groupCurrentSelection && %activeSelection.getObjectIndex( getScene(0) ) != -1 )
+   {
+      toolsMessageBoxOK( "Error", "Cannot add Scene to a new SceneGroup" );
+      return;
+   }
+
+   // Find our parent.
+
+   %parent = getScene(0);
+   if( !%groupCurrentSelection && isObject( %activeSelection ) && %activeSelection.getCount() > 0 )
+   {
+      %firstSelectedObject = %activeSelection.getObject( 0 );
+      if( %firstSelectedObject.isMemberOfClass( "SimGroup" ) )
+         %parent = %firstSelectedObject;
+      else if( %firstSelectedObject.getId() != getScene(0).getId() )
+         %parent = %firstSelectedObject.parentGroup;
+   }
+   
+   // If we are about to do a group-selected as well,
+   // starting recording an undo compound.
+   
+   if( %groupCurrentSelection )
+      Editor.getUndoManager().pushCompound( "Group Selected" );
+   
+   // Create the SimGroup.
+   
+   %object = new SceneGroup()
+   {
+      parentGroup = %parent;
+   };
+   MECreateUndoAction::submit( %object );
+   
+   // Put selected objects into the group, if requested.
+   
+   if( %groupCurrentSelection && isObject( %activeSelection ) )
+   {
+      %undo = UndoActionReparentObjects::create( EditorTree );
+      
+      %numObjects = %activeSelection.getCount();
+      for( %i = 0; %i < %numObjects; %i ++ )
+      {
+         %sel = %activeSelection.getObject( %i );
+         %undo.add( %sel, %sel.parentGroup, %object );
+         %object.add( %sel );
+      }
+      
+      %undo.addToManager( Editor.getUndoManager() );
+   }
+      
+   // Stop recording for group-selected.
+   
+   if( %groupCurrentSelection )
+      Editor.getUndoManager().popCompound();
+   
+   // When not grouping selection, make the newly created SimGroup the
+   // current selection.
+   
+   if( !%groupCurrentSelection )
+   {
+      EWorldEditor.clearSelection();
+      EWorldEditor.selectObject( %object );
+   }
+
+   // Refresh the Gui.
+   
+   %this.syncGui();
+}
+
 function EWorldEditor::toggleLockChildren( %this, %simGroup )
 {
    foreach( %child in %simGroup )
@@ -2847,6 +2926,16 @@ function EWAddSimGroupButton::onCtrlClick( %this )
    EWorldEditor.addSimGroup( true );
 }
 
+function EWAddSceneGroupButton::onDefaultClick( %this )
+{
+   EWorldEditor.addSceneGroup();
+}
+
+function EWAddSceneGroupButton::onCtrlClick( %this )
+{
+   EWorldEditor.addSceneGroup( true );
+}
+
 //------------------------------------------------------------------------------
 
 function EWToolsToolbar::reset( %this )

+ 1 - 0
Templates/BaseGame/game/tools/worldEditor/scripts/editorPrefs.ed.tscript

@@ -34,6 +34,7 @@ EditorSettings.setDefaultValue(  "orthoFOV",                "50" );
 EditorSettings.setDefaultValue(  "orthoShowGrid",           "1" );
 EditorSettings.setDefaultValue(  "AutosaveInterval",        "5" );
 EditorSettings.setDefaultValue(  "forceSidebarToSide",        "1" );
+EditorSettings.setDefaultValue(  "subSceneCreateScalar",        $SubScene::createScalar );
 
 if( isFile( "C:/Program Files/Torsion/Torsion.exe" ) )
    EditorSettings.setDefaultValue(  "torsionPath",          "C:/Program Files/Torsion/Torsion.exe" );

+ 8 - 9
Templates/BaseGame/game/tools/worldEditor/scripts/menuHandlers.ed.tscript

@@ -329,9 +329,6 @@ function EditorSaveMission()
 
    if(EWorldEditor.isDirty || ETerrainEditor.isMissionDirty)
    {
-      //Inform objects a save is happening, in case there is any special pre-save behavior a class needs to do
-      getScene(0).callOnChildren("onSaving", $Server::MissionFile);
-   
       getScene(0).save($Server::MissionFile);
       
    }
@@ -604,18 +601,20 @@ function EditorOpenSceneAppend(%levelAsset)
 
 function MakeSelectionASublevel()
 {
-   /*%size = EWorldEditor.getSelectionSize();
+   %size = EWorldEditor.getSelectionSize();
    if ( %size == 0 )
       return;
       
-   //Make a new Scene object
-      
+   //Make a group for the objects to go into to be processed as into a
+   //subscene
+   %group = new SimGroup();
    for(%i=0; %i < %size; %i++)
    {
-      
+      %group.add(EWorldEditor.getSelectedObject(%i));
    }
-   %a = EWorldEditor.getSelectedObject(0);
-   %b = EWorldEditor.getSelectedObject(1);*/
+   
+   //Now that we have a group, process it into a subscene
+   EWorldEditor.createSelectedAsSubScene(%group);
 }
 
 function updateEditorRecentLevelsList(%levelAssetId)