//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "interior/interiorInstance.h"
#include "interior/lightUpdateGrouper.h"
#include "interior/interior.h"
#include "interior/interiorSubObject.h"
#include "interior/pathedInterior.h"
#include "console/consoleTypes.h"
#include "sceneGraph/sceneGraph.h"
#include "sceneGraph/sceneState.h"
#include "core/bitStream.h"
#include "dgl/dgl.h"
#include "dgl/gBitmap.h"
#include "math/mathIO.h"
#include "dgl/materialList.h"
#include "editor/editor.h"
#include "interior/interiorResObjects.h"
#include "game/trigger.h"
#include "sim/simPath.h"
#include "interior/forceField.h"
#include "dgl/materialList.h"
#include "sceneGraph/lightManager.h"
#include "collision/convex.h"
#include "audio/audioDataBlock.h"
#include "core/frameAllocator.h"

#include "sim/netConnection.h"
#include "platform/profiler.h"

//--------------------------------------------------------------------------
//-------------------------------------- Local classes, data, and functions
//
const U32 csgMaxZoneSize = 256;
bool sgScopeBoolArray[256];

class InteriorRenderImage : public SceneRenderImage
{
  public:
   InteriorRenderImage() : mDetailLevel(0) { }

   U32   mDetailLevel;
   U32   mBaseZone;
};

ConsoleFunctionGroupBegin(Interiors, "");

#if defined(TORQUE_DEBUG) || defined (INTERNAL_RELEASE)
ConsoleFunction( setInteriorRenderMode, void, 2, 2, "(int modeNum)")
{
   S32 mode = dAtoi(argv[1]);
   if (mode < 0 || mode > Interior::ShowDetailLevel)
      mode = 0;

   Interior::smRenderMode = mode;
}

ConsoleFunction( setInteriorFocusedDebug, void, 2, 2, "(bool enable)")
{
   if (dAtob(argv[1])) {
      Interior::smFocusedDebug = true;
   } else {
      Interior::smFocusedDebug = false;
   }
}

#endif

ConsoleFunction( isPointInside, bool, 2, 4, "(Point3F pos) or (float x, float y, float z)")
{
   static bool lastValue = false;

   if(!(argc == 2 || argc == 4))
   {
      Con::errorf(ConsoleLogEntry::General, "cIsPointInside: invalid parameters");
      return(lastValue);
   }

   Point3F pos;
   if(argc == 2)
      dSscanf(argv[1], "%g %g %g", &pos.x, &pos.y, &pos.z);
   else
   {
      pos.x = dAtof(argv[1]);
      pos.y = dAtof(argv[2]);
      pos.z = dAtof(argv[3]);
   }

   RayInfo collision;
   if(gClientContainer.castRay(pos, Point3F(pos.x, pos.y, pos.z - 2000.f), InteriorObjectType, &collision))
   {
      if(collision.face == -1)
         Con::errorf(ConsoleLogEntry::General, "cIsPointInside: failed to find hit face on interior");
      else
      {
         InteriorInstance * interior = dynamic_cast<InteriorInstance *>(collision.object);
         if(interior)
            lastValue = !interior->getDetailLevel(0)->isSurfaceOutsideVisible(collision.face);
         else
            Con::errorf(ConsoleLogEntry::General, "cIsPointInside: invalid interior on collision");
      }
   }

   return(lastValue);
}


ConsoleFunctionGroupEnd(Interiors);

ConsoleMethod( InteriorInstance, setAlarmMode, void, 3, 3, "(string mode) Mode is 'On' or 'Off'")
{
   bool alarm;
   if (dStricmp(argv[2], "On") == 0)
      alarm = true;
   else
      alarm = false;

   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   object->setAlarmMode(alarm);
}

ConsoleMethod( InteriorInstance, activateLight, void, 3, 3, "(string lightName)")
{
   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   const char* pLightName = argv[2];
   object->activateLight(pLightName);
}

ConsoleMethod( InteriorInstance, deactivateLight, void, 3, 3, "(string lightName)")
{
   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   const char* pLightName = argv[2];
   object->deactivateLight(pLightName);
}

ConsoleMethod( InteriorInstance, echoTriggerableLights, void, 2, 2, "List lights usable with activateLight()/deactivateLight().")
{
   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   object->echoTriggerableLights();
}

ConsoleMethod( InteriorInstance, magicButton, void, 2, 2, "@deprecated unimplemented")
{
   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   object->addChildren();
}

ConsoleMethod( InteriorInstance, setSkinBase, void, 3, 3, "(string basename)")
{
   if (object->isClientObject()) {
      Con::errorf(ConsoleLogEntry::General, "InteriorInstance: client objects may not receive console commands.  Ignored");
      return;
   }

   object->setSkinBase(argv[2]);
}

ConsoleMethod( InteriorInstance, getNumDetailLevels, S32, 2, 2, "")
{
   return(object->getNumDetailLevels());
}

ConsoleMethod( InteriorInstance, setDetailLevel, void, 3, 3, "(int level)")
{
   if(object->isServerObject())
   {
      NetConnection * toServer = NetConnection::getConnectionToServer();
      NetConnection * toClient = NetConnection::getLocalClientConnection();
      if(!toClient || !toServer)
         return;

      S32 index = toClient->getGhostIndex(object);
      if(index == -1)
         return;

      InteriorInstance * clientInstance = dynamic_cast<InteriorInstance*>(toServer->resolveGhost(index));
      if(clientInstance)
         clientInstance->setDetailLevel(dAtoi(argv[2]));
   }
   else
      object->setDetailLevel(dAtoi(argv[2]));
}

//--------------------------------------------------------------------------
//-------------------------------------- Static functions
//
IMPLEMENT_CO_NETOBJECT_V1(InteriorInstance);

//------------------------------------------------------------------------------
//-------------------------------------- InteriorInstance
//
bool InteriorInstance::smDontRestrictOutside = false;
bool InteriorInstance::smRenderDynamicLights = true;
U32  InteriorInstance::smLightUpdatePeriod = 66;  // 66 ms between updates
F32  InteriorInstance::smDetailModification = 1.0f;


InteriorInstance::InteriorInstance()
{
   mAlarmState             = false;
   mDoSimpleDynamicRender  = false;
   mUseGLLighting          = false;

   mInteriorFileName = NULL;
   mTypeMask = InteriorObjectType | StaticObjectType | StaticRenderedObjectType;
   mNetFlags.set(Ghostable | ScopeAlways);

   mUpdateGrouper = NULL;

   mShowTerrainInside = false;

   mSkinBase = StringTable->insert("base");
   mAudioProfile = 0;
   mAudioEnvironment = 0;
   mForcedDetailLevel = -1;

   mConvexList = new Convex;
   mCRC = 0;
}

InteriorInstance::~InteriorInstance()
{
   U32 i;

   for (i = 0; i < mLightInfo.size(); i++)
      destructInPlace(&mLightInfo[i]);

   for (i = 0; i < mInteriorSubObjects.size(); i++)
   {
      // DMM: Fix for first-class objects...
      for (U32 j = 0; j < mInteriorSubObjects[i].size(); j++)
         delete (mInteriorSubObjects[i])[j];

      destructInPlace(&mInteriorSubObjects[i]);
   }
   delete mUpdateGrouper;

   delete mConvexList;
   mConvexList = NULL;

   for (i = 0; i < mVertexColorsNormal.size(); i++)
   {
      delete mVertexColorsNormal[i];
      mVertexColorsNormal[i] = NULL;
   }

   for (i = 0; i < mVertexColorsAlarm.size(); i++)
   {
      delete mVertexColorsAlarm[i];
      mVertexColorsAlarm[i] = NULL;
   }

   for (i = 0; i < mMaterialMaps.size(); i++)
   {
      delete mMaterialMaps[i];
      mMaterialMaps[i] = NULL;
   }
}


void InteriorInstance::init()
{
   // Does nothing for the moment
}


void InteriorInstance::destroy()
{
   // Also does nothing for the moment
}


//--------------------------------------------------------------------------
// Inspection
static AudioProfile * saveAudioProfile = 0;
static AudioEnvironment * saveAudioEnvironment = 0;
void InteriorInstance::inspectPreApply()
{
   saveAudioProfile = mAudioProfile;
   saveAudioEnvironment = mAudioEnvironment;
}

void InteriorInstance::inspectPostApply()
{
   if((mAudioProfile != saveAudioProfile) || (mAudioEnvironment != saveAudioEnvironment))
      setMaskBits(AudioMask);

   // Update the Transform on Editor Apply.
   setMaskBits(TransformMask);
}

void InteriorInstance::onStaticModified( const char* slotName )
{
   if( (!dStricmp( slotName, "position") || !dStricmp( slotName, "rotation") || !dStricmp( slotName, "scale")) && !mUseGLLighting )
      Con::executef( 1, "onNeedRelight" );

   if( !dStricmp( slotName, "useGLLighting" ))
      setMaskBits(InitMask);
}

//--------------------------------------------------------------------------
//-------------------------------------- Console functionality
//
void InteriorInstance::initPersistFields()
{
   Parent::initPersistFields();

   addGroup("Media");
   addField("interiorFile",      TypeFilename,              Offset(mInteriorFileName, InteriorInstance));
   endGroup("Media");

   addGroup("Audio");
   addField("audioProfile",      TypeAudioProfilePtr,       Offset(mAudioProfile, InteriorInstance));
   addField("audioEnvironment",  TypeAudioEnvironmentPtr,   Offset(mAudioEnvironment, InteriorInstance));
   endGroup("Audio");

   addGroup("Misc");
   addField("useGLLighting",     TypeBool,                  Offset(mUseGLLighting, InteriorInstance));
   addField("showTerrainInside", TypeBool,                  Offset(mShowTerrainInside, InteriorInstance));
   endGroup("Misc");

}

void InteriorInstance::consoleInit()
{
   Con::addVariable("pref::Interior::LightUpdatePeriod",    TypeS32,  &smLightUpdatePeriod);
   Con::addVariable("pref::Interior::ShowEnvironmentMaps",  TypeBool, &Interior::smRenderEnvironmentMaps);
   Con::addVariable("pref::Interior::DynamicLights",        TypeBool, &smRenderDynamicLights);
   Con::addVariable("pref::Interior::VertexLighting",       TypeBool, &Interior::smUseVertexLighting);
   Con::addVariable("pref::Interior::TexturedFog",          TypeBool, &Interior::smUseTexturedFog);
   Con::addVariable("pref::Interior::lockArrays",           TypeBool, &Interior::smLockArrays);

   Con::addVariable("pref::Interior::detailAdjust", TypeF32, &InteriorInstance::smDetailModification);

   // DEBUG ONLY!!!
#ifdef TORQUE_DEBUG
   Con::addVariable("Interior::DontRestrictOutside", TypeBool, &smDontRestrictOutside);
#endif
}

//--------------------------------------------------------------------------
void InteriorInstance::renewOverlays()
{
   StringTableEntry baseName = dStricmp(mSkinBase, "base") == 0 ? "blnd" : mSkinBase;

   for (U32 i = 0; i < mMaterialMaps.size(); i++)
   {
      MaterialList* pMatList = mMaterialMaps[i];

      for (U32 j = 0; j < pMatList->mMaterialNames.size(); j++)
      {
         const char* pName     = pMatList->mMaterialNames[j];
         const U32 len = dStrlen(pName);
         if (len < 6)
            continue;

         const char* possible = pName + (len - 5);
         if (dStricmp(".blnd", possible) == 0)
         {
            char newName[256];
            AssertFatal(len < 200, "InteriorInstance::renewOverlays: Error, len exceeds allowed name length");

            dStrncpy(newName, pName, possible - pName);
            newName[possible - pName] = '\0';
            dStrcat(newName, ".");
            dStrcat(newName, baseName);

            TextureHandle test = TextureHandle(newName, MeshTexture, false);
            if (test.getGLName() != 0)
            {
               pMatList->mMaterials[j] = test;
            }
            else
            {
               pMatList->mMaterials[j] = TextureHandle(pName, MeshTexture, false);
            }
         }
      }
   }
}


//--------------------------------------------------------------------------
void InteriorInstance::setSkinBase(const char* newBase)
{
   if (dStricmp(mSkinBase, newBase) == 0)
      return;

   mSkinBase = StringTable->insert(newBase);

   if (isServerObject())
      setMaskBits(SkinBaseMask);
   else
      renewOverlays();
}


//--------------------------------------------------------------------------
// from resManager.cc
extern U32 calculateCRC(void * buffer, S32 len, U32 crcVal );

bool InteriorInstance::onAdd()
{
   U32 i;

   // Load resource
   mInteriorRes = ResourceManager->load(mInteriorFileName, true);

   if (bool(mInteriorRes) == false)
   {
      Con::errorf(ConsoleLogEntry::General, "Unable to load interior: %s", mInteriorFileName);
      NetConnection::setLastError("Unable to load interior: %s", mInteriorFileName);
      return false;
   }

   if(isClientObject())
   {
      if(mCRC != mInteriorRes.getCRC())
      {
         NetConnection::setLastError("Local interior file '%s' does not match version on server.", mInteriorFileName);
         return false;
      }

      for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
      {
         // ok, if the material list load failed...
         // if this is a local connection, we'll assume that's ok
         // and just have white textures...
         // otherwise we want to return false.
         Interior* pInterior = mInteriorRes->getDetailLevel(i);
         if(!pInterior->prepForRendering(mInteriorRes.getFilePath()) )
               return false;
      }
   }
   else
      mCRC = mInteriorRes.getCRC();

   if(!Parent::onAdd())
      return false;

   // Set up vertex lighting.
   mVertexColorsNormal.setSize(mInteriorRes->getNumDetailLevels());
   mVertexColorsAlarm.setSize(mInteriorRes->getNumDetailLevels());
   for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
   {
      mVertexColorsNormal[i] = new Vector<ColorI>;
      mVertexColorsAlarm[i]  = new Vector<ColorI>;
      VECTOR_SET_ASSOCIATION((*mVertexColorsNormal[i]));
      VECTOR_SET_ASSOCIATION((*mVertexColorsAlarm[i]));
   }

   // Ok, everything's groovy!  Let's cache our hashed filename for renderimage sorting...
   mInteriorFileHash = _StringTable::hashString(mInteriorFileName);

   // Setup bounding information
   mObjBox = mInteriorRes->getDetailLevel(0)->getBoundingBox();
   resetWorldBox();
   setRenderTransform(mObjToWorld);

   // Setup mLightInfo structure
   mLightInfo.setSize(mInteriorRes->getNumDetailLevels());

   for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
   {
      Interior* pInterior = mInteriorRes->getDetailLevel(i);
      constructInPlace(&mLightInfo[i]);
      LightInfo& rInfo = mLightInfo[i];

      rInfo.mSurfaceInvalid.setSize(pInterior->mSurfaces.size());
      rInfo.mSurfaceInvalid.clear();

      rInfo.mStateDataInfo.setSize(pInterior->mNumLightStateEntries);
      dMemset(rInfo.mStateDataInfo.address(), 0x00,
              sizeof(LightInfo::StateDataInfo) * rInfo.mStateDataInfo.size());

      rInfo.mLights.setSize(pInterior->mAnimatedLights.size());
      for (U32 j = 0; j < rInfo.mLights.size(); j++)
      {
         LightInfo::Light& rLight = rInfo.mLights[j];
         rLight.curState = 0;
         rLight.curTime  = 0;

         Interior::LightState& rState = pInterior->mLightStates[pInterior->mAnimatedLights[j].stateIndex];
         rLight.curColor.set(rState.red, rState.green, rState.blue);
         rLight.active   = false;
         rLight.alarm    = (pInterior->mAnimatedLights[j].flags & Interior::AlarmLight) != 0;

         installLight(i, j);
      }
   }

   // Setup lightgrouper
   mUpdateGrouper = new LightUpdateGrouper(LightUpdateBitStart, LightUpdateBitEnd);
   for (U32 d = 0; d < mInteriorRes->getNumDetailLevels(); d++)
   {
      Interior* pInterior = mInteriorRes->getDetailLevel(d);

      for (U32 j = 0; j < pInterior->mNumTriggerableLights; j++)
         mUpdateGrouper->addKey(makeUpdateKey(d, j));
   }

   if (isClientObject())
   {
      // create all the subObjects
      mInteriorSubObjects.setSize(mInteriorRes->getNumDetailLevels());
      for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
      {
         constructInPlace(&mInteriorSubObjects[i]);

         Interior* pInterior = mInteriorRes->getDetailLevel(i);
         for (U32 j = 0; j < pInterior->mSubObjects.size(); j++)
            mInteriorSubObjects[i].push_back(pInterior->mSubObjects[j]->clone(this));
      }
   }
   else
   {
      // creates all subobjects
      mInteriorSubObjects.setSize(mInteriorRes->getNumDetailLevels());
      for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
      {
         constructInPlace(&mInteriorSubObjects[i]);

         Interior* pInterior = mInteriorRes->getDetailLevel(i);

         for (U32 j = 0; j < pInterior->mSubObjects.size(); j++)
            mInteriorSubObjects[i].push_back(pInterior->mSubObjects[j]->clone(this));
      }
   }

   // Do any handle loading, etc. required.

   if (isClientObject())
   {
      setLightUpdatedTime(Platform::getVirtualMilliseconds());

      for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
      {
         Interior* pInterior = mInteriorRes->getDetailLevel(i);

         // Force the lightmap manager to download textures if we're
         // running the mission editor.  Normally they are only
         // downloaded after the whole scene is lit.
         gInteriorLMManager.addInstance(pInterior->getLMHandle(), mLMHandle, this);
         if (gEditingMission)
         {
            gInteriorLMManager.useBaseTextures(pInterior->getLMHandle(), mLMHandle);
            gInteriorLMManager.downloadGLTextures(pInterior->getLMHandle());
         }

         // Install material list
         mMaterialMaps.push_back(new MaterialList(pInterior->mMaterialList));

         // A client interior starts up it's ambient animations on add.  Ambients
         //  are just past the triggerables.
         for (U32 j = pInterior->mNumTriggerableLights; j < pInterior->mAnimatedLights.size(); j++)
            activateLight(i, j);
      }

      renewOverlays();
   }
   else
   {
      // WTF goes here? -- BJG
   }

   addToScene();
   return true;
}


void InteriorInstance::onRemove()
{
   // Kill our convexes.
   mConvexList->nukeList();

   // Clean up our lightmaps.
   if(isClientObject())
   {
      if(bool(mInteriorRes) && mLMHandle != 0xFFFFFFFF)
      {
         for(U32 i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
         {
            Interior * pInterior = mInteriorRes->getDetailLevel(i);
            if (pInterior->getLMHandle() != 0xFFFFFFFF)
               gInteriorLMManager.removeInstance(pInterior->getLMHandle(), mLMHandle);
         }
      }
   }

   removeFromScene();
   Parent::onRemove();
}


//--------------------------------------------------------------------------
bool InteriorInstance::onSceneAdd(SceneGraph* pGraph)
{
   AssertFatal(mInteriorRes, "Error, should not have been added to the scene if there's no interior!");

   if (Parent::onSceneAdd(pGraph) == false)
      return false;

   if (mInteriorRes->getDetailLevel(0)->mZones.size() > 1)
   {
      AssertWarn(getNumCurrZones() == 1, "There should be one and only one zone for an interior that manages zones");
      mSceneManager->registerZones(this, (mInteriorRes->getDetailLevel(0)->mZones.size() - 1));
   }

   return true;
}


//--------------------------------------------------------------------------
void InteriorInstance::onSceneRemove()
{
   AssertFatal(mInteriorRes, "Error, should not have been added to the scene if there's no interior!");

   if (isManagingZones())
      mSceneManager->unregisterZones(this);

   Parent::onSceneRemove();
}


//--------------------------------------------------------------------------
bool InteriorInstance::getOverlappingZones(SceneObject* obj,
                                           U32*         zones,
                                           U32*         numZones)
{
   MatrixF xForm(true);
   Point3F invScale(1.0f / getScale().x,
                    1.0f / getScale().y,
                    1.0f / getScale().z);
   xForm.scale(invScale);
   xForm.mul(getWorldTransform());
   xForm.mul(obj->getTransform());
   xForm.scale(obj->getScale());

   U32 waterMark = FrameAllocator::getWaterMark();

   U16* zoneVector = (U16*)FrameAllocator::alloc(mInteriorRes->getDetailLevel(0)->mZones.size() * sizeof(U16));
   U32 numRetZones = 0;

   bool outsideToo = mInteriorRes->getDetailLevel(0)->scanZones(obj->getObjBox(),
                                                                xForm,
                                                                zoneVector,
                                                                &numRetZones);
   if (numRetZones > SceneObject::MaxObjectZones)
   {
      Con::warnf(ConsoleLogEntry::General, "Too many zones returned for query on %s.  Returning first %d",
                 mInteriorFileName, SceneObject::MaxObjectZones);
   }

   for (U32 i = 0; i < getMin(numRetZones, U32(SceneObject::MaxObjectZones)); i++)
      zones[i] = zoneVector[i] + mZoneRangeStart - 1;
   *numZones = numRetZones;

   FrameAllocator::setWaterMark(waterMark);

   return outsideToo;
}


//--------------------------------------------------------------------------
U32 InteriorInstance::getPointZone(const Point3F& p)
{
   AssertFatal(mInteriorRes, "Error, no interior!");

   Point3F osPoint = p;
   mWorldToObj.mulP(osPoint);
   osPoint.convolveInverse(mObjScale);

   const S32 zone = mInteriorRes->getDetailLevel(0)->getZoneForPoint(osPoint);

   // If we're in solid (-1) or outside, we need to return 0
   if (zone == -1 || zone == 0)
      return 0;

   return (zone-1) + mZoneRangeStart;
}

// does a hack check to determine how much a point is 'inside'.. should have
// portals prebuilt with the transfer energy to each other portal in the zone
// from the neighboring zone.. these values can be used to determine the factor
// from within an individual zone.. also, each zone could be marked with
// average material property for eax environment audio
// ~0: outside -> 1: inside
bool InteriorInstance::getPointInsideScale(const Point3F & pos, F32 * pScale)
{
   AssertFatal(mInteriorRes, "InteriorInstance::getPointInsideScale: no interior");

   Interior * interior = mInteriorRes->getDetailLevel(0);

   Point3F p = pos;
   mWorldToObj.mulP(p);
   p.convolveInverse(mObjScale);

   U32 zoneIndex = interior->getZoneForPoint(p);
   if(zoneIndex == -1)  // solid?
   {
      *pScale = 1.f;
      return(true);
   }
   else if(zoneIndex == 0) // outside?
   {
      *pScale = 0.f;
      return(true);
   }

   U32 waterMark = FrameAllocator::getWaterMark();
   const Interior::Portal** portals = (const Interior::Portal**)FrameAllocator::alloc(256 * sizeof(const Interior::Portal*));
   U32 numPortals = 0;

   Interior::Zone & zone = interior->mZones[zoneIndex];

   U32 i;
   for(i = 0; i < zone.portalCount; i++)
   {
      const Interior::Portal & portal = interior->mPortals[interior->mZonePortalList[zone.portalStart + i]];
      if(portal.zoneBack == 0 || portal.zoneFront == 0)
      {
         AssertFatal(numPortals < 256, "Error, overflow in temporary portal buffer!");
         portals[numPortals++] = &portal;
      }
   }

   // inside?
   if(numPortals == 0)
   {
      *pScale = 1.f;

      FrameAllocator::setWaterMark(waterMark);
      return(true);
   }

   Point3F* portalCenters = (Point3F*)FrameAllocator::alloc(numPortals * sizeof(Point3F));
   U32 numPortalCenters = 0;

   // scale using the distances to the portals in this zone...
   for(i = 0; i < numPortals; i++)
   {
      const Interior::Portal * portal = portals[i];
      if(!portal->triFanCount)
         continue;

      Point3F center(0, 0, 0);
      for(U32 j = 0; j < portal->triFanCount; j++)
      {
         const Interior::TriFan & fan = interior->mWindingIndices[portal->triFanStart + j];
         const U32 numPoints = fan.windingCount;

         if(!numPoints)
            continue;

         for(U32 k = 0; k < numPoints; k++)
         {
            const Point3F & a = interior->mPoints[interior->mWindings[fan.windingStart + k]].point;
            center += a;
         }

         center /= numPoints;
         portalCenters[numPortalCenters++] = center;
      }
   }

   // 'magic' check here...
   F32 magic = Con::getFloatVariable("Interior::insideDistanceFalloff", 10.f);

   F32 val = 0.f;
   for(i = 0; i < numPortalCenters; i++)
      val += 1.f - mClampF(Point3F(portalCenters[i] - p).len() / magic, 0.f, 1.f);

   *pScale = 1.f - mClampF(val, 0.f, 1.f);

   FrameAllocator::setWaterMark(waterMark);
   return(true);
}

//--------------------------------------------------------------------------
ColorF gInteriorFogColor(1, 1, 1);

void InteriorInstance::renderObject(SceneState* state, SceneRenderImage* sceneImage)
{
   AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on entry");

   if(gEditingMission && isHidden())
      return;

   PROFILE_START(InteriorRenderObject);

   PROFILE_START(IRO_GetZones);
   U32 storedWaterMark = FrameAllocator::getWaterMark();
   dglSetRenderPrimType(2);

   InteriorRenderImage* interiorImage = static_cast<InteriorRenderImage*>(sceneImage);

   Point3F osPoint = state->getCameraPosition();
   mRenderWorldToObj.mulP(osPoint);
   osPoint.convolveInverse(mObjScale);

   // Get the object space y vector
   Point3F osCamVector;
   state->mModelview.getColumn(1, &osCamVector);
   mRenderWorldToObj.mulV(osCamVector);
   osCamVector.convolve(getScale());
   osCamVector.normalize();

   Point3F osZVec(0, 0, 1);
   mRenderWorldToObj.mulV(osZVec);
   osZVec.convolve(getScale());

   // First, we want to test the planes and setup the fog...
   U32 zoneOffset = mZoneRangeStart != 0xFFFFFFFF ? mZoneRangeStart : 0;

   Interior* pInterior = mInteriorRes->getDetailLevel(interiorImage->mDetailLevel);

   U32 baseZone = 0xFFFFFFFF;
   if (getNumCurrZones() == 1)
   {
      baseZone = getCurrZone(0);
   }
   else
   {
      for (U32 i = 0; i < getNumCurrZones(); i++)
      {
         if (state->getZoneState(getCurrZone(i)).render == true)
         {
            if (baseZone == 0xFFFFFFFF)
            {
               baseZone = getCurrZone(i);
               break;
            }
         }
      }

      if (baseZone == 0xFFFFFFFF)
         baseZone = getCurrZone(0);
   }
   PROFILE_END();


   PROFILE_START(IRO_ComputeActivePolys);
   Point3F worldOrigin;
   getRenderTransform().getColumn(3, &worldOrigin);

   ZoneVisDeterminer zoneVis;

   if (interiorImage->mDetailLevel == 0)
   {
      zoneVis.runFromState(state, zoneOffset, baseZone);
      pInterior->setupActivePolyList(zoneVis, state, osPoint, osCamVector, osZVec, worldOrigin.z, getScale());
   }
   else
   {
      // Something else...
      pInterior->prepTempRender(state,
                                getCurrZone(0),
                                0,
                                mRenderObjToWorld, mObjScale,
                                state->mFlipCull);
      zoneVis.runFromRects(state, zoneOffset, baseZone);
      pInterior->setupActivePolyList(zoneVis, state, osPoint, osCamVector, osZVec, worldOrigin.z, getScale());
   }
   PROFILE_END();

   PROFILE_START(IRO_UpdateAnimatedLights);

   // Update the animated lights...
   if (!Interior::smUseVertexLighting)
   {
      LightInfo& rLightInfo = mLightInfo[interiorImage->mDetailLevel];
      downloadLightmaps(state, pInterior, rLightInfo);
   }

   PROFILE_END();

   PROFILE_START(IRO_RenderSolids);

   if(mDoSimpleDynamicRender)
      installLights();

   // Set up the model view and the global render state...
   RectI viewport;
   dglGetViewport(&viewport);
   glMatrixMode(GL_PROJECTION);
   glPushMatrix();
   glMatrixMode(GL_MODELVIEW);
   glPushMatrix();
   dglMultMatrix(&mRenderObjToWorld);
   glScalef(mObjScale.x, mObjScale.y, mObjScale.z);

   glEnable(GL_BLEND);
   glDisable(GL_CULL_FACE);
   glEnable(GL_TEXTURE_2D);

   // We need to decide if we're going to use the low-res textures
   Point3F camPoint = state->getCameraPosition();
   Point3F closestPoint = getRenderWorldBox().getClosestPoint(camPoint);
   F32 dist = (camPoint - closestPoint).len();
   if (dist != 0.0)
   {
      F32 length = dglProjectRadius(dist, 1.0f / pInterior->mAveTexGenLength);
      if (length < (1.0 / 16.0))
      {
         TextureManager::setSmallTexturesActive(true);
      }
   }

   pInterior->setupFog(state);
   pInterior->setOSCamPosition(osPoint);
   if (interiorImage->mBaseZone != 0)
      state->setupZoneProjection(interiorImage->mBaseZone + mZoneRangeStart - 1);
   else
      state->setupObjectProjection(this);

   gInteriorFogColor = state->getFogColor();

   // make sure to build vertex color information in case interiors were
   //  added after the relight scene
   if(&((*mVertexColorsNormal[interiorImage->mDetailLevel])[0]) == NULL)
	   rebuildVertexColors();

   if(mDoSimpleDynamicRender)
   {
      pInterior->renderAsShape();
   }
   else
   {
      pInterior->render(mAlarmState, mMaterialMaps[interiorImage->mDetailLevel], mLMHandle,
                        mVertexColorsNormal[interiorImage->mDetailLevel],
                        mVertexColorsAlarm[interiorImage->mDetailLevel]);
   }

   pInterior->clearFog();

   if(mDoSimpleDynamicRender)
      uninstallLights();

   PROFILE_END();
   PROFILE_START(IRO_RenderDynamicLights);

   // Do dynamic lights if appropriate.
   if (smRenderDynamicLights == true)
   {
      // Are there any lights?
      const S32 numLights = gClientSceneGraph->getLightManager()->getNumLights();
      if (numLights > 0)
      {
         // Read out light info...
         U32 lightWaterMark = FrameAllocator::getWaterMark();
         ::LightInfo** lightArray = (::LightInfo**)FrameAllocator::alloc(sizeof(::LightInfo*) * numLights);
         gClientSceneGraph->getLightManager()->getLights(lightArray);

         for (U32 i = 0; i < numLights; i++)
         {
            // Only consider point lights.
            if (lightArray[i]->mType != ::LightInfo::Point)
               continue;

            Point3F lightPoint = lightArray[i]->mPos;
            mRenderWorldToObj.mulP(lightPoint);
            lightPoint.convolveInverse(mObjScale);

            Box3F box;
            box.min = lightPoint;
            box.max = lightPoint;
            box.min -= Point3F(lightArray[i]->mRadius, lightArray[i]->mRadius, lightArray[i]->mRadius);
            box.max += Point3F(lightArray[i]->mRadius, lightArray[i]->mRadius, lightArray[i]->mRadius);

            // TODO: Account for scale...
            if (mObjBox.isOverlapped(box) == false)
               continue;

            // The number of light surfaces cannot exceed the total number of non-null surfaces in
            //  interior...
            U32  subWaterMark = FrameAllocator::getWaterMark();
            U32* lightSurfaces    = (U32*)FrameAllocator::alloc(pInterior->mSurfaces.size() * sizeof(U32));
            U32  numLightSurfaces = 0;
            if (pInterior->buildLightPolyList(lightSurfaces, &numLightSurfaces,
                                              box, mRenderWorldToObj, getScale()) == false)
            {
               FrameAllocator::setWaterMark(subWaterMark);
               continue;
            }

            pInterior->renderLights(lightArray[i], mRenderWorldToObj, getScale(), lightSurfaces, numLightSurfaces);
            FrameAllocator::setWaterMark(subWaterMark);
         }

         FrameAllocator::setWaterMark(lightWaterMark);
      }
   }
   PROFILE_END();

   glDisable(GL_BLEND);
   if (dglDoesSupportARBMultitexture())
   {
      glActiveTextureARB(GL_TEXTURE1_ARB);
      glDisable(GL_TEXTURE_2D);
      glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
      glActiveTextureARB(GL_TEXTURE0_ARB);
      glDisable(GL_TEXTURE_2D);
      glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
   }
   else
   {
      glDisable(GL_TEXTURE_2D);
      glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
   }

   glMatrixMode(GL_PROJECTION);
   glPopMatrix();
   dglSetViewport(viewport);

   glMatrixMode(GL_MODELVIEW);
   glPopMatrix();

   AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on exit");

   FrameAllocator::setWaterMark(storedWaterMark);
   dglSetRenderPrimType(0);

   // Reset the small textures...
   TextureManager::setSmallTexturesActive(false);

   PROFILE_END();
}


//--------------------------------------------------------------------------
bool InteriorInstance::scopeObject(const Point3F&        rootPosition,
                                   const F32             /*rootDistance*/,
                                   bool*                 zoneScopeState)
{
   AssertFatal(isManagingZones(), "Error, should be a zone manager if we are called on to scope the scene!");
   if (bool(mInteriorRes) == false)
      return false;

   Interior* pInterior = getDetailLevel(0);
   AssertFatal(pInterior->mZones.size() <= csgMaxZoneSize, "Error, too many zones!  Increase max");
   bool* pInteriorScopingState = sgScopeBoolArray;
   dMemset(pInteriorScopingState, 0, sizeof(bool) * pInterior->mZones.size());

   // First, let's transform the point into the interior's space
   Point3F interiorRoot = rootPosition;
   getWorldTransform().mulP(interiorRoot);
   interiorRoot.convolveInverse(getScale());

   S32 realStartZone = getPointZone(rootPosition);
   if (realStartZone != 0)
      realStartZone = realStartZone - mZoneRangeStart + 1;

   bool continueOut = pInterior->scopeZones(realStartZone,
                                            interiorRoot,
                                            pInteriorScopingState);

   // Copy pInteriorScopingState to zoneScopeState
   for (S32 i = 1; i < pInterior->mZones.size(); i++)
      zoneScopeState[i + mZoneRangeStart - 1] = pInteriorScopingState[i];

   return continueOut;
}


//--------------------------------------------------------------------------
U32 InteriorInstance::calcDetailLevel(SceneState* state, const Point3F& wsPoint)
{
   AssertFatal(mInteriorRes, "Error, should not try to calculate the deatil level without a resource to work with!");
   AssertFatal(getNumCurrZones() > 0, "Error, must belong to a zone for this to work");

   if (smDetailModification < 0.3)
      smDetailModification = 0.3;
   if (smDetailModification > 1.0)
      smDetailModification = 1.0;

   // Early out for simple interiors
   if (mInteriorRes->getNumDetailLevels() == 1)
      return 0;

   if((mForcedDetailLevel >= 0) && (mForcedDetailLevel < mInteriorRes->getNumDetailLevels()))
      return(mForcedDetailLevel);

   Point3F osPoint = wsPoint;
   mRenderWorldToObj.mulP(osPoint);
   osPoint.convolveInverse(mObjScale);

   // First, see if the point is in the object space bounding box of the highest detail
   //  If it is, then the detail level is zero.
   if (mObjBox.isContained(osPoint))
      return 0;

   // Otherwise, we're going to have to do some ugly trickery to get the projection.
   //  I've stolen the worldToScreenScale from dglMatrix, we'll have to calculate the
   //  projection of the bounding sphere of the lowest detail level.
   //  worldToScreenScale = (near * view.extent.x) / (right - left)
   RectI viewport;
   F64   frustum[4] = { 1e10, -1e10, 1e10, -1e10 };

   bool init = false;
   SceneObjectRef* pWalk = mZoneRefHead;
   AssertFatal(pWalk != NULL, "Error, object must exist in at least one zone to call this!");
   while (pWalk)
   {
      const SceneState::ZoneState& rState = state->getZoneState(pWalk->zone);
      if (rState.render == true)
      {
         // frustum
         if (rState.frustum[0] < frustum[0]) frustum[0] = rState.frustum[0];
         if (rState.frustum[1] > frustum[1]) frustum[1] = rState.frustum[1];
         if (rState.frustum[2] < frustum[2]) frustum[2] = rState.frustum[2];
         if (rState.frustum[3] > frustum[3]) frustum[3] = rState.frustum[3];

         // viewport
         if (init == false)
            viewport = rState.viewport;
         else
            viewport.unionRects(rState.viewport);

         init = true;
      }
      pWalk = pWalk->nextInObj;
   }

   AssertFatal(init, "InteriorInstance::calcDetailLevel - Error, at least one zone must be rendered here!");

   const F32 worldToScreenScale  = (state->getNearPlane() * viewport.extent.x) / (frustum[1] - frustum[0]);
   const SphereF& lowSphere      = mInteriorRes->getDetailLevel(mInteriorRes->getNumDetailLevels() - 1)->mBoundingSphere;
   const F32 dist                = (lowSphere.center - osPoint).len();
   F32 projRadius                = (lowSphere.radius / dist) * worldToScreenScale;

   // Scale the projRadius based on the objects maximum scale axis
   projRadius *= getMax(mFabs(mObjScale.x), getMax(mFabs(mObjScale.y), mFabs(mObjScale.z)));

   // Multiply based on detail preference...
   projRadius *= smDetailModification;

   // Ok, now we have the projected radius, we need to search through the interiors to
   //  find the largest interior that will support this projection.
   U32 final = mInteriorRes->getNumDetailLevels() - 1;
   for (U32 i = 0; i< mInteriorRes->getNumDetailLevels() - 1; i++)
   {
      Interior* pDetail = mInteriorRes->getDetailLevel(i);

      if (pDetail->mMinPixels < projRadius)
      {
         final = i;
         break;
      }
   }

   // Ok, that's it.
   return final;
}


//--------------------------------------------------------------------------
bool InteriorInstance::prepRenderImage(SceneState* state,   const U32 stateKey,
                                       const U32 startZone, const bool modifyBaseState)
{
   if (isLastState(state, stateKey))
      return false;

   PROFILE_START(InteriorPrepRenderImage);

   setLastState(state, stateKey);

   U32 realStartZone;
   if (startZone != 0xFFFFFFFF)
   {
      AssertFatal(startZone != 0, "Hm.  This really shouldn't happen.  Should only get inside zones here");
      AssertFatal(isManagingZones(), "Must be managing zones if we're here...");

      realStartZone = startZone - mZoneRangeStart + 1;
   }
   else
   {
      realStartZone = getPointZone(state->getCameraPosition());
      if (realStartZone != 0)
         realStartZone = realStartZone - mZoneRangeStart + 1;
   }

   bool render = true;
   if (modifyBaseState == false)
   {
      // Regular query.  We only return a render zone if our parent zone is rendered.
      //  Otherwise, we always render
      if (state->isObjectRendered(this) == false)
      {
         PROFILE_END();
         return false;
      }
   }
   else
   {
      if (mShowTerrainInside == true)
         state->enableTerrainOverride();
   }

   U32 detailLevel = 0;
   if (startZone == 0xFFFFFFFF)
      detailLevel = calcDetailLevel(state, state->getCameraPosition());

   if (!Interior::smUseVertexLighting)
   {
      // Since we're rendering: update the lights and the alarm state
      U32 msSinceLightsUpdated = Platform::getVirtualMilliseconds() - getLightUpdatedTime();
      if (msSinceLightsUpdated > smLightUpdatePeriod)
      {
         setLightUpdatedTime(Platform::getVirtualMilliseconds());
         updateAllLights(msSinceLightsUpdated);
      }
   }

   U32 baseZoneForPrep = getCurrZone(0);
   bool multipleZones = false;
   if (getNumCurrZones() > 1)
   {
      U32 numRenderedZones = 0;
      baseZoneForPrep = 0xFFFFFFFF;
      for (U32 i = 0; i < getNumCurrZones(); i++)
      {
         if (state->getZoneState(getCurrZone(i)).render == true)
         {
            numRenderedZones++;
            if (baseZoneForPrep == 0xFFFFFFFF)
               baseZoneForPrep = getCurrZone(i);
         }
      }

      if (numRenderedZones > 1)
         multipleZones = true;
   }

   bool continueOut = mInteriorRes->getDetailLevel(0)->prepRender(state,
                                                                  baseZoneForPrep,
                                                                  realStartZone, mZoneRangeStart,
                                                                  mRenderObjToWorld, mObjScale,
                                                                  modifyBaseState & !smDontRestrictOutside,
                                                                  smDontRestrictOutside | multipleZones,
                                                                  state->mFlipCull);
   if (smDontRestrictOutside)
      continueOut = true;

   InteriorRenderImage* image = new InteriorRenderImage;
   image->obj = this;
   image->mDetailLevel = detailLevel;
   image->mBaseZone    = realStartZone;
   image->textureSortKey = mInteriorFileHash;
   state->insertRenderImage(image);

   // Add renderimages for any sub-objects, first the independant subs, then the dependants
   //  for this detail...
   Point3F osPoint = state->getCameraPosition();
   mRenderWorldToObj.mulP(osPoint);
   osPoint.convolveInverse(mObjScale);

   U32 i;
   for (i = 0; i < mInteriorSubObjects[0].size(); i++)
   {
      InteriorSubObject* iso = (mInteriorSubObjects[0])[i];

      if (iso->renderDetailDependant() == false)
      {
         // We want to check the zone that this object is in.  If we are traversing upwards
         //  though, we have no information about our parent zone, and must return the
         //  render image regardless.
         if (modifyBaseState && iso->getZone() == 0)
         {
            // Must return
         }
         else
         {
            U32 realZone;
            if (iso->getZone() == 0)
               realZone = getCurrZone(0);
            else
               realZone = iso->getZone() + mZoneRangeStart - 1;

            if (state->getZoneState(realZone).render == false)
            {
               // Nuke it...
               continue;
            }
         }

         //  Register the render image, if any, for this ISO.
         SubObjectRenderImage* sri = iso->getRenderImage(state, osPoint);
         if (sri)
         {
            sri->mDetailLevel = 0;
            state->insertRenderImage(sri);
         }
      }
   }

   //  Condier any ISOs from the current LOD.
   for (i = 0; i < mInteriorSubObjects[detailLevel].size(); i++)
   {
      InteriorSubObject* iso = (mInteriorSubObjects[detailLevel])[i];

      if (iso->renderDetailDependant() == true)
      {
         // We want to check the zone that this object is in.  If we are traversing upwards
         //  though, we have no information about our parent zone, and must return the
         //  render image regardless.
         if (modifyBaseState && iso->getZone() == 0)
         {
            // Must return
         }
         else
         {
            U32 realZone;
            if (iso->getZone() == 0)
               realZone = getCurrZone(0);
            else
               realZone = iso->getZone() + mZoneRangeStart - 1;

            if (state->getZoneState(realZone).render == false)
            {
               // Nuke it...
               continue;
            }
         }

         SubObjectRenderImage* sri = iso->getRenderImage(state, osPoint);
         if (sri)
         {
            sri->mDetailLevel = detailLevel;
            state->insertRenderImage(sri);
         }
      }
   }

   PROFILE_END();
   return continueOut;
}


//--------------------------------------------------------------------------
bool InteriorInstance::castRay(const Point3F& s, const Point3F& e, RayInfo* info)
{
   info->object = this;
   return mInteriorRes->getDetailLevel(0)->castRay(s, e, info);
}


//------------------------------------------------------------------------------
void InteriorInstance::setTransform(const MatrixF & mat)
{
   Parent::setTransform(mat);

   // Since the interior is a static object, it's render transform changes 1 to 1
   //  with it's collision transform
   setRenderTransform(mat);

   // Notify subobjects that care about the transform change...
   if (bool(mInteriorRes))
   {
      for (U32 i = 0; i < mInteriorSubObjects.size(); i++)
      {
         for (U32 j = 0; j < mInteriorSubObjects[i].size(); j++)
            (mInteriorSubObjects[i])[j]->noteTransformChange();
      }
   }

   if (isServerObject())
      setMaskBits(TransformMask);
}


//------------------------------------------------------------------------------
bool InteriorInstance::buildPolyList(AbstractPolyList* list, const Box3F& wsBox, const SphereF&)
{
   if (bool(mInteriorRes) == false)
      return false;

   // Setup collision state data
   list->setTransform(&getTransform(), getScale());
   list->setObject(this);

   return mInteriorRes->getDetailLevel(0)->buildPolyList(list, wsBox, mWorldToObj, getScale());
}



void InteriorInstance::buildConvex(const Box3F& box, Convex* convex)
{
   if (bool(mInteriorRes) == false)
      return;

   mConvexList->collectGarbage();

   Box3F realBox = box;
   mWorldToObj.mul(realBox);
   realBox.min.convolveInverse(mObjScale);
   realBox.max.convolveInverse(mObjScale);

   if (realBox.isOverlapped(getObjBox()) == false)
      return;

   U32 waterMark = FrameAllocator::getWaterMark();

   if ((convex->getObject()->getType() & VehicleObjectType) &&
       mInteriorRes->getDetailLevel(0)->mVehicleConvexHulls.size() > 0)
   {
      // Can never have more hulls than there are hulls in the interior...
      U16* hulls = (U16*)FrameAllocator::alloc(mInteriorRes->getDetailLevel(0)->mVehicleConvexHulls.size() * sizeof(U16));
      U32 numHulls = 0;

      Interior* pInterior = mInteriorRes->getDetailLevel(0);
      if (pInterior->getIntersectingVehicleHulls(realBox, hulls, &numHulls) == false)
      {
         FrameAllocator::setWaterMark(waterMark);
         return;
      }

      for (U32 i = 0; i < numHulls; i++)
      {
         // See if this hull exists in the working set already...
         Convex* cc = 0;
         CollisionWorkingList& wl = convex->getWorkingList();
         for (CollisionWorkingList* itr = wl.wLink.mNext; itr != &wl; itr = itr->wLink.mNext)
         {
            if (itr->mConvex->getType() == InteriorConvexType &&
                (static_cast<InteriorConvex*>(itr->mConvex)->getObject() == this &&
                 static_cast<InteriorConvex*>(itr->mConvex)->hullId    == -S32(hulls[i] + 1)))
            {
               cc = itr->mConvex;
               break;
            }
         }
         if (cc)
            continue;

         // Create a new convex.
         InteriorConvex* cp = new InteriorConvex;
         mConvexList->registerObject(cp);
         convex->addToWorkingList(cp);
         cp->mObject   = this;
         cp->pInterior = pInterior;
         cp->hullId    = -S32(hulls[i] + 1);
         cp->box.min.x = pInterior->mVehicleConvexHulls[hulls[i]].minX;
         cp->box.min.y = pInterior->mVehicleConvexHulls[hulls[i]].minY;
         cp->box.min.z = pInterior->mVehicleConvexHulls[hulls[i]].minZ;
         cp->box.max.x = pInterior->mVehicleConvexHulls[hulls[i]].maxX;
         cp->box.max.y = pInterior->mVehicleConvexHulls[hulls[i]].maxY;
         cp->box.max.z = pInterior->mVehicleConvexHulls[hulls[i]].maxZ;
      }
   }
   else
   {
      // Can never have more hulls than there are hulls in the interior...
      U16* hulls = (U16*)FrameAllocator::alloc(mInteriorRes->getDetailLevel(0)->mConvexHulls.size() * sizeof(U16));
      U32 numHulls = 0;

      Interior* pInterior = mInteriorRes->getDetailLevel(0);
      if (pInterior->getIntersectingHulls(realBox, hulls, &numHulls) == false)
      {
         FrameAllocator::setWaterMark(waterMark);
         return;
      }

      for (U32 i = 0; i < numHulls; i++)
      {
         // See if this hull exists in the working set already...
         Convex* cc = 0;
         CollisionWorkingList& wl = convex->getWorkingList();
         for (CollisionWorkingList* itr = wl.wLink.mNext; itr != &wl; itr = itr->wLink.mNext)
         {
            if (itr->mConvex->getType() == InteriorConvexType &&
                (static_cast<InteriorConvex*>(itr->mConvex)->getObject() == this &&
                 static_cast<InteriorConvex*>(itr->mConvex)->hullId    == hulls[i]))
            {
               cc = itr->mConvex;
               break;
            }
         }

         if (cc)
            continue;

         // Create a new convex.
         InteriorConvex* cp = new InteriorConvex;
         mConvexList->registerObject(cp);
         convex->addToWorkingList(cp);
         cp->mObject   = this;
         cp->pInterior = pInterior;
         cp->hullId    = hulls[i];
         cp->box.min.x = pInterior->mConvexHulls[hulls[i]].minX;
         cp->box.min.y = pInterior->mConvexHulls[hulls[i]].minY;
         cp->box.min.z = pInterior->mConvexHulls[hulls[i]].minZ;
         cp->box.max.x = pInterior->mConvexHulls[hulls[i]].maxX;
         cp->box.max.y = pInterior->mConvexHulls[hulls[i]].maxY;
         cp->box.max.z = pInterior->mConvexHulls[hulls[i]].maxZ;
      }
   }
   FrameAllocator::setWaterMark(waterMark);
}


//------------------------------------------------------------------------------
U32 InteriorInstance::packUpdate(NetConnection* c, U32 mask, BitStream* stream)
{
   U32 retMask = Parent::packUpdate(c, mask, stream);

   if (stream->writeFlag((mask & InitMask) != 0))
   {
      // Initial update, write the whole kit and kaboodle
      stream->write(mCRC);

      stream->writeString(mInteriorFileName);
      stream->writeFlag(mShowTerrainInside);

      // Write the transform (do _not_ use writeAffineTransform.  Since this is a static
      //  object, the transform must be RIGHT THE *&)*$&^ ON or it will goof up the
      //  synchronization between the client and the server.
      mathWrite(*stream, mObjToWorld);
      mathWrite(*stream, mObjScale);

      // Write the alarm state
      stream->writeFlag(mAlarmState);

      // Write the skinbase
      stream->writeString(mSkinBase);

      // audio profile
      if(stream->writeFlag(mAudioProfile))
         stream->writeRangedU32(mAudioProfile->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);

      // audio environment:
      if(stream->writeFlag(mAudioEnvironment))
         stream->writeRangedU32(mAudioEnvironment->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);

      // Lighting behavior.
      stream->writeFlag(mUseGLLighting);
   }
   else
   {
      if (stream->writeFlag((mask & TransformMask) != 0))
      {
         mathWrite(*stream, mObjToWorld);
         mathWrite(*stream, mObjScale);
      }

      stream->writeFlag(mAlarmState);

      // Check the lights to see if we need to update any of their states
      LightUpdateGrouper::BitIterator itr;
      for (itr = mUpdateGrouper->begin(); itr.valid() && itr.getNumKeys(); itr++)
      {
         if (stream->writeFlag((mask & itr.getMask()) != 0))
         {
            LightUpdateGrouper::BitIterator::iterator kItr;
            for (kItr = itr.begin(); kItr != itr.end(); kItr++)
            {
               U32 key = *kItr;
               U32 detail = detailFromUpdateKey(key);
               U32 index  = indexFromUpdateKey(key);

               stream->writeFlag(mLightInfo[detail].mLights[index].active);
            }
         }
      }

      if (stream->writeFlag(mask & SkinBaseMask))
         stream->writeString(mSkinBase);

      // audio update:
      if(stream->writeFlag(mask & AudioMask))
      {
         // profile:
         if(stream->writeFlag(mAudioProfile))
            stream->writeRangedU32(mAudioProfile->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);

         // environment:
         if(stream->writeFlag(mAudioEnvironment))
            stream->writeRangedU32(mAudioEnvironment->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);
      }
   }

   return retMask;
}


//------------------------------------------------------------------------------
void InteriorInstance::unpackUpdate(NetConnection* c, BitStream* stream)
{
   Parent::unpackUpdate(c, stream);

   MatrixF temp;
   Point3F tempScale;

   if (stream->readFlag())
   {
      // Initial Update
      // CRC
      stream->read(&mCRC);

      // File
      mInteriorFileName = stream->readSTString();

      // Terrain flag
      mShowTerrainInside = stream->readFlag();

      // Transform
      mathRead(*stream, &temp);
      mathRead(*stream, &tempScale);
      setScale(tempScale);
      setTransform(temp);

      // Alarm state: Note that we handle this ourselves on the initial update
      //  so that the state is always full on or full off...
      mAlarmState = stream->readFlag();

      mSkinBase = stream->readSTString();

      // audio profile:
      if(stream->readFlag())
      {
         U32 profileId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
         mAudioProfile = dynamic_cast<AudioProfile*>(Sim::findObject(profileId));
      }
      else
         mAudioProfile = 0;

      // audio environment:
      if(stream->readFlag())
      {
         U32 profileId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
         mAudioEnvironment = dynamic_cast<AudioEnvironment*>(Sim::findObject(profileId));
      }
      else
         mAudioEnvironment = 0;

      // Lighting behavior.
      mDoSimpleDynamicRender = mUseGLLighting = stream->readFlag();
   }
   else
   {
      // Normal update
      if (stream->readFlag())
      {
         mathRead(*stream, &temp);
         mathRead(*stream, &tempScale);
         setScale(tempScale);
         setTransform(temp);
      }

      setAlarmMode(stream->readFlag());

      LightUpdateGrouper::BitIterator itr;
      for (itr = mUpdateGrouper->begin(); itr.valid() && itr.getNumKeys(); itr++)
      {
         if (stream->readFlag())
         {
            LightUpdateGrouper::BitIterator::iterator kItr;
            for (kItr = itr.begin(); kItr != itr.end(); kItr++)
            {
               U32 key = *kItr;
               U32 detail = detailFromUpdateKey(key);
               U32 index  = indexFromUpdateKey(key);

               if (stream->readFlag())
                  activateLight(detail, index);
               else
                  deactivateLight(detail, index);
            }
         }
      }

      if (stream->readFlag())
      {
         mSkinBase = stream->readSTString();
         renewOverlays();
      }

      // audio update:
      if(stream->readFlag())
      {
         // profile:
         if(stream->readFlag())
         {
            U32 profileId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
            mAudioProfile = dynamic_cast<AudioProfile*>(Sim::findObject(profileId));
         }
         else
            mAudioProfile = 0;

         // environment:
         if(stream->readFlag())
         {
            U32 profileId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
            mAudioEnvironment = dynamic_cast<AudioEnvironment*>(Sim::findObject(profileId));
         }
         else
            mAudioEnvironment = 0;
      }
   }
}


//------------------------------------------------------------------------------
Interior* InteriorInstance::getDetailLevel(const U32 level)
{
   return mInteriorRes->getDetailLevel(level);
}

U32 InteriorInstance::getNumDetailLevels()
{
   return mInteriorRes->getNumDetailLevels();
}

//--------------------------------------------------------------------------
//-------------------------------------- Alarm functionality
//
void InteriorInstance::setAlarmMode(const bool alarm)
{
   if (mInteriorRes->getDetailLevel(0)->mHasAlarmState == false)
      return;

   if (mAlarmState == alarm)
      return;

   mAlarmState = alarm;
   if (isServerObject())
   {
      setMaskBits(AlarmMask);
   }
   else
   {
      // DMMTODO: Invalidate current light state
   }
}


//--------------------------------------------------------------------------
void InteriorInstance::rebuildVertexColors()
{
   U32 i;
   for (i = 0; i < mVertexColorsNormal.size(); i++)
   {
      delete mVertexColorsNormal[i];
      mVertexColorsNormal[i] = NULL;
   }
   for (i = 0; i < mVertexColorsAlarm.size(); i++)
   {
      delete mVertexColorsAlarm[i];
      mVertexColorsAlarm[i] = NULL;
   }

   if (bool(mInteriorRes) == false)
      return;

   mVertexColorsNormal.setSize(mInteriorRes->getNumDetailLevels());
   mVertexColorsAlarm.setSize(mInteriorRes->getNumDetailLevels());

   for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
   {
      mVertexColorsNormal[i] = new Vector<ColorI>;
      mVertexColorsAlarm[i]  = new Vector<ColorI>;
      VECTOR_SET_ASSOCIATION((*mVertexColorsNormal[i]));
      VECTOR_SET_ASSOCIATION((*mVertexColorsAlarm[i]));
   }

   for (i = 0; i < mInteriorRes->getNumDetailLevels(); i++)
   {
      Interior* pInterior = mInteriorRes->getDetailLevel(i);
      pInterior->rebuildVertexColors(mLMHandle,
                                     mVertexColorsNormal[i],
                                     mVertexColorsAlarm[i]);
   }
}


//--------------------------------------------------------------------------
void InteriorInstance::updateLightMap(Interior* pInterior, LightInfo& rLightInfo, const U32 surfaceIndex)
{
   AssertFatal(surfaceIndex < pInterior->mSurfaces.size(), "Error, out of range surface index");
   static U8 _newLightMapBuffer[256*256*3];

   const Interior::Surface& rSurface = pInterior->mSurfaces[surfaceIndex];
   if (rSurface.lightCount == 0)
      return;

   // Get the surface's original bitmap
   TextureHandle* originalLMapHandle = ((mAlarmState == false) ?
                                        gInteriorLMManager.getHandle(pInterior->getLMHandle(), mLMHandle,
                                                                     pInterior->mNormalLMapIndices[surfaceIndex]) :
                                        gInteriorLMManager.getHandle(pInterior->getLMHandle(), mLMHandle,
                                                                     pInterior->mAlarmLMapIndices[surfaceIndex]));

   const GBitmap* pOriginalLMap = originalLMapHandle->getBitmap();
   AssertFatal(pOriginalLMap != NULL, "error, no lightmap on the handle!");
   AssertFatal(pOriginalLMap->getFormat() == GBitmap::RGB, "error, bad lightmap format!");

   // First, we need to create a buffer that will receive the new lightmap
   U32 dimX = rSurface.mapSizeX;
   U32 dimY = rSurface.mapSizeY;
   U8* pNewLightmap = _newLightMapBuffer;

   // copy the original lightmap
   const U8 * src = pOriginalLMap->getAddress(rSurface.mapOffsetX, rSurface.mapOffsetY);
   U8 * dest = pNewLightmap;

   U32 runSize = rSurface.mapSizeX * 3;
   U32 srcStep = pOriginalLMap->getWidth() * 3;

   for(U32 y = 0; y < rSurface.mapSizeY; y++)
   {
      dMemcpy(dest, src, runSize);
      dest += runSize;
      src += srcStep;
   }

   // ...now we have the original lightmap, add in the animateds...
   for (U32 i = 0; i < rSurface.lightCount; i++)
   {
      const LightInfo::StateDataInfo& rInfo = rLightInfo.mStateDataInfo[rSurface.lightStateInfoStart + i];

      // Only add in states that affect this surface...duh.
      if (rInfo.curMap != NULL && rInfo.alarm == (mAlarmState != Normal))
      {
         intensityMapMerge(pNewLightmap, dimX, dimY,
                           rInfo.curMap, rInfo.curColor);
      }
   }

   // OK, now we have the final, current lightmap.  subimage it in...
   glBindTexture(GL_TEXTURE_2D, originalLMapHandle->getGLName());

   if (Con::getBoolVariable("$pref::OpenGL::disableSubImage", false))
   {
      const U8 *src = pNewLightmap;
      U8 *dest = (U8 *) pOriginalLMap->getAddress(rSurface.mapOffsetX, rSurface.mapOffsetY);
      U32 destStep = pOriginalLMap->getWidth() * 3;

      // copy back into the original lightmap
      for (U32 y = 0; y < rSurface.mapSizeY; y++)
      {
         dMemcpy(dest, src, runSize);
         src += runSize;
         dest += destStep;
      }

      glTexImage2D(GL_TEXTURE_2D,
                   0,
                   GL_RGB,
                   pOriginalLMap->getWidth(), pOriginalLMap->getHeight(),
                   0,
                   GL_RGB, GL_UNSIGNED_BYTE,
                   pOriginalLMap->getBits(0));
   }
   else
   {
      glTexSubImage2D(GL_TEXTURE_2D,
                      0,
                      rSurface.mapOffsetX, rSurface.mapOffsetY,
                      rSurface.mapSizeX,   rSurface.mapSizeY,
                      GL_RGB, GL_UNSIGNED_BYTE,
                      pNewLightmap);
   }
}


//--------------------------------------------------------------------------
void InteriorInstance::downloadLightmaps(SceneState* /*state*/,
                                         Interior*   pInterior,
                                         LightInfo&  rLightInfo)
{
   extern U16* sgActivePolyList;
   extern U32  sgActivePolyListSize;

   for (U32 i = 0; i < sgActivePolyListSize; i++)
   {
      if (rLightInfo.mSurfaceInvalid.test(sgActivePolyList[i]) == true)
      {
         updateLightMap(pInterior, rLightInfo, sgActivePolyList[i]);
         rLightInfo.mSurfaceInvalid.clear(sgActivePolyList[i]);
      }
   }
}


//--------------------------------------------------------------------------
void InteriorInstance::addChildren()
{
   if (bool(mInteriorRes) == false)
      return;

   char nameBuffer[256];
   U32 i;

   // First thing to do, add a group with our name
   SimGroup* myGroup = getGroup();

   // Load all the interior game objects...
   for(i = 0; i < mInteriorRes->getNumGameEntities(); i++)
   {
      ItrGameEntity *ent = mInteriorRes->getGameEntity(i);
      ConsoleObject *obj = ConsoleObject::create(ent->mGameClass);
      GameBase *gb = dynamic_cast<GameBase*>(obj);
      if(!gb)
      {
         Con::errorf("Invalid game class for entity: %s", ent->mGameClass);
         delete obj;
         continue;
      }
      gb->setField("dataBlock", ent->mDataBlock);
      gb->setModStaticFields(true);
      for(U32 j = 0; j < ent->mDictionary.size(); j++)
         gb->setDataField(StringTable->insert(ent->mDictionary[j].name), NULL, ent->mDictionary[j].value);
      gb->setModStaticFields(false);
      Point3F origin = ent->mPos;
      origin.convolve(mObjScale);
      getTransform().mulP(origin);
      MatrixF xform(true);
      xform.setColumn(3, origin);
      gb->setTransform(xform);
      if(!gb->registerObject())
      {
         Con::errorf("Failed to register entity: %s: %s", ent->mGameClass, ent->mDataBlock);
         delete gb;
         continue;
      }
      myGroup->addObject(gb);
   }

   // Next, for each door in our resource, we'll be creating a group
   for (i = 0; i < mInteriorRes->getNumInteriorPathFollowers(); i++)
   {
      InteriorPathFollower* pSource = mInteriorRes->getInteriorPathFollower(i);

      StringTableEntry name = StringTable->insert(pSource->mName);
      PathedInterior* child = new PathedInterior;
      child->mName             = name;
      child->mInteriorResIndex = pSource->mInteriorResIndex;
      child->mPathIndex        = pSource->mPathIndex;
      child->mOffset           = pSource->mOffset;
      child->mInteriorResName  = mInteriorFileName;
      child->setField("dataBlock", pSource->mDataBlock);

      for(U32 i = 0; i < pSource->mDictionary.size(); i++)
         child->setDataField(pSource->mDictionary[i].name, NULL, pSource->mDictionary[i].value);

      // Create a group for the door
      SimGroup* doorGroup = new SimGroup;
      dSprintf(nameBuffer, 255, "%s_g", child->mName);
      doorGroup->registerObject();
      myGroup->addObject(doorGroup, nameBuffer);

      // Ok, now we need to add the path and any triggers that affect the door here...
      Path* pSimPath      = new Path;
      pSimPath->registerObject();
      doorGroup->addObject(pSimPath);

      for (U32 j = 0; j < pSource->mWayPoints.size(); j++)
      {
         Marker* pMarker    = new Marker;
         pMarker->mSeqNum   = j;
         pMarker->mMSToNext = pSource->mWayPoints[j].msToNext;
         pMarker->mSmoothingType = pSource->mWayPoints[j].smoothingType;

         pMarker->registerObject();

         pSimPath->addObject(pMarker);

         Point3F markerPos = pSource->mWayPoints[j].pos;
         markerPos.convolve(mObjScale);
         getTransform().mulP(markerPos);
         MatrixF xform(true);
         xform.setColumn(3, markerPos);
         pMarker->setTransform(xform);
      }
      //pSimPath->finishPath();

      // Now we need to add any triggers relevant to this door...
      //
      for (U32 k = 0; k < pSource->mTriggerIds.size(); k++)
      {
         InteriorResTrigger *pITrigger = mInteriorRes->getTrigger(pSource->mTriggerIds[k]);
         // Add the object...
         Trigger* pTrigger = new Trigger;
         pTrigger->setField("dataBlock", pITrigger->mDataBlock);
         for(U32 i = 0; i < pITrigger->mDictionary.size(); i++)
            pTrigger->setDataField(StringTable->insert(pITrigger->mDictionary[i].name), NULL, pITrigger->mDictionary[i].value);

         MatrixF newXForm;
         createTriggerTransform(pITrigger, &newXForm);
         pTrigger->setTriggerPolyhedron(pITrigger->mPolyhedron);
         pTrigger->setScale(mObjScale);
         pTrigger->setTransform(newXForm);

         pTrigger->registerObject();
         doorGroup->addObject(pTrigger, pITrigger->mName);
      }

      // And finally, add the door, taking care to get the transform right...
      MatrixF childTransform(true);
      Point3F childOffset = child->mOffset;
      childOffset.convolve(mObjScale);
      getTransform().mulP(childOffset);
      childOffset.neg();
      childTransform.setColumn(3, childOffset);
      childTransform.mul(getTransform());
      child->mBaseTransform = childTransform;
      child->mBaseScale     = mObjScale;
      doorGroup->addObject(child, child->mName);
      if (!child->registerObject())
      {
         Con::warnf(ConsoleLogEntry::General, "Warning, could not register door.  Door skipped!");
         delete child;
         continue;
      }
   }
}


void InteriorInstance::createTriggerTransform(const InteriorResTrigger* trigger, MatrixF* transform)
{
   Point3F offset;
   MatrixF xform = getTransform();
   xform.getColumn(3, &offset);

   Point3F triggerOffset = trigger->mOffset;
   triggerOffset.convolve(mObjScale);
   getTransform().mulV(triggerOffset);
   offset += triggerOffset;
   xform.setColumn(3, offset);

   *transform = xform;
}

bool InteriorInstance::readLightmaps(GBitmap**** lightmaps)
{
   AssertFatal(mInteriorRes, "Error, no interior loaded!");
   AssertFatal(lightmaps, "Error, no lightmaps or numdetails result pointers");
   AssertFatal(*lightmaps == NULL, "Error, already have a pointer in the lightmaps result field!");

   // Load resource
   Stream* pStream = ResourceManager->openStream(mInteriorFileName);
   if (pStream == NULL) {
      Con::errorf(ConsoleLogEntry::General, "Unable to load interior: %s", mInteriorFileName);
      return false;
   }

   InteriorResource* pResource = new InteriorResource;
   bool success = pResource->read(*pStream);
   ResourceManager->closeStream(pStream);

   if (success == false)
   {
      delete pResource;
      return false;
   }
   AssertFatal(pResource->getNumDetailLevels() == mInteriorRes->getNumDetailLevels(),
               "Mismatched detail levels!");

   *lightmaps  = new GBitmap**[mInteriorRes->getNumDetailLevels()];

   for (U32 i = 0; i < pResource->getNumDetailLevels(); i++)
   {
      Interior* pInterior = pResource->getDetailLevel(i);
      (*lightmaps)[i] = new GBitmap*[pInterior->mLightmaps.size()];
      for (U32 j = 0; j < pInterior->mLightmaps.size(); j++)
      {
         ((*lightmaps)[i])[j] = pInterior->mLightmaps[j];
         pInterior->mLightmaps[j] = NULL;
      }
      pInterior->mLightmaps.clear();
   }

   delete pResource;
   return true;
}


