17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 00:29:19 +02:00
Files
maszyna/scene/sceneterrain.cpp
Hirek193 8cd0bab27b Editor terrain: texture browser, undo, conversion from terrain files
- Terrain work mode shows a browser of the textures folder in place of the
  node bank; textures/materials can be dragged onto the palette entries
  (replacing the material), the material field and the water material field
- Ctrl+Z/Ctrl+Y for heightmap terrain changes: a brush stroke or an
  operation is one step of the editor history
- Ground conversion reads only the files of the terrain directives
  (.txtf with origin/rotate, or .btf without text), paints the chunks with
  the materials of the triangles, drops only the shapes of those files
- Fix: ground grids converted out-of-range floats to int, so whole-scene
  queries returned the triangles of a single cell (shifted chunks)
- Fix: terrain layers copied from textures already uploaded without their
  data stayed grey
- Fix: ImGui ID conflict between terrain tool radios and section headers
- Fix: layers of terrain .txtf files named with the scenery/ prefix and
  could not be saved
- Editor selftest commands for the terrain
2026-10-09 23:39:26 +02:00

1168 lines
40 KiB
C++

/*
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/.
*/
#include "stdafx.h"
#include "scene/sceneterrain.h"
#include "scene/scene.h"
#include "rendering/renderer.h"
#include "rendering/nullrenderer.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/parser.h"
#include "utilities/utilities.h"
#include <bit>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <sstream>
#include <stack>
#include <unordered_map>
/*
binary terrain file layout, all values little endian:
uint32 file id ('EU07')
uint32 format id ('BTF1')
uint64 length of the source text file
uint32 checksum (crc-32) of the source text file
uint32 section size, uint32 cell size, uint32 number of sections along the side of the region
uint32 material count
uint32 section count
uint64 vertex count
material names, null-terminated
section directory, per section:
uint32 section index, uint64 offset of the section data in the file, uint32 size of the section data, float32 bounding radius of the section
section data, per section:
uint32 block id ('BTFS')
uint32 section index
uint32 shape count
per shape:
uint32 material index
uint32 index of the section cell the shape is located in
float64 squared minimum and maximum visibility range
float32 x12 diffuse, ambient and specular colours
float64 x3 centre, float32 bounding radius
uint32 vertex count
per vertex: float32 x3 position relative to the centre of the section, float32 x3 normal, float32 x2 texture coordinates
the shapes are the ones the scenery loader would put in the section cells for the same text. where the loader would merge them
further into section-wide pieces depends on the materials, which are known only at run time, so that part is left for loading.
*/
namespace scene
{
// defined along with the scenery loader code which filters out legacy switch trackbeds
extern std::vector<std::string> switchtrackbedtextures;
// data shared by the pieces of a binary terrain file put to use
struct terrain_source
{
struct material
{
material_handle handle{null_handle};
bool translucent{false};
bool clamps{false}; // texture coordinates are kept off the edges of the texture
bool clampt{false};
bool skip{false}; // geometry using the material isn't loaded
};
std::string file;
std::vector<material> materials;
std::uint16_t reference{0}; // see terrain_file::reference_of()
};
namespace
{
std::uint32_t const EU07_FILEHEADER{MAKE_ID4('E', 'U', '0', '7')};
std::uint32_t const EU07_FILEVERSION_TERRAIN{MAKE_ID4('B', 'T', 'F', '2')};
std::uint32_t const EU07_TERRAINBLOCK{MAKE_ID4('B', 'T', 'F', 'S')};
std::size_t const EU07_TERRAINVERTEXSIZE{8 * sizeof(float)};
struct file_header
{
std::uint64_t sourcesize{0};
std::uint32_t sourcechecksum{0};
std::uint32_t materialcount{0};
std::uint32_t sectioncount{0};
std::uint64_t vertexcount{0};
std::uint64_t extraoffset{0};
std::uint32_t extrasize{0};
};
// adds value to the data in the byte order of the file
template <typename Type_> void put(std::string &Output, Type_ Value)
{
char bytes[sizeof(Type_)];
std::memcpy(bytes, &Value, sizeof(Type_));
if (std::endian::native != std::endian::little)
{
std::reverse(std::begin(bytes), std::end(bytes));
}
Output.append(bytes, sizeof(Type_));
}
// extracts values in the byte order of the file from a block of data, keeping track of its bounds
class data_reader
{
public:
data_reader(char const *Data, std::size_t const Size) : m_data(Data), m_size(Size) {}
template <typename Type_> Type_ get()
{
Type_ value{};
if (false == has(sizeof(Type_)))
{
m_good = false;
m_position = m_size;
return value;
}
char bytes[sizeof(Type_)];
std::memcpy(bytes, m_data + m_position, sizeof(Type_));
if (std::endian::native != std::endian::little)
{
std::reverse(std::begin(bytes), std::end(bytes));
}
std::memcpy(&value, bytes, sizeof(Type_));
m_position += sizeof(Type_);
return value;
}
std::string get_string()
{
std::string text;
while (has(1) && m_data[m_position] != 0)
{
text += m_data[m_position++];
}
if (false == has(1))
{
m_good = false;
return text;
}
++m_position; // terminator
return text;
}
// extracts a series of values at once. NOTE: the caller is expected to check the data is there
void get(float *Values, std::size_t const Count)
{
if (false == has(Count * sizeof(float)))
{
m_good = false;
m_position = m_size;
return;
}
if (std::endian::native == std::endian::little)
{
std::memcpy(Values, m_data + m_position, Count * sizeof(float));
m_position += Count * sizeof(float);
}
else
{
for (std::size_t index{0}; index < Count; ++index)
{
Values[index] = get<float>();
}
}
}
void skip(std::size_t const Size)
{
if (false == has(Size))
{
m_good = false;
m_position = m_size;
return;
}
m_position += Size;
}
bool has(std::size_t const Size) const
{
return Size <= m_size - m_position;
}
bool good() const
{
return m_good;
}
private:
char const *m_data;
std::size_t m_size;
std::size_t m_position{0};
bool m_good{true};
};
// loads content of specified file into provided string. returns: true on success, false otherwise
bool read_file(std::string const &Filename, std::string &Data)
{
std::ifstream input{Filename, std::ios::binary};
if (input.fail())
{
return false;
}
auto const size{input.rdbuf()->pubseekoff(0, std::ios_base::end)};
input.rdbuf()->pubseekoff(0, std::ios_base::beg);
if (size < 0 || static_cast<std::uintmax_t>(size) >= Data.max_size())
{
return false;
}
Data.resize(static_cast<std::size_t>(size));
input.read(Data.data(), static_cast<std::streamsize>(Data.size()));
return static_cast<std::size_t>(std::max<std::streamsize>(0, input.gcount())) == Data.size();
}
// loads the fixed part of binary terrain file header. returns: true if the file can be used, false otherwise
bool read_header(std::istream &Input, file_header &Header)
{
char bytes[60];
Input.read(bytes, sizeof(bytes));
if (Input.gcount() != static_cast<std::streamsize>(sizeof(bytes)))
{
return false;
}
data_reader data{bytes, sizeof(bytes)};
if (data.get<std::uint32_t>() != EU07_FILEHEADER || data.get<std::uint32_t>() != EU07_FILEVERSION_TERRAIN)
{
// wrong file type
return false;
}
Header.sourcesize = data.get<std::uint64_t>();
Header.sourcechecksum = data.get<std::uint32_t>();
// the content is arranged for specific layout of the scene, and can't be used with any other
if (data.get<std::uint32_t>() != static_cast<std::uint32_t>(EU07_SECTIONSIZE) || data.get<std::uint32_t>() != static_cast<std::uint32_t>(EU07_CELLSIZE) ||
data.get<std::uint32_t>() != static_cast<std::uint32_t>(EU07_REGIONSIDESECTIONCOUNT))
{
return false;
}
Header.materialcount = data.get<std::uint32_t>();
Header.sectioncount = data.get<std::uint32_t>();
Header.vertexcount = data.get<std::uint64_t>();
Header.extraoffset = data.get<std::uint64_t>();
Header.extrasize = data.get<std::uint32_t>();
return data.good();
}
bool read_header(std::string const &Filename, file_header &Header)
{
std::ifstream input{Filename, std::ios::binary};
return (false == input.fail()) && read_header(input, Header);
}
// stand-in for the renderer used during conversion of text terrain. the conversion doesn't need
// anything of the materials but a way to tell them apart, so it goes by their names alone
class conversion_renderer : public null_renderer
{
public:
material_handle Fetch_Material(std::string const &Filename, bool const Loadnow = true) override
{
// the names are brought to the form the material manager would look them up by
auto name{Filename};
if (name.find("make:") != 0 && name.find("internal_src:") != 0)
{
if (contains(name, '|'))
{
name.erase(name.find('|'));
}
erase_extension(name);
replace_slashes(name);
erase_leading_slashes(name);
}
auto const lookup{m_lookup.find(name)};
if (lookup != m_lookup.end())
{
return lookup->second;
}
m_materials.emplace_back(std::make_unique<opengl_material>());
m_materials.back()->name = name;
m_names.emplace_back(name);
m_lookup.emplace(name, static_cast<material_handle>(m_materials.size()));
return static_cast<material_handle>(m_materials.size());
}
IMaterial const *Material(material_handle const Material) const override
{
return (Material > 0 && static_cast<std::size_t>(Material) <= m_materials.size() ? m_materials[Material - 1].get() : IMaterial::null_material());
}
// names of the materials, in the order of their handles, starting with handle 1
std::vector<std::string> const &names() const
{
return m_names;
}
private:
std::vector<std::unique_ptr<opengl_material>> m_materials;
std::vector<std::string> m_names;
std::unordered_map<std::string, material_handle> m_lookup;
};
// puts the stand-in renderer in place of the real one for as long as the object lives.
// the conversion is done by the code which loads scenery geometry from text, and that code asks the global renderer for the materials
class conversion_scope
{
public:
conversion_scope() : m_trackbeds(Global.CreateSwitchTrackbeds)
{
auto renderer{std::make_unique<conversion_renderer>()};
m_materials = renderer.get();
m_renderer = std::move(renderer);
std::swap(GfxRenderer, m_renderer);
// the binary file holds everything the text does. geometry of legacy switch trackbeds is filtered out on load, as that depends on the settings
Global.CreateSwitchTrackbeds = false;
}
~conversion_scope()
{
std::swap(GfxRenderer, m_renderer);
Global.CreateSwitchTrackbeds = m_trackbeds;
}
conversion_scope(conversion_scope const &) = delete;
conversion_scope &operator=(conversion_scope const &) = delete;
std::vector<std::string> const &materials() const
{
return m_materials->names();
}
private:
std::unique_ptr<gfx_renderer> m_renderer;
conversion_renderer *m_materials{nullptr};
bool m_trackbeds;
};
// collects what the loader needs to know about the material with specified name
terrain_source::material fetch_material(std::string const &Name)
{
terrain_source::material material;
material.handle = GfxRenderer->Fetch_Material(Name);
// the appearance is determined from the assigned diffuse texture, the same way it's done for the geometry loaded from text
auto const texturehandle = material.handle != null_handle ? GfxRenderer->Material(material.handle)->GetTexture(0) : null_handle;
auto const &texture = texturehandle ? GfxRenderer->Texture(texturehandle) : *ITexture::null_texture();
material.clamps = texturehandle ? contains(texture.get_traits(), 's') : false;
material.clampt = texturehandle ? contains(texture.get_traits(), 't') : false;
material.translucent = (texturehandle != null_handle) && contains(texture.get_name(), '@') && (true == texture.get_has_alpha());
if (Global.CreateSwitchTrackbeds)
{
// geometry with blacklisted texture, part of old switch trackbed
auto const materialname{GfxRenderer->Material(material.handle)->GetName()};
material.skip =
std::any_of(std::begin(switchtrackbedtextures), std::end(switchtrackbedtextures), [&](std::string const &Texture) { return contains(materialname, Texture); });
}
return material;
}
} // namespace
// calculates checksum (crc-32) of provided data
std::uint32_t terrain_file::checksum(char const *Data, std::size_t Size)
{
// lookup for 8 bytes of input at a time
static auto const tables{[]() {
std::array<std::array<std::uint32_t, 256>, 8> lookup{};
for (std::uint32_t index{0}; index < 256; ++index)
{
auto value{index};
for (auto bit{0}; bit < 8; ++bit)
{
value = (value & 1 ? 0xEDB88320u ^ (value >> 1) : value >> 1);
}
lookup[0][index] = value;
}
for (std::uint32_t index{0}; index < 256; ++index)
{
for (std::size_t table{1}; table < lookup.size(); ++table)
{
lookup[table][index] = (lookup[table - 1][index] >> 8) ^ lookup[0][lookup[table - 1][index] & 0xFF];
}
}
return lookup;
}()};
auto const *data{reinterpret_cast<unsigned char const *>(Data)};
std::uint32_t crc{0xFFFFFFFFu};
while (Size >= 8)
{
auto const low{crc ^ (static_cast<std::uint32_t>(data[0]) | (static_cast<std::uint32_t>(data[1]) << 8) | (static_cast<std::uint32_t>(data[2]) << 16) | (static_cast<std::uint32_t>(data[3]) << 24))};
crc = tables[7][low & 0xFF] ^ tables[6][(low >> 8) & 0xFF] ^ tables[5][(low >> 16) & 0xFF] ^ tables[4][low >> 24] ^ tables[3][data[4]] ^ tables[2][data[5]] ^ tables[1][data[6]] ^ tables[0][data[7]];
data += 8;
Size -= 8;
}
while (Size-- > 0)
{
crc = tables[0][(crc ^ *data++) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFu;
}
// converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise
bool terrain_file::write(std::string const &Text, std::string const &Binaryfile, std::string &Message)
{
conversion_scope scope;
auto region{std::make_unique<basic_region>()};
scratch_data scratchpad{};
std::string extra;
{
std::size_t begin{0};
while (begin < Text.size())
{
auto end{Text.find('\n', begin)};
end = (end == std::string::npos ? Text.size() : end + 1);
auto const first{Text.find_first_not_of(" \t", begin)};
if (first < end && Text.compare(first, 3, "//$") == 0)
{
auto line{Text.substr(first, end - first)};
while (false == line.empty() && (line.back() == '\n' || line.back() == '\r'))
line.pop_back();
extra += line + "\n";
}
begin = end;
}
}
{
cParser input{Text, cParser::buffer_TEXT};
// content of the files the text would include isn't covered by its checksum, so there can't be any
input.expandIncludes = false;
auto token{input.getToken<std::string>()};
while (false == token.empty())
{
if (token == "node")
{
auto const begin{static_cast<std::size_t>(input.TokenBegin())};
node_data nodedata;
input.getTokens(4);
input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type;
if (nodedata.type == "sweep")
{
auto rest{input.getToken<std::string>()};
while (false == rest.empty() && rest != "endsweep")
rest = input.getToken<std::string>();
auto const end{static_cast<std::size_t>(input.TokenEnd())};
auto const offset{scratchpad.location.offset.empty() ? glm::dvec3{0.0} : scratchpad.location.offset.top()};
auto const placed{offset != glm::dvec3{0.0}};
if (placed)
extra += "origin " + std::to_string(offset.x) + ' ' + std::to_string(offset.y) + ' ' + std::to_string(offset.z) + "\n";
extra += Text.substr(begin, end - begin) + "\n";
if (placed)
extra += "endorigin\n";
token = input.getToken<std::string>();
continue;
}
if (nodedata.type != "triangles" && nodedata.type != "triangle_strip" && nodedata.type != "triangle_fan")
{
Message = "node of type \"" + nodedata.type + "\" (line " + std::to_string(input.Line()) + ") can't be a part of binary terrain";
return false;
}
auto shape{shape_node().import(input, nodedata)};
// the shapes are all sent to the section cells, which groups them the way it'd be done for translucent geometry.
// whether they can be merged into section-wide pieces instead is decided when the file is loaded, see the layout notes
shape.m_data.translucent = true;
region->insert(shape, scratchpad, true);
}
else if (token == "origin")
{
glm::dvec3 offset;
input.getTokens(3);
input >> offset.x >> offset.y >> offset.z;
scratchpad.location.offset.emplace(offset + (scratchpad.location.offset.empty() ? glm::dvec3() : scratchpad.location.offset.top()));
}
else if (token == "endorigin")
{
if (false == scratchpad.location.offset.empty())
{
scratchpad.location.offset.pop();
}
}
else if (token == "rotate")
{
input.getTokens(3);
input >> scratchpad.location.rotation.x >> scratchpad.location.rotation.y >> scratchpad.location.rotation.z;
}
else
{
Message = "\"" + token + "\" (line " + std::to_string(input.Line()) + ") can't be a part of binary terrain";
return false;
}
token = input.getToken<std::string>();
}
}
// sections holding any geometry
std::vector<std::uint32_t> sections;
for (std::size_t index{0}; index < region->m_sections.size(); ++index)
{
auto const *section{region->m_sections[index]};
if (section == nullptr)
{
continue;
}
if (std::any_of(std::begin(section->m_cells), std::end(section->m_cells), [](basic_cell const &Cell) { return false == Cell.m_shapestranslucent.empty(); }))
{
sections.emplace_back(static_cast<std::uint32_t>(index));
}
}
auto const temporaryfile{Binaryfile + ".tmp"};
std::uint64_t shapecount{0};
std::uint64_t vertexcount{0};
{
std::ofstream output{temporaryfile, std::ios::binary | std::ios::trunc};
if (output.fail())
{
Message = "can't write to file \"" + temporaryfile + "\"";
return false;
}
std::string data;
put(data, EU07_FILEHEADER);
put(data, EU07_FILEVERSION_TERRAIN);
put(data, static_cast<std::uint64_t>(Text.size()));
put(data, terrain_file::checksum(Text.data(), Text.size()));
put(data, static_cast<std::uint32_t>(EU07_SECTIONSIZE));
put(data, static_cast<std::uint32_t>(EU07_CELLSIZE));
put(data, static_cast<std::uint32_t>(EU07_REGIONSIDESECTIONCOUNT));
put(data, static_cast<std::uint32_t>(scope.materials().size()));
put(data, static_cast<std::uint32_t>(sections.size()));
auto const vertexcountposition{data.size()};
put(data, vertexcount); // filled in when the count is known
auto const extraposition{data.size()};
put(data, std::uint64_t{0});
put(data, std::uint32_t{0});
for (auto const &material : scope.materials())
{
data.append(material.c_str(), material.size() + 1);
}
auto const directoryposition{data.size()};
// the directory is written once the section data is in place and its location known
std::size_t const directoryentrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)};
data.append(sections.size() * directoryentrysize, '\0');
output.write(data.data(), static_cast<std::streamsize>(data.size()));
std::uint64_t offset{data.size()};
std::string directory;
for (auto const sectionindex : sections)
{
auto *section{region->m_sections[sectionindex]};
data.clear();
put(data, EU07_TERRAINBLOCK);
put(data, sectionindex);
std::uint32_t sectionshapecount{0};
for (auto const &cell : section->m_cells)
{
sectionshapecount += static_cast<std::uint32_t>(cell.m_shapestranslucent.size());
}
put(data, sectionshapecount);
for (std::size_t cellindex{0}; cellindex < section->m_cells.size(); ++cellindex)
{
for (auto &shape : section->m_cells[cellindex].m_shapestranslucent)
{
auto const radius{shape.radius()};
auto const &shapedata{shape.data()};
put(data, static_cast<std::uint32_t>(shapedata.material - 1));
put(data, static_cast<std::uint32_t>(cellindex));
put(data, shapedata.rangesquared_min);
put(data, shapedata.rangesquared_max);
for (auto const &colour : {shapedata.lighting.diffuse, shapedata.lighting.ambient, shapedata.lighting.specular})
{
put(data, colour.r);
put(data, colour.g);
put(data, colour.b);
put(data, colour.a);
}
put(data, shapedata.area.center.x);
put(data, shapedata.area.center.y);
put(data, shapedata.area.center.z);
put(data, radius);
put(data, static_cast<std::uint32_t>(shapedata.vertices.size()));
for (auto const &vertex : shapedata.vertices)
{
glm::vec3 const position{vertex.position - section->m_area.center};
put(data, position.x);
put(data, position.y);
put(data, position.z);
put(data, vertex.normal.x);
put(data, vertex.normal.y);
put(data, vertex.normal.z);
put(data, vertex.texture.x);
put(data, vertex.texture.y);
}
++shapecount;
vertexcount += shapedata.vertices.size();
}
}
if (data.size() > std::numeric_limits<std::uint32_t>::max())
{
Message = "too much geometry in a single section of the scene";
output.close();
std::error_code error;
std::filesystem::remove(temporaryfile, error);
return false;
}
output.write(data.data(), static_cast<std::streamsize>(data.size()));
put(directory, sectionindex);
put(directory, offset);
put(directory, static_cast<std::uint32_t>(data.size()));
put(directory, section->m_area.radius);
offset += data.size();
}
output.write(extra.data(), static_cast<std::streamsize>(extra.size()));
data.clear();
put(data, static_cast<std::uint64_t>(offset));
put(data, static_cast<std::uint32_t>(extra.size()));
output.seekp(static_cast<std::streamoff>(extraposition));
output.write(data.data(), static_cast<std::streamsize>(data.size()));
// fill in the parts left for later
output.seekp(static_cast<std::streamoff>(directoryposition));
output.write(directory.data(), static_cast<std::streamsize>(directory.size()));
data.clear();
put(data, vertexcount);
output.seekp(static_cast<std::streamoff>(vertexcountposition));
output.write(data.data(), static_cast<std::streamsize>(data.size()));
output.flush();
if (output.fail())
{
Message = "can't write to file \"" + temporaryfile + "\"";
output.close();
std::error_code error;
std::filesystem::remove(temporaryfile, error);
return false;
}
}
// the finished file replaces the previous version in one go, so an interrupted conversion doesn't leave a broken one behind
std::error_code error;
std::filesystem::rename(temporaryfile, Binaryfile, error);
if (error)
{
Message = "can't write to file \"" + Binaryfile + "\"";
std::filesystem::remove(temporaryfile, error);
return false;
}
Message = std::to_string(sections.size()) + " section(s), " + std::to_string(shapecount) + " shape(s), " + std::to_string(vertexcount / 3) + " triangle(s)";
return true;
}
// converts specified text terrain file to binary format. returns: true on success, false otherwise
bool terrain_file::convert(std::string const &Textfile, std::string const &Binaryfile, std::string *Message)
{
std::string message;
std::string text;
auto result{false};
if (read_file(Textfile, text))
{
result = write(text, Binaryfile, message);
}
else
{
message = "can't read the file";
}
if (Message != nullptr)
{
*Message = message;
}
return result;
}
// makes sure binary version of specified text terrain file is in place and matches the text, as far as the settings ask for it
terrain_file::state terrain_file::prepare(std::string const &Textfile, std::string const &Binaryfile)
{
file_header header;
auto const binarypresent{read_header(Binaryfile, header)};
std::error_code error;
auto const textsize{std::filesystem::file_size(Textfile, error)};
if (error)
{
// with no text to check the binary file against it's taken as it is
return (binarypresent ? state::binary : state::missing);
}
// how far the check goes is up to the settings (file.binary.terrain.checksum): none at all, length of the text, or its length and checksum
if (binarypresent && Global.file_binary_terrain_checksum <= 0)
{
return state::binary;
}
std::string text;
auto textloaded{false};
if (binarypresent && (header.sourcesize == textsize))
{
if (Global.file_binary_terrain_checksum == 1)
{
// NOTE: this takes a text changed without a change of its length for the one the binary file was made from
return state::binary;
}
// the length is the same; the checksum tells whether the content is, too
textloaded = read_file(Textfile, text);
if (textloaded && (text.size() == header.sourcesize) && (checksum(text.data(), text.size()) == header.sourcechecksum))
{
return state::binary;
}
}
if (false == textloaded && false == read_file(Textfile, text))
{
ErrorLog("Bad file: can't read terrain file \"" + Textfile + "\"");
// NOTE: binary file which doesn't match the text isn't used, the scenery is expected to show what its files say
return state::missing;
}
WriteLog("Terrain file \"" + Textfile + "\": " + (binarypresent ? "binary version doesn't match the text" : "no usable binary version found") + ", converting...");
std::string message;
if (write(text, Binaryfile, message))
{
WriteLog("Terrain file \"" + Textfile + "\": conversion done, " + message);
return state::binary;
}
ErrorLog("Bad file: terrain file \"" + Textfile + "\" wasn't converted to binary format, " + message + ". The file will be loaded as text");
if (binarypresent)
{
// the binary file left behind is of no use, and would be taken for the real thing if the text went missing
std::filesystem::remove(Binaryfile, error);
}
return state::text;
}
std::string terrain_file::extra(std::string const &Binaryfile)
{
std::ifstream input{Binaryfile, std::ios::binary};
file_header header;
if (input.fail() || false == read_header(input, header) || header.extrasize == 0)
{
return {};
}
std::string text(header.extrasize, '\0');
input.seekg(static_cast<std::streamoff>(header.extraoffset));
input.read(text.data(), static_cast<std::streamsize>(text.size()));
if (static_cast<std::size_t>(std::max<std::streamsize>(0, input.gcount())) != text.size())
{
ErrorLog("Bad file: \"" + Binaryfile + "\" is damaged, the editor data it holds can't be read");
return {};
}
return text;
}
std::vector<terrain_reference> &terrain_file::references()
{
static std::vector<terrain_reference> references;
return references;
}
std::uint16_t terrain_file::reference_of(std::string const &File)
{
auto file{File};
replace_slashes(file);
auto const &list{references()};
for (std::size_t index{0}; index < list.size() && index < std::numeric_limits<std::uint16_t>::max(); ++index)
{
auto const base{Global.asCurrentSceneryPath + list[index].file};
if (file.ends_with(base + ".txtf") || file.ends_with(base + ".btf"))
{
return static_cast<std::uint16_t>(index + 1);
}
}
return 0;
}
namespace
{
// world position of a vertex of a shape placed with specified rotation and offset, the way basic_region::insert() does it
glm::dvec3 place_vertex(glm::dvec3 Position, glm::vec3 const &Rotation, glm::dvec3 const &Offset)
{
if (Rotation != glm::vec3(0.f))
{
auto const rotation{glm::radians(glm::dvec3{Rotation})};
Position = glm::rotateZ<double>(Position, rotation.z);
Position = glm::rotateX<double>(Position, rotation.x);
Position = glm::rotateY<double>(Position, rotation.y);
}
return Position + Offset;
}
bool read_text_triangles(std::string const &Text, terrain_reference const &Reference, terrain_file::triangle_visitor const &Visit, std::string &Message)
{
conversion_scope scope;
std::stack<glm::dvec3> offsets;
offsets.push(Reference.offset);
auto rotation{Reference.rotation};
std::size_t count{0};
cParser input{Text, cParser::buffer_TEXT};
input.expandIncludes = false;
auto token{input.getToken<std::string>()};
while (false == token.empty())
{
if (token == "node")
{
node_data nodedata;
input.getTokens(4);
input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type;
if (nodedata.type == "triangles" || nodedata.type == "triangle_strip" || nodedata.type == "triangle_fan")
{
auto const shape{shape_node().import(input, nodedata)};
auto const &data{shape.data()};
auto const *material{GfxRenderer->Material(data.material)};
auto const name{material != nullptr ? material->GetName() : std::string{}};
for (std::size_t index{0}; index + 2 < data.vertices.size(); index += 3)
{
Visit(name, {place_vertex(data.vertices[index].position, rotation, offsets.top()), place_vertex(data.vertices[index + 1].position, rotation, offsets.top()),
place_vertex(data.vertices[index + 2].position, rotation, offsets.top())});
++count;
}
}
else
{
// anything else the file holds (the editor keeps sweeps there) is of no interest here
auto const end{"end" + nodedata.type};
auto rest{input.getToken<std::string>()};
while (false == rest.empty() && rest != end)
rest = input.getToken<std::string>();
}
}
else if (token == "origin")
{
glm::dvec3 offset;
input.getTokens(3);
input >> offset.x >> offset.y >> offset.z;
offsets.push(offset + offsets.top());
}
else if (token == "endorigin")
{
if (offsets.size() > 1)
{
offsets.pop();
}
}
else if (token == "rotate")
{
input.getTokens(3);
input >> rotation.x >> rotation.y >> rotation.z;
}
token = input.getToken<std::string>();
}
Message = std::to_string(count) + " triangle(s)";
return true;
}
bool read_binary_triangles(std::string const &Binaryfile, terrain_file::triangle_visitor const &Visit, std::string &Message)
{
std::string content;
if (false == read_file(Binaryfile, content))
{
Message = "can't read the file";
return false;
}
std::istringstream stream{content};
file_header header;
if (false == read_header(stream, header))
{
Message = "the file is of either unrecognized type or version";
return false;
}
std::vector<std::string> materials;
for (std::uint32_t index{0}; index < header.materialcount; ++index)
{
std::string name;
if (false == static_cast<bool>(std::getline(stream, name, '\0')))
{
Message = "the file is damaged";
return false;
}
materials.emplace_back(std::move(name));
}
auto const directorystart{static_cast<std::size_t>(stream.tellg())};
std::size_t const entrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)};
if (directorystart > content.size() || content.size() - directorystart < header.sectioncount * entrysize)
{
Message = "the file is damaged";
return false;
}
data_reader directory{content.data() + directorystart, header.sectioncount * entrysize};
std::size_t count{0};
for (std::uint32_t entry{0}; entry < header.sectioncount; ++entry)
{
auto const sectionindex{directory.get<std::uint32_t>()};
auto const offset{directory.get<std::uint64_t>()};
auto const size{directory.get<std::uint32_t>()};
directory.get<float>();
if (false == directory.good() || offset > content.size() || content.size() - offset < size)
{
Message = "the file is damaged";
return false;
}
// the vertices are relative to the centre of their section, see attach()
auto const column{static_cast<int>(sectionindex % EU07_REGIONSIDESECTIONCOUNT)};
auto const row{static_cast<int>(sectionindex / EU07_REGIONSIDESECTIONCOUNT)};
auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2};
glm::dvec3 const centre{static_cast<double>(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast<double>(centeroffset + row * EU07_SECTIONSIZE)};
data_reader reader{content.data() + offset, size};
if (reader.get<std::uint32_t>() != EU07_TERRAINBLOCK)
{
Message = "the file is damaged";
return false;
}
reader.get<std::uint32_t>(); // section index
auto shapecount{reader.get<std::uint32_t>()};
while (shapecount-- > 0 && reader.good())
{
auto const materialindex{reader.get<std::uint32_t>()};
// cell, ranges, colours, centre and radius
reader.skip(sizeof(std::uint32_t) + 2 * sizeof(double) + 12 * sizeof(float) + 3 * sizeof(double) + sizeof(float));
auto const vertexcount{reader.get<std::uint32_t>()};
if (false == reader.good() || false == reader.has(static_cast<std::size_t>(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= materials.size())
{
Message = "the file is damaged";
return false;
}
std::array<glm::dvec3, 3> triangle;
for (std::uint32_t vertex{0}; vertex < vertexcount; ++vertex)
{
float values[8];
reader.get(values, 8);
triangle[vertex % 3] = centre + glm::dvec3{values[0], values[1], values[2]};
if (vertex % 3 == 2)
{
Visit(materials[materialindex], triangle);
++count;
}
}
}
if (false == reader.good())
{
Message = "the file is damaged";
return false;
}
}
Message = std::to_string(count) + " triangle(s)";
return true;
}
} // namespace
bool terrain_file::read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message, bool *Text)
{
auto const textfile{Global.asCurrentSceneryPath + Reference.file + ".txtf"};
auto const binaryfile{Global.asCurrentSceneryPath + Reference.file + ".btf"};
std::string message;
auto result{false};
auto const fromtext{FileExists(textfile)};
if (fromtext)
{
std::string text;
result = read_file(textfile, text) && read_text_triangles(text, Reference, Visit, message);
if (message.empty())
message = "can't read the file";
}
else if (FileExists(binaryfile))
{
// the binary file holds the geometry where the text alone puts it; it stands in for files placed without offset or rotation only
result = read_binary_triangles(binaryfile, Visit, message);
}
else
{
message = "the file is missing";
}
if (Message != nullptr)
*Message = message;
if (Text != nullptr)
*Text = fromtext;
return result;
}
// makes content of specified binary terrain file a part of provided region
bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred)
{
std::ifstream input{Binaryfile, std::ios::binary};
file_header header;
if (input.fail() || false == read_header(input, header))
{
ErrorLog("Bad file: \"" + Binaryfile + "\" is of either unrecognized type or version");
return false;
}
auto source{std::make_shared<terrain_source>()};
source->file = Binaryfile;
source->reference = reference_of(Binaryfile);
source->materials.reserve(header.materialcount);
for (std::uint32_t index{0}; index < header.materialcount; ++index)
{
std::string name;
if (false == static_cast<bool>(std::getline(input, name, '\0')))
{
break;
}
source->materials.emplace_back(fetch_material(name));
}
std::string data(header.sectioncount * (sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)), '\0');
input.read(data.data(), static_cast<std::streamsize>(data.size()));
data_reader reader{data.data(), static_cast<std::size_t>(std::max<std::streamsize>(0, input.gcount()))};
std::uint32_t sectioncount{0};
for (std::uint32_t index{0}; index < header.sectioncount && reader.good(); ++index)
{
auto const sectionindex{reader.get<std::uint32_t>()};
terrain_block block;
block.source = source;
block.offset = reader.get<std::uint64_t>();
block.size = reader.get<std::uint32_t>();
auto const radius{reader.get<float>()};
if (false == reader.good() || sectionindex >= Region.m_sections.size())
{
break;
}
// location of the section follows from its index, see basic_region::section()
auto const column{static_cast<int>(sectionindex % EU07_REGIONSIDESECTIONCOUNT)};
auto const row{static_cast<int>(sectionindex / EU07_REGIONSIDESECTIONCOUNT)};
auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2};
auto &section{Region.section(glm::dvec3{static_cast<double>(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast<double>(centeroffset + row * EU07_SECTIONSIZE)})};
// the section has to be seen as reaching as far as its geometry does, before that geometry is loaded
section.m_area.radius = std::max(section.m_area.radius, radius);
section.m_terrain.emplace_back(block);
if (false == Deferred)
{
section.load_terrain();
}
++sectioncount;
}
if (false == reader.good() || sectioncount != header.sectioncount || source->materials.size() != header.materialcount)
{
ErrorLog("Bad file: \"" + Binaryfile + "\" is damaged, part of its content may be missing");
}
WriteLog("Terrain file \"" + Binaryfile + "\": " + std::to_string(sectioncount) + " section(s), " + std::to_string(header.vertexcount / 3) + " triangle(s)" +
(Deferred ? ", loaded when needed" : ""));
return true;
}
// loads specified piece of a binary terrain file into provided section
void terrain_file::load(basic_section &Section, terrain_block const &Block)
{
if (Block.source == nullptr)
{
return;
}
auto const &source{*Block.source};
std::string data(Block.size, '\0');
{
std::ifstream input{source.file, std::ios::binary};
input.seekg(static_cast<std::streamoff>(Block.offset));
input.read(data.data(), static_cast<std::streamsize>(data.size()));
if (input.fail() || static_cast<std::size_t>(std::max<std::streamsize>(0, input.gcount())) != data.size())
{
ErrorLog("Bad file: failed to load terrain section from \"" + source.file + "\"");
return;
}
}
data_reader reader{data.data(), data.size()};
if (reader.get<std::uint32_t>() != EU07_TERRAINBLOCK)
{
// the file isn't what it was when its directory was read
ErrorLog("Bad file: failed to load terrain section from \"" + source.file + "\", the file was changed");
return;
}
reader.get<std::uint32_t>(); // section index
auto shapecount{reader.get<std::uint32_t>()};
// large pieces of opaque geometry are held by the section itself, merged by their appearance. see basic_section::insert()
std::vector<shape_node> sectionshapes;
auto const place = [&](shape_node &Shape, std::vector<shape_node> &Shapes, glm::dvec3 const &Origin) {
Shape.origin(Origin);
if (true == Section.m_geometrycreated)
{
// the section was put to use already, it won't prepare the geometry on its own
Shape.create_geometry(Section.m_geometrybank);
}
Shapes.emplace_back(std::move(Shape));
};
while (shapecount-- > 0 && reader.good())
{
auto const materialindex{reader.get<std::uint32_t>()};
auto const cellindex{reader.get<std::uint32_t>()};
shape_node shape;
auto &shapedata{shape.m_data};
shapedata.rangesquared_min = reader.get<double>();
shapedata.rangesquared_max = reader.get<double>();
for (auto *colour : {&shapedata.lighting.diffuse, &shapedata.lighting.ambient, &shapedata.lighting.specular})
{
colour->r = reader.get<float>();
colour->g = reader.get<float>();
colour->b = reader.get<float>();
colour->a = reader.get<float>();
}
shapedata.area.center.x = reader.get<double>();
shapedata.area.center.y = reader.get<double>();
shapedata.area.center.z = reader.get<double>();
shapedata.area.radius = reader.get<float>();
auto const vertexcount{reader.get<std::uint32_t>()};
if (false == reader.good() || false == reader.has(static_cast<std::size_t>(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= source.materials.size() ||
cellindex >= Section.m_cells.size())
{
ErrorLog("Bad file: terrain section data in \"" + source.file + "\" is damaged");
break;
}
auto const &material{source.materials[materialindex]};
if (true == material.skip)
{
reader.skip(static_cast<std::size_t>(vertexcount) * EU07_TERRAINVERTEXSIZE);
continue;
}
shapedata.material = material.handle;
shapedata.translucent = material.translucent;
shapedata.terrainfile = source.reference;
shapedata.vertices.resize(vertexcount);
for (auto &vertex : shapedata.vertices)
{
float values[8];
reader.get(values, 8);
vertex.position = Section.m_area.center + glm::dvec3{values[0], values[1], values[2]};
vertex.normal = glm::vec3{values[3], values[4], values[5]};
vertex.texture = glm::vec2{values[6], values[7]};
// clamp texture coordinates if texture wrapping is off
// NOTE: for the text this is done before a large triangle gets divided along the section borders, here it comes after.
// the coordinates of the vertices added by the division can thus differ, by the clamp margin for a texture mapped the way clamping expects
if (true == material.clamps)
{
vertex.texture.s = std::clamp(vertex.texture.s, 0.001f, 0.999f);
}
if (true == material.clampt)
{
vertex.texture.t = std::clamp(vertex.texture.t, 0.001f, 0.999f);
}
}
if (true == shapedata.translucent || shapedata.rangesquared_max <= 90000.0 || shapedata.rangesquared_min > 0.0)
{
// small, translucent or not always visible shapes are placed in the sub-cells
auto &cell{Section.m_cells[cellindex]};
cell.m_active = true;
cell.m_area.radius = std::max<float>(cell.m_area.radius, static_cast<float>(glm::length(cell.m_area.center - shapedata.area.center) + shapedata.area.radius));
place(shape, (shapedata.translucent ? cell.m_shapestranslucent : cell.m_shapesopaque), cell.m_area.center);
}
else
{
auto merged{false};
for (auto &target : sectionshapes)
{
if (true == target.merge(shape))
{
merged = true;
break;
}
}
if (false == merged)
{
sectionshapes.emplace_back(std::move(shape));
}
}
}
for (auto &shape : sectionshapes)
{
place(shape, Section.m_shapes, Section.m_area.center);
}
}
} // namespace scene
//---------------------------------------------------------------------------