diff --git a/CMakeLists.txt b/CMakeLists.txt index 0258f915..1882b258 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -237,6 +237,7 @@ set(SOURCES "application/editormodevehicle.cpp" "application/editormodeselection.cpp" "application/editormodeviewport.cpp" +"application/editormodeterrainchunks.cpp" "application/editormodesignal.cpp" "application/editormodesweep.cpp" "application/editormodeturntable.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 59bbdc57..e5e345d3 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1495,6 +1495,10 @@ bool editor_mode::update() render_orthophoto_window(); render_switch_designer(); render_environment_window(); + if (m_terrain_open) + render_terrain_chunks_window(); + else + m_chunklist.scanned = false; // the files are read again when the terrain is the field of work again draw_selection_overlay(); render_context_menu(); @@ -1610,6 +1614,11 @@ void editor_mode::render_terrain_ui() add_chunks_around(m_terrain_add_count); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Makes the chunks missing around the camera, shaped as the ground under them")); + if (ImGui::Button(STR_C("List of the chunks..."))) + m_chunklist.open = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("All the chunks of the terrain with the files they're kept in and their triangles;\n" + "a double click on a chunk takes the camera to it")); render_ground_conversion(); if (dynamic_cast(m_node) != nullptr) { @@ -2121,6 +2130,9 @@ void editor_mode::draw_terrain_overlay() if (m_terrain_tool == terrain_tool::spacing) for (auto const &key : m_terrain_selection) chunk_outline(key, cursor.y, IM_COL32(90, 220, 255, 230), 3.0f); + // the chunk chosen in the list of the chunks + if (m_chunklist.open && m_chunklist.chosen) + chunk_outline(*m_chunklist.chosen, cursor.y, IM_COL32(255, 150, 40, 230), 3.0f); if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering) { auto const key = heightmap::chunk_at(cursor.x, cursor.z); diff --git a/application/editormode.h b/application/editormode.h index 653d70ae..bd56cbe4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -588,6 +588,33 @@ class editor_mode : public application_mode, private editor_track::observer float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited bool m_water_level_dragged{false}; std::string m_terrain_status; + // the window listing the chunks of the terrain, the files they're kept in and their triangles (editormodeterrainchunks.cpp) + void render_terrain_chunks_window(); + // the camera brought over the chunk, to look at the whole of it + void go_to_terrain_chunk(heightmap::chunk_key const &Key); + struct terrain_chunk_row + { + heightmap::chunk_key key{0, 0}; + int cells{0}; // quads along a side; 0: unknown + bool stored{false}; // in a pack file as it's on the disk + }; + struct terrain_chunk_list + { + bool open{true}; + bool scanned{false}; // the pack files were read + bool modified{false}; // the terrain had unsaved changes when last seen: once it hasn't, it was saved and the files are read again + std::vector> stored; // the chunks of the pack files, as read last + std::vector rows; // the chunks listed: of the files and of the memory, filtered and sorted + std::size_t chunks{0}; // listed before the filter + std::size_t files{0}; + std::size_t triangles{0}; // of the full grids of all the chunks + double built{-1.0}; // when the rows were made + bool resort{true}; + int sortcolumn{0}; + bool descending{false}; + char filter[64]{}; + std::optional chosen; // outlined in the view + } m_chunklist; // the heightmap terrain, shared with the scenery loader so it shows in every mode terrain_streamer &m_streamer{EditorTerrain}; diff --git a/application/editormodeterrainchunks.cpp b/application/editormodeterrainchunks.cpp new file mode 100644 index 00000000..8ccce7ce --- /dev/null +++ b/application/editormodeterrainchunks.cpp @@ -0,0 +1,361 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// the list of the chunks of the heightmap terrain: the pack files they're kept in, their grids and triangles, and the camera +// taken to the chunk double clicked + +#include "stdafx.h" +#include "application/editormode.h" +#include "editor/editorFormat.hpp" +#include "utilities/Globals.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include + +namespace +{ + +// how often the list takes in what changed in memory: the chunks loaded, edited and drawn +double constexpr list_refresh{0.5}; + +enum terrain_chunk_column +{ + column_chunk, + column_file, + column_spacing, + column_triangles, + column_drawn, + column_state, + column_count +}; + +enum class chunk_state +{ + stored, // in the file, not in memory + loaded, + edited, // changed since it was loaded or saved + added // not in the files yet +}; + +// a large number with its thousands set apart, to be read at a glance +std::string grouped(std::size_t const Value) +{ + auto digits{std::to_string(Value)}; + for (auto i = static_cast(digits.size()) - 3; i > 0; i -= 3) + digits.insert(static_cast(i), " "); + return digits; +} + +std::size_t grid_triangles(int const Cells) +{ + return static_cast(Cells) * static_cast(Cells) * 2; +} + +} // namespace + +void editor_mode::render_terrain_chunks_window() +{ + auto &list{m_chunklist}; + if (false == list.open) + { + // read again when it's opened, the files may have changed meanwhile + list.scanned = false; + return; + } + ImGui::SetNextWindowSize(ImVec2(560.0f * Global.ui_scale, 460.0f * Global.ui_scale), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin((std::string(STR_C("Terrain chunks")) + "###terrainchunks").c_str(), &list.open, ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return; + } + if (false == m_streamer.active()) + { + ImGui::TextWrapped("%s", STR_C("The scenery has no heightmap terrain yet")); + list.scanned = false; + list.rows.clear(); + list.chosen.reset(); + ImGui::End(); + return; + } + + // the files are read when the list opens, when it's asked to, and after the terrain was saved + auto const modified{m_streamer.modified()}; + auto const saved{list.modified && false == modified}; + list.modified = modified; + auto const now{ImGui::GetTime()}; + if (false == list.scanned || saved) + { + list.stored = m_streamer.stored_chunks(); + list.scanned = true; + list.built = -1.0; + } + + // the chunks of the files with the ones in memory, which may be edited, added since, or of another grid by now + if (list.built < 0.0 || now - list.built > list_refresh || list.resort) + { + std::vector resident; + m_streamer.collect(resident); + std::vector rows; + rows.reserve(list.stored.size() + resident.size()); + for (auto const &[key, cells] : list.stored) + if (false == m_streamer.removed(key)) + rows.push_back({key, cells, true}); + auto const stored{rows.size()}; + for (auto const *chunk : resident) + { + auto const key{chunk->key()}; + auto const found{std::lower_bound(rows.begin(), rows.begin() + stored, key, [](terrain_chunk_row const &Row, heightmap::chunk_key const &Key) { return Row.key < Key; })}; + if (found != rows.begin() + stored && found->key == key) + found->cells = chunk->cells(); + else + rows.push_back({key, chunk->cells(), false}); + } + // what the rows stand for, before the filter + std::vector packs; + list.triangles = 0; + for (auto const &row : rows) + { + list.triangles += grid_triangles(row.cells); + packs.push_back(heightmap::pack_of(row.key)); + } + std::sort(packs.begin(), packs.end()); + list.files = static_cast(std::unique(packs.begin(), packs.end()) - packs.begin()); + list.chunks = rows.size(); + // the filter: a part of the name of the file, or of the coordinates of the chunk + if (list.filter[0] != '\0') + { + std::string const filter{list.filter}; + std::erase_if(rows, [&](terrain_chunk_row const &Row) { + return heightmap::pack_file_name(heightmap::pack_of(Row.key)).find(filter) == std::string::npos && + format("%d,%d", Row.key.first, Row.key.second).find(filter) == std::string::npos; + }); + } + list.rows = std::move(rows); + list.built = now; + list.resort = true; + } + + // the state of a chunk and what's drawn of it, as it's now + auto const state_of = [&](terrain_chunk_row const &Row, editor_terrain const *Chunk) { + if (false == Row.stored) + return chunk_state::added; + if (Chunk == nullptr) + return chunk_state::stored; + return Chunk->modified() ? chunk_state::edited : chunk_state::loaded; + }; + auto const drawn_of = [](editor_terrain const *Chunk) -> std::size_t { return Chunk != nullptr ? Chunk->triangles() : 0; }; + + // summary, of all the chunks whatever the filter + std::vector resident; + m_streamer.collect(resident); + std::size_t drawn{0}, unsaved{0}; + for (auto const *chunk : resident) + { + drawn += drawn_of(chunk); + if (chunk->modified()) + ++unsaved; + } + auto const area{static_cast(list.chunks) * heightmap::chunk_size * heightmap::chunk_size / 1e6}; + ImGui::Text(STR_C("Chunks: %zu in %zu files, %.2f km2"), list.chunks, list.files, area); + ImGui::Text(STR_C("In memory: %zu, changed and not saved: %zu"), resident.size(), unsaved); + ImGui::Text(STR_C("Terrain triangles: %s"), grouped(list.triangles).c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Of the full grids of all the chunks, as their point spacing makes them; the holes not taken off")); + ImGui::Text(STR_C("Drawn now: %s"), grouped(drawn).c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Triangles of the chunks drawn around the camera, with the grids made coarser with the distance")); + + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12.0f); + if (ImGui::InputTextWithHint("##chunkfilter", STR_C("Filter: file or x,z"), list.filter, sizeof(list.filter))) + list.built = -1.0; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Read the files again"))) + { + list.scanned = false; + list.built = -1.0; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The files are read when the list opens and after the terrain is saved")); + ImGui::SameLine(); + auto const here{heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z)}; + auto findhere{false}; + if (ImGui::Button(STR_C("Under the camera"))) + { + list.chosen = here; + findhere = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Shows the chunk the camera is over in the list")); + ImGui::TextDisabled("%s", STR_C("Double click: the camera goes to the chunk")); + + auto const flags{ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingFixedFit}; + if (ImGui::BeginTable("##terrainchunks", column_count, flags, ImVec2(0.0f, 0.0f))) + { + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn(STR_C("Chunk"), ImGuiTableColumnFlags_DefaultSort, 0.0f, column_chunk); + ImGui::TableSetupColumn(STR_C("File"), 0, 0.0f, column_file); + ImGui::TableSetupColumn(STR_C("Spacing"), 0, 0.0f, column_spacing); + ImGui::TableSetupColumn(STR_C("Triangles"), ImGuiTableColumnFlags_PreferSortDescending, 0.0f, column_triangles); + ImGui::TableSetupColumn(STR_C("Drawn"), ImGuiTableColumnFlags_PreferSortDescending, 0.0f, column_drawn); + ImGui::TableSetupColumn(STR_C("State"), ImGuiTableColumnFlags_WidthStretch, 0.0f, column_state); + ImGui::TableHeadersRow(); + + if (auto *specs{ImGui::TableGetSortSpecs()}; specs != nullptr && specs->SpecsCount > 0) + { + if (specs->SpecsDirty) + { + list.sortcolumn = specs->Specs[0].ColumnUserID; + list.descending = specs->Specs[0].SortDirection == ImGuiSortDirection_Descending; + specs->SpecsDirty = false; + list.resort = true; + } + } + if (list.resort) + { + // the values which change with the camera are sorted as they were at the time + auto const value = [&](terrain_chunk_row const &Row) -> std::tuple { + switch (list.sortcolumn) + { + case column_file: + { + auto const pack{heightmap::pack_of(Row.key)}; + return {pack.first, pack.second, 0}; + } + case column_spacing: + return {-Row.cells, 0, 0}; + case column_triangles: + return {static_cast(grid_triangles(Row.cells)), 0, 0}; + case column_drawn: + return {static_cast(drawn_of(m_streamer.chunk(Row.key))), 0, 0}; + case column_state: + return {static_cast(state_of(Row, m_streamer.chunk(Row.key))), 0, 0}; + default: + return {0, 0, 0}; + } + }; + std::vector, std::size_t>> order; + order.reserve(list.rows.size()); + for (std::size_t i = 0; i < list.rows.size(); ++i) + order.emplace_back(value(list.rows[i]), i); + auto const descending{list.descending}; + auto const &rows{list.rows}; + std::sort(order.begin(), order.end(), [&](auto const &A, auto const &B) { + if (A.first != B.first) + return descending ? B.first < A.first : A.first < B.first; + // the same value: by the place of the chunk, the same way + auto const &a{rows[A.second].key}, &b{rows[B.second].key}; + return descending ? b < a : a < b; + }); + std::vector sorted; + sorted.reserve(list.rows.size()); + for (auto const &entry : order) + sorted.push_back(list.rows[entry.second]); + list.rows = std::move(sorted); + list.resort = false; + } + + auto const height{ImGui::GetTextLineHeightWithSpacing()}; + ImGuiListClipper clipper; + clipper.Begin(static_cast(list.rows.size())); + if (findhere) + { + auto const found{std::find_if(list.rows.begin(), list.rows.end(), [&](terrain_chunk_row const &Row) { return Row.key == here; })}; + if (found != list.rows.end()) + { + auto const index{static_cast(found - list.rows.begin())}; + clipper.IncludeItemByIndex(index); + ImGui::SetScrollY(std::max(0.0f, index * height - ImGui::GetWindowHeight() * 0.5f)); + } + } + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + auto const &row{list.rows[static_cast(i)]}; + auto const *chunk{m_streamer.chunk(row.key)}; + ImGui::TableNextRow(); + if (row.key == here) + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(90, 140, 220, 60)); + ImGui::TableSetColumnIndex(column_chunk); + auto const chosen{list.chosen && *list.chosen == row.key}; + auto const label{format("%d, %d##chunk%d_%d", row.key.first, row.key.second, row.key.first, row.key.second)}; + if (ImGui::Selectable(label.c_str(), chosen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) + { + list.chosen = row.key; + if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + go_to_terrain_chunk(row.key); + } + ImGui::TableSetColumnIndex(column_file); + ImGui::TextUnformatted(heightmap::pack_file_name(heightmap::pack_of(row.key)).c_str()); + ImGui::TableSetColumnIndex(column_spacing); + if (row.cells > 0) + ImGui::Text("%g m", heightmap::chunk_size / row.cells); + else + ImGui::TextDisabled("?"); + ImGui::TableSetColumnIndex(column_triangles); + ImGui::TextUnformatted(grouped(grid_triangles(row.cells)).c_str()); + ImGui::TableSetColumnIndex(column_drawn); + if (auto const count{drawn_of(chunk)}; count > 0) + ImGui::TextUnformatted(grouped(count).c_str()); + else + ImGui::TextDisabled("-"); + ImGui::TableSetColumnIndex(column_state); + switch (state_of(row, chunk)) + { + case chunk_state::stored: + ImGui::TextDisabled("%s", STR_C("in the file")); + break; + case chunk_state::loaded: + ImGui::TextUnformatted(STR_C("loaded")); + break; + case chunk_state::edited: + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.35f, 1.0f), "%s", STR_C("changed, not saved")); + break; + case chunk_state::added: + ImGui::TextColored(ImVec4(0.45f, 0.9f, 0.45f, 1.0f), "%s", STR_C("new, not saved")); + break; + } + } + } + ImGui::EndTable(); + } + ImGui::End(); +} + +void editor_mode::go_to_terrain_chunk(heightmap::chunk_key const &Key) +{ + auto const corner{heightmap::chunk_corner(Key)}; + auto const half{heightmap::chunk_size * 0.5}; + glm::dvec3 centre{corner.x + half, 0.0, corner.y + half}; + // the height of the ground in the middle; a chunk not in memory is read for it + if (auto const *chunk{m_streamer.chunk(Key, true)}) + centre.y = chunk->height_at(centre.x, centre.z); + else if (double ground{0.0}; m_streamer.height_at(Camera.Pos.x, Camera.Pos.z, ground)) + centre.y = ground; + m_chunklist.chosen = Key; + if (Global.EditorOrtho) + { + // the top view only moves over it + m_focus_active = false; + Camera.Pos.x = centre.x; + Camera.Pos.z = centre.z; + Camera.Velocity = glm::dvec3{0.0}; + Global.pCamera = Camera; + return; + } + // from the side the camera looks from, down at the chunk, from far enough to see the whole of it + auto const yaw{static_cast(Camera.Angle.y)}; + auto const pitch{glm::radians(35.0)}; + glm::dvec3 const look{-std::sin(yaw) * std::cos(pitch), -std::sin(pitch), -std::cos(yaw) * std::cos(pitch)}; + fly_to(centre, look, heightmap::chunk_size * 1.2); +} diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 1344ed1d..0357e32e 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -979,6 +979,46 @@ bool terrain_streamer::exists(chunk_key const &Key) const return pack != m_packs.end() && pack->second.status == pack_state::state::ready && pack->second.file->has(Key); } +std::vector> terrain_streamer::stored_chunks() +{ + std::vector> result; + if (false == m_active) + return result; + std::error_code ec; + std::filesystem::directory_iterator const end; + for (std::filesystem::directory_iterator item{m_directory, ec}; false == static_cast(ec) && item != end; item.increment(ec)) + { + if (item->path().extension() != ".tch") + continue; + auto const filename{item->path().filename().string()}; + heightmap::pack_file file; + std::string error; + std::lock_guard lock{m_filemutex}; + if (false == file.open(m_directory + filename, &error)) + { + ErrorLog("Bad file: " + error, logtype::file); + continue; + } + // the terrain reads a pack only from the file named after it + if (filename != heightmap::pack_file_name(file.key())) + continue; + auto const cells{file.chunk_cells()}; + result.insert(result.end(), cells.begin(), cells.end()); + } + std::sort(result.begin(), result.end()); + return result; +} + +editor_terrain *terrain_streamer::chunk(chunk_key const &Key, bool const Load) +{ + if (false == m_active) + return nullptr; + if (Load) + return load_now(Key); + auto const lookup{m_chunks.find(Key)}; + return lookup != m_chunks.end() ? lookup->second.get() : nullptr; +} + // editing editor_terrain *terrain_streamer::load_now(chunk_key const &Key) diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index a93632dc..ada72b6d 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -79,6 +79,14 @@ class terrain_streamer std::size_t resident() const { return m_chunks.size(); } // chunks in the pack files and in memory, less the removed ones; the files known so far bool exists(chunk_key const &Key) const; + // the chunks of all the pack files in the folder of the terrain, each with the cells along its side (0: unknown), ordered + // by key, as the files are on the disk: the chunks added or removed since the last save are as they were. reads the + // headers of the files, so it's for a list made now and then, not for every frame + std::vector> stored_chunks(); + // removed in the editor, still in the files until saved + bool removed(chunk_key const &Key) const { return m_removed.count(Key) > 0; } + // the chunk in memory, or nullptr; Load: read from its pack file if it isn't in memory yet + editor_terrain *chunk(chunk_key const &Key, bool const Load = false); // editing; the chunks changed are written to the files by save() heightmap::manifest const &manifest() const { return m_manifest; } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 48a1cd6d..4d7a6dd4 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10086,3 +10086,77 @@ msgstr "Zmienia się z porami roku" msgid "How much the colour and the height change through the year with the date of the scenario:\ndry and low in winter, greening up in spring, fresh in May, drying out from August.\n0: the same all year (e.g. a watered lawn), 1: fully" msgstr "Jak bardzo kolor i wysokość zmieniają się w ciągu roku z datą scenariusza:\nzimą sucha i niska, wiosną zielenieje, w maju świeża, od sierpnia wysycha.\n0: taka sama przez cały rok (np. podlewany trawnik), 1: w pełni" + +# scenery editor: the list of the terrain chunks + +msgid "List of the chunks..." +msgstr "Lista kafli..." + +msgid "All the chunks of the terrain with the files they're kept in and their triangles;\na double click on a chunk takes the camera to it" +msgstr "Wszystkie kafle terenu z plikami, w których są zapisane, i ich trójkątami;\ndwuklik na kaflu przenosi do niego kamerę" + +msgid "Terrain chunks" +msgstr "Kafle terenu" + +msgid "The scenery has no heightmap terrain yet" +msgstr "Sceneria nie ma jeszcze terenu z map wysokości" + +msgid "Chunks: %zu in %zu files, %.2f km2" +msgstr "Kafle: %zu w plikach: %zu, %.2f km²" + +msgid "In memory: %zu, changed and not saved: %zu" +msgstr "W pamięci: %zu, zmienione i niezapisane: %zu" + +msgid "Terrain triangles: %s" +msgstr "Trójkąty terenu: %s" + +msgid "Of the full grids of all the chunks, as their point spacing makes them; the holes not taken off" +msgstr "Pełnych siatek wszystkich kafli, jakie daje ich odstęp punktów; dziury nie są odejmowane" + +msgid "Drawn now: %s" +msgstr "Rysowane teraz: %s" + +msgid "Triangles of the chunks drawn around the camera, with the grids made coarser with the distance" +msgstr "Trójkąty kafli rysowanych wokół kamery, z siatkami rzadszymi wraz z odległością" + +msgid "Filter: file or x,z" +msgstr "Filtr: plik lub x,z" + +msgid "Read the files again" +msgstr "Wczytaj pliki ponownie" + +msgid "The files are read when the list opens and after the terrain is saved" +msgstr "Pliki są czytane przy otwarciu listy i po zapisaniu terenu" + +msgid "Under the camera" +msgstr "Pod kamerą" + +msgid "Shows the chunk the camera is over in the list" +msgstr "Pokazuje na liście kafel, nad którym jest kamera" + +msgid "Double click: the camera goes to the chunk" +msgstr "Dwuklik: kamera przenosi się do kafla" + +msgid "Chunk" +msgstr "Kafel" + +msgid "Triangles" +msgstr "Trójkąty" + +msgid "Drawn" +msgstr "Rysowane" + +msgid "State" +msgstr "Stan" + +msgid "in the file" +msgstr "w pliku" + +msgid "loaded" +msgstr "wczytany" + +msgid "changed, not saved" +msgstr "zmieniony, niezapisany" + +msgid "new, not saved" +msgstr "nowy, niezapisany" diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp index 434aa1e6..e76e8cc5 100644 --- a/scene/heightmapterrain.cpp +++ b/scene/heightmapterrain.cpp @@ -156,6 +156,29 @@ std::uint32_t checksum(std::vector const &Data) return scene::terrain_file::checksum(reinterpret_cast(Data.data()), Data.size()); } +// reads the data of a slice of a pack file, unpacked and checked +template bool load_slice(std::ifstream &File, Slice_ const &Slice, std::vector &Out) +{ + File.clear(); + std::vector stored(Slice.stored); + File.seekg(static_cast(Slice.offset)); + File.read(reinterpret_cast(stored.data()), stored.size()); + if (File.gcount() != static_cast(stored.size())) + return false; + if (Slice.codec == codec_stored) + Out = std::move(stored); + else if (Slice.codec == codec_zstd) + { + Out.resize(Slice.size); + auto const result{ZSTD_decompress(Out.data(), Out.size(), stored.data(), stored.size())}; + if (ZSTD_isError(result) || result != Slice.size) + return false; + } + else + return false; + return Out.size() == Slice.size && checksum(Out) == Slice.crc; +} + int floor_div(int const Value, int const Divisor) { return (Value >= 0 ? Value / Divisor : -((-Value + Divisor - 1) / Divisor)); @@ -980,6 +1003,36 @@ std::vector pack_file::chunks() const return result; } +std::vector> pack_file::chunk_cells() const +{ + std::vector> result; + std::ifstream file{m_path, std::ios::binary}; + for (int z = 0; z < pack_side; ++z) + for (int x = 0; x < pack_side; ++x) + { + auto const &slices{m_slices[static_cast(z) * pack_side + x]}; + if (slices.empty()) + continue; + int cells{0}; + for (auto const &slice : slices) + { + if (slice.tag != tag_chunk) + continue; + std::vector data; + std::uint16_t count{0}; + if (file.is_open() && load_slice(file, slice, data)) + { + reader input{data.data(), data.size()}; + if (input.get(count)) + cells = count; + } + break; + } + result.emplace_back(chunk_key{m_key.first * pack_side + x, m_key.second * pack_side + z}, cells); + } + return result; +} + std::shared_ptr pack_file::read(chunk_key const &Key, std::string *Error) const { auto const fail = [&](std::string const &Message) -> std::shared_ptr { @@ -994,25 +1047,7 @@ std::shared_ptr pack_file::read(chunk_key const &Key, std::string *E if (false == file.good()) return fail("can't open the file"); - auto const load = [&](slice const &Slice, std::vector &Out) { - std::vector stored(Slice.stored); - file.seekg(static_cast(Slice.offset)); - file.read(reinterpret_cast(stored.data()), stored.size()); - if (file.gcount() != static_cast(stored.size())) - return false; - if (Slice.codec == codec_stored) - Out = std::move(stored); - else if (Slice.codec == codec_zstd) - { - Out.resize(Slice.size); - auto const result{ZSTD_decompress(Out.data(), Out.size(), stored.data(), stored.size())}; - if (ZSTD_isError(result) || result != Slice.size) - return false; - } - else - return false; - return Out.size() == Slice.size && checksum(Out) == Slice.crc; - }; + auto const load = [&](slice const &Slice, std::vector &Out) { return load_slice(file, Slice, Out); }; auto chunk{std::make_shared()}; chunk->key = Key; diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h index 0dacc292..cd131dc9 100644 --- a/scene/heightmapterrain.h +++ b/scene/heightmapterrain.h @@ -271,6 +271,8 @@ class pack_file bool has(chunk_key const &Key) const; // chunks the file holds std::vector chunks() const; + // the same, each with the cells along a side of the chunk, read from its header alone; 0 where the header can't be read + std::vector> chunk_cells() const; // reads specified chunk. returns: the chunk, or null on failure std::shared_ptr read(chunk_key const &Key, std::string *Error = nullptr) const; pack_key key() const { return m_key; }