diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index 9cf71339..41988abf 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -2,6 +2,7 @@ menu.missing-content=Missing Content! world.convert-request=Content indices have changed! Convert world files? world.upgrade-request=World format is outdated! Convert world files? +world.convert-with-loss=Convert world with data loss? pack.remove-confirm=Do you want to erase all pack(s) content from the world forever? error.pack-not-found=Could not to find pack error.dependency-not-found=Dependency pack is not found diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 20021b53..2ea4610f 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -47,6 +47,7 @@ world.generators.flat=Плоский world.Create World=Создать Мир world.convert-request=Есть изменения в индексах! Конвертировать мир? world.upgrade-request=Формат мира устарел! Конвертировать мир? +world.convert-with-loss=Конвертировать мир с потерями? world.delete-confirm=Удалить мир безвозвратно? # Настройки diff --git a/src/content/ContentReport.cpp b/src/content/ContentReport.cpp index 2dd72259..257bd3b3 100644 --- a/src/content/ContentReport.cpp +++ b/src/content/ContentReport.cpp @@ -56,11 +56,40 @@ std::shared_ptr ContentReport::create( indices, blocks_c, items_c, regionsVersion); report->blocks.setup(blocklist, content->blocks); report->items.setup(itemlist, content->items); + + for (const auto& [name, map] : root["blocks-data"].asObject()) { + data::StructLayout layout; + layout.deserialize(map); + auto def = content->blocks.find(name); + if (def == nullptr) { + continue; + } + if (def->dataStruct == nullptr) { + ContentIssue issue {ContentIssueType::BLOCK_DATA_LAYOUTS_UPDATE}; + report->issues.push_back(issue); + report->dataLoss.push_back(name+": discard data"); + continue; + } + auto incapatibility = layout.checkCompatibility(*def->dataStruct); + if (!incapatibility.empty()) { + ContentIssue issue {ContentIssueType::BLOCK_DATA_LAYOUTS_UPDATE}; + report->issues.push_back(issue); + for (const auto& error : incapatibility) { + report->dataLoss.push_back( + "[" + name + "] field " + error.name + " - " + + data::to_string(error.type) + ); + } + } + report->blocksDataLayouts[name] = std::move(layout); + } + report->buildIssues(); if (report->isUpgradeRequired() || report->hasContentReorder() || - report->hasMissingContent()) { + report->hasMissingContent() || + report->hasDataLoss()) { return report; } else { return nullptr; diff --git a/src/content/ContentReport.hpp b/src/content/ContentReport.hpp index c5c0e087..1c7cb274 100644 --- a/src/content/ContentReport.hpp +++ b/src/content/ContentReport.hpp @@ -4,11 +4,13 @@ #include #include #include +#include #include "constants.hpp" #include "data/dv.hpp" #include "typedefs.hpp" #include "Content.hpp" +#include "data/StructLayout.hpp" #include "files/world_regions_fwd.hpp" namespace fs = std::filesystem; @@ -17,6 +19,7 @@ enum class ContentIssueType { REORDER, MISSING, REGION_FORMAT_UPDATE, + BLOCK_DATA_LAYOUTS_UPDATE, }; struct ContentIssue { @@ -121,7 +124,9 @@ public: ContentUnitLUT items; uint regionsVersion; + std::unordered_map blocksDataLayouts; std::vector issues; + std::vector dataLoss; ContentReport( const ContentIndices* indices, @@ -136,6 +141,10 @@ public: const Content* content ); + inline const std::vector& getDataLoss() const { + return dataLoss; + } + inline bool hasContentReorder() const { return blocks.hasContentReorder() || items.hasContentReorder(); } @@ -145,6 +154,9 @@ public: inline bool isUpgradeRequired() const { return regionsVersion < REGION_FORMAT_VERSION; } + inline bool hasDataLoss() const { + return !dataLoss.empty(); + } void buildIssues(); const std::vector& getIssues() const; diff --git a/src/data/StructLayout.hpp b/src/data/StructLayout.hpp index 4e0ff886..784be27b 100644 --- a/src/data/StructLayout.hpp +++ b/src/data/StructLayout.hpp @@ -30,6 +30,12 @@ namespace data { TYPE_ERROR, MISSING, }; + inline const char* to_string(FieldIncapatibilityType type) { + const char* names[] = { + "none", "data_loss", "type_error", "missing" + }; + return names[static_cast(type)]; + } struct FieldIncapatibility { std::string name; diff --git a/src/files/WorldConverter.cpp b/src/files/WorldConverter.cpp index ca50bc6e..3bd9b7b4 100644 --- a/src/files/WorldConverter.cpp +++ b/src/files/WorldConverter.cpp @@ -13,6 +13,7 @@ #include "util/ThreadPool.hpp" #include "voxels/Chunk.hpp" #include "items/Inventory.hpp" +#include "voxels/Block.hpp" #include "WorldFiles.hpp" namespace fs = std::filesystem; @@ -85,6 +86,7 @@ void WorldConverter::createConvertTasks() { const auto& regions = wfile->getRegions(); for (auto& issue : report->getIssues()) { switch (issue.issueType) { + case ContentIssueType::BLOCK_DATA_LAYOUTS_UPDATE: case ContentIssueType::REGION_FORMAT_UPDATE: break; case ContentIssueType::MISSING: @@ -111,8 +113,24 @@ WorldConverter::WorldConverter( { if (upgradeMode) { createUpgradeTasks(); - } else { + } else if (report->hasContentReorder()) { createConvertTasks(); + } else { + // blocks data conversion requires correct block indices + // so it must be done AFTER voxels conversion + const auto& regions = wfile->getRegions(); + for (auto& issue : report->getIssues()) { + switch (issue.issueType) { + case ContentIssueType::BLOCK_DATA_LAYOUTS_UPDATE: + addRegionsTasks( + REGION_LAYER_BLOCKS_DATA, + ConvertTaskType::CONVERT_BLOCKS_DATA + ); + break; + default: + break; + } + } } } @@ -187,6 +205,34 @@ void WorldConverter::convertPlayer(const fs::path& file) const { files::write_json(file, map); } +void WorldConverter::convertBlocksData(int x, int z, const ContentReport& report) const { + logger.info() << "converting blocks data"; + wfile->getRegions().processBlocksData(x, z, + [=](BlocksMetadata& heap, std::unique_ptr voxelsData) { + Chunk chunk(0, 0); + chunk.decode(voxelsData.get()); + + const auto& indices = content->getIndices()->blocks; + + BlocksMetadata newHeap; + for (const auto& entry : heap) { + size_t index = entry.index; + const auto& def = indices.require(chunk.voxels[index].id); + const auto& newStruct = *def.dataStruct; + const auto& found = report.blocksDataLayouts.find(def.name); + if (found == report.blocksDataLayouts.end()) { + logger.error() << "no previous fields layout found for block" + << def.name << " - discard"; + continue; + } + const auto& prevStruct = found->second; + uint8_t* dst = newHeap.allocate(index, newStruct.size()); + newStruct.convert(prevStruct, entry.data(), dst, true); + } + heap = std::move(newHeap); + }); +} + void WorldConverter::convert(const ConvertTask& task) const { if (!fs::is_regular_file(task.file)) return; @@ -203,6 +249,9 @@ void WorldConverter::convert(const ConvertTask& task) const { case ConvertTaskType::PLAYER: convertPlayer(task.file); break; + case ConvertTaskType::CONVERT_BLOCKS_DATA: + convertBlocksData(task.x, task.z, *report); + break; } } diff --git a/src/files/WorldConverter.hpp b/src/files/WorldConverter.hpp index fe9e35de..6331acb6 100644 --- a/src/files/WorldConverter.hpp +++ b/src/files/WorldConverter.hpp @@ -24,6 +24,8 @@ enum class ConvertTaskType { PLAYER, /// @brief refresh region file version UPGRADE_REGION, + /// @brief convert blocks data to updated layouts + CONVERT_BLOCKS_DATA, }; struct ConvertTask { @@ -49,6 +51,7 @@ class WorldConverter : public Task { void convertPlayer(const fs::path& file) const; void convertVoxels(const fs::path& file, int x, int z) const; void convertInventories(const fs::path& file, int x, int z) const; + void convertBlocksData(int x, int z, const ContentReport& report) const; void addRegionsTasks( RegionLayerIndex layerid, diff --git a/src/files/WorldFiles.cpp b/src/files/WorldFiles.cpp index 748a3cbc..7f59e602 100644 --- a/src/files/WorldFiles.cpp +++ b/src/files/WorldFiles.cpp @@ -21,6 +21,7 @@ #include "objects/EntityDef.hpp" #include "objects/Player.hpp" #include "physics/Hitbox.hpp" +#include "data/StructLayout.hpp" #include "settings.hpp" #include "typedefs.hpp" #include "util/data_io.hpp" @@ -116,6 +117,15 @@ void WorldFiles::writeIndices(const ContentIndices* indices) { write_indices(indices->blocks, root.list("blocks")); write_indices(indices->items, root.list("items")); write_indices(indices->entities, root.list("entities")); + + auto& structsMap = root.object("blocks-data"); + for (const auto* def : indices->blocks.getIterable()) { + if (def->dataStruct == nullptr) { + continue; + } + structsMap[def->name] = def->dataStruct->serialize(); + } + files::write_json(getIndicesFile(), root); } diff --git a/src/files/WorldRegions.cpp b/src/files/WorldRegions.cpp index 35d15ac7..87ee4351 100644 --- a/src/files/WorldRegions.cpp +++ b/src/files/WorldRegions.cpp @@ -4,6 +4,7 @@ #include #include +#include "debug/Logger.hpp" #include "coders/byte_utils.hpp" #include "coders/rle.hpp" #include "coders/binary_json.hpp" @@ -13,6 +14,8 @@ #define REGION_FORMAT_MAGIC ".VOXREG" +static debug::Logger logger("world-regions"); + WorldRegion::WorldRegion() : chunksData( std::make_unique[]>(REGION_CHUNKS_COUNT) @@ -95,15 +98,21 @@ void WorldRegions::put( ) { size_t size = srcSize; auto& layer = layers[layerid]; - if (layer.compression != compression::Method::NONE) { - data = compression::compress( - data.get(), size, size, layer.compression); - } int regionX, regionZ, localX, localZ; calc_reg_coords(x, z, regionX, regionZ, localX, localZ); WorldRegion* region = layer.getOrCreateRegion(regionX, regionZ); region->setUnsaved(true); + + if (data == nullptr) { + region->put(localX, localZ, nullptr, 0, 0); + return; + } + + if (layer.compression != compression::Method::NONE) { + data = compression::compress( + data.get(), size, size, layer.compression); + } region->put(localX, localZ, std::move(data), size, srcSize); } @@ -251,9 +260,7 @@ BlocksMetadata WorldRegions::getBlocksData(int x, int z) { return heap; } -void WorldRegions::processInventories( - int x, int z, const inventoryproc& func -) { +void WorldRegions::processInventories(int x, int z, const InventoryProc& func) { processRegion(x, z, REGION_LAYER_INVENTORIES, [=](std::unique_ptr data, uint32_t* size) { auto inventories = load_inventories(data.get(), *size); @@ -264,6 +271,65 @@ void WorldRegions::processInventories( }); } +void WorldRegions::processBlocksData(int x, int z, const BlockDataProc& func) { + auto& voxLayer = layers[REGION_LAYER_VOXELS]; + auto& datLayer = layers[REGION_LAYER_BLOCKS_DATA]; + if (voxLayer.getRegion(x, z) || datLayer.getRegion(x, z)) { + throw std::runtime_error("not implemented for in-memory regions"); + } + auto datRegfile = datLayer.getRegFile({x, z}); + if (datRegfile == nullptr) { + throw std::runtime_error("could not open region file"); + } + auto voxRegfile = voxLayer.getRegFile({x, z}); + if (voxRegfile == nullptr) { + logger.warning() << "missing voxels region - discard blocks data for " + << x << "_" << z; + abort(); // TODO: delete region file + } + for (uint cz = 0; cz < REGION_SIZE; cz++) { + for (uint cx = 0; cx < REGION_SIZE; cx++) { + int gx = cx + x * REGION_SIZE; + int gz = cz + z * REGION_SIZE; + + uint32_t datLength; + uint32_t datSrcSize; + auto datData = RegionsLayer::readChunkData( + gx, gz, datLength, datSrcSize, datRegfile.get() + ); + if (datData == nullptr) { + continue; + } + uint32_t voxLength; + uint32_t voxSrcSize; + auto voxData = RegionsLayer::readChunkData( + gx, gz, voxLength, voxSrcSize, voxRegfile.get() + ); + if (voxData == nullptr) { + logger.warning() + << "missing voxels for chunk (" << gx << ", " << gz << ")"; + put(gx, gz, REGION_LAYER_BLOCKS_DATA, nullptr, 0); + continue; + } + voxData = compression::decompress( + voxData.get(), voxLength, voxSrcSize, voxLayer.compression + ); + + BlocksMetadata blocksData; + blocksData.deserialize(datData.get(), datLength); + try { + func(blocksData, std::move(voxData)); + } catch (const std::exception& err) { + logger.error() << "an error ocurred while processing blocks " + "data in chunk (" << gx << ", " << gz << "): " << err.what(); + blocksData = {}; + } + auto bytes = blocksData.serialize(); + put(gx, gz, REGION_LAYER_BLOCKS_DATA, bytes.release(), bytes.size()); + } + } +} + dv::value WorldRegions::fetchEntities(int x, int z) { if (generatorTestMode) { return nullptr; @@ -282,7 +348,7 @@ dv::value WorldRegions::fetchEntities(int x, int z) { } void WorldRegions::processRegion( - int x, int z, RegionLayerIndex layerid, const regionproc& func + int x, int z, RegionLayerIndex layerid, const RegionProc& func ) { auto& layer = layers[layerid]; if (layer.getRegion(x, z)) { diff --git a/src/files/WorldRegions.hpp b/src/files/WorldRegions.hpp index 4fe37ab7..722e5c48 100644 --- a/src/files/WorldRegions.hpp +++ b/src/files/WorldRegions.hpp @@ -64,9 +64,10 @@ struct regfile { std::unique_ptr read(int index, uint32_t& size, uint32_t& srcSize); }; -using regionsmap = std::unordered_map>; -using regionproc = std::function(std::unique_ptr,uint32_t*)>; -using inventoryproc = std::function; +using RegionsMap = std::unordered_map>; +using RegionProc = std::function(std::unique_ptr,uint32_t*)>; +using InventoryProc = std::function; +using BlockDataProc = std::function)>; /// @brief Region file pointer keeping inUse flag on until destroyed class regfile_ptr { @@ -125,7 +126,7 @@ struct RegionsLayer { compression::Method compression = compression::Method::NONE; /// @brief In-memory regions data - regionsmap regions; + RegionsMap regions; /// @brief In-memory regions map mutex std::mutex mapMutex; @@ -231,10 +232,11 @@ public: /// @param layerid regions layer index /// @param func processing callback void processRegion( - int x, int z, RegionLayerIndex layerid, const regionproc& func); + int x, int z, RegionLayerIndex layerid, const RegionProc& func); - void processInventories( - int x, int z, const inventoryproc& func); + void processInventories(int x, int z, const InventoryProc& func); + + void processBlocksData(int x, int z, const BlockDataProc& func); /// @brief Get regions directory by layer index /// @param layerid layer index diff --git a/src/graphics/ui/gui_util.cpp b/src/graphics/ui/gui_util.cpp index 2045713a..70bd94f5 100644 --- a/src/graphics/ui/gui_util.cpp +++ b/src/graphics/ui/gui_util.cpp @@ -3,6 +3,7 @@ #include "elements/Label.hpp" #include "elements/Menu.hpp" #include "elements/Button.hpp" +#include "elements/TextBox.hpp" #include "gui_xml.hpp" #include "logic/scripting/scripting.hpp" @@ -77,3 +78,47 @@ void guiutil::confirm( menu->addPage("", panel); menu->setPage(""); } + +void guiutil::confirmWithMemo( + gui::GUI* gui, + const std::wstring& text, + const std::wstring& memo, + const runnable& on_confirm, + std::wstring yestext, + std::wstring notext) { + + if (yestext.empty()) yestext = langs::get(L"Yes"); + if (notext.empty()) notext = langs::get(L"No"); + + auto menu = gui->getMenu(); + auto panel = std::make_shared(glm::vec2(600, 500), glm::vec4(8.0f), 8.0f); + panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f)); + panel->add(std::make_shared