#include #include #include #include #include #include #include #include #include #include #include #include #include "GameData.h" #include "Components.h" #include "CharacterModule.h" #include "SunModule.h" #include "PhysicsModule.h" #include "StaticGeometryModule.h" #include "TerrainModule.h" #define TERRAIN_SIZE 65 #define TERRAIN_WORLD_SIZE 500.0f #define ENDLESS_TERRAIN_FILE_PREFIX Ogre::String("EndlessWorldTerrain") #define ENDLESS_TERRAIN_FILE_SUFFIX Ogre::String("dat") #define ENDLESS_PAGING // max range for a int16 #define ENDLESS_PAGE_MIN_X (-0x7FFF) #define ENDLESS_PAGE_MIN_Y (-0x7FFF) #define ENDLESS_PAGE_MAX_X 0x7FFF #define ENDLESS_PAGE_MAX_Y 0x7FFF namespace ECS { class DummyPageProvider; /* Components */ struct TerrainPrivate { DummyPageProvider *mDummyPageProvider; Ogre::Timer mSunUpdate; }; #define BRUSH_SIZE 64 struct HeightData { Ogre::Image img; Ogre::Image img_brushes; Ogre::Image img_noise; static HeightData *singleton; HeightData() { img.load( "world_map.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); img_brushes.load( "brushes.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); img_noise.load( "terrain.png", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); } static HeightData *get_singleton() { if (!singleton) singleton = new HeightData(); return singleton; } float get_brush_height(int id, int x, int y) { int m = 0; switch (id) { case 0: m = 0; break; case 1: m = BRUSH_SIZE; break; default: OgreAssert(false, "bad brush id"); break; } return img_brushes.getColourAt(x, y + m, 0).r; } float get_world_x(int x) { return (float)(x - (int)img.getWidth() / 2) * BRUSH_SIZE; } float get_world_y(int y) { return (float)(y - (int)img.getHeight() / 2) * BRUSH_SIZE; } int get_img_x(float world_x) { float world_img_x = world_x + (float)img.getWidth() * (float)BRUSH_SIZE / 2.0f; int ret = (world_img_x + BRUSH_SIZE - 1) / BRUSH_SIZE; // ret = Ogre::Math::Clamp(ret, 0, (int)img.getWidth() - 1); return ret; } int get_img_y(float world_z) { float world_img_y = world_z + (float)img.getHeight() * (float)BRUSH_SIZE / 2.0f; int ret = (world_img_y + BRUSH_SIZE - 1) / BRUSH_SIZE; // ret = Ogre::Math::Clamp(ret, 0, (int)img.getHeight() - 1); return ret; } void update_heightmap(const Ogre::Image &heightmap) { img = heightmap; } void save_heightmap() { Ogre::String group = Ogre::ResourceGroupManager::getSingleton() .findGroupContainingResource("world_map.png"); Ogre::FileInfoListPtr fileInfoList( Ogre::ResourceGroupManager::getSingleton() .findResourceFileInfo(group, "world_map.png")); OgreAssert(fileInfoList->size() == 1, "worpd_map.png should be there and only once"); Ogre::String path = fileInfoList->at(0).archive->getName() + "/" + "world_map.png"; Ogre::FileSystemLayer::removeFile(path); img.save(path); } float get_base_height(long world_x, long world_y) { float height = 0.0f; int world_img_x = world_x + (int)img.getWidth() * BRUSH_SIZE / 2; int world_img_y = world_y + (int)img.getHeight() * BRUSH_SIZE / 2; Ogre::ColourValue color, colorb1, colorb2; // float d; int div = 1; int map_img_x = world_img_x / (BRUSH_SIZE) / div; int map_img_y = world_img_y / (BRUSH_SIZE) / div; int brush_img_x = (world_img_x / div) % BRUSH_SIZE; int brush_img_y = (world_img_y / div) % BRUSH_SIZE; if (world_img_x < 0 || world_img_x >= img.getWidth() * BRUSH_SIZE || world_img_y < 0 || world_img_y >= img.getWidth() * BRUSH_SIZE) { height = 0.0f; goto out; } color = img.getColourAt(map_img_x, map_img_y, 0); colorb1 = img_brushes.getColourAt(brush_img_x, brush_img_y + BRUSH_SIZE, 0); colorb2 = img_brushes.getColourAt(brush_img_x, brush_img_y, 0); // d = Ogre::Math::saturate(color.r - 0.05f); height = color.r; out: return height; } float get_noise_height(long world_x, long world_y) { int h; Ogre::Vector2 noisePoint; struct noise_types { Ogre::Vector2 noiseOfft; float noiseMul; float noiseBias; float noiseAmp; }; static struct noise_types noise_pass[] = { { { -100.0f, 70.0f }, 10.2f, -0.55f, 5.0f }, { { -130.0f, 55.0f }, 5.35f, -0.55f, 1.0f } }; static float noise_values[] = { 0.0f, 0.0f }; for (h = 0; h < (int)sizeof(noise_values) / (int)sizeof(noise_values[0]); h++) { noisePoint = (Ogre::Vector2(world_x, world_y) + noise_pass[h].noiseOfft) * noise_pass[h].noiseMul; int noise_x = (int)(noisePoint.x + img_noise.getWidth() / 2) % img_noise.getWidth(); int noise_y = (int)(noisePoint.y + img_noise.getHeight() / 2) % img_noise.getHeight(); Ogre::ColourValue noise_color = img_noise.getColourAt(noise_x, noise_y, 0); noise_values[h] = (noise_color.r + noise_pass[h].noiseBias) * noise_pass[h].noiseAmp; } return noise_values[0] + noise_values[1]; } float get_height(Ogre::TerrainGroup *terrainGroup, long world_x, long world_y) { long grid_center_x = img.getWidth() * BRUSH_SIZE / 2; long grid_center_y = img.getHeight() * BRUSH_SIZE / 2; long world_grid_x = world_x + grid_center_x; long world_grid_y = world_y + grid_center_y; float amplitude = 150.0f; if (world_grid_x < 0 || world_grid_y < 0) { #if 0 std::cout << "world: " << world_x << " " << world_y << " "; std::cout << "grid: " << world_grid_x << " "; std::cout << world_grid_y << std::endl; #endif return -amplitude; } OgreAssert(world_grid_x >= 0, "bad world x"); OgreAssert(world_grid_y >= 0, "bad world y"); long cell = 8; long b1 = (world_grid_x / cell) * cell + cell / 2; long b2 = (world_grid_y / cell) * cell + cell / 2; long o1 = Ogre::Math::Abs(world_grid_x - b1); long o2 = Ogre::Math::Abs(world_grid_y - b2); // std::cout << "h: " << o1 << " " << o2 << std::endl; // std::cout << "h: " << (o1 * o1 + o2 * o2) << std::endl; float hbase = 1.0 / (1.0f + 0.02f * (o1 * o1 + o2 * o2)); float h1 = hbase * 0.5f + 0.5f; float h2 = (1.0f - hbase) * 0.5f; float mheight = (Ogre::Math::lerp(h2, h1, get_base_height(world_x, world_y)) - 0.51f) * amplitude; float height = mheight; if (mheight > 0.5f) height += 2.0f + get_noise_height(world_x, world_y); else if (mheight < -0.5f) height -= 2.0f + get_noise_height(world_x, world_y); // OgreAssert(false, "height"); return height; } }; HeightData *HeightData::singleton = nullptr; class FlatTerrainDefiner : public Ogre::TerrainPagedWorldSection::TerrainDefiner, public Ogre::FrameListener { Ogre::SceneManager *mScnMgr; // Ogre::Bullet::DynamicsWorld *mWorld; struct gen_collider { Ogre::TerrainGroup *group; long x; long y; }; std::deque collider_queue; std::deque colliderRemove_queue; public: FlatTerrainDefiner(Ogre::SceneManager * scm /*, Ogre::Bullet::DynamicsWorld *world */) : Ogre::TerrainPagedWorldSection::TerrainDefiner() , Ogre::FrameListener() , mScnMgr(scm) // , mWorld(world) { Ogre::Root::getSingleton().addFrameListener(this); } private: std::mutex mtx; public: void createTerrainChunk(Ogre::TerrainGroup *terrainGroup, long x, long y) { Ogre::Terrain *terrain = terrainGroup->getTerrain(x, y); float minH = terrain->getMinHeight(); float maxH = terrain->getMaxHeight(); uint16_t terrainSize = terrainGroup->getTerrainSize(); OgreAssert(terrain && terrain->getHeightData() && terrain->isLoaded(), "invalid terrain supplied"); uint16_t size = terrain->getSize(); float *heightData = terrain->getHeightData(); Ogre::SceneNode *node = terrain->_getRootSceneNode(); float worldSize = terrain->getWorldSize(); float scaled = worldSize / (size - 1); Ogre::Vector3 bodyPosition = terrain->getPosition(); // bodyPosition.y += (maxH + minH) / 2.0f; bodyPosition.x += worldSize / 2.0f; bodyPosition.z += worldSize / 2.0f; Ogre::Vector3 offset = node->_getDerivedPosition() - bodyPosition; flecs::entity e = PhysicsModule::createTerrainChunkBody( node, heightData, offset, Ogre::Vector3(scaled, 1, scaled), size); node->getUserObjectBindings().setUserAny("_collider", e); } void define(Ogre::TerrainGroup *terrainGroup, long x, long y) override { std::lock_guard guard(mtx); uint16_t terrainSize = terrainGroup->getTerrainSize(); float *heightMap = OGRE_ALLOC_T(float, terrainSize *terrainSize, MEMCATEGORY_GEOMETRY); // float *heightMapCollider = OGRE_ALLOC_T( // float, terrainSize *terrainSize, MEMCATEGORY_GEOMETRY); // Ogre::Vector2 worldOrigin = // Ogre::Vector2(img.getWidth(), img.getHeight()) * 0.5f; float chunk = 128.0f; Ogre::Vector2 revisedValuePoint; Ogre::Vector3 worldPos; terrainGroup->convertTerrainSlotToWorldPosition(x, y, &worldPos); for (int i = 0; i < terrainSize; i++) for (int j = 0; j < terrainSize; j++) { long world_x = (long)(worldPos.x + j - (terrainSize - 1) / 2); long world_y = (long)(worldPos.z + i - (terrainSize - 1) / 2); float height = 0.0f; height += HeightData::get_singleton()->get_height( terrainGroup, world_x, world_y); // height = -2.0f; heightMap[i * terrainSize + j] = height; // heightMapCollider[(terrainSize - i - 1) * // terrainSize + // j] = height; } terrainGroup->defineTerrain(x, y, heightMap); Ogre::LogManager::getSingleton().logError( "defined terrain at " + Ogre::StringConverter::toString(x) + " " + Ogre::StringConverter::toString(y)); // collider_queue.push_back( // { terrainGroup, x, y, heightMapCollider }); delete[] heightMap; collider_queue.push_back({ terrainGroup, x, y }); } bool frameStarted(const Ogre::FrameEvent &evt) override { (void)evt; update(); return true; } void update() { std::lock_guard guard(mtx); static bool created = false; while (!collider_queue.empty()) { Ogre::TerrainGroup *group = collider_queue.front().group; if (group->isDerivedDataUpdateInProgress()) break; long x = collider_queue.front().x; long y = collider_queue.front().y; // std::cout << x << " " << y << " " // << collider_queue.size() << std::endl; Ogre::Terrain *terrain = group->getTerrain(x, y); Ogre::Vector3 worldPos; group->convertTerrainSlotToWorldPosition(x, y, &worldPos); #if 0 std::cout << "terrain: " << terrain; if (terrain) std::cout << terrain->getHeightData() << " " << terrain->isLoaded() << " " << terrain->isDerivedDataUpdateInProgress() << std::endl; #endif if (terrain && terrain->getHeightData() && terrain->isLoaded() && !terrain->isDerivedDataUpdateInProgress()) { Ogre::LogManager::getSingleton().logError( "can create collider for " + Ogre::StringConverter::toString(x) + " " + Ogre::StringConverter::toString(y)); // float minH = terrain->getMinHeight(); // float maxH = terrain->getMaxHeight(); int size = terrain->getSize(); float worldSize = terrain->getWorldSize(); { createTerrainChunk(group, x, y); } #if 0 if (true) { btRigidBody *body = mWorld->addTerrainRigidBody( group, x, y, 2, 0x7ffffffd & (~16)); OgreAssert( body, "Could not create RigidBody"); Ogre::LogManager::getSingleton().logError( "created rigid body " + Ogre::StringConverter::toString( Ogre::Bullet::convert( body->getWorldTransform() .getOrigin()))); Ogre::LogManager::getSingleton().logError( "minHeight " + Ogre::StringConverter::toString( minH)); Ogre::LogManager::getSingleton().logError( "maxHeight " + Ogre::StringConverter::toString( maxH)); Ogre::LogManager::getSingleton().logError( "size " + Ogre::StringConverter::toString( size)); Ogre::LogManager::getSingleton().logError( "world size " + Ogre::StringConverter::toString( worldSize)); Ogre::LogManager::getSingleton().logError( "created collider for " + Ogre::StringConverter::toString( x) + " " + Ogre::StringConverter::toString( y)); created = true; } #endif // FIXME: create entities and components instead #if 0 Ogre::SceneNode *items = terrain->_getRootSceneNode() ->createChildSceneNode(); for (int i = 0; i < ECS::get() .altar_items.size(); i++) { const struct PlacementObjects::item &item = ECS::get() .altar_items[i]; Ogre::Entity *ent = group->getSceneManager() ->createEntity( item.entity); Ogre::SceneNode *what = items->createChildSceneNode(); what->attachObject(ent); what->setOrientation(item.rotation); what->setPosition(item.position); } #endif /* Spawn items */ StaticGeometryModule::addGeometryForSlot(x, y); collider_queue.pop_front(); } else { /* Terrain data not ready maybe move to next terrain */ gen_collider m = collider_queue.front(); collider_queue.pop_front(); collider_queue.push_back(m); break; // allow system to move on } } if (collider_queue.empty() && !ECS::get().mTerrainReady) { ECS::get_mut().mTerrainReady = true; ECS::modified(); } } }; class DummyPageProvider : public Ogre::PageProvider { public: DummyPageProvider(/* btDynamicsWorld *world */) : Ogre::PageProvider() { } bool prepareProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *section) { return true; } bool loadProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *section) { return true; } bool unloadProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *section) { long x, y; ECS::get().mTerrainGroup->unpackIndex(page->CHUNK_ID, &x, &y); StaticGeometryModule::removeGeometryForSlot(x, y); return true; } bool unprepareProceduralPage(Ogre::Page *page, Ogre::PagedWorldSection *section) { return true; } }; TerrainModule::TerrainModule(flecs::world &ecs) { struct CanSetPlayerPosition {}; ecs.module(); ecs.component().add(flecs::Singleton); ecs.component().add(flecs::Singleton); ecs.component().add(flecs::Singleton); ecs.component(); ecs.component().add(flecs::Singleton); ecs.import (); ecs.import (); ecs.import (); ecs.set({ nullptr, {} }); ecs.system("SetupUpdateTerrain") .kind(flecs::OnUpdate) .each([](const EngineData &eng, const Camera &camera, const Sun &sun, Terrain &terrain, TerrainPrivate &priv) { if (!terrain.mTerrainGroup && sun.mSun && eng.mScnMgr) { std::cout << "Terrain setup\n"; if (!priv.mDummyPageProvider) priv.mDummyPageProvider = new DummyPageProvider( /* eng.mWorld ->getBtWorld() */); terrain.mTerrainGlobals = OGRE_NEW Ogre::TerrainGlobalOptions(); OgreAssert(terrain.mTerrainGlobals, "Failed to allocate global options"); terrain.mTerrainGroup = OGRE_NEW Ogre::TerrainGroup( eng.mScnMgr, Ogre::Terrain::ALIGN_X_Z, TERRAIN_SIZE, TERRAIN_WORLD_SIZE); terrain.mTerrainGroup->setFilenameConvention( ENDLESS_TERRAIN_FILE_PREFIX, ENDLESS_TERRAIN_FILE_SUFFIX); terrain.mTerrainGroup->setOrigin( terrain.mTerrainPos); // Configure global terrain.mTerrainGlobals->setMaxPixelError(1); // testing composite map // mTerrainGlobals->setCompositeMapDistance(30); terrain.mTerrainGlobals->setCompositeMapDistance( 300); //mTerrainGlobals->setUseRayBoxDistanceCalculation(true); terrain.mTerrainGlobals ->getDefaultMaterialGenerator() ->setLightmapEnabled(false); terrain.mTerrainGlobals->setCompositeMapAmbient( eng.mScnMgr->getAmbientLight()); terrain.mTerrainGlobals->setCompositeMapDiffuse( sun.mSun->getDiffuseColour()); terrain.mTerrainGlobals->setLightMapDirection( sun.mSun->getDerivedDirection()); // Configure default import settings for if we use imported image Ogre::Terrain::ImportData &defaultimp = terrain.mTerrainGroup ->getDefaultImportSettings(); defaultimp.terrainSize = TERRAIN_SIZE; defaultimp.worldSize = TERRAIN_WORLD_SIZE; defaultimp.inputScale = 1.0f; // defaultimp.minBatchSize = 33; defaultimp.minBatchSize = 5; defaultimp.maxBatchSize = 65; Ogre::Image combined; combined.loadTwoImagesAsRGBA( "Ground23_col.jpg", "Ground23_spec.png", "General"); Ogre::TextureManager::getSingleton().loadImage( "Ground23_diffspec", "General", combined); defaultimp.layerList.resize(1); defaultimp.layerList[0].worldSize = 60; defaultimp.layerList[0].textureNames.push_back( "Ground23_diffspec"); // Paging setup terrain.mPageManager = OGRE_NEW Ogre::PageManager(); // Since we're not loading any pages from .page files, we need a way just // to say we've loaded them without them actually being loaded terrain.mPageManager->setPageProvider( priv.mDummyPageProvider); terrain.mPageManager->addCamera(camera.mCamera); terrain.mPageManager->setDebugDisplayLevel(0); terrain.mTerrainPaging = OGRE_NEW Ogre::TerrainPaging( terrain.mPageManager); terrain.mPagedWorld = terrain.mPageManager->createWorld(); #if 0 terrain.mTerrainGroup->setAutoUpdateLod( Ogre::TerrainAutoUpdateLodFactory:: getAutoUpdateLod( Ogre::BY_DISTANCE)); #endif terrain.mTerrainPagedWorldSection = terrain.mTerrainPaging ->createWorldSection( terrain.mPagedWorld, terrain.mTerrainGroup, 300, 500, ENDLESS_PAGE_MIN_X, ENDLESS_PAGE_MIN_Y, ENDLESS_PAGE_MAX_X, ENDLESS_PAGE_MAX_Y); terrain.definer = OGRE_NEW FlatTerrainDefiner( eng.mScnMgr /*, eng.mWorld */); terrain.mTerrainPagedWorldSection->setDefiner( terrain.definer); terrain.mTerrainGroup->freeTemporaryResources(); std::cout << "Terrain setup done\n"; ECS::get().set({}); terrain.mTerrainGroup->loadAllTerrains(true); } if (sun.mSun && priv.mSunUpdate.getMilliseconds() > 1000) { terrain.mTerrainGlobals->setCompositeMapAmbient( eng.mScnMgr->getAmbientLight()); terrain.mTerrainGlobals->setCompositeMapDiffuse( sun.mSun->getDiffuseColour()); terrain.mTerrainGlobals->setLightMapDirection( sun.mSun->getDerivedDirection()); std::cout << "sun pitch: " << sun.mSunNode->getOrientation() .getPitch() << "\n"; priv.mSunUpdate.reset(); //terrain.mTerrainGroup->autoUpdateLodAll() } }); ecs.system("UpdateTerrainStatus") .kind(flecs::OnUpdate) .without() .each([](const ECS::Camera &cam, const Terrain &terrain) { std::cout << "mTerrainReady: " << terrain.mTerrainReady << "\n"; if (cam.mCameraNode && terrain.mTerrainReady) { long x, y; Ogre::Vector3 pos = cam.mCameraNode->getPosition(); terrain.mTerrainGroup ->convertWorldPositionToTerrainSlot( pos, &x, &y); if (terrain.mTerrainGroup->getTerrain(x, y) && terrain.mTerrainGroup->getTerrain(x, y) ->isLoaded()) ECS::get().add(); } }); ecs.system("UpdatePlacementObjects") .kind(flecs::OnUpdate) .each([](const Terrain &terrain, PlacementObjects &placement) { if (placement.altar_items.size() == 0) { struct PlacementObjects::item item; int i, j; int worldSize = terrain.mTerrainGroup ->getTerrainWorldSize(); uint16_t terrainSize = terrain.mTerrainGroup->getTerrainSize(); item.entity = "altar.glb"; item.rotation = Ogre::Quaternion(0, 0, 0, 1); item.position = Ogre::Vector3(0, 0, 0); float height = HeightData::get_singleton()->get_height( terrain.mTerrainGroup, 0, 0); item.position.y = height; placement.altar_items.push_back(item); for (i = -64000; i < 64000; i += 2000) for (j = -64000; j < 64000; j += 2000) { if (i == 0 && j == 0) continue; Ogre::Vector3 position(i, 0, j); long xslot, yslot; terrain.mTerrainGroup ->convertWorldPositionToTerrainSlot( position, &xslot, &yslot); Ogre::Vector3 slotpos; terrain.mTerrainGroup ->convertTerrainSlotToWorldPosition( xslot, yslot, &slotpos); Ogre::Vector3 offset = (position - slotpos) * terrainSize / worldSize; height = HeightData::get_singleton() ->get_height( terrain.mTerrainGroup, (long)position .x, (long)position .z); #if 0 height = terrain.mTerrainGroup ->getHeightAtWorldPosition( position); #endif if (height > -9.0f) continue; #ifdef VDEBUG std::cout << "worldSize: " << worldSize - 1 << std::endl; std::cout << "height: " << i << " " << j << " " << height << std::endl; #endif item.entity = "altar.glb"; item.rotation = Ogre::Quaternion(0, 0, 0, 1); position.y = height; item.position = position; placement.altar_items.push_back( item); } #ifdef VDEBUG for (i = 0; i < placement.altar_items.size(); i++) { std::cout << "placement: " << i << " " << placement.altar_items[i] .position << std::endl; } #endif ECS::get().add(); } }); #if 0 ecs.system("SetPlayerPosition") .kind(flecs::OnUpdate) .with() .each([this](const Terrain &terrain) { flecs::entity player = ECS::get()) return; if (!player.has()) return; if (!player.has()) return; CharacterLocation &loc = player.get_mut(); const CharacterBase &ch = player.get(); if (!ch.mBodyNode) { std::cout << "no player yet"; return; } float height = get_height(terrain.mTerrainGroup, loc.position); loc.position.y = height + 0.0f; ch.mBodyNode->setPosition(loc.position); ch.mBodyNode->setOrientation(Ogre::Quaternion()); player.modified(); ECS::get().remove(); }); #endif ecs.system("UpdateTerrainGroup") .kind(flecs::OnUpdate) .interval(2.0f) .each([](const Terrain &terrain) { if (!terrain.mTerrainGroup ->isDerivedDataUpdateInProgress()) terrain.mTerrainGroup->update(false); }); } float TerrainModule::get_height(Ogre::TerrainGroup *group, const Ogre::Vector3 &position) { int worldSize = group->getTerrainWorldSize(); uint16_t terrainSize = group->getTerrainSize(); long xslot, yslot; group->convertWorldPositionToTerrainSlot(position, &xslot, &yslot); Ogre::Vector3 slotpos; group->convertTerrainSlotToWorldPosition(xslot, yslot, &slotpos); Ogre::Vector3 offset = (position - slotpos) * terrainSize / worldSize; float height = HeightData::get_singleton()->get_height( group, (long)position.x, (long)position.z); return height; } float TerrainModule::get_world_x(int x) { return HeightData::get_singleton()->get_world_x(x); } float TerrainModule::get_world_y(int y) { return HeightData::get_singleton()->get_world_y(y); } int TerrainModule::get_img_x(float world_x) { return HeightData::get_singleton()->get_img_x(world_x); } int TerrainModule::get_img_y(float world_z) { return HeightData::get_singleton()->get_img_y(world_z); } void TerrainModule::update_heightmap(const Ogre::Image &heightmap) { return HeightData::get_singleton()->update_heightmap(heightmap); } void TerrainModule::save_heightmap() { HeightData::get_singleton()->save_heightmap(); } void TerrainModule::defineTerrain(long x, long y) { ECS::get().definer->define(ECS::get().mTerrainGroup, x, y); } }