From e0b3425eff12bd8132bf551a8d03c96322536b51 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 13 Dec 2024 05:54:41 +0300 Subject: [PATCH] migrate blocks interaction (scripting) to global chunks storage (WIP) --- dev/tests/example.lua | 8 +- res/content/base/scripts/world.lua | 22 ++-- src/frontend/debug_panel.cpp | 3 +- src/frontend/hud.cpp | 3 +- src/graphics/render/WorldRenderer.cpp | 2 +- src/logic/scripting/lua/libs/libblock.cpp | 11 ++ src/maths/voxmaths.hpp | 19 +++- src/util/WeakPtrsMap.hpp | 43 ------- src/voxels/Chunks.cpp | 76 ++++--------- src/voxels/Chunks.hpp | 2 - src/voxels/ChunksStorage.cpp | 132 +++++++++++++++++++--- src/voxels/ChunksStorage.hpp | 23 +++- src/world/Level.cpp | 8 ++ src/world/LevelEvents.cpp | 6 +- src/world/LevelEvents.hpp | 11 +- src/world/World.cpp | 2 +- 16 files changed, 227 insertions(+), 144 deletions(-) delete mode 100644 src/util/WeakPtrsMap.hpp diff --git a/dev/tests/example.lua b/dev/tests/example.lua index 27833cd10..ae6c05f22 100644 --- a/dev/tests/example.lua +++ b/dev/tests/example.lua @@ -1,12 +1,16 @@ -test.set_setting("chunks.load-distance", 2) +test.set_setting("chunks.load-distance", 3) test.set_setting("chunks.load-speed", 16) +test.reconfig_packs({"base"}, {}) test.new_world("demo", "2019", "core:default") local pid = player.create("Xerxes") assert(player.get_name(pid) == "Xerxes") test.sleep_until(function() return world.count_chunks() >= 9 end, 1000) print(world.count_chunks()) -assert(block.get(0, 0, 0) == block.index("core:obstacle")) + +timeit(1000000, block.get, 0, 0, 0) +timeit(1000000, block.get_slow, 0, 0, 0) + block.destruct(0, 0, 0, pid) assert(block.get(0, 0, 0) == 0) test.close_world(true) diff --git a/res/content/base/scripts/world.lua b/res/content/base/scripts/world.lua index 62d111ae3..3f69dc444 100644 --- a/res/content/base/scripts/world.lua +++ b/res/content/base/scripts/world.lua @@ -1,12 +1,14 @@ function on_block_broken(id, x, y, z, playerid) - gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { - lifetime=1.0, - spawn_interval=0.0001, - explosion={4, 4, 4}, - texture="blocks:"..block.get_textures(id)[1], - random_sub_uv=0.1, - size={0.1, 0.1, 0.1}, - spawn_shape="box", - spawn_spread={0.4, 0.4, 0.4} - }) + if gfx then + gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { + lifetime=1.0, + spawn_interval=0.0001, + explosion={4, 4, 4}, + texture="blocks:"..block.get_textures(id)[1], + random_sub_uv=0.1, + size={0.1, 0.1, 0.1}, + spawn_shape="box", + spawn_spread={0.4, 0.4, 0.4} + }) + end end diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index c9f08203b..f78fc6a8e 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -21,6 +21,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/ChunksStorage.hpp" #include "world/Level.hpp" #include "world/World.hpp" @@ -95,7 +96,7 @@ std::shared_ptr create_debug_panel( std::to_wstring(ParticlesRenderer::aliveEmitters); })); panel->add(create_label([&]() { - return L"chunks: "+std::to_wstring(level.chunks->getChunksCount())+ + return L"chunks: "+std::to_wstring(level.chunksStorage->size())+ L" visible: "+std::to_wstring(ChunksRenderer::visibleChunks); })); panel->add(create_label([&]() { diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 20199f0ae..55449ad9b 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -269,6 +269,7 @@ void Hud::updateHotbarControl() { void Hud::updateWorldGenDebugVisualization() { auto& level = frontend.getLevel(); + auto& chunks = *level.chunks; auto generator = frontend.getController()->getChunksController()->getGenerator(); auto debugInfo = generator->createDebugInfo(); @@ -293,7 +294,7 @@ void Hud::updateWorldGenDebugVisualization() { int az = z - (height - areaHeight) / 2; data[(flippedZ * width + x) * 4 + 1] = - level.chunks->getChunk(ax + ox, az + oz) ? 255 : 0; + chunks.getChunk(ax + ox, az + oz) ? 255 : 0; data[(flippedZ * width + x) * 4 + 0] = level.chunksStorage->fetch(ax + ox, az + oz) ? 255 : 0; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 19d6d232f..a0fd4c83d 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -86,7 +86,7 @@ WorldRenderer::WorldRenderer( auto& settings = engine->getSettings(); level.events->listen( EVT_CHUNK_HIDDEN, - [this](lvl_event_type, Chunk* chunk) { chunks->unload(chunk); } + [this](LevelEventType, Chunk* chunk) { chunks->unload(chunk); } ); auto assets = engine->getAssets(); skybox = std::make_unique( diff --git a/src/logic/scripting/lua/libs/libblock.cpp b/src/logic/scripting/lua/libs/libblock.cpp index 2f24e0feb..bf61549e5 100644 --- a/src/logic/scripting/lua/libs/libblock.cpp +++ b/src/logic/scripting/lua/libs/libblock.cpp @@ -7,6 +7,7 @@ #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" #include "voxels/voxel.hpp" +#include "voxels/ChunksStorage.hpp" #include "world/Level.hpp" #include "maths/voxmaths.hpp" #include "data/StructLayout.hpp" @@ -114,6 +115,15 @@ static int l_get(lua::State* L) { return lua::pushinteger(L, id); } +static int l_get_slow(lua::State* L) { + auto x = lua::tointeger(L, 1); + auto y = lua::tointeger(L, 2); + auto z = lua::tointeger(L, 3); + auto vox = level->chunksStorage->get(x, y, z); + int id = vox == nullptr ? -1 : vox->id; + return lua::pushinteger(L, id); +} + static int l_get_x(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); @@ -598,6 +608,7 @@ const luaL_Reg blocklib[] = { {"is_replaceable_at", lua::wrap}, {"set", lua::wrap}, {"get", lua::wrap}, + {"get_slow", lua::wrap}, {"get_X", lua::wrap}, {"get_Y", lua::wrap}, {"get_Z", lua::wrap}, diff --git a/src/maths/voxmaths.hpp b/src/maths/voxmaths.hpp index 07b42e221..feeb9c8f3 100644 --- a/src/maths/voxmaths.hpp +++ b/src/maths/voxmaths.hpp @@ -1,7 +1,5 @@ #pragma once -#include "typedefs.hpp" - inline constexpr int floordiv(int a, int b) { if (a < 0 && a % b) { return (a / b) - 1; @@ -9,6 +7,23 @@ inline constexpr int floordiv(int a, int b) { return a / b; } +inline constexpr bool is_pot(int a) { + return (a > 0) && ((a & (a - 1)) == 0); +} + +inline constexpr unsigned floorlog2(unsigned x) { + return x == 1 ? 0 : 1 + floorlog2(x >> 1); +} + +template +inline constexpr int floordiv(int a) { + if constexpr (is_pot(b)) { + return a >> floorlog2(b); + } else { + return floordiv(a, b); + } +} + inline constexpr int ceildiv(int a, int b) { if (a > 0 && a % b) { return a / b + 1; diff --git a/src/util/WeakPtrsMap.hpp b/src/util/WeakPtrsMap.hpp deleted file mode 100644 index 0d206d652..000000000 --- a/src/util/WeakPtrsMap.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -namespace util { - template - class WeakPtrsMap { - std::unordered_map> map; - std::mutex mutex; - public: - std::weak_ptr& operator[](const K& k) { - return map[k]; - } - - std::shared_ptr fetch(const K& k) { - auto found = map.find(k); - if (found == map.end()) { - return nullptr; - } - auto ptr = found->second.lock(); - if (ptr == nullptr) { - map.erase(found); - } - return ptr; - } - - void erase(const K& k) { - map.erase(k); - } - - size_t size() const { - return map.size(); - } - - void lock() { - mutex.lock(); - } - - void unlock() { - mutex.unlock(); - } - }; -} diff --git a/src/voxels/Chunks.cpp b/src/voxels/Chunks.cpp index 943249647..390ac16c6 100644 --- a/src/voxels/Chunks.cpp +++ b/src/voxels/Chunks.cpp @@ -9,7 +9,6 @@ #include "data/StructLayout.hpp" #include "coders/byte_utils.hpp" -#include "coders/json.hpp" #include "content/Content.hpp" #include "files/WorldFiles.hpp" #include "graphics/core/Mesh.hpp" @@ -39,7 +38,6 @@ Chunks::Chunks( worldFiles(wfile) { areaMap.setCenter(ox-w/2, oz-d/2); areaMap.setOutCallback([this](int, int, const auto& chunk) { - save(chunk.get()); this->level->events->trigger(EVT_CHUNK_HIDDEN, chunk.get()); }); } @@ -48,13 +46,13 @@ voxel* Chunks::get(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return nullptr; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { return nullptr; } - Chunk* chunk = ptr->get(); // not thread safe + Chunk* chunk = ptr->get(); if (chunk == nullptr) { return nullptr; } @@ -107,27 +105,27 @@ const AABB* Chunks::isObstacleAt(float x, float y, float z) const { bool Chunks::isSolidBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->rt.solid; //-V522 + return indices->blocks.require(v->id).rt.solid; } bool Chunks::isReplaceableBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->replaceable; //-V522 + return indices->blocks.require(v->id).replaceable; } bool Chunks::isObstacleBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->obstacle; //-V522 + return indices->blocks.require(v->id).obstacle; } ubyte Chunks::getLight(int32_t x, int32_t y, int32_t z, int channel) const { if (y < 0 || y >= CHUNK_H) { return 0; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { @@ -146,8 +144,8 @@ light_t Chunks::getLight(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return 0; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { @@ -166,8 +164,8 @@ Chunk* Chunks::getChunkByVoxel(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return nullptr; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); if (auto ptr = areaMap.getIf(cx, cz)) { return ptr->get(); } @@ -369,8 +367,8 @@ void Chunks::set( if (y < 0 || y >= CHUNK_H) { return; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { return; @@ -673,7 +671,11 @@ void Chunks::resize(uint32_t newW, uint32_t newD) { } bool Chunks::putChunk(const std::shared_ptr& chunk) { - return areaMap.set(chunk->x, chunk->z, chunk); + if (areaMap.set(chunk->x, chunk->z, chunk)) { + level->events->trigger(LevelEventType::EVT_CHUNK_SHOWN, chunk.get()); + return true; + } + return false; } // reduce nesting on next modification @@ -692,11 +694,11 @@ void Chunks::getVoxels(VoxelsVolume* volume, bool backlight) const { int h = volume->getH(); int d = volume->getD(); - int scx = floordiv(x, CHUNK_W); - int scz = floordiv(z, CHUNK_D); + int scx = floordiv(x); + int scz = floordiv(z); - int ecx = floordiv(x + w, CHUNK_W); - int ecz = floordiv(z + d, CHUNK_D); + int ecx = floordiv(x + w); + int ecz = floordiv(z + d); int cw = ecx - scx + 1; int cd = ecz - scz + 1; @@ -768,35 +770,3 @@ void Chunks::getVoxels(VoxelsVolume* volume, bool backlight) const { void Chunks::saveAndClear() { areaMap.clear(); } - -void Chunks::save(Chunk* chunk) { - if (chunk != nullptr) { - AABB aabb( - glm::vec3(chunk->x * CHUNK_W, -INFINITY, chunk->z * CHUNK_D), - glm::vec3( - (chunk->x + 1) * CHUNK_W, INFINITY, (chunk->z + 1) * CHUNK_D - ) - ); - auto entities = level->entities->getAllInside(aabb); - auto root = dv::object(); - root["data"] = level->entities->serialize(entities); - if (!entities.empty()) { - level->entities->despawn(std::move(entities)); - chunk->flags.entities = true; - } - worldFiles->getRegions().put( - chunk, - chunk->flags.entities ? json::to_binary(root, true) - : std::vector() - ); - } -} - -void Chunks::saveAll() { - const auto& chunks = areaMap.getBuffer(); - for (size_t i = 0; i < areaMap.area(); i++) { - if (auto& chunk = chunks[i]) { - save(chunk.get()); - } - } -} diff --git a/src/voxels/Chunks.hpp b/src/voxels/Chunks.hpp index 01648301a..fd33eb9a9 100644 --- a/src/voxels/Chunks.hpp +++ b/src/voxels/Chunks.hpp @@ -124,8 +124,6 @@ class Chunks { void resize(uint32_t newW, uint32_t newD); void saveAndClear(); - void save(Chunk* chunk); - void saveAll(); const std::vector>& getChunks() const { return areaMap.getBuffer(); diff --git a/src/voxels/ChunksStorage.cpp b/src/voxels/ChunksStorage.cpp index d3dd25467..8a872eea6 100644 --- a/src/voxels/ChunksStorage.cpp +++ b/src/voxels/ChunksStorage.cpp @@ -3,6 +3,7 @@ #include #include "content/Content.hpp" +#include "coders/json.hpp" #include "debug/Logger.hpp" #include "files/WorldFiles.hpp" #include "items/Inventories.hpp" @@ -15,16 +16,27 @@ #include "Block.hpp" #include "Chunk.hpp" +inline long long keyfrom(int x, int z) { + union { + int pos[2]; + long long key; + } ekey; + ekey.pos[0] = x; + ekey.pos[1] = z; + return ekey.key; +} + static debug::Logger logger("chunks-storage"); -ChunksStorage::ChunksStorage(Level* level) - : level(level), - chunksMap(std::make_shared>()) { +ChunksStorage::ChunksStorage(Level* level) : level(level) { } std::shared_ptr ChunksStorage::fetch(int x, int z) { - std::lock_guard lock(*chunksMap); - return chunksMap->fetch({x, z}); + const auto& found = chunksMap.find(keyfrom(x, z)); + if (found == chunksMap.end()) { + return nullptr; + } + return found->second; } static void check_voxels(const ContentIndices& indices, Chunk& chunk) { @@ -51,24 +63,22 @@ static void check_voxels(const ContentIndices& indices, Chunk& chunk) { } } +void ChunksStorage::erase(int x, int z) { + chunksMap.erase(keyfrom(x, z)); +} + std::shared_ptr ChunksStorage::create(int x, int z) { - if (auto ptr = chunksMap->fetch({x, z})) { - return ptr; + const auto& found = chunksMap.find(keyfrom(x, z)); + if (found != chunksMap.end()) { + return found->second; } + auto chunk = std::make_shared(x, z); + chunksMap[keyfrom(x, z)] = chunk; + World* world = level->getWorld(); auto& regions = world->wfile.get()->getRegions(); - auto& localChunksMap = chunksMap; - auto chunk = std::shared_ptr( - new Chunk(x, z), - [localChunksMap, x, z](Chunk* ptr) { - std::lock_guard lock(*localChunksMap); - localChunksMap->erase({x, z}); - delete ptr; - } - ); - (*chunksMap)[glm::ivec2(chunk->x, chunk->z)] = chunk; if (auto data = regions.getVoxels(chunk->x, chunk->z)) { const auto& indices = *level->content->getIndices(); @@ -110,6 +120,92 @@ std::shared_ptr ChunksStorage::create(int x, int z) { return chunk; } +void ChunksStorage::pinChunk(std::shared_ptr chunk) { + pinnedChunks[{chunk->x, chunk->z}] = std::move(chunk); +} + +void ChunksStorage::unpinChunk(int x, int z) { + pinnedChunks.erase({x, z}); +} + size_t ChunksStorage::size() const { - return chunksMap->size(); + return chunksMap.size(); +} + +voxel* ChunksStorage::get(int x, int y, int z) const { + if (y < 0 || y >= CHUNK_H) { + return nullptr; + } + + int cx = floordiv(x); + int cz = floordiv(z); + + const auto& found = chunksMap.find(keyfrom(cx, cz)); + if (found == chunksMap.end()) { + return nullptr; + } + const auto& chunk = found->second; + int lx = x - cx * CHUNK_W; + int lz = z - cz * CHUNK_D; + return &chunk->voxels[(y * CHUNK_D + lz) * CHUNK_W + lx]; +} + +void ChunksStorage::incref(Chunk* chunk) { + auto key = reinterpret_cast(chunk); + const auto& found = refCounters.find(key); + if (found == refCounters.end()) { + refCounters[key] = 1; + return; + } + found->second++; +} + +void ChunksStorage::decref(Chunk* chunk) { + auto key = reinterpret_cast(chunk); + const auto& found = refCounters.find(key); + if (found == refCounters.end()) { + abort(); + } + if (--found->second == 0) { + union { + int pos[2]; + long long key; + } ekey; + ekey.pos[0] = chunk->x; + ekey.pos[1] = chunk->z; + + save(chunk); + chunksMap.erase(ekey.key); + refCounters.erase(found); + } +} + +void ChunksStorage::save(Chunk* chunk) { + if (chunk == nullptr) { + return; + } + AABB aabb( + glm::vec3(chunk->x * CHUNK_W, -INFINITY, chunk->z * CHUNK_D), + glm::vec3( + (chunk->x + 1) * CHUNK_W, INFINITY, (chunk->z + 1) * CHUNK_D + ) + ); + auto entities = level->entities->getAllInside(aabb); + auto root = dv::object(); + root["data"] = level->entities->serialize(entities); + if (!entities.empty()) { + level->entities->despawn(std::move(entities)); + chunk->flags.entities = true; + } + level->getWorld()->wfile->getRegions().put( + chunk, + chunk->flags.entities ? json::to_binary(root, true) + : std::vector() + ); +} + +void ChunksStorage::saveAll() { + for (const auto& [_, chunk] : chunksMap) { + save(chunk.get()); + } } diff --git a/src/voxels/ChunksStorage.hpp b/src/voxels/ChunksStorage.hpp index cda81bcbb..9d8d03323 100644 --- a/src/voxels/ChunksStorage.hpp +++ b/src/voxels/ChunksStorage.hpp @@ -1,16 +1,22 @@ #pragma once +#include +#include + #define GLM_ENABLE_EXPERIMENTAL +#include #include -#include "util/WeakPtrsMap.hpp" +#include "voxel.hpp" class Chunk; class Level; class ChunksStorage { Level* level; - std::shared_ptr> chunksMap; + std::unordered_map> chunksMap; + std::unordered_map> pinnedChunks; + std::unordered_map refCounters; public: ChunksStorage(Level* level); ~ChunksStorage() = default; @@ -18,5 +24,18 @@ class ChunksStorage { std::shared_ptr fetch(int x, int z); std::shared_ptr create(int x, int z); + void pinChunk(std::shared_ptr chunk); + void unpinChunk(int x, int z); + + voxel* get(int x, int y, int z) const; + size_t size() const; + + void incref(Chunk* chunk); + void decref(Chunk* chunk); + + void erase(int x, int z); + + void save(Chunk* chunk); + void saveAll(); }; diff --git a/src/world/Level.cpp b/src/world/Level.cpp index 402c8e5fc..a89dc2e60 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -54,12 +54,20 @@ Level::Level( entities->setNextID(worldInfo.nextEntityId); } + events->listen(LevelEventType::EVT_CHUNK_SHOWN, [this](LevelEventType, Chunk* chunk) { + chunksStorage->incref(chunk); + }); + events->listen(LevelEventType::EVT_CHUNK_HIDDEN, [this](LevelEventType, Chunk* chunk) { + chunksStorage->decref(chunk); + }); + uint matrixSize = (settings.chunks.loadDistance.get() + settings.chunks.padding.get()) * 2; chunks = std::make_unique( matrixSize, matrixSize, 0, 0, world->wfile.get(), this ); + lighting = std::make_unique(content, chunks.get()); inventories = std::make_unique(*this); diff --git a/src/world/LevelEvents.cpp b/src/world/LevelEvents.cpp index 00017cc66..cb98cb9dd 100644 --- a/src/world/LevelEvents.cpp +++ b/src/world/LevelEvents.cpp @@ -4,14 +4,14 @@ using std::vector; -void LevelEvents::listen(lvl_event_type type, const chunk_event_func& func) { +void LevelEvents::listen(LevelEventType type, const ChunkEventFunc& func) { auto& callbacks = chunk_callbacks[type]; callbacks.push_back(func); } -void LevelEvents::trigger(lvl_event_type type, Chunk* chunk) { +void LevelEvents::trigger(LevelEventType type, Chunk* chunk) { const auto& callbacks = chunk_callbacks[type]; - for (const chunk_event_func& func : callbacks) { + for (const ChunkEventFunc& func : callbacks) { func(type, chunk); } } diff --git a/src/world/LevelEvents.hpp b/src/world/LevelEvents.hpp index d0518b5ca..f7bbaa7f6 100644 --- a/src/world/LevelEvents.hpp +++ b/src/world/LevelEvents.hpp @@ -6,16 +6,17 @@ class Chunk; -enum lvl_event_type { +enum LevelEventType { + EVT_CHUNK_SHOWN, EVT_CHUNK_HIDDEN, }; -using chunk_event_func = std::function; +using ChunkEventFunc = std::function; class LevelEvents { - std::unordered_map> + std::unordered_map> chunk_callbacks; public: - void listen(lvl_event_type type, const chunk_event_func& func); - void trigger(lvl_event_type type, Chunk* chunk); + void listen(LevelEventType type, const ChunkEventFunc& func); + void trigger(LevelEventType type, Chunk* chunk); }; diff --git a/src/world/World.cpp b/src/world/World.cpp index 812f6d919..0067964cf 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -66,7 +66,7 @@ void World::writeResources(const Content* content) { void World::write(Level* level) { const Content* content = level->content; - level->chunks->saveAll(); + level->chunksStorage->saveAll(); info.nextEntityId = level->entities->peekNextID(); wfile->write(this, content);