diff --git a/CMakeLists.txt b/CMakeLists.txt index 165db6b792..577b6f6b2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,7 +306,7 @@ endif() # Compiler settings if (CMAKE_COMPILER_IS_GNUCC) - add_definitions (-Wall -Wextra -Wno-unused-parameter -Wno-reorder -std=c++03 -pedantic -Wno-long-long) + add_definitions (-Wall -Wextra -Wno-unused-parameter -Wno-reorder -std=c++98 -pedantic -Wno-long-long) # Silence warnings in OGRE headers. Remove once OGRE got fixed! add_definitions (-Wno-ignored-qualifiers) diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 482007090c..e2cb0e5c41 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -72,15 +72,20 @@ add_openmw_dir (mwbase ) # Main executable + IF(OGRE_STATIC) +ADD_DEFINITIONS(-DENABLE_PLUGIN_OctreeSceneManager -DENABLE_PLUGIN_ParticleFX -DENABLE_PLUGIN_GL) +set(OGRE_STATIC_PLUGINS ${OGRE_Plugin_OctreeSceneManager_LIBRARIES} ${OGRE_Plugin_ParticleFX_LIBRARIES} ${OGRE_RenderSystem_GL_LIBRARIES}) IF(WIN32) -ADD_DEFINITIONS(-DENABLE_PLUGIN_CgProgramManager -DENABLE_PLUGIN_OctreeSceneManager -DENABLE_PLUGIN_ParticleFX -DENABLE_PLUGIN_-DENABLE_PLUGIN_Direct3D9 -DENABLE_PLUGIN_GL) -set(OGRE_STATIC_PLUGINS ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${OGRE_Plugin_OctreeSceneManager_LIBRARIES} ${OGRE_Plugin_ParticleFX_LIBRARIES} ${OGRE_RenderSystem_Direct3D9_LIBRARIES} ${OGRE_RenderSystem_GL_LIBRARIES}) -ELSE(WIN32) -ADD_DEFINITIONS(-DENABLE_PLUGIN_CgProgramManager -DENABLE_PLUGIN_OctreeSceneManager -DENABLE_PLUGIN_ParticleFX -DENABLE_PLUGIN_GL) -set(OGRE_STATIC_PLUGINS ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${Cg_LIBRARIES} ${OGRE_Plugin_OctreeSceneManager_LIBRARIES} ${OGRE_Plugin_ParticleFX_LIBRARIES} ${OGRE_RenderSystem_GL_LIBRARIES}) +ADD_DEFINITIONS(-DENABLE_PLUGIN_Direct3D9) +list (APPEND OGRE_STATIC_PLUGINS ${OGRE_RenderSystem_Direct3D9_LIBRARIES}) ENDIF(WIN32) +IF (Cg_FOUND) +ADD_DEFINITIONS(-DENABLE_PLUGIN_CgProgramManager) +list (APPEND OGRE_STATIC_PLUGINS ${OGRE_Plugin_CgProgramManager_LIBRARIES} ${Cg_LIBRARIES}) +ENDIF (Cg_FOUND) ENDIF(OGRE_STATIC) + add_executable(openmw ${OPENMW_LIBS} ${OPENMW_LIBS_HEADER} ${OPENMW_FILES} diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 60ecd43034..f8499d1e55 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -203,48 +203,6 @@ unsigned int Moon::getPhaseInt() const return 0; } -void SkyManager::ModVertexAlpha(Entity* ent, unsigned int meshType) -{ - // Get the vertex colour buffer of this mesh - const Ogre::VertexElement* ves_diffuse = ent->getMesh()->getSubMesh(0)->vertexData->vertexDeclaration->findElementBySemantic( Ogre::VES_DIFFUSE ); - HardwareVertexBufferSharedPtr colourBuffer = ent->getMesh()->getSubMesh(0)->vertexData->vertexBufferBinding->getBuffer(ves_diffuse->getSource()); - - // Lock - void* pData = colourBuffer->lock(HardwareBuffer::HBL_NORMAL); - - // Iterate over all vertices - int vertex_size = colourBuffer->getVertexSize(); - float * currentVertex = NULL; - for (unsigned int i=0; igetNumVertices(); ++i) - { - // Get a pointer to the vertex colour - ves_diffuse->baseVertexPointerToElement( pData, ¤tVertex ); - - unsigned char alpha=0; - if (meshType == 0) alpha = i%2 ? 0 : 255; // this is a cylinder, so every second vertex belongs to the bottom-most row - else if (meshType == 1) - { - if (i>= 49 && i <= 64) alpha = 0; // bottom-most row - else if (i>= 33 && i <= 48) alpha = 64; // second bottom-most row - else alpha = 255; - } - // NB we would have to swap R and B depending on rendersystem specific VertexElementType, but doesn't matter since they are both 1 - uint8 tmpR = static_cast(255); - uint8 tmpG = static_cast(255); - uint8 tmpB = static_cast(255); - uint8 tmpA = static_cast(alpha); - - // Modify - *((uint32*)currentVertex) = tmpR | (tmpG << 8) | (tmpB << 16) | (tmpA << 24); - - // Move to the next vertex - pData = static_cast (pData) + vertex_size; - } - - // Unlock - ent->getMesh()->getSubMesh(0)->vertexData->vertexBufferBinding->getBuffer(ves_diffuse->getSource())->unlock(); -} - SkyManager::SkyManager (SceneNode* pMwRoot, Camera* pCamera) : mHour(0.0f) , mDay(0) @@ -357,7 +315,6 @@ void SkyManager::create() atmosphere_ent->setRenderQueueGroup(RQG_SkiesEarly); atmosphere_ent->setVisibilityFlags(RV_Sky); atmosphere_ent->getSubEntity (0)->setMaterialName ("openmw_atmosphere"); - ModVertexAlpha(atmosphere_ent, 0); } @@ -371,8 +328,6 @@ void SkyManager::create() clouds_ent->setRenderQueueGroup(RQG_SkiesEarly+5); clouds_ent->getSubEntity(0)->setMaterialName ("openmw_clouds"); clouds_ent->setCastShadows(false); - - ModVertexAlpha(clouds_ent, 1); } mCreated = true; diff --git a/apps/openmw/mwrender/sky.hpp b/apps/openmw/mwrender/sky.hpp index ee13608531..52fd7b4aa8 100644 --- a/apps/openmw/mwrender/sky.hpp +++ b/apps/openmw/mwrender/sky.hpp @@ -218,8 +218,6 @@ namespace MWRender float mGlare; // target float mGlareFade; // actual - void ModVertexAlpha(Ogre::Entity* ent, unsigned int meshType); - bool mEnabled; bool mSunEnabled; bool mMasserEnabled; diff --git a/apps/openmw/mwrender/terrain.cpp b/apps/openmw/mwrender/terrain.cpp index 676139cf58..2c2e9e6fcf 100644 --- a/apps/openmw/mwrender/terrain.cpp +++ b/apps/openmw/mwrender/terrain.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../mwworld/esmstore.hpp" @@ -178,6 +179,16 @@ namespace MWRender } } + // when loading from a heightmap, Ogre::Terrain does not update the derived data (normal map, LOD) + // synchronously, even if we supply synchronous = true parameter to loadTerrain. + // the following to be the only way to make sure derived data is ready when rendering the next frame. + while (mTerrainGroup.isDerivedDataUpdateInProgress()) + { + // we need to wait for this to finish + OGRE_THREAD_SLEEP(5); + Root::getSingleton().getWorkQueue()->processResponses(); + } + mTerrainGroup.freeTemporaryResources(); } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index db4fb67ff9..5f7a4e3200 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -331,26 +331,26 @@ namespace MWWorld { return mGlobalVariables->getGlobals(); } - + std::string World::getCurrentCellName () const { std::string name; Ptr::CellStore *cell = mWorldScene->getCurrentCell(); if (cell->mCell->isExterior()) - { + { if (cell->mCell->mName != "") - { + { name = cell->mCell->mName; - } - else - { + } + else + { const ESM::Region* region = MWBase::Environment::get().getWorld()->getStore().get().search(cell->mCell->mRegion); if (region) name = region->mName; else - { + { const ESM::GameSetting *setting = MWBase::Environment::get().getWorld()->getStore().get().search("sDefaultCellname"); @@ -360,13 +360,13 @@ namespace MWWorld name = "Wilderness"; } - } - } - else - { + } + } + else + { name = cell->mCell->mName; - } - + } + return name; } @@ -455,12 +455,12 @@ namespace MWWorld if (!reference.getRefData().isEnabled()) { reference.getRefData().enable(); - + if(mWorldScene->getActiveCells().find (reference.getCell()) != mWorldScene->getActiveCells().end() && reference.getRefData().getCount()) mWorldScene->addObjectToScene (reference); } } - + void World::removeContainerScripts(const Ptr& reference) { if( reference.getTypeName()==typeid (ESM::Container).name() || @@ -485,7 +485,7 @@ namespace MWWorld if (reference.getRefData().isEnabled()) { reference.getRefData().disable(); - + if(mWorldScene->getActiveCells().find (reference.getCell())!=mWorldScene->getActiveCells().end() && reference.getRefData().getCount()) mWorldScene->removeObjectFromScene (reference); } @@ -716,14 +716,14 @@ namespace MWWorld void World::moveObject(const Ptr &ptr, CellStore &newCell, float x, float y, float z) { ESM::Position &pos = ptr.getRefData().getPosition(); - pos.pos[0] = x, pos.pos[1] = y, pos.pos[2] = z; + pos.pos[0] = x; + pos.pos[1] = y; + pos.pos[2] = z; Ogre::Vector3 vec(x, y, z); CellStore *currCell = ptr.getCell(); bool isPlayer = ptr == mPlayer->getPlayer(); bool haveToMove = mWorldScene->isCellActive(*currCell) || isPlayer; - - removeContainerScripts(ptr); if (*currCell != newCell) { @@ -736,7 +736,8 @@ namespace MWWorld int cellY = newCell.mCell->getGridY(); mWorldScene->changeCell(cellX, cellY, pos, false); } - else { + else + { if (!mWorldScene->isCellActive(*currCell)) copyObjectToCell(ptr, newCell, pos); else if (!mWorldScene->isCellActive(newCell)) @@ -744,6 +745,7 @@ namespace MWWorld MWWorld::Class::get(ptr).copyToCell(ptr, newCell); mWorldScene->removeObjectFromScene(ptr); mLocalScripts.remove(ptr); + removeContainerScripts (ptr); haveToMove = false; } else @@ -751,10 +753,18 @@ namespace MWWorld MWWorld::Ptr copy = MWWorld::Class::get(ptr).copyToCell(ptr, newCell); - addContainerScripts(copy, &newCell); - mRendering->moveObjectToCell(copy, vec, currCell); + std::string script = + MWWorld::Class::get(ptr).getScript(ptr); + if (!script.empty()) + { + mLocalScripts.remove(ptr); + removeContainerScripts (ptr); + mLocalScripts.add(script, copy); + addContainerScripts (copy, &newCell); + } + if (MWWorld::Class::get(ptr).isActor()) { MWBase::MechanicsManager *mechMgr = @@ -763,16 +773,6 @@ namespace MWWorld mechMgr->removeActor(ptr); mechMgr->addActor(copy); } - else - { - std::string script = - MWWorld::Class::get(ptr).getScript(ptr); - if (!script.empty()) - { - mLocalScripts.remove(ptr); - mLocalScripts.add(script, copy); - } - } } ptr.getRefData().setCount(0); } diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index d13568a688..bb200ee570 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -64,7 +64,7 @@ macro (opencs_units_noqt dir) foreach (u ${ARGN}) add_unit (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_units) +endmacro (opencs_units_noqt) macro (opencs_hdrs dir) foreach (u ${ARGN}) @@ -76,4 +76,4 @@ macro (opencs_hdrs_noqt dir) foreach (u ${ARGN}) add_hdr (OPENCS ${dir} ${u}) endforeach (u) -endmacro (opencs_hdrs) +endmacro (opencs_hdrs_noqt) diff --git a/extern/shiny/CMakeLists.txt b/extern/shiny/CMakeLists.txt index 603336413e..6eadcc1676 100644 --- a/extern/shiny/CMakeLists.txt +++ b/extern/shiny/CMakeLists.txt @@ -24,13 +24,6 @@ set(SOURCE_FILES Main/ShaderSet.cpp ) -# In Debug mode, write the shader sources to the current directory -if (DEFINED CMAKE_BUILD_TYPE) - if (CMAKE_BUILD_TYPE STREQUAL "Debug") - add_definitions(-DSHINY_WRITE_SHADER_DEBUG) - endif() -endif() - if (DEFINED SHINY_USE_WAVE_SYSTEM_INSTALL) # use system install else() diff --git a/extern/shiny/Main/Factory.cpp b/extern/shiny/Main/Factory.cpp index 82d6648110..21f13e30b6 100644 --- a/extern/shiny/Main/Factory.cpp +++ b/extern/shiny/Main/Factory.cpp @@ -50,7 +50,7 @@ namespace sh { assert(mCurrentLanguage != Language_None); - bool anyShaderDirty = false; + bool removeBinaryCache = false; if (boost::filesystem::exists (mPlatform->getCacheFolder () + "/lastModified.txt")) { @@ -182,24 +182,33 @@ namespace sh } } - std::string sourceFile = mPlatform->getBasePath() + "/" + it->second->findChild("source")->getValue(); + std::string sourceAbsolute = mPlatform->getBasePath() + "/" + it->second->findChild("source")->getValue(); + std::string sourceRelative = it->second->findChild("source")->getValue(); ShaderSet newSet (it->second->findChild("type")->getValue(), cg_profile, hlsl_profile, - sourceFile, + sourceAbsolute, mPlatform->getBasePath(), it->first, &mGlobalSettings); - int lastModified = boost::filesystem::last_write_time (boost::filesystem::path(sourceFile)); - if (mShadersLastModified.find(sourceFile) != mShadersLastModified.end() - && mShadersLastModified[sourceFile] != lastModified) + int lastModified = boost::filesystem::last_write_time (boost::filesystem::path(sourceAbsolute)); + mShadersLastModifiedNew[sourceRelative] = lastModified; + if (mShadersLastModified.find(sourceRelative) != mShadersLastModified.end()) { - newSet.markDirty (); - anyShaderDirty = true; + if (mShadersLastModified[sourceRelative] != lastModified) + { + // delete any outdated shaders based on this shader set + removeCache (it->first); + // remove the whole binary cache (removing only the individual shaders does not seem to be possible at this point with OGRE) + removeBinaryCache = true; + } + } + else + { + // if we get here, this is either the first run or a new shader file was added + // in both cases we can safely delete + removeCache (it->first); } - - mShadersLastModified[sourceFile] = lastModified; - mShaderSets.insert(std::make_pair(it->first, newSet)); } } @@ -293,7 +302,7 @@ namespace sh } } - if (mPlatform->supportsShaderSerialization () && mReadMicrocodeCache && !anyShaderDirty) + if (mPlatform->supportsShaderSerialization () && mReadMicrocodeCache && !removeBinaryCache) { std::string file = mPlatform->getCacheFolder () + "/shShaderCache.txt"; if (boost::filesystem::exists(file)) @@ -313,11 +322,11 @@ namespace sh if (mReadSourceCache) { - // save the last modified time of shader sources + // save the last modified time of shader sources (as of when they were loaded) std::ofstream file; file.open(std::string(mPlatform->getCacheFolder () + "/lastModified.txt").c_str()); - for (LastModifiedMap::const_iterator it = mShadersLastModified.begin(); it != mShadersLastModified.end(); ++it) + for (LastModifiedMap::const_iterator it = mShadersLastModifiedNew.begin(); it != mShadersLastModifiedNew.end(); ++it) { file << it->first << "\n" << it->second << std::endl; } @@ -580,4 +589,41 @@ namespace sh assert(m); m->createForConfiguration (configuration, 0); } + + void Factory::removeCache(const std::string& pattern) + { + if ( boost::filesystem::exists(mPlatform->getCacheFolder()) + && boost::filesystem::is_directory(mPlatform->getCacheFolder())) + { + boost::filesystem::directory_iterator end_iter; + for( boost::filesystem::directory_iterator dir_iter(mPlatform->getCacheFolder()) ; dir_iter != end_iter ; ++dir_iter) + { + if (boost::filesystem::is_regular_file(dir_iter->status()) ) + { + boost::filesystem::path file = dir_iter->path(); + + std::string pathname = file.filename().string(); + + // get first part of filename, e.g. main_fragment_546457654 -> main_fragment + // there is probably a better method for this... + std::vector tokens; + boost::split(tokens, pathname, boost::is_any_of("_")); + tokens.erase(--tokens.end()); + std::string shaderName; + for (std::vector::const_iterator vector_iter = tokens.begin(); vector_iter != tokens.end();) + { + shaderName += *(vector_iter++); + if (vector_iter != tokens.end()) + shaderName += "_"; + } + + if (shaderName == pattern) + { + boost::filesystem::remove(file); + std::cout << "Removing outdated shader: " << file << std::endl; + } + } + } + } + } } diff --git a/extern/shiny/Main/Factory.hpp b/extern/shiny/Main/Factory.hpp index 799dd71eb0..6d4175c976 100644 --- a/extern/shiny/Main/Factory.hpp +++ b/extern/shiny/Main/Factory.hpp @@ -185,6 +185,7 @@ namespace sh ConfigurationMap mConfigurations; LodConfigurationMap mLodConfigurations; LastModifiedMap mShadersLastModified; + LastModifiedMap mShadersLastModifiedNew; PropertySetGet mGlobalSettings; @@ -201,6 +202,8 @@ namespace sh MaterialInstance* findInstance (const std::string& name); MaterialInstance* searchInstance (const std::string& name); + + void removeCache (const std::string& pattern); }; } diff --git a/extern/shiny/Main/ShaderInstance.cpp b/extern/shiny/Main/ShaderInstance.cpp index 07ef8dfe28..1539128aba 100644 --- a/extern/shiny/Main/ShaderInstance.cpp +++ b/extern/shiny/Main/ShaderInstance.cpp @@ -337,8 +337,7 @@ namespace sh size_t pos; bool readCache = Factory::getInstance ().getReadSourceCache () && boost::filesystem::exists( - Factory::getInstance ().getCacheFolder () + "/" + mName) - && !mParent->isDirty (); + Factory::getInstance ().getCacheFolder () + "/" + mName); bool writeCache = Factory::getInstance ().getWriteSourceCache (); @@ -363,12 +362,6 @@ namespace sh if (Factory::getInstance ().getShaderDebugOutputEnabled ()) writeDebugFile(source, name + ".pre"); - else - { - #ifdef SHINY_WRITE_SHADER_DEBUG - writeDebugFile(source, name + ".pre"); - #endif - } // why do we need our own preprocessor? there are several custom commands available in the shader files // (for example for binding uniforms to properties or auto constants) - more below. it is important that these @@ -648,12 +641,6 @@ namespace sh if (Factory::getInstance ().getShaderDebugOutputEnabled ()) writeDebugFile(source, name); - else - { -#ifdef SHINY_WRITE_SHADER_DEBUG - writeDebugFile(source, name); -#endif - } if (!mProgram->getSupported()) { diff --git a/extern/shiny/Main/ShaderSet.cpp b/extern/shiny/Main/ShaderSet.cpp index 2702ece194..413d7d1a26 100644 --- a/extern/shiny/Main/ShaderSet.cpp +++ b/extern/shiny/Main/ShaderSet.cpp @@ -17,7 +17,6 @@ namespace sh , mName(name) , mCgProfile(cgProfile) , mHlslProfile(hlslProfile) - , mIsDirty(false) { if (type == "vertex") mType = GPT_Vertex; diff --git a/extern/shiny/Main/ShaderSet.hpp b/extern/shiny/Main/ShaderSet.hpp index 776750598a..a423b6779f 100644 --- a/extern/shiny/Main/ShaderSet.hpp +++ b/extern/shiny/Main/ShaderSet.hpp @@ -30,9 +30,6 @@ namespace sh /// so it does not matter if you pass any extra properties that the shader does not care about. ShaderInstance* getInstance (PropertySetGet* properties); - void markDirty() { mIsDirty = true; } - ///< Signals that the cache is out of date, and thus should not be used this time - private: PropertySetGet* getCurrentGlobalSettings() const; std::string getBasePath() const; @@ -41,12 +38,8 @@ namespace sh std::string getHlslProfile() const; int getType() const; - bool isDirty() { return mIsDirty; } - friend class ShaderInstance; - bool mIsDirty; - private: GpuProgramType mType; std::string mSource; diff --git a/files/materials/atmosphere.shader b/files/materials/atmosphere.shader index 295fa93768..16edc78c56 100644 --- a/files/materials/atmosphere.shader +++ b/files/materials/atmosphere.shader @@ -7,19 +7,18 @@ SH_BEGIN_PROGRAM shUniform(float4x4, wvp) @shAutoConstant(wvp, worldviewproj_matrix) - shColourInput(float4) - shOutput(float4, colourPassthrough) + shOutput(float, alphaFade) SH_START_PROGRAM { shOutputPosition = shMatrixMult(wvp, shInputPosition); - colourPassthrough = colour; + alphaFade = shInputPosition.z < 150.0 ? 0.0 : 1.0; } #else SH_BEGIN_PROGRAM - shInput(float4, colourPassthrough) + shInput(float, alphaFade) #if MRT shDeclareMrtOutput(1) #endif @@ -27,7 +26,7 @@ SH_START_PROGRAM { - shOutputColour(0) = colourPassthrough * atmosphereColour; + shOutputColour(0) = atmosphereColour * float4(1,1,1,alphaFade); #if MRT shOutputColour(1) = float4(1,1,1,1); diff --git a/files/materials/clouds.shader b/files/materials/clouds.shader index f4258bf5d4..4b1868fb40 100644 --- a/files/materials/clouds.shader +++ b/files/materials/clouds.shader @@ -8,21 +8,20 @@ shUniform(float4x4, wvp) @shAutoConstant(wvp, worldviewproj_matrix) shVertexInput(float2, uv0) shOutput(float2, UV) - shColourInput(float4) - shOutput(float4, colourPassthrough) + shOutput(float, alphaFade) SH_START_PROGRAM { - colourPassthrough = colour; shOutputPosition = shMatrixMult(wvp, shInputPosition); UV = uv0; + alphaFade = (shInputPosition.z <= 200.f) ? ((shInputPosition.z <= 100.f) ? 0.0 : 0.25) : 1.0; } #else SH_BEGIN_PROGRAM shInput(float2, UV) - shInput(float4, colourPassthrough) + shInput(float, alphaFade) #if MRT shDeclareMrtOutput(1) #endif @@ -42,7 +41,7 @@ float4 albedo = shSample(diffuseMap1, scrolledUV) * (1-cloudBlendFactor) + shSample(diffuseMap2, scrolledUV) * cloudBlendFactor; - shOutputColour(0) = colourPassthrough * float4(cloudColour, 1) * albedo * float4(1,1,1, cloudOpacity); + shOutputColour(0) = float4(cloudColour, 1) * albedo * float4(1,1,1, cloudOpacity * alphaFade); #if MRT shOutputColour(1) = float4(1,1,1,1); diff --git a/readme.txt b/readme.txt index 91690ff574..3124744c30 100644 --- a/readme.txt +++ b/readme.txt @@ -114,8 +114,10 @@ Bug #521: MWGui::InventoryWindow creates a duplicate player actor at the origin Bug #524: Beast races are able to wear shoes Bug #527: Background music fails to play Bug #533: The arch at Gnisis entrance is not displayed +Bug #534: Terrain gets its correct shape only some time after the cell is loaded Bug #536: The same entry can be added multiple times to the journal Bug #539: Race selection is broken +Bug #544: Terrain normal map corrupt when the map is rendered Feature #39: Video Playback Feature #151: ^-escape sequences in text output Feature #392: Add AI related script functions