Jelajahi Sumber

Merge pull request #1311 from Areloch/SubScenes_Gamemode_PR

Implementation of Subscenes, SceneGroups and Gamemodes
Areloch 9 bulan lalu
induk
melakukan
a88aa7a007
67 mengubah file dengan 4345 tambahan dan 520 penghapusan
  1. 132 43
      Engine/source/T3D/Scene.cpp
  2. 25 27
      Engine/source/T3D/Scene.h
  3. 362 0
      Engine/source/T3D/SceneGroup.cpp
  4. 55 0
      Engine/source/T3D/SceneGroup.h
  5. 556 0
      Engine/source/T3D/SubScene.cpp
  6. 126 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. 53 2
      Engine/source/T3D/convexShape.cpp
  11. 57 54
      Engine/source/T3D/convexShape.h
  12. 460 0
      Engine/source/T3D/gameMode.cpp
  13. 117 0
      Engine/source/T3D/gameMode.h
  14. 6 0
      Engine/source/T3D/prefab.cpp
  15. 4 4
      Engine/source/T3D/trigger.cpp
  16. 1 1
      Engine/source/T3D/trigger.h
  17. 1 0
      Engine/source/console/consoleObject.h
  18. 303 109
      Engine/source/console/persistenceManager.cpp
  19. 7 1
      Engine/source/console/persistenceManager.h
  20. 12 0
      Engine/source/console/simObject.cpp
  21. 6 0
      Engine/source/console/simObject.h
  22. 31 1
      Engine/source/environment/decalRoad.cpp
  23. 3 0
      Engine/source/environment/decalRoad.h
  24. 56 2
      Engine/source/environment/meshRoad.cpp
  25. 3 0
      Engine/source/environment/meshRoad.h
  26. 34 1
      Engine/source/environment/river.cpp
  27. 3 0
      Engine/source/environment/river.h
  28. 7 0
      Engine/source/gui/core/guiControl.h
  29. 44 34
      Engine/source/gui/editor/inspector/group.cpp
  30. 46 31
      Engine/source/gui/editor/inspector/variableInspector.cpp
  31. 6 1
      Engine/source/scene/sceneObject.cpp
  32. 2 0
      Engine/source/scene/sceneObject.h
  33. 15 5
      Templates/BaseGame/game/core/utility/scripts/helperFunctions.tscript
  34. 14 44
      Templates/BaseGame/game/core/utility/scripts/scene.tscript
  35. 4 15
      Templates/BaseGame/game/data/ExampleModule/ExampleModule.tscript
  36. 1 0
      Templates/BaseGame/game/data/ExampleModule/levels/ExampleLevel.asset.taml
  37. 1 1
      Templates/BaseGame/game/data/ExampleModule/levels/ExampleLevel.mis
  38. 26 8
      Templates/BaseGame/game/data/ExampleModule/scripts/shared/ExampleGameMode.tscript
  39. 76 12
      Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.gui
  40. 308 54
      Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.tscript
  41. TEMPAT SAMPAH
      Templates/BaseGame/game/data/UI/images/toggleMarker.png
  42. TEMPAT SAMPAH
      Templates/BaseGame/game/data/UI/images/toggleMarker_h.png
  43. 3 0
      Templates/BaseGame/game/data/UI/images/toggleMarker_h_image.asset.taml
  44. 3 0
      Templates/BaseGame/game/data/UI/images/toggleMarker_image.asset.taml
  45. 2 0
      Templates/BaseGame/game/tools/assetBrowser/main.tscript
  46. 0 1
      Templates/BaseGame/game/tools/assetBrowser/scripts/addModuleWindow.tscript
  47. 35 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/gameMode.tscript
  48. 121 3
      Templates/BaseGame/game/tools/assetBrowser/scripts/assetTypes/level.tscript
  49. 25 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/editAsset.tscript
  50. 2 1
      Templates/BaseGame/game/tools/assetBrowser/scripts/popupMenus.tscript
  51. 176 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/gameMode.tscript.template
  52. 11 10
      Templates/BaseGame/game/tools/assetBrowser/scripts/templateFiles/module.tscript.template
  53. 253 0
      Templates/BaseGame/game/tools/assetBrowser/scripts/utils.tscript
  54. 4 0
      Templates/BaseGame/game/tools/gui/editorSettingsWindow.ed.tscript
  55. 13 9
      Templates/BaseGame/game/tools/gui/fieldTypes/fieldTypes.tscript
  56. 1 0
      Templates/BaseGame/game/tools/levels/DefaultEditorLevel.asset.taml
  57. 5 19
      Templates/BaseGame/game/tools/levels/DefaultEditorLevel.mis
  58. 2 0
      Templates/BaseGame/game/tools/settings.xml
  59. 1 1
      Templates/BaseGame/game/tools/worldEditor/gui/ToolsPaletteWindow.ed.gui
  60. 26 2
      Templates/BaseGame/game/tools/worldEditor/gui/WorldEditorTreeWindow.ed.gui
  61. 3 0
      Templates/BaseGame/game/tools/worldEditor/main.tscript
  62. 114 6
      Templates/BaseGame/game/tools/worldEditor/scripts/EditorGui.ed.tscript
  63. 160 0
      Templates/BaseGame/game/tools/worldEditor/scripts/buttonPalette.tscript
  64. 1 0
      Templates/BaseGame/game/tools/worldEditor/scripts/editorPrefs.ed.tscript
  65. 139 0
      Templates/BaseGame/game/tools/worldEditor/scripts/editors/worldEditor.ed.tscript
  66. 54 0
      Templates/BaseGame/game/tools/worldEditor/scripts/interfaces/subSceneEditing.tscript
  67. 8 9
      Templates/BaseGame/game/tools/worldEditor/scripts/menuHandlers.ed.tscript

+ 132 - 43
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
@@ -157,30 +148,88 @@ void Scene::removeObject(SimObject* object)
    Parent::removeObject(object);
 }
 
-void Scene::addDynamicObject(SceneObject* object)
+void Scene::addDynamicObject(SimObject* object)
 {
    mDynamicObjects.push_back(object);
 
+   SimGroup* cleanupGroup;
+   if(Sim::findObject("MissionCleanup", cleanupGroup))
+   {
+      cleanupGroup->addObject(object);
+   }
+
    //Do it like regular, though we should probably bail if we're trying to add non-scene objects to the scene?
-   Parent::addObject(object);
+   //Parent::addObject(object);
 }
 
-void Scene::removeDynamicObject(SceneObject* object)
+void Scene::removeDynamicObject(SimObject* object)
 {
    mDynamicObjects.remove(object);
 
+   SimGroup* cleanupGroup;
+   if (Sim::findObject("MissionCleanup", cleanupGroup))
+   {
+      cleanupGroup->removeObject(object);
+   }
+
    //Do it like regular, though we should probably bail if we're trying to add non-scene objects to the scene?
-   Parent::removeObject(object);
+   //Parent::removeObject(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)
@@ -205,7 +254,7 @@ void Scene::dumpUtilizedAssets()
    Con::printf("Dumping utilized assets in scene!");
 
    Vector<StringTableEntry> utilizedAssetsList;
-   for (U32 i = 0; i < mPermanentObjects.size(); i++)
+   /*for (U32 i = 0; i < mPermanentObjects.size(); i++)
    {
       mPermanentObjects[i]->getUtilizedAssets(&utilizedAssetsList);
    }
@@ -213,7 +262,7 @@ void Scene::dumpUtilizedAssets()
    for (U32 i = 0; i < mDynamicObjects.size(); i++)
    {
       mDynamicObjects[i]->getUtilizedAssets(&utilizedAssetsList);
-   }
+   }*/
 
    for (U32 i = 0; i < utilizedAssetsList.size(); i++)
    {
@@ -247,6 +296,9 @@ StringTableEntry Scene::getLevelAsset()
 
 bool Scene::saveScene(StringTableEntry fileName)
 {
+   if (!isServerObject())
+      return false;
+
    //So, we ultimately want to not only save out the level, but also collate all the assets utilized
    //by the static objects in the scene so we can have those before we parse the level file itself
    //Useful for preloading or stat tracking
@@ -257,6 +309,21 @@ bool Scene::saveScene(StringTableEntry fileName)
       fileName = getOriginatingFile();
    }
 
+   for (SimGroupIterator itr(this); *itr; ++itr)
+   {
+      if((*itr)->isMethod("onSaving"))
+      {
+         Con::executef((*itr), "onSaving", 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 +353,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;
@@ -314,11 +384,26 @@ void Scene::getUtilizedAssetsFromSceneObject(SimObject* object, Vector<StringTab
 }
 
 //
-Vector<SceneObject*> Scene::getObjectsByClass(String className, bool checkSubscenes)
+Vector<SceneObject*> Scene::getObjectsByClass(String className)
 {
    return Vector<SceneObject*>();
 }
 
+void Scene::loadAtPosition(const Point3F& position)
+{
+   for (U32 i = 0; i < mSubScenes.size(); i++)
+   {
+      Box3F testBox = Box3F(0.5);
+      testBox.setCenter(position);
+
+      if (mSubScenes[i]->testBox(testBox))
+      {
+         mSubScenes[i]->setUnloadTimeMS(-1);
+         mSubScenes[i]->load();
+      }
+   }
+}
+
 DefineEngineFunction(getScene, Scene*, (U32 sceneId), (0),
    "Get the root Scene object that is loaded.\n"
    "@return The id of the Root Scene. Will be 0 if no root scene is loaded")
@@ -413,9 +498,13 @@ 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));
 }
+
+DefineEngineMethod(Scene, loadAtPosition, void, (Point3F position), (Point3F::Zero),
+   "Loads any subscenes at a given point by force.\n")
+{
+   object->loadAtPosition(position);
+}

+ 25 - 27
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,15 +27,12 @@ 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;
+   Vector<SimObject*> mPermanentObjects;
+   Vector<SimObject*> 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;
@@ -63,8 +69,8 @@ public:
    void addObject(SimObject* object) override;
    void removeObject(SimObject* object) override;
 
-   void addDynamicObject(SceneObject* object);
-   void removeDynamicObject(SceneObject* object);
+   void addDynamicObject(SimObject* object);
+   void removeDynamicObject(SimObject* object);
    void clearDynamicObjects() { mDynamicObjects.clear(); }
 
    void dumpUtilizedAssets();
@@ -80,12 +86,14 @@ public:
    void unpackUpdate(NetConnection *conn, BitStream *stream) override;
 
    //
-   Vector<SceneObject*> getObjectsByClass(String className, bool checkSubscenes);
+   Vector<SceneObject*> getObjectsByClass(String className);
 
    void getUtilizedAssetsFromSceneObject(SimObject* object, Vector<StringTableEntry>* usedAssetsList);
 
    template <class T>
-   Vector<T*> getObjectsByClass(bool checkSubscenes);
+   Vector<T*> getObjectsByClass();
+
+   void loadAtPosition(const Point3F& position);
 
    static Scene *getRootScene() 
    { 
@@ -96,11 +104,13 @@ public:
    }
 
    static Vector<Scene*> smSceneList;
+
+   DECLARE_CALLBACK(void, onSaving, (const char* fileName));
 };
 
 
 template <class T>
-Vector<T*> Scene::getObjectsByClass(bool checkSubscenes)
+Vector<T*> Scene::getObjectsByClass()
 {
    Vector<T*> foundObjects;
 
@@ -121,18 +131,6 @@ Vector<T*> Scene::getObjectsByClass(bool checkSubscenes)
          foundObjects.push_back(curObject);
    }
 
-   if (checkSubscenes)
-   {
-      for (U32 i = 0; i < mSubScenes.size(); i++)
-      {
-         Vector<T*> appendList = mSubScenes[i]->getObjectsByClass<T>(true);
-
-         for (U32 a = 0; a < appendList.size(); a++)
-         {
-            foundObjects.push_back(appendList[a]);
-         }
-      }
-   }
-
    return foundObjects;
 }
+#endif

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

@@ -0,0 +1,362 @@
+#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)
+{
+   Parent::onInspect(inspector);
+   
+   //Put the SubScene group before everything that'd be SubScene-effecting, for orginazational purposes
+   GuiInspectorGroup* sceneGroupGrp = inspector->findExistentGroup(StringTable->insert("Editing"));
+   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)
+{
+   if (isServerObject())
+   {
+      setMaskBits(TransformMask);
+
+      MatrixF newXform = mat;
+      MatrixF oldXform = getTransform();
+      oldXform.affineInverse();
+
+      MatrixF offset;
+      offset.mul(newXform, oldXform);
+
+      // Update all child transforms
+      for (SimSetIterator itr(this); *itr; ++itr)
+      {
+         SceneObject* child = dynamic_cast<SceneObject*>(*itr);
+         if (child)
+         {
+            MatrixF childMat;
+
+            //add the "offset" caused by the parents change, and add it to it's own
+            // This is needed by objects that update their own render transform thru interpolate tick
+            // Mostly for stationary objects.
+            childMat.mul(offset, child->getTransform());
+            child->setTransform(childMat);
+
+            PhysicsShape* childPS = dynamic_cast<PhysicsShape*>(child);
+            if (childPS)
+               childPS->storeRestorePos();
+         }
+      }
+   }
+
+   Parent::setTransform(mat);
+}
+
+void SceneGroup::setRenderTransform(const MatrixF& mat)
+{
+   MatrixF newXform = mat;
+   MatrixF oldXform = getRenderTransform();
+   oldXform.affineInverse();
+
+   MatrixF offset;
+   offset.mul(newXform, oldXform);
+
+   // Update all child transforms
+   for (SimSetIterator itr(this); *itr; ++itr)
+   {
+      SceneObject* child = dynamic_cast<SceneObject*>(*itr);
+      if (child)
+      {
+         MatrixF childMat;
+
+         //add the "offset" caused by the parents change, and add it to it's own
+         // This is needed by objects that update their own render transform thru interpolate tick
+         // Mostly for stationary objects.
+         childMat.mul(offset, child->getRenderTransform());
+         child->setRenderTransform(childMat);
+
+         PhysicsShape* childPS = dynamic_cast<PhysicsShape*>(child);
+         if (childPS)
+            childPS->storeRestorePos();
+      }
+   }
+
+   Parent::setRenderTransform(mat);
+}
+
+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

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

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

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

@@ -0,0 +1,126 @@
+#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
+
+class GameMode;
+
+class SubScene : public SceneGroup
+{
+   typedef SceneGroup Parent;
+
+public:
+   enum MaskBits
+   {
+      NextFreeMask = Parent::NextFreeMask << 0
+   };
+
+   void onLevelChanged() {}
+
+protected:
+   static bool smTransformChildren;
+
+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 mFreezeLoading;
+
+   String mLoadIf;
+   String mOnLoadCommand;
+   String mOnUnloadCommand;
+
+   S32 mTickPeriodMS;
+   U32 mCurrTick;
+
+   bool mGlobalLayer;
+
+public:
+   SubScene();
+   virtual ~SubScene();
+
+   DECLARE_CONOBJECT(SubScene);
+   DECLARE_CATEGORY("Object \t Collection");
+
+   static void initPersistFields();
+   static void consoleInit();
+   StringTableEntry getTypeHint() const override { return (getLevelAsset()) ? getLevelAsset()->getAssetName() : StringTable->EmptyString(); }
+
+   // 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;
+
+   void setTransform(const MatrixF& mat) override;
+   void setRenderTransform(const MatrixF& mat) override;
+
+   bool testBox(const Box3F& testBox);
+   bool evaluateCondition();
+   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 _removeContents(SimGroupIterator);
+   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_CALLBACK(void, onLoaded, ());
+   DECLARE_CALLBACK(void, onUnloaded, ());
+   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_
 

+ 53 - 2
Engine/source/T3D/convexShape.cpp

@@ -317,10 +317,10 @@ void ConvexShape::initPersistFields()
    addGroup( "Internal" );
 
       addProtectedField( "surface", TypeRealString, 0, &protectedSetSurface, &defaultProtectedGetFn,
-         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors );
+         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
 	  addProtectedField( "surfaceTexture", TypeRealString, 0, &protectedSetSurfaceTexture, &defaultProtectedGetFn,
-         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors );
+         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
    endGroup( "Internal" );
 
@@ -498,6 +498,57 @@ bool ConvexShape::writeField( StringTableEntry fieldname, const char *value )
    return Parent::writeField( fieldname, value );
 }
 
+U32 ConvexShape::getSpecialFieldSize(StringTableEntry fieldName)
+{
+   if (fieldName == StringTable->insert("surface") || fieldName == StringTable->insert("surfaceTexture"))
+   {
+      return mSurfaces.size();
+   }
+
+   return 0;
+}
+
+const char* ConvexShape::getSpecialFieldOut(StringTableEntry fieldName, const U32& index)
+{
+   if (index >= smMaxSurfaces)
+      return NULL;
+
+   if (fieldName == StringTable->insert("surface"))
+   {
+      if(index >= mSurfaces.size())
+         return NULL;
+
+      const MatrixF& mat = mSurfaces[index];
+
+      QuatF quat(mat);
+      Point3F pos(mat.getPosition());
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+
+      dSprintf(buffer, 1024, "%g %g %g %g %g %g %g %i %g %g %g %g %g %i %i",
+         quat.x, quat.y, quat.z, quat.w, pos.x, pos.y, pos.z, mSurfaceUVs[index].matID,
+         mSurfaceUVs[index].offset.x, mSurfaceUVs[index].offset.y, mSurfaceUVs[index].scale.x,
+         mSurfaceUVs[index].scale.y, mSurfaceUVs[index].zRot, mSurfaceUVs[index].horzFlip, mSurfaceUVs[index].vertFlip);
+
+      return StringTable->insert(buffer);
+   }
+   else if (fieldName == StringTable->insert("surfaceTexture"))
+   {
+      if (index >= mSurfaceTextures.size())
+         return NULL;
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+
+      dSprintf(buffer, 1024, "%s", mSurfaceTextures[index].getMaterial());
+
+      return StringTable->insert(buffer);
+   }
+
+   return NULL;
+}
+
 void ConvexShape::onScaleChanged()
 {
    if ( isProperlyAdded() )

+ 57 - 54
Engine/source/T3D/convexShape.h

@@ -83,7 +83,7 @@ class ConvexShape : public SceneObject
    typedef SceneObject Parent;
    friend class GuiConvexEditorCtrl;
    friend class GuiConvexEditorUndoAction;
-	friend class ConvexShapeCollisionConvex;
+   friend class ConvexShapeCollisionConvex;
 
 public:
 
@@ -113,10 +113,10 @@ public:
       U32 p1;
       U32 p2;
 
-      U32 operator []( U32 index ) const
+      U32 operator [](U32 index) const
       {
-         AssertFatal( index >= 0 && index <= 2, "index out of range" );
-         return *( (&p0) + index );
+         AssertFatal(index >= 0 && index <= 2, "index out of range");
+         return *((&p0) + index);
       }
    };
 
@@ -126,23 +126,23 @@ public:
       Vector< U32 > points;
       Vector< U32 > winding;
       Vector< Point2F > texcoords;
-      Vector< Triangle > triangles;			
+      Vector< Triangle > triangles;
       Point3F tangent;
       Point3F normal;
       Point3F centroid;
       F32 area;
       S32 id;
-   }; 
+   };
 
    struct surfaceMaterial
    {
       // The name of the Material we will use for rendering
       DECLARE_MATERIALASSET(surfaceMaterial, Material);
-      
+
       DECLARE_ASSET_SETGET(surfaceMaterial, Material);
 
       // The actual Material instance
-      BaseMatInstance*  materialInst;
+      BaseMatInstance* materialInst;
 
       surfaceMaterial()
       {
@@ -174,26 +174,26 @@ public:
       U32 mPrimCount;
    };
 
-	struct Geometry
-	{  
-      void generate(const Vector< PlaneF > &planes, const Vector< Point3F > &tangents, const Vector< surfaceMaterial > surfaceTextures, const Vector< Point2F > texOffset, const Vector< Point2F > texScale, const Vector< bool > horzFlip, const Vector< bool > vertFlip);
+   struct Geometry
+   {
+      void generate(const Vector< PlaneF >& planes, const Vector< Point3F >& tangents, const Vector< surfaceMaterial > surfaceTextures, const Vector< Point2F > texOffset, const Vector< Point2F > texScale, const Vector< bool > horzFlip, const Vector< bool > vertFlip);
 
-		Vector< Point3F > points;      
-		Vector< Face > faces;
-	};
+      Vector< Point3F > points;
+      Vector< Face > faces;
+   };
 
-   static bool smRenderEdges;   
+   static bool smRenderEdges;
 
    // To prevent bitpack overflows.
    // This is only indirectly enforced by trucation when serializing.
    static const S32 smMaxSurfaces = 100;
 
 public:
-   
+
    ConvexShape();
    virtual ~ConvexShape();
 
-   DECLARE_CONOBJECT( ConvexShape );
+   DECLARE_CONOBJECT(ConvexShape);
    DECLARE_CATEGORY("Object \t Simple");
 
    // ConsoleObject
@@ -203,73 +203,76 @@ public:
    void inspectPostApply() override;
    bool onAdd() override;
    void onRemove() override;
-   void writeFields(Stream &stream, U32 tabStop) override;
-   bool writeField( StringTableEntry fieldname, const char *value ) override;
+   void writeFields(Stream& stream, U32 tabStop) override;
+   bool writeField(StringTableEntry fieldname, const char* value) override;
+
+   U32 getSpecialFieldSize(StringTableEntry fieldName) override;
+   const char* getSpecialFieldOut(StringTableEntry fieldName, const U32& index) override;
 
    // NetObject
-   U32 packUpdate( NetConnection *conn, U32 mask, BitStream *stream ) override;
-   void unpackUpdate( NetConnection *conn, BitStream *stream ) override;
+   U32 packUpdate(NetConnection* conn, U32 mask, BitStream* stream) override;
+   void unpackUpdate(NetConnection* conn, BitStream* stream) override;
 
    // SceneObject
    void onScaleChanged() override;
-   void setTransform( const MatrixF &mat ) override;   
-   void prepRenderImage( SceneRenderState *state ) override;
-   void buildConvex( const Box3F &box, Convex *convex ) override;
-   bool buildPolyList( PolyListContext context, AbstractPolyList *polyList, const Box3F &box, const SphereF &sphere ) override;
-   bool buildExportPolyList(ColladaUtils::ExportData* exportData, const Box3F &box, const SphereF &) override;
-   bool castRay( const Point3F &start, const Point3F &end, RayInfo *info ) override;
-   bool collideBox( const Point3F &start, const Point3F &end, RayInfo *info ) override;
+   void setTransform(const MatrixF& mat) override;
+   void prepRenderImage(SceneRenderState* state) override;
+   void buildConvex(const Box3F& box, Convex* convex) override;
+   bool buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere) override;
+   bool buildExportPolyList(ColladaUtils::ExportData* exportData, const Box3F& box, const SphereF&) override;
+   bool castRay(const Point3F& start, const Point3F& end, RayInfo* info) override;
+   bool collideBox(const Point3F& start, const Point3F& end, RayInfo* info) override;
 
 
-   void updateBounds( bool recenter );
+   void updateBounds(bool recenter);
    void recenter();
 
    /// Geometry access.
    /// @{
-         
-      MatrixF getSurfaceWorldMat( S32 faceid, bool scaled = false ) const;
-      void cullEmptyPlanes( Vector< U32 > *removedPlanes );
-		void exportToCollada();
-      void resizePlanes( const Point3F &size );
-      void getSurfaceLineList( S32 surfId, Vector< Point3F > &lineList );
-      Geometry& getGeometry() { return mGeometry; }
-      Vector<MatrixF>& getSurfaces() { return mSurfaces; }
-      void getSurfaceTriangles( S32 surfId, Vector< Point3F > *outPoints, Vector< Point2F > *outCoords, bool worldSpace );
+
+   MatrixF getSurfaceWorldMat(S32 faceid, bool scaled = false) const;
+   void cullEmptyPlanes(Vector< U32 >* removedPlanes);
+   void exportToCollada();
+   void resizePlanes(const Point3F& size);
+   void getSurfaceLineList(S32 surfId, Vector< Point3F >& lineList);
+   Geometry& getGeometry() { return mGeometry; }
+   Vector<MatrixF>& getSurfaces() { return mSurfaces; }
+   void getSurfaceTriangles(S32 surfId, Vector< Point3F >* outPoints, Vector< Point2F >* outCoords, bool worldSpace);
 
    /// @}
 
    /// Geometry Visualization.
    /// @{
 
-      void renderFaceEdges( S32 faceid, const ColorI &color = ColorI::WHITE, F32 lineWidth = 1.0f );
+   void renderFaceEdges(S32 faceid, const ColorI& color = ColorI::WHITE, F32 lineWidth = 1.0f);
 
    /// @}
 
-      String getMaterialName() { return mMaterialName; }
+   String getMaterialName() { return mMaterialName; }
 
 protected:
 
    void _updateMaterial();
-   void _updateGeometry( bool updateCollision = false );
+   void _updateGeometry(bool updateCollision = false);
    void _updateCollision();
-   void _export( OptimizedPolyList *plist, const Box3F &box, const SphereF &sphere );
+   void _export(OptimizedPolyList* plist, const Box3F& box, const SphereF& sphere);
 
-   void _renderDebug( ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance *mat );
+   void _renderDebug(ObjectRenderInst* ri, SceneRenderState* state, BaseMatInstance* mat);
 
-   static S32 QSORT_CALLBACK _comparePlaneDist( const void *a, const void *b );
+   static S32 QSORT_CALLBACK _comparePlaneDist(const void* a, const void* b);
 
-   static bool protectedSetSurface( void *object, const char *index, const char *data );
+   static bool protectedSetSurface(void* object, const char* index, const char* data);
+
+   static bool protectedSetSurfaceTexture(void* object, const char* index, const char* data);
+   static bool protectedSetSurfaceUV(void* object, const char* index, const char* data);
 
-   static bool protectedSetSurfaceTexture( void *object, const char *index, const char *data );
-   static bool protectedSetSurfaceUV(void *object, const char *index, const char *data);
-  
 protected:
-   
+
    DECLARE_MATERIALASSET(ConvexShape, Material);
    DECLARE_ASSET_SETGET(ConvexShape, Material);
 
    // The actual Material instance
-   BaseMatInstance*  mMaterialInst;
+   BaseMatInstance* mMaterialInst;
 
    // The GFX vertex and primitive buffers
    /*GFXVertexBufferHandle< VertexType > mVertexBuffer;
@@ -278,7 +281,7 @@ protected:
    U32 mVertCount;
    U32 mPrimCount;*/
 
-   Geometry mGeometry;  
+   Geometry mGeometry;
 
    Vector< PlaneF > mPlanes;
 
@@ -291,14 +294,14 @@ protected:
    Vector< surfaceUV > mSurfaceUVs;
    Vector< surfaceBuffers > mSurfaceBuffers;
 
-   Convex *mConvexList;
+   Convex* mConvexList;
 
-   PhysicsBody *mPhysicsRep; 
+   PhysicsBody* mPhysicsRep;
 
    /// Geometry visualization
    /// @{      
 
-      F32 mNormalLength;   
+   F32 mNormalLength;
 
    /// @}
 

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

@@ -0,0 +1,460 @@
+#include "gameMode.h"
+
+#ifdef TORQUE_TOOLS
+#include "gui/containers/guiDynamicCtrlArrayCtrl.h"
+#endif
+
+#include "console/arrayObject.h"
+
+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, (SubScene*), ("SubScene"),
+   "@brief Called when a subScene has been loaded and has game mode implications.\n\n");
+IMPLEMENT_CALLBACK(GameMode, onSubsceneUnloaded, void, (SubScene*), ("SubScene"),
+   "@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()),
+   mIsActive(false),
+   mIsAlwaysActive(false)
+{
+   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");
+     addField("alwaysActive", TypeBool, Offset(mIsAlwaysActive, GameMode), "Is the gamemode always 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();
+}
+
+void GameMode::setAlwaysActive(const bool& alwaysActive)
+{
+   mIsAlwaysActive = alwaysActive;
+}
+
+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);
+}
+
+DefineEngineMethod(GameMode, isALwaysActive, bool, (), ,
+   "Returns if the GameMode is currently active.\n"
+   "@return The active status of the GameMode")
+{
+   return object->isActive();
+}
+
+DefineEngineMethod(GameMode, setAlwaysActive, void, (bool alwaysActive), (true),
+   "Sets the active state of the GameMode.\n"
+   "@param active A bool of the state the GameMode should be set to")
+{
+   object->setAlwaysActive(alwaysActive);
+}
+
+DefineEngineFunction(getGameModesList, ArrayObject*, (), , "")
+{
+   ArrayObject* dictionary = new ArrayObject();
+   dictionary->registerObject();
+
+   char activeValBuffer[16];
+
+   for (SimGroup::iterator itr = Sim::getRootGroup()->begin(); itr != Sim::getRootGroup()->end(); itr++)
+   {
+      GameMode* gm = dynamic_cast<GameMode*>(*itr);
+      if (gm)
+      {
+         dSprintf(activeValBuffer, 16, "%d", (gm->mIsActive || gm->mIsAlwaysActive));
+         dictionary->push_back(gm->getName(), activeValBuffer);
+      }
+   }
+
+   return dictionary;
+}
+
+//-----------------------------------------------------------------------------
+// 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

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

@@ -0,0 +1,117 @@
+#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
+
+#ifndef SUB_SCENE_H
+#include "SubScene.h"
+#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;
+   bool mIsAlwaysActive;
+
+public:
+
+   GameMode();
+   ~GameMode() = default;
+   static void initPersistFields();
+   bool onAdd() override;
+   void onRemove() override;
+
+   bool isActive() { return mIsActive; }
+   void setActive(const bool& active);
+
+   bool isAlwaysActive() { return mIsAlwaysActive; }
+   void setAlwaysActive(const bool& alwaysActive);
+
+   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, (SubScene*));
+   DECLARE_CALLBACK(void, onSubsceneUnloaded, (SubScene*));
+};
+
+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

+ 6 - 0
Engine/source/T3D/prefab.cpp

@@ -371,6 +371,12 @@ void Prefab::_loadFile( bool addFileNotify )
       return;
    }
 
+   SimObjectPtr<Scene> rootScene = Scene::getRootScene();
+   if(rootScene.isValid())
+   {
+      rootScene->addDynamicObject(group);
+   }
+
    if ( addFileNotify )
       Torque::FS::AddChangeNotification( mFilename, this, &Prefab::_onFileChanged );
 

+ 4 - 4
Engine/source/T3D/trigger.cpp

@@ -173,7 +173,7 @@ Trigger::Trigger()
    mPhysicsRep = NULL;
    mTripOnce = false;
    mTrippedBy = 0xFFFFFFFF;
-   mTripCondition = "";
+   mTripIf = "";
 
    //Default up a basic square
    Point3F vecs[3] = { Point3F(1.0, 0.0, 0.0),
@@ -379,7 +379,7 @@ void Trigger::initPersistFields()
       "representing the edges extending from the corner.\n");
 
    addField("TripOnce", TypeBool, Offset(mTripOnce, Trigger),"Do we trigger callacks just the once?");
-   addField("TripCondition", TypeRealString, Offset(mTripCondition, Trigger),"evaluation condition to trip callbacks (true/false)");
+   addField("tripIf", TypeRealString, Offset(mTripIf, Trigger),"evaluation condition to trip callbacks (true/false)");
    addField("TrippedBy", TypeGameTypeMasksType, Offset(mTrippedBy, Trigger), "typemask filter");
    addProtectedField("enterCommand", TypeCommand, Offset(mEnterCommand, Trigger), &setEnterCmd, &defaultProtectedGetFn,
       "The command to execute when an object enters this trigger. Object id stored in %%obj. Maximum 1023 characters." );
@@ -697,13 +697,13 @@ bool Trigger::testTrippable()
 
 bool Trigger::testCondition()
 {
-   if (mTripCondition.isEmpty())
+   if (mTripIf.isEmpty())
       return true; //we've got no tests to run so just do it
 
    //test the mapper plugged in condition line
    String resVar = getIdString() + String(".result");
    Con::setBoolVariable(resVar.c_str(), false);
-   String command = resVar + "=" + mTripCondition + ";";
+   String command = resVar + "=" + mTripIf + ";";
    Con::evaluatef(command.c_str());
    if (Con::getBoolVariable(resVar.c_str()) == 1)
    {

+ 1 - 1
Engine/source/T3D/trigger.h

@@ -87,7 +87,7 @@ class Trigger : public GameBase
    bool              mTripped;
    S32               mTrippedBy;
 
-   String            mTripCondition;
+   String            mTripIf;
    String            mEnterCommand;
    String            mLeaveCommand;
    String            mTickCommand;

+ 1 - 0
Engine/source/console/consoleObject.h

@@ -498,6 +498,7 @@ public:
       FIELD_ComponentInspectors = BIT(1),       ///< Custom fields used by components. They are likely to be non-standard size/configuration, so 
                                                 ///< They are handled specially
       FIELD_CustomInspectors = BIT(2),          ///< Display as a button in inspectors.
+      FIELD_SpecialtyArrayField = BIT(3)
    };
 
    struct Field

+ 303 - 109
Engine/source/console/persistenceManager.cpp

@@ -652,6 +652,31 @@ S32 PersistenceManager::getPropertyIndex(ParsedObject* parsedObject, const char*
    return propertyIndex;
 }
 
+S32 PersistenceManager::getSpecialPropertyAtOffset(ParsedObject* parsedObject, const char* fieldName, U32 offsetPos)
+{
+   S32 propertyIndex = -1;
+
+   if (!parsedObject)
+      return propertyIndex;
+
+   U32 hitCount = -1;
+   for (U32 i = 0; i < parsedObject->properties.size(); i++)
+   {
+      if (dStricmp(fieldName, parsedObject->properties[i].name) == 0)
+      {
+         hitCount++;
+
+         if (hitCount == offsetPos)
+         {
+            propertyIndex = i;
+            break;
+         }
+      }
+   }
+
+   return propertyIndex;
+}
+
 char* PersistenceManager::getObjectIndent(ParsedObject* object)
 {
    char* indent = Con::getReturnBuffer(2048);
@@ -1361,166 +1386,335 @@ void PersistenceManager::updateObject(SimObject* object, ParsedObject* parentObj
       if ( f->type >= AbstractClassRep::ARCFirstCustomField || f->flag.test(AbstractClassRep::FieldFlags::FIELD_ComponentInspectors))
          continue;
 
-      for(U32 j = 0; S32(j) < f->elementCount; j++)
+      if (f->flag.test(AbstractClassRep::FIELD_SpecialtyArrayField))
       {
-         const char* value = getFieldValue(object, f->pFieldname, j);
+         U32 fieldArraySize = object->getSpecialFieldSize(f->pFieldname);
 
-         // Make sure we got a value
-         if (!value)
-            continue;
+         for(U32 j = 0; j < fieldArraySize; j++)
+         {
+            const char* value = object->getSpecialFieldOut(f->pFieldname, j);
 
-         // Let's see if this field is already in the file
-         S32 propertyIndex = getPropertyIndex(parsedObject, f->pFieldname, j);
+            // Make sure we got a value
+            if (!value)
+               continue;
 
-         if (propertyIndex > -1)
-         {
-            ParsedProperty& prop = parsedObject->properties[propertyIndex];
+            // Let's see if this field is already in the file
+            S32 propertyIndex = getSpecialPropertyAtOffset(parsedObject, f->pFieldname, j);
 
-            // If this field is on the remove list then remove it and continue
-            if (findRemoveField(object, f->pFieldname, j) || !object->writeField(f->pFieldname, value))
+            if (propertyIndex > -1)
             {
-               removeField( parsedObject->properties[ propertyIndex ] );
-               dFree( value );
-               continue;
-            }
+               ParsedProperty& prop = parsedObject->properties[propertyIndex];
 
-            // Run the parsed value through the console system conditioners so
-            // that it will better match the data we got back from the object.
-            const char* evalue = Con::getFormattedData(f->type, prop.value, f->table, f->flag);
-
-            // If our data doesn't match then we get to update it.
-            //
-            // As for copy-sources, we just assume here that if a property setting
-            // is there in the file, the user does not want it inherited from the copy-source
-            // even in the case the actual values are identical.
-            
-            if( dStricmp(value, evalue) != 0 )
+               // If this field is on the remove list then remove it and continue
+               if (findRemoveField(object, f->pFieldname, j))
+               {
+                  removeField(parsedObject->properties[propertyIndex]);
+                  dFree(value);
+                  continue;
+               }
+
+               // Run the parsed value through the console system conditioners so
+               // that it will better match the data we got back from the object.
+               const char* evalue = Con::getFormattedData(f->type, prop.value, f->table, f->flag);
+
+               // If our data doesn't match then we get to update it.
+               //
+               // As for copy-sources, we just assume here that if a property setting
+               // is there in the file, the user does not want it inherited from the copy-source
+               // even in the case the actual values are identical.
+
+               if (dStricmp(value, evalue) != 0)
+               {
+                  if (value[0] == '\0' &&
+                     dStricmp(getFieldValue(defaultObject, f->pFieldname, j), value) == 0 &&
+                     (!object->getCopySource() || dStricmp(getFieldValue(object->getCopySource(), f->pFieldname, j), value) == 0))
+                  {
+                     removeField(prop);
+                  }
+                  else
+                  {
+                     // TODO: This should be wrapped in a helper method... probably.
+                     // Detect and collapse relative path information
+                     if (f->type == TypeFilename ||
+                        f->type == TypeStringFilename ||
+                        f->type == TypeImageFilename ||
+                        f->type == TypePrefabFilename ||
+                        f->type == TypeShapeFilename ||
+                        f->type == TypeSoundFilename)
+                     {
+                        char fnBuf[1024];
+                        Con::collapseScriptFilename(fnBuf, 1024, value);
+
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, fnBuf, true);
+                     }
+                     else if (f->type == TypeCommand || f->type == TypeString || f->type == TypeRealString)
+                     {
+                        char cmdBuf[1024];
+                        expandEscape(cmdBuf, value);
+
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, cmdBuf, true);
+                     }
+                     else
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, value, true);
+                  }
+               }
+            }
+            else
             {
-               if( value[ 0 ] == '\0' &&
-                   dStricmp( getFieldValue( defaultObject, f->pFieldname, j ), value ) == 0 &&
-                   ( !object->getCopySource() || dStricmp( getFieldValue( object->getCopySource(), f->pFieldname, j ), value ) == 0 ) )
+               // No need to process a removed field that doesn't exist in the file
+               if (findRemoveField(object, f->pFieldname, j))
+               {
+                  dFree(value);
+                  continue;
+               }
+
+               bool mustUpdate = false;
+
+               // If we didn't find the property in the ParsedObject
+               // then we need to compare against the default value
+               // for this property and save it out if it is different
+
+               const char* defaultValue = defaultObject->getSpecialFieldOut(f->pFieldname, j);
+               if (!defaultValue || dStricmp(value, defaultValue) != 0)
                {
-                  removeField( prop );
+                  // Value differs.  Check whether it also differs from the
+                  // value in the copy source if there is one.
+
+                  if (object->getCopySource())
+                  {
+                     const char* copySourceValue = getFieldValue(object->getCopySource(), f->pFieldname, j);
+                     if (!copySourceValue || dStricmp(copySourceValue, value) != 0)
+                        mustUpdate = true;
+
+                     if (copySourceValue)
+                        dFree(copySourceValue);
+                  }
+                  else
+                     mustUpdate = true;
                }
                else
+               {
+                  // Value does not differ.  If it differs from the copy source's value,
+                  // though, we still want to write it out as otherwise we'll see the
+                  // copy source's value override us.
+
+                  if (object->getCopySource())
+                  {
+                     const char* copySourceValue = getFieldValue(object->getCopySource(), f->pFieldname, j);
+                     if (copySourceValue && dStricmp(copySourceValue, value) != 0)
+                        mustUpdate = true;
+
+                     if (copySourceValue)
+                        dFree(copySourceValue);
+                  }
+               }
+
+               // The default value for most string type fields is
+               // NULL so we can't just continue here or we'd never ever
+               // write them out...
+               //
+               //if (!defaultValue)
+               //   continue;
+
+               // If the object's value is different from the default
+               // value then add it to the ParsedObject's newLines                        
+               if (mustUpdate)
                {
                   // TODO: This should be wrapped in a helper method... probably.
                   // Detect and collapse relative path information
-                  if (f->type == TypeFilename       ||
-                      f->type == TypeStringFilename ||
-                      f->type == TypeImageFilename  ||
-                      f->type == TypePrefabFilename ||
-                      f->type == TypeShapeFilename  ||
-                      f->type == TypeSoundFilename )
+                  if (f->type == TypeFilename ||
+                     f->type == TypeStringFilename ||
+                     f->type == TypeImageFilename ||
+                     f->type == TypePrefabFilename ||
+                     f->type == TypeShapeFilename ||
+                     f->type == TypeSoundFilename)
                   {
                      char fnBuf[1024];
                      Con::collapseScriptFilename(fnBuf, 1024, value);
 
-                     updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, fnBuf, true);
+                     newLines.push_back(createNewProperty(f->pFieldname, fnBuf, f->elementCount > 1, j));
                   }
-                  else if (f->type == TypeCommand || f->type == TypeString || f->type == TypeRealString)
+                  else if (f->type == TypeCommand)
                   {
                      char cmdBuf[1024];
                      expandEscape(cmdBuf, value);
 
-                     updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, cmdBuf, true);
+                     newLines.push_back(createNewProperty(f->pFieldname, cmdBuf, f->elementCount > 1, j));
                   }
                   else
-                     updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, value, true);
+                     newLines.push_back(createNewProperty(f->pFieldname, value, f->elementCount > 1, j));
                }
+
+               if (defaultValue)
+                  dFree(defaultValue);
             }
+
+            //dFree(value);
          }
-         else
+      }
+      else
+      {
+         for (U32 j = 0; S32(j) < f->elementCount; j++)
          {
-            // No need to process a removed field that doesn't exist in the file
-            if (findRemoveField(object, f->pFieldname, j) || !object->writeField(f->pFieldname, value))
-            {
-               dFree( value );
+            const char* value = getFieldValue(object, f->pFieldname, j);
+
+            // Make sure we got a value
+            if (!value)
                continue;
-            }
-            
-            bool mustUpdate = false;
 
-            // If we didn't find the property in the ParsedObject
-            // then we need to compare against the default value
-            // for this property and save it out if it is different
+            // Let's see if this field is already in the file
+            S32 propertyIndex = getPropertyIndex(parsedObject, f->pFieldname, j);
 
-            const char* defaultValue = getFieldValue(defaultObject, f->pFieldname, j);
-            if( !defaultValue || dStricmp( value, defaultValue ) != 0 )
+            if (propertyIndex > -1)
             {
-               // Value differs.  Check whether it also differs from the
-               // value in the copy source if there is one.
-               
-               if( object->getCopySource() )
+               ParsedProperty& prop = parsedObject->properties[propertyIndex];
+
+               // If this field is on the remove list then remove it and continue
+               if (findRemoveField(object, f->pFieldname, j) || !object->writeField(f->pFieldname, value))
                {
-                  const char* copySourceValue = getFieldValue( object->getCopySource(), f->pFieldname, j );
-                  if( !copySourceValue || dStricmp( copySourceValue, value ) != 0 )
-                     mustUpdate = true;
-                     
-                  if( copySourceValue )
-                     dFree( copySourceValue );
+                  removeField(parsedObject->properties[propertyIndex]);
+                  dFree(value);
+                  continue;
+               }
+
+               // Run the parsed value through the console system conditioners so
+               // that it will better match the data we got back from the object.
+               const char* evalue = Con::getFormattedData(f->type, prop.value, f->table, f->flag);
+
+               // If our data doesn't match then we get to update it.
+               //
+               // As for copy-sources, we just assume here that if a property setting
+               // is there in the file, the user does not want it inherited from the copy-source
+               // even in the case the actual values are identical.
+
+               if (dStricmp(value, evalue) != 0)
+               {
+                  if (value[0] == '\0' &&
+                     dStricmp(getFieldValue(defaultObject, f->pFieldname, j), value) == 0 &&
+                     (!object->getCopySource() || dStricmp(getFieldValue(object->getCopySource(), f->pFieldname, j), value) == 0))
+                  {
+                     removeField(prop);
+                  }
+                  else
+                  {
+                     // TODO: This should be wrapped in a helper method... probably.
+                     // Detect and collapse relative path information
+                     if (f->type == TypeFilename ||
+                        f->type == TypeStringFilename ||
+                        f->type == TypeImageFilename ||
+                        f->type == TypePrefabFilename ||
+                        f->type == TypeShapeFilename ||
+                        f->type == TypeSoundFilename)
+                     {
+                        char fnBuf[1024];
+                        Con::collapseScriptFilename(fnBuf, 1024, value);
+
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, fnBuf, true);
+                     }
+                     else if (f->type == TypeCommand || f->type == TypeString || f->type == TypeRealString)
+                     {
+                        char cmdBuf[1024];
+                        expandEscape(cmdBuf, value);
+
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, cmdBuf, true);
+                     }
+                     else
+                        updateToken(prop.valueLine, prop.valuePosition, prop.endPosition - prop.valuePosition, value, true);
+                  }
                }
-               else
-                  mustUpdate = true;
             }
             else
             {
-               // Value does not differ.  If it differs from the copy source's value,
-               // though, we still want to write it out as otherwise we'll see the
-               // copy source's value override us.
-               
-               if( object->getCopySource() )
+               // No need to process a removed field that doesn't exist in the file
+               if (findRemoveField(object, f->pFieldname, j) || !object->writeField(f->pFieldname, value))
                {
-                  const char* copySourceValue = getFieldValue( object->getCopySource(), f->pFieldname, j );
-                  if( copySourceValue && dStricmp( copySourceValue, value ) != 0 )
-                     mustUpdate = true;
-                     
-                  if( copySourceValue )
-                     dFree( copySourceValue );
+                  dFree(value);
+                  continue;
                }
-            }
 
-            // The default value for most string type fields is
-            // NULL so we can't just continue here or we'd never ever
-            // write them out...
-            //
-            //if (!defaultValue)
-            //   continue;
+               bool mustUpdate = false;
 
-            // If the object's value is different from the default
-            // value then add it to the ParsedObject's newLines                        
-            if ( mustUpdate )
-            {
-               // TODO: This should be wrapped in a helper method... probably.
-               // Detect and collapse relative path information
-               if (f->type == TypeFilename       ||
-                   f->type == TypeStringFilename ||
-                   f->type == TypeImageFilename  ||
-                   f->type == TypePrefabFilename ||
-                   f->type == TypeShapeFilename  ||
-                   f->type == TypeSoundFilename )
+               // If we didn't find the property in the ParsedObject
+               // then we need to compare against the default value
+               // for this property and save it out if it is different
+
+               const char* defaultValue = getFieldValue(defaultObject, f->pFieldname, j);
+               if (!defaultValue || dStricmp(value, defaultValue) != 0)
                {
-                  char fnBuf[1024];
-                  Con::collapseScriptFilename(fnBuf, 1024, value);
+                  // Value differs.  Check whether it also differs from the
+                  // value in the copy source if there is one.
+
+                  if (object->getCopySource())
+                  {
+                     const char* copySourceValue = getFieldValue(object->getCopySource(), f->pFieldname, j);
+                     if (!copySourceValue || dStricmp(copySourceValue, value) != 0)
+                        mustUpdate = true;
 
-                  newLines.push_back(createNewProperty(f->pFieldname, fnBuf, f->elementCount > 1, j));
+                     if (copySourceValue)
+                        dFree(copySourceValue);
+                  }
+                  else
+                     mustUpdate = true;
                }
-               else if (f->type == TypeCommand)
+               else
                {
-                  char cmdBuf[1024];
-                  expandEscape(cmdBuf, value);
+                  // Value does not differ.  If it differs from the copy source's value,
+                  // though, we still want to write it out as otherwise we'll see the
+                  // copy source's value override us.
 
-                  newLines.push_back(createNewProperty(f->pFieldname, cmdBuf, f->elementCount > 1, j));
+                  if (object->getCopySource())
+                  {
+                     const char* copySourceValue = getFieldValue(object->getCopySource(), f->pFieldname, j);
+                     if (copySourceValue && dStricmp(copySourceValue, value) != 0)
+                        mustUpdate = true;
+
+                     if (copySourceValue)
+                        dFree(copySourceValue);
+                  }
                }
-               else
-                  newLines.push_back(createNewProperty(f->pFieldname, value, f->elementCount > 1, j));              
+
+               // The default value for most string type fields is
+               // NULL so we can't just continue here or we'd never ever
+               // write them out...
+               //
+               //if (!defaultValue)
+               //   continue;
+
+               // If the object's value is different from the default
+               // value then add it to the ParsedObject's newLines                        
+               if (mustUpdate)
+               {
+                  // TODO: This should be wrapped in a helper method... probably.
+                  // Detect and collapse relative path information
+                  if (f->type == TypeFilename ||
+                     f->type == TypeStringFilename ||
+                     f->type == TypeImageFilename ||
+                     f->type == TypePrefabFilename ||
+                     f->type == TypeShapeFilename ||
+                     f->type == TypeSoundFilename)
+                  {
+                     char fnBuf[1024];
+                     Con::collapseScriptFilename(fnBuf, 1024, value);
+
+                     newLines.push_back(createNewProperty(f->pFieldname, fnBuf, f->elementCount > 1, j));
+                  }
+                  else if (f->type == TypeCommand)
+                  {
+                     char cmdBuf[1024];
+                     expandEscape(cmdBuf, value);
+
+                     newLines.push_back(createNewProperty(f->pFieldname, cmdBuf, f->elementCount > 1, j));
+                  }
+                  else
+                     newLines.push_back(createNewProperty(f->pFieldname, value, f->elementCount > 1, j));
+               }
+
+               if (defaultValue)
+                  dFree(defaultValue);
             }
 
-            if (defaultValue)
-               dFree( defaultValue );
+            dFree(value);
          }
-
-         dFree( value );
       }
    }
 

+ 7 - 1
Engine/source/console/persistenceManager.h

@@ -221,6 +221,12 @@ protected:
    // Attempts to look up the property in the ParsedObject
    S32 getPropertyIndex(ParsedObject* parsedObject, const char* fieldName, U32 arrayPos = 0);
 
+   // Attempts to look up the special array property in the ParsedObject
+   // This is distinct from getPropertyIndex because while that assumes it's an array'd field
+   // This figures the property in question is one that is specially tagged for implicit, arbitrarily sized lists
+   // Like ConvexShape's 'surfaces' or a spline's 'node' properties
+   S32 getSpecialPropertyAtOffset(ParsedObject* parsedObject, const char* fieldName, U32 offsetPos);
+
    // Gets the amount of indent on the ParsedObject.
    char* getObjectIndent(ParsedObject* object);
 
@@ -320,4 +326,4 @@ public:
    DECLARE_CONOBJECT(PersistenceManager);
 };
 
-#endif
+#endif

+ 12 - 0
Engine/source/console/simObject.cpp

@@ -101,6 +101,8 @@ SimObjectId SimObject::smForcedId = 0;
 bool SimObject::preventNameChanging = false;
 
 IMPLEMENT_CALLBACK(SimObject, onInspectPostApply, void, (SimObject* obj), (obj), "Generic callback for when an object is edited");
+IMPLEMENT_CALLBACK(SimObject, onSelected, void, (SimObject* obj), (obj), "Generic callback for when an object is selected");
+IMPLEMENT_CALLBACK(SimObject, onUnselected, void, (SimObject* obj), (obj), "Generic callback for when an object is un-selected");
 
 namespace Sim
 {
@@ -527,6 +529,14 @@ bool SimObject::save(const char *pcFileName, bool bOnlySelected, const char *pre
 
 }
 
+bool SimObject::saveAppend(const char* pcFileName, bool bOnlySelected, const char* preappend)
+{
+   
+
+   return true;
+
+}
+
 //-----------------------------------------------------------------------------
 
 SimPersistID* SimObject::getOrCreatePersistentId()
@@ -2207,11 +2217,13 @@ void SimObject::setSelected( bool sel )
    {
       mFlags.set( Selected );
       _onSelected();
+      onSelected_callback(this);
    }
    else
    {
       mFlags.clear( Selected );
       _onUnselected();
+      onUnselected_callback(this);
    }
 }
 

+ 6 - 0
Engine/source/console/simObject.h

@@ -530,6 +530,9 @@ class SimObject: public ConsoleObject, public TamlCallbacks
       void setDataFieldType(const U32 fieldTypeId, StringTableEntry slotName, const char *array);
       void setDataFieldType(const char *typeName, StringTableEntry slotName, const char *array);
 
+      virtual U32 getSpecialFieldSize(StringTableEntry fieldName) { return 0; }
+      virtual const char* getSpecialFieldOut(StringTableEntry fieldName, const U32& index) { return NULL; }
+
       /// Get reference to the dictionary containing dynamic fields.
       ///
       /// See @ref simobject_console "here" for a detailed discussion of what this
@@ -579,6 +582,7 @@ class SimObject: public ConsoleObject, public TamlCallbacks
 
       /// Save object as a TorqueScript File.
       virtual bool save( const char* pcFilePath, bool bOnlySelected = false, const char *preappend = NULL );
+      virtual bool saveAppend(const char* pcFilePath, bool bOnlySelected = false, const char* preappend = NULL);
 
       /// Check if a method exists in the objects current namespace.
       virtual bool isMethod( const char* methodName );
@@ -981,6 +985,8 @@ class SimObject: public ConsoleObject, public TamlCallbacks
       
       DECLARE_CONOBJECT( SimObject );
       DECLARE_CALLBACK(void, onInspectPostApply, (SimObject* obj));
+      DECLARE_CALLBACK(void, onSelected, (SimObject* obj));
+      DECLARE_CALLBACK(void, onUnselected, (SimObject* obj));
       
       static SimObject* __findObject( const char* id ) { return Sim::findObject( id ); }
       static const char* __getObjectId( ConsoleObject* object )

+ 31 - 1
Engine/source/environment/decalRoad.cpp

@@ -322,7 +322,7 @@ void DecalRoad::initPersistFields()
    addGroup( "Internal" );
 
       addProtectedField( "node", TypeString, 0, &addNodeFromField, &emptyStringProtectedGetFn,
-         "Do not modify, for internal use." );
+         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
    endGroup( "Internal" );
 
@@ -473,6 +473,36 @@ bool DecalRoad::writeField( StringTableEntry fieldname, const char *value )
    return Parent::writeField( fieldname, value );
 }
 
+U32 DecalRoad::getSpecialFieldSize(StringTableEntry fieldName)
+{
+   if (fieldName == StringTable->insert("node"))
+   {
+      return mNodes.size();
+   }
+
+   return 0;
+}
+
+const char* DecalRoad::getSpecialFieldOut(StringTableEntry fieldName, const U32& index)
+{
+   if (fieldName == StringTable->insert("node"))
+   {
+      if (index >= mNodes.size())
+         return NULL;
+
+      const RoadNode& node = mNodes[index];
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+      dSprintf(buffer, 1024, "%f %f %f %f", node.point.x, node.point.y, node.point.z, node.width);
+
+      return StringTable->insert(buffer);
+   }
+
+   return NULL;
+}
+
+
 void DecalRoad::onEditorEnable()
 {
 }

+ 3 - 0
Engine/source/environment/decalRoad.h

@@ -169,6 +169,9 @@ public:
    void onStaticModified(const char* slotName, const char*newValue = NULL) override;
    void writeFields(Stream &stream, U32 tabStop) override;
    bool writeField( StringTableEntry fieldname, const char *value ) override;
+
+   U32 getSpecialFieldSize(StringTableEntry fieldName) override;
+   const char* getSpecialFieldOut(StringTableEntry fieldName, const U32& index) override;
    
 	// NetObject
 	U32 packUpdate(NetConnection *, U32, BitStream *) override;

+ 56 - 2
Engine/source/environment/meshRoad.cpp

@@ -956,10 +956,10 @@ void MeshRoad::initPersistFields()
    addGroup( "Internal" );
 
       addProtectedField( "Node", TypeString, 0, &addNodeFromField, &emptyStringProtectedGetFn,
-         "Do not modify, for internal use." );
+         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
       addProtectedField( "ProfileNode", TypeString, 0, &addProfileNodeFromField, &emptyStringProtectedGetFn,
-         "Do not modify, for internal use." );
+         "Do not modify, for internal use.", AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
    endGroup( "Internal" );
 
@@ -1168,6 +1168,60 @@ bool MeshRoad::writeField( StringTableEntry fieldname, const char *value )
    return Parent::writeField( fieldname, value );
 }
 
+U32 MeshRoad::getSpecialFieldSize(StringTableEntry fieldName)
+{
+   if (fieldName == StringTable->insert("Node"))
+   {
+      return mNodes.size();
+   }
+   else if (fieldName == StringTable->insert("ProfileNode"))
+   {
+      return mSideProfile.mNodes.size();
+   }
+
+   return 0;
+}
+
+const char* MeshRoad::getSpecialFieldOut(StringTableEntry fieldName, const U32& index)
+{
+   if (fieldName == StringTable->insert("Node"))
+   {
+      if (index >= mNodes.size())
+         return NULL;
+
+      const MeshRoadNode& node = mNodes[index];
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+      dSprintf(buffer, 1024, "Node = \"%g %g %g %g %g %g %g %g\";", node.point.x, node.point.y, node.point.z, node.width, node.depth, node.normal.x, node.normal.y, node.normal.z);
+
+      return StringTable->insert(buffer);
+   }
+   else if (fieldName == StringTable->insert("ProfileNode"))
+   {
+      Point3F nodePos = mSideProfile.mNodes[index].getPosition();
+      U8 smooth, mtrl;
+
+      if (index)
+         mtrl = mSideProfile.mSegMtrls[index - 1];
+      else
+         mtrl = 0;
+
+      if (mSideProfile.mNodes[index].isSmooth())
+         smooth = 1;
+      else
+         smooth = 0;
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+      dSprintf(buffer, 1024, "ProfileNode = \"%.6f %.6f %d %d\";", nodePos.x, nodePos.y, smooth, mtrl);
+
+      return StringTable->insert(buffer);
+   }
+
+   return NULL;
+}
+
 void MeshRoad::onEditorEnable()
 {
 }

+ 3 - 0
Engine/source/environment/meshRoad.h

@@ -525,6 +525,9 @@ public:
    void writeFields(Stream &stream, U32 tabStop) override;
    bool writeField( StringTableEntry fieldname, const char *value ) override;
 
+   U32 getSpecialFieldSize(StringTableEntry fieldName) override;
+   const char* getSpecialFieldOut(StringTableEntry fieldName, const U32& index) override;
+
    // NetObject
    U32 packUpdate(NetConnection *, U32, BitStream *) override;
    void unpackUpdate(NetConnection *, BitStream *) override;

+ 34 - 1
Engine/source/environment/river.cpp

@@ -648,7 +648,8 @@ void River::initPersistFields()
 
    addGroup( "Internal" );
 
-      addProtectedField( "Node", TypeString, 0, &addNodeFromField, &emptyStringProtectedGetFn, "For internal use, do not modify." );
+      addProtectedField( "Node", TypeString, 0, &addNodeFromField, &emptyStringProtectedGetFn, "For internal use, do not modify.",
+         AbstractClassRep::FIELD_HideInInspectors | AbstractClassRep::FIELD_SpecialtyArrayField);
 
    endGroup( "Internal" );
 
@@ -785,6 +786,38 @@ bool River::writeField( StringTableEntry fieldname, const char *value )
    return Parent::writeField( fieldname, value );
 }
 
+U32 River::getSpecialFieldSize(StringTableEntry fieldName)
+{
+   if (fieldName == StringTable->insert("node"))
+   {
+      return mNodes.size();
+   }
+
+   return 0;
+}
+
+const char* River::getSpecialFieldOut(StringTableEntry fieldName, const U32& index)
+{
+   if (fieldName == StringTable->insert("node"))
+   {
+      if (index >= mNodes.size())
+         return NULL;
+
+      const RiverNode& node = mNodes[index];
+
+      char buffer[1024];
+      dMemset(buffer, 0, 1024);
+      dSprintf(buffer, 1024, "Node = \"%f %f %f %f %f %f %f %f\";", node.point.x, node.point.y, node.point.z,
+         node.width,
+         node.depth,
+         node.normal.x, node.normal.y, node.normal.z);
+
+      return StringTable->insert(buffer);
+   }
+
+   return NULL;
+}
+
 void River::innerRender( SceneRenderState *state )
 {   
    GFXDEBUGEVENT_SCOPE( River_innerRender, ColorI( 255, 0, 0 ) );

+ 3 - 0
Engine/source/environment/river.h

@@ -394,6 +394,9 @@ public:
    void writeFields(Stream &stream, U32 tabStop) override;
    bool writeField( StringTableEntry fieldname, const char *value ) override;
 
+   U32 getSpecialFieldSize(StringTableEntry fieldName) override;
+   const char* getSpecialFieldOut(StringTableEntry fieldName, const U32& index) override;
+
    // NetObject
    U32 packUpdate(NetConnection *, U32, BitStream *) override;
    void unpackUpdate(NetConnection *, BitStream *) override;

+ 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;
+      }
       
       /// @}
       

+ 44 - 34
Engine/source/gui/editor/inspector/group.cpp

@@ -178,7 +178,7 @@ GuiInspectorField* GuiInspectorGroup::constructField( S32 fieldType )
 
       // return our new datablock field with correct datablock type enumeration info
       return dbFieldClass;
-}
+   }
 
    // Nope, not a datablock. So maybe it has a valid inspector field override we can use?
    if(!cbt->getInspectorFieldType())
@@ -641,40 +641,50 @@ void GuiInspectorGroup::addInspectorField(StringTableEntry name, StringTableEntr
 {
    S32 fieldType = -1;
 
-   if (typeName == StringTable->insert("int"))
-      fieldType = TypeS32;
-   else if (typeName == StringTable->insert("float"))
-      fieldType = TypeF32;
-   else if (typeName == StringTable->insert("vector"))
-      fieldType = TypePoint3F;
-   else if (typeName == StringTable->insert("vector2"))
-      fieldType = TypePoint2F;
-   else if (typeName == StringTable->insert("material"))
-      fieldType = TypeMaterialAssetId;
-   else if (typeName == StringTable->insert("image"))
-      fieldType = TypeImageAssetId;
-   else if (typeName == StringTable->insert("shape"))
-      fieldType = TypeShapeAssetId;
-   else if (typeName == StringTable->insert("sound"))
-      fieldType = TypeSoundAssetId;
-   else if (typeName == StringTable->insert("bool"))
-      fieldType = TypeBool;
-   else if (typeName == StringTable->insert("object"))
-      fieldType = TypeSimObjectPtr;
-   else if (typeName == StringTable->insert("string"))
-      fieldType = TypeString;
-   else if (typeName == StringTable->insert("colorI"))
-      fieldType = TypeColorI;
-   else if (typeName == StringTable->insert("colorF"))
-      fieldType = TypeColorF;
-   else if (typeName == StringTable->insert("ease"))
-      fieldType = TypeEaseF;
-   else if (typeName == StringTable->insert("command"))
-      fieldType = TypeCommand;
-   else if (typeName == StringTable->insert("filename"))
-      fieldType = TypeStringFilename;
+   String typeNameTyped = typeName;
+   if (!typeNameTyped.startsWith("Type"))
+      typeNameTyped = String("Type") + typeNameTyped;
+
+   ConsoleBaseType* typeRef = AbstractClassRep::getTypeByName(typeNameTyped.c_str());
+   if(typeRef)
+   {
+      fieldType = typeRef->getTypeID();
+   }
    else
-      fieldType = -1;
+   {
+      if (typeName == StringTable->insert("int"))
+         fieldType = TypeS32;
+      else if (typeName == StringTable->insert("float"))
+         fieldType = TypeF32;
+      else if (typeName == StringTable->insert("vector"))
+         fieldType = TypePoint3F;
+      else if (typeName == StringTable->insert("vector2"))
+         fieldType = TypePoint2F;
+      else if (typeName == StringTable->insert("material"))
+         fieldType = TypeMaterialAssetId;
+      else if (typeName == StringTable->insert("image"))
+         fieldType = TypeImageAssetId;
+      else if (typeName == StringTable->insert("shape"))
+         fieldType = TypeShapeAssetId;
+      else if (typeName == StringTable->insert("sound"))
+         fieldType = TypeSoundAssetId;
+      else if (typeName == StringTable->insert("bool"))
+         fieldType = TypeBool;
+      else if (typeName == StringTable->insert("object"))
+         fieldType = TypeSimObjectPtr;
+      else if (typeName == StringTable->insert("string"))
+         fieldType = TypeString;
+      else if (typeName == StringTable->insert("colorI"))
+         fieldType = TypeColorI;
+      else if (typeName == StringTable->insert("colorF"))
+         fieldType = TypeColorF;
+      else if (typeName == StringTable->insert("ease"))
+         fieldType = TypeEaseF;
+      else if (typeName == StringTable->insert("command"))
+         fieldType = TypeCommand;
+      else if (typeName == StringTable->insert("filename"))
+         fieldType = TypeStringFilename;
+   }
 
    GuiInspectorField* fieldGui;
 

+ 46 - 31
Engine/source/gui/editor/inspector/variableInspector.cpp

@@ -176,38 +176,53 @@ void GuiVariableInspector::addField(const char* name, const char* label, const c
    //find the field type
    S32 fieldTypeMask = -1;
 
-   if (newField->mFieldTypeName == StringTable->insert("int"))
-      fieldTypeMask = TypeS32;
-   else if (newField->mFieldTypeName == StringTable->insert("float"))
-      fieldTypeMask = TypeF32;
-   else if (newField->mFieldTypeName == StringTable->insert("vector"))
-      fieldTypeMask = TypePoint3F;
-   else if (newField->mFieldTypeName == StringTable->insert("vector2"))
-      fieldTypeMask = TypePoint2F;
-   else if (newField->mFieldTypeName == StringTable->insert("material"))
-      fieldTypeMask = TypeMaterialAssetId;
-   else if (newField->mFieldTypeName == StringTable->insert("image"))
-      fieldTypeMask = TypeImageAssetId;
-   else if (newField->mFieldTypeName == StringTable->insert("shape"))
-      fieldTypeMask = TypeShapeAssetId;
-   else if (newField->mFieldTypeName == StringTable->insert("bool"))
-      fieldTypeMask = TypeBool;
-   else if (newField->mFieldTypeName == StringTable->insert("object"))
-      fieldTypeMask = TypeSimObjectPtr;
-   else if (newField->mFieldTypeName == StringTable->insert("string"))
-      fieldTypeMask = TypeString;
-   else if (newField->mFieldTypeName == StringTable->insert("colorI"))
-      fieldTypeMask = TypeColorI;
-   else if (newField->mFieldTypeName == StringTable->insert("colorF"))
-      fieldTypeMask = TypeColorF;
-   else if (newField->mFieldTypeName == StringTable->insert("ease"))
-      fieldTypeMask = TypeEaseF;
-   else if (newField->mFieldTypeName == StringTable->insert("command"))
-      fieldTypeMask = TypeCommand;
-   else if (newField->mFieldTypeName == StringTable->insert("filename"))
-      fieldTypeMask = TypeStringFilename;
+   String typeNameTyped = typeName;
+   if (!typeNameTyped.startsWith("Type"))
+      typeNameTyped = String("Type") + typeNameTyped;
+
+   ConsoleBaseType* typeRef = AbstractClassRep::getTypeByName(typeNameTyped.c_str());
+   if (typeRef)
+   {
+      fieldTypeMask = typeRef->getTypeID();
+
+      if (!typeRef->getInspectorFieldType())
+         fieldTypeMask = TypeString;
+
+      newField->mFieldTypeName = StringTable->insert(typeRef->getTypeName());
+   }
    else
-      fieldTypeMask = -1;
+   {
+      if (newField->mFieldTypeName == StringTable->insert("int"))
+         fieldTypeMask = TypeS32;
+      else if (newField->mFieldTypeName == StringTable->insert("float"))
+         fieldTypeMask = TypeF32;
+      else if (newField->mFieldTypeName == StringTable->insert("vector"))
+         fieldTypeMask = TypePoint3F;
+      else if (newField->mFieldTypeName == StringTable->insert("vector2"))
+         fieldTypeMask = TypePoint2F;
+      else if (newField->mFieldTypeName == StringTable->insert("material"))
+         fieldTypeMask = TypeMaterialAssetId;
+      else if (newField->mFieldTypeName == StringTable->insert("image"))
+         fieldTypeMask = TypeImageAssetId;
+      else if (newField->mFieldTypeName == StringTable->insert("shape"))
+         fieldTypeMask = TypeShapeAssetId;
+      else if (newField->mFieldTypeName == StringTable->insert("bool"))
+         fieldTypeMask = TypeBool;
+      else if (newField->mFieldTypeName == StringTable->insert("object"))
+         fieldTypeMask = TypeSimObjectPtr;
+      else if (newField->mFieldTypeName == StringTable->insert("string"))
+         fieldTypeMask = TypeString;
+      else if (newField->mFieldTypeName == StringTable->insert("colorI"))
+         fieldTypeMask = TypeColorI;
+      else if (newField->mFieldTypeName == StringTable->insert("colorF"))
+         fieldTypeMask = TypeColorF;
+      else if (newField->mFieldTypeName == StringTable->insert("ease"))
+         fieldTypeMask = TypeEaseF;
+      else if (newField->mFieldTypeName == StringTable->insert("command"))
+         fieldTypeMask = TypeCommand;
+      else if (newField->mFieldTypeName == StringTable->insert("filename"))
+         fieldTypeMask = TypeStringFilename;
+   }
 
    newField->mFieldType = fieldTypeMask;
    //

+ 6 - 1
Engine/source/scene/sceneObject.cpp

@@ -1724,7 +1724,7 @@ void SceneObject::updateRenderChangesByParent(){
 		MatrixF offset;
 		offset.mul(renderXform, xform);
 
-   	    MatrixF mat;
+      MatrixF mat;
 		
 		//add the "offset" caused by the parents change, and add it to it's own
 		// This is needed by objects that update their own render transform thru interpolate tick
@@ -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();
 }
 
 //------------------------------------------------------------------------------

+ 14 - 44
Templates/BaseGame/game/core/utility/scripts/scene.tscript

@@ -1,52 +1,22 @@
 function callGamemodeFunction(%gameModeFuncName, %arg0, %arg1, %arg2, %arg3, %arg4, %arg5, %arg6)
 {
-   %activeSceneCount = getSceneCount();
-      
-   %hasGameMode = 0;
-   for(%i=0; %i < %activeSceneCount; %i++)
+   %validGameModeCall = false;
+   %gamemodeList = getGameModesList();
+   %gameModeCount = %gamemodeList.count();
+   for(%i=0; %i < %gameModeCount; %i++)
    {
-      %gamemodeName = getScene(%i).gameModeName;
-      if(%gamemodeName !$= "")
+      %gameModeObj = %gamemodeList.getKey(%i);
+      %active = %gamemodeList.getValue(%i);
+      
+      if(!isObject(%gameModeObj) || !%active)
+         continue;
+      
+      if(%gameModeObj.isMethod(%gameModeFuncName))
       {
-         //if the scene defines a game mode, go ahead and envoke it here
-         if(isObject(%gamemodeName) && %gamemodeName.isMethod(%gameModeFuncName))
-         {
-            eval(%gamemodeName @ "."@%gameModeFuncName@"(\""@%arg0@"\", \""@%arg1@"\", \""@%arg2@"\", \""@%arg3@"\", \""@%arg4@"\", \""@%arg5@"\", \""@%arg6@"\");" );
-            %hasGameMode = 1;
-         }
-         else
-         {
-            //if we don't have an object, attempt the static call  
-            if(isMethod(%gamemodeName, %gameModeFuncName))
-            {
-               eval(%gamemodeName @ "::"@%gameModeFuncName@"(\""@%arg0@"\", \""@%arg1@"\", \""@%arg2@"\", \""@%arg3@"\", \""@%arg4@"\", \""@%arg5@"\", \""@%arg6@"\");" );
-               %hasGameMode = 1;
-            }
-         }
+         eval(%gameModeObj @ "."@%gameModeFuncName@"(\""@%arg0@"\", \""@%arg1@"\", \""@%arg2@"\", \""@%arg3@"\", \""@%arg4@"\", \""@%arg5@"\", \""@%arg6@"\");" );
+         %validGameModeCall = true;
       }
    }
    
-   //if none of our scenes have gamemodes, we need to kick off a default
-   if(%hasGameMode == 0)
-   {
-      %defaultModeName = ProjectSettings.value("Gameplay/GameModes/defaultModeName");
-      if(%defaultModeName !$= "")
-      {
-         if(isObject(%defaultModeName) && %defaultModeName.isMethod(%gameModeFuncName))
-         {
-            eval(%defaultModeName @ "."@%gameModeFuncName@"(\""@%arg0@"\", \""@%arg1@"\", \""@%arg2@"\", \""@%arg3@"\", \""@%arg4@"\", \""@%arg5@"\", \""@%arg6@"\");" );
-            %hasGameMode = 1;
-         }
-         else
-         {
-            if(isMethod(%defaultModeName, %gameModeFuncName))
-            {
-               eval(%defaultModeName @ "::"@%gameModeFuncName@"(\""@%arg0@"\", \""@%arg1@"\", \""@%arg2@"\", \""@%arg3@"\", \""@%arg4@"\", \""@%arg5@"\", \""@%arg6@"\");" );
-               %hasGameMode = 1;
-            }  
-         }
-      }
-   }  
-   
-   return %hasGameMode;
+   return %validGameModeCall;
 }

+ 4 - 15
Templates/BaseGame/game/data/ExampleModule/ExampleModule.tscript

@@ -9,25 +9,12 @@ function ExampleModule::onDestroy(%this)
 //This is called when the server is initially set up by the game application
 function ExampleModule::initServer(%this)
 {
-   %this.queueExec("./scripts/server/ExampleGameMode");
+   %this.queueExec("./scripts/shared/ExampleGameMode");
 }
 
 //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
@@ -38,7 +25,7 @@ function ExampleModule::onDestroyGameServer(%this)
 //This is called when the client is initially set up by the game application
 function ExampleModule::initClient(%this)
 {
-    %this.queueExec("./scripts/client/inputCommands");
+   %this.queueExec("./scripts/client/inputCommands");
     
     //client scripts
    exec("./scripts/client/defaultkeybinds");
@@ -46,6 +33,8 @@ function ExampleModule::initClient(%this)
    %prefPath = getPrefpath();
    if(isScriptFile(%prefPath @ "/keybinds"))
       exec(%prefPath @ "/keybinds");
+      
+   %this.queueExec("./scripts/shared/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"/>

+ 1 - 1
Templates/BaseGame/game/data/ExampleModule/levels/ExampleLevel.mis

@@ -1,7 +1,7 @@
 //--- OBJECT WRITE BEGIN ---
 new Scene(ExampleLevel) {
    Enabled = "1";
-   gameModeName="ExampleGameMode";
+   gameModes="ExampleGameMode";
 
    new LevelInfo(theLevelInfo) {
       fogColor = "0.6 0.6 0.7 1";

+ 26 - 8
Templates/BaseGame/game/data/ExampleModule/scripts/server/ExampleGameMode.tscript → Templates/BaseGame/game/data/ExampleModule/scripts/shared/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

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

@@ -9,8 +9,8 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
    tooltipProfile = "GuiToolTipProfile";
    isContainer = "1";
    canSaveDynamicFields = "1";
+      currentMenuIdx = "0";
       launchInEditor = "0";
-      previewButtonSize = "445 120";
 
    new GuiInputCtrl(ChooseLevelInputHandler) {
       ignoreMouseEvents = "1";
@@ -43,39 +43,50 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
       extent = "310 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";
+         visible = "0";
+         command = "ChooseLevelMenu.openMenu(2);";
+         tooltipProfile = "GuiToolTipProfile";
+         internalName = "ConfigBtn";
+         class = "ChooseLevelMenuButton";
+         hidden = "1";
+      };
    };
    new GuiControl(ChooseLevelMenuNavButtonOverlay) {
       position = "0 61";
       extent = "1281 60";
       horizSizing = "width";
       profile = "GuiNonModalDefaultProfile";
-      visible = "0";
       tooltipProfile = "GuiToolTipProfile";
       isContainer = "1";
-      hidden = "1";
 
       new GuiBitmapCtrl(ChooseLevelMenuPrevNavIcon) {
          BitmapAsset = "UI:Keyboard_Black_Q_image";
@@ -94,12 +105,66 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
          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 = "1 1";
+            extent = "443 60";
+            horizSizing = "width";
+            vertSizing = "height";
+            profile = "GuiMenuDefaultProfile";
+            tooltipProfile = "GuiToolTipProfile";
+         };
+      };
+      new GuiBitmapCtrl(GameModePreviewBitmap) {
+         position = "448 0";
+         extent = "440 440";
+         horizSizing = "left";
+         profile = "GuiNonModalDefaultProfile";
+         visible = "0";
+         tooltipProfile = "GuiToolTipProfile";
+         hidden = "1";
+      };
+      new GuiTextCtrl(GameModeNameText) {
+         text = "DeathMatchGame";
+         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";
@@ -112,7 +177,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
          new GuiStackControl(LevelPreviewArray) {
             padding = "5";
             position = "0 1";
-            extent = "445 120";
+            extent = "445 60";
             horizSizing = "width";
             vertSizing = "height";
             profile = "GuiMenuDefaultProfile";
@@ -214,7 +279,7 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
                   tooltipProfile = "GuiToolTipProfile";
                };
                new GuiTextEditCtrl(serverNameCTRL) {
-                  text = "";
+                  text = "Torque 3D Server";
                   position = "606 4";
                   extent = "295 22";
                   horizSizing = "left";
@@ -240,7 +305,6 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
                   tooltipProfile = "GuiToolTipProfile";
                };
                new GuiTextEditCtrl(serverPassCTRL) {
-                  text = "";
                   position = "606 4";
                   extent = "295 22";
                   horizSizing = "left";
@@ -313,8 +377,8 @@ $guiContent = new GuiControl(ChooseLevelMenu) {
       profile = "GuiMenuPanelProfile";
       tooltipProfile = "GuiToolTipProfile";
 
-      new GuiIconButtonCtrl(ChooseLevelStartBtn) {
-         BitmapAsset = "UI:Keyboard_Black_Return_image";
+      new GuiIconButtonCtrl(ChooseLevelNextBtn) {
+         BitmapAsset = "UI:Keyboard_Black_Blank_image";
          sizeIconToButton = "1";
          makeIconSquare = "1";
          textLocation = "Center";

+ 308 - 54
Templates/BaseGame/game/data/UI/guis/ChooseLevelMenu.tscript

@@ -25,7 +25,7 @@ function ChooseLevelMenu::onAdd( %this )
    if(!isObject(ChooseLevelAssetQuery))
       new AssetQuery(ChooseLevelAssetQuery);
       
-   %this.previewButtonSize = "445 120";
+   $LevelPreviewButtonSize = "445 60";
 }
 
 function ChooseLevelMenu::fillPrefEntries( %this )
@@ -40,8 +40,107 @@ function ChooseLevelMenu::fillPrefEntries( %this )
 function ChooseLevelMenu::onWake(%this)
 {
    %this.fillPrefEntries();
+   
+   refreshGameModesList();
+   refreshLevelsList();
+
+   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;
+      
+   //If we've only got one gamemode, just force it to use that one, as we can
+   //assume that is THE game's gamemode
+   %gamemodeList = getGameModesList();
+   if(%gamemodeList.count() == 1)
+   {
+      %gameModeObj = %gamemodeList.getKey(0);
+      if(isObject(%gameModeObj))
+      {
+         %gameModeObj.active = true;
+         
+         ChooseLevelMenuTabList-->LevelBtn.active = true;
+         ChooseLevelMenuTabList-->GameModeBtn.active = false;
+         
+         ChooseLevelMenu.openMenu(1);
+         
+         return;
+      }
+   }
+   else
+   {
+      //Otherwise open the gamemode list as normal
+      %this.schedule(32, openMenu, 0);
+   }
+}
+
+function refreshGameModesList()
+{
+   GameModePreviewArray.clear();
+   
+   $pref::Server::GameMode = "";
+   ChooseLevelMenuTabList-->LevelBtn.active = false;
+   
+   //popilate the gamemodes list first
+   %gamemodeList = getGameModesList();
+   %gameModeCount = %gamemodeList.count();
+   
+   for (%i=0; %i<%gameModeCount; %i++)
+   {
+      %gameModeObj = %gamemodeList.getKey(%i);
+      if(isObject(%gameModeObj))
+      {
+         %gameModeName = %gameModeObj.gameModeName;
+         if(%gameModeName $= "")
+            %gameModeName = %gameModeObj.getName();
+            
+         %preview = new GuiContainer() {
+            position = "0 0";
+            extent = $LevelPreviewButtonSize;
+            horizSizing = "right";
+            vertSizing = "bottom";
+            gameModeObj = %gameModeObj;
+            gameModeDesc = %gameModeObj.description;
+            previewImage = %gameModeObj.previewImage;
+            cansave = false;
+            
+            new GuiToggleButtonCtrl() {
+               position = $LevelPreviewButtonSize.y SPC 0;
+               extent = $LevelPreviewButtonSize.x - $LevelPreviewButtonSize.y - 5 SPC $LevelPreviewButtonSize.y;
+               profile = GuiMenuButtonProfile;
+               horizSizing = "right";
+               vertSizing = "bottom";
+               internalName = "button";
+               class = "GameModePreviewButton";
+               text = %gameModeName;
+               command = "ToggleGameMode(" @ %i @ ");"; //allow doubleclick to quick action it
+               textMargin = 120;
+            };
+            
+            new GuiBitmapCtrl() {
+               position = "0 0";
+               extent = $LevelPreviewButtonSize.y SPC $LevelPreviewButtonSize.y;
+               internalName = "checkbox";
+               bitmapAsset = "UI:toggleMarker_image";
+            };
+         };
+        
+         
+         GameModePreviewArray.add(%preview);
+      }
+   }
+}
+
+function refreshLevelsList()
+{
    LevelPreviewArray.clear();
    
+   //fetch the levelAssets   
    ChooseLevelAssetQuery.clear();
    AssetDatabase.findAssetType(ChooseLevelAssetQuery, "LevelAsset");
       
@@ -57,6 +156,10 @@ function ChooseLevelMenu::onWake(%this)
       return;
    }
    
+   %gameModesList = getGameModesList();
+   
+   //filter the levelAssets by the gamemode selected
+   %filteredIndex = 0;
    for(%i=0; %i < %count; %i++)
 	{
 	   %assetId = ChooseLevelAssetQuery.getAsset(%i);
@@ -71,6 +174,46 @@ function ChooseLevelMenu::onWake(%this)
       if ( !isFile(%file) )
          continue;
          
+      if( %levelAsset.isSubScene )
+         continue;
+         
+      //filter for selected gamemode
+      %levelGameModes = %levelAsset.gameModesNames;
+      
+      //If the level has no gamemodes defined, we just assume the default case of it being a wildcard to whatever gamemode
+      //is available
+      if(%levelGameModes $= "")
+      {
+         %foundGameModeMatch = true;
+      }
+      else
+      {
+         %foundGameModeMatch = false;
+         for(%gm = 0; %gm < getTokenCount(%levelGameModes, ";"); %gm++)
+         {
+            %gameModeName = getToken(%levelGameModes, ";", %gm);
+            for(%g=0; %g < %gameModesList.count(); %g++)
+            {
+               %gameModeObj = %gameModesList.getKey(%g);
+               
+               if(!isObject(%gameModeObj) || (!%gameModeObj.active && !%gameModeObj.alwaysActive && %gameModesList.count() > 1))
+                  continue;
+                  
+               if(%gameModeName $= %gameModeObj.getName())
+               {
+                  %foundGameModeMatch = true;
+                  break;  
+               }
+            }
+            
+            if(%foundGameModeMatch)
+               break;
+         }
+      }
+      
+      if(!%foundGameModeMatch)
+         continue;
+         
       %levelPreviewImg = %levelAsset.getPreviewImagePath();
    
       if (!isFile(%levelPreviewImg))
@@ -78,7 +221,7 @@ function ChooseLevelMenu::onWake(%this)
          
       %preview = new GuiIconButtonCtrl() {
          position = "0 0";
-         extent = %this.previewButtonSize;
+         extent = $LevelPreviewButtonSize;
          buttonType = "PushButton";
          profile = GuiMenuButtonLeftJustProfile;
          horizSizing = "right";
@@ -86,7 +229,7 @@ function ChooseLevelMenu::onWake(%this)
          internalName = "button";
          class = "LevelPreviewButton";
          //command = "$selectedLevelAsset = " @ %assetId @ ";";
-         altCommand = "ChooseLevelBegin(1);"; //allow doubleclick to quick action it
+         altCommand = "ChooseLevelBegin(" @ %filteredIndex @ ");"; //allow doubleclick to quick action it
          text = %levelAsset.levelName;
          bitmapAsset = %levelPreviewImg;
          levelAsset = %levelAsset;
@@ -101,26 +244,19 @@ function ChooseLevelMenu::onWake(%this)
       };
       
       LevelPreviewArray.add(%preview);
+      
+      %filteredIndex++;
    }
-   
+
+   GameModePreviewArray.listPosition = 0;   
    LevelPreviewArray.listPosition = 0;
    
    // 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 +269,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)
@@ -155,10 +291,31 @@ function ChooseLevelMenu::syncGUI(%this)
      
    ChooseLevelBackBtn.setBitmap(BaseUIActionMap.getCommandButtonBitmap(%device, "BaseUIBackOut"));
    
-   ChooseLevelStartBtn.setBitmap(ChooseLevelActionMap.getCommandButtonBitmap(%device, "ChooseLevelBegin"));
+   if(ChooseLevelMenu.currentMenuIdx == 0)
+      ChooseLevelNextBtn.text = "Toggle Mode";
+   else
+      ChooseLevelNextBtn.text = "Start Game";
+   
+   ChooseLevelNextBtn.setBitmap(ChooseLevelActionMap.getCommandButtonBitmap(%device, "ChooseLevelMenuOption"));
    
    ChooseLevelMenuPrevNavIcon.setBitmap(ChooseLevelActionMap.getCommandButtonBitmap(%device, "ChooseLevelMenuPrevMenu"));
    ChooseLevelMenuNextNavIcon.setBitmap(ChooseLevelActionMap.getCommandButtonBitmap(%device, "ChooseLevelMenuNextMenu"));
+   
+   %hasActiveGameMode = false;
+   for(%i=0; %i < GameModePreviewArray.getCount(); %i++)
+   {
+      %btn = GameModePreviewArray.getObject(%i);
+      if(!isObject(%btn.gameModeObj))
+         continue;
+         
+      if((%btn.gameModeObj.isActive() || %btn.gameModeObj.isAlwaysActive()) == true)
+      {
+         %hasActiveGameMode = true;
+         break;
+      }
+   }
+   
+   ChooseLevelMenuTabList-->LevelBtn.active = %hasActiveGameMode;
 }
 
 function LevelPreviewArray::syncGUI(%this)
@@ -169,14 +326,40 @@ function LevelPreviewArray::syncGUI(%this)
    $selectedLevelAsset = %btn.levelAssetId;
 }
 
+function GameModePreviewArray::syncGUI(%this)
+{
+   for(%i=0; %i < %this.getCount(); %i++)
+   {
+      %btn = %this.getObject(%i);
+      %btn-->button.setHighlighted(false);
+      
+      %bitmapAst = (%btn.gameModeObj.active || %btn.gameModeObj.alwaysActive) ? "UI:toggleMarker_h_image" : "UI:toggleMarker_image";
+      %btn-->checkbox.setBitmap(%bitmapAst);
+   }
+   
+   %btn = %this.getObject(%this.listPosition);
+   %btn-->button.setHighlighted(true);
+}
+
 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);
+      
+      //bail if we're trying to step into a hidden or disabled menu
+      if(!ChooseLevelMenu.isMenuAvailable(ChooseLevelMenu.currentMenuIdx))
+      {
+         ChooseLevelMenu.currentMenuIdx = %currentIdx;
+         return;
+      }
     
       if(%currentIdx == ChooseLevelMenu.currentMenuIdx)
          return;
@@ -187,12 +370,23 @@ 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);
+      
+      //bail if we're trying to step into a hidden or disabled menu
+      if(!ChooseLevelMenu.isMenuAvailable(ChooseLevelMenu.currentMenuIdx))
+      {
+         ChooseLevelMenu.currentMenuIdx = %currentIdx;
+         return;
+      }
     
       if(%currentIdx == ChooseLevelMenu.currentMenuIdx)
          return;
@@ -201,15 +395,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 +416,78 @@ function ChooseLevelMenu::openMenu(%this, %menuIdx)
    %this.syncGui();
 }
 
-function ChooseLevelBegin(%val)
+function ChooseLevelMenu::isMenuAvailable(%this, %menuIdx)
+{
+   if(%menuIdx == 0)
+      %btn = %this-->GameModeBtn;
+   else if(%menuIdx == 1)
+      %btn = %this-->LevelBtn;
+   else if(%menuIdx == 2)
+      %btn = %this-->ConfigBtn;
+      
+   return %btn.isActive() && %btn.isVisible();
+}
+
+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)
+         ToggleGameMode(ChooseLevelMenu.listPosition);
+      else if(ChooseLevelMenu.currentMenuIdx == 1)
+         ChooseLevelBegin(ChooseLevelMenu.listPosition);
+   }
+}
+
+function ToggleGameMode(%index)
+{
+   if(%index $= "")
+      %index = $MenuList.listPosition;
+   
+   %entry = GameModePreviewArray.getObject(%index);
+   if(!isObject(%entry) || !isObject(%entry.gameModeObj))
+   {
+      MessageBoxOK("Error", "Selected game mode does not have a valid mode");
+      return; 
+   }
+   
+   %entry.gameModeObj.active = !%entry.gameModeObj.active;
+   
+   refreshLevelsList();
+   
+   $MenuList.syncGui();
+   ChooseLevelMenu.syncGui();
+   
+}
+
+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 +507,23 @@ function ChooseLevelMenu::onSleep( %this )
    }
 }
 
+function GameModePreviewButton::onHighlighted(%this, %highlighted)
+{
+   if(%highlighted)
+   {
+      $MenuList.listPosition = $MenuList.getObjectIndex(%this.getParent());
+      
+      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)

TEMPAT SAMPAH
Templates/BaseGame/game/data/UI/images/toggleMarker.png


TEMPAT SAMPAH
Templates/BaseGame/game/data/UI/images/toggleMarker_h.png


+ 3 - 0
Templates/BaseGame/game/data/UI/images/toggleMarker_h_image.asset.taml

@@ -0,0 +1,3 @@
+<ImageAsset
+    AssetName="toggleMarker_h_image"
+    imageFile="@assetFile=toggleMarker_h.png"/>

+ 3 - 0
Templates/BaseGame/game/data/UI/images/toggleMarker_image.asset.taml

@@ -0,0 +1,3 @@
+<ImageAsset
+    AssetName="toggleMarker_image"
+    imageFile="@assetFile=toggleMarker.png"/>

+ 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();

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

@@ -0,0 +1,35 @@
+AssetBrowser::registerObjectType("GameModeType", "Gamemode Objects", "Gamemode");
+
+function GameModeType::setupCreateNew()
+{
+   
+}
+
+function GameModeType::onCreateNew()
+{
+   
+}
+
+function GameModeType::buildBrowserElement(%this, %className, %previewData)
+{
+   //echo("DatablockObjectType::buildBrowserElement() - " @ %datablock @ ", " @ %name @ ", " @ %previewData);
+   %previewData.assetName = %this.getName();
+   %previewData.assetPath = %this.getFileName();
+   
+   %previewData.previewImage = "ToolsModule:genericAssetIcon_image";
+   
+   //Lets see if we have a icon for specifically for this datablock type
+   %dataClass = %this.getClassName();
+   %dataClass = strreplace(%dataClass, "Data", "");
+   
+   %previewImage = "tools/classIcons/" @ %dataClass @ ".png";
+   if(isFile(%previewImage))
+   {
+      %previewData.previewImage = %previewImage;
+   }
+   
+   //%previewData.assetFriendlyName = %assetDef.assetName;
+   %previewData.assetDesc = %this;
+   %previewData.tooltip =  "\nGameMode Name: " @ %previewData.assetName @ 
+                           "\nPath: " @ %previewData.assetPath;
+}

+ 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();
+}

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

@@ -9,24 +9,23 @@ 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
 function @@::onCreateGameServer(%this)
 {
+    //--DATABLOCK EXEC BEGIN--
     //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 END--
 }
 
 //This is called when the server is shut down due to the game/map being exited
@@ -37,6 +36,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();

+ 13 - 9
Templates/BaseGame/game/tools/gui/fieldTypes/fieldTypes.tscript

@@ -1,20 +1,24 @@
 function GuiVariableInspector::onInspectorFieldModified(%this, %targetObj, %fieldName, %index, %oldValue, %newValue)
 {
-   echo("FIELD CHANGED: " @ %fieldName @ " from " @ %oldValue @ " to " @ %newValue);
+   //echo("FIELD CHANGED: " @ %fieldName @ " from " @ %oldValue @ " to " @ %newValue);
 }
 
 function GuiInspectorVariableGroup::onConstructField(%this, %fieldName, %fieldLabel, %fieldTypeName, %fieldDesc, %fieldDefaultVal, %fieldDataVals, %callbackName, %ownerObj)
 {
-   %inspector = %this.getParent();
-   %makeCommand = %this @ ".build" @ %fieldTypeName @ "Field(\""@ %fieldName @ "\",\"" @ %fieldLabel @ "\",\"" @ %fieldDesc @ "\",\"" @ 
-            %fieldDefaultVal @ "\",\"" @ %fieldDataVals @ "\",\"" @ %inspector @ "." @ %callbackName @ "\",\"" @ %ownerObj @"\");";
-   eval(%makeCommand);
+   return GuiInspectorGroup::onConstructField(%this, %fieldName, %fieldLabel, %fieldTypeName, %fieldDesc, %fieldDefaultVal, %fieldDataVals, %callbackName, %ownerObj);
 }
 
 function GuiInspectorGroup::onConstructField(%this, %fieldName, %fieldLabel, %fieldTypeName, %fieldDesc, %fieldDefaultVal, %fieldDataVals, %callbackName, %ownerObj)
 {
-   %inspector = %this.getParent();
-   %makeCommand = %this @ ".build" @ %fieldTypeName @ "Field(\""@ %fieldName @ "\",\"" @ %fieldLabel @ "\",\"" @ %fieldDesc @ "\",\"" @ 
-            %fieldDefaultVal @ "\",\"" @ %fieldDataVals @ "\",\"" @ %inspector @ "." @ %callbackName @ "\",\"" @ %ownerObj @"\");";
-   eval(%makeCommand);
+   if(%this.isMethod("build" @ %fieldTypeName @ "Field"))
+   {
+      %inspector = %this.getParent();
+      %makeCommand = %this @ ".build" @ %fieldTypeName @ "Field(\""@ %fieldName @ "\",\"" @ %fieldLabel @ "\",\"" @ %fieldDesc @ "\",\"" @ 
+               %fieldDefaultVal @ "\",\"" @ %fieldDataVals @ "\",\"" @ %inspector @ "." @ %callbackName @ "\",\"" @ %ownerObj @"\");";
+      %ret = eval(%makeCommand);
+      
+      if(%ret == true || %ret $= "")
+         return true;
+   }
+   return false;
 }

+ 1 - 0
Templates/BaseGame/game/tools/levels/DefaultEditorLevel.asset.taml

@@ -2,6 +2,7 @@
     AssetName="DefaultEditorLevel"
     LevelFile="@assetFile=DefaultEditorLevel.mis"
     LevelName="DefaultEditorLevel"
+    PostFXPresetFile="@assetFile=tools/levels/DefaultEditorLevel.postfxpreset.tscript"
     description="An empty room"
     previewImageAsset0="@asset=ToolsModule:DefaultEditorLevel_preview_image"
     previewImageAsset1="@asset=ToolsModule:DefaultEditorLevel_preview_image"

+ 5 - 19
Templates/BaseGame/game/tools/levels/DefaultEditorLevel.mis

@@ -1,33 +1,20 @@
 //--- OBJECT WRITE BEGIN ---
 new Scene(EditorTemplateLevel) {
-   canSave = "1";
-   canSaveDynamicFields = "1";
-      Enabled = "1";
+   isEditing = "1";
+      enabled = "1";
 
    new LevelInfo(theLevelInfo) {
-      nearClip = "0.1";
-      visibleDistance = "1000";
-      visibleGhostDistance = "0";
-      decalBias = "0.0015";
-      fogColor = "0.6 0.6 0.7 1";
-      fogDensity = "0";
+      FogColor = "0.6 0.6 0.7 1";
       fogDensityOffset = "700";
-      fogAtmosphereHeight = "0";
       canvasClearColor = "0 0 0 255";
-      ambientLightBlendPhase = "1";
-      ambientLightBlendCurve = "0 0 -1 -1";
       soundAmbience = "AudioAmbienceDefault";
-      soundDistanceModel = "Linear";
-      canSave = "1";
-      canSaveDynamicFields = "1";
-         Enabled = "1";
+         enabled = "1";
    };
    new ScatterSky(DynamicSky) {
       sunScale = "0.991102 0.921582 0.83077 1";
       zOffset = "-3000";
       azimuth = "25";
       brightness = "5";
-      flareType = "LightFlareExample1";
       MoonMatAsset = "Core_Rendering:moon_wglow";
       useNightCubemap = "1";
       nightCubemap = "nightCubemap";
@@ -44,12 +31,11 @@ new Scene(EditorTemplateLevel) {
       persistentId = "289ad401-3140-11ed-aae8-c0cb519281fc";
          reflectionPath = "tools/levels/DefaultEditorLevel/probes/";
    };
-   
    new GroundPlane() {
       scaleU = "32";
       scaleV = "32";
       MaterialAsset = "Prototyping:FloorGray";
-         Enabled = "1";
+         enabled = "1";
          position = "0 0 0";
          rotation = "1 0 0 0";
          scale = "1 1 1";

+ 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

+ 1 - 1
Templates/BaseGame/game/tools/worldEditor/gui/ToolsPaletteWindow.ed.gui

@@ -42,7 +42,7 @@ $guiContent = new GuiControl() {
       minSize = "50 50";
       EdgeSnap = false;
       text = "";
-      class = "EWToolsPaletteWindowClass";
+      class = "ButtonPalette"; 
       
       new GuiDynamicCtrlArrayControl(ToolsPaletteArray) {
          canSaveDynamicFields = "0";

+ 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";

+ 3 - 0
Templates/BaseGame/game/tools/worldEditor/main.tscript

@@ -69,6 +69,7 @@ function initializeWorldEditor()
    exec("./scripts/probeBake.ed." @ $TorqueScriptFileExtension);
    exec("./scripts/visibility/visibilityLayer.ed." @ $TorqueScriptFileExtension);
    exec("./scripts/visibility/probeViz." @ $TorqueScriptFileExtension);
+   exec("./scripts/buttonPalette." @ $TorqueScriptFileExtension);
    
    exec("tools/gui/postFxEditor." @ $TorqueScriptFileExtension );
    exec("tools/gui/renderTargetVisualizer.ed." @ $TorqueScriptFileExtension);
@@ -77,6 +78,8 @@ function initializeWorldEditor()
    loadDirectory(expandFilename("./scripts/editors"));
    loadDirectory(expandFilename("./scripts/interfaces"));
    
+   exec("./scripts/interfaces/subSceneEditing.tscript");
+   
    // Create the default editor plugins before calling buildMenus.
       
    new ScriptObject( WorldEditorPlugin )

+ 114 - 6
Templates/BaseGame/game/tools/worldEditor/scripts/EditorGui.ed.tscript

@@ -1069,11 +1069,13 @@ function WorldEditorInspectorPlugin::onWorldEditorStartup( %this )
    //connect editor windows
    GuiWindowCtrl::attach( EWInspectorWindow, EWTreeWindow);
    
-   %map = new ActionMap();   
+   %map = new ActionMap(); 
+   
+   /*   
    %map.bindCmd( keyboard, "1", "EWorldEditorNoneModeBtn.performClick();", "" );  // Select
    %map.bindCmd( keyboard, "2", "EWorldEditorMoveModeBtn.performClick();", "" );  // Move
    %map.bindCmd( keyboard, "3", "EWorldEditorRotateModeBtn.performClick();", "" );  // Rotate
-   %map.bindCmd( keyboard, "4", "EWorldEditorScaleModeBtn.performClick();", "" );  // Scale
+   %map.bindCmd( keyboard, "4", "EWorldEditorScaleModeBtn.performClick();", "" );  // Scale*/
    %map.bindCmd( keyboard, "f", "FitToSelectionBtn.performClick();", "" );// Fit Camera to Selection
    %map.bindCmd( keyboard, "z", "EditorGuiStatusBar.setCamera(\"Standard Camera\");", "" );// Free camera
    %map.bindCmd( keyboard, "n", "ToggleNodeBar->renderHandleBtn.performClick();", "" );// Render Node
@@ -1093,21 +1095,38 @@ function WorldEditorInspectorPlugin::onWorldEditorStartup( %this )
 function WorldEditorInspectorPlugin::onActivated( %this )
 {   
    Parent::onActivated( %this );
+   
+   //Clears the button pallete stack
+   EWToolsPaletteWindow.setStackCtrl(ToolsPaletteArray); //legacy ctrl adhereance
+   EWToolsPaletteWindow.clearButtons();
+   
+   EWToolsPaletteWindow.setActionMap(WorldEditorInspectorPlugin.map);
+   
+   //Adds a button to the pallete stack
+                          //Name     Icon                         Click Command                         Tooltip text    Keybind
+   EWToolsPaletteWindow.addButton("Select", "ToolsModule:arrow_n_image", "EWorldEditorNoneModeBtn::onClick();", "", "Select Arrow", "1");
+   EWToolsPaletteWindow.addButton("Move", "ToolsModule:translate_n_image", "EWorldEditorMoveModeBtn::onClick();", "", "Move Selection", "2");
+   EWToolsPaletteWindow.addButton("Rotate", "ToolsModule:rotate_n_image", "EWorldEditorRotateModeBtn::onClick();", "", "Rotate Selection", "3");
+   EWToolsPaletteWindow.addButton("Scale", "ToolsModule:Scale_n_image", "EWorldEditorScaleModeBtn::onClick();", "", "Scale Selection", "4");
+   
+   EWToolsPaletteWindow.refresh();
 
    EditorGui-->InspectorWindow.setVisible( true );   
    EditorGui-->TreeWindow.setVisible( true );
    EditorGui-->WorldEditorToolbar.setVisible( true );
-   %this.map.push();
+   //%this.map.push();
 }
 
 function WorldEditorInspectorPlugin::onDeactivated( %this )
 {   
    Parent::onDeactivated( %this );
+   
+   EWToolsPaletteWindow.popActionMap();
 
    EditorGui-->InspectorWindow.setVisible( false );  
    EditorGui-->TreeWindow.setVisible( false ); 
    EditorGui-->WorldEditorToolbar.setVisible( false );
-   %this.map.pop();
+   //%this.map.pop();
 }
 
 function WorldEditorInspectorPlugin::onEditMenuSelect( %this, %editMenu )
@@ -2044,7 +2063,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
    {
@@ -2140,6 +2159,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 @ " );";
+            }
          }  
       }
    }
@@ -2179,7 +2206,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 )
@@ -2620,6 +2647,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 )
@@ -2887,6 +2985,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 )

+ 160 - 0
Templates/BaseGame/game/tools/worldEditor/scripts/buttonPalette.tscript

@@ -0,0 +1,160 @@
+function ButtonPalette::setStackCtrl(%this, %ctrl)
+{
+   %this.stackCtrl = %ctrl;
+}
+
+function ButtonPalette::getStackCtrl(%this)
+{
+   if(isObject(%this.stackCtrl))
+      return %this.stackCtrl;
+   else
+      return %this;
+}
+
+function ButtonPalette::setActionMap(%this, %actionMap)
+{
+   %this.actionMap = %actionMap;
+   %this.actionMap.push();
+}
+
+function ButtonPalette::getActionMap(%this)
+{
+   return %this.actionMap;
+}
+
+function ButtonPalette::popActionMap(%this, %actionMap)
+{
+   %this.actionMap.pop();
+}
+
+function ButtonPalette::clearButtons(%this)
+{
+   %stackCtrl = %this.getStackCtrl();
+   %stackCtrl.clear();
+   
+   for(%i=0; %i < %this.numButtons; %i++)
+   {
+      if(isObject(%this.actionMap))
+      {
+         %this.actionMap.unbind("keyboard", getField(%this.buttons[%i], 5));
+      }
+      
+      %this.buttons[%i] = "";
+   }
+   
+   %this.numButtons = 0;
+}
+
+ //Name, Icon, Click Command, Variable, Tooltip text, Keybind
+function ButtonPalette::addButton(%this, %name, %iconAsset, %command, %syncVariable, %toolTip, %keybind)
+{    
+   if(%this.numButtons $= "")
+      %this.numButtons = 0;
+      
+   for(%i=0; %i < %this.numButtons; %i++)
+   {
+      %buttonInfo = %this.buttons[%i];
+      
+      //echo("Testing button info: " @ getField(%buttonInfo, 0) @ " against new button name: " @ %name);
+      if(getField(%buttonInfo, 0) $= %name) //no duplicates
+         return;
+   }
+      
+   %this.buttons[%this.numButtons] = %name TAB %iconAsset TAB %command TAB %syncVariable TAB %toolTip TAB %keybind;
+
+   %this.numButtons++;
+}
+
+function ButtonPalette::removeButton(%this, %name)
+{
+   %found = false;
+   for(%i=0; %i < %this.numButtons; %i++)
+   {
+      if(getField(%this.buttons[%i], 0) $= %name)
+      {
+         %found = true;
+         
+         if(isObject(%this.actionMap))
+         {
+            %this.actionMap.unbind("keyboard", getField(%this.buttons[%i], 5));
+         }
+      }
+      
+      if(%found)
+      {
+         %this.buttons[%i] = %this.buttons[%i+1];
+      }
+   }
+   
+   if(%found)
+      %this.numButtons--;
+      
+   return %found;
+}
+
+function ButtonPalette::findButton(%this, %name)
+{
+   %stackCtrl = %this.getStackCtrl();
+   for(%i=0; %i < %stackCtrl.getCount(); %i++)
+   {
+      %btnObj = %stackCtrl.getObject(%i);
+      
+      if(%btnObj.buttonName $= %name)
+         return %btnObj; 
+   }
+   
+   return 0;
+}
+
+function ButtonPalette::refresh(%this)
+{
+   %stackCtrl = %this.getStackCtrl();
+   %stackCtrl.clear();
+   
+   %this.visible = true;
+   %extents = "25 25";
+   
+   if(%this.numButtons == 0)
+   {
+      %this.visible = false;
+      return;  
+   }
+   
+   for(%i=0; %i < %this.numButtons; %i++)
+   {
+      %buttonInfo = %this.buttons[%i];
+      
+      %paletteButton = new GuiBitmapButtonCtrl() {
+         canSaveDynamicFields = "0";
+         internalName = getField(%buttonInfo, 0) @ "_paletteButton";
+         Enabled = "1";
+         isContainer = "0";
+         Profile = "ToolsGuiButtonProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         Position = "0 0";
+         Extent = "25 19";
+         MinExtent = "8 2";
+         canSave = "1";
+         Visible = "1";
+         tooltipprofile = "ToolsGuiToolTipProfile";
+         ToolTip = getField(%buttonInfo, 4) SPC "(" @ getField(%buttonInfo, 5) @ ")";
+         hovertime = "1000";
+         bitmapAsset = getField(%buttonInfo, 1);
+         buttonType = "RadioButton";
+         useMouseEvents = "0";
+         buttonName = getField(%buttonInfo, 0);
+         command = getField(%buttonInfo, 2);
+         variable = getField(%buttonInfo, 3);
+      };
+      
+      %extents.y += 23;
+      
+      if(isObject(%this.actionMap))
+         %this.actionMap.bindCmd( keyboard, getField(%buttonInfo, 5), %paletteButton @ ".performClick();", "" ); 
+      
+      %stackCtrl.add(%paletteButton);
+   }
+   
+   %this.extent.y = %extents.y;
+}

+ 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" );

+ 139 - 0
Templates/BaseGame/game/tools/worldEditor/scripts/editors/worldEditor.ed.tscript

@@ -554,3 +554,142 @@ function simGroup::onInspectPostApply(%this)
     %this.callOnChildren("setHidden",%this.hidden);
     %this.callOnChildren("setLocked",%this.locked);    
 }
+
+function simGroup::SelectFiteredObjects(%this, %min, %max)
+{
+    EWorldEditor.clearSelection();
+    %this.callOnChildren("filteredSelect", %min, %max );
+}
+
+function SceneObject::filteredSelect(%this, %min, %max)
+{
+   echo("SceneObject::filteredSelect() - min: " @ %min @ " max: " @ %max);
+    %box = %this.getWorldBox();
+    %xlength = mAbs(getWord(%box,0) - getWord(%box,3));
+    %ylength = mAbs(getWord(%box,1) - getWord(%box,4));
+    %Zlength = mAbs(getWord(%box,2) - getWord(%box,5));
+    %diagSq = mPow(%xlength,2)+mPow(%ylength,2)+mPow(%Zlength,2);
+    if (%diagSq > mPow(%min,2) && %diagSq < mPow(%max,2))
+    {
+        EWorldEditor.selectObject(%this);
+    }
+}
+
+function simGroup::onInspect(%obj, %inspector)
+{
+   //Find the 'Editing' group in the inspector
+   %group = %inspector.findExistentGroup("Editing");
+   if(isObject(%group))
+   {
+      //We add a field of the type 'SimGroupSelectionButton'. This isn't a 'real' type, so when the inspector group tries to add it
+      //it will route down through GuiInspectorGroup(the namespace of %group) and call onConstructField in an attemp to see if there's any
+      //script defined functions that can build a field of that type.
+      //We happen to define the required 'build @ <fieldTypeName> @ Field()' function below, allowing us to build out the custom field type
+      %group.addField("Select Objects", "SimGroupSelectionButton", "Select filtered objects");
+    }
+}
+
+function GuiInspectorGroup::buildSimGroupSelectionButtonField(%this, %fieldName, %fieldLabel, %fieldDesc,
+                                 %fieldDefaultVal, %fieldDataVals, %callback, %ownerObj)
+{
+   
+   //Set defaults if needbe
+   if(%ownerObj.minSize $= "")
+      %ownerObj.minSize = 0.1;
+   if(%ownerObj.maxSize $= "")
+      %ownerObj.maxSize = 1;
+      
+   %container = new GuiControl() {
+      canSaveDynamicFields = "0";
+      Profile = "EditorContainerProfile";
+      HorizSizing = "right";
+      VertSizing = "bottom";
+      Position = "0 0";
+      Extent = "300 80";
+      MinExtent = "8 2";
+      canSave = "0";
+      Visible = "1";
+      hovertime = "100";
+      tooltip = "";// %tooltip;
+      tooltipProfile = "EditorToolTipProfile";
+      
+      new GuiTextCtrl() {
+         profile = GuiInspectorFieldProfile;
+         text = %fieldLabel;
+         Position = "16 2";
+         Extent = "300 18";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+      };
+      
+      new GuiControl() {
+         Position = "40 20";
+         Extent = "270 18";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         
+         new GuiTextCtrl() {
+            profile = GuiInspectorFieldProfile;
+            text = "Minimum Size:";
+            Position = "0 2";
+            Extent = "150 18";
+            HorizSizing = "right";
+            VertSizing = "bottom";
+         };
+         
+         new GuiTextEditCtrl() {
+            profile = GuiInspectorTextEditProfile;
+            variable =  %ownerObj @ ".minSize";
+            Position = "150 2";
+            Extent = "150 18";
+            HorizSizing = "left";
+            VertSizing = "bottom";
+         };
+      };
+      
+      new GuiControl() {
+         Position = "40 40";
+         Extent = "270 18";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         
+         new GuiTextCtrl() {
+            profile = GuiInspectorFieldProfile;
+            text = "Maximum Size:";
+            Position = "0 2";
+            Extent = "150 18";
+            HorizSizing = "right";
+            VertSizing = "bottom";
+         };
+         
+         new GuiTextEditCtrl() {
+            profile = GuiInspectorTextEditProfile;
+            variable = %ownerObj @ ".maxSize";
+            Position = "150 2";
+            Extent = "150 18";
+            HorizSizing = "left";
+            VertSizing = "bottom";
+         };
+      };
+      
+      new GuiButtonCtrl() {
+         canSaveDynamicFields = "0";
+         Profile = "ToolsGuiButtonProfile";
+         HorizSizing = "right";
+         VertSizing = "bottom";
+         Position = "40 60";
+         Extent = "265 18";
+         MinExtent = "8 2";
+         canSave = "0";
+         Visible = "1";
+         hovertime = "100";
+         tooltip = ""; //%tooltip;
+         tooltipProfile = "EditorToolTipProfile";
+         text = "Select";
+         maxLength = "1024";
+         command = %ownerObj @ ".SelectFiteredObjects("@ %ownerObj.minSize @","@ %ownerObj.maxSize @");";
+      };
+   };
+   
+   %this-->stack.add(%container);
+}

+ 54 - 0
Templates/BaseGame/game/tools/worldEditor/scripts/interfaces/subSceneEditing.tscript

@@ -0,0 +1,54 @@
+function SubScene::onSelected(%this)
+{
+   EWToolsPaletteWindow.clearButtons();
+
+   //Adds a button to the pallete stack
+   EWToolsPaletteWindow.addButton("Select", "ToolsModule:arrow_n_image", "EWorldEditorNoneModeBtn::onClick();", "", "Select Arrow", "1");
+   EWToolsPaletteWindow.addButton("Move", "ToolsModule:translate_n_image", "SubSceneMoveModeBtn::onClick();", "", "Move Selection", "2");
+   EWToolsPaletteWindow.addButton("Rotate", "ToolsModule:rotate_n_image", "SubSceneRotateModeBtn::onClick();", "", "Rotate Selection", "3");
+   EWToolsPaletteWindow.addButton("Scale", "ToolsModule:Scale_n_image", "EWorldEditorScaleModeBtn::onClick();", "", "Scale Selection", "4");
+   
+   EWToolsPaletteWindow.addButton("SubSceneMove", "ToolsModule:translate_n_image", "SubSceneChildMoveModeBtn::onClick();", "", "Move SubScene + Children", "5");
+   EWToolsPaletteWindow.addButton("SubSceneRotate", "ToolsModule:rotate_n_image", "SubSceneChildRotateModeBtn::onClick();", "", "Rotate SubScene + Children", "6");
+   
+   EWToolsPaletteWindow.refresh();
+}
+
+function SubScene::onUnselected(%this)
+{
+   EWToolsPaletteWindow.clearButtons();
+
+   //Adds a button to the pallete stack
+   EWToolsPaletteWindow.addButton("Select", "ToolsModule:arrow_n_image", "EWorldEditorNoneModeBtn::onClick();", "", "Select Arrow", "1");
+   EWToolsPaletteWindow.addButton("Move", "ToolsModule:translate_n_image", "EWorldEditorMoveModeBtn::onClick();", "", "Move Selection", "2");
+   EWToolsPaletteWindow.addButton("Rotate", "ToolsModule:rotate_n_image", "EWorldEditorRotateModeBtn::onClick();", "", "Rotate Selection", "3");
+   EWToolsPaletteWindow.addButton("Scale", "ToolsModule:Scale_n_image", "EWorldEditorScaleModeBtn::onClick();", "", "Scale Selection", "4");
+   
+   $SubScene::transformChildren = false;
+   
+   EWToolsPaletteWindow.refresh();
+}
+
+function SubSceneMoveModeBtn::onClick(%this)
+{
+   EWorldEditorMoveModeBtn::onClick();
+   $SubScene::transformChildren = false;
+}
+
+function SubSceneRotateModeBtn::onClick(%this)
+{
+   EWorldEditorRotateModeBtn::onClick();
+   $SubScene::transformChildren = false;
+}
+
+function SubSceneChildMoveModeBtn::onClick()
+{
+   EWorldEditorMoveModeBtn::onClick();
+   $SubScene::transformChildren = true;
+}
+
+function SubSceneChildRotateModeBtn::onClick()
+{
+   EWorldEditorRotateModeBtn::onClick();
+   $SubScene::transformChildren = true;
+}

+ 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)