mirror of
				https://github.com/OpenMW/openmw.git
				synced 2025-10-29 03:26:38 +00:00 
			
		
		
		
	Merge branch 'terrain18' into occlusionquery
This commit is contained in:
		
						commit
						cc7d3af701
					
				
					 7 changed files with 173 additions and 159 deletions
				
			
		|  | @ -23,7 +23,8 @@ RenderingManager::RenderingManager (OEngine::Render::OgreRenderer& _rend, const | ||||||
| :mRendering(_rend), mObjects(mRendering), mActors(mRendering, environment), mAmbientMode(0), mDebugging(engine) | :mRendering(_rend), mObjects(mRendering), mActors(mRendering, environment), mAmbientMode(0), mDebugging(engine) | ||||||
| { | { | ||||||
|     mRendering.createScene("PlayerCam", 55, 5); |     mRendering.createScene("PlayerCam", 55, 5); | ||||||
|     mTerrainManager = new TerrainManager(mRendering.getScene()); |     mTerrainManager = new TerrainManager(mRendering.getScene(), | ||||||
|  |                                          environment); | ||||||
| 
 | 
 | ||||||
|     //The fog type must be set before any terrain objects are created as if the
 |     //The fog type must be set before any terrain objects are created as if the
 | ||||||
|     //fog type is set to FOG_NONE then the initially created terrain won't have any fog
 |     //fog type is set to FOG_NONE then the initially created terrain won't have any fog
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| #include <OgreTerrain.h> | #include <OgreTerrain.h> | ||||||
| #include <OgreTerrainGroup.h> | #include <OgreTerrainGroup.h> | ||||||
|  | #include <boost/lexical_cast.hpp> | ||||||
|  | 
 | ||||||
|  | #include "../mwworld/world.hpp" | ||||||
| 
 | 
 | ||||||
| #include "terrainmaterial.hpp" | #include "terrainmaterial.hpp" | ||||||
| #include "terrain.hpp" | #include "terrain.hpp" | ||||||
| 
 | 
 | ||||||
| #include "components/esm/loadland.hpp" |  | ||||||
| 
 |  | ||||||
| #include <boost/lexical_cast.hpp> |  | ||||||
| 
 | 
 | ||||||
| using namespace Ogre; | using namespace Ogre; | ||||||
| 
 | 
 | ||||||
|  | @ -15,33 +15,33 @@ namespace MWRender | ||||||
| 
 | 
 | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
|      |      | ||||||
|     TerrainManager::TerrainManager(SceneManager* mgr) |     TerrainManager::TerrainManager(Ogre::SceneManager* mgr, const MWWorld::Environment& evn) : | ||||||
|  |         mEnvironment(evn), mTerrainGroup(TerrainGroup(mgr, Terrain::ALIGN_X_Z, mLandSize, mWorldSize)) | ||||||
|     { |     { | ||||||
|         mTerrainGlobals = OGRE_NEW TerrainGlobalOptions(); |  | ||||||
| 
 | 
 | ||||||
|         TerrainMaterialGeneratorPtr matGen; |         TerrainMaterialGeneratorPtr matGen; | ||||||
|         TerrainMaterialGeneratorB* matGenP = new TerrainMaterialGeneratorB(); |         TerrainMaterialGeneratorB* matGenP = new TerrainMaterialGeneratorB(); | ||||||
|         matGen.bind(matGenP); |         matGen.bind(matGenP); | ||||||
|         mTerrainGlobals->setDefaultMaterialGenerator(matGen); |         mTerrainGlobals.setDefaultMaterialGenerator(matGen); | ||||||
| 
 | 
 | ||||||
|         TerrainMaterialGenerator::Profile* const activeProfile = |         TerrainMaterialGenerator::Profile* const activeProfile = | ||||||
|             mTerrainGlobals->getDefaultMaterialGenerator() |             mTerrainGlobals.getDefaultMaterialGenerator() | ||||||
|                            ->getActiveProfile(); |                            ->getActiveProfile(); | ||||||
|         mActiveProfile = static_cast<TerrainMaterialGeneratorB::SM2Profile*>(activeProfile); |         mActiveProfile = static_cast<TerrainMaterialGeneratorB::SM2Profile*>(activeProfile); | ||||||
| 
 | 
 | ||||||
|         //The pixel error should be as high as possible without it being noticed
 |         //The pixel error should be as high as possible without it being noticed
 | ||||||
|         //as it governs how fast mesh quality decreases.
 |         //as it governs how fast mesh quality decreases.
 | ||||||
|         mTerrainGlobals->setMaxPixelError(8); |         mTerrainGlobals.setMaxPixelError(8); | ||||||
| 
 | 
 | ||||||
|         mTerrainGlobals->setLayerBlendMapSize(32); |         mTerrainGlobals.setLayerBlendMapSize(32); | ||||||
|         mTerrainGlobals->setDefaultGlobalColourMapSize(65); |         mTerrainGlobals.setDefaultGlobalColourMapSize(65); | ||||||
| 
 | 
 | ||||||
|         //10 (default) didn't seem to be quite enough
 |         //10 (default) didn't seem to be quite enough
 | ||||||
|         mTerrainGlobals->setSkirtSize(128); |         mTerrainGlobals.setSkirtSize(128); | ||||||
| 
 | 
 | ||||||
|         //due to the sudden flick between composite and non composite textures,
 |         //due to the sudden flick between composite and non composite textures,
 | ||||||
|         //this seemed the distance where it wasn't too noticeable
 |         //this seemed the distance where it wasn't too noticeable
 | ||||||
|         mTerrainGlobals->setCompositeMapDistance(mWorldSize*2); |         mTerrainGlobals.setCompositeMapDistance(mWorldSize*2); | ||||||
|          |          | ||||||
|         mActiveProfile->setLightmapEnabled(false); |         mActiveProfile->setLightmapEnabled(false); | ||||||
|         mActiveProfile->setLayerSpecularMappingEnabled(false); |         mActiveProfile->setLayerSpecularMappingEnabled(false); | ||||||
|  | @ -53,16 +53,11 @@ namespace MWRender | ||||||
|         //disabled
 |         //disabled
 | ||||||
|         mActiveProfile->setCompositeMapEnabled(false); |         mActiveProfile->setCompositeMapEnabled(false); | ||||||
| 
 | 
 | ||||||
|         mTerrainGroup = OGRE_NEW TerrainGroup(mgr, |         mTerrainGroup.setOrigin(Vector3(mWorldSize/2, | ||||||
|                                               Terrain::ALIGN_X_Z, |  | ||||||
|                                               mLandSize, |  | ||||||
|                                               mWorldSize); |  | ||||||
| 
 |  | ||||||
|         mTerrainGroup->setOrigin(Vector3(mWorldSize/2, |  | ||||||
|                                          0, |                                          0, | ||||||
|                                          -mWorldSize/2)); |                                          -mWorldSize/2)); | ||||||
| 
 | 
 | ||||||
|         Terrain::ImportData& importSettings = mTerrainGroup->getDefaultImportSettings(); |         Terrain::ImportData& importSettings = mTerrainGroup.getDefaultImportSettings(); | ||||||
| 
 | 
 | ||||||
|         importSettings.inputBias    = 0; |         importSettings.inputBias    = 0; | ||||||
|         importSettings.terrainSize  = mLandSize; |         importSettings.terrainSize  = mLandSize; | ||||||
|  | @ -77,22 +72,20 @@ namespace MWRender | ||||||
| 
 | 
 | ||||||
|     TerrainManager::~TerrainManager() |     TerrainManager::~TerrainManager() | ||||||
|     { |     { | ||||||
|         OGRE_DELETE mTerrainGroup; |  | ||||||
|         OGRE_DELETE mTerrainGlobals; |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
|      |      | ||||||
|     void TerrainManager::setDiffuse(const ColourValue& diffuse) |     void TerrainManager::setDiffuse(const ColourValue& diffuse) | ||||||
|     { |     { | ||||||
|         mTerrainGlobals->setCompositeMapDiffuse(diffuse); |         mTerrainGlobals.setCompositeMapDiffuse(diffuse); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
|      |      | ||||||
|     void TerrainManager::setAmbient(const ColourValue& ambient) |     void TerrainManager::setAmbient(const ColourValue& ambient) | ||||||
|     { |     { | ||||||
|         mTerrainGlobals->setCompositeMapAmbient(ambient); |         mTerrainGlobals.setCompositeMapAmbient(ambient); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
|  | @ -102,6 +95,12 @@ namespace MWRender | ||||||
|         const int cellX = store->cell->getGridX(); |         const int cellX = store->cell->getGridX(); | ||||||
|         const int cellY = store->cell->getGridY(); |         const int cellY = store->cell->getGridY(); | ||||||
| 
 | 
 | ||||||
|  |         ESM::Land* land = mEnvironment.mWorld->getStore().lands.search(cellX, cellY); | ||||||
|  |         if ( land != NULL ) | ||||||
|  |         { | ||||||
|  |             land->loadData(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         //split the cell terrain into four segments
 |         //split the cell terrain into four segments
 | ||||||
|         const int numTextures = ESM::Land::LAND_TEXTURE_SIZE/2; |         const int numTextures = ESM::Land::LAND_TEXTURE_SIZE/2; | ||||||
| 
 | 
 | ||||||
|  | @ -110,7 +109,7 @@ namespace MWRender | ||||||
|             for ( int y = 0; y < 2; y++ ) |             for ( int y = 0; y < 2; y++ ) | ||||||
|             { |             { | ||||||
|                 Terrain::ImportData terrainData = |                 Terrain::ImportData terrainData = | ||||||
|                     mTerrainGroup->getDefaultImportSettings(); |                     mTerrainGroup.getDefaultImportSettings(); | ||||||
| 
 | 
 | ||||||
|                 const int terrainX = cellX * 2 + x; |                 const int terrainX = cellX * 2 + x; | ||||||
|                 const int terrainY = cellY * 2 + y; |                 const int terrainY = cellY * 2 + y; | ||||||
|  | @ -122,42 +121,51 @@ namespace MWRender | ||||||
|                                                       mLandSize*mLandSize, |                                                       mLandSize*mLandSize, | ||||||
|                                                       MEMCATEGORY_GEOMETRY); |                                                       MEMCATEGORY_GEOMETRY); | ||||||
| 
 | 
 | ||||||
|                 //copy the height data row by row
 |                 if ( land != NULL ) | ||||||
|                 for ( int terrainCopyY = 0; terrainCopyY < mLandSize; terrainCopyY++ ) |  | ||||||
|                 { |                 { | ||||||
|                                            //the offset of the current segment
 |                     //copy the height data row by row
 | ||||||
|                     const size_t yOffset = y * (mLandSize-1) * ESM::Land::LAND_SIZE + |                     for ( int terrainCopyY = 0; terrainCopyY < mLandSize; terrainCopyY++ ) | ||||||
|                                            //offset of the row
 |                     { | ||||||
|                                            terrainCopyY * ESM::Land::LAND_SIZE; |                                                //the offset of the current segment
 | ||||||
|                     const size_t xOffset = x * (mLandSize-1); |                         const size_t yOffset = y * (mLandSize-1) * ESM::Land::LAND_SIZE + | ||||||
|  |                                                //offset of the row
 | ||||||
|  |                                                terrainCopyY * ESM::Land::LAND_SIZE; | ||||||
|  |                         const size_t xOffset = x * (mLandSize-1); | ||||||
| 
 | 
 | ||||||
|                     memcpy(&terrainData.inputFloat[terrainCopyY*mLandSize], |                         memcpy(&terrainData.inputFloat[terrainCopyY*mLandSize], | ||||||
|                            &store->land[1][1]->landData->heights[yOffset + xOffset], |                                &land->landData->heights[yOffset + xOffset], | ||||||
|                            mLandSize*sizeof(float)); |                                mLandSize*sizeof(float)); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 else | ||||||
|  |                 { | ||||||
|  |                     memset(terrainData.inputFloat, 0, mLandSize*mLandSize*sizeof(float)); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 std::map<uint16_t, int> indexes; |                 std::map<uint16_t, int> indexes; | ||||||
|                 initTerrainTextures(&terrainData, store, |                 initTerrainTextures(&terrainData, cellX, cellY, | ||||||
|                                     x * numTextures, y * numTextures, |                                     x * numTextures, y * numTextures, | ||||||
|                                     numTextures, indexes); |                                     numTextures, indexes); | ||||||
| 
 | 
 | ||||||
|                 if (mTerrainGroup->getTerrain(terrainX, terrainY) == NULL) |                 if (mTerrainGroup.getTerrain(terrainX, terrainY) == NULL) | ||||||
|                 { |                 { | ||||||
|                     mTerrainGroup->defineTerrain(terrainX, terrainY, &terrainData); |                     mTerrainGroup.defineTerrain(terrainX, terrainY, &terrainData); | ||||||
| 
 | 
 | ||||||
|                     mTerrainGroup->loadTerrain(terrainX, terrainY, true); |                     mTerrainGroup.loadTerrain(terrainX, terrainY, true); | ||||||
| 
 | 
 | ||||||
|                     Terrain* terrain = mTerrainGroup->getTerrain(terrainX, terrainY); |                     Terrain* terrain = mTerrainGroup.getTerrain(terrainX, terrainY); | ||||||
|                     initTerrainBlendMaps(terrain, store, |                     initTerrainBlendMaps(terrain, | ||||||
|  |                                          cellX, cellY, | ||||||
|                                          x * numTextures, y * numTextures, |                                          x * numTextures, y * numTextures, | ||||||
|                                          numTextures, |                                          numTextures, | ||||||
|                                          indexes); |                                          indexes); | ||||||
| 
 | 
 | ||||||
|                     if ( store->land[1][1]->landData->usingColours ) |                     if ( land->landData->usingColours ) | ||||||
|                     { |                     { | ||||||
|                         // disable or enable global colour map (depends on available vertex colours)
 |                         // disable or enable global colour map (depends on available vertex colours)
 | ||||||
|                         mActiveProfile->setGlobalColourMapEnabled(true); |                         mActiveProfile->setGlobalColourMapEnabled(true); | ||||||
|                         TexturePtr vertex = getVertexColours(store, |                         TexturePtr vertex = getVertexColours(land, | ||||||
|  |                                                              cellX, cellY, | ||||||
|                                                              x*(mLandSize-1), |                                                              x*(mLandSize-1), | ||||||
|                                                              y*(mLandSize-1), |                                                              y*(mLandSize-1), | ||||||
|                                                              mLandSize); |                                                              mLandSize); | ||||||
|  | @ -177,7 +185,7 @@ namespace MWRender | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         mTerrainGroup->freeTemporaryResources(); |         mTerrainGroup.freeTemporaryResources(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
|  | @ -188,8 +196,8 @@ namespace MWRender | ||||||
|         { |         { | ||||||
|             for ( int y = 0; y < 2; y++ ) |             for ( int y = 0; y < 2; y++ ) | ||||||
|             { |             { | ||||||
|                 mTerrainGroup->unloadTerrain(store->cell->getGridX() * 2 + x, |                 mTerrainGroup.unloadTerrain(store->cell->getGridX() * 2 + x, | ||||||
|                                              store->cell->getGridY() * 2 + y); |                                             store->cell->getGridY() * 2 + y); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -197,11 +205,10 @@ namespace MWRender | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|     void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData, |     void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData, | ||||||
|                                              MWWorld::Ptr::CellStore* store, |                                              int cellX, int cellY, | ||||||
|                                              int fromX, int fromY, int size, |                                              int fromX, int fromY, int size, | ||||||
|                                              std::map<uint16_t, int>& indexes) |                                              std::map<uint16_t, int>& indexes) | ||||||
|     { |     { | ||||||
|         assert(store != NULL && "store must be a valid pointer"); |  | ||||||
|         assert(terrainData != NULL && "Must have valid terrain data"); |         assert(terrainData != NULL && "Must have valid terrain data"); | ||||||
|         assert(fromX >= 0 && fromY >= 0 && |         assert(fromX >= 0 && fromY >= 0 && | ||||||
|                "Can't get a terrain texture on terrain outside the current cell"); |                "Can't get a terrain texture on terrain outside the current cell"); | ||||||
|  | @ -219,7 +226,7 @@ namespace MWRender | ||||||
|         { |         { | ||||||
|             for ( int x = fromX - 1; x < fromX + size + 1; x++ ) |             for ( int x = fromX - 1; x < fromX + size + 1; x++ ) | ||||||
|             { |             { | ||||||
|                 ltexIndexes.insert(getLtexIndexAt(store, x, y)); |                 ltexIndexes.insert(getLtexIndexAt(cellX, cellY, x, y)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -244,7 +251,7 @@ namespace MWRender | ||||||
|             { |             { | ||||||
|                 //NB: All vtex ids are +1 compared to the ltex ids
 |                 //NB: All vtex ids are +1 compared to the ltex ids
 | ||||||
| 
 | 
 | ||||||
|                 assert( (int)store->landTextures->ltex.size() >= (int)ltexIndex - 1 && |                 assert( (int)mEnvironment.mWorld->getStore().landTexts.getSize() >= (int)ltexIndex - 1 && | ||||||
|                        "LAND.VTEX must be within the bounds of the LTEX array"); |                        "LAND.VTEX must be within the bounds of the LTEX array"); | ||||||
|                  |                  | ||||||
|                 std::string texture; |                 std::string texture; | ||||||
|  | @ -254,7 +261,7 @@ namespace MWRender | ||||||
|                 } |                 } | ||||||
|                 else |                 else | ||||||
|                 { |                 { | ||||||
|                     texture = store->landTextures->ltex[ltexIndex-1].texture; |                     texture = mEnvironment.mWorld->getStore().landTexts.search(ltexIndex-1)->texture; | ||||||
|                     //TODO this is needed due to MWs messed up texture handling
 |                     //TODO this is needed due to MWs messed up texture handling
 | ||||||
|                     texture = texture.substr(0, texture.rfind(".")) + ".dds"; |                     texture = texture.substr(0, texture.rfind(".")) + ".dds"; | ||||||
|                 } |                 } | ||||||
|  | @ -280,11 +287,10 @@ namespace MWRender | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|     void TerrainManager::initTerrainBlendMaps(Terrain* terrain, |     void TerrainManager::initTerrainBlendMaps(Terrain* terrain, | ||||||
|                                               MWWorld::Ptr::CellStore* store, |                                               int cellX, int cellY, | ||||||
|                                               int fromX, int fromY, int size, |                                               int fromX, int fromY, int size, | ||||||
|                                               const std::map<uint16_t, int>& indexes) |                                               const std::map<uint16_t, int>& indexes) | ||||||
|     { |     { | ||||||
|         assert(store != NULL && "store must be a valid pointer"); |  | ||||||
|         assert(terrain != NULL && "Must have valid terrain"); |         assert(terrain != NULL && "Must have valid terrain"); | ||||||
|         assert(fromX >= 0 && fromY >= 0 && |         assert(fromX >= 0 && fromY >= 0 && | ||||||
|                "Can't get a terrain texture on terrain outside the current cell"); |                "Can't get a terrain texture on terrain outside the current cell"); | ||||||
|  | @ -313,7 +319,7 @@ namespace MWRender | ||||||
|         { |         { | ||||||
|             for ( int texX = fromX - 1; texX < fromX + size + 1; texX++ ) |             for ( int texX = fromX - 1; texX < fromX + size + 1; texX++ ) | ||||||
|             { |             { | ||||||
|                 const uint16_t ltexIndex = getLtexIndexAt(store, texX, texY); |                 const uint16_t ltexIndex = getLtexIndexAt(cellX, cellY, texX, texY); | ||||||
| 
 | 
 | ||||||
|                 //check if it is the base texture (which isn't in the map) and
 |                 //check if it is the base texture (which isn't in the map) and
 | ||||||
|                 //if it is don't bother altering the blend map for it
 |                 //if it is don't bother altering the blend map for it
 | ||||||
|  | @ -332,8 +338,10 @@ namespace MWRender | ||||||
|                 float* const pBlend = terrain->getLayerBlendMap(layerIndex) |                 float* const pBlend = terrain->getLayerBlendMap(layerIndex) | ||||||
|                                              ->getBlendPointer(); |                                              ->getBlendPointer(); | ||||||
| 
 | 
 | ||||||
|                 for ( int y = -1; y < splatSize + 1; y++ ){ |                 for ( int y = -1; y < splatSize + 1; y++ ) | ||||||
|                     for ( int x = -1; x < splatSize + 1; x++ ){ |                 { | ||||||
|  |                     for ( int x = -1; x < splatSize + 1; x++ ) | ||||||
|  |                     { | ||||||
| 
 | 
 | ||||||
|                         //Note: Y is reversed
 |                         //Note: Y is reversed
 | ||||||
|                         const int splatY = blendMapSize - 1 - relY * splatSize - y; |                         const int splatY = blendMapSize - 1 - relY * splatSize - y; | ||||||
|  | @ -373,7 +381,7 @@ namespace MWRender | ||||||
| 
 | 
 | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|     int TerrainManager::getLtexIndexAt(MWWorld::Ptr::CellStore* store, |     int TerrainManager::getLtexIndexAt(int cellX, int cellY, | ||||||
|                                        int x, int y) |                                        int x, int y) | ||||||
|     { |     { | ||||||
|         //check texture index falls within the 9 cell bounds
 |         //check texture index falls within the 9 cell bounds
 | ||||||
|  | @ -386,12 +394,6 @@ namespace MWRender | ||||||
|                y < 2*ESM::Land::LAND_TEXTURE_SIZE && |                y < 2*ESM::Land::LAND_TEXTURE_SIZE && | ||||||
|                "Trying to get land textures that are out of bounds"); |                "Trying to get land textures that are out of bounds"); | ||||||
| 
 | 
 | ||||||
|         assert(store != NULL && "Store pointer must be valid"); |  | ||||||
| 
 |  | ||||||
|         //default center cell is indexed at (1,1)
 |  | ||||||
|         int cellX = 1; |  | ||||||
|         int cellY = 1; |  | ||||||
| 
 |  | ||||||
|         if ( x < 0 ) |         if ( x < 0 ) | ||||||
|         { |         { | ||||||
|             cellX--; |             cellX--; | ||||||
|  | @ -414,22 +416,32 @@ namespace MWRender | ||||||
|             y -= ESM::Land::LAND_TEXTURE_SIZE; |             y -= ESM::Land::LAND_TEXTURE_SIZE; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return store->land[cellX][cellY] | 
 | ||||||
|                     ->landData |         ESM::Land* land = mEnvironment.mWorld->getStore().lands.search(cellX, cellY); | ||||||
|                     ->textures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; |         if ( land != NULL ) | ||||||
|  |         { | ||||||
|  |             land->loadData(); | ||||||
|  |             return land->landData | ||||||
|  |                        ->textures[y * ESM::Land::LAND_TEXTURE_SIZE + x]; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     //----------------------------------------------------------------------------------------------
 |     //----------------------------------------------------------------------------------------------
 | ||||||
| 
 | 
 | ||||||
|     TexturePtr TerrainManager::getVertexColours(MWWorld::Ptr::CellStore* store, |     TexturePtr TerrainManager::getVertexColours(ESM::Land* land, | ||||||
|                                                     int fromX, int fromY, int size) |                                                 int cellX, int cellY, | ||||||
|  |                                                 int fromX, int fromY, int size) | ||||||
|     { |     { | ||||||
|         TextureManager* const texMgr = TextureManager::getSingletonPtr(); |         TextureManager* const texMgr = TextureManager::getSingletonPtr(); | ||||||
| 
 | 
 | ||||||
|         const std::string colourTextureName = "VtexColours_" + |         const std::string colourTextureName = "VtexColours_" + | ||||||
|                                               boost::lexical_cast<std::string>(store->cell->getGridX()) + |                                               boost::lexical_cast<std::string>(cellX) + | ||||||
|                                               "_" + |                                               "_" + | ||||||
|                                               boost::lexical_cast<std::string>(store->cell->getGridY()) + |                                               boost::lexical_cast<std::string>(cellY) + | ||||||
|                                               "_" + |                                               "_" + | ||||||
|                                               boost::lexical_cast<std::string>(fromX) + |                                               boost::lexical_cast<std::string>(fromX) + | ||||||
|                                               "_" + |                                               "_" + | ||||||
|  | @ -451,26 +463,42 @@ namespace MWRender | ||||||
|         const PixelBox& pixelBox = pixelBuffer->getCurrentLock(); |         const PixelBox& pixelBox = pixelBuffer->getCurrentLock(); | ||||||
|           |           | ||||||
|         uint8* pDest = static_cast<uint8*>(pixelBox.data); |         uint8* pDest = static_cast<uint8*>(pixelBox.data); | ||||||
|           | 
 | ||||||
|         const char* const colours = store->land[1][1]->landData->colours; |         if ( land != NULL ) | ||||||
|         for ( int y = 0; y < size; y++ ) |  | ||||||
|         { |         { | ||||||
|             for ( int x = 0; x < size; x++ ) |             const char* const colours = land->landData->colours; | ||||||
|  |             for ( int y = 0; y < size; y++ ) | ||||||
|             { |             { | ||||||
|                 const size_t colourOffset = (y+fromY)*3*65 + (x+fromX)*3; |                 for ( int x = 0; x < size; x++ ) | ||||||
|  |                 { | ||||||
|  |                     const size_t colourOffset = (y+fromY)*3*65 + (x+fromX)*3; | ||||||
| 
 | 
 | ||||||
|                 assert( colourOffset >= 0 && colourOffset < 65*65*3 && |                     assert( colourOffset < 65*65*3 && | ||||||
|                         "Colour offset is out of the expected bounds of record" ); |                             "Colour offset is out of the expected bounds of record" ); | ||||||
| 
 | 
 | ||||||
|                 const unsigned char r = colours[colourOffset + 0]; |                     const unsigned char r = colours[colourOffset + 0]; | ||||||
|                 const unsigned char g = colours[colourOffset + 1]; |                     const unsigned char g = colours[colourOffset + 1]; | ||||||
|                 const unsigned char b = colours[colourOffset + 2]; |                     const unsigned char b = colours[colourOffset + 2]; | ||||||
| 
 | 
 | ||||||
|                 //as is the case elsewhere we need to flip the y
 |                     //as is the case elsewhere we need to flip the y
 | ||||||
|                 const size_t imageOffset = (size - 1 - y)*size*4 + x*4; |                     const size_t imageOffset = (size - 1 - y)*size*4 + x*4; | ||||||
|                 pDest[imageOffset + 0] = b; |                     pDest[imageOffset + 0] = b; | ||||||
|                 pDest[imageOffset + 1] = g; |                     pDest[imageOffset + 1] = g; | ||||||
|                 pDest[imageOffset + 2] = r; |                     pDest[imageOffset + 2] = r; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             for ( int y = 0; y < size; y++ ) | ||||||
|  |             { | ||||||
|  |                 for ( int x = 0; x < size; x++ ) | ||||||
|  |                 { | ||||||
|  |                     for ( int k = 0; k < 3; k++ ) | ||||||
|  |                     { | ||||||
|  |                         *pDest++ = 0; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|           |           | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
| #define _GAME_RENDER_TERRAIN_H | #define _GAME_RENDER_TERRAIN_H | ||||||
| 
 | 
 | ||||||
| #include <OgreTerrain.h> | #include <OgreTerrain.h> | ||||||
|  | #include <OgreTerrainGroup.h> | ||||||
| #include "terrainmaterial.hpp" | #include "terrainmaterial.hpp" | ||||||
| 
 | 
 | ||||||
| #include "../mwworld/ptr.hpp" | #include "../mwworld/ptr.hpp" | ||||||
|  | @ -23,7 +24,7 @@ namespace MWRender{ | ||||||
|      */ |      */ | ||||||
|     class TerrainManager{ |     class TerrainManager{ | ||||||
|     public: |     public: | ||||||
|         TerrainManager(Ogre::SceneManager*); |         TerrainManager(Ogre::SceneManager* mgr, const MWWorld::Environment& env); | ||||||
|         virtual ~TerrainManager(); |         virtual ~TerrainManager(); | ||||||
| 
 | 
 | ||||||
|         void setDiffuse(const Ogre::ColourValue& diffuse); |         void setDiffuse(const Ogre::ColourValue& diffuse); | ||||||
|  | @ -32,8 +33,10 @@ namespace MWRender{ | ||||||
|         void cellAdded(MWWorld::Ptr::CellStore* store); |         void cellAdded(MWWorld::Ptr::CellStore* store); | ||||||
|         void cellRemoved(MWWorld::Ptr::CellStore* store); |         void cellRemoved(MWWorld::Ptr::CellStore* store); | ||||||
|     private: |     private: | ||||||
|         Ogre::TerrainGlobalOptions* mTerrainGlobals; |         Ogre::TerrainGlobalOptions mTerrainGlobals; | ||||||
|         Ogre::TerrainGroup* mTerrainGroup; |         Ogre::TerrainGroup mTerrainGroup; | ||||||
|  | 
 | ||||||
|  |         const MWWorld::Environment& mEnvironment; | ||||||
| 
 | 
 | ||||||
|         Ogre::TerrainMaterialGeneratorB::SM2Profile* mActiveProfile; |         Ogre::TerrainMaterialGeneratorB::SM2Profile* mActiveProfile; | ||||||
| 
 | 
 | ||||||
|  | @ -53,7 +56,8 @@ namespace MWRender{ | ||||||
|          * layer |          * layer | ||||||
|          * |          * | ||||||
|          * @param terrainData the terrain data to setup the textures for |          * @param terrainData the terrain data to setup the textures for | ||||||
|          * @param store the cell store for the given terrain cell |          * @param cellX the coord of the cell | ||||||
|  |          * @param cellY the coord of the cell | ||||||
|          * @param fromX the ltex index in the current cell to start making the texture from |          * @param fromX the ltex index in the current cell to start making the texture from | ||||||
|          * @param fromY the ltex index in the current cell to start making the texture from |          * @param fromY the ltex index in the current cell to start making the texture from | ||||||
|          * @param size the size (number of splats) to get |          * @param size the size (number of splats) to get | ||||||
|  | @ -61,7 +65,7 @@ namespace MWRender{ | ||||||
|          *          can be used by initTerrainBlendMaps |          *          can be used by initTerrainBlendMaps | ||||||
|          */ |          */ | ||||||
|         void initTerrainTextures(Ogre::Terrain::ImportData* terrainData, |         void initTerrainTextures(Ogre::Terrain::ImportData* terrainData, | ||||||
|                                  MWWorld::Ptr::CellStore* store, |                                  int cellX, int cellY, | ||||||
|                                  int fromX, int fromY, int size, |                                  int fromX, int fromY, int size, | ||||||
|                                  std::map<uint16_t, int>& indexes); |                                  std::map<uint16_t, int>& indexes); | ||||||
| 
 | 
 | ||||||
|  | @ -69,14 +73,15 @@ namespace MWRender{ | ||||||
|          * Creates the blend (splatting maps) for the given terrain from the ltex data. |          * Creates the blend (splatting maps) for the given terrain from the ltex data. | ||||||
|          * |          * | ||||||
|          * @param terrain the terrain object for the current cell |          * @param terrain the terrain object for the current cell | ||||||
|          * @param store the cell store for the given terrain cell |          * @param cellX the coord of the cell | ||||||
|  |          * @param cellY the coord of the cell | ||||||
|          * @param fromX the ltex index in the current cell to start making the texture from |          * @param fromX the ltex index in the current cell to start making the texture from | ||||||
|          * @param fromY the ltex index in the current cell to start making the texture from |          * @param fromY the ltex index in the current cell to start making the texture from | ||||||
|          * @param size the size (number of splats) to get |          * @param size the size (number of splats) to get | ||||||
|          * @param indexes the mapping of ltex to blend map produced by initTerrainTextures |          * @param indexes the mapping of ltex to blend map produced by initTerrainTextures | ||||||
|          */ |          */ | ||||||
|         void initTerrainBlendMaps(Ogre::Terrain* terrain, |         void initTerrainBlendMaps(Ogre::Terrain* terrain, | ||||||
|                                   MWWorld::Ptr::CellStore* store, |                                   int cellX, int cellY, | ||||||
|                                   int fromX, int fromY, int size, |                                   int fromX, int fromY, int size, | ||||||
|                                   const std::map<uint16_t, int>& indexes); |                                   const std::map<uint16_t, int>& indexes); | ||||||
| 
 | 
 | ||||||
|  | @ -85,22 +90,25 @@ namespace MWRender{ | ||||||
|          * starts at (0,0). This supports getting values from the surrounding |          * starts at (0,0). This supports getting values from the surrounding | ||||||
|          * cells so negative x, y is acceptable |          * cells so negative x, y is acceptable | ||||||
|          * |          * | ||||||
|          * @param store the cell store for the current cell |          * @param cellX the coord of the cell | ||||||
|  |          * @param cellY the coord of the cell | ||||||
|          * @param x, y the splat position of the ltex index to get relative to the |          * @param x, y the splat position of the ltex index to get relative to the | ||||||
|          *             first splat of the current cell |          *             first splat of the current cell | ||||||
|          */ |          */ | ||||||
|         int getLtexIndexAt(MWWorld::Ptr::CellStore* store, int x, int y); |         int getLtexIndexAt(int cellX, int cellY, int x, int y); | ||||||
| 
 | 
 | ||||||
|         /**
 |         /**
 | ||||||
|          * Due to the fact that Ogre terrain doesn't support vertex colours |          * Due to the fact that Ogre terrain doesn't support vertex colours | ||||||
|          * we have to generate them manually |          * we have to generate them manually | ||||||
|          * |          * | ||||||
|          * @param store the cell store for the given terrain cell |          * @param cellX the coord of the cell | ||||||
|  |          * @param cellY the coord of the cell | ||||||
|          * @param fromX the *vertex* index in the current cell to start making texture from |          * @param fromX the *vertex* index in the current cell to start making texture from | ||||||
|          * @param fromY the *vertex* index in the current cell to start making the texture from |          * @param fromY the *vertex* index in the current cell to start making the texture from | ||||||
|          * @param size the size (number of vertexes) to get |          * @param size the size (number of vertexes) to get | ||||||
|          */ |          */ | ||||||
|         Ogre::TexturePtr getVertexColours(MWWorld::Ptr::CellStore* store, |         Ogre::TexturePtr getVertexColours(ESM::Land* land, | ||||||
|  |                                           int cellX, int cellY, | ||||||
|                                           int fromX, int fromY, int size); |                                           int fromX, int fromY, int size); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ namespace ESM | ||||||
| { | { | ||||||
| void Land::load(ESMReader &esm) | void Land::load(ESMReader &esm) | ||||||
| { | { | ||||||
|  |     mEsm = &esm; | ||||||
|  | 
 | ||||||
|     // Get the grid location
 |     // Get the grid location
 | ||||||
|     esm.getSubNameIs("INTV"); |     esm.getSubNameIs("INTV"); | ||||||
|     esm.getSubHeaderIs(8); |     esm.getSubHeaderIs(8); | ||||||
|  | @ -51,7 +53,7 @@ void Land::load(ESMReader &esm) | ||||||
|     landData = NULL; |     landData = NULL; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Land::loadData(ESMReader &esm) | void Land::loadData() | ||||||
| { | { | ||||||
|     if (dataLoaded) |     if (dataLoaded) | ||||||
|     { |     { | ||||||
|  | @ -62,17 +64,17 @@ void Land::loadData(ESMReader &esm) | ||||||
| 
 | 
 | ||||||
|     if (hasData) |     if (hasData) | ||||||
|     { |     { | ||||||
|         esm.restoreContext(context); |         mEsm->restoreContext(context); | ||||||
| 
 | 
 | ||||||
|         //esm.getHNExact(landData->normals, sizeof(VNML), "VNML");
 |         //esm.getHNExact(landData->normals, sizeof(VNML), "VNML");
 | ||||||
|         if (esm.isNextSub("VNML")) |         if (mEsm->isNextSub("VNML")) | ||||||
|         { |         { | ||||||
|             esm.skipHSubSize(12675); |             mEsm->skipHSubSize(12675); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         VHGT rawHeights; |         VHGT rawHeights; | ||||||
| 
 | 
 | ||||||
|         esm.getHNExact(&rawHeights, sizeof(VHGT), "VHGT"); |         mEsm->getHNExact(&rawHeights, sizeof(VHGT), "VHGT"); | ||||||
|         int currentHeightOffset = rawHeights.heightOffset; |         int currentHeightOffset = rawHeights.heightOffset; | ||||||
|         for (int y = 0; y < LAND_SIZE; y++) |         for (int y = 0; y < LAND_SIZE; y++) | ||||||
|         { |         { | ||||||
|  | @ -87,20 +89,20 @@ void Land::loadData(ESMReader &esm) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (esm.isNextSub("WNAM")) |         if (mEsm->isNextSub("WNAM")) | ||||||
|         { |         { | ||||||
|             esm.skipHSubSize(81); |             mEsm->skipHSubSize(81); | ||||||
|         } |         } | ||||||
|         if (esm.isNextSub("VCLR")) |         if (mEsm->isNextSub("VCLR")) | ||||||
|         { |         { | ||||||
|             landData->usingColours = true; |             landData->usingColours = true; | ||||||
|             esm.getHExact(&landData->colours, 3*LAND_NUM_VERTS); |             mEsm->getHExact(&landData->colours, 3*LAND_NUM_VERTS); | ||||||
|         }else{ |         }else{ | ||||||
|             landData->usingColours = false; |             landData->usingColours = false; | ||||||
|         } |         } | ||||||
|         //TODO fix magic numbers
 |         //TODO fix magic numbers
 | ||||||
|         uint16_t vtex[512]; |         uint16_t vtex[512]; | ||||||
|         esm.getHNExact(&vtex, 512, "VTEX"); |         mEsm->getHNExact(&vtex, 512, "VTEX"); | ||||||
| 
 | 
 | ||||||
|         int readPos = 0; //bit ugly, but it works
 |         int readPos = 0; //bit ugly, but it works
 | ||||||
|         for ( int y1 = 0; y1 < 4; y1++ ) |         for ( int y1 = 0; y1 < 4; y1++ ) | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ struct Land | ||||||
| 
 | 
 | ||||||
|     // File context. This allows the ESM reader to be 'reset' to this
 |     // File context. This allows the ESM reader to be 'reset' to this
 | ||||||
|     // location later when we are ready to load the full data set.
 |     // location later when we are ready to load the full data set.
 | ||||||
|  |     ESMReader* mEsm; | ||||||
|     ESM_Context context; |     ESM_Context context; | ||||||
| 
 | 
 | ||||||
|     bool hasData; |     bool hasData; | ||||||
|  | @ -70,7 +71,7 @@ struct Land | ||||||
|     /**
 |     /**
 | ||||||
|      * Actually loads data |      * Actually loads data | ||||||
|      */ |      */ | ||||||
|     void loadData(ESMReader &esm); |     void loadData(); | ||||||
| 
 | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Frees memory allocated for land data |      * Frees memory allocated for land data | ||||||
|  |  | ||||||
|  | @ -124,9 +124,6 @@ namespace ESMS | ||||||
|     CellRefList<Static, D>            statics; |     CellRefList<Static, D>            statics; | ||||||
|     CellRefList<Weapon, D>            weapons; |     CellRefList<Weapon, D>            weapons; | ||||||
| 
 | 
 | ||||||
|     const Land* land[3][3]; |  | ||||||
|     const LTexList* landTextures; |  | ||||||
| 
 |  | ||||||
|     void load (const ESMStore &store, ESMReader &esm) |     void load (const ESMStore &store, ESMReader &esm) | ||||||
|     { |     { | ||||||
|         if (mState!=State_Loaded) |         if (mState!=State_Loaded) | ||||||
|  | @ -138,21 +135,6 @@ namespace ESMS | ||||||
| 
 | 
 | ||||||
|             loadRefs (store, esm); |             loadRefs (store, esm); | ||||||
| 
 | 
 | ||||||
|             if ( ! (cell->data.flags & ESM::Cell::Interior) ) |  | ||||||
|             { |  | ||||||
|                 for ( size_t x = 0; x < 3; x++ ) |  | ||||||
|                 { |  | ||||||
|                     for ( size_t y = 0; y < 3; y++ ) |  | ||||||
|                     { |  | ||||||
|                         land[x][y] = loadTerrain(cell->data.gridX + x - 1, |  | ||||||
|                                                  cell->data.gridY + y - 1, |  | ||||||
|                                                  store, |  | ||||||
|                                                  esm); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 landTextures = &store.landTexts; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             mState = State_Loaded; |             mState = State_Loaded; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -198,24 +180,6 @@ namespace ESMS | ||||||
| 
 | 
 | ||||||
|   private: |   private: | ||||||
| 
 | 
 | ||||||
|     Land* loadTerrain(int X, int Y, const ESMStore &store, ESMReader &esm) |  | ||||||
|     { |  | ||||||
|         // load terrain
 |  | ||||||
|         Land *land = store.lands.search(X, Y); |  | ||||||
|         if (land != NULL) |  | ||||||
|         { |  | ||||||
|             land->loadData(esm); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return land; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     void unloadTerrain(int X, int Y, const ESMStore &store) { |  | ||||||
|         assert (false && |  | ||||||
|                 "This function is not implemented due to the fact that we now store overlapping land blocks so" && |  | ||||||
|                 "we cannot be sure that the land segment is not being used by another CellStore"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     template<class Functor, class List> |     template<class Functor, class List> | ||||||
|     bool forEachImp (Functor& functor, List& list) |     bool forEachImp (Functor& functor, List& list) | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -201,15 +201,21 @@ namespace ESMS | ||||||
| 
 | 
 | ||||||
|     // TODO: For multiple ESM/ESP files we need one list per file.
 |     // TODO: For multiple ESM/ESP files we need one list per file.
 | ||||||
|     std::vector<LandTexture> ltex; |     std::vector<LandTexture> ltex; | ||||||
|     int count; |  | ||||||
| 
 | 
 | ||||||
|     LTexList() : count(0) |     LTexList() | ||||||
|     { |     { | ||||||
|       // More than enough to hold Morrowind.esm.
 |       // More than enough to hold Morrowind.esm.
 | ||||||
|       ltex.reserve(128); |       ltex.reserve(128); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     int getSize() { return count; } |     const LandTexture* search(size_t index) const | ||||||
|  |     { | ||||||
|  |         assert(index < ltex.size()); | ||||||
|  |         return <ex.at(index); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     int getSize() { return ltex.size(); } | ||||||
|  |     int getSize() const { return ltex.size(); } | ||||||
| 
 | 
 | ||||||
|     virtual void listIdentifier (std::vector<std::string>& identifier) const {} |     virtual void listIdentifier (std::vector<std::string>& identifier) const {} | ||||||
| 
 | 
 | ||||||
|  | @ -233,12 +239,18 @@ namespace ESMS | ||||||
|    */ |    */ | ||||||
|   struct LandList : RecList |   struct LandList : RecList | ||||||
|   { |   { | ||||||
|     virtual ~LandList() {} |     virtual ~LandList() | ||||||
|  |     { | ||||||
|  |       for ( LandMap::iterator itr = lands.begin(); itr != lands.end(); ++itr ) | ||||||
|  |       { | ||||||
|  |           delete itr->second; | ||||||
|  |       } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // Map containing all landscapes
 |     // Map containing all landscapes
 | ||||||
|     typedef std::map<int, Land*> LandsCol; |     typedef std::pair<int, int> LandCoord; | ||||||
|     typedef std::map<int, LandsCol> Lands; |     typedef std::map<LandCoord, Land*> LandMap; | ||||||
|     Lands lands; |     LandMap lands; | ||||||
| 
 | 
 | ||||||
|     int count; |     int count; | ||||||
|     LandList() : count(0) {} |     LandList() : count(0) {} | ||||||
|  | @ -249,15 +261,13 @@ namespace ESMS | ||||||
|     // Find land for the given coordinates. Return null if no data.
 |     // Find land for the given coordinates. Return null if no data.
 | ||||||
|     Land *search(int x, int y) const |     Land *search(int x, int y) const | ||||||
|     { |     { | ||||||
|       Lands::const_iterator it = lands.find(x); |       LandMap::const_iterator itr = lands.find(std::make_pair<int, int>(x, y)); | ||||||
|       if(it==lands.end()) |       if ( itr == lands.end() ) | ||||||
|  |       { | ||||||
|         return NULL; |         return NULL; | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       LandsCol::const_iterator it2 = it->second.find(y); |       return itr->second; | ||||||
|       if(it2 == it->second.end()) |  | ||||||
|         return NULL; |  | ||||||
| 
 |  | ||||||
|       return it2->second; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     void load(ESMReader &esm, const std::string &id) |     void load(ESMReader &esm, const std::string &id) | ||||||
|  | @ -266,11 +276,11 @@ namespace ESMS | ||||||
| 
 | 
 | ||||||
|       // Create the structure and load it. This actually skips the
 |       // Create the structure and load it. This actually skips the
 | ||||||
|       // landscape data and remembers the file position for later.
 |       // landscape data and remembers the file position for later.
 | ||||||
|       Land *land = new Land; |       Land *land = new Land(); | ||||||
|       land->load(esm); |       land->load(esm); | ||||||
| 
 | 
 | ||||||
|       // Store the structure
 |       // Store the structure
 | ||||||
|       lands[land->X][land->Y] = land; |       lands[std::make_pair<int, int>(land->X, land->Y)] = land; | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue